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