Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/facett

facett-map/src/gpu/bake.rs

//! **The baked-vertex store hook + the blob codec** (GFX_V2 backlog: the client-side
//! NVMe baked-vertex cache, measured 6.5× over decode+classify+bake).
//!
//! # What this is
//!
//! The tile lane ([`crate::layer::MapFacet`]'s tiled hand-over) keys every tile group by
//! CONTENT — a 128-bit FNV over the ways' identity + geometry + style + the group's
//! bake origin (`layer::tile_content_key`) — and the renderer keeps the uploaded payload
//! in VRAM under that key. A VRAM miss costs the whole bake: the ear clip of every
//! filled ring (the process-global tessellation memo is cold on a fresh process), the
//! Mercator reprojection, the quad expansion with miter/bevel joins, the fill triangle
//! expansion. All of it is a pure function of the key's inputs, and all of it is POD
//! streams (`Vertex`, `GpuWayMeta`, `LineVertex`) once baked.
//!
//! So: on a VRAM miss the facet asks an installed [`BakeStore`] for the key's blob
//! FIRST. A hit decodes straight into a [`GpuGeometry`] (a `bytemuck` cast per stream —
//! no tessellation, no reprojection, no expansion) and goes to the device; a miss bakes
//! as before and OFFERS the encoded blob to the store. The store is the HOST's (korp
//! keeps it on NVMe under `~/.korp`, bounded, nuked with its tile cache when the data
//! version moves) — facett-map does no disk IO and has no cache-dir concept; this module
//! is the hook and the codec, nothing more. Same key discipline as VRAM, one writer.
//!
//! # The key is not quite enough for a file that outlives the process
//!
//! `tile_content_key` substitutes bbox bits + vertex count for the coordinates (its
//! stated contract: a taken layer's ways are immutable for its lifetime — a PROCESS
//! lifetime argument). Two things close the gap for a blob that persists: the host nukes
//! its store whenever the data version moves (a re-cut is a new world, by design), and
//! the blob carries [`BAKE_FORMAT_GEN`] — the layout of the three POD types + this
//! header — so a facett that changes a vertex struct reads every older blob as a MISS.
//!
//! # Corrupt is a miss
//!
//! [`decode_baked`] answers `None` for a wrong magic, a wrong generation, a length that
//! does not tile into whole elements, a stream that does not fit, a meta entry whose
//! span leaves its stream, a LOD out of range, or a non-monotone `line_lod_end`. The
//! facet then bakes and re-offers, so a bad file heals itself on the next sight.

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

use super::{GpuFillGeometry, GpuGeometry, GpuWayMeta, LineVertex, Vertex};
use crate::style::LOD_COUNT;

/// `"FCTBAKE1"` — the blob magic.
pub const BAKE_MAGIC: u64 = 0x3145_4B41_4254_4346;
/// The blob LAYOUT generation: bump when `Vertex`, `GpuWayMeta`, `LineVertex` or this
/// header change shape — **or when the ORDER of a stream changes**, which is just as
/// invisible to the content key and just as wrong on screen.
///
/// `2` (2026-08-22): the line and fill streams are now emitted in
/// [`crate::paint_order`] order instead of LOD-bucket / input order, and
/// `line_lod_end` is a cumulative per-tier COUNT rather than a contiguous span. A
/// gen-1 blob holds the same vertices in the old, tangled order, and the content key
/// cannot tell — it hashes the ways, not the stream. Bumping makes every stale blob a
/// miss that re-bakes.
///
/// `3` (2026-08-22): `GpuWayMeta::_pad` became `flags`, and bit 0 marks an AREA — the
/// only geometry the minimum-projected-extent gate applies to (stinky `kalm-3`). A
/// gen-2 blob has zeroes there, so every building footprint it carries would slip the
/// gate and the tiny-fill budget would come back for exactly the tiles the NVMe tier
/// serves.
///
/// `4` (2026-08-22): the fill lane's ring OUTLINE stream changed type — 24 B [`Vertex`]
/// `LineList` pairs became 48 B [`LineVertex`] quads, so it gets the same SDF coverage
/// ramp the roads get (stinky `mapgpu-1`). A gen-3 blob's outline stream has half the
/// stride and would decode as garbage geometry, not as a missing feature.
///
/// `5` (2026-08-25): `LOD_COUNT` went 4 → 5 for the z9 `Lod::District` tier, and this
/// header carries `line_lod_end` as `[u32; LOD_COUNT]`. So the header grew — `pad8(16)`
/// became `pad8(20)`, i.e. 16 B → 24 B — and **every stream offset after it shifted by
/// eight bytes**. A gen-4 blob read with a gen-5 layout does not decode as a missing
/// feature; it decodes as garbage geometry from the wrong offset. Independently of the
/// header, the tier VALUE in `GpuWayMeta::lod` was renumbered by the same change
/// (`City` 2 → 3, `Detail` 3 → 4), so a gen-4 blob's `lod = 2` means "city" under the
/// numbering it was written with and "district" under the one that would read it. One
/// bump covers both; every stale blob becomes a miss and re-bakes.
pub const BAKE_FORMAT_GEN: u32 = 5;

/// Header: magic · gen · reserved · 8 stream counts · `line_lod_end` (padded to 8 B).
const HEADER_BYTES: usize = 8 + 4 + 4 + 8 * 8 + pad8(4 * LOD_COUNT);

const fn pad8(n: usize) -> usize {
    (n + 7) & !7
}

/// **What a host installs**: a content-keyed blob store. `get` is synchronous and on
/// the paint thread — a host backs it with a RAM/page-cache-warm read; `put` is
/// fire-and-forget — a host queues the bytes to a writer and returns.
pub trait BakeStore: Send + Sync {
    /// The blob for `key`, if the store has one.
    fn get(&self, key: u128) -> Option<Vec<u8>>;
    /// Offer the blob for `key` (the store may drop it — over budget, disk full).
    fn put(&self, key: u128, blob: Vec<u8>);
}

static STORE: OnceLock<Mutex<Option<Arc<dyn BakeStore>>>> = OnceLock::new();

fn store_slot() -> &'static Mutex<Option<Arc<dyn BakeStore>>> {
    STORE.get_or_init(|| Mutex::new(None))
}

/// Install (or replace) the process-wide bake store. `None` uninstalls.
///
/// Installing also registers the tier's live About row
/// ([`about_note`], via `facett_core::render::lane::register_status_note`) — so any
/// host that installs a store (korp's NVMe tier, a test's RAM store) gets the
/// hit-rate line on its About card and in `state_json.gpu.notes` with no host code.
/// Registration is idempotent; the note reports nothing while no store is installed.
pub fn install_bake_store(store: Option<Arc<dyn BakeStore>>) {
    if let Ok(mut g) = store_slot().lock() {
        *g = store;
    }
    facett_core::render::lane::register_status_note(about_note);
}

/// **The baked-vertex tier's one live status line** — the V2.7 "read
/// `bake_counters()` into the About line" item. `None` while no store is installed
/// (a tier that does not exist reports nothing, never a zeroed reading); with a
/// store but no lookups yet it says so rather than faking a 0 % hit rate (the
/// identity-value trap: a cold tier and a useless tier are different facts).
#[must_use]
pub fn about_note() -> Option<String> {
    bake_store()?;
    let (hits, misses, corrupt) = bake_counters();
    let looked = hits + misses;
    if looked == 0 {
        return Some("bake cache: installed, no lookups yet".to_string());
    }
    #[allow(clippy::cast_precision_loss)]
    let pct = hits as f64 / looked as f64 * 100.0;
    Some(format!("bake cache: {hits} hit · {misses} miss · {corrupt} corrupt · {pct:.0} % hit"))
}

/// The installed store, if any.
#[must_use]
pub fn bake_store() -> Option<Arc<dyn BakeStore>> {
    store_slot().lock().ok().and_then(|g| g.clone())
}

// ── Counters: what the lane DID, for tests and the About line ────────────────
static HITS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
static MISSES: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
static CORRUPT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);

/// `(hits, misses, corrupt)` since process start — a hit is a blob that decoded and
/// replaced a bake; a miss is a bake the store had nothing for; corrupt is a blob the
/// store HAD that did not decode (counted as a miss too).
#[must_use]
pub fn bake_counters() -> (u64, u64, u64) {
    use std::sync::atomic::Ordering::Relaxed;
    (HITS.load(Relaxed), MISSES.load(Relaxed), CORRUPT.load(Relaxed))
}

/// **The lane's one door**: the geometry for `key`, from the store when it has a good
/// blob, else from `bake()` — and a fresh bake is offered to the store. With no store
/// installed this is exactly `bake()`.
pub fn baked_or_build(key: u128, bake: impl FnOnce() -> GpuGeometry) -> GpuGeometry {
    use std::sync::atomic::Ordering::Relaxed;
    let Some(store) = bake_store() else { return bake() };
    if let Some(blob) = store.get(key) {
        if let Some(geo) = decode_baked(&blob) {
            HITS.fetch_add(1, Relaxed);
            return geo;
        }
        CORRUPT.fetch_add(1, Relaxed);
    }
    MISSES.fetch_add(1, Relaxed);
    let geo = bake();
    store.put(key, encode_baked(&geo));
    geo
}

/// The header + the eight POD streams, back to back, each 8-byte aligned.
#[must_use]
pub fn encode_baked(g: &GpuGeometry) -> Vec<u8> {
    let streams: [&[u8]; 8] = [
        bytemuck::cast_slice(&g.vertices),
        bytemuck::cast_slice(&g.way_meta),
        bytemuck::cast_slice(&g.line_vertices),
        bytemuck::cast_slice(&g.line_meta),
        bytemuck::cast_slice(&g.fills.tri_vertices),
        bytemuck::cast_slice(&g.fills.tri_meta),
        bytemuck::cast_slice(&g.fills.outline_vertices),
        bytemuck::cast_slice(&g.fills.outline_meta),
    ];
    let counts: [u64; 8] = [
        g.vertices.len() as u64,
        g.way_meta.len() as u64,
        g.line_vertices.len() as u64,
        g.line_meta.len() as u64,
        g.fills.tri_vertices.len() as u64,
        g.fills.tri_meta.len() as u64,
        g.fills.outline_vertices.len() as u64,
        g.fills.outline_meta.len() as u64,
    ];
    let total = HEADER_BYTES + streams.iter().map(|s| pad8(s.len())).sum::<usize>();
    let mut out = Vec::with_capacity(total);
    out.extend_from_slice(&BAKE_MAGIC.to_le_bytes());
    out.extend_from_slice(&BAKE_FORMAT_GEN.to_le_bytes());
    out.extend_from_slice(&0u32.to_le_bytes());
    for c in counts {
        out.extend_from_slice(&c.to_le_bytes());
    }
    for e in g.line_lod_end {
        out.extend_from_slice(&e.to_le_bytes());
    }
    out.resize(HEADER_BYTES, 0);
    for s in streams {
        out.extend_from_slice(s);
        out.resize(pad8(out.len()), 0);
    }
    debug_assert_eq!(out.len(), total);
    out
}

fn read_u64(b: &[u8], at: usize) -> Option<u64> {
    b.get(at..at + 8).map(|s| u64::from_le_bytes(s.try_into().expect("8 bytes")))
}
fn read_u32(b: &[u8], at: usize) -> Option<u32> {
    b.get(at..at + 4).map(|s| u32::from_le_bytes(s.try_into().expect("4 bytes")))
}

/// Take `n` POD elements of `T` from `bytes` at `*at` (advancing past the 8 B pad);
/// `None` if the stream does not fit.
fn take_stream<T: bytemuck::Pod>(bytes: &[u8], at: &mut usize, n: u64) -> Option<Vec<T>> {
    let n = usize::try_from(n).ok()?;
    let len = n.checked_mul(std::mem::size_of::<T>())?;
    let s = bytes.get(*at..at.checked_add(len)?)?;
    *at += pad8(len);
    // `pod_collect_to_vec` copies and never cares about the source alignment.
    Some(bytemuck::pod_collect_to_vec(s))
}

/// Every meta entry's span must lie inside its stream, and its LOD inside the tiers.
fn metas_fit(meta: &[GpuWayMeta], verts: usize) -> bool {
    meta.iter().all(|m| {
        (m.lod as usize) < LOD_COUNT
            && (m.vert_start as usize)
                .checked_add(m.vert_count as usize)
                .is_some_and(|end| end <= verts)
    })
}

/// The blob back into a [`GpuGeometry`] — `None` on ANY inconsistency (see the module
/// note: corrupt is a miss, never a wrong picture).
#[must_use]
pub fn decode_baked(bytes: &[u8]) -> Option<GpuGeometry> {
    if bytes.len() < HEADER_BYTES || read_u64(bytes, 0)? != BAKE_MAGIC || read_u32(bytes, 8)? != BAKE_FORMAT_GEN {
        return None;
    }
    let mut counts = [0u64; 8];
    for (i, c) in counts.iter_mut().enumerate() {
        *c = read_u64(bytes, 16 + 8 * i)?;
    }
    let mut line_lod_end = [0u32; LOD_COUNT];
    for (i, e) in line_lod_end.iter_mut().enumerate() {
        *e = read_u32(bytes, 16 + 64 + 4 * i)?;
    }
    let mut at = HEADER_BYTES;
    let vertices: Vec<Vertex> = take_stream(bytes, &mut at, counts[0])?;
    let way_meta: Vec<GpuWayMeta> = take_stream(bytes, &mut at, counts[1])?;
    let line_vertices: Vec<LineVertex> = take_stream(bytes, &mut at, counts[2])?;
    let line_meta: Vec<GpuWayMeta> = take_stream(bytes, &mut at, counts[3])?;
    let tri_vertices: Vec<Vertex> = take_stream(bytes, &mut at, counts[4])?;
    let tri_meta: Vec<GpuWayMeta> = take_stream(bytes, &mut at, counts[5])?;
    let outline_vertices: Vec<LineVertex> = take_stream(bytes, &mut at, counts[6])?;
    let outline_meta: Vec<GpuWayMeta> = take_stream(bytes, &mut at, counts[7])?;
    if at != bytes.len() {
        return None; // trailing garbage is not a blob we wrote
    }
    if !metas_fit(&way_meta, vertices.len())
        || !metas_fit(&line_meta, line_vertices.len())
        || !metas_fit(&tri_meta, tri_vertices.len())
        || !metas_fit(&outline_meta, outline_vertices.len())
    {
        return None;
    }
    // The tiers tile the quad buffer: non-decreasing, ending at its length.
    if line_lod_end.windows(2).any(|w| w[0] > w[1])
        || line_lod_end.last().copied().unwrap_or(0) as usize != line_vertices.len()
    {
        return None;
    }
    Some(GpuGeometry {
        vertices,
        way_meta,
        line_vertices,
        line_meta,
        line_lod_end,
        fills: GpuFillGeometry { tri_vertices, tri_meta, outline_vertices, outline_meta },
    })
}

#[cfg(test)]
mod tests {
    use super::*;

    fn a_geometry() -> GpuGeometry {
        let th = facett_core::Theme::default();
        let mut ways = Vec::new();
        for i in 0..40 {
            let x = 18.0 + f64::from(i) * 1e-3;
            let tags = vec![("highway".to_string(), "residential".to_string())];
            ways.extend(crate::Way::from_lonlat(i64::from(i), &[(x, 59.3), (x + 5e-4, 59.3005), (x + 1e-3, 59.3)], &tags, &th));
        }
        let origin = super::super::ways_origin(ways.iter()).unwrap_or([0.0, 0.0]);
        let mut g = super::super::build_gpu_geometry(ways.iter().map(|w| (w, w.style.lod as u32)), origin);
        // A couple of fills too, through the real fill bake off a real ear clip.
        let mut fills = Vec::new();
        for i in 0..5 {
            let x = 18.1 + f64::from(i) * 1e-3;
            let tags = vec![("natural".to_string(), "water".to_string())];
            fills.extend(crate::Way::from_lonlat(
                100 + i64::from(i),
                &[(x, 59.2), (x + 4e-4, 59.2), (x + 4e-4, 59.2004), (x, 59.2004), (x, 59.2)],
                &tags,
                &th,
            ));
        }
        let cache = crate::tessellate::FillTessCache::new(fills.len());
        g.fills = super::super::build_gpu_fill_geometry(
            fills.iter().enumerate().filter_map(|(i, w)| cache.tris(i, &w.lonlat, &w.holes).map(|t| (w, w.style.lod as u32, t))),
            origin,
        );
        assert!(!g.line_vertices.is_empty() && !g.fills.tri_vertices.is_empty(), "the fixture bakes both lanes");
        g
    }

    fn same(a: &GpuGeometry, b: &GpuGeometry) -> bool {
        a.vertices == b.vertices
            && a.way_meta == b.way_meta
            && a.line_vertices == b.line_vertices
            && a.line_meta == b.line_meta
            && a.line_lod_end == b.line_lod_end
            && a.fills.tri_vertices == b.fills.tri_vertices
            && a.fills.tri_meta == b.fills.tri_meta
            && a.fills.outline_vertices == b.fills.outline_vertices
            && a.fills.outline_meta == b.fills.outline_meta
    }

    /// LAW 2: the blob round-trips every stream byte for byte, and an empty geometry too.
    #[test]
    fn the_blob_round_trips_every_stream() {
        let g = a_geometry();
        let blob = encode_baked(&g);
        let back = decode_baked(&blob).expect("decodes");
        assert!(same(&g, &back), "every stream and line_lod_end survive the trip");
        let empty = GpuGeometry { vertices: vec![], way_meta: vec![], line_vertices: vec![], line_meta: vec![], line_lod_end: [0; LOD_COUNT], fills: GpuFillGeometry::default() };
        let back = decode_baked(&encode_baked(&empty)).expect("an empty blob decodes");
        assert!(same(&empty, &back));
    }

    /// Corrupt is a MISS: garbage, a truncation, a wrong generation, trailing bytes, a
    /// flipped count and a meta span past its stream all answer `None`, never a picture.
    #[test]
    fn a_corrupt_blob_is_a_miss_not_a_wrong_picture() {
        let g = a_geometry();
        let blob = encode_baked(&g);
        assert!(decode_baked(b"not a blob at all").is_none());
        assert!(decode_baked(&blob[..blob.len() / 2]).is_none(), "truncated");
        let mut trailing = blob.clone();
        trailing.extend_from_slice(&[0u8; 8]);
        assert!(decode_baked(&trailing).is_none(), "trailing bytes");
        let mut newer = blob.clone();
        newer[8..12].copy_from_slice(&(BAKE_FORMAT_GEN + 1).to_le_bytes());
        assert!(decode_baked(&newer).is_none(), "a newer generation");
        let mut count = blob.clone();
        let n = u64::from_le_bytes(count[16..24].try_into().unwrap()) + 1;
        count[16..24].copy_from_slice(&n.to_le_bytes());
        assert!(decode_baked(&count).is_none(), "a count that does not fit");
        // A meta span past its stream: poke the first line_meta's vert_count.
        let lm_at = HEADER_BYTES + pad8(g.vertices.len() * std::mem::size_of::<Vertex>()) + pad8(g.way_meta.len() * std::mem::size_of::<GpuWayMeta>()) + pad8(g.line_vertices.len() * std::mem::size_of::<LineVertex>());
        let mut span = blob.clone();
        span[lm_at + 20..lm_at + 24].copy_from_slice(&u32::MAX.to_le_bytes());
        assert!(decode_baked(&span).is_none(), "a meta span past its stream");
    }

    /// **MEASURED, not argued**: a dense tile group (4 000 roads + 400 lakes, the
    /// live 2.6-vertex shape) — the full bake (ear clip + reproject + quad-expand +
    /// fill-expand) against `decode_baked` of its blob, the work a store HIT replaces.
    /// The ratio is printed; the only gate is that a decode is not SLOWER than a bake
    /// (if it ever is, the tier is pointless and should say so here).
    #[test]
    fn a_decode_beats_the_bake_it_replaces() {
        let th = facett_core::Theme::default();
        let mut ways = Vec::new();
        for i in 0..4000_i64 {
            let x = 17.0 + (i % 200) as f64 * 2e-4;
            let y = 58.0 + (i / 200) as f64 * 2e-4;
            let tags = vec![("highway".to_string(), "residential".to_string())];
            ways.extend(crate::Way::from_lonlat(i, &[(x, y), (x + 1e-4, y + 1.2e-4), (x + 2.1e-4, y + 0.3e-4)], &tags, &th));
        }
        let mut fills = Vec::new();
        for i in 0..400_i64 {
            let x = 17.1 + (i % 20) as f64 * 3e-4;
            let y = 58.1 + (i / 20) as f64 * 3e-4;
            let tags = vec![("natural".to_string(), "water".to_string())];
            fills.extend(crate::Way::from_lonlat(
                90_000 + i,
                &[(x, y), (x + 2e-4, y), (x + 2.4e-4, y + 1.2e-4), (x + 2e-4, y + 2e-4), (x + 0.5e-4, y + 1.6e-4), (x, y)],
                &tags,
                &th,
            ));
        }
        let origin = super::super::ways_origin(ways.iter().chain(fills.iter())).unwrap();
        let bake = || {
            // A FRESH cache each time: the first sight of a ring set, which is what a
            // store hit spares (the process memo is also bypassed by unique coordinates
            // only on the first run; so time the first run separately).
            let cache = crate::tessellate::FillTessCache::new(fills.len());
            let mut g = super::super::build_gpu_geometry(ways.iter().map(|w| (w, w.style.lod as u32)), origin);
            g.fills = super::super::build_gpu_fill_geometry(
                fills.iter().enumerate().filter_map(|(i, w)| cache.tris(i, &w.lonlat, &w.holes).map(|t| (w, w.style.lod as u32, t))),
                origin,
            );
            g
        };
        let t = std::time::Instant::now();
        let g = bake();
        let cold_bake_ms = t.elapsed().as_secs_f64() * 1e3;
        let t = std::time::Instant::now();
        for _ in 0..5 {
            std::hint::black_box(bake());
        }
        let warm_bake_ms = t.elapsed().as_secs_f64() * 1e3 / 5.0;
        let blob = encode_baked(&g);
        let t = std::time::Instant::now();
        let enc = encode_baked(&g);
        let encode_ms = t.elapsed().as_secs_f64() * 1e3;
        assert_eq!(enc.len(), blob.len());
        let t = std::time::Instant::now();
        for _ in 0..5 {
            std::hint::black_box(decode_baked(&blob).expect("decodes"));
        }
        let decode_ms = t.elapsed().as_secs_f64() * 1e3 / 5.0;
        eprintln!(
            "[bake] {} roads + {} lakes → {} line verts, {} fill tris, blob {:.2} MiB · bake cold {cold_bake_ms:.2} ms / warm-memo {warm_bake_ms:.2} ms · encode {encode_ms:.2} ms · decode {decode_ms:.2} ms → decode is {:.1}× faster than a cold bake, {:.1}× than a memo-warm one",
            ways.len(),
            fills.len(),
            g.line_vertices.len(),
            g.fills.tri_vertices.len() / 3,
            blob.len() as f64 / (1024.0 * 1024.0),
            cold_bake_ms / decode_ms,
            warm_bake_ms / decode_ms,
        );
        assert!(decode_ms < cold_bake_ms, "a decode ({decode_ms:.2} ms) must beat the bake it replaces ({cold_bake_ms:.2} ms)");
    }

    /// The door: with no store it bakes; with a store a MISS bakes and OFFERS the blob,
    /// a HIT skips the bake entirely (the closure is never called), and a corrupt blob
    /// is a miss that re-offers a good one.
    #[test]
    fn the_store_door_hits_misses_and_heals() {
        struct Mem(Mutex<std::collections::HashMap<u128, Vec<u8>>>, std::sync::atomic::AtomicUsize);
        impl BakeStore for Mem {
            fn get(&self, key: u128) -> Option<Vec<u8>> {
                self.0.lock().unwrap().get(&key).cloned()
            }
            fn put(&self, key: u128, blob: Vec<u8>) {
                self.1.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
                self.0.lock().unwrap().insert(key, blob);
            }
        }
        let mem = Arc::new(Mem(Mutex::new(Default::default()), 0.into()));
        // The slot is process-global: take it for this test and release it after.
        install_bake_store(Some(mem.clone()));
        let key = 0xDEAD_BEEF_u128 << 64 | 0x1234;
        let g = a_geometry();
        let (h0, m0, c0) = bake_counters();
        let mut bakes = 0;
        let out = baked_or_build(key, || { bakes += 1; a_geometry() });
        assert!(same(&out, &g));
        assert_eq!(bakes, 1, "a miss bakes");
        assert_eq!(mem.1.load(std::sync::atomic::Ordering::Relaxed), 1, "…and offers the blob");
        let out = baked_or_build(key, || { bakes += 1; a_geometry() });
        assert!(same(&out, &g), "the hit is the same geometry");
        assert_eq!(bakes, 1, "a hit never bakes");
        let (h1, m1, c1) = bake_counters();
        assert_eq!((h1 - h0, m1 - m0, c1 - c0), (1, 1, 0));
        // Corrupt the stored blob: the next ask is a corrupt-miss that re-bakes and heals.
        mem.0.lock().unwrap().get_mut(&key).unwrap().truncate(40);
        let out = baked_or_build(key, || { bakes += 1; a_geometry() });
        assert!(same(&out, &g));
        assert_eq!(bakes, 2, "a corrupt blob re-bakes");
        let (_, m2, c2) = bake_counters();
        assert_eq!((m2 - m1, c2 - c1), (1, 1));
        assert!(decode_baked(&mem.0.lock().unwrap()[&key]).is_some(), "…and the store holds a good blob again");

        // ── The About note reads THESE counters through the lane registry ─────
        // With the store installed the note is a live row carrying the counts
        // above, and it reaches `GpuStatus` (the About card's one row writer)
        // through `register_status_note` — asserted on the PROBE's rows, not on
        // `about_note()` alone, so the registration wire is part of the proof.
        let note = about_note().expect("a live note while the store is installed");
        assert!(
            note.starts_with("bake cache: ") && note.contains(" hit") && note.contains("% hit"),
            "the note carries the hit rate: {note}"
        );
        let rows = facett_core::render::lane::GpuStatus::probe().rows();
        assert!(
            rows.iter().any(|r| r == &note),
            "the registered note reaches the About rows verbatim — rows: {rows:?}"
        );
        install_bake_store(None);
        // RED half: with NO store the tier reports NOTHING — an absent tier must
        // not fake a zeroed reading (and the registered callback goes silent
        // rather than lingering as a stale row).
        assert_eq!(about_note(), None, "no store ⇒ no row");
        assert!(
            !facett_core::render::lane::GpuStatus::probe()
                .rows()
                .iter()
                .any(|r| r.starts_with("bake cache: ")),
            "…and the probe carries no bake row either"
        );
    }
}