facett-map3d/tests/gpu_analyze_spike.rs
//! **The 1-in-16 stats refresh must not spike the frame** — the perf proof for
//! `WgpuVelloBackend::paint`'s sampled scene analysis (GFX_V2 V2.9b "Still open").
//!
//! # The defect this pins
//!
//! `paint` refreshed `RenderStats` every [`GPU_ANALYZE_EVERY`]-th frame by running
//! `analyze_scene` — a full software transform + clip + shade + SORT of EVERY mesh
//! triangle — on the GPU lane, purely to fill a statistic. At korp's 600 000-triangle
//! budget that is a periodic multi-millisecond hitch with a regular cadence, which
//! reads as a broken frame pacer where random jitter would read as load.
//!
//! # What is measured
//!
//! Per-frame wall time of `backend.paint` across ≥ 3 analyse periods at a realistic
//! (~600 k) triangle count, refresh frames identified by the `full_analyzes()`
//! counter — not assumed from the cadence; frame 0 excluded (it builds the GPU
//! payload once, a different and legitimate cost). The asserted metric is
//! `worst refresh` against a timed EXACT full analysis of the same scene in the
//! same build — the defect's own magnitude is the yardstick, which keeps the bound
//! meaningful in debug and release alike (a kittest steady frame is ~20 µs of
//! nothing, so "multiples of a neighbour" would flag any bounded work at all).
//! Release builds additionally pin an absolute ceiling per refresh.
//!
//! # Direction of red (non-vacuity)
//!
//! `force_full_analysis_for_test(true)` reintroduces the unbounded full pass, and
//! the SAME instrument must then show the spike — proving the measurement can fail.
//!
//! # Value (a fast wrong number is worse than a slow right one)
//!
//! The statistic keeps consumers (`Map3D::visible_triangles`, the demo HUDs,
//! `robot_ui`), so the estimate is asserted against the exact conformance oracle
//! (`RenderBackend::analyze`, unchanged and byte-exact): within 10 % above the
//! sample cap, byte-equal at or below it, and the overlay counted exactly.
//!
//! No device needed: the stats path of `paint` is pure CPU (the wgpu callback
//! no-ops with no renderer installed), so this runs on the plain kittest harness.
#![cfg(feature = "wgpu")]
use std::cell::RefCell;
use std::rc::Rc;
use std::time::{Duration, Instant};
use facett_map3d::backend::{GPU_ANALYZE_EVERY, GPU_ANALYZE_SAMPLE_MAX};
use facett_map3d::extrude::{Mesh3, WaterMesh};
use facett_map3d::{
Map3D, RenderBackend, RenderScene, RenderStats, SceneFlags, Viewport, WgpuVelloBackend,
};
/// The refresh must cost at most `1/MIN_COLLAPSE` of the full analysis it
/// replaced, measured in the SAME build — the build-invariant spike bound (a
/// kittest steady frame is ~20 µs of nothing, so "multiples of a neighbour" would
/// flag ANY bounded work; the defect's magnitude is the full pass, so that is the
/// yardstick). At [`GPU_ANALYZE_SAMPLE_MAX`] = 4096 of ~600 k triangles the
/// bounded refresh is ~1/146 of the full pass by construction — 50 leaves margin
/// without ever excusing a real spike.
const MIN_COLLAPSE: f64 = 50.0;
/// Release-build ceiling for one refresh: an eighth of a 60 fps frame budget.
/// Asserted only without `debug_assertions` (a debug kernel is ~100× slower and
/// would pin a number that means nothing for the shipped build).
const MAX_REFRESH_RELEASE: Duration = Duration::from_micros(2083);
/// Tile the synthetic town's extruded mesh until it reaches `target_tris` — a
/// realistic korp-budget scene built from REAL extruder output (normals, colours,
/// winding all genuine), assembled by concatenation so the fixture costs
/// milliseconds, not an OSM extrusion. Small jitter keeps depths distinct while
/// every copy stays inside the framed design box.
fn tiled_mesh(seed: &Mesh3, target_tris: usize) -> Mesh3 {
let reps = target_tris.div_ceil(seed.triangle_count()).max(1);
let mut m = Mesh3::default();
m.positions.reserve(seed.positions.len() * reps);
m.normals.reserve(seed.normals.len() * reps);
m.building.reserve(seed.building.len() * reps);
m.colors.reserve(seed.colors.len() * reps);
m.indices.reserve(seed.indices.len() * reps);
for r in 0..reps {
let base = m.positions.len() as u32;
let dx = ((r % 7) as f32 - 3.0) * 0.02;
let dz = ((r % 5) as f32 - 2.0) * 0.02;
m.positions.extend(seed.positions.iter().map(|p| [p[0] + dx, p[1], p[2] + dz]));
m.normals.extend_from_slice(&seed.normals);
m.building.extend_from_slice(&seed.building);
m.colors.extend_from_slice(&seed.colors);
m.indices.extend(seed.indices.iter().map(|i| i + base));
}
m
}
/// The first `cap` triangles of `seed` (indices still address the seed's vertex
/// arrays) — a mesh at or below the sample cap, for the byte-exact small path.
fn truncated_mesh(seed: &Mesh3, cap: usize) -> Mesh3 {
let n = seed.triangle_count().min(cap);
Mesh3 {
positions: seed.positions.clone(),
normals: seed.normals.clone(),
colors: seed.colors[..n].to_vec(),
indices: seed.indices[..n * 3].to_vec(),
building: seed.building.clone(),
}
}
/// One timed frame: how long `paint` took and the `full_analyzes()` reading after.
struct Frame {
dur: Duration,
analyzes: u64,
stats: RenderStats,
}
/// The widget rect every scene in this file projects into (matches the
/// `gpu_stats_sampling` harness geometry).
fn widget_rect() -> egui::Rect {
egui::Rect::from_min_size(egui::pos2(20.0, 20.0), egui::vec2(600.0, 440.0))
}
/// Build the frame's `RenderScene` over `mesh` (+ optional overlay) using the
/// deterministic synthetic town's camera/sun (spin off, clock pinned — the camera
/// is a constant, so the standalone `analyze` in the value test sees the exact
/// scene the harness painted).
fn scene_of<'a>(
m: &'a Map3D,
mesh: &'a Mesh3,
water: &'a WaterMesh,
overlay: Option<&'a Mesh3>,
) -> RenderScene<'a> {
RenderScene {
mesh,
water,
camera: m.render_camera(),
sun: m.sun(),
viewport: Viewport::from_rect(widget_rect()),
bg: egui::Color32::from_rgb(20, 24, 40),
flags: SceneFlags::default(),
sky_lut: None,
clock_s: 1.0,
du_per_m: m.design_units_per_metre(),
overlay,
overlay_emissive: &[],
}
}
/// The deterministic camera/sun donor every test shares.
fn donor() -> Map3D {
let mut m = Map3D::synthetic("spike").with_clock(1.0);
m.spin = false;
m
}
/// Paint `frames` egui frames of one scene through `backend`, timing each
/// `paint` call and recording `full_analyzes()` after it — the refresh frames are
/// identified from the counter, not assumed from the cadence.
fn drive_timed(
backend: Rc<WgpuVelloBackend>,
mesh: Rc<Mesh3>,
overlay: Option<Rc<Mesh3>>,
frames: usize,
) -> Vec<Frame> {
let m = donor();
let water = WaterMesh::default();
let log: Rc<RefCell<Vec<Frame>>> = Rc::new(RefCell::new(Vec::new()));
let log_w = log.clone();
let mut harness = egui_kittest::Harness::builder()
.with_size(egui::vec2(640.0, 480.0))
.build_ui(move |ui| {
let scene = scene_of(&m, &mesh, &water, overlay.as_deref());
let t0 = Instant::now();
let stats = backend.paint(&scene, ui);
let dur = t0.elapsed();
log_w.borrow_mut().push(Frame { dur, analyzes: backend.full_analyzes(), stats });
});
harness.run_steps(frames);
drop(harness);
Rc::try_unwrap(log).unwrap_or_else(|_| panic!("harness dropped its log handle")).into_inner()
}
/// `(worst refresh, median steady)`, frame 0 excluded (the one-time GPU payload
/// build). Also prints the whole per-frame table so the numbers are on record.
fn frame_costs(log: &[Frame], label: &str) -> (Duration, Duration) {
let mut refresh: Vec<Duration> = Vec::new();
let mut steady: Vec<Duration> = Vec::new();
println!("── {label}: per-frame paint time ({} frames) ──", log.len());
for i in 1..log.len() {
let is_refresh = log[i].analyzes > log[i - 1].analyzes;
if is_refresh {
refresh.push(log[i].dur);
} else {
steady.push(log[i].dur);
}
println!(
" frame {i:3} {:>9.1} µs {}",
log[i].dur.as_secs_f64() * 1e6,
if is_refresh { "← stats refresh" } else { "" }
);
}
assert!(refresh.len() >= 2, "the run must span ≥ 2 refresh frames (got {})", refresh.len());
assert!(steady.len() >= 8, "need a steady-frame baseline (got {})", steady.len());
steady.sort();
let median = steady[steady.len() / 2];
let worst = *refresh.iter().max().expect("non-empty");
println!(
" {label}: steady median {:.1} µs · worst refresh {:.1} µs ({:.1}× steady)",
median.as_secs_f64() * 1e6,
worst.as_secs_f64() * 1e6,
worst.as_secs_f64() / median.as_secs_f64().max(1e-9),
);
(worst, median)
}
/// One timed EXACT full analysis of the scene — the defect's own magnitude on this
/// build, the yardstick both spike tests measure against.
fn timed_full_analysis(mesh: &Mesh3) -> Duration {
let m = donor();
let water = WaterMesh::default();
let backend = WgpuVelloBackend::new();
let t0 = Instant::now();
let stats = backend.analyze(&scene_of(&m, mesh, &water, None));
let dur = t0.elapsed();
assert!(stats.visible_triangles > 0, "the yardstick analysis saw the town");
println!(" full exact analysis of the same scene: {:.1} µs", dur.as_secs_f64() * 1e6);
dur
}
/// **THE RED LINE.** At a korp-budget triangle count, the periodic stats refresh
/// must have collapsed to a bounded fraction of the full analysis it replaced —
/// before the bounded sample the refresh WAS the full pass (ratio 1), measured red
/// 2026-08-25: refresh frames 3.25–4.25 s (debug) against ~20 µs neighbours,
/// 217 715× a steady frame, at exactly frames 16/32/48. In release the bound also
/// pins an absolute ceiling: one refresh ≤ 1/8 of a 60 fps frame budget.
#[test]
fn the_periodic_stats_refresh_must_not_spike_the_frame() {
let seed = donor();
let mesh = Rc::new(tiled_mesh(seed.mesh(), 600_000));
println!("fixture: {} triangles ({} verts)", mesh.triangle_count(), mesh.vertex_count());
let full = timed_full_analysis(&mesh);
let backend = Rc::new(WgpuVelloBackend::new());
let log = drive_timed(backend, mesh, None, GPU_ANALYZE_EVERY as usize * 3 + 2);
let (worst, _steady) = frame_costs(&log, "sampled");
assert!(
worst.as_secs_f64() * MIN_COLLAPSE <= full.as_secs_f64(),
"the 1-in-{GPU_ANALYZE_EVERY} stats refresh costs {:.1} µs — not even {MIN_COLLAPSE}× \
below the {:.1} µs full pass it was supposed to replace. The rhythmic hitch is back.",
worst.as_secs_f64() * 1e6,
full.as_secs_f64() * 1e6,
);
#[cfg(not(debug_assertions))]
assert!(
worst <= MAX_REFRESH_RELEASE,
"release build: one refresh costs {:.1} µs — over an eighth of a 60 fps frame budget \
({:.1} µs); that is a visible hitch, bounded or not",
worst.as_secs_f64() * 1e6,
MAX_REFRESH_RELEASE.as_secs_f64() * 1e6,
);
#[cfg(debug_assertions)]
let _ = MAX_REFRESH_RELEASE;
}
/// **NON-VACUITY.** Reintroduce the unbounded full pass (test-only switch) and the
/// SAME instrument must fail its own bound — the refresh IS the full pass again,
/// so the `MIN_COLLAPSE` margin cannot hold. The measurement can fail, so its
/// green means something.
#[test]
fn non_vacuity_the_forced_full_pass_still_spikes() {
let seed = donor();
let mesh = Rc::new(tiled_mesh(seed.mesh(), 600_000));
let full = timed_full_analysis(&mesh);
let backend = Rc::new(WgpuVelloBackend::new());
backend.force_full_analysis_for_test(true);
let log = drive_timed(backend, mesh, None, GPU_ANALYZE_EVERY as usize * 2 + 2);
let (worst, _steady) = frame_costs(&log, "forced-full");
assert!(
worst.as_secs_f64() * MIN_COLLAPSE > full.as_secs_f64(),
"the forced full per-refresh analysis measures {:.1} µs against a {:.1} µs full pass — \
the spike instrument cannot see the defect it exists to catch, so its green proves \
nothing",
worst.as_secs_f64() * 1e6,
full.as_secs_f64() * 1e6,
);
}
/// **VALUE.** The statistic kept its consumers, so the estimate must track the
/// exact oracle: within 10 % of `analyze` (unchanged, byte-exact) above the cap,
/// and the overlay's triangles counted EXACTLY (the sampled base is identical with
/// and without the overlay, so the difference is the overlay's true visible count).
#[test]
fn the_estimate_tracks_the_exact_count_and_counts_the_overlay_exactly() {
let seed = donor();
let mesh = Rc::new(tiled_mesh(seed.mesh(), 600_000));
let overlay = Rc::new(truncated_mesh(seed.mesh(), 400));
let water = WaterMesh::default();
let m = donor();
let backend = Rc::new(WgpuVelloBackend::new());
let exact_base = backend.analyze(&scene_of(&m, &mesh, &water, None)).visible_triangles;
let exact_with =
backend.analyze(&scene_of(&m, &mesh, &water, Some(&overlay))).visible_triangles;
assert!(exact_base > 0, "the framed town projects visible geometry");
assert!(exact_with > exact_base, "the overlay adds visible triangles");
let est_base = drive_timed(Rc::new(WgpuVelloBackend::new()), mesh.clone(), None, 2)
.last()
.expect("frames painted")
.stats
.visible_triangles;
let est_with =
drive_timed(Rc::new(WgpuVelloBackend::new()), mesh.clone(), Some(overlay.clone()), 2)
.last()
.expect("frames painted")
.stats
.visible_triangles;
let err = (est_base as f64 - exact_base as f64).abs() / exact_base as f64;
println!(
"estimate {est_base} vs exact {exact_base} → {:.2}% error · overlay est Δ{} vs exact Δ{}",
err * 100.0,
est_with as i64 - est_base as i64,
exact_with - exact_base,
);
assert!(
err <= 0.10,
"the sampled estimate ({est_base}) strays {:.1}% from the exact count ({exact_base}) — \
a fast wrong number is worse than a slow right one",
err * 100.0
);
assert_eq!(
est_with - est_base,
exact_with - exact_base,
"the overlay must be counted exactly, not estimated — it rides outside the sample"
);
}
/// **VALUE, small path.** At or below [`GPU_ANALYZE_SAMPLE_MAX`] triangles the
/// paint-lane stats stay BYTE-EXACT (no estimate, no meaning change) — which is
/// why every pre-existing small-scene test is untouched by the sampling.
#[test]
fn at_or_below_the_cap_the_painted_stats_are_byte_exact() {
let seed = donor();
let small = Rc::new(truncated_mesh(seed.mesh(), GPU_ANALYZE_SAMPLE_MAX));
assert!(small.triangle_count() <= GPU_ANALYZE_SAMPLE_MAX);
let water = WaterMesh::default();
let m = donor();
let backend = Rc::new(WgpuVelloBackend::new());
let exact = backend.analyze(&scene_of(&m, &small, &water, None));
let painted =
drive_timed(Rc::new(WgpuVelloBackend::new()), small.clone(), None, 2).pop().unwrap().stats;
assert!(exact.visible_triangles > 0);
assert_eq!(painted.visible_triangles, exact.visible_triangles);
assert_eq!(painted.lit_fraction, exact.lit_fraction);
assert_eq!(painted.ink_outside_rect, exact.ink_outside_rect);
assert_eq!(painted.max_extent_px, exact.max_extent_px);
}