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>
477 lines
26 KiB
Markdown
477 lines
26 KiB
Markdown
---
|
||
title: "Troblum Round 1 — Feasibility, Performance, and Determinism Stress-Test"
|
||
workshop: tile-derivation-contract
|
||
participant: Troblum
|
||
round: 1
|
||
date: 2026-06-07
|
||
---
|
||
|
||
# Troblum Round 1: Where the Proposal Holds and Where It Doesn't
|
||
|
||
I read Tyre's Round 1. There is a lot right in it. There are also several places
|
||
where the numbers don't hold and one architectural decision I disagree with on
|
||
technical grounds. I will work through both questions in order.
|
||
|
||
---
|
||
|
||
## Question 1: Algorithmic Morphology vs Noise Fields
|
||
|
||
The brief asks which parts of Q-101 are best served by explicit morphology algorithms
|
||
versus multi-octave / domain-warped noise, and which cross-scale coherence techniques
|
||
avoid "squaring" across the 78 km → 1 km → 64 m → 1 m jumps.
|
||
|
||
My position is that the question is slightly misframed. It is not morphology *versus*
|
||
noise. The correct framing is: **which properties are constraint-satisfying (morphology)
|
||
and which are detail-filling (noise)?** The answer to that determines which tool is
|
||
appropriate at each scale jump.
|
||
|
||
### Scale jump 1: 78 km → 1 km (classification, not derivation)
|
||
|
||
At this boundary, the job is classification. You are asking: what *kind* of terrain
|
||
does this region contain? The answer must be discrete — a MorphologyFamily enum, an
|
||
integer slope class, a glaciation grade. Noise is wrong here. A noise field varies
|
||
continuously; a morphology family does not. A valley floor does not gradually become
|
||
a fjord coast as you move across it; it transitions. The classification inputs
|
||
(elevation, flow_accumulation, slope, lithology, sea_level_margin) are all integer
|
||
or categorical. The output (`MorphologyFamily`, `RegionProfile` parameters) should
|
||
be a pure integer/lookup-table computation.
|
||
|
||
**Correct tool:** deterministic integer classification. Tyre has this right.
|
||
|
||
The anti-squaring work at this boundary is handled by Tyre's dual-classification
|
||
blending at the `ChunkContext` level (primary + secondary with `blend_weight`). I
|
||
have concerns about that mechanism that I will address under the stress-test section,
|
||
but the *type* of tool is correct.
|
||
|
||
### Scale jump 2: 1 km → 64 m (structural shape, explicit algorithm)
|
||
|
||
At this boundary the job is structural shape derivation. What is the actual terrain
|
||
doing within this chunk? Is there a meander channel? A cliff face? A dune ridge?
|
||
|
||
This is where morphology algorithms earn their place. A meander algorithm needs to
|
||
produce a geometrically consistent channel — one that has a coherent sinuosity, a
|
||
consistent bankfull width, and lateral migration that respects upstream continuity.
|
||
A noise field cannot produce this. You would get a wandering path, not a river.
|
||
|
||
**Correct tool:** explicit parameterized algorithms, one per MorphologyFamily. Each
|
||
algorithm receives `ChunkContext` and produces `ElevationDelta` values. The algorithm
|
||
is deterministic given the seed and context.
|
||
|
||
However: explicit morphology algorithms have a failure mode that is not mentioned in
|
||
Tyre's Round 1. **They produce features with characteristic scales.** A meander
|
||
algorithm parameterized for 500m wavelength sinuosity will produce visible repetition
|
||
if any chunk parameters hash to similar values. This is a form of squaring at the
|
||
algorithm level rather than the grid level. The mitigation is:
|
||
|
||
1. The algorithm parameters must be seeded per-chunk (Tyre has this via `chunk_seed`).
|
||
2. The seed must drive *continuous variation in shape parameters* (sinuosity, amplitude,
|
||
phase offset), not just a discrete selection from a small parameter table. If there
|
||
are only 8 possible parameter sets, you will see 8 repeating terrain shapes across
|
||
a large area.
|
||
3. The per-chunk seed derivation must use `SeedChain::derive(Block, chunk_id)` as Tyre
|
||
specifies — this is correct and provides good avalanche properties per D-224.
|
||
|
||
### Scale jump 3: 64 m → 1 m (detail filling, noise is appropriate)
|
||
|
||
This is where noise fields are appropriate. The structural shape has been established
|
||
by the morphology algorithm (the channel is here, the cliff face is here). The job
|
||
now is to fill in detail variation: micro-roughness on a rock face, ripple patterns
|
||
on a sand flat, surface irregularity on a soil plane. These are not structurally
|
||
meaningful — they do not change what the terrain *is*, only its surface texture.
|
||
|
||
**Correct tool:** multi-octave value noise, position-keyed on world coordinates (not
|
||
chunk-local coordinates). This is exactly what D-228 specifies for the cohesion
|
||
matrix: "global, position-keyed continuous noise field — never per-chunk."
|
||
|
||
The specific noise type matters here. Tyre specifies "value-noise + bilinear
|
||
interpolation" for the warp offset. I agree with avoiding Perlin/Simplex for
|
||
structural work (gradient noise has characteristic directional artifacts that read
|
||
as artificial), but for surface detail at the 1m scale, value noise with bilinear
|
||
interpolation is appropriate and cheap.
|
||
|
||
### Cross-scale coherence: where "squaring" actually comes from
|
||
|
||
The workshop brief asks specifically about cross-scale coherence techniques that avoid
|
||
squaring at the three boundaries. I want to be more precise about the failure modes,
|
||
because each boundary has a different one:
|
||
|
||
**Boundary 1 (78 km → 1 km):** Squaring here would be abrupt transitions between
|
||
morphology zones at region edges. This is visible in generated terrain as sudden
|
||
changes in terrain character — the flat meander plain becoming a cliff coast in one
|
||
chunk. Mitigation: Tyre's dual-classification blending. I have concerns about the
|
||
implementation (see stress-test), but the mechanism is sound.
|
||
|
||
**Boundary 2 (1 km → 64 m):** Squaring here would be chunk-grid-aligned features —
|
||
channels that terminate at chunk edges, cliff faces that align with chunk boundaries.
|
||
Mitigation: domain warping. This is the primary purpose of the warp offset. When
|
||
the warp magnitude is larger than 1 m but smaller than 64 m, it displaces the
|
||
feature positions enough that no feature aligns with the chunk grid. Tyre's 8 m
|
||
maximum warp is reasonable; I would argue for testing at 12 m to ensure grid
|
||
artifacts are suppressed at moderate distances.
|
||
|
||
**Boundary 3 (64 m → 1 m):** Squaring at this boundary would be visible as texture
|
||
tiling — the same 1 m surface detail pattern repeating at 64 m intervals. Mitigation:
|
||
the global position-keyed noise field. Because the noise is keyed to world coordinates,
|
||
the pattern at tile (x, y) is identical regardless of which chunk computed it.
|
||
|
||
One cross-scale coherence concern that is not addressed in Tyre's Round 1: **feature
|
||
scale vs chunk scale mismatch.** Consider a meander river with a wavelength of ~200 m.
|
||
A single 64 m chunk sees only a fraction of one meander bend. The algorithm that
|
||
computes the channel position within the chunk must derive it from the global meander
|
||
curve, not generate a local approximation. Otherwise adjacent chunks will produce
|
||
incoherent sections of "river" that do not connect.
|
||
|
||
This requires the meander algorithm to evaluate the global curve at arbitrary world
|
||
coordinates — the algorithm must be a *continuous spatial function*, not a
|
||
per-chunk generator. This is a real design constraint that the MorphologyFamily
|
||
interface needs to make explicit. Tyre's current interface hands the algorithm a
|
||
`ChunkContext` and expects an `ElevationDelta` per tile. That is correct, but the
|
||
algorithm implementation must evaluate a globally-consistent curve, seeded from
|
||
something broader than `chunk_seed`. I would specify this as a requirement:
|
||
any MorphologyFamily that produces features larger than a single chunk must derive
|
||
its feature-scale parameters from a seed derived at a coarser level
|
||
(`SeedChain::derive(Block, block_id)` or higher), not from `chunk_seed`.
|
||
|
||
---
|
||
|
||
## Question 2: Stress-Testing Tyre's RegionHint API
|
||
|
||
### 2.1 The warp budget claim: 0.5 ms for ~5,000 hash + bilinear evaluations
|
||
|
||
Tyre claims ~90 ns/evaluation. Let me check this.
|
||
|
||
A single evaluation is: two splitmix64 calls (one per axis), bilinear interpolation
|
||
over a 2D integer grid (4 multiplications, 4 additions, one integer table lookup per
|
||
corner = 4 lookups total), and truncation to i16. On an out-of-order superscalar CPU
|
||
at 3.6 GHz:
|
||
|
||
- splitmix64: 5–6 instructions with good ILP, roughly 2–3 cycles per call.
|
||
Two calls: ~5 cycles = ~1.4 ns.
|
||
- Bilinear over 4 integer corners: 4 table lookups (likely L1-cached if the noise
|
||
grid is small), 4 multiplications, additions: ~8–12 cycles = ~2.5–3.5 ns.
|
||
- Total per evaluation: ~4–5 ns with warm L1 cache.
|
||
|
||
For 5,000 evaluations: ~20–25 µs, not 0.5 ms. Tyre's budget is 20x conservative
|
||
on this particular item. That is fine — more headroom for the morphology algorithm.
|
||
|
||
But there is a catch: **cache behavior of the noise grid.** If the warp function
|
||
uses a discrete noise grid to interpolate from, that grid must be in cache during
|
||
the chunk derivation. Tyre says "integer-grid hash + bilinear interpolation." If
|
||
"integer-grid hash" means he is hashing the grid coordinates directly (no lookup
|
||
table), the cache issue goes away — it is pure computation. If there is a lookup
|
||
table, its size matters. A 256×256 grid of i16 values = 128 KB, which fits in L2
|
||
but not L1 on most hardware. Random access into a 128 KB table would incur L2
|
||
latency (~8 cycles per access), raising the per-evaluation cost to ~40 ns and the
|
||
total to ~200 µs. Still within the 0.5 ms budget, but Tyre should specify whether
|
||
the warp function is hash-based (fully L1-resident) or table-lookup-based.
|
||
|
||
**My recommendation:** hash-based, no lookup table. `splitmix64` applied to the
|
||
quantized grid coordinates gives well-distributed values with no cache footprint.
|
||
This is the correct implementation for a position-keyed global field.
|
||
|
||
### 2.2 The ChunkContext construction cost: 0.2 ms claim
|
||
|
||
Tyre claims ~0.2 ms for `ChunkContext` construction. The work is:
|
||
- 4×4 `RegionProfile` lookups (16 lookups from the RegionProfile cache)
|
||
- 64×64 integer bilinear interpolation from Layer-1 resolution to chunk resolution
|
||
- 64×64 D8 flow direction downsampling
|
||
|
||
The 64×64 bilinear interpolation: 4,096 interpolations, each ~8 cycles on out-of-order
|
||
hardware = ~32,768 cycles = ~9 µs at 3.6 GHz. The D8 downsampling is similar. The
|
||
`RegionProfile` lookups depend entirely on cache state — 16 lookups into what is
|
||
presumably an in-memory hashmap or fixed-size array.
|
||
|
||
This is well under 0.2 ms if everything is in L1/L2 cache. The concern is the
|
||
first access after eviction. If `RegionProfile` data has been evicted from cache
|
||
(the player moved to a new area), the 16 lookups may each incur L3 or RAM latency.
|
||
At 40 ns/access (L3) that is 640 ns — still trivial. At 100 ns/access (RAM) with
|
||
cache miss: 1.6 µs. Still negligible.
|
||
|
||
**Verdict:** 0.2 ms is 20x conservative for `ChunkContext` construction. The actual
|
||
cost is ~10–50 µs depending on cache state. This is good; it confirms the budget
|
||
has real headroom.
|
||
|
||
### 2.3 The morphology algorithm budget: 1–3 ms, family-dependent
|
||
|
||
This is the one line in Tyre's budget that I flagged as "Gestalt/Troblum to validate."
|
||
I cannot validate it without knowing the algorithm implementations, but I can define
|
||
the constraint:
|
||
|
||
For 4,096 tiles at a 3 ms budget: 732 ns per tile. That is substantial time. A
|
||
meander channel algorithm that requires computing a curve position (which involves
|
||
a trigonometric or polynomial evaluation) per tile will cost roughly:
|
||
- Sine/cosine: 20–30 cycles = ~7 ns (modern hardware with FPU)
|
||
- Polynomial approximation: 10–15 cycles = ~3–4 ns
|
||
|
||
A full meander evaluation (curve position + distance to channel + bank profile):
|
||
roughly 50–100 cycles = 15–30 ns per tile. At 4,096 tiles: 60–120 µs. Well within
|
||
the 3 ms budget.
|
||
|
||
The risky algorithms are those with **per-tile branching** (if/else chains on
|
||
material type) and **non-trivial data dependencies** (algorithms that need to know
|
||
about adjacent tiles). The second is the critical one: any algorithm that reads
|
||
adjacent tile results to compute the current tile is not embarrassingly parallel
|
||
and cannot be SIMD-vectorized. The `ChunkContext` must provide all the context
|
||
the algorithm needs without requiring cross-tile reads during derivation.
|
||
|
||
Tyre's interface (`ChunkContext` + warped position → `ElevationDelta`) correctly
|
||
forces this constraint. The algorithm sees only its own position and the pre-computed
|
||
chunk-level context. This is the right design. Gestalt needs to know it is a hard
|
||
constraint.
|
||
|
||
### 2.4 The blend_weight dual-classification mechanism
|
||
|
||
This is where I have a concrete disagreement with Tyre's design.
|
||
|
||
The proposal: boundary chunks carry both a `primary` and `secondary` `RegionProfile`,
|
||
with an integer `blend_weight` (255 = fully primary, 128 = equal mix). The blend
|
||
width is "one chunk (64 m) on each side."
|
||
|
||
**Problem 1: What does "blending" two MorphologyFamily algorithms mean?**
|
||
|
||
Tyre's `derive_voxel` step 2 says: "resolve TileAxes twice (primary + secondary
|
||
RegionProfile), then integer-blend the parameters before selecting materials." But
|
||
`ElevationDelta` from a meander algorithm and `ElevationDelta` from a fjord algorithm
|
||
are not the same kind of value. Interpolating them linearly produces terrain that
|
||
is neither a meander nor a fjord — it is an incoherent average that reads as broken
|
||
terrain. This is worse than a hard transition. A hard transition at a natural feature
|
||
boundary (a ridge, a coastline) is geomorphically plausible. A 64 m wide zone of
|
||
averaged-meander-fjord terrain is not.
|
||
|
||
This is not a theoretical concern. Amplitude-blended morphology transitions appear
|
||
in real generated terrain systems and they produce what is colloquially called
|
||
"mush terrain" — the transition zone has no readable character.
|
||
|
||
**Problem 2: The blend zone is 64 m wide, but 64 m is exactly one chunk.**
|
||
|
||
The blend is supposed to hide the region boundary. A 64 m blend zone covers one
|
||
chunk on each side. At normal play distance (say, 50–200 m from the character),
|
||
a 64 m zone is directly visible. If the terrain in that zone reads as "mush," the
|
||
player sees it. This is the opposite of hiding the seam.
|
||
|
||
**My proposed fix:**
|
||
|
||
Do not blend morphology algorithms. Instead:
|
||
|
||
1. At the region boundary, select the morphology family based on which *local
|
||
terrain type* is more consistent with the position. This is the "morphological
|
||
footprint" approach: a fjord wall adjacent to a meander plain transitions at
|
||
the geomorphically natural boundary — the base of the cliff — not at a 64 m
|
||
blending zone.
|
||
|
||
2. For the small number of morphology family pairs that genuinely need a transition
|
||
(e.g., meander reach → braided delta, which occurs naturally at a gradient change),
|
||
the transition is driven by the *local slope at the chunk level*, not by a
|
||
region-level blend weight. The chunk that contains the gradient break produces
|
||
the transition because its terrain type naturally changes at that location.
|
||
|
||
3. The only parameter blending that makes sense is blending *continuous parameters
|
||
within the same family* — `meander_intensity`, `erosion_grade`, `dune_orientation`.
|
||
These are Tyre's `RegionProfile` parameters, and blending them is legitimate
|
||
because they modulate a single algorithm's behavior. Do this.
|
||
|
||
This keeps the `secondary: Option<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 ~10–50 µs per region.
|
||
Total for 6,000 regions: 60–300 ms per body.
|
||
|
||
This is not expensive. Precomputing all RegionProfiles at body load is acceptable.
|
||
Lazy recomputation adds request latency that is visible in the Atlas viewer —
|
||
the first Atlas pan to an unloaded region takes 50–300 µs while RegionProfile
|
||
is computed. Not bad, but not zero.
|
||
|
||
My recommendation: precompute all RegionProfiles at body load, store in a
|
||
fixed-size Vec indexed by region_id. 6,000 × (size of RegionProfile struct) at
|
||
roughly 32 bytes per struct = 192 KB per body. Trivial memory footprint.
|
||
|
||
---
|
||
|
||
## Summary of Disagreements and Concerns
|
||
|
||
**Agree with:**
|
||
- Three-typed-carrier architecture (RegionProfile → ChunkContext → VoxelColumn).
|
||
Correct decomposition.
|
||
- Integer-only structural decisions, f64 confined to warp computation. Right.
|
||
- Global position-keyed warp field for chunk-boundary seam suppression.
|
||
Sound mechanism.
|
||
- SeedChain::derive for per-chunk seeds. Correct.
|
||
- Performance budget is conservative overall — the 5 ms cap has substantial headroom.
|
||
|
||
**Disagree with / flag for Round 2:**
|
||
|
||
1. **Algorithm blending across MorphologyFamily boundaries is wrong.** Blending
|
||
meander + fjord ElevationDeltas produces incoherent terrain. Blend parameters
|
||
within a family; do not interpolate between families. Requires a compatibility
|
||
matrix from Gestalt.
|
||
|
||
2. **Warp truncation to integer metres will produce stepped artifacts.** Keep
|
||
the warp at f64 sub-metre precision through the position computation; quantize
|
||
only the final voxel coordinate.
|
||
|
||
3. **The globally-coherent feature constraint is missing from the interface
|
||
contract.** Any family with feature wavelength > 64 m must use a Block-level or
|
||
higher seed, not the chunk seed. This needs to be explicit in the MorphologyFamily
|
||
trait/interface.
|
||
|
||
4. **`ChunkContext.flow_direction: [u8; 64*64]` is wrong.** A per-tile D8 direction
|
||
does not exist at D8 resolution. Replace with a per-chunk basin direction (one u8)
|
||
plus meander curve parameters.
|
||
|
||
5. **The warp function needs a golden-seed regression test.** Same class of
|
||
stability concern as `SeedDomain` discriminants. Any change to it re-rolls
|
||
every world.
|
||
|
||
6. **f64 FMA contraction is a determinism risk.** The warp function must be
|
||
protected from compiler FMA optimization or explicitly tested as bit-identical
|
||
across optimization levels.
|