Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/facett

facett-map3d/tests/taa_present.rs

//! **The present pass really antialiases — measured against a supersampled truth**
//! (GFX_V2 §5 item 7, TAA).
//!
//! ## The metric, and why it cannot be faked by a blur
//!
//! The same scene is rendered twice: once at `pixels_per_point = 1` (the shipped
//! path) and once at `pixels_per_point = SS` (`SS = 3`), which is the **identical
//! framing at 3× the pixels** — egui's viewport is sized in *points*, so pixel
//! `(i, j)` of the 1× image covers exactly the 3×3 block `(3i..3i+3, 3j..3j+3)` of
//! the supersampled one, and the block's **centre subsample** `(3i+1, 3j+1)` sits at
//! exactly the 1× pixel centre. That gives two references for free, from one render:
//!
//! * **BOX** — the mean of each 3×3 block. Nine samples per pixel: the *true
//!   coverage*, i.e. what an antialiased renderer should produce.
//! * **CTR** — the centre subsample of each block. One sample per pixel at the exact
//!   1× pixel centre: what an *aliased* renderer produces.
//!
//! The score is the ratio `rms(img, BOX) / rms(img, CTR)`:
//!
//! * an aliased image is the point sample, so it sits near CTR and far from BOX →
//!   ratio **> 1**;
//! * a genuinely antialiased image sits near BOX and away from CTR → ratio **< 1**.
//!
//! `1.0` is therefore not a tuned constant: it is the crossover between "this image
//! is a point sample" and "this image is coverage". Both halves are printed, so a
//! degenerate run (both errors ~0, i.e. a flat frame) is visible rather than hidden —
//! and the frame's dynamic range is asserted so a blank pane cannot score well.
//!
//! A blur does not win here: blurring moves the image away from CTR *and* away from
//! BOX (BOX is sharp — nine samples of a hard edge still resolve it in one pixel), so
//! the ratio does not collapse the way real sub-pixel convergence does.
//!
//! ## The three claims, and their controls
//!
//! 1. **Still camera** — the presented image converges on coverage, measured against
//!    the same pipeline with the temporal lane switched off (`TaaMode::Off`), in the
//!    same run on the same device.
//! 2. **The motion vectors are motion vectors** — warp the previous frame by the field
//!    the device computed and it lands on this frame. Nothing about the picture's
//!    prettiness; the field is checked against the motion it claims to predict.
//! 3. **Moving camera** — the shipped path against `TaaMode::ZeroVelocity`, the
//!    mutation that is exactly "TAA wired up without motion vectors". A still camera
//!    converges without them, so only a moving one can tell the two apart.
//!
//! Everything is measured inside a content rect that EXCLUDES the HUD overlays
//! `Map3D::ui` paints into the same pane — see [`proof::CONTENT`] for the measurement
//! that forced that.
//!
//! Run:
//! `FACETT_TEST_GPU=discrete cargo test -p facett-map3d --features wgpu --test taa_present -- --nocapture --test-threads=1`

#[cfg(not(feature = "wgpu"))]
#[test]
fn taa_present_proof_needs_the_wgpu_feature() {
    eprintln!(
        "[taa_present] SKIPPED and therefore PROVED NOTHING — built without `--features wgpu`, \
         so no present pass was recorded and NOTHING about antialiasing was measured."
    );
}

#[cfg(feature = "wgpu")]
mod proof {
    use egui_kittest::wgpu::{create_render_state, default_wgpu_setup, WgpuTestRenderer};
    use egui_kittest::Harness;
    use facett_map3d::{Map3D, SunSky, V3};
    use std::cell::RefCell;
    use std::rc::Rc;

    /// Pane size in POINTS. The supersampled reference renders the same points at
    /// `SS` pixels each.
    const PANE: f32 = 512.0;
    /// Supersampling factor. **Odd on purpose**: an odd factor has a centre subsample
    /// that coincides with the 1× pixel centre, which is what makes CTR a real point
    /// sample rather than an interpolation.
    const SS: u32 = 3;
    /// Frames accumulated before the measurement — three whole 8-phase jitter cycles.
    const FRAMES: usize = 24;
    /// Frames held at the start pose before any pan, so the map's own first-frame
    /// camera framing is over and done with before the measurement begins.
    const WARMUP: usize = 4;

    /// The scene: a lit synthetic town under a sky, with every RESOLUTION-DEPENDENT
    /// post pass off (bloom is half-res; SSAO's kernel is screen-space), so the 1×
    /// and the supersampled render differ only in how the geometry is sampled — which
    /// is the thing being measured.
    fn scene() -> Map3D {
        let mut m = Map3D::synthetic("taa")
            .with_gpu(true)
            .with_sky(true)
            .with_bloom(false)
            .with_ssao(false)
            .with_shadows(0.0)
            .with_compass(false)
            .with_scale_bar(false)
            .with_minimap(false)
            .with_sun(SunSky::from_angles(0.7, 0.55));
        m.spin = false;
        m.set_clock(0.0);
        pose(&mut m, 0.0);
        m
    }

    /// Put the camera at `az_offset` radians from the base pose and pin it (both the
    /// value and the damper goal, then settle) so nothing eases between frames except
    /// what this function moves.
    fn pose(m: &mut Map3D, az_offset: f32) {
        let az = 0.9 + az_offset;
        m.cam.azimuth = az;
        m.cam.goal_azimuth = az;
        m.cam.elevation = 0.35;
        m.cam.goal_elevation = 0.35;
        m.cam.distance = 2.2;
        m.cam.goal_distance = 2.2;
        m.cam.target = V3::new(0.0, 0.15, 0.0);
        m.cam.goal_target = m.cam.target;
        m.cam.settle();
    }

    /// Render `frames` GPU frames from `start_az`, moving the camera by
    /// `pan_per_frame` radians between them, and return the LAST frame at `ppp` pixels
    /// per point — plus the number of frames the TAA pass reports having resolved,
    /// the observable that separates "the resolve ran" from "the picture happens to
    /// look smooth".
    fn render_seq_mode(
        frames: usize,
        start_az: f32,
        pan_per_frame: f32,
        ppp: f32,
        mode: Option<facett_map3d::gpu::TaaMode>,
    ) -> (image::RgbaImage, u64) {
        let (img, frames, _) = render_seq_vel(frames, start_az, pan_per_frame, ppp, mode);
        (img, frames)
    }

    /// The last frame, the frame count, and that frame's `(w, h, motion vectors)`.
    type FramesWithVelocity = (image::RgbaImage, u64, Option<(u32, u32, Vec<[f32; 2]>)>);

    /// As [`render_seq_mode`], also handing back the **motion vectors the last frame
    /// computed** — the field the resolve reprojected with, read off the device.
    fn render_seq_vel(
        frames: usize,
        start_az: f32,
        pan_per_frame: f32,
        ppp: f32,
        mode: Option<facett_map3d::gpu::TaaMode>,
    ) -> FramesWithVelocity {
        let rs = create_render_state(
            facett_map3d::gpu::proof_wgpu_setup().unwrap_or_else(default_wgpu_setup),
            Default::default(),
        );
        eprintln!("[taa_present] adapter: {}", rs.adapter.get_info().name);
        match mode {
            Some(m) => facett_map3d::gpu::install_renderer_with_taa_for_test(&rs, m),
            None => facett_map3d::gpu::install_renderer(&rs),
        }
        let probe = rs.clone();
        let view = Rc::new(RefCell::new(scene()));
        let v2 = view.clone();
        let mut harness = Harness::builder()
            .with_size(egui::vec2(PANE, PANE))
            .with_pixels_per_point(ppp)
            .renderer(WgpuTestRenderer::from_render_state(rs))
            .build_ui(move |ui| {
                facett_core::set_theme(ui.ctx(), facett_core::Theme::sci_fi());
                use facett_core::Facet as _;
                v2.borrow_mut().ui(ui);
            });
        let mut img = None;
        for f in 0..frames {
            // WARMUP: the first frames are held at the start pose. `Map3D::ui` frames
            // its own camera on the first frame it runs — MEASURED: two single-frame
            // renders at azimuths 0.02 apart came out with a best rigid shift of
            // exactly (0, 0), i.e. the pose this test set was overridden — so a
            // sequence that starts panning immediately is measuring the auto-framing,
            // not the pan.
            let step = f.saturating_sub(WARMUP);
            pose(&mut view.borrow_mut(), start_az + pan_per_frame * step as f32);
            harness.step();
            img = Some(harness.render().expect("wgpu render produced an image"));
        }
        let resolved = facett_map3d::gpu::taa_frames_resolved(&probe).expect("the renderer is installed");
        let vel = facett_map3d::gpu::read_velocity_for_test(&probe);
        (img.expect("at least one frame"), resolved, vel)
    }

    fn luma(p: &[u8]) -> f64 {
        0.2126 * f64::from(p[0]) + 0.7152 * f64::from(p[1]) + 0.0722 * f64::from(p[2])
    }

    /// `(BOX, CTR)` luma planes at 1× resolution, derived from one supersampled image:
    /// the 3×3 block mean (true coverage) and the block's centre subsample (the point
    /// sample an aliased renderer produces).
    fn references(ss: &image::RgbaImage, w: u32, h: u32) -> (Vec<f64>, Vec<f64>) {
        assert_eq!((ss.width(), ss.height()), (w * SS, h * SS), "the supersample is an exact multiple");
        let mut boxed = Vec::with_capacity((w * h) as usize);
        let mut ctr = Vec::with_capacity((w * h) as usize);
        for y in 0..h {
            for x in 0..w {
                let mut sum = 0.0;
                for dy in 0..SS {
                    for dx in 0..SS {
                        sum += luma(&ss.get_pixel(x * SS + dx, y * SS + dy).0);
                    }
                }
                boxed.push(sum / f64::from(SS * SS));
                ctr.push(luma(&ss.get_pixel(x * SS + SS / 2, y * SS + SS / 2).0));
            }
        }
        (boxed, ctr)
    }

    fn plane(img: &image::RgbaImage) -> Vec<f64> {
        img.pixels().map(|p| luma(&p.0)).collect()
    }

    /// **The region every number in this file is measured over: the 3D content, and
    /// nothing else.** Fractions of the pane.
    ///
    /// `Map3D::ui` paints its own overlays INTO the same pane with egui, not through
    /// the camera: a logo inset (top right), a GPU badge (top left) and a stats line
    /// (bottom). They are high-contrast, they are antialiased by egui at whatever
    /// resolution the pane happens to be, and they do not move when the camera does —
    /// so they corrupt both halves of this file. MEASURED: with the overlays included,
    /// warping the previous frame by the motion vectors scored *worse* than not warping
    /// it at all (19.6 vs 15.5) purely because a static logo was being displaced;
    /// excluded, the same warp lands 2.2× closer. Stated here because a crop that is
    /// not justified is a crop that was tuned until the test passed.
    const CONTENT: (f64, f64, f64, f64) = (0.06, 0.40, 0.60, 0.88);

    /// The [`CONTENT`] rect in pixels for a `w × h` image.
    fn content_rect(w: u32, h: u32) -> (u32, u32, u32, u32) {
        let (a, b, c, d) = CONTENT;
        (
            (f64::from(w) * a) as u32,
            (f64::from(h) * b) as u32,
            (f64::from(w) * c) as u32,
            (f64::from(h) * d) as u32,
        )
    }

    /// RMS over the content rect only.
    fn rms(a: &[f64], b: &[f64], w: u32, h: u32) -> f64 {
        assert_eq!(a.len(), b.len());
        let (x0, y0, x1, y1) = content_rect(w, h);
        let mut s = 0.0;
        let mut n = 0usize;
        for y in y0..y1 {
            for x in x0..x1 {
                let i = (y * w + x) as usize;
                s += (a[i] - b[i]) * (a[i] - b[i]);
                n += 1;
            }
        }
        (s / n as f64).sqrt()
    }

    /// Peak-to-trough luma over the content rect — a frame with no range there makes
    /// every error above meaningless.
    fn spread(v: &[f64], w: u32, h: u32) -> f64 {
        let (x0, y0, x1, y1) = content_rect(w, h);
        let (mut lo, mut hi) = (f64::MAX, f64::MIN);
        for y in y0..y1 {
            for x in x0..x1 {
                let p = v[(y * w + x) as usize];
                lo = lo.min(p);
                hi = hi.max(p);
            }
        }
        hi - lo
    }

    /// The gate every arm of this file honours: without a real GPU the numbers below
    /// would be llvmpipe's, and this file would go green having proved nothing about
    /// the hardware the app ships on.
    fn gpu_gate(test: &str) -> bool {
        if std::env::var("FACETT_TEST_GPU").as_deref() == Ok("discrete") {
            return true;
        }
        eprintln!(
            "[taa_present] {test}: SKIPPED and therefore PROVED NOTHING — FACETT_TEST_GPU=discrete \
             was not set, so no discrete adapter was selected and NOTHING was measured about the \
             present pass's antialiasing on real hardware."
        );
        false
    }

    /// The pair of numbers this file argues with: how far the presented image sits
    /// from the coverage truth, and the same distance normalised by how far it sits
    /// from the point sample.
    fn score(img: &image::RgbaImage, boxed: &[f64], ctr: &[f64]) -> (f64, f64) {
        let got = plane(img);
        let (w, h) = (img.width(), img.height());
        let e_box = rms(&got, boxed, w, h);
        let e_ctr = rms(&got, ctr, w, h);
        (e_box, e_box / e_ctr.max(1e-9))
    }

    /// **The claim**: after N frames on a still camera, the presented image is closer
    /// to the 9-sample coverage truth than the SAME pipeline with the temporal lane
    /// switched off — a ratio against a control measured in this run, on this device,
    /// not against a constant.
    #[test]
    fn the_present_pass_converges_toward_the_supersampled_truth() {
        if !gpu_gate("the_present_pass_converges_toward_the_supersampled_truth") {
            return;
        }
        use facett_map3d::gpu::TaaMode;

        let (img, frames_on) = render_seq_mode(FRAMES, 0.0, 0.0, 1.0, Some(TaaMode::On));
        let (w, h) = (img.width(), img.height());
        let (off, frames_off) = render_seq_mode(FRAMES, 0.0, 0.0, 1.0, Some(TaaMode::Off));
        // The reference is rendered with the temporal lane OFF: nine honest point
        // samples per pixel, so the truth this is measured against is the rasteriser's
        // own, not another TAA image that could be flattering itself.
        let (ss, _) = render_seq_mode(FRAMES, 0.0, 0.0, SS as f32, Some(TaaMode::Off));
        image::DynamicImage::ImageRgba8(img.clone()).save("/tmp/map3d_taa_on.png").ok();
        image::DynamicImage::ImageRgba8(off.clone()).save("/tmp/map3d_taa_off.png").ok();
        image::DynamicImage::ImageRgba8(ss.clone()).save("/tmp/map3d_taa_ref.png").ok();

        let (boxed, ctr) = references(&ss, w, h);
        let (e_on, ratio_on) = score(&img, &boxed, &ctr);
        let (e_off, ratio_off) = score(&off, &boxed, &ctr);
        eprintln!(
            "[taa_present] {w}x{h}, {FRAMES} frames | TAA on : rms(BOX)={e_on:.3} ratio={ratio_on:.3} \
             resolved={frames_on} | TAA off: rms(BOX)={e_off:.3} ratio={ratio_off:.3} resolved={frames_off}"
        );

        // ── THE WIRING IS LIVE, and the control really is the control.
        assert_eq!(
            frames_on, FRAMES as u64,
            "the resolve must have run on every frame — {frames_on} of {FRAMES}"
        );
        assert_eq!(frames_off, 0, "the control must have resolved NOTHING ({frames_off})");

        // ── NON-VACUITY: a flat frame would make both errors ~0 and the ratio noise.
        assert!(
            spread(&plane(&img), w, h) > 60.0 && spread(&boxed, w, h) > 60.0,
            "the frame must have real contrast to alias in the first place (1x spread {:.1}, ref {:.1})",
            spread(&plane(&img), w, h),
            spread(&boxed, w, h)
        );
        // And the control is genuinely aliased: it sits CLOSER to the one-sample point
        // render than to the nine-sample coverage. If it did not, there would be no
        // aliasing here for TAA to remove and every number below would be noise.
        assert!(
            ratio_off > 1.0,
            "the no-TAA control must BE a point sample (ratio {ratio_off:.3}) — otherwise this scene \
             does not alias and the comparison proves nothing"
        );

        // ── THE HEADLINE: the presented image crosses to the coverage side.
        assert!(
            ratio_on < 1.0,
            "the presented image must sit closer to the 9-sample COVERAGE than to the 1-sample \
             POINT render — ratio {ratio_on:.3} (rms vs BOX {e_on:.3})"
        );
        // ── …and by a margin over the control, on the same scene and device.
        assert!(
            e_on < e_off * 0.8,
            "TAA must cut the distance to the supersampled truth well below the control's \
             ({e_on:.3} vs {e_off:.3})"
        );
    }

    /// Radians of azimuth per frame in the moving arm — about two pixels of screen
    /// motion per frame at this pose, i.e. a slow pan, not a whip. A *fast* pan would
    /// be an easier test: the history reprojects further, so a missing reprojection
    /// smears further. Slow is the honest setting.
    const PAN: f32 = 0.004;

    /// Bilinear luma sample of `img` at pixel coordinates `(x, y)`, clamped to the
    /// image. Used to warp one frame by the motion vectors the next one computed.
    fn sample_bilinear(p: &[f64], w: u32, h: u32, x: f64, y: f64) -> f64 {
        let cx = x.clamp(0.0, f64::from(w) - 1.001);
        let cy = y.clamp(0.0, f64::from(h) - 1.001);
        let (x0, y0) = (cx.floor() as u32, cy.floor() as u32);
        let (fx, fy) = (cx - f64::from(x0), cy - f64::from(y0));
        let at = |xx: u32, yy: u32| p[(yy * w + xx) as usize];
        let (x1, y1) = ((x0 + 1).min(w - 1), (y0 + 1).min(h - 1));
        let a = at(x0, y0) * (1.0 - fx) + at(x1, y0) * fx;
        let b = at(x0, y1) * (1.0 - fx) + at(x1, y1) * fx;
        a * (1.0 - fy) + b * fy
    }

    /// **The motion vectors are real motion vectors** — proved against the image they
    /// claim to predict, not against a second copy of the same matrix arithmetic.
    ///
    /// Take two consecutive poses. Render each with the temporal lane OFF, so both are
    /// plain unjittered frames of the scene. Read the velocity field the second frame
    /// computed. Then warp the FIRST image by that field: `A(x - v(x))` must land on
    /// `B(x)`. If it does — much closer than the unwarped `A(x)` does — the field is
    /// the camera's actual screen motion, in the right units and the right direction.
    /// A zero field, a doubled one and a sign-flipped one all fail this and all look
    /// merely "a bit soft" in the final picture, which is why it is measured here.
    #[test]
    fn the_motion_vectors_predict_the_image_motion() {
        if !gpu_gate("the_motion_vectors_predict_the_image_motion") {
            return;
        }
        use facett_map3d::gpu::TaaMode;
        // Fast enough that the unwarped difference is unmistakable — a slow pan would
        // make "warping helped" indistinguishable from "the frames were similar".
        const STEP: f32 = 0.02;

        // A: settled at the start pose. B: the same, plus ONE step of pan.
        let (a, _) = render_seq_mode(WARMUP + 1, 0.0, 0.0, 1.0, Some(TaaMode::Off));
        let (b, _) = render_seq_mode(WARMUP + 2, 0.0, STEP, 1.0, Some(TaaMode::Off));
        // The LIVE path over the same poses: the last frame's velocity is computed
        // against the frame before it, which is exactly the A→B step.
        let (_, resolved, vel) = render_seq_vel(WARMUP + 2, 0.0, STEP, 1.0, Some(TaaMode::On));
        assert_eq!(resolved, (WARMUP + 2) as u64, "every frame resolved");
        let (vw, vh, vel) = vel.expect("the velocity target was allocated and read back");
        let (w, h) = (a.width(), a.height());
        // The velocity field covers the MAP PANE, which egui insets inside the window
        // by its panel margin — so the field is smaller than the screenshot and the two
        // have to be lined up before anything is compared. The inset is symmetric, and
        // that assumption is checked rather than assumed: only the pane's pixels may
        // differ between the two poses (the chrome around it does not move).
        let (ox, oy) = ((w - vw) / 2, (h - vh) / 2);
        let (pa, pb) = (plane(&a), plane(&b));
        for y in 0..h {
            for x in 0..w {
                let inside = x >= ox && x < ox + vw && y >= oy && y < oy + vh;
                if !inside {
                    let i = (y * w + x) as usize;
                    assert!(
                        (pa[i] - pb[i]).abs() < 1.0,
                        "pixel ({x},{y}) is outside the assumed pane rect but MOVED — the \
                         velocity field is not where this test thinks it is"
                    );
                }
            }
        }

        // A margin keeps the pane edges — where the previous frame genuinely has no
        // data — out of both sums, so this measures reprojection, not clamping.
        const M: u32 = 12;
        // **AND THE HUD IS EXCLUDED.** `Map3D::ui` paints its own overlays INTO the same
        // pane — the logo inset (top right), the GPU badge (top left) and the stats line
        // (bottom) — with egui, not through the camera. They do not move when the camera
        // does, so warping them by the motion vectors can only ever add error, and they
        // are high-contrast enough to dominate a whole-pane RMS. MEASURED: including
        // them, warping "made it worse" (19.6 vs 15.5) while a Lucas-Kanade estimate on
        // the 3D content agreed with the field to within a third of a pixel.
        let (cx0, cx1) = (M.max(16), vw * 62 / 100);
        let (cy0, cy1) = (vh * 40 / 100, vh * 90 / 100);
        let (mut unwarped, mut warped, mut n, mut max_px) = (0.0f64, 0.0f64, 0usize, 0.0f64);
        for y in cy0..cy1 {
            for x in cx0..cx1 {
                let v = vel[(y * vw + x) as usize];
                let (dx, dy) = (f64::from(v[0]) * f64::from(vw), f64::from(v[1]) * f64::from(vh));
                max_px = max_px.max(dx.hypot(dy));
                let i = ((y + oy) * w + x + ox) as usize;
                let du = pb[i] - pa[i];
                let dw = pb[i]
                    - sample_bilinear(&pa, w, h, f64::from(x + ox) - dx, f64::from(y + oy) - dy);
                unwarped += du * du;
                warped += dw * dw;
                n += 1;
            }
        }
        let unwarped = (unwarped / n as f64).sqrt();
        let warped = (warped / n as f64).sqrt();
        eprintln!(
            "[taa_present] motion vectors: warping A by v lands on B — rms(B, A)={unwarped:.3} \
             rms(B, A∘v)={warped:.3}  ({:.2}x closer), peak |v|={max_px:.2}px",
            unwarped / warped.max(1e-9)
        );

        // ── NON-VACUITY: the two frames really are different pictures, and the field
        // really does carry motion (a zero field would trivially tie the two sums).
        assert!(unwarped > 4.0, "the two poses must differ measurably ({unwarped:.3})");
        assert!(max_px > 2.0, "the velocity field must carry real motion (peak {max_px:.2}px)");

        // ── THE CLAIM: the field predicts the motion.
        assert!(
            warped < unwarped * 0.6,
            "warping the previous frame by the motion vectors must land it much closer to this \
             frame ({warped:.3} vs {unwarped:.3}) — a wrong-signed, wrong-scaled or zero field \
             cannot do this"
        );
    }

    /// Diagnostic sweep (kept out of the assertions): how the three arms separate as
    /// the camera moves faster.
    #[test]
    #[ignore]
    fn pan_sweep_diagnostic() {
        if !gpu_gate("pan_sweep_diagnostic") {
            return;
        }
        use facett_map3d::gpu::TaaMode;
        for pan in [0.002f32, 0.004, 0.01, 0.02, 0.04] {
            let end = pan * (FRAMES - 1 - WARMUP) as f32;
            let (on, _) = render_seq_mode(FRAMES, 0.0, pan, 1.0, Some(TaaMode::On));
            let (zero, _) = render_seq_mode(FRAMES, 0.0, pan, 1.0, Some(TaaMode::ZeroVelocity));
            let (off, _) = render_seq_mode(FRAMES, 0.0, pan, 1.0, Some(TaaMode::Off));
            let (ss, _) = render_seq_mode(FRAMES, end, 0.0, SS as f32, Some(TaaMode::Off));
            let (boxed, ctr) = references(&ss, on.width(), on.height());
            let (e_on, r_on) = score(&on, &boxed, &ctr);
            let (e_zero, _) = score(&zero, &boxed, &ctr);
            let (e_off, _) = score(&off, &boxed, &ctr);
            eprintln!(
                "[taa_present] pan={pan:.3} rad/frame: velocity={e_on:.3} (ratio {r_on:.3}) \
                 zero={e_zero:.3} noTAA={e_off:.3}"
            );
        }
    }

    /// **The velocity half, and the mutation it exists to fail.**
    ///
    /// A still camera converges with NO motion vectors at all — the history lands back
    /// on the same pixel by luck — which is exactly why a static proof cannot see a
    /// zero-velocity TAA. So this one MOVES the camera, and compares three renders of
    /// the same final pose:
    ///
    /// * **On** — the shipped path: depth-reprojected motion vectors;
    /// * **ZeroVelocity** — the mutation: identical jitter, identical resolve, but the
    ///   1×1 zero-velocity texture. This is what "wire TAA up without motion vectors"
    ///   actually ships, and on a flight-mode camera it smears;
    /// * **Off** — no temporal lane at all: aliased, but never smeared.
    ///
    /// Measured against the supersampled truth AT THE FINAL POSE. The mutation must be
    /// measurably worse than the shipped path, and the shipped path must still land on
    /// the coverage side of the point sample — a moving TAA that merely stopped
    /// smearing while giving up its antialiasing would pass the first and fail the
    /// second.
    ///
    /// **WHAT THIS DELIBERATELY DOES NOT CLAIM** (measured, `pan_sweep_diagnostic`,
    /// cap 0.90): a moving TAA frame is *not* uniformly closer to the still-pose truth
    /// than no AA at all. rms vs BOX, by pan rate:
    ///
    /// | rad/frame | velocity | zero-velocity | no TAA |
    /// |-----------|----------|---------------|--------|
    /// | 0.002     | 3.053    | 4.336         | 3.639  |
    /// | 0.004     | 3.771    | 6.049         | 3.686  |
    /// | 0.010     | 4.515    | 8.165         | 3.687  |
    /// | 0.020     | 4.912    | 9.487         | 3.663  |
    /// | 0.040     | 4.059    | 11.337        | 3.697  |
    ///
    /// Velocity beats zero-velocity everywhere, by 1.4× to 2.8× — that is the claim.
    /// But between ~0.01 and ~0.02 rad/frame the reprojected history is still *further*
    /// from a still-pose reference than an un-antialiased frame: each frame resamples
    /// the history bilinearly and the softness accumulates. That is TAA's known
    /// motion-softness, and the standard cures for it (a velocity-adaptive feedback
    /// weight, a Catmull-Rom history filter) are not in this change. This metric also
    /// cannot see what TAA actually buys under motion — temporal stability — so "worse
    /// here" is not "worse on screen"; it is simply not proved either way, and that is
    /// written down rather than asserted away.
    #[test]
    fn a_moving_camera_reprojects_instead_of_smearing() {
        if !gpu_gate("a_moving_camera_reprojects_instead_of_smearing") {
            return;
        }
        use facett_map3d::gpu::TaaMode;

        let end = PAN * (FRAMES - 1 - WARMUP) as f32;
        let (on, frames_on) = render_seq_mode(FRAMES, 0.0, PAN, 1.0, Some(TaaMode::On));
        let (zero, frames_zero) = render_seq_mode(FRAMES, 0.0, PAN, 1.0, Some(TaaMode::ZeroVelocity));
        let (off, _) = render_seq_mode(FRAMES, 0.0, PAN, 1.0, Some(TaaMode::Off));
        // The truth for the pose the pan ENDS at, held still, temporal lane off.
        let (ss, _) = render_seq_mode(FRAMES, end, 0.0, SS as f32, Some(TaaMode::Off));
        let (w, h) = (on.width(), on.height());
        image::DynamicImage::ImageRgba8(on.clone()).save("/tmp/map3d_taa_moving_on.png").ok();
        image::DynamicImage::ImageRgba8(zero.clone()).save("/tmp/map3d_taa_moving_zerovel.png").ok();

        let (boxed, ctr) = references(&ss, w, h);
        let (e_on, r_on) = score(&on, &boxed, &ctr);
        let (e_zero, r_zero) = score(&zero, &boxed, &ctr);
        let (e_off, r_off) = score(&off, &boxed, &ctr);
        eprintln!(
            "[taa_present] moving {w}x{h}, {FRAMES} frames @ {PAN} rad/frame | velocity: \
             rms(BOX)={e_on:.3} ratio={r_on:.3} | ZERO-velocity: rms(BOX)={e_zero:.3} ratio={r_zero:.3} \
             | no TAA: rms(BOX)={e_off:.3} ratio={r_off:.3}"
        );

        assert_eq!(frames_on, FRAMES as u64, "the resolve ran on every moving frame");
        assert_eq!(frames_zero, FRAMES as u64, "…and so did the mutation's");
        assert!(
            spread(&boxed, w, h) > 60.0,
            "the reference frame must have real contrast ({:.1})",
            spread(&boxed, w, h)
        );

        // ── THE MUTATION: dropping the motion vectors must COST something measurable.
        // If this does not fire, the velocity pass is decoration and the frame path is
        // shipping zero-velocity TAA under a green test.
        assert!(
            e_zero > e_on * 1.15,
            "TAA without motion vectors must be measurably worse on a moving camera — \
             zero-velocity {e_zero:.3} vs reprojected {e_on:.3}; if these are the same, the \
             velocity pass is not reaching the resolve"
        );
        // ── AND THE OTHER FAILURE MODE: not smearing is not enough. The moving frame
        // must still be antialiased — closer to coverage than to the point sample —
        // or TAA has bought a smear-free image by contributing nothing. The no-TAA
        // control is printed beside it and NOT asserted against: see the table above
        // for why that comparison is not a claim this file makes.
        assert!(
            r_on < 1.0,
            "a moving TAA frame must still land on the COVERAGE side: ratio {r_on:.3} \
             (rms {e_on:.3}; the no-TAA control scores {e_off:.3} at ratio {r_off:.3})"
        );
        // The mutation must not be excused by the same rule: a smeared frame is not
        // allowed to score as "coverage" merely because a blur moves it off the point
        // sample. It is worse than the shipped path by the margin asserted above.
        assert!(e_zero > e_on, "…and the mutation stays the worse of the two ({e_zero:.3} vs {e_on:.3})");
    }
}