feat(simulation): activate background generation tier end-to-end (#968, D-206/D-225)

The D-206 queue (#924) and D-203 cache (#917) were built but never connected to
the running app — the whole background tier was inert. This closes the loop:
submit -> Rayon -> cascade -> completion -> drain -> cache.

- gen_queue.rs: GenWorkItem::AnalyzeBody now carries enqueuer-resolved inputs
  { body_id, heightmap_path, sea_level, body_seed: SeedChain } (D-225 boundary —
  run_work_item stays pure compute, no path/DB resolution). run_work_item runs
  the real cascade: load heightmap.png -> downsample to GRID_W×GRID_H working
  grid (D-202) -> run_layer1 -> BodyWorldState; load failure -> Failed.
  GenCompletion::BodyAnalyzed carries the computed BodyWorldState.
- cascade.rs: CascadeSnapshot::into_body_world_state() conversion.
- plugin.rs (new): GenerationPlugin registers GenerationQueue +
  BodyWorldStateCache and adds a PreInput drain system that inserts BodyAnalyzed
  states into the cache (off the Rayon workers — a cheap channel drain, never
  the ~45ms cascade). Wired into both the production and test app setups.

Tests run the real cascade on a tiny temp heightmap PNG (no committed fixture);
the plugin test proves the full submit->...->cache loop. The proxy (#969) is the
production submitter.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-24 10:47:59 +02:00
co-authored by Claude Opus 4.7
parent 53263492ab
commit 8cedf670f0
5 changed files with 239 additions and 44 deletions
+24
View File
@@ -17,6 +17,7 @@
use std::path::Path;
use crate::atlas::body_world_state::{BodyWorldState, RiverNetwork};
use crate::atlas::heightmap::{self, BodyHeightmap, HeightmapLoadError};
use crate::atlas::layer1::{self, Layer1Output};
use crate::seed::SeedChain;
@@ -48,6 +49,29 @@ pub struct CascadeSnapshot {
pub layer1: Option<Layer1Output>,
}
impl CascadeSnapshot {
/// Convert into a [`BodyWorldState`] for the D-203 cache (#968). Moves the
/// heightmap raster and the Layer-1 outputs in; `last_accessed` starts at 0
/// (the cache stamps it on read). A snapshot that stopped at Layer 0 yields
/// empty river/basin/attractor data.
pub fn into_body_world_state(self) -> BodyWorldState {
let (river_network, drainage_basins, attractors) = match self.layer1 {
Some(l1) => (l1.river_network, l1.drainage_basins, l1.attractors),
None => (RiverNetwork::default(), Vec::new(), Vec::new()),
};
BodyWorldState {
body_id: self.body_id,
heightmap: self.heightmap.data,
heightmap_width: self.heightmap.width,
heightmap_height: self.heightmap.height,
river_network,
drainage_basins,
attractors,
last_accessed: 0,
}
}
}
/// Run the cascade from a heightmap already in memory, up to `up_to`.
///
/// Pure (no I/O); this is the testable core. `body_seed` is this body's