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