facett-map/tests/gpu_lane_inset_pane.rs
//! **THE GUARD: a GPU-lane way must land on the SAME pixel as the CPU painter puts it,
//! when the map pane is NOT the whole window.**
//!
//! ## The bug this exists to make impossible
//!
//! `MapPaintCallback::prepare` writes `DrawUniforms::viewport_{w,h}` from the
//! `ScreenDescriptor` — the whole logical surface — and `line.wgsl`'s `px_to_ndc`
//! divides the vertex's screen-space px by exactly that. So the vertex stream is in
//! WINDOW coordinates and NDC ±1 must mean the WINDOW's edges.
//!
//! egui-wgpu does not leave it that way: `Renderer::render` sets the pass viewport to
//! the **callback's own rect** before invoking the callback. NDC ±1 then means the
//! PANE's edges, and the entire window coordinate space is squeezed into the pane:
//!
//! ```text
//! drawn_px = pane_origin + window_px · (pane_size / window_size)
//! ```
//!
//! Measured on the shipped korp-ui on a real Xvfb window on 2026-08-03: the landmass
//! fill and the labels (CPU painter, `MapView::project`) landed correctly while the
//! coastline and roads (GPU lane) were drawn as a second, smaller, displaced Sweden —
//! all four corners within 2 px of that formula, at two different window sizes.
//!
//! ## Why every existing guard was green
//!
//! `tests/map_facet_gpu_lane.rs` compares `MapTransform::project` with
//! `MapView::project` on the CPU. That comparison never converts to NDC and never
//! learns which viewport the render pass will carry, so it cannot see this at all.
//! And every offscreen harness renders a pane that IS the whole surface — where
//! `pane_origin == 0` and `pane_size == window_size` make the wrong formula the
//! IDENTITY. The defect is invisible in the posture the tests render in and present in
//! the posture a human lands in. So this guard renders an **inset** pane, which is the
//! only posture that can tell the two formulas apart.
//!
//! ## Why it is not hollow
//!
//! The oracle is a **sentinel**, not a threshold (`.nornir/suite-robot-testing.md` §6):
//! ONE magenta way, drawn at exactly the camera centre, whose ink centroid must be at
//! the pane centre. That coordinate is derived from the layout, not from the renderer,
//! so the two cannot agree by construction. A second assertion pins the FAILING
//! direction explicitly: the ink must not sit where the squeeze formula predicts.
//! The absent-sentinel case is covered too — `a_pane_with_no_gpu_way_has_no_sentinel_ink`
//! renders the same scene with the way removed and requires the ink to be gone, so a
//! detector that answers "found it" on an empty canvas cannot pass.
//!
//! Run (needs a real device, opens NO window):
//! ```text
//! env -u WAYLAND_DISPLAY -u DISPLAY FACETT_TEST_GPU=discrete \
//! cargo test -p facett-map --features wgpu --test gpu_lane_inset_pane -- --nocapture
//! ```
#![cfg(feature = "wgpu")]
use facett_map::layer::{LatLon, MapFacet, MapTheme, MapView, 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;
const GATE: &str = "FACETT_TEST_GPU";
/// The whole offscreen surface — the "window".
const SURFACE: egui::Vec2 = egui::vec2(900.0, 640.0);
/// Where the map pane sits inside it. Deliberately **neither at the origin nor the
/// full size** — that is the entire point of this file.
const PANE_MIN: egui::Pos2 = egui::pos2(240.0, 160.0);
const PANE_SIZE: egui::Vec2 = egui::vec2(520.0, 360.0);
const CLON: f64 = 18.07;
const CLAT: f64 = 59.32;
const ZOOM: f32 = 12.0;
/// The sentinel ink — a colour that appears nowhere in `MapTheme::default()`.
const SENTINEL: egui::Color32 = egui::Color32::from_rgb(255, 0, 255);
/// ONE way: a small cross centred exactly on the camera centre, so its ink centroid
/// belongs at the pane centre and nowhere else. Unfilled, so the GPU **line** lane
/// takes it; tagged `motorway` so it survives the LOD gate at [`ZOOM`].
fn sentinel_way() -> Way {
let theme = facett_core::Theme::default();
let d = 0.0040;
let pts = [
(CLON - d, CLAT - d * 0.5),
(CLON, CLAT),
(CLON + d, CLAT + d * 0.5),
(CLON, CLAT),
(CLON + d, CLAT - d * 0.5),
];
let tags = vec![("highway".to_owned(), "motorway".to_owned())];
let mut w = Way::from_lonlat(1, &pts, &tags, &theme).expect("a 5-point sentinel cross");
w.osm_id = 1;
w.fill = None;
w.stroke = SENTINEL;
w.style.color = SENTINEL;
w.width = 6.0;
w
}
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
}
/// Render the facet **inside `PANE_MIN`/`PANE_SIZE` of a `SURFACE`-sized window**, and
/// return the frame. `with_ways = false` renders the identical scene with no GPU way at
/// all — the absent-sentinel control.
fn render_inset(with_ways: bool) -> image::RgbaImage {
use egui_kittest::wgpu::WgpuTestRenderer;
let _gpu = one_gpu::hold();
let view = MapView::new(LatLon::new(CLAT, CLON), ZOOM, (PANE_SIZE.x, PANE_SIZE.y));
let mut f = MapFacet::new("inset-pane-guard", view, MapTheme::default());
if with_ways {
f.push_layer(Box::new(WaysLayer::new("osm-roads", vec![sentinel_way()])));
} else {
f.push_layer(Box::new(WaysLayer::new("osm-roads", Vec::new())));
}
let mut h = egui_kittest::Harness::builder()
.with_size(SURFACE)
.renderer(WgpuTestRenderer::from_render_state(fresh_render_state()))
.build_ui(move |ui| {
let rect = egui::Rect::from_min_size(PANE_MIN, PANE_SIZE);
let mut child = ui.new_child(egui::UiBuilder::new().max_rect(rect));
child.set_clip_rect(rect);
f.ui(&mut child);
});
let mut img = None;
for _ in 0..4 {
img = Some(h.render().expect("the wgpu backend produced a frame"));
h.step();
}
img.expect("at least one frame")
}
/// Centroid + count of the sentinel-coloured pixels, in **logical** px (the frame may
/// be rendered at a device pixel ratio).
fn sentinel_centroid(img: &image::RgbaImage) -> (Option<(f64, f64)>, usize) {
let sx = f64::from(img.width()) / f64::from(SURFACE.x as u32);
let sy = f64::from(img.height()) / f64::from(SURFACE.y as u32);
let (mut ax, mut ay, mut n) = (0.0f64, 0.0f64, 0usize);
for (x, y, p) in img.enumerate_pixels() {
let (r, g, b) = (i32::from(p.0[0]), i32::from(p.0[1]), i32::from(p.0[2]));
// Magenta-dominant: the sentinel and nothing in the default map theme.
if r > 170 && b > 170 && g < 90 {
ax += f64::from(x);
ay += f64::from(y);
n += 1;
}
}
if n == 0 {
return (None, 0);
}
(Some((ax / n as f64 / sx, ay / n as f64 / sy)), n)
}
fn save(img: &image::RgbaImage, name: &str) {
let path = format!("{}/scratch/cargo/gpu_lane_inset_{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 a_gpu_way_lands_where_the_camera_centre_is_even_in_an_inset_pane() {
if std::env::var(GATE).is_err() {
eprintln!("SKIP: set {GATE}=discrete to run this against the real device");
return;
}
let img = render_inset(true);
save(&img, "with_way");
let (found, n) = sentinel_centroid(&img);
let (ix, iy) = found.expect(
"the GPU line lane drew NO sentinel ink anywhere in the frame — the way never \
reached the device, which is a different defect from drawing it in the wrong place",
);
let centre = PANE_MIN + PANE_SIZE * 0.5;
let (want_x, want_y) = (f64::from(centre.x), f64::from(centre.y));
// Where the squeeze bug PUTS it, spelled out so a regression names itself:
// drawn = pane_origin + window_px · (pane_size / window_size)
let bug_x = f64::from(PANE_MIN.x) + want_x * f64::from(PANE_SIZE.x) / f64::from(SURFACE.x);
let bug_y = f64::from(PANE_MIN.y) + want_y * f64::from(PANE_SIZE.y) / f64::from(SURFACE.y);
eprintln!(
"sentinel ink: {n} px, centroid ({ix:.1}, {iy:.1}); pane centre ({want_x:.1}, \
{want_y:.1}); squeeze-bug prediction ({bug_x:.1}, {bug_y:.1})"
);
// TOLERANCE, not a threshold oracle: the sentinel cross is a few px of stroke and
// the centroid of an anti-aliased miter is not exactly its centre. The bug's
// displacement here is ~120 px, two orders above this.
const TOL: f64 = 10.0;
let err = ((ix - want_x).powi(2) + (iy - want_y).powi(2)).sqrt();
assert!(
err <= TOL,
"the GPU lane drew the camera-centre way at ({ix:.1}, {iy:.1}), {err:.1} px from \
the pane centre ({want_x:.1}, {want_y:.1}). The squeeze bug predicts \
({bug_x:.1}, {bug_y:.1}) — {:.1} px away from where it landed. The CPU painter \
puts the same way at the pane centre, so the pane draws its ways in a second, \
displaced copy of itself.",
((ix - bug_x).powi(2) + (iy - bug_y).powi(2)).sqrt(),
);
}
/// The ABSENT-SENTINEL control: the same scene with no way at all must produce no
/// sentinel ink. Without this, a detector that answered "found it" on a blank canvas
/// would make the guard above unfalsifiable.
#[test]
fn a_pane_with_no_gpu_way_has_no_sentinel_ink() {
if std::env::var(GATE).is_err() {
eprintln!("SKIP: set {GATE}=discrete to run this against the real device");
return;
}
let img = render_inset(false);
save(&img, "no_way");
let (found, n) = sentinel_centroid(&img);
assert!(
found.is_none(),
"{n} sentinel-coloured pixel(s) in a frame that was given NO way — the detector \
answers yes on an empty canvas, so the guard above proves nothing"
);
}