Files
settled-reach/docs/workshops/tile-derivation-contract/troblum-round1.md
T
jpmschweitzerandClaude Opus 4.8 fa8b6ebf08 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>
2026-06-07 12:14:55 +02:00

26 KiB
Raw Blame History

title, workshop, participant, round, date
title workshop participant round date
Troblum Round 1 — Feasibility, Performance, and Determinism Stress-Test tile-derivation-contract Troblum 1 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 familymeander_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:

// 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.