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

//! **A re-bake must REPLACE the geometry in VRAM, not add to it.**
//!
//! `MapFacet` deliberately re-bakes: `gpu_geom_sig` is a *signature* of the enabled way
//! set precisely so that toggling a layer, or korp streaming a new viewport in, sends a
//! new payload. Its own doc says why a bool latch would not do — *"a bool latch would
//! leave a switched-off layer's roads painted on the GPU forever, with the CPU painter
//! correctly not drawing them."*
//!
//! `GpuMapRenderer::upload` then `push`ed the new chunks onto `self.chunks` /
//! `self.line_chunks` and never cleared the old ones. `run_cull` dispatches over every
//! chunk and `paint` draws every chunk, so the switched-off layer's roads stayed painted
//! on the GPU forever anyway — the exact failure the signature latch exists to prevent,
//! reproduced one level down, the same shape as the discarded-sizing-frame bug
//! `gpu_geom_sent_at` was added for. VRAM grew by a whole payload on every toggle, pan or
//! viewport fetch.
//!
//! The observables here are the renderer's OWN chunk census (`line_chunk_count`,
//! `line_chunk_buffer_bytes`) — what the cull dispatches over and what the draw loops
//! through — not a bool and not a CPU-side `Vec::len()`.
//!
//! **They read the QUAD lane, and that is deliberate** (stinky `mapdraw-4`). The census
//! was originally taken on `chunk_count`/`chunk_buffer_bytes`, the 24 B `LineList`
//! hairline stream — which `paint` has never drawn. Since `HAIRLINE_LANE` turned that
//! stream off it holds no VRAM at all, so a leak measured there would have read 0 == 0
//! forever: a guard against a VRAM leak, watching a lane with no VRAM. The pretty-line
//! quads are the bytes a live frame actually holds, and the file's own
//! "else this test is blind" premise assertion is what caught the swap.
//!
//! Run: `cargo test -p facett-map --features wgpu --test upload_replaces_geometry`
#![cfg(feature = "wgpu")]

use facett_map::Way;
use facett_map::gpu::{GpuGeometry, GpuMapRenderer, build_gpu_geometry, ways_origin};

/// 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;

/// `n` roads in their own lon bands — a stand-in for one enabled layer's payload.
fn roads(n: usize, first_id: i64) -> Vec<Way> {
    let c = egui::Color32::from_rgb(0xE8, 0xB0, 0x30);
    (0..n)
        .map(|i| {
            let lon = 9.0 + i as f64 * 0.1;
            Way::line(first_id + i as i64, "road", vec![(lon, 47.0), (lon + 0.05, 47.02), (lon + 0.09, 47.0)], c, 5.0)
        })
        .collect()
}

fn geometry(ways: &[Way]) -> GpuGeometry {
    let origin = ways_origin(ways.iter()).expect("non-empty fixture has an origin");
    build_gpu_geometry(ways.iter().map(|w| (w, w.style.lod as u32)), origin)
}

/// **RED-WHEN-BROKEN.** Upload payload A, then payload B (the "a layer was toggled off"
/// re-bake — B is a strict SUBSET of A). What the cull dispatches over and the draw loops
/// through afterwards must be **B alone**.
///
/// Asserted on the chunk census and on the total VRAM bytes, because those are what the
/// per-frame `run_cull` loop and the `paint` loop actually walk. Before the fix the census
/// was A **plus** B: the roads the host had switched off kept drawing, and every fetch
/// leaked a payload's worth of VRAM.
#[test]
fn a_re_bake_replaces_the_uploaded_geometry_instead_of_adding_to_it() {
    let Some((device, queue)) = common::device_or_skip("a_re_bake_replaces_the_uploaded_geometry_instead_of_adding_to_it", "a re-bake REPLACES the uploaded geometry")
    else {
        return;
    };
    let fmt = wgpu::TextureFormat::Rgba8UnormSrgb;
    let mut r = GpuMapRenderer::new(&device, fmt);
    // A small chunk budget so ONE payload is already several chunks — otherwise a
    // 1-vs-2 census could not tell "replaced" from "the second one happened to fit".
    r.set_max_buffer_size_for_test(4 * 1024);

    let big = geometry(&roads(200, 1));
    r.upload_for_test(&device, &queue, &big);
    let line_chunks_a = r.line_chunk_count();
    let bytes_a: u64 = r.line_chunk_buffer_bytes().iter().map(|(g, m, c)| g + m + c).sum();
    assert!(
        line_chunks_a > 1,
        "the budget really did split payload A ({line_chunks_a} chunks) — else this test is blind"
    );
    assert!(bytes_a > 0, "payload A really reached VRAM");

    // The re-bake: the host switched a layer off, so the new set is SMALLER.
    let small = geometry(&roads(40, 1));
    r.upload_for_test(&device, &queue, &small);
    let line_chunks_b = r.line_chunk_count();
    let bytes_b: u64 = r.line_chunk_buffer_bytes().iter().map(|(g, m, c)| g + m + c).sum();

    // What a lone upload of the SMALL payload looks like — the ground truth.
    let mut fresh = GpuMapRenderer::new(&device, fmt);
    fresh.set_max_buffer_size_for_test(4 * 1024);
    fresh.upload_for_test(&device, &queue, &small);
    let want_line_chunks = fresh.line_chunk_count();
    let want_bytes: u64 = fresh.line_chunk_buffer_bytes().iter().map(|(g, m, c)| g + m + c).sum();

    assert_eq!(
        line_chunks_b, want_line_chunks,
        "after the re-bake the cull must dispatch over — and the draw loop through — the \
         NEW geometry only ({line_chunks_b} chunks vs {want_line_chunks} for a fresh upload \
         of the same payload; payload A alone was {line_chunks_a})"
    );
    assert_eq!(
        bytes_b, want_bytes,
        "and the VRAM held must be the new payload's, not the sum \
         ({bytes_b} B vs {want_bytes} B; A alone held {bytes_a} B)"
    );
    assert!(
        bytes_b < bytes_a,
        "a strictly smaller way set must hold strictly less VRAM ({bytes_b} B after, {bytes_a} B before)"
    );
}

/// The replacement must not break the growing case either: re-baking with a BIGGER set
/// (korp streams a denser viewport in) leaves exactly the bigger set.
#[test]
fn a_re_bake_with_more_ways_also_lands_exactly_the_new_set() {
    let Some((device, queue)) = common::device_or_skip("a_re_bake_with_more_ways_also_lands_exactly_the_new_set", "a wider re-bake lands exactly the new set")
    else {
        return;
    };
    let fmt = wgpu::TextureFormat::Rgba8UnormSrgb;
    let mut r = GpuMapRenderer::new(&device, fmt);
    r.set_max_buffer_size_for_test(4 * 1024);

    r.upload_for_test(&device, &queue, &geometry(&roads(40, 1)));
    let big = geometry(&roads(200, 1));
    r.upload_for_test(&device, &queue, &big);

    let mut fresh = GpuMapRenderer::new(&device, fmt);
    fresh.set_max_buffer_size_for_test(4 * 1024);
    fresh.upload_for_test(&device, &queue, &big);

    assert_eq!(r.line_chunk_count(), fresh.line_chunk_count());
    assert_eq!(r.line_chunk_buffer_bytes(), fresh.line_chunk_buffer_bytes());
}