Skip to main content

SCANLINES

Constant SCANLINES 

Source
pub const SCANLINES: &str = "\nstruct Post {\n    size: vec2<u32>,\n    frame: u32,\n    seed: u32,\n    p0: vec4<f32>,\n    p1: vec4<f32>,\n    p2: vec4<f32>,\n    p3: vec4<f32>,\n    region: vec4<u32>,\n    extent: vec4<u32>,\n}\n\n@group(0) @binding(0) var<uniform> post: Post;\n@group(0) @binding(1) var src_tex: texture_2d<f32>;\n@group(0) @binding(2) var dst_tex: texture_storage_2d<rgba16float, write>;\n\nfn as_half(c: vec4<f32>) -> vec4<f32> {\n    return vec4<f32>(unpack2x16float(pack2x16float(c.xy)), unpack2x16float(pack2x16float(c.zw)));\n}\n\nfn grade_exposure(src: vec4<f32>, p: vec2<u32>, q0: vec4<f32>, q1: vec4<f32>, q2: vec4<f32>, q3: vec4<f32>) -> vec4<f32> {\n    return vec4<f32>(src.rgb * q0.x, src.a);\n}\n\nfn grade_black(src: vec4<f32>, p: vec2<u32>, q0: vec4<f32>, q1: vec4<f32>, q2: vec4<f32>, q3: vec4<f32>) -> vec4<f32> {\n    let denom = 1.0 - q0.x;\n    if (abs(denom) < 1e-6) { return vec4<f32>(0.0, 0.0, 0.0, src.a); }\n    return vec4<f32>(max((src.rgb - vec3<f32>(q0.x)) / denom, vec3<f32>(0.0)), src.a);\n}\n\nfn grade_saturation(src: vec4<f32>, p: vec2<u32>, q0: vec4<f32>, q1: vec4<f32>, q2: vec4<f32>, q3: vec4<f32>) -> vec4<f32> {\n    let l = dot(src.rgb, vec3<f32>(q0.y, q0.z, q0.w));\n    return vec4<f32>(max(vec3<f32>(l) + (src.rgb - vec3<f32>(l)) * q0.x, vec3<f32>(0.0)), src.a);\n}\n\nfn grade_contrast(src: vec4<f32>, p: vec2<u32>, q0: vec4<f32>, q1: vec4<f32>, q2: vec4<f32>, q3: vec4<f32>) -> vec4<f32> {\n    if (q0.z > 0.5) {\n        return vec4<f32>(clamp(vec3<f32>(\n            safe_pow(max(src.r, 0.0), q0.x),\n            safe_pow(max(src.g, 0.0), q0.x),\n            safe_pow(max(src.b, 0.0), q0.x)\n        ) * q0.y, vec3<f32>(0.0), vec3<f32>(1.0)), src.a);\n    }\n    return vec4<f32>((src.rgb - vec3<f32>(q0.y)) * q0.x + vec3<f32>(q0.y), src.a);\n}\n\nfn grade_warmth(src: vec4<f32>, p: vec2<u32>, q0: vec4<f32>, q1: vec4<f32>, q2: vec4<f32>, q3: vec4<f32>) -> vec4<f32> {\n    if (q0.y > 0.5) {\n        let l = clamp(luma(src.rgb), 0.0, 1.0);\n        let t = edge_step(q0.z, q0.w, l);\n        let warm = mix(q1.xy, q1.zw, t);\n        let scale = mix(vec2<f32>(1.0), warm, q0.x);\n        return vec4<f32>(max(vec3<f32>(src.r * scale.x, src.g, src.b * scale.y), vec3<f32>(0.0)), src.a);\n    }\n    return vec4<f32>(src.r * (1.0 + q0.x), src.g, src.b * (1.0 - q0.x), src.a);\n}\n\nfn grade_tone(src: vec4<f32>, p: vec2<u32>, q0: vec4<f32>, q1: vec4<f32>, q2: vec4<f32>, q3: vec4<f32>) -> vec4<f32> {\n    if (q0.x < 0.5) {\n        return vec4<f32>(aces_channel(src.r * q0.y), aces_channel(src.g * q0.y), aces_channel(src.b * q0.y), src.a);\n    }\n    if (q0.x < 1.5) {\n        return vec4<f32>(neutral_map(src.rgb, q0.y, q0.z, q0.w, q1.x > 0.5), src.a);\n    }\n    return vec4<f32>(agx(src.rgb), src.a);\n}\n\nfn grade_vignette(src: vec4<f32>, p: vec2<u32>, q0: vec4<f32>, q1: vec4<f32>, q2: vec4<f32>, q3: vec4<f32>) -> vec4<f32> {\n    if (abs(q0.x) < 1e-8) { return src; }\n    let dim = max(vec2<f32>(src_dim()), vec2<f32>(1.0));\n    let st = (vec2<f32>(p) + vec2<f32>(0.5)) / dim;\n    var factor = 1.0;\n    if (q0.y > 0.5) {\n        let d = vec2<f32>(st.x - 0.5, (st.y - 0.5) * q0.w);\n        factor = 1.0 - q0.x * edge_step(q1.y, q1.z, length(d));\n    } else {\n        let uv = st * 2.0 - vec2<f32>(1.0);\n        let r = length(vec2<f32>(uv.x, uv.y * q0.w)) / max(q1.x, 1e-4);\n        factor = 1.0 - q0.x * safe_pow(max(r, 0.0), q0.z);\n    }\n    return vec4<f32>(src.rgb * factor, src.a);\n}\n\nfn grade_grain(src: vec4<f32>, p: vec2<u32>, q0: vec4<f32>, q1: vec4<f32>, q2: vec4<f32>, q3: vec4<f32>) -> vec4<f32> {\n    let mixed = mixed_seed();\n    let n = grain_hash(p.x, p.y, mixed ^ 7u) + grain_hash(p.x, p.y, mixed ^ 19u) - 1.0;\n    let add = n * q0.x * (1.0 - q0.y * luma(src.rgb));\n    var out = src.rgb + vec3<f32>(add);\n    if (q0.z > 0.5) { out = max(out, vec3<f32>(0.0)); }\n    return vec4<f32>(out, src.a);\n}\n\nfn grade_dither(src: vec4<f32>, p: vec2<u32>, q0: vec4<f32>, q1: vec4<f32>, q2: vec4<f32>, q3: vec4<f32>) -> vec4<f32> {\n    if (abs(q0.x) < 1e-8) { return src; }\n    let h = hash21(f32(p.x) + f32(post.seed % 1024u), f32(p.y) + f32(post.frame % 1024u));\n    let d = (h - 0.5) * q0.x / 255.0;\n    return vec4<f32>(src.rgb + vec3<f32>(d), src.a);\n}\n\nfn grade_encode(src: vec4<f32>, p: vec2<u32>, q0: vec4<f32>, q1: vec4<f32>, q2: vec4<f32>, q3: vec4<f32>) -> vec4<f32> {\n    return vec4<f32>(encode_channel(src.r), encode_channel(src.g), encode_channel(src.b), src.a);\n}\n\nfn grade_bw(src: vec4<f32>, p: vec2<u32>, q0: vec4<f32>, q1: vec4<f32>, q2: vec4<f32>, q3: vec4<f32>) -> vec4<f32> {\n    let l = dot(src.rgb, vec3<f32>(q0.x, q0.y, q0.z));\n    return vec4<f32>(l, l, l, src.a);\n}\n\nfn grade_noir(src: vec4<f32>, p: vec2<u32>, q0: vec4<f32>, q1: vec4<f32>, q2: vec4<f32>, q3: vec4<f32>) -> vec4<f32> {\n    let l = luma(src.rgb);\n    var out = vec3<f32>(l);\n    if (q0.z > 0.5) {\n        let keep = vec3<f32>(q1.x, q1.y, q1.z);\n        let kdir = keep - vec3<f32>(luma(keep));\n        let chroma = src.rgb - vec3<f32>(l);\n        let kn = length(kdir);\n        let cn = length(chroma);\n        if (kn > 1e-4 && cn > 1e-4) {\n            let gate = edge_step(q0.w, 1.0, dot(chroma, kdir) / (kn * cn));\n            out = mix(vec3<f32>(l), src.rgb, gate);\n        }\n    }\n    out = (out - vec3<f32>(0.5)) * q0.x + vec3<f32>(0.5);\n    if (q0.y < 0.999) {\n        out = max((out - vec3<f32>(q0.y)) / max(1.0 - q0.y, 1e-4), vec3<f32>(0.0));\n    }\n    return vec4<f32>(max(out, vec3<f32>(0.0)), src.a);\n}\n\nfn grade_sepia(src: vec4<f32>, p: vec2<u32>, q0: vec4<f32>, q1: vec4<f32>, q2: vec4<f32>, q3: vec4<f32>) -> vec4<f32> {\n    let tint = vec3<f32>(\n        dot(src.rgb, vec3<f32>(0.393, 0.769, 0.189)),\n        dot(src.rgb, vec3<f32>(0.349, 0.686, 0.168)),\n        dot(src.rgb, vec3<f32>(0.272, 0.534, 0.131))\n    );\n    return vec4<f32>(mix(src.rgb, tint, clamp(q0.x, 0.0, 1.0)), src.a);\n}\n\nfn grade_posterize(src: vec4<f32>, p: vec2<u32>, q0: vec4<f32>, q1: vec4<f32>, q2: vec4<f32>, q3: vec4<f32>) -> vec4<f32> {\n    let levels = max(q0.x, 2.0);\n    return vec4<f32>(quantize(src.r, levels), quantize(src.g, levels), quantize(src.b, levels), src.a);\n}\n\nfn grade_flicker(src: vec4<f32>, p: vec2<u32>, q0: vec4<f32>, q1: vec4<f32>, q2: vec4<f32>, q3: vec4<f32>) -> vec4<f32> {\n    let h = grain_hash(post.frame, 0u, post.seed ^ 0xF11CE001u);\n    let scale = 1.0 + (h - 0.5) * q0.x;\n    return vec4<f32>(src.rgb * scale, src.a);\n}\n\nfn grade_scanlines(src: vec4<f32>, p: vec2<u32>, q0: vec4<f32>, q1: vec4<f32>, q2: vec4<f32>, q3: vec4<f32>) -> vec4<f32> {\n    if (abs(q0.x) < 1e-8) { return src; }\n    let period = max(u32(q0.y), 2u);\n    var out = src.rgb;\n    if (p.y % period == period - 1u) { out = src.rgb * max(1.0 - q0.x, 0.0); }\n    return vec4<f32>(out, src.a);\n}\n\nfn grade_aperture(src: vec4<f32>, p: vec2<u32>, q0: vec4<f32>, q1: vec4<f32>, q2: vec4<f32>, q3: vec4<f32>) -> vec4<f32> {\n    if (abs(q0.x) < 1e-8) { return src; }\n    let s = clamp(q0.x, 0.0, 1.0);\n    var out = src.rgb;\n    let column = p.x % 3u;\n    if (column == 0u) { out = vec3<f32>(src.r, src.g * (1.0 - s), src.b * (1.0 - s)); }\n    else if (column == 1u) { out = vec3<f32>(src.r * (1.0 - s), src.g, src.b * (1.0 - s)); }\n    else { out = vec3<f32>(src.r * (1.0 - s), src.g * (1.0 - s), src.b); }\n    return vec4<f32>(out, src.a);\n}\n\nfn grade_duotone(src: vec4<f32>, p: vec2<u32>, q0: vec4<f32>, q1: vec4<f32>, q2: vec4<f32>, q3: vec4<f32>) -> vec4<f32> {\n    let l = clamp(luma(src.rgb), 0.0, 1.0);\n    let shadow = vec3<f32>(q0.x, q0.y, q0.z);\n    let highlight = vec3<f32>(q1.x, q1.y, q1.z);\n    return vec4<f32>(mix(shadow, highlight, l), src.a);\n}\n\nfn grade_gradient_map(src: vec4<f32>, p: vec2<u32>, q0: vec4<f32>, q1: vec4<f32>, q2: vec4<f32>, q3: vec4<f32>) -> vec4<f32> {\n    let l = clamp(luma(src.rgb), 0.0, 1.0);\n    var lower = q0;\n    var upper = q3;\n    if (q0.w <= l) { lower = q0; }\n    if (q1.w <= l) { lower = q1; }\n    if (q2.w <= l) { lower = q2; }\n    if (q3.w <= l) { lower = q3; }\n    if (q0.w >= l) { upper = q0; }\n    else if (q1.w >= l) { upper = q1; }\n    else if (q2.w >= l) { upper = q2; }\n    else { upper = q3; }\n    let span = upper.w - lower.w;\n    var t = 0.0;\n    if (abs(span) >= 1e-6) { t = clamp((l - lower.w) / span, 0.0, 1.0); }\n    return vec4<f32>(mix(lower.xyz, upper.xyz, t), src.a);\n}\n\nfn src_dim() -> vec2<u32> {\n    return min(textureDimensions(src_tex), post.extent.xy);\n}\n\nfn load_src(p: vec2<i32>) -> vec4<f32> {\n    let dim = vec2<i32>(src_dim());\n    if (dim.x <= 0 || dim.y <= 0) { return vec4<f32>(0.0); }\n    let c = clamp(p, vec2<i32>(0), dim - vec2<i32>(1));\n    return textureLoad(src_tex, c, 0);\n}\n\nfn sample_src(x: f32, y: f32) -> vec4<f32> {\n    let dim = vec2<f32>(src_dim());\n    if (dim.x < 1.0 || dim.y < 1.0) { return vec4<f32>(0.0); }\n    let xc = clamp(x, 0.0, dim.x - 1.0);\n    let yc = clamp(y, 0.0, dim.y - 1.0);\n    let x0 = i32(floor(xc));\n    let y0 = i32(floor(yc));\n    let tx = xc - floor(xc);\n    let ty = yc - floor(yc);\n    let a = load_src(vec2<i32>(x0, y0));\n    let b = load_src(vec2<i32>(x0 + 1, y0));\n    let c = load_src(vec2<i32>(x0, y0 + 1));\n    let d = load_src(vec2<i32>(x0 + 1, y0 + 1));\n    return mix(mix(a, b, tx), mix(c, d, tx), ty);\n}\n\nfn store(p: vec2<u32>, c: vec4<f32>) {\n    textureStore(dst_tex, p, c);\n}\n\nfn luma(c: vec3<f32>) -> f32 {\n    return dot(c, vec3<f32>(0.2126, 0.7152, 0.0722));\n}\n\nfn luma_at(x: i32, y: i32) -> f32 {\n    return luma(load_src(vec2<i32>(x, y)).rgb);\n}\n\nfn sobel(x: i32, y: i32) -> f32 {\n    let gx = -luma_at(x - 1, y - 1) + luma_at(x + 1, y - 1) - 2.0 * luma_at(x - 1, y) + 2.0 * luma_at(x + 1, y) - luma_at(x - 1, y + 1) + luma_at(x + 1, y + 1);\n    let gy = -luma_at(x - 1, y - 1) - 2.0 * luma_at(x, y - 1) - luma_at(x + 1, y - 1) + luma_at(x - 1, y + 1) + 2.0 * luma_at(x, y + 1) + luma_at(x + 1, y + 1);\n    return sqrt(gx * gx + gy * gy);\n}\n\nfn normalize3(v: vec3<f32>) -> vec3<f32> {\n    let len = length(v);\n    if (len < 1e-8) { return vec3<f32>(0.0, 0.0, 1.0); }\n    return v / len;\n}\n\nfn safe_pow(a: f32, b: f32) -> f32 {\n    if (a <= 0.0) { return 0.0; }\n    return pow(a, b);\n}\n\nfn edge_step(edge0: f32, edge1: f32, x: f32) -> f32 {\n    let span = edge1 - edge0;\n    if (abs(span) < 1e-8) {\n        if (x < edge0) { return 0.0; }\n        return 1.0;\n    }\n    let t = clamp((x - edge0) / span, 0.0, 1.0);\n    return t * t * (3.0 - 2.0 * t);\n}\n\nfn grain_hash(x: u32, y: u32, seed: u32) -> f32 {\n    var h = x * 0x8da6b343u ^ y * 0xd8163841u ^ seed * 0xcb1ab31fu;\n    h = h ^ (h >> 15u);\n    h = h * 0x2c1b3c6du;\n    h = h ^ (h >> 12u);\n    return f32(h >> 8u) / 16777216.0;\n}\n\nfn mixed_seed() -> u32 {\n    return post.seed ^ (post.frame * 0x9e3779b9u);\n}\n\nfn hash21(x: f32, y: f32) -> f32 {\n    var qx = fract(x * 123.34);\n    var qy = fract(y * 456.21);\n    let d = qx * (qx + 45.32) + qy * (qy + 45.32);\n    qx = qx + d;\n    qy = qy + d;\n    return fract(qx * qy);\n}\n\nfn grain_hash_i(x: i32, y: i32, seed: u32) -> f32 {\n    return grain_hash(u32(x), u32(y), seed);\n}\n\nfn value_noise(x: f32, y: f32, seed: u32) -> f32 {\n    let x0 = floor(x);\n    let y0 = floor(y);\n    let tx = x - x0;\n    let ty = y - y0;\n    let sx = tx * tx * (3.0 - 2.0 * tx);\n    let sy = ty * ty * (3.0 - 2.0 * ty);\n    let xi = i32(x0);\n    let yi = i32(y0);\n    let a = grain_hash_i(xi, yi, seed);\n    let b = grain_hash_i(xi + 1, yi, seed);\n    let c = grain_hash_i(xi, yi + 1, seed);\n    let d = grain_hash_i(xi + 1, yi + 1, seed);\n    let ab = a + (b - a) * sx;\n    let cd = c + (d - c) * sx;\n    return ab + (cd - ab) * sy;\n}\n\nfn bayer(x: u32, y: u32) -> f32 {\n    var v = 0u;\n    var xx = x & 7u;\n    var yy = y & 7u;\n    var half = 4u;\n    var mul = 1u;\n    for (var i = 0u; i < 3u; i = i + 1u) {\n        let cx = xx >= half;\n        let cy = yy >= half;\n        if (xx >= half) { xx = xx - half; }\n        if (yy >= half) { yy = yy - half; }\n        var add = 0u;\n        if (cx && !cy) { add = 2u; }\n        else if (!cx && cy) { add = 3u; }\n        else if (cx && cy) { add = 1u; }\n        v = v + add * mul;\n        mul = mul * 4u;\n        half = half / 2u;\n    }\n    return (f32(v) + 0.5) / 64.0;\n}\n\nfn saturate3(c: vec3<f32>, amount: f32) -> vec3<f32> {\n    let l = luma(c);\n    return max(vec3<f32>(l) + (c - vec3<f32>(l)) * amount, vec3<f32>(0.0));\n}\n\nfn quantize(v: f32, levels: f32) -> f32 {\n    let t = clamp(v, 0.0, 1.0);\n    let q = min(floor(t * levels), levels - 1.0);\n    return q / (levels - 1.0);\n}\n\nfn keep_rgb(rgb: vec3<f32>, threshold: f32, knee: f32, knee_width: f32, soft: f32) -> vec3<f32> {\n    let l = max(rgb.r, max(rgb.g, rgb.b));\n    var hard = 0.0;\n    if (l > threshold) { hard = max(l - threshold, 0.0) / max(l, 1e-4); }\n    var knee_t = 0.0;\n    if (knee_width > 1e-6) { knee_t = clamp((l - knee) / knee_width, 0.0, 1.0); }\n    return rgb * (hard + knee_t * knee_t * soft);\n}\n\nfn aces_channel(x: f32) -> f32 {\n    let a = x * (x * 2.51 + 0.03);\n    let b = x * (x * 2.43 + 0.59) + 0.14;\n    if (abs(b) < 1e-8) { return 0.0; }\n    return clamp(a / b, 0.0, 1.0);\n}\n\nfn agx_contrast(x: f32) -> f32 {\n    let x2 = x * x;\n    let x4 = x2 * x2;\n    return 15.5 * x4 * x2 - 40.14 * x4 * x + 31.96 * x4 - 6.868 * x2 * x + 0.4298 * x2 + 0.1191 * x - 0.00232;\n}\n\nfn agx(c: vec3<f32>) -> vec3<f32> {\n    let r0 = vec3<f32>(0.842479062253094, 0.0784335999999992, 0.0792237451477643);\n    let r1 = vec3<f32>(0.0423282422610123, 0.878468636469772, 0.0791661274605434);\n    let r2 = vec3<f32>(0.0423756549057051, 0.0784336, 0.879142973793104);\n    let v = vec3<f32>(dot(r0, c), dot(r1, c), dot(r2, c));\n    let min_ev = -12.47393;\n    let max_ev = 4.026069;\n    let span = max_ev - min_ev;\n    let t0 = clamp((log2(max(v.r, 1e-10)) - min_ev) / span, 0.0, 1.0);\n    let t1 = clamp((log2(max(v.g, 1e-10)) - min_ev) / span, 0.0, 1.0);\n    let t2 = clamp((log2(max(v.b, 1e-10)) - min_ev) / span, 0.0, 1.0);\n    return clamp(vec3<f32>(agx_contrast(t0), agx_contrast(t1), agx_contrast(t2)), vec3<f32>(0.0), vec3<f32>(1.0));\n}\n\nfn neutral_map(c: vec3<f32>, start: f32, toe: f32, desat: f32, clamp_hi: bool) -> vec3<f32> {\n    let low = min(c.r, min(c.g, c.b));\n    var offset = toe;\n    if (low < 0.08) { offset = low - 6.25 * low * low; }\n    var v = c - vec3<f32>(offset);\n    let peak = max(v.r, max(v.g, v.b));\n    if (peak >= start && peak > 1e-8) {\n        let d = 1.0 - start;\n        let denom = peak + d - start;\n        var mapped = start;\n        if (abs(denom) >= 1e-8) { mapped = 1.0 - d * d / denom; }\n        v = v * (mapped / peak);\n        let g = 1.0 - 1.0 / (desat * (peak - mapped) + 1.0);\n        v = mix(v, vec3<f32>(mapped), g);\n    }\n    if (clamp_hi) { v = clamp(v, vec3<f32>(0.0), vec3<f32>(1.0)); }\n    return v;\n}\n\nfn band_shade(light: f32, bands: f32, softness: f32, threshold: f32, shadow: f32) -> f32 {\n    let bands_f = max(bands, 1.0);\n    let light_c = clamp(light, 0.0, 1.0);\n    let x = min(light_c * bands_f, bands_f - 1e-4);\n    var w = 0.0;\n    if (softness > 0.0) { w = edge_step(0.5 - softness * 0.5, 0.5 + softness * 0.5, fract(x)); }\n    let idx = floor(x) + w;\n    var q = light_c;\n    if (bands_f > 1.0) { q = clamp(idx / (bands_f - 1.0), 0.0, 1.0); }\n    var gate = 1.0;\n    if (softness <= 1e-5) {\n        if (light_c < threshold) { gate = 0.0; }\n    } else {\n        gate = edge_step(threshold - softness, threshold + softness, light_c);\n    }\n    return q * (shadow + (1.0 - shadow) * gate);\n}\n\nfn palette(i: u32) -> vec3<f32> {\n    if (i == 0u) { return vec3<f32>(0.0, 0.0, 0.0); }\n    if (i == 1u) { return vec3<f32>(0.0122865, 0.0241576, 0.0865005); }\n    if (i == 2u) { return vec3<f32>(0.208637, 0.0185002, 0.0865005); }\n    if (i == 3u) { return vec3<f32>(0.0, 0.242281, 0.0822827); }\n    if (i == 4u) { return vec3<f32>(0.40724, 0.0843762, 0.0368895); }\n    if (i == 5u) { return vec3<f32>(0.114435, 0.0953075, 0.0781874); }\n    if (i == 6u) { return vec3<f32>(0.539479, 0.545724, 0.571125); }\n    if (i == 7u) { return vec3<f32>(1.0, 0.879622, 0.806952); }\n    if (i == 8u) { return vec3<f32>(1.0, 0.0, 0.0742136); }\n    if (i == 9u) { return vec3<f32>(1.0, 0.366253, 0.0); }\n    if (i == 10u) { return vec3<f32>(1.0, 0.838799, 0.0202886); }\n    if (i == 11u) { return vec3<f32>(0.0, 0.775822, 0.0368895); }\n    if (i == 12u) { return vec3<f32>(0.0221739, 0.417885, 1.0); }\n    if (i == 13u) { return vec3<f32>(0.226966, 0.181164, 0.332452); }\n    if (i == 14u) { return vec3<f32>(1.0, 0.184475, 0.391572); }\n    return vec3<f32>(1.0, 0.603827, 0.401978);\n}\n\nfn nearest_palette(c: vec3<f32>) -> vec3<f32> {\n    var best = palette(0u);\n    var best_d = 1e30;\n    for (var i = 0u; i < 16u; i = i + 1u) {\n        let swatch = palette(i);\n        let d = dot(c - swatch, c - swatch);\n        if (d < best_d) { best_d = d; best = swatch; }\n    }\n    return best;\n}\n\nfn encode_channel(c: f32) -> f32 {\n    if (c <= 0.0031308) { return c * 12.92; }\n    return 1.055 * safe_pow(max(c, 0.0), 1.0 / 2.4) - 0.055;\n}\n\nfn gaussian(p: vec2<u32>, dirx: i32, diry: i32, radius: u32, spread: f32) -> vec4<f32> {\n    var acc = vec4<f32>(0.0);\n    var total = 0.0;\n    var s = spread;\n    if (s <= 1e-4) { s = 1.0; }\n    let r = i32(min(radius, 64u));\n    for (var k = -r; k <= r; k = k + 1) {\n        let w = exp(-f32(k * k) / s);\n        acc = acc + load_src(vec2<i32>(i32(p.x) + k * dirx, i32(p.y) + k * diry)) * w;\n        total = total + w;\n    }\n    return acc / max(total, 1e-8);\n}\n\nfn ring_radius(ring: u32) -> f32 {\n    if (ring == 0u) { return post.p1.x; }\n    if (ring == 1u) { return post.p1.y; }\n    if (ring == 2u) { return post.p1.z; }\n    return post.p1.w;\n}\n\nfn ring_gain(ring: u32) -> f32 {\n    if (ring == 0u) { return post.p2.x; }\n    if (ring == 1u) { return post.p2.y; }\n    if (ring == 2u) { return post.p2.z; }\n    return post.p2.w;\n}\n\nfn ring_channel(c: f32, threshold: f32, clamp_hi: f32) -> f32 {\n    var v = max(c - threshold, 0.0);\n    if (clamp_hi > 0.0) { v = min(v, clamp_hi); }\n    return v;\n}\n\n@compute @workgroup_size(8, 8)\nfn main(@builtin(global_invocation_id) id: vec3<u32>) {\n    let dst_dim = textureDimensions(dst_tex);\n    let scissor = post.region.z != 0u;\n    let p = select(id.xy, id.xy + post.region.xy, scissor);\n    if (scissor && (p.x >= post.region.z || p.y >= post.region.w)) { return; }\n    if (p.x >= dst_dim.x || p.y >= dst_dim.y) { return; }\n    if (post.size.x != 0u && (p.x >= post.size.x || p.y >= post.size.y)) { return; }\n    let src = load_src(vec2<i32>(p));\n    store(p, grade_scanlines(src, p, post.p0, post.p1, post.p2, post.p3));\n\n}\n";