facett-core 0.1.18

facett — visual kernel: render a node/edge Scene into egui (wgpu fast path to come)
Documentation
// ─────────────────────────────────────────────────────────────────────────────
// TAA — temporal anti-aliasing resolve with velocity reprojection.
// GFX_V2 §5: "velocity-driven TAA resolve + Halton jitter — the fix for sub-pixel
// road lines shimmering under camera motion/rotation. MSAA does not solve thin
// vector lines."
//
// ONE fullscreen pass. Each pixel:
//
//   1. reads this frame's (JITTERED) colour;
//   2. builds the 3x3 min/max colour box of the current frame around it;
//   3. reprojects into the history by subtracting the motion vector, and samples;
//   4. CLAMPS the history into that box — the history rejection that stops a moving
//      camera from smearing last frame's geometry across this frame's;
//   5. blends `mix(current, history, weight)`.
//
// WHY THE BOX CLAMP DOES NOT DEFEAT THE ANTIALIASING. At an aliased edge the 3x3
// box of the current frame spans background AND foreground, so it is wide and the
// history passes through unclamped — which is exactly where the sub-pixel detail
// has to accumulate. Deep inside a flat region the box is narrow and a stale
// history is snapped back to it. The clamp is therefore tight precisely where
// ghosting would show and loose precisely where convergence is needed.
//
// WHY THE WEIGHT IS PASSED IN RATHER THAN BAKED. A fixed feedback factor makes the
// resolve an exponential moving average, and an EMA over a *periodic* jitter
// sequence never settles — it oscillates at the sequence's period, so a converged
// frame still carries a fraction of the aliasing. The host ramps the weight as
// `min(cap, frame/(frame+1))` so the early frames form a true running mean of the
// jitter samples and only then decay into an EMA. That ramp is what makes
// convergence measurable instead of asymptotic-in-principle.
//
// Velocity is in **UV units per frame** and is always bound — a caller with no
// motion binds a 1x1 zero texture rather than the shader growing a second path.
// ─────────────────────────────────────────────────────────────────────────────

struct TaaU {
    /// `res.xy` = pixel size, `res.zw` = 1/pixel size.
    res: vec4<f32>,
    /// `x` = history weight (0 = current only), `y`,`z` = this frame's jitter in px
    /// (recorded for provenance; the resolve does not need it because the jitter is
    /// already baked into where the geometry landed), `w` unused.
    ///
    /// There is deliberately NO "history valid" flag here. `frame == 0` is the only
    /// state with no history, and the host's weight ramp is already `0.0` there — so a
    /// second gate could never report red. It WAS one, briefly; mutating it away left
    /// every test green, which is how it was caught. Removed rather than tested harder
    /// (LAW #5: one writer beats two copies watched for agreement).
    params: vec4<f32>,
}

@group(0) @binding(0) var<uniform> U: TaaU;
@group(0) @binding(1) var cur_tex: texture_2d<f32>;
@group(0) @binding(2) var hist_tex: texture_2d<f32>;
@group(0) @binding(3) var vel_tex: texture_2d<f32>;
@group(0) @binding(4) var smp: sampler;

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

@vertex
fn taa_vs(@builtin(vertex_index) vi: u32) -> VsOut {
    // The oversized triangle covering clip space: (-1,-1), (3,-1), (-1,3).
    var p = array<vec2<f32>, 3>(
        vec2<f32>(-1.0, -1.0),
        vec2<f32>( 3.0, -1.0),
        vec2<f32>(-1.0,  3.0),
    );
    var o: VsOut;
    let xy = p[vi];
    o.clip = vec4<f32>(xy, 0.0, 1.0);
    o.uv = vec2<f32>(xy.x * 0.5 + 0.5, 1.0 - (xy.y * 0.5 + 0.5));
    return o;
}

@fragment
fn taa_resolve_fs(i: VsOut) -> @location(0) vec4<f32> {
    let uv = i.uv;
    let texel = U.res.zw;

    let cur = textureSampleLevel(cur_tex, smp, uv, 0.0);

    // The 3x3 colour box of THIS frame — the history-rejection bound.
    var lo = cur;
    var hi = cur;
    for (var dy = -1; dy <= 1; dy = dy + 1) {
        for (var dx = -1; dx <= 1; dx = dx + 1) {
            let n = textureSampleLevel(
                cur_tex,
                smp,
                uv + vec2<f32>(f32(dx), f32(dy)) * texel,
                0.0,
            );
            lo = min(lo, n);
            hi = max(hi, n);
        }
    }

    // Reproject: where was this surface last frame?
    let vel = textureSampleLevel(vel_tex, smp, uv, 0.0).xy;
    let huv = uv - vel;
    let inside = huv.x >= 0.0 && huv.x <= 1.0 && huv.y >= 0.0 && huv.y <= 1.0;

    var hist = textureSampleLevel(hist_tex, smp, huv, 0.0);
    hist = clamp(hist, lo, hi);

    // A pixel whose reprojection left the frame has no history, and the only honest
    // answer is the current frame. The sampler is `ClampToEdge`, so without this test a
    // rejected history would silently become *the border texel*, which is plausible
    // enough to look like it worked.
    //
    // HONESTLY: this test has NOT been seen red on its own. The box clamp above fires
    // first — an off-screen sample is bounded into the current frame's own 3x3 range, so
    // dropping the bounds test moves the measured residual by 0.4 luma out of 71
    // (71.28 vs 71.70) and no assertion notices. It is belt on braces, kept because it
    // is correct and free, and it is the CLAMP that the off-screen device test actually
    // measures. Stated rather than implied, the same way `picking.rs` states it for
    // `is_srgb`.
    //
    // The first frame needs no test here either: its weight is already 0 (see
    // `TaaPass::weight`).
    let w = select(0.0, U.params.x, inside);
    return mix(cur, hist, w);
}