facett-demo/tests/gfx_v2_device.rs
//! **The GFX v2 tab's device-side proof** — what the GPU actually did, not that a
//! frame was drawn.
//!
//! This file exists because of the one finding that runs through the whole GFX_V2
//! track: five closed items turned out to be "the feature was already there and
//! nothing was watching it". Concretely, on this same repo:
//!
//! * `facett-flowsim`'s GPU advect lane was complete and proven, and **nothing
//! called it** — every shipped frame was still the CPU painter.
//! * Item 2's `cull.wgsl` was orphaned: deleting the entire compute pass failed no
//! test.
//! * With flowsim's host unwired, a pixel assertion still **passed at 486 px with
//! zero device frames**, because the CPU painter drew the thing being asserted.
//!
//! So every test here asserts something only the device could have produced:
//!
//! | claim | the evidence |
//! |-------|--------------|
//! | the item-8 id pass ran | a real `PickId` read back out of an `R32Uint` texel, **and** `pick_passes_run()` moved |
//! | the item-4 collision pass ran | the **indirect draw buffer**, read back: `instance_count`, `labels_won`, `labels_drawn` |
//! | the collision pass *decided* something | `labels_won` is strictly less than the candidates offered — most labels lost |
//! | it was a GPU | the **adapter name**, printed by every test |
//!
//! That last row is not decoration. A GPU test on a box with no WebGPU-class
//! adapter skips gracefully and goes green having run no shader, which is
//! indistinguishable from a pass. Each test therefore prints the adapter it ran on,
//! and [`adapter_or_skip`] prints an unmistakable line when it does skip.
//!
//! Run: `cargo test -p facett-demo --test gfx_v2_device -- --nocapture`
#![cfg(all(feature = "gfx-v2-gpu", not(target_arch = "wasm32")))]
use facett_core::engine::pick::PickId;
use facett_core::Facet;
use facett_demo::gfx_v2::{self, GfxV2Tab};
/// **Serialises every device test in this file, and that is load-bearing.**
///
/// `gfx_v2::armed::arm()` is a PROCESS-WIDE slot with last-arm-wins semantics — it has
/// to be, because `egui_kittest`'s `WgpuTestRenderer::from_render_state` refuses a
/// `RenderState` that has already been used, so each harness needs its own device.
/// Under cargo's default parallel runner that means one test's `arm()` REPLACES the
/// device another test is mid-way through painting with.
///
/// Measured 2026-08-01, and it is not theoretical: run in parallel,
/// `the_collision_pass_…` failed reporting `adapter = llvmpipe` while
/// `armed_the_tab_names_the_adapter_it_ran_on` passed on the RTX 4090 in the same
/// binary — two tests, two devices, one slot. Run with `--test-threads=1` all six
/// passed. A guard whose colour depends on the scheduler is not a guard.
///
/// So every test here takes this lock first. `lock()` is deliberately un-poisoned
/// (`unwrap_or_else(|e| e.into_inner())`): a panicking test must not convert every
/// later test into a poison error and hide the original failure.
static DEVICE: std::sync::Mutex<()> = std::sync::Mutex::new(());
/// Take the device lock for the duration of one test.
fn device_lock() -> std::sync::MutexGuard<'static, ()> {
DEVICE.lock().unwrap_or_else(|e| e.into_inner())
}
/// A real `egui_wgpu::RenderState` — the object an eframe host hands its widgets —
/// with no window, plus the adapter name.
///
/// Returns `None` on a box with no WebGPU-class adapter, and says so loudly. A
/// silent `return` is exactly how a GPU suite reports green having run nothing.
fn armed_state() -> Option<(egui_wgpu::RenderState, String)> {
let setup = egui_kittest::wgpu::default_wgpu_setup();
let rs = egui_kittest::wgpu::create_render_state(setup, egui_wgpu::RendererOptions::PREDICTABLE);
let name = rs.adapter.get_info().name;
if name.is_empty() {
eprintln!("[gfx_v2_device] SKIPPED — adapter reported no name");
return None;
}
gfx_v2::arm(&rs);
Some((rs, name))
}
/// Arm, or print an unmissable skip line and hand back `None`.
macro_rules! adapter_or_skip {
($who:literal) => {
match armed_state() {
Some(v) => v,
None => {
eprintln!("[gfx_v2_device::{}] SKIPPED — no WebGPU-class adapter", $who);
return;
}
}
};
}
/// Drive the tab through a real `egui_kittest` harness on the wgpu renderer, for
/// `steps` frames, and hand back the tab's `state_json`.
///
/// The size matters: the panes are half the width each, and a pane too small puts
/// every label in one grid cell, which would make "the collision pass rejected
/// most candidates" true for the wrong reason.
fn run_tab(steps: usize, prepare: impl FnOnce(&mut GfxV2Tab)) -> Option<serde_json::Value> {
let mut tab = GfxV2Tab::new(gfx_v2::TITLE);
prepare(&mut tab);
let mut harness = harness_on(tab)?;
harness.run_steps(steps);
Some(harness.state().state_json())
}
/// A harness painting through **the render state the lanes were armed with**.
///
/// `WgpuTestRenderer::default()` builds its OWN device, and that is a trap this suite
/// walked into: the lane installed its `LabelCollider` into the armed `RenderState`
/// while the harness painted through a different one, so `prepare` looked the
/// collider up in the wrong `CallbackResources`, found nothing, and returned early.
///
/// The host-side witness said `lane = "gpu"` and `frames >= 2` — because `draw()` had
/// legitimately submitted a callback — and only the indirect-buffer readback showed
/// the truth: `vertex_count = 0`, meaning `run()` had never written the args and no
/// compute pass had ever executed. That is this track's signature failure reproduced
/// exactly, and the reason the readback is not optional.
fn harness_on(tab: GfxV2Tab) -> Option<egui_kittest::Harness<'static, GfxV2Tab>> {
// A FRESH state per harness, armed immediately: `from_render_state` panics on a
// `RenderState` that has already been used, so each harness must own one, and
// `gfx_v2::arm` therefore has to be re-armable (see its docs).
let (rs, _) = armed_state()?;
Some(
egui_kittest::Harness::builder()
.with_size(egui::vec2(1200.0, 700.0))
.renderer(egui_kittest::wgpu::WgpuTestRenderer::from_render_state(rs))
.build_ui_state(|ui, t: &mut GfxV2Tab| t.ui(ui), tab),
)
}
// ─────────────────────────────────────────────────────────────────────────────
// Item 8 — the id pass, through the tab's own batch.
// ─────────────────────────────────────────────────────────────────────────────
/// **The tab is a real consumer of `pick_at`.** Build the tab's own pick batch from
/// the tab's own projected scene, resolve two clicks on two different places
/// through the shipped entry point, and get the two right ids back.
///
/// `pick_passes_run()` is asserted to have MOVED — but only alongside the ids,
/// because a pass over an empty batch bumps the counter while proving nothing. That
/// pairing is the whole discipline.
#[test]
fn the_tab_resolves_two_different_places_through_the_device_id_pass() {
let _device = device_lock();
let (rs, adapter) = adapter_or_skip!("id_pass");
eprintln!("[gfx_v2_device] adapter = {adapter}");
use facett_core::render::gpu::pick_at;
let tab = GfxV2Tab::new(gfx_v2::TITLE);
let scene = tab.scene();
// A square pane, projected exactly the way the 2D pane is.
let (w, h) = (700.0f32, 700.0f32);
let view = tab.view_2d(h * 0.5);
let projected = scene.project(&view, (w * 0.5, h * 0.5), h * 0.5);
// Two clearly separated, visible CITIES — distinguishable targets, or the test
// proves nothing.
let mut targets: Vec<(usize, f32, f32)> = Vec::new();
for (i, p) in scene.places.iter().enumerate() {
if !p.is_city() {
continue;
}
let pr = projected[i];
if !pr.visible || pr.x < 20.0 || pr.y < 20.0 || pr.x > w - 20.0 || pr.y > h - 20.0 {
continue;
}
if targets.iter().all(|&(_, x, y)| ((x - pr.x).powi(2) + (y - pr.y).powi(2)).sqrt() > 60.0) {
targets.push((i, pr.x, pr.y));
}
if targets.len() == 2 {
break;
}
}
assert_eq!(targets.len(), 2, "the scene must offer two well-separated visible cities");
let (ia, ax, ay) = targets[0];
let (ib, bx, by) = targets[1];
let (want_a, want_b) = (scene.places[ia].id, scene.places[ib].id);
assert_ne!(want_a, want_b, "the two targets must be DISTINGUISHABLE");
// THE TAB'S OWN batch writer — not a copy of it. A test that re-implemented the
// batch would be asserting against its own geometry, which is exactly how a batch
// built in screen space rather than pane space passes every test and is still
// 32.6 px out of place in the app.
let rect = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(w, h));
let batch = tab.pick_batch(&projected, rect, 1.0);
assert!(batch.len() > 400, "the batch carries the real scene, got {} vertices", batch.len());
let before = facett_core::render::gpu::pick_passes_run();
let got_a = pick_at(&rs, &batch, (w as u32, h as u32), egui::pos2(ax, ay), 1.0, true)
.expect("the device lane must answer, not decline");
let got_b = pick_at(&rs, &batch, (w as u32, h as u32), egui::pos2(bx, by), 1.0, true)
.expect("the device lane must answer, not decline");
assert_eq!(
got_a, want_a,
"clicking {} read {:#010x}, expected {:#010x}",
scene.places[ia].name, got_a.0, want_a.0
);
assert_eq!(
got_b, want_b,
"clicking {} read {:#010x}, expected {:#010x}",
scene.places[ib].name, got_b.0, want_b.0
);
assert_ne!(got_a, got_b, "two clicks on two cities must not return one id");
// And the ids resolve back to the real names — the answer a user sees.
assert_eq!(scene.by_id(got_a).map(|p| p.name), Some(scene.places[ia].name));
assert_eq!(scene.by_id(got_b).map(|p| p.name), Some(scene.places[ib].name));
let ran = facett_core::render::gpu::pick_passes_run() - before;
assert!(ran >= 2, "two clicks must have submitted two id passes, got {ran}");
eprintln!(
"[gfx_v2_device] adapter={adapter} picked {} ({:#010x}) and {} ({:#010x}) in {ran} id passes",
scene.places[ia].name, got_a.0, scene.places[ib].name, got_b.0
);
}
/// A click on empty space is `Some(NOTHING)` — a real miss — and never `None`.
///
/// The two are different answers and the tab depends on telling them apart: `None`
/// means the lane declined and the CPU must answer, while a fabricated `NOTHING`
/// would silently deselect and look identical to a legitimate background click.
#[test]
fn empty_space_is_a_real_miss_and_not_a_declined_lane() {
let _device = device_lock();
let (rs, adapter) = adapter_or_skip!("miss");
eprintln!("[gfx_v2_device] adapter = {adapter}");
use facett_core::render::gpu::{pick_at, PickBatch};
let id = PickId::new(9, 4242);
let mut batch = PickBatch::new();
batch.push_quad(egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(40.0, 40.0)), id);
let hit = pick_at(&rs, &batch, (400, 400), egui::pos2(20.0, 20.0), 1.0, true);
assert_eq!(hit, Some(id), "the one quad is hittable, so the lane is genuinely working");
let miss = pick_at(&rs, &batch, (400, 400), egui::pos2(300.0, 300.0), 1.0, true);
assert_eq!(
miss,
Some(PickId::NOTHING),
"a click on empty space must be Some(NOTHING) — `None` would mean the lane declined",
);
// Outside the pane: THAT is a decline, and it must be distinguishable.
let outside = pick_at(&rs, &batch, (400, 400), egui::pos2(-5.0, 20.0), 1.0, true);
assert_eq!(outside, None, "a probe outside the pane declines rather than guessing");
eprintln!("[gfx_v2_device] adapter={adapter} miss/decline are distinguishable");
}
// ─────────────────────────────────────────────────────────────────────────────
// Item 4 — the collision pass, read back out of the indirect draw buffer.
// ─────────────────────────────────────────────────────────────────────────────
/// **The label collision pass ran, and it DECIDED something.**
///
/// The evidence is the indirect draw buffer the device wrote, copied back
/// explicitly: `instance_count` (what the draw actually uses), `labels_won` (what
/// passed the collision rule) and `labels_drawn` (what also fitted the budget).
/// Nothing on the CPU chose any of those numbers.
///
/// The load-bearing assertion is not "a label drew" but `labels_won < candidates`:
/// the pass must have thrown most of the real scene away. A pass that accepted
/// everything would draw an unreadable pile and would still satisfy any
/// "something rendered" check.
#[test]
fn the_collision_pass_wrote_the_indirect_buffer_and_rejected_most_candidates() {
let _device = device_lock();
let (_rs, adapter) = adapter_or_skip!("labels");
eprintln!("[gfx_v2_device] adapter = {adapter}");
// Several frames: the collider uploads once and the compute passes re-run every
// frame. Three frames also catches a `cs_clear` that was skipped, which would
// show up as the grid never being reset.
let mut tab = GfxV2Tab::new(gfx_v2::TITLE);
tab.set_gpu_labels(true);
let mut harness = harness_on(tab).expect("an armed harness");
harness.run_steps(3);
// RENDER, and not merely `run_steps`. `run_steps` drives the egui update loop —
// it builds the UI and collects the paint callbacks — but the wgpu renderer only
// EXECUTES them when a frame is actually rendered. Reading the indirect buffer
// after `run_steps` alone returns its untouched initial state, all zeros, which is
// indistinguishable from a compute pass that ran and decided nothing. Measured
// here: `vertex_count = 0` without this line, 6 with it.
let _ = harness.render().expect("the wgpu renderer produced an image");
// The host-side witness first: did the lane take the frames at all?
let s = harness.state().state_json();
let labels = &s["labels"];
assert_eq!(
labels["lane"], "gpu",
"the label lane must have taken the frame; it declined with: {:?}",
labels["declined"]
);
let candidates = labels["candidates"].as_u64().expect("candidates reported");
assert!(candidates > 100, "the real scene offered {candidates} candidates — expected many");
assert!(labels["frames"].as_u64().unwrap() >= 2, "the compute callback ran on several frames");
// THE DEVICE-SIDE PROOF: read the indirect draw buffer back.
let stats = harness
.state_mut()
.read_label_device_stats()
.expect("the collider must be installed and readable after a GPU frame");
eprintln!(
"[gfx_v2_device] adapter={adapter} candidates={candidates} won={} drawn={} \
instance_count={} vertex_count={} glyph_overflow={}",
stats.labels_won, stats.labels_drawn, stats.instance_count, stats.vertex_count, stats.glyph_overflow
);
// The args were written by `run`, not left at whatever was there: the label quad
// is 6 vertices. Anything else means the buffer was never initialised.
assert_eq!(stats.vertex_count, 6, "the indirect args carry the label quad's 6 vertices");
// The pass produced work for the draw.
assert!(stats.labels_won > 0, "some label must have won a grid cell");
assert!(stats.instance_count > 0, "the indirect draw must have instances to draw");
// THE HEADLINE: it decided. Most of the real scene lost.
assert!(
(stats.labels_won as u64) < candidates / 2,
"the collision pass must reject most of the {candidates} candidates, but {} won — \
a pass that accepts everything draws an unreadable pile and still passes any \
'something rendered' check",
stats.labels_won,
);
// And it did not overflow, so the picture is not silently missing ink.
assert_eq!(stats.glyph_overflow, 0, "the glyph buffer must not have overflowed");
// `labels_drawn` is `labels_won` after the ink budget, so it can never exceed it.
assert!(
stats.labels_drawn <= stats.labels_won,
"drawn ({}) cannot exceed won ({})",
stats.labels_drawn,
stats.labels_won,
);
}
/// **The GPU lane and the CPU fallback produce genuinely different pictures.**
///
/// This is the assertion that would have caught the flowsim bug. There, the host
/// was unwired and a pixel check still passed at 486 px because the CPU painter drew
/// the thing being measured. Here the two lanes are rendered separately and the
/// frames must DIFFER — through `golden::moved`, which requires both a share of
/// changed pixels and a changed region with real extent in **both** axes, so a thin
/// strip like a caption line cannot satisfy it.
///
/// Red-when-broken: unwire `LabelLane::draw` (make it return false) and both frames
/// become the CPU pile-up, `moved` reports "byte-identical", and this fails. That is
/// exactly the failure the flowsim assertion could not produce.
#[test]
fn the_gpu_label_lane_paints_a_different_picture_than_the_cpu_fallback() {
let _device = device_lock();
let (_rs, adapter) = adapter_or_skip!("gpu_vs_cpu");
eprintln!("[gfx_v2_device] adapter = {adapter}");
let shot = |gpu: bool| -> image::RgbaImage {
let mut tab = GfxV2Tab::new(gfx_v2::TITLE);
tab.set_gpu_labels(gpu);
let mut h = harness_on(tab).expect("an armed harness");
h.run_steps(3);
h.render().expect("the wgpu renderer produced an image")
};
let cpu = shot(false);
let gpu = shot(true);
// `moved`, never `assert_ne!` on encoded bytes: it demands BOTH a share of
// changed pixels and a changed region with real extent in each axis, so a thin
// strip like the pane caption cannot satisfy it. Three suites in this tree were
// measured passing a whole-PNG `assert_ne!` on things the change never caused.
let diff = facett_core::golden::moved(&cpu, &gpu, facett_core::golden::MinChange::PANE)
.expect("the GPU label lane must paint a materially different picture than the CPU pile-up");
eprintln!(
"[gfx_v2_device] adapter={adapter} gpu-vs-cpu labels: {} px ({:.2}%), bbox {:?}, max delta {}",
diff.pixels,
diff.percent(),
diff.bbox,
diff.max_channel_delta
);
}
// ─────────────────────────────────────────────────────────────────────────────
// Item 6 — the parity claim, measured on rendered frames.
// ─────────────────────────────────────────────────────────────────────────────
/// **The constrained 3D pane lands exactly on the 2D pane**, measured through a real
/// rendered frame rather than only in unit-test arithmetic.
///
/// `parity_max_pt` is the worst per-place disagreement in points between the two
/// panes while the constraint is on, computed during `ui()` from the pane-LOCAL
/// projected positions of all 614 places. Pane-local, because the two panes sit at
/// different screen offsets and comparing raw screen coordinates would be measuring
/// the layout.
#[test]
fn the_constrained_panes_agree_exactly_in_a_real_rendered_frame() {
let _device = device_lock();
let (_rs, adapter) = adapter_or_skip!("parity");
eprintln!("[gfx_v2_device] adapter = {adapter}");
let on = run_tab(2, |t| t.set_constrain(true)).expect("an armed harness");
let parity = on["view"]["parity_max_pt"].as_f64().expect("a constrained frame reports parity");
// ── the honest bound, and why it is not exactly zero ──────────────────────
//
// The two panes run the SAME derived `View`, and with the same rect they project
// BIT-identically (asserted in `gfx_v2::tests::the_derived_view_projects_bit_identically_twice`
// with `to_bits()` equality). On screen the panes sit at different offsets, so
// the pane-local comparison subtracts a different `rect.left()` from each side,
// and that f32 subtraction — not the projection — leaves a residue.
//
// Measured on this adapter: 6.1e-5 pt, i.e. about 1/16000 of a pixel, which is
// the ULP of an f32 near 1000. The bound below is 1/1000 pt: two orders of
// magnitude above the observed residue and four below anything visible. A real
// projection difference — the perspective pane, say — measures more than 20 pt
// (`without_the_constraint_the_two_panes_genuinely_disagree`), so this bound
// cannot be satisfied by a broken derivation.
assert!(
parity < 1e-3,
"the derived 2D view must land on itself to within f32 subtraction error, \
worst disagreement was {parity} pt",
);
assert_eq!(on["view"]["two_d"]["is_constrained_2d"], true);
assert_eq!(on["view"]["three_d"]["is_constrained_2d"], false, "the STORED view is still a real 3D view");
// The red half: with the constraint off, the tab must not report a stale parity
// from a previous frame — it never measured one.
let off = run_tab(2, |_| {}).expect("an armed harness");
assert!(
off["view"]["parity_max_pt"].is_null(),
"with the constraint off nothing is measured, so nothing may be reported: {:?}",
off["view"]["parity_max_pt"],
);
eprintln!("[gfx_v2_device] adapter={adapter} constrained parity = {parity} pt over 614 places");
}
// ─────────────────────────────────────────────────────────────────────────────
// The honesty contract.
// ─────────────────────────────────────────────────────────────────────────────
/// **Armed, the tab reports the adapter; the label lane reports `gpu`.**
///
/// The inverse — that an un-armed tab reports `cpu` and `armed: false` — is asserted
/// in the mega-matrix cell, which runs with no device. Both halves are needed: a
/// field that always says "gpu" and a field that always says "cpu" are equally
/// useless, and only the pair can tell them apart.
#[test]
fn armed_the_tab_names_the_adapter_it_ran_on() {
let _device = device_lock();
let (_rs, adapter) = adapter_or_skip!("adapter");
let s = run_tab(3, |t| t.set_gpu_labels(true)).expect("an armed harness");
let reported = s["gpu"]["adapter"].as_str().expect("the adapter is named");
assert_eq!(reported, adapter, "the tab must report the adapter it actually ran on");
assert_eq!(s["gpu"]["armed"], true);
assert_eq!(s["labels"]["lane"], "gpu", "declined: {:?}", s["labels"]["declined"]);
assert!(s["labels"]["declined"].is_null(), "a lane that took the frame declines for no reason");
assert_eq!(s["pick"]["host_installed"], true, "arming installs the item-8 pick host");
eprintln!("[gfx_v2_device] adapter={adapter} state_json names it: {reported}");
}