From fa8b6ebf0829164bb3c1a180728eb294261965db Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Sun, 7 Jun 2026 12:14:55 +0200 Subject: [PATCH] docs(governance): file D-239 tile derivation contract, resolve Q-101 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolves Q-101 — how a coarse Layer-1 cell + seed becomes coherent ~1m voxel geometry across the scale jumps. The tile-derivation-contract workshop output: - three-carrier refinement chain RegionProfile -> ChunkContext -> VoxelColumn, pure deterministic, no authoring at the derivation layers - district-temperature climate primitive (2x2 km, C, nullable) + separate moisture; everything climatic derives from temperature(+moisture) - scattered, transient freeze/snow model (freshwater +5..-10, sea ice own band, snow moisture-gated; forms cold phase / melts warm phase) - stateless f64-to-voxel domain warp (anti-squaring) - 8 morphology families over a frozen 17-zone vocabulary, gated decision tree - seams prevented at source (gate ordering + build-time matrix); valid geomorphic seams kept sharp + warped Includes workshop brief, round-1 positions, and workshop-outcomes.md. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../tile-derivation-contract/D-239-draft.md | 96 +++ .../gestalt-round1.md | 595 ++++++++++++++++++ .../tile-derivation-contract/miri-round1.md | 103 +++ ...tile-derivation-contract-workshop-brief.md | 85 +++ .../troblum-round1.md | 476 ++++++++++++++ .../tile-derivation-contract/tyre-round1.md | 342 ++++++++++ .../workshop-outcomes.md | 102 +++ governance/README.md | 3 +- governance/decisions/architecture.md | 37 +- governance/questions/architecture.md | 3 +- 10 files changed, 1839 insertions(+), 3 deletions(-) create mode 100644 docs/workshops/tile-derivation-contract/D-239-draft.md create mode 100644 docs/workshops/tile-derivation-contract/gestalt-round1.md create mode 100644 docs/workshops/tile-derivation-contract/miri-round1.md create mode 100644 docs/workshops/tile-derivation-contract/tile-derivation-contract-workshop-brief.md create mode 100644 docs/workshops/tile-derivation-contract/troblum-round1.md create mode 100644 docs/workshops/tile-derivation-contract/tyre-round1.md create mode 100644 docs/workshops/tile-derivation-contract/workshop-outcomes.md diff --git a/docs/workshops/tile-derivation-contract/D-239-draft.md b/docs/workshops/tile-derivation-contract/D-239-draft.md new file mode 100644 index 000000000..a94e6d625 --- /dev/null +++ b/docs/workshops/tile-derivation-contract/D-239-draft.md @@ -0,0 +1,96 @@ +# D-239 (REVISED post-verification): Tile derivation contract — coarse→fine refinement chain (Q-101) + +domain: architecture · type: confirmed · date: 2026-06-07 · resolves: Q-101 + +**Decision.** The walkable tile is materialised by a **three-carrier refinement chain**, each +stage a pure deterministic function of `(seed, atlas, body-params, position)` per D-227: +`RegionProfile` (~1 km) → `ChunkContext` (64 m) → `VoxelColumn` (1 m). Each scale boundary is its +own derivation with its own failure modes. + +### Determinism — no authoring at the derivation layers, ever +L1–L5 derivation has **zero per-body override hooks**. All authorial control lives **upstream** at +the body-parameter / atlas / system-specialization layer; the gating params (`GlaciationGrade`, +`precipitation_class`, `RIVER_THRESHOLD`, `tectonic_class`) are **derived** from body params +(stellar type / orbit / hydrosphere / lithology), never authored. `RIVER_THRESHOLD` specifically is +a derived **per-body-class** value `(hydrosphere, tectonic_activity, precipitation_class) → threshold`, +not the global constant `200`. Lore-anchored bodies are honoured by **setting params** and stand as +**validation cases** — if a body reads wrong, fix its params, never patch the derivation. +**Validation caveat:** derived terrain can only honour a lore body if its params *permit* it — e.g. +tidal flats require a moon param (the D-228 tidal term); confirm each validation body's params before +treating its terrain as a contract (Velen's tidal-flat coast presumes Velen has a moon — verify). + +### Anti-"squaring" (warp) +A global position-keyed **domain-warp field** suppresses grid/seam artifacts. The warp is a +**stateless, hash-based pure function of `(seed, body_id, position)`** — no lookup table, no +thread-local cache (so it's order-independent across threads/platforms). It keeps **f64 sub-metre +precision through to the final voxel coordinate, then quantises by truncation to the integer voxel +address** — a cast, not a comparison, so IEEE-754-deterministic across targets. The ±8 m warp range +makes FMA-contraction ULP variance (~1e-15 m) unable to shift the rounded voxel, so no platform +guards are needed. The warp is *position math*, not a structural decision — D-010 integer discipline +on all material/morphology decisions is preserved. + +### Morphology — 8 families +Selected by a strict **decision tree over integer `RegionProfile` inputs**: LavaField · FjordWall · +CliffCoast · BraidedDelta · DuneStrand · IncisedGorge · MeanderReach · AlluvialPlain (fallback). +Requires adding `tectonic_class` to `RegionProfile`. MountainPass is a zone label sharing IncisedGorge +geometry. **Hard gates (boolean, applied pre-selection in tree order):** fjord requires +`GlaciationGrade ≥ 2`; LavaField requires `tectonic_class = Volcanic`; lithology bounds slope/form +(see laws below). + +### Frozen 17-zone `MorphologyZone` display vocabulary +Derived labels per D-228, **enum→label map frozen at authoring time**. The canonical freeze point is +the Rust `MorphologyZone` enum; **adding/renaming a zone requires a D-record amendment** (a code +reviewer enforces this). The 17: open ocean, lake, tidal flat, dune strand, cliff coast, fjord, +delta, estuarine, alluvial plain, river bank, meander reach, braided plain, valley floor, mountain +pass, alpine, volcanic, wetland. **Note — zones ≠ families:** four zones (tidal flat, estuarine, +alpine, wetland) have no distinct generator family; they are derived sub-classifications from +family + elevation/water-height conditions (e.g. tidal flat = BraidedDelta at the tidal margin; +alpine = AlluvialPlain/IncisedGorge above the treeline-elevation threshold). + +### Zone seams — prevent incompatible, allow valid (don't patch) +The boolean gates are discontinuities on continuous inputs, so "continuous→continuous" is NOT the +mechanism. The mechanism is the **decision-tree gate ordering + a build-time compatibility matrix**: +*incompatible* family pairs (e.g. MeanderReach↔Volcanic — geomorphically incoherent) cannot be +adjacent classifier outputs; the matrix is a **build-time invariant test**, never a runtime override. +*Valid* geomorphic seams (cliff↔fjord at the glaciation threshold, lithology faults) are **permitted +sharp transitions** — kept sharp (real geology is sharp), made non-grid/organic by the warp; no +modulation feathering. The warp also guarantees seams don't produce degenerate slivers at chunk scale +(preserving the per-chunk tactical-content rule below). + +### Believability laws (binding — inline, not just titles) +- **Drainage monotonicity:** channels descend; respect the D8 thalweg (D-208); tributaries join from + upstream; river mouths sit at sea level. +- **Lithology → landform:** Rock → vertical faces/cliffs; Sand → ≤ angle of repose (~32°), dunes not + cliffs; Gravel → braided channels/fans, not single-thread meander; Soil → rolling/floodplain; + Wetland → ≤5° flats; Lava → sheets/shield slopes + lava tubes, immature drainage. +- **Glaciation → form:** fjord requires GlaciationGrade ≥ 2; U-valleys ≥ 1; moraine ridges ≥ 1; + cirques ≥ 2. GlaciationGrade 0 high-relief is V-ridges (fluvial), never glacial U-profiles. +- **Climate → vegetation:** treeline transitions Forest → Scrub → Barren (no Forest→Barren skip); + riparian Thicket/Scrub band 1–3 tiles along every perennial waterway; boundaries follow + contours/moisture, not chunk edges. + +### Game-feel constraints +Every chunk carries ≥1 tactical decision point; cover ≠ concealment (mechanically distinct); +high-ground asymmetric-but-not-dominant; chokepoints narrow enough to matter (river crossings +3–15 m, gorge floors 2–8 m); **seasonal/tidal state must produce real passability changes, not +cosmetic ones** — BraidedDelta/MeanderReach `ElevationDelta` calibrated so channels fall below the +Q-105 high-water threshold and levees stay above it (flood = genuinely impassable, not recoloured). + +### Mechanics +- No per-tile `flow_direction[64×64]` in `ChunkContext` (D8 ≈ 152 m/cell — coarser than a chunk) → + one basin-direction + global meander-curve params. Features with wavelength > 64 m (meanders, + dunes, cliff continuations) seed from **Region-or-higher**, not the chunk seed. +- **Caching:** `RegionProfile` is stored in `BodyWorldState` (D-203) (~6k/body, populated in the + D-206 background pass) so the Atlas reads zone labels without triggering voxel derivation; voxel + derivation is on-demand + cached, never persisted (D-227). Budget ~2.2–4.2 ms/chunk **(validate + per-family in Phase 4 — FjordWall/IncisedGorge are far costlier than the AlluvialPlain fallback).** + +**Resolves:** Q-101. **Touches:** Q-102 (cohesion = the warp). **Leaves open:** Q-103 (mutator op +schema), Q-105 (region seasonal/clock state — the ElevationDelta calibration above is a forward +contract to it). +**Cross-refs:** D-227, D-228, D-203, D-206, D-208/209/210, D-010, D-234, D-142, D-217. +**Raised by:** tile-derivation-contract workshop (Tyre, Gestalt, Troblum, Miri) + lead-interviewed +decisions, 2026-06-07; verified by an adversarial pass from the same four. +**Dissent:** Tyre's initial cross-family elevation-blend was resolved against (prevent-at-source). +Early-integer-truncation of the warp (raised against Gestalt's `ElevationDelta` ranges and by Tyre) +was resolved against in favour of f64-to-voxel. diff --git a/docs/workshops/tile-derivation-contract/gestalt-round1.md b/docs/workshops/tile-derivation-contract/gestalt-round1.md new file mode 100644 index 000000000..e6fd7921b --- /dev/null +++ b/docs/workshops/tile-derivation-contract/gestalt-round1.md @@ -0,0 +1,595 @@ +--- +title: "Gestalt Round 1 — Morphology Algorithm Family + Game-Feel Constraints" +workshop: tile-derivation-contract +participant: Gestalt +round: 1 +date: 2026-06-07 +--- + +# Gestalt Round 1: The Morphology Family and What Terrain Must Do + +Let me break down what this actually means mechanically. We have two distinct +problems that must be solved simultaneously: (1) the classification logic that +selects which algorithm runs for a given region, and (2) the game-feel contract +that algorithm must satisfy. These are not separable — an algorithm that is +geologically plausible but produces flat, featureless terrain at the 64 m chunk +scale is a failed algorithm regardless of its realism score. + +--- + +## Framing: What the Morphology Algorithm Actually Produces + +Tyre's API contract gives each morphology family a clean interface: + +- **Receives:** `ChunkContext` (RegionProfile + warped position + elevation grid + + flow data) +- **Produces:** `ElevationDelta: i16` per voxel column (signed integer metres, + delta from base heightmap) + +That `ElevationDelta` output is then consumed by the material/vegetation/water +derivation steps. So the morphology family's job is: given a region's coarse +character, produce the fine-grain elevation variation within each chunk that (a) +reads as physically coherent and (b) produces tactically interesting geometry. + +The D-228 axis for "shape / geometry" is explicitly `f(elevation-step × material)` — +sand slumps, rock faces go vertical. The morphology algorithm drives `elevation`; +material drives how that elevation is expressed as renderable geometry. These two +together produce cover, line-of-sight, and chokepoints. + +--- + +## Part 1: The Six Morphology Families + +I am enumerating eight families, not six — six is the minimum; eight is what the +world actually needs. The selector logic is why this makes sense: some of these +are statistically rare (fjord, lava field) but their game-feel is so distinct +that collapsing them into a broader family would produce detectable sameness. + +### Selection Logic Overview + +The selector is a decision tree over integer inputs. All inputs are available in +`RegionProfile` as Tyre has sketched it. I will specify what each input node +needs to be, and flag one addition. + +**Inputs required (all integer or enum — D-010 compliant):** + +| Input | Type | Source | +|-------|------|--------| +| `slope_class` | `Flat / Gentle / Steep / Cliff` | 3×3 elevation kernel, already in RegionProfile | +| `drainage_class` | `None / Ephemeral / Perennial / Major` | D8 flow accumulation | +| `sea_level_margin` | `i32` metres | elevation − sea_level | +| `lithology` | enum | body params + elevation strata | +| `glaciation` | `u8` grade 0–4 | body params (temperature + tectonic age) | +| `tectonic_class` | `Passive / Active / Volcanic` | body params (new — see below) | +| `sub_biome` | enum | D-210 | + +**One addition to RegionProfile I need:** `tectonic_class: TectonicClass` (Passive / +Active / Volcanic). This is a body-level parameter (like `hydrosphere` and +`tectonic_activity`) that modulates which high-energy families are eligible. A +geologically dead body cannot produce lava fields or fault scarps. This keeps the +classification honest and prevents algorithm outputs that contradict the body's +established character. + +--- + +### Family 1: Meander Reach + +**Selection context:** +- `slope_class` = Flat or Gentle +- `drainage_class` = Perennial or Major +- `sea_level_margin` > +20 m (above tidal influence) +- `glaciation` <= 1 (not glaciated) +- `lithology` = Alluvial or Sedimentary + +**What the algorithm produces:** + +A sinuous channel with a defined thalweg (deepest flow line), pointbar deposits +on the inside of bends, and a cutbank face on the outside. The active floodplain +is 3–12 m below the surrounding terrace. Oxbow lakes appear where prior meanders +were cut off — these are ponds of still water on the floodplain, 4–20 m across. + +The meander geometry is driven by a parametric sine-distortion of the D8 flow +direction at chunk scale, with the amplitude controlled by `meander_intensity` +(already in `RegionProfile`). The sinuosity parameter (ratio of channel length +to valley length) ranges 1.3–2.8 depending on `meander_intensity`. + +**ElevationDelta range:** −8 to +4 m (channel is below floodplain; terrace is +above; total relief within a 64 m chunk is modest). + +**Tactical character:** Long sight lines broken by river banks and terrace edges. +The pointbar is open ground with poor cover; the cutbank is a steep earthen wall +(1–3 m, provides hard cover). Oxbow lakes are impassable unless frozen or bridged. +River crossing is a chokepoint — the channel itself, 3–15 m wide, forces engagement +on the banks. Meander bends create natural flanking ambush positions where the bank +curves away from a pursuer's sightline. + +--- + +### Family 2: Incised Gorge / Badland + +**Selection context:** +- `slope_class` = Steep or Cliff +- `drainage_class` = Perennial or Major +- `sea_level_margin` > +50 m (high above sea level) +- `tectonic_class` = Active (or high uplift rate from body params) +- `lithology` = Sedimentary or Metamorphic + +**What the algorithm produces:** + +A V-shaped or slot canyon geometry. The channel is narrow (2–8 m wide at floor) +and deeply incised (15–80 m below the surrounding plateau). Canyon walls are +vertical or near-vertical rock faces. At the top, the plateau surface continues +at the base heightmap elevation. Talus slopes appear at the base of cliff faces +(coarser scree — Gravel lithology — where the cliff transitions to channel floor). + +Within a 64 m chunk, a gorge manifests as: the plateau surface at base elevation +(flat or gently rolling), a cliff edge dropping sharply, the canyon floor, and +the far cliff rising again. Not every chunk contains the full cross-section — +chunks on the plateau see only the cliff edge; chunks on the floor see only walls. + +**ElevationDelta range:** −80 to 0 m (plateau stays at base; gorge cuts downward). + +**Tactical character:** Extreme vertical asymmetry — the dominant tactical variable. +A character on the plateau rim has total LOS advantage over the floor, can fire +down, cannot be flanked from below. But cliff faces are un-climbable without gear +(a mechanical constraint the tile data supports: Cliff shape on Rock material = +impassable). Gorge floors are extremely exposed if the rim is held. Gorge passages +are decisive chokepoints — a party controlling a narrow canyon floor controls the +route. The slot geometry creates situations where the player cannot retreat without +crossing the enemy's field of fire. + +--- + +### Family 3: Braided Delta / Distributary Fan + +**Selection context:** +- `slope_class` = Flat or Gentle +- `drainage_class` = Major +- `sea_level_margin` between −5 and +15 m (near sea level) +- `lithology` = Alluvial +- NOT `glaciation` >= 3 + +**What the algorithm produces:** + +Multiple shallow, anastomosing channels (2–6 m wide, 0.3–1.5 m deep) dividing +and rejoining across a nearly-flat fan surface. Islands of slightly higher ground +(natural levees and splays) sit between channels. The whole surface is within 2–4 m +of sea level; many tiles are waterlogged or tidal-flat state at high water. + +Channel positions are driven by a multi-seed Voronoi partition of the chunk area +with D8 flow direction as an attractor — each Voronoi center generates a channel +branch. The `meander_intensity` parameter controls how much the branches deviate +from a straight downslope path. + +**ElevationDelta range:** −2 to +3 m (almost flat; channels are slight incisions; +levees are slight rises). + +**Tactical character:** Mobility is the dominant constraint. The network of small +channels fragments the surface into irregular islands — movement requires knowing +which crossings are wadeable (Shallow Water at low tide) vs blocked (Deep Water +at high tide). The Q-105 tidal/seasonal water model makes the same terrain +tactically different at different times of day. Cover is minimal (low, flat +vegetation, no high ground) but concealment is high (dense reed beds, tall grass, +poor LOS in all directions). Ambushes are set in reed cover; pursuit is slow; +routing is non-obvious. + +--- + +### Family 4: Dune Strand / Aeolian Plain + +**Selection context:** +- `sea_level_margin` between −2 and +30 m +- `lithology` = Sand +- `sub_biome` = Arid or Coastal +- `drainage_class` = None or Ephemeral +- `slope_class` = Flat or Gentle + +**What the algorithm produces:** + +Transverse or barchan dune forms, oriented perpendicular to `dune_orientation` +(compass octant in RegionProfile — prevailing wind). Dune height 2–12 m; +inter-dune corridors are flat sand at base elevation. The slip face (steep lee +side, ~32° angle of repose) is the sharp edge; the windward stoss side is gentle. + +Within a 64 m chunk: typically 1–3 dune crests visible, with inter-dune troughs. +The pattern repeats with seed-driven wavelength variation (0.7–1.4× nominal spacing) +to prevent visible periodicity. + +**ElevationDelta range:** −3 to +12 m (troughs below base; crests above). + +**Tactical character:** Dune crests are the key terrain feature — short-range cover +on the stoss side (the gentle approach), sudden exposure on the slip face (the sharp +drop). Crest control is inherently ephemeral in the sense that the geometry forces +skyline exposure — you are visible from anywhere on the stoss side the moment you +crest. The inter-dune corridors are covered routes but lead into bowls with no +exit cover. Movement slows on loose sand (a material-driven movement penalty). +LOS is extremely range-dependent: from a dune top you see far; in a trough you see +perhaps 20 m. + +--- + +### Family 5: Cliff Coast / Sea Stack + +**Selection context:** +- `sea_level_margin` between −20 and +10 m +- `slope_class` = Steep or Cliff +- `lithology` = Rock or Metamorphic (NOT Sand — that is Dune Strand) +- `glaciation` <= 1 + +**What the algorithm produces:** + +A wave-cut platform at or just below sea level, then a cliff face rising 10–80 m +to a clifftop plateau. The cliff face is sheer rock (Cliff shape, Rock material, +impassable). Sea stacks are isolated rock pillars standing in shallow water +offshore — generated as local elevation spikes in the sub-sea zone where a noise +field exceeds a lithology-dependent threshold (hard rock produces stacks; soft rock +produces a smooth platform). + +The clifftop is the base heightmap elevation; the platform and stack geometry are +ElevationDelta downward from there (platform −10 to −30 m relative to clifftop, +meaning near sea level; stacks are +0 to +20 m above sea level but below the main +cliff top). + +**ElevationDelta range:** −30 to 0 m (cliff is a step-down to the coast; no upward +delta from clifftop). + +**Tactical character:** Unambiguous dominance of the high ground. Cliff-edge +positions are impregnable from below (cliff face is impassable) and exposed from +above. Clifftop defenders have infinite LOS over the ocean approach. Attackers from +the sea face a wall. The wave-cut platform, when exposed at low tide, is a +short-duration tactical route — accessible only during specific water states (the +Q-105 tidal model), creating timed-window scenarios. Sea stacks provide intermediate +cover in an otherwise featureless marine approach — the only interrupt of a long +open crossing. + +--- + +### Family 6: Fjord Wall / Glaciated Inlet + +**Selection context:** +- `sea_level_margin` between −50 and +200 m +- `slope_class` = Cliff (required) +- `glaciation` >= 2 +- `lithology` = Rock or Metamorphic + +**What the algorithm produces:** + +Near-vertical rock walls rising from deep water. The U-shaped cross-section +(glacial, not V-shaped fluvial) means the wall base is at or below sea level and +the wall face continues upward to the fjord rim hundreds of meters above. Hanging +valleys produce waterfalls (a narrow shelf partway up the wall where a tributary +glacier once entered). The water within the fjord is deep (ElevationDelta far +below sea level on the water side). + +At the 64 m chunk scale: most fjord wall chunks are almost entirely cliff face. +The only variation is whether the chunk is at the waterline (bottom of cliff meets +water), mid-wall (continuous cliff face), or near the rim (cliff top with overhanging +vegetation at the edge). Hanging valley shelves appear occasionally as a narrow +flat ledge at a consistent elevation band across the wall. + +**ElevationDelta range:** −200 to +50 m (deep water below; wall can rise significantly +above base heightmap where the hanging valley creates a ledge). + +**Tactical character:** Maximum verticality. The fjord is effectively a sealed +corridor — the wall faces are impassable; movement is restricted to the narrow +strip at the water's edge (when it exists) or to the fjord rim. The hanging valley +shelves are the critical feature: a narrow ledge 40–80 m above the waterline that +provides fire positions looking down the full length of the fjord. Controlling a +hanging valley shelf means controlling the entire fjord below. Approach by water is +extremely exposed — no cover, walls on both sides, no retreat options. + +--- + +### Family 7: Alluvial Plain / Floodplain Terrace + +**Selection context:** +- `slope_class` = Flat +- `drainage_class` = None or Ephemeral (no active major channel in this region) +- `sea_level_margin` > +5 m +- `lithology` = Alluvial or Sedimentary +- `glaciation` <= 1 + +**What the algorithm produces:** + +The default "open country" family. Low-relief terrain with gentle undulation from +a filtered noise field. Occasional low terrace edges (1–3 m) mark former floodplain +levels. Isolated stone outcrops (lithology-dependent) provide local relief. The +dominant feature is the long sight line — this is the family where LOS extends to +the full render distance. + +**ElevationDelta range:** −3 to +5 m (mild undulation, terrace edges). + +**Tactical character:** This is the exposed terrain family, and it is here by design. +The game needs open ground — not every biome should be a maze of cover. Alluvial +plains force movement decisions about crossing open ground, using dead ground +(slight hollows where elevation puts you below the local crestline), and timing +movement to reach scattered cover (the occasional outcrop or terrace edge). The +terrace edges are the key tactical micro-feature: a 2 m earthen drop that provides +hard cover from one direction but exposes you from the other. + +--- + +### Family 8: Lava Field / Volcanic Surface + +**Selection context:** +- `tectonic_class` = Volcanic +- `lithology` = Lava (basalt) +- `sub_biome` = Volcanic or Barren + +**What the algorithm produces:** + +Two sub-types depending on lava flow age (body parameter): +- **Pahoehoe (fresh/smooth):** Gently rolling surface with occasional lava tubes + (collapsed tunnels — impassable gaps, 2–8 m across, 5–20 m deep). Glassy, + uneven surface with no vegetation. +- **A'a (older/rough):** Sharply irregular surface with scoria ridges 1–4 m high. + Near-impassable except along flow channels where the surface smoothed. + +**ElevationDelta range:** −20 to +8 m (lava tubes cut down; scoria ridges add height). + +**Tactical character:** Irregular terrain with extreme movement costs on A'a surface +(material: Lava, movement penalty severe). Lava tubes are natural ambush corridors — +narrow, enclosed, high-concealment, but with no exit options once committed. The +pahoehoe surface, while traversable, produces a distinctive movement sound profile +(glassy clicks vs soft dirt) that may affect stealth mechanics. + +--- + +## Part 2: The Selection Decision Tree + +This is the classifier that produces `MorphologyFamily` from `RegionProfile`. It +is a deterministic decision tree, not a probability distribution — each input +combination maps to exactly one primary family (with a secondary family for +boundary blending, as Tyre's API already anticipates). + +``` +if tectonic_class == Volcanic AND lithology == Lava: + → LavaField + +else if sea_level_margin IN [-200, +10] AND slope_class IN [Steep, Cliff]: + if glaciation >= 2: + → FjordWall + else: + → CliffCoast + +else if sea_level_margin IN [-5, +15] AND drainage_class == Major AND lithology == Alluvial: + → BraidedDelta + +else if lithology == Sand AND drainage_class IN [None, Ephemeral]: + → DuneStrand + +else if drainage_class IN [Perennial, Major] AND slope_class IN [Steep, Cliff] + AND sea_level_margin > +50: + → IncisedGorge + +else if drainage_class IN [Perennial, Major] AND slope_class IN [Flat, Gentle] + AND sea_level_margin > +20: + → MeanderReach + +else: + → AlluvialPlain ← the default / open-country family +``` + +**Why AlluvialPlain is the fallback:** It is the correct geological answer for +"didn't meet any special conditions." It is also the most tactically important +family to have abundant — players need open ground. If everything is a special +terrain family, every encounter becomes a cave-fight. + +**Secondary family for blending:** When a chunk sits on a region boundary, the +secondary family is the result of running this tree on the adjacent `RegionProfile`. +The families that blend gracefully (MeanderReach + AlluvialPlain) are common and +well-behaved. The families that do NOT blend gracefully (FjordWall + DuneStrand) +should never be adjacent — and if the world generates that adjacency, it indicates +a body parameter inconsistency that the body-params validator should flag. + +--- + +## Part 3: Family Transition Boundaries — Where the Families Actually Meet + +These are the terrain edges that players will encounter most often. They are +not smooth transitions — they are geomorphological discontinuities, and that is +correct. The domain warp (Tyre's mechanism) prevents the discontinuity from +aligning with a chunk grid line, but the discontinuity itself should be sharp. + +**Meander Reach → Alluvial Plain:** +The river floodplain (Meander) transitions to the surrounding upland (Alluvial) +at the terrace edge. This is the most common transition in lowland terrain. +Tactically: crossing from open plain to the floodplain means descending the terrace +edge — a 1–3 m earthen step that provides cover in one direction. + +**Incised Gorge → Alluvial Plain:** +The plateau continues as AlluvialPlain; the gorge cuts into it. From the surface, +this appears as a sudden cliff edge. The transition is the most dramatic visible +boundary in the game — you walk across open ground and the terrain simply drops +away. Tactically: the cliff edge is a LOS boundary — you cannot see into the gorge +until you are at the rim. + +**Braided Delta → Meander Reach:** +Moving upstream, the multi-channel delta collapses into a single meandering channel. +The transition is gradual — channels reduce in number, ground height increases +slightly. Tactically: the covered, fragmented delta gives way to the more open +meander terrain. + +**Cliff Coast → Alluvial Plain:** +Clifftop terrain is AlluvialPlain at the heightmap elevation; below is CliffCoast. +The transition is the cliff edge — same character as Gorge → Plain but coastal. + +**Fjord Wall → AlluvialPlain (at the fjord head):** +At the landward end of a fjord, the walls close off and the valley floor becomes +accessible. This narrow valley floor is AlluvialPlain — a classic settlement site. +Tactically: the fjord head is the one location where the wall-dominated fjord opens +into traversable terrain. It is always a strategic point. + +--- + +## Part 4: Game-Feel Constraints the Contract Must Satisfy + +This is the section I am most insistent on. The morphology algorithm family must +satisfy these constraints, or it fails as a design — regardless of geological +fidelity. These are not aesthetic preferences; they are mechanical requirements +that flow from D-008 (action design) and the five pillars. + +### Constraint 1: Every chunk must contain at least one tactical decision point + +A "tactical decision point" is any terrain feature that creates a meaningful +choice about movement, positioning, or engagement: a crest, a bank, a chokepoint, +a crossing, a blind corner, a piece of cover. + +Alluvial plain is the hardest family to satisfy this constraint — flat terrain +with no features is not interesting. The solution: the terrace edges (1–3 m +earthen drops, never absent from AlluvialPlain chunks) and the isolated outcrops +(seed-driven, present in ~60% of chunks) always provide at least one feature. +The constraint is violated if an algorithm produces a perfectly flat, featureless +64×64 m surface. This must be checked during family implementation. + +### Constraint 2: Cover and concealment must be distinct + +D-228's composite tile schema gives us the tools: `TerrainMaterial` (hard cover +— does a rock stop a projectile), `Vegetation` (soft cover / concealment — does +a reed bed block LOS), and `elevation` / shape (geometric cover — am I below +the crestline). These are orthogonal axes, and a well-designed morphology family +exploits the distinction. + +- Meander pointbar: low elevation (no geometric cover) + dense Vegetation + (concealment) + Alluvial soil (no hard cover) → concealment without protection +- Gorge cliff face: Cliff shape (un-crossable) + Rock material (hard cover) + no + Vegetation → hard cover with no concealment +- Dune crest: geometric cover only (elevation blocks LOS from below) + no hard cover + (sand stops nothing) + no concealment (bare sand, no vegetation) +- Reed delta: Vegetation concealment + no geometric cover + no hard cover → the + ambush terrain + +The morphology algorithm must place these in combination, not uniformly. A family +that produces uniform concealment throughout is not interesting. The good terrain +is the one where the player has to read the ground to understand which tiles give +which kind of protection. + +### Constraint 3: Verticality must create asymmetric engagement conditions + +D-008 explicitly calls for Z-levels and vertical asymmetry. The terrain derivation +is the source of that asymmetry at the world level (buildings create it at the +settlement level). The constraints: + +- **The high ground must be visibly identifiable** from the approach. Players should + be able to see that the gorge rim dominates the floor, that the dune crest exposes + them, that the fjord shelf controls the inlet. The rendered geometry must make this + legible — and that geometry comes from the ElevationDelta values the morphology + algorithm produces. + +- **The high ground must not be universally dominant.** If every elevated position + is trivially superior, the game collapses to "whoever controls the high ground + wins." The counter: (a) the high ground is often exposed from range (silhouette), + (b) close terrain (gorge wall, dense reed bed) negates ranged advantages, (c) the + cliff face is impassable from below but also from above (you cannot exploit the + position you cannot reach). The morpology algorithm must produce terrain where + the high ground advantage is real but contestable. + +- **Chokepoints must be narrow enough to matter.** A river ford that is 50 m wide + is not a chokepoint. The meander algorithm must produce crossings narrow enough + (3–15 m) that controlling one bank can meaningfully contest passage. The gorge + floor must be narrow enough (2–8 m at the cliff base) that it cannot be flanked. + +### Constraint 4: The derivation must be legible at the Atlas layer + +The Atlas (Phase 3–4 deliverable) must show morphology zone boundaries as visible +map features — players planning movement at the region scale need to read the terrain. +This means: + +- `RegionProfile.morphology_zone` must map cleanly to a distinct Atlas color / icon + — Tyre's API already provides this; I am confirming the family vocabulary supports + this legibility. +- The transition between families must be visible at Atlas resolution (the 1 km + region scale) even though the texture of that transition is driven by chunk-scale + geometry. A "MeanderReach region" on the Atlas should look like a floodplain, not + just a flat-colored zone. + +### Constraint 5: Seasonal and dynamic state must change tactical character, not just appearance + +Q-105 ties the tidal and seasonal water model to derived tile states. The morphology +families that depend on this model are BraidedDelta and MeanderReach — both produce +floodplain terrain where the Q-105 water-height determines which tiles are passable. + +The contract: the morphology algorithm must produce ElevationDelta values that, when +combined with the regional water-height model, create a genuine tactical difference +between high-water and low-water states. "The delta floods at high tide" must mean +"you cannot cross the delta at high tide, period" — not "the color of the water +tiles changes." This requires the delta channel elevations to be genuinely below +the Q-105 high-water threshold, and the island levees to be genuinely above it. + +This is an explicit coupling between the morphology algorithm's ElevationDelta +output and the Q-105 model's water-height range. The contract must pin this: +BraidedDelta and MeanderReach algorithms must produce elevations relative to +sea_level such that: +- `channel_floor_elevation ≈ sea_level − 0.5 to sea_level + 0.5 m` +- `levee_elevation ≈ sea_level + 1 to sea_level + 3 m` +- `high_water_height ≈ sea_level + 1 to sea_level + 1.5 m` (Q-105 tidal term) + +This puts channels under water at high tide and levees dry at low tide — a real +tactical difference. + +--- + +## Part 5: What I Need From Other Participants + +**From Tyre:** +- Confirmation that `tectonic_class` can be added to `RegionProfile` without + touching the D8 / Layer-1 pipeline. My expectation: it derives from body params + (already available at the region classification stage), so it is a free addition. +- The morphology algorithm's contract for its per-chunk warp offset interaction. + Specifically: does the warp apply before or after the algorithm sees the + ElevationDelta? My position: warp applies to the spatial query position, not to + the ElevationDelta output. The algorithm produces a delta for the warped position; + material derivation then runs on that delta. This keeps the algorithm implementations + simple. +- Clarification on the secondary family blending: when `blend_weight < 255`, + does the caller blend two ElevationDelta values (one per family), or does each + family produce its own full `TileAxes` and the caller blends those? I prefer + blending ElevationDelta only — material/vegetation derivation from a blended + elevation is cleaner than blending two independent material sets. + +**From Miri:** +- Which families need sub-region variation that cannot be expressed by the algorithm + alone? My expectation: MeanderReach needs oxbow lake positions (a secondary zone + within a meander region chunk). I propose this is handled by the algorithm + internally — oxbow lake detection runs a simple cutoff test on meander radius + vs wavelength, producing a local sub-feature without requiring a second region + classification. Miri should confirm this matches how real oxbow lakes form + (spoiler: it does — they form when meander radius exceeds a threshold relative + to channel width, exactly the kind of threshold test the algorithm can implement). +- Confirmation that the 8 families cover the morphology diversity the worldbuilder + expects. I anticipate Miri will want to add a Mountain Pass family (which I have + omitted as a deliberate choice — see note below). + +**Note on Mountain Pass:** D-234 references "mountain-pass → ribbon only (streets +linear along the terrain axis)" as a morphology zone affecting settlement. I have +not included MountainPass as a distinct morphology FAMILY because a mountain pass +is not a terrain-generating algorithm — it is an Incised Gorge at high elevation +with specific access semantics. The RegionProfile's `morphology_zone` can carry +MountainPass as a zone classification (distinct from IncisedGorge in semantics and +Atlas display) while using the IncisedGorge algorithm for geometry. This keeps the +algorithm count lean without losing the semantic distinction D-234 requires. + +**From Troblum:** +- Performance validation on the algorithm implementations. The families I am most + concerned about are BraidedDelta (multi-seed Voronoi is potentially expensive) + and FjordWall (the hanging valley shelf detection may require neighbor lookups). + Both should be implementable in ~1–2 ms/chunk; Troblum should stress-test. + +--- + +## Summary Table + +| Family | Primary Selector | ElevationDelta | Key Tactical Feature | +|--------|-----------------|----------------|---------------------| +| MeanderReach | Flat + Perennial/Major + above tide | −8 to +4 m | River crossing chokepoints; bank cover | +| IncisedGorge | Steep/Cliff + Perennial + high elevation + Active | −80 to 0 m | Extreme vertical asymmetry; floor exposed | +| BraidedDelta | Flat + Major + near sea level + Alluvial | −2 to +3 m | Tidal-cycle movement changes; concealment | +| DuneStrand | Sand + arid/coastal + no drainage | −3 to +12 m | Crest exposure; dead-ground corridors | +| CliffCoast | Near sea level + Steep/Cliff + non-glaciated + Rock | −30 to 0 m | Timed access via wave-cut platform | +| FjordWall | Cliff + glaciated >= 2 + Rock | −200 to +50 m | Hanging valley fire positions; sealed corridor | +| AlluvialPlain | Fallback (no special conditions) | −3 to +5 m | Open ground; terrace edges; long LOS | +| LavaField | Volcanic + Lava lithology | −20 to +8 m | Movement cost; tube ambush corridors | + +The algorithm-family vocabulary doubles as the `MorphologyZone` vocabulary for D-228 +region-level semantics, with MountainPass added as a zone classification (not a +distinct algorithm) for D-234 compatibility. diff --git a/docs/workshops/tile-derivation-contract/miri-round1.md b/docs/workshops/tile-derivation-contract/miri-round1.md new file mode 100644 index 000000000..a91f995d9 --- /dev/null +++ b/docs/workshops/tile-derivation-contract/miri-round1.md @@ -0,0 +1,103 @@ +--- +title: "Miri Round 1 — Morphological realism and named-feature stability" +workshop: tile-derivation-contract +participant: Miri +round: 1 +date: 2026-06-07 +note: "Authored by the Miri agent (read-only tool-set); transcribed to disk by the lead." +--- + +# Miri Round 1: What the Morphology Family Must Honor + +## Framing: what "reads true" on an alien world + +The Reach's bodies are not Earth — different gravity, hydrospheres, tectonic +histories — but they obey the same physical laws. "Reads true" ≠ "looks like +Earth"; it means terrain behaviour is internally consistent with the body's +parameters. A moonless world shows no tidal flats; a tropical ocean world shows +no fjords; a volcanic body has immature drainage and lava sheets, not deeply +incised river valleys; a GlaciationGrade-0 body has V-shaped fluvial valleys, never +glacial U-profiles. The uncanny valley in terrain is one rule broken visibly in +one spot — one river flowing uphill destroys a hundred correct ones. + +## 1. Morphological laws the derivation must honour + +- **Drainage consistency (non-negotiable).** Channels descend monotonically; the + meander algorithm adds sinuosity but must respect the D8 thalweg direction + (D-208) — never raise the channel floor. Tributaries join from upstream; river + mouths sit at sea level, no exceptions. +- **Lithology → landform.** TerrainMaterial bounds the allowed slope/form: Rock → + vertical faces/cliffs; Sand → ≤ angle of repose (~32°), dunes not cliffs; Gravel + → braided channels/fans (not single-thread meander); Soil → rolling/floodplain; + Wetland → ≤5° flats; Lava → sheets/shield slopes + lava tubes, immature drainage. + The family selector must honour this at region scale (no dune algorithm on a rock + coast; no incised-gorge family on high-Lava regions). +- **Glaciation history → form (globally visible).** Fjord requires GlaciationGrade + ≥ 2; U-valleys ≥ 1; moraine ridges ≥ 1; cirques ≥ 2. GlaciationGrade-0 high-relief + is knife-edge V-ridges from fluvial erosion, not glacial. Fjord-vs-cliff-coast is + a **boolean exclusion** on GlaciationGrade, not a weighted roll. +- **Climate → vegetation coherence.** Treeline transitions Forest → Scrub → Barren + (no direct Forest→Barren); polar latitudes are Tundra/Barren even at sea level; + riparian Thicket/Scrub band 1–3 tiles along every perennial waterway (visual + + tactical); natural vegetation boundaries follow contours/moisture, not chunk edges. +- **Coast type is multi-factor** (slope × lithology × glaciation × tidal/moon × + sediment): fjord / cliff coast / dune strand / tidal flat / delta / lava coast, + with the fjord↔cliff gate hard-keyed on GlaciationGrade. + +## 2. Named features — the wiki/atlas consistency contract + +D-228 is right that named types are derived display labels, never stored — which +makes **label derivation canonical + stable** load-bearing: the `MorphologyZone` +enum→label-string mapping must be frozen at content-authoring time, or every named +feature across all bodies shifts at once (bulk lore contradiction). The stable API +wiki authors/Atlas depend on is the **zone vocabulary**, not the underlying axis tuple. + +**Priority-1 lore-anchored bodies (direct wiki→generator contracts):** +- **Kallast (Ran)** — "grain fields visible from low orbit" ⇒ alluvial-plain dominance, Soil, very low slope; no mountain ranges/fjords. +- **Velen (Van Maanen's Star, D-050)** — temperate-maritime, fog ⇒ CoastalLowland, tidal-flat/dune coast, no cliff coasts/fjords. +- **Ross 128 primary** — "predominantly ocean" ⇒ low-elevation island terrain, dune/tidal coast, no cliff coasts/fjords. +- **Cygni B** — dense high-iron ⇒ Rock/Lava substrate, volcanic plains, immature drainage; not incised valleys/alluvial plains. +- **Grünfeld** — dim flux + decades of soil amendment ⇒ marginal Gravel/Rock substrate, Scrub/Barren natural baseline (settlement layer adds Crop). + +**Priority-2 type-consistency:** wherever the Atlas shows "fjord"/"mountain pass"/ +"river bank"/"delta", the geometry must follow through (deep water by cliff walls; +saddle profile + flanking elevation; riparian band + bench; distributary fan). + +**Proposed canonical MorphologyZone vocabulary (~17):** open ocean, lake, tidal +flat, dune strand, cliff coast, fjord, delta, estuarine, alluvial plain, river +bank, meander reach, braided plain, valley floor, mountain pass, alpine, volcanic, +wetland. Agree these before Phase-4 content authoring resumes (named now, not +necessarily implemented). + +## 3. Answers to Tyre's questions + +- **Sub-chunk variation:** no secondary zone field needed on `ChunkContext`. Meander + reach (ox-bows) and delta (distributary islands) handle sub-chunk features + *internally*; tidal-flat wet/dry is a whole-region state from the Q-105 water + height. Tyre's `ChunkContext` as sketched suffices. +- **RegionProfile persistence:** store `RegionProfile` (incl. `morphology_zone`) in + `BodyWorldState` at the **same cache tier as D-209 feature tags** — the Atlas + reads labels at region resolution (~6,000 regions/body) and must not trigger + chunk-level derivation. ~tens of KB/body; populate in the D-206 background pass + (~50 ms/body order). + +## 4. Body-class modulation of RIVER_THRESHOLD + +The global `RIVER_THRESHOLD = 200` should vary by body class via an integer lookup +`(hydrosphere, tectonic_activity, precipitation_class) → threshold`. Crucially, +`precipitation_class` (and `GlaciationGrade`) should be **derived from body-level +params** (stellar type, orbital radius, hydrosphere), not authored per body — +authored values silently drift from the terrain the generator actually produces. + +## 5. Setting-voice note (Phase-6, flagged not to foreclose) + +Keep the label vocabulary as plain English geography terms; the Drifter's-Guide +setting voice layers on top in content authoring. Round 2 should not constrain the +vocabulary in a way that blocks that later layer. + +## 6. What I need in Round 2 +- **Gestalt:** the ≥6 families + selectors; confirm the Priority-1 lore bodies are + reliably producible, and that GlaciationGrade-0 → no-fjord is a hard pre-selector gate. +- **Tyre:** confirm `RegionProfile` lives in `BodyWorldState` at the D-209 tier; confirm + `GlaciationGrade`/`precipitation_class` are derived from body params, not authored. +- **Troblum:** confirm memory feasibility of ~6,000 `RegionProfile`s/body in the cache. diff --git a/docs/workshops/tile-derivation-contract/tile-derivation-contract-workshop-brief.md b/docs/workshops/tile-derivation-contract/tile-derivation-contract-workshop-brief.md new file mode 100644 index 000000000..0cff3a0fe --- /dev/null +++ b/docs/workshops/tile-derivation-contract/tile-derivation-contract-workshop-brief.md @@ -0,0 +1,85 @@ +--- +title: "Workshop Brief: Tile Derivation Contract (Q-101)" +description: "Resolve Q-101 — how a coarse Layer-1 cell + seed becomes coherent ~1m voxel geometry across ~3 scale jumps, deterministically. The keystone of the 'what's in a tile' question." +type: workshop +status: active +workshop: tile-derivation-contract +created: 2026-06-07 +decision_refs: [D-227, D-228] +--- + +# Workshop Brief: Tile Derivation Contract (Q-101) + +## The question + +The generation cascade is built down through Layer 4 (heightmap → drainage → economy → +settlement → quarter skeleton + per-building tags). The **tile itself (Layer 5) is the one +unbuilt layer** — and the design stalled at **Q-101**, the keystone: + +> How does a coarse, map-scale hint (a ~78 km Layer-1 cell) plus a seed become fine, +> ~1 m-voxel **coherent** geometry — deterministically (D-010), without "squaring" (visible +> grid artifacts at scale boundaries) — across ~3 scale jumps: +> **78 km cell → ~1 km region → 64 m chunk → 1 m voxel**? + +The frame is already decided: +- **D-227** — derive-don't-store: `subtile(x,y,z) = derive(seed, atlas, pos)`, recomputed on + demand + cached, never persisted; volumetric voxels (1 m) / subvoxels (0.5 m); surface + + subsurface. Only tile-mutators are saved. +- **D-228** — a tile is orthogonal derived axes (`TerrainMaterial`/`FloorMaterial`/`Vegetation`/ + `Water`/`elevation`); named types are display labels; snow/ice are seasonal region state. + +What's **open**: the *how* — the inter-layer hint+seed→geometry API (Tyre sketched a +`RegionHint`) and a **context-driven morphology algorithm family** (≥6 distinct algorithms +selected by local context: rivers meander in lowlands / incise in mountains; coasts dune / +crag / fjord by slope + lithology + glaciation; etc.). + +Adjacent open questions to keep in view (not the focus, but the contract must not preclude them): +Q-102 (cohesion matrix — seam-free variation), Q-103 (tile-mutator op schema), Q-105 (region +seasonal/clock state). + +**Deliverable:** a resolution for Q-101 → a D-record (claim a fresh `D` in the architecture +domain) defining (a) the `RegionHint`/refinement API across the scale jumps, (b) the morphology +algorithm-family selection model, (c) the anti-squaring / cross-scale-coherence approach, within +the D-010 determinism + D-227 perf (~<5 ms/chunk) constraints. + +## Participants + +Tyre, Gestalt, Troblum, Miri (+ Qatux documenting) + +## Questions for participants + +**Tyre** (architect — owns the inter-layer API): +1. The `RegionHint` shape: what does each layer hand *down* the chain (78 km → region → chunk → + voxel)? Sketch the refinement-function signature(s) and what state each scale carries. +2. The anti-"squaring" mechanism — how do adjacent cells/chunks derive coherently at their shared + boundary without seams or visible grid? (domain warping? overlapping kernels? boundary + blending?) How does this compose with the D-227 by-chunk cache + eviction? +3. The determinism + perf contract: how does `derive(hint, seed, pos)` stay pure (D-010) and hit + the per-chunk budget? + +**Gestalt** (systems — the morphology family + fun): +1. Enumerate the ≥6 distinct morphology algorithms and the **local context** that selects each + (slope, lithology, glaciation, drainage, climate). Where are the boundaries between families? +2. What makes the *derived* terrain tactically interesting (cover, LOS, chokepoints, + verticality) — i.e. the game-feel constraints the contract must serve, not just plausibility. + +**Troblum** (tech second opinion — feasibility/perf): +1. Algorithmic morphology vs noise fields: which parts of Q-101 are best served by explicit + morphology algorithms vs multi-octave/domain-warped noise? Cross-scale coherence techniques. +2. Stress-test Tyre's API + the <5 ms/chunk + determinism claims; where does it break at scale? + +**Miri** (worldbuilder — believability): +1. Does the morphology family produce worlds that *read true* (rivers behave, coasts vary by + geology, biomes cohere)? What real-world morphology must the family honor to avoid uncanny terrain? +2. The D-228 derived-label layer — which named features matter for the wiki/atlas to stay consistent. + +## Workshop Format + +**Round 1 — independent positions.** Each participant writes their answer to disk at +`docs/workshops/tile-derivation-contract/{agent}-round1.md`. No cross-talk yet. + +**Round 2 — synthesis.** After a user checkpoint, converge: Tyre + Troblum lock the `RegionHint` +API + coherence approach; Gestalt + Miri lock the morphology family + selection model; resolve +tensions. Qatux drafts the Q-101 resolution + the new D-record. + +(Lean 2-round design pass, not the full 5-round format — Q-101 is one focused contract.) diff --git a/docs/workshops/tile-derivation-contract/troblum-round1.md b/docs/workshops/tile-derivation-contract/troblum-round1.md new file mode 100644 index 000000000..e55d51a93 --- /dev/null +++ b/docs/workshops/tile-derivation-contract/troblum-round1.md @@ -0,0 +1,476 @@ +--- +title: "Troblum Round 1 — Feasibility, Performance, and Determinism Stress-Test" +workshop: tile-derivation-contract +participant: Troblum +round: 1 +date: 2026-06-07 +--- + +# Troblum Round 1: Where the Proposal Holds and Where It Doesn't + +I read Tyre's Round 1. There is a lot right in it. There are also several places +where the numbers don't hold and one architectural decision I disagree with on +technical grounds. I will work through both questions in order. + +--- + +## Question 1: Algorithmic Morphology vs Noise Fields + +The brief asks which parts of Q-101 are best served by explicit morphology algorithms +versus multi-octave / domain-warped noise, and which cross-scale coherence techniques +avoid "squaring" across the 78 km → 1 km → 64 m → 1 m jumps. + +My position is that the question is slightly misframed. It is not morphology *versus* +noise. The correct framing is: **which properties are constraint-satisfying (morphology) +and which are detail-filling (noise)?** The answer to that determines which tool is +appropriate at each scale jump. + +### Scale jump 1: 78 km → 1 km (classification, not derivation) + +At this boundary, the job is classification. You are asking: what *kind* of terrain +does this region contain? The answer must be discrete — a MorphologyFamily enum, an +integer slope class, a glaciation grade. Noise is wrong here. A noise field varies +continuously; a morphology family does not. A valley floor does not gradually become +a fjord coast as you move across it; it transitions. The classification inputs +(elevation, flow_accumulation, slope, lithology, sea_level_margin) are all integer +or categorical. The output (`MorphologyFamily`, `RegionProfile` parameters) should +be a pure integer/lookup-table computation. + +**Correct tool:** deterministic integer classification. Tyre has this right. + +The anti-squaring work at this boundary is handled by Tyre's dual-classification +blending at the `ChunkContext` level (primary + secondary with `blend_weight`). I +have concerns about that mechanism that I will address under the stress-test section, +but the *type* of tool is correct. + +### Scale jump 2: 1 km → 64 m (structural shape, explicit algorithm) + +At this boundary the job is structural shape derivation. What is the actual terrain +doing within this chunk? Is there a meander channel? A cliff face? A dune ridge? + +This is where morphology algorithms earn their place. A meander algorithm needs to +produce a geometrically consistent channel — one that has a coherent sinuosity, a +consistent bankfull width, and lateral migration that respects upstream continuity. +A noise field cannot produce this. You would get a wandering path, not a river. + +**Correct tool:** explicit parameterized algorithms, one per MorphologyFamily. Each +algorithm receives `ChunkContext` and produces `ElevationDelta` values. The algorithm +is deterministic given the seed and context. + +However: explicit morphology algorithms have a failure mode that is not mentioned in +Tyre's Round 1. **They produce features with characteristic scales.** A meander +algorithm parameterized for 500m wavelength sinuosity will produce visible repetition +if any chunk parameters hash to similar values. This is a form of squaring at the +algorithm level rather than the grid level. The mitigation is: + +1. The algorithm parameters must be seeded per-chunk (Tyre has this via `chunk_seed`). +2. The seed must drive *continuous variation in shape parameters* (sinuosity, amplitude, + phase offset), not just a discrete selection from a small parameter table. If there + are only 8 possible parameter sets, you will see 8 repeating terrain shapes across + a large area. +3. The per-chunk seed derivation must use `SeedChain::derive(Block, chunk_id)` as Tyre + specifies — this is correct and provides good avalanche properties per D-224. + +### Scale jump 3: 64 m → 1 m (detail filling, noise is appropriate) + +This is where noise fields are appropriate. The structural shape has been established +by the morphology algorithm (the channel is here, the cliff face is here). The job +now is to fill in detail variation: micro-roughness on a rock face, ripple patterns +on a sand flat, surface irregularity on a soil plane. These are not structurally +meaningful — they do not change what the terrain *is*, only its surface texture. + +**Correct tool:** multi-octave value noise, position-keyed on world coordinates (not +chunk-local coordinates). This is exactly what D-228 specifies for the cohesion +matrix: "global, position-keyed continuous noise field — never per-chunk." + +The specific noise type matters here. Tyre specifies "value-noise + bilinear +interpolation" for the warp offset. I agree with avoiding Perlin/Simplex for +structural work (gradient noise has characteristic directional artifacts that read +as artificial), but for surface detail at the 1m scale, value noise with bilinear +interpolation is appropriate and cheap. + +### Cross-scale coherence: where "squaring" actually comes from + +The workshop brief asks specifically about cross-scale coherence techniques that avoid +squaring at the three boundaries. I want to be more precise about the failure modes, +because each boundary has a different one: + +**Boundary 1 (78 km → 1 km):** Squaring here would be abrupt transitions between +morphology zones at region edges. This is visible in generated terrain as sudden +changes in terrain character — the flat meander plain becoming a cliff coast in one +chunk. Mitigation: Tyre's dual-classification blending. I have concerns about the +implementation (see stress-test), but the mechanism is sound. + +**Boundary 2 (1 km → 64 m):** Squaring here would be chunk-grid-aligned features — +channels that terminate at chunk edges, cliff faces that align with chunk boundaries. +Mitigation: domain warping. This is the primary purpose of the warp offset. When +the warp magnitude is larger than 1 m but smaller than 64 m, it displaces the +feature positions enough that no feature aligns with the chunk grid. Tyre's 8 m +maximum warp is reasonable; I would argue for testing at 12 m to ensure grid +artifacts are suppressed at moderate distances. + +**Boundary 3 (64 m → 1 m):** Squaring at this boundary would be visible as texture +tiling — the same 1 m surface detail pattern repeating at 64 m intervals. Mitigation: +the global position-keyed noise field. Because the noise is keyed to world coordinates, +the pattern at tile (x, y) is identical regardless of which chunk computed it. + +One cross-scale coherence concern that is not addressed in Tyre's Round 1: **feature +scale vs chunk scale mismatch.** Consider a meander river with a wavelength of ~200 m. +A single 64 m chunk sees only a fraction of one meander bend. The algorithm that +computes the channel position within the chunk must derive it from the global meander +curve, not generate a local approximation. Otherwise adjacent chunks will produce +incoherent sections of "river" that do not connect. + +This requires the meander algorithm to evaluate the global curve at arbitrary world +coordinates — the algorithm must be a *continuous spatial function*, not a +per-chunk generator. This is a real design constraint that the MorphologyFamily +interface needs to make explicit. Tyre's current interface hands the algorithm a +`ChunkContext` and expects an `ElevationDelta` per tile. That is correct, but the +algorithm implementation must evaluate a globally-consistent curve, seeded from +something broader than `chunk_seed`. I would specify this as a requirement: +any MorphologyFamily that produces features larger than a single chunk must derive +its feature-scale parameters from a seed derived at a coarser level +(`SeedChain::derive(Block, block_id)` or higher), not from `chunk_seed`. + +--- + +## Question 2: Stress-Testing Tyre's RegionHint API + +### 2.1 The warp budget claim: 0.5 ms for ~5,000 hash + bilinear evaluations + +Tyre claims ~90 ns/evaluation. Let me check this. + +A single evaluation is: two splitmix64 calls (one per axis), bilinear interpolation +over a 2D integer grid (4 multiplications, 4 additions, one integer table lookup per +corner = 4 lookups total), and truncation to i16. On an out-of-order superscalar CPU +at 3.6 GHz: + +- splitmix64: 5–6 instructions with good ILP, roughly 2–3 cycles per call. + Two calls: ~5 cycles = ~1.4 ns. +- Bilinear over 4 integer corners: 4 table lookups (likely L1-cached if the noise + grid is small), 4 multiplications, additions: ~8–12 cycles = ~2.5–3.5 ns. +- Total per evaluation: ~4–5 ns with warm L1 cache. + +For 5,000 evaluations: ~20–25 µs, not 0.5 ms. Tyre's budget is 20x conservative +on this particular item. That is fine — more headroom for the morphology algorithm. + +But there is a catch: **cache behavior of the noise grid.** If the warp function +uses a discrete noise grid to interpolate from, that grid must be in cache during +the chunk derivation. Tyre says "integer-grid hash + bilinear interpolation." If +"integer-grid hash" means he is hashing the grid coordinates directly (no lookup +table), the cache issue goes away — it is pure computation. If there is a lookup +table, its size matters. A 256×256 grid of i16 values = 128 KB, which fits in L2 +but not L1 on most hardware. Random access into a 128 KB table would incur L2 +latency (~8 cycles per access), raising the per-evaluation cost to ~40 ns and the +total to ~200 µs. Still within the 0.5 ms budget, but Tyre should specify whether +the warp function is hash-based (fully L1-resident) or table-lookup-based. + +**My recommendation:** hash-based, no lookup table. `splitmix64` applied to the +quantized grid coordinates gives well-distributed values with no cache footprint. +This is the correct implementation for a position-keyed global field. + +### 2.2 The ChunkContext construction cost: 0.2 ms claim + +Tyre claims ~0.2 ms for `ChunkContext` construction. The work is: +- 4×4 `RegionProfile` lookups (16 lookups from the RegionProfile cache) +- 64×64 integer bilinear interpolation from Layer-1 resolution to chunk resolution +- 64×64 D8 flow direction downsampling + +The 64×64 bilinear interpolation: 4,096 interpolations, each ~8 cycles on out-of-order +hardware = ~32,768 cycles = ~9 µs at 3.6 GHz. The D8 downsampling is similar. The +`RegionProfile` lookups depend entirely on cache state — 16 lookups into what is +presumably an in-memory hashmap or fixed-size array. + +This is well under 0.2 ms if everything is in L1/L2 cache. The concern is the +first access after eviction. If `RegionProfile` data has been evicted from cache +(the player moved to a new area), the 16 lookups may each incur L3 or RAM latency. +At 40 ns/access (L3) that is 640 ns — still trivial. At 100 ns/access (RAM) with +cache miss: 1.6 µs. Still negligible. + +**Verdict:** 0.2 ms is 20x conservative for `ChunkContext` construction. The actual +cost is ~10–50 µs depending on cache state. This is good; it confirms the budget +has real headroom. + +### 2.3 The morphology algorithm budget: 1–3 ms, family-dependent + +This is the one line in Tyre's budget that I flagged as "Gestalt/Troblum to validate." +I cannot validate it without knowing the algorithm implementations, but I can define +the constraint: + +For 4,096 tiles at a 3 ms budget: 732 ns per tile. That is substantial time. A +meander channel algorithm that requires computing a curve position (which involves +a trigonometric or polynomial evaluation) per tile will cost roughly: +- Sine/cosine: 20–30 cycles = ~7 ns (modern hardware with FPU) +- Polynomial approximation: 10–15 cycles = ~3–4 ns + +A full meander evaluation (curve position + distance to channel + bank profile): +roughly 50–100 cycles = 15–30 ns per tile. At 4,096 tiles: 60–120 µs. Well within +the 3 ms budget. + +The risky algorithms are those with **per-tile branching** (if/else chains on +material type) and **non-trivial data dependencies** (algorithms that need to know +about adjacent tiles). The second is the critical one: any algorithm that reads +adjacent tile results to compute the current tile is not embarrassingly parallel +and cannot be SIMD-vectorized. The `ChunkContext` must provide all the context +the algorithm needs without requiring cross-tile reads during derivation. + +Tyre's interface (`ChunkContext` + warped position → `ElevationDelta`) correctly +forces this constraint. The algorithm sees only its own position and the pre-computed +chunk-level context. This is the right design. Gestalt needs to know it is a hard +constraint. + +### 2.4 The blend_weight dual-classification mechanism + +This is where I have a concrete disagreement with Tyre's design. + +The proposal: boundary chunks carry both a `primary` and `secondary` `RegionProfile`, +with an integer `blend_weight` (255 = fully primary, 128 = equal mix). The blend +width is "one chunk (64 m) on each side." + +**Problem 1: What does "blending" two MorphologyFamily algorithms mean?** + +Tyre's `derive_voxel` step 2 says: "resolve TileAxes twice (primary + secondary +RegionProfile), then integer-blend the parameters before selecting materials." But +`ElevationDelta` from a meander algorithm and `ElevationDelta` from a fjord algorithm +are not the same kind of value. Interpolating them linearly produces terrain that +is neither a meander nor a fjord — it is an incoherent average that reads as broken +terrain. This is worse than a hard transition. A hard transition at a natural feature +boundary (a ridge, a coastline) is geomorphically plausible. A 64 m wide zone of +averaged-meander-fjord terrain is not. + +This is not a theoretical concern. Amplitude-blended morphology transitions appear +in real generated terrain systems and they produce what is colloquially called +"mush terrain" — the transition zone has no readable character. + +**Problem 2: The blend zone is 64 m wide, but 64 m is exactly one chunk.** + +The blend is supposed to hide the region boundary. A 64 m blend zone covers one +chunk on each side. At normal play distance (say, 50–200 m from the character), +a 64 m zone is directly visible. If the terrain in that zone reads as "mush," the +player sees it. This is the opposite of hiding the seam. + +**My proposed fix:** + +Do not blend morphology algorithms. Instead: + +1. At the region boundary, select the morphology family based on which *local + terrain type* is more consistent with the position. This is the "morphological + footprint" approach: a fjord wall adjacent to a meander plain transitions at + the geomorphically natural boundary — the base of the cliff — not at a 64 m + blending zone. + +2. For the small number of morphology family pairs that genuinely need a transition + (e.g., meander reach → braided delta, which occurs naturally at a gradient change), + the transition is driven by the *local slope at the chunk level*, not by a + region-level blend weight. The chunk that contains the gradient break produces + the transition because its terrain type naturally changes at that location. + +3. The only parameter blending that makes sense is blending *continuous parameters + within the same family* — `meander_intensity`, `erosion_grade`, `dune_orientation`. + These are Tyre's `RegionProfile` parameters, and blending them is legitimate + because they modulate a single algorithm's behavior. Do this. + +This keeps the `secondary: Option` and `blend_weight` fields, but +changes their semantics: they modulate parameters within a family, not select between +families. The `MorphologyFamily` at a boundary chunk is always `primary.morphology_family`. + +**Caveat:** Some family transitions are fundamentally incompatible (fjord → meander), +some are compatible (meander → braided-delta). Gestalt needs to define the +compatibility matrix. The incompatible pairs should be topographically prevented +by the classifier — they should not be adjacent in the first place. + +### 2.5 The warp truncation to i16: a correctness issue + +Tyre says the warp offset is "immediately truncated to an i16 (integer metre +displacement) before any structural decision uses it." + +This is a problem. The warp offset's value is in the range ±8 m. An i16 can hold +±32,767, so the range is fine. But **truncating to integer metres means the +warp has 1 m quantization.** With a maximum warp of 8 m, the warp offset takes +one of only 17 discrete values (−8, −7, ..., 0, ..., 8). The warp field is no +longer a smooth displacement — it is a stepped field with 1 m jumps. At locations +where the warp transitions from (say) 3 m to 4 m, you get a 1 m step in the +effective position. This produces faint linear artifacts at the 1 m grid scale, +exactly what the warp was supposed to eliminate. + +The fix is to keep the warp offset at sub-metre precision during position computation +and only quantize the *result* (the warped position) to voxel coordinates at the +final step. The warp computation is f64; the structural decision (which voxel am I?) +quantizes at the end. Tyre's constraint — "f64 confined to within-voxel interpolation" +— is correct, but the warp itself is within-voxel precision. It should be: + +```rust +// warp_offset returns (f64, f64) in metres — NOT truncated +let (dx, dy) = warp_offset(seed, body_id, pos.x as f64, pos.y as f64); +// Apply warp to continuous world position, THEN quantize to voxel +let warped_x = (pos.x as f64 + dx).round() as i32; +let warped_y = (pos.y as f64 + dy).round() as i32; +// All structural decisions use (warped_x, warped_y) — integer +``` + +This preserves D-010 integer discipline for structural decisions while avoiding the +1 m quantization artifact in the warp. The f64 is used only for the displacement +computation; the output is an integer voxel coordinate. + +### 2.6 The D-010 determinism claim: where it is actually fragile + +Tyre's integer discipline is the right approach. I want to be specific about where +it is fragile: + +**f64 is deterministic on x86-64.** IEEE 754 double-precision arithmetic is +bit-identical across all x86-64 implementations for the same instruction sequence. +The risk is not arithmetic non-determinism — it is **compiler floating-point +optimization**. Specifically: + +- `fmadd` (fused multiply-add) contraction: a compiler may transform `a * b + c` + into a single `VFMADD` instruction, which produces a different result than + the separate `vmulsd` + `vaddsd` pair because the intermediate is not rounded. + GCC/LLVM may do this with `-O2 -mfma` (which Rust enables by default on x86-64). +- The fix: mark all warp computations as `#[inline(never)]` or explicitly disable + FMA contraction for the warp function. Alternatively, cast through integer at + the intermediate steps to force rounding. +- If the warp function is ever run on a non-x86-64 target (ARM, WASM), f64 + arithmetic may differ. This is a future concern but worth noting. + +**The `warp_offset` function must be in a tested regression harness.** Any change +to it — even a "refactoring" — changes every generated world. This is the same +class of stability concern that D-224 pins for `SeedDomain` discriminants and +`AttractorType`. Add a golden-seed test for `warp_offset` at the same time as +it is implemented. + +**HashMap iteration order.** Tyre does not mention this because his design uses +integer arrays and enums, not hashmaps. Good. But if any `MorphologyFamily` +implementation uses a `HashMap` internally (for parameter tables, neighbor lookups), +it must be replaced with a sorted data structure or fixed-size array. Flag this +for Gestalt. + +### 2.7 The global feature coherence problem: the river that doesn't connect + +This is the largest structural risk in the proposal that Tyre has not addressed, +and it is not a performance problem — it is a correctness problem. + +Tyre's Open Issue 1 identifies it partially: "At 1 m voxel resolution, a river +cell needs flow direction at meter scale." But the problem is deeper than flow +direction. + +A river with 200 m meander wavelength spans approximately 3 chunks (64 m each). +The chunk that contains the meander peak must produce terrain that connects +geometrically with the chunk that contains the meander trough. If each chunk +derives its meander curve position independently from its `chunk_seed`, the curves +will not connect — the river will be discontinuous at chunk boundaries. + +Tyre's domain warp (maximum 8 m displacement) suppresses the visual artifact of +the chunk boundary for materials and small features, but it cannot fix a 10 m +lateral displacement of the river channel at the chunk edge. Domain warping moves +positions, not the river channel. + +**The fix requires a globally-continuous meander curve.** The meander algorithm +must be parameterized by features that are consistent across chunks: the D8 flow +direction, the channel centerline as derived from a global seed, and a phase offset +that is continuous across chunk edges. + +Concretely: the meander centerline should be a function of distance along the D8 +network, seeded from a river-specific seed (derived from `SeedChain::derive(Layer1Topography, river_id)`), not from the chunk seed. The chunk computes +its local section of the curve by evaluating the function at the world coordinates +of each tile. Adjacent chunks evaluate the same function at adjacent coordinates +and get a connected curve. + +This means the meander algorithm cannot be a simple per-chunk generator. It must be +a *continuous spatial function evaluated at world coordinates*. This is implementable +but it is a design constraint that the MorphologyFamily interface must make +explicit. + +I would add to Tyre's interface requirement for Gestalt: + +> Any MorphologyFamily that produces features with characteristic wavelength larger +> than 64 m must derive those features from a seed at Block level or above +> (`SeedChain::derive(Block, block_id)`), not from `chunk_seed`. The chunk seed +> may only drive sub-64m variation. + +### 2.8 The river flow direction resolution gap + +Tyre flags this as Open Issue 1 but does not resolve it. I will. + +The D8 network is at 512×256 Layer-1 grid resolution. The body in Q-101 spans ~78 km. +At 512 cells wide, each D8 cell is approximately 152 m × 152 m. The chunk is 64 m. +Each chunk falls within a single D8 cell (64 < 152). So there is no D8 resolution +mismatch at the chunk level — a chunk looks up one D8 cell. + +But the river *channel* within the chunk is at 1 m resolution. The D8 direction +(one of 8 octants) tells you the basin-scale flow direction. The channel centerline +within the chunk needs to be at meter resolution. This is the meander algorithm's job: +given D8 direction as a basin-scale constraint, derive a locally-coherent channel +position at 1 m resolution using the global meander curve. + +**Tyre's `flow_direction: [u8; 64*64]` in `ChunkContext` is wrong for this purpose.** +At the D8 resolution, there is one flow direction per ~152 m cell, not per 1 m tile. +Storing a `[u8; 64*64]` flow direction implies a per-tile D8 value, which does not +exist. This array should be a single `flow_direction: u8` (the basin-scale D8 octant +for this chunk's position) plus the global meander seed parameters. + +The `river_cells: u64` bitmask of 8×8 macro-cells with river presence is more +defensible — it tells the chunk which of its 64 m sub-regions are near a river — +but "8×8 macro-cells" in a 64-tile chunk means 8 m cells, which is denser than +the D8 resolution. This should be documented as a downsampled indicator, not a +direct D8 output. + +### 2.9 The RegionProfile persistence question + +Tyre's Open Issue 2: "For Phase 4 Atlas use, we may want to compute `RegionProfile` +lazily on Atlas request rather than precomputing for all regions — ~6,000 per body." + +The cost numbers: 6,000 RegionProfile computations per body. Each RegionProfile +is a classification (integer comparisons, lookup tables, one flood-fill shared +across the whole body). The flood-fill is O(n) for n = 131,072 cells (512×256) — +call it 1 ms per body. The per-region classification is ~10–50 µs per region. +Total for 6,000 regions: 60–300 ms per body. + +This is not expensive. Precomputing all RegionProfiles at body load is acceptable. +Lazy recomputation adds request latency that is visible in the Atlas viewer — +the first Atlas pan to an unloaded region takes 50–300 µs while RegionProfile +is computed. Not bad, but not zero. + +My recommendation: precompute all RegionProfiles at body load, store in a +fixed-size Vec indexed by region_id. 6,000 × (size of RegionProfile struct) at +roughly 32 bytes per struct = 192 KB per body. Trivial memory footprint. + +--- + +## Summary of Disagreements and Concerns + +**Agree with:** +- Three-typed-carrier architecture (RegionProfile → ChunkContext → VoxelColumn). + Correct decomposition. +- Integer-only structural decisions, f64 confined to warp computation. Right. +- Global position-keyed warp field for chunk-boundary seam suppression. + Sound mechanism. +- SeedChain::derive for per-chunk seeds. Correct. +- Performance budget is conservative overall — the 5 ms cap has substantial headroom. + +**Disagree with / flag for Round 2:** + +1. **Algorithm blending across MorphologyFamily boundaries is wrong.** Blending + meander + fjord ElevationDeltas produces incoherent terrain. Blend parameters + within a family; do not interpolate between families. Requires a compatibility + matrix from Gestalt. + +2. **Warp truncation to integer metres will produce stepped artifacts.** Keep + the warp at f64 sub-metre precision through the position computation; quantize + only the final voxel coordinate. + +3. **The globally-coherent feature constraint is missing from the interface + contract.** Any family with feature wavelength > 64 m must use a Block-level or + higher seed, not the chunk seed. This needs to be explicit in the MorphologyFamily + trait/interface. + +4. **`ChunkContext.flow_direction: [u8; 64*64]` is wrong.** A per-tile D8 direction + does not exist at D8 resolution. Replace with a per-chunk basin direction (one u8) + plus meander curve parameters. + +5. **The warp function needs a golden-seed regression test.** Same class of + stability concern as `SeedDomain` discriminants. Any change to it re-rolls + every world. + +6. **f64 FMA contraction is a determinism risk.** The warp function must be + protected from compiler FMA optimization or explicitly tested as bit-identical + across optimization levels. diff --git a/docs/workshops/tile-derivation-contract/tyre-round1.md b/docs/workshops/tile-derivation-contract/tyre-round1.md new file mode 100644 index 000000000..b5e10abdb --- /dev/null +++ b/docs/workshops/tile-derivation-contract/tyre-round1.md @@ -0,0 +1,342 @@ +--- +title: "Tyre Round 1 — RegionHint API, anti-squaring, determinism/perf" +workshop: tile-derivation-contract +participant: Tyre +round: 1 +date: 2026-06-07 +--- + +# Tyre Round 1: The Derivation API Contract + +## Framing the problem correctly + +The 78 km → 1 km → 64 m → 1 m span is not a single derivation problem — it is +three distinct problems with different failure modes, and conflating them is how +you get squaring. My position up front: **each scale boundary needs a different +coherence mechanism**, and the `RegionHint` I sketched earlier needs to be split +into three typed carriers, one per boundary. The overall structure is: + +``` +Layer 1 cell (78 km) → RegionProfile (region-scale morphology zone + parameters) +RegionProfile → ChunkContext (64 m, resolved from heightmap position) +ChunkContext + seed → VoxelColumn (1 m, the actual derive call) +``` + +The `RegionHint` name survives but becomes specifically the `RegionProfile` — the +thing the region-level pass fills once and hands to all chunks within it. + +--- + +## Scale boundary 1: 78 km cell → ~1 km region (RegionProfile) + +This boundary is **classification**, not interpolation. A region does not gradually +become a fjord coast — it is or it isn't, based on contextual inputs. The squaring +risk here is not visual grid artifacts; it is abrupt zone transitions at region edges. + +### What the 78 km cell hands down + +The Layer 1 outputs already available per D-208 / D-209 / D-227: +- `elevation` (f32 heightmap, but read as integer-scaled u16 per D-010) +- `flow_accumulation` (D8 integer count) +- `flow_direction` (D8 integer 0–7) +- `sub_biome` (D-210 / D-228 — climate + vegetation zone) +- `river_network` membership (bool) + +What the region pass adds (runs once per region, stored as `RegionProfile`): + +```rust +pub struct RegionProfile { + // Classification inputs — all integer or enum + pub morphology_zone: MorphologyZone, // fjord / meander / delta / etc. + pub lithology: Lithology, // resolved from body parameters + elevation strata + pub glaciation: GlaciationGrade, // 0..4 integer grade + pub slope_class: SlopeClass, // Flat / Gentle / Steep / Cliff (computed from 3×3 elev kernel) + pub drainage_class: DrainageClass, // None / Ephemeral / Perennial / Major + pub sea_level_margin: i32, // elevation - sea_level, integer metres + + // Morphology algorithm selector — derived from all above + pub morphology_family: MorphologyFamily, + + // Parameters handed to ChunkContext + pub relief_amplitude: u16, // max voxel-height variation within region, integer metres + pub meander_intensity: u8, // 0–255 integer + pub erosion_grade: u8, // 0–255 integer + pub dune_orientation: u8, // compass octant (0–7), prevailing wind +} +``` + +`MorphologyZone` is the D-228 "region-level morphology zone" field, resolved here. +`MorphologyFamily` is the algorithm selector — that is Gestalt's domain to enumerate; +I just need to know what I am selecting between. + +### Lithology: resolving the ocean/lake mask gap + +Q-101 flags the D-223 ocean-polygon removal as an "unowned dependency". My resolution: +**sea-level threshold + flood-fill from heightmap edge, all integer**. + +```rust +fn classify_water_body( + body_params: &BodyParams, // has sea_level, radius + elevation_grid: &Grid, // integer-scaled metres + pos: (u16, u16), +) -> WaterBodyClass { + // cells below sea_level threshold that are connected to the heightmap + // edge (flood-fill) → ocean; isolated depressions below sea_level → lake +} +``` + +This is a single flood-fill pass — O(n) over the Layer-1 grid (512×256 = 131k cells), +produces a bitmask. Deterministic, integer-only, no authored polygon dependency. +Lithology strata are body-class parameters (tectonic activity, age) → integer lookup +table per `(body_class, elevation_band)`. No f32 in classification. + +### Region boundary blending (anti-squaring at this boundary) + +The morphology zone must not flip discretely at region edges. Mechanism: **dual +classification with a blending weight**, resolved at the `ChunkContext` level. + +Each region classifies to a primary and a secondary `MorphologyFamily`, with an +integer weight `primary_weight: u8` (255 = fully primary, 128 = equal mix). Chunks +near region boundaries inherit both families and blend their parameter tables. +The boundary width is one chunk (64 m) on each side — chosen to be invisible at +normal view distances and narrow enough not to corrupt chunk interiors. + +--- + +## Scale boundary 2: ~1 km region → 64 m chunk (ChunkContext) + +This is the **critical** boundary for squaring. Adjacent chunks from different +regions must tile seamlessly. The failure mode here is visible grid lines where +chunk borders coincide with region zone changes. + +### ChunkContext struct + +```rust +pub struct ChunkContext { + // Position identity + pub chunk_pos: ChunkPos, // integer chunk grid coordinates + pub chunk_seed: SeedChain, // SeedChain::derive(Block, chunk_id) + + // Inherited from RegionProfile (primary) + pub primary: RegionProfile, + + // Boundary blending — present only for boundary chunks + pub secondary: Option, + pub blend_weight: u8, // 255 = fully primary (no blend) + + // Heightmap slice — integer, resolved at chunk resolution + pub elevation_min: u16, + pub elevation_max: u16, + pub elevation_grid: [u16; 64*64], // 4096 u16 values, bilinear from Layer 1 → chunk res + + // Flow data at chunk resolution + pub flow_direction: [u8; 64*64], // D8 octant per chunk-scale cell + pub river_cells: u64, // bitmask of 8×8 macro-cells with river presence +} +``` + +`ChunkContext` is **computed once per chunk** and cached per D-227 (same eviction +tier as the derived tile data itself). Computing it requires reading the four +surrounding `RegionProfile` records plus the heightmap tile. Cost: integer arithmetic +over 64×64 arrays — well under 1 ms. + +### Anti-squaring: domain warping at the chunk scale + +The principal technique for seam-free transitions at chunk boundaries is +**position-domain warping using a global continuous noise field** — matching the +D-228 "global, position-keyed continuous noise field — never per-chunk" directive +for the cohesion matrix at the material layer. + +The same principle applies at the morphology level: the warping offsets are derived +from a **global field** keyed on (body_id, world_seed), evaluated at any (x, y) +position. Any chunk can independently compute the warp offset for any position +within or adjacent to itself — producing identical values for the shared boundary. +This is the structural guarantee of seam-freedom: warp values are global functions, +not per-chunk state. + +Concretely: + +```rust +fn warp_offset(seed: u64, body_id: u64, x: f64, y: f64) -> (f64, f64) { + // Two independent value-noise fields (no coherent noise dependency) + // Using integer-grid hash + bilinear interpolation to keep it f64-confined + // Returns sub-voxel displacement in (dx, dy), magnitude capped at ~8m +} +``` + +The warp magnitude (~8 m maximum) is chosen to be larger than one tile (1 m) but +smaller than a chunk (64 m). This means: +- Chunk-grid lines are visibly displaced by up to 8 m in both directions +- No straight-line grid artifact can persist for more than 8 m +- Adjacent chunks compute the warp for their shared edge independently and get + identical values (pure function of world coordinates + seed) + +The warp is applied in **world-space coordinates before the morphology algorithm runs** — +the algorithm sees warped positions and produces terrain that has no knowledge of +the chunk grid. The chunk boundary is invisible because neither algorithm has it. + +### Relation to the D-227 chunk cache + +The domain-warp field does not need to be cached separately. Each chunk computes +warp offsets only for its own tiles (the 64×64 grid) plus a 16-tile overlap margin +on each side (for the blend zone). This overlap is cheap — 96×96 evaluations vs 64×64. +The warp function itself is O(1) per position; the cost scales with chunk area, not +with neighbor count. + +--- + +## Scale boundary 3: 64 m chunk → 1 m voxel (the derive call) + +This is where D-227's `subtile(x,y,z) = derive(seed, atlas, position)` lives. + +### Refined function signature + +```rust +pub fn derive_voxel( + ctx: &ChunkContext, // pre-computed chunk context (cached) + pos: VoxelPos, // integer (x, y, z) — 1 m resolution +) -> TileAxes { + // Returns the D-228 composite: + // TerrainMaterial, FloorMaterial, Vegetation, Water, elevation +} + +pub struct TileAxes { + pub terrain_material: TerrainMaterial, // u8 enum + pub floor_material: FloorMaterial, // u8 enum + pub vegetation: VegetationKind, // u8 enum + pub water: WaterDepth, // u8 enum + pub elevation: u16, // integer metres +} +``` + +The `pos` parameter carries world-space coordinates, not chunk-local coordinates. +The algorithm: + +1. Apply `warp_offset(ctx.chunk_seed.seed(), body_id, pos.x, pos.y)` — warp the + position. +2. If `ctx.blend_weight < 255`: resolve `TileAxes` twice (primary + secondary + `RegionProfile`), then integer-blend the parameters before selecting materials. + The blend uses the same warp offset to prevent the blend seam from aligning with + the chunk edge. +3. Call the `MorphologyFamily`-specific derivation function with the `ChunkContext` + parameters. +4. Derive `TerrainMaterial` from `lithology + elevation + slope`. +5. Derive `Vegetation` from `sub_biome + terrain_material + elevation`. +6. Derive `Water` from regional water-height vs `elevation` (the D-228 cheap + seasonal/tidal water model). +7. Return `TileAxes`. + +Step 3 is Gestalt's domain (the morphology algorithm implementations). The contract +from my side: each family receives `ChunkContext` + warped position and returns an +`ElevationDelta: i16` (delta from the base heightmap elevation, signed, integer metres). +That delta feeds into step 4 onward. + +### Structural decisions (D-010 integer discipline) + +All structural decisions — which material, which morphology zone, which elevation band — +are **integer-only**. f64 is confined to the warp offset computation and any +within-voxel interpolation. This is not ceremonial: D-227 makes it save-critical. +A single f32 comparison that flips on a different hardware FPU would desync every +mutator reference in a save file. + +Specific constraints I am placing: +- `ElevationDelta` is `i16` (integer metres). No sub-voxel elevation in the + structural layer. Sub-voxel geometry is a render concern (shape-from-material, + D-228: "sand slumps to angle of repose, rock breaks to vertical face"). +- Material selection is a series of integer comparisons / lookup table reads. +- The warp offset is f64 during computation but is immediately truncated to an + `i16` (integer metre displacement) before any structural decision uses it. + The sub-metre remainder is discarded — only the rendered visual uses sub-voxel + precision, and that is a client concern. + +--- + +## Performance contract + +Target: <5 ms per chunk, per D-227. + +Budget breakdown (64×64 = 4,096 tiles per chunk): + +| Work item | Cost estimate | Basis | +|-----------|--------------|-------| +| `ChunkContext` construction (once per chunk) | ~0.2 ms | 4×4 RegionProfile lookups + 64×64 integer array ops | +| Warp offset computation (4,096 + margin positions) | ~0.5 ms | ~5,000 hash + bilinear evaluations | +| Morphology algorithm (4,096 tiles, varies by family) | ~1–3 ms | family dependent; Gestalt/Troblum to validate | +| Material + vegetation + water derivation (4,096 tiles) | ~0.5 ms | integer table lookups | +| **Total** | **~2.2–4.2 ms** | headroom for complex families | + +The chunk cache means this cost is paid once per chunk per play session (or per +Atlas view request). Adjacent chunks share no mutable state; they are embarrassingly +parallel under Rayon (D-208 precedent). + +The `ChunkContext` is the critical cache object — it amortizes the `RegionProfile` +lookups across all 4,096 tile derivations in the chunk. Evicting a chunk from cache +means recomputing its `ChunkContext` plus all tile derivations; the `RegionProfile` +cache above it is longer-lived (one per ~1 km region vs one per 64 m chunk). + +--- + +## What I need from teammates + +**From Gestalt:** +- The ≥6 morphology family names and their context selectors, so I can populate + `MorphologyFamily` as a typed enum with explicit `#[repr(u8)]` discriminants + (stability requirement — same as `SeedDomain`). +- The `ElevationDelta` range each family can produce. I need to know if any family + wants sub-voxel precision (answer: no — see D-010 argument above) or more than + ±255 m variation within a chunk (answer: physically implausible for a 64 m chunk). +- Any context inputs the family selector needs beyond what `RegionProfile` already + carries. My position: `RegionProfile` should be sufficient; if a family needs a + per-tile input not in `RegionProfile`, that is a signal the classification belongs + at the `ChunkContext` level, not the voxel level. + +**From Troblum:** +- Stress-test the warp budget specifically. 5,000 hash + bilinear evaluations in + 0.5 ms is ~90 ns/evaluation — plausible on modern hardware but worth verifying. + The warp can be precomputed into a lookup grid within the `ChunkContext` if that + is faster, at the cost of ~12 KB of additional cache memory per chunk. +- Validate the `ChunkContext` construction cost. The 64×64 integer bilinear + interpolation from Layer-1 resolution to chunk resolution is ~8× upsampling; + that should be fast but Troblum should confirm. + +**From Miri:** +- Which morphology zones need sub-region variation within a single 64 m chunk? + For example: a meander reach with an oxbow lake needs the lake to appear within + a chunk that the region classifies as "meander reach". Does that require a + secondary zone classification at the chunk level, or does the morphology algorithm + handle it internally? +- The D-228 "named features matter for the wiki/atlas" list — which ones require + the `RegionProfile.morphology_zone` to be queryable at Atlas-generation time? + That determines whether `RegionProfile` needs to be persisted to the layer cache + or can be recomputed on Atlas queries. + +--- + +## Open issues I am flagging for Round 2 + +1. **River flow direction at the voxel level.** Q-101 notes "river flow direction + derives from the D8 network at query time." The D8 network is Layer-1 resolution + (512×256). At 1 m voxel resolution, a river cell needs flow direction at meter + scale. The meander algorithm (Gestalt's domain) presumably drives local course + variation — but does the D8 direction act as a basin-scale constraint or a + cell-level input? This affects whether `flow_direction` in `ChunkContext` is + one value per chunk or one value per 8×8 macro-cell within the chunk. + +2. **`RegionProfile` persistence vs recompute.** The Atlas viewer (D-225) currently + caches `Layer1Output` (rivers, basins, attractors). `RegionProfile` is a finer + grain. For Phase 4 Atlas use, we may want to compute `RegionProfile` lazily on + Atlas request rather than precomputing for all regions — the number of regions + per body is on the order of 78km/1km ≈ ~6,000 per body. That is cheap to + recompute on demand but expensive to precompute at atlas-load time for all bodies. + +3. **`MorphologyFamily` transition zones and pathological adjacencies.** What happens + when a fjord coast is adjacent to a meander delta? The blend mechanism handles it + at the chunk level but the `RegionProfile` classifier needs to produce a sensible + `secondary` morphology zone for the boundary chunks. This requires the + morphology-zone vocabulary (Gestalt's Round 1 output) before Round 2 can finalize. + +4. **Body-class modulation of `RIVER_THRESHOLD`.** Q-101 flags this. My position: + `RIVER_THRESHOLD` should be a body parameter computed from `(hydrosphere, tectonic_activity, + precipitation_class)` using an integer lookup table. This gives Nigel the handle + without touching the D8 algorithm itself. The lookup table lives in `body_params.rs` + alongside other per-body scalar parameters. diff --git a/docs/workshops/tile-derivation-contract/workshop-outcomes.md b/docs/workshops/tile-derivation-contract/workshop-outcomes.md new file mode 100644 index 000000000..29bc708ec --- /dev/null +++ b/docs/workshops/tile-derivation-contract/workshop-outcomes.md @@ -0,0 +1,102 @@ +--- +title: "Workshop Outcomes: Tile Derivation Contract (Q-101)" +description: "Final outcomes of the tile-derivation-contract workshop — resolution of Q-101 via D-239, the coarse→fine tile refinement chain, morphology family, anti-squaring warp, and district-temperature climate primitive." +type: workshop +status: complete +workshop: tile-derivation-contract +created: 2026-06-07 +decision_refs: [D-239] +resolves: [Q-101] +participants: [Tyre, Gestalt, Troblum, Miri, Qatux] +--- + +# Workshop Outcomes: Tile Derivation Contract (Q-101) + +**Result:** Q-101 resolved → **[[D-239]]** filed (architecture domain, confirmed, 2026-06-07). + +The keystone "what's in a tile" question — how a coarse Layer-1 cell + seed becomes +coherent ~1 m voxel geometry across ~3 scale jumps — is now contracted. This was the one +unbuilt layer of the Phase-4 cascade (L0–L4 already exist). + +## What was decided (D-239 in brief) + +1. **Three-carrier refinement chain.** `RegionProfile` (~1 km) → `ChunkContext` (64 m) → + `VoxelColumn` (1 m). Each a pure deterministic function of `(seed, atlas, body-params, + position)` per D-227. Each scale boundary is its own derivation. + +2. **No authoring at the derivation layers, ever.** Zero per-body override hooks. All + gating params (`RIVER_THRESHOLD` → derived per-body-class, not the global `200`; + `tectonic_class`; `GlaciationGrade`; `precipitation_class`) derive from body params. + Lore bodies (Kallast, Velen, Cygni B) are honoured by *setting params* and serve as + validation cases — fix params, never patch the derivation. + +3. **District temperature is the climate primitive.** A scalar °C per 2×2 km district, + **nullable** (no atmosphere → null → no climate branch; airless surface ice is geology). + Derived from sun (luminosity + insolation), planet (latitude + elevation lapse + + orbital/axial phase = season, day-phase = diurnal), atmosphere (greenhouse → base, + heat-retention → diurnal-swing amplitude). **Moisture** is a separate primitive. + Everything climatic — precipitation, vegetation/treeline, glaciation, the seasonal/clock + state (Q-105) — derives from temperature (+ moisture). Formalises D-210's temperature proxy. + +4. **Scattered, transient freeze/snow model.** Ice/snow is a *band with coherent spatial + scatter*, not a hard `temp<0` contour: freshwater (lakes/rivers) **+5 → −10 °C** district + mean (the +5 is night-frost reaching the coldest tiles); sea ice its own lower/wider band + (≈ −2 onset, pack-ice pattern); snow (land) the same, **gated on moisture**. **Transient** + in the marginal band — forms in the cold phase (night/season), melts in the warm phase → + time-of-day passability shifts. Forward contract to Q-105. + +5. **Anti-"squaring" warp.** A stateless, hash-based position-keyed domain-warp; **f64 + sub-metre precision through to the final voxel coordinate, then truncate to the integer + voxel address** (a cast, IEEE-754-deterministic). ±8 m range makes FMA ULP variance + harmless. Position math, not a structural decision — D-010 integer discipline preserved. + +6. **8 morphology families** via a gated decision tree over integer `RegionProfile` inputs: + LavaField · FjordWall · CliffCoast · BraidedDelta · DuneStrand · IncisedGorge · + MeanderReach · AlluvialPlain (fallback). Hard gates: fjord ≥ GlaciationGrade 2; LavaField + = Volcanic; lithology bounds slope/form. + +7. **Frozen 17-zone `MorphologyZone` vocabulary.** Freeze point = the Rust enum; changes need + a D-record amendment (classifier *tuning* stays free). Four zones (tidal flat, estuarine, + alpine, wetland) are derived sub-classifications, not generator families. + +8. **Seams — prevent incompatible, allow valid (never patch).** Decision-tree gate ordering + + a build-time compatibility-matrix invariant blocks incompatible adjacencies (e.g. + MeanderReach↔Volcanic). Valid geomorphic seams (cliff↔fjord, lithology faults) are kept + **sharp** (real geology is sharp), made organic by the warp — no feathering. + +9. **Believability laws + game-feel constraints** are binding (drainage monotonicity, + lithology→landform, glaciation→form, climate→vegetation; ≥1 tactical point/chunk, cover ≠ + concealment, chokepoint widths, real seasonal passability via `ElevationDelta`↔Q-105). + +## Process notes + +- **2-round lean format** (independent positions → lead-interviewed synthesis), not the full + 5-round workshop. Round-1 positions: `tyre-round1.md`, `gestalt-round1.md`, + `troblum-round1.md`, `miri-round1.md`. +- The four core calls (seams, body-params-fully-derived, vocabulary lock, warp precision) + were made by the human via AskUserQuestion. The climate model (temperature primitive, + separate moisture, the three-sibling scattered freeze, day/night swing, transient ice/snow) + was a human-driven design pivot during synthesis. +- An **adversarial verification pass** (the same four perspectives) caught ~10 issues, all + folded into the final D-239: the "continuous→continuous" seam argument was false (boolean + gates are discontinuities) → rewritten to gate-ordering + matrix; warp determinism gaps + (FMA / statelessness / truncate-not-compare); "Block-or-higher" → "Region-or-higher"; + D-209 tier → D-203; missing LavaField tectonic gate; seasonal regained mechanical teeth; + the 17-zones-vs-8-families gap closed; believability laws inlined; "frozen" given a real + enforcement mechanism. + +## Open siblings (left for later, contract does not preclude them) + +- **Q-102** — cohesion matrix: effectively *is* the D-239 warp; can be closed against it or + folded in when the material-scatter layer is built. +- **Q-103** — tile-mutator op schema: open, needed at the save system (Phase 5+). +- **Q-105** — region seasonal/clock state: D-239's temperature(time) + `ElevationDelta` + calibration is the forward contract to it. + +## Process friction captured + +- **Team mode is broken in clide** — see `docs/briefings/clide-team-mode-friction.md`. + Teammates ran in-process (no pane), the Team Chat view stayed inert, no `clide team` verb. + The workshop ran as background Agent subagents instead. Hand-off material for the clide team. +- The `miri` agent type is **read-only** (no Write/Bash) — the lead transcribed + `miri-round1.md` to disk. diff --git a/governance/README.md b/governance/README.md index d0b1279eb..a28c22c7f 100644 --- a/governance/README.md +++ b/governance/README.md @@ -304,6 +304,7 @@ line in place — keep the Q-record for the audit trail rather than deleting it. - [D-236: Sol / GJ-0 — player-inaccessible, Atlas highest-level only, deeper Sol is future DLC](decisions/scope.md#d-236-sol--gj-0--player-inaccessible-atlas-highest-level-only-deeper-sol-is-future-dlc) — _scope_ - [D-237: Authored per-system specialisation layer — `economic_specialization` + `cultural_specialization` + `dominant_faction`](decisions/architecture.md#d-237-authored-per-system-specialisation-layer--economic-specialization--cultural-specialization--dominant-faction) — _architecture_ - [D-238: Symmetric shadowcasting (Albert Ford) selected for LOS computation](decisions/perception.md#d-238-symmetric-shadowcasting-albert-ford-selected-for-los-computation) — _perception_ +- [D-239: Tile derivation contract — coarse→fine refinement chain (resolves Q-101)](decisions/architecture.md#d-239-tile-derivation-contract--coarsefine-refinement-chain-resolves-q-101) — _architecture_ ## Open questions @@ -375,7 +376,6 @@ line in place — keep the Q-record for the audit trail rather than deleting it. - [Q-093: Tile-based exploration map in player insert (Google Maps for the implant)](questions/architecture.md#q-093-tile-based-exploration-map-in-player-insert-google-maps-for-the-implant) — _architecture_ - [Q-097: Strip "What They Don't Talk About" from corporation pages](questions/content.md#q-097-strip-what-they-dont-talk-about-from-corporation-pages) — _content_ - [Q-099: Mod content catalog — body rows / terrain_reference overlay for systems.db](questions/architecture.md#q-099-mod-content-catalog--body-rows--terrain-reference-overlay-for-systemsdb) — _architecture_ -- [Q-101: Refinement / derivation contract — coarse hint → fine geometry + morphology algorithm family](questions/architecture.md#q-101-refinement--derivation-contract--coarse-hint--fine-geometry--morphology-algorithm-family) — _architecture_ - [Q-102: Cohesion-matrix algorithm — seam-free continuous variation](questions/architecture.md#q-102-cohesion-matrix-algorithm--seam-free-continuous-variation) — _architecture_ - [Q-103: Tile-mutator op schema](questions/architecture.md#q-103-tile-mutator-op-schema) — _architecture_ - [Q-105: Region seasonal/clock state — the shared cheap-dynamism source](questions/architecture.md#q-105-region-seasonalclock-state--the-shared-cheap-dynamism-source) — _architecture_ @@ -416,6 +416,7 @@ line in place — keep the Q-record for the audit trail rather than deleting it. - [Q-096: Replace sprint workflow with kanban + milestones](questions/process.md#q-096-replace-sprint-workflow-with-kanban--milestones) — _process_ - [Q-098: Persistence of generated river/city mapping outputs](questions/architecture.md#q-098-persistence-of-generated-rivercity-mapping-outputs) — _architecture_ - [Q-100: Biome authority — Python sim vs Rust cascade](questions/architecture.md#q-100-biome-authority--python-sim-vs-rust-cascade) — _architecture_ +- [Q-101: Refinement / derivation contract — coarse hint → fine geometry + morphology algorithm family](questions/architecture.md#q-101-refinement--derivation-contract--coarse-hint--fine-geometry--morphology-algorithm-family) — _architecture_ - [Q-104: Floor-index ↔ absolute voxel-z coordinate mapping](questions/architecture.md#q-104-floor-index--absolute-voxel-z-coordinate-mapping) — _architecture_ - [Q-106: Era-band stacking depth for layered architecture-flavor](questions/architecture.md#q-106-era-band-stacking-depth-for-layered-architecture-flavor) — _architecture_ diff --git a/governance/decisions/architecture.md b/governance/decisions/architecture.md index 3922f0c31..c5c6a7f89 100644 --- a/governance/decisions/architecture.md +++ b/governance/decisions/architecture.md @@ -1739,4 +1739,39 @@ Technical foundation decisions that constrain implementation: engine, client-ser --- -*96 decisions (D-001 through D-237, excluding gaps). Last updated: 2026-05-31 (system-economic-specialization workshop — D-237 authored per-system specialization layer: `economic_specialization` + `cultural_specialization` + `dominant_faction`; D-233 re-amended to source from D-237 authored layer).* +### D-239: Tile derivation contract — coarse→fine refinement chain (resolves Q-101) +- **Date:** 2026-06-07 +- **Resolves:** [Q-101](../questions/architecture.md#q-101) +- **Decision:** The walkable tile is materialised by a **three-carrier refinement chain**, each stage a pure deterministic function of `(seed, atlas, body-params, position)` per [D-227](#d-227): `RegionProfile` (~1 km) → `ChunkContext` (64 m) → `VoxelColumn` (1 m). Each scale boundary is its own derivation with its own failure modes. + + **(1) Determinism — no authoring at the derivation layers, ever.** L1–L5 derivation has **zero per-body override hooks**. All authorial control lives **upstream** at the body-parameter / atlas / system-specialization layer; all gating params (`RIVER_THRESHOLD` — a derived per-body-class value `(hydrosphere, tectonic_activity, precipitation_class) → threshold`, not the global `200`; `tectonic_class`; `GlaciationGrade`; `precipitation_class`) are **derived from body params** (stellar type / orbit / hydrosphere / lithology / temperature history), never authored. Lore-anchored bodies (Kallast = plains, Velen = coast, Cygni B = volcanic) are honoured by **setting params** and stand as **validation cases** — if a body reads wrong, fix its params, never patch the derivation. *Validation caveat:* derived terrain can only honour a lore body if its params permit it (e.g. tidal flats require a moon param for the D-228 tidal term — verify Velen's params before treating its coast as a contract). + + **(2) Climate — district temperature is the primitive; moisture is a separate primitive.** A simple scalar **district temperature in °C, resolved per 2×2 km district, nullable**, derived from: the **sun** (luminosity + insolation), the **planet** (the district's latitude + elevation lapse rate + orbital/axial phase = the season term, *and* day-phase = the diurnal term), and the **atmosphere** (greenhouse → the base, and heat-retention → the **diurnal-swing amplitude**; thick air = small day/night swing, thin = large). **No atmosphere → temperature is `null`**, and the entire climate/vegetation/weather branch is simply absent (an airless body's surface ice is *geology* per [D-227](#d-227), not climate). Tiles inherit their district's temperature. **Moisture** is the second primitive (water availability, from `hydrosphere`). **Everything climatic derives from temperature (+ moisture):** precipitation = f(temp, moisture); vegetation/treeline = temperature bands × elevation; long-term / seasonal-minimum temperature → `GlaciationGrade`; the cheap region seasonal/clock state ([Q-105](../questions/architecture.md#q-105)) is literally temperature(time). This formalises [D-210](#d-210)'s temperature proxy into the keystone scalar. No authored climate inputs anywhere. + + **(3) Freeze & snow — a scattered phase transition, transient in the marginal band.** Ice/snow is not a hard `temp < 0` contour but a band with **deterministic, spatially-coherent scatter** (seed-noise → a ragged, natural freeze line + microclimate; clustered patches, never per-tile dice), branched by surface type: + - **Fresh / "sweet" water (lakes, rivers):** scatter band **+5 °C → −10 °C (district mean)** — the upper +5 is night-frost (the diurnal swing) reaching the coldest / most-exposed tiles even when the mean is above zero; −10 = frozen across the whole day/night cycle. + - **Salt water (oceans):** its own **lower, wider band** (seawater onset ≈ −2 °C) with a distinct **pack-ice spatial pattern** (sheets + leads, not a lake skinning over). + - **Snow (land):** the land parallel, **gated on moisture** (cold + wet → snow accumulates; cold + dry → bare frozen ground), same scattered band. + - **Transient:** because temperature is clock-bound (season + day/night), in the marginal band ice/snow **forms in the cold phase and melts in the warm phase** — dawn frost burns off, a stream iced at dawn is crossable by noon. Passability is therefore time-of-day dynamic. (Forward contract to [Q-105](../questions/architecture.md#q-105).) + + **(4) Anti-"squaring" (warp).** A global position-keyed **domain-warp field** suppresses grid/seam artifacts — a **stateless, hash-based** pure function of `(seed, body_id, position)` (no lookup table / thread-local cache, so order-independent across threads/platforms). It keeps **f64 sub-metre precision through to the final voxel coordinate, then quantises by truncation to the integer voxel address** (a cast, not a comparison → IEEE-754-deterministic across targets). The ±8 m warp range makes FMA-contraction ULP variance (~1e-15 m) unable to shift the rounded voxel, so no platform guards are needed. The warp is *position math*, not a structural decision — [D-010](#d-010) integer discipline on all material/morphology decisions is preserved. + + **(5) Morphology — 8 families** via a strict **decision tree over integer `RegionProfile` inputs**: LavaField · FjordWall · CliffCoast · BraidedDelta · DuneStrand · IncisedGorge · MeanderReach · AlluvialPlain (fallback). Adds `tectonic_class` to `RegionProfile`. MountainPass is a zone label sharing IncisedGorge geometry. **Hard gates (boolean, pre-selection, in tree order):** fjord requires `GlaciationGrade ≥ 2`; LavaField requires `tectonic_class = Volcanic`; lithology bounds slope/form (see laws). + + **(6) Frozen 17-zone `MorphologyZone` display vocabulary** (derived labels per [D-228](#d-228), enum→label map frozen). The **canonical freeze point is the Rust `MorphologyZone` enum**; adding/renaming/removing a zone **requires a D-record amendment** (reviewer-enforced; classifier tuning that merely re-classifies a region is *not* a vocabulary change and stays free). The 17: open ocean, lake, tidal flat, dune strand, cliff coast, fjord, delta, estuarine, alluvial plain, river bank, meander reach, braided plain, valley floor, mountain pass, alpine, volcanic, wetland. **Zones ≠ families:** four (tidal flat, estuarine, alpine, wetland) are derived sub-classifications from family + elevation/water-height, not distinct generator families. + + **(7) Zone seams — prevent incompatible, allow valid (never patch).** The boolean gates are discontinuities, so "continuous inputs → continuous classification" is *not* the mechanism. The mechanism is **decision-tree gate ordering + a build-time compatibility-matrix invariant**: incompatible family pairs (e.g. MeanderReach↔Volcanic) cannot be adjacent classifier outputs (a build-time test, never a runtime override). Valid geomorphic seams (cliff↔fjord at the glaciation threshold, lithology faults) are **permitted sharp transitions** — kept sharp (real geology is sharp), made non-grid/organic by the warp; no modulation feathering. The warp also prevents degenerate slivers at chunk scale. + + **(8) Believability laws (binding):** drainage monotonicity (respect the D8 thalweg, [D-208](#d-208); tributaries join upstream; mouths at sea level); lithology→landform (Rock → vertical faces; Sand → ≤~32° angle of repose, dunes not cliffs; Gravel → braided channels/fans not single-thread meander; Soil → rolling/floodplain; Wetland → ≤5° flats; Lava → sheets/shield slopes + tubes, immature drainage); glaciation→form (fjord ≥2, U-valleys ≥1, moraines ≥1, cirques ≥2; grade 0 = V-ridges, never glacial U); climate→vegetation (treeline Forest→Scrub→Barren, no skip; riparian Thicket/Scrub 1–3 tiles along perennial waterways) — all flowing from the temperature primitive. + + **(9) Game-feel constraints:** every chunk carries ≥1 tactical decision point; cover ≠ concealment; high-ground asymmetric-but-not-dominant; chokepoints narrow enough to matter (river crossings 3–15 m, gorge floors 2–8 m); seasonal/tidal state produces **real passability changes, not cosmetic** — BraidedDelta/MeanderReach `ElevationDelta` calibrated so channels fall below and levees above the [Q-105](../questions/architecture.md#q-105) high-water threshold. + + **(10) Mechanics:** no per-tile `flow_direction[64×64]` in `ChunkContext` (D8 ≈ 152 m/cell, coarser than a chunk) → one basin-direction + global meander-curve params; features with wavelength > 64 m seed from **Region-or-higher**, not the chunk seed. `RegionProfile` is stored in `BodyWorldState` ([D-203](#d-203), ~6k/body, populated in the [D-206](#d-206) background pass) so the Atlas reads zone labels without triggering voxel derivation; voxel derivation is on-demand + cached, never persisted ([D-227](#d-227)). Budget ~2.2–4.2 ms/chunk (validate per-family in Phase 4 — FjordWall/IncisedGorge are far costlier than the AlluvialPlain fallback). +- **Rationale:** [D-227](#d-227)/[D-228](#d-228) fixed *what* a tile is (derive-don't-store voxels; orthogonal derived axes); D-239 fixes *how* the finished upper cascade (L0–L4) becomes actual tiles — the one unbuilt layer. The district-temperature primitive collapses the scattered climate inputs (precipitation, glaciation, season, snow/ice, vegetation) onto one derived scalar + moisture, keeping the whole climate branch deterministic and null-cutting airless bodies cleanly. Prevent-at-source seams + the warp give coherent, organic terrain without a runtime patch. The frozen vocabulary protects authored Atlas/wiki content while letting the classifier evolve. +- **Raised by:** tile-derivation-contract workshop (Tyre — refinement chain, warp, determinism; Gestalt — 8 families, game-feel; Troblum — feasibility, warp precision, scale corrections; Miri — believability laws, vocabulary, lore reconciliation), lead-interviewed decisions + an adversarial verification pass, 2026-06-07. +- **Dissent:** Tyre's initial cross-family elevation-blend was resolved against (prevent-at-source). Early-integer-truncation of the warp (raised against Gestalt's `ElevationDelta` ranges and by Tyre) was resolved against in favour of f64-to-voxel. +- **Cross-reference:** [D-227](#d-227) (derive-don't-store voxel model), [D-228](#d-228) (composite tile axes / cohesion / seasonal state), [D-210](#d-210) (temperature proxy — formalised), [D-203](#d-203) (BodyWorldState cache), [D-206](#d-206) (background analysis pass), [D-208](#d-208) (drainage / D8), [D-010](#d-010) (determinism), [D-234](#d-234) (street/footprint geometry — consumes morphology), [D-142](content.md#d-142) (zone types), [D-217](#d-217) (tile condition), [Q-102](../questions/architecture.md#q-102) (cohesion = the warp), [Q-103](../questions/architecture.md#q-103) (mutator schema — open), [Q-105](../questions/architecture.md#q-105) (seasonal/clock state — temperature/ElevationDelta forward contract) + +--- + +*97 decisions (D-001 through D-239, excluding gaps). Last updated: 2026-06-07 (tile-derivation-contract workshop — D-239 tile derivation contract resolving Q-101: three-carrier refinement chain, district temperature primitive + scattered transient freeze/snow model, 8 morphology families + frozen 17-zone vocabulary, prevent-at-source seams).* diff --git a/governance/questions/architecture.md b/governance/questions/architecture.md index de9162327..ab5562fed 100644 --- a/governance/questions/architecture.md +++ b/governance/questions/architecture.md @@ -356,7 +356,8 @@ Technical foundation questions: engine, protocols, data structures, performance, --- ### Q-101: Refinement / derivation contract — coarse hint → fine geometry + morphology algorithm family -- **Status:** Open — the atlas-derivation workshop's primary technical output, not yet specified (2026-05-25) +- **Status:** Resolved 2026-06-07 by [D-239](../decisions/architecture.md#d-239) +- **Resolution:** Resolved by the tile-derivation-contract workshop → D-239: a three-carrier refinement chain (`RegionProfile` ~1 km → `ChunkContext` 64 m → `VoxelColumn` 1 m), pure deterministic functions of `(seed, atlas, body-params, position)` with no authoring at the derivation layers; a stateless f64-to-voxel domain-warp as the anti-squaring mechanism; 8 morphology families (LavaField · FjordWall · CliffCoast · BraidedDelta · DuneStrand · IncisedGorge · MeanderReach · AlluvialPlain) selected by a gated decision tree over a frozen 17-zone vocabulary; a district-temperature climate primitive (2×2 km, °C, nullable) + separate moisture from which all climate/vegetation/glaciation and a scattered, transient freeze/snow model derive; `RIVER_THRESHOLD` becomes a derived per-body-class value; seams prevented at source (gate ordering + build-time matrix), valid geomorphic seams kept sharp + warped. Body-class river-density modulation and D8 flow-direction are folded in. (Ocean/lake mask: derive from the heightmap sea-level threshold per D-239's body-params input.) - **Question:** How does a coarse, map-scale hint (a ~78 km Layer-1 cell) plus a seed become fine, ~1 m-voxel coherent geometry — deterministically, without squaring — across ~3 scale jumps (78 km cell → ~1 km region → 64 m chunk → 1 m voxel)? Needs: the inter-layer hint+seed→geometry API (Tyre sketched a `RegionHint`); a **context-driven morphology algorithm family** selected by local context (rivers meander in lowlands / incise in mountains; coasts dune / crag / fjord by slope + lithology + glaciation), ≥6 distinct generators (meander, incised gorge, braided delta, dune strand, cliff coast, fjord, mountain pass); integer-only *structural* decisions (D-010), f64 confined to within-voxel interpolation; clean transitions between adjacent morphology families. **Unowned dependency:** the ocean/lake mask — D-223 stripped the markers.json polygons that D-209 `CoastalAccess`/`LakeShore` extraction referenced, so the mask must instead be derived from the heightmap sea-level threshold or a baked water-bodies layer. Lithology is a required input (promoted load-bearing by the volumetric subsurface, D-227). Also in scope: **body-class parameters** (hydrosphere, tectonic activity, atmosphere) must modulate river-network *density* — the effective river threshold — so an arid body and an oceanic body differ in *how many* rivers, not just where they run (today `RIVER_THRESHOLD` is a global constant, Nigel); and **river flow direction** (upstream/downstream — gates effort vs speed, and feeds the seasonal water model) derives from the D8 network at query time. - **Context:** The heart of the atlas→tile derivation; surfaces as the cascade builds the region → chunk → voxel layers. - **Cross-reference:** [D-227](../decisions/architecture.md#d-227), [D-228](../decisions/architecture.md#d-228), [D-208](../decisions/architecture.md#d-208), [D-209](../decisions/architecture.md#d-209), [D-223](../decisions/architecture.md#d-223), [D-010](../decisions/architecture.md#d-010), [D-222](../decisions/architecture.md#d-222)