feat(simulation): T-1137 windowed district layer — queue-served, coalesced, determinism-tested (D-226 T-1124 SS1-SS4)

AtlasLayerRequest gains window_center/window_n (serde-default, absent
= whole-body, wire back-compat; demux untouched — up_to stays the
discriminator). DistrictWindowLayer echoes center/n + six parallel
arrays (morphology, elev_q, temp_dc i16 with the region sentinel,
moisture_q, vegetation incl Marine=6, glaciation).

Serving per the amendment's binding model: NEVER inline —
GenWorkItem::DeriveWindow rides the Rayon queue, completion drain
caches into DistrictWindowCache (bounded FIFO 256; no staleness by
D-227 purity, capacity bound only), serve_district_window polls the
cache and returns Pending-shaped None until derived. Per-connection
coalescing: submit_window supersedes a still-pending item for the
same (ConnectionId, body) — the surviving item is the newer one,
proven by dedicated tests.

TerrainAnalysis decision (option b, numbers in ticket/PR): re-derive
via run_layer1 in the DeriveWindow branch rather than caching ~1.5MB
x 50 LRU slots (~100MB permanent, the exact D-203 bloat T-1044's own
text guarded against); ~45ms one-time on the Rayon path, invisible to
the tick thread. T-1044 confirmed within-cascade-only (cascade.rs:358
still drops the analysis before BodyWorldState) — the fork was open.

Window derive loop promoted from aliveness_probe::render_window_panels;
determinism promoted from probe-only proof to a real test (two passes
byte-identical). New fixture atlas_response_ready_with_window
exercises all six arrays incl. the airless sentinel and Marine; three
existing fixtures gain district_window: None. Server clamps window_n
to 1..=DISTRICT_WINDOW_MAX_N=64 (never trust the wire).

1774/1774 lib + 19/19 bridge_tcp green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-21 12:13:59 +02:00
co-authored by Claude Fable 5
parent f6db47f4a1
commit 172ce124c8
10 changed files with 1062 additions and 20 deletions
@@ -1 +1 @@
ˆ§body_id¥ghost¦status¨NotFound¦layer1À­district_gridÀªroad_graphÀ«settlementsÀ«region_gridÀ²quarter_footprintsÀ
§body_id¥ghost¦status¨NotFound¦layer1À­district_gridÀªroad_graphÀ«settlementsÀ«region_gridÀ¯district_windowÀ²quarter_footprintsÀ
@@ -1 +1 @@
êbody_id二J1c存tatus判ending奸ayer1嶺district_grid尷road_graph屨settlements屨region_grid徽quarter_footprints
body_id二J1c存tatus判ending奸ayer1嶺district_grid尷road_graph屨settlements屨region_grid嶸district_window徽quarter_footprints
Binary file not shown.
+302 -3
View File
@@ -14,6 +14,7 @@
//! - `AnalyzeBody`: D8 drainage + attractor extraction for a body.
//! - `GenerateSkeleton`: Phase 1 QuarterSkeleton for a city.
//! - `FillChunk`: Phase 2 chunk fill for a pre-loaded quarter.
//! - `DeriveWindow`: District-resolution window derive (D-226 T-1124, T-1137).
//!
//! Completion events are delivered to the main thread via
//! `GenerationQueue::drain_completions()`, called once per tick from a Bevy
@@ -30,11 +31,13 @@ use crossbeam_channel::{Receiver, Sender};
use crate::atlas::attractor_matching::CityRecord;
use crate::atlas::body_world_state::BodyWorldState;
use crate::atlas::cascade::{run_cascade_from_heightmap, CascadeLayer};
use crate::atlas::district_profile::BodyParams;
use crate::atlas::district_profile::{BodyParams, ClimateConstants, DistrictPos};
use crate::atlas::heightmap::{load_heightmap_png, GRID_H, GRID_W};
use crate::atlas::layer_proxy::{build_district_window_layer, DistrictWindowLayer};
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;
use crate::bridge::ConnectionId;
use crate::seed::SeedChain;
use crate::simulation::generator::{BuildingPropertyTag, CityGenerationContext, QuarterWorldState};
@@ -148,12 +151,71 @@ pub enum GenWorkItem {
/// this variant needs no `Box` to stay clippy `large_enum_variant`-clean.
block_tags: Vec<BuildingPropertyTag>,
},
/// Derive a district-resolution window (D-226 T-1124 amendment, T-1137).
///
/// **Binding serving model:** window derives ride this SAME Rayon queue as
/// every other expensive atlas path — never inline on the `PreInput` drain
/// (the amendment's §1 is explicit: a window derive at n=32/64 is
/// ~729 ms, which would blow the "cheap channel drain" contract
/// `drain_generation_completions` documents).
///
/// **TerrainAnalysis availability (T-1137 binding decision, with numbers):**
/// `BodyWorldState` does NOT retain `TerrainAnalysis` after cascade
/// completion (T-1044 scoped its transient-carry fix to *within-cascade*
/// reuse only — `cascade.rs` drops it once `DistrictProfile`+`RoadGraph`
/// finish; see the doc on `CascadeSnapshot::terrain_analysis`). Caching it
/// alongside every `BodyWorldStateCache` entry would cost ~2 MB × 50-body
/// capacity ≈ 100 MB of PERMANENT resident cost, paid by every cached body
/// whether or not a window is ever requested for it — the exact D-203
/// budget concern T-1044's own ticket text guarded against. Re-deriving via
/// `run_layer1` per window-serving cache miss costs ~45 ms ONE-TIME, paid
/// only when a window is actually requested, and — because this work item
/// already runs off the tick thread on the Rayon queue — that cost is
/// invisible to the main thread; it is the same order of magnitude as one
/// window derive itself, not a multiplier on it. So: re-derive
/// (`heightmap_path`/`sea_level` below), matching `aliveness_probe`'s
/// existing `--render` workaround, NOT a `TerrainAnalysis` field cached on
/// `BodyWorldState`.
DeriveWindow {
body_id: String,
/// Coalescing/routing key (D-226 T-1124 amendment §1 "recommended"
/// per-connection coalescing) — NOT used by `run_work_item` itself
/// (the derive is connection-agnostic), only by
/// `GenerationQueue::submit_window` to decide which still-pending item
/// a new one for the same connection+body supersedes.
conn_id: ConnectionId,
/// Mod-resolved source heightmap PNG (mirrors `AnalyzeBody`).
heightmap_path: PathBuf,
sea_level: f32,
/// This body's `SeedChain` position — `derive_district`'s seed input.
body_seed: SeedChain,
/// Body physical parameters. Boxed for the same `large_enum_variant`
/// reason `AnalyzeBody.body_params` is boxed.
body_params: Box<BodyParams>,
/// Window centre + side length in districts. `n` is ALREADY clamped to
/// `[1, DISTRICT_WINDOW_MAX_N]` by the caller (`handle_atlas_request`)
/// before this item is built — never trusted from the wire again here.
center: DistrictPos,
n: u32,
},
}
impl GenWorkItem {
pub fn body_id(&self) -> Option<&str> {
if let GenWorkItem::AnalyzeBody { body_id, .. } = self {
Some(body_id)
match self {
GenWorkItem::AnalyzeBody { body_id, .. } => Some(body_id),
_ => None,
}
}
/// Coalescing key for `DeriveWindow` items only — `(connection, body)`.
/// `None` for every other variant (they don't coalesce this way).
pub fn window_supersede_key(&self) -> Option<(ConnectionId, &str)> {
if let GenWorkItem::DeriveWindow {
body_id, conn_id, ..
} = self
{
Some((*conn_id, body_id))
} else {
None
}
@@ -187,6 +249,19 @@ pub enum GenCompletion {
/// Boxed to keep `GenCompletion` variant sizes balanced.
filled: Box<FilledChunk>,
},
/// A district window finished deriving (D-226 T-1124 amendment, T-1137).
/// The main thread inserts `layer` into the `DistrictWindowCache` keyed by
/// `(body_id, layer.center, layer.n)` — NOT pushed directly into any
/// in-flight response (the window's requester re-polls per the existing
/// D-225 loop and hits the now-populated cache on its next request; see
/// `handle_atlas_request`'s window branch).
WindowDerived {
body_id: String,
/// Boxed to keep `GenCompletion` variant sizes balanced (six
/// `Vec`s — comparable to `SkeletonGenerated`/`ChunkFilled`'s own
/// boxing rationale).
layer: Box<DistrictWindowLayer>,
},
/// Work item failed — body_id or city_id for logging.
Failed { item: GenWorkItem, reason: String },
}
@@ -303,6 +378,43 @@ impl GenerationQueue {
self.dispatch_next();
}
/// Submit a `DeriveWindow` item with per-connection coalescing (D-226
/// T-1124 amendment §1, "recommended"): if a `DeriveWindow` item for the
/// SAME `(connection, body)` is still sitting in the pending queue
/// (not yet dispatched to a Rayon worker), it is replaced in place by the
/// new one — a pan-burst that queues several window requests for the same
/// connection+body before the first is dispatched collapses to one derive.
///
/// Deliberately does **not** attempt to cancel an item already dispatched
/// to a Rayon worker (no cancellation channel exists, and the amendment
/// does not require it — bounding queue buildup is the goal, not
/// interrupting in-flight compute). `item` MUST be a `DeriveWindow`
/// variant; any other variant is submitted via the plain `submit` path
/// with no coalescing (this method still accepts it for caller
/// convenience, but the supersede check is a no-op when
/// `window_supersede_key()` returns `None`).
pub fn submit_window(&self, item: GenWorkItem, priority: GenPriority) {
if let Some(key) = item.window_supersede_key() {
let key = (key.0, key.1.to_string());
let mut pending = self.pending.lock().unwrap();
pending.retain(|q| {
q.item
.window_supersede_key()
.map(|k| (k.0, k.1.to_string()) != key)
.unwrap_or(true)
});
let pos = pending
.iter()
.position(|q| q.priority > priority)
.unwrap_or(pending.len());
pending.insert(pos, QueuedWork { priority, item });
drop(pending);
self.dispatch_next();
} else {
self.submit(item, priority);
}
}
/// Drain all completed items from the channel and dispatch pending work.
///
/// Call once per tick from the main thread. Returns all completions
@@ -491,6 +603,52 @@ fn run_work_item(item: &GenWorkItem) -> GenCompletion {
filled: Box::new(filled),
}
}
GenWorkItem::DeriveWindow {
body_id,
conn_id: _, // routing-only (queue-level coalescing); the derive itself is connection-agnostic
heightmap_path,
sea_level,
body_seed,
body_params,
center,
n,
} => match load_heightmap_png(heightmap_path, body_id, *sea_level) {
Ok(hm) => {
// Same GRID_W×GRID_H downsample AnalyzeBody applies (D-202) — the
// window derive must run on the SAME working-grid resolution the
// whole-body cascade uses, or district positions between the two
// views would disagree (derive_district maps DistrictPos through
// ta.w/ta.h, T-1137 decision note).
let working = if hm.width > GRID_W || hm.height > GRID_H {
hm.downsample(GRID_W, GRID_H)
} else {
hm
};
// Re-derive TerrainAnalysis via run_layer1 (T-1137 binding
// decision — see the DeriveWindow variant doc for the numbers).
// This is the SAME workaround aliveness_probe --render already
// uses when CascadeSnapshot.terrain_analysis is None.
let (_, ta) = crate::atlas::layer1::run_layer1(&working);
let climate = ClimateConstants::default();
let layer = build_district_window_layer(
*body_seed,
body_id,
body_params,
&ta,
*center,
*n,
&climate,
);
GenCompletion::WindowDerived {
body_id: body_id.clone(),
layer: Box::new(layer),
}
}
Err(e) => GenCompletion::Failed {
item: item.clone(),
reason: format!("heightmap load failed: {e}"),
},
},
}
}
@@ -814,4 +972,145 @@ mod tests {
"a chunk containing a building must derive shell voxels"
);
}
// -------------------------------------------------------------------
// DeriveWindow / submit_window coalescing (D-226 T-1124 amendment, T-1137)
// -------------------------------------------------------------------
/// Build a `DeriveWindow` work item pointing at a tiny test heightmap,
/// mirroring `analyze()`'s fixture shape.
fn derive_window(body_id: &str, conn_id: ConnectionId, center: DistrictPos) -> GenWorkItem {
GenWorkItem::DeriveWindow {
body_id: body_id.to_string(),
conn_id,
heightmap_path: test_heightmap_path(),
sea_level: 0.3,
body_seed: SeedChain::for_body(42, body_id),
body_params: Box::new(BodyParams {
hydrosphere: Some("ocean".into()),
atmosphere: Some("breathable".into()),
planet_class: Some("temperate".into()),
body_radius_km: Some(6371.0),
..Default::default()
}),
center,
n: 4,
}
}
/// The full `DeriveWindow` → Rayon → `WindowDerived` round trip: the
/// real re-derive-via-`run_layer1` path (T-1137 binding decision) runs
/// end to end and produces a populated `DistrictWindowLayer`.
#[test]
fn derive_window_round_trip_produces_populated_layer() {
let q = make_queue();
q.submit(
derive_window("TestBody", ConnectionId(0), (2, -1)),
GenPriority::Immediate,
);
std::thread::sleep(Duration::from_millis(150));
let completions = q.drain_completions();
assert_eq!(completions.len(), 1);
let GenCompletion::WindowDerived { body_id, layer } = &completions[0] else {
panic!("expected WindowDerived, got {:?}", completions[0]);
};
assert_eq!(body_id, "TestBody");
assert_eq!(layer.center, (2, -1));
assert_eq!(layer.n, 4);
assert_eq!(layer.morphology.len(), 16);
assert_eq!(layer.elev_q.len(), 16);
assert_eq!(layer.temp_dc.len(), 16);
assert_eq!(layer.moisture_q.len(), 16);
assert_eq!(layer.vegetation.len(), 16);
assert_eq!(layer.glaciation.len(), 16);
}
/// `submit_window` coalescing (D-226 T-1124 amendment §1 "recommended"):
/// two `DeriveWindow` items for the SAME `(connection, body)` queued
/// while the pool is saturated collapse to ONE pending entry — the
/// second submission replaces the first rather than queuing alongside it.
#[test]
fn submit_window_coalesces_same_connection_and_body() {
// Single-thread pool: the first item occupies the only worker, so
// subsequent DeriveWindow submissions stay in `pending` long enough
// to inspect (mirrors `priority_ordering_respected_under_saturation`'s
// saturation trick). Uses `analyze()` (real cascade work — measurable
// latency), NOT `FillChunk` (documented "trivially fast" — it can
// complete before the next `submit_window` call even runs, which
// would make `pending_count()` observe 0 instead of 1, a real race
// this test hit before switching occupiers).
let q = GenerationQueue::with_threads(1);
q.submit(analyze("Occupier"), GenPriority::Low);
let conn = ConnectionId(7);
q.submit_window(derive_window("Coal", conn, (0, 0)), GenPriority::Immediate);
assert_eq!(
q.pending_count(),
1,
"one DeriveWindow queued behind the saturating item"
);
// A second DeriveWindow for the SAME (connection, body) supersedes
// the first — pending count stays at 1, not 2.
q.submit_window(derive_window("Coal", conn, (5, 5)), GenPriority::Immediate);
assert_eq!(
q.pending_count(),
1,
"same (connection, body) DeriveWindow must supersede, not queue alongside"
);
// Drain everything and confirm exactly one WindowDerived for "Coal",
// carrying the SECOND (superseding) center — not the first.
std::thread::sleep(Duration::from_millis(150));
let mut completions = q.drain_completions();
std::thread::sleep(Duration::from_millis(150));
completions.extend(q.drain_completions());
let window_completions: Vec<_> = completions
.iter()
.filter_map(|c| {
if let GenCompletion::WindowDerived { body_id, layer } = c {
if body_id == "Coal" {
return Some(layer);
}
}
None
})
.collect();
assert_eq!(
window_completions.len(),
1,
"exactly one WindowDerived for the coalesced body, not two"
);
assert_eq!(
window_completions[0].center,
(5, 5),
"the surviving item must be the SECOND (superseding) submission"
);
}
/// `submit_window` does NOT coalesce across different connections or
/// different bodies — only an exact `(connection, body)` match supersedes.
#[test]
fn submit_window_does_not_coalesce_different_keys() {
let q = GenerationQueue::with_threads(1);
// See `submit_window_coalesces_same_connection_and_body`'s comment on
// why the occupier must be `analyze()`, not `FillChunk`.
q.submit(analyze("Occupier2"), GenPriority::Low);
// Different connections, same body — must NOT coalesce.
q.submit_window(
derive_window("Shared", ConnectionId(1), (0, 0)),
GenPriority::Immediate,
);
q.submit_window(
derive_window("Shared", ConnectionId(2), (1, 1)),
GenPriority::Immediate,
);
assert_eq!(
q.pending_count(),
2,
"different connections requesting the same body must NOT coalesce"
);
}
}
+684 -11
View File
@@ -11,16 +11,19 @@
//! Pure handler logic; the bridge wiring (message routing) is the proxy's other
//! half. No baking — the heightmap is the only source of truth (D-225).
use bevy_ecs::prelude::Resource;
use serde::{Deserialize, Serialize};
use crate::atlas::body_params_reader::BodyParamsReader;
use crate::atlas::body_world_state::{BodyWorldState, BodyWorldStateCache, SimTick};
use crate::atlas::cascade::CascadeLayer;
use crate::atlas::city_context_reader::CityContextReader;
use crate::atlas::district_profile::DistrictPos;
use crate::atlas::gen_queue::{GenPriority, GenWorkItem, GenerationQueue};
use crate::atlas::layer1::Layer1Output;
use crate::atlas::road_graph::RoadNodeKind;
use crate::atlas::source_resolver::{BodySourceResolver, SourceResolveError};
use crate::bridge::ConnectionId;
use crate::seed::{SeedChain, SeedDomain};
use crate::simulation::generator::{AttractorType, DistrictType, MaintenanceAuthority, ZoningType};
@@ -28,7 +31,15 @@ use crate::simulation::generator::{AttractorType, DistrictType, MaintenanceAutho
/// (the loader prefers the chunk; this is only the floor).
const DEFAULT_SEA_LEVEL: f32 = 0.3;
/// A client request for a body's generation layers (D-225).
/// Hard server-side clamp on [`AtlasLayerRequest::window_n`] (D-226 T-1124
/// amendment §4, binding numbers). 64×64 districts ≈ 131 km per side — the
/// 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.
pub const DISTRICT_WINDOW_MAX_N: u32 = 64;
/// A client request for a body's generation layers (D-225), extended with an
/// optional district-resolution window query (D-226 T-1124 amendment §1, T-1137).
///
/// `up_to` is a forward-compat seam that is **not yet honored**: `run_work_item`
/// (`gen_queue.rs`) currently runs the cascade through `CascadeLayer::Region`
@@ -39,6 +50,16 @@ const DEFAULT_SEA_LEVEL: f32 = 0.3;
pub struct AtlasLayerRequest {
pub body_id: String,
pub up_to: CascadeLayer,
/// District-window centre (D-226 T-1124 amendment §1, T-1137). `None` = no
/// window requested (whole-body layers only — today's behavior, byte-unchanged
/// for every existing caller thanks to `#[serde(default)]`).
#[serde(default)]
pub window_center: Option<DistrictPos>,
/// Window side length in districts. Ignored when `window_center` is `None`.
/// Clamped server-side to `[1, DISTRICT_WINDOW_MAX_N]` — **never trusted
/// from the wire** (D-226 T-1124 amendment §4).
#[serde(default)]
pub window_n: u32,
}
/// Status of a layer response (D-225).
@@ -72,7 +93,8 @@ pub struct DistrictGridLayer {
/// A layer response: the computed `Layer1Output` + the coarse district grid
/// (D-225, T-1046) + the road-graph and settlement overlays (T-960 §1/§2) +
/// the region climate grid (T-1113) + the quarter-footprint overlay (T-1112,
/// T-1119), or a non-ready status.
/// T-1119) + the district-resolution window query (T-1124, T-1137), or a
/// non-ready status.
///
/// Growth ceiling (governance-bounded): the one-`Option`-field-per-layer
/// pattern tops out at six fields for the **dense whole-body layer family**
@@ -87,15 +109,14 @@ pub struct DistrictGridLayer {
/// The D-226 T-1124 amendment (2026-07-18) RESOLVED what carries the next
/// addition, and it is NOT this family: a **windowed viewport query** is a
/// categorically different payload (keyed on the *request* `(body, center, n)`,
/// re-fetched per pan, not a per-body snapshot). T-1124 specifies a
/// `district_window: Option<DistrictWindowLayer>` field (wiring is a follow-up
/// ticket, T-1137 — not yet added here) that rides on `AtlasLayerResponse` but
/// is explicitly OUTSIDE the whole-body family and does not count against this
/// six-field ceiling (D-226 T-1124 §2). The windowed family has its own hard
/// cap: exactly ONE windowed-query field; a second windowed query (a second
/// viewport, a windowed chunk-preview) is a dedicated response message by rule,
/// not a second `Option` here (D-226 T-1124 §2, symmetric with the
/// request-side five-shape demux ceiling in `bridge/mod.rs`).
/// re-fetched per pan, not a per-body snapshot). `district_window` (wired here,
/// T-1137) rides on `AtlasLayerResponse` but is explicitly OUTSIDE the
/// whole-body family and does not count against the six-field ceiling above
/// (D-226 T-1124 §2). The windowed family has its own hard cap: exactly ONE
/// windowed-query field; a second windowed query (a second viewport, a
/// windowed chunk-preview) is a dedicated response message by rule, not a
/// second `Option` here (D-226 T-1124 §2, symmetric with the request-side
/// five-shape demux ceiling in `bridge/mod.rs`).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AtlasLayerResponse {
pub body_id: String,
@@ -116,6 +137,12 @@ pub struct AtlasLayerResponse {
/// The region climate grid for the Atlas overlay (D-243 §3, T-1113).
/// `Some` on a cache hit once the Region layer has run; `None` otherwise.
pub region_grid: Option<RegionGridLayer>,
/// The requested district window (D-226 T-1124 amendment, T-1137), or
/// `None` when the request carried no `window_center` / no window data is
/// cached yet for a pending derive. Distinct from the five layers above:
/// keyed on the REQUEST `(body, center, n)`, not on the body alone — see
/// the struct-level doc.
pub district_window: Option<DistrictWindowLayer>,
/// The quarter-footprint overlay (D-226 T-1112 amendment, T-1119). `Some`
/// on a cache hit once at least one settlement's quarter skeleton has been
/// generated (`state.quarters` non-empty); `None` otherwise, including a
@@ -235,6 +262,180 @@ pub fn build_region_grid(
})
}
// ---------------------------------------------------------------------------
// DistrictWindowLayer (D-226 T-1124 amendment, T-1137)
// ---------------------------------------------------------------------------
/// The requested district window: an `n × n` grid of TRUE 2 km districts
/// centred on `center`, derived on-demand via `district_profile::derive_district`
/// (D-226 T-1124 amendment §2). **Echoes `center`/`n` back** — this is the
/// client's race-condition guard, not a convenience field: because
/// `derive_district` is pure and deterministic (D-227), the same `(center, n)`
/// query always yields the same payload, so the echoed tuple *is* the
/// cache/staleness key the client compares against its most recently requested
/// window (`body_id` disambiguation rides the enclosing `AtlasLayerResponse`,
/// not the echo — see the amendment).
///
/// All six arrays are dense row-major `n × n` (`i = row * n + col`), matching
/// the `DistrictGridLayer`/`RegionGridLayer` indexing convention. Per-cell wire
/// cost is 7 bytes (1+1+2+1+1+1) before MessagePack framing overhead (D-226
/// T-1124 amendment §4).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DistrictWindowLayer {
pub center: DistrictPos,
pub n: u32,
/// `MorphologyZone` discriminant, the frozen 17-zone vocabulary (D-239 §6).
pub morphology: Vec<u8>,
/// 0-100, matches `DistrictGridLayer.elev_q` encoding.
pub elev_q: Vec<u8>,
/// Deci-°C, [`REGION_TEMP_NONE_DC`] sentinel — the SAME scheme as
/// `RegionGridLayer.mean_temp_dc`, deliberately not a separate
/// district-tier quantization (one temperature colorizer spans both zoom
/// levels, D-226 T-1124 amendment §2).
pub temp_dc: Vec<i16>,
/// 0-100, matches `DistrictGridLayer` precedent.
pub moisture_q: Vec<u8>,
/// `VegetationClass` discriminant, 0-6 including `Marine = 6` (T-1126) —
/// any client palette MUST be exhaustive over `Marine` (D-226 T-1124
/// amendment §3, non-negotiable — the ocean-blind-vegetation bug this
/// field caught at the district tier).
pub vegetation: Vec<u8>,
/// `GlaciationGrade` discriminant, 0-4 (T-1127).
pub glaciation: Vec<u8>,
}
/// Key for the server-side window derive cache (T-1137): `(body_id, center, n)`.
/// 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`]).
pub type DistrictWindowKey = (String, DistrictPos, u32);
/// Bounded LRU-ish cache of completed district-window derives (T-1137), a
/// sibling to [`BodyWorldStateCache`] rather than a field on it: windows are
/// keyed on the *request* `(body, center, n)`, not the body alone (see the
/// struct-level doc on [`AtlasLayerResponse`]), so they don't fit the
/// per-body cache's keying at all. Eviction is capacity-only FIFO-by-insertion
/// (not access-recency LRU like `BodyWorldStateCache`) — window requests are
/// comparatively rare and cheap to re-derive on a genuine miss (a background
/// re-submit, never a stall), so exact recency tracking isn't worth the
/// bookkeeping; a simple bound against unbounded growth is enough.
#[derive(Resource, Debug, Default)]
pub struct DistrictWindowCache {
entries: std::collections::BTreeMap<DistrictWindowKey, DistrictWindowLayer>,
/// Insertion order, oldest first — the eviction queue.
order: std::collections::VecDeque<DistrictWindowKey>,
capacity: usize,
}
/// Default capacity for [`DistrictWindowCache`] — generous relative to
/// `BodyWorldStateCache::CACHE_CAPACITY` (50 bodies) since each entry here is
/// far smaller (a handful of `Vec<u8>`/`Vec<i16>` at `n ≤ 64`, ≤ 28 KiB raw vs.
/// `BodyWorldState`'s full heightmap + districts + regions), and several
/// windows can legitimately be live per body (a player panning around).
pub const DISTRICT_WINDOW_CACHE_CAPACITY: usize = 256;
impl DistrictWindowCache {
pub fn new(capacity: usize) -> Self {
Self {
entries: std::collections::BTreeMap::new(),
order: std::collections::VecDeque::new(),
capacity,
}
}
/// Look up a cached window by its full key. Never mutates — window
/// validity has no time component (D-227), so there is nothing to bump.
pub fn get(&self, key: &DistrictWindowKey) -> Option<&DistrictWindowLayer> {
self.entries.get(key)
}
/// Insert a completed window derive, evicting the oldest entry first if
/// at capacity. Re-inserting an existing key replaces the value without
/// moving it in the eviction order (D-227: the value can only ever be
/// identical, so this is a no-op in practice, but stays correct either way).
pub fn insert(&mut self, key: DistrictWindowKey, layer: DistrictWindowLayer) {
if !self.entries.contains_key(&key) {
if self.entries.len() >= self.capacity {
if let Some(victim) = self.order.pop_front() {
self.entries.remove(&victim);
}
}
self.order.push_back(key.clone());
}
self.entries.insert(key, layer);
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
}
/// Build a [`DistrictWindowLayer`] by deriving every district in the
/// `n × n` window around `center` (T-1137). Mirrors
/// `aliveness_probe::render_window_panels`'s derive loop exactly (the probe
/// this design promotes to a served layer, D-226 T-1124 amendment §2) — same
/// row-major indexing, same `derive_district` call per cell.
///
/// `n` MUST already be clamped to `[1, DISTRICT_WINDOW_MAX_N]` by the caller —
/// this function trusts it verbatim (the clamp is `handle_atlas_request`'s
/// job, applied once at the wire boundary, not re-checked on every internal
/// caller per the existing codebase convention of clamping at the edge).
pub fn build_district_window_layer(
seed: SeedChain,
body_id: &str,
params: &crate::atlas::district_profile::BodyParams,
ta: &crate::atlas::features::TerrainAnalysis,
center: DistrictPos,
n: u32,
climate: &crate::atlas::district_profile::ClimateConstants,
) -> DistrictWindowLayer {
let n_i = n as i32;
let half = n_i / 2;
let cells = (n * n) as usize;
let mut morphology = vec![0u8; cells];
let mut elev_q = vec![0u8; cells];
let mut temp_dc = vec![REGION_TEMP_NONE_DC; cells];
let mut moisture_q = vec![0u8; cells];
let mut vegetation = vec![0u8; cells];
let mut glaciation = vec![0u8; cells];
for row in 0..n_i {
for col in 0..n_i {
// Row 0 = northmost, matching aliveness_probe's render_window_panels
// (derive_district maps negative wy to negative lat_frac = north).
let dp = (center.0 - half + col, center.1 - half + row);
let prof = crate::atlas::district_profile::derive_district(
seed, body_id, params, ta, dp, climate,
);
let i = (row * n_i + col) as usize;
morphology[i] = prof.morphology_zone as u8;
elev_q[i] = prof.elev_q.clamp(0, 100) as u8;
temp_dc[i] = match prof.temperature_c {
Some(t) => {
((t * 10.0).round() as i32).clamp(i16::MIN as i32 + 1, i16::MAX as i32) as i16
}
None => REGION_TEMP_NONE_DC,
};
moisture_q[i] = prof.moisture_q.clamp(0, 100) as u8;
vegetation[i] = prof.vegetation_class as u8;
glaciation[i] = prof.glaciation_grade as u8;
}
}
DistrictWindowLayer {
center,
n,
morphology,
elev_q,
temp_dc,
moisture_q,
vegetation,
glaciation,
}
}
// ---------------------------------------------------------------------------
// QuarterFootprintLayer (D-226 T-1112 amendment, T-1119)
// ---------------------------------------------------------------------------
@@ -544,6 +745,97 @@ pub fn build_settlement_layer(state: &BodyWorldState) -> Option<SettlementLayer>
Some(SettlementLayer { settlements })
}
/// Resolve `req`'s district-window query, if any (D-226 T-1124 amendment,
/// T-1137). Returns `None` immediately when `req.window_center` is absent (no
/// window requested — the common case, zero cost).
///
/// **Independent of the whole-body cache state** (the amendment is explicit:
/// "the window derivation depends only on `TerrainAnalysis` + `BodyParams`
/// being resolvable for the body ... not on which whole-body layers the
/// cascade has cached") — so this runs whether `handle_atlas_request` is
/// about to take its cache-hit or cache-miss branch, sharing neither's control
/// flow.
///
/// Cache hit (`(body_id, center, n)` already in `window_cache`) → `Some`
/// immediately, no queue submission (D-227: a previously-derived window for
/// this body+seed is valid forever, no staleness check needed). Cache miss →
/// submit a `DeriveWindow` work item (queue-based, per the amendment's binding
/// serving model — never inline here) and return `None`; the *next* request
/// for this `(body, center, n)` re-checks the cache and finds it populated
/// once `drain_generation_completions` has 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]` here — the ONE place
/// that clamp is applied; nothing downstream re-checks the wire value.
#[allow(clippy::too_many_arguments)]
fn serve_district_window(
req: &AtlasLayerRequest,
window_cache: &mut DistrictWindowCache,
queue: &GenerationQueue,
resolver: &BodySourceResolver,
body_params_reader: Option<&BodyParamsReader>,
world_seed: u64,
conn_id: ConnectionId,
) -> Option<DistrictWindowLayer> {
let center = req.window_center?;
let n = req.window_n.clamp(1, DISTRICT_WINDOW_MAX_N);
let key: DistrictWindowKey = (req.body_id.clone(), center, n);
if let Some(layer) = window_cache.get(&key) {
return Some(layer.clone());
}
// Miss — resolve heightmap + body params and submit a background derive.
// Read/resolve failures are non-fatal for the window (log + skip): the
// window simply stays None on this response, same as an unrun whole-body
// layer, rather than failing the entire AtlasLayerResponse.
let heightmap_path = match resolver.resolve(&req.body_id) {
Ok(p) => p,
Err(e) => {
tracing::warn!(
body_id = %req.body_id,
error = %e,
"district window request: heightmap resolve failed — window stays None"
);
return None;
}
};
let Some(reader) = body_params_reader else {
tracing::warn!(
body_id = %req.body_id,
"district window request: no body_params_reader wired — window stays None"
);
return None;
};
let body_params = match reader.read_body_params(&req.body_id) {
Ok(p) => p,
Err(e) => {
tracing::warn!(
body_id = %req.body_id,
error = %e,
"district window request: body_params read failed — window stays None"
);
return None;
}
};
queue.submit_window(
GenWorkItem::DeriveWindow {
body_id: req.body_id.clone(),
conn_id,
heightmap_path,
sea_level: DEFAULT_SEA_LEVEL,
body_seed: SeedChain::for_body(world_seed, &req.body_id),
body_params: Box::new(body_params),
center,
n,
},
GenPriority::Immediate,
);
None
}
/// Serve one layer request (D-225). `current_tick` stamps the cache LRU on hit;
/// `world_seed` derives the body's `SeedChain` for the enqueued analysis.
///
@@ -557,16 +849,35 @@ pub fn build_settlement_layer(state: &BodyWorldState) -> Option<SettlementLayer>
/// causing the cascade to stop at `CascadeLayer::Settlement` (pre-T-1032
/// behaviour). A successful read passes `Some(Box::new(params))`, enabling
/// the full `CascadeLayer::DistrictProfile` path.
///
/// `window_cache` + `conn_id` serve the optional district-window query
/// (D-226 T-1124 amendment, T-1137) via [`serve_district_window`] — see that
/// function for the caching/coalescing model. `conn_id` is used ONLY as the
/// window request's coalescing key; nothing else in this function is
/// connection-aware (the D-254 §2 convention this proxy already follows).
#[allow(clippy::too_many_arguments)]
pub fn handle_atlas_request(
req: &AtlasLayerRequest,
cache: &mut BodyWorldStateCache,
window_cache: &mut DistrictWindowCache,
queue: &GenerationQueue,
resolver: &BodySourceResolver,
city_reader: Option<&CityContextReader>,
body_params_reader: Option<&BodyParamsReader>,
world_seed: u64,
current_tick: SimTick,
conn_id: ConnectionId,
) -> AtlasLayerResponse {
let district_window = serve_district_window(
req,
window_cache,
queue,
resolver,
body_params_reader,
world_seed,
conn_id,
);
// Cache hit — serve immediately.
if let Some(state) = cache.get(&req.body_id, current_tick) {
let layer1 = Layer1Output {
@@ -599,6 +910,7 @@ pub fn handle_atlas_request(
road_graph,
settlements,
region_grid,
district_window,
quarter_footprints,
};
}
@@ -673,6 +985,7 @@ pub fn handle_atlas_request(
road_graph: None,
settlements: None,
region_grid: None,
district_window,
quarter_footprints: None,
}
}
@@ -686,6 +999,7 @@ pub fn handle_atlas_request(
road_graph: None,
settlements: None,
region_grid: None,
district_window,
quarter_footprints: None,
},
Err(e) => AtlasLayerResponse {
@@ -696,6 +1010,7 @@ pub fn handle_atlas_request(
road_graph: None,
settlements: None,
region_grid: None,
district_window,
quarter_footprints: None,
},
}
@@ -829,6 +1144,340 @@ mod tests {
assert_eq!(grid.moisture_q[1], 5);
}
// -----------------------------------------------------------------------
// DistrictWindowLayer (D-226 T-1124 amendment, T-1137)
// -----------------------------------------------------------------------
/// Minimal deterministic heightmap fixture, mirroring
/// `district_profile::tests::test_hm` (T-1137: the window path shares the
/// same on-demand `derive_district` call, so it earns the same fixture
/// shape).
fn window_test_hm() -> crate::atlas::heightmap::BodyHeightmap {
use crate::atlas::heightmap::BodyHeightmap;
let (w, h) = (64u32, 32u32);
let n = (w * h) as usize;
let data = (0..n)
.map(|i| {
let r = (i / w as usize) as f32 / h as f32;
let c = (i % w as usize) as f32 / w as f32;
(r * 0.6 + c * 0.4).min(1.0)
})
.collect();
BodyHeightmap {
body_id: "test".into(),
width: w,
height: h,
data,
sea_level: 0.3,
}
}
fn window_test_ta(
hm: &crate::atlas::heightmap::BodyHeightmap,
) -> crate::atlas::features::TerrainAnalysis {
use crate::atlas::drainage;
use crate::atlas::features::TerrainAnalysis;
let dr = drainage::analyze(&hm.data, hm.width, hm.height, hm.sea_level);
TerrainAnalysis::analyze(hm, &dr)
}
fn window_test_params() -> crate::atlas::district_profile::BodyParams {
crate::atlas::district_profile::BodyParams {
hydrosphere: Some("ocean".into()),
atmosphere: Some("breathable".into()),
planet_class: Some("temperate".into()),
body_radius_km: Some(6371.0),
..Default::default()
}
}
/// `build_district_window_layer` produces a dense `n × n` row-major grid
/// (the `DistrictGridLayer`/`RegionGridLayer` indexing convention) whose
/// cell count and per-array lengths match `n`, and whose values are
/// pulled straight from the corresponding `derive_district` profile field
/// (T-1137).
#[test]
fn build_district_window_layer_produces_dense_n_by_n_grid() {
let hm = window_test_hm();
let ta = window_test_ta(&hm);
let params = window_test_params();
let climate = crate::atlas::district_profile::ClimateConstants::default();
let seed = SeedChain::root(42).derive(SeedDomain::Body, 1);
let n = 4u32;
let layer =
build_district_window_layer(seed, "test_body", &params, &ta, (10, -5), n, &climate);
assert_eq!(layer.center, (10, -5));
assert_eq!(layer.n, n);
let cells = (n * n) as usize;
assert_eq!(layer.morphology.len(), cells);
assert_eq!(layer.elev_q.len(), cells);
assert_eq!(layer.temp_dc.len(), cells);
assert_eq!(layer.moisture_q.len(), cells);
assert_eq!(layer.vegetation.len(), cells);
assert_eq!(layer.glaciation.len(), cells);
// Spot-check one cell against a direct derive_district call — the
// window builder must not transform the profile's values, only pack
// them (row 0, col 0 → district (center.0 - n/2, center.1 - n/2)).
let half = (n / 2) as i32;
let dp = (10 - half, -5 - half);
let prof = crate::atlas::district_profile::derive_district(
seed,
"test_body",
&params,
&ta,
dp,
&climate,
);
assert_eq!(layer.morphology[0], prof.morphology_zone as u8);
assert_eq!(layer.elev_q[0], prof.elev_q.clamp(0, 100) as u8);
assert_eq!(layer.moisture_q[0], prof.moisture_q.clamp(0, 100) as u8);
assert_eq!(layer.vegetation[0], prof.vegetation_class as u8);
assert_eq!(layer.glaciation[0], prof.glaciation_grade as u8);
}
/// Clamped-window edge: `n = 1` is the minimum valid window (a single
/// district) — no panic, no empty output, exactly one cell per array.
#[test]
fn build_district_window_layer_handles_n_equals_one() {
let hm = window_test_hm();
let ta = window_test_ta(&hm);
let params = window_test_params();
let climate = crate::atlas::district_profile::ClimateConstants::default();
let seed = SeedChain::root(1).derive(SeedDomain::Body, 1);
let layer =
build_district_window_layer(seed, "test_body", &params, &ta, (0, 0), 1, &climate);
assert_eq!(layer.n, 1);
assert_eq!(layer.morphology.len(), 1);
assert_eq!(layer.elev_q.len(), 1);
assert_eq!(layer.temp_dc.len(), 1);
assert_eq!(layer.moisture_q.len(), 1);
assert_eq!(layer.vegetation.len(), 1);
assert_eq!(layer.glaciation.len(), 1);
}
/// Determinism spot-check (D-010, T-1123 precedent promoted to a real
/// test per the ticket): two full derive passes over the SAME window are
/// byte-identical, at a window size large enough to exercise many cells
/// (mirrors `aliveness_probe --render`'s own two-pass proof, now pinned
/// as a unit test rather than a probe-only demonstration).
#[test]
fn build_district_window_layer_two_passes_are_byte_identical() {
let hm = window_test_hm();
let ta = window_test_ta(&hm);
let params = window_test_params();
let climate = crate::atlas::district_profile::ClimateConstants::default();
let seed = SeedChain::root(7).derive(SeedDomain::Body, 3);
let n = 8u32;
let first =
build_district_window_layer(seed, "test_body", &params, &ta, (3, -2), n, &climate);
let second =
build_district_window_layer(seed, "test_body", &params, &ta, (3, -2), n, &climate);
assert_eq!(
first, second,
"two full derive passes over the same (center, n) must be byte-identical (D-010/D-227)"
);
}
/// [`DistrictWindowCache`] insert/get round-trips, and a capacity-1 cache
/// evicts the oldest entry FIFO — mirroring `BodyWorldStateCache`'s own
/// `evicts_lru_on_overflow` precedent, adapted to this cache's
/// capacity-only insertion-order eviction (no access-recency tracking,
/// per the struct doc: D-227 means a cached window has no staleness to
/// track, only unbounded growth to bound).
#[test]
fn district_window_cache_insert_get_and_evict() {
let mut cache = DistrictWindowCache::new(2);
let key_a: DistrictWindowKey = ("Alpha".into(), (0, 0), 4);
let key_b: DistrictWindowKey = ("Beta".into(), (1, 1), 4);
let key_c: DistrictWindowKey = ("Gamma".into(), (2, 2), 4);
let mk = |center, n| DistrictWindowLayer {
center,
n,
morphology: vec![0; (n * n) as usize],
elev_q: vec![0; (n * n) as usize],
temp_dc: vec![REGION_TEMP_NONE_DC; (n * n) as usize],
moisture_q: vec![0; (n * n) as usize],
vegetation: vec![0; (n * n) as usize],
glaciation: vec![0; (n * n) as usize],
};
assert!(cache.get(&key_a).is_none());
cache.insert(key_a.clone(), mk((0, 0), 4));
cache.insert(key_b.clone(), mk((1, 1), 4));
assert_eq!(cache.len(), 2);
assert!(cache.get(&key_a).is_some());
assert!(cache.get(&key_b).is_some());
// Cache at capacity (2): inserting a third entry evicts key_a (oldest).
cache.insert(key_c.clone(), mk((2, 2), 4));
assert_eq!(cache.len(), 2);
assert!(
cache.get(&key_a).is_none(),
"key_a should have been evicted"
);
assert!(cache.get(&key_b).is_some());
assert!(cache.get(&key_c).is_some());
}
/// `handle_atlas_request`'s window branch clamps `window_n` server-side to
/// `[1, DISTRICT_WINDOW_MAX_N]` — a request claiming an oversized `n` on
/// the wire never reaches `build_district_window_layer` un-clamped. This
/// exercises the full request→submit→drain→cache→re-request loop with an
/// out-of-range `window_n`, confirming the CACHED layer (once the
/// background derive completes) carries the CLAMPED `n`, not the
/// requested one.
#[test]
fn handle_atlas_request_clamps_oversized_window_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("GJ1c");
let queue = GenerationQueue::with_threads(1);
let oversized_req = AtlasLayerRequest {
body_id: "GJ1c".to_string(),
up_to: CascadeLayer::Topography,
window_center: Some((0, 0)),
window_n: DISTRICT_WINDOW_MAX_N * 10, // wildly over the wire — must clamp, not trust
};
let resp = handle_atlas_request(
&oversized_req,
&mut cache,
&mut window_cache,
&queue,
&resolver,
None,
Some(&params_reader),
42,
1,
test_conn_id(),
);
// First request: window not yet cached → None, but a DeriveWindow
// must have been submitted (checked via the drain below).
assert!(resp.district_window.is_none());
// Wait for the Rayon DeriveWindow work item to complete.
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 == "GJ1c" {
return Some(layer);
}
}
None
});
let layer = window_completion.expect("DeriveWindow must complete for GJ1c");
assert_eq!(
layer.n, DISTRICT_WINDOW_MAX_N,
"server must clamp window_n to DISTRICT_WINDOW_MAX_N, never trust the wire value"
);
}
/// `serve_district_window` returns `None` (no window requested) when the
/// request carries no `window_center` — the common case, and the ONLY
/// path every pre-T-1137 caller takes (wire back-compat: an old client's
/// `{body_id, up_to}` frame decodes with `window_center: None` via
/// `#[serde(default)]`).
#[test]
fn handle_atlas_request_no_window_center_leaves_district_window_none() {
let mut cache = BodyWorldStateCache::new(CACHE_CAPACITY);
let mut window_cache = DistrictWindowCache::new(DISTRICT_WINDOW_CACHE_CAPACITY);
let (_db, resolver) = empty_resolver();
let queue = GenerationQueue::with_threads(1);
let resp = handle_atlas_request(
&req("GJ1c"), // window_center: None
&mut cache,
&mut window_cache,
&queue,
&resolver,
None,
None,
42,
1,
test_conn_id(),
);
assert!(resp.district_window.is_none());
assert!(
window_cache.is_empty(),
"no window requested → no DeriveWindow submitted, cache stays empty"
);
}
/// `AtlasLayerResponse.district_window` survives a MessagePack round trip
/// (mirrors the existing `atlas_layer_response_with_new_layers_round_trips_msgpack`
/// precedent) — the wire shape every field the amendment specifies:
/// echoed `center`/`n`, all six parallel arrays including the
/// `REGION_TEMP_NONE_DC` sentinel and `VegetationClass::Marine = 6`.
#[test]
fn district_window_layer_round_trips_msgpack_inside_response() {
let window = DistrictWindowLayer {
center: (10, -5),
n: 2,
morphology: vec![0, 8, 14, 16],
elev_q: vec![0, 45, 98, 60],
temp_dc: vec![205, 150, REGION_TEMP_NONE_DC, 80],
moisture_q: vec![90, 55, 0, 100],
vegetation: vec![6, 3, 0, 5], // includes Marine = 6
glaciation: vec![0, 0, 4, 1],
};
let resp = AtlasLayerResponse {
body_id: "GJ1c".into(),
status: AtlasLayerStatus::Ready,
layer1: None,
district_grid: None,
road_graph: None,
settlements: None,
region_grid: None,
district_window: Some(window.clone()),
quarter_footprints: None,
};
let bytes = rmp_serde::to_vec_named(&resp).expect("encode");
let decoded: AtlasLayerResponse = rmp_serde::from_slice(&bytes).expect("decode");
let dw = decoded
.district_window
.expect("district_window survives round trip");
assert_eq!(dw, window);
assert_eq!(dw.center, (10, -5));
assert_eq!(dw.n, 2);
assert_eq!(
dw.temp_dc[2], REGION_TEMP_NONE_DC,
"airless sentinel preserved"
);
assert_eq!(dw.vegetation[0], 6, "Marine discriminant preserved");
}
/// Wire back-compat (D-226 T-1124 amendment §1): a pre-T-1137 request
/// frame carrying only `{body_id, up_to}` — no `window_center`/`window_n`
/// keys at all — decodes cleanly via `#[serde(default)]`, byte-unchanged
/// for every existing caller.
#[test]
fn old_request_frame_without_window_fields_decodes_with_none() {
#[derive(serde::Serialize)]
struct OldAtlasLayerRequest {
body_id: String,
up_to: CascadeLayer,
}
let old = OldAtlasLayerRequest {
body_id: "GJ1c".into(),
up_to: CascadeLayer::Topography,
};
let bytes = rmp_serde::to_vec_named(&old).expect("encode old-shape frame");
let decoded: AtlasLayerRequest = rmp_serde::from_slice(&bytes).expect("decode");
assert_eq!(decoded.body_id, "GJ1c");
assert_eq!(decoded.up_to, CascadeLayer::Topography);
assert_eq!(decoded.window_center, None);
assert_eq!(decoded.window_n, 0);
}
/// T-1119: `build_quarter_footprint_layer` returns `None` when no
/// settlement's quarter skeleton has been generated (`state.quarters`
/// empty), mirroring `build_district_grid`/`build_region_grid`'s
@@ -1403,6 +2052,7 @@ mod tests {
road_graph: build_road_graph_layer(&state),
settlements: build_settlement_layer(&state),
region_grid: build_region_grid(&state),
district_window: None,
quarter_footprints: build_quarter_footprint_layer(&state, world_seed),
};
@@ -1439,9 +2089,15 @@ mod tests {
AtlasLayerRequest {
body_id: body_id.to_string(),
up_to: CascadeLayer::Topography,
window_center: None,
window_n: 0,
}
}
fn test_conn_id() -> ConnectionId {
ConnectionId(1)
}
const REL: &str = "wiki/star-systems/GJ-1/bodies/GJ1c/heightmap.png";
/// systems.db with one bodies row, + a base root containing a tiny 16-bit
@@ -1517,16 +2173,19 @@ mod tests {
});
let (_db, resolver) = empty_resolver();
let queue = GenerationQueue::with_threads(1);
let mut window_cache = DistrictWindowCache::new(DISTRICT_WINDOW_CACHE_CAPACITY);
let resp = handle_atlas_request(
&req("GJ1c"),
&mut cache,
&mut window_cache,
&queue,
&resolver,
None,
None,
42,
1,
test_conn_id(),
);
assert_eq!(resp.status, AtlasLayerStatus::Ready);
assert_eq!(resp.layer1.expect("layer1").body_id, "GJ1c");
@@ -1537,16 +2196,19 @@ mod tests {
let mut cache = BodyWorldStateCache::new(CACHE_CAPACITY);
let (_db, resolver) = resolver_with_body("GJ1c");
let queue = GenerationQueue::with_threads(1);
let mut window_cache = DistrictWindowCache::new(DISTRICT_WINDOW_CACHE_CAPACITY);
let resp = handle_atlas_request(
&req("GJ1c"),
&mut cache,
&mut window_cache,
&queue,
&resolver,
None,
None,
42,
1,
test_conn_id(),
);
assert_eq!(resp.status, AtlasLayerStatus::Pending);
assert!(resp.layer1.is_none());
@@ -1567,16 +2229,19 @@ mod tests {
let mut cache = BodyWorldStateCache::new(CACHE_CAPACITY);
let (_db, resolver) = empty_resolver();
let queue = GenerationQueue::with_threads(1);
let mut window_cache = DistrictWindowCache::new(DISTRICT_WINDOW_CACHE_CAPACITY);
let resp = handle_atlas_request(
&req("ghost"),
&mut cache,
&mut window_cache,
&queue,
&resolver,
None,
None,
42,
1,
test_conn_id(),
);
assert_eq!(resp.status, AtlasLayerStatus::NotFound);
}
@@ -1651,16 +2316,19 @@ mod tests {
let mut cache = BodyWorldStateCache::new(CACHE_CAPACITY);
let (_db, resolver, params_reader, _root) = resolver_and_params_reader("GJ1c");
let queue = GenerationQueue::with_threads(1);
let mut window_cache = DistrictWindowCache::new(DISTRICT_WINDOW_CACHE_CAPACITY);
let resp = handle_atlas_request(
&req("GJ1c"),
&mut cache,
&mut window_cache,
&queue,
&resolver,
None,
Some(&params_reader),
42,
1,
test_conn_id(),
);
assert_eq!(resp.status, AtlasLayerStatus::Pending);
@@ -1694,12 +2362,14 @@ mod tests {
let ready = handle_atlas_request(
&req("GJ1c"),
&mut cache,
&mut window_cache,
&queue,
&resolver,
None,
Some(&params_reader),
42,
2,
test_conn_id(),
);
assert_eq!(ready.status, AtlasLayerStatus::Ready);
assert!(
@@ -1714,16 +2384,19 @@ mod tests {
let mut cache = BodyWorldStateCache::new(CACHE_CAPACITY);
let (_db, resolver) = resolver_with_body("GJ1c");
let queue = GenerationQueue::with_threads(1);
let mut window_cache = DistrictWindowCache::new(DISTRICT_WINDOW_CACHE_CAPACITY);
let resp = handle_atlas_request(
&req("GJ1c"),
&mut cache,
&mut window_cache,
&queue,
&resolver,
None,
None, // no body_params_reader
42,
1,
test_conn_id(),
);
assert_eq!(resp.status, AtlasLayerStatus::Pending);
+30 -1
View File
@@ -27,7 +27,10 @@ use crate::atlas::city_context_reader::{
use crate::atlas::district_mix::{compute_district_mix, population_tier};
use crate::atlas::district_profile::{DistrictPos, DistrictProfile};
use crate::atlas::gen_queue::{GenCompletion, GenPriority, GenWorkItem, GenerationQueue};
use crate::atlas::layer_proxy::{handle_atlas_request, AtlasLayerResponse, AtlasLayerStatus};
use crate::atlas::layer_proxy::{
handle_atlas_request, AtlasLayerResponse, AtlasLayerStatus, DistrictWindowCache,
DISTRICT_WINDOW_CACHE_CAPACITY,
};
use crate::atlas::road_graph::{RoadGraph, RoadNode};
use crate::atlas::scale;
use crate::atlas::skeleton_gen::derive_complexity;
@@ -61,6 +64,7 @@ impl Plugin for GenerationPlugin {
fn build(&self, app: &mut App) {
app.insert_resource(GenerationQueue::new())
.insert_resource(BodyWorldStateCache::new(CACHE_CAPACITY))
.insert_resource(DistrictWindowCache::new(DISTRICT_WINDOW_CACHE_CAPACITY))
.add_systems(
Update,
drain_generation_completions.in_set(TickPhase::PreInput),
@@ -78,10 +82,18 @@ impl Plugin for GenerationPlugin {
/// Drain inbound atlas layer requests and serve each through the proxy (#969,
/// D-225): cache hit → Ready, miss → resolve + enqueue + Pending. Responses are
/// buffered for the bridge to flush in `PostSnapshot`.
///
/// `window_cache` serves the optional district-window query (D-226 T-1124
/// amendment, T-1137) — see `handle_atlas_request`/`serve_district_window`.
/// Unlike the rest of `handle_atlas_request`, the window path IS
/// connection-aware (its coalescing key), so `conn_id` — already threaded
/// through this loop for response routing (D-254 §2) — is passed one level
/// further in for that one purpose only.
fn serve_atlas_requests(
mut requests: ResMut<AtlasRequestBuffer>,
mut responses: ResMut<AtlasResponseBuffer>,
mut cache: ResMut<BodyWorldStateCache>,
mut window_cache: ResMut<DistrictWindowCache>,
queue: Res<GenerationQueue>,
resolver: Option<Res<BodySourceResolverResource>>,
city_reader: Option<Res<CityContextReaderResource>>,
@@ -106,12 +118,14 @@ fn serve_atlas_requests(
Some(r) => handle_atlas_request(
&req,
&mut cache,
&mut window_cache,
&queue,
&r.0,
reader,
params_reader,
world_seed,
tick,
conn_id,
),
None => AtlasLayerResponse {
body_id: req.body_id.clone(),
@@ -121,6 +135,7 @@ fn serve_atlas_requests(
road_graph: None,
settlements: None,
region_grid: None,
district_window: None,
quarter_footprints: None,
},
};
@@ -209,6 +224,7 @@ pub fn serve_browse_requests(
fn drain_generation_completions(
queue: Res<GenerationQueue>,
mut cache: ResMut<BodyWorldStateCache>,
mut window_cache: ResMut<DistrictWindowCache>,
city_reader: Option<Res<CityContextReaderResource>>,
trait_catalog: Option<Res<TraitCatalogReaderResource>>,
rng: Option<Res<SimRng>>,
@@ -390,6 +406,15 @@ fn drain_generation_completions(
"FillChunk derived (no Phase-5 consumer yet)"
);
}
GenCompletion::WindowDerived { body_id, layer } => {
// D-226 T-1124 amendment, T-1137: cache the completed window —
// NOT pushed into any in-flight response (this drain has no
// notion of which connection(s) are waiting). The requester's
// NEXT poll (the existing D-225 re-request loop) hits
// `handle_atlas_request`'s window branch, which finds this
// entry via `DistrictWindowCache::get` and serves it.
window_cache.insert((body_id, layer.center, layer.n), *layer);
}
}
}
}
@@ -832,6 +857,7 @@ mod tests {
let mut world = World::new();
world.insert_resource(GenerationQueue::new());
world.insert_resource(BodyWorldStateCache::new(CACHE_CAPACITY));
world.insert_resource(DistrictWindowCache::new(DISTRICT_WINDOW_CACHE_CAPACITY));
world.resource::<GenerationQueue>().submit(
GenWorkItem::AnalyzeBody {
@@ -876,10 +902,13 @@ mod tests {
AtlasLayerRequest {
body_id: "GJ1c".to_string(),
up_to: CascadeLayer::Topography,
window_center: None,
window_n: 0,
},
)]));
world.insert_resource(AtlasResponseBuffer::default());
world.insert_resource(BodyWorldStateCache::new(CACHE_CAPACITY));
world.insert_resource(DistrictWindowCache::new(DISTRICT_WINDOW_CACHE_CAPACITY));
world.insert_resource(GenerationQueue::with_threads(1));
// No resolver / SimRng / SimulationTime — all optional in the system.
+4
View File
@@ -1199,6 +1199,8 @@ mod inbound_tests {
let req = AtlasLayerRequest {
body_id: "GJ1c".into(),
up_to: CascadeLayer::Topography,
window_center: None,
window_n: 0,
};
let frame = rmp_serde::to_vec_named(&req).unwrap();
assert!(
@@ -1242,6 +1244,8 @@ mod inbound_tests {
let atlas_frame = rmp_serde::to_vec_named(&AtlasLayerRequest {
body_id: "GJ1c".into(),
up_to: CascadeLayer::Topography,
window_center: None,
window_n: 0,
})
.unwrap();
let star_map_frame = rmp_serde::to_vec_named(&StarMapRequest { star_map: true }).unwrap();
+2
View File
@@ -369,6 +369,8 @@ fn single_tick_drains_all_ready_inbound_frames() {
let req = AtlasLayerRequest {
body_id: "GJ1c".into(),
up_to: CascadeLayer::Topography,
window_center: None,
window_n: 0,
};
let payload = rmp_serde::to_vec_named(&req).expect("failed to serialize");
write_framed(&mut stream, &payload).expect("write atlas frame");
+38 -3
View File
@@ -4,9 +4,9 @@
use settled_reach_server::atlas::body_world_state::{DrainageBasin, RiverNetwork};
use settled_reach_server::atlas::layer1::Layer1Output;
use settled_reach_server::atlas::layer_proxy::{
AtlasLayerResponse, AtlasLayerStatus, QuarterFootprintEntry, QuarterFootprintLayer,
RegionGridLayer, RoadGraphEdge, RoadGraphLayer, RoadGraphNode, SettlementEntry,
SettlementLayer, SettlementSizeClass,
AtlasLayerResponse, AtlasLayerStatus, DistrictWindowLayer, QuarterFootprintEntry,
QuarterFootprintLayer, RegionGridLayer, RoadGraphEdge, RoadGraphLayer, RoadGraphNode,
SettlementEntry, SettlementLayer, SettlementSizeClass, REGION_TEMP_NONE_DC,
};
use settled_reach_server::atlas::region_profile::{SeasonPhase, WeatherState};
use settled_reach_server::atlas::road_graph::RoadNodeKind;
@@ -676,6 +676,7 @@ fn generate_atlas_layer_response_fixtures() {
road_graph: Some(road_graph),
settlements: Some(settlements),
region_grid: Some(region_grid),
district_window: None,
quarter_footprints: Some(quarter_footprints),
};
write_fixture(
@@ -691,6 +692,7 @@ fn generate_atlas_layer_response_fixtures() {
road_graph: None,
settlements: None,
region_grid: None,
district_window: None,
quarter_footprints: None,
};
write_fixture(
@@ -706,10 +708,43 @@ fn generate_atlas_layer_response_fixtures() {
road_graph: None,
settlements: None,
region_grid: None,
district_window: None,
quarter_footprints: None,
};
write_fixture(
"atlas_response_not_found",
&rmp_serde::to_vec_named(&not_found).unwrap(),
);
// D-226 T-1124 amendment, T-1137: a Ready response carrying a populated
// district_window — the windowed-family field, distinct from the five
// whole-body layers above. A small n=2 window keeps the fixture readable
// while exercising every field (including the REGION_TEMP_NONE_DC
// sentinel and VegetationClass::Marine = 6, both non-negotiable per the
// amendment §3).
let window = DistrictWindowLayer {
center: (10, -5),
n: 2,
morphology: vec![0, 8, 14, 16], // OpenOcean, AlluvialPlain, Alpine, Wetland
elev_q: vec![0, 45, 98, 60],
temp_dc: vec![205, 150, REGION_TEMP_NONE_DC, 80], // 20.5°C, 15.0°C, airless sentinel, 8.0°C
moisture_q: vec![90, 55, 0, 100],
vegetation: vec![6, 3, 0, 5], // Marine, Forest, Absent, RiparianThicket
glaciation: vec![0, 0, 4, 1], // None, None, IceCap, Light
};
let ready_with_window = AtlasLayerResponse {
body_id: "GJ1c".into(),
status: AtlasLayerStatus::Ready,
layer1: None,
district_grid: None,
road_graph: None,
settlements: None,
region_grid: None,
district_window: Some(window),
quarter_footprints: None,
};
write_fixture(
"atlas_response_ready_with_window",
&rmp_serde::to_vec_named(&ready_with_window).unwrap(),
);
}