ff-render 0.18.1

GPU compositing pipeline for real-time preview (wgpu-based)
Documentation
// Separable Gaussian blur: one 1D pass along `direction` (horizontal or vertical).
// The node runs this shader twice (H then V) for a full 2D Gaussian. Uses
// `textureLoad` for exact per-texel convolution with clamp-to-edge, so the GPU
// result matches the CPU reference within tolerance.

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: BlurUniforms;

// Matches Rust #[repr(C)] BlurUniforms (field order/size kept in sync).
struct BlurUniforms {
    direction: vec2<f32>,          // (1,0) = horizontal, (0,1) = vertical
    tap_count: u32,                // odd, 1..=15; taps beyond it carry weight 0
    _pad: u32,
    weights: array<vec4<f32>, 4>,  // 16 weights (15 used), packed 4-per-vec4 for std140
}

@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
    let dims = vec2<i32>(textureDimensions(input_tex));
    // @builtin(position) is the framebuffer coord (pixel_index + 0.5); truncating
    // to i32 recovers the integer pixel index.
    let center = vec2<i32>(in.position.xy);
    let dir = vec2<i32>(u.direction);              // (1,0) or (0,1)
    let half_span = i32((u.tap_count - 1u) / 2u);

    var acc = vec4<f32>(0.0);
    // Fixed bound (max 15 taps); unused taps have weight 0 and add nothing.
    for (var i: u32 = 0u; i < 15u; i = i + 1u) {
        let w = u.weights[i / 4u][i % 4u];
        // Clamp-to-edge: samples past the border reuse the border texel.
        let coord = clamp(center + dir * (i32(i) - half_span), vec2<i32>(0), dims - vec2<i32>(1));
        acc = acc + textureLoad(input_tex, coord, 0) * w;
    }
    return acc;
}