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

nordisk/facett

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}");
}