Nimbin[12]?SDL & Graphics / sdl_runtime_compiler / src/rendering/gl_renderer.cpp

sdl_runtime_compiler git · main

SDL3 game for running and compiling code at runtime

sdl3 c++ compiler dlopen cmake · first commit 2026-04-19 · last commit 2026-07-03 (3 months ago) · synced 3 days ago · upstream: git.ide3.de/hsnr/sdl-runtime-compiler

C++ 72.3% C 26.2%
git clone https://git.christianimmanuel.de/sdl-graphics/sdl_runtime_compiler.gitwget https://git.christianimmanuel.de/sdl-graphics/sdl_runtime_compiler/archive/sdl_runtime_compiler.tar.gz
src/rendering/gl_renderer.cpp 46.5 KB · 1299 lines raw
#include <SDL3/SDL.h>
#include <cstring>

#include "rendering/gl_renderer.hpp"
#include "utils/frame_profiler.hpp"

namespace
Nimbin
{

// ─────────────────────────────────────────────────────────────────────────────
// GLSL shaders
// ─────────────────────────────────────────────────────────────────────────────

static const char* kBlitVertSrc = R"GLSL(
#version 330 core
layout(location = 0) in vec2 aPos;
out vec2 vUV;
void main()
{
    vUV = aPos * 0.5 + 0.5;     // [-1,1] → [0,1]
    gl_Position = vec4(aPos, 0.0, 1.0);
}
)GLSL";

static const char* kBlitFragSrc = R"GLSL(
#version 330 core
in  vec2 vUV;
out vec4 FragColor;
uniform sampler2D u_SceneTex;
void main()
{
    FragColor = texture(u_SceneTex, vUV);
}
)GLSL";

static const char* kCrosshairFragSrc = R"GLSL(
#version 330 core
out vec4 FragColor;
void main() { FragColor = vec4(1.0, 1.0, 1.0, 1.0); }
)GLSL";


static const char* kExtractFragSrc = R"GLSL(
#version 330 core
in  vec2 vUV;
out vec4 FragColor;
uniform sampler2D u_SceneTex;
void main()
{
    vec4 s = texture(u_SceneTex, vUV);
    // The alpha channel carries the emissive_strength written by the main
    // shader. Multiply the scene RGB by that mask: pixels with alpha=0
    // produce zero bright output; pixels with alpha=1 contribute their
    // full color to the bloom.
    FragColor = vec4(s.rgb * s.a, 1.0);
}
)GLSL";

static const char* kBlurFragSrc = R"GLSL(
#version 330 core
in  vec2 vUV;
out vec4 FragColor;
uniform sampler2D u_BlurTex;
uniform vec2  u_BlurDir;
uniform vec2  u_BlurTexelSize;

// 3-tap separable Gaussian using hardware bilinear filtering.
// Equivalent to a 5-tap discrete kernel with sigma ≈ 1.0
// (texels -2..+2 with the standard binomial weights 1,4,6,4,1 / 16).
// Center weight = 6/16 = 0.375
// Outer weight  = (4+1)/16 = 0.3125 per side
// Outer offset  = (1*4 + 2*1) / (4+1) = 1.2 texels
const float W_CENTER = 0.375;
const float W_OFFSET = 0.3125;
const float OFFSET   = 1.2;

void main()
{
    vec2 off = u_BlurDir * u_BlurTexelSize;
    vec3 col = texture(u_BlurTex, vUV).rgb * W_CENTER;
    col += texture(u_BlurTex, vUV + off * OFFSET).rgb * W_OFFSET;
    col += texture(u_BlurTex, vUV - off * OFFSET).rgb * W_OFFSET;
    FragColor = vec4(col, 1.0);
}
)GLSL";

static const char* kCompositeFragSrc = R"GLSL(
#version 330 core
in  vec2 vUV;
out vec4 FragColor;
uniform sampler2D u_SceneTex;
uniform sampler2D u_BloomTex;
uniform float     u_BloomStrength;

void main()
{
    vec3 scene = texture(u_SceneTex, vUV).rgb;
    vec3 bloom = texture(u_BloomTex, vUV).rgb;
    FragColor = vec4(scene + bloom * u_BloomStrength, 1.0);
}
)GLSL";

static const char* kVertSrc = R"GLSL(
#version 330 core
 
layout(location = 0) in vec3 aPos;
layout(location = 1) in vec3 aNormal;
layout(location = 2) in vec3 aColor;
 
uniform mat4 u_MVP;
uniform mat4 u_Model;          // NEW — for world-space position
uniform mat3 u_NormalMat;
 
out vec3 vNormal;
out vec3 vColor;
out vec3 vWorldPos;             // NEW
 
void main()
{
    vec4 worldPos = u_Model * vec4(aPos, 1.0);
    vWorldPos     = worldPos.xyz;
    gl_Position   = u_MVP * vec4(aPos, 1.0);
    vNormal       = normalize(u_NormalMat * aNormal);
    vColor        = aColor;
}
)GLSL";

static const char* kFragSrc = R"GLSL(
#version 330 core

in  vec3 vNormal;
in  vec3 vColor;
in  vec3 vWorldPos;
out vec4 FragColor;

uniform vec3  u_Color;
uniform vec3  u_BodyColor;
uniform float u_BackDim;
uniform bool  u_UseFront;
uniform bool  u_Wireframe;
uniform bool  u_UseVertexColor;
uniform bool  u_Mandelbrot;
uniform bool  u_WallPaper;

uniform vec3  u_LightDir;
uniform vec3  u_SunColor;
uniform float u_Intensity;
uniform vec3  u_Ambient;

uniform vec3  u_FlashPos;
uniform vec3  u_FlashDir;
uniform vec3  u_FlashColor;
uniform float u_FlashRange;
uniform float u_FlashInnerCos;
uniform float u_FlashOuterCos;
uniform float u_FlashIntensity;

uniform vec3  u_EmissiveColor;
uniform float u_EmissiveStrength;

const float LINE_SPACING_M = 0.7;
const float LINE_THICKNESS = 0.015;
const vec3  LINE_COLOR     = vec3(0.45, 0.55, 0.78);

vec3 paper_color(float world_y, vec3 paper_base) {
    float t = world_y / LINE_SPACING_M;
    float frac = abs(t - floor(t + 0.5));
    float line_dist = frac * LINE_SPACING_M;
    float line = 1.0 - smoothstep(LINE_THICKNESS * 0.5,
                                  LINE_THICKNESS * 1.5,
                                  line_dist);
    return mix(paper_base, LINE_COLOR, line);
}

const vec3 BAND_PALETTE[8] = vec3[8](
    vec3(0.85, 0.40, 0.55), vec3(0.95, 0.55, 0.30),
    vec3(0.95, 0.85, 0.35), vec3(0.55, 0.85, 0.40),
    vec3(0.30, 0.80, 0.75), vec3(0.40, 0.55, 0.95),
    vec3(0.65, 0.40, 0.90), vec3(0.85, 0.45, 0.85)
);
const vec3 INSIDE_SET = vec3(0.04, 0.03, 0.08);
const int MAX_ITER = 20;

vec3 mandelbrot_color(vec2 c) {
    vec2 z = vec2(0.0);
    int i = 0;
    for (int k = 0; k < MAX_ITER; ++k) {
        if (dot(z, z) > 1.0) break;
        z = vec2(z.x*z.x - z.y*z.y + c.x, 2.0*z.x*z.y + c.y);
        i = k + 1;
    }
    if (i >= MAX_ITER) {
        float angle = atan(z.y, z.x);
        float t = (angle + 3.14159) / 6.28318;
        int idx = int(t * 8.0) & 7;
        return BAND_PALETTE[idx] * 0.35;
    }
    return BAND_PALETTE[i & 7];
}

vec3 mandelbrot_smooth(vec2 c) {
    vec2 dx = dFdx(c) * 0.5;
    vec2 dy = dFdy(c) * 0.5;
    vec3 col = vec3(0.0);
    col += mandelbrot_color(c + (-dx - dy) * 0.25);
    col += mandelbrot_color(c + ( dx - dy) * 0.25);
    col += mandelbrot_color(c + (-dx + dy) * 0.25);
    col += mandelbrot_color(c + ( dx + dy) * 0.25);
    return col * 0.25;
}

void main()
{
    vec3 baseColor;
    if      (u_Mandelbrot) baseColor = mandelbrot_smooth(vColor.xy);
    else if (u_WallPaper)  baseColor = paper_color(vColor.x, u_Color);
    else if (u_UseFront)   baseColor = (vColor.x > 0.5)
                                       ? u_BodyColor * u_BackDim
                                       : u_BodyColor;
    else                   baseColor = u_UseVertexColor ? vColor : u_Color;

    if (u_Wireframe) {
        FragColor = vec4(baseColor, 0.0);
        return;
    }

    vec3 N = normalize(vNormal);
    float sun_d = max(dot(N, -u_LightDir), 0.0);
    vec3  sun   = u_SunColor * sun_d * u_Intensity;

    vec3 flash = vec3(0.0);
    if (u_FlashIntensity > 0.0) {
        vec3  to_surface = vWorldPos - u_FlashPos;
        float dist       = length(to_surface);
        vec3  beam       = to_surface / max(dist, 0.0001);

        float forward = dot(u_FlashDir, beam);   // 1=on-axis, 0=perp, <0=behind

        if (forward > u_FlashOuterCos) {
            float diff = max(dot(N, -beam), 0.0);

            // Smooth cone: full brightness inside inner_cos, fades to zero at outer_cos.
            float cone = smoothstep(u_FlashOuterCos, u_FlashInnerCos, forward);

            // Linear distance falloff; fully dark at flashlight_range.
            float atten = clamp(1.0 - dist / u_FlashRange, 0.0, 1.0);

            flash = u_FlashColor * diff * cone * atten * u_FlashIntensity;
        }
    }

    vec3 light_factor = u_Ambient + sun + flash;
    light_factor = min(light_factor, vec3(1.0));

    // Add emissive contribution: makes the surface bright regardless of lighting.
    vec3 lit = baseColor * light_factor + u_EmissiveColor * u_EmissiveStrength;

    // Final color. Alpha is the bloom mask — extraction reads this.
    FragColor = vec4(clamp(lit, 0.0, 1.0), u_EmissiveStrength);
}
)GLSL";

// ─────────────────────────────────────────────────────────────────────────────
// box_Mesh  (internal helper for draw_Box)
//
// Generates a lit, face-normal box centred at origin scaled by hx/hy/hz.
// Layout: [px,py,pz, nx,ny,nz] per vertex, 4 verts per face, 2 tris per face.
// ─────────────────────────────────────────────────────────────────────────────
static void box_Mesh(float hx, float hy, float hz,
                     DynArray<float>& verts,
                     DynArray<int>&   indices)
{
    struct Face { float nx,ny,nz; float v[4][3]; };
    const Face faces[6] = {
        // +X face (right): vertices at x=+hx, normal points +X
        { 1,  0,  0, {{ hx,-hy,-hz },{ hx, hy,-hz },{ hx, hy, hz },{ hx,-hy, hz }} },
        // -X face (left):  vertices at x=-hx, normal points -X
        {-1,  0,  0, {{-hx,-hy, hz },{-hx, hy, hz },{-hx, hy,-hz },{-hx,-hy,-hz }} },
        // +Y face (top):   vertices at y=+hy, normal points +Y
        { 0,  1,  0, {{-hx, hy,-hz },{ hx, hy,-hz },{ hx, hy, hz },{-hx, hy, hz }} },
        // -Y face (bottom):vertices at y=-hy, normal points -Y
        { 0, -1,  0, {{-hx,-hy, hz },{ hx,-hy, hz },{ hx,-hy,-hz },{-hx,-hy,-hz }} },
        // +Z face (front): vertices at z=+hz, normal points +Z
        { 0,  0,  1, {{-hx,-hy, hz },{-hx, hy, hz },{ hx, hy, hz },{ hx,-hy, hz }} },
        // -Z face (back):  vertices at z=-hz, normal points -Z
        { 0,  0, -1, {{ hx,-hy,-hz },{ hx, hy,-hz },{-hx, hy,-hz },{-hx,-hy,-hz }} },
    };
    for (int f = 0; f < 6; f++) {
        int base = static_cast<int>(verts.size()) / 6;
        for (int v = 0; v < 4; v++) {
            verts.push_back(faces[f].v[v][0]); verts.push_back(faces[f].v[v][1]);
            verts.push_back(faces[f].v[v][2]); verts.push_back(faces[f].nx);
            verts.push_back(faces[f].ny);       verts.push_back(faces[f].nz);
        }
        indices.push_back(base); indices.push_back(base+1); indices.push_back(base+2);
        indices.push_back(base); indices.push_back(base+2); indices.push_back(base+3);
    }
}

// ─────────────────────────────────────────────────────────────────────────────
// GlRenderer::draw_Box  (new — draws a solid lit box at current MVP)
// ─────────────────────────────────────────────────────────────────────────────
void GlRenderer::draw_Box(float hx, float hy, float hz)
{
    DynArray<float> verts;
    verts.reserve(6 * 4 * 6);
    DynArray<int>   indices;
    indices.reserve(6 * 6);
    box_Mesh(hx, hy, hz, verts, indices);
    draw_Mesh(verts.data(), static_cast<int>(verts.size()) / 6,
             indices.data(), static_cast<int>(indices.size()));
}

// ─────────────────────────────────────────────────────────────────────────────
// Mat4
// ─────────────────────────────────────────────────────────────────────────────

Mat4 Mat4::identity()
{
    Mat4 m;
    m.m[0]=m.m[5]=m.m[10]=m.m[15]=1.0f;
    return m;
}

Mat4 Mat4::perspective(float fovY, float aspect, float near, float far)
{
    Mat4 m;
    float f = 1.0f / SDL_tanf(fovY * 0.5f);
    m.m[0]  =  f / aspect;
    m.m[5]  =  f;
    m.m[10] =  (far + near) / (near - far);
    m.m[11] = -1.0f;
    m.m[14] =  (2.0f * far * near) / (near - far);
    return m;
}

Mat4 Mat4::mul(const Mat4& a, const Mat4& b)
{
    Mat4 r;
    for (int col = 0; col < 4; col++)
        for (int row = 0; row < 4; row++) {
            float s = 0;
            for (int k = 0; k < 4; k++)
                s += a.m[k*4+row] * b.m[col*4+k];
            r.m[col*4+row] = s;
        }
    return r;
}

Mat4 buildViewMat4(const Camera& cam)
{
    double yaw   = Config::Physics::binrad * (double)cam.yaw;
    double pitch = Config::Physics::binrad * (double)cam.pitch;

    double cy = SDL_cos(yaw),   sy = SDL_sin(yaw);
    double cp = SDL_cos(pitch), sp = SDL_sin(pitch);

    float rx=(float)cy,      ry=0.0f,       rz=(float)(-sy);
    float ux=(float)(sy*sp), uy=(float)cp,  uz=(float)(cy*sp);
    float fx=(float)(sy*cp), fy=(float)(-sp),fz=(float)(cy*cp);

    float tx=(float)cam.pos.x, ty=(float)cam.pos.y, tz=(float)cam.pos.z;
    float ttx=-(rx*tx+ry*ty+rz*tz);
    float tty=-(ux*tx+uy*ty+uz*tz);
    float ttz=-(fx*tx+fy*ty+fz*tz);

    Mat4 v;
    v.m[0]=rx; v.m[4]=ry; v.m[8] =rz; v.m[12]=ttx;
    v.m[1]=ux; v.m[5]=uy; v.m[9] =uz; v.m[13]=tty;
    v.m[2]=fx; v.m[6]=fy; v.m[10]=fz; v.m[14]=ttz;
    v.m[3]=0;  v.m[7]=0;  v.m[11]=0;  v.m[15]=1;
    return v;
}

Mat4 buildModelMat4(const Vec3& pos, float yaw)
{
    float cy=SDL_cosf(yaw), sy=SDL_sinf(yaw);
    Mat4 m;
    m.m[0]= cy;   m.m[4]=0.0f; m.m[8] = sy;   m.m[12]=(float)pos.x;
    m.m[1]=0.0f;  m.m[5]=1.0f; m.m[9] =0.0f;  m.m[13]=(float)pos.y;
    m.m[2]=-sy;   m.m[6]=0.0f; m.m[10]= cy;   m.m[14]=(float)pos.z;
    m.m[3]=0.0f;  m.m[7]=0.0f; m.m[11]=0.0f;  m.m[15]=1.0f;
    return m;
}

Mat4 buildModelMat4(const Vec3& pos, float yaw, float pitch)
{
    float cy = SDL_cosf(yaw),   sy = SDL_sinf(yaw);
    float cp = SDL_cosf(pitch), sp = SDL_sinf(pitch);

    Mat4 m;
    // column 0 (rotated X axis)
    m.m[0]  =  cy;
    m.m[1]  =  0.0f;
    m.m[2]  = -sy;
    m.m[3]  =  0.0f;
    // column 1 (rotated Y axis)
    m.m[4]  =  sy * sp;
    m.m[5]  =  cp;
    m.m[6]  =  cy * sp;
    m.m[7]  =  0.0f;
    // column 2 (rotated Z axis)
    m.m[8]  =  sy * cp;
    m.m[9]  = -sp;
    m.m[10] =  cy * cp;
    m.m[11] =  0.0f;
    // translation
    m.m[12] = (float)pos.x;
    m.m[13] = (float)pos.y;
    m.m[14] = (float)pos.z;
    m.m[15] =  1.0f;
    return m;
}


// ─────────────────────────────────────────────────────────────────────────────
// GlRenderer::init
// ─────────────────────────────────────────────────────────────────────────────
void GlRenderer::pre_Config() {
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3);
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
    SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
    SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);
    SDL_GL_SetAttribute(SDL_GL_MULTISAMPLEBUFFERS, 1);
    SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES, 4);
}

bool GlRenderer::init(SDL_Window* window)
{
    _ctx = SDL_GL_CreateContext(window);
    if (!_ctx) {
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
                     "GL context creation failed: %s",
                     SDL_GetError());
        return false;
    }
    SDL_GL_MakeCurrent    (window, _ctx);
    SDL_GL_SetSwapInterval(0);

    int samples = 0;
    SDL_GL_GetAttribute(SDL_GL_MULTISAMPLESAMPLES, &samples);
    SDL_Log("MSAA samples: %d", samples);

    if (!gladLoadGLES2Loader((GLADloadproc)SDL_GL_GetProcAddress)) {
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to initialize GLAD"); return false;
    }

    GLuint vert = compile_Shader(GL_VERTEX_SHADER,   kVertSrc);
    GLuint frag = compile_Shader(GL_FRAGMENT_SHADER, kFragSrc);
    if (!vert || !frag)           return false;
    if (!link_Program(vert, frag)) return false;

    glDeleteShader(vert);
    glDeleteShader(frag);

    _uMVP       = glGetUniformLocation(_prog, "u_MVP");
    _uNormalMat = glGetUniformLocation(_prog, "u_NormalMat");
    _uColor     = glGetUniformLocation(_prog, "u_Color");
    _uBodyColor = glGetUniformLocation(_prog, "u_BodyColor");
    _uBackDim   = glGetUniformLocation(_prog, "u_BackDim");
    _uUseFront  = glGetUniformLocation(_prog, "u_UseFront");
    _uLightDir  = glGetUniformLocation(_prog, "u_LightDir");
    _uAmbient   = glGetUniformLocation(_prog, "u_Ambient");
    _uIntensity = glGetUniformLocation(_prog, "u_Intensity");
    _uWireframe = glGetUniformLocation(_prog, "u_Wireframe");
    _uUseVertexColor = glGetUniformLocation(_prog, "u_UseVertexColor");
    _uMandelbrot     = glGetUniformLocation(_prog, "u_Mandelbrot");
    _uWallPaper      = glGetUniformLocation(_prog, "u_WallPaper");
    _uTextLineSpacing= glGetUniformLocation(_prog, "u_TextLineSpacing");



    _uModel           = glGetUniformLocation(_prog, "u_Model");
    _uSunColor        = glGetUniformLocation(_prog, "u_SunColor");
    _uFlashPos        = glGetUniformLocation(_prog, "u_FlashPos");
    _uFlashDir        = glGetUniformLocation(_prog, "u_FlashDir");
    _uFlashColor      = glGetUniformLocation(_prog, "u_FlashColor");
    _uFlashRange      = glGetUniformLocation(_prog, "u_FlashRange");
    _uFlashInnerCos   = glGetUniformLocation(_prog, "u_FlashInnerCos");
    _uFlashOuterCos   = glGetUniformLocation(_prog, "u_FlashOuterCos");
    _uFlashIntensity  = glGetUniformLocation(_prog, "u_FlashIntensity");

    glGenVertexArrays        (1, &_vao); glGenBuffers(1, &_vbo); glGenBuffers(1, &_ebo);
    glBindVertexArray        (_vao);
    glBindBuffer             (GL_ARRAY_BUFFER, _vbo);
    glVertexAttribPointer    (0, 3, GL_FLOAT, GL_FALSE, 6*sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);
    glVertexAttribPointer    (1, 3, GL_FLOAT, GL_FALSE, 6*sizeof(float), (void*)(3*sizeof(float)));
    glEnableVertexAttribArray(1);
    glBindVertexArray        (0);

    glGenVertexArrays        (1, &_line_vao); glGenBuffers(1, &_line_vbo);
    glBindVertexArray        (_line_vao);
    glBindBuffer             (GL_ARRAY_BUFFER, _line_vbo);
    glVertexAttribPointer    (0, 3, GL_FLOAT, GL_FALSE, 3*sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);
    glVertexAttrib3f         (1, 0.0f, 0.0f, 1.0f);
    glBindVertexArray        (0);

    glGenVertexArrays(1, &_vao_c);
    glGenBuffers     (1, &_vbo_c);
    glGenBuffers     (1, &_ebo_c);
    glBindVertexArray(_vao_c);
    glBindBuffer(GL_ARRAY_BUFFER, _vbo_c);
    // 9 floats per vertex: pos(3) + normal(3) + color(3)
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 9 * sizeof(float),
                          (void*)0);
    glEnableVertexAttribArray(0);
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 9 * sizeof(float),
                          (void*)(3 * sizeof(float)));
    glEnableVertexAttribArray(1);
    glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, 9 * sizeof(float),
                          (void*)(6 * sizeof(float)));
    glEnableVertexAttribArray(2);
    glBindVertexArray(0);

    glEnable(GL_DEPTH_TEST);
    glDepthFunc(GL_LESS);

    // Bloom infrastructure setup.
    create_FullscreenQuad_();
    create_BlitProgram_();

    create_ExtractProgram_();
    create_BlurProgram_();
    create_CompositeProgram_();

    {   // crosshair: reuse the blit vertex shader (vec2 aPos @ loc 0)
        GLuint v = compile_Shader(GL_VERTEX_SHADER,   kBlitVertSrc);
        GLuint f = compile_Shader(GL_FRAGMENT_SHADER, kCrosshairFragSrc);
        _crosshair_prog = glCreateProgram();
        glAttachShader(_crosshair_prog, v);
        glAttachShader(_crosshair_prog, f);
        glLinkProgram (_crosshair_prog);
        glDeleteShader(v); glDeleteShader(f);

        glGenVertexArrays(1, &_crosshair_vao);
        glGenBuffers     (1, &_crosshair_vbo);
        glBindVertexArray(_crosshair_vao);
        glBindBuffer     (GL_ARRAY_BUFFER, _crosshair_vbo);
        glVertexAttribPointer    (0, 2, GL_FLOAT, GL_FALSE, 2*sizeof(float), (void*)0);
        glEnableVertexAttribArray(0);
        glBindVertexArray(0);
    }
 
    // Lookup the new emissive uniforms on the main program.
    _uEmissiveColor    = glGetUniformLocation(_prog, "u_EmissiveColor");
    _uEmissiveStrength = glGetUniformLocation(_prog, "u_EmissiveStrength");


    { // thats for the menu
        static const float tri[] = {
            0.0f, 0.5f, 0.0f,
            -0.5f,-0.5f, 0.0f,
            0.5f,-0.5f, 0.0f
        };
        glGenVertexArrays(1, &_menu_vao);
        glGenBuffers     (1, &_menu_vbo);
        glBindVertexArray(_menu_vao);
        glBindBuffer     (GL_ARRAY_BUFFER, _menu_vbo);
        glBufferData     (GL_ARRAY_BUFFER, sizeof(tri), tri, GL_STATIC_DRAW);
        glVertexAttribPointer    (0, 3, GL_FLOAT, GL_FALSE, 3*sizeof(float), (void*)0);
        glEnableVertexAttribArray(0);
        glVertexAttrib3f (1, 0.0f, 0.0f, 1.0f);
        glBindVertexArray(0);
    }

    SDL_Log("OpenGL %s, GLSL %s",
        glGetString(GL_VERSION), glGetString(GL_SHADING_LANGUAGE_VERSION));
    return true;
}

void
GlRenderer::begin_Frame(int w, int h)
{
    _view_w = w; _view_h = h;
    _aspect = (h != 0) ? (float)w/(float)h : 1.0f;

    // Lazy-create or resize the FBO to match the window.
    if (_scene_fbo == 0) create_FBO_(w, h);
    else                  resize_FBO_(w, h);

    // Bind the FBO as the render target.
    glBindFramebuffer(GL_FRAMEBUFFER, _scene_fbo_ms);
    glViewport  (0, 0, w, h);
    glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
    glClear     (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    glUseProgram(_prog);
    glUniform1i (_uMandelbrot, 0);
    glUniform1i (_uWallPaper,  0);
    glUniform1i (_uUseFront,   0);
    Mat4 ident = Mat4::identity();
    glUniformMatrix4fv(_uModel, 1, GL_FALSE, ident.m);
    glUniform1i (_uUseVertexColor, 0);
    glUniform3f (_uEmissiveColor, 0.0f, 0.0f, 0.0f);
    glUniform1f (_uEmissiveStrength, 0.0f);
    _proj = Mat4::perspective(SDL_PI_F/3.0f, _aspect, 0.1f, 200.0f);
}

void
GlRenderer::end_Frame(SDL_Window* window)
{
    glBindFramebuffer(GL_READ_FRAMEBUFFER, _scene_fbo_ms);
    glBindFramebuffer(GL_DRAW_FRAMEBUFFER, _scene_fbo);
    glBlitFramebuffer(0, 0, _fbo_w, _fbo_h,
                      0, 0, _fbo_w, _fbo_h,
                      GL_COLOR_BUFFER_BIT, GL_NEAREST);   // MS resolve requires NEAREST
    glBindFramebuffer(GL_FRAMEBUFFER, 0);

    // Run bloom passes (extract + blur).
    { FP_ZONE("end_Frame.bloom");
        run_BloomPipeline_();

        // Composite scene + bloom to screen.
        glBindFramebuffer(GL_FRAMEBUFFER, 0);
        glViewport(0, 0, _view_w, _view_h);
        glDisable(GL_DEPTH_TEST);

        glUseProgram   (_composite_prog);
        glActiveTexture(GL_TEXTURE0);
        glBindTexture  (GL_TEXTURE_2D, _scene_color_tex);
        glUniform1i    (_uComposeScene, 0);
        glActiveTexture(GL_TEXTURE1);
        glBindTexture  (GL_TEXTURE_2D, _blur_tex[1]);   // final bloom
        glUniform1i    (_uComposeBloom, 1);
        glUniform1f    (_uComposeStrength, _bloom_strength);
    }

    { FP_ZONE("end_Frame.composite");
        glBindVertexArray(_fullscreen_vao);
        glDrawArrays(GL_TRIANGLES, 0, 6);
        glBindVertexArray(0);

        glActiveTexture(GL_TEXTURE0);                  // reset to default unit
        glEnable(GL_DEPTH_TEST);
        glUseProgram(_prog);
    }

    draw_Crosshair_();

    { FP_ZONE("end_Frame.swap");
        SDL_GL_SwapWindow(window);
    }
}


void
GlRenderer::clear_Depth()
{
    glClear(GL_DEPTH_BUFFER_BIT);
}

// ─────────────────────────────────────────────────────────────────────────────
// Uniform setters
// ─────────────────────────────────────────────────────────────────────────────
void GlRenderer::set_MVP(const Mat4& model, const Mat4& mvp)
{
    glUniformMatrix4fv(_uMVP,   1, GL_FALSE, mvp.m);
    glUniformMatrix4fv(_uModel, 1, GL_FALSE, model.m);
}

void GlRenderer::set_NormalMat(const Mat4& model)
{
    // Upper-left 3×3 of model IS the normal matrix for orthogonal transforms
    // (rotation + translation only). Normals land in world space, which matches
    // the world-space lighting in the fragment shader.
    // If non-uniform scale is ever added, replace with transpose(inverse(model_3x3)).
    float n[9] = {
        model.m[0], model.m[1], model.m[2],
        model.m[4], model.m[5], model.m[6],
        model.m[8], model.m[9], model.m[10]
    };
    glUniformMatrix3fv(_uNormalMat, 1, GL_FALSE, n);
}


void GlRenderer::set_Color(float r, float g, float b)
{ glUniform3f(_uColor, r, g, b); }

void GlRenderer::set_Light(const Light& light)
{
    // Sun
    glUniform3f(_uLightDir,
        (float)light.sun_direction.x,
        (float)light.sun_direction.y,
        (float)light.sun_direction.z);
    glUniform3f(_uSunColor,
        (float)light.sun_color.x,
        (float)light.sun_color.y,
        (float)light.sun_color.z);
    glUniform1f(_uIntensity, light.sun_intensity);

    // Ambient
    glUniform3f(_uAmbient,
        (float)light.ambient.x,
        (float)light.ambient.y,
        (float)light.ambient.z);

    // Flashlight
    glUniform3f(_uFlashPos,
        (float)light.flashlight_pos.x,
        (float)light.flashlight_pos.y,
        (float)light.flashlight_pos.z);
    glUniform3f(_uFlashDir,
        (float)light.flashlight_dir.x,
        (float)light.flashlight_dir.y,
        (float)light.flashlight_dir.z);
    glUniform3f(_uFlashColor,
        (float)light.flashlight_color.x,
        (float)light.flashlight_color.y,
        (float)light.flashlight_color.z);
    glUniform1f(_uFlashRange,     light.flashlight_range);
    glUniform1f(_uFlashInnerCos,  light.flashlight_inner_cos);
    glUniform1f(_uFlashOuterCos,  light.flashlight_outer_cos);
    glUniform1f(_uFlashIntensity, light.flashlight_intensity);
}

void GlRenderer::set_Wireframe(bool on)
{ glUniform1i(_uWireframe, on ? 1 : 0); }

void GlRenderer::set_ColorUseVertex(bool on)
{ glUniform1i(_uUseVertexColor, on ? 1 : 0); }

void GlRenderer::set_TextLineSpacing(float m)
{ glUniform1f(_uTextLineSpacing, m); }

void
GlRenderer::set_Emissive(float r, float g, float b, float strength)
{
    glUniform3f(_uEmissiveColor, r, g, b);
    glUniform1f(_uEmissiveStrength, strength);
}

void
GlRenderer::set_FaceShade(float r, float g, float b, float back_dim, bool use_front)
{
    glUniform3f(_uBodyColor, r, g, b);
    glUniform1f(_uBackDim,   back_dim);
    glUniform1i(_uUseFront,  use_front ? 1 : 0);
}


void
GlRenderer::set_DepthTest(bool on)
{
    if (on) glEnable (GL_DEPTH_TEST);
    else    glDisable(GL_DEPTH_TEST);
}


void GlRenderer::set_CullBack(bool on)
{
    if (on) { glEnable(GL_CULL_FACE); glCullFace(GL_FRONT); }
    else      glDisable(GL_CULL_FACE);
}

/////////////////
/// DRAW

void
GlRenderer::draw_Crosshair_()
{
    if (!_crosshair_enabled || _view_w <= 0 || _view_h <= 0) return;

    // Pixel dimensions → NDC half-extents (per-axis, so it stays square).
    const float gap = 4.0f, len = 9.0f, thick = 1.5f;
    const float gx = gap   / static_cast<float>(_view_w) * 2.0f, gy = gap   / static_cast<float>(_view_h) * 2.0f;
    const float lx = len   / static_cast<float>(_view_w) * 2.0f, ly = len   / static_cast<float>(_view_h) * 2.0f;
    const float tx = thick / static_cast<float>(_view_w) * 2.0f, ty = thick / static_cast<float>(_view_h) * 2.0f;

    // Four arms, center gap, NO overlap (overlap would invert twice = no-op).
    float v[48];
    int   k = 0;
    auto quad = [&](float x0, float y0, float x1, float y1) {
        v[k++]=x0; v[k++]=y0;  v[k++]=x1; v[k++]=y0;  v[k++]=x1; v[k++]=y1;
        v[k++]=x0; v[k++]=y0;  v[k++]=x1; v[k++]=y1;  v[k++]=x0; v[k++]=y1;
    };
    quad(  gx,        -ty,   gx + lx,  ty);          // right
    quad(-(gx + lx),  -ty,  -gx,       ty);          // left
    quad( -tx,         gy,   tx,       gy + ly);     // up
    quad( -tx,      -(gy + ly), tx,   -gy);          // down

    glDisable  (GL_DEPTH_TEST);
    glEnable   (GL_BLEND);
    glBlendFunc(GL_ONE_MINUS_DST_COLOR, GL_ZERO);    // out = (1 - dst) → invert

    glUseProgram(_crosshair_prog);
    glBindVertexArray(_crosshair_vao);
    glBindBuffer (GL_ARRAY_BUFFER, _crosshair_vbo);
    glBufferData (GL_ARRAY_BUFFER, sizeof(v), v, GL_STREAM_DRAW);
    glDrawArrays (GL_TRIANGLES, 0, 24);
    glBindVertexArray(0);

    glDisable   (GL_BLEND);
    glEnable    (GL_DEPTH_TEST);
    glUseProgram(_prog);
}


// ─────────────────────────────────────────────────────────────────────────────
// draw_Mesh / draw_Lines
// ─────────────────────────────────────────────────────────────────────────────
void GlRenderer::draw_Mesh(const float* verts,   int vert_count,
                          const int*   indices, int index_count)
{
    glBindVertexArray(_vao);

    glBindBuffer     (GL_ARRAY_BUFFER, _vbo);
    glBufferData     (GL_ARRAY_BUFFER, static_cast<size_t>(vert_count)*6*sizeof(float), verts, GL_STREAM_DRAW);
    glBindBuffer     (GL_ELEMENT_ARRAY_BUFFER, _ebo);
    glBufferData     (GL_ELEMENT_ARRAY_BUFFER, static_cast<size_t>(index_count)*sizeof(int), indices, GL_STREAM_DRAW);
    glDrawElements   (GL_TRIANGLES, index_count, GL_UNSIGNED_INT, nullptr);

    glBindVertexArray(0);
}


void
GlRenderer::draw_MeshColored(const float* verts,   int vert_count,
                            const int*   indices, int index_count)
{
    glBindVertexArray(_vao_c);

    glBindBuffer     (GL_ARRAY_BUFFER, _vbo_c);
    glBufferData     (GL_ARRAY_BUFFER,
                      static_cast<size_t> (vert_count) * 9 * sizeof(float),
                      verts, GL_STATIC_DRAW);

    // Re-point attributes at _vbo_c — draw_MeshCached may have repointed
    // this shared VAO at a per-Text::Token::Data VBO.
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 9*sizeof(float), (void*)0);
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 9*sizeof(float), (void*)(3*sizeof(float)));
    glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, 9*sizeof(float), (void*)(6*sizeof(float)));

    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, _ebo_c);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER,
                 static_cast<size_t>(index_count) * sizeof(int),
                 indices, GL_STATIC_DRAW);
    glDrawElements   (GL_TRIANGLES, index_count, GL_UNSIGNED_INT, nullptr);

    glBindVertexArray(0);
}


void
GlRenderer::draw_MeshCached(unsigned int& vbo, unsigned int& ebo, int& index_count,
                            bool dirty,
                            const float* verts, int vert_count,
                            const int* indices, int idx_count)
{
    glBindVertexArray(_vao_c);

    if (vbo == 0) glGenBuffers(1, &vbo);
    if (ebo == 0) glGenBuffers(1, &ebo);

    glBindBuffer(GL_ARRAY_BUFFER, vbo);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);

    if (dirty) {
        glBufferData(GL_ARRAY_BUFFER,
                     (size_t)vert_count * 9 * sizeof(float),
                     verts, GL_STATIC_DRAW);
        glBufferData(GL_ELEMENT_ARRAY_BUFFER,
                     (size_t)idx_count * sizeof(int),
                     indices, GL_STATIC_DRAW);
        index_count = idx_count;
    }

    // _vao_c's attribute pointers were bound to _vbo_c at init; rebind to ours.
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 9*sizeof(float), (void*)0);
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 9*sizeof(float), (void*)(3*sizeof(float)));
    glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, 9*sizeof(float), (void*)(6*sizeof(float)));

    glDrawElements(GL_TRIANGLES, index_count, GL_UNSIGNED_INT, nullptr);

    glBindVertexArray(0);
}


void
GlRenderer::free_MeshBuffers(unsigned int vbo, unsigned int ebo)
{
    if (!SDL_GL_GetCurrentContext()) return;
    if (vbo) glDeleteBuffers(1, &vbo);
    if (ebo) glDeleteBuffers(1, &ebo);
}


void GlRenderer::draw_Lines(const float* points, int point_count)
{
    glBindVertexArray(_line_vao);

    glBindBuffer(GL_ARRAY_BUFFER, _line_vbo);
    glBufferData(GL_ARRAY_BUFFER, static_cast<size_t>(point_count)*3*sizeof(float), points, GL_STREAM_DRAW);
    glDrawArrays(GL_LINES, 0, point_count);

    glBindVertexArray(0);
}

void GlRenderer::draw_Menu()
{
    glUseProgram(_prog);
    Mat4 identity = Mat4::identity();
    glUniformMatrix4fv(_uMVP, 1, GL_FALSE, identity.m);

    float n[9] = {1,0,0, 0,1,0, 0,0,1};
    glUniformMatrix3fv(_uNormalMat, 1, GL_FALSE, n);

    glUniform3f(_uColor,      0.2f, 0.4f, 0.8f);
    glUniform3f(_uLightDir,   0.0f, 0.0f, 1.0f);
    glUniform3f(_uAmbient,    1.0f, 1.0f, 1.0f);
    glUniform1f(_uIntensity,  1.0f);
    glUniform1i(_uWireframe,  1);
    glUniform3f(_uSunColor,   1.0f, 1.0f, 1.0f);
    glUniform1i(_uMandelbrot, 0);
    glUniform1i(_uWallPaper,  0);
    glUniform3f(_uFlashPos,   0.0f, 0.0f, 0.0f);
    glUniform3f(_uFlashDir,   0.0f, 0.0f, -1.0f);
    glUniform3f(_uFlashColor, 0.0f, 0.0f,  0.0f);
    glUniform1f(_uFlashRange,     1.0f);
    glUniform1f(_uFlashInnerCos,  0.97f);
    glUniform1f(_uFlashOuterCos,  0.90f);
    glUniform1f(_uFlashIntensity, 0.0f);

    Mat4 ident_m = Mat4::identity();
    glUniformMatrix4fv(_uModel, 1, GL_FALSE, ident_m.m);


    glBindVertexArray(_menu_vao);

    glDrawArrays(GL_TRIANGLES, 0, 3);

    glBindVertexArray(0);
}


void
GlRenderer::draw_Emissive(const Camera& cam,
                         const float* verts, int vert_count,
                         const int*   indices, int index_count,
                         float r, float g, float b, float strength) {
    if (!_emissive_prog || vert_count == 0 || index_count == 0) return;

    // Push state: switch program, enable additive blending, disable depth
    // write (we still depth-TEST so halos respect occluders, but we don't
    // want halos blocking each other in the depth buffer).
    glUseProgram(_emissive_prog);

    // The vertex shader needs an MVP. We use the current proj * view * I —
    // verts are already in world space.
    Mat4 view = buildViewMat4(cam);
    Mat4 mvp  = Mat4::mul(_proj, view);
    glUniformMatrix4fv(_uEmissiveMVP, 1, GL_FALSE, mvp.m);

    glUniform3f(_uEmissiveColor, r, g, b);
    glUniform1f(_uEmissiveStrength, strength);

    glEnable   (GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE);   // additive
    glDepthMask(GL_FALSE);                // no depth write

    glBindVertexArray(_emissive_vao);
    glBindBuffer(GL_ARRAY_BUFFER, _emissive_vbo);
    glBufferData(GL_ARRAY_BUFFER,
                 static_cast<size_t>(vert_count) * 5 * sizeof(float),
                 verts, GL_STREAM_DRAW);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, _emissive_ebo);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER,
                 static_cast<size_t>(index_count) * sizeof(int),
                 indices, GL_STREAM_DRAW);
    glDrawElements(GL_TRIANGLES, index_count, GL_UNSIGNED_INT, nullptr);
    glBindVertexArray(0);

    // Pop state.
    glDisable   (GL_BLEND);
    glDepthMask (GL_TRUE);
    glUseProgram(_prog);    // restore main program
}

// ─────────────────────────────────────────────────────────────────────────────
// Shader helpers
// ─────────────────────────────────────────────────────────────────────────────
GLuint GlRenderer::compile_Shader(GLenum type, const char* src)
{
    GLuint s = glCreateShader(type);
    glShaderSource (s, 1, &src, nullptr);
    glCompileShader(s);
    GLint ok; glGetShaderiv(s, GL_COMPILE_STATUS, &ok);
    if (!ok) {
        char log[512]; glGetShaderInfoLog(s,512,nullptr,log);
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Shader compile error: %s", log);
        glDeleteShader(s);
        return 0;
    }
    return s;
}

bool GlRenderer::link_Program(GLuint vert, GLuint frag)
{
    _prog = glCreateProgram();
    glAttachShader(_prog, vert); glAttachShader(_prog, frag);
    glLinkProgram (_prog);
    GLint ok; glGetProgramiv(_prog, GL_LINK_STATUS, &ok);
    if (!ok) {
        char log[512]; glGetProgramInfoLog(_prog,512,nullptr,log);
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Program link error: %s", log);
        glDeleteProgram(_prog); _prog=0;
        return false;
    }
    return true;
}

float
GlRenderer::get_Aspect() const
{ return _aspect; }

const Mat4&
GlRenderer::get_Projection() const
{ return _proj; }

void
GlRenderer::enable_MandelbrotMode(bool on)
{ glUniform1i(_uMandelbrot, on ? 1 : 0); }

void
GlRenderer::enable_WallPaperMode(bool on)
{ glUniform1i(_uWallPaper, on ? 1 : 0); }


void
GlRenderer::create_FullscreenQuad_()
{
    static const float quad_verts[] = {
        -1.0f, -1.0f,
         1.0f, -1.0f,
         1.0f,  1.0f,
        -1.0f, -1.0f,
         1.0f,  1.0f,
        -1.0f,  1.0f,
    };
    glGenVertexArrays(1, &_fullscreen_vao);
    glGenBuffers(1, &_fullscreen_vbo);
    glBindVertexArray(_fullscreen_vao);
    glBindBuffer(GL_ARRAY_BUFFER, _fullscreen_vbo);
    glBufferData(GL_ARRAY_BUFFER, sizeof(quad_verts), quad_verts, GL_STATIC_DRAW);
    glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2*sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);
    glBindVertexArray(0);
}

void
GlRenderer::create_BlitProgram_()
{
    GLuint v = compile_Shader(GL_VERTEX_SHADER,   kBlitVertSrc);
    GLuint f = compile_Shader(GL_FRAGMENT_SHADER, kBlitFragSrc);
    _blit_prog = glCreateProgram();
    glAttachShader(_blit_prog, v);
    glAttachShader(_blit_prog, f);
    glLinkProgram(_blit_prog);
    GLint ok=0; glGetProgramiv(_blit_prog, GL_LINK_STATUS, &ok);
    if (!ok) {
        char log[512]; glGetProgramInfoLog(_blit_prog, 512, nullptr, log);
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Blit program link error: %s", log);
    }
    glDeleteShader(v);
    glDeleteShader(f);
    _uBlitTex = glGetUniformLocation(_blit_prog, "u_SceneTex");
}

void
GlRenderer::create_FBO_(int w, int h)
{
    GLint max_samples = 0;
    glGetIntegerv(GL_MAX_SAMPLES, &max_samples);
    if (_msaa_samples > max_samples) _msaa_samples = max_samples;
    if (_msaa_samples < 1)           _msaa_samples = 1;

    // ── Multisampled FBO: the scene renders here ────────────────────────
    glGenFramebuffers(1, &_scene_fbo_ms);
    glBindFramebuffer(GL_FRAMEBUFFER, _scene_fbo_ms);

    glGenRenderbuffers(1, &_scene_color_rbo_ms);
    glBindRenderbuffer(GL_RENDERBUFFER, _scene_color_rbo_ms);
    glRenderbufferStorageMultisample(GL_RENDERBUFFER, _msaa_samples, GL_RGBA8, w, h);
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
                              GL_RENDERBUFFER, _scene_color_rbo_ms);

    glGenRenderbuffers(1, &_scene_depth_rbo_ms);
    glBindRenderbuffer(GL_RENDERBUFFER, _scene_depth_rbo_ms);
    glRenderbufferStorageMultisample(GL_RENDERBUFFER, _msaa_samples,
                                     GL_DEPTH_COMPONENT24, w, h);
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT,
                              GL_RENDERBUFFER, _scene_depth_rbo_ms);

    if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "MSAA scene FBO incomplete");

    // ── Single-sample resolve FBO: bloom + composite read this texture ──
    glGenFramebuffers(1, &_scene_fbo);
    glBindFramebuffer(GL_FRAMEBUFFER, _scene_fbo);

    glGenTextures(1, &_scene_color_tex);
    glBindTexture(GL_TEXTURE_2D, _scene_color_tex);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
    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);
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
                           GL_TEXTURE_2D, _scene_color_tex, 0);
    // Depth now lives on the MS FBO, so the resolve target needs none.

    if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Resolve FBO incomplete");

    glBindFramebuffer(GL_FRAMEBUFFER, 0);
    _fbo_w = w; _fbo_h = h;
}

void
GlRenderer::create_ExtractProgram_()
{
    GLuint v = compile_Shader(GL_VERTEX_SHADER,   kBlitVertSrc);
    GLuint f = compile_Shader(GL_FRAGMENT_SHADER, kExtractFragSrc);
    _extract_prog = glCreateProgram();
    glAttachShader(_extract_prog, v);
    glAttachShader(_extract_prog, f);
    glLinkProgram(_extract_prog);
    glDeleteShader(v); glDeleteShader(f);
    _uExtractTex = glGetUniformLocation(_extract_prog, "u_SceneTex");
}

void
GlRenderer::create_BlurProgram_()
{
    GLuint v = compile_Shader(GL_VERTEX_SHADER,   kBlitVertSrc);
    GLuint f = compile_Shader(GL_FRAGMENT_SHADER, kBlurFragSrc);
    _blur_prog = glCreateProgram();
    glAttachShader(_blur_prog, v);
    glAttachShader(_blur_prog, f);
    glLinkProgram(_blur_prog);
    glDeleteShader(v); glDeleteShader(f);
    _uBlurTex       = glGetUniformLocation(_blur_prog, "u_BlurTex");
    _uBlurDir       = glGetUniformLocation(_blur_prog, "u_BlurDir");
    _uBlurTexelSize = glGetUniformLocation(_blur_prog, "u_BlurTexelSize");
}

void
GlRenderer::create_BloomFBOs_(int scene_w, int scene_h)
{
    int w = scene_w / 4;
    int h = scene_h / 4;
    if (w < 1) w = 1;
    if (h < 1) h = 1;
    _blur_w = w; _blur_h = h;

    auto make_fbo = [&](GLuint& fbo, GLuint& tex) {
        glGenFramebuffers(1, &fbo);
        glBindFramebuffer(GL_FRAMEBUFFER, fbo);
        glGenTextures(1, &tex);
        glBindTexture(GL_TEXTURE_2D, tex);
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, w, h, 0,
                     GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
        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);
        glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
                               GL_TEXTURE_2D, tex, 0);
        GLenum s = glCheckFramebufferStatus(GL_FRAMEBUFFER);
        if (s != GL_FRAMEBUFFER_COMPLETE)
            SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Bloom FBO incomplete: 0x%x", s);
    };

    make_fbo(_bright_fbo,  _bright_tex);
    make_fbo(_blur_fbo[0], _blur_tex[0]);
    make_fbo(_blur_fbo[1], _blur_tex[1]);

    glBindFramebuffer(GL_FRAMEBUFFER, 0);
}

void
GlRenderer::destroy_BloomFBOs_()
{
    if (_bright_tex)   { glDeleteTextures    (1, &_bright_tex); _bright_tex = 0; }
    if (_bright_fbo)   { glDeleteFramebuffers(1, &_bright_fbo); _bright_fbo = 0; }
    for (int i = 0; i < 2; i++) {
        if (_blur_tex[i]) { glDeleteTextures    (1, &_blur_tex[i]); _blur_tex[i] = 0; }
        if (_blur_fbo[i]) { glDeleteFramebuffers(1, &_blur_fbo[i]); _blur_fbo[i] = 0; }
    }
    _blur_w = 0; _blur_h = 0;
}

void
GlRenderer::resize_BloomFBOs_(int scene_w, int scene_h)
{
    int want_w = scene_w / 4; if (want_w < 1) want_w = 1;
    int want_h = scene_h / 4; if (want_h < 1) want_h = 1;
    if (want_w == _blur_w && want_h == _blur_h) return;
    destroy_BloomFBOs_();
    create_BloomFBOs_(scene_w, scene_h);
}

void
GlRenderer::run_BloomPipeline_()
{
    { FP_ZONE("bloom.resize"); resize_BloomFBOs_(_fbo_w, _fbo_h); }

    glDisable(GL_DEPTH_TEST);
    glBindVertexArray(_fullscreen_vao);

    { FP_ZONE("bloom.extract");
        glBindFramebuffer(GL_FRAMEBUFFER, _bright_fbo);
        glViewport(0, 0, _blur_w, _blur_h);
        glUseProgram(_extract_prog);
        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_2D, _scene_color_tex);
        glUniform1i(_uExtractTex, 0);
        glDrawArrays(GL_TRIANGLES, 0, 6);
    }


    // ── Blur ping-pong ──────────────────────────────────────────────────
    // First iteration reads from _bright_tex, then ping-pongs between
    // _blur_tex[0] and _blur_tex[1]. 4 full iterations = 8 passes.
    { FP_ZONE("bloom.blur");
        const int ITERATIONS = 2;

        glUseProgram(_blur_prog);
        glUniform2f(_uBlurTexelSize, 1.0f / static_cast<float>(_blur_w),
                                      1.0f / static_cast<float>(_blur_h));

        GLuint src_tex = _bright_tex;
        for (int i = 0; i < ITERATIONS; i++) {
            // Horizontal pass: src → blur_tex[0] via blur_fbo[0]
            glBindFramebuffer(GL_FRAMEBUFFER, _blur_fbo[0]);
            glBindTexture(GL_TEXTURE_2D, src_tex);
            glUniform1i(_uBlurTex, 0);
            glUniform2f(_uBlurDir, 1.0f, 0.0f);
            glDrawArrays(GL_TRIANGLES, 0, 6);

            // Vertical pass: blur_tex[0] → blur_tex[1] via blur_fbo[1]
            glBindFramebuffer(GL_FRAMEBUFFER, _blur_fbo[1]);
            glBindTexture(GL_TEXTURE_2D, _blur_tex[0]);
            glUniform1i(_uBlurTex, 0);
            glUniform2f(_uBlurDir, 0.0f, 1.0f);
            glDrawArrays(GL_TRIANGLES, 0, 6);

            // Next iteration's input is blur_tex[1]. We swap the names so the
            // final result always ends up in blur_tex[0] for compositing.
            src_tex = _blur_tex[1];
        }
    }

    // After loop: final bloom is in _blur_tex[1]. We'll sample it directly.
    glBindVertexArray(0);
    glEnable(GL_DEPTH_TEST);
}

void
GlRenderer::create_CompositeProgram_()
{
    GLuint v = compile_Shader(GL_VERTEX_SHADER,   kBlitVertSrc);
    GLuint f = compile_Shader(GL_FRAGMENT_SHADER, kCompositeFragSrc);
    _composite_prog = glCreateProgram();
    glAttachShader(_composite_prog, v);
    glAttachShader(_composite_prog, f);
    glLinkProgram(_composite_prog);
    glDeleteShader(v); glDeleteShader(f);
    _uComposeScene    = glGetUniformLocation(_composite_prog, "u_SceneTex");
    _uComposeBloom    = glGetUniformLocation(_composite_prog, "u_BloomTex");
    _uComposeStrength = glGetUniformLocation(_composite_prog, "u_BloomStrength");
}

void
GlRenderer::destroy_FBO_()
{
    if (_scene_color_tex)   {glDeleteTextures     (1, &_scene_color_tex);   _scene_color_tex   = 0;}
    if (_scene_depth_rbo)   {glDeleteRenderbuffers(1, &_scene_depth_rbo);   _scene_depth_rbo   = 0;}
    if (_scene_fbo)         {glDeleteFramebuffers (1, &_scene_fbo);         _scene_fbo         = 0;}
    if (_scene_color_rbo_ms){glDeleteRenderbuffers(1, &_scene_color_rbo_ms);_scene_color_rbo_ms= 0;}
    if (_scene_depth_rbo_ms){glDeleteRenderbuffers(1, &_scene_depth_rbo_ms);_scene_depth_rbo_ms= 0;}
    if (_scene_fbo_ms)      {glDeleteFramebuffers (1, &_scene_fbo_ms);      _scene_fbo_ms      = 0;}
    _fbo_w = 0; _fbo_h = 0;
}

void
GlRenderer::resize_FBO_(int w, int h)
{
    if (w == _fbo_w && h == _fbo_h) return;
    destroy_FBO_();
    create_FBO_(w, h);
}


} // namespace Nimbin