ff-render 0.18.1

GPU compositing pipeline for real-time preview (wgpu-based)
Documentation
// Film grain: per-pixel pseudo-random noise added in YCbCr (BT.709) so luma and
// chroma grain can be dialled independently. The Wang hash is seeded from the
// pixel position and `frame_index`, so the pattern is deterministic per frame but
// changes every frame (no temporal sticking). The same hash runs on the CPU path.

struct VertexOutput {
    @builtin(position) position: vec4<f32>,
    @location(0) uv: vec2<f32>,
}

@vertex
fn vs_main(@builtin(vertex_index) vertex_idx: u32) -> VertexOutput {
    var positions = array<vec2<f32>, 6>(
        vec2<f32>(-1.0,  1.0), vec2<f32>( 1.0,  1.0), vec2<f32>(-1.0, -1.0),
        vec2<f32>(-1.0, -1.0), vec2<f32>( 1.0,  1.0), vec2<f32>( 1.0, -1.0),
    );
    var uvs = array<vec2<f32>, 6>(
        vec2<f32>(0.0, 0.0), vec2<f32>(1.0, 0.0), vec2<f32>(0.0, 1.0),
        vec2<f32>(0.0, 1.0), vec2<f32>(1.0, 0.0), vec2<f32>(1.0, 1.0),
    );
    var out: VertexOutput;
    out.position = vec4<f32>(positions[vertex_idx], 0.0, 1.0);
    out.uv = uvs[vertex_idx];
    return out;
}

@group(0) @binding(0) var input_tex: texture_2d<f32>;
@group(0) @binding(1) var<uniform> u: FilmGrainUniforms;

// Matches Rust FilmGrainUniforms (field order/size kept in sync).
struct FilmGrainUniforms {
    luma_strength:   f32,
    chroma_strength: f32,
    frame_index:     u32,
    _pad:            u32,
}

// Wang hash: fast integer hash used as a per-pixel PRNG. Kept byte-identical to
// the Rust CPU path (wrapping u32 arithmetic).
fn wang_hash(seed_in: u32) -> u32 {
    var seed = (seed_in ^ 61u) ^ (seed_in >> 16u);
    seed = seed * 9u;
    seed = seed ^ (seed >> 4u);
    seed = seed * 0x27d4eb2du;
    seed = seed ^ (seed >> 15u);
    return seed;
}

fn rand01(seed: u32) -> f32 {
    return f32(wang_hash(seed)) / 4294967295.0;
}

@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
    let px = vec2<u32>(vec2<i32>(in.position.xy));
    let base = px.x * 1973u + px.y * 9277u + u.frame_index * 26699u;

    // Grain centred on zero, scaled by the respective strength.
    let g_luma = (rand01(base) - 0.5) * u.luma_strength;
    let g_cb = (rand01(base + 1u) - 0.5) * u.chroma_strength;
    let g_cr = (rand01(base + 2u) - 0.5) * u.chroma_strength;

    let color = textureLoad(input_tex, vec2<i32>(in.position.xy), 0);

    // RGB -> YCbCr (BT.709), Cb/Cr centred on 0.
    var y = dot(color.rgb, vec3<f32>(0.2126, 0.7152, 0.0722));
    var cb = (color.b - y) / 1.8556;
    var cr = (color.r - y) / 1.5748;

    y = y + g_luma;
    cb = cb + g_cb;
    cr = cr + g_cr;

    // YCbCr -> RGB.
    let r = y + 1.5748 * cr;
    let g = y - 0.1873 * cb - 0.4681 * cr;
    let b = y + 1.8556 * cb;

    let rgb = clamp(vec3<f32>(r, g, b), vec3<f32>(0.0), vec3<f32>(1.0));
    return vec4<f32>(rgb, color.a);
}