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());
}