docs(governance): file D-239 tile derivation contract, resolve Q-101

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) <noreply@anthropic.com>
This commit is contained in:
2026-06-07 12:14:55 +02:00
co-authored by Claude Opus 4.8
parent 6b07ab5070
commit fa8b6ebf08
10 changed files with 1839 additions and 3 deletions
@@ -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
L1L5 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 13 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
315 m, gorge floors 28 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.24.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.
@@ -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 04 | 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 312 m below the surrounding terrace. Oxbow lakes appear where prior meanders
were cut off — these are ponds of still water on the floodplain, 420 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.32.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
(13 m, provides hard cover). Oxbow lakes are impassable unless frozen or bridged.
River crossing is a chokepoint — the channel itself, 315 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 (28 m wide at floor)
and deeply incised (1580 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 (26 m wide, 0.31.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 24 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 212 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 13 dune crests visible, with inter-dune troughs.
The pattern repeats with seed-driven wavelength variation (0.71.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 1080 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 4080 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 (13 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, 28 m across, 520 m deep). Glassy,
uneven surface with no vegetation.
- **A'a (older/rough):** Sharply irregular surface with scoria ridges 14 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 13 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 (13 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
(315 m) that controlling one bank can meaningfully contest passage. The gorge
floor must be narrow enough (28 m at the cliff base) that it cannot be flanked.
### Constraint 4: The derivation must be legible at the Atlas layer
The Atlas (Phase 34 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 ~12 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.
@@ -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 13 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.
@@ -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.)
@@ -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: 56 instructions with good ILP, roughly 23 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: ~812 cycles = ~2.53.5 ns.
- Total per evaluation: ~45 ns with warm L1 cache.
For 5,000 evaluations: ~2025 µ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 ~1050 µs depending on cache state. This is good; it confirms the budget
has real headroom.
### 2.3 The morphology algorithm budget: 13 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: 2030 cycles = ~7 ns (modern hardware with FPU)
- Polynomial approximation: 1015 cycles = ~34 ns
A full meander evaluation (curve position + distance to channel + bank profile):
roughly 50100 cycles = 1530 ns per tile. At 4,096 tiles: 60120 µ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, 50200 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<RegionProfile>` 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 ~1050 µs per region.
Total for 6,000 regions: 60300 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 50300 µ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.
@@ -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 07)
- `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, // 0255 integer
pub erosion_grade: u8, // 0255 integer
pub dune_orientation: u8, // compass octant (07), 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<u16>, // 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<RegionProfile>,
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) | ~13 ms | family dependent; Gestalt/Troblum to validate |
| Material + vegetation + water derivation (4,096 tiles) | ~0.5 ms | integer table lookups |
| **Total** | **~2.24.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.
@@ -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 (L0L4 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.
+2 -1
View File
@@ -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_
+36 -1
View File
@@ -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.** L1L5 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 13 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 315 m, gorge floors 28 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.24.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 (L0L4) 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).*
+2 -1
View File
@@ -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)