// ─────────────────────────────────────────────────────────────────────────────
// 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);
}