facett-map/tests/line_gpu.rs
//! **Headless-device proof of the pretty GPU line lane** (only built with
//! `--features wgpu`). The GPU line lane cannot run on the plain `egui_kittest`
//! harness (no `GpuMapRenderer` installed), so this validates it on a real
//! (lavapipe) device: it builds the `LineRenderer` (proving `line.wgsl` compiles +
//! the pipeline is valid), renders a small road network to a texture through the
//! quad/miter/SDF-AA/casing pipeline, and asserts the result is (a) width-scaled,
//! (b) anti-aliased (partial-coverage edge pixels exist), and (c) cased (a darker
//! outline hugs the bright fill). It also writes the before/after PNGs
//! (`osm2d-lines-{before,after}.png`) so the pretty roads can be eyeballed.
//!
//! Run: `cargo test -p facett-map --features wgpu --test line_gpu -- --nocapture`
#![cfg(feature = "wgpu")]
use facett_map::gpu::line::{build_line_geometry, LinePolyline, LineRenderer, LineVertex};
use facett_map::gpu::{build_gpu_geometry, ways_origin, MapTransform};
use facett_map::Way;
/// The ONE GPU gate for this crate's device-backed tests (`tests/common/mod.rs`):
/// a missing adapter becomes a SKIP row in the matrix and a panic under CI, never a
/// silent `ok`.
mod common;
const W: u32 = 480;
const H: u32 = 360;
/// A small road network in "pixels-from-centre" space (so a zoom-1, origin-0
/// transform maps `pos` straight to screen offsets): a horizontal arterial, a
/// vertical cross street, and a diagonal — three junctions, to exercise the joins.
fn network() -> Vec<LinePolyline> {
let arterial = LinePolyline {
pts: vec![[-200.0, -10.0], [-40.0, -10.0], [60.0, 40.0], [200.0, 40.0]],
width_px: 9.0,
col: [0.95, 0.75, 0.15, 1.0], // warm arterial
depth: 0.0, dash: false,
};
let cross = LinePolyline { pts: vec![[0.0, -140.0], [0.0, 150.0]], width_px: 6.0, col: [0.20, 0.55, 0.95, 1.0], depth: 0.0, dash: false };
let diagonal = LinePolyline { pts: vec![[-160.0, 120.0], [140.0, -120.0]], width_px: 5.0, col: [0.85, 0.30, 0.35, 1.0], depth: 0.0, dash: false };
vec![arterial, cross, diagonal]
}
/// The paper (light land background) and the dark casing colour — kept distinct so
/// the pixel classifier can tell casing from paper.
const PAPER: [f64; 3] = [0.86, 0.86, 0.82];
/// deck.gl `widthScale` the LOOK tests paint at — a street-zoom value
/// (`line_width_scale(100_000.0)` = 2.0), NOT the identity 1.0. Since e88969f the
/// themed width is baked RAW and this uniform is what turns it into pixels, so a
/// test pinned at 1.0 would be measuring an un-scaled road nobody ever sees.
const LOOK_SCALE: f32 = 2.0;
const CASING: [f32; 4] = [0.10, 0.11, 0.14, 1.0];
/// Render the network to an RGBA8 image through the pretty-line pipeline.
fn render(
device: &wgpu::Device,
queue: &wgpu::Queue,
lines: &[LinePolyline],
casing_px: f32,
feather_px: f32,
width_scale: f32,
) -> image::RgbaImage {
// zoom 1, origin 0, centre 0 → screen = pos + rect.center (pos is px-from-centre).
let rect = egui::Rect::from_min_size(egui::pos2(0.0, 0.0), egui::vec2(W as f32, H as f32));
let transform = MapTransform::new([0.0, 0.0], [0.0, 0.0], 1.0, 1.0, rect);
let verts = build_line_geometry(lines);
render_verts(device, queue, &verts, &transform, casing_px, feather_px, width_scale)
}
/// The paint parameters `render_verts` varies. Split out so the LOOK tests and the
/// WIDTH-measurement test share one readback path (LAW #5) instead of twinning the
/// 60 lines of texture + 256-aligned-copy boilerplate.
#[derive(Clone, Copy)]
struct Paint {
/// Clear colour of the target.
clear: [f64; 4],
casing_px: f32,
casing_col: [f32; 4],
feather_px: f32,
/// deck.gl `widthScale` for the frame — the multiplier
/// [`facett_map::style::line_width_scale`] hands the vertex shader.
width_scale: f32,
}
impl Paint {
/// The look the evidence PNGs are rendered with: light paper + dark casing. The
/// `width_scale` is deck.gl's `widthScale` for the frame — the LIVE lane resolves
/// it from the camera via `facett_map::style::line_width_scale`, so a test that
/// only ever passes `1.0` is sitting on the identity value and cannot tell a live
/// multiply from a dead one.
fn look(casing_px: f32, feather_px: f32, width_scale: f32) -> Self {
Self { clear: [PAPER[0], PAPER[1], PAPER[2], 1.0], casing_px, casing_col: CASING, feather_px, width_scale }
}
}
/// Render a PRE-BUILT `LineVertex` quad buffer (as the live GPU lane uploads it)
/// through the pretty-line pipeline with the given origin-local transform.
fn render_verts(
device: &wgpu::Device,
queue: &wgpu::Queue,
verts: &[LineVertex],
transform: &MapTransform,
casing_px: f32,
feather_px: f32,
width_scale: f32,
) -> image::RgbaImage {
render_paint(device, queue, verts, transform, Paint::look(casing_px, feather_px, width_scale))
}
/// The ONE readback path: render `verts` through the real [`LineRenderer`] into an
/// RGBA8 texture and map it back.
fn render_paint(
device: &wgpu::Device,
queue: &wgpu::Queue,
verts: &[LineVertex],
transform: &MapTransform,
paint: Paint,
) -> 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("line_verts"),
contents: bytemuck::cast_slice(verts),
usage: wgpu::BufferUsages::VERTEX,
});
renderer.write_uniforms(
queue,
transform,
W as f32,
H as f32,
paint.casing_px,
paint.casing_col,
paint.feather_px,
paint.width_scale,
);
// `tex` is what gets read back, so it stays single-sample — it is the RESOLVE target.
let tex = device.create_texture(&wgpu::TextureDescriptor {
label: Some("line_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());
// **The 4× attachment the shipped line pipeline demands** — lane GG #2. This file
// drives `LineRenderer`'s own pipeline, which since GG #2 carries
// `facett_map::gpu::map_msaa_state()` so that a partially covered edge fragment stops
// owning the whole pixel's depth. A pass whose attachments are single-sample is a
// wgpu VALIDATION FAILURE against it, not a frame with worse antialiasing, so this
// target has to match the pipeline exactly and take the sample count from the same
// constant rather than from a literal `4` that could drift away from it.
let msaa_tex = device.create_texture(&wgpu::TextureDescriptor {
label: Some("line_target_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: &[],
});
let msaa_view = msaa_tex.create_view(&wgpu::TextureViewDescriptor::default());
// 256-aligned readback buffer.
let bpp = 4u32;
let unpadded = W * bpp;
let align = 256u32;
let padded = unpadded.div_ceil(align) * align;
let read_buf = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("line_readback"),
size: (padded * H) as u64,
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
mapped_at_creation: false,
});
let mut enc = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("line_enc") });
{
let depth_view = device
.create_texture(&wgpu::TextureDescriptor {
label: Some("line_pass_depth"),
size: wgpu::Extent3d { width: W, height: H, depth_or_array_layers: 1 },
mip_level_count: 1,
// 4×, to match the colour attachment and the pipeline — every attachment
// of one pass shares its sample count.
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 mut pass = enc.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("line_pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &msaa_view,
depth_slice: None,
// The 4× samples resolve into `tex`, which is what the readback copies.
resolve_target: Some(&view),
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color {
r: paint.clear[0],
g: paint.clear[1],
b: paint.clear[2],
a: paint.clear[3],
}),
store: wgpu::StoreOp::Store,
},
})],
// The line pipeline carries a depth state since lane MP-a, so a pass it is
// recorded into MUST provide the attachment — a pass without one is a wgpu
// validation failure, not a silently untested ordering.
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
view: &depth_view,
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 save(img: &image::RgbaImage, name: &str) {
let dir = &std::path::PathBuf::from(std::env::var_os("HOME").unwrap_or_default()).join("git/workspace_nordisk/scratch");
std::fs::create_dir_all(dir).ok();
let path = dir.join(format!("{name}.png"));
image::DynamicImage::ImageRgba8(img.clone()).save(&path).expect("save PNG");
eprintln!("[line-gpu] wrote {}", path.display());
}
fn near(p: &image::Rgba<u8>, r: f64, g: f64, b: f64, tol: i32) -> bool {
(p.0[0] as i32 - (r * 255.0) as i32).abs() <= tol
&& (p.0[1] as i32 - (g * 255.0) as i32).abs() <= tol
&& (p.0[2] as i32 - (b * 255.0) as i32).abs() <= tol
}
/// The light paper background.
fn is_bg(p: &image::Rgba<u8>) -> bool {
near(p, PAPER[0], PAPER[1], PAPER[2], 16)
}
/// The dark casing outline.
fn is_casing(p: &image::Rgba<u8>) -> bool {
near(p, CASING[0] as f64, CASING[1] as f64, CASING[2] as f64, 22)
}
/// A road-fill pixel — near any of the three fill colours.
fn is_bright(p: &image::Rgba<u8>) -> bool {
near(p, 0.95, 0.75, 0.15, 26) || near(p, 0.20, 0.55, 0.95, 26) || near(p, 0.85, 0.30, 0.35, 26)
}
#[test]
fn pretty_line_lane_is_width_scaled_antialiased_and_cased() {
let Some((device, queue)) = common::device_or_skip("pretty_line_lane_is_width_scaled_antialiased_and_cased", "the pretty line lane's width/AA/casing on real pixels")
else {
return;
};
// AFTER: the pretty lane — thick, cased, AA.
let after = render(&device, &queue, &network(), 3.0, 1.5, LOOK_SCALE);
save(&after, "osm2d-lines-after");
// BEFORE: the old look — 1px hairlines, no casing, no feather.
let thin: Vec<LinePolyline> =
network().into_iter().map(|mut l| { l.width_px = 1.0; l }).collect();
let before = render(&device, &queue, &thin, 0.0, 0.5, LOOK_SCALE);
save(&before, "osm2d-lines-before");
let bright_after = after.pixels().filter(|p| is_bright(p)).count();
let bright_before = before.pixels().filter(|p| is_bright(p)).count();
let casing_after = after.pixels().filter(|p| is_casing(p)).count();
// Anti-aliasing: intermediate pixels that are neither background, full-bright,
// nor solid casing — the feathered edge coverage.
let aa_edges = after
.pixels()
.filter(|p| !is_bg(p) && !is_bright(p) && !is_casing(p))
.count();
eprintln!(
"[line-gpu] bright after={bright_after} before={bright_before} · casing={casing_after} · aa_edges={aa_edges}"
);
// (a) Width-scaled: the 9/6/5px cased roads cover many more pixels than the 1px
// hairlines — proof the quad expansion gives real, scalable width.
//
// TWO clauses, because a ratio alone is not a guard. `bright_after > 0 * 3` is
// `> 0`, and that is exactly what this line was before the `LOOK_SCALE` fix:
// `bright_before` was 0 (the un-scaled hairlines never reached the bright
// classifier) and "3× wider than the hairlines" passed against nothing. The
// absolute floor pins the BEFORE frame to a real, visible hairline network —
// measured 1 965 bright px at `LOOK_SCALE` 2 on lavapipe; the floor is a quarter
// of that — so the ratio has a denominator that means something. Red-proven
// 2026-08-22: painting the hairlines in a non-fill colour drops `bright_before`
// to 0, the ratio clause stays GREEN (12 169 > 0), and only this floor fires.
const HAIRLINE_FLOOR: usize = 500;
assert!(
bright_before >= HAIRLINE_FLOOR,
"the BEFORE hairlines must be visible ({bright_before} bright px, floor {HAIRLINE_FLOOR}) — \
a ratio against zero is no guard at all"
);
assert!(
bright_after > bright_before * 3,
"the pretty roads are much wider than the hairlines ({bright_after} vs {bright_before} bright px)"
);
assert!(bright_after > 2_000, "the fill covers a real area (got {bright_after})");
// (b) Cased: a dark casing outline is present around the fill.
assert!(casing_after > 500, "a dark casing hugs the roads (got {casing_after})");
// (c) Anti-aliased: the SDF fragment shader produces feathered edge pixels.
assert!(aa_edges > 500, "the road edges are anti-aliased, not hard 1px (got {aa_edges})");
// testmatrix cell — the GPU pretty-line lane rendered width-scaled, cased, AA
// roads (only reached when a real GPU adapter was found). Surfaces in the
// dwarves testmatrix (`--features testmatrix`), no-op otherwise.
facett_core::testmatrix::emit(
"facett-map/gpu-line",
"pretty_line_lane_is_width_scaled_antialiased_and_cased",
bright_before >= HAIRLINE_FLOOR && bright_after > bright_before * 3 && casing_after > 500 && aa_edges > 500,
&format!("GPU line lane: bright after={bright_after} vs before={bright_before}, casing={casing_after}, aa_edges={aa_edges}"),
);
}
/// A small OSM-style road clip fed through the SAME builder the live viewer uses
/// (`build_gpu_geometry`) — three roads crossing near a common junction.
fn road_clip() -> Vec<Way> {
let c = egui::Color32::from_rgb(0xE8, 0xB0, 0x30);
vec![
Way::line(1, "road", vec![(9.50, 47.13), (9.52, 47.14), (9.55, 47.14)], c, 6.0),
Way::line(2, "road", vec![(9.52, 47.12), (9.52, 47.16)], egui::Color32::from_rgb(0x40, 0x90, 0xF0), 4.0),
Way::line(3, "road", vec![(9.49, 47.16), (9.55, 47.12)], egui::Color32::from_rgb(0xD0, 0x50, 0x58), 3.0),
]
}
/// **The live-lane proof.** The running GPU lane no longer draws raw 1px `LineList`
/// hairlines — `build_gpu_geometry` (the exact builder the paint callback uploads)
/// now emits the pretty **triangle-list quad** buffer (`line_vertices`), and the
/// `LineRenderer` draws it with a dark casing pass + SDF-AA. This proves, on the
/// SAME geometry the live callback ships:
/// (a) the quad count exceeds the hairline count (6 verts/segment vs 2) — the raw
/// hairlines have been superseded by real, scalable width;
/// (b) rendering that quad buffer produces a dark casing outline (the casing pass
/// is live) + anti-aliased edges.
/// GPU-gated (self-skips with no adapter); emits `nornir_testmatrix::functional_status`.
#[test]
fn live_gpu_lane_emits_pretty_quads_not_hairlines() {
let ways = road_clip();
let origin = ways_origin(ways.iter()).expect("non-empty clip has an origin");
let geo = build_gpu_geometry(ways.iter().map(|w| (w, w.style.lod as u32)), origin);
// (a) DATA proof (no GPU needed): the pretty lane emits **6 vertices a segment**
// (two triangles), and the LOD prefix table ends at the full length (all clip
// ways are Country tier).
//
// This used to read `quad_verts == 3 * geo.vertices.len()` — 3x the raw
// `LineList` hairline buffer's 2 verts a segment. That reference stream is now
// off (`HAIRLINE_LANE`, stinky `mapdraw-4`): nothing drew it and a shipped
// frame was baking, uploading and culling it anyway. Counting the SEGMENTS says
// the same thing without keeping a dead lane alive to be measured against, and
// it is the stronger form — `3 * 0 == 0` would have been satisfied by a quad
// lane that emitted nothing at all.
//
// **Plus the round ends** (lane GG #25): an open way carries two — a cap at each
// end — of 6 vertices each, and a sharp enough corner carries a third. These
// clip ways are gentle, so the count is exact rather than a bound; if that ever
// stops holding the right answer is to count `decode_round_end` vertices, not to
// loosen this to `>=`.
let segments: usize = ways.iter().map(|w| w.lonlat.len().saturating_sub(1)).sum();
let caps = ways.iter().filter(|w| w.lonlat.len() >= 2).count() * 2;
let hairline_verts = geo.vertices.len();
let quad_verts = geo.line_vertices.len();
let round_verts = geo.line_vertices.iter().filter(|v| facett_map::gpu::line::decode_round_end(v.miter).0).count();
let want = 6 * segments + 6 * caps;
let quads_beat_hairlines = segments > 0 && quad_verts == want && hairline_verts == 0;
assert_eq!(round_verts, 6 * caps, "one 6-vertex round end per open way END, and no more");
assert!(
segments > 0 && quad_verts == want,
"live lane emits 6 pretty-quad vertices a segment plus 6 a round end: {quad_verts} \
for {segments} segments and {caps} caps (want {want})"
);
assert_eq!(
hairline_verts, 0,
"and the superseded LineList hairline stream is not baked at all (HAIRLINE_LANE)"
);
// The LAST entry, derived — `line_lod_end` is a CUMULATIVE per-tier count, so the
// total is its final element and not index 2. Hard-coded, this read the cumulative
// through the third tier and only equalled the total because this fixture happens to
// carry no deeper-tier lines; it would have gone quietly wrong the moment it did, and
// again when a fifth tier landed.
assert_eq!(
*geo.line_lod_end.last().expect("a non-empty per-tier prefix table") as usize,
quad_verts,
"the per-LOD prefix table spans the whole quad buffer"
);
let Some((device, queue)) = common::device_or_skip("live_gpu_lane_emits_pretty_quads_not_hairlines", "the render arm; the geometry arm above still ran")
else {
nornir_testmatrix::functional_status(
"facett-map",
"live_gpu_line_lane_pretty_quads",
quads_beat_hairlines,
&format!("quads={quad_verts} segments={segments} hairlines={hairline_verts} (render arm skipped: no adapter)"),
);
return;
};
// Fit the origin-local quad geometry into the viewport (the live transform is
// origin-local too; here we just centre + scale the baked positions to fill).
let (mut mnx, mut mny, mut mxx, mut mxy) = (f32::MAX, f32::MAX, f32::MIN, f32::MIN);
for v in &geo.line_vertices {
mnx = mnx.min(v.pos[0]);
mny = mny.min(v.pos[1]);
mxx = mxx.max(v.pos[0]);
mxy = mxy.max(v.pos[1]);
}
let extent = (mxx - mnx).max(mxy - mny).max(1e-12);
let zoom = 0.8 * (W.min(H) as f32) / extent;
let transform = MapTransform {
zoom_x: zoom,
zoom_y: zoom,
ref_x: 0.5 * (mnx + mxx),
ref_y: 0.5 * (mny + mxy),
screen_center_x: W as f32 * 0.5,
screen_center_y: H as f32 * 0.5,
// North-up and flat: this golden is about the miter/casing/AA look, not the
// camera, and the identity ground frame keeps it byte-comparable to the
// image the pre-rotation lane produced.
bearing_deg: 0.0,
tilt_deg: 0.0,
};
let img = render_verts(&device, &queue, &geo.line_vertices, &transform, 3.0, 1.5, LOOK_SCALE);
save(&img, "osm2d-gpu-live-after");
// BEFORE: the raw 1px-hairline look the old lane drew — reproduce it by rendering
// the same centrelines as 1px, no casing, minimal feather.
let thin: Vec<LinePolyline> = ways
.iter()
.map(|w| LinePolyline {
pts: w
.lonlat
.iter()
.map(|&(lon, lat)| {
let (mx, my) = facett_map::mercator(lon, lat);
facett_map::gpu::rebase_vertex_origin(mx, my, origin)
})
.collect(),
width_px: 1.0,
col: [0.2, 0.2, 0.2, 1.0],
depth: 0.0, dash: false,
})
.collect();
let before_verts = build_line_geometry(&thin);
let before = render_verts(&device, &queue, &before_verts, &transform, 0.0, 0.5, LOOK_SCALE);
save(&before, "osm2d-gpu-live-before");
let casing = img.pixels().filter(|p| is_casing(p)).count();
let bright = img.pixels().filter(|p| is_bright(p)).count();
let aa_edges = img.pixels().filter(|p| !is_bg(p) && !is_bright(p) && !is_casing(p)).count();
eprintln!("[line-gpu] LIVE lane casing={casing} bright={bright} aa_edges={aa_edges}");
let cased = casing > 200;
let widened = bright > 1_000;
let ok = quads_beat_hairlines && cased && widened && aa_edges > 200;
nornir_testmatrix::functional_status(
"facett-map",
"live_gpu_line_lane_pretty_quads",
ok,
&format!("quads={quad_verts} segments={segments} hairlines={hairline_verts} casing={casing} bright={bright} aa={aa_edges}"),
);
assert!(cased, "a dark casing hugs the live roads (got {casing})");
assert!(widened, "the live roads have real width, not 1px (bright={bright})");
assert!(aa_edges > 200, "the live road edges are anti-aliased (got {aa_edges})");
}
// ── The WIDTH guard (LAW #2: assert APPLIED output, not a state round-trip) ───
/// Slope of the measurement line (rise/run). A DELIBERATE slant: a perfectly
/// horizontal line samples the coverage profile at ONE sub-pixel phase, and for a
/// ~1 px road that single phase can read 0.5 px or 1.5 px depending on whether the
/// centreline happens to sit on a pixel centre or a pixel boundary. Slanting by 1/6
/// walks the phase through a full cycle every 6 columns, so averaging over 300
/// columns integrates the phase out.
const SLOPE: f32 = 60.0 / 360.0;
/// Measure the width the SHADER actually drew, in pixels.
///
/// The vertex shader rasterises the quad at `hw + r` and the fragment shader feathers
/// with `coverage_inside(edge_px, r)` = `smoothstep(-r, r, edge_px)`, where
/// `edge_px = hw − |side|·(hw + r)` is the signed distance INWARD from the road's true
/// edge. The ramp is therefore CENTRED on that edge: 1 out to `hw − r`, 50% exactly at
/// `hw`, 0 at `hw + r` — where the quad ends, so nothing is truncated. A symmetric
/// smoothstep contributes half its span, so the perpendicular integral is exactly
///
/// ```text
/// ∫ coverage = 2·((hw − r) + r) = 2·hw = w_px
/// ```
///
/// **No feather correction term.** That is the whole point: the drawn width IS the
/// nominal width, so this measurement can be compared to
/// `facett_map::style::line_width_px_scaled` directly. Until 2026-08-01 the ramp sat
/// one feather inside the boundary (`smoothstep(-r, r, edge_px - r)`), the integral
/// came to `w_px − 2·feather_px`, and this function added `2·feather_px` back — a
/// guard calibrated to the defect it was supposed to catch. A nominal 3.000 px road
/// measured 2.002 px.
///
/// A vertical column crosses that perpendicular profile stretched by `1/cos θ`, so
/// `column_sum · cos θ` is the drawn width — a direct read of the vertex shader's
/// `clamp(half_px·2·width_scale, width_min_px, width_max_px)`, with nothing on the CPU
/// side able to fake it.
fn measured_width_px(device: &wgpu::Device, queue: &wgpu::Queue, style_width: f32, width_scale: f32, feather_px: f32) -> f32 {
// A slanted line through the centre, in px-from-centre space; zoom 1 so the baked
// positions land straight on screen.
let (x0, x1) = (-180.0f32, 180.0f32);
let line = LinePolyline {
pts: vec![[x0, x0 * SLOPE], [x1, x1 * SLOPE]],
width_px: style_width,
col: [1.0, 1.0, 1.0, 1.0],
depth: 0.0, dash: false,
};
let verts = build_line_geometry(&[line]);
let rect = egui::Rect::from_min_size(egui::pos2(0.0, 0.0), egui::vec2(W as f32, H as f32));
let transform = MapTransform::new([0.0, 0.0], [0.0, 0.0], 1.0, 1.0, rect);
// Opaque BLACK paper + a fully transparent casing colour, so the casing pass
// writes nothing and `pixel.r / 255` IS the fill's coverage (Rgba8Unorm, linear,
// straight alpha over black).
let paint = Paint {
clear: [0.0, 0.0, 0.0, 1.0],
casing_px: 0.0,
casing_col: [0.0, 0.0, 0.0, 0.0],
feather_px,
width_scale,
};
let img = render_paint(device, queue, &verts, &transform, paint);
// Interior columns only — well clear of both butt caps.
let (lo, hi) = (W / 2 - 150, W / 2 + 150);
let cols: f32 = (hi - lo) as f32;
let total: f32 = (lo..hi)
.map(|x| (0..H).map(|y| img.get_pixel(x, y).0[0] as f32 / 255.0).sum::<f32>())
.sum();
let cos_theta = 1.0 / (1.0 + SLOPE * SLOPE).sqrt();
let _ = feather_px; // the ramp is centred on the edge — no correction term
total / cols * cos_theta
}
/// **The width-parity net, on APPLIED OUTPUT.** `facett_map::style::line_width_px_scaled`
/// is the CPU painter's width formula; `line.wgsl`'s vertex shader is meant to be its
/// exact GPU twin (`clamp(half_px·2·width_scale, min, max)`). This renders a road on a
/// real device at several `widthScale`s and MEASURES the pixels it covered, then
/// compares that to what the CPU function says — so the two lanes cannot drift.
///
/// This is the guard the "two scales of land and lines" bug needed and did not have.
/// Its CPU-side counterpart, `facett-demo/tests/osm2d_deckgl_quality.rs::
/// line_width_is_pixel_clamped_and_shared_by_both_lanes`, used to compare
/// `line_width_px(w, z)` with `line_width_px_scaled(w, line_width_scale(z))` — the SAME
/// Rust expression, since `line_width_px` is defined as that call — so it could never
/// go red. Rewritten 2026-08-01 onto hand-computed literal pixel widths; it now pins the
/// CPU formula for real, but it still never consults the shader. Only THIS test does.
///
/// Deliberately swept OFF the identity: `width_scale = 1.0` alone cannot tell a live
/// multiply from a dead one, and both pixel CLAMPS are exercised.
#[test]
fn shader_width_matches_the_cpu_width_formula_on_real_pixels() {
use facett_map::style::{LINE_WIDTH_MAX_PX, LINE_WIDTH_MIN_PX, line_width_px_scaled};
let Some((device, queue)) = common::device_or_skip("shader_width_matches_the_cpu_width_formula_on_real_pixels", "the shader width vs the CPU width formula on real pixels")
else {
return;
};
let feather = 0.5f32;
// (themed style.width, deck.gl widthScale) → the CPU formula's answer.
let cases: [(f32, f32); 6] = [
(3.0, 1.0), // 3 px — the neutral scale
(3.0, 2.0), // 6 px — a LIVE multiply (identity-value trap avoided)
(5.0, 2.0), // 10 px
(9.0, 1.0), // 9 px
(9.0, 3.0), // 27 → 24 px, the widthMaxPixels CEILING
(12.0, 2.0), // 24 → 24 px, the ceiling from the other side
];
let mut worst = 0.0f32;
for (style_w, scale) in cases {
let expect = line_width_px_scaled(style_w, scale);
let got = measured_width_px(&device, &queue, style_w, scale, feather);
let err = (got - expect).abs();
eprintln!("[line-gpu] width style={style_w} scale={scale} → cpu={expect:.3} px, drawn={got:.3} px (Δ{err:.3})");
assert!(
err < 0.35,
"the shader drew {got:.3} px where facett_map::style::line_width_px_scaled({style_w}, {scale}) \
says {expect:.3} px — the GPU line lane and the CPU painter are at TWO SCALES again"
);
worst = worst.max(err);
}
// A road at the CEILING is genuinely wider than one below it (the sweep is not
// silently saturating everything to one width).
assert!(
line_width_px_scaled(9.0, 3.0) > line_width_px_scaled(3.0, 1.0),
"the width hierarchy survives the clamp"
);
assert_eq!(line_width_px_scaled(9.0, 3.0), LINE_WIDTH_MAX_PX, "the 27 px case really is the clamped one");
// The widthMinPixels FLOOR, measured too: a hairline road that resolves below the
// floor must still be drawn AT the floor, not at its sub-pixel raw width.
let thin_feather = 0.25f32;
let floored = measured_width_px(&device, &queue, 0.6, 0.5, thin_feather);
let unfloored_would_be = 0.6 * 0.5; // 0.3 px — invisible without the clamp
eprintln!("[line-gpu] width floor: drawn={floored:.3} px (raw would be {unfloored_would_be} px, floor {LINE_WIDTH_MIN_PX})");
assert!(
(floored - LINE_WIDTH_MIN_PX).abs() < 0.35,
"a sub-pixel road is drawn at the widthMinPixels floor ({LINE_WIDTH_MIN_PX} px), got {floored:.3} px"
);
facett_core::testmatrix::emit(
"facett-map/gpu-line",
"shader_width_matches_the_cpu_width_formula_on_real_pixels",
worst < 0.35,
&format!("GPU line width vs facett_map::style::line_width_px_scaled: worst Δ {worst:.3} px over 6 scale/clamp cases"),
);
}
/// Scratch hook for the shader-unification before/after byte compare: dumps the raw
/// RGBA of the evidence frame when `FACETT_DIGEST_OUT` is set.
#[test]
fn dump_map_line_framebuffer() {
let Some(out) = std::env::var_os("FACETT_DIGEST_OUT") else { return };
let Some((device, queue)) = common::device_or_skip("dump_map_line_framebuffer", "the framebuffer dump (a digest tool, not a proof)")
else {
return;
};
let img = render(&device, &queue, &network(), 3.0, 1.5, LOOK_SCALE);
let path = std::path::Path::new(&out).with_extension("map.raw");
std::fs::write(&path, img.as_raw()).expect("write raw framebuffer");
eprintln!("[digest] map line framebuffer -> {} ({} bytes)", path.display(), img.as_raw().len());
}
// ── The INNER-MITER CLAMP, on real device pixels (GFX_V2 §3.A) ────────────────
mod miter_geom;
/// A network of **staircases** — two same-handed turns with a short segment between
/// them, the shape whose concave corners overrun each other and bowtie. Three of them, at
/// three near-reflex turn angles, laid out across the frame in px-from-centre space
/// (zoom 1), each with a 2–5 px middle segment under a 24 px stroke (12 × widthScale 2,
/// at the `widthMaxPixels` ceiling) — the segment is several times NARROWER than the road
/// is wide, which is the regime where the unclamped retreat escapes the ribbon entirely
/// instead of hiding inside it.
fn miter_network() -> Vec<LinePolyline> {
let place = |turn: f32, mid: f32, arm: f32, at: [f32; 2]| LinePolyline {
pts: miter_geom::staircase(turn, mid, arm).into_iter().map(|p| [p[0] + at[0], p[1] + at[1]]).collect(),
width_px: 12.0,
col: [1.0, 1.0, 1.0, 1.0],
depth: 0.0, dash: false,
};
vec![
place(165.0, 3.0, 70.0, [-120.0, -95.0]),
place(175.0, 2.0, 60.0, [-20.0, 40.0]),
place(150.0, 5.0, 80.0, [110.0, -40.0]),
]
}
/// Is `p` inside the triangle? Half-plane signs, inclusive, so a shared internal edge
/// belongs to both of its triangles and cannot read as a hole.
fn in_tri(p: [f32; 2], t: &[[f32; 2]; 3]) -> bool {
let s = |a: [f32; 2], b: [f32; 2]| (b[0] - a[0]) * (p[1] - a[1]) - (b[1] - a[1]) * (p[0] - a[0]);
let (d0, d1, d2) = (s(t[0], t[1]), s(t[1], t[2]), s(t[2], t[0]));
(d0 >= 0.0 && d1 >= 0.0 && d2 >= 0.0) || (d0 <= 0.0 && d1 <= 0.0 && d2 <= 0.0)
}
/// The model's coverage verdict for one pixel: `Some(true/false)` when the pixel centre
/// AND the four corners of a `band`-inflated box around it all agree, `None` when they do
/// not — i.e. the pixel straddles a geometry boundary, where a one-sample rasteriser's
/// answer is a tie-break rule rather than a fact and asserting on it would be measuring
/// wgpu's fill convention instead of the clamp.
fn model_says(px: [f32; 2], tris: &[[[f32; 2]; 3]], band: f32) -> Option<bool> {
// ── CONFIDENT MEANS "NO TRIANGLE EDGE CROSSES THIS PIXEL", not "the UNION's
// boundary does not" — and the difference is worth three pixels.
//
// Since GG #2 the target is 4× MSAA and `line.wgsl` returns its coverage as a
// `@builtin(sample_mask)`, which the hardware ANDs with the samples the RASTERISER
// covered. `coverage_mask` hands back the LOW `n` bits — chosen so a road's casing
// and fill nest — and those bits need not be the ones the rasteriser covered. On a
// fully covered fragment that is harmless, which is the shipped case: the quad is
// rasterised one feather WIDER than the road, so at the road's edge the rasteriser
// sees the whole pixel. THIS test deliberately paints at `feather = 0`, where the
// quad edge and the coverage edge are the same line, and there the two masks can
// disagree — a triangle covering two samples can contribute none.
//
// MEASURED on oden / RTX 4090, 2026-09-05, on origin/main (i.e. before lane GG #25
// touched anything): the `unclamped` diagonal read **3 mismatches**, at
// (251..253, 221), every one of them `[127, 127, 127]` — exactly two of four samples
// — where the old five-probe rule called the pixel confidently inside because the
// UNION of the bowtie's overlapping triangles covers it while a triangle EDGE runs
// straight through it. The model was not wrong about the geometry; it was claiming
// an opinion on pixels where the sample mask makes the answer a tie-break rule, which
// is the very thing this function exists to refuse.
//
// So: a 5×5 grid over the halo (a 1 px sliver passes between five probes; it does not
// pass between twenty-five), and agreement demanded PER TRIANGLE.
const N: i32 = 2;
let mut probes = Vec::with_capacity(25);
for iy in -N..=N {
for ix in -N..=N {
probes.push([px[0] + band * ix as f32 / N as f32, px[1] + band * iy as f32 / N as f32]);
}
}
for t in tris {
let here = in_tri(px, t);
if probes.iter().any(|p| in_tri(*p, t) != here) {
return None;
}
}
Some(tris.iter().any(|t| in_tri(px, t)))
}
/// Every triangle of a vertex buffer, in applied screen pixels.
fn applied_tris(verts: &[LineVertex], t: &MapTransform, u: &facett_map::gpu::line::MapLineUniforms) -> Vec<[[f32; 2]; 3]> {
verts
.as_chunks::<3>().0.iter()
.map(|c| {
[
facett_map::gpu::line::applied_screen_pos(&c[0], t, u),
facett_map::gpu::line::applied_screen_pos(&c[1], t, u),
facett_map::gpu::line::applied_screen_pos(&c[2], t, u),
]
})
.collect()
}
/// **The link that makes the whole clamp proof real.** `facett-map/tests/
/// inner_miter_clamp.rs` asserts self-intersection, spans and width on the positions
/// [`facett_map::gpu::line::applied_screen_pos`] computes — a Rust transcription of
/// `line.wgsl`'s `vs_main`, and therefore worth nothing on its own. This renders the
/// pathological network on a real device with a hard edge (`feather = 0`, so `r > 128` IS
/// "inside the geometry") and checks the framebuffer pixel by pixel against that model.
///
/// It does it as a **2 × 2 matrix**, which is what makes it a mutation rather than a
/// hopeful match: two vertex buffers (ceilings applied, ceilings stripped) are each
/// rendered AND each modelled, and all four combinations are scored.
///
/// ```text
/// frame: clamped frame: unclamped
/// model: clamped 0 mismatches MANY mismatches
/// model: unclamped MANY mismatches 0 mismatches
/// ```
///
/// The diagonal proves the transcription is faithful. The off-diagonal proves the shader
/// actually READS `inner_lim` — if it ignored the attribute the two frames would be
/// identical, the off-diagonal would collapse to zero with the diagonal, and this test
/// goes red. Neither half can pass alone, which is the property a hollow guard lacks.
#[test]
fn gpu_draws_exactly_the_clamped_join_geometry_the_cpu_model_predicts() {
let Some((device, queue)) = common::device_or_skip("gpu_draws_exactly_the_clamped_join_geometry_the_cpu_model_predicts", "the inner-miter pixel proof")
else {
return;
};
let lines = miter_network();
// **Quads only.** Lane GG #25's round ends are half-DISCS carved out of two
// triangles by the fragment stage, and this proof's model is a set of triangles —
// see `miter_geom::quads_only`. The clamp is asserted on the geometry the clamp is
// about; the discs are asserted by their own device oracle below.
let clamped_verts = miter_geom::quads_only(&build_line_geometry(&lines));
let bare_verts = miter_geom::unclamped(&clamped_verts);
let (transform, uniforms) = miter_geom::frame(W as f32, H as f32, 1.0, 1.0, 2.0, 0.0);
// Opaque BLACK paper and a fully transparent casing, so only the fill writes and the
// red channel is a hard inside/outside verdict.
let paint =
Paint { clear: [0.0, 0.0, 0.0, 1.0], casing_px: 0.0, casing_col: [0.0; 4], feather_px: 0.0, width_scale: 2.0 };
let frames = [
("clamped", render_paint(&device, &queue, &clamped_verts, &transform, paint)),
("unclamped", render_paint(&device, &queue, &bare_verts, &transform, paint)),
];
let models = [
("clamped", applied_tris(&clamped_verts, &transform, &uniforms)),
("unclamped", applied_tris(&bare_verts, &transform, &uniforms)),
];
assert_eq!(models[0].1.len(), 18, "3 staircases × 3 segments × 2 triangles");
// Score every (model, frame) pair: agreeing inside pixels, agreeing outside pixels,
// mismatches, and pixels straddling a boundary (where a one-sample rasteriser's answer
// is a tie-break rule, not a fact, and asserting on it would measure wgpu's fill
// convention instead of the clamp).
let score = |tris: &[[[f32; 2]; 3]], img: &image::RgbaImage| -> (usize, usize, usize) {
let (mut ink, mut empty, mut bad) = (0usize, 0usize, 0usize);
for y in 0..H {
for x in 0..W {
let p = [x as f32 + 0.5, y as f32 + 0.5];
let Some(m) = model_says(p, tris, 1.0) else { continue };
match (m, img.get_pixel(x, y).0[0] > 128) {
(true, true) => ink += 1,
(false, false) => empty += 1,
_ => bad += 1,
}
}
}
(ink, empty, bad)
};
let mut matrix = [[0usize; 2]; 2];
let mut inked = 0usize;
for (mi, (mname, tris)) in models.iter().enumerate() {
for (fi, (fname, img)) in frames.iter().enumerate() {
let (ink, empty, bad) = score(tris, img);
matrix[mi][fi] = bad;
eprintln!(
"[line-gpu] inner-miter {mname:>9} model vs {fname:>9} frame: \
{ink} inside + {empty} outside agree, {bad} MISMATCH"
);
if mi == fi && mi == 0 {
inked = ink;
}
}
}
// ── GREEN, the diagonal: each model is exactly what the device drew for its own
// vertex buffer. This is what licenses every assertion in inner_miter_clamp.rs.
for (i, name) in ["clamped", "unclamped"].into_iter().enumerate() {
assert!(
matrix[i][i] == 0,
"the {name} framebuffer disagrees with `applied_screen_pos` on {} confidently-classified \
pixels — the CPU model is NOT what the shader draws, so the geometric proofs in \
inner_miter_clamp.rs are describing geometry that never reaches a pixel",
matrix[i][i]
);
}
// Not vacuous: real ink, and the model committed to most of the frame.
assert!(inked > 4_000, "the network must actually cover thousands of pixels, got {inked}");
// ── RED, the off-diagonal: the two frames really differ, so the shader is reading
// `inner_lim`. A floor of 2% of the inked area — the noise floor here is exactly 0,
// measured on the diagonal, so this is decisively above it.
let floor = inked / 50;
for (mi, fi, label) in [(0usize, 1usize, "clamped model vs unclamped frame"), (1, 0, "unclamped model vs clamped frame")] {
assert!(
matrix[mi][fi] > floor,
"MUTATION FAILED: {label} mismatches on only {} pixels (floor {floor} = 2% of the {inked} \
inked pixels). The two vertex buffers differ ONLY in `inner_lim`, so if the frames agree \
then `line.wgsl` never reads the attribute and the 48 B vertex growth is dead weight"
, matrix[mi][fi]);
}
eprintln!(
"[line-gpu] inner-miter 2×2: diagonal {} / {} mismatches, off-diagonal {} / {} (floor {floor} of {inked} inked px)",
matrix[0][0], matrix[1][1], matrix[0][1], matrix[1][0]
);
facett_core::testmatrix::emit(
"facett-map/gpu-line",
"gpu_draws_exactly_the_clamped_join_geometry",
matrix[0][0] == 0 && matrix[1][1] == 0 && matrix[0][1] > floor && matrix[1][0] > floor,
&format!(
"GFX_V2 item 5 on real pixels: model↔frame diagonal {}/{} mismatches, off-diagonal {}/{} over {inked} inked px",
matrix[0][0], matrix[1][1], matrix[0][1], matrix[1][0]
),
);
}
/// **The clamp changes the picture, and the picture is sound.** Renders the same network
/// twice on a real device — once with the ceilings, once with them stripped — and requires
/// the frames to differ by [`facett_core::golden::moved`]'s two floors (a share of the
/// frame AND a bounding box with extent in both axes, so a thin sliver cannot pass).
/// Never `assert_ne!` on encoded PNG bytes.
///
/// It also re-runs the geometric predicates on this exact frame's transform, so the
/// evidence PNG and the assertion are the same case rather than two hopeful cousins.
#[test]
fn the_inner_miter_clamp_visibly_redraws_the_pathological_joins() {
let Some((device, queue)) = common::device_or_skip("the_inner_miter_clamp_visibly_redraws_the_pathological_joins", "the inner-miter before/after")
else {
return;
};
let lines = miter_network();
let verts = build_line_geometry(&lines);
let (transform, uniforms) = miter_geom::frame(W as f32, H as f32, 1.0, 1.0, 2.0, 1.5);
let paint = Paint::look(3.0, 1.5, 2.0);
let after = render_paint(&device, &queue, &verts, &transform, paint);
let before = render_paint(&device, &queue, &miter_geom::unclamped(&verts), &transform, paint);
// The geometry behind THIS frame: sound clamped, self-intersecting unclamped.
for l in &lines {
let v = build_line_geometry(std::slice::from_ref(l));
let f = miter_geom::faults(&v, &l.pts, &transform, &uniforms);
assert!(f.is_empty(), "the frame's own geometry must be sound: {f:#?}");
let bad = miter_geom::faults(&miter_geom::unclamped(&v), &l.pts, &transform, &uniforms);
assert!(!bad.is_empty(), "the frame's own geometry must be pathological unclamped — it is not");
}
let d = facett_core::golden::moved(&before, &after, facett_core::golden::MinChange::WIDGET)
.expect("the inner-miter clamp must visibly redraw the pathological joins");
eprintln!(
"[line-gpu] inner-miter before/after: {} px differ ({:.3}% of the frame), bbox {:?}, max channel Δ {}",
d.pixels,
d.percent(),
d.bbox,
d.max_channel_delta
);
// Evidence, through the same `save` the other proofs in this file use — an
// `is_dir()`-guarded writer of its own would skip SILENTLY when the path was wrong,
// which is how the first draft of this test wrote no PNGs at all and still passed.
save(&before, "miter-clamp-before");
save(&after, "miter-clamp-after");
save(&facett_core::golden::diff_image(&before, &after), "miter-clamp-diff");
facett_core::testmatrix::emit(
"facett-map/gpu-line",
"inner_miter_clamp_visibly_redraws_the_joins",
true,
&format!("GFX_V2 item 5: {} px ({:.3}%) of the frame change when the ceilings are applied", d.pixels, d.percent()),
);
}
// ── The casing must be a fraction of its road, not a constant ────────────────
/// sRGB luma of a read-back pixel, `0..=255`.
fn luma(p: &image::Rgba<u8>) -> f32 {
0.2126 * p.0[0] as f32 + 0.7152 * p.0[1] as f32 + 0.0722 * p.0[2] as f32
}
/// **Dark ink, integrated — not thresholded.** A 2 px road under a 1 px feather has
/// almost no fully-saturated pixel at all, so a classifier that counts "pixels near the
/// casing colour" reports `0` for a casing that is plainly there (it did, on the first
/// cut of this test). This sums each pixel's darkening below paper and normalises by a
/// full-strength darkening, giving **equivalent fully-inked pixels** — linear in the
/// drawn width, which is what the eye integrates.
fn dark_ink(img: &image::RgbaImage, paper: f32, casing: f32) -> f32 {
img.pixels().map(|p| (paper - luma(p)).max(0.0)).sum::<f32>() / (paper - casing)
}
/// One straight horizontal way of `width_px` raw themed width, crossing the whole
/// frame, in the day palette's white secondary-road colour (`#FFFFFF`).
fn one_street(width_px: f32) -> Vec<LinePolyline> {
vec![LinePolyline {
pts: vec![[-(W as f32) * 0.6, 0.0], [(W as f32) * 0.6, 0.0]],
width_px,
col: [1.0, 1.0, 1.0, 1.0],
depth: 0.0, dash: false,
}]
}
/// **A minor street must not be mostly casing — and an arterial must keep its ribbon.**
///
/// THE DEFECT. `line.wgsl` added a FLAT `u.width_add` (2 px) of half-width in the
/// casing pass, so the casing's share of a drawn road grew as the road got thinner. At
/// a street zoom (`width_scale = 2`) a residential way resolves to 2 px of fill and
/// drew inside a 6 px band of near-black navy — 4 px of dark wrapping 2 px of road. A
/// city viewport holds tens of thousands of those, and that is the grain MEASURED on
/// the shipped korp-ui over Stockholm at z12.7: `#191C24` (this exact casing colour,
/// before any AA blend) covered 2.81 % of the 1254×724 map canvas, and 15.67 % of the
/// canvas was ink darker than grey 70.
///
/// THE LAW ASSERTED: **the dark casing a way lays down may not be wider than the way**.
/// Measured as equivalent-full-pixels of darkening divided by the drawn length, which
/// is the casing's on-screen width in px — directly comparable to the road's own
/// resolved `w_px`.
///
/// SEEN RED. With `let hw = w_px * 0.5 + u.width_add;` restored in `line.wgsl` (the
/// flat casing) this test reports, verbatim:
///
/// ```text
/// [line-gpu] casing law: minor w=2.0px casing=3.62px (1.81x its own width) · arterial w=6.0px casing=3.62px · arterial/minor casing = 1.00x
/// thread 'a_minor_street_is_not_mostly_casing' panicked at facett-map/tests/line_gpu.rs:
/// a 2.0 px residential street drew a 3.62 px dark casing — 1.81x its own width. The
/// outline out-inks the road it is outlining, which is the navy static measured on
/// korp-ui over Stockholm at z12.7
/// ```
///
/// The arterial arm is the anti-blind-green and it has two halves: deleting the casing
/// outright would satisfy the width assertion perfectly, and a casing that is still
/// FLAT gives the motorway and the alley the *same* 3.62 px (ratio 1.00 above). Only a
/// rule proportional to the way's own width passes all three.
#[test]
fn a_minor_street_is_not_mostly_casing() {
let Some((device, queue)) = common::device_or_skip("a_minor_street_is_not_mostly_casing", "a minor street is not mostly casing")
else {
return;
};
// The SHIPPED paint: `LINE_CASING_PX` (2.0) as the ceiling and `LINE_FEATHER_PX`
// (1.0), at a street zoom (`style::line_width_scale` ≈ 2 over a city camera). A
// test that paints with its own wider casing measures a look nobody ships.
const SCALE: f32 = 2.0;
let paint = Paint {
clear: [PAPER[0], PAPER[1], PAPER[2], 1.0],
casing_px: 2.0,
casing_col: CASING,
feather_px: 1.0,
width_scale: SCALE,
};
let rect = egui::Rect::from_min_size(egui::pos2(0.0, 0.0), egui::vec2(W as f32, H as f32));
let transform = MapTransform::new([0.0, 0.0], [0.0, 0.0], 1.0, 1.0, rect);
let px = |c: [f64; 3]| luma(&image::Rgba([(c[0] * 255.0) as u8, (c[1] * 255.0) as u8, (c[2] * 255.0) as u8, 255]));
let paper = px(PAPER);
let casing = px([CASING[0] as f64, CASING[1] as f64, CASING[2] as f64]);
// The way spans the frame, so the drawn length is the frame width.
let length = W as f32;
// The casing's on-screen width in px, for a way of raw themed width `style_w`.
let casing_width_px = |style_w: f32, name: &str| {
let verts = build_line_geometry(&one_street(style_w));
let img = render_paint(&device, &queue, &verts, &transform, paint);
save(&img, name);
dark_ink(&img, paper, casing) / length
};
// `style.width` 1.0 = residential / unclassified / service; 3.0 = motorway / trunk
// (`facett_map::style::classify`). `line_width_px_scaled` turns them into pixels.
let (minor_w, arterial_w) = (
facett_map::style::line_width_px_scaled(1.0, SCALE),
facett_map::style::line_width_px_scaled(3.0, SCALE),
);
let minor_casing = casing_width_px(1.0, "casing-minor-street");
let arterial_casing = casing_width_px(3.0, "casing-arterial");
let share = minor_casing / minor_w;
let class_ratio = arterial_casing / minor_casing.max(1e-6);
eprintln!(
"[line-gpu] casing law: minor w={minor_w:.1}px casing={minor_casing:.2}px \
({share:.2}x its own width) · arterial w={arterial_w:.1}px casing={arterial_casing:.2}px \
· arterial/minor casing = {class_ratio:.2}x"
);
// (a) THE CLAIM. The outline may not be wider than the thing it outlines.
assert!(
share <= 1.0,
"a {minor_w:.1} px residential street drew a {minor_casing:.2} px dark casing — \
{share:.2}x its own width. The outline out-inks the road it is outlining, which is \
the navy static measured on korp-ui over Stockholm at z12.7"
);
// (b) THE ANTI-BLIND-GREEN, both halves.
assert!(
minor_casing > 0.3,
"the minor street lost its casing altogether ({minor_casing:.2} px) — an outline that \
is not drawn is not a calmer map, it is a road with no edge"
);
assert!(
class_ratio >= 2.0,
"the arterial's casing is only {class_ratio:.2}x the residential street's — the casing \
is still a CONSTANT, not a fraction of the road, so the thin ways are still mostly \
outline"
);
facett_core::testmatrix::emit(
"facett-map/gpu-line",
"a_minor_street_is_not_mostly_casing",
share <= 1.0 && minor_casing > 0.3 && class_ratio >= 2.0,
&format!("casing is proportional: minor {minor_casing:.2}px on a {minor_w:.1}px road ({share:.2}x), arterial/minor = {class_ratio:.2}x"),
);
}
// ── ROUND ENDS: caps and the acute-corner notch (lane GG #25) ────────────────
/// The paint the two round-end oracles measure under: **opaque black paper, a fully
/// transparent casing and no feather**, so `r > 128` is a hard inside/outside verdict on
/// the fill geometry alone. The same arrangement
/// `gpu_draws_exactly_the_clamped_join_geometry_the_cpu_model_predicts` uses, and for
/// the same reason — a feathered, cased frame measures the ramp and the outline as well
/// as the shape, and the shape is what is on trial here.
const HARD: Paint =
Paint { clear: [0.0, 0.0, 0.0, 1.0], casing_px: 0.0, casing_col: [0.0; 4], feather_px: 0.0, width_scale: 2.0 };
/// The raw themed width the round-end oracles draw at, and the half-width it resolves to
/// under [`HARD`]'s `width_scale`. 8 × 2 = 16 px of road, so the cap is an 8 px radius —
/// big enough that a semicircle and a bar are tens of pixels apart, and small enough to
/// sit well inside a 480×360 frame.
const ROUND_W: f32 = 8.0;
/// Rows of ink in one column, counted on the hard threshold.
fn ink_height(img: &image::RgbaImage, x: u32) -> u32 {
(0..H).filter(|&y| img.get_pixel(x, y).0[0] > 128).count() as u32
}
/// **A DEAD-END ROAD ENDS IN A SEMICIRCLE, NOT A BAR** — lane GG #25, the audit's own
/// red oracle.
///
/// `vertex_join` gave both endpoints `flat(perp(d))`: a bisector perpendicular to the
/// last segment, factor 1, and nothing else. The quad therefore stopped dead on the
/// perpendicular through the last point — a **butt end**. Every cul-de-sac, every stub
/// into a T-junction and every clipped way in the map ended in a flat bar, and where two
/// of those bars met at an angle the union left a notch.
///
/// The claim is measured as a **profile**, not as "is there ink past the end", because
/// three different caps all put ink past the end and only one of them is round:
///
/// ```text
/// k px past the end: 0 2 4 6 7 9
/// butt (before): 16 0 0 0 0 0
/// square: 16 16 16 16 16 0
/// round (want): 16 15.5 13.9 10.6 7.7 0
/// ```
///
/// so the assertion is `height(k) ≈ 2·√(hw² − k²)` at four offsets at once. A butt end
/// fails at `k = 2`; a square cap fails at `k = 7`; a cap of the wrong radius fails at
/// `k = 9`, where the road must be gone.
///
/// **The red arm runs in this test.** The same polyline is rendered a second time with
/// the round-end vertices stripped by DATA (`miter_geom::quads_only`) — the identical
/// code path, one filter — and the ink past the end must collapse to nothing. Without
/// that arm a frame that simply drew a longer road would satisfy the profile.
#[test]
fn a_dead_end_road_ends_in_a_semicircle_not_a_bar() {
let Some((device, queue)) = common::device_or_skip(
"a_dead_end_road_ends_in_a_semicircle_not_a_bar",
"round caps on the GPU line lane",
) else {
return;
};
// A horizontal way that STOPS inside the frame: origin-local px, so screen = pos +
// pane centre. The end lands at screen x = 300, well clear of the right edge.
let end_x = W / 2 + 60;
let road = vec![LinePolyline {
pts: vec![[-140.0, 0.0], [60.0, 0.0]],
width_px: ROUND_W,
col: [1.0, 1.0, 1.0, 1.0],
depth: 0.0, dash: false,
}];
let rect = egui::Rect::from_min_size(egui::pos2(0.0, 0.0), egui::vec2(W as f32, H as f32));
let transform = MapTransform::new([0.0, 0.0], [0.0, 0.0], 1.0, 1.0, rect);
let hw = facett_map::style::line_width_px_scaled(ROUND_W, HARD.width_scale) * 0.5;
let round_verts = build_line_geometry(&road);
let butt_verts = miter_geom::quads_only(&round_verts);
assert!(
round_verts.len() > butt_verts.len(),
"the expansion emitted no round ends at all ({} vertices either way) — the mutation \
below would then be a no-op and this test could not fail",
round_verts.len()
);
let round = render_paint(&device, &queue, &round_verts, &transform, HARD);
let butt = render_paint(&device, &queue, &butt_verts, &transform, HARD);
save(&round, "round-cap-after");
save(&butt, "round-cap-before");
// NON-VACUITY: the road itself is on screen at its full drawn width, in BOTH arms.
for (name, img) in [("round", &round), ("butt", &butt)] {
let body = ink_height(img, end_x - 40);
assert!(
(body as f32 - hw * 2.0).abs() <= 2.0,
"{name}: the road's own body measures {body} px where the width formula says \
{:.1} — the fixture is not drawing the road this test is about",
hw * 2.0
);
}
// THE PROFILE. `2·√(hw² − k²)` is the semicircle; ±2 px absorbs the resolve's edge
// pixels at a 4× target.
let mut worst = 0.0f32;
for k in [0u32, 2, 4, 6, 7] {
let want = 2.0 * (hw * hw - (k as f32) * (k as f32)).max(0.0).sqrt();
let got = ink_height(&round, end_x + k) as f32;
eprintln!("[line-gpu] round cap: {k} px past the end → {got} px of ink, semicircle says {want:.1}");
worst = worst.max((got - want).abs());
assert!(
(got - want).abs() <= 2.0,
"at {k} px past the end the cap is {got} px tall but a semicircle of radius \
{hw:.1} is {want:.1} px — a butt end reads 0 here and a square cap reads {:.0}",
hw * 2.0
);
}
// And it STOPS at the radius: a cap longer than `hw` is not this cap.
let past = ink_height(&round, end_x + (hw.ceil() as u32) + 2);
assert_eq!(past, 0, "the cap still has {past} px of ink {:.0} px past the end — it is not a disc of radius {hw:.1}", hw.ceil() + 2.0);
// ── THE RED ARM ──────────────────────────────────────────────────────────
let ink_past = |img: &image::RgbaImage| -> u32 { (1..=(hw.ceil() as u32)).map(|k| ink_height(img, end_x + k)).sum() };
let (with_caps, without) = (ink_past(&round), ink_past(&butt));
eprintln!("[line-gpu] round cap: {with_caps} px of ink past the end WITH the round ends, {without} WITHOUT");
assert!(
with_caps > 60,
"the cap covers only {with_caps} px past the end; a half-disc of radius {hw:.1} is \
about {:.0} px",
std::f32::consts::FRAC_PI_2 * hw * hw
);
assert!(
without * 20 < with_caps,
"MUTATION FAILED: stripping the round-end vertices still leaves {without} px of ink \
past the end against {with_caps} with them. Then the semicircle is not coming from \
those vertices and this test is measuring something else"
);
facett_core::testmatrix::emit(
"facett-map/gpu-line",
"a_dead_end_road_ends_in_a_semicircle",
true,
&format!(
"GG #25: cap profile matches 2·√(hw²−k²) within {worst:.1} px at hw = {hw:.1}; \
{with_caps} px of ink past the end with the round ends, {without} without"
),
);
}
/// **AN ACUTE JUNCTION HAS NO NOTCH** — lane GG #25, the audit's second red oracle.
///
/// [`MITER_LIMIT`] is documented as a bevel and is not one: past the limit the corner is
/// simply pulled back to `4·hw` **along the bisector**, and at the limit `cos(τ/2) < ¼`,
/// so that point is less than `hw` from either segment's axis. Both quads therefore
/// converge INWARD at the sharpest corner they have — the road necks to a waist and
/// leaves a wedge of paper on the convex side. That is the "notch at acute meets".
///
/// The fixture is a hairpin whose corner sits on the pane centre and whose convex
/// bisector points along `+x`, so the notch is a half-disc of paper at a known place.
/// The oracle probes a ring of radius `0.75·hw` through that half-plane: with the round
/// joint every probe is ink, and with the round-end vertices stripped — the same data
/// mutation the cap oracle uses — several are paper. Both halves are required, so
/// neither a fix that painted the whole pane nor a fixture with no notch can pass.
#[test]
fn an_acute_corner_has_no_notch_on_its_convex_side() {
let Some((device, queue)) = common::device_or_skip(
"an_acute_corner_has_no_notch_on_its_convex_side",
"the acute-corner notch on the GPU line lane",
) else {
return;
};
// Incoming (1,0) into the origin, outgoing back along ≈(−1,0) with a 6 px rise: a
// ~176° turn, so the miter is clamped and the join is the pathological one. The
// bisector comes out along −x and the turn is a LEFT one, which puts the CONVEX
// corner — and the notch — on +x.
let hairpin = vec![LinePolyline {
pts: vec![[-140.0, 0.0], [0.0, 0.0], [-140.0, 6.0]],
width_px: ROUND_W,
col: [1.0, 1.0, 1.0, 1.0],
depth: 0.0, dash: false,
}];
let rect = egui::Rect::from_min_size(egui::pos2(0.0, 0.0), egui::vec2(W as f32, H as f32));
let transform = MapTransform::new([0.0, 0.0], [0.0, 0.0], 1.0, 1.0, rect);
let hw = facett_map::style::line_width_px_scaled(ROUND_W, HARD.width_scale) * 0.5;
let corner = [(W / 2) as f32, (H / 2) as f32];
let round_verts = build_line_geometry(&hairpin);
let notched_verts = miter_geom::quads_only(&round_verts);
assert!(
round_verts.len() >= notched_verts.len() + 12,
"the hairpin must emit a round JOINT as well as its two caps ({} vs {} vertices) — \
without one the mutation below only removes the caps and says nothing about the notch",
round_verts.len(),
notched_verts.len()
);
let round = render_paint(&device, &queue, &round_verts, &transform, HARD);
let notched = render_paint(&device, &queue, ¬ched_verts, &transform, HARD);
save(&round, "acute-notch-after");
save(¬ched, "acute-notch-before");
// The convex half-plane is `+x`; probe a ring at 75 % of the joint's radius.
let r = 0.75 * hw;
let probe = |img: &image::RgbaImage, deg: f32| -> bool {
let t = deg.to_radians();
let x = (corner[0] + r * t.cos()).round() as u32;
let y = (corner[1] + r * t.sin()).round() as u32;
img.get_pixel(x.min(W - 1), y.min(H - 1)).0[0] > 128
};
const RING: [f32; 7] = [-85.0, -60.0, -30.0, 0.0, 30.0, 60.0, 85.0];
let inked = |img: &image::RgbaImage| RING.iter().filter(|&&d| probe(img, d)).count();
let (with_joint, without) = (inked(&round), inked(¬ched));
eprintln!(
"[line-gpu] acute notch: {with_joint}/{} ring probes inked WITH the round joint, \
{without}/{} WITHOUT (r = {r:.1} px at hw = {hw:.1})",
RING.len(),
RING.len()
);
assert_eq!(
with_joint,
RING.len(),
"the convex side of a {:.0}° corner still has paper at radius {r:.1} px — {} of {} \
probes are background, so the notch is still there",
176,
RING.len() - with_joint,
RING.len()
);
assert!(
without + 2 <= RING.len(),
"MUTATION FAILED: stripping the round-end vertices left {without} of {} probes inked \
anyway. Then this corner never had a notch and the fixture proves nothing about \
closing one",
RING.len()
);
facett_core::testmatrix::emit(
"facett-map/gpu-line",
"an_acute_corner_has_no_notch",
true,
&format!("GG #25: {with_joint}/{} convex probes inked with the round joint, {without}/{} without", RING.len(), RING.len()),
);
}