feat(simulation): step-canvas serving — tagged envelope (D-255, T-1181)

The D-225-discharging wire migration per D-255(c): a new
server/src/atlas/step_canvas.rs subsystem (six-rung StepCanvasRung
ladder — Global variable-extent rung 0 through Chunk 64m — the
StepCanvasRequest/StepCanvasResponse tagged envelope extending the
proven ShapeProbe discriminated-shape pattern, PNG-per-field dense +
MessagePack-native sparse encoding per the T-1179 measured table, and
both server cache tiers: the structurally keep-always GlobalTierCache
and the dual-axis StepCanvasCache with storage TTLs per rung and
SIM_STATE_TTL clock-bucket staleness per the D-227 amendment (1)
formula). Wired through bridge/{mod,tcp,local}.rs (sixth demux shape,
send_step_canvas_response mirroring the five existing senders),
gen_queue.rs (DeriveStepCanvas work item with per-connection-per-rung
coalescing, reusing the shared TerrainAnalysisCache), and plugin.rs
(serve/complete systems, lazy D-206 rung-0 population).

Acceptance gate (mandatory per D-227 amendment (3)):
tests/step_canvas_acceptance_gate.rs — cache-hit == cache-miss
byte-identical for every rung, lossless encode round-trips, cache
round-trips vs fresh derive, distinct-center sanity. 5/5 pass.

Station-spacing cap ADOPTED: course stations floored to District
spacing (2,048 m) at finer rungs — the S2-measured +38-87% chunk/block
course cost had zero display benefit at the same station density
(COURSE_STATION_SPACING_FLOOR_M).

Documented honest gaps, not shortcuts: settlement_id is a proximity
approximation (no footprint polygons exist yet); glaciation/flooded_q
sim-state planes are wire-shape-ready D-253 stubs; rung-0 uses signed
equator-anchored rows (the canonical D-256 core convention — T-1186's
wrong-latitude behavior applies unchanged and unfixed here, by
instruction).

Legacy district_window carrier byte-unchanged: window_derivation_golden
6/6 byte-identical, all district_window suites pass unmodified. Full
suite at implementation time: 2127 passed across 45 binaries.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-25 03:13:08 +02:00
co-authored by Claude Fable 5
parent 5151c9010f
commit cd2f342421
12 changed files with 2699 additions and 23 deletions
+171
View File
@@ -258,6 +258,48 @@ pub enum GenWorkItem {
/// special-cased away.
min_wl_m: u32,
},
/// Derive a D-255(a) step canvas (T-1181, the tagged-envelope migration).
///
/// **Binding serving model — same as `DeriveWindow`:** step canvases ride
/// this SAME Rayon queue, never inline on the `PreInput` drain (D-255(d):
/// "never inline here"). Reuses the SAME `TerrainAnalysisCache` LRU
/// `DeriveWindow` already populates — a step-canvas request and a legacy
/// `district_window` request for the same body share one
/// `run_layer1`-derived `(Layer1Output, TerrainAnalysis)` cache entry
/// (keyed on `body_id` alone), so a player already panning the legacy
/// window viewer and then opening the new stepped map (or vice versa)
/// pays the ~45 ms `run_layer1` cost at most once per body, not once per
/// carrier.
DeriveStepCanvas {
body_id: String,
/// Coalescing/routing key — NOT used by `run_work_item` itself (the
/// derive is connection-agnostic), only by
/// `GenerationQueue::submit_step_canvas` to decide which still-pending
/// item a new one for the same connection+body+rung supersedes.
conn_id: ConnectionId,
heightmap_path: PathBuf,
sea_level: f32,
body_seed: SeedChain,
body_params: Box<BodyParams>,
/// The body's placed settlements, pre-resolved at dispatch time from
/// `BodyWorldState.placements` if already cached (empty otherwise —
/// `settlement_id` coverage is simply all-zero until the body's own
/// `AnalyzeBody` cascade has placed settlements; see
/// `step_canvas::serve_step_canvas_request`'s doc).
placements: Vec<crate::atlas::attractor_matching::CityPlacement>,
/// The D-255(a) rung this canvas targets.
rung: crate::atlas::step_canvas::StepCanvasRung,
/// World-metre centre (ignored for `StepCanvasRung::Global`, whose
/// canvas is whole-body/origin-anchored).
center: (i64, i64),
/// Canvas pixel budget for every FIXED rung (ignored for `Global`,
/// whose extent is the body's own region grid).
extent: (u32, u32),
/// Octave cutoff in whole metres, already quantized by the caller
/// (`step_canvas::quantize_min_wl_m_for_rung`) — never a raw wire
/// value.
min_wl_m: u32,
},
}
impl GenWorkItem {
@@ -293,6 +335,29 @@ impl GenWorkItem {
None
}
}
/// Coalescing key for `DeriveStepCanvas` items only — `(connection, body,
/// rung)` (T-1181, mirroring `window_supersede_key`'s exact reasoning):
/// a pan-burst that queues several step-canvas requests for the same
/// connection+body+rung before the first is dispatched collapses to one
/// derive. A different rung for the same connection+body does NOT
/// coalesce — every rung is a separate in-flight derive, not a competing
/// update to the same one. `None` for every other variant.
pub fn step_canvas_supersede_key(
&self,
) -> Option<(ConnectionId, &str, crate::atlas::step_canvas::StepCanvasRung)> {
if let GenWorkItem::DeriveStepCanvas {
body_id,
conn_id,
rung,
..
} = self
{
Some((*conn_id, body_id, *rung))
} else {
None
}
}
}
// ---------------------------------------------------------------------------
@@ -352,6 +417,22 @@ pub enum GenCompletion {
/// boxing rationale).
layer: Box<DistrictWindowLayer>,
},
/// A D-255(a) step canvas finished deriving (T-1181). The main thread
/// inserts `canvas` into `GlobalTierCache` (rung `Global`) or
/// `StepCanvasCache` (every other rung), keyed as
/// `serve_step_canvas_request` documents — NOT pushed into any in-flight
/// response, same re-poll-and-hit-cache model as `WindowDerived`.
StepCanvasDerived {
body_id: String,
rung: crate::atlas::step_canvas::StepCanvasRung,
center: (i64, i64),
extent: (u32, u32),
min_wl_m: u32,
/// Boxed to keep `GenCompletion` variant sizes balanced — an
/// `EncodedStepCanvas` carries eight PNG/msgpack-encoded fields,
/// comparable to `WindowDerived`'s own boxing rationale.
canvas: Box<crate::atlas::step_canvas::EncodedStepCanvas>,
},
/// Work item failed — body_id or city_id for logging.
Failed { item: GenWorkItem, reason: String },
}
@@ -523,6 +604,36 @@ impl GenerationQueue {
}
}
/// Submit a `DeriveStepCanvas` item with per-connection-per-rung
/// coalescing (T-1181, mirroring `submit_window`'s exact discipline): if
/// an item for the SAME `(connection, body, rung)` is still pending
/// (not yet dispatched), it is replaced in place — a step-cross burst
/// that queues several requests for the same connection+body+rung before
/// the first is dispatched collapses to one derive. `item` MUST be a
/// `DeriveStepCanvas` variant; any other variant falls through to plain
/// `submit` with no coalescing.
pub fn submit_step_canvas(&self, item: GenWorkItem, priority: GenPriority) {
if let Some(key) = item.step_canvas_supersede_key() {
let key = (key.0, key.1.to_string(), key.2);
let mut pending = self.pending.lock().unwrap();
pending.retain(|q| {
q.item
.step_canvas_supersede_key()
.map(|k| (k.0, k.1.to_string(), k.2) != 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
@@ -988,6 +1099,66 @@ fn run_work_item(
reason: format!("heightmap load failed: {e}"),
},
},
GenWorkItem::DeriveStepCanvas {
body_id,
conn_id: _, // routing-only (queue-level coalescing); the derive itself is connection-agnostic
heightmap_path,
sea_level,
body_seed,
body_params,
placements,
rung,
center,
extent,
min_wl_m,
} => match load_heightmap_png(heightmap_path, body_id, *sea_level) {
Ok(hm) => {
// Same GRID_W×GRID_H downsample AnalyzeBody/DeriveWindow apply
// (D-202) — the true DistrictPos<->world-metres mapping must
// stay consistent across every derive path.
let working = if hm.width > GRID_W || hm.height > GRID_H {
hm.downsample(GRID_W, GRID_H)
} else {
hm
};
// Same per-body TerrainAnalysisCache LRU DeriveWindow already
// populates (T-1137 binding decision, extended here) — keyed
// on body_id alone, so a step-canvas request and a legacy
// district_window request for the same body share one
// ~45 ms run_layer1 re-derive, never pay it twice.
let (l1, ta) = terrain_cache
.lock()
.unwrap()
.get_or_derive(body_id, &working);
let climate = ClimateConstants::default();
let raw = crate::atlas::step_canvas::build_step_canvas(
*body_seed,
body_id,
body_params,
&ta,
&l1.river_network,
placements,
*rung,
*center,
*extent,
&climate,
*min_wl_m,
);
let canvas = crate::atlas::step_canvas::encode_step_canvas(&raw);
GenCompletion::StepCanvasDerived {
body_id: body_id.clone(),
rung: *rung,
center: *center,
extent: *extent,
min_wl_m: *min_wl_m,
canvas: Box::new(canvas),
}
}
Err(e) => GenCompletion::Failed {
item: item.clone(),
reason: format!("heightmap load failed: {e}"),
},
},
}
}