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

//! **The chunk budget must respect the limit the DEVICE enforces on a storage
//! binding, not the larger one it enforces on a buffer** (only with `--features wgpu`).
//!
//! `GpuMapRenderer` splits country-scale geometry into chunks so no single buffer
//! exceeds the device's budget (fix #12). Every one of those chunk buffers is then
//! handed to `create_bind_group` as a STORAGE binding — and wgpu governs storage
//! bindings by `max_storage_buffer_binding_size` (128 MiB by default), which is HALF
//! the `max_buffer_size` (256 MiB) the budget was being taken from. So the partition
//! could emit a chunk that `create_buffer` accepted and `create_bind_group` refused.
//!
//! It fired for real on 2026-08-04, on oden's RTX 4090, the moment korp-ui's camera
//! stepped onto the new z15 rung of the [6, 9, 12, 15] pyramid — a hard panic that
//! killed the app mid-drive:
//!
//! ```text
//! wgpu error: Validation Error
//!   In Device::create_bind_group, label = 'line_cull_bg'
//!     Buffer binding 1 range 198672192 exceeds `max_*_buffer_binding_size` limit 134217728
//! ```
//!
//! **Why this file exists rather than an extra line in `gpu_chunk.rs`.** That test
//! asserts every chunk is `<= max_buffer_size` and it was GREEN across the entire
//! crash, because it checks the same wrong limit the code did. A guard that reads its
//! expectation from the thing under test cannot fail with it. This one asserts against
//! `device.limits()` directly, so the two numbers have independent authors.
//!
//! Run: `cargo test -p facett-map --features wgpu --test storage_binding_budget -- --nocapture --test-threads=1`
//! (`--test-threads=1` because several `wgpu::Instance` bring-ups in one process hang
//! on this box — `kalm-5` in `workspace_nordisk/stinky`.)
#![cfg(feature = "wgpu")]

use facett_map::Way;
use facett_map::gpu::{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;

fn one_road() -> Vec<Way> {
    vec![Way::line(
        1,
        "road",
        vec![(9.00, 47.00), (9.05, 47.02), (9.10, 47.00)],
        egui::Color32::from_rgb(0xE8, 0xB0, 0x30),
        6.0,
    )]
}

#[test]
fn the_chunk_budget_never_exceeds_what_a_storage_binding_may_hold() {
    let Some((device, queue)) = common::device_or_skip("the_chunk_budget_never_exceeds_what_a_storage_binding_may_hold", "the chunk budget vs this device's max_storage_buffer_binding_size — the z15 line_cull_bg panic")
    else {
        return;
    };
    let limits = device.limits();
    let binding_cap = limits.max_storage_buffer_binding_size;

    // The fixture the device would have to be able to bind for this test to mean
    // anything: the two limits must actually differ, or the assertion below is
    // satisfied by coincidence and would stay green with the defect restored.
    assert!(
        limits.max_buffer_size > binding_cap,
        "this device reports max_buffer_size {} <= max_storage_buffer_binding_size {} — the two \
         limits do not differ here, so this test cannot tell the fix from the defect and must not \
         be read as a pass",
        limits.max_buffer_size,
        binding_cap,
    );

    let ways = one_road();
    let origin = ways_origin(ways.iter()).expect("non-empty fixture has an origin");
    let geo = build_gpu_geometry(ways.iter().map(|w| (w, w.style.lod as u32)), origin);
    let mut r = GpuMapRenderer::new(&device, wgpu::TextureFormat::Rgba8UnormSrgb);
    r.upload_for_test(&device, &queue, &geo);

    let budget = r.max_buffer_size();
    println!(
        "[storage-budget] device: max_buffer_size={} max_storage_buffer_binding_size={} \
         | renderer chunk budget={}",
        limits.max_buffer_size, binding_cap, budget,
    );
    assert!(
        budget <= binding_cap,
        "the chunk budget is {budget} B but a storage BINDING on this device may hold at most \
         {binding_cap} B — the partition can emit a chunk that create_buffer accepts and \
         create_bind_group then refuses, which is the z15 'line_cull_bg' panic",
    );

    // And the chunks the partition actually produced honour it, so the budget is not
    // merely stored correctly but used.
    //
    // **The loop below iterates ZERO times, and says so.** It walks
    // `chunk_buffer_bytes()`, the 24 B `LineList` hairline lane — which nothing draws
    // and which `HAIRLINE_LANE` now switches off at the bake (stinky `mapdraw-4`), so
    // there are no chunks to bound. Even when the lane was live it was vacuous here:
    // `one_road()` is a three-point way, a few hundred bytes against a 134 217 728 B
    // cap, so no iteration of it could fail, and reading it as proof that the partition
    // respects the cap is exactly the hollow green `granska-25` named. It is kept
    // because the budget VALUE above is what this file is for; the partition under a
    // cap it can actually reach is proven by
    // `a_line_payload_over_the_storage_cap_is_split_into_bindable_chunks` below, on the
    // LIVE quad lane and on a fixture whose premise is asserted.
    for (i, (geom_b, meta_b, compact_b)) in r.chunk_buffer_bytes().into_iter().enumerate() {
        for (what, n) in [("geom", geom_b), ("meta", meta_b), ("compact", compact_b)] {
            assert!(
                n <= binding_cap,
                "chunk {i}'s {what} buffer is {n} B, over the {binding_cap} B storage-binding cap",
            );
        }
    }
}

/// Enough zig-zag road to push the **pretty-line** stream past a storage binding.
///
/// `LineVertex` is 48 B and the quad baker emits **6** of them per segment (MEASURED:
/// 1 900 ways x 150 segments baked 1 710 000 quad vertices), so a segment costs 288 B and
/// 134 217 728 B is 466 034 segments. `WAYS × (PTS - 1)` is sized ~1.25× over that, and
/// the test ASSERTS the resulting byte total actually clears the device's cap rather than
/// assuming it — a fixture that quietly fell under the cap would turn the whole check
/// back into the vacuous loop it replaces. That assertion has been seen RED: the first
/// sizing here used 12 vertices per segment, baked 82 080 000 B, and the run stopped on
/// "the fixture ... is UNDER this device's 134 217 728 B storage-binding cap" instead of
/// reporting a green it had not earned.
const WAYS: usize = 3_900;
const PTS: usize = 151;

fn a_country_of_roads() -> Vec<Way> {
    let c = egui::Color32::from_rgb(0xE8, 0xB0, 0x30);
    (0..WAYS)
        .map(|w| {
            let lon0 = 9.0 + (w % 100) as f64 * 0.01;
            let lat0 = 47.0 + (w / 100) as f64 * 0.01;
            let pts: Vec<(f64, f64)> = (0..PTS)
                .map(|p| {
                    // A zig-zag: consecutive points always differ, so the baker cannot
                    // dedupe the segment away and the byte count is predictable.
                    (lon0 + p as f64 * 1e-4, lat0 + if p % 2 == 0 { 1e-4 } else { -1e-4 })
                })
                .collect();
            Way::line(w as i64 + 1, "road", pts, c, 6.0)
        })
        .collect()
}

/// **The partition, on a payload that actually reaches the cap** — the arm the
/// three-point fixture above could never be.
///
/// The z15 crash was not in the 24 B hairline lane the older loop reads; it was
/// `line_cull_bg`, a 48 B `LineVertex` binding of 198 672 192 B, and until now nothing
/// could even read those bytes (`line_chunk_buffer_bytes` is new for this test). So this
/// asserts three things in order, and the first two exist to stop the third going hollow:
///
/// 1. the baked pretty-line stream is LARGER than this device's storage-binding cap —
///    the fixture premise, restated as an assertion;
/// 2. the partition therefore had to split it, `line_chunk_count() >= 2`;
/// 3. every one of those bindings is within the cap.
///
/// Seen RED: with `min(max_buffer_size, max_storage_buffer_binding_size)` at
/// `gpu/mod.rs` reverted to `max_buffer_size` — the defect — this fixture produces ONE
/// 216 MiB line chunk and wgpu refuses the bind group with the verbatim 2026-08-04
/// message. With the `min` in place it produces two, both under the cap.
#[test]
fn a_line_payload_over_the_storage_cap_is_split_into_bindable_chunks() {
    const CHECK: &str = "a_line_payload_over_the_storage_cap_is_split_into_bindable_chunks";
    let Some((device, queue)) = common::device_or_skip(
        CHECK,
        "a pretty-line payload larger than max_storage_buffer_binding_size splits into \
         bindable chunks — the lane the z15 line_cull_bg panic was in",
    ) else {
        return;
    };
    let limits = device.limits();
    let binding_cap = limits.max_storage_buffer_binding_size;
    assert!(
        limits.max_buffer_size > binding_cap,
        "this device reports max_buffer_size {} <= max_storage_buffer_binding_size {binding_cap} \
         — the two limits do not differ here, so this test cannot tell the fix from the defect",
        limits.max_buffer_size,
    );

    let ways = a_country_of_roads();
    let origin = ways_origin(ways.iter()).expect("non-empty fixture has an origin");
    let geo = build_gpu_geometry(ways.iter().map(|w| (w, w.style.lod as u32)), origin);
    let line_bytes =
        (geo.line_vertices.len() * std::mem::size_of::<facett_map::gpu::LineVertex>()) as u64;
    println!(
        "[storage-budget] fixture: {} ways x {PTS} pts -> {} LineVertex = {line_bytes} B \
         (binding cap {binding_cap} B, buffer cap {} B)",
        ways.len(),
        geo.line_vertices.len(),
        limits.max_buffer_size,
    );

    // (1) THE PREMISE. Without this the loop below is the vacuous one it replaces.
    assert!(
        line_bytes > binding_cap,
        "the fixture baked {line_bytes} B of pretty-line geometry, which is UNDER this \
         device's {binding_cap} B storage-binding cap — the partition is never forced to \
         split and every assertion after this one is vacuous. Raise WAYS/PTS.",
    );
    // …and under the buffer cap, or the defect would fail at create_buffer instead of at
    // create_bind_group and this would be testing the wrong refusal.
    assert!(
        line_bytes < limits.max_buffer_size,
        "the fixture is {line_bytes} B, over max_buffer_size {} — the defect this guards \
         is a binding refusal one call AFTER a create_buffer that succeeded",
        limits.max_buffer_size,
    );

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

    let chunks = r.line_chunk_buffer_bytes();
    let widest = chunks.iter().map(|c| c.0.max(c.1).max(c.2)).max().unwrap_or(0);
    println!(
        "[storage-budget] {} line chunk(s), widest binding {widest} B, budget {} B",
        chunks.len(),
        r.max_buffer_size(),
    );

    // (2) it split.
    assert!(
        chunks.len() >= 2,
        "{line_bytes} B of pretty-line geometry against a {binding_cap} B binding cap \
         produced {} chunk(s) — the partition did not split a payload that cannot be bound \
         whole, which is the z15 panic",
        chunks.len(),
    );
    // (3) every binding fits.
    for (i, (src_b, meta_b, compact_b)) in chunks.into_iter().enumerate() {
        for (what, n) in [("src", src_b), ("meta", meta_b), ("compact", compact_b)] {
            assert!(
                n <= binding_cap,
                "line chunk {i}'s {what} buffer is {n} B, over the {binding_cap} B \
                 storage-binding cap — verbatim the 2026-08-04 refusal: \
                 'Buffer binding 1 range 198672192 exceeds max_*_buffer_binding_size limit 134217728'",
            );
        }
    }
}