facett-map/tests/cull_envelope.rs
//! **The cull compares the right envelope** — `Way::bbox` in f64, dilated by the stroke
//! the painter actually draws (stinky `seam-15`).
//!
//! # Two defects, one envelope
//!
//! 1. **The bbox was f32, derived from the f32 `points` stream.** This crate already
//! knows that stream is too coarse to draw from — `Way::points`' own doc says it
//! snaps "to a ~5px grid at ZOOM_MAX", which is why the painter and the GPU bake
//! both reproject out of `lonlat` in f64. The bbox never got the same treatment, and
//! the bbox is what EVERY cull rests on: `ways_origin`, the `GpuWayMeta` the compute
//! cull tests, the `MapIndex` lattice, and `WaysLayer::draws` comparing it against a
//! viewport derived in f64.
//! 2. **It was never dilated by the drawn width.** A way's envelope is its
//! CENTRELINE's. A motorway whose centreline misses the pane by one pixel was culled
//! entirely — taking a 24 px ribbon, its casing and its feather with it, so the road
//! ended in mid-air at the edge of the canvas and the next pan snapped it back.
//!
//! # The item filed the magnitude as INFERRED. This measures it.
//!
//! `seam-15` computed ~32 px at zoom 22 from the ULP size and said so honestly. The
//! first test here measures the real thing — the old f32 path against the f64 one, on
//! real Swedish coordinates — and reports the error in screen pixels at four cameras.
//!
//! Run: `flock ~/scratch/locks/gpu.lock cargo test -p facett-map --features wgpu --test cull_envelope`
use facett_map::{MapPalette, Way, mercator, project};
mod common;
/// **MEASURED, not inferred: how far off the f32 envelope was, in screen pixels.**
///
/// Walks real Swedish coordinates through both paths — `to_mercator_f32(lon as f32,
/// lat as f32)`, which is what `Way::bbox` used to be built from, and `mercator(lon,
/// lat)`, which is what it is built from now — and converts the worst disagreement into
/// pixels at each camera through `world_px`.
///
/// The assertion is on the FIX, not on the old error: the bbox a `Way` carries must now
/// agree with the f64 Mercator of its own extreme vertices to f64 precision. The old
/// number is printed because a magnitude nobody measured is how `seam-15` sat open.
#[test]
fn the_bbox_is_the_f64_envelope_and_the_old_f32_error_is_this_big() {
let pal = MapPalette::cartographic(false);
// Sweden, corner to corner, plus the two cities the rest of this suite uses.
let pts: Vec<(f64, f64)> = vec![
(11.0, 55.3),
(13.0, 55.4),
(18.06, 59.33),
(17.64, 59.85),
(12.0, 57.7),
(20.3, 63.8),
(24.15, 65.85),
(18.0, 68.35),
];
let mut worst_merc = 0.0_f64;
for &(lon, lat) in &pts {
let (fx, fy) = project::to_mercator_f32(lon as f32, lat as f32);
let (dx, dy) = mercator(lon, lat);
worst_merc = worst_merc.max((f64::from(fx) - dx).abs()).max((f64::from(fy) - dy).abs());
}
for zoom in [12.0_f64, 15.0, 18.0, 22.0] {
let world_px = 256.0 * 2f64.powf(zoom);
eprintln!("[envelope] the old f32 bbox was off by {:.1} px at zoom {zoom:.0}", worst_merc * world_px);
}
assert!(worst_merc > 0.0, "the two projections must actually disagree, or this measures nothing");
// ── THE FIX: a Way's bbox now IS the f64 envelope of its own vertices. ───
let mut worst_bbox = 0.0_f64;
for &(lon, lat) in &pts {
let d = 1.0e-3;
let ring = vec![(lon, lat), (lon + d, lat), (lon + d, lat + d), (lon, lat + d), (lon, lat)];
let w = Way::from_lonlat_with(1, &ring, &[("building".to_string(), "yes".to_string())], &pal)
.expect("a closed ring is a way");
let (mut x0, mut y0, mut x1, mut y1) = (f64::MAX, f64::MAX, f64::MIN, f64::MIN);
for &(rlon, rlat) in &ring {
let (x, y) = mercator(rlon, rlat);
x0 = x0.min(x);
y0 = y0.min(y);
x1 = x1.max(x);
y1 = y1.max(y);
}
for (got, want) in w.bbox.iter().zip([x0, y0, x1, y1]) {
worst_bbox = worst_bbox.max((got - want).abs());
}
}
// At zoom 22 one pixel is 1/(256·2²²) ≈ 9.3e-10 Mercator units. The f64 envelope
// must be far inside that — this is the assertion the f32 bbox could never pass.
let one_px_at_z22 = 1.0 / (256.0 * 2f64.powi(22));
assert!(
worst_bbox < one_px_at_z22 * 1.0e-3,
"the bbox must be the f64 envelope of the way's own vertices — worst disagreement \
{worst_bbox:e} Mercator units, i.e. {:.3} px at zoom 22 (budget one thousandth of a pixel)",
worst_bbox / one_px_at_z22
);
eprintln!(
"[envelope] the f64 bbox agrees with the f64 Mercator to {worst_bbox:e} units ({:.2e} px at z22)",
worst_bbox / one_px_at_z22
);
nornir_testmatrix::functional_status(
"facett-map",
"bbox_is_the_f64_envelope",
worst_bbox < one_px_at_z22 * 1.0e-3,
&format!(
"old f32 error {:.1} px at z22; new bbox agrees to {:.2e} px",
worst_merc * 256.0 * 2f64.powi(22),
worst_bbox / one_px_at_z22
),
);
}
/// **A way whose CENTRELINE is off the pane is still handed to the draw.**
///
/// The applied output is the `vertex_count` `draw_indirect` reads, straight out of the
/// real cull.
///
/// **Why this is a GPU test and not a pixel test.** `MapView::merc_bbox` — what the CPU
/// painter culls against — uses the pane's half-DIAGONAL, so it already over-covers a
/// 520×400 pane by 68 px on every side; the missing dilation simply cannot show there.
/// `MapTransform::viewport_bbox` is the tight one: it unprojects the four corners, and
/// that is the box the compute cull tests every way's bbox against. So the defect lives
/// on the GPU lane, which is the lane that draws, and this is where it has to be
/// asserted. (Stating it rather than quietly testing the wrong lane: the first draft of
/// this test rendered pixels and passed for the wrong reason.)
///
/// RED: drop the `pad` from `MapTransform::viewport_bbox` — the near way's count goes
/// to 0.
///
/// **Gated per-FN, not per-FILE.** `facett_map::gpu` only exists with `--features
/// wgpu`, and without this attribute the whole target failed to COMPILE on a default
/// `cargo test -p facett-map` — which took the CPU test above, and the other 200 tests
/// this crate declares, down with it (measured 2026-08-22: 0 tests ran). The file stays
/// ungated so the f64-envelope proof runs on every build.
#[cfg(feature = "wgpu")]
#[test]
fn a_way_just_off_the_pane_survives_the_cull_that_its_stroke_reaches_into() {
let Some((device, queue)) = common::device_or_skip(
"a_way_just_off_the_pane_survives_the_cull_that_its_stroke_reaches_into",
"the cull viewport is dilated by the widest stroke the painter can draw",
) else {
return;
};
use facett_map::gpu::{GpuMapRenderer, MapTransform, build_gpu_geometry, ways_origin};
const ZOOM: f32 = 14.0;
let world_px = 256.0 * 2f64.powf(f64::from(ZOOM));
let (cx, cy) = mercator(18.06, 59.33);
let px = 1.0 / world_px;
let rect = egui::Rect::from_min_size(egui::pos2(0.0, 0.0), egui::vec2(520.0, 400.0));
let lon_of = |mx: f64| mx * 360.0 - 180.0;
// The pane's left edge, then a vertical road 4 px OUTSIDE it — inside
// `CULL_PAD_PX` (15 px at any zoom), so part of its ribbon lands on the canvas —
// and one 400 px outside, which nothing can reach.
let left_edge = cx - 260.0 * px;
let near_lon = lon_of(left_edge - 4.0 * px);
let far_lon = lon_of(left_edge - 400.0 * px);
let count_for = |lon: f64| -> u32 {
let t = vec![("highway".to_string(), "motorway".to_string())];
let ways = [
Way::from_lonlat_with(1, &[(lon, 59.20), (lon, 59.46)], &t, &MapPalette::cartographic(false))
.expect("a 2-vertex line is a way"),
];
let origin = ways_origin(ways.iter()).expect("non-empty fixture");
let geo = build_gpu_geometry(ways.iter().map(|w| (w, w.style.lod as u32)), origin);
let mut r = GpuMapRenderer::new(&device, wgpu::TextureFormat::Rgba8Unorm);
r.upload_for_test(&device, &queue, &geo);
// The SAME transform the live frame builds, so the SAME `viewport_bbox`.
let tf = MapTransform::new(origin, [cx, cy], world_px as f32, world_px as f32, rect);
let mut enc = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("edge") });
r.run_cull(&queue, &mut enc, tf.viewport_bbox(rect), 3, 0.0);
queue.submit(Some(enc.finish()));
device.poll(wgpu::PollType::wait_indefinitely()).ok();
r.line_draw_counts(&device, &queue).iter().sum()
};
let near = count_for(near_lon);
let far = count_for(far_lon);
assert!(
near > 0,
"a road 4 px outside the pane must survive the cull — its ribbon reaches in, and its \
bbox is only its CENTRELINE's. Got {near} quad vertices."
);
assert_eq!(
far, 0,
"a road 400 px outside must be culled — got {far} quad vertices. Without this the test \
passes on a cull that stopped culling."
);
eprintln!("[envelope] gpu cull: 4 px off the pane keeps {near} quad verts; 400 px off keeps {far}");
nornir_testmatrix::functional_status(
"facett-map",
"cull_pad_keeps_the_edge_stroke",
near > 0 && far == 0,
&format!("4 px off the pane keeps {near} quad verts; 400 px off keeps {far}"),
);
}