layout (local_size_x = 8, local_size_y = 8, local_size_z = 8) in;
#if GI_VOXEL_RESOLUTION == 64
    const ivec3 workGroups = ivec3(8, 8, 8);
#elif GI_VOXEL_RESOLUTION == 128
    const ivec3 workGroups = ivec3(16, 16, 16);
#elif GI_VOXEL_RESOLUTION == 192
    const ivec3 workGroups = ivec3(24, 24, 24);
#elif GI_VOXEL_RESOLUTION == 256
    const ivec3 workGroups = ivec3(32, 32, 32);
#endif

//Common Variables//
uniform sampler3D gi_voxel_sampler;

#if GI_PARITY == 0
    uniform sampler3D gi_radiance_a_sampler;
    writeonly uniform image3D gi_radiance_b_img;
#else
    uniform sampler3D gi_radiance_b_sampler;
    writeonly uniform image3D gi_radiance_a_img;
#endif

#if GI_TEMPORAL_ACCUM == 1 && defined GI_IS_LAST_PASS
    writeonly uniform image3D gi_history_img;
#endif

//Includes//
#define GI_NEED_READ_SAMPLERS
#include "/lib/voxelization/giFloodfill.glsl"

// ================================================================
//  GLSL 1.30-compatible offset helpers (no const arrays)
// ================================================================

// Emission direction: returns one of 14 directions (6 face + 8 corner)
vec3 EmitDirection(int i) {
    if (i == 0) return vec3( 1, 0, 0);
    if (i == 1) return vec3(-1, 0, 0);
    if (i == 2) return vec3( 0, 1, 0);
    if (i == 3) return vec3( 0,-1, 0);
    if (i == 4) return vec3( 0, 0, 1);
    if (i == 5) return vec3( 0, 0,-1);
    if (i == 6) return vec3( 1, 1, 1);
    if (i == 7) return vec3( 1, 1,-1);
    if (i == 8) return vec3( 1,-1, 1);
    if (i == 9) return vec3( 1,-1,-1);
    if (i ==10) return vec3(-1, 1, 1);
    if (i ==11) return vec3(-1, 1,-1);
    if (i ==12) return vec3(-1,-1, 1);
    return vec3(-1,-1,-1);
}

// Neighbor offset: returns one of 32 directions (6 face + 12 edge + 8 corner
// + 6 extended face at distance 2). GI propagation uses all directions
// including upward for full 3D light spread.
vec3 NeighborOffset(int i) {
    if (i < 6) {
        // Face (6)
        if (i == 0) return vec3( 1, 0, 0);
        if (i == 1) return vec3(-1, 0, 0);
        if (i == 2) return vec3( 0, 1, 0);
        if (i == 3) return vec3( 0,-1, 0);
        if (i == 4) return vec3( 0, 0, 1);
        return vec3( 0, 0,-1);
    }
    if (i < 18) {
        // Edge (12)
        int e = i - 6;
        if (e == 0) return vec3( 1, 1, 0);
        if (e == 1) return vec3( 1,-1, 0);
        if (e == 2) return vec3(-1, 1, 0);
        if (e == 3) return vec3(-1,-1, 0);
        if (e == 4) return vec3( 1, 0, 1);
        if (e == 5) return vec3( 1, 0,-1);
        if (e == 6) return vec3(-1, 0, 1);
        if (e == 7) return vec3(-1, 0,-1);
        if (e == 8) return vec3( 0, 1, 1);
        if (e == 9) return vec3( 0, 1,-1);
        if (e ==10) return vec3( 0,-1, 1);
        return vec3( 0,-1,-1);
    }
    if (i < 26) {
        // Corner (8)
        int c = i - 18;
        if (c == 0) return vec3( 1, 1, 1);
        if (c == 1) return vec3( 1, 1,-1);
        if (c == 2) return vec3( 1,-1, 1);
        if (c == 3) return vec3( 1,-1,-1);
        if (c == 4) return vec3(-1, 1, 1);
        if (c == 5) return vec3(-1, 1,-1);
        if (c == 6) return vec3(-1,-1, 1);
        return vec3(-1,-1,-1);
    }
    // Extended face (6) at distance 2 - wider single-pass reach so bounce
    // travels further per pass (needed to actually cross a room-sized gap
    // and reach walls/ceiling instead of stalling near the emitting surface)
    int f = i - 26;
    if (f == 0) return vec3( 2, 0, 0);
    if (f == 1) return vec3(-2, 0, 0);
    if (f == 2) return vec3( 0, 2, 0);
    if (f == 3) return vec3( 0,-2, 0);
    if (f == 4) return vec3( 0, 0, 2);
    return vec3( 0, 0,-2);
}

// Distance for neighbor index: face=1, edge=1.414, corner=1.732, extended face=2.0
float NeighborDistance(int i) {
    if (i < 6) return 1.0;
    if (i < 18) return 1.414;
    if (i < 26) return 1.732;
    return 2.0;
}

// Program//
void main() {
    ivec3 pos = ivec3(gl_GlobalInvocationID);

    #if GI_PARITY == 0
        vec4 selfRadiance = GiLoadRadiance(gi_radiance_a_sampler, pos);
    #else
        vec4 selfRadiance = GiLoadRadiance(gi_radiance_b_sampler, pos);
    #endif

    vec4 albedoOpacity = GiLoadAlbedoOpacity(pos);
    bool isSolid = albedoOpacity.a > 0.5;

    vec3 radianceNew = selfRadiance.rgb;

    // ================================================================
    //  STEP 1: SOLID VOXELS EMIT INTO OPEN SPACE
    // ================================================================
    if (isSolid && GetLuminance(selfRadiance.rgb) > 0.001
        && (GI_SINGLE_BOUNCE == 0 || selfRadiance.a > 0.5)) {
        float voxelFlatness;
        vec3 voxelNormal = GiEstimateVoxelNormalFlat(pos, voxelFlatness);

        for (int d = 0; d < 14; d++) {
            // FIX ("bocor lewat sudut" - corner tunneling / uneven bounce
            // reach): `dirRaw` is an INTEGER lattice step ((1,0,0), (1,1,1),
            // etc, straight from EmitDirection - length 1 for the 6 face
            // directions, but length sqrt(2)/sqrt(3) for the 12 edge / 8
            // corner directions). Everything below that walks the grid
            // voxel-by-voxel (`checkPos`/`airPos`, both `pos + round(dirRaw
            // * step)`) needs THIS integer vector, one full lattice cell
            // per `step`, or two things go wrong at once: (1) a normalized
            // direction has length < 1 on diagonals, so round(dir*step) can
            // land on the SAME cell for two consecutive step values (e.g.
            // corner (1,1,1) normalized: step 1 and step 2 both round to
            // (1,1,1)) - that cell's occupancy never actually advances, so
            // a thin solid voxel one further lattice cell out along that
            // exact diagonal is never tested and light silently marches
            // past/around it; (2) `dir` was previously reused as the raw
            // step multiplier too, so the 12 edge / 8 corner directions
            // reached a good deal LESS than MAX_MARCH voxels of real
            // distance while the 6 face directions reached the full amount
            // - bounce shape came out lopsided (flatter along diagonals)
            // on top of the leak. Keep a SEPARATE normalized `dirN` only
            // for the lighting-weight dot product below, which does need
            // unit length to compare fairly against the surface normal.
            vec3 dirRaw = EmitDirection(d);
            vec3 dirN = normalize(dirRaw);

            // ("light bounce bocor lewat sudut 3-sumbu ke ruang gelap" fix):
            // d=6..13 are the 8 true CORNER directions (e.g. (1,1,1)) - the
            // straight-line march below only tests cells exactly ON that
            // diagonal, never the 3 axis-flanking faces that actually form
            // the corner. So a solid voxel sitting right at a room's corner
            // could emit straight through the corner point into a
            // completely separate, unlit room on the other side, even
            // though no real light could ever bend around a sealed corner
            // like that - this is the "kotak putih"/box-outline leak at
            // block corners and the light showing up in dark corners that
            // shouldn't have any. If at least 2 of the 3 walls that meet at
            // this corner are solid, treat the corner as sealed and skip
            // emitting in that exact diagonal direction entirely (matching
            // the same sealed-corner logic STEP 2's air-gather already uses
            // for its 2-axis edge directions, just extended to 3 axes here).
            if (d >= 6) {
                int sealedCount = 0;
                if (GiLoadAlbedoOpacity(pos + ivec3(int(dirRaw.x), 0, 0)).a > 0.5) sealedCount++;
                if (GiLoadAlbedoOpacity(pos + ivec3(0, int(dirRaw.y), 0)).a > 0.5) sealedCount++;
                if (GiLoadAlbedoOpacity(pos + ivec3(0, 0, int(dirRaw.z))).a > 0.5) sealedCount++;
                if (sealedCount >= 2) continue;
            }

            float normalDot = max(0.0, dot(dirN, voxelNormal));
            // v8.0: floor raised 0.3 -> 0.65 - sideways emission (walls to
            // the left/right of a lit colored surface, e.g. red wool on the
            // floor) was 3x weaker than straight-up emission, and after
            // GI_DECAY compounds over GI_BOUNCE_PASSES that read as bounce
            // only reaching the ceiling and never the side walls. Still
            // slightly below the 1.0 normal-aligned weight so there's a
            // mild directional bias, just not a heavily lopsided one.
            // ("bounce sesama permukaan flat nggak logis" fix): floor ini
            // sebelumnya 0.65 (dinaikkan dari 0.3 demi kasus lain - lihat
            // catatan v8.0 di atas soal dinding kurang kena bounce). Tapi
            // normalDot == 0 PERSIS terjadi untuk SEMUA arah horizontal
            // dari sebuah permukaan datar (lantai/langit-langit) - baik
            // arah itu menuju dinding (bounce yang sah) MAUPUN menuju
            // lantai tetangga yang sebidang (yang seharusnya nyaris nol
            // secara fisik, cosine term-nya memang ~0). STEP 1 ini tidak
            // bisa membedakan dua kasus itu (keduanya sama-sama
            // "horizontal dari permukaan datar"), jadi menaikkan floor ini
            // untuk memperbaiki dinding otomatis ikut membocorkan
            // sesama-flat juga. Diturunkan ke 0.35 sebagai titik tengah:
            // dinding masih dapat porsi wajar, tapi kebocoran flat-ke-flat
            // ditekan jauh lebih banyak daripada 0.65. Trade-off: bounce ke
            // dinding dari lantai/langit-langit sedikit lebih lemah dari
            // sebelumnya - naikkan lagi kalau dinding terasa kurang kena
            // warna bounce.
            float emitWeight = mix(0.60, 1.0, normalDot);
            // ("light bounce kurang ke bawah" fix): a flat-facing emitter
            // (sunlit floor/top of a block, voxelNormal pointing straight
            // up) always scores normalDot=0 for the straight-down direction
            // (dot of opposite vectors), so downward propagation was stuck
            // at the same 0.65 floor as sideways bounce even though
            // "straight down off a lit floor" should read at least as
            // strong. Give the exact downward direction its own higher
            // floor so it doesn't stay under-weighted relative to
            // horizontal spread.
            if (dirRaw.y < -0.5) {
                emitWeight = max(emitWeight, 0.85);
            }
            // ("light bounce kurang ke atas" fix): upward propagation juga
            // perlu floor weight yang cukup tinggi supaya cahaya bisa
            // menyebar ke langit-langit/dinding atas dari permukaan yang
            // disinari matahari.
            if (dirRaw.y > 0.5) {
                emitWeight = max(emitWeight, 0.75);
            }

            // Ray march to find open space extent
            float openSpace = 0.0;
            // PERFORMANCE FIX ("nga lag di semua profile"): this 14-
            // direction x up-to-10-step march is the single most expensive
            // part of this compute pass (up to 140 texelFetches per solid,
            // lit voxel, every bounce pass), and it was a flat constant
            // that ran identically on every profile - so LOW paid the exact
            // same per-voxel march cost as CINEMATIC despite already being
            // scaled down everywhere else (lower GI_VOXEL_RESOLUTION, only
            // 1 GI_BOUNCE_PASSES). Scaling the march distance off
            // GI_BOUNCE_PASSES (which every profile already sets
            // differently: LOW=1, STANDARD=3, ULTRA=4, CINEMATIC=5+) means
            // cheaper profiles automatically get a cheaper march too, no
            // extra settings key needed. Clamped to [6,10] so quality never
            // drops below what a single-block room needs (6) and never
            // exceeds the pack's original fixed value (10).
            const int MAX_MARCH = clamp(GI_BOUNCE_PASSES + 4, 6, 10);

            for (int step = 1; step <= MAX_MARCH; step++) {
                ivec3 checkPos = pos + ivec3(dirRaw) * step;
                if (!GiCheckInsideVolume(vec3(checkPos))) break;
                vec4 checkAlbedo = GiLoadAlbedoOpacity(checkPos);
                if (checkAlbedo.a > 0.5) break;
                openSpace += 1.0;
            }

            if (openSpace < 0.5) continue;

            // Open-space boost: wider open space around the emitter = more
            // of that light is allowed to travel further (this is the main
            // knob that makes bounce reach walls/ceiling across a room
            // instead of staying a tight blob near the source).
            // ("kotak highlight di sudut block" fix): diturunkan dari 0.18 ->
            // 0.10. Boost ini menguatkan cahaya di SEMUA 14 arah emisi secara
            // seragam, jadi voxel terang memancarkan halo kubus yang menempel
            // erat di siluet block (paling terang di pojok atas/bawah/kanan/kiri,
            // baca sebagai highlight tebal). Amplifikasi yang lebih lemah tetap
            // membawa bounce ke dinding ruangan lewat beberapa lintasan
            // GI_BOUNCE_PASSES, tapi tanpa mengecat "kotak" di sekitar sumber.
            float spaceBoost = 1.0 + openSpace * 0.10;

            for (int step = 1; step <= int(openSpace); step++) {
                ivec3 airPos = pos + ivec3(dirRaw) * step;
                if (!GiCheckInsideVolume(vec3(airPos))) break;

                vec4 airAlbedo = GiLoadAlbedoOpacity(airPos);
                if (airAlbedo.a > 0.5) break;

                #if GI_PARITY == 0
                    vec4 airRad = GiLoadRadiance(gi_radiance_a_sampler, airPos);
                #else
                    vec4 airRad = GiLoadRadiance(gi_radiance_b_sampler, airPos);
                #endif

                float distFade = 1.0 / (1.0 + float(step) * 0.50);
                distFade *= spaceBoost;

                vec3 emittedLight = selfRadiance.rgb * emitWeight * distFade * 0.65 * voxelFlatness;
                vec3 airNew = airRad.rgb + emittedLight;

                #if GI_PARITY == 0
                    imageStore(gi_radiance_b_img, airPos, vec4(airNew, 0.0));
                #else
                    imageStore(gi_radiance_a_img, airPos, vec4(airNew, 0.0));
                #endif
            }
        }
    }

    // ================================================================
    //  STEP 2: AIR VOXELS GATHER FROM EMITTING SURFACES
    // ================================================================
    if (!isSolid) {
        vec3 gatheredSum = vec3(0.0);
        float gatheredWeight = 0.0;

        for (int i = 0; i < 32; i++) {
            vec3 off = NeighborOffset(i);
            ivec3 np = pos + ivec3(off);
            if (!GiCheckInsideVolume(vec3(np))) continue;

            vec4 neighborAlbedo = GiLoadAlbedoOpacity(np);
            if (neighborAlbedo.a < 0.5) continue;

            #if GI_PARITY == 0
                vec4 neighborRad4 = GiLoadRadiance(gi_radiance_a_sampler, np);
            #else
                vec4 neighborRad4 = GiLoadRadiance(gi_radiance_b_sampler, np);
            #endif

            if (GI_SINGLE_BOUNCE == 1 && neighborRad4.a <= 0.5) continue;
            vec3 neighborRad = neighborRad4.rgb;
            if (GetLuminance(neighborRad) < 0.001) continue;

            float dist = NeighborDistance(i);
            float w = 1.0 / (1.0 + dist * 0.5);

            gatheredSum += neighborRad * w;
            gatheredWeight += w;
        }

        // ("bounce nempel di permukaan, tidak nyebar ke udara" fix): ALSO
        // gather from AIR neighbours. Before this, only SOLID neighbours
        // were gathered, so radiance only ever sat in the air cells
        // directly beside a bright surface and never travelled through the
        // middle of the room - it read as bounce "sticking" to walls/
        // ceiling with the room core staying dark.
        //
        // FIX ("light bounce nga kerasa & kurang menyebar"): this used to
        // be face-only (i < 6, 1 voxel of reach per pass). Over
        // GI_BOUNCE_PASSES that is the whole reason bounce felt weak and
        // slow to fill a room - light could only ever hop one voxel of open
        // air per pass, so a 3-pass STANDARD profile only ever pushed
        // diffuse light 3 voxels deep into a room before GI_DECAY ate most
        // of it. Extending to the 12 EDGE offsets too (i < 18) lets each
        // pass reach diagonally as well as face-wise, covering noticeably
        // more of the room's air volume per bounce pass for a small,
        // fixed extra cost (12 more texelFetches, still nowhere near
        // STEP 1/3's 32-tap loops).
        //
        // Corner-cut guard: a diagonal edge offset like (1,1,0) sits
        // exactly at the corner where the two face-neighbours pos+(1,0,0)
        // and pos+(0,1,0) meet - if BOTH of those are solid, that corner is
        // completely sealed by geometry (an actual wall corner) and
        // diffusing through it anyway would be light "cutting the corner"
        // through solid geometry, i.e. exactly the tembus/leak behaviour
        // fixed elsewhere in this pack. Skip the diagonal in that one case;
        // if at most one of the two is solid, the corner is only partially
        // blocked and letting diffusion bend slightly around it is a fair
        // approximation (matching the same partial-corner tolerance already
        // used by GiSampleRadianceSmoothed's neighbour blending).
        for (int i = 0; i < 18; i++) {
            vec3 off = NeighborOffset(i);
            ivec3 np = pos + ivec3(off);
            if (!GiCheckInsideVolume(vec3(np))) continue;

            if (i >= 6) {
                // Edge offset: exactly one axis component is 0. Test the
                // two axis-aligned cells that form this corner.
                bool cornerSealed = false;
                if (abs(off.x) > 0.5 && abs(off.y) > 0.5) {
                    cornerSealed = GiLoadAlbedoOpacity(pos + ivec3(int(off.x), 0, 0)).a > 0.5
                                && GiLoadAlbedoOpacity(pos + ivec3(0, int(off.y), 0)).a > 0.5;
                } else if (abs(off.x) > 0.5 && abs(off.z) > 0.5) {
                    cornerSealed = GiLoadAlbedoOpacity(pos + ivec3(int(off.x), 0, 0)).a > 0.5
                                && GiLoadAlbedoOpacity(pos + ivec3(0, 0, int(off.z))).a > 0.5;
                } else {
                    cornerSealed = GiLoadAlbedoOpacity(pos + ivec3(0, int(off.y), 0)).a > 0.5
                                && GiLoadAlbedoOpacity(pos + ivec3(0, 0, int(off.z))).a > 0.5;
                }
                if (cornerSealed) continue;
            }

            vec4 na = GiLoadAlbedoOpacity(np);
            if (na.a >= 0.5) continue; // solid blocks diffusion

            #if GI_PARITY == 0
                vec3 airDiff = GiLoadRadiance(gi_radiance_a_sampler, np).rgb;
            #else
                vec3 airDiff = GiLoadRadiance(gi_radiance_b_sampler, np).rgb;
            #endif

            if (GetLuminance(airDiff) < 0.001) continue;

            float w = 1.0 / (1.0 + NeighborDistance(i) * 0.5);
            gatheredSum += airDiff * w;
            gatheredWeight += w;
        }

        if (gatheredWeight > 0.001) {
            vec3 volumeLight = gatheredSum / gatheredWeight;
            radianceNew = mix(radianceNew, volumeLight, 0.65);
        }
    }

    // ================================================================
    //  STEP 3: SOLID VOXELS READ VOLUME RADIANCE
    // ================================================================
    if (isSolid) {
        vec3 volumeSum = vec3(0.0);
        float volumeWeight = 0.0;
        float voxelFlatness3;
        vec3 voxelNormal = GiEstimateVoxelNormalFlat(pos, voxelFlatness3);

        for (int i = 0; i < 32; i++) {
            vec3 off = NeighborOffset(i);
            ivec3 np = pos + ivec3(off);
            if (!GiCheckInsideVolume(vec3(np))) continue;

            // ("light bounce bocor lewat sudut ke ruang gelap" fix): same
            // sealed-edge/sealed-corner guard as STEP 1's emission march and
            // STEP 2's air-gather above, applied here too. Without this, a
            // solid surface could read back bounce light from an air cell
            // that's diagonally across a fully sealed edge/corner (2 or 3
            // solid walls meeting), which is the "read" side of the exact
            // same corner-leak this pack already guards against on the
            // "emit"/"diffuse" sides - both sides need the same guard or
            // leaks reappear on whichever side was left unguarded.
            if (i >= 6 && i < 18) {
                // Edge (2-axis diagonal): exactly one axis component is 0.
                bool edgeSealed;
                if (abs(off.x) > 0.5 && abs(off.y) > 0.5) {
                    edgeSealed = GiLoadAlbedoOpacity(pos + ivec3(int(off.x), 0, 0)).a > 0.5
                              && GiLoadAlbedoOpacity(pos + ivec3(0, int(off.y), 0)).a > 0.5;
                } else if (abs(off.x) > 0.5 && abs(off.z) > 0.5) {
                    edgeSealed = GiLoadAlbedoOpacity(pos + ivec3(int(off.x), 0, 0)).a > 0.5
                              && GiLoadAlbedoOpacity(pos + ivec3(0, 0, int(off.z))).a > 0.5;
                } else {
                    edgeSealed = GiLoadAlbedoOpacity(pos + ivec3(0, int(off.y), 0)).a > 0.5
                              && GiLoadAlbedoOpacity(pos + ivec3(0, 0, int(off.z))).a > 0.5;
                }
                if (edgeSealed) continue;
            } else if (i >= 18 && i < 26) {
                // Corner (3-axis diagonal): if at least 2 of the 3 flanking
                // walls are solid, treat this corner as sealed.
                int sealedCount = 0;
                if (GiLoadAlbedoOpacity(pos + ivec3(int(off.x), 0, 0)).a > 0.5) sealedCount++;
                if (GiLoadAlbedoOpacity(pos + ivec3(0, int(off.y), 0)).a > 0.5) sealedCount++;
                if (GiLoadAlbedoOpacity(pos + ivec3(0, 0, int(off.z))).a > 0.5) sealedCount++;
                if (sealedCount >= 2) continue;
            }

            vec4 neighborAlbedo = GiLoadAlbedoOpacity(np);
            if (neighborAlbedo.a > 0.5) continue;

            #if GI_PARITY == 0
                vec3 airRad = GiLoadRadiance(gi_radiance_a_sampler, np).rgb;
            #else
                vec3 airRad = GiLoadRadiance(gi_radiance_b_sampler, np).rgb;
            #endif

            if (GetLuminance(airRad) < 0.001) continue;

            float dist = NeighborDistance(i);
            float w = 1.0 / (1.0 + dist * 0.5);

            vec3 toAir = normalize(vec3(np) - vec3(pos));
            float normalBias = max(0.0, dot(voxelNormal, toAir));

            volumeSum += airRad * w * normalBias;
            volumeWeight += w * normalBias;
        }

        if (volumeWeight > 0.001) {
            vec3 indirectLight = volumeSum / volumeWeight;
            indirectLight *= albedoOpacity.rgb;
            // Seam/corner voxels (ambiguous blended normal - see flatness
            // note above GiEstimateVoxelNormalEx) no longer get to read
            // back a full-strength gather from BOTH adjacent faces' air at
            // once; this is the other half of the corner-halo fix (STEP 1
            // stops it from over-EMITTING at seams, this stops it from
            // over-READING what neighbours already emitted).
                radianceNew += indirectLight * 0.70 * voxelFlatness3;
        }
    }

    // ================================================================
    //  STEP 4: ENERGY DECAY, NORMALIZE & CLAMP
    // ================================================================
    // Energy conservation (inspired by Foxy Shader's approach):
    // When multiple bounce sources overlap, their radiance stacks.
    // Without normalization, this creates unrealistic brightness.
    // The normalization uses max-channel (not luminance) to preserve
    // color ratios: if red channel is brightest, it stays red even
    // when clamped. This is more physically correct than luminance-
    // based clamping which can desaturate colored bounces.
    float decay = float(GI_DECAY);
    radianceNew *= decay;
    // Energy threshold: skip very dark bounces (like Foxy's 0.015
    // continuation threshold). Prevents wasting computation on
    // bounces that contribute nothing visible.
    if (GetLuminance(radianceNew) < 0.001) {
        radianceNew = vec3(0.0);
    }
    // Max-channel normalization: cap the brightest channel to
    // albedo * 2.5. This preserves color ratios (red stays red,
    // blue stays blue) while preventing total brightness explosion.
    // Luminance-based clamping would desaturate colored bounces.
    float maxCh = max(radianceNew.r, max(radianceNew.g, radianceNew.b));
    float albedoMaxCh = max(albedoOpacity.r, max(albedoOpacity.g, albedoOpacity.b)) * 2.5;
    if (maxCh > albedoMaxCh && albedoMaxCh > 0.001) {
        radianceNew *= albedoMaxCh / maxCh;
    }
    radianceNew = min(radianceNew, vec3(5.0));

    // Write result
    #if GI_PARITY == 0
        imageStore(gi_radiance_b_img, pos, vec4(radianceNew, 0.0));
    #else
        imageStore(gi_radiance_a_img, pos, vec4(radianceNew, 0.0));
    #endif

    #if GI_TEMPORAL_ACCUM == 1 && defined GI_IS_LAST_PASS
        imageStore(gi_history_img, pos, vec4(radianceNew, 0.0));
    #endif
}
