diff --git a/decisions/architecture.md b/decisions/architecture.md index 24cf87a50..e1119f82d 100644 --- a/decisions/architecture.md +++ b/decisions/architecture.md @@ -1481,4 +1481,28 @@ Technical foundation decisions that constrain implementation: engine, client-ser --- -*84 decisions (D-001 through D-224, excluding gaps). Last updated: 2026-05-23 (D-224 — SeedChain seed-derivation contract)* +### D-225: Atlas layer-stream proxy — compute-on-demand, mod-first (resolves Q-098) +- **Date:** 2026-05-23 +- **Decision:** How per-body generation-cascade layer data reaches the Godot client for the Phase-4 Atlas progress viewer (#960). **Resolves Q-098.** Derived layer data is **never baked** into the install — that would both bloat the install (~100MB+ for ~267 bodies) and make modded bodies second-class. Instead a server-side **layer-stream proxy** computes on demand from moddable source files and streams to the client: + + **(1) Transport — existing IPC stream + additive message tag.** Not a second socket, not a per-frame envelope rewrite. The bridge today carries no message-type discriminator (server→client is always `ObserverSnapshot`, client→server always `Vec`). The atlas request/response ride the *same* TCP stream as new message types, disambiguated **structurally** in v1 (a snapshot has `entities`/`tick`; the atlas messages do not). A full `BridgeMessage` envelope-everywhere migration is deferred — it would be a needless wire break, and client+server co-ship (D-005/D-192) so it can be done later as cleanup. + + **(2) Single framed MessagePack response, not chunked.** `Layer1Output` is ~hundreds of KB worst case (512×256), well under the 16 MB frame cap. The **raster is not streamed** — the client already loads `reliefmap.png` from disk; the proxy streams only the *computed* `Layer1Output` (rivers, basins, attractors + sub-biome). MessagePack matches the rest of the bridge. + + **(3) Mod-first source resolution.** A new `BodySourceResolver` with an ordered search: mod dirs (override) → base install (`wiki/...`, the floor). It reads the body's relative `terrain_reference` from `systems.db` (read-only) and returns the first existing `/`. First-party and mod bodies flow through the **identical** resolve→`run_cascade`→stream path — the `heightmap.png` is the sole source of truth, no baked layer data. v1 wires the base root only; the resolver type + search order exist and are tested with a synthetic mod root, so the seam is mod-first from day one. + + **(4) Cache via background queue on miss (D-203/D-206).** Request → `BodyWorldStateCache` lookup. Hit → serialize and return synchronously (a serialize, no compute). Miss → enqueue an `Immediate` `AnalyzeBody` on the `GenerationQueue` (D-206 background tier) and reply `Pending`; push the layer response when the completion lands. **The ~45 ms cascade must never run on the tick thread** (D-203: no blocking CPU/DB for heightmap data on main). The client shows the existing "TERRAIN DATA PENDING" panel until the response arrives. Eviction → recompute (always valid — determinism guaranteed). + + **(5) Whole `Layer1Output` per response; client composites additive overlays.** The layers are produced together in one drainage pass, so per-layer requests save no compute and only add round-trips. Overlay toggles (heightmap + rivers + attractors + sub-biome shown *together*) are a pure client-side render concern (the existing `_overlay_visibility` pattern). The request carries `body_id` + `up_to_layer` (a forward-compat seam; v1 honors `Topography`). + +- **Critical-path dependency:** the proxy is inert until `gen_queue.rs::run_work_item`'s `AnalyzeBody` actually runs `run_cascade` → builds `BodyWorldState` → populates the cache (today a documented stub, deferred from #142), and `GenCompletion::BodyAnalyzed` carries the computed state, not just `body_id`. This activation is the long pole and is tracked as its own ticket blocking #960. +- **Rationale:** Baking privileges first-party content (a mod body cannot ship baked artifacts it cannot produce) and adds install bloat. Computing from the moddable heightmap on demand — the cascade is deterministic and ~45 ms, and D-200/D-203/D-206 already provide the background-compute + LRU tiers — keeps mods first-class, adds zero storage, and uses the architecture as intended. Reusing the existing IPC stream (vs a second socket) avoids a parallel connection lifecycle for an occasional, user-initiated, latest-wins-irrelevant request. +- **Deferred / spun off:** the full `BridgeMessage` envelope-everywhere migration (later cleanup, not this ticket); the mod **content catalog** — mods adding *new* bodies need body rows + `terrain_reference` discoverable, but `systems.db` is binary / source-canonical (D-189) and mods cannot append to it → **Q-099**. D-225 resolves mod *file* resolution only; base-install resolution is enough to ship #960. +- **Implementation:** #960 (client viewer + proxy) plus the `AnalyzeBody` activation ticket. New surface: `server/src/atlas/source_resolver.rs`, `server/src/atlas/layer_proxy.rs`; `gen_queue.rs` `run_work_item` activation + `GenCompletion` payload; bridge receive/send branching; client `protocol.gd` / `sim_bridge.gd` / `atlas_viewer.gd`. +- **Raised by:** Jeroen (mod-first directive) + Tyre (design pass) + Claude, `/whats-next` refinement of #960, 2026-05-23. +- **Cross-reference:** Q-098 (resolved by this), Q-099 (mod content catalog — spun off), [D-191](#d-191) (Atlas viewer), [D-166](#d-166) (per-layer Atlas progress viewer), [D-200](#d-200) / [D-203](#d-203) (three-tier execution, LRU cache), [D-005](#d-005) / [D-192](#d-192) (client+server co-ship — no version handshake), [D-224](#d-224) (SeedChain — feeds the cascade the proxy runs) +- **Dissent:** None + +--- + +*85 decisions (D-001 through D-225, excluding gaps). Last updated: 2026-05-23 (D-225 — Atlas layer-stream proxy, resolves Q-098)* diff --git a/decisions/questions-architecture.md b/decisions/questions-architecture.md index 99b325df4..6e0c322da 100644 --- a/decisions/questions-architecture.md +++ b/decisions/questions-architecture.md @@ -331,11 +331,20 @@ Technical foundation questions: engine, protocols, data structures, performance, --- ### Q-098: Persistence of generated river/city mapping outputs -- **Status:** Open — to resolve during Phase 4 execution-model work (#952) +- **Status:** Resolved — D-225 (atlas layer-stream proxy), 2026-05-23 +- **Resolution:** Resolved against the question's own premise. The viewer does **not** need a durable store under the LRU: the cascade is deterministic and ~45 ms, so eviction → recompute is acceptable. Baking into `systems.db` (option a) is rejected — it bloats the install and makes modded bodies second-class. Decision: **lazy compute on demand**, served by a mod-first **layer-stream proxy** that resolves a body's source files (base + mod dirs) and streams the computed `Layer1Output` over the existing IPC bridge, backed by the D-203 in-memory LRU (miss → background `AnalyzeBody`; eviction → recompute). See [D-225](architecture.md#d-225). The mod-content-catalog corner (mods adding *new* body rows / `terrain_reference` to the binary `systems.db`) is spun off to Q-099. - **Question:** The deterministic cascade can recompute river courses (D8 drainage, D-208) and city placements (economic sim + attractor matching, D-211) from seed at any time, so persisting them is a *cost* optimization, not a correctness need. But the compute is expensive — recomputing per session or per atlas view is waste. How are these mapping outputs persisted so they are computed **once per body and kept**? D-203's BodyWorldState cache is an LRU — it *evicts* (volatile). The fork: **(a)** build-time bake into `systems.db` (D-200 build-time tier — precompute all, ship); **(b)** lazy compute + persist at runtime (cache DB / savegame — compute on first visit, keep); **(c)** hybrid. Whatever the answer, the in-memory LRU should sit over a *durable* store so eviction triggers a cheap reload, not a recompute. - **Context:** Raised 2026-05-22 looking ahead from the Phase 4 markers strip (#951). Refines D-200 (three-tier execution) and D-203 (LRU cache). Gates the Atlas layer viewer (#960), which needs persisted mapping to render without recomputing. Determinism (#952) guarantees recompute is always a valid fallback. - **Cross-reference:** [D-200](architecture.md#d-200), [D-203](architecture.md#d-203), [D-208](architecture.md#d-208), [D-211](architecture.md#d-211), #952 (determinism harness), #960 (atlas viewer) --- -*47 questions (8 resolved, 1 partially resolved, 38 open). Last updated: 2026-05-22.* +### Q-099: Mod content catalog — body rows / terrain_reference overlay for systems.db +- **Status:** Open — spun off from D-225 (2026-05-23) +- **Question:** D-225 resolves mod *file* resolution (a mod body's source `heightmap.png` is found by searching mod dirs over the base install). But a mod adding a *new* body also needs that body discoverable: the `bodies` row and its `terrain_reference` live in `systems.db`, which is binary and source-canonical (D-189) — mods cannot append to it. How does a mod register new bodies (and other DB-resident catalog rows)? Options: a mod manifest the server merges into an in-memory catalog overlay at load; a parallel mod catalog DB layered over the `systems.db` reads; or a documented mod build step. Out of scope for #960 (base-install resolution ships the viewer); needed before third-party bodies are first-class. +- **Context:** Raised 2026-05-23 from the D-225 mod-first layer-stream proxy design. The proxy makes first-party and mod bodies flow through an identical resolve→compute→stream path *given a resolvable source file*; this question is the remaining gap — getting a mod's new body into the catalog the resolver consults. +- **Cross-reference:** [D-225](architecture.md#d-225), [D-189](architecture.md#d-189) (systems.db source-canonical), #960 (atlas viewer — base-install only for now) + +--- + +*48 questions (9 resolved, 1 partially resolved, 38 open). Last updated: 2026-05-23.*