Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/facett

facett-map3d-flight/tests/ground_pixels_probe.rs

//! **MEASUREMENT: what the 3D pane actually PUTS ON SCREEN, per cartographic class.**
//!
//! The companion to `ground_loss_probe` (which stops at the mesh). This one carries the
//! SAME real extract all the way to pixels, through facett-map3d's own GPU depth path
//! (`egui_kittest` + wgpu — no server, no window, no korp), and reports the share of the
//! pane each class paints.
//!
//! Why it exists: the mesh census says the extruded ground cover spans **16.6 % of the
//! design box**, within a whisker of the 16.89 % green the 2D pane paints from the same
//! ways — yet the 3D pane was reported at 0.00 % green. Those two facts cannot both be
//! about the mesh, so the question is what happens between the mesh and the pixel, and
//! only pixels answer it.
//!
//! The classes are matched by HUE ANGLE, not by palette hex: the 3D lane lights, fogs,
//! ACES-tonemaps and sRGB-encodes every fragment, so a park that reads `#C8E6C9` flat in
//! 2D can never land within a Chebyshev-10 ball of that hex in 3D. A probe that demands
//! the hex measures the tonemap, not the map — which is the whole of what
//! `korp/tools/mapdrive/ink.py` reports about a 3D pane, and why its `0.00 % within
//! tolerance for any cartographic class` is correct and uninformative.
//!
//! **This probe's own first verdict was wrong for a subtler reason, and the correction
//! is the point of `hue_report`.** It classified by nearest palette chromaticity by
//! DISTANCE. The palette's classes sit at very unequal distances from neutral (`land`
//! `#F2EFE9` radius `0.0058`; `green` `#C8E6C9` radius `0.0352`), so the near-neutral
//! class is an attractor that swallows every desaturated pixel regardless of hue.
//! MEASURED 2026-08-04: 21 155 plainly green park pixels all voted `land` and the table
//! reported `green 0.00 %` on a pane with visible parks. Both tables are printed now —
//! the hue-angle VERDICT and the distance CONTROL — because seeing them disagree is the
//! evidence that the method matters:
//!
//! ```text
//!                  by hue angle   by distance
//!   green               5.54 %        0.03 %
//!   water              31.49 %       26.18 %
//!   land                4.59 %       56.41 %
//! ```
//!
//! `#[ignore]` — needs a local extract and a GPU adapter:
//!
//! ```text
//! GROUND_PROBE_WAYS=/path/to/ways.parquet \
//!   cargo test -p facett-map3d-flight --features wgpu --test ground_pixels_probe \
//!   -- --ignored --nocapture
//! ```

#![cfg(feature = "wgpu")]

use egui_kittest::Harness;
use egui_kittest::wgpu::{WgpuTestRenderer, create_render_state, default_wgpu_setup};
use facett_core::Facet as _;
use facett_map::MapPalette;
use facett_map3d::Map3D;

const W: f32 = 900.0;
const H: f32 = 700.0;

fn render(view: Map3D) -> image::RgbaImage {
    render_with(view, true)
}

/// `gpu = false` keeps `Map3D`'s default [`facett_map3d::backend::CpuPainterBackend`] —
/// the same mesh, the same camera, the same per-triangle source colours, but WITHOUT the
/// wgpu lighting/IBL/fog/ACES chain. It is the control that separates "the class never
/// reached the mesh" from "the class reached the mesh and the shading ate its colour".
fn render_with(mut view: Map3D, gpu: bool) -> image::RgbaImage {
    let rs = create_render_state(default_wgpu_setup(), Default::default());
    facett_map3d::gpu::install_renderer(&rs);
    view = view.with_gpu(gpu);
    let mut harness = Harness::builder()
        .with_size(egui::vec2(W, H))
        .renderer(WgpuTestRenderer::from_render_state(rs))
        .build_ui(move |ui| {
            facett_core::set_theme(ui.ctx(), facett_core::Theme::sci_fi());
            view.ui(ui);
        });
    harness.run_steps(5);
    harness.render().expect("wgpu render")
}

/// The `MapPalette::cartographic(day)` classes, in the order `style.rs` declares them.
const DAY: [(&str, [u8; 3]); 6] = [
    ("land", [0xF2, 0xEF, 0xE9]),
    ("water", [0xA8, 0xD0, 0xE6]),
    ("green", [0xC8, 0xE6, 0xC9]),
    ("building", [0xE2, 0xDB, 0xCF]),
    ("road_generic", [0xDE, 0xD8, 0xCB]),
    ("path", [0xB9, 0x98, 0x73]),
];

/// **Chromaticity** `(r, g)` of a colour with the luma divided out. The 3D lane lights,
/// fogs and ACES-tonemaps every fragment, so a class' rendered LUMA is a free variable
/// while its HUE is (or should be) the surviving signal. Comparing chromaticities is the
/// only way to ask "did this class' colour reach the pixel" of a shaded render at all.
fn chroma(c: [u8; 3]) -> (f64, f64) {
    let s = f64::from(c[0]) + f64::from(c[1]) + f64::from(c[2]);
    if s <= 0.0 {
        return (1.0 / 3.0, 1.0 / 3.0);
    }
    (f64::from(c[0]) / s, f64::from(c[1]) / s)
}

/// How far a class' chromaticity sits from neutral grey — its **chroma radius**. The
/// number that says whether a render preserved a palette or flattened it.
fn chroma_radius(c: [u8; 3]) -> f64 {
    let (r, g) = chroma(c);
    ((r - 1.0 / 3.0).powi(2) + (g - 1.0 / 3.0).powi(2)).sqrt()
}

/// Hue-dominance classes, evaluated in order (first match wins).
fn class_of(p: [u8; 4]) -> &'static str {
    let (r, g, b) = (p[0] as i32, p[1] as i32, p[2] as i32);
    let sum = r + g + b;
    if b > g + 25 && b > r + 40 && sum > 240 {
        // Sky and water both read blue-dominant; the pane's upper band is sky, so this
        // is reported as one class and split by row below.
        return "blue (sky/water)";
    }
    if g > r + 12 && g > b + 12 {
        return "green (cover)";
    }
    if r > g + 10 && r > b + 20 {
        return "warm (roof/road/building)";
    }
    if sum < 150 {
        return "near-black";
    }
    "neutral (grey/terrain)"
}

/// Per-class share of a rendered pane, printed with a label. Returns the HUE-angle
/// counts + the pane's pixel total, so a caller asserts on the same measurement the
/// table printed.
fn tally_pane(
    label: &str,
    img: &image::RgbaImage,
) -> (std::collections::BTreeMap<&'static str, usize>, usize) {
    let (w, h) = (img.width(), img.height());
    let mut t: std::collections::BTreeMap<&'static str, usize> = Default::default();
    for y in 0..h {
        for x in 0..w {
            *t.entry(class_of(img.get_pixel(x, y).0)).or_default() += 1;
        }
    }
    let n = f64::from(w * h);
    println!("\n  == {label} ==");
    println!("    -- CONTROL: absolute 8-bit channel gaps (`class_of`) — NOT the verdict --");
    for (k, v) in &t {
        println!("    {k:<28} {v:>8}  ({:>6.2} %)", 100.0 * *v as f64 / n);
    }
    chroma_report(img)
}

/// **The verdict table: nearest palette class by HUE ANGLE.**
///
/// The honest question to ask a shaded render. The 3D lane lights, fogs, ACES-tonemaps
/// and sRGB-encodes every fragment, so a class' rendered LUMA and its SATURATION are
/// both free variables; its HUE is the only thing that should survive, and hue is an
/// ANGLE around neutral, not a distance from a point.
///
/// **Why not nearest-by-chromaticity-distance, which this probe used first.** The
/// palette's classes sit at wildly unequal distances from neutral — `land` `#F2EFE9`
/// has a chroma radius of `0.0058`, `green` `#C8E6C9` has `0.0352`, six times more. A
/// near-neutral class is therefore an ATTRACTOR: every desaturated pixel in the frame
/// lands nearer `land` than any saturated class, whatever its hue. MEASURED
/// 2026-08-04: 21 155 unambiguously green-dominant park pixels (e.g. `rgb(231,238,231)`)
/// all voted `land`, and the table read `green 0.00 %` on a pane with visible parks in
/// it. By hue angle the same pixel sits **1.7°** from `green` and **102°** from `land`.
///
/// So the table below reports, per class: share by hue angle, and separately the
/// **retained chroma radius** — how much saturation the render kept. Those are two
/// different questions and merging them is what produced the wrong verdict.
///
/// **Honest bound.** Hue angle separates `green` (118°) and `water` (170°) decisively.
/// It CANNOT separate `land` (14.0°), `road_generic` (15.6°), `building` (10.9°) and
/// `path` (2.2°) — the whole warm family lies within 13° — so those four are reported
/// individually but summed as `warm` for any verdict. Pixels below [`CHROMA_FLOOR`]
/// have no meaningful hue at all and are counted as achromatic rather than assigned.
fn chroma_report(img: &image::RgbaImage) -> (std::collections::BTreeMap<&'static str, usize>, usize) {
    let out = hue_report(img);
    nearest_chroma_report(img);
    out
}

/// Chroma radius below which a pixel has no meaningful hue — its angle is numerical
/// noise. `0.004` sits just under `land`'s own radius (`0.0058`), so the palest real
/// cartographic class still classifies while a washed-out pixel does not pretend to.
const CHROMA_FLOOR: f64 = 0.004;

/// The signed hue angle (radians) of a colour around the neutral point in `(r, g)`
/// chromaticity — the quantity a lit, fogged, tonemapped render should preserve.
fn hue_angle(c: [u8; 3]) -> f64 {
    let (r, g) = chroma(c);
    (g - 1.0 / 3.0).atan2(r - 1.0 / 3.0)
}

/// Smallest absolute difference between two angles, wrapped to `[0, π]`.
fn angle_gap(a: f64, b: f64) -> f64 {
    let d = (a - b + std::f64::consts::PI).rem_euclid(2.0 * std::f64::consts::PI) - std::f64::consts::PI;
    d.abs()
}

/// **THE ONE CLASSIFIER** (L5) — which palette class this pixel is, by HUE ANGLE, or
/// `None` when it is too dark or too washed out to carry a hue at all.
///
/// # Why this function exists rather than a second copy of the rule
///
/// It used to be two. [`class_of`] answered the same question with absolute 8-bit
/// channel gaps (`g > r + 12 && g > b + 12`), [`hue_report`] answered it by angle, and
/// they disagreed by an order of magnitude on the same frame: **green 0.00 % vs
/// 5.54 %**, blue 3.32 % vs 31.49 %. `49e878f` established that the angle is the right
/// instrument and wrote `hue_report`; the assertions that landed later were wired to
/// `class_of` while their floors were calibrated against `hue_report`'s numbers — the
/// docstring on `MIN_GREEN_PCT` (since replaced by [`MIN_KEPT_VS_CPU`]) still quotes 5.54 %, which `class_of` can never
/// produce. The probe was therefore red for a reason that was neither the renderer nor
/// the mesh: two classifiers, one floor.
///
/// The GPU chain lights, IBLs, fogs and ACES-tonemaps every fragment, so the rendered
/// LUMA and the 8-bit channel SPREAD are both free variables — `#C8E6C9` carries only
/// 30 LSB of spread to start with, and ~7 survive. The HUE survives: the same pixel is
/// 1.7 deg from `green` and 102 deg from `land`. A rule that demands 12 LSB is measuring
/// the tonemap, not the palette.
fn hue_class(rgb: [u8; 3]) -> Option<&'static str> {
    if i32::from(rgb[0]) + i32::from(rgb[1]) + i32::from(rgb[2]) < 90 {
        return None; // near-black: panel chrome / unlit sky, no meaningful chromaticity
    }
    if chroma_radius(rgb) < CHROMA_FLOOR {
        return None; // washed out past the palest real cartographic class
    }
    let a = hue_angle(rgb);
    DAY.iter()
        .map(|(name, c)| (*name, angle_gap(a, hue_angle(*c))))
        .min_by(|x, y| x.1.total_cmp(&y.1))
        .map(|(name, _)| name)
}

/// **THE verdict.** Share of the pane per palette class by hue angle, plus the mean
/// retained chroma radius of the pixels assigned to each. Returns the per-class pixel
/// counts and the pane's pixel total so the caller asserts on the SAME numbers this
/// prints — a table and a verdict that could disagree is how the probe shipped red.
fn hue_report(img: &image::RgbaImage) -> (std::collections::BTreeMap<&'static str, usize>, usize) {
    let (w, h) = (img.width(), img.height());
    let pal: Vec<f64> = DAY.iter().map(|(_, c)| hue_angle(*c)).collect();
    let mut share = vec![0usize; DAY.len()];
    let mut radius = vec![0.0f64; DAY.len()];
    let (mut dark, mut achromatic) = (0usize, 0usize);
    for y in 0..h {
        for x in 0..w {
            let p = img.get_pixel(x, y).0;
            let rgb = [p[0], p[1], p[2]];
            if i32::from(p[0]) + i32::from(p[1]) + i32::from(p[2]) < 90 {
                dark += 1;
                continue;
            }
            let rad = chroma_radius(rgb);
            if rad < CHROMA_FLOOR {
                achromatic += 1;
                continue;
            }
            // ONE classifier (L5): the table below and the assertion at the end read
            // the same `hue_class`, so the printed number and the failed number can
            // never be two different measurements again.
            let Some(name) = hue_class(rgb) else { continue };
            let i = DAY.iter().position(|(n, _)| *n == name).expect("hue_class returns a DAY name");
            share[i] += 1;
            radius[i] += rad;
        }
    }
    let n = f64::from(w * h);
    println!("    -- VERDICT: nearest palette class BY HUE ANGLE --");
    println!("    {:<14}{:>9}{:>10}{:>14}{:>14}{:>9}", "class", "share %", "hue deg", "src radius", "px radius", "kept");
    let warm: [&str; 4] = ["land", "building", "road_generic", "path"];
    let mut warm_share = 0usize;
    for (i, (name, hex)) in DAY.iter().enumerate() {
        if warm.contains(name) {
            warm_share += share[i];
        }
        if share[i] == 0 {
            continue;
        }
        let src = chroma_radius(*hex);
        let got = radius[i] / share[i] as f64;
        println!(
            "    {name:<14}{:>9.2}{:>10.1}{:>14.4}{:>14.4}{:>8.0}%",
            100.0 * share[i] as f64 / n,
            pal[i].to_degrees(),
            src,
            got,
            100.0 * got / src.max(1e-9)
        );
    }
    println!(
        "    {:<14}{:>9.2}   (land+building+road+path — within 13 deg of each other,\n\
         {:>32}so hue cannot tell them apart and does not pretend to)",
        "= warm",
        100.0 * warm_share as f64 / n,
        ""
    );
    println!("    {:<14}{:>9.2}   (below CHROMA_FLOOR — no meaningful hue)", "(achromatic)", 100.0 * achromatic as f64 / n);
    println!("    {:<14}{:>9.2}", "(near-black)", 100.0 * dark as f64 / n);
    let mut counts: std::collections::BTreeMap<&'static str, usize> = Default::default();
    for (i, (name, _)) in DAY.iter().enumerate() {
        counts.insert(name, share[i]);
    }
    counts.insert("= warm", warm_share);
    (counts, (w * h) as usize)
}

/// The ORIGINAL nearest-by-chromaticity-distance table, kept as the control that shows
/// why it cannot be the verdict: run it next to [`hue_report`] on the same frame and
/// the near-neutral `land` class swallows every desaturated pixel.
fn nearest_chroma_report(img: &image::RgbaImage) {
    let (w, h) = (img.width(), img.height());
    let pal: Vec<(f64, f64)> = DAY.iter().map(|(_, c)| chroma(*c)).collect();
    let mut share = vec![0usize; DAY.len()];
    let mut radius = vec![0.0f64; DAY.len()];
    let mut dark = 0usize;
    for y in 0..h {
        for x in 0..w {
            let p = img.get_pixel(x, y).0;
            let rgb = [p[0], p[1], p[2]];
            // Near-black pixels have no meaningful chromaticity (the panel chrome and
            // the unlit sky) — counted separately rather than assigned to a class.
            if i32::from(p[0]) + i32::from(p[1]) + i32::from(p[2]) < 90 {
                dark += 1;
                continue;
            }
            let (cr, cg) = chroma(rgb);
            let (i, _) = pal
                .iter()
                .enumerate()
                .map(|(i, (pr, pg))| (i, (cr - pr).powi(2) + (cg - pg).powi(2)))
                .min_by(|a, b| a.1.total_cmp(&b.1))
                .unwrap();
            share[i] += 1;
            radius[i] += chroma_radius(rgb);
        }
    }
    let n = f64::from(w * h);
    println!("    -- CONTROL: nearest palette class by CHROMATICITY DISTANCE (biased) --");
    println!("    {:<14}{:>9}{:>14}{:>14}{:>9}", "class", "share %", "src radius", "px radius", "kept");
    for (i, (name, hex)) in DAY.iter().enumerate() {
        if share[i] == 0 {
            continue;
        }
        let src = chroma_radius(*hex);
        let got = radius[i] / share[i] as f64;
        println!(
            "    {name:<14}{:>9.2}{:>14.4}{:>14.4}{:>8.0}%",
            100.0 * share[i] as f64 / n,
            src,
            got,
            100.0 * got / src.max(1e-9)
        );
    }
    println!("    {:<14}{:>9.2}", "(near-black)", 100.0 * dark as f64 / n);
}

/// **The camera the probe frames through — korp's path takes one, so this must too.**
///
/// It used to pass `None`, which is the DENSITY fallback: the `WindowSource::Densest`
/// branch, reached in korp only on a country overview aimed at open water. That was
/// tolerable while the town window was 3 km across and swallowed a park whatever cell it
/// landed in. It stopped being tolerable when the window came down to 1.16 km
/// (`extrude::TOWN_HALF_MAX_M`, 2026-08-05): the densest cell of building centroids in a
/// Swedish extract is a dormitory grid with **no ground cover in it at all**, and the
/// probe read `green 0.00 %` — on BOTH arms, the GPU one and the CPU-painter control.
/// Two arms agreeing at zero is the control doing its job: the class was missing from
/// the MESH, not eaten by the shading. A narrower window really does cut the parks out
/// of a framing chosen by building density, which is a fact about the fallback and not
/// about the renderer this probe measures.
///
/// So the probe aims where korp aims: a camera. `GROUND_PROBE_LON` / `_LAT` / `_SPAN_M`
/// override it; the default is central Stockholm at
/// `extrude::TOWN_ZOOM_WIDE_SPAN_M`-scale, i.e. a region overview, where the window sits
/// at its cap — the framing the user gets at every rung korp's pyramid serves.
fn probe_view() -> Option<facett_map3d::extrude::ViewWindow> {
    let f = |k: &str, d: f64| std::env::var(k).ok().and_then(|s| s.parse().ok()).unwrap_or(d);
    Some(facett_map3d::extrude::ViewWindow {
        lon: f("GROUND_PROBE_LON", 18.0686),
        lat: f("GROUND_PROBE_LAT", 59.3293),
        span_m: f("GROUND_PROBE_SPAN_M", facett_map3d::extrude::TOWN_ZOOM_WIDE_SPAN_M),
    })
}

/// **This probe REPORTS RED on the shipped lane, as of 2026-08-04, and that is the
/// point.** With the floors below wired to the measured shares, a real Stockholm
/// extract through the real GPU lane gives:
///
/// ```text
///   VERDICT: green 0.00 %  blue 3.32 %  warm 0.23 %  (floors 0.5 / 2 / 0.1)
///   neutral (grey/terrain)  473795  (75.21 %)
///   near-black              133855  (21.25 %)
/// ```
///
/// while `ground_loss_probe` proves the SAME extract reaches the mesh with **26.26 %**
/// of the design box coloured as cover. Mesh 26 %, pane 0 % — the loss is downstream of
/// the mesh, in the shading chain, exactly where this probe's ablation arms look. The
/// run above was repeated against `origin/main` (6ae6db6) with the same extract and
/// gives `green 0.00 %  warm 0.50 %`, so the red is pre-existing, not a regression of
/// the unit work on this branch.
///
/// It is `#[ignore]`d, `#![cfg(feature = "wgpu")]` and needs `GROUND_PROBE_WAYS`, so no
/// default command runs it — but when someone does run it, it now returns a VERDICT
/// instead of a table for a human to misread as a pass.
#[test]
#[ignore = "needs a local ways.parquet (GROUND_PROBE_WAYS) and a GPU adapter"]
fn what_the_3d_pane_paints() {
    let path = std::env::var("GROUND_PROBE_WAYS")
        .expect("set GROUND_PROBE_WAYS to a GeoParquet ways file");
    let cap: usize = std::env::var("GROUND_PROBE_MAX")
        .ok()
        .and_then(|s| s.parse().ok())
        .unwrap_or(320_000);
    let pal = MapPalette::cartographic(true);
    let ways = facett_osm::read_ways_with(&path, cap, &pal).expect("read ways");

    // THE korp CALL — `Korp::rebuild_map3d`, verbatim (korp/src/main.rs). `_roofed` is
    // the coloured spelling korp switched to: the flat one extrudes every building in
    // the 2D cartographic fill (`#E2DBCF` on `#F2EFE9`) and never reaches the OSM
    // colours or the roof palette, which is the white plain this probe was written for.
    let map = Map3D::from_geometry_in_view_roofed("Map — 3D (OSM buildings extruded)", &ways, probe_view());
    println!("  ways in            {}", ways.len());
    println!("  triangles          {}", map.triangle_count());
    println!("  metres_per_du      {:.1}", map.metres_per_design_unit());
    println!("  camera.distance    {:.4}", map.cam.distance);
    println!("  camera.elevation   {:.4} rad", map.cam.elevation);
    println!("  altitude_m         {:.1}", map.altitude_m());
    println!("  lod_tier           {}", map.lod_tier().name());

    let out = std::env::var("GROUND_PROBE_PNG")
        .unwrap_or_else(|_| "/tmp/ground_pixels_probe.png".to_string());
    // The CPU-painter control FIRST (same mesh, same camera, no wgpu shading chain).
    let cpu = render_with(
        Map3D::from_geometry_in_view_roofed("Map — 3D (OSM buildings extruded)", &ways, probe_view()),
        false,
    );
    let cpu_out = out.replace(".png", "-cpu.png");
    cpu.save(&cpu_out).expect("write cpu png");
    let (cpu_hue, cpu_n) = tally_pane("CPU PAINTER (no wgpu shading)", &cpu);
    println!("  wrote {cpu_out}");

    let img = render(map);
    img.save(&out).expect("write png");
    println!("  wrote {out}");
    // The verdict below reads THESE counts — the same frame, the same classifier.
    let (hue, hue_n) = tally_pane("GPU (the shipped korp lane)", &img);

    // ── ABLATION: which half of the GPU chain eats the palette. Each variant is the
    //    SAME scene with one lever moved, rendered through the same path, so the
    //    difference in the chroma table is attributable to that lever alone.
    let fresh = || Map3D::from_geometry_in_view_roofed("Map — 3D (OSM buildings extruded)", &ways, probe_view());
    for (name, m) in [
        ("GPU, sky OFF", fresh().with_sky(false)),
        ("GPU, ssao OFF", fresh().with_ssao(false)),
        ("GPU, bloom OFF", fresh().with_bloom(false)),
        ("GPU, shadows OFF", fresh().with_shadows(0.0)),
        (
            "GPU, sky+bloom+ssao+shadows OFF",
            fresh().with_sky(false).with_bloom(false).with_ssao(false).with_shadows(0.0),
        ),
    ] {
        let f = render_with(m, true);
        f.save(out.replace(".png", &format!("-{}.png", name.replace([' ', ',', '+'], "_"))))
            .ok();
        tally_pane(name, &f);
    }

    let (w, h) = (img.width(), img.height());
    let mut tally: std::collections::BTreeMap<&'static str, usize> = Default::default();
    // Split the pane in half vertically so "blue" can be read as sky (top) or water
    // (bottom) rather than as one ambiguous number.
    let mut top: std::collections::BTreeMap<&'static str, usize> = Default::default();
    let mut bot: std::collections::BTreeMap<&'static str, usize> = Default::default();
    for y in 0..h {
        for x in 0..w {
            let c = class_of(img.get_pixel(x, y).0);
            *tally.entry(c).or_default() += 1;
            if y < h / 2 { *top.entry(c).or_default() += 1 } else { *bot.entry(c).or_default() += 1 }
        }
    }
    let n = (w * h) as f64;
    println!("\n  PANE {w}x{h}");
    for (k, v) in &tally {
        println!("    {k:<28} {v:>8}  ({:>6.2} %)", 100.0 * *v as f64 / n);
    }
    println!("  upper half / lower half:");
    for k in tally.keys() {
        let t = top.get(k).copied().unwrap_or(0);
        let b = bot.get(k).copied().unwrap_or(0);
        println!(
            "    {k:<28} top {:>6.2} %   bottom {:>6.2} %",
            200.0 * t as f64 / n,
            200.0 * b as f64 / n
        );
    }

    // ── THE VERDICT. Everything above is a table; this is the sentence. ───────────
    //
    // This file had ZERO assertions — `grep -c assert` = 0 — and its only failure paths
    // were `.expect("read ways")` / `.expect("wgpu render")`: infrastructure, never the
    // measured quantity. It would have printed `green 0.00 %` and exited 0, which is
    // precisely the report it was written to investigate. A heading carries a verdict
    // and a heading cannot go red.
    //
    // The floors are deliberately far below the measured shares (green 5.54 %, blue
    // 31.49 % on 2026-08-04): this is a probe for a class DISAPPEARING, not a golden of
    // one extract's composition.
    //
    // ── …AND THE NUMBERS THEY ARE COMPARED AGAINST ARE `hue_class`'s ─────────────
    //
    // They used to be `class_of`'s, off the `tally` above, while every floor in this
    // file was calibrated against the hue table. Same frame, two classifiers, an order
    // of magnitude apart: `green` 0.00 % by channel gap, 5.54 % by hue angle; `blue`
    // 3.32 % vs 31.49 %. So `green > 0.5` was a comparison between a number this probe
    // never measured and a number it could not produce, and it failed for a reason that
    // was neither the renderer nor the mesh. Blue and warm passed only because their
    // floors happened to sit under the deflated readings — three assertions, one of
    // them red, none of them measuring what its own docstring said.
    //
    // The verdict now reads the SAME `hue_class` the table printed (L5, one writer).
    // `class_of` survives above as a printed control, because the gap between the two
    // tables IS the desaturation finding and is worth seeing.
    // ── …AND THE VERDICT IS A RATIO AGAINST THE CPU CONTROL, not an absolute share ──
    //
    // The floors used to be absolute percentages of the pane, calibrated once against
    // one extract and one framing. Both moved (2026-08-05: the town window came down
    // from a 3 km scene to 1.16 km, and the probe started aiming a CAMERA instead of
    // taking the density fallback), and the readings moved with them — `green` went from
    // 1.32 % to 0.55 % against a 0.5 % floor, a green with 9 % of headroom, which is the
    // kind of green that is one reframing away from a red that means nothing.
    //
    // The question this probe exists to ask is not "how much park is in this extract".
    // It is **"does the shading chain deliver the colour the mesh has"**, and that has a
    // denominator already rendered three screens up: the CPU-painter arm — same mesh,
    // same camera, same per-triangle colours, no wgpu lighting/IBL/fog/ACES at all. A
    // ratio against it is independent of the extract, the framing and the window size,
    // and it goes to zero for exactly the failure being watched for.
    let n_hue = hue_n as f64;
    let share = |t: &std::collections::BTreeMap<&'static str, usize>, n: f64, k: &str| {
        100.0 * t.get(k).copied().unwrap_or(0) as f64 / n
    };
    let cpu_n = cpu_n as f64;
    println!("\n  {:<10}{:>12}{:>12}{:>12}", "class", "CPU %", "GPU %", "GPU/CPU");
    let mut ratio = |k: &str| {
        let (c, g) = (share(&cpu_hue, cpu_n, k), share(&hue, n_hue, k));
        let r = if c > 0.0 { g / c } else { f64::INFINITY };
        println!("  {k:<10}{c:>12.2}{g:>12.2}{r:>12.2}");
        (c, g, r)
    };
    let (_, green, green_r) = ratio("green");
    let (_, blue, blue_r) = ratio("water");
    let (_, warm, warm_r) = ratio("= warm");
    println!(
        "\n  VERDICT: green {green:.2} %  blue {blue:.2} %  warm {warm:.2} %  \
         (kept vs the CPU control: {green_r:.2} / {blue_r:.2} / {warm_r:.2}, floor {MIN_KEPT_VS_CPU})"
    );
    // MEASURED 2026-08-05 (RTX 4090 [discrete/Vulkan], 320 000-way Stockholm extract,
    // camera at the wide anchor): green 0.60, water 0.51, warm 0.42. The floor sits at
    // 0.25 — well under all three and far above the zero this watches for.
    assert!(
        green_r > MIN_KEPT_VS_CPU,
        "the GPU chain keeps {green_r:.2} of the cover the SAME mesh paints on the CPU \
         ({green:.2} % vs {:.2} %) — this IS the `0.00 % green` report, restated as a \
         number the build can fail on and one no reframing can move",
        share(&cpu_hue, cpu_n, "green"),
    );
    assert!(blue_r > MIN_KEPT_VS_CPU, "the GPU chain keeps {blue_r:.2} of the sky+water the mesh has");
    assert!(warm_r > MIN_KEPT_VS_CPU, "the GPU chain keeps {warm_r:.2} of the roofs/roads/buildings the mesh has");
}

/// **Floor under the share of a class the GPU chain must KEEP relative to the CPU
/// painter** — the one number the verdict reads, replacing three absolute pane-share
/// floors that were calibrated against one extract and one framing and could not survive
/// either changing.
///
/// The CPU arm is the same mesh, the same camera and the same per-triangle colours with
/// no wgpu lighting, IBL, fog or ACES in the way, so the ratio isolates the shading chain
/// and nothing else. It is not expected to be 1.0 — a lit, fogged, tonemapped render
/// legitimately moves pixels between hue families, and the sky is analytic on one arm and
/// a flat gradient on the other. It is expected to be far from 0.
///
/// MEASURED 2026-08-05 (RTX 4090 [discrete/Vulkan], `sweden-recut/ways.parquet`, 320 000
/// ways, camera at `extrude::TOWN_ZOOM_WIDE_SPAN_M` over central Stockholm): green
/// **0.60**, water **0.51**, warm **0.42**. Seen RED at 0.45 on that reading (warm
/// fails first), so the floor is a line the build can cross.
const MIN_KEPT_VS_CPU: f64 = 0.25;