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:
@@ -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.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user