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

nordisk/facett

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