Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/facett

facett-demo/tests/gpu_chunk.rs

//! **Fix #12 — chunk the wgpu geom buffer.** A whole country's way geometry
//! (Sweden ≈ 314 MB of `Vertex`) exceeds wgpu's default `max_buffer_size`
//! (256 MiB), so the old single-`geom`-buffer upload failed
//! `Device::create_buffer` with a Validation Error → panic. The upload now splits
//! the geometry into chunks, each buffer `≤ max_buffer_size`, with a `cull`
//! dispatch + `draw_indirect` per chunk.
//!
//! This test drives the **real** chunked upload on the egui_kittest wgpu device
//! and asserts, as DATA (no pixels):
//!   1. country-scale geometry would overflow one buffer (the bug's precondition);
//!   2. `upload_for_test` runs WITHOUT a panic (was a Validation panic);
//!   3. `line_chunk_count() ≥ 2` and EVERY per-chunk buffer is `≤ max_buffer_size`.
//!
//! **The census reads the PRETTY-LINE lane**, and it has to (stinky `mapdraw-4`). It
//! was written against `chunk_count()`/`chunk_buffer_bytes()`, the 24 B `LineList`
//! hairline stream — which `MapPaintCallback::paint` has never drawn and which
//! `facett_map::gpu::HAIRLINE_LANE` now switches off at the bake, so it holds no
//! buffers to bound. The 48 B `LineVertex` quads are the payload a live frame really
//! partitions, and they are also the stream wgpu refused at z15
//! (`line_cull_bg … range 198672192`), so this is the lane the fix was always about.
//!
//! Run: `cargo test -p facett-demo --test gpu_chunk -- --nocapture`
//!
//! FAILS-ON-BUG / PASSES-ON-FIX: with the pre-fix single-buffer upload, step (2)
//! panics on a country-scale set (or step 3's per-buffer bound is violated when
//! the budget is forced small) — `git stash` the gpu/mod.rs change to see it.

use facett::map::Way;
use facett::map::gpu::{GpuMapRenderer, build_gpu_geometry, ways_origin};

/// Build the GPU geometry payload from a way set (lines only, as the live path).
/// Vertices are baked origin-local (`mercator_f64(lonlat) − origin`); the chunk
/// partition only depends on vertex COUNT, so the data-local origin is irrelevant
/// to what this test asserts — pass the dataset's shared origin all the same.
fn geom_of(ways: &[Way]) -> facett::map::gpu::GpuGeometry {
    let origin = ways_origin(ways.iter()).unwrap_or([0.0, 0.0]);
    build_gpu_geometry(ways.iter().map(|w| (w, w.style.lod as u32)), origin)
}

/// A fresh wgpu render state from egui_kittest's default test setup (real GPU).
fn render_state() -> egui_wgpu::RenderState {
    egui_kittest::wgpu::create_render_state(egui_kittest::wgpu::default_wgpu_setup(), Default::default())
}

/// Load a large way set. Prefer Sweden (the real overflow case) via the env
/// override; else liechtenstein; else the synthetic sample. Returns the ways +
/// a label so the emit names the source.
fn big_ways() -> (Vec<Way>, &'static str) {
    let theme = facett::Theme::default();
    let home = std::env::var("HOME").unwrap_or_default();
    let sweden = format!("{home}/work/osm/sweden/ways.parquet");
    let sweden = sweden.as_str();
    if std::path::Path::new(sweden).exists() {
        // 600k ways is plenty to blow past 256 MiB of Vertex (≈ tens of MB → the
        // forced-small-budget arm still proves chunking even if real bytes < cap).
        if let Ok(w) = facett::osm::read_ways(sweden, 1_500_000, &theme) && !w.is_empty() {
            return (w, "sweden");
        }
    }
    let lk = format!("{home}/work/osm/liechtenstein/ways.parquet");
    let lk = lk.as_str();
    if std::path::Path::new(lk).exists()
        && let Ok(w) = facett::osm::read_ways(lk, 400_000, &theme)
        && !w.is_empty()
    {
        return (w, "liechtenstein");
    }
    let (_p, ways) = facett::geomap::osm2d::sample_dataset(20_000);
    (ways, "synthetic")
}

#[test]
fn country_scale_geometry_uploads_in_chunks_without_panic() {
    let rs = render_state();
    let (ways, src) = big_ways();
    let geo = geom_of(&ways);
    let total_bytes =
        (geo.line_vertices.len() * std::mem::size_of::<facett::map::gpu::LineVertex>()) as u64;

    let mut renderer = GpuMapRenderer::new(&rs.device, rs.target_format);
    let device_cap = renderer.max_buffer_size();
    eprintln!(
        "EMIT12 src={src} ways={} quad_verts={} total_geom_bytes={total_bytes} device_max_buffer_size={device_cap}",
        ways.len(),
        geo.line_vertices.len()
    );

    // ── Arm A: the device-cap path. If this country's real geometry exceeds the
    // device buffer cap, the single-buffer upload would have panicked here. ──
    let exceeds_real_cap = total_bytes > device_cap;

    // ── Arm B: force a small budget so the partition ALWAYS splits + we can bound
    // every buffer against it (a 4 MiB cap → many chunks for any real country). ──
    let forced_cap = 4 * 1024 * 1024; // 4 MiB
    renderer.set_max_buffer_size_for_test(forced_cap);

    // The upload must NOT panic (the bug was a create_buffer Validation panic).
    renderer.upload_for_test(&rs.device, &rs.queue, &geo);

    let chunks = renderer.line_chunk_buffer_bytes();
    let chunk_count = renderer.line_chunk_count();
    let max_geom = chunks.iter().map(|c| c.0).max().unwrap_or(0);
    eprintln!(
        "EMIT12 chunk_count={chunk_count} forced_cap={forced_cap} max_chunk_geom_bytes={max_geom} exceeds_real_cap={exceeds_real_cap}"
    );
    for (i, (g, m, c)) in chunks.iter().enumerate() {
        eprintln!("EMIT12  chunk[{i}] geom={g} meta={m} compact={c}");
    }

    // The upload produced at least one chunk and every buffer respects the budget.
    assert!(chunk_count >= 1, "upload produced geometry chunks");
    for (g, m, c) in &chunks {
        assert!(*g <= forced_cap, "chunk geom buffer {g} ≤ forced budget {forced_cap}");
        assert!(*c <= forced_cap, "chunk compact buffer {c} ≤ forced budget {forced_cap}");
        let _ = m; // meta is per-way (tiny), bounded by construction.
    }

    // With a real country the forced 4 MiB budget forces MANY chunks (>1): that is
    // the partition working. A tiny synthetic fixture may fit one chunk — only
    // demand the split when the data is actually large enough to need it.
    if total_bytes > forced_cap {
        assert!(chunk_count >= 2, "geom larger than the budget split into ≥2 chunks (got {chunk_count})");
    }

    // Emit one matrix row: ok iff a multi-chunk upload of country-scale geometry
    // completed without panic and every buffer stayed within the budget.
    #[cfg(feature = "testmatrix")]
    {
        let all_bounded = chunks.iter().all(|(g, _m, c)| *g <= forced_cap && *c <= forced_cap);
        let ok = chunk_count >= 1 && all_bounded;
        facett::core::testmatrix::emit(
            "facett-map",
            "gpu_geom_buffer_chunked",
            ok,
            &format!(
                "src={src} ways={} quad_verts={} total_bytes={total_bytes} device_cap={device_cap} chunks={chunk_count} max_chunk_geom={max_geom}",
                ways.len(),
                geo.line_vertices.len()
            ),
        );
    }
}