#include "crPostProcess.h"

#include <iryoku/AreaTex.h>
#include <iryoku/SearchTex.h>
#include <SDL3/SDL.h>

#include "crGraphics.h"
#include "crGraphicsStats.h"
#include "crMath.h"

bool crPostProcess::Init( crGraphics* graphics, int2 viewport )
{
    _graphics = graphics;

    glGenVertexArrays( 1, &_quadVao );
    glGenBuffers( 1, &_quadVbo );

    glBindVertexArray( _quadVao );

    // fullscreen strip quad — matches fullscreen.vert (location 0 = clip pos, 1 = uv)
    const float quad[] =
    {
        -1.0f, -1.0f,   0.0f, 0.0f,
         1.0f, -1.0f,   1.0f, 0.0f,
        -1.0f,  1.0f,   0.0f, 1.0f,
         1.0f,  1.0f,   1.0f, 1.0f,
    };
    glBindBuffer( GL_ARRAY_BUFFER, _quadVbo );
    glBufferData( GL_ARRAY_BUFFER, sizeof( quad ), quad, GL_STATIC_DRAW );

    glEnableVertexAttribArray( 0 );
    glVertexAttribPointer( 0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof( float ), reinterpret_cast<void*>( 0 ) );
    glEnableVertexAttribArray( 1 );
    glVertexAttribPointer( 1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof( float ), reinterpret_cast<void*>( 2 * sizeof( float ) ) );

    glBindVertexArray( 0 );
    glBindBuffer( GL_ARRAY_BUFFER, 0 );

    _outputSize = viewport;
    if( CreateTargets() == false )
        return false;

    _rippleBatch.Init();
    _ripples.Reserve( QUEUED_RIPPLE_CAP );   // a full queue pushes one instance each — never realloc mid-frame

    CreateSmaaLuts();
    CacheUniformLocations();

    SDL_LogTrace( CR_LOG_CATEGORY_POSTPROCESS, "crPostProcess: initted( %dx%d )", viewport.x, viewport.y );
    return true;
}
void crPostProcess::Cleanup()
{
    _rippleBatch.Cleanup();

    DestroyTargets();

    glDeleteTextures( 1, &_smaaAreaTex );
    glDeleteTextures( 1, &_smaaSearchTex );
    _smaaAreaTex   = 0;
    _smaaSearchTex = 0;

    glDeleteBuffers( 1, &_quadVbo );
    glDeleteVertexArrays( 1, &_quadVao );
    _quadVbo = 0;
    _quadVao = 0;
}

void crPostProcess::Resize( int2 viewport )
{
    if( ( viewport.x == _outputSize.x ) && ( viewport.y == _outputSize.y ) )
        return;

    _outputSize = viewport;

    DestroyTargets();
    if( CreateTargets() == false )
        return;

    SDL_LogTrace( CR_LOG_CATEGORY_POSTPROCESS, "crPostProcess: resized( %dx%d, scene %dx%d )", viewport.x, viewport.y, _scene.size.x, _scene.size.y );
}

void crPostProcess::SetRenderScale( float scale )
{
    scale = crMath::Clamp( scale, RENDER_SCALE_MINMAX.x, RENDER_SCALE_MINMAX.y );
    if( scale == _renderScale )
        return;

    _renderScale = scale;

    DestroyTargets();
    if( CreateTargets() == false )
        return;

    SDL_LogTrace( CR_LOG_CATEGORY_POSTPROCESS, "crPostProcess: render scale( %.2f, scene %dx%d )", _renderScale, _scene.size.x, _scene.size.y );
}
float crPostProcess::RenderScale() const
{
    return _renderScale;
}

void crPostProcess::BeginScene( color4 clearColor )
{
    glBindFramebuffer( GL_FRAMEBUFFER, _scene.fbo );
    glViewport( 0, 0, _scene.size.x, _scene.size.y );

    // the pipeline stores linear — hand GL the linear equivalent of the authored (sRGB) color
    const color4 linear = crMath::SrgbToLinear( clearColor );
    glClearColor( linear.r, linear.g, linear.b, linear.a );
    glDepthMask( GL_TRUE );   // declare, don't inherit — glClear only clears depth while the write mask is on
    glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
}
void crPostProcess::Execute()
{
    glDisable( GL_DEPTH_TEST );   // 3D passes end here — fullscreen passes and 2D overlays run depthless
    glDisable( GL_BLEND );        // passes overwrite the full screen; batches re-enable blend on their next flush

    // per-pixel color ops all live in the final composite (one fullscreen draw, also the linear -> sRGB encoder);
    // bloom (multi-target) and radial blur (multi-sample) keep their own passes
    const bool fx = enabled;   // master off = effects off; the encode pass still runs

    const RenderTarget* input = &_scene;

    if( fx && bloom.enabled )
    {
        {// bright extract: scene -> bloomA (half res)
            glUseProgram( _graphics->Program( EShader::PP_BLOOM_BRIGHT ) );
            glUniform1f( _locations.bloomThreshold, bloom.threshold );
            glUniform1f( _locations.bloomKnee, bloom.knee );
            glUniform1f( _locations.bloomChromaBias, bloom.chromaBias );

            BindTarget( _bloomA );
            glActiveTexture( GL_TEXTURE0 );
            glBindTexture( GL_TEXTURE_2D, _scene.texture );
            DrawQuad();
        }
        {// separable blur: bloomA -> bloomB (H), bloomB -> bloomA (V)
            glUseProgram( _graphics->Program( EShader::PP_BLOOM_BLUR ) );

            glUniform2f( _locations.bloomDirection, 1.0f / static_cast<float>( _bloomA.size.x ), 0.0f );
            BindTarget( _bloomB );
            glBindTexture( GL_TEXTURE_2D, _bloomA.texture );
            DrawQuad();

            glUniform2f( _locations.bloomDirection, 0.0f, 1.0f / static_cast<float>( _bloomA.size.y ) );
            BindTarget( _bloomA );
            glBindTexture( GL_TEXTURE_2D, _bloomB.texture );
            DrawQuad();
        }
        {// composite: scene + blurred bloom
            glUseProgram( _graphics->Program( EShader::PP_BLOOM_COMPOSITE ) );
            glUniform1f( _locations.bloomIntensity, bloom.intensity );

            BindTarget( _ping );

            glActiveTexture( GL_TEXTURE1 );
            glBindTexture( GL_TEXTURE_2D, _bloomA.texture );
            glActiveTexture( GL_TEXTURE0 );
            glBindTexture( GL_TEXTURE_2D, _scene.texture );
            DrawQuad();

            glActiveTexture( GL_TEXTURE1 );
            glBindTexture( GL_TEXTURE_2D, 0 );
            glActiveTexture( GL_TEXTURE0 );

            input = &_ping;
        }
    }

    {// queued shockwaves — advance on the render clock, feed the distortion map, drop the dead
        const uint64_t now = SDL_GetTicks();
        float dt = ( _rippleTicks != 0 ) ? ( static_cast<float>( now - _rippleTicks ) * 0.001f ) : 0.0f;
        if( dt > 0.1f )
            dt = 0.1f;
        _rippleTicks = now;

        for( int32_t i = _queuedCount - 1; i >= 0; --i )
        {
            QueuedRipple& r = _queuedRipples[ i ];
            r.age += dt;
            if( r.age >= r.life )
            {
                _queuedRipples[ i ] = _queuedRipples[ _queuedCount - 1 ];
                --_queuedCount;
                _queuedCursor = _queuedCount;   // keep the ring cursor on the free tail after compaction
                continue;
            }

            PushRipple( r.pos, r.radius, ( r.age / r.life ), r.strength );
        }
    }

    // distortion map — world-space ripple quads summed into a half-res UV-offset field; the final
    // composite displaces its scene fetch by it. rendered even when empty is a waste, so gate on count
    const bool distortOn = ( fx && distort.enabled && ( _ripples.Size() > 0 ) );
    if( distortOn )
    {
        BindTarget( _distortMap );
        glClearColor( 0.0f, 0.0f, 0.0f, 0.0f );
        glClear( GL_COLOR_BUFFER_BIT );

        _rippleBatch.Flush( _graphics->Program( EShader::PP_DISTORT_RIPPLE ), 0, _ripples.Data(), _ripples.Size() );

        // Flush leaves billboard state on — restore the fullscreen-pass defaults
        glDisable( GL_DEPTH_TEST );
        glDisable( GL_BLEND );
    }

    if( fx && radialBlur.enabled )
    {
        glUseProgram( _graphics->Program( EShader::PP_RADIAL_BLUR ) );
        glUniform2f( _locations.radialCenter, radialBlur.center.x, radialBlur.center.y );
        glUniform1f( _locations.radialStrength, radialBlur.strength );
        glUniform1i( _locations.radialSamples, radialBlur.samples );

        const RenderTarget* output = ( input == &_ping ) ? &_pong : &_ping;
        BindTarget( *output );

        glActiveTexture( GL_TEXTURE0 );
        glBindTexture( GL_TEXTURE_2D, input->texture );
        DrawQuad();

        input = output;
    }

    {// final composite — always runs: chromatic sampling + tonemap + color grade + vignette + the sRGB encode
        glUseProgram( _graphics->Program( EShader::PP_COMPOSITE ) );

        glUniform1i( _locations.tonemapEnabled, ( fx && tonemap.enabled ) ? 1 : 0 );
        glUniform1f( _locations.exposure, tonemap.exposure );
        glUniform1i( _locations.tonemapMode, tonemap.mode );

        glUniform1i( _locations.gradeEnabled, ( fx && colorGrade.enabled ) ? 1 : 0 );
        glUniform3f( _locations.lift,  colorGrade.lift.x,  colorGrade.lift.y,  colorGrade.lift.z );
        glUniform3f( _locations.gamma, colorGrade.gamma.x, colorGrade.gamma.y, colorGrade.gamma.z );
        glUniform3f( _locations.gain,  colorGrade.gain.x,  colorGrade.gain.y,  colorGrade.gain.z );

        glUniform1i( _locations.chromaticEnabled, ( fx && chromatic.enabled ) ? 1 : 0 );
        glUniform1f( _locations.chromaticStrength, chromatic.strength );

        glUniform1i( _locations.vignetteEnabled, ( fx && vignette.enabled ) ? 1 : 0 );
        glUniform1f( _locations.vignetteRadius, vignette.radius );
        glUniform1f( _locations.vignetteSoftness, vignette.softness );
        glUniform1f( _locations.vignetteIntensity, vignette.intensity );
        // fog is forward now (primitive.frag + ribbon build) so transparents fog by their own depth — not here

        glUniform1i( _locations.distortEnabled, distortOn ? 1 : 0 );
        if( distortOn )
        {
            glActiveTexture( GL_TEXTURE2 );
            glBindTexture( GL_TEXTURE_2D, _distortMap.texture );
            glActiveTexture( GL_TEXTURE0 );
        }

        glUniform1f( _locations.glitchStrength, fx ? glitch.strength : 0.0f );
        glUniform1f( _locations.glitchTime, static_cast<float>( SDL_GetTicks() % 100000ULL ) * 0.001f );   // visual-only clock
        glUniform1f( _locations.saturation, fx ? saturation : 1.0f );

        const bool smaaOn = ( fx && smaa.enabled );
        if( smaaOn )
        {
            BindTarget( _ldr );   // SMAA needs the tonemapped, gamma-encoded image — it takes over the backbuffer
        }
        else
        {
            glBindFramebuffer( GL_FRAMEBUFFER, 0 );
            glViewport( 0, 0, _outputSize.x, _outputSize.y );
        }

        glActiveTexture( GL_TEXTURE0 );
        glBindTexture( GL_TEXTURE_2D, input->texture );
        DrawQuad();

        if( smaaOn )
            ExecuteSmaa();
    }

    _ripples.Clear();

    glBindTexture( GL_TEXTURE_2D, 0 );
}

void crPostProcess::PushRipple( float3 worldPos, float radius, float phase01, float strength )
{
    crBatchedBillboards::BillboardInstance inst;
    inst.pos    = worldPos;
    inst.size   = ( radius * 2.0f );
    inst.color  = color4( strength * distort.strength, phase01, 0.0f, 0.0f );   // params, not light
    inst.uvMin  = float2( 0.0f, 0.0f );
    inst.uvMax  = float2( 1.0f, 1.0f );
    inst.cosSin = float2( 1.0f, 0.0f );
    _ripples.Add( inst );
}

void crPostProcess::QueueRipple( float3 worldPos, float radius, float life, float strength )
{
    QueuedRipple& r = _queuedRipples[ _queuedCursor ];   // full pool overwrites the oldest slot in ring order
    r.pos      = worldPos;
    r.age      = 0.0f;
    r.life     = life;
    r.radius   = radius;
    r.strength = strength;

    _queuedCursor = ( ( _queuedCursor + 1 ) % QUEUED_RIPPLE_CAP );
    if( _queuedCount < QUEUED_RIPPLE_CAP )
        ++_queuedCount;
}
void crPostProcess::ClearQueuedRipples()
{
    _queuedCount  = 0;
    _queuedCursor = 0;
}

GLuint crPostProcess::SceneDepthTexture() const
{
    return _scene.depthTexture;
}
int2 crPostProcess::SceneSize() const
{
    return _scene.size;
}

void crPostProcess::ExecuteSmaa()
{
    const float rtW = static_cast<float>( _scene.size.x );
    const float rtH = static_cast<float>( _scene.size.y );

    {// pass 1: luma edge detection — LDR -> edges
        glUseProgram( _graphics->Program( EShader::PP_SMAA_EDGE ) );
        glUniform4f( _locations.smaaEdgeRtMetrics, 1.0f / rtW, 1.0f / rtH, rtW, rtH );

        BindTarget( _smaaEdges );
        glClearColor( 0.0f, 0.0f, 0.0f, 0.0f );
        glClear( GL_COLOR_BUFFER_BIT );   // the PS discards non-edge pixels — stale texels must be zero

        glActiveTexture( GL_TEXTURE0 );
        glBindTexture( GL_TEXTURE_2D, _ldr.texture );
        DrawQuad();
    }

    {// pass 2: blend weights — edges + LUTs -> blend
        glUseProgram( _graphics->Program( EShader::PP_SMAA_WEIGHT ) );
        glUniform4f( _locations.smaaWeightRtMetrics, 1.0f / rtW, 1.0f / rtH, rtW, rtH );

        BindTarget( _smaaBlend );
        glClear( GL_COLOR_BUFFER_BIT );

        glActiveTexture( GL_TEXTURE2 );
        glBindTexture( GL_TEXTURE_2D, _smaaSearchTex );
        glActiveTexture( GL_TEXTURE1 );
        glBindTexture( GL_TEXTURE_2D, _smaaAreaTex );
        glActiveTexture( GL_TEXTURE0 );
        glBindTexture( GL_TEXTURE_2D, _smaaEdges.texture );
        DrawQuad();
    }

    {// pass 3: neighborhood blend (or a debug view) -> backbuffer
        glBindFramebuffer( GL_FRAMEBUFFER, 0 );
        glViewport( 0, 0, _outputSize.x, _outputSize.y );

        if( smaa.debugView != 0 )
        {
            glUseProgram( _graphics->Program( EShader::PP_BLIT ) );

            glBindTexture( GL_TEXTURE_2D, ( smaa.debugView == 1 ) ? _smaaEdges.texture : _smaaBlend.texture );
            DrawQuad();
        }
        else
        {
            glUseProgram( _graphics->Program( EShader::PP_SMAA_BLEND ) );
            glUniform4f( _locations.smaaBlendRtMetrics, 1.0f / rtW, 1.0f / rtH, rtW, rtH );

            glActiveTexture( GL_TEXTURE1 );
            glBindTexture( GL_TEXTURE_2D, _smaaBlend.texture );
            glActiveTexture( GL_TEXTURE0 );
            glBindTexture( GL_TEXTURE_2D, _ldr.texture );
            DrawQuad();
        }
    }

    glActiveTexture( GL_TEXTURE2 );
    glBindTexture( GL_TEXTURE_2D, 0 );
    glActiveTexture( GL_TEXTURE1 );
    glBindTexture( GL_TEXTURE_2D, 0 );
    glActiveTexture( GL_TEXTURE0 );
}

bool crPostProcess::CreateTargets()
{
    const int32_t scaledX = static_cast<int32_t>( crMath::Round( static_cast<float>( _outputSize.x ) * _renderScale ) );
    const int32_t scaledY = static_cast<int32_t>( crMath::Round( static_cast<float>( _outputSize.y ) * _renderScale ) );

    const int2 scaled = { ( scaledX > 0 ) ? scaledX : 1,
                          ( scaledY > 0 ) ? scaledY : 1 };
    const int2 half   = { ( scaled.x / 2 ) > 0 ? ( scaled.x / 2 ) : 1,
                          ( scaled.y / 2 ) > 0 ? ( scaled.y / 2 ) : 1 };

    bool ok = true;
    ok = ok && CreateTarget( _scene, scaled, true, GL_RGBA16F, GL_RGBA, GL_HALF_FLOAT );
    ok = ok && CreateTarget( _ping, scaled, false, GL_RGBA16F, GL_RGBA, GL_HALF_FLOAT );
    ok = ok && CreateTarget( _pong, scaled, false, GL_RGBA16F, GL_RGBA, GL_HALF_FLOAT );
    ok = ok && CreateTarget( _bloomA, half, false, GL_RGBA16F, GL_RGBA, GL_HALF_FLOAT );
    ok = ok && CreateTarget( _bloomB, half, false, GL_RGBA16F, GL_RGBA, GL_HALF_FLOAT );

    ok = ok && CreateTarget( _ldr, scaled, false, GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE );
    ok = ok && CreateTarget( _smaaEdges, scaled, false, GL_RG8, GL_RG, GL_UNSIGNED_BYTE );
    ok = ok && CreateTarget( _smaaBlend, scaled, false, GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE );

    ok = ok && CreateTarget( _distortMap, half, false, GL_RG16F, GL_RG, GL_HALF_FLOAT );   // signed UV offsets

    if( ok == false )
        SDL_LogError( CR_LOG_CATEGORY_POSTPROCESS, "crPostProcess: render target creation failed( %dx%d )", scaled.x, scaled.y );

    return ok;
}
void crPostProcess::DestroyTargets()
{
    DestroyTarget( _scene );
    DestroyTarget( _ping );
    DestroyTarget( _pong );
    DestroyTarget( _bloomA );
    DestroyTarget( _bloomB );
    DestroyTarget( _ldr );
    DestroyTarget( _smaaEdges );
    DestroyTarget( _smaaBlend );
    DestroyTarget( _distortMap );
}
void crPostProcess::CreateSmaaLuts()
{
    glPixelStorei( GL_UNPACK_ALIGNMENT, 1 );

    glGenTextures( 1, &_smaaAreaTex );
    glBindTexture( GL_TEXTURE_2D, _smaaAreaTex );
    glTexImage2D( GL_TEXTURE_2D, 0, GL_RG8, AREATEX_WIDTH, AREATEX_HEIGHT, 0, GL_RG, GL_UNSIGNED_BYTE, areaTexBytes );
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE );
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE );

    glGenTextures( 1, &_smaaSearchTex );
    glBindTexture( GL_TEXTURE_2D, _smaaSearchTex );
    glTexImage2D( GL_TEXTURE_2D, 0, GL_R8, SEARCHTEX_WIDTH, SEARCHTEX_HEIGHT, 0, GL_RED, GL_UNSIGNED_BYTE, searchTexBytes );
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST );   // the search LUT encodes exact offsets — must not be filtered
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST );
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE );
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE );

    glBindTexture( GL_TEXTURE_2D, 0 );
    glPixelStorei( GL_UNPACK_ALIGNMENT, 4 );
}
void crPostProcess::CacheUniformLocations()
{
    GLuint program = _graphics->Program( EShader::PP_BLOOM_BRIGHT );
    glUseProgram( program );
    glUniform1i( glGetUniformLocation( program, "u_tex" ), 0 );
    _locations.bloomThreshold = glGetUniformLocation( program, "u_threshold" );
    _locations.bloomKnee      = glGetUniformLocation( program, "u_knee" );
    _locations.bloomChromaBias = glGetUniformLocation( program, "u_chromaBias" );

    program = _graphics->Program( EShader::PP_BLOOM_BLUR );
    glUseProgram( program );
    glUniform1i( glGetUniformLocation( program, "u_tex" ), 0 );
    _locations.bloomDirection = glGetUniformLocation( program, "u_direction" );

    program = _graphics->Program( EShader::PP_BLOOM_COMPOSITE );
    glUseProgram( program );
    glUniform1i( glGetUniformLocation( program, "u_tex" ), 0 );
    glUniform1i( glGetUniformLocation( program, "u_bloom" ), 1 );
    _locations.bloomIntensity = glGetUniformLocation( program, "u_intensity" );

    program = _graphics->Program( EShader::PP_RADIAL_BLUR );
    glUseProgram( program );
    glUniform1i( glGetUniformLocation( program, "u_tex" ), 0 );
    _locations.radialCenter   = glGetUniformLocation( program, "u_center" );
    _locations.radialStrength = glGetUniformLocation( program, "u_strength" );
    _locations.radialSamples  = glGetUniformLocation( program, "u_samples" );

    program = _graphics->Program( EShader::PP_COMPOSITE );
    glUseProgram( program );
    glUniform1i( glGetUniformLocation( program, "u_tex" ), 0 );
    _locations.tonemapEnabled    = glGetUniformLocation( program, "u_tonemapEnabled" );
    _locations.exposure          = glGetUniformLocation( program, "u_exposure" );
    _locations.tonemapMode       = glGetUniformLocation( program, "u_tonemapMode" );
    _locations.gradeEnabled      = glGetUniformLocation( program, "u_gradeEnabled" );
    _locations.lift              = glGetUniformLocation( program, "u_lift" );
    _locations.gamma             = glGetUniformLocation( program, "u_gamma" );
    _locations.gain              = glGetUniformLocation( program, "u_gain" );
    _locations.chromaticEnabled  = glGetUniformLocation( program, "u_chromaticEnabled" );
    _locations.chromaticStrength = glGetUniformLocation( program, "u_chromaticStrength" );
    _locations.vignetteEnabled   = glGetUniformLocation( program, "u_vignetteEnabled" );
    _locations.vignetteRadius    = glGetUniformLocation( program, "u_vignetteRadius" );
    _locations.vignetteSoftness  = glGetUniformLocation( program, "u_vignetteSoftness" );
    _locations.vignetteIntensity = glGetUniformLocation( program, "u_vignetteIntensity" );
    glUniform1i( glGetUniformLocation( program, "u_distortTex" ), 2 );
    _locations.distortEnabled    = glGetUniformLocation( program, "u_distortEnabled" );
    _locations.glitchStrength    = glGetUniformLocation( program, "u_glitchStrength" );
    _locations.glitchTime        = glGetUniformLocation( program, "u_glitchTime" );
    _locations.saturation        = glGetUniformLocation( program, "u_saturation" );

    program = _graphics->Program( EShader::PP_SMAA_EDGE );
    glUseProgram( program );
    glUniform1i( glGetUniformLocation( program, "u_tex" ), 0 );
    _locations.smaaEdgeRtMetrics = glGetUniformLocation( program, "u_rtMetrics" );

    program = _graphics->Program( EShader::PP_SMAA_WEIGHT );
    glUseProgram( program );
    glUniform1i( glGetUniformLocation( program, "u_edgesTex" ), 0 );
    glUniform1i( glGetUniformLocation( program, "u_areaTex" ), 1 );
    glUniform1i( glGetUniformLocation( program, "u_searchTex" ), 2 );
    _locations.smaaWeightRtMetrics = glGetUniformLocation( program, "u_rtMetrics" );

    program = _graphics->Program( EShader::PP_SMAA_BLEND );
    glUseProgram( program );
    glUniform1i( glGetUniformLocation( program, "u_tex" ), 0 );
    glUniform1i( glGetUniformLocation( program, "u_blendTex" ), 1 );
    _locations.smaaBlendRtMetrics = glGetUniformLocation( program, "u_rtMetrics" );

    program = _graphics->Program( EShader::PP_BLIT );
    glUseProgram( program );
    glUniform1i( glGetUniformLocation( program, "u_tex" ), 0 );

    glUseProgram( 0 );
}

void crPostProcess::BindTarget( const RenderTarget& rt )
{
    glBindFramebuffer( GL_FRAMEBUFFER, rt.fbo );
    glViewport( 0, 0, rt.size.x, rt.size.y );
}
void crPostProcess::DrawQuad()
{
    glBindVertexArray( _quadVao );
    glDrawArrays( GL_TRIANGLE_STRIP, 0, 4 );
    glBindVertexArray( 0 );

    ++crGraphicsStats::drawCalls;
    crGraphicsStats::vertices += 4;
}

/*static*/ bool crPostProcess::CreateTarget( RenderTarget& rt, int2 size, bool depth, GLenum internalFormat, GLenum format, GLenum type )
{
    glGenTextures( 1, &rt.texture );
    glBindTexture( GL_TEXTURE_2D, rt.texture );
    // HDR targets store linear half-float — values past 1.0 survive, bloom/tonemap see real brightness.
    // RGBA16F renderability is extension-gated on ES 3.0 (checked as a hard requirement in crGraphics::Init)
    glTexImage2D( GL_TEXTURE_2D, 0, internalFormat, size.x, size.y, 0, format, type, nullptr );
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );   // half-res bloom upsamples with bilinear
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE );   // CA/blur sample past 0..1 at the edges
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE );
    glBindTexture( GL_TEXTURE_2D, 0 );

    glGenFramebuffers( 1, &rt.fbo );
    glBindFramebuffer( GL_FRAMEBUFFER, rt.fbo );
    glFramebufferTexture2D( GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, rt.texture, 0 );

    if( depth )
    {
        glGenTextures( 1, &rt.depthTexture );
        glBindTexture( GL_TEXTURE_2D, rt.depthTexture );
        glTexImage2D( GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT24, size.x, size.y, 0, GL_DEPTH_COMPONENT, GL_UNSIGNED_INT, nullptr );
        glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST );   // depth textures are not filterable in ES 3.0
        glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST );
        glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE );
        glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE );
        glBindTexture( GL_TEXTURE_2D, 0 );
        glFramebufferTexture2D( GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, rt.depthTexture, 0 );
    }

    const GLenum status = glCheckFramebufferStatus( GL_FRAMEBUFFER );
    glBindFramebuffer( GL_FRAMEBUFFER, 0 );

    if( status != GL_FRAMEBUFFER_COMPLETE )
    {
        SDL_LogError( CR_LOG_CATEGORY_POSTPROCESS, "crPostProcess: framebuffer incomplete( 0x%x )", status );
        DestroyTarget( rt );
        return false;
    }

    rt.size = size;
    return true;
}
/*static*/ void crPostProcess::DestroyTarget( RenderTarget& rt )
{
    glDeleteFramebuffers( 1, &rt.fbo );
    glDeleteTextures( 1, &rt.texture );
    glDeleteTextures( 1, &rt.depthTexture );
    rt.fbo          = 0;
    rt.texture      = 0;
    rt.depthTexture = 0;
    rt.size         = { 0, 0 };
}
