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

//! **The tile lane asks the baked-vertex store before it bakes** (GFX_V2 backlog: the
//! client-side NVMe baked-vertex cache).
//!
//! A `MapFacet` over tile-stamped ways (lines + filled areas) is driven through the
//! GPU lane with an in-memory [`BakeStore`] installed. The first frame of the first
//! facet MISSES: it bakes (the layer's ear clips run — `ear_clips() == fills`) and
//! offers one blob per tile group.
//!
//! **A tile group is `(draw band, tile)` since 2026-08-23**, not a tile — that is what
//! makes the declared paint order hold ACROSS tiles on the device
//! (`tests/gpu_lane_paint_order.rs`). This fixture's two tiles each carry a line band
//! and a fill band, so the group count here is FOUR, and the counts below are derived
//! from `facett_map::way_draw_band` over the fixture rather than written as a literal —
//! one writer, so a future class-ladder change moves the fixture and the expectation
//! together instead of reddening this file for no reason. A SECOND facet over the same ways — fresh layers,
//! cold tessellation slots, same content keys — then HITS: its first frame decodes
//! the blobs and **never tessellates** (`ear_clips() == 0`), and the store's hit
//! counter moves by the number of groups. Frames are `step`ped, not rendered, so the
//! renderer's VRAM registry never learns the keys and every frame genuinely asks for
//! the payload — the question this test is about is the store, not VRAM residency
//! (`tile_gpu_cache.rs` owns that one).
//!
//! Seen RED on the code this replaces: the second facet's `ear_clips()` equals its
//! fill count (it baked from scratch) and the hit counter does not move.
//!
//! Run (needs the real device, opens NO window):
//! ```text
//! env -u WAYLAND_DISPLAY -u DISPLAY FACETT_TEST_GPU=discrete \
//!   cargo test -p facett-map --features wgpu --test bake_store_tile_lane -- --nocapture
//! ```
#![cfg(feature = "wgpu")]

use std::sync::{Arc, Mutex};

use facett_map::gpu::bake::{bake_counters, install_bake_store, BakeStore};
use facett_map::layer::{LatLon, MapFacet, MapTheme, MapView, WaysLayer};
use facett_map::Way;

const GATE: &str = "FACETT_TEST_GPU";
const CANVAS: egui::Vec2 = egui::vec2(900.0, 640.0);

struct Mem {
    blobs: Mutex<std::collections::HashMap<u128, Vec<u8>>>,
    puts: std::sync::atomic::AtomicUsize,
    gets: std::sync::atomic::AtomicUsize,
}
impl BakeStore for Mem {
    fn get(&self, key: u128) -> Option<Vec<u8>> {
        self.gets.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
        self.blobs.lock().unwrap().get(&key).cloned()
    }
    fn put(&self, key: u128, blob: Vec<u8>) {
        self.puts.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
        self.blobs.lock().unwrap().insert(key, blob);
    }
}

/// Roads (lines) and lakes (fills), each half stamped with one of two owner tiles —
/// exactly korp's `pack_owner_tile` shape. Unique coordinates (the tessellation memo is
/// process-global). Two tiles x two draw bands = four tile groups; see
/// [`fixture_group_count`].
fn ways() -> Vec<Way> {
    let theme = facett_core::Theme::default();
    let mut out = Vec::new();
    for i in 0..120_i64 {
        let f = i as f64 / 120.0;
        let (lon, lat) = (17.71 + f * 0.10, 59.61 + (f * 9.0).sin() * 0.03);
        let pts = [(lon, lat), (lon + 0.004, lat + 0.002), (lon + 0.009, lat - 0.001), (lon + 0.014, lat + 0.003)];
        let tags = vec![("highway".to_owned(), "primary".to_owned())];
        let mut w = Way::from_lonlat(i + 1, &pts, &tags, &theme).expect("a road");
        w.tile_key = if i < 60 { 0x0BAD_0001 } else { 0x0BAD_0002 };
        out.push(w);
    }
    for i in 0..16_i64 {
        let x = 17.72 + i as f64 * 0.006;
        let ring = [(x, 59.63), (x + 0.003, 59.63), (x + 0.003, 59.633), (x + 0.0015, 59.635), (x, 59.633), (x, 59.63)];
        let tags = vec![("natural".to_owned(), "water".to_owned())];
        let mut w = Way::from_lonlat(1000 + i, &ring, &tags, &theme).expect("a lake");
        w.tile_key = if i < 8 { 0x0BAD_0001 } else { 0x0BAD_0002 };
        out.push(w);
    }
    out
}

/// **The number of `(draw band, tile)` groups [`ways`] spans**, asked through the SAME
/// function the lane keys its groups on. The expectations below are this number, so the
/// test states the rule ("one miss and one blob per group") rather than a magic 2 or 4.
fn fixture_group_count() -> usize {
    let mut s = std::collections::HashSet::new();
    for w in ways() {
        s.insert((facett_map::way_draw_band(&w), w.tile_key));
    }
    s.len()
}

fn facet(name: &str) -> MapFacet {
    let view = MapView::new(LatLon::new(59.625, 17.76), 12.0, (CANVAS.x, CANVAS.y));
    let mut f = MapFacet::new(name, view, MapTheme::default());
    f.push_layer(Box::new(WaysLayer::new("osm-mixed", ways())));
    f
}

fn fresh_render_state() -> egui_wgpu::RenderState {
    let setup = facett_core::render::gpu::adapter_wgpu::facett_wgpu_options().wgpu_setup;
    let rs = egui_kittest::wgpu::create_render_state(setup, Default::default());
    facett_map::gpu::install_renderer(&rs);
    rs
}

/// Step `facet.ui` for `steps` frames (laid out, NOT rendered — see the module note).
fn step(f: MapFacet, steps: u32) -> MapFacet {
    use egui_kittest::wgpu::WgpuTestRenderer;
    let f = std::rc::Rc::new(std::cell::RefCell::new(f));
    let inner = f.clone();
    let mut h = egui_kittest::Harness::builder()
        .with_size(CANVAS)
        .renderer(WgpuTestRenderer::from_render_state(fresh_render_state()))
        .build_ui(move |ui| inner.borrow_mut().ui(ui));
    for _ in 0..steps {
        h.step();
    }
    drop(h);
    std::rc::Rc::try_unwrap(f).ok().expect("the harness released the facet").into_inner()
}

#[test]
fn the_tile_lane_serves_a_second_facet_from_the_store_without_tessellating() {
    let mode = std::env::var(GATE).unwrap_or_default();
    if mode != "discrete" && mode != "software" {
        eprintln!("SKIPPED and therefore PROVED NOTHING: set {GATE}=discrete to run the tile lane on this box's GPU.");
        return;
    }
    let mem = Arc::new(Mem { blobs: Mutex::new(Default::default()), puts: 0.into(), gets: 0.into() });
    install_bake_store(Some(mem.clone()));
    let (h0, m0, _) = bake_counters();

    // Facet A: cold store → every group MISSES, bakes, and offers its blob.
    let groups = fixture_group_count();
    assert_eq!(groups, 4, "fixture: 2 owner tiles x 2 draw bands (the roads' line band, the lakes' fill band)");
    let a = step(facet("A"), 1);
    let fills = ways().iter().filter(|w| w.fill.is_some()).count();
    assert!(fills >= 16);
    assert_eq!(a.ear_clips(), fills, "A baked: every lake was ear-clipped on this facet");
    let (h1, m1, _) = bake_counters();
    eprintln!(
        "[bake-store] after A: hits {} misses {} · store keys {} puts {} gets {}",
        h1 - h0, m1 - m0, mem.blobs.lock().unwrap().len(),
        mem.puts.load(std::sync::atomic::Ordering::Relaxed), mem.gets.load(std::sync::atomic::Ordering::Relaxed),
    );
    assert_eq!(
        m1 - m0,
        groups as u64,
        "every tile group missed — once each, whatever egui's pass count"
    );
    // egui_kittest runs more than one pass on a first frame; every pass after the first
    // already HITS the store (the blobs were offered synchronously). One offer per group.
    assert_eq!(
        mem.puts.load(std::sync::atomic::Ordering::Relaxed),
        groups,
        "…and one blob per group was offered, once each"
    );
    assert_eq!(mem.blobs.lock().unwrap().len(), groups);

    // Facet B: same ways, fresh layer (cold tessellation slots), same content keys →
    // the store serves both groups and NO ear clip runs.
    let b = step(facet("B"), 1);
    let (h2, m2, c2) = bake_counters();
    assert!(h2 - h1 >= groups as u64, "every group HIT the store (every pass): {}", h2 - h1);
    assert_eq!(m2 - m1, 0, "nothing baked");
    assert_eq!(c2, 0, "nothing was corrupt");
    assert_eq!(b.ear_clips(), 0, "B never tessellated — the blob carried the fill lane");
    eprintln!(
        "[bake-store] A: {} ear clips, {groups} blobs offered ({} bytes) · B: {} ear clips, {groups} hits · store gets {}",
        a.ear_clips(),
        mem.blobs.lock().unwrap().values().map(Vec::len).sum::<usize>(),
        b.ear_clips(),
        mem.gets.load(std::sync::atomic::Ordering::Relaxed),
    );
    install_bake_store(None);
}