facett-map/tests/lod_ladder_vs_pyramid.rs
//! **THE LADDER AND THE PYRAMID NOW STEP TOGETHER — and this is the law that keeps
//! them that way.**
//!
//! There are **two ladders**, sharing one rule ([`lod_step`], `kalm-2`) and not one list:
//!
//! * the **cartographic tier** — [`LOD_TIER_ZOOMS`] `= [0, 6, 9, 12, 15]`, which decides
//! what is DRAWN (`gpu/cull.wgsl`: a way draws only when `lod <= viewport_lod`);
//! * the **tile pyramid** — `[6, 9, 12, 15]` on the live Sweden extract, which decides
//! what is FETCHED.
//!
//! # What this file used to assert, and why it was right to
//!
//! Until 2026-08-25 the cartographic ladder was `[0, 6, 12, 15]` and the two lists
//! differed by exactly one rung — **z9**. This file existed to state that as a
//! measurement rather than an aesthetic (GFX_V2 §V2.10 C: *"whether that costs anything
//! is a MEASUREMENT nobody has taken"*), and it asserted the gap was there:
//!
//! > At camera **9, 10 and 11** the fetch steps up to the z9 rung and the draw gate does
//! > **not move** — `view_lod_tier` returns the same `Region` it returned at camera 6.
//!
//! MEASURED, and this is the number that decided it: **the z9 rung is 23.16× the bytes
//! of z6** (442 768 951 B against 19 113 939 B, on the live Sweden extract) **for the
//! same drawn tier**. Cut, stored, shipped over the wire, decoded — and then rendering
//! what the rung below it already rendered.
//!
//! # The decision
//!
//! This file laid out three options and said which was whose call. **The owner took
//! option 1** (2026-08-25): add the rung — `LOD_TIER_ZOOMS = [0, 6, 9, 12, 15]`, a fifth
//! [`Lod`] variant ([`Lod::District`]), and a pass over the style rules that assign one.
//! `highway=tertiary`/`tertiary_link` and `waterway=canal` moved onto it; `secondary`
//! deliberately did not. The accepted cost, stated so it is not re-litigated: **tertiary
//! roads no longer draw between camera 6 and camera 9.**
//!
//! Option 2 (move the Region step to 9) and option 3 (accept and document) are recorded
//! in the history of this file rather than deleted, because the reasoning that ranked
//! them is what makes option 1 legible.
//!
//! # The law now
//!
//! The assertion is therefore INVERTED, and generalised so it is a law rather than a new
//! hand-written expectation:
//!
//! | check | fails on |
//! |---|---|
//! | [`the_two_ladders_step_together_at_every_rung`] | a served rung that changes nothing on screen — the defect this file was written about |
//! | [`every_tier_step_lands_on_a_rung_the_pyramid_serves`] | the converse: a tier declared at a zoom the pyramid does not cut |
//! | [`the_ladder_is_one_strictly_ascending_zoom_per_tier`] | a malformed ladder, so neither law above can pass over one |
//! | [`the_road_hierarchy_steps_across_the_whole_ladder`] | the cartographic split itself, as a table |
//! | [`the_live_pyramid_weighs_each_rung`] | the byte weights, when the extract is on this box |
//!
//! **The second law is the one that matters for whoever adds tier six.** z9 was free —
//! already cut, already served. z8, z10 and z13 are not: each needs a 44 GB re-cut before
//! a tier at that zoom renders anything but the rung below it. A tier declared at an
//! unserved zoom does not fail loudly on its own; it just quietly draws the coarser data.
//!
//! # Why korp's law does not cover this
//!
//! `korp/tests/lod_one_threshold.rs::fetch_level_and_draw_tier_step_together` asserts
//! *"neither side may step without the other"* — for every camera, the tier the painter
//! draws equals the tier of the level the fetch picked. That is necessary and not
//! sufficient: it compares fetch against draw, and on the old ladder they **agreed about
//! ignoring z9**, so it was green throughout. This file states the law over the RUNGS
//! instead of over the cameras, which is why it can see what a camera sweep cannot.
//!
//! [`PYRAMID_ZOOMS`] is a copy of korp's `map_tiles::DEFAULT_TILE_ZOOMS` and cannot be
//! read from the original — korp depends on facett, not the reverse. korp's test reads
//! both originals and is what catches drift in the copy.
//!
//! Run: `cargo test -p facett-map --test lod_ladder_vs_pyramid -- --nocapture`
use facett_map::layer::{lod_step, view_lod_tier, LOD_TIER_ZOOMS};
use facett_map::style::{classify_with, Lod, MapPalette, LOD_COUNT};
/// The live Sweden extract's pyramid rungs (`korp`'s recipe `zooms` axis). Not a facett
/// constant — the pyramid belongs to the data, and this file is about the two lists
/// DISAGREEING, so it must be able to state the other one.
const PYRAMID_ZOOMS: [u8; 4] = [6, 9, 12, 15];
/// The slippy range the map is documented over (`layer.rs`: "the slippy range is 0..=22").
const CAMERAS: std::ops::RangeInclusive<u8> = 0..=22;
/// The tier's own name, read off [`Lod`] rather than transcribed. The hand-written
/// `match` this replaces silently mislabelled every tier from index 2 up the moment a
/// variant was inserted — it said "City" for what had become `District`.
fn tier_name(t: u32) -> String {
format!("{:?}", Lod::from_u32(t))
}
/// ── THE LAW: every rung the pyramid serves must change what is drawn ────────────
///
/// Asserted as a set, not as a hand-written expectation, so it stays true under a change
/// to either list rather than merely under the change somebody remembered to update.
#[test]
fn the_two_ladders_step_together_at_every_rung() {
let mut rows = Vec::new();
for z in CAMERAS {
let zf = f32::from(z);
rows.push((z, lod_step(&PYRAMID_ZOOMS, zf), view_lod_tier(zf)));
}
// Cameras where the fetch stepped up but the draw gate did not.
let mut wasted: Vec<u8> = Vec::new();
for w in rows.windows(2) {
let (_, prev_rung, prev_tier) = w[0];
let (z, rung, tier) = w[1];
if rung > prev_rung && tier == prev_tier {
wasted.push(z);
}
}
eprintln!("EMIT-LOD camera | pyramid rung | cartographic tier");
for (z, rung, tier) in &rows {
let step_marker = if wasted.contains(z) { " <- fetch stepped, draw did not" } else { "" };
eprintln!(
"EMIT-LOD {z:>2} | z{:<2} | {} ({}){step_marker}",
PYRAMID_ZOOMS[*rung],
tier_name(*tier),
tier
);
}
// NON-VACUITY: the walk must have seen both ladders actually move, or "they agree" is
// a statement about two constant functions.
let rungs: std::collections::BTreeSet<usize> = rows.iter().map(|r| r.1).collect();
let tiers: std::collections::BTreeSet<u32> = rows.iter().map(|r| r.2).collect();
assert_eq!(rungs.len(), PYRAMID_ZOOMS.len(), "every pyramid rung is reachable in 0..=22");
assert_eq!(tiers.len(), LOD_TIER_ZOOMS.len(), "every cartographic tier is reachable in 0..=22");
assert!(
wasted.is_empty(),
"camera(s) {wasted:?} fetch a finer pyramid rung and draw the tier the rung below \
already drew. That level is cut, stored, shipped over the wire and decoded to \
change nothing on screen — z9 cost 23.16x the bytes of z6 to do exactly that \
until 2026-08-25. Either give the rung a cartographic tier (the fix that was \
taken; see the module doc) or stop cutting it.\n\
Note korp's `fetch_level_and_draw_tier_step_together` is GREEN on this shape, \
because fetch and draw agree about ignoring the rung."
);
// …and the specific band that used to be flat is not, any more. Cameras 9, 10 and 11
// fetch z9; they must now draw a tier camera 6 does not.
let at6 = view_lod_tier(6.0);
for z in [9.0_f32, 10.0, 11.0] {
assert_ne!(
view_lod_tier(z),
at6,
"camera {z} fetches the z9 rung and must no longer draw the tier camera 6 draws"
);
}
assert_eq!(at6, Lod::Region as u32, "…camera 6 is the Region tier");
assert_eq!(view_lod_tier(9.0), Lod::District as u32, "…and camera 9 is the District tier");
}
/// **The converse — a tier may only step where there is data to step onto.**
///
/// This is the direction that catches tier six. Nothing about a tier declared at an
/// unserved zoom fails on its own; it simply renders the coarser rung, which is the
/// defect above arrived at from the other side.
#[test]
fn every_tier_step_lands_on_a_rung_the_pyramid_serves() {
// The first entry is the floor (`0`, the country tier) and is deliberately NOT a
// pyramid rung: a camera coarser than the whole pyramid clamps UP to the coarsest
// level, because that is the only data out there. Every entry AFTER it is a step, and
// a step needs somewhere to land.
let unserved: Vec<u8> =
LOD_TIER_ZOOMS[1..].iter().copied().filter(|z| !PYRAMID_ZOOMS.contains(z)).collect();
assert!(
unserved.is_empty(),
"the ladder steps at camera(s) {unserved:?}, which the pyramid {PYRAMID_ZOOMS:?} \
does not cut — those tiers render the rung below them and are invisible. z9 was \
free because it was already cut; z8/z10/z13 each need a re-cut first."
);
}
/// **Shape, so neither law above can pass over a malformed ladder.**
#[test]
fn the_ladder_is_one_strictly_ascending_zoom_per_tier() {
assert_eq!(
LOD_TIER_ZOOMS.len(),
LOD_COUNT,
"the ladder must have exactly one camera zoom per `Lod` tier"
);
for (i, w) in LOD_TIER_ZOOMS.windows(2).enumerate() {
assert!(w[1] > w[0], "the ladder must strictly ascend — entry {} ({}) <= {}", i + 1, w[1], w[0]);
}
// And each rung reads back as its own index, or `view_lod_tier` and the array it is
// built from have drifted apart.
for (i, &z) in LOD_TIER_ZOOMS.iter().enumerate() {
assert_eq!(
view_lod_tier(f32::from(z)),
i as u32,
"camera {z} is ladder entry {i} and must read back as tier {i}"
);
}
}
/// **THE CARTOGRAPHIC CONSEQUENCE, MEASURED** — which road classes appear at which
/// camera, stated as a table, so the split the owner decided is a number a future lane can
/// check rather than an intention it has to infer.
#[test]
fn the_road_hierarchy_steps_across_the_whole_ladder() {
let pal = MapPalette::cartographic(true);
let lod_of = |tags: &[(&str, &str)]| classify_with(tags, &pal).lod;
// `(name, tags, the LOD the ladder must give it)`.
type Row = (&'static str, &'static [(&'static str, &'static str)], Lod);
let rows: [Row; 6] = [
("motorway", &[("highway", "motorway")], Lod::Country),
("primary", &[("highway", "primary")], Lod::Country),
("secondary", &[("highway", "secondary")], Lod::Region),
("tertiary", &[("highway", "tertiary")], Lod::District),
("residential", &[("highway", "residential")], Lod::City),
("footway", &[("highway", "footway")], Lod::Detail),
];
eprintln!("EMIT-LOD class | tier | draws from camera");
for (name, tags, want) in rows {
let got = lod_of(tags);
eprintln!("EMIT-LOD {name:<12} | {got:<9?} | {}", LOD_TIER_ZOOMS[got as usize]);
assert_eq!(got, want, "{name} must classify as {want:?}, got {got:?}");
}
// The waterway ladder splits at the same rung and for the same reason: a canal is an
// infrastructure watercourse at the scale a tertiary road is an infrastructure road.
// The river above it and the stream below it must NOT move with it, or this is a
// wholesale re-tiering rather than the one split that was approved.
assert_eq!(lod_of(&[("waterway", "river")]), Lod::Country, "a river stays country-tier");
assert_eq!(lod_of(&[("waterway", "canal")]), Lod::District, "a canal moves to the z9 tier");
assert_eq!(lod_of(&[("waterway", "stream")]), Lod::City, "a stream stays city-tier");
// AND THE THING THAT MUST NOT HAVE MOVED. Splitting `Lod::Region` was approved for
// tertiary and canals; taking secondary with them would be a far larger look change
// than the one decided, and it is the easiest slip to make here.
assert_eq!(
lod_of(&[("highway", "secondary")]),
Lod::Region,
"secondary roads must STAY at the region tier — only tertiary moved"
);
assert_eq!(LOD_TIER_ZOOMS[Lod::Region as usize], 6, "…and the region tier is still camera 6");
}
/// ── WHAT THE FLAT SPOT COST, on the real Sweden pyramid ──────────────────────────
///
/// Reads the live extract when it is on this box and prints the byte weight of each rung
/// beside the tier it buys. Skips loudly rather than silently when the pyramid is absent —
/// a GPU-less box gets a *stated* skip, not a green that looks like a measurement.
///
/// MEASURED 2026-08-24 on oden, `/home/korp/.korp/work/sweden/tiles`:
///
/// | rung | tiles | bytes | ratio to z6 |
/// |---|---|---|---|
/// | z6 | 19 | 19 113 939 | 1.00× |
/// | z9 | 676 | 442 768 951 | **23.16×** |
/// | z12 | 28 488 | 3 884 716 129 | 203.2× |
/// | z15 | 1 560 752 | 43 388 512 204 | 2269.9× |
///
/// (1 589 935 tiles in total, which is the Sweden pyramid's own published tile count — the
/// cross-check that says this walked the whole thing and not a subtree.)
///
/// So the z9 rung WAS **23.16× the bytes of z6 for the same drawn tier**. That is the
/// number "4 tiers against N zooms" was missing, and it turned a redesign into a choice
/// between three concrete options — **option 1 was taken on 2026-08-25**:
///
/// 1. **Add a rung** — `LOD_TIER_ZOOMS = [0, 6, 9, 12, 15]`. Every fetched rung then
/// changes what is drawn. Costs a fifth `Lod` variant and a pass over every style rule
/// that assigns one, which is the expensive option and the one V2.10 implied.
/// **← TAKEN.** `Lod::District`, tertiary + canal moved onto it, `BAKE_FORMAT_GEN`
/// bumped 4 → 5 because the blob header is sized by `LOD_COUNT`.
/// 2. **MOVE the Region step from 6 to 9** — `[0, 9, 12, 15]`. Same four tiers, no `Lod`
/// change, no style pass, and it aligns the two ladders **exactly**: the mutation that
/// red-proves the test above is this exact edit, and it empties the waste list. What it
/// costs is cartographic, not technical — cameras 6, 7 and 8 would draw `Country`
/// instead of `Region`, i.e. a regional overview of Sweden gets national-level content.
/// That is the owner's call, and it is a one-line call.
/// 3. **Accept it and write it down** where the recipe's `zooms` axis is set, so the next
/// person reads "cameras 9–11 pull a finer rung than they draw" as a decision rather
/// than finding it again.
///
/// The ONE thing that is not an option is leaving it undocumented, which is the state
/// V2.10 found it in.
#[test]
fn the_live_pyramid_weighs_each_rung() {
let root = std::path::Path::new("/home/korp/.korp/work/sweden/tiles");
if !root.is_dir() {
eprintln!(
"EMIT-LOD SKIPPED (STATED, not silent): no live pyramid at {} on this box. The \
ladder finding above is pure and ran; only the byte weights need the extract.",
root.display()
);
return;
}
let mut total_tiles = 0u64;
let mut weights: Vec<(u8, u64, u64)> = Vec::new();
for z in PYRAMID_ZOOMS {
let dir = root.join(format!("z{z}"));
let (mut tiles, mut bytes) = (0u64, 0u64);
let mut stack = vec![dir.clone()];
while let Some(d) = stack.pop() {
let Ok(entries) = std::fs::read_dir(&d) else { continue };
for e in entries.flatten() {
let p = e.path();
match e.file_type() {
Ok(t) if t.is_dir() => stack.push(p),
Ok(t) if t.is_file() => {
tiles += 1;
bytes += e.metadata().map(|m| m.len()).unwrap_or(0);
}
_ => {}
}
}
}
total_tiles += tiles;
weights.push((z, tiles, bytes));
}
// A rung that read zero files means the walk failed (permissions, most likely), and a
// zero-weight table would read exactly like "this rung is free".
if weights.iter().any(|w| w.1 == 0) {
eprintln!(
"EMIT-LOD SKIPPED (STATED): the pyramid is present but at least one rung read \
ZERO files — almost certainly not readable as this user. Weights: {weights:?}"
);
return;
}
let base = weights[0].2.max(1);
eprintln!("EMIT-LOD rung | tiles | bytes | x z6 | tier that camera draws");
for (z, tiles, bytes) in &weights {
eprintln!(
"EMIT-LOD z{z:<2} | {tiles:>9} | {bytes:>14} | {:>8.2}x | {}",
*bytes as f64 / base as f64,
tier_name(view_lod_tier(f32::from(*z)))
);
}
eprintln!("EMIT-LOD total tiles: {total_tiles}");
// THE ASSERTION, and it is about the shape rather than the numbers — the extract is
// allowed to be re-cut, but a finer rung that weighs no more than a coarser one means
// the walk measured something other than the pyramid.
for w in weights.windows(2) {
assert!(
w[1].2 > w[0].2,
"rung z{} must weigh more than z{} ({} vs {} bytes) — a pyramid whose finer \
rung is lighter was not walked correctly",
w[1].0,
w[0].0,
w[1].2,
w[0].2
);
}
// And what the rung now buys, as a number. The weight is why the flat spot was worth
// fixing rather than documenting; keeping the ratio printed is what would make a
// REGRESSION (the tier being taken away again) visible as a cost and not just as a
// failing assert.
let z6 = weights.iter().find(|w| w.0 == 6).expect("z6 rung").2;
let z9 = weights.iter().find(|w| w.0 == 9).expect("z9 rung").2;
eprintln!(
"EMIT-LOD z9 IS EARNED: camera 9-11 fetches z9 ({:.2}x the bytes of z6) and now \
draws {} where camera 6 draws {}",
z9 as f64 / z6 as f64,
tier_name(view_lod_tier(9.0)),
tier_name(view_lod_tier(6.0))
);
assert!(
z9 > z6 * 2,
"the z9 rung must be materially heavier than z6 for its tier to be worth having \
({z9} vs {z6})"
);
assert_ne!(
view_lod_tier(9.0),
view_lod_tier(6.0),
"…and those bytes must buy a different tier, or the flat spot is back"
);
}