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