facett-map3d/tests/gpu_clock.rs
//! **perf/v2.3 — the GPU tail of korp's mousewheel→GPU TRACE line, 3D lane.**
//!
//! Two observables, read through the ONE stats seam
//! (`facett_core::render::lane::clock_stats_of` /
//! `facett_map3d::gpu::lane_clock_stats`):
//!
//! * **Upload wall clock** — `Map3dRenderer::upload` notes `{last_upload_us,
//! last_upload_bytes, uploads_total}` per replace-on-change event. The byte count
//! is asserted with EQUALITY against the payload fed (mesh vertices + water grid +
//! water indices; LAW 2 — forced red recorded below), and the generation gate is
//! asserted the other way round: re-sending an unchanged payload is NOT an upload
//! event.
//! * **In-GPU pass time** — the TIMESTAMP_QUERY bracket `Map3dPaintCallback::prepare`
//! wraps around everything it records (shadow → G-buffer/SSAO → reflection → sky →
//! main → water → post), driven here through the REAL egui path: a kittest wgpu
//! harness with the renderer installed, exactly `gpu_depth`'s posture. The
//! kittest-default device requests NO features, which is the natural
//! forced-unavailable arm (`gpu_timestamps = Some(false)`, `last_gpu_pass_us =
//! None`, never `0`); a second render state requests the features where the
//! adapter has them and a sample must land `> 0` within a few frames.
//!
//! Adapters are printed for every arm — a number measured on llvmpipe must say so.
//!
//! Run: `cargo test -p facett-map3d --features wgpu --test gpu_clock -- --nocapture`
#![cfg(feature = "wgpu")]
use std::sync::Mutex;
use egui_kittest::wgpu::{create_render_state, default_wgpu_setup, WgpuTestRenderer};
use egui_kittest::Harness;
use facett_core::render::gpu::PASS_CLOCK_FEATURES;
use facett_core::Facet as _;
use facett_map::Way;
use facett_map3d::extrude::{Mesh3, WaterMesh};
use facett_map3d::gpu::{
build_gpu_geometry, build_gpu_geometry_with_water, lane_clock_stats, lane_name,
proof_wgpu_setup, Map3dRenderer, Vertex,
};
use facett_map3d::{Map3D, ViewPreset};
/// Both tests write the same process-global lane record; serialised so one test's
/// uploads cannot smear the other's byte-count deltas.
static LANE_LOCK: Mutex<()> = Mutex::new(());
/// One render state, adapter printed (the tree's convention — a number measured on a
/// CPU rasteriser and reported as a GPU number is exactly the kind of claim this
/// suite keeps having to retract).
fn render_state(setup: egui_wgpu::WgpuSetup) -> egui_wgpu::RenderState {
let rs = create_render_state(setup, Default::default());
let info = rs.adapter.get_info();
println!(" adapter: {} [{:?}/{:?}]", info.name, info.device_type, info.backend);
rs
}
/// A one-quad mesh whose bytes are decided by `seed` (the `gpu_geometry_swap`
/// fixture shape — small on purpose; the byte accounting is exact regardless of
/// size).
fn mesh(seed: f32) -> Mesh3 {
let mut m = Mesh3::default();
let up = [0.0, 1.0, 0.0];
for p in [
[-0.5 + seed, 0.0, -0.5],
[0.5 + seed, 0.0, -0.5],
[0.5 + seed, 0.0, 0.5],
[-0.5 + seed, 0.0, 0.5],
] {
m.positions.push(p);
m.normals.push(up);
}
m.indices.extend_from_slice(&[0, 1, 2, 0, 2, 3]);
let c = egui::Color32::from_rgb((seed * 40.0) as u8 + 10, 80, 200);
m.colors.push(c);
m.colors.push(c);
m
}
/// The exact bytes `upload` hands the device: the expanded mesh vertices, plus the
/// water grid's positions (3×f32 each — `WaterVertex` is position-only) and u32
/// indices. Uniform/target allocations are capacity, not payload.
fn payload_bytes(geo: &facett_map3d::gpu::GpuGeometry) -> u64 {
(geo.vertices.len() * std::mem::size_of::<Vertex>()
+ geo.water_positions.len() * std::mem::size_of::<[f32; 3]>()
+ geo.water_indices.len() * std::mem::size_of::<u32>()) as u64
}
/// **Upload events carry wall-time and the EXACT payload byte count — and the
/// generation gate keeps a re-sent payload from counting as one.**
///
/// Forced red (LAW 2), 2026-08-12, on the kittest default adapter: with the
/// `uploaded_bytes += (water verts + indices)` line removed from
/// `Map3dRenderer::upload` the equality failed verbatim:
///
/// ```text
/// assertion `left == right` failed: the upload event's byte count equals the payload fed
/// left: 240
/// right: 312
/// ```
///
/// i.e. the accounting silently dropped the water grid — the class of hole the
/// equality (not `> 0`) assertion exists to catch. Restored, both sides read 312.
#[test]
fn an_upload_event_notes_wall_time_and_the_exact_bytes_fed() {
let _g = LANE_LOCK.lock().unwrap();
let rs = render_state(proof_wgpu_setup().unwrap_or_else(default_wgpu_setup));
let mut r = Map3dRenderer::new(&rs.device, wgpu::TextureFormat::Rgba8UnormSrgb);
let water = WaterMesh {
positions: vec![[-1.0, 0.0, -1.0], [1.0, 0.0, -1.0], [1.0, 0.0, 1.0], [-1.0, 0.0, 1.0]],
indices: vec![0, 1, 2, 0, 2, 3],
};
let geo = build_gpu_geometry_with_water(&mesh(0.0), &water);
let expected = payload_bytes(&geo);
assert!(
geo.water_positions.len() == 4 && expected > 0,
"fixture sanity: water must be part of the payload or the accounting proof is partial"
);
let before = lane_clock_stats();
r.upload_for_test(&rs.device, &geo);
let after = lane_clock_stats();
assert_eq!(after.uploads_total, before.uploads_total + 1, "one upload event counted");
assert_eq!(
after.last_upload_bytes, expected,
"the upload event's byte count equals the payload fed"
);
assert!(after.last_upload_us > 0, "a real upload takes measurable wall time");
// The generation gate: the SAME stamp re-sent (the every-frame shape) moves no
// bytes and must not count — `uploads_total` counts re-bakes, not frames.
r.upload_for_test(&rs.device, &geo);
assert_eq!(
lane_clock_stats().uploads_total,
after.uploads_total,
"re-sending an unchanged generation is not an upload event"
);
// A REBUILT scene is one.
let geo2 = build_gpu_geometry(&mesh(3.0));
r.upload_for_test(&rs.device, &geo2);
let last = lane_clock_stats();
assert_eq!(last.uploads_total, after.uploads_total + 1, "a new generation counts");
assert_eq!(last.last_upload_bytes, payload_bytes(&geo2), "and carries ITS byte count");
}
/// The adversarial two-building scene from `gpu_depth`, GPU lane on — the proven
/// posture in which `Map3dPaintCallback::prepare` really runs under kittest.
fn scene() -> Map3D {
fn building(id: i64, ring: &[(f64, f64)], col: egui::Color32) -> Way {
let mut r: Vec<(f64, f64)> = ring.to_vec();
if let Some(&f) = r.first() {
r.push(f);
}
Way::filled(id, "building", r, col, egui::Color32::from_rgb(20, 20, 20), 1.0)
}
let far = building(
1,
&[(9.50, 47.12), (9.56, 47.12), (9.56, 47.15), (9.50, 47.15)],
egui::Color32::from_rgb(40, 80, 240),
);
let near = building(
2,
&[(9.52, 47.10), (9.54, 47.10), (9.54, 47.115), (9.52, 47.115)],
egui::Color32::from_rgb(240, 50, 40),
);
let mut m = Map3D::from_ways("clock", &[near, far], 0.35);
m.spin = false;
m.set_clock(0.0);
m.view_preset(ViewPreset::Front);
m.cam.goal_elevation = 0.18;
m.cam.goal_distance = 2.4;
m.cam.goal_target = facett_map3d::V3::new(0.0, 0.15, 0.0);
m.cam.settle();
m.with_gpu(true)
}
/// Render `frames` real egui frames through the installed renderer — each
/// `harness.render()` runs the full egui-wgpu pass, including
/// `Map3dPaintCallback::prepare` (where the clock lives) and the submit that lets
/// the readback ring advance.
fn drive(rs: egui_wgpu::RenderState, frames: usize) {
facett_map3d::gpu::install_renderer(&rs);
let mut view = scene();
let mut harness = Harness::builder()
.with_size(egui::vec2(420.0, 420.0))
.renderer(WgpuTestRenderer::from_render_state(rs))
.build_ui(move |ui| {
facett_core::set_theme(ui.ctx(), facett_core::Theme::sci_fi());
view.ui(ui);
});
for _ in 0..frames {
harness.run_steps(1);
let _ = harness.render().expect("wgpu render");
}
}
/// **The two honesty arms of the 3D pass clock, through the real egui path.**
///
/// Forced red (LAW 2), 2026-08-12: with the `clock.begin(...)` call removed from
/// `Map3dPaintCallback::prepare` (probe never runs, timestamps never written) the
/// featureless arm failed verbatim:
///
/// ```text
/// the kittest-default device requests no features, so the probe must record Some(false)
/// left: None
/// right: Some(false)
/// ```
///
/// — the flag stayed `None` ("never probed"), proving the assertion tracks the
/// probe actually running inside `prepare`, not just the enum's default.
#[test]
fn pass_time_is_none_without_the_feature_and_micros_with_it() {
let _g = LANE_LOCK.lock().unwrap();
// ── Arm 1: the kittest default — its DeviceDescriptor requests NO features, so
// even an adapter that has timestamps yields a device that cannot measure. The
// natural "device requested without the feature" arm.
println!(" featureless arm:");
drive(render_state(default_wgpu_setup()), 4);
assert!(
facett_core::render::lane::frames_of(lane_name()) > 0,
"precondition: prepare must have encoded frames, or nothing was measured at all"
);
let s = lane_clock_stats();
assert_eq!(
s.gpu_timestamps,
Some(false),
"the kittest-default device requests no features, so the probe must record Some(false)"
);
assert_eq!(s.last_gpu_pass_us, None, "an unmeasurable device reports None, not 0");
// ── Arm 2: the features requested where the adapter carries them.
let mut setup = proof_wgpu_setup().unwrap_or_else(default_wgpu_setup);
if let egui_wgpu::WgpuSetup::CreateNew(create) = &mut setup {
create.device_descriptor = std::sync::Arc::new(|adapter: &wgpu::Adapter| {
wgpu::DeviceDescriptor {
label: Some("map3d_gpu_clock"),
required_features: adapter.features() & PASS_CLOCK_FEATURES,
required_limits: wgpu::Limits {
max_texture_dimension_2d: 8192,
..wgpu::Limits::default()
},
..Default::default()
}
});
}
println!(" real-timestamp arm:");
let rs = render_state(setup);
if !rs.device.features().contains(PASS_CLOCK_FEATURES) {
println!(
" [honest skip] this adapter does not offer TIMESTAMP_QUERY(+INSIDE_ENCODERS) — \
the real-timestamp arm cannot run here (the featureless arm above still proved None)"
);
return;
}
// A fresh renderer instance (fresh probe) against the featured device; up to 12
// real frames for the non-blocking ring to surface a sample.
drive(rs, 12);
let s = lane_clock_stats();
assert_eq!(s.gpu_timestamps, Some(true), "the probe on a featured device says real");
let us = s
.last_gpu_pass_us
.unwrap_or_else(|| panic!("a pass sample must land within 12 frames: {s:?}"));
assert!(us > 0, "a resolved sample is strictly positive by construction");
println!(" 3D pass window (shadow→post): {us} µs");
}