facett-map/tests/cpu_casing.rs
//! **THE GUARD: the CPU painter draws the SAME road the GPU lane draws** (`kalm-4`).
//!
//! ## The defect
//!
//! The GPU line lane strokes every way **twice** — a wider dark casing band, then the
//! bright fill on top — and `MapFacet::paint_features` emitted a single
//! `egui::Shape::line` with `f.color` and **no casing at all**. One road, two looks,
//! decided by nothing the user can see: whether the GPU lane happened to take the frame.
//!
//! And the CPU painter is not a degraded mode. It is what draws under a bearing, under a
//! tilt, on a host whose adapter bring-up was refused, and in **every `egui_kittest`
//! harness test in this repo** — which is the sharper end of it: no CPU-side test could
//! see a casing regression, because there was no CPU-side casing. That is why the
//! proportional-casing fix had to be guarded with a real headless wgpu device.
//!
//! ## Why this is not a hollow guard
//!
//! `Assert::Drew` would pass here on the unfixed code — the road was always drawn, just
//! without its outline. So this measures the **painted band's WIDTH in pixels** against
//! the width the shared casing law says it must be, and it measures the **colour at the
//! band's edge** against the colour at its centre. A painter that draws the fill and no
//! casing gives a band `line_w` px wide whose edge and centre are the same hue; a
//! painter that obeys the law gives `line_w + 2*casing_extra_px(line_w)` with a
//! different hue at the edge. Neither number can be satisfied by drawing *something*.
//!
//! There is no `#![cfg(feature = "wgpu")]` on this file on purpose: the lane under test
//! is the one that runs when there is no GPU lane. `egui_kittest`'s renderer is a
//! dev-dependency and puts real pixels on the table either way, and no host installs a
//! `GpuMapRenderer` here, so `way_lane_plan` reports `CpuOnly::NoRenderer` and the CPU
//! painter owns the pane — which is the whole point.
//!
//! Run: `cargo test -p facett-map --test cpu_casing -- --test-threads=1`
use facett_map::Way;
use facett_map::layer::{LatLon, MapFacet, MapTheme, MapView, WaysLayer};
/// **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 CANVAS: egui::Vec2 = egui::vec2(900.0, 640.0);
/// One dead-straight EAST-WEST road across the middle of the pane.
///
/// Straight and horizontal so a single vertical column of pixels cuts it at a right
/// angle: the band's height in that column IS its width, with no cos(theta) to argue
/// about. A `trunk` because the top road tier is `Lod::Country` and so survives the LOD
/// gate at any zoom the harness drives.
///
/// **`ROAD_W = 12.0` and not a `trunk`'s own 3.0.** At 3.0 px the law adds
/// `clamp(3.0*0.5*0.5, 0.35, 2.0) = 0.75` px per side, and a 0.75 px outline never
/// produces a PURE pixel — it lands entirely inside the stroke's feathered edge, so the
/// measured edge came back `[67, 112, 136]`, an honest blend of casing and road, and the
/// hue assert failed against a picture that was drawing the casing correctly. At 12.0 px
/// the law is at its `CASING_MAX_PX` ceiling of 2.0 px per side, which is wide enough to
/// own a pixel outright. The WIDTH assert is the one that pins the law's arithmetic; the
/// HUE assert only needs the casing to be resolvable.
const ROAD_W: f32 = 12.0;
fn straight_road(theme: &facett_core::Theme) -> Vec<Way> {
let lat = 59.32;
let pts: Vec<(f64, f64)> = (0..=20).map(|i| (18.00 + i as f64 * 0.008, lat)).collect();
let tags = vec![("highway".to_owned(), "trunk".to_owned())];
let mut w = Way::from_lonlat(1, &pts, &tags, theme).expect("a straight road");
w.style.width = ROAD_W;
vec![w]
}
/// The ONE camera both the render and the expected width read — a city zoom. The width
/// law tracks zoom (lane GG #27: `MapFacet::line_width_scale` is the live
/// `style::line_width_scale(world_px)`, no longer a pinned `1.0`), so the expectation
/// below has to be resolved at the SAME `world_px` the painter strokes at.
fn view() -> MapView {
MapView::new(LatLon::new(59.32, 18.08), 12.0, (CANVAS.x, CANVAS.y))
}
/// Render the facet on the CPU painter.
///
/// **The theme is passed in and used for BOTH halves on purpose.** A way's colours are
/// resolved at construction by `Way::from_lonlat(.., theme)` — i.e. from
/// `MapPalette::from_theme` — while `MapTheme::cartographic` swaps in the concrete-hex
/// preset for the pane. A first cut of this test built the ways from `Theme::default()`
/// and the pane from `cartographic(day)`, so the road drew themed grey `[227, 223, 215]`
/// while the test looked for cartographic orange `#E9A23B`, and both asserts failed
/// against a picture that was perfectly correct. One theme, both halves.
fn render_cpu(theme: &facett_core::Theme) -> (MapTheme, image::RgbaImage) {
render_cpu_cased(theme, None)
}
/// [`render_cpu`], with the pane's casing ink optionally overridden.
///
/// The override is how the palette claim is proven: set it to a colour no palette would
/// ever pick and the edge pixel has to become that colour. A painter reading a `const`
/// ignores it and the assert cannot be satisfied by accident.
fn render_cpu_cased(
theme: &facett_core::Theme,
casing: Option<egui::Color32>,
) -> (MapTheme, image::RgbaImage) {
let _gpu = one_gpu::hold();
let mut map_theme = MapTheme::day(theme);
if let Some(c) = casing {
map_theme.palette.casing = c;
}
let mut f = MapFacet::new("cpu-casing", view(), map_theme.clone());
f.push_layer(Box::new(WaysLayer::new("osm-roads", straight_road(theme))));
let mut h = egui_kittest::Harness::builder().with_size(CANVAS).build_ui(move |ui| {
f.ui(ui);
});
h.run_steps(3);
(map_theme, h.render().expect("the CPU painter rendered into a framebuffer"))
}
/// Chebyshev distance between two sRGB triples.
fn dist(a: [u8; 3], b: egui::Color32) -> i32 {
(a[0] as i32 - b.r() as i32)
.abs()
.max((a[1] as i32 - b.g() as i32).abs())
.max((a[2] as i32 - b.b() as i32).abs())
}
/// The rows of the image that are MAP CANVAS — i.e. mostly the land colour.
///
/// The harness draws the facet inside its own chrome, and that chrome is pure black.
/// A first cut scanned the whole column, counted 24 384 px of black frame as "not land"
/// and reported a painted band 640 px wide — the entire image height — about a road
/// 4.5 px across. The canvas has to be found before anything is measured inside it.
fn canvas_rows(img: &image::RgbaImage, land: egui::Color32) -> (u32, u32) {
let mut rows: Vec<u32> = Vec::new();
for y in 0..img.height() {
let n = (0..img.width())
.filter(|&x| {
let p = img.get_pixel(x, y).0;
dist([p[0], p[1], p[2]], land) <= 6
})
.count();
if n as f32 > img.width() as f32 * 0.5 {
rows.push(y);
}
}
assert!(!rows.is_empty(), "no row of this render is mostly land — the pane never painted");
(rows[0], rows[rows.len() - 1])
}
/// The contiguous run of NON-LAND pixels in column `x`, within the canvas rows.
fn painted_band(img: &image::RgbaImage, x: u32, land: egui::Color32, rows: (u32, u32)) -> Option<(u32, u32)> {
let mut first = None;
let mut last = 0;
for y in rows.0..=rows.1 {
let p = img.get_pixel(x, y).0;
if dist([p[0], p[1], p[2]], land) > 6 {
first.get_or_insert(y);
last = y;
}
}
first.map(|f| (f, last))
}
#[test]
fn the_cpu_painter_draws_a_casing_and_it_is_the_width_the_shared_law_says() {
let (map_theme, img) = render_cpu(&facett_core::Theme::default());
let pal = map_theme.palette;
let rows = canvas_rows(&img, pal.land);
// The core is the shared width law at THIS camera: `clamp(ROAD_W × line_width_scale
// (world_px), 1, 24)` — at zoom 12 the ramp is on its 3.0× clamp and 36 px hits the
// 24 px ceiling. The casing law then adds `clamp(core * 0.5 * 0.5, 0.35, 2.0)` per
// side, which at 24 px is the 2.0 px ceiling.
let core_px = facett_map::style::line_width_px(ROAD_W, view().world_px());
let extra_px = facett_map::style::casing_extra_px(core_px);
let want_px = core_px + 2.0 * extra_px;
assert!(extra_px > 0.0, "the law must add SOMETHING or this test proves nothing");
// Sample columns well inside the pane, away from the ends of the polyline.
let cols: Vec<u32> = (0..7).map(|i| img.width() / 4 + i * 40).collect();
let mut bands = Vec::new();
for &x in &cols {
if let Some((y0, y1)) = painted_band(&img, x, pal.land, rows) {
bands.push((x, y0, y1, (y1 - y0 + 1) as f32));
}
}
assert!(
bands.len() >= 5,
"the road must cross at least 5 of the 7 sampled columns, crossed {} — \
if it crossed none the pane is blank and every number below is meaningless",
bands.len()
);
let widths: Vec<f32> = bands.iter().map(|b| b.3).collect();
let median = {
let mut w = widths.clone();
w.sort_by(|a, b| a.partial_cmp(b).unwrap());
w[w.len() / 2]
};
eprintln!(
"[cpu-casing] core {core_px:.2} px + 2*{extra_px:.2} px casing = {want_px:.2} px; \
measured band widths {widths:?}, median {median:.2}"
);
// The band must be the CASED width, not the bare core. The window is generous
// downward only as far as it has to be for antialiasing (the painted run picks up
// the feathered edge), and it is asserted STRICTLY above the core: `median > core`
// is the half of this that a no-casing painter fails outright.
assert!(
median > core_px + 0.5,
"the painted band is {median:.2} px — that is the BARE CORE ({core_px:.2} px), \
so the CPU painter drew no casing (kalm-4)"
);
assert!(
(median - want_px).abs() <= 2.0,
"the painted band is {median:.2} px but the shared casing law says {want_px:.2} px \
({core_px:.2} core + 2 x {extra_px:.2}) — the two lanes have drifted"
);
// ── THE EDGE IS NOT THE CENTRE ───────────────────────────────────────────
// Width alone could be satisfied by a fatter stroke in the road's own colour, which
// is a different bug wearing this one's clothes. The casing has to be a casing.
let (x, y0, y1, _) = bands[bands.len() / 2];
let px = |y: u32| {
let p = img.get_pixel(x, y).0;
[p[0], p[1], p[2]]
};
let centre = px((y0 + y1) / 2);
let edge = px(y0);
eprintln!(
"[cpu-casing] column {x}: edge {edge:?} (casing {:?}), centre {centre:?} (road {:?})",
[pal.casing.r(), pal.casing.g(), pal.casing.b()],
[pal.road_motorway.r(), pal.road_motorway.g(), pal.road_motorway.b()],
);
assert!(
dist(edge, pal.casing) < dist(edge, pal.road_motorway),
"the band's EDGE pixel {edge:?} is nearer the road hue than the casing hue — \
the outline is not being painted in the casing colour"
);
assert!(
dist(centre, pal.road_motorway) < dist(centre, pal.casing),
"the band's CENTRE pixel {centre:?} is nearer the casing hue than the road hue — \
the casing painted OVER the fill instead of under it"
);
}
#[test]
fn the_casing_ink_follows_the_palette_and_is_not_a_hardcoded_navy() {
// The GPU lane's casing WAS the literal `#191C24` for every palette there is. On the
// cartographic DAY map the casing is `#B7AE9E`, a warm grey on warm paper; MEASURED
// on the live z12/z15 panes with `korp tools/mapdrive/ink.py`, that hue covered
// 0.03-0.06 % of the canvas — it was not on the map at all.
//
// Two themes are NOT enough to prove this: every `MapPalette::from_theme` casing is
// `mix(land, BLACK, 0.55)`, so `Theme::default` gives `#0E0E11` and `amber_crt`
// `#0D0C0B` — one step apart, and a test that cannot tell the two apart proves
// nothing about which one it read. So the pane's casing ink is overridden to a
// colour no palette would pick, and the edge pixel has to become it.
let theme = facett_core::Theme::default();
let loud = [
egui::Color32::from_rgb(255, 0, 0),
egui::Color32::from_rgb(0, 200, 0),
];
let mut edges = Vec::new();
for want in loud {
let (mt, img) = render_cpu_cased(&theme, Some(want));
let land = mt.palette.land;
let rows = canvas_rows(&img, land);
let x = img.width() / 2;
let (y0, y1) = painted_band(&img, x, land, rows).expect("the road crossed the middle column");
// One pixel INSIDE the band's first row: row `y0` itself is the stroke's outer
// feather against the paper and is a blend by construction.
let p = img.get_pixel(x, y0 + 1).0;
let edge = [p[0], p[1], p[2]];
let centre = {
let c = img.get_pixel(x, (y0 + y1) / 2).0;
[c[0], c[1], c[2]]
};
eprintln!("[cpu-casing] casing set to {want:?} -> edge {edge:?}, centre {centre:?}");
assert!(
dist(edge, want) < 40,
"the pane's casing ink was set to {want:?} and the road's edge pixel came \
back {edge:?} — the casing is not reading the palette (kalm-4)"
);
assert!(
dist(centre, want) > 40,
"the road's CENTRE pixel {centre:?} is the casing colour too — the casing \
painted OVER the fill instead of under it"
);
edges.push(edge);
}
assert_ne!(
edges[0], edges[1],
"both casing colours produced the same edge pixel — the ink is a constant"
);
}