facett-map/tests/gpu_clock.rs
//! **perf/v2.3 — the GPU tail of korp's mousewheel→GPU TRACE line, 2D lane.**
//!
//! Two observables, both read through the ONE stats seam
//! (`facett_core::render::lane::clock_stats_of` / `facett_map::gpu::lane_clock_stats`):
//!
//! * **Upload wall clock** — `GpuMapRenderer::upload` notes `{last_upload_us,
//! last_upload_bytes, uploads_total}` per re-bake. The byte count is asserted with
//! EQUALITY against the payload fed (LAW 2: an accounting that silently drops a
//! stream — say, the pretty-line meta — moves this number and fails here; forced
//! red recorded in the test doc below).
//! * **In-GPU pass time** — TIMESTAMP_QUERY around the cull compute pass via
//! `run_cull_clocked`, the same body `MapPaintCallback::prepare` calls. Where the
//! device carries the feature the sample must land `> 0` within a few frames; a
//! device REQUESTED WITHOUT the feature (the forced arm below — every kittest
//! default harness has this shape) must report `None` and `gpu_timestamps =
//! Some(false)`. Never `0`: a faked zero is indistinguishable from a free pass.
//!
//! Adapters are printed for every arm — a number measured on llvmpipe must say so.
//!
//! Run: `cargo test -p facett-map --features wgpu --test gpu_clock -- --nocapture`
#![cfg(feature = "wgpu")]
use std::sync::Mutex;
use facett_map::gpu::{
build_gpu_geometry, lane_clock_stats, lane_name, ways_origin, GpuGeometry, GpuMapRenderer,
GpuWayMeta, LineVertex, Vertex,
};
use facett_map::Way;
mod common;
/// Both tests below write the same process-global lane record; they serialise on
/// this so a byte-count delta cannot be smeared by the other test's upload.
static LANE_LOCK: Mutex<()> = Mutex::new(());
/// A small mixed fixture: three line roads (two tiers) — enough to populate the
/// hairline, pretty-line and meta streams the byte accounting must cover.
fn roads() -> Vec<Way> {
let c = egui::Color32::from_rgb(0xE8, 0xB0, 0x30);
let mut ways = vec![
Way::line(1, "road", vec![(9.00, 47.00), (9.05, 47.02), (9.10, 47.00)], c, 6.0),
Way::line(2, "road", vec![(10.00, 47.00), (10.05, 47.02), (10.10, 47.00)], c, 5.0),
Way::line(3, "road", vec![(11.00, 47.00), (11.10, 47.00)], c, 4.0),
];
ways[2].style.lod = facett_map::Lod::City;
ways
}
fn geometry() -> GpuGeometry {
let ways = roads();
let origin = ways_origin(ways.iter()).expect("non-empty fixture has an origin");
build_gpu_geometry(ways.iter().map(|w| (w, w.style.lod as u32)), origin)
}
/// The exact bytes `upload` hands the device: every stream's vertices + per-way
/// meta (the chunk partition tiles the input, and each span's rebased meta keeps the
/// span's way count, so chunked totals equal these flat totals). Compact/indirect
/// buffers are capacity, not payload, and are deliberately absent.
fn payload_bytes(geo: &GpuGeometry) -> u64 {
use std::mem::size_of;
let v = size_of::<Vertex>();
let m = size_of::<GpuWayMeta>();
// The ring OUTLINES joined the 48 B `LineVertex` streams with `mapgpu-1` — they are
// quads now, not `LineList` pairs — so they are counted with the roads, not with the
// 24 B triangle stream.
((geo.vertices.len() + geo.fills.tri_vertices.len()) * v
+ (geo.way_meta.len() + geo.fills.tri_meta.len() + geo.fills.outline_meta.len() + geo.line_meta.len()) * m
+ (geo.line_vertices.len() + geo.fills.outline_vertices.len()) * size_of::<LineVertex>()) as u64
}
/// **Upload events carry wall-time and the EXACT payload byte count.**
///
/// Forced red (LAW 2), 2026-08-12, RTX 4090 [DiscreteGpu/Vulkan]: with
/// `+ line_payload_bytes` removed from `GpuMapRenderer::upload`'s sum the equality
/// failed verbatim:
///
/// ```text
/// assertion `left == right` failed: the upload event's byte count equals the payload fed
/// left: 336
/// right: 1872
/// ```
///
/// i.e. the accounting silently dropped the pretty-line stream (the fixture's quads +
/// their meta are most of the payload) — exactly the class of hole the equality
/// (not `> 0`) assertion exists to catch. Restored, both sides read 1872.
#[test]
fn an_upload_event_notes_wall_time_and_the_exact_bytes_fed() {
let _g = LANE_LOCK.lock().unwrap();
let Some((device, queue)) = common::device_or_skip(
"gpu_clock_upload",
"upload stats note µs>0 and the exact payload byte count",
) else {
return;
};
let mut renderer = GpuMapRenderer::new(&device, wgpu::TextureFormat::Rgba8UnormSrgb);
let geo = geometry();
let expected = payload_bytes(&geo);
assert!(expected > 0, "fixture sanity: an empty payload proves nothing");
let before = lane_clock_stats();
renderer.upload_for_test(&device, &queue, &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");
// A re-bake is a SECOND event (this lane re-uploads whenever a payload arrives —
// the change gate lives host-side in `MapFacet`'s geometry signature).
renderer.upload_for_test(&device, &queue, &geo);
let again = lane_clock_stats();
assert_eq!(again.uploads_total, after.uploads_total + 1, "upload events accumulate");
assert_eq!(again.last_upload_bytes, expected, "same payload, same byte count");
}
/// The matrix check name the real-timestamp arm files itself (and its skip) under.
const TS_ARM: &str = "pass_time_is_micros_with_the_timestamp_feature";
/// What that arm proves, for whoever reads a SKIPPED row without opening this file.
const TS_DETAIL: &str =
"an in-GPU cull pass sampled through TIMESTAMP_QUERY lands a non-zero microsecond \
reading — the only arm that can tell a measured pass from a faked zero";
/// A device requested WITH the pass-clock features, where the adapter has them.
/// Prints the adapter either way — the honest-reporting rule of this suite.
///
/// **Every `None` here is announced.** Both exits used to be silent — `.ok()?` on the
/// adapter, and a bare `println!` + `return None` when the adapter lacked the feature —
/// and the caller's `let … else { println!(); return; }` then handed libtest an `ok`. On
/// a host without TIMESTAMP_QUERY the whole real-timestamp arm was a PASS that had never
/// touched a queue, which is `granska-25` one capability to the right of where it was
/// fixed. `common::skip` puts a non-passing row in the matrix and panics under
/// `CI` / `FACETT_REQUIRE_GPU`.
fn feature_device() -> Option<(wgpu::Device, wgpu::Queue, String)> {
// **The one-device-at-a-time turn, taken here too.** This is the ONE device bring-up
// in this crate's tests that cannot go through `common::headless_device`:
// `headless_device` asks for `downlevel_defaults` and no optional features on
// purpose, and this file needs `PASS_CLOCK_FEATURES` (TIMESTAMP_QUERY). So it takes
// the same turn `headless_device` takes, for the same measured reason — two device
// bring-ups on two libtest threads SIGSEGV this host (oden, 2026-08-27:
// `line_gpu` 1/8, `rail_tier_pixels` 2/5, `place_label_tier_pixels` 3/5).
let _turn = common::one_gpu::hold();
// The same hook `common::headless_device` carries, for the same reason: on the only
// boxes where anyone would notice this branch is wrong, the branch is unreachable.
// `FACETT_FORCE_NO_GPU` takes the whole file down the no-adapter path; the arm-1 gate
// fires first there, so this arm needs its OWN switch to be reachable at all — and an
// untestable skip path is what this whole edit is about. With it,
// `FACETT_FORCE_NO_TIMESTAMPS=1 CI=1 cargo test --test gpu_clock` is the red.
for v in ["FACETT_FORCE_NO_GPU", "FACETT_FORCE_NO_TIMESTAMPS"] {
if std::env::var(v).is_ok() {
common::skip(TS_ARM, &format!("{v}=1 forced the no-timestamp path"), TS_DETAIL);
return None;
}
}
let instance = wgpu::Instance::default();
let Ok(adapter) = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::HighPerformance,
compatible_surface: None,
force_fallback_adapter: false,
apply_limit_buckets: false,
})) else {
common::skip(TS_ARM, "no wgpu adapter on this host", TS_DETAIL);
return None;
};
let info = adapter.get_info();
let name = format!("{} [{:?}/{:?}]", info.name, info.device_type, info.backend);
let wanted = facett_core::render::gpu::PASS_CLOCK_FEATURES;
if !adapter.features().contains(wanted) {
common::skip(
TS_ARM,
&format!("adapter {name} carries no TIMESTAMP_QUERY / {wanted:?}"),
TS_DETAIL,
);
return None;
}
let dev = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
label: Some("gpu_clock_ts"),
required_features: wanted,
required_limits: wgpu::Limits::downlevel_defaults(),
..Default::default()
}));
let Ok((device, queue)) = dev else {
common::skip(
TS_ARM,
&format!("adapter {name} advertises the timestamp features but refused the device"),
TS_DETAIL,
);
return None;
};
Some((device, queue, name))
}
/// Drive the REAL clocked cull (the body `prepare` calls) once and wait the
/// submission out — test-only blocking; the live path never waits.
fn clocked_cull_frame(renderer: &mut GpuMapRenderer, device: &wgpu::Device, queue: &wgpu::Queue) {
// One call IS one submitted frame here, so the clock's frame number is a call
// count (GG #37: `begin` needs it to tell a second pane from a new frame).
static FRAME: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
let frame = FRAME.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + 1;
let mut enc =
device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("clock_frame") });
renderer.run_cull_clocked(device, queue, &mut enc, [-1.0e9, -1.0e9, 1.0e9, 1.0e9], 2, 0.0, frame);
queue.submit(Some(enc.finish()));
device.poll(wgpu::PollType::wait_indefinitely()).ok();
}
/// **The two honesty arms of the pass clock.**
///
/// Forced red (LAW 2), 2026-08-12, RTX 4090 [DiscreteGpu/Vulkan]: with
/// `clock.end(encoder)` removed from `run_cull_clocked` (timestamps written, never
/// resolved) the featured arm failed verbatim after 20 frames:
///
/// ```text
/// with TIMESTAMP_QUERY present a pass sample must land within 20 frames:
/// LaneClockStats { last_upload_us: 615, last_upload_bytes: 1872, uploads_total: 2,
/// last_gpu_pass_us: None, gpu_timestamps: Some(true) }
/// ```
///
/// — and the featureless arm (arm 1) still passed inside the same run, proving the
/// two arms are independent.
#[test]
fn pass_time_is_none_without_the_feature_and_micros_with_it() {
let _g = LANE_LOCK.lock().unwrap();
// ── Arm 1: a device requested WITHOUT the feature (the kittest-default shape,
// forced deliberately — on this box the adapter may well support timestamps,
// which is the point: the verdict must track the DEVICE, not the adapter).
let Some((device, queue)) = common::device_or_skip(
"gpu_clock_no_feature",
"a device without TIMESTAMP_QUERY reports None, never 0",
) else {
return;
};
{
let mut renderer = GpuMapRenderer::new(&device, wgpu::TextureFormat::Rgba8UnormSrgb);
renderer.upload_for_test(&device, &queue, &geometry());
clocked_cull_frame(&mut renderer, &device, &queue);
let s = lane_clock_stats();
assert_eq!(
s.gpu_timestamps,
Some(false),
"a featureless device must record an honest 'cannot measure'"
);
assert_eq!(
s.last_gpu_pass_us, None,
"an adapter/device without the feature reports None, not 0"
);
}
// ── Arm 2: the feature requested and granted — a real sample must land.
// `feature_device` has already filed the SKIP row and failed a CI host by the time
// it hands back `None`; the `println!` that used to stand here was the whole of the
// report, and libtest wrote `ok` under it.
let Some((device, queue, adapter)) = feature_device() else { return };
println!(" real-timestamp arm on: {adapter}");
let mut renderer = GpuMapRenderer::new(&device, wgpu::TextureFormat::Rgba8UnormSrgb);
renderer.upload_for_test(&device, &queue, &geometry());
const N: usize = 20;
for _ in 0..N {
clocked_cull_frame(&mut renderer, &device, &queue);
if lane_clock_stats().last_gpu_pass_us.is_some() {
break;
}
}
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!("with TIMESTAMP_QUERY present a pass sample must land within {N} frames: {s:?}")
});
assert!(us > 0, "a resolved sample is strictly positive by construction");
println!(
" cull pass on {adapter}: {us} µs (frames_of({})={})",
lane_name(),
facett_core::render::lane::frames_of(lane_name())
);
}