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

//! **A footprint smaller than a mark is not drawn** — the minimum projected extent
//! (stinky `kalm-3`), asserted on both lanes.
//!
//! # The defect
//!
//! MEASURED off `$KORP_STATE` on two settled z12 panes over Stockholm: `building_ways`
//! **37 378** and **34 708** against an `osm_ways` cap of **80 000** — 43–47 % of
//! everything korp's server is allowed to send, every one of them a polygon the fill
//! lane tessellates. At camera 12.7 the pane's Mercator scale is 1.70 Mpx for the world,
//! so a 20 m house spans **1.67 px**. Half the budget, spent on shapes with no shape.
//!
//! The discrete [`Lod`](facett_map::Lod) ladder cannot express this. A building's tier
//! is a property of the building; its size on screen is a property of the camera, and
//! there is no tier that means "once it is big enough". The gate is continuous.
//!
//! # Why this file has three arms
//!
//! The item's own reason for not being fixed was that "the gate has to hold on BOTH
//! lanes or the lakes draw twice". So:
//!
//! 1. **CPU** — the pixels. A 20 m footprint at z12 must leave the canvas untouched, and
//!    the SAME footprint at z16 must paint. Not `feats.len()`: a length is a state
//!    round-trip that cannot tell a dropped feature from one drawn transparent.
//! 2. **GPU** — the `vertex_count` `draw_indirect` reads, out of the real cull, with the
//!    gate off and on. Same rule, same number, one Rust writer
//!    ([`facett_map::style::min_fill_extent_merc`]) feeding both.
//! 3. **LINES ARE EXEMPT** — a road stub of exactly the dropped building's size must
//!    still draw. This is the arm that matters most: a gate that quietly deleted the
//!    street network would satisfy arms 1 and 2 perfectly.
//!
//! Run under the box's shared GPU lock:
//! `flock ~/scratch/locks/gpu.lock cargo test -p facett-map --features wgpu --test fill_extent_gate`

use egui_kittest::Harness;
use facett_map::layer::{LatLon, MapFacet, MapTheme, MapView, WaysLayer};
use facett_map::{MapPalette, Way};

mod common;

// **The one-device-at-a-time turn lives in `common`** — `common/one_gpu.rs`, reached as
// `common::one_gpu` because this file already says `mod common;`. Declaring a second
// `#[path]` copy at the crate root of this test binary would compile two DIFFERENT
// statics out of one file, and two mutexes over one device serialise nothing.

/// A closed square ring `deg` degrees across at `(lon, lat)`, tagged `tags`.
fn square(id: i64, lon: f64, lat: f64, deg: f64, tags: &[(&str, &str)]) -> Way {
    let t: Vec<(String, String)> = tags.iter().map(|(k, v)| ((*k).to_string(), (*v).to_string())).collect();
    let r = vec![(lon, lat), (lon + deg, lat), (lon + deg, lat + deg), (lon, lat + deg), (lon, lat)];
    Way::from_lonlat_with(id, &r, &t, &MapPalette::cartographic(false)).expect("a closed ring is a way")
}

/// ~20 m at this latitude — the footprint the census counted 37 378 of.
const HOUSE_DEG: f64 = 2.0e-4;

fn render_at(zoom: f32, ways: Vec<Way>) -> (u32, u32, Vec<u8>) {
    let _turn = common::one_gpu::hold();
    let view = MapView::new(LatLon::new(59.3300, 18.0600), zoom, (520.0, 400.0));
    let mut f = MapFacet::new("extent", view, MapTheme::default())
        .with_layer(Box::new(WaysLayer::new("osm-ways", ways)));
    let mut harness = Harness::builder().with_size(egui::vec2(520.0, 400.0)).build_ui(move |ui| {
        f.ui(ui);
    });
    harness.run_steps(3);
    let img = harness.render().expect("wgpu render produced an image");
    (img.width(), img.height(), img.as_raw().clone())
}

/// How many pixels differ between a frame with the fixture and the same frame empty.
fn ink(zoom: f32, ways: Vec<Way>) -> usize {
    let (_, _, empty) = render_at(zoom, Vec::new());
    let (_, _, with) = render_at(zoom, ways);
    with.as_chunks::<4>().0.iter().zip(empty.as_chunks::<4>().0).filter(|(a, b)| a != b).count()
}

// ─────────────────────────────────────────────────────────────────────────────

/// **Arm 1 + arm 3, on pixels.** The 20 m house is gone at z12 and back at z16; a road
/// stub of exactly the same size draws at BOTH.
///
/// RED: delete the `w.fill.is_some()` branch from `WaysLayer::draws` — the house paints
/// its ~1 px at z12 and the first assertion fires. RED the other way: drop the
/// `w.fill.is_some()` condition so the gate applies to lines too — the road stub vanishes
/// at z12 and the third assertion fires.
#[test]
fn a_sub_pixel_footprint_is_dropped_but_a_road_of_the_same_size_is_not() {
    if common::headless_device("a_sub_pixel_footprint_is_dropped_but_a_road_of_the_same_size_is_not").is_none() {
        return;
    }
    let house = || vec![square(1, 18.0600, 59.3300, HOUSE_DEG, &[("building", "yes")])];
    // The same extent, as a LINE: a residential stub two vertices long.
    let stub = || {
        let t = vec![("highway".to_string(), "residential".to_string())];
        vec![
            Way::from_lonlat_with(2, &[(18.0600, 59.3300), (18.0600 + HOUSE_DEG, 59.3300 + HOUSE_DEG)], &t, &MapPalette::cartographic(false))
                .expect("a 2-vertex line is a way"),
        ]
    };

    let house_z12 = ink(12.0, house());
    let house_z16 = ink(16.0, house());
    let stub_z12 = ink(12.0, stub());

    assert_eq!(
        house_z12, 0,
        "a 20 m footprint at camera 12 is ~1.2 px and must not be drawn at all — it painted \
         {house_z12} pixels. 43-47 % of korp's z12 way budget is exactly this shape."
    );
    assert!(
        house_z16 > 0,
        "the SAME footprint must come back once the camera earns it (z16) — it painted \
         {house_z16} pixels. Without this the gate is indistinguishable from losing the building."
    );
    assert!(
        stub_z12 > 0,
        "a ROAD of exactly the dropped footprint's extent must still draw at z12 — it painted \
         {stub_z12} pixels. A gate that took the street network with it would satisfy every \
         other assertion in this file."
    );

    eprintln!("[extent-gate] house z12 {house_z12} px, house z16 {house_z16} px, road stub z12 {stub_z12} px");
    nornir_testmatrix::functional_status(
        "facett-map",
        "fill_extent_gate_cpu_pixels",
        house_z12 == 0 && house_z16 > 0 && stub_z12 > 0,
        &format!("house z12 {house_z12} px, house z16 {house_z16} px, road stub z12 {stub_z12} px"),
    );
}

/// **Arm 2: the GPU lane applies the SAME rule.** Drive the real cull with the gate off
/// and on, and read the `vertex_count` `draw_indirect` reads.
///
/// RED: delete the `FLAG_FILL` test from `cull.wgsl`'s `visible()` — the gated count
/// equals the ungated one and the second assertion fires. RED the other way: set
/// `flags: 0` on the fill metas in `build_gpu_fill_geometry` — same thing, because the
/// cull can no longer tell a lake from a road.
#[cfg(feature = "wgpu")]
#[test]
fn the_gpu_cull_drops_the_same_footprints_the_cpu_painter_does() {
    use facett_map::gpu::{GpuMapRenderer, build_gpu_fill_geometry, ways_origin};
    use facett_map::tessellate::FillTessCache;

    let Some((device, queue)) = common::device_or_skip(
        "the_gpu_cull_drops_the_same_footprints_the_cpu_painter_does",
        "the GPU cull applies the minimum-projected-extent gate to areas",
    ) else {
        return;
    };

    // 40 little houses and one big lake, all in one bake.
    let mut ways: Vec<Way> = (0..40u16)
        .map(|i| square(100 + i64::from(i), 18.06 + f64::from(i) * 1.0e-3, 59.33, HOUSE_DEG, &[("building", "yes")]))
        .collect();
    ways.push(square(1, 18.00, 59.30, 0.05, &[("natural", "water")]));

    let origin = ways_origin(ways.iter()).expect("non-empty fixture");
    let cache = FillTessCache::new(ways.len());
    let items: Vec<_> = ways
        .iter()
        .enumerate()
        .filter_map(|(i, w)| cache.tris(i, &w.lonlat, &w.holes).map(|t| (w, w.style.lod as u32, t)))
        .collect();
    // A fills-only bake: the line lane has nothing to say about area extents.
    let mut geo = facett_map::gpu::build_gpu_geometry(std::iter::empty(), origin);
    geo.fills = build_gpu_fill_geometry(items.into_iter(), origin);
    assert_eq!(geo.fills.tri_meta.len(), ways.len(), "every ring baked a triangle span");

    let mut r = GpuMapRenderer::new(&device, wgpu::TextureFormat::Rgba8Unorm);
    r.upload_for_test(&device, &queue, &geo);

    let bbox = [-1.0e6_f32, -1.0e6, 1.0e6, 1.0e6];
    let cull = |min_ext: f32| {
        let mut enc = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("extent") });
        r.run_cull(&queue, &mut enc, bbox, 3, min_ext);
        queue.submit(Some(enc.finish()));
        device.poll(wgpu::PollType::wait_indefinitely()).ok();
        r.fill_draw_counts(&device, &queue)
    };

    let (tris_off, out_off) = cull(0.0);
    // z12: world_px = 256 * 2^12 = 1 048 576, so the floor is 3 / 1 048 576 Mercator units.
    let min_ext = facett_map::style::min_fill_extent_merc(256.0 * 2f64.powi(12));
    let (tris_on, out_on) = cull(min_ext);

    assert!(tris_off > 0, "with the gate OFF the lane must draw the whole fixture, got {tris_off}");
    assert!(
        tris_on > 0,
        "the LAKE is 0.05° across and must survive the gate — a gate that empties the fill lane \
         is not a gate"
    );
    assert!(
        tris_on < tris_off,
        "the gate must actually remove the 40 sub-pixel houses: {tris_on} vs {tris_off} triangle \
         vertices. Equal means `cull.wgsl` never read `FLAG_FILL`."
    );
    assert!(
        out_on < out_off,
        "and their ring OUTLINES must go with them ({out_on} vs {out_off}) — a wireframe of a \
         building nobody can see is the same pixel"
    );

    eprintln!("[extent-gate] gpu tris {tris_off} -> {tris_on}, outlines {out_off} -> {out_on} (min_ext {min_ext:e})");
    nornir_testmatrix::functional_status(
        "facett-map",
        "fill_extent_gate_gpu_cull",
        tris_on > 0 && tris_on < tris_off && out_on < out_off,
        &format!("tris {tris_off} -> {tris_on}, outlines {out_off} -> {out_on}"),
    );
}