diff --git a/client/tests/fixtures/msgpack/atlas_response_ready_with_window.msgpack b/client/tests/fixtures/msgpack/atlas_response_ready_with_window.msgpack index 32c85b742..90ecca37d 100644 Binary files a/client/tests/fixtures/msgpack/atlas_response_ready_with_window.msgpack and b/client/tests/fixtures/msgpack/atlas_response_ready_with_window.msgpack differ diff --git a/server/src/atlas/district_profile.rs b/server/src/atlas/district_profile.rs index fb1892ada..89c16a9b7 100644 --- a/server/src/atlas/district_profile.rs +++ b/server/src/atlas/district_profile.rs @@ -1569,6 +1569,128 @@ pub fn derive_at_metres( ) } +/// The orbital-rung derivation (T-1152, zoom ladder design doc §2/§4): the +/// coarse-granularity twin of [`derive_at_metres`] that skips [`invent_primitives`] +/// entirely — **no coastline warp, no detail-scatter octave sum, no classification +/// noise call of any kind**. Per the design doc's orbital row: "`region_baseline_at_district` +/// only — bilinear blend of 4 region baselines, no `invent_primitives`, no +/// classification [driver]." Orbital sample spacing (≥205 km, D-243's region rung +/// and coarser) sits below `detail_scatter`'s own octave floor +/// (`OCTAVE_WAVELENGTHS_M`'s coarsest entry is 32,768 m ≈ 32.8 km — an order of +/// magnitude finer than a region), so the invented terrain has nothing left to +/// contribute at this spacing; calling it would burn cycles synthesizing detail +/// no orbital pixel can resolve. What DOES vary at orbital spacing is the +/// **envelope** the heightmap itself carries (the `TerrainAnalysis` continental +/// shape) and the **region climate baseline** — this function samples exactly +/// those two, nothing else. +/// +/// **Cost model (design doc R1 — measure first):** one `bilinear` (elevation), +/// one `bilinear_bool` (ocean mask), one `region_baseline_at_district` call (its +/// own cost is 4×`derive_region_baseline_c` on a cache miss, O(1) on a cache hit) +/// — no octave sum, no coast-warp trig, no character/envelope computation. See +/// `server/tests/zoom_ladder_bench.rs`'s `bench_derive_orbital_at_metres` for the +/// measured per-cell figure this claim rests on. +/// +/// Produces the SAME six-field tail every other rung produces (`morphology_zone`, +/// `elev_q`, `temperature_c`, `moisture_q`, `vegetation_class`, `glaciation_grade`) +/// by routing the bilinear-only primitives through the same +/// [`build_district_profile`] classification tail every other rung uses — one +/// classification pipeline, never a second orbital-only decision tree (D-227: +/// classification thresholds don't get a coarse-rung variant any more than the +/// quarter rung got its own "quarter mode" thresholds, design doc §6). +/// +/// **R2 (stepped fields):** `moisture_q`/`temperature_c`/`morphology_zone`/etc. +/// are exactly as stepped here as at every other rung — `region_baseline_at_district` +/// floor-divides to the containing `DistrictPos` regardless of caller spacing (see +/// [`derive_at_metres`]'s own doc on this), so this function does not make +/// temperature MORE continuous at orbital scale; it inherits the same +/// district-tier step the design doc documents as permanent, by construction. +/// +/// **`slope_q` is fixed at 0`** — the bilinear-only envelope carries no +/// per-cell slope signal at orbital spacing (`ta.slope_deg` is a district-scale +/// proxy; sampling it here would imply a precision the coarse envelope doesn't +/// have). `slope_q` only affects morphology gates 3–6 (FjordWall/CliffCoast/ +/// BraidedDelta/DuneStrand) and the invented-primitives `carve` term this +/// function never runs — passing 0 means those gates fall through to their +/// low-slope alternatives, which is the correct behavior for a coastline sampled +/// at coarser-than-detail-scatter resolution (no invented ruggedness to report). +pub fn derive_orbital_at_metres( + seed: SeedChain, + body_id: &str, + body_params: &BodyParams, + ta: &TerrainAnalysis, + wx: f64, + wy: f64, + climate: &ClimateConstants, +) -> DistrictProfile { + // Same world-metres -> fractional heightmap pixel + latitude mapping + // derive_at_metres uses — the envelope is the SAME TerrainAnalysis grid at + // every rung, only the sampling density differs. + let (px, py, _world_x_m, _world_y_m, lat_deg) = match body_params.body_radius_km { + Some(r_km) if r_km > 0.0 => { + let circumference_m = std::f64::consts::TAU * r_km * 1000.0; + let meridian_m = std::f64::consts::PI * r_km * 1000.0; + let px = (wx / circumference_m).rem_euclid(1.0) * ta.w as f64; + let lat_frac = (wy / meridian_m).clamp(-0.5, 0.5); + let py = (0.5 + lat_frac) * ta.h.saturating_sub(1) as f64; + (px, py, wx, wy, -lat_frac * 180.0) + } + _ => { + let dm = scale::DISTRICT_M as f64; + let px = (wx / dm).clamp(0.0, ta.w.saturating_sub(1) as f64); + let py = (wy / dm).clamp(0.0, ta.h.saturating_sub(1) as f64); + let lat_deg = if ta.h > 1 { + 90.0 - (py / (ta.h - 1) as f64) * 180.0 + } else { + 0.0 + }; + (px, py, px * dm, py * dm, lat_deg) + } + }; + + let params = BodyParams { + latitude_deg: lat_deg, + ..body_params.clone() + }; + + // The envelope only — no coast-warp, no detail-scatter. This is exactly + // `invent_primitives`' step-1 "driver tier" raw bilinear reads, promoted to + // be the FINAL primitives instead of a one-step-stale input to invention. + let elev_q = + ((bilinear(&ta.elev_pct, ta.w, ta.h, px, py) as f64 * 100.0).round() as i32).clamp(0, 100); + let ocean_fraction_q = ((bilinear_bool(&ta.ocean_mask, ta.w, ta.h, px, py) as f64 * 100.0) + .round() as i32) + .clamp(0, 100); + // No invented ruggedness at orbital spacing (see the function doc's note + // on slope_q) — the envelope carries no per-cell slope signal this coarse. + let slope_q = 0; + + let district_pos: DistrictPos = ( + (wx / scale::DISTRICT_M as f64).floor() as i32, + (wy / scale::DISTRICT_M as f64).floor() as i32, + ); + let region_baseline_c = region_profile::region_baseline_at_district( + seed.seed(), + body_id, + district_pos, + ¶ms, + climate, + seed, + None, // no pre-built cache; derive on-the-fly, same posture as derive_at_metres + ); + + build_district_profile( + seed, + ¶ms, + climate, + slope_q, + elev_q, + ocean_fraction_q, + region_baseline_c, + BasinDirection::default(), + ) +} + /// Bilinear interpolation of a row-major `f32` field at fractional `(px, py)`. /// Columns wrap (equirectangular); rows clamp at the poles. fn bilinear(field: &[f32], w: usize, h: usize, px: f64, py: f64) -> f32 { @@ -2066,6 +2188,153 @@ mod tests { ); } + // ------------------------------------------------------------------- + // derive_orbital_at_metres (T-1152, design doc §2/§4 orbital row) + // ------------------------------------------------------------------- + + /// Determinism (D-010/D-227): two independent orbital derives at the same + /// position produce a bit-identical `DistrictProfile`, mirroring + /// `derive_district_is_deterministic`'s pattern for the finer rungs. + #[test] + fn derive_orbital_at_metres_is_deterministic() { + let hm = test_hm(); + let ta = test_ta(&hm); + let climate = ClimateConstants::default(); + let p = earth_params(); + let dm = scale::REGION_M as f64; + + let a = derive_orbital_at_metres( + test_seed(), + "test_body", + &p, + &ta, + 3.0 * dm, + 2.0 * dm, + &climate, + ); + let b = derive_orbital_at_metres( + test_seed(), + "test_body", + &p, + &ta, + 3.0 * dm, + 2.0 * dm, + &climate, + ); + assert_district_profiles_eq(&a, &b); + } + + /// The orbital path must NOT run `invent_primitives` — the design doc's + /// central constraint (§2: "no invent_primitives at orbital wavelengths"). + /// Direct proof: `slope_q` is always exactly 0 (invention is the only + /// source of nonzero slope_q at this call depth — see + /// `derive_orbital_at_metres`'s doc on why slope_q is fixed), sampled + /// across enough distinct positions that a nonzero value appearing even + /// once would falsify the claim. + #[test] + fn derive_orbital_at_metres_never_invents_slope() { + let hm = test_hm(); + let ta = test_ta(&hm); + let climate = ClimateConstants::default(); + let p = earth_params(); + let dm = scale::REGION_M as f64; + + for i in 0..25 { + let wx = (i * 7) as f64 * dm * 0.37; + let wy = (i * 11) as f64 * dm * 0.29; + let prof = + derive_orbital_at_metres(test_seed(), "test_body", &p, &ta, wx, wy, &climate); + assert_eq!( + prof.slope_q, 0, + "orbital derive must never report invented slope (position {i})" + ); + } + } + + /// The orbital derive's `elev_q`/`temperature_c` must come from the SAME + /// envelope + region-baseline sources `derive_at_metres` reads — not an + /// independent/divergent computation. At a position where the invented + /// scatter happens to contribute exactly zero (impossible to guarantee by + /// construction, so this test instead checks the WEAKER, always-true + /// property: both paths' `elev_q` derive from the same underlying + /// bilinear envelope, so they must be close — within the invented + /// scatter's own bounded contribution range, not arbitrarily different). + /// This guards against the orbital path silently reading a different + /// terrain field entirely (a copy-paste bug this refactor is exactly the + /// kind of change that could introduce). + #[test] + fn derive_orbital_at_metres_elevation_tracks_the_same_envelope() { + let hm = test_hm(); + let ta = test_ta(&hm); + let climate = ClimateConstants::default(); + let p = earth_params(); + let dm = scale::DISTRICT_M as f64; + + // Sample at a DISTRICT-aligned position (within the orbital function's + // legal domain — it accepts any world position, this just makes the + // district-mode comparison call meaningful) so both paths read the + // exact same fractional heightmap pixel. + let wx = 40.0 * dm; + let wy = 20.0 * dm; + let orbital = derive_orbital_at_metres(test_seed(), "test_body", &p, &ta, wx, wy, &climate); + let full = derive_at_metres(test_seed(), "test_body", &p, &ta, wx, wy, &climate, 0.0); + + // The invented scatter is a bounded perturbation on top of the raw + // envelope (detail_scatter's amplitude is capped well under 100 elev_q + // points) — the two must be in the same ballpark, not exactly equal + // (that would defeat the point of invention existing at all at the + // finer rung) and not wildly different (that would mean the orbital + // path is reading a different field). + let elev_diff = (orbital.elev_q - full.elev_q).abs(); + assert!( + elev_diff <= 50, + "orbital elev_q ({}) and full-derive elev_q ({}) must come from the \ + same envelope, not diverge arbitrarily", + orbital.elev_q, + full.elev_q + ); + } + + /// Orbital-scale windows must still fill all six dense wire arrays the + /// client's colorizer family reads (T-1152: "the orbital cells must fill + /// the same six dense arrays the DistrictWindowLayer carries") — this is + /// checked at the `DistrictProfile` level (the pre-packing source of + /// those six fields): every field the packer reads + /// (`morphology_zone`/`elev_q`/`temperature_c`/`moisture_q`/ + /// `vegetation_class`/`glaciation_grade`) must be populated the same way + /// regardless of rung — this test asserts the orbital output is a + /// legitimate `DistrictProfile`, not a partially-filled stand-in. + #[test] + fn derive_orbital_at_metres_populates_all_six_wire_fields() { + let hm = test_hm(); + let ta = test_ta(&hm); + let climate = ClimateConstants::default(); + let p = earth_params(); + let dm = scale::REGION_M as f64; + + let prof = derive_orbital_at_metres( + test_seed(), + "test_body", + &p, + &ta, + 5.0 * dm, + 3.0 * dm, + &climate, + ); + assert!((0..=100).contains(&prof.elev_q)); + assert!((0..=100).contains(&prof.moisture_q)); + // temperature_c is Some for a breathable-atmosphere body (earth_params). + assert!(prof.temperature_c.is_some()); + // morphology_zone/vegetation_class/glaciation_grade are enums with no + // "unset" state — successfully constructing the DistrictProfile at + // all (no panic) is the actual assertion; the field reads below just + // confirm they're reachable typed values, matching the discipline + // `derive_district_is_deterministic` and neighbours already use. + let _ = prof.morphology_zone; + let _ = prof.vegetation_class; + let _ = prof.glaciation_grade; + } + #[test] fn derive_district_profile_is_deterministic() { let hm = test_hm(); diff --git a/server/src/atlas/gen_queue.rs b/server/src/atlas/gen_queue.rs index 780ae17d7..5ce2f8198 100644 --- a/server/src/atlas/gen_queue.rs +++ b/server/src/atlas/gen_queue.rs @@ -34,7 +34,9 @@ use crate::atlas::cascade::{run_cascade_from_heightmap, CascadeLayer}; use crate::atlas::district_profile::{BodyParams, ClimateConstants, DistrictPos}; use crate::atlas::features::TerrainAnalysis; use crate::atlas::heightmap::{load_heightmap_png, GRID_H, GRID_W}; -use crate::atlas::layer_proxy::{build_district_window_layer, DistrictWindowLayer}; +use crate::atlas::layer_proxy::{ + build_district_window_layer, DistrictWindowLayer, WindowGranularity, +}; use crate::atlas::shell::{fill_chunk, FilledChunk}; use crate::atlas::skeleton_gen::{assign_all_block_tags, generate_quarter_skeleton}; use crate::atlas::trait_catalog_reader::ExteriorCatalog; @@ -208,11 +210,18 @@ pub enum GenWorkItem { /// before this item is built — never trusted from the wire again here. center: DistrictPos, n: u32, - /// Derivation granularity (T-1150) — `WINDOW_GRANULARITY_DISTRICT` (1) - /// or `WINDOW_GRANULARITY_QUARTER` (4). Already resolved via - /// `resolve_window_granularity` by the caller. - granularity: u32, + /// Derivation granularity (T-1150, widened T-1152 to the full + /// [`WindowGranularity`] vocabulary — `District`/`Quarter` (finer) + /// plus `Region` (coarser, T-1152)). Already resolved via + /// `resolve_window_granularity_v2` by the caller — this is a + /// concrete rung, never a raw wire value. + granularity: WindowGranularity, /// Octave cutoff in whole metres (T-1149/T-1150), `0` = no cutoff. + /// Meaningless for `granularity: Region` (`derive_orbital_at_metres` + /// never calls the octave-scatter path this cuts off) but still + /// carried and echoed uniformly — see `derive_orbital_at_metres`'s + /// doc for why the field is harmless-but-unused there, not + /// special-cased away. min_wl_m: u32, }, } @@ -227,13 +236,17 @@ impl GenWorkItem { /// Coalescing key for `DeriveWindow` items only — `(connection, body, /// granularity)` (T-1150, design doc §3 [SOFT] recommendation, extending - /// T-1137's `(connection, body)`). `granularity` is part of the key so an - /// in-flight district-spacing (granularity 1) pan-burst is never - /// superseded by an unrelated quarter-spacing (granularity 4) request for - /// the same connection+body, and vice versa — the two rungs are separate - /// in-flight derives, not competing updates to the same one. + /// T-1137's `(connection, body)`; widened T-1152 to carry the full + /// [`WindowGranularity`] enum rather than the legacy `u32`, so `Region` + /// occupies its own coalescing slot exactly like `District`/`Quarter` do + /// — this is one of the five T-1150 touch points the R5 redesign must + /// carry the new representation through). `granularity` is part of the + /// key so an in-flight district-spacing pan-burst is never superseded by + /// an unrelated quarter- or region-spacing request for the same + /// connection+body, and vice versa — every rung is a separate in-flight + /// derive, not a competing update to the same one. /// `None` for every other variant (they don't coalesce this way). - pub fn window_supersede_key(&self) -> Option<(ConnectionId, &str, u32)> { + pub fn window_supersede_key(&self) -> Option<(ConnectionId, &str, WindowGranularity)> { if let GenWorkItem::DeriveWindow { body_id, conn_id, @@ -1150,19 +1163,14 @@ mod tests { /// coalescing tests can exercise the granularity axis of /// `window_supersede_key()` without a second near-duplicate helper. fn derive_window(body_id: &str, conn_id: ConnectionId, center: DistrictPos) -> GenWorkItem { - derive_window_at( - body_id, - conn_id, - center, - crate::atlas::layer_proxy::WINDOW_GRANULARITY_DISTRICT, - ) + derive_window_at(body_id, conn_id, center, WindowGranularity::District) } fn derive_window_at( body_id: &str, conn_id: ConnectionId, center: DistrictPos, - granularity: u32, + granularity: WindowGranularity, ) -> GenWorkItem { GenWorkItem::DeriveWindow { body_id: body_id.to_string(), @@ -1316,21 +1324,11 @@ mod tests { let conn = ConnectionId(9); q.submit_window( - derive_window_at( - "GranBody", - conn, - (0, 0), - crate::atlas::layer_proxy::WINDOW_GRANULARITY_DISTRICT, - ), + derive_window_at("GranBody", conn, (0, 0), WindowGranularity::District), GenPriority::Immediate, ); q.submit_window( - derive_window_at( - "GranBody", - conn, - (0, 0), - crate::atlas::layer_proxy::WINDOW_GRANULARITY_QUARTER, - ), + derive_window_at("GranBody", conn, (0, 0), WindowGranularity::Quarter), GenPriority::Immediate, ); assert_eq!( @@ -1352,21 +1350,11 @@ mod tests { let conn = ConnectionId(11); q.submit_window( - derive_window_at( - "SameGranBody", - conn, - (0, 0), - crate::atlas::layer_proxy::WINDOW_GRANULARITY_QUARTER, - ), + derive_window_at("SameGranBody", conn, (0, 0), WindowGranularity::Quarter), GenPriority::Immediate, ); q.submit_window( - derive_window_at( - "SameGranBody", - conn, - (5, 5), - crate::atlas::layer_proxy::WINDOW_GRANULARITY_QUARTER, - ), + derive_window_at("SameGranBody", conn, (5, 5), WindowGranularity::Quarter), GenPriority::Immediate, ); assert_eq!( diff --git a/server/src/atlas/layer_proxy.rs b/server/src/atlas/layer_proxy.rs index 8b48c8b84..b8942d869 100644 --- a/server/src/atlas/layer_proxy.rs +++ b/server/src/atlas/layer_proxy.rs @@ -37,8 +37,51 @@ const DEFAULT_SEA_LEVEL: f32 = 0.3; /// same window size `aliveness_probe --render`'s default already proved out /// server-side (T-1123). **Never trust `window_n` from the wire** — every /// caller clamps to `[1, DISTRICT_WINDOW_MAX_N]` before deriving. +/// +/// **Finer-than-district / district rungs ONLY (T-1152).** This ceiling was +/// calibrated for district spacing; applying it unchanged to `Region` +/// requests would be nonsensical — see [`DISTRICT_WINDOW_MAX_N_REGION`]'s doc +/// for why `Region` needs its own, much larger per-axis ceiling on the SAME +/// `n` (window extent in districts). pub const DISTRICT_WINDOW_MAX_N: u32 = 64; +/// Per-axis ceiling on [`AtlasLayerRequest::window_n`] for +/// [`WindowGranularity::Region`] requests ONLY (T-1152 step 4: "work out what +/// n means at region granularity ... and document it"). `n` is always the +/// window extent in DISTRICTS regardless of rung (T-1150 design doc §2, +/// unchanged) — but [`DISTRICT_WINDOW_MAX_N`] (64 districts ≈ 131 km) was +/// sized for the district rung's own per-cell world extent, and reusing it +/// unchanged for `Region` would clamp every region window to well under +/// ONE region's own 100-district side ([`crate::atlas::scale::DISTRICTS_PER_REGION`]), +/// making [`WindowGranularity::cell_grid_side`] round every legal `n` down +/// to a degenerate 1×1 cell grid — a "region view" that can never show more +/// than one region cell is not a useful rung. +/// +/// Derived (not a new magic number, per the D-243 instruction): the largest +/// `n` for which `cell_grid_side(n) == sqrt(WIRE_CAP_CELLS)` (64 region +/// cells across, the same [`WIRE_CAP_CELLS`]-derived side length district +/// mode already reaches at its own cap) is +/// `sqrt(WIRE_CAP_CELLS) * DISTRICTS_PER_REGION = 64 * 100 = 6,400` +/// districts (≈13,100 km — comfortably covering a planetary hemisphere's +/// worth of region tiles in one capped request). The WIRE-SIZE ceiling +/// ([`WIRE_CAP_CELLS`] via [`clamp_window_n_v2`]) is still the actual +/// enforcement point (never trusted from the wire) — this constant only +/// widens the PER-AXIS ceiling far enough that the wire-size math has room +/// to matter for `Region`, exactly mirroring how [`DISTRICT_WINDOW_MAX_N`] +/// relates to the wire-size ceiling at district granularity (see +/// `clamp_window_n`'s doc: "Applied AFTER the per-axis clamp so a request +/// that already satisfies [the per-axis cap] still shrinks further"). +pub const DISTRICT_WINDOW_MAX_N_REGION: u32 = + WIRE_CAP_CELLS_SQRT * crate::atlas::scale::DISTRICTS_PER_REGION as u32; + +/// `WIRE_CAP_CELLS`'s integer square root (64) — computed once as a `const` +/// so [`DISTRICT_WINDOW_MAX_N_REGION`]'s derivation is checkable at compile +/// time rather than repeating the literal `64` as an uncommented magic +/// number. `WIRE_CAP_CELLS = 4_096 = 64²` exactly (see that constant's own +/// doc), so this is exact integer arithmetic, not an approximation. +const WIRE_CAP_CELLS_SQRT: u32 = 64; +const _: () = assert!(WIRE_CAP_CELLS_SQRT * WIRE_CAP_CELLS_SQRT == WIRE_CAP_CELLS); + /// [`AtlasLayerRequest::window_granularity`] encoding (T-1150, zoom ladder /// design doc §3/§5): the number of derived cells per district side. `0` on /// the wire (the `#[serde(default)]` absent case) and `1` both mean district @@ -52,6 +95,33 @@ pub const DISTRICT_WINDOW_MAX_N: u32 = 64; pub const WINDOW_GRANULARITY_DISTRICT: u32 = 1; pub const WINDOW_GRANULARITY_QUARTER: u32 = 4; +/// The `granularity: u32` value [`DistrictWindowLayer`]'s echo (and the +/// internal cache/coalescing keys) use for [`WindowGranularity::Region`] — +/// **a key-space value, never a legal WIRE INPUT.** [`resolve_window_granularity`] +/// (the legacy field's resolver) never produces this value, and a client +/// sending it on the wire in the legacy `window_granularity` field is +/// indistinguishable from any other unrecognized value — it still resolves +/// to `District` (§ `resolve_window_granularity`'s exhaustive fallback), NOT +/// `Region`. The only way to actually request `Region` is +/// `window_granularity_v2 = Some(WindowGranularity::Region)`. +/// +/// **Why this exists at all, given `Region` has no legacy representation:** +/// the aliasing discipline (T-1150 design doc §3, the mandatory +/// granularity-4-vs-1 test) requires that every distinct [`WindowGranularity`] +/// occupy a distinct slot in [`DistrictWindowKey`]/the coalescing key, both of +/// which carry a `u32` granularity component for wire back-compat. Reusing +/// `0` (today's "absent" sentinel, mapped to `District`) or any value +/// `resolve_window_granularity` could legally receive would silently alias a +/// `Region` window onto a `District` or `Quarter` cache slot depending on +/// what a future caller happened to pass — exactly the bug class §3 exists +/// to close. `u32::MAX` can never collide with a real multiplier (multipliers +/// are small integers by construction — 1, 4, and any future finer rung), +/// so it's the natural "this is a key-space tag, not a spacing multiplier" +/// value. A T-1152-aware client reads `granularity_v2` and never looks at +/// this number for `Region` responses; it exists purely so the legacy `u32` +/// slot in the key tuple stays total and never lies about aliasing. +pub const WINDOW_GRANULARITY_REGION_KEY: u32 = u32::MAX; + /// Server-side wire-size ceiling (T-1150, design doc §3 "Cell-count cap"): /// `window_n² × granularity² ≤ WIRE_CAP_CELLS`. At `WIRE_CAP_CELLS = 4,096`, /// district `n=64` (the existing [`DISTRICT_WINDOW_MAX_N`] cap) sits exactly @@ -63,19 +133,161 @@ pub const WINDOW_GRANULARITY_QUARTER: u32 = 4; /// trusted from the wire) rather than merely asserted. pub const WIRE_CAP_CELLS: u32 = 4_096; +// --------------------------------------------------------------------------- +// WindowGranularity (T-1152, R5 redesign — D-226 T-1143-rulings amendment's +// "Wire-contract note (T-1150, PR #191 review — Tyre)") +// --------------------------------------------------------------------------- + +/// The full window-derivation-granularity vocabulary (T-1152), superseding +/// `window_granularity: u32`'s finer-than-district-only ceiling (Tyre's +/// wire-contract note, D-226 T-1143-rulings amendment: "the `u32` multiplier +/// field expresses finer-than-district integer multiples only ... a new +/// magic value is not the path"). This is the R5 redesign the note demands: +/// an explicit **named-variant enum**, the same wire pattern [`RoadNodeKind`] +/// already uses on this carrier (a plain `#[derive(Serialize, Deserialize)]` +/// enum with no `#[repr]`/manual impl serializes as its variant name over +/// `rmp_serde`, not an integer discriminant — deliberately NOT the +/// `repr(u8)`-cast-to-`Vec` convention the six dense per-cell arrays use; +/// this field is a scalar tag, not a bulk payload, so the string-tag +/// legibility is worth the few extra wire bytes one field costs). +/// +/// **Why an enum and not a signed/log-scale int (the R5 alternative the risk +/// row named):** a log-scale `i32` still needs a lookup table to turn back +/// into a spacing, and a "cannot express" bug (someone passing `-2` and +/// expecting quarter-of-quarter) is silent at the type level. An enum with an +/// exhaustive match in [`WindowGranularity::spacing_m`] makes "this variant +/// has no defined spacing" a compile error, not a runtime surprise — the same +/// reasoning `MorphologyZone`'s exhaustive-match discipline already +/// established for this codebase (D-239 §6). +/// +/// **Precedence over the legacy `u32` field (documented here, the single +/// place both fields are reconciled):** [`AtlasLayerRequest::window_granularity_v2`] +/// wins whenever present and non-`None`; the legacy `u32` +/// [`AtlasLayerRequest::window_granularity`] is consulted ONLY when +/// `window_granularity_v2` is absent (`#[serde(default)]`, every pre-T-1152 +/// client). This is a strict either/or, not a merge — a client sending BOTH +/// fields (a mixed old/new build, or a future client hedging compatibility) +/// gets the `v2` field's answer, silently ignoring the legacy `u32`. See +/// [`resolve_window_granularity_v2`], the single widening point for this +/// enum (mirroring `resolve_window_granularity`'s role for the legacy field). +/// +/// **Unknown → District** at every resolution boundary (never trust the +/// wire) — same posture as the legacy `u32` path and every other wire-decoded +/// enum in this module. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] +pub enum WindowGranularity { + /// 512 m/cell (D-243 `QUARTER_M`) — finer than district, T-1150 Option B. + Quarter, + /// 2,048 m/cell (D-243 `DISTRICT_M`) — the original, still-default rung. + District, + /// 204,800 m/cell (D-243 `REGION_M`) — the coarser-than-district rung R5 + /// flagged as unrepresentable in the `u32` multiplier encoding. Serves + /// BOTH the design doc's named "Region" row and the "Orbital/planetary" + /// row (R4: "Acceptable if the planetary rung's tiling effectively + /// subsumes it" — they share one derivation mode, `derive_orbital_at_metres`, + /// and one spacing; there is no third, coarser rung to distinguish them + /// by today, so one variant covers both named rows without inventing a + /// spacing the design doc never specified). + Region, +} + +impl WindowGranularity { + /// Cell spacing in metres — the single source of truth every caller + /// (server derive loop, cap math, client mirror) must read through, + /// rather than re-deriving the metre value from the variant name. + /// Sourced from `scale::` (D-243), never a magic number local to this + /// module (T-1152 instruction: "Update the D-243-derived spacing + /// constants from `scale::` rather than new magic numbers"). + pub fn spacing_m(self) -> f64 { + match self { + WindowGranularity::Quarter => crate::atlas::scale::QUARTER_M as f64, + WindowGranularity::District => DISTRICT_M as f64, + WindowGranularity::Region => crate::atlas::scale::REGION_M as f64, + } + } + + /// The legacy `window_granularity: u32` value this variant maps to/from + /// for finer-than-district rungs — `None` for `Region`, which the legacy + /// field cannot express by construction (Tyre's note; this is precisely + /// the gap the enum exists to close). Used only by + /// [`resolve_window_granularity_v2`]'s legacy-fallback branch and by + /// [`DistrictWindowLayer`]'s echo, which still carries the legacy `u32` + /// unchanged for wire back-compat (see that struct's doc). + fn legacy_u32(self) -> Option { + match self { + WindowGranularity::District => Some(WINDOW_GRANULARITY_DISTRICT), + WindowGranularity::Quarter => Some(WINDOW_GRANULARITY_QUARTER), + WindowGranularity::Region => None, + } + } + + /// The `u32` this variant occupies in [`DistrictWindowKey`]/the + /// coalescing key/`DistrictWindowLayer.granularity`'s echo — total over + /// every variant (unlike [`Self::legacy_u32`], which is partial). Finer- + /// than-district variants echo their real legacy multiplier (so a + /// T-1150-only client — one that reads `granularity` but has never heard + /// of `granularity_v2` — still sees the correct, meaningful number for + /// District/Quarter); `Region` echoes the reserved + /// [`WINDOW_GRANULARITY_REGION_KEY`] key-space tag (see that const's doc + /// for why `0` or any real multiplier would be unsafe here). + fn key_u32(self) -> u32 { + self.legacy_u32().unwrap_or(WINDOW_GRANULARITY_REGION_KEY) + } + + /// The derived cell-grid side length (in CELLS, at this granularity) for + /// a window whose extent is `n` DISTRICTS (T-1152 step 4: "work out what + /// n means at region granularity against D-243's region=100-district + /// side and document it" — this is that answer, made a total function + /// instead of inline arithmetic at each call site). + /// + /// `n` is ALWAYS the window extent in DISTRICTS regardless of + /// granularity (T-1150 design doc §2, unchanged by this ticket: "the + /// window's `n` stays the DISTRICT extent"). The derived grid's side + /// length scales by [`Self::spacing_m`] relative to [`DISTRICT_M`]: + /// + /// - `Quarter` (512 m, 4 cells/district side): `side = n * 4` — MORE + /// cells than districts requested (T-1150's existing behavior, + /// unchanged). + /// - `District` (2,048 m, 1:1): `side = n` — unchanged. + /// - `Region` (204,800 m = 100 districts/side, D-243 + /// `DISTRICTS_PER_REGION`): `side = round(n / 100)`, minimum 1. A + /// region-granularity request with the SAME `n` as a district request + /// derives a FAR SMALLER cell grid (a region window with `n=64` + /// districts — the per-axis cap — derives only a 1×1 region cell, + /// since 64 districts is well under one region's 100-district side). + /// This inversion (finer rungs MULTIPLY cell count by `n`; `Region` + /// DIVIDES it) is why [`clamp_window_n`]'s wire-cap math needs a + /// region-aware branch too (see that function) — a flat + /// `n² × multiplier² ≤ WIRE_CAP_CELLS` formula would make `n` for + /// `Region` requests nonsensically tiny if `multiplier` were naively + /// `1/100`. Rounding (not floor/ceil) keeps the mapping the closest + /// integer approximation of the true ratio; minimum 1 so an `n` smaller + /// than one region never derives a degenerate empty grid. + pub fn cell_grid_side(self, n: u32) -> i32 { + match self { + WindowGranularity::Quarter => (n * WINDOW_GRANULARITY_QUARTER) as i32, + WindowGranularity::District => n as i32, + WindowGranularity::Region => { + let dpr = crate::atlas::scale::DISTRICTS_PER_REGION as f64; + ((n as f64 / dpr).round() as i32).max(1) + } + } + } +} + /// Resolve a wire-supplied `window_granularity` value to one of the two legal /// granularities, clamping anything else down to district spacing — **never /// trust the wire** (same posture as `window_n`/`normalize_window_center`). /// -/// **This is THE single widening point (Tyre C2, PR #191 review).** The -/// field only ever expresses finer-than-district integer multiples (see -/// [`AtlasLayerRequest::window_granularity`]'s doc for the full type-seam -/// contract); adding a future finer rung means adding its legal value here -/// and nowhere else. Do NOT add a value < 1 or attempt to encode -/// coarser-than-district rungs (region/orbital) through this function — -/// design doc §5/§9 R5 requires a signed/log-scale or enum redesign for that -/// direction, which this `u32` cannot express regardless of what this -/// function returns. +/// **This is THE single widening point (Tyre C2, PR #191 review) for the +/// LEGACY `u32` field only.** The field only ever expresses finer-than-district +/// integer multiples (see [`AtlasLayerRequest::window_granularity`]'s doc for +/// the full type-seam contract); adding a future finer rung means adding its +/// legal value here and nowhere else. Do NOT add a value < 1 or attempt to +/// encode coarser-than-district rungs (region/orbital) through this function +/// — that direction is [`WindowGranularity::Region`]'s job via +/// [`resolve_window_granularity_v2`], never a new magic `u32` value (the R5 +/// redesign this enum performs is EXACTLY the alternative to doing that). fn resolve_window_granularity(raw: u32) -> u32 { if raw == WINDOW_GRANULARITY_QUARTER { WINDOW_GRANULARITY_QUARTER @@ -84,6 +296,36 @@ fn resolve_window_granularity(raw: u32) -> u32 { } } +/// Resolve a request's granularity to a [`WindowGranularity`] — **the single +/// widening point for the full (finer- and coarser-than-district) vocabulary** +/// (T-1152, mirroring [`resolve_window_granularity`]'s role for the legacy +/// `u32` alone). Precedence (documented once, here — see +/// [`WindowGranularity`]'s struct doc for the rationale): +/// +/// 1. `window_granularity_v2` present → resolved directly (unknown/malformed +/// variants can't reach this function at all — `rmp_serde` rejects an +/// unrecognized enum variant name at decode time, so "unknown" for THIS +/// field means "absent", not "present but garbage"; the legacy `u32` +/// path is what actually needs the value-level fallback because a `u32` +/// has no closed vocabulary). +/// 2. `window_granularity_v2` absent → fall back to the legacy `u32` path via +/// [`resolve_window_granularity`], mapped onto the two variants it can +/// express. +/// +/// A future coarser-than-region rung is added by widening this function's +/// match AND [`WindowGranularity`]'s variant list together — never by +/// smuggling a new value through the legacy `u32` (that field's ceiling is +/// permanent per Tyre's note, not a restriction this function works around). +fn resolve_window_granularity_v2(req: &AtlasLayerRequest) -> WindowGranularity { + match req.window_granularity_v2 { + Some(g) => g, + None => match resolve_window_granularity(req.window_granularity) { + WINDOW_GRANULARITY_QUARTER => WindowGranularity::Quarter, + _ => WindowGranularity::District, + }, + } +} + /// Clamp `window_n` against BOTH the existing per-axis cap /// ([`DISTRICT_WINDOW_MAX_N`]) and the granularity-aware wire-size ceiling /// ([`WIRE_CAP_CELLS`]) — `window_n² × granularity² ≤ WIRE_CAP_CELLS` (T-1150 @@ -106,6 +348,47 @@ fn clamp_window_n(raw_n: u32, granularity: u32) -> u32 { n.min(cap_n.floor().max(1.0) as u32) } +/// [`WindowGranularity`]-aware twin of [`clamp_window_n`] (T-1152 step 4) — +/// the SAME two-stage discipline (per-axis clamp, THEN the wire-size +/// ceiling on the DERIVED cell count, never trusted from the wire), but +/// computed through [`WindowGranularity::cell_grid_side`] so it is correct +/// for BOTH directions (finer multiplies cell count; `Region` divides it — +/// see that method's doc) instead of assuming the finer-only +/// `n × multiplier` relationship [`clamp_window_n`] hard-codes. +/// +/// - **`District`/`Quarter`:** per-axis cap is [`DISTRICT_WINDOW_MAX_N`] +/// (64, unchanged) — byte-identical clamped `n` to [`clamp_window_n`] for +/// every input these two variants can produce (verified by +/// `clamp_window_n_v2_matches_legacy_for_finer_than_district_rungs`, +/// below). +/// - **`Region`:** per-axis cap is [`DISTRICT_WINDOW_MAX_N_REGION`] (6,400 — +/// see that constant's doc for the derivation), then the SAME +/// wire-size-ceiling shrink applies on top via [`WindowGranularity::cell_grid_side`] +/// — a request whose `cell_grid_side(n)` would exceed +/// `sqrt(WIRE_CAP_CELLS)` region cells across is walked back by *halving* +/// `n` until it fits (region's `cell_grid_side` is a ROUNDING division, not +/// the finer rungs' exact multiplication, so there's no closed-form inverse +/// the way `cap_n = sqrt(WIRE_CAP_CELLS) / g` is for the finer case — a +/// short bounded loop is the correct tool here, not a formula that would +/// have to fight its own rounding). +fn clamp_window_n_v2(raw_n: u32, granularity: WindowGranularity) -> u32 { + match granularity { + WindowGranularity::District | WindowGranularity::Quarter => clamp_window_n( + raw_n, + granularity + .legacy_u32() + .unwrap_or(WINDOW_GRANULARITY_DISTRICT), + ), + WindowGranularity::Region => { + let mut n = raw_n.clamp(1, DISTRICT_WINDOW_MAX_N_REGION); + while granularity.cell_grid_side(n).pow(2) as u32 > WIRE_CAP_CELLS && n > 1 { + n /= 2; + } + n.max(1) + } + } +} + /// Quantized `window_min_wl_m` bands (T-1150, zoom ladder design doc §5): /// `0` (no cutoff) plus every entry of /// [`crate::atlas::detail_scatter::OCTAVE_WAVELENGTHS_M`] — the SAME array @@ -192,6 +475,17 @@ pub struct AtlasLayerRequest { /// the redesign R5 already flags, not a value to add here. #[serde(default)] pub window_granularity: u32, + /// The R5-redesigned granularity vocabulary (T-1152), able to express + /// coarser-than-district rungs the legacy `window_granularity: u32` + /// cannot (Tyre's wire-contract note — see [`WindowGranularity`]'s doc + /// for the full rationale). `#[serde(default)]` (`None`) is the absent + /// case: every pre-T-1152 client (and every T-1150 client that only ever + /// sends the legacy `u32`) omits this field entirely and is byte-compatible + /// — [`resolve_window_granularity_v2`] falls back to the legacy field + /// when this is `None`. When BOTH fields are present, THIS field wins + /// (documented once, on [`WindowGranularity`], not duplicated here). + #[serde(default)] + pub window_granularity_v2: Option, /// Octave cutoff for the invented-terrain scatter (T-1149's /// `min_wavelength_m`), in whole metres. `0` (absent) = no cutoff = the /// pre-T-1150 behavior. @@ -444,10 +738,25 @@ pub struct DistrictWindowLayer { /// The derived cell grid's actual side length is `n * granularity`. pub n: u32, /// Derivation granularity (T-1150): [`WINDOW_GRANULARITY_DISTRICT`] (1) - /// or [`WINDOW_GRANULARITY_QUARTER`] (4). Echoed so the client's cache - /// key and staleness guard can distinguish a district-spacing window from - /// a quarter-spacing window requested at the identical `(center, n)`. + /// or [`WINDOW_GRANULARITY_QUARTER`] (4) for finer-than-district rungs; + /// [`WINDOW_GRANULARITY_REGION_KEY`] (a reserved key-space tag, NOT a + /// spacing multiplier) when [`Self::granularity_v2`] is `Region` — see + /// that constant's doc. Kept unchanged (never repurposed or removed) for + /// wire back-compat with every pre-T-1152 client, which reads only this + /// field and has no concept of `granularity_v2`. Echoed so the client's + /// cache key and staleness guard can distinguish windows at different + /// finer-than-district rungs requested at the identical `(center, n)`. pub granularity: u32, + /// The R5-redesigned granularity (T-1152) — see [`WindowGranularity`]'s + /// doc. Always populated (never `None`): the server always resolves a + /// concrete rung internally via [`resolve_window_granularity_v2`] + /// regardless of which wire field the request used, so the response + /// always carries the enum echo alongside the legacy `u32` one. A + /// T-1152-aware client reads THIS field for staleness/cache-key + /// comparison; `granularity` (above) exists only for pre-T-1152 clients, + /// who never see `Region` responses in the first place (they have no way + /// to request one). + pub granularity_v2: WindowGranularity, /// The `min_wavelength_m` octave cutoff (T-1149) this window was derived /// with, in whole metres (`0` = no cutoff). Echoed for the same reason as /// `granularity` — two windows at identical `(center, n, granularity)` @@ -473,16 +782,26 @@ pub struct DistrictWindowLayer { pub glaciation: Vec, } -/// Key for the server-side window derive cache (T-1137, extended T-1150): -/// `(body_id, center, n, granularity, min_wl_m)`. D-227 purity means a cached -/// window is valid forever for a given body+seed — no staleness/TTL -/// invalidation is needed, only a bound on unbounded growth (see -/// [`DistrictWindowCache`]). `granularity`/`min_wl_m` MUST be part of the key -/// — the design doc's aliasing risk (§3): a granularity-4 request at the same -/// `(body, center, n)` as a granularity-1 request is a DIFFERENT payload and -/// must land in a different cache slot, never silently overwrite or be served -/// by the other. -pub type DistrictWindowKey = (String, DistrictPos, u32, u32, u32); +/// Key for the server-side window derive cache (T-1137, extended T-1150, +/// extended again T-1152): `(body_id, center, n, granularity, min_wl_m)`. +/// D-227 purity means a cached window is valid forever for a given +/// body+seed — no staleness/TTL invalidation is needed, only a bound on +/// unbounded growth (see [`DistrictWindowCache`]). `granularity`/`min_wl_m` +/// MUST be part of the key — the design doc's aliasing risk (§3): a +/// granularity-4 request at the same `(body, center, n)` as a granularity-1 +/// request is a DIFFERENT payload and must land in a different cache slot, +/// never silently overwrite or be served by the other. +/// +/// **The granularity slot is [`WindowGranularity`] itself (T-1152), not the +/// legacy `u32`** — carrying the full enum here (rather than relying on +/// [`WindowGranularity::key_u32`]'s reserved-sentinel trick alone) is what +/// makes a `Region` window's cache slot structurally distinct from a +/// `District`/`Quarter` one, satisfying the "carry the new representation" +/// requirement at the cache-key touch point directly rather than through an +/// encoding side-channel. `WindowGranularity`'s `Ord` derive (declaration +/// order: `Quarter < District < Region`) makes this legal as a `BTreeMap` +/// key (D-010 determinism — ordered iteration, no `HashMap`). +pub type DistrictWindowKey = (String, DistrictPos, u32, WindowGranularity, u32); /// Bounded LRU-ish cache of completed district-window derives (T-1137), a /// sibling to [`BodyWorldStateCache`] rather than a field on it: windows are @@ -565,15 +884,28 @@ struct WindowCell { /// cell is an independent function of its own world-metre position, nothing /// shared mutably. /// -/// `step_m` is the metre spacing between cells (T-1150): `DISTRICT_M` at -/// district granularity, `QUARTER_M` at quarter granularity — the caller -/// picks it, this function is granularity-agnostic (it only knows metres). +/// `granularity` (T-1150, widened to [`WindowGranularity`] T-1152) selects +/// BOTH the metre spacing between cells ([`WindowGranularity::spacing_m`]) +/// AND the derivation function: finer-than-district rungs (`District`, +/// `Quarter`) call `derive_at_metres` (full derivation, `invent_primitives` +/// included) exactly as T-1150 shipped; `Region` calls +/// [`crate::atlas::district_profile::derive_orbital_at_metres`] instead — the +/// design doc §2/§4 orbital row's region-baseline-blend-only path, no +/// `invent_primitives` call. This is the ONE place the two derivation +/// functions fork based on rung — everything else in the window-building +/// pipeline (`scatter_row`, the six-array packing, the cache/echo plumbing) +/// is identical regardless of which function ran, because both produce a +/// `DistrictProfile` and this function's WindowCell-packing tail (below) is +/// shared. +/// /// `half_cells` is HALF the cell-grid side (`side / 2`, already in the /// caller's cell units, not districts), so `center` (a `DistrictPos`, always /// district-scale) is converted to a world-metre origin once by the caller -/// and offset here in `step_m` units — this is what makes the quarter grid -/// cover the SAME world rect as the district grid at 4x the cell density -/// (design doc §2 Option B). +/// and offset here in `spacing_m()` units — this is what makes the quarter +/// grid cover the SAME world rect as the district grid at 4x the cell +/// density (design doc §2 Option B), and what makes a region-granularity +/// window cover a proportionally larger world rect at 1/100x the cell +/// density (D-243: `DISTRICTS_PER_REGION = 100`). #[allow(clippy::too_many_arguments)] fn derive_window_cell( seed: SeedChain, @@ -583,25 +915,33 @@ fn derive_window_cell( climate: &crate::atlas::district_profile::ClimateConstants, center_world_m: (f64, f64), half_cells: i32, - step_m: f64, + granularity: WindowGranularity, min_wavelength_m: f64, row: i32, col: i32, ) -> WindowCell { // Row 0 = northmost, matching aliveness_probe's render_window_panels // (derive_at_metres maps negative wy to negative lat_frac = north). + let step_m = granularity.spacing_m(); let wx = center_world_m.0 + (col - half_cells) as f64 * step_m; let wy = center_world_m.1 + (row - half_cells) as f64 * step_m; - let prof = crate::atlas::district_profile::derive_at_metres( - seed, - body_id, - params, - ta, - wx, - wy, - climate, - min_wavelength_m, - ); + let prof = match granularity { + WindowGranularity::Region => crate::atlas::district_profile::derive_orbital_at_metres( + seed, body_id, params, ta, wx, wy, climate, + ), + WindowGranularity::District | WindowGranularity::Quarter => { + crate::atlas::district_profile::derive_at_metres( + seed, + body_id, + params, + ta, + wx, + wy, + climate, + min_wavelength_m, + ) + } + }; WindowCell { morphology: prof.morphology_zone as u8, elev_q: prof.elev_q.clamp(0, 100) as u8, @@ -652,17 +992,6 @@ fn center_to_world_m(center: DistrictPos) -> (f64, f64) { (center.0 as f64 * dm, center.1 as f64 * dm) } -/// Cell step size in metres for a given granularity (T-1150): district -/// spacing (2,048 m) or quarter spacing (512 m). Any other value resolves to -/// district spacing (mirrors [`resolve_window_granularity`]'s fallback). -fn step_m_for_granularity(granularity: u32) -> f64 { - if granularity == WINDOW_GRANULARITY_QUARTER { - crate::atlas::scale::QUARTER_M as f64 - } else { - DISTRICT_M as f64 - } -} - /// Build a [`DistrictWindowLayer`] by deriving every cell in the window /// around `center` (T-1137, extended T-1150). Mirrors /// `aliveness_probe::render_window_panels`'s derive loop exactly (the probe @@ -677,11 +1006,13 @@ fn step_m_for_granularity(granularity: u32) -> f64 { /// of clamping at the edge). /// /// `n` is always the window extent in DISTRICTS (design doc §2 Option B: "the -/// window's `n` stays the DISTRICT extent"). At `granularity = 1` the derived -/// cell grid is `n × n` districts; at `granularity = 4` it is `(4n) × (4n)` -/// quarters covering the SAME world rect — full reclassification at the finer -/// spacing (`derive_at_metres` with `min_wavelength_m` matching the rung), -/// never a coarser-cell interpolation. +/// window's `n` stays the DISTRICT extent"). The derived cell-grid side +/// length is [`WindowGranularity::cell_grid_side`] — `n × 4` at `Quarter`, +/// `n` at `District`, `round(n / 100)` at `Region` (T-1152, D-243's +/// `DISTRICTS_PER_REGION = 100`; see that method's doc for the full +/// finer-multiplies/coarser-divides rationale). Full reclassification at the +/// rung's own spacing (`derive_at_metres`/`derive_orbital_at_metres` per +/// [`derive_window_cell`]'s dispatch), never a coarser-cell interpolation. /// /// **Row-chunked `par_iter` (T-1151):** each cell is a pure function of its /// own position (D-227), so rows can derive in parallel with no shared @@ -689,10 +1020,13 @@ fn step_m_for_granularity(granularity: u32) -> f64 { /// task-dispatch overhead against the ~1.2 µs/cell derive cost (design doc /// §7: naive per-cell parallelization risks the dispatch overhead itself /// costing more than the work) — one Rayon task per row means `side` tasks of -/// `side` cells each, not `side²` tasks of one cell each. [`build_district_window_layer_serial`] -/// is kept alongside this as the golden-comparison baseline (T-1151 -/// acceptance: bit-identical serial vs. parallel output, exact row-major -/// array ordering preserved either way). +/// `side` cells each, not `side²` tasks of one cell each. This applies +/// unchanged to the `Region` rung (T-1152 step 4: "progressive capped-density +/// tiling on the SAME carrier ... No new message shape") — the SAME row-chunked +/// parallel loop, cache, and coalescing machinery serve every rung. +/// [`build_district_window_layer_serial`] is kept alongside this as the +/// golden-comparison baseline (T-1151 acceptance: bit-identical serial vs. +/// parallel output, exact row-major array ordering preserved either way). #[allow(clippy::too_many_arguments)] pub fn build_district_window_layer( seed: SeedChain, @@ -702,14 +1036,13 @@ pub fn build_district_window_layer( center: DistrictPos, n: u32, climate: &crate::atlas::district_profile::ClimateConstants, - granularity: u32, + granularity: WindowGranularity, min_wl_m: u32, ) -> DistrictWindowLayer { use rayon::prelude::*; - let side = (n * granularity.max(1)) as i32; + let side = granularity.cell_grid_side(n); let half = side / 2; - let step_m = step_m_for_granularity(granularity); let min_wavelength_m = min_wl_m as f64; let center_world_m = center_to_world_m(center); let cells = (side * side) as usize; @@ -738,7 +1071,7 @@ pub fn build_district_window_layer( climate, center_world_m, half, - step_m, + granularity, min_wavelength_m, row, col, @@ -765,7 +1098,8 @@ pub fn build_district_window_layer( DistrictWindowLayer { center, n, - granularity, + granularity: granularity.key_u32(), + granularity_v2: granularity, min_wl_m, morphology, elev_q, @@ -789,12 +1123,11 @@ fn build_district_window_layer_serial( center: DistrictPos, n: u32, climate: &crate::atlas::district_profile::ClimateConstants, - granularity: u32, + granularity: WindowGranularity, min_wl_m: u32, ) -> DistrictWindowLayer { - let side = (n * granularity.max(1)) as i32; + let side = granularity.cell_grid_side(n); let half = side / 2; - let step_m = step_m_for_granularity(granularity); let min_wavelength_m = min_wl_m as f64; let center_world_m = center_to_world_m(center); let cells = (side * side) as usize; @@ -815,7 +1148,7 @@ fn build_district_window_layer_serial( climate, center_world_m, half, - step_m, + granularity, min_wavelength_m, row, col, @@ -837,7 +1170,8 @@ fn build_district_window_layer_serial( DistrictWindowLayer { center, n, - granularity, + granularity: granularity.key_u32(), + granularity_v2: granularity, min_wl_m, morphology, elev_q, @@ -1253,10 +1587,13 @@ fn normalize_window_center(params: &BodyParams, center: DistrictPos) -> District /// processed the completion (the existing D-225 poll-and-recheck-cache /// pattern every other layer already uses, not a push). /// -/// `window_n` is clamped to `[1, DISTRICT_WINDOW_MAX_N]` AND the +/// `window_n` is clamped to `[1, DISTRICT_WINDOW_MAX_N]` (or +/// `DISTRICT_WINDOW_MAX_N_REGION` at `Region` granularity, T-1152) AND the /// granularity-aware `WIRE_CAP_CELLS` ceiling here — the ONE place that clamp -/// is applied; nothing downstream re-checks the wire value. `window_granularity` -/// is resolved via [`resolve_window_granularity`] at the same boundary (T-1150). +/// is applied; nothing downstream re-checks the wire value. The granularity +/// itself is resolved via [`resolve_window_granularity_v2`] at the same +/// boundary (T-1150, widened T-1152 — see that function's doc for the +/// legacy-`u32`-vs-`window_granularity_v2` precedence rule). #[allow(clippy::too_many_arguments)] fn serve_district_window( req: &AtlasLayerRequest, @@ -1268,8 +1605,8 @@ fn serve_district_window( conn_id: ConnectionId, ) -> Option { let raw_center = req.window_center?; - let granularity = resolve_window_granularity(req.window_granularity); - let n = clamp_window_n(req.window_n, granularity); + let granularity = resolve_window_granularity_v2(req); + let n = clamp_window_n_v2(req.window_n, granularity); // T-1150 design doc §5: quantize BEFORE either the cache key or the // DeriveWindow work item sees it — the raw wire value never reaches // either (same discipline as window_n's clamp above and @@ -1731,13 +2068,14 @@ mod tests { (10, -5), n, &climate, - WINDOW_GRANULARITY_DISTRICT, + WindowGranularity::District, 0, ); assert_eq!(layer.center, (10, -5)); assert_eq!(layer.n, n); assert_eq!(layer.granularity, WINDOW_GRANULARITY_DISTRICT); + assert_eq!(layer.granularity_v2, WindowGranularity::District); assert_eq!(layer.min_wl_m, 0); let cells = (n * n) as usize; assert_eq!(layer.morphology.len(), cells); @@ -1785,7 +2123,7 @@ mod tests { (0, 0), 1, &climate, - WINDOW_GRANULARITY_DISTRICT, + WindowGranularity::District, 0, ); assert_eq!(layer.n, 1); @@ -1819,7 +2157,7 @@ mod tests { (3, -2), n, &climate, - WINDOW_GRANULARITY_DISTRICT, + WindowGranularity::District, 0, ); let second = build_district_window_layer( @@ -1830,7 +2168,7 @@ mod tests { (3, -2), n, &climate, - WINDOW_GRANULARITY_DISTRICT, + WindowGranularity::District, 0, ); assert_eq!( @@ -1863,7 +2201,7 @@ mod tests { center, n, &climate, - WINDOW_GRANULARITY_DISTRICT, + WindowGranularity::District, 0, ); let serial = build_district_window_layer_serial( @@ -1874,7 +2212,7 @@ mod tests { center, n, &climate, - WINDOW_GRANULARITY_DISTRICT, + WindowGranularity::District, 0, ); assert_eq!( @@ -1942,7 +2280,7 @@ mod tests { center, n, &climate, - WINDOW_GRANULARITY_DISTRICT, + WindowGranularity::District, 0, ); let window_from_pass2 = build_district_window_layer( @@ -1953,7 +2291,7 @@ mod tests { center, n, &climate, - WINDOW_GRANULARITY_DISTRICT, + WindowGranularity::District, 0, ); assert_eq!( @@ -1972,13 +2310,14 @@ mod tests { #[test] fn district_window_cache_insert_get_and_evict() { let mut cache = DistrictWindowCache::new(2); - let key_a: DistrictWindowKey = ("Alpha".into(), (0, 0), 4, WINDOW_GRANULARITY_DISTRICT, 0); - let key_b: DistrictWindowKey = ("Beta".into(), (1, 1), 4, WINDOW_GRANULARITY_DISTRICT, 0); - let key_c: DistrictWindowKey = ("Gamma".into(), (2, 2), 4, WINDOW_GRANULARITY_DISTRICT, 0); + let key_a: DistrictWindowKey = ("Alpha".into(), (0, 0), 4, WindowGranularity::District, 0); + let key_b: DistrictWindowKey = ("Beta".into(), (1, 1), 4, WindowGranularity::District, 0); + let key_c: DistrictWindowKey = ("Gamma".into(), (2, 2), 4, WindowGranularity::District, 0); let mk = |center, n| DistrictWindowLayer { center, n, granularity: WINDOW_GRANULARITY_DISTRICT, + granularity_v2: WindowGranularity::District, min_wl_m: 0, morphology: vec![0; (n * n) as usize], elev_q: vec![0; (n * n) as usize], @@ -2026,6 +2365,7 @@ mod tests { window_center: Some((0, 0)), window_n: DISTRICT_WINDOW_MAX_N * 10, // wildly over the wire — must clamp, not trust window_granularity: 0, + window_granularity_v2: None, window_min_wl_m: 0, }; @@ -2086,6 +2426,7 @@ mod tests { window_center: Some((0, 0)), window_n: 32, window_granularity: WINDOW_GRANULARITY_QUARTER, + window_granularity_v2: None, window_min_wl_m: 0, }; @@ -2261,6 +2602,7 @@ mod tests { window_center: Some((10, -5)), window_n: 4, window_granularity: WINDOW_GRANULARITY_DISTRICT, + window_granularity_v2: None, window_min_wl_m: 4_000, }; let req_b = AtlasLayerRequest { @@ -2269,6 +2611,7 @@ mod tests { window_center: Some((10, -5)), window_n: 4, window_granularity: WINDOW_GRANULARITY_DISTRICT, + window_granularity_v2: None, window_min_wl_m: 4_300, }; @@ -2307,7 +2650,7 @@ mod tests { body_id, layer.center, layer.n, - layer.granularity, + layer.granularity_v2, layer.min_wl_m, ), *layer, @@ -2486,6 +2829,7 @@ mod tests { window_center: Some((12276, 3021)), window_n: 4, window_granularity: 0, + window_granularity_v2: None, window_min_wl_m: 0, }; let resp1 = handle_atlas_request( @@ -2517,7 +2861,7 @@ mod tests { body_id, layer.center, layer.n, - layer.granularity, + layer.granularity_v2, layer.min_wl_m, ), *layer, @@ -2542,6 +2886,7 @@ mod tests { window_center: Some((4, 383)), // the hand-computed canonical twin window_n: 4, window_granularity: 0, + window_granularity_v2: None, window_min_wl_m: 0, }; let resp2 = handle_atlas_request( @@ -2624,6 +2969,7 @@ mod tests { window_center: center, window_n: n, window_granularity: WINDOW_GRANULARITY_DISTRICT, + window_granularity_v2: None, window_min_wl_m: 0, }; let quarter_req = AtlasLayerRequest { @@ -2632,6 +2978,7 @@ mod tests { window_center: center, window_n: n, window_granularity: WINDOW_GRANULARITY_QUARTER, + window_granularity_v2: None, window_min_wl_m: 0, }; @@ -2671,7 +3018,7 @@ mod tests { body_id, layer.center, layer.n, - layer.granularity, + layer.granularity_v2, layer.min_wl_m, ), *layer, @@ -2723,6 +3070,8 @@ mod tests { assert_eq!(district_layer.granularity, WINDOW_GRANULARITY_DISTRICT); assert_eq!(quarter_layer.granularity, WINDOW_GRANULARITY_QUARTER); + assert_eq!(district_layer.granularity_v2, WindowGranularity::District); + assert_eq!(quarter_layer.granularity_v2, WindowGranularity::Quarter); // n echoes the DISTRICT extent unchanged at both granularities // (design doc §2: "the window's n stays the DISTRICT extent"). assert_eq!(district_layer.n, n); @@ -2749,6 +3098,227 @@ mod tests { ); } + /// **MANDATORY aliasing regression for the T-1152 coarser rung** — the + /// SAME discipline `granularity_4_and_granularity_1_requests_produce_distinct_cache_entries` + /// established for finer-than-district rungs, extended to `Region` + /// (T-1150 design doc §3's aliasing risk, generalized by the T-1152 + /// wire-contract note: "mirror the T-1150 aliasing tests for at least + /// one coarser rung"). A `Region`-granularity request and a + /// `District`-granularity request at the IDENTICAL `(body, center, n)` + /// must produce DISTINCT cache entries, distinct payload shapes, and the + /// clamp/echo contract must hold at the coarse rung too (PR #191 C1 + /// lesson generalized: the client's mirror of `clamp_window_n_v2` MUST + /// be derivable from the same constants this test exercises). + #[test] + fn region_and_district_requests_produce_distinct_cache_entries() { + let mut cache = BodyWorldStateCache::new(CACHE_CAPACITY); + let mut window_cache = DistrictWindowCache::new(DISTRICT_WINDOW_CACHE_CAPACITY); + let (_db, resolver, params_reader, _root) = + resolver_and_params_reader_with_radius("OrbitalAliasBody", 6371.0); + // See the district/quarter alias test above for the thread-count + // rationale (AnalyzeBody + two DeriveWindow items must all be able to + // dispatch concurrently within one drain_completions() call). + let queue = GenerationQueue::with_threads(3); + + let center = Some((10, -5)); + let n = 4u32; + + let district_req = AtlasLayerRequest { + body_id: "OrbitalAliasBody".to_string(), + up_to: CascadeLayer::Topography, + window_center: center, + window_n: n, + window_granularity: WINDOW_GRANULARITY_DISTRICT, + window_granularity_v2: None, + window_min_wl_m: 0, + }; + let region_req = AtlasLayerRequest { + body_id: "OrbitalAliasBody".to_string(), + up_to: CascadeLayer::Topography, + window_center: center, + window_n: n, + // Legacy field is irrelevant here — window_granularity_v2 takes + // precedence per resolve_window_granularity_v2's documented rule. + window_granularity: 0, + window_granularity_v2: Some(WindowGranularity::Region), + window_min_wl_m: 0, + }; + + handle_atlas_request( + &district_req, + &mut cache, + &mut window_cache, + &queue, + &resolver, + None, + Some(¶ms_reader), + 42, + 1, + test_conn_id(), + ); + handle_atlas_request( + ®ion_req, + &mut cache, + &mut window_cache, + &queue, + &resolver, + None, + Some(¶ms_reader), + 42, + 1, + test_conn_id(), + ); + + std::thread::sleep(Duration::from_millis(300)); + let completions = queue.drain_completions(); + for c in completions { + if let GenCompletion::WindowDerived { body_id, layer } = c { + if body_id == "OrbitalAliasBody" { + window_cache.insert( + ( + body_id, + layer.center, + layer.n, + layer.granularity_v2, + layer.min_wl_m, + ), + *layer, + ); + } + } + } + + assert_eq!( + window_cache.len(), + 2, + "district and region requests at the SAME (body, center, n) must occupy \ + TWO distinct cache entries, not alias onto one" + ); + + let district_resp = handle_atlas_request( + &district_req, + &mut cache, + &mut window_cache, + &queue, + &resolver, + None, + Some(¶ms_reader), + 42, + 2, + test_conn_id(), + ); + let region_resp = handle_atlas_request( + ®ion_req, + &mut cache, + &mut window_cache, + &queue, + &resolver, + None, + Some(¶ms_reader), + 42, + 2, + test_conn_id(), + ); + + let district_layer = district_resp + .district_window + .expect("district request must hit its own cached entry"); + let region_layer = region_resp + .district_window + .expect("region request must hit its own cached entry"); + + assert_eq!(district_layer.granularity_v2, WindowGranularity::District); + assert_eq!(region_layer.granularity_v2, WindowGranularity::Region); + // The legacy u32 echo must NEVER collide with a real multiplier — + // WINDOW_GRANULARITY_REGION_KEY is the reserved key-space tag (see + // that constant's doc), distinct from both WINDOW_GRANULARITY_DISTRICT + // (1) and WINDOW_GRANULARITY_QUARTER (4). + assert_eq!(region_layer.granularity, WINDOW_GRANULARITY_REGION_KEY); + assert_ne!(region_layer.granularity, district_layer.granularity); + + // n echoes the DISTRICT extent unchanged (design doc §2), same as + // every other rung — the derived CELL GRID is what differs. + assert_eq!(district_layer.n, n); + assert_eq!(region_layer.n, n); + assert_eq!(district_layer.morphology.len(), (n * n) as usize); + // n=4 districts is far under one region's 100-district side, so + // cell_grid_side rounds down to the minimum 1x1 region cell — + // exercising the "region divides, doesn't multiply" cell-count + // relationship WindowGranularity::cell_grid_side documents. + assert_eq!( + region_layer.morphology.len(), + 1, + "n=4 districts is far under one region's 100-district side; \ + cell_grid_side must round down to a single region cell, not zero \ + and not a district-sized grid" + ); + } + + /// The clamp/echo contract at the coarse (Region) rung (T-1152, the PR + /// #191 C1 lesson generalized): a region request whose `n` would derive + /// MORE than `sqrt(WIRE_CAP_CELLS)` region cells across must clamp `n` + /// down and echo the CLAMPED value — Stig's client-side mirror of + /// `clamp_window_n_v2` must be derivable from + /// `DISTRICT_WINDOW_MAX_N_REGION`/`WIRE_CAP_CELLS`/`DISTRICTS_PER_REGION` + /// alone, exactly as `_clamp_window_n_mirror()` already mirrors + /// `clamp_window_n` for the finer rungs. + #[test] + fn region_request_oversized_n_clamps_and_echoes_clamped_n() { + let mut cache = BodyWorldStateCache::new(CACHE_CAPACITY); + let mut window_cache = DistrictWindowCache::new(DISTRICT_WINDOW_CACHE_CAPACITY); + let (_db, resolver, params_reader, _root) = + resolver_and_params_reader_with_radius("OrbitalCapBody", 6371.0); + let queue = GenerationQueue::with_threads(1); + + // Far over DISTRICT_WINDOW_MAX_N_REGION (6,400) — must clamp, never + // trust the wire (same discipline as the district-rung oversized-n test). + let oversized_region_req = AtlasLayerRequest { + body_id: "OrbitalCapBody".to_string(), + up_to: CascadeLayer::Topography, + window_center: Some((0, 0)), + window_n: DISTRICT_WINDOW_MAX_N_REGION * 10, + window_granularity: 0, + window_granularity_v2: Some(WindowGranularity::Region), + window_min_wl_m: 0, + }; + + let resp = handle_atlas_request( + &oversized_region_req, + &mut cache, + &mut window_cache, + &queue, + &resolver, + None, + Some(¶ms_reader), + 42, + 1, + test_conn_id(), + ); + assert!(resp.district_window.is_none(), "first request — cache miss"); + + std::thread::sleep(Duration::from_millis(300)); + let completions = queue.drain_completions(); + let window_completion = completions.into_iter().find_map(|c| { + if let GenCompletion::WindowDerived { body_id, layer } = c { + if body_id == "OrbitalCapBody" { + return Some(layer); + } + } + None + }); + let layer = window_completion.expect("DeriveWindow must complete for OrbitalCapBody"); + assert_eq!(layer.granularity_v2, WindowGranularity::Region); + assert!( + layer.n <= DISTRICT_WINDOW_MAX_N_REGION, + "echoed n must be clamped to DISTRICT_WINDOW_MAX_N_REGION, not the raw oversized value" + ); + let side = layer.granularity_v2.cell_grid_side(layer.n); + assert!( + (side as u32) * (side as u32) <= WIRE_CAP_CELLS, + "clamped cell count must never exceed WIRE_CAP_CELLS at Region granularity either" + ); + } + /// The echoed `center` on `DistrictWindowLayer` is the NORMALIZED value, /// not the raw wire value — the client's D-227 staleness guard (D-226 /// T-1124 amendment §2) must see what was ACTUALLY derived, so it can @@ -2768,6 +3338,7 @@ mod tests { window_center: Some((12276, 3021)), // raw, out-of-range window_n: 4, window_granularity: 0, + window_granularity_v2: None, window_min_wl_m: 0, }; handle_atlas_request( @@ -2848,6 +3419,7 @@ mod tests { center: (10, -5), n: 2, granularity: WINDOW_GRANULARITY_DISTRICT, + granularity_v2: WindowGranularity::District, min_wl_m: 0, morphology: vec![0, 8, 14, 16], elev_q: vec![0, 45, 98, 60], @@ -3529,6 +4101,7 @@ mod tests { window_center: None, window_n: 0, window_granularity: 0, + window_granularity_v2: None, window_min_wl_m: 0, } } diff --git a/server/src/atlas/plugin.rs b/server/src/atlas/plugin.rs index eca7a6ff7..60a285a99 100644 --- a/server/src/atlas/plugin.rs +++ b/server/src/atlas/plugin.rs @@ -418,7 +418,7 @@ fn drain_generation_completions( body_id, layer.center, layer.n, - layer.granularity, + layer.granularity_v2, layer.min_wl_m, ), *layer, @@ -914,6 +914,7 @@ mod tests { window_center: None, window_n: 0, window_granularity: 0, + window_granularity_v2: None, window_min_wl_m: 0, }, )])); diff --git a/server/src/bridge/mod.rs b/server/src/bridge/mod.rs index be787b510..21c6b5590 100644 --- a/server/src/bridge/mod.rs +++ b/server/src/bridge/mod.rs @@ -1202,6 +1202,7 @@ mod inbound_tests { window_center: None, window_n: 0, window_granularity: 0, + window_granularity_v2: None, window_min_wl_m: 0, }; let frame = rmp_serde::to_vec_named(&req).unwrap(); @@ -1249,6 +1250,7 @@ mod inbound_tests { window_center: None, window_n: 0, window_granularity: 0, + window_granularity_v2: None, window_min_wl_m: 0, }) .unwrap(); diff --git a/server/tests/bridge_tcp.rs b/server/tests/bridge_tcp.rs index ff213b725..957802a26 100644 --- a/server/tests/bridge_tcp.rs +++ b/server/tests/bridge_tcp.rs @@ -372,6 +372,7 @@ fn single_tick_drains_all_ready_inbound_frames() { window_center: None, window_n: 0, window_granularity: 0, + window_granularity_v2: None, window_min_wl_m: 0, }; let payload = rmp_serde::to_vec_named(&req).expect("failed to serialize"); diff --git a/server/tests/gen_fixtures.rs b/server/tests/gen_fixtures.rs index c43192109..5b0f48cda 100644 --- a/server/tests/gen_fixtures.rs +++ b/server/tests/gen_fixtures.rs @@ -6,7 +6,7 @@ use settled_reach_server::atlas::layer1::Layer1Output; use settled_reach_server::atlas::layer_proxy::{ AtlasLayerResponse, AtlasLayerStatus, DistrictWindowLayer, QuarterFootprintEntry, QuarterFootprintLayer, RegionGridLayer, RoadGraphEdge, RoadGraphLayer, RoadGraphNode, - SettlementEntry, SettlementLayer, SettlementSizeClass, REGION_TEMP_NONE_DC, + SettlementEntry, SettlementLayer, SettlementSizeClass, WindowGranularity, REGION_TEMP_NONE_DC, WINDOW_GRANULARITY_DISTRICT, }; use settled_reach_server::atlas::region_profile::{SeasonPhase, WeatherState}; @@ -727,6 +727,7 @@ fn generate_atlas_layer_response_fixtures() { center: (10, -5), n: 2, granularity: WINDOW_GRANULARITY_DISTRICT, + granularity_v2: WindowGranularity::District, min_wl_m: 0, morphology: vec![0, 8, 14, 16], // OpenOcean, AlluvialPlain, Alpine, Wetland elev_q: vec![0, 45, 98, 60], diff --git a/server/tests/zoom_ladder_bench.rs b/server/tests/zoom_ladder_bench.rs index d917774e9..45fae8b76 100644 --- a/server/tests/zoom_ladder_bench.rs +++ b/server/tests/zoom_ladder_bench.rs @@ -17,11 +17,14 @@ use std::time::Instant; use settled_reach_server::atlas::district_profile::{ - derive_at_metres, BodyParams, ClimateConstants, + derive_at_metres, derive_orbital_at_metres, BodyParams, ClimateConstants, }; use settled_reach_server::atlas::drainage; use settled_reach_server::atlas::features::TerrainAnalysis; use settled_reach_server::atlas::heightmap::BodyHeightmap; +use settled_reach_server::atlas::layer_proxy::{ + build_district_window_layer, WindowGranularity, DISTRICT_WINDOW_MAX_N_REGION, WIRE_CAP_CELLS, +}; use settled_reach_server::atlas::scale; use settled_reach_server::seed::{SeedChain, SeedDomain}; @@ -151,3 +154,187 @@ fn bench_derive_at_metres_district_and_quarter_spacing() { println!(); } + +/// Time `n_cells` sequential `derive_orbital_at_metres` calls — the +/// region-baseline-blend-only path (T-1152, design doc §2/§4/§9 R1), no +/// `invent_primitives` call at any point. Mirrors `time_derive_sweep`'s shape +/// exactly so the two numbers are directly comparable. +fn time_orbital_sweep( + seed: SeedChain, + body_id: &str, + params: &BodyParams, + ta: &TerrainAnalysis, + climate: &ClimateConstants, + grid_side: u32, + step_m: f64, +) -> (std::time::Duration, f64) { + let n_cells = (grid_side * grid_side) as u64; + let t0 = Instant::now(); + for row in 0..grid_side { + for col in 0..grid_side { + let wx = col as f64 * step_m; + let wy = row as f64 * step_m; + let prof = derive_orbital_at_metres(seed, body_id, params, ta, wx, wy, climate); + std::hint::black_box(prof.elev_q); + } + } + let elapsed = t0.elapsed(); + let per_cell_ns = elapsed.as_secs_f64() * 1e9 / n_cells as f64; + (elapsed, per_cell_ns) +} + +/// T-1152 / design doc §9 R1: "MEASURE FIRST" — per-cell cost of the +/// orbital-mode region-baseline-blend-only path (no `invent_primitives`) at +/// coarse (region-scale, ≥205 km) spacings, plus a realistic full-orbital-frame +/// extrapolation (1600×900 canvas). This is the number the design doc's §4/§7 +/// planetary-rung cost story rested on as an UNMEASURED extrapolation — +/// this test replaces "extrapolated from the uncut per-cell rate" with an +/// actually-measured orbital-path rate. +#[test] +#[ignore] +fn bench_derive_orbital_at_metres_region_spacing() { + let hm = bench_hm(); + let ta = bench_ta(&hm); + let params = bench_params(); + let climate = ClimateConstants::default(); + let seed = SeedChain::root(99).derive(SeedDomain::Body, 1); + let grid_side = 64u32; // 4,096 cells/sweep, same shape as the district/quarter sweeps above + + println!("\n=== T-1152 orbital-rung derive_orbital_at_metres benchmark ==="); + println!( + "grid: {grid_side}x{grid_side} = {} cells/sweep\n", + grid_side * grid_side + ); + + let region_m = scale::REGION_M as f64; + + // Region spacing (204,800 m) — the coarsest named rung short of the + // planet-wide elastic seam (D-243). + let (elapsed, per_cell_ns) = + time_orbital_sweep(seed, "bench", ¶ms, &ta, &climate, grid_side, region_m); + println!( + "orbital, region spacing (204.8km): {:>8.2} ms total, {:>7.1} ns/cell ({:.3} µs/cell)", + elapsed.as_secs_f64() * 1000.0, + per_cell_ns, + per_cell_ns / 1000.0 + ); + + // Same spacing, for direct comparison: the FULL derive_at_metres path + // (invent_primitives included) at the SAME region spacing — quantifies + // exactly what skipping invention buys, at the spacing where it matters. + let (elapsed_full, per_cell_ns_full) = time_derive_sweep( + seed, "bench", ¶ms, &ta, &climate, grid_side, region_m, 0.0, + ); + println!( + "district-mode (full derive_at_metres) at region spacing: {:>8.2} ms total, {:>7.1} ns/cell ({:.3} µs/cell)", + elapsed_full.as_secs_f64() * 1000.0, + per_cell_ns_full, + per_cell_ns_full / 1000.0 + ); + println!( + "orbital speedup vs. full derive at the same spacing: {:.2}x\n", + per_cell_ns_full / per_cell_ns + ); + + // Realistic full-orbital-frame estimate: a 1600x900 canvas at + // ~1-2 px/cell equivalents (design doc §4's worked example resolution + // class). Single-thread extrapolation from the MEASURED per-cell rate — + // labelled as an extrapolation, not claimed as independently measured at + // full canvas size (the parallel/chunked throughput is a SEPARATE + // measurement, T-1151's row-chunked par_iter, already landed and reused + // unchanged by the orbital rung's serving path — see the ticket report). + for (label, px_per_cell) in [("1 px/cell", 1u32), ("2 px/cell", 2u32)] { + let cols = 1600 / px_per_cell; + let rows = 900 / px_per_cell; + let cells = (cols as u64) * (rows as u64); + let est_ms = cells as f64 * per_cell_ns / 1e6; + println!( + "full-canvas 1600x900 @ {label} ({cols}x{rows} = {cells} cells): \ + {est_ms:.1} ms single-thread (EXTRAPOLATED from the measured per-cell rate above)" + ); + } + + println!(); +} + +/// **T-1152 R1 — the number that actually governs interactive latency**, as +/// opposed to the full-canvas single-shot extrapolation above (which the +/// design doc's own carrier ruling makes moot — Jeroen's ruling is +/// progressive capped-density TILING, never a whole-canvas one-shot derive). +/// This measures a single served Region-granularity window tile through the +/// REAL production path (`build_district_window_layer`, including its +/// row-chunked `par_iter`, T-1151) at the wire-size cap — the same function +/// `serve_district_window`/`run_work_item`'s `DeriveWindow` arm calls, not a +/// hand-rolled sweep. This is the measured (not extrapolated) parallel +/// number the design doc's §7 flagged as missing ("no chunked-par_iter +/// benchmark has been run"). +#[test] +#[ignore] +fn bench_served_region_window_tile_at_wire_cap() { + let hm = bench_hm(); + let ta = bench_ta(&hm); + let params = bench_params(); + let climate = ClimateConstants::default(); + let seed = SeedChain::root(99).derive(SeedDomain::Body, 1); + + println!("\n=== T-1152 served Region-window-tile benchmark (real production path) ==="); + + // The largest n the server will ever actually derive at Region + // granularity is DISTRICT_WINDOW_MAX_N_REGION, clamped further by + // clamp_window_n_v2 to the WIRE_CAP_CELLS ceiling — use the SAME + // capped n a real client's oversized request would resolve to. + let n = DISTRICT_WINDOW_MAX_N_REGION; + + // Warm-up call (first call on a body pays no extra cost here since ta is + // already built — this just avoids counting one-time allocator warm-up + // noise in the timed sample). + let _ = build_district_window_layer( + seed, + "bench", + ¶ms, + &ta, + (0, 0), + n, + &climate, + WindowGranularity::Region, + 0, + ); + + let iterations = 20; + let t0 = Instant::now(); + let mut last_side = 0usize; + for _ in 0..iterations { + let layer = build_district_window_layer( + seed, + "bench", + ¶ms, + &ta, + (0, 0), + n, + &climate, + WindowGranularity::Region, + 0, + ); + last_side = (layer.morphology.len() as f64).sqrt().round() as usize; + std::hint::black_box(layer.elev_q.len()); + } + let elapsed = t0.elapsed(); + let per_call_ms = elapsed.as_secs_f64() * 1000.0 / iterations as f64; + + println!( + "n={n} (DISTRICT_WINDOW_MAX_N_REGION), derived {last_side}x{last_side} region cells \ + ({} cells, WIRE_CAP_CELLS={WIRE_CAP_CELLS}):", + last_side * last_side + ); + println!( + " {iterations} calls, {:.2} ms total, {per_call_ms:.3} ms/call \ + (row-chunked par_iter, {} Rayon threads available)", + elapsed.as_secs_f64() * 1000.0, + std::thread::available_parallelism() + .map(|n| n.get()) + .unwrap_or(0) + ); + println!( + " compare: shipped district n=64 cap measures ~5 ms/call (design doc §7, MEASURED)\n" + ); +}