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/rail_dash_pixels.rs

//! **A RAIL'S CORE IS DASHED ALONG ITS ARC LENGTH, ON BOTH LANES** — HO-T3, the dash lane
//! GG #28 measured absent (*"`LineVertex` is 48 B with no arc length, `line.wgsl` has no
//! dash term"*).
//!
//! # The law under test
//!
//! `style::dash_period_px`: a dashed way's core alternates lit / casing with a period of
//! `DASH_PERIOD_RATIO · w_px` **along the way**, `w_px` being the one resolved width both
//! lanes already agree on (GG #27). "Along the way" is the whole claim — a period taken
//! from screen `x` would give a 45° rail `1/√2` as many dashes as a horizontal one of the
//! same length, and a period taken from the pane would crawl under every pan.
//!
//! # What is measured
//!
//! Real device pixels through the shipped `LineRenderer` (casing pass + fill pass, the
//! same two draws the live lane submits) on geometry baked by `build_gpu_geometry` from a
//! `Way` whose kind is [`WayKind::Rail`] — so the KIND gate is under test, not a flag the
//! test set itself. The centreline is walked at 1 px steps and each sample classified lit
//! (the core ink) or dark (the casing showing through); the maximal lit runs are counted.
//!
//! 1. **a horizontal rail** carries `ceil(L_px / period)` lit runs (the way starts lit);
//! 2. **a 45° rail of the SAME Mercator length** carries the same number — arc length, not
//!    screen x;
//! 3. **a road of the same geometry** carries exactly one run — dash off;
//! 4. **the same rail panned by a fractional pixel offset** puts every run start within
//!    1.5 px of where it was, measured along the way — no crawling.
//!
//! Then the CPU painter, through the `egui_kittest` harness `rail_tier_pixels.rs` uses:
//! the row through a horizontal rail's band alternates the rail ink with the casing ink
//! at the same period, `dash_period_px(cpu width)`.
//!
//! # THE RED
//!
//! Before the dash lane: `a_rail_is_dashed_along_its_arc_length_on_the_gpu_lane` —
//! `the horizontal rail carries 1 lit run(s) along 400 px; period 48 px says 9` — the core
//! was one uniform stroke on every sample. The CPU arm read 1 run the same way.
//!
//! Run: `env -u WAYLAND_DISPLAY -u DISPLAY FACETT_TEST_GPU=integrated cargo test -p facett-map
//! --features wgpu --test rail_dash_pixels -- --test-threads=1 --nocapture`
#![cfg(feature = "wgpu")]

use facett_map::gpu::line::LineRenderer;
use facett_map::gpu::{build_gpu_geometry, ways_origin, LineVertex, MapTransform};
use facett_map::layer::{LatLon, MapFacet, MapTheme, MapView, WaysLayer};
use facett_map::style::{dash_period_px, DASH_DUTY, DASH_PERIOD_RATIO};
use facett_map::{Way, WayKind};

mod common;

const W: u32 = 512;
const H: u32 = 512;
/// The on-screen length every GPU fixture is fitted to.
const L_PX: f32 = 400.0;
/// Themed width, bumped like `rail_tier_pixels.rs` bumps its rail's so the core is wider
/// than the AA feather and a centreline sample reads PURE ink.
const STYLE_W: f32 = 12.0;
const FEATHER_PX: f32 = 1.0;
/// The core ink the fixtures are recoloured to — bright, so lit/dark is a luma verdict.
const CORE: egui::Color32 = egui::Color32::from_rgb(0xF0, 0xF0, 0xE0);
const CASING: [f32; 4] = [0.10, 0.11, 0.14, 1.0];
/// A mid-dark paper, far from the core in luma and never on the centreline anyway.
const PAPER: [f64; 4] = [0.20, 0.35, 0.55, 1.0];

const START: (f64, f64) = (18.00, 59.32);

/// A straight way of Mercator length `len` from `START` in direction `(dx, dy)` (unit,
/// Mercator), `n` points, as `kind`. Points are spaced in MERCATOR so the two fixtures
/// carry the same arc length to f64 precision.
fn straight(id: i64, dir: [f64; 2], len: f64, tags: &[(&str, &str)], pal: &facett_map::MapPalette) -> Way {
    let (mx, my) = facett_map::mercator(START.0, START.1);
    let n = 11;
    let pts: Vec<(f64, f64)> = (0..n)
        .map(|i| {
            let t = f64::from(i) / f64::from(n - 1);
            facett_map::unmercator(mx + dir[0] * len * t, my + dir[1] * len * t)
        })
        .collect();
    let mut w = Way::from_lonlat_with(id, &pts, tags, pal).expect("a straight way");
    w.style.width = STYLE_W;
    w.style.color = CORE;
    w
}

/// Mercator length of the horizontal fixture: 0.2° of longitude.
fn fixture_len() -> f64 {
    let (a, _) = facett_map::mercator(START.0, START.1);
    let (b, _) = facett_map::mercator(START.0 + 0.2, START.1);
    b - a
}

const RAIL: &[(&str, &str)] = &[("railway", "rail")];
const ROAD: &[(&str, &str)] = &[("highway", "primary")];

/// The transform that puts `way`'s midpoint at the pane centre (+ `pan` px) and fits its
/// length to `L_PX` — north-up, flat, uniform zoom.
fn fit(way: &Way, origin: [f64; 2], pan: [f32; 2]) -> MapTransform {
    let zoom = L_PX / fixture_len() as f32;
    let (a, b) = (way.lonlat[0], way.lonlat[way.lonlat.len() - 1]);
    let (ax, ay) = facett_map::mercator(a.0, a.1);
    let (bx, by) = facett_map::mercator(b.0, b.1);
    MapTransform {
        zoom_x: zoom,
        zoom_y: zoom,
        ref_x: (0.5 * (ax + bx) - origin[0]) as f32,
        ref_y: (0.5 * (ay + by) - origin[1]) as f32,
        screen_center_x: W as f32 * 0.5 + pan[0],
        screen_center_y: H as f32 * 0.5 + pan[1],
        bearing_deg: 0.0,
        tilt_deg: 0.0,
    }
}

/// Render `verts` through the shipped two-pass line renderer into an RGBA8 image. The
/// same attachment recipe as `line_gpu.rs` (4× MSAA colour + depth, resolved into a
/// single-sample readback target).
fn render(device: &wgpu::Device, queue: &wgpu::Queue, verts: &[LineVertex], t: &MapTransform) -> image::RgbaImage {
    use wgpu::util::DeviceExt;
    let format = wgpu::TextureFormat::Rgba8Unorm;
    let renderer = LineRenderer::new(device, format);
    let vbuf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
        label: Some("rail_dash_verts"),
        contents: bytemuck::cast_slice(verts),
        usage: wgpu::BufferUsages::VERTEX,
    });
    renderer.write_uniforms(queue, t, W as f32, H as f32, 3.0, CASING, FEATHER_PX, 1.0);
    let tex = device.create_texture(&wgpu::TextureDescriptor {
        label: Some("rail_dash_target"),
        size: wgpu::Extent3d { width: W, height: H, depth_or_array_layers: 1 },
        mip_level_count: 1,
        sample_count: facett_core::render::gpu::NO_MSAA_SAMPLES,
        dimension: wgpu::TextureDimension::D2,
        format,
        usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
        view_formats: &[],
    });
    let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
    let msaa = device
        .create_texture(&wgpu::TextureDescriptor {
            label: Some("rail_dash_msaa"),
            size: wgpu::Extent3d { width: W, height: H, depth_or_array_layers: 1 },
            mip_level_count: 1,
            sample_count: facett_map::gpu::MAP_MSAA_SAMPLES,
            dimension: wgpu::TextureDimension::D2,
            format,
            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
            view_formats: &[],
        })
        .create_view(&wgpu::TextureViewDescriptor::default());
    let depth = device
        .create_texture(&wgpu::TextureDescriptor {
            label: Some("rail_dash_depth"),
            size: wgpu::Extent3d { width: W, height: H, depth_or_array_layers: 1 },
            mip_level_count: 1,
            sample_count: facett_map::gpu::MAP_MSAA_SAMPLES,
            dimension: wgpu::TextureDimension::D2,
            format: facett_map::gpu::MAP_DEPTH_FORMAT,
            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
            view_formats: &[],
        })
        .create_view(&wgpu::TextureViewDescriptor::default());
    let bpp = 4u32;
    let padded = (W * bpp).div_ceil(256) * 256;
    let read_buf = device.create_buffer(&wgpu::BufferDescriptor {
        label: Some("rail_dash_readback"),
        size: u64::from(padded * H),
        usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
        mapped_at_creation: false,
    });
    let mut enc = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("rail_dash_enc") });
    {
        let mut pass = enc.begin_render_pass(&wgpu::RenderPassDescriptor {
            label: Some("rail_dash_pass"),
            color_attachments: &[Some(wgpu::RenderPassColorAttachment {
                view: &msaa,
                depth_slice: None,
                resolve_target: Some(&view),
                ops: wgpu::Operations {
                    load: wgpu::LoadOp::Clear(wgpu::Color { r: PAPER[0], g: PAPER[1], b: PAPER[2], a: PAPER[3] }),
                    store: wgpu::StoreOp::Store,
                },
            })],
            depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
                view: &depth,
                depth_ops: Some(wgpu::Operations { load: wgpu::LoadOp::Clear(1.0), store: wgpu::StoreOp::Discard }),
                stencil_ops: None,
            }),
            timestamp_writes: None,
            occlusion_query_set: None,
            multiview_mask: None,
        });
        renderer.draw(&mut pass, &vbuf, verts.len() as u32);
    }
    enc.copy_texture_to_buffer(
        wgpu::TexelCopyTextureInfo { texture: &tex, mip_level: 0, origin: wgpu::Origin3d::ZERO, aspect: wgpu::TextureAspect::All },
        wgpu::TexelCopyBufferInfo {
            buffer: &read_buf,
            layout: wgpu::TexelCopyBufferLayout { offset: 0, bytes_per_row: Some(padded), rows_per_image: Some(H) },
        },
        wgpu::Extent3d { width: W, height: H, depth_or_array_layers: 1 },
    );
    queue.submit([enc.finish()]);
    let slice = read_buf.slice(..);
    slice.map_async(wgpu::MapMode::Read, |_| {});
    device.poll(wgpu::PollType::wait_indefinitely()).ok();
    let data = slice.get_mapped_range().expect("wgpu: get_mapped_range on a mapped buffer");
    let mut img = image::RgbaImage::new(W, H);
    for y in 0..H {
        for x in 0..W {
            let o = (y * padded + x * bpp) as usize;
            img.put_pixel(x, y, image::Rgba([data[o], data[o + 1], data[o + 2], data[o + 3]]));
        }
    }
    img
}

fn luma(p: &image::Rgba<u8>) -> f32 {
    0.2126 * f32::from(p[0]) + 0.7152 * f32::from(p[1]) + 0.0722 * f32::from(p[2])
}

/// Lit (`Some(true)`), dark (`Some(false)`), or an AA blend nobody should vote on.
fn classify(p: &image::Rgba<u8>) -> Option<bool> {
    let l = luma(p);
    if l >= 160.0 {
        Some(true)
    } else if l <= 100.0 {
        Some(false)
    } else {
        None
    }
}

/// Walk `way`'s centreline at 1 px steps under `t`, 8 px in from each end so the round
/// caps stay out of the census, and return the arc position (px from the way's start) at
/// which each maximal LIT run begins.
fn lit_run_starts(img: &image::RgbaImage, way: &Way, origin: [f64; 2], t: &MapTransform) -> Vec<f32> {
    let (a, b) = (way.lonlat[0], way.lonlat[way.lonlat.len() - 1]);
    let (ax, ay) = facett_map::mercator(a.0, a.1);
    let (bx, by) = facett_map::mercator(b.0, b.1);
    let pa = t.project([(ax - origin[0]) as f32, (ay - origin[1]) as f32]);
    let pb = t.project([(bx - origin[0]) as f32, (by - origin[1]) as f32]);
    let len = ((pb[0] - pa[0]).powi(2) + (pb[1] - pa[1]).powi(2)).sqrt();
    let mut starts = Vec::new();
    let mut prev: Option<bool> = None;
    let mut s = 8.0f32;
    while s <= len - 8.0 {
        let f = s / len;
        let x = pa[0] + (pb[0] - pa[0]) * f;
        let y = pa[1] + (pb[1] - pa[1]) * f;
        let (xi, yi) = (x.round() as i64, y.round() as i64);
        assert!(xi >= 0 && yi >= 0 && xi < i64::from(W) && yi < i64::from(H), "the fixture left the pane at s={s}");
        if let Some(lit) = classify(img.get_pixel(xi as u32, yi as u32)) {
            if lit && prev != Some(true) {
                starts.push(s);
            }
            prev = Some(lit);
        }
        s += 1.0;
    }
    starts
}

/// The number of lit runs the law predicts along `len_px` of a way that starts lit:
/// every `k · period < len` begins one.
fn predicted_runs(len_px: f32, period_px: f32) -> usize {
    (0..).take_while(|k| *k as f32 * period_px < len_px).count()
}

fn bake(way: &Way, origin: [f64; 2]) -> Vec<LineVertex> {
    build_gpu_geometry(std::iter::once((way, way.style.lod as u32)), origin).line_vertices
}

#[test]
fn a_rail_is_dashed_along_its_arc_length_on_the_gpu_lane() {
    let Some((device, queue)) = common::device_or_skip(
        "rail_dash_gpu",
        "a WayKind::Rail core alternates lit/casing at dash_period_px along its arc length; a road does not",
    ) else {
        return;
    };
    let pal = facett_map::MapPalette::cartographic(false);
    let len = fixture_len();
    let inv_sqrt2 = std::f64::consts::FRAC_1_SQRT_2;
    let horizontal = straight(1, [1.0, 0.0], len, RAIL, &pal);
    let diagonal = straight(2, [inv_sqrt2, inv_sqrt2], len, RAIL, &pal);
    let road = straight(3, [1.0, 0.0], len, ROAD, &pal);
    assert_eq!(horizontal.style.kind, WayKind::Rail, "railway=rail classifies as the Rail kind");
    assert_eq!(road.style.kind, WayKind::Road, "highway=primary classifies as a Road");
    assert!(horizontal.style.dashed() && !road.style.dashed(), "the style gate: rail dashes, road does not");

    let origin = ways_origin([&horizontal, &diagonal, &road].into_iter()).expect("an origin");
    // width_scale is 1.0 in `render`, so the resolved width IS the themed width (in band).
    let w_px = facett_map::style::line_width_px_scaled(STYLE_W, 1.0);
    let period = dash_period_px(w_px);
    let want = predicted_runs(L_PX, period);

    let t_h = fit(&horizontal, origin, [0.0, 0.0]);
    let img_h = render(&device, &queue, &bake(&horizontal, origin), &t_h);
    let runs_h = lit_run_starts(&img_h, &horizontal, origin, &t_h);
    eprintln!("[rail-dash] horizontal rail: {} lit runs at {:?} (period {period} px, want {want})", runs_h.len(), runs_h);
    assert!(
        (runs_h.len() as i64 - want as i64).abs() <= 1,
        "the horizontal rail carries {} lit run(s) along {L_PX} px; period {period} px \
         (= {DASH_PERIOD_RATIO} × {w_px} px width) says {want}. A uniform core reads 1 here.",
        runs_h.len()
    );
    // Consecutive lit runs start one period apart. The FIRST run is skipped: the census
    // begins 8 px in (past the round cap), so its recorded start is the inset, not 0.
    for pair in runs_h[1..].windows(2) {
        let gap = pair[1] - pair[0];
        assert!((gap - period).abs() <= 2.0, "lit runs start {gap} px apart; the period is {period} px");
    }

    let t_d = fit(&diagonal, origin, [0.0, 0.0]);
    let img_d = render(&device, &queue, &bake(&diagonal, origin), &t_d);
    let runs_d = lit_run_starts(&img_d, &diagonal, origin, &t_d);
    eprintln!("[rail-dash] diagonal rail: {} lit runs at {:?}", runs_d.len(), runs_d);
    assert_eq!(
        runs_d.len(),
        runs_h.len(),
        "a 45° rail of the same Mercator length carries {} lit runs where the horizontal one \
         carries {} — a period taken from screen x, not arc length, reads 1/√2 as many",
        runs_d.len(),
        runs_h.len()
    );

    let img_r = render(&device, &queue, &bake(&road, origin), &t_h);
    let runs_r = lit_run_starts(&img_r, &road, origin, &t_h);
    assert_eq!(runs_r.len(), 1, "a road of the same geometry is ONE lit run, not {} — dash is off for roads", runs_r.len());

    // ── NO CRAWLING: pan by a fractional offset, the dashes stay where the way is.
    let t_p = fit(&horizontal, origin, [37.3, -21.7]);
    let img_p = render(&device, &queue, &bake(&horizontal, origin), &t_p);
    let runs_p = lit_run_starts(&img_p, &horizontal, origin, &t_p);
    assert_eq!(runs_p.len(), runs_h.len(), "a pan changed the dash count {} → {}", runs_h.len(), runs_p.len());
    for (a, b) in runs_h.iter().zip(&runs_p) {
        assert!((a - b).abs() <= 1.5, "a lit run crawled under pan: started at {a} px along the way, now {b} px");
    }

    if std::env::var("FACETT_MAP_DUMP_PNG").is_ok() {
        let dir = std::env::temp_dir();
        img_h.save(dir.join("rail-dash-horizontal.png")).ok();
        img_d.save(dir.join("rail-dash-diagonal.png")).ok();
        img_r.save(dir.join("rail-dash-road.png")).ok();
    }
}

/// **The WGSL twins of `DASH_PERIOD_RATIO` / `DASH_DUTY` are the Rust values**, read out
/// of the shader text the pipeline compiles — the arrangement `MIN_COVERAGE_ALPHA` and
/// `CASING_DEPTH_BIAS` already live under.
#[test]
fn the_shader_carries_the_dash_law_as_the_style_module_declares_it() {
    let src = facett_map::gpu::line::LINE_WGSL;
    let read = |name: &str| -> f32 {
        let needle = format!("const {name}: f32 = ");
        let at = src.find(&needle).unwrap_or_else(|| panic!("line.wgsl declares no `{needle}…;` — the dash law has no shader twin"));
        let rest = &src[at + needle.len()..];
        let end = rest.find(';').expect("a terminated const");
        rest[..end].trim().parse::<f32>().unwrap_or_else(|e| panic!("`{name}` is not a literal: {e}"))
    };
    assert_eq!(read("DASH_PERIOD_RATIO"), DASH_PERIOD_RATIO, "line.wgsl's DASH_PERIOD_RATIO drifted from style.rs");
    assert_eq!(read("DASH_DUTY"), DASH_DUTY, "line.wgsl's DASH_DUTY drifted from style.rs");
}

// ── The CPU painter ──────────────────────────────────────────────────────────

const CANVAS: egui::Vec2 = egui::vec2(900.0, 640.0);
const CPU_CAMERA: f32 = 12.0;

fn cpu_render(kind_tags: &[(&str, &str)]) -> (MapTheme, f32, image::RgbaImage) {
    let theme = facett_core::Theme::default();
    let map_theme = MapTheme::day(&theme);
    let pal = map_theme.palette;
    let lat = 59.32;
    let pts: Vec<(f64, f64)> = (0..=20).map(|i| (17.90 + f64::from(i) * 0.02, lat)).collect();
    let mut w = Way::from_lonlat_with(1, &pts, kind_tags, &pal).expect("a straight way");
    w.style.width = STYLE_W;
    let view = MapView::new(LatLon::new(lat, 18.10), CPU_CAMERA, (CANVAS.x, CANVAS.y));
    let mut f = MapFacet::new("rail-dash-cpu", view, map_theme.clone());
    let cpu_w = f.cpu_line_width_px_for_test(STYLE_W);
    f.push_layer(Box::new(WaysLayer::new("net-rails", vec![w])));
    let mut h = egui_kittest::Harness::builder().with_size(CANVAS).build_ui(move |ui| {
        f.ui(ui);
    });
    h.run_steps(3);
    (map_theme, cpu_w, h.render().expect("the CPU painter rendered"))
}

fn cheb(a: &image::Rgba<u8>, b: egui::Color32) -> i32 {
    (i32::from(a[0]) - i32::from(b.r()))
        .abs()
        .max((i32::from(a[1]) - i32::from(b.g())).abs())
        .max((i32::from(a[2]) - i32::from(b.b())).abs())
}

/// Along the row with the most `ink`, the maximal runs of `ink` (casing or anything else
/// between them).
fn ink_runs_on_best_row(img: &image::RgbaImage, ink: egui::Color32) -> (u32, usize) {
    let mut best = (0u32, 0usize);
    for y in 0..img.height() {
        let n = (0..img.width()).filter(|&x| cheb(img.get_pixel(x, y), ink) <= 10).count();
        if n > best.1 {
            best = (y, n);
        }
    }
    let y = best.0;
    let mut runs = 0;
    let mut prev = false;
    for x in 0..img.width() {
        let is_ink = cheb(img.get_pixel(x, y), ink) <= 10;
        if is_ink && !prev {
            runs += 1;
        }
        prev = is_ink;
    }
    (y, runs)
}

#[test]
fn the_cpu_painter_dashes_a_rail_at_the_same_period_and_leaves_a_road_solid() {
    let _gpu = common::one_gpu::hold();
    let (theme, cpu_w, rail) = cpu_render(RAIL);
    let (_, _, road) = cpu_render(ROAD);
    let ink = theme.palette.rail();
    let period = dash_period_px(cpu_w);
    // The way crosses the whole 900 px canvas, so the row carries ~canvas/period runs; the
    // phase at the left edge is whatever the off-canvas start left it, hence ±2.
    let want = (CANVAS.x / period).round() as i64;
    let (row, runs) = ink_runs_on_best_row(&rail, ink);
    eprintln!("[rail-dash] CPU rail row {row}: {runs} runs of {ink:?}; width {cpu_w} px, period {period} px, want ~{want}");
    assert!(
        (runs as i64 - want).abs() <= 2,
        "the CPU painter drew the rail's core as {runs} run(s) across {} px; dash_period_px({cpu_w}) = {period} says ~{want}",
        CANVAS.x
    );
    let road_ink = road_core_ink(&road);
    let (_, road_runs) = ink_runs_on_best_row(&road, road_ink);
    assert_eq!(road_runs, 1, "the CPU painter dashed a ROAD into {road_runs} runs");
}

/// The road's own ink, read off the picture: the most common non-land hue on the busiest
/// row, so the road test does not have to know which tier `highway=primary` resolves to.
fn road_core_ink(img: &image::RgbaImage) -> egui::Color32 {
    let mut counts: std::collections::HashMap<[u8; 3], usize> = std::collections::HashMap::new();
    for p in img.pixels() {
        *counts.entry([p[0], p[1], p[2]]).or_default() += 1;
    }
    // Land is the majority; the road core is the runner-up among saturated-enough pixels.
    let mut v: Vec<_> = counts.into_iter().collect();
    v.sort_by_key(|e| std::cmp::Reverse(e.1));
    let land = v[0].0;
    let road = v.iter().skip(1).find(|(c, _)| c.iter().zip(land).map(|(a, b)| (i32::from(*a) - i32::from(b)).abs()).max().unwrap() > 20).expect("a road hue").0;
    egui::Color32::from_rgb(road[0], road[1], road[2])
}

// ── The bench: 2 000 rail ways, wall clock per frame ─────────────────────────

/// **Frame time of the two-pass line lane over 2 000 rail ways** — the number HO-T3 asks
/// for before/after the dash term. Wall clock from `submit` to the fence, median of 40
/// frames after 5 warm-ups, no readback in the loop; the adapter is printed so a figure
/// cannot be quoted without its hardware.
#[test]
fn rail_lane_frame_time_over_2000_rail_ways() {
    let Some((device, queue)) = common::device_or_skip("rail_dash_bench", "frame time over 2 000 rail ways") else {
        return;
    };
    let pal = facett_map::MapPalette::cartographic(false);
    let (mx0, my0) = facett_map::mercator(START.0, START.1);
    let step = fixture_len() / 50.0;
    let mut ways = Vec::with_capacity(2000);
    for i in 0..2000i64 {
        let row = (i / 50) as f64;
        let col = (i % 50) as f64;
        let pts: Vec<(f64, f64)> = (0..8)
            .map(|k| {
                let t = f64::from(k) / 7.0;
                // One full sine period along the way (t runs 0..=1).
                facett_map::unmercator(mx0 + (col + t) * step, my0 + row * step + (t * std::f64::consts::TAU).sin() * step * 0.1)
            })
            .collect();
        let mut w = Way::from_lonlat_with(i, &pts, RAIL, &pal).expect("a rail way");
        w.style.width = 2.0;
        ways.push(w);
    }
    let origin = ways_origin(ways.iter()).expect("origin");
    let geo = build_gpu_geometry(ways.iter().map(|w| (w, w.style.lod as u32)), origin);
    let verts = geo.line_vertices;
    let extent = fixture_len() as f32 * (1.0 + 40.0 / 50.0);
    let t = MapTransform {
        zoom_x: W as f32 / extent,
        zoom_y: W as f32 / extent,
        ref_x: (mx0 - origin[0]) as f32 + extent * 0.5,
        ref_y: (my0 - origin[1]) as f32 + extent * 0.5,
        screen_center_x: W as f32 * 0.5,
        screen_center_y: H as f32 * 0.5,
        bearing_deg: 0.0,
        tilt_deg: 0.0,
    };
    use wgpu::util::DeviceExt;
    let format = wgpu::TextureFormat::Rgba8Unorm;
    let renderer = LineRenderer::new(&device, format);
    let vbuf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
        label: Some("bench_verts"),
        contents: bytemuck::cast_slice(&verts),
        usage: wgpu::BufferUsages::VERTEX,
    });
    renderer.write_uniforms(&queue, &t, W as f32, H as f32, 3.0, CASING, FEATHER_PX, 3.0);
    // Each target names its sample count at a `sample_count:` field — the form
    // `facett-core/tests/one_msaa_writer.rs` reads. The old helper closure took the count
    // as a parameter (`sample_count: samples`), which the law cannot tell from a pinned
    // literal (red on main since 5011265). The single-sample default is the ONE writer's
    // constant; the two map targets override it with the map's sanctioned 4x constant.
    let desc = |label, fmt| wgpu::TextureDescriptor {
        label: Some(label),
        size: wgpu::Extent3d { width: W, height: H, depth_or_array_layers: 1 },
        mip_level_count: 1,
        sample_count: facett_core::render::gpu::NO_MSAA_SAMPLES,
        dimension: wgpu::TextureDimension::D2,
        format: fmt,
        usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
        view_formats: &[],
    };
    let view = |d: wgpu::TextureDescriptor| device.create_texture(&d).create_view(&wgpu::TextureViewDescriptor::default());
    let resolve = view(desc("bench_resolve", format));
    let msaa = view(wgpu::TextureDescriptor {
        sample_count: facett_map::gpu::MAP_MSAA_SAMPLES,
        ..desc("bench_msaa", format)
    });
    let depth = view(wgpu::TextureDescriptor {
        sample_count: facett_map::gpu::MAP_MSAA_SAMPLES,
        ..desc("bench_depth", facett_map::gpu::MAP_DEPTH_FORMAT)
    });
    let mut samples_us = Vec::with_capacity(45);
    for _ in 0..45 {
        let mut enc = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("bench_enc") });
        {
            let mut pass = enc.begin_render_pass(&wgpu::RenderPassDescriptor {
                label: Some("bench_pass"),
                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
                    view: &msaa,
                    depth_slice: None,
                    resolve_target: Some(&resolve),
                    ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), store: wgpu::StoreOp::Store },
                })],
                depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
                    view: &depth,
                    depth_ops: Some(wgpu::Operations { load: wgpu::LoadOp::Clear(1.0), store: wgpu::StoreOp::Discard }),
                    stencil_ops: None,
                }),
                timestamp_writes: None,
                occlusion_query_set: None,
                multiview_mask: None,
            });
            renderer.draw(&mut pass, &vbuf, verts.len() as u32);
        }
        let t0 = std::time::Instant::now();
        queue.submit([enc.finish()]);
        device.poll(wgpu::PollType::wait_indefinitely()).ok();
        samples_us.push(t0.elapsed().as_micros() as u64);
    }
    let mut steady: Vec<u64> = samples_us[5..].to_vec();
    steady.sort_unstable();
    let median = steady[steady.len() / 2];
    let min = steady[0];
    let bytes = verts.len() * std::mem::size_of::<LineVertex>();
    eprintln!(
        "[rail-dash bench] 2000 rail ways, {} LineVertex ({bytes} B VRAM, {} B/vertex): frame median {median} µs, min {min} µs (wall, submit→fence, casing+fill, 512×512 4×MSAA)",
        verts.len(),
        std::mem::size_of::<LineVertex>()
    );
    assert!(median > 0, "a frame that takes no time was not rendered");
}