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