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
+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.