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/map_facet_gpu_lane.rs

//! **THE GUARD: korp's default 2D map pane must ENCODE GPU FRAMES.**
//!
//! ## The bug this exists to make impossible
//!
//! `facett_map::gpu::GpuMapRenderer` was installed by korp at startup, the About box
//! reported the renderer, `renderer_installed()` was `true` — and `MapFacet::ui`, the
//! pane korp actually mounts (`map_carnav: true`), was a pure CPU painter with **no GPU
//! branch anywhere in it**. So the lane was *installed and unreachable by construction*:
//! zero frames encoded, ever, on a box with an RTX 4090.
//! `facett-core/src/render/lane.rs` documents the resulting state by name —
//! `GpuLaneUse::InstalledUnused`, "**This is korp's map facet.**"
//!
//! Every cheaper check was green throughout. `renderer_installed()`: green. "wgpu
//! initialised successfully" on stderr: printed. An adapter banner naming the 4090:
//! correct. A non-blank render proof: passed — *the CPU painter drew it*. The ONLY
//! signal that could have gone red is the number of frames the lane encoded, so that
//! number is what this file asserts, and it asserts it against a real device.
//!
//! ## Why this cannot be a hollow guard
//!
//! A frame counter alone is not enough: a `note_frame` moved to the top of `prepare`,
//! or an unconditional bump, would satisfy "frames > 0" while proving nothing. So the
//! same harness drives the SAME facet twice:
//!
//! * **flat, north-up camera** → the lane must encode ≥ 1 frame; and
//! * **bearing 45°** → the lane must encode **no further frames**, because
//!   [`MapTransform`] has no rotation term and `MapFacet` therefore falls back to the
//!   CPU painter.
//!
//! The pair is only satisfiable by a counter that tracks the real decision. It also
//! pins the fallback itself: if someone routes a rotated camera through the flat
//! transform, the second half goes red instead of the basemap silently un-rotating
//! under rotated labels.
//!
//! Run (needs the real device, opens NO window):
//! ```text
//! env -u WAYLAND_DISPLAY -u DISPLAY FACETT_TEST_GPU=discrete \
//!   cargo test -p facett-map --features wgpu --test map_facet_gpu_lane -- --nocapture
//! ```
#![cfg(feature = "wgpu")]

use facett_core::render::lane::{self, GpuLaneUse, GpuStatus};
use facett_map::gpu::MapTransform;
use facett_map::layer::{LatLon, MapFacet, MapTheme, MapView, PointsLayer, WaysLayer};
use facett_map::Way;
/// **The shared one-device-at-a-time lock** — `common/one_gpu.rs`. `egui_kittest::Harness`
/// brings up its own wgpu device, libtest runs `#[test]` bodies on parallel threads, and
/// two device bring-ups at once take this binary out with a hard `signal: 11` — not a test
/// failure, a SIGSEGV whose output looks nothing like the assertion that was running.
/// MEASURED 2026-08-27 on oden by taking the lock back OUT of the two files that had it:
/// `rail_tier_pixels` 2/5 runs and `place_label_tier_pixels` 3/5 runs died that way. This
/// file rendered 0/8 without it, which is a LOWER rate and not a safe one — the hazard is
/// the concurrent bring-up, and every file that does one takes the same turn.
#[path = "common/one_gpu.rs"]
mod one_gpu;

/// The repo's existing "this proof needs the discrete GPU" gate
/// (`facett-about/tests/gpu_rows_on_real_hardware.rs`, `facett_map3d::gpu::proof_wgpu_setup`).
const GATE: &str = "FACETT_TEST_GPU";

const CANVAS: egui::Vec2 = egui::vec2(900.0, 640.0);

/// A Stockholm-ish road network as real [`Way`]s — unfilled (so the GPU line lane
/// takes them) and tiered `Lod::Country`/`Region` via their `highway` tags so they
/// survive the LOD gate at the zoom the harness drives.
fn roads(n: usize) -> Vec<Way> {
    let theme = facett_core::Theme::default();
    (0..n)
        .map(|i| {
            let f = i as f64 / n as f64;
            let (lon, lat) = (18.02 + f * 0.10, 59.30 + (f * 9.0).sin() * 0.03);
            let pts = [
                (lon, lat),
                (lon + 0.004, lat + 0.002),
                (lon + 0.009, lat - 0.001),
                (lon + 0.014, lat + 0.003),
            ];
            let tags = vec![("highway".to_owned(), "primary".to_owned())];
            let mut w = Way::from_lonlat(i as i64 + 1, &pts, &tags, &theme).expect("a 4-point road");
            w.osm_id = i as i64 + 1;
            w
        })
        .collect()
}

/// korp's stack shape in miniature: a roads `WaysLayer` (GPU-eligible) plus a
/// `PointsLayer` (never GPU-eligible), so the test also covers a mixed stack rather
/// than the one-layer easy case.
fn facet(view: MapView) -> MapFacet {
    let mut f = MapFacet::new("gpu-lane-guard", view, MapTheme::default());
    f.push_layer(Box::new(WaysLayer::new("osm-roads", roads(240))));
    f.push_layer(Box::new(PointsLayer::new(
        "korp-hits",
        vec![(18.05, 59.31), (18.07, 59.33)],
        egui::Color32::RED,
    )));
    f
}

fn flat_view() -> MapView {
    MapView::new(LatLon::new(59.32, 18.07), 12.0, (CANVAS.x, CANVAS.y))
}

/// A fresh wgpu `RenderState` on the device **facett's own policy** picks, with
/// `GpuMapRenderer` installed into it — exactly what korp's eframe creation closure does.
///
/// Never `egui_kittest::wgpu::default_wgpu_setup()`: its `native_adapter_selector`
/// comment reads *"Prefer software rasterizers"*, so a proof built on it would be
/// measuring llvmpipe on a box with a 4090 and saying nothing about it. Bringing this
/// setup up also RECORDS the selection into the globals `GpuStatus::probe()` reads, which
/// is how the About rows below come to name the real card.
///
/// One per `drive`: `WgpuTestRenderer::from_render_state` panics on a reused state.
fn fresh_render_state() -> egui_wgpu::RenderState {
    let setup = facett_core::render::gpu::adapter_wgpu::facett_wgpu_options().wgpu_setup;
    let rs = egui_kittest::wgpu::create_render_state(setup, Default::default());
    facett_map::gpu::install_renderer(&rs);
    rs
}

/// Drive `facet.ui` for `steps` frames through egui_kittest's **wgpu** backend on a fresh
/// real device, and report how many frames the map lane encoded during it.
///
/// **Every step is rendered, not just the last one.** `Harness::step` only walks egui;
/// `Harness::render` is what submits that frame's paint callbacks to the device. The
/// upload-once latch puts the geometry payload on the FIRST frame's callback, so a
/// `run_steps(3); render()` shape submits only frame 3 — whose callback carries
/// `geometry: None`, leaving `geom_ready` false and the lane legitimately at 0. That
/// cost an hour and it is the same thing a real eframe app does correctly: it presents
/// every frame it lays out. This mirrors the app, so the number measured here is the
/// number the app produces.
fn drive(mut f: MapFacet, steps: u32) -> (u64, image::RgbaImage) {
    use egui_kittest::wgpu::WgpuTestRenderer;
    let _gpu = one_gpu::hold();
    let before = lane::lane_use().frames();
    let mut h = egui_kittest::Harness::builder()
        .with_size(CANVAS)
        .renderer(WgpuTestRenderer::from_render_state(fresh_render_state()))
        .build_ui(move |ui| f.ui(ui));
    let mut img = None;
    for _ in 0..steps {
        img = Some(h.render().expect("the wgpu backend produced a frame"));
        h.step();
    }
    (lane::lane_use().frames() - before, img.expect("at least one frame"))
}

/// The fraction of pixels that differ from the modal (background) colour — the
/// "something was actually drawn" measure `tests/render_proof.rs` already uses, lifted
/// here so the two proofs measure aliveness the same way.
fn content_ratio(img: &image::RgbaImage) -> f64 {
    let mut hist = std::collections::HashMap::<u32, u32>::new();
    for p in img.pixels() {
        let k = ((p.0[0] as u32 >> 3) << 10) | ((p.0[1] as u32 >> 3) << 5) | (p.0[2] as u32 >> 3);
        *hist.entry(k).or_default() += 1;
    }
    let modal = *hist.iter().max_by_key(|(_, c)| **c).map(|(k, _)| k).expect("a non-empty image");
    let (mr, mg, mb) =
        (((modal >> 10) & 0x1f) as i32, ((modal >> 5) & 0x1f) as i32, (modal & 0x1f) as i32);
    let lit = img
        .pixels()
        .filter(|p| {
            let (r, g, b) = ((p.0[0] >> 3) as i32, (p.0[1] >> 3) as i32, (p.0[2] >> 3) as i32);
            (r - mr).abs() + (g - mg).abs() + (b - mb).abs() > 3
        })
        .count();
    lit as f64 / (img.width() * img.height()) as f64
}

fn save(img: &image::RgbaImage, name: &str) {
    let path = format!("{}/scratch/cargo/map_facet_gpu_lane_{name}.png", std::env::var("HOME").unwrap_or_default());
    if image::DynamicImage::ImageRgba8(img.clone()).save(&path).is_ok() {
        eprintln!("LOOK: saved {path} ({}×{})", img.width(), img.height());
    }
}

#[test]
fn korps_map_facet_encodes_real_gpu_frames() {
    let mode = std::env::var(GATE).unwrap_or_default();
    if mode != "discrete" && mode != "software" {
        eprintln!(
            "SKIPPED and therefore PROVED NOTHING: set {GATE}=discrete to run korp's map \
             facet on this box's discrete GPU and count the frames its lane encodes \
             (deliberately not a silent pass)."
        );
        return;
    }
    let want_software = mode == "software";

    lane::reset_for_test();

    // Each `drive` opens its own device (a `RenderState` is single-use here), so the run
    // deliberately keeps that count at the three it measures rather than opening a fourth
    // just to read a global — this file shares a GPU with the rest of the suite.
    let (flat_frames, flat_img) = drive(facet(flat_view()), 3);

    let st = GpuStatus::probe();
    let sel = st.adapter.clone().expect("the wgpu bring-up must have RECORDED an adapter selection");
    eprintln!("LOOK: {}", sel.banner());
    for row in st.rows() {
        eprintln!("LOOK row: {row}");
    }
    assert_eq!(
        st.is_software(),
        want_software,
        "{GATE}={mode} but the device that came up is: {}",
        sel.banner()
    );

    // ── THE ASSERTION. An adapter is present, the renderer is installed, and the
    //    facet has drawn: a zero here is the exact state korp shipped in. ──
    eprintln!("MEASURED: flat north-up camera encoded {flat_frames} GPU frame(s)");
    assert!(
        flat_frames > 0,
        "GPU lane encoded {flat_frames} frames while adapter {:?} is present and \
         GpuMapRenderer is installed — this is `installed but UNUSED`, i.e. MapFacet::ui \
         is painting korp's whole basemap on one CPU core again. Lane rows: {:?}",
        sel.chosen.name,
        lane::lanes()
    );
    match lane::lane_use() {
        GpuLaneUse::InUse { frames } => assert!(frames > 0),
        other => panic!("the lane fold must read IN USE, got {other:?}"),
    }
    assert!(
        GpuStatus::probe().lane_line().contains("IN USE"),
        "the About row a user reads must say IN USE: {}",
        GpuStatus::probe().lane_line()
    );

    // ── PIXEL ATTRIBUTION, which a frame counter alone cannot give (the caveat
    //    `lane.rs` itself states: "a lane that encoded a frame of nothing still bumps
    //    the counter"). This pairing is unusually strong here: `features_for_cpu`
    //    removed EVERY line feature of the taken layer from the CPU list, so the only
    //    thing left on the CPU in this stack is two red marker dots. Roads on this
    //    canvas therefore cannot have been painted by anything but the GPU lane.
    save(&flat_img, "flat_gpu");
    let flat_ratio = content_ratio(&flat_img);
    eprintln!("MEASURED: flat frame content ratio {flat_ratio:.4} (roads = GPU-only pixels)");
    assert!(
        flat_ratio > 0.01,
        "the GPU lane encoded {flat_frames} frames but the canvas is {flat_ratio:.4} alive — \
         two marker dots' worth. The lane counted frames of NOTHING: the cull dropped every \
         way, or the transform put them off-screen. (The CPU painter cannot rescue this \
         picture: its line features were removed for the layers the GPU took.)"
    );

    // ── The CHOICE is observable, not inferred. `MapFacet::state_json` must name the
    //    lane, so a headless robot can tell "the GPU line lane is live" from "the CPU
    //    painter is doing all of it" — the two states that look identical from outside,
    //    and the reason a silent no-op lane survived so long. Both routes stay
    //    first-class: the CPU one must report a REASON, never a failure. ──
    let flat_state = facet(flat_view()).state_json();
    eprintln!("MEASURED state_json.way_lane (flat)   : {}", flat_state["way_lane"]);
    assert_eq!(flat_state["way_lane"]["lane"], serde_json::json!("gpu"));
    assert_eq!(flat_state["way_lane"]["gpu_layers"], serde_json::json!(1), "the roads layer");
    assert!(flat_state["way_lane"]["cpu_reason"].is_null(), "the GPU has them: no reason");
    assert!(facet(flat_view()).way_lane().is_gpu());

    // ── A TURNED CAMERA STAYS ON THE GPU. This half of the file used to assert the
    //    opposite, and it was correct when `MapTransform` was the bearing-0 reduction of
    //    the view core. It is not any more: the transform carries the compass and projects
    //    through the same `GroundFrame2d` the CPU painter applies, so the rotation
    //    fallback was REMOVED on measured grounds (layer.rs, `way_lane_plan_scan`):
    //    0.857 ms -> 12.28 ms, 14.3x, and `lane_frames` 249 -> 0 the instant the map began
    //    its default idle spin. Asserting the fallback here kept a guard red against
    //    working code, which teaches everyone to ignore it.
    //
    //    What makes leaving a spun camera on the lane SAFE is proved next door by
    //    `the_gpu_transform_reproduces_the_cpu_projection_only_when_flat`: the compass-
    //    carrying transform reproduces the rotated `MapView::project` sub-pixel, and a
    //    ONE-DEGREE error there shows up as 705 px. That is the assertion this one leans
    //    on; here we only check the lane actually takes the work. ──
    let spun_state = facet(flat_view().with_bearing(45.0)).state_json();
    eprintln!("MEASURED state_json.way_lane (spun)   : {}", spun_state["way_lane"]);
    assert_eq!(
        spun_state["way_lane"]["lane"],
        serde_json::json!("gpu"),
        "a rotated camera is a camera the lane can reproduce — if this says `cpu`, either \
         the rotation fallback came back or `MapTransform::with_compass` stopped being wired"
    );
    assert!(
        spun_state["way_lane"]["cpu_reason"].is_null(),
        "the GPU has the ways under bearing too, so there is no reason to name"
    );
    assert!(facet(flat_view().with_bearing(45.0)).way_lane().is_gpu());

    // ── NON-VACUITY: the rotated camera must ENCODE FRAMES, not merely claim the lane.
    //    `state_json` is a verdict typed before any frame is submitted; only the counter
    //    says work happened. A lane that reported `gpu` and encoded nothing is exactly the
    //    silent no-op this whole file exists to make impossible — it just moved from the
    //    flat camera to the turned one. ──
    let (spun_frames, spun_img) = drive(facet(flat_view().with_bearing(45.0)), 3);
    eprintln!("MEASURED: bearing 45 camera encoded {spun_frames} GPU frame(s)");
    assert!(
        spun_frames > 0,
        "a rotated camera claimed the GPU lane and then encoded {spun_frames} frames. The \
         verdict and the work disagree, which is the `installed_unused` defect wearing a \
         different camera angle."
    );

    // …and it must PAINT. A lane that encodes frames into an empty pane is worse than no
    // lane: the frames make it look alive.
    save(&spun_img, "spun_gpu");
    let spun_ratio = content_ratio(&spun_img);
    eprintln!("MEASURED: bearing 45 frame content ratio {spun_ratio:.4}");
    assert!(
        spun_ratio > 0.01,
        "the rotated camera encoded frames and the pane came out {spun_ratio:.4} alive — \
         the roads are going somewhere other than the screen"
    );

    // …and a tilted camera likewise, for the same reason: `with_compass` takes tilt too.
    let (tilted_frames, _) = drive(facet(flat_view().with_tilt(50.0)), 3);
    eprintln!("MEASURED: tilt 50 camera encoded {tilted_frames} GPU frame(s)");
    assert!(
        tilted_frames > 0,
        "a pitched camera must reach the lane as well — `with_compass(bearing, tilt)` \
         carries both, so a tilt-only fallback would be a fallback nothing justifies"
    );

    // ── The CPU route is still first-class and must still be able to NAME itself. It is
    //    no longer reachable by turning the camera, so the reason that survives is
    //    `no_renderer` — pinned by the unit test beside `CpuOnly`, not here, because this
    //    file only runs where a renderer DID install. `CpuOnly::RotatedCamera` is kept as
    //    a variant for a host whose transform this lane cannot express; it is deliberately
    //    unreachable from these three cameras. ──
    assert!(
        !spun_state["way_lane"]["lane"].is_null(),
        "the lane must always answer with something a reader can act on"
    );
}

/// **The camera reduction is EXACT, not approximate** — no GPU needed.
///
/// `MapFacet`'s GPU lane deliberately writes no projection of its own (LAW #5): it
/// hands [`MapTransform`] `world_px()` and `mercator(center)` because at
/// `bearing == tilt == 0` both sides ARE
/// `rect.center + (merc − center_merc) · world_px`. This asserts that over a spread of
/// zooms and off-centre points — and asserts the pair DIVERGES under a bearing, which
/// is the whole reason `gpu_lane_layers` refuses a rotated camera. A test that only
/// visited bearing 0 could not tell a working gate from a missing one.
#[test]
fn the_gpu_transform_reproduces_the_cpu_projection_only_when_flat() {
    let rect = egui::Rect::from_min_size(egui::pos2(0.0, 0.0), CANVAS);
    let probes = [(18.07, 59.32), (18.02, 59.30), (18.16, 59.36), (17.90, 59.25)];

    let mut checked = 0usize;
    let mut worst_flat = 0.0f32;
    let mut best_spun = 0.0f32;
    let mut worst_spun_gpu = 0.0f32;
    for zoom in [6.0f32, 9.0, 12.0, 15.0, 18.0] {
        let flat = MapView::new(LatLon::new(59.32, 18.07), zoom, (CANVAS.x, CANVAS.y));
        let spun = flat.with_bearing(45.0);
        let ws = flat.world_px();
        let (cx, cy) = facett_map::mercator(flat.center.lon, flat.center.lat);
        // The origin the bake would use is irrelevant to the projected pixel (it
        // cancels), so any data-local value exercises the rebase arithmetic.
        let origin = [cx - 0.001, cy - 0.001];
        let t = MapTransform::new(origin, [cx, cy], ws as f32, ws as f32, rect);

        for (lon, lat) in probes {
            let (mx, my) = facett_map::mercator(lon, lat);
            let local = [(mx - origin[0]) as f32, (my - origin[1]) as f32];
            let gpu = t.project(local);
            let cpu = flat.project(lon, lat);
            worst_flat = worst_flat.max((gpu[0] - cpu.0).abs()).max((gpu[1] - cpu.1).abs());

            // The FLAT transform against the ROTATED projection must NOT agree — this is
            // the non-identity probe. It proves `with_compass` below is doing real work
            // rather than being a no-op that would make the spun assertion vacuous.
            let cpu_spun = spun.project(lon, lat);
            best_spun = best_spun.max((gpu[0] - cpu_spun.0).abs().max((gpu[1] - cpu_spun.1).abs()));

            // THE ASSERTION THAT MATTERS NOW: the ROTATED transform must reproduce the
            // ROTATED projection, sub-pixel, exactly as the flat pair do. `MapTransform`
            // carries the compass since the rotation fallback was removed, so this is
            // what makes a spun camera safe to leave on the GPU lane.
            let t_spun = MapTransform::new(origin, [cx, cy], ws as f32, ws as f32, rect)
                .with_compass(45.0, 0.0);
            let gpu_spun = t_spun.project(local);
            worst_spun_gpu = worst_spun_gpu
                .max((gpu_spun[0] - cpu_spun.0).abs())
                .max((gpu_spun[1] - cpu_spun.1).abs());
            checked += 1;
        }
    }
    assert_eq!(checked, 20, "the whole sweep must have run");
    assert!(
        worst_flat < 0.5,
        "flat: the GPU transform must reproduce MapView::project sub-pixel, worst was {worst_flat}px"
    );
    assert!(
        best_spun > 20.0,
        "rotated: the FLAT transform must visibly DISAGREE with a rotated projection \
         (worst {best_spun}px). This is the non-identity probe — if a bearing-0 transform \
         already matched a bearing-45 projection, the assertion below could pass with \
         `with_compass` deleted, and this whole file would be sitting on an identity value"
    );
    assert!(
        worst_spun_gpu < 0.5,
        "rotated: the transform CARRYING the compass must reproduce the rotated \
         MapView::project sub-pixel, worst was {worst_spun_gpu}px. This is what makes it \
         correct to leave a spun camera on the GPU lane instead of falling back — and the \
         fallback was removed on measured grounds (0.857ms -> 12.28ms, lane_frames 249 -> 0 \
         the instant the map started its default idle spin), so this assertion is the only \
         thing standing between that decision and roads drawn in the wrong place"
    );
}