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

nordisk/facett

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