// gpu_frame_renderer.cpp - Implementación del FrameRenderer con offscreen + postpro

#include "core/rendering/gpu/gpu_frame_renderer.hpp"

#include <SDL3/SDL.h>
#include <SDL3/SDL_gpu.h>

#include <cmath>
#include <cstdint>
#include <cstring>
#include <iostream>

namespace Rendering::GPU {

    GpuFrameRenderer::~GpuFrameRenderer() { destroy(); }

    auto GpuFrameRenderer::init(SDL_Window* window, float logical_w, float logical_h, float render_w, float render_h) -> bool {
        logical_w_ = logical_w;
        logical_h_ = logical_h;
        render_w_ = render_w;
        render_h_ = render_h;

        if (!device_.init(window)) {
            return false;
        }
        // Pipeline de líneas: escribe sobre el offscreen (formato fijo).
        if (!line_pipeline_.init(device_, offscreen_format_)) {
            device_.destroy();
            return false;
        }
        // Pipeline de bloom: escriu sobre les bloom textures (mateix format).
        if (!bloom_pipeline_.init(device_, offscreen_format_)) {
            line_pipeline_.destroy();
            device_.destroy();
            return false;
        }
        // Pipeline de postpro: escribe sobre swapchain (formato del swapchain).
        if (!postfx_pipeline_.init(device_, device_.swapchainFormat())) {
            bloom_pipeline_.destroy();
            line_pipeline_.destroy();
            device_.destroy();
            return false;
        }
        if (!createOffscreen()) {
            postfx_pipeline_.destroy();
            bloom_pipeline_.destroy();
            line_pipeline_.destroy();
            device_.destroy();
            return false;
        }
        return true;
    }

    auto GpuFrameRenderer::createOffscreen() -> bool {
        SDL_GPUDevice* dev = device_.get();
        if (dev == nullptr) {
            return false;
        }

        // Textura offscreen del tamaño físico de render, COLOR_TARGET + SAMPLER.
        // El tamaño lógico se aplica a los vértices vía UBO; el offscreen puede
        // ser de mayor resolución para ganar nitidez tras el upscale a la swapchain.
        SDL_GPUTextureCreateInfo tex_info{};
        tex_info.type = SDL_GPU_TEXTURETYPE_2D;
        tex_info.format = offscreen_format_;
        tex_info.usage = SDL_GPU_TEXTUREUSAGE_COLOR_TARGET | SDL_GPU_TEXTUREUSAGE_SAMPLER;
        tex_info.width = static_cast<uint32_t>(render_w_);
        tex_info.height = static_cast<uint32_t>(render_h_);
        tex_info.layer_count_or_depth = 1;
        tex_info.num_levels = 1;
        tex_info.sample_count = SDL_GPU_SAMPLECOUNT_1;
        offscreen_texture_ = SDL_CreateGPUTexture(dev, &tex_info);
        if (offscreen_texture_ == nullptr) {
            std::cerr << "[GpuFrameRenderer] SDL_CreateGPUTexture (offscreen): "
                      << SDL_GetError() << '\n';
            return false;
        }

        // Sampler lineal con clamp-to-edge (evita sangrado en los bordes del bloom).
        SDL_GPUSamplerCreateInfo sampler_info{};
        sampler_info.min_filter = SDL_GPU_FILTER_LINEAR;
        sampler_info.mag_filter = SDL_GPU_FILTER_LINEAR;
        sampler_info.mipmap_mode = SDL_GPU_SAMPLERMIPMAPMODE_LINEAR;
        sampler_info.address_mode_u = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE;
        sampler_info.address_mode_v = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE;
        sampler_info.address_mode_w = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE;
        linear_sampler_ = SDL_CreateGPUSampler(dev, &sampler_info);
        if (linear_sampler_ == nullptr) {
            std::cerr << "[GpuFrameRenderer] SDL_CreateGPUSampler: "
                      << SDL_GetError() << '\n';
            return false;
        }

        // Bloom textures: mateixa mida i format que l'offscreen. Es fan servir
        // ping-pong (A = sortida de la passada H, B = sortida de la V i lectura
        // del composite). COLOR_TARGET + SAMPLER, igual que l'offscreen.
        tex_info.usage = SDL_GPU_TEXTUREUSAGE_COLOR_TARGET | SDL_GPU_TEXTUREUSAGE_SAMPLER;
        bloom_texture_a_ = SDL_CreateGPUTexture(dev, &tex_info);
        bloom_texture_b_ = SDL_CreateGPUTexture(dev, &tex_info);
        if ((bloom_texture_a_ == nullptr) || (bloom_texture_b_ == nullptr)) {
            std::cerr << "[GpuFrameRenderer] SDL_CreateGPUTexture (bloom): "
                      << SDL_GetError() << '\n';
            return false;
        }
        return true;
    }

    void GpuFrameRenderer::destroyOffscreen() {
        SDL_GPUDevice* dev = device_.get();
        if (dev == nullptr) {
            offscreen_texture_ = nullptr;
            linear_sampler_ = nullptr;
            return;
        }
        if (offscreen_texture_ != nullptr) {
            SDL_ReleaseGPUTexture(dev, offscreen_texture_);
            offscreen_texture_ = nullptr;
        }
        if (bloom_texture_a_ != nullptr) {
            SDL_ReleaseGPUTexture(dev, bloom_texture_a_);
            bloom_texture_a_ = nullptr;
        }
        if (bloom_texture_b_ != nullptr) {
            SDL_ReleaseGPUTexture(dev, bloom_texture_b_);
            bloom_texture_b_ = nullptr;
        }
        if (linear_sampler_ != nullptr) {
            SDL_ReleaseGPUSampler(dev, linear_sampler_);
            linear_sampler_ = nullptr;
        }
    }

    void GpuFrameRenderer::destroy() {
        destroyOffscreen();
        postfx_pipeline_.destroy();
        bloom_pipeline_.destroy();
        line_pipeline_.destroy();
        device_.destroy();
        vertices_.clear();
        indices_.clear();
    }

    auto GpuFrameRenderer::resizeRenderTarget(float render_w, float render_h) -> bool {
        // Solo seguro fuera de un frame: si el cmd buffer está vivo y referencia
        // la textura antigua, recrearla provocaría un cuelgue/UB.
        if (isInsideFrame()) {
            std::cerr << "[GpuFrameRenderer] resizeRenderTarget llamado dentro de frame, ignorado\n";
            return false;
        }
        destroyOffscreen();
        render_w_ = render_w;
        render_h_ = render_h;
        return createOffscreen();
    }

    auto GpuFrameRenderer::beginFrame(float clear_r, float clear_g, float clear_b) -> bool {
        // Los clear_* se ignoran: el fondo lo pinta el postpro. Mantenemos la
        // firma para no romper el SDLManager.
        (void)clear_r;
        (void)clear_g;
        (void)clear_b;

        SDL_GPUDevice* dev = device_.get();
        if (dev == nullptr) {
            return false;
        }

        cmd_buffer_ = SDL_AcquireGPUCommandBuffer(dev);
        if (cmd_buffer_ == nullptr) {
            std::cerr << "[GpuFrameRenderer] SDL_AcquireGPUCommandBuffer: " << SDL_GetError() << '\n';
            return false;
        }

        if (!SDL_WaitAndAcquireGPUSwapchainTexture(cmd_buffer_, device_.window(), &swapchain_texture_, nullptr, nullptr)) {
            std::cerr << "[GpuFrameRenderer] WaitAndAcquire: " << SDL_GetError() << '\n';
            SDL_SubmitGPUCommandBuffer(cmd_buffer_);
            cmd_buffer_ = nullptr;
            return false;
        }

        if (swapchain_texture_ == nullptr) {
            // Ventana minimizada o swapchain no disponible: solo submit y salir.
            SDL_SubmitGPUCommandBuffer(cmd_buffer_);
            cmd_buffer_ = nullptr;
            return false;
        }

        // Abrir render pass sobre OFFSCREEN con clear a negro.
        SDL_GPUColorTargetInfo color_target{};
        color_target.texture = offscreen_texture_;
        color_target.clear_color = SDL_FColor{.r = 0.0F, .g = 0.0F, .b = 0.0F, .a = 1.0F};
        color_target.load_op = SDL_GPU_LOADOP_CLEAR;
        color_target.store_op = SDL_GPU_STOREOP_STORE;
        color_target.cycle = false;

        render_pass_ = SDL_BeginGPURenderPass(cmd_buffer_, &color_target, 1, nullptr);
        if (render_pass_ == nullptr) {
            std::cerr << "[GpuFrameRenderer] SDL_BeginGPURenderPass (offscreen): "
                      << SDL_GetError() << '\n';
            SDL_SubmitGPUCommandBuffer(cmd_buffer_);
            cmd_buffer_ = nullptr;
            return false;
        }
        // Sin SetGPUViewport: el offscreen se llena entero a tamaño lógico.

        vertices_.clear();
        indices_.clear();
        return true;
    }

    void GpuFrameRenderer::setViewport(float x, float y, float w, float h) {
        viewport_x_ = x;
        viewport_y_ = y;
        viewport_w_ = w;
        viewport_h_ = h;
        // El viewport solo se aplica en el pase final (composite). Si estamos
        // ya dentro del composite, lo aplicaríamos inmediatamente, pero la API
        // está pensada para llamarse antes de endFrame/al cambiar de ventana.
    }

    void GpuFrameRenderer::setVSync(bool enabled) {
        SDL_GPUDevice* dev = device_.get();
        SDL_Window* win = device_.window();
        if (dev == nullptr || win == nullptr) {
            return;
        }

        // A SDL_GPU, només VSYNC està garantit. IMMEDIATE i MAILBOX són opcionals
        // i poden no estar disponibles segons backend/driver/compositor (típicament
        // Wayland sense unredirect, X11 amb compositor agressiu, etc.). Si demanem
        // un mode no suportat, SDL retorna error i la swapchain queda igual.
        //
        // Estratègia: si l'usuari demana VSync OFF, provem IMMEDIATE i si no està
        // suportat fem fallback a MAILBOX (no és no-VSync però sí permet superar
        // els 60Hz sense tearing). Si cap dels dos, ens quedem amb VSYNC i avisem.
        auto try_set = [&](SDL_GPUPresentMode mode, const char* name) -> bool {
            if (!SDL_WindowSupportsGPUPresentMode(dev, win, mode)) {
                return false;
            }
            if (!SDL_SetGPUSwapchainParameters(dev, win, SDL_GPU_SWAPCHAINCOMPOSITION_SDR, mode)) {
                std::cerr << "[GpuFrameRenderer] SDL_SetGPUSwapchainParameters("
                          << name << "): " << SDL_GetError() << '\n';
                return false;
            }
            std::cout << "[GpuFrameRenderer] Present mode: " << name << '\n';
            return true;
        };

        if (enabled) {
            try_set(SDL_GPU_PRESENTMODE_VSYNC, "VSYNC");
            return;
        }

        if (try_set(SDL_GPU_PRESENTMODE_IMMEDIATE, "IMMEDIATE")) {
            return;
        }
        if (try_set(SDL_GPU_PRESENTMODE_MAILBOX, "MAILBOX (fallback)")) {
            return;
        }
        // Tots dos rebutjats: el driver/compositor força VSync. Hi tornem
        // explícitament perquè la swapchain quedi en estat conegut.
        std::cerr << "[GpuFrameRenderer] VSync OFF no disponible (driver/compositor "
                     "força VSYNC). Mantenim VSYNC.\n";
        try_set(SDL_GPU_PRESENTMODE_VSYNC, "VSYNC (forçat)");
    }

    void GpuFrameRenderer::applyFinalViewport() {
        if (render_pass_ == nullptr) {
            return;
        }
        if (viewport_w_ <= 0.0F || viewport_h_ <= 0.0F) {
            return;
        }
        SDL_GPUViewport vp{};
        vp.x = viewport_x_;
        vp.y = viewport_y_;
        vp.w = viewport_w_;
        vp.h = viewport_h_;
        vp.min_depth = 0.0F;
        vp.max_depth = 1.0F;
        SDL_SetGPUViewport(render_pass_, &vp);
    }

    void GpuFrameRenderer::pushLine(float x1, float y1, float x2, float y2, float thickness, float r, float g, float b, float a) {
        const float DX = x2 - x1;
        const float DY = y2 - y1;
        const float LEN = std::sqrt((DX * DX) + (DY * DY));
        if (LEN < 1e-6F) {
            return;
        }

        // Antialias geomètric: quan està ON, extruim 0.5 píxel extra a cada
        // banda del gruix demanat i posem edge_dist = ±1 als laterals; el
        // fragment shader fa fade als bords. Quan està OFF, geometria nua
        // (thickness exacte) i edge_dist = 0 a tots els vèrtexs → el
        // smoothstep del shader produeix alpha=1 sempre.
        const float AA_PADDING = antialias_enabled_ ? 0.5F : 0.0F;
        const float HALF = (thickness * 0.5F) + AA_PADDING;
        const float NX = -DY / LEN * HALF;
        const float NY = DX / LEN * HALF;
        const float EDGE = antialias_enabled_ ? 1.0F : 0.0F;

        const auto BASE_INDEX = static_cast<uint16_t>(vertices_.size());

        vertices_.push_back({x1 + NX, y1 + NY, r, g, b, a, +EDGE});
        vertices_.push_back({x1 - NX, y1 - NY, r, g, b, a, -EDGE});
        vertices_.push_back({x2 + NX, y2 + NY, r, g, b, a, +EDGE});
        vertices_.push_back({x2 - NX, y2 - NY, r, g, b, a, -EDGE});

        indices_.push_back(BASE_INDEX + 0);
        indices_.push_back(BASE_INDEX + 1);
        indices_.push_back(BASE_INDEX + 2);
        indices_.push_back(BASE_INDEX + 1);
        indices_.push_back(BASE_INDEX + 3);
        indices_.push_back(BASE_INDEX + 2);
    }

    void GpuFrameRenderer::pushRect(float x, float y, float w, float h, float r, float g, float b, float a) {
        if (w <= 0.0F || h <= 0.0F) {
            return;
        }
        const float X1 = x;
        const float Y1 = y;
        const float X2 = x + w;
        const float Y2 = y + h;

        const auto BASE_INDEX = static_cast<uint16_t>(vertices_.size());

        // edge_dist=0 → el fragment shader dóna alpha plena (no fade).
        vertices_.push_back({X1, Y1, r, g, b, a, 0.0F});
        vertices_.push_back({X2, Y1, r, g, b, a, 0.0F});
        vertices_.push_back({X1, Y2, r, g, b, a, 0.0F});
        vertices_.push_back({X2, Y2, r, g, b, a, 0.0F});

        indices_.push_back(BASE_INDEX + 0);
        indices_.push_back(BASE_INDEX + 1);
        indices_.push_back(BASE_INDEX + 2);
        indices_.push_back(BASE_INDEX + 1);
        indices_.push_back(BASE_INDEX + 3);
        indices_.push_back(BASE_INDEX + 2);
    }

    void GpuFrameRenderer::flushBatch() {
        if (vertices_.empty() || indices_.empty()) {
            return;
        }

        SDL_GPUDevice* dev = device_.get();

        const auto VBO_SIZE = static_cast<uint32_t>(vertices_.size() * sizeof(LineVertex));
        const auto IBO_SIZE = static_cast<uint32_t>(indices_.size() * sizeof(uint16_t));

        SDL_GPUBufferCreateInfo vbo_info{};
        vbo_info.usage = SDL_GPU_BUFFERUSAGE_VERTEX;
        vbo_info.size = VBO_SIZE;
        SDL_GPUBuffer* vbo = SDL_CreateGPUBuffer(dev, &vbo_info);

        SDL_GPUBufferCreateInfo ibo_info{};
        ibo_info.usage = SDL_GPU_BUFFERUSAGE_INDEX;
        ibo_info.size = IBO_SIZE;
        SDL_GPUBuffer* ibo = SDL_CreateGPUBuffer(dev, &ibo_info);

        SDL_GPUTransferBufferCreateInfo tbo_info{};
        tbo_info.usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD;
        tbo_info.size = VBO_SIZE + IBO_SIZE;
        SDL_GPUTransferBuffer* tbo = SDL_CreateGPUTransferBuffer(dev, &tbo_info);

        auto* mapped = static_cast<uint8_t*>(SDL_MapGPUTransferBuffer(dev, tbo, false));
        std::memcpy(mapped, vertices_.data(), VBO_SIZE);
        std::memcpy(mapped + VBO_SIZE, indices_.data(), IBO_SIZE);
        SDL_UnmapGPUTransferBuffer(dev, tbo);

        // Copy pass FUERA del render pass.
        SDL_EndGPURenderPass(render_pass_);
        render_pass_ = nullptr;

        SDL_GPUCopyPass* copy_pass = SDL_BeginGPUCopyPass(cmd_buffer_);
        SDL_GPUTransferBufferLocation vbo_src{.transfer_buffer = tbo, .offset = 0};
        SDL_GPUBufferRegion vbo_dst{.buffer = vbo, .offset = 0, .size = VBO_SIZE};
        SDL_UploadToGPUBuffer(copy_pass, &vbo_src, &vbo_dst, false);
        SDL_GPUTransferBufferLocation ibo_src{.transfer_buffer = tbo, .offset = VBO_SIZE};
        SDL_GPUBufferRegion ibo_dst{.buffer = ibo, .offset = 0, .size = IBO_SIZE};
        SDL_UploadToGPUBuffer(copy_pass, &ibo_src, &ibo_dst, false);
        SDL_EndGPUCopyPass(copy_pass);

        // Reabrir render pass sobre OFFSCREEN (load_op=LOAD para preservar el clear).
        SDL_GPUColorTargetInfo color_target{};
        color_target.texture = offscreen_texture_;
        color_target.load_op = SDL_GPU_LOADOP_LOAD;
        color_target.store_op = SDL_GPU_STOREOP_STORE;
        render_pass_ = SDL_BeginGPURenderPass(cmd_buffer_, &color_target, 1, nullptr);

        SDL_BindGPUGraphicsPipeline(render_pass_, line_pipeline_.get());

        // UBO de líneas usa el tamaño lógico (también del offscreen).
        LineUniforms ubo{.viewport_width = logical_w_,
            .viewport_height = logical_h_,
            .padding_0 = 0.0F,
            .padding_1 = 0.0F};
        SDL_PushGPUVertexUniformData(cmd_buffer_, 0, &ubo, sizeof(ubo));

        SDL_GPUBufferBinding vbo_binding{.buffer = vbo, .offset = 0};
        SDL_BindGPUVertexBuffers(render_pass_, 0, &vbo_binding, 1);

        SDL_GPUBufferBinding ibo_binding{.buffer = ibo, .offset = 0};
        SDL_BindGPUIndexBuffer(render_pass_, &ibo_binding, SDL_GPU_INDEXELEMENTSIZE_16BIT);

        SDL_DrawGPUIndexedPrimitives(render_pass_,
            static_cast<uint32_t>(indices_.size()),
            1,
            0,
            0,
            0);

        SDL_ReleaseGPUBuffer(dev, vbo);
        SDL_ReleaseGPUBuffer(dev, ibo);
        SDL_ReleaseGPUTransferBuffer(dev, tbo);
    }

    void GpuFrameRenderer::bloomPass() {
        // Tanca el render pass actual (sobre l'offscreen) abans de canviar de
        // target. Cada passada de bloom obre el seu propi render pass.
        if (render_pass_ != nullptr) {
            SDL_EndGPURenderPass(render_pass_);
            render_pass_ = nullptr;
        }

        // Si el bloom està desactivat (per config o per l'interruptor mestre
        // F6), fem clear a negre sobre bloom_b perquè el composite el samplegi
        // com a "sense bloom" sense haver de tenir un path alternatiu al shader.
        const bool BLOOM_ON = postfx_enabled_ && postfx_params_.bloom_enabled;
        const float TEXEL_X = 1.0F / render_w_;
        const float TEXEL_Y = 1.0F / render_h_;
        const float SIGMA = postfx_params_.bloom_sigma_px;
        const float THRESHOLD = postfx_params_.bloom_threshold;

        if (!BLOOM_ON) {
            // Clear bloom_b a negre i prou.
            SDL_GPUColorTargetInfo clear_target{};
            clear_target.texture = bloom_texture_b_;
            clear_target.clear_color = SDL_FColor{.r = 0.0F, .g = 0.0F, .b = 0.0F, .a = 1.0F};
            clear_target.load_op = SDL_GPU_LOADOP_CLEAR;
            clear_target.store_op = SDL_GPU_STOREOP_STORE;
            clear_target.cycle = false;
            SDL_GPURenderPass* clear_pass = SDL_BeginGPURenderPass(cmd_buffer_, &clear_target, 1, nullptr);
            if (clear_pass != nullptr) {
                SDL_EndGPURenderPass(clear_pass);
            }
            return;
        }

        // === PASS H: high-pass + gaussiana horitzontal ===
        // Llegim de l'offscreen i escrivim a bloom_texture_a_.
        {
            SDL_GPUColorTargetInfo target{};
            target.texture = bloom_texture_a_;
            target.clear_color = SDL_FColor{.r = 0.0F, .g = 0.0F, .b = 0.0F, .a = 1.0F};
            target.load_op = SDL_GPU_LOADOP_CLEAR;
            target.store_op = SDL_GPU_STOREOP_STORE;
            target.cycle = false;
            SDL_GPURenderPass* pass = SDL_BeginGPURenderPass(cmd_buffer_, &target, 1, nullptr);
            if (pass == nullptr) {
                std::cerr << "[GpuFrameRenderer] BeginRenderPass (bloom H): "
                          << SDL_GetError() << '\n';
                return;
            }
            SDL_BindGPUGraphicsPipeline(pass, bloom_pipeline_.get());

            SDL_GPUTextureSamplerBinding binding{};
            binding.texture = offscreen_texture_;
            binding.sampler = linear_sampler_;
            SDL_BindGPUFragmentSamplers(pass, 0, &binding, 1);

            BloomUniforms ubo{};
            ubo.texel_size_x = TEXEL_X;
            ubo.texel_size_y = TEXEL_Y;
            ubo.direction_x = 1.0F;
            ubo.direction_y = 0.0F;
            ubo.threshold = THRESHOLD;
            ubo.extract = 1.0F;  // primera passada → aplica high-pass
            ubo.sigma = SIGMA;
            ubo.pad_a = 0.0F;
            SDL_PushGPUFragmentUniformData(cmd_buffer_, 0, &ubo, sizeof(ubo));

            SDL_DrawGPUPrimitives(pass, 3, 1, 0, 0);
            SDL_EndGPURenderPass(pass);
        }

        // === PASS V: gaussiana vertical (sense high-pass) ===
        // Llegim de bloom_texture_a_ i escrivim a bloom_texture_b_.
        {
            SDL_GPUColorTargetInfo target{};
            target.texture = bloom_texture_b_;
            target.clear_color = SDL_FColor{.r = 0.0F, .g = 0.0F, .b = 0.0F, .a = 1.0F};
            target.load_op = SDL_GPU_LOADOP_CLEAR;
            target.store_op = SDL_GPU_STOREOP_STORE;
            target.cycle = false;
            SDL_GPURenderPass* pass = SDL_BeginGPURenderPass(cmd_buffer_, &target, 1, nullptr);
            if (pass == nullptr) {
                std::cerr << "[GpuFrameRenderer] BeginRenderPass (bloom V): "
                          << SDL_GetError() << '\n';
                return;
            }
            SDL_BindGPUGraphicsPipeline(pass, bloom_pipeline_.get());

            SDL_GPUTextureSamplerBinding binding{};
            binding.texture = bloom_texture_a_;
            binding.sampler = linear_sampler_;
            SDL_BindGPUFragmentSamplers(pass, 0, &binding, 1);

            BloomUniforms ubo{};
            ubo.texel_size_x = TEXEL_X;
            ubo.texel_size_y = TEXEL_Y;
            ubo.direction_x = 0.0F;
            ubo.direction_y = 1.0F;
            ubo.threshold = 0.0F;
            ubo.extract = 0.0F;  // segona passada → blur pur
            ubo.sigma = SIGMA;
            ubo.pad_a = 0.0F;
            SDL_PushGPUFragmentUniformData(cmd_buffer_, 0, &ubo, sizeof(ubo));

            SDL_DrawGPUPrimitives(pass, 3, 1, 0, 0);
            SDL_EndGPURenderPass(pass);
        }
    }

    void GpuFrameRenderer::compositePass() {
        // Cierra el render pass actual (sobre offscreen).
        if (render_pass_ != nullptr) {
            SDL_EndGPURenderPass(render_pass_);
            render_pass_ = nullptr;
        }

        // Pase final: render pass sobre SWAPCHAIN con clear a negro (cubre el
        // letterbox del viewport físico).
        SDL_GPUColorTargetInfo target{};
        target.texture = swapchain_texture_;
        target.clear_color = SDL_FColor{.r = 0.0F, .g = 0.0F, .b = 0.0F, .a = 1.0F};
        target.load_op = SDL_GPU_LOADOP_CLEAR;
        target.store_op = SDL_GPU_STOREOP_STORE;
        target.cycle = false;
        render_pass_ = SDL_BeginGPURenderPass(cmd_buffer_, &target, 1, nullptr);
        if (render_pass_ == nullptr) {
            std::cerr << "[GpuFrameRenderer] BeginRenderPass (composite): "
                      << SDL_GetError() << '\n';
            return;
        }
        applyFinalViewport();

        SDL_BindGPUGraphicsPipeline(render_pass_, postfx_pipeline_.get());

        // Bind de dos samplers: slot 0 = escena offscreen, slot 1 = bloom V.
        // El bloom V conté ja el resultat de les dues passes separables.
        SDL_GPUTextureSamplerBinding sampler_bindings[2]{};
        sampler_bindings[0].texture = offscreen_texture_;
        sampler_bindings[0].sampler = linear_sampler_;
        sampler_bindings[1].texture = bloom_texture_b_;
        sampler_bindings[1].sampler = linear_sampler_;
        SDL_BindGPUFragmentSamplers(render_pass_, 0, sampler_bindings, 2);

        // Uniforms del postpro. Si una sección está desactivada, anulamos sus
        // contribuciones (intensidad / amplitud / max=min) en lugar de tener
        // un branch en el shader.
        // L'interruptor mestre (postfx_enabled_) gateja totes les seccions: si
        // està OFF (F6), tot queda anul·lat i el composite és passthrough de
        // l'escena offscreen.
        const bool BLOOM_ON = postfx_enabled_ && postfx_params_.bloom_enabled;
        const bool FLICKER_ON = postfx_enabled_ && postfx_params_.flicker_enabled;
        const bool BG_ON = postfx_enabled_ && postfx_params_.background_enabled;
        const float BLOOM_INTENSITY = BLOOM_ON ? postfx_params_.bloom_intensity : 0.0F;
        const float FLICKER_AMPLITUDE = FLICKER_ON ? postfx_params_.flicker_amplitude : 0.0F;
        const float BG_MIN_R = BG_ON ? postfx_params_.background_min_r : 0.0F;
        const float BG_MIN_G = BG_ON ? postfx_params_.background_min_g : 0.0F;
        const float BG_MIN_B = BG_ON ? postfx_params_.background_min_b : 0.0F;
        const float BG_MAX_R = BG_ON ? postfx_params_.background_max_r : 0.0F;
        const float BG_MAX_G = BG_ON ? postfx_params_.background_max_g : 0.0F;
        const float BG_MAX_B = BG_ON ? postfx_params_.background_max_b : 0.0F;

        // Tiempo en segundos desde el inicio de SDL (wall-clock real, robusto a FPS variables).
        const float TIME_SECONDS = static_cast<float>(SDL_GetTicks()) / 1000.0F;

        PostFxUniforms ubo{};
        ubo.time = TIME_SECONDS;
        ubo.bloom_intensity = BLOOM_INTENSITY;
        ubo.flicker_amplitude = FLICKER_AMPLITUDE;
        ubo.flicker_frequency_hz = postfx_params_.flicker_frequency_hz;
        ubo.background_pulse_freq_hz = postfx_params_.background_pulse_freq_hz;
        ubo.pad_a = 0.0F;
        ubo.pad_b = 0.0F;
        ubo.pad_c = 0.0F;
        ubo.background_min_r = BG_MIN_R;
        ubo.background_min_g = BG_MIN_G;
        ubo.background_min_b = BG_MIN_B;
        ubo.background_min_a = 1.0F;
        ubo.background_max_r = BG_MAX_R;
        ubo.background_max_g = BG_MAX_G;
        ubo.background_max_b = BG_MAX_B;
        ubo.background_max_a = 1.0F;

        SDL_PushGPUFragmentUniformData(cmd_buffer_, 0, &ubo, sizeof(ubo));

        // Fullscreen triangle: 3 vértices generados en el shader, sin VBO.
        SDL_DrawGPUPrimitives(render_pass_, 3, 1, 0, 0);
    }

    void GpuFrameRenderer::endFrame() {
        if (cmd_buffer_ == nullptr) {
            return;
        }
        flushBatch();
        bloomPass();
        compositePass();
        if (render_pass_ != nullptr) {
            SDL_EndGPURenderPass(render_pass_);
            render_pass_ = nullptr;
        }
        SDL_SubmitGPUCommandBuffer(cmd_buffer_);
        cmd_buffer_ = nullptr;
        swapchain_texture_ = nullptr;
    }

}  // namespace Rendering::GPU
