facett-map3d/tests/overlay_frame_stability.rs
//! **A riding overlay must not make the frame flicker** (korp ride, tune round 2:
//! "den flimrar") — consecutive-frame stability of the REAL wgpu lane while a
//! per-frame overlay rides with its near-plane override active.
//!
//! The suspects the second ride named — the per-frame overlay vbuf rewrite racing
//! the draw, an oscillating `near_override` re-banding the depth range, the
//! emissive-w path — all share one observable: two frames of an UNCHANGED scene
//! that differ. So that is the assertion, on applied output: render the same
//! pinned-clock scene on two consecutive harness frames and require the
//! framebuffers BYTE-IDENTICAL. Any nondeterminism in the upload path, the near
//! band, or the post chain fires it.
//!
//! LAW-2 red: the comparator itself is proven able to see a difference — a
//! deliberate one-hair camera nudge between two renders must produce a non-zero
//! pixel diff. A blind diff (wrong buffer, stale readback) passes the identity
//! half by accident and fails the nudge half.
//!
//! Run: `cargo test -p facett-map3d --features wgpu --test overlay_frame_stability`
#![cfg(feature = "wgpu")]
use egui_kittest::Harness;
use egui_kittest::wgpu::{WgpuTestRenderer, create_render_state, default_wgpu_setup};
use facett_core::Facet as _;
use facett_map3d::extrude::Mesh3;
use facett_map3d::{Map3D, OverlayPose};
/// An avatar-stand-in: a few triangles near the eye (the rider's-seat regime the
/// near-override exists for), plus an emissive face so the bloom path runs.
fn body() -> Mesh3 {
Mesh3 {
positions: vec![
[0.0, 0.05, 0.30],
[-0.20, 0.0, -0.20],
[0.20, 0.0, -0.20],
[0.0, 0.12, -0.10],
],
normals: vec![[0.0, 1.0, 0.0]; 4],
colors: vec![egui::Color32::from_rgb(35, 36, 40), egui::Color32::from_rgb(198, 40, 40)],
indices: vec![0, 1, 2, 0, 3, 2],
building: vec![false; 4],
}
}
/// The pinned scene: the synthetic fixture, clock stopped, overlay riding close to
/// the camera so `near_override` is live (the flight regime under test).
fn rigged() -> Map3D {
let mut m = Map3D::synthetic("frame-stability").with_clock(1.0);
m.spin = false;
m.set_overlay(
&body(),
&[0.0, 4.0],
OverlayPose { position: [0.0, 0.35, 0.0], scale: 0.4, ..OverlayPose::default() },
);
m
}
/// A harness whose renderer runs `mode`. The strict byte-identity claim below needs
/// [`TaaMode::Off`]: a temporal accumulator changes the frame on purpose every frame, so
/// with it on, "two consecutive frames are byte-identical" is a statement about the
/// antialiasing and not about the overlay upload, the near band or the post chain — the
/// three suspects this file was written to arraign. The temporal lane gets its own
/// claim, [`the_temporal_lane_settles_instead_of_flickering`], rather than being allowed
/// to hide inside this one.
fn harness_mode(view: Map3D, mode: facett_map3d::gpu::TaaMode) -> Harness<'static> {
let rs = create_render_state(default_wgpu_setup(), Default::default());
facett_map3d::gpu::install_renderer_with_taa_for_test(&rs, mode);
let view = view.with_gpu(true);
let mut cell = Some(view);
let mut held: Option<Map3D> = None;
Harness::builder()
.with_size(egui::vec2(640.0, 480.0))
.renderer(WgpuTestRenderer::from_render_state(rs))
.build_ui(move |ui| {
let v = held.get_or_insert_with(|| cell.take().expect("view moved in once"));
facett_core::set_theme(ui.ctx(), facett_core::Theme::sci_fi());
v.ui(ui);
})
}
fn harness(view: Map3D) -> Harness<'static> {
harness_mode(view, facett_map3d::gpu::TaaMode::Off)
}
fn diff_px(a: &image::RgbaImage, b: &image::RgbaImage) -> usize {
assert_eq!((a.width(), a.height()), (b.width(), b.height()), "same framebuffer shape");
a.pixels().zip(b.pixels()).filter(|(p, q)| p != q).count()
}
/// **Two consecutive frames of an unchanged riding scene are byte-identical** —
/// and the comparator is proven sighted first.
#[test]
fn an_unchanged_riding_frame_renders_byte_identical_twice() {
// ── the comparator's own red: a nudged camera must read as different ──────
let mut nudged = rigged();
nudged.cam.azimuth += 0.02;
nudged.cam.goal_azimuth = nudged.cam.azimuth;
let mut h2 = harness(nudged);
h2.run_steps(4);
let moved = h2.render().expect("wgpu render (nudged)");
let mut h1 = harness(rigged());
h1.run_steps(4);
let base = h1.render().expect("wgpu render (base)");
let seen = diff_px(&base, &moved);
assert!(
seen > 0,
"the diff must SEE a one-hair camera nudge — a blind comparator proves nothing",
);
// ── the claim: same scene, next frame, zero changed pixels ────────────────
let a = h1.render().expect("frame A");
h1.run_steps(1);
let b = h1.render().expect("frame B");
let flicker = diff_px(&a, &b);
assert_eq!(
flicker, 0,
"an unchanged riding scene must be pixel-stable frame to frame — {flicker} px \
changed (overlay upload, near band or post chain is nondeterministic)",
);
}
/// Drive `frames` real GPU frames through `view` with the temporal lane on, and report
/// the per-frame series of `(max channel delta, px moving by more than `LOUD`)` between
/// consecutive frames.
///
/// **NOTE THE FRAME DRIVER.** `run_steps` advances egui's logic; `render()` is what puts
/// a frame through the GPU lane, and therefore what advances the temporal accumulation.
/// Measuring "settled" after `run_steps(24)` and two renders measures frame 4 of the
/// ramp, not frame 28 — MEASURED that way it read 124 code values and looked like a
/// defect. Every frame here is a real render.
fn settle_series(view: Map3D, frames: usize) -> Vec<(u8, usize)> {
let mut h = harness_mode(view, facett_map3d::gpu::TaaMode::On);
let mut prev: Option<image::RgbaImage> = None;
let mut out = Vec::with_capacity(frames);
for _ in 0..frames {
h.run_steps(1);
let img = h.render().expect("a rendered frame");
if let Some(p) = prev.take() {
out.push((max_channel_delta(&p, &img), loud_px(&p, &img)));
}
prev = Some(img);
}
out
}
/// **AND THE TEMPORAL LANE ITSELF MUST SETTLE, not shimmer.**
///
/// The claim above is measured with TAA off, which would be a convenient place to stop:
/// the file's original complaint was "den flimrar", and a temporal antialiaser is by
/// construction a thing that changes every pixel every frame. So the question it raises
/// has to be asked rather than routed around — *does the AA lane put the flicker back?*
///
/// The ramp is what stops it: `min(cap, frame/(frame+1))` makes the early frames a
/// running mean and only then decays into an EMA, so the per-frame correction collapses
/// instead of holding. What is asserted is that shape, in two directions — the residual
/// must fall a long way from the ramp, and it must be CONFINED to a small part of the
/// frame rather than being a whole-image throb.
///
/// Not `== 0`: the 0.97 feedback cap is deliberate (an unbounded running mean would never
/// respond to a scene change at all), so a fixed 3% of each frame is the current one and
/// a periodic jitter sequence leaves a periodic residual. The honest claim is bounded,
/// shrinking and small — not absent.
///
/// **THE BLOOM ARM, AND THE SUSPICION IT CLEARED.** The temporal resolve runs *after*
/// the composite, so it accumulates a frame the half-res glow chain has already
/// filtered — and a half-res filter over a jittered image is exactly the shape that
/// varies with the jitter phase over areas too wide for a 3×3 neighbourhood clamp to
/// bound. That predicted a bloom-dependent shimmer, so both arms are measured rather
/// than assumed. MEASURED (640×480, RADV STRIX1): bloom on and bloom off give the same
/// series to within one code value, settling to 23, with **4 px of 307 200** moving by
/// more than 8. The interaction is not there at this bloom intensity, and the prediction
/// is recorded as refuted instead of quietly dropped.
///
/// The residual itself is the periodic one the design predicts: the series settles into
/// a repeating 8-frame pattern, which is the jitter cycle showing through the 3% of each
/// frame that the 0.97 feedback cap leaves un-averaged. Bounded, periodic, and on four
/// pixels.
#[test]
fn the_temporal_lane_settles_instead_of_flickering() {
/// Frames driven through the lane. Comfortably past the ramp: `frame/(frame+1)`
/// reaches the 0.97 feedback cap at frame 33.
const FRAMES: usize = 48;
for (name, view) in [("bloom-on", rigged()), ("bloom-off", rigged().with_bloom(false))] {
let series = settle_series(view, FRAMES);
let (d0, _) = series[0];
let (late, late_loud) = *series.last().expect("at least two frames");
let deltas: Vec<u8> = series.iter().map(|(d, _)| *d).collect();
eprintln!("EMIT-TAASETTLE {name}: consecutive max channel delta = {deltas:?}");
eprintln!(
"EMIT-TAASETTLE {name}: ramp={d0} settled={late}, {late_loud} px of {PANE_PX} \
move by more than {LOUD} code values"
);
// NON-VACUITY: the lane must be moving pixels during the ramp, or "it settled"
// is a statement about a pass that never fired.
assert!(d0 > 0, "{name}: the temporal lane must move pixels during the ramp ({d0})");
// ── SETTLING, by a wide margin over the ramp rather than by a hair.
assert!(
u32::from(late) * 4 <= u32::from(d0),
"{name}: the accumulation must settle, not oscillate — settled delta {late} \
against a ramp delta of {d0} (series {deltas:?})"
);
// ── AND CONFINED. A residual of a few code values on a fringe of edge pixels is
// TAA working; the same number spread over the frame would be a throb.
// 4 px MEASURED on both arms; the budget is two orders of magnitude above that
// so a driver or a scene tweak does not make this file flaky, and still three
// orders below anything that would read as a throb.
let cap = PANE_PX / 1000;
assert!(
late_loud <= cap,
"{name}: the settled residual must be confined — {late_loud} of {PANE_PX} px \
move by more than {LOUD} code values between two consecutive frames of an \
unchanged scene (budget {cap})"
);
}
}
/// Pixels in the 640×480 pane.
const PANE_PX: usize = 640 * 480;
/// A per-channel change at or below this is under the noise an eye reads as flicker on a
/// moving-image display; above it, count the pixel.
const LOUD: u8 = 8;
/// How many pixels changed by more than [`LOUD`] on any channel.
fn loud_px(a: &image::RgbaImage, b: &image::RgbaImage) -> usize {
a.pixels()
.zip(b.pixels())
.filter(|(p, q)| p.0.iter().zip(q.0.iter()).any(|(x, y)| x.abs_diff(*y) > LOUD))
.count()
}
/// Largest absolute per-channel difference between two frames — the flicker an eye
/// would see, as opposed to `diff_px`'s count of pixels that moved at all.
fn max_channel_delta(a: &image::RgbaImage, b: &image::RgbaImage) -> u8 {
assert_eq!((a.width(), a.height()), (b.width(), b.height()), "same framebuffer shape");
a.pixels()
.zip(b.pixels())
.flat_map(|(p, q)| p.0.iter().zip(q.0.iter()).map(|(x, y)| x.abs_diff(*y)))
.max()
.unwrap_or(0)
}