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