facett-map3d/tests/gpu_stats_sampling.rs
//! **The GPU lane must not shade the whole town in software every frame to fill a
//! statistic** — the sampling + single-build proofs for `WgpuVelloBackend::paint`.
//!
//! # The two defects these pin (both found 2026-08-12, perf/v2.1-nav)
//!
//! * `paint` ended with an unconditional `analyze_scene` — a full software
//! transform + clip + shade + sort of EVERY mesh triangle, per frame, purely to
//! fill `RenderStats::visible_triangles`. During a korp drag that pass, not the
//! GPU work, was most of the frame's CPU. The fix samples: a full analysis on the
//! first frame after a mesh swap and every `GPU_ANALYZE_EVERY`-th frame, the
//! cached numbers in between.
//! * The per-frame geometry handoff deep-copied the cached payload (~72 MB at
//! korp's 600k-triangle budget) under a comment claiming "cheap Rc clone". The
//! fix is BY CONSTRUCTION (LAW #5): the cache hands out one `Arc`, and
//! `GpuGeometry` no longer implements `Clone`, so the copy cannot be
//! reintroduced without failing to compile. What a runtime test CAN measure is
//! pinned here: the cache holds the SAME allocation across frames
//! (`Arc::ptr_eq`), i.e. the payload is built once, not per frame.
//!
//! # Direction of red
//!
//! * Re-inline the unconditional `analyze_scene` call (or neuter the refresh gate)
//! and `full_analyzes()` reads one-per-frame — seen red 2026-08-12: 24 analyses
//! for 24 paints against the expected 2.
//! * Rebuild the cache every frame and the `Arc::ptr_eq` proof goes red the same
//! way (seen red 2026-08-12 with the `is_none` gate forced open).
//!
//! No device needed: `paint` registers the egui callback but the stats path is pure
//! CPU, so this runs on the plain kittest harness and asserts real painted frames.
#![cfg(feature = "wgpu")]
use std::cell::{Cell, RefCell};
use std::rc::Rc;
use facett_map3d::backend::GPU_ANALYZE_EVERY;
use facett_map3d::extrude::WaterMesh;
use facett_map3d::{
Map3D, RenderBackend, RenderScene, RenderStats, SceneFlags, Viewport, WgpuVelloBackend,
};
/// Paint `frames` egui frames of one synthetic scene through ONE shared backend and
/// hand back (paints actually run, the last frame's stats). The paint count is
/// measured, not assumed — egui may run extra passes — so the expected analysis
/// count downstream is computed from what actually happened.
fn drive(backend: Rc<WgpuVelloBackend>, frames: usize) -> (u64, RenderStats) {
let mut m = Map3D::synthetic("sampling").with_clock(1.0);
m.spin = false;
let water = WaterMesh::default();
let paints = Rc::new(Cell::new(0u64));
let last: Rc<RefCell<Option<RenderStats>>> = Rc::new(RefCell::new(None));
let (paints_w, last_w) = (paints.clone(), last.clone());
let mut harness = egui_kittest::Harness::builder()
.with_size(egui::vec2(640.0, 480.0))
.build_ui(move |ui| {
let rect = egui::Rect::from_min_size(egui::pos2(20.0, 20.0), egui::vec2(600.0, 440.0));
let scene = RenderScene {
mesh: m.mesh(),
water: &water,
camera: m.render_camera(),
sun: m.sun(),
viewport: Viewport::from_rect(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: None,
overlay_emissive: &[],
};
*last_w.borrow_mut() = Some(backend.paint(&scene, ui));
paints_w.set(paints_w.get() + 1);
});
harness.run_steps(frames);
let stats = last.borrow().clone().expect("at least one frame painted");
(paints.get(), stats)
}
/// How many FULL analyses `n` paints of an unchanged scene must cost: one on the
/// first frame, one every `GPU_ANALYZE_EVERY`-th after — i.e. `⌈n / EVERY⌉`.
fn expected_analyses(paints: u64) -> u64 {
paints.div_ceil(u64::from(GPU_ANALYZE_EVERY))
}
/// **THE RED LINE.** A run of frames over an unchanged scene costs ~1/16th of the
/// full analyses it used to — and the stats it reports are still the real ones.
#[test]
fn an_unchanged_scene_is_analyzed_once_per_sample_window_not_per_frame() {
let backend = Rc::new(WgpuVelloBackend::new());
let (paints, stats) = drive(backend.clone(), GPU_ANALYZE_EVERY as usize + 8);
assert!(paints > u64::from(GPU_ANALYZE_EVERY), "the run spans at least one refresh window");
assert_eq!(
backend.full_analyzes(),
expected_analyses(paints),
"{} paints must cost ⌈{}/{}⌉ full software analyses — one per frame is the defect this pins",
paints,
paints,
GPU_ANALYZE_EVERY,
);
// The sampled numbers are the real analysis, not a fabrication: the framed
// synthetic town projects visible geometry and the lane says who it is.
assert_eq!(stats.backend, "wgpu-vello");
assert!(stats.visible_triangles > 0, "the sampled stats still carry the real projection");
assert_eq!(stats.ink_outside_rect, 0, "…including the bug oracle");
}
/// The geometry payload is built ONCE and every frame hands out the same
/// allocation (`Arc::ptr_eq`). The deep-copy this replaces cannot come back by
/// accident — `GpuGeometry` is not `Clone` — so what this pins at runtime is the
/// build-once half: force the cache to rebuild per frame and it goes red.
#[test]
fn the_cached_payload_is_one_allocation_across_frames() {
let backend = Rc::new(WgpuVelloBackend::new());
let (_, _) = drive(backend.clone(), 3);
let first = backend.cached_geometry_for_test().expect("built on the first paint");
let (_, _) = drive(backend.clone(), 3);
let later = backend.cached_geometry_for_test().expect("still cached");
assert!(
std::sync::Arc::ptr_eq(&first, &later),
"the payload must be the SAME allocation across frames — a fresh pointer means the \
cache is rebuilding (and under the old code, deep-copying) per frame"
);
}
/// A mesh swap (`reset_cache`) drops BOTH caches: the next frame re-analyzes
/// immediately (no 15 frames of the old town's numbers) and rebuilds the payload.
#[test]
fn reset_cache_forces_a_fresh_analysis_and_a_fresh_payload() {
let backend = Rc::new(WgpuVelloBackend::new());
let (paints, _) = drive(backend.clone(), 3);
let before = backend.full_analyzes();
assert_eq!(before, expected_analyses(paints), "short run: the first-frame analysis only");
let old = backend.cached_geometry_for_test().expect("cached");
backend.reset_cache();
let (_, stats) = drive(backend.clone(), 1);
assert_eq!(
backend.full_analyzes(),
before + 1,
"the first frame after a mesh swap re-analyzes NOW — stale stats on a new mesh is the \
lie the reset exists to prevent"
);
assert!(stats.visible_triangles > 0);
let new = backend.cached_geometry_for_test().expect("rebuilt");
assert!(!std::sync::Arc::ptr_eq(&old, &new), "the payload was rebuilt for the new mesh");
}