Files
VoxelForge/OPSTACK-PROGRESS.md
T
Fr0zka 45c61dd00e fix(opstack): hoist the acquired column box out of the integer-XY scope
VoxelDensityOpStack.cpp(832): C2065 'Box' undeclared.

The six-box LRU refactor moved acquisition to `FColumnBox& Box = Cache.Acquire(...)`
inside `if (bIntegerXY)`, but the Computed flag is set after the column is
computed, outside that block. The previous single-box version had Box at
function scope so the write-back compiled.

Hoists `FColumnBox* AcquiredBox` beside MemoColumn and writes back through it.
The guard becomes `if (AcquiredBox)` instead of `if (bIntegerXY)`: non-null
implies the integer path, so the pointer proves its own safety rather than
relying on two conditions staying in agreement.

No caching or logic change. Other Box. uses (761-763) are in scope, verified.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 00:50:34 +02:00

4234 lines
256 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# OPSTACK — progress log
> **APPEND ONLY. Never rewrite or reorder entries.** Write the entry for a piece of work *before*
> starting it, so an abrupt session end still leaves an accurate marker.
>
> **Entry format:** date · what · believed-true · **UNVERIFIED** (everything not yet built by Jahni) ·
> next single action.
>
> This file is how a fresh context resumes. Read the last entry first, then `OPSTACK-PLAN.md §9`.
---
## 2026-07-27 — branch created, design locked, nothing built
**What:** Branch `experimental` created from `69fa73e tmp` and checked out. `main` untouched and is the
known-good fallback world. Design finalised across `OPSTACK-PLAN.md` (incl. §2.5 op taxonomy and §2.6
acceptance bar, both added after Jahni's objection that a naive "ops" reading would just be his old
room-operations system). Kickoff prompt written to `OPSTACK-PROMPT.md`.
**Believed true:** the plugin builds and runs correctly as of this commit — everything through F20
phases 1+2 is built and working (confirmed by Jahni 2026-07-26; the "PENDING BUILD" markers still in
`fable-idea.md` / `ARCHITECTURE.md` are stale and are queue item Q3). `bEnableDensityVolume` is already
set to false. Lighting is deprecated for now. Live direction before this refactor was 3D density
generation (overhangs, caves inside mountains).
**UNVERIFIED:** nothing yet — no code has been written for this refactor.
**Known open bugs, not yet fixed, documented in `AUDIT-2026-07.md §1`:** C1 large-seed noise collapse ·
C2 three cache keys missing `LayoutVersion` · C3 `FMath::PerlinNoise2D` on the SDF hot path (dormant) ·
C4 `GetPlayerPosition` zero-vector sentinel · C5 unbounded-join-on-shutdown · C6 diff-layer scaling.
**Next single action:** Phase 0.5 — the three automation tests (density purity across worker threads,
`ClassifyTile` vs brute force, `DiffLayer` under contention). Then the Phase 1 skeleton header only.
Then STOP for a build. See `OPSTACK-PROMPT.md``WHAT TO DO`.
---
## 2026-07-27 — Q4 done: the design docs are in git
**What:** `.gitignore` replaced the single `!CODEMAP.md` exception with `!*.md`, and committed
`ARCHITECTURE.md`, `AUDIT-2026-07.md`, `CLAUDE.md`, `OPSTACK-PLAN.md`, `OPSTACK-PROGRESS.md`,
`OPSTACK-PROMPT.md`, `REVIEW_FINDINGS.md`, `fable-idea.md` (commit `3128852`). `CODEMAP.md.bak` stays
ignored (`.bak` doesn't match `*.md`).
**Why first, out of queue order:** every `*.md` was ignored, so a `git commit` of this progress file
would have silently recorded nothing. The whole unattended crash-safety discipline depends on this
file being committable, so it had to lead.
**Believed true / verified:** verified — `git ls-files "*.md"` now lists all nine.
**UNVERIFIED:** nothing (no C++ touched).
**Next single action:** Phase 0.5 tests + the Phase 1 skeleton header.
---
## 2026-07-27 — starting Phase 0.5 (tests) + Phase 1 skeleton header
**Plan for this batch, written before doing it** (so an abrupt end leaves a marker):
1. `Source/VoxelForge/Private/Tests/VoxelForgeDeterminismTest.cpp` — density purity, shuffled order,
multi-threaded (`ParallelFor`), bit-equality.
2. `Source/VoxelForge/Private/Tests/VoxelForgeClassifyTileTest.cpp``ClassifyTile` verdict vs a
brute-force sweep of the exact mesher lattice.
3. `Source/VoxelForge/Private/Tests/VoxelForgeDiffLayerTest.cpp` — N readers + 1 writer, monotonic
version, no crash.
4. `Public/VoxelDensityOp.h` — the Phase 1 contract. HEADER ONLY, nothing wired into `GetDensityAt`.
**Known design constraint found while reading:** `ClassifyTile` and `GetDensityAt`'s per-chunk caches
need a live `UVoxelStrateManager`, whose only entry point is
`Initialize(UVoxelSettings*, int32)` reading `TSoftObjectPtr` pools. Tests therefore build transient
`UVoxelStrateDefinition` objects and point soft pointers at them. **If that resolve turns out not to
work headless, test 2 self-skips rather than failing** — noted here because a skip that looks like a
pass is exactly the kind of thing that hides a bug.
**UNVERIFIED:** everything below this line until Jahni builds. No test has ever been compiled in this
plugin — `Private/Tests/` is a new directory and the module has never pulled in `AutomationTest.h`.
**Next single action:** write the three test files.
---
## 2026-07-27 — batch A+B landed, BUILD GATE reached
**Commits, oldest first:**
| Commit | What | Risk |
|---|---|---|
| `3128852` | `.gitignore !*.md` + the eight design docs into git (Q4) | none |
| `6eec796` | the three Phase 0.5 tests + `VoxelForgeTestFixture.h` | new files only |
| `d41d34e` | `Public/VoxelDensityOp.h` + `EVoxelTileClass` moved to `VoxelTypes.h` | header-only |
| `73f6b26` | **AUDIT C2 fix** — five caches now key on `GetLayoutVersion()` | ⚠️ the only commit that touches a hot path |
| `b4d13e0` | regression test for C2 | new file only |
**Believed true:** `73f6b26` is behaviour-neutral at a static layout — the layout version only
moves on `Initialize`, so a running world sees the same keys it saw before, plus one `uint32`
compare per chunk-change. It changes behaviour only after a `RebuildStrates` / editor live edit,
which is the bug.
**Design decision worth not re-litigating:** `ClassifyBox` in `VoxelDensityOp.h` is deliberately
NOT source-only. `ApplyBoundarySeal` does `Max(D, SealFactor·BaseDensity)` inside its band — it
*forces* solid regardless of input, and pure direction (`FillOnly`) cannot express that. Over a box
sitting entirely in the top seal band the source says AllAir, `FillOnly` kills AllAir, both
hypotheses die → Mixed, whereas `ClassifyTile` returns AllSolid there today. Not a hole, but a
silent loss of exactly the trivial tiles T1.d exists to skip. So forcing ops reset the fold, and
ops after them still apply. The full mapping from the fold to today's `ClassifyTile` is written out
in the header.
**Deliberately NOT done, and why:** AUDIT **C1** (unbounded `SeedF`) is a one-line fix at six
sites, and `OPSTACK-PLAN §8` lists it first. I left it. It is dormant at small seeds, and applying
it changes `SeedF` from `Seed` to `hash(Seed)&0x3FFF` — i.e. it re-rolls the whole world's noise
even at today's seed, forcing a re-tune. Doing that in the same build Jahni is using to judge four
other unverified changes costs him attribution for no present-day benefit. It should land on its
own, deliberately, when he has an hour to re-tune. **It is still a real bug — do not lose it.**
**UNVERIFIED — everything above.** None of it has been compiled. In particular the test fixture's
`TSoftObjectPtr` → transient-object resolve has never run.
**Next single action:** build-free queue. Q1 (`OPSTACK-DECOMPOSITION.md`), then Q2, then Q3.
---
## 2026-07-27 — build-free queue done. END OF UNATTENDED SESSION.
**What (queue items, all zero-build-risk):**
- **Q1 — `OPSTACK-DECOMPOSITION.md`** (commit `c188ee8`). All 8 archetypes read line by line and
broken into source / combiners / modifiers / structural post, with every `FStrateGenerationParams`
field traced to its destination op. Three findings that change sequencing are in its §0.
- **Q2 — the param audit** is §9 of that same file. Every field is claimed except
`WaterLevelRelative`, which is a render/water property misfiled in the density struct (and `Lerp`'d
across strate boundaries, where a water plane arguably shouldn't be). Reported, not deleted.
- **Q3 — stale markers ticked** (commit `831ee2f`): `fable-idea` F20 phases 1/2 + F18,
`REVIEW_FINDINGS` perf pass 2 + batch 3, `ARCHITECTURE`'s biome full-param redesign. NOT ticked:
`ARCHITECTURE`'s F6 master material graph — its C++ half is built but the graph is editor-side
work that is genuinely still open.
- **Q4 — `.gitignore`** (commit `3128852`, done first out of order because progress-log commits
depended on it).
- CODEMAP §3 rows for every new/moved symbol; `OPSTACK-PLAN` phase markers ticked.
**The three findings, so they are not lost if only this file is read:**
1. **The op contract probably needs an SDF channel as well as a density channel.** Rooms, pits and
chimneys are `SmoothMin`'d in SDF space before a *single* carve, and three of the four SDF
archetypes add roughness to the SDF, not to density. A single-channel `Eval` can only overwrite —
which is also why cross-source `SmoothUnion` (*"a maze inside a mountain that looks like it
belongs"*) is not expressible without it. **Decide before porting Maze**; it is far cheaper now
than after four ports. Not applied — it is Jahni's call.
2. **Worm tunnels are why TunnelNetwork can never skip a tile.** A fielded 3D-noise carve with no
bounds forces `CarveOnly` everywhere, killing `AllSolid` for the most-used archetype. Its
amplitude is trivially capped by `WormStrength`, so ~10 lines of scalar bound recovers deep-rock
skipping. Suggests one numeric bound belongs in Phase 2, not Phase 3 as the plan has it.
3. **Disturbances already carry lattice bounds `ClassifyTile` discards** (it only tests
`ChasmDensity > 0` strate-wide). A win available to SurfaceWorld independently of everything else.
**Believed true:** the working tree is a coherent, committed state. Nine commits on `experimental`,
`main` untouched.
**UNVERIFIED — the whole C++ batch.** Nothing has been compiled. Specifically at risk:
`Private/Tests/` is a new directory, the module has never included `AutomationTest.h`, and the test
fixture's `TSoftObjectPtr` → transient-`UVoxelStrateDefinition` resolve has never run.
**Next single action: BUILD.** Then fix what the tests say, then answer
`OPSTACK-DECOMPOSITION.md §11`, then port Maze. Do not write more plugin C++ before the build.
---
## 2026-07-27 — correction to the 3rd entry above
The entry *"starting Phase 0.5"* says the `ClassifyTile` test **self-skips** if the fixture's
`TSoftObjectPtr` resolve fails headless. That was the plan; it is **not** what was written. All four
tests call `AddError(World.WhyInvalid())` and FAIL, with a message that says explicitly it is a
fixture failure and not a density bug. Failing is the right behaviour — a skip that reads as a pass
is exactly what hides bugs — but the earlier entry describes code that does not exist, so it is
corrected here rather than edited (this log is append-only).
---
## 2026-07-27 — starting Phase 1: the Maze port, OFF the hot path
**Jahni said continue and build later, delegating the open design calls. Two decided, with reasons:**
**(1) `Eval` becomes two-channel** — `FVoxelOpSample { Density, Sdf }` — per
`OPSTACK-DECOMPOSITION.md §0.1`. Maze itself forces the question: its roughness perturbs the **SDF**
(`MazeSDF += noise·Rough`), not the density. Applied to density instead, the same noise scales with
the local gradient and is a visibly different effect. Single-channel could not port Maze faithfully,
never mind compose two sources with `SmoothMin` later. Cost: one float.
**(2) The stack's density channel is INTERNAL convention (positive = SOLID), negated once by the
caller.** This **reverses what `VoxelDensityOp.h` said yesterday** (it specified MC). Reason: every
existing archetype body is written in internal convention and negates on `return`. Porting in MC
would mean flipping the sign of every line at transcription time — on the plugin's documented #1
source of confusion. Internal makes each port a literal transcription instead. `ApplyDisturbances`
and the diff layer genuinely are MC-space, but they live in `GetDensityAt` *after* the archetype
today and are NOT in this stack, so the question is deferred, not dodged.
**The shape of this batch, and why it is not "unverified code on unverified code":** the ops, the
stack and the Maze port are **all new files**, plus one mechanical extraction. **`GetDensityAt` and
`ClassifyTile` are NOT touched** — nothing in the running game can change. The port is validated by
a test that runs the op stack against `GetMazeDensity` over thousands of points and asserts
bit-equality, so Phase 1's real question ("does the source/modifier split fall out naturally?") gets
an empirical answer instead of an opinion. Wiring the stack into `GetDensityAt` waits for the build.
**Files:** `Public/VoxelDensityPrimitives.h` (spine/seal/passage lifted out of `VoxelGenerator.cpp`
so ops and the generator share ONE copy) · `Public/VoxelDensityOpStack.h` +
`Private/VoxelDensityOpStack.cpp` · `Private/Tests/VoxelForgeOpStackMazeTest.cpp` ·
edits to `VoxelDensityOp.h` and `VoxelGenerator.cpp` (include the primitives, delete the local copies).
**UNVERIFIED:** all of it, plus everything from the previous batch.
**Next single action:** write those files, then STOP.
---
## 2026-07-27 — Phase 1 done (written, not built). Maze DOES decompose.
**The Phase 1 question is answered.** `OPSTACK-PLAN §4`'s stop-trigger asked whether the
source/modifier split falls out naturally from the existing code. It does — Maze becomes seven ops
with no contortion, and three of them are already shared with other archetypes:
```
FConstantRockSource ← also TunnelNetwork's and VerticalShafts' first line
FLatticeCorridorSource ← Maze only (role 1: what makes a maze a maze)
FSdfRoughnessMod ← also VerticalShafts, FloatingIslands
FSdfCarveOp ← the same six lines currently copied in three archetypes
FOriginSpineOp ─┐
FBoundarySealOp ├─ identical in all six density functions
FPassageCarveOp ┘
```
No revert, no stop-trigger. Commit `4c53d3b`.
**What is deliberately NOT wired:** `GetDensityAt` and `ClassifyTile` are untouched, so nothing in a
running world can change. `OPSTACK-PLAN §4` Phase 1 step 3 ("GetDensityAt gains one branch") is
**held back until the build is green** — wiring an uncompiled stack into the hot path would be
exactly the stacked-unverified-work pattern `AUDIT §P3` documents. The port is validated instead by
`VoxelForge.OpStack.MazeEquivalence`.
**Two contract decisions taken (Jahni delegated them):** two-channel `Eval`, and INTERNAL sign
convention inside the stack. Both are argued in the previous entry and in the commit message. The
second **reverses** what `VoxelDensityOp.h` said on 2026-07-26 — if a later context finds MC-in-stack
written anywhere, that text is stale.
**One pre-existing hairline bug found and NOT silently patched:** at the inner edge of a seal band,
`1 - Dist/Thickness` can round to exactly `0.0f`, so `SealFactor·BaseDensity` is 0, internal density
lands on 0, and the mesher's `D >= IsoLevel` counts that point as AIR. Today's `ClassifyTile`
excludes those z from column testing and can therefore emit `AllSolid` over them. It needs the
archetype to produce air at exactly that z, so the window is hairline — but a false `AllSolid` is a
hole. The **new** seal op keeps a 1-voxel safety margin before it forces. The old path is untouched;
`VoxelForge.Determinism.ClassifyTileSoundness` would catch it if it ever fires.
**UNVERIFIED:** everything, still. Nothing has been compiled.
**Next single action: BUILD.** Then, in order: fix what the tests report → read
`MazeEquivalence`'s two numbers (how many samples differ, and how many tiles the stack can prove
uniform) → only then wire the stack into `GetDensityAt` behind a per-strate opt-in.
---
## 2026-07-27 — FIRST GREEN BUILD. Six tests ran. Five passed.
**The plugin compiles and the tests execute.** Results
(`VoxelM/Saved/Automation/Automation2026.07.27-02.36.57.csv`):
| Test | Result | What it means |
|---|---|---|
| `ClassifyTileSoundness` | ✅ | 600 tiles: 471 Mixed, 76 AllSolid, 53 AllAir. 24 brute-forced against the exact mesher lattice, **zero holes**. T1.d's soundness is machine-checked for the first time. |
| `DensityPurity` | ✅ | 10k points, shuffled order, multi-threaded, with and without carves — **bit-identical throughout**. No cache-key bug of the AUDIT C2 family survives in the density path. |
| `LiveEditInvalidation` | ✅ | 64/64 probes moved after a live edit. The C2 fix works. |
| `BoxVerdictFold` | ✅ | The op-stack fold's logic, case by case. |
| `MazeEquivalence` | ✅ (with warning) | see below |
| `DiffLayerContention` | ❌ | **my test was wrong, not the plugin** — see below |
### The two numbers that mattered
**454 of 20000 Maze samples differ, largest |delta| 1.907e-06, and ZERO land on the opposite side
of the isosurface.** 1.907e-06 is exactly one ULP at a float of magnitude 16 — i.e. the ports are
*geometrically identical*: not one triangle would move. §2.6 accepts this. Leading hypothesis for
the residue, now fixed and awaiting a re-run: the original routes the noise coordinates through an
`FVector` (double in UE5) before casting back to float, so it rounds float→double→float, while the
op passed floats straight through. Under `/fp:fast` those round in different places. The op now
reproduces the detour deliberately, with a comment saying not to "simplify" it. **If the next run
still shows drift, the next candidate is FMA contraction differing across translation units.**
**23 of 60 Maze tiles proved uniform.** Today's `ClassifyTile` proves **zero** for Maze — every cave
archetype falls through to `"pas prouvable en v1"`. That is ~38% of tiles becoming skippable for an
archetype that has never skipped one, and it is the first hard evidence for the perf half of the
whole refactor.
### The failure was mine
`Expected 'every carve was recorded' to be 400, but it was 3200.` `GetTotalModificationCount()` sums
**stored entries**, not operations — a stroke is filed under every chunk its AABB overlaps, and 400
radius-6 spheres straddling chunk corners store 8 entries each. The concurrency the test actually
exists to check all passed: **7 reader threads, 28.7 million read rounds against 760 writes and 6
`Clear()`s, no crash, monotonic version, clean state afterwards.** Assertion rewritten to compare
the stored count against the fan-out `ApplyModification` itself reported, which is a stronger check.
Worth noting for AUDIT C6: that getter is the right metric for the diff-layer scaling wall (stored
entries are what grow without bound) and the wrong name for it.
**UNVERIFIED:** the three fixes in this entry (the diff-layer assertion, the `FVector` rounding
detour, and the `FVoxelOpStack` move-only/dllexport fix that made the build pass) have not been
re-run.
**Next single action:** rebuild, re-run, and check whether `MazeEquivalence` now reports 0 differing
samples. Then wire the stack into `GetDensityAt` behind a per-strate opt-in — Phase 1 step 3, which
was deliberately held back until the build went green. It now has.
---
## 2026-07-27 (afternoon) — ALL SIX TESTS GREEN. One hypothesis killed.
Run at 12:02 today (in `Saved/Logs/VoxelM.log`; the CSV export was taken later).
| Test | Result |
|---|---|
| `ClassifyTileSoundness` | ✅ |
| `DensityPurity` | ✅ |
| `DiffLayerContention` | ✅ **now passes** — the assertion fix was right; 33.3M read rounds |
| `LiveEditInvalidation` | ✅ 64/64 probes moved |
| `BoxVerdictFold` | ✅ |
| `MazeEquivalence` | ✅ (same warning) |
**Phase 0.5's gate is met.** All three original tests plus the two op-stack ones are green on the
current code, so the op stack is no longer being built on unverified ground.
### The `FVector` rounding hypothesis was WRONG — and the way it was wrong is informative
The previous entry predicted the fix would take `MazeEquivalence` from 454 differing samples to 0.
The re-run returned **exactly 454 samples, exactly `1.90734863e-06`, at exactly `(-23, 55, -660)`**
bit-for-bit the same result. The `float → double → float` detour is a no-op, which is what
`/fp:precise` semantics say it should be. Hypothesis eliminated cleanly; the detour is harmless and
stays (it costs nothing and documents the original's shape), but it is **not** the cause.
**Stopped guessing, added a bisect** to `MazeEquivalence`: it re-runs the comparison four times,
disabling roughness → seal → spine → passages, on **both sides**, and reports which stage's removal
makes it bit-exact. One run now answers a question two guesses failed to.
**Standing hypothesis, to be confirmed or killed by that bisect:** the residue is compiler
float-contraction across translation units (`/fp:fast` lets the same expression reassociate
differently in `VoxelGenerator.cpp` and `VoxelDensityOpStack.cpp`), worth ~1 ULP. It fits the
~2% hit rate: **only voxels inside the narrow SDF blend shell have an unsaturated carve factor**
everywhere else `Carve` is exactly 0 or exactly 1 and both paths agree bit for bit. If that is
confirmed, **bit-identity is not achievable in principle for these ports**, and the standard for
every later archetype becomes "zero isosurface crossings", not "zero differing floats". That is a
conclusion worth having explicitly rather than re-deriving per port.
**UNVERIFIED:** the bisect itself.
**Next single action:** re-run `MazeEquivalence` and read the bisect table. Then Phase 1 step 3 —
wire the stack into `GetDensityAt` behind a per-strate opt-in.
---
## 2026-07-27 (afternoon) — the residue is the COMPILER. Measured, not guessed.
**Bisect result:**
```
roughness off -> 151 / 5000 differ (max |delta| 1.907e-06)
roughness + seal off -> 155 / 5000 differ (max |delta| 9.537e-07)
roughness + seal + spine off -> 126 / 5000 differ (max |delta| 9.537e-07)
corridors + carve ONLY -> 126 / 5000 differ (max |delta| 9.537e-07)
```
The residue survives every stage removal, down to **constant rock + capsule SDF + carve** — code
that is a character-for-character transcription. So it is not in anything the decomposition added.
**Cause, from the engine source rather than from memory** (`VCToolChain.cs`):
```csharp
case FPSemanticsMode.Default: // Default is imprecise FP semantics.
case FPSemanticsMode.Imprecise: Arguments.Add("/fp:fast"); break;
```
UBT's own doc for that mode: *"FP math isn't IEEE-754 compliant: the compiler is allowed to transform
math expressions in ways that might result in differently rounded results."* The plugin sets no
override, so identical source in `VoxelGenerator.cpp` and `VoxelDensityOpStack.cpp` may legitimately
reassociate differently — worth about 1 ULP.
Two prior hypotheses were wrong (the `FVector` round-trip, then "check the roughness window / carve
blend"). The bisect cost one build and settled it. **Noted as a working lesson: on a numeric
discrepancy, bisect before hypothesising a third time.**
**Why exactly ~2.3% of samples:** `Blend - Sdf` catastrophically cancels at the edge of the blend
shell, amplifying a 1-ULP SDF difference into a 1-ULP density difference. Outside that thin shell
`Carve` is exactly 0 or exactly 1 and both paths agree bit for bit.
### Consequences recorded
1. **`OPSTACK-PLAN §2.6`** — bit-identity is not achievable in principle for these ports, at any
level of care. The operational bar for every remaining archetype, now encoded in the test:
**hard-fail on isosurface crossings · tolerate ULP-scale deltas · warn on anything larger**
(that last one is real port drift, and the test no longer cries wolf about the floor).
2. **`AUDIT-2026-07.md §C9` (new)** — the part that matters more than the port: `ARCHITECTURE §9.1`'s
multiplayer model is "replicate the seed, every peer regenerates identically", and under
`/fp:fast` that holds **only between bit-identical binaries**. Same build, same platform: fine
(`DensityPurity` proves it). Windows client + Linux dedicated server both regenerating
authoritative geometry: a real desync source, presenting as rare unreproducible geometry-only
divergence. The knob is `FPSemantics = FPSemanticsMode.Precise` in `VoxelForge.Build.cs`, and
**it should not be turned speculatively** — it blocks the vectorisation T2.a was chasing, on the
plugin's hot loop, for an unmeasured cost. Decision needs a profile and a confirmed
cross-platform requirement.
**UNVERIFIED:** the test's new ULP-tolerance branch (expect `MazeEquivalence` to report the same 454
samples as INFO rather than WARNING next run).
**Next single action:** Phase 1 step 3 — wire the stack into `GetDensityAt` behind a per-strate
opt-in. Phase 1's question is fully answered: Maze decomposes cleanly, the stack is
window-invariant, and it proves 23/60 tiles uniform where `ClassifyTile` proves zero.
---
## 2026-07-27 — /fp:fast hypothesis DEAD. Third wrong guess. Switching to instrumentation.
**Jahni built with `FPSemantics = FPSemanticsMode.Precise` and got a BYTE-IDENTICAL result:** same
454 samples, same `1.90734863e-06`, same `(-23, 55, -660)`. A different float model producing
identical output is not "the same rounding error twice" — it is proof that **rounding is not the
cause at all.** The residue is a real, deterministic LOGIC difference somewhere in a transcription I
have read three times and believe to be identical.
**Track record on this one discrepancy, recorded because the pattern matters more than the bug:**
| # | Hypothesis | Killed by |
|---|---|---|
| 1 | `FVector` float→double→float round-trip | re-run returned the identical result |
| 2 | "check the roughness window / carve blend / octave count" | the bisect: residue survives to `corridors + carve ONLY` |
| 3 | `/fp:fast` cross-TU reassociation | `/fp:precise` build returned the identical result |
Three hypotheses, all plausible, all reasoned from *what could explain it* rather than from
measurement. Each cost a build cycle. **The lesson is not "be smarter", it is "instrument earlier":**
the bisect (measurement) produced more information in one run than two hypotheses did in three.
**Corrected in the docs:** `AUDIT-2026-07.md §C9` and `OPSTACK-PLAN §2.6` both assert the `/fp:fast`
story as the explanation for the residue. **That specific claim is now falsified and must be walked
back** — see the next entry. (The *separate* C9 finding, that UBT's FP default differs by toolchain
and the MP model assumes bit-reproducible terrain, still stands on its own: it was read out of
`VCToolChain.cs` / `ClangToolChain.cs`, not inferred from this test.)
**What changed in code:** `FVoxelOpStack::EvalSample` now exposes the full `FVoxelOpSample`, and
`MazeEquivalence` dumps the worst point in raw hex — both densities, the stack's internal SDF, and
the carve factor reconstructed from each side. That last one localises the divergence: identical
recovered carve with differing density ⇒ the fault is after the conversion; differing carve ⇒ it is
in the SDF (lattice edges or `VoxelSDF::Capsule`) or in `SmoothStep01`.
**UNVERIFIED:** the instrumentation itself.
**Next single action:** re-run `MazeEquivalence` and read the WORST-POINT DUMP. Phase 1 step 3
(wiring the stack into `GetDensityAt`) is deliberately **paused** until this is understood — it is a
small unexplained numeric difference, and those do not get smaller when you build on them.
---
## 2026-07-27 — CORRECTION: hypothesis 3 was never tested. My error, not Jahni's.
**`FPSemantics = FPSemanticsMode.Precise` was set in `Source/VoxelM/VoxelM.Build.cs` — the GAME
module.** `FPSemantics` is a `ModuleRules` property, i.e. **per module**. Every line of density code
lives in the `VoxelForge` module, which was still compiling `/fp:fast`. The float model never
changed for the code under test.
**So the previous entry is wrong and is retracted:** `/fp:fast` is NOT eliminated. The run that
"reproduced the residue under precise semantics" reproduced it under *fast* semantics, which is
exactly what it should have done and proves nothing.
**The failure mode, stated plainly because it is the third time in one day:** I accepted "it's with
fpsemantics precise" as fact and reasoned a confident conclusion from it — one paragraph after
writing that the lesson was to instrument rather than assume. The check took one `grep` and I did it
only after Jahni suggested it. **Verify the premise before reasoning from it, especially when the
premise is what makes the conclusion interesting.**
**Consequently:** the notes in the previous entry saying `AUDIT §C9` and `OPSTACK-PLAN §2.6` are
falsified are themselves withdrawn. Those documents' `/fp:fast` explanation is back to *plausible
and now genuinely about to be tested*. Nothing in them needs changing yet.
**What changed:** `FPSemantics = FPSemanticsMode.Precise` added to **`VoxelForge.Build.cs`**, clearly
marked as a temporary experiment with removal instructions and a read-the-result guide.
`VoxelM.Build.cs` can keep or drop its copy — it is harmless either way and irrelevant to this test.
**One build now answers everything**, because the WORST-POINT DUMP instrumentation is already in:
| Result | Meaning | Next |
|---|---|---|
| 454 → **0** | the FP model WAS the cause | remove the line; decide separately whether precise is worth its cost on the hot path (needs a profile) |
| 454 → **454** | real logic difference | remove the line; read the dump — identical recovered carve ⇒ fault after the conversion, differing carve ⇒ fault in the SDF or `SmoothStep01` |
**UNVERIFIED:** everything about this experiment.
**Next single action:** rebuild, re-run `MazeEquivalence`, read the two numbers. Phase 1 step 3 stays
paused.
---
## 2026-07-27 — FPSemantics on VoxelForge does not build. Reverted; measuring a safer way.
**What happened:** setting `FPSemantics` on the VoxelForge module broke the build with ~30 errors —
`UMaterialInterface`, `USoundBase`, `TSubclassOf<AActor>`, `APawn`, `ENABLE_DRAW_DEBUG` all
"undefined type". **None of them are FP-related.**
**Why:** UBT can only share a precompiled header between modules whose **compile environments
match**. Changing `FPSemantics` changed VoxelForge's environment, so it lost eligibility for the
engine's shared PCH — and with it ~30 includes the plugin has always been getting for free.
**Genuine latent finding, worth its own item some day:** several public headers use engine types they
never include (`VoxelBiomeDefinition.h`, `VoxelStrateDefinition.h`, `VoxelSettings.h`,
`VoxelStrateTypes.h`, `VoxelContentManager.h`, `VoxelDensityVolume.h`, and `VoxelWorld.cpp`). The
plugin compiles today only because the shared PCH supplies them. UE has been moving away from
implicit shared-PCH includes for years, so this will need doing eventually — **but not inside an
unrelated diagnostic**, which is why it was reverted rather than chased.
**Reverted**, with the reason written into `VoxelForge.Build.cs` so nobody retries it blind.
### The FP question, answered without touching build settings
`MazeEquivalence` now compiles a **verbatim copy of the Maze core into the TEST's translation unit**
and compares three implementations of the same source:
```
A = GetMazeDensity (VoxelGenerator.cpp TU)
B = the operator stack (VoxelDensityOpStack.cpp TU)
C = MazeCoreVerbatim (the test's own TU)
```
- **A != C** ⇒ identical source, different TU, different result ⇒ the compiler, not the port.
Nothing to fix; record it and move on.
- **A == C, B != C** ⇒ the source IS stable across TUs ⇒ the operator stack differs for a **logic**
reason, and it is in `FLatticeCorridorSource` or `FSdfCarveOp`.
Duplicating code is normally a fault; here it is the only instrument that answers the question,
because three careful readings all concluded "identical" and the test disagrees. It is marked
diagnostic-only and comes out once the answer is in.
**UNVERIFIED:** everything in this entry.
**Next single action:** rebuild (normal incremental now — the Build.cs change is reverted) and read
the THREE-WAY block. Phase 1 step 3 still paused.
---
## 2026-07-27 — THREE-WAY VERDICT: the fault is MINE, in the operator stack.
```
A generator TU vs B opstack TU : 126 differ
A generator TU vs C test TU : 0 differ <-- identical source, different TU, SAME result
B opstack TU vs C test TU : 126 differ
```
**A == C settles it: the source is stable across translation units.** So the compiler was never the
cause, and the operator stack differs for a **logic** reason. Fourth hypothesis dead — but this one
points at code I own, which is the first time the answer has been actionable.
**Correction to walk back in the docs** (not yet done — do it once the cause is known, so it is
corrected with the right explanation rather than twice):
- `OPSTACK-PLAN §2.6`'s green note claims bit-identity is unachievable because of `/fp:fast`.
**False.** `A == C` proves identical source reproduces exactly across TUs here.
- `AUDIT-2026-07.md §C9`'s *first* consequence ("refactors cannot be bit-identical") is likewise
false and must go. **C9's second half stands** — UBT's FP default genuinely differs by toolchain,
read straight out of `VCToolChain.cs` / `ClangToolChain.cs`, and the MP model does assume
bit-reproducible terrain. That half was never inferred from this test.
- The test's own INFO text ("this is the expected floor... /fp:fast") is wrong for the same reason
and gets rewritten with the real cause.
**Also learned, and worth keeping:** setting `FPSemantics` on VoxelForge costs the module the
engine's shared PCH and exposes ~30 missing includes across seven files. Recorded in `Build.cs`.
### Where the fault is NOT
Read line by line against the verbatim copy, all identical: the ctor's `FMath::Max` clamps, the
cell `FloorToInt`, `NodeCenter`, `EdgeOpen`'s hashes and salts, the `{-1,0}³` sweep and its add
order, the capsule loop, the `FMath::Min` fold, the carve's clamp/smoothstep/subtract, and the four
structural-post no-ops. Three readings said "identical" and the measurement disagrees, so **reading
is not going to find it** — hence more instrument, less staring.
### The instrument now in place
`MazeCoreVerbatim` optionally returns its **SDF** and edge count, and the three-way compares the SDF
channels directly instead of inferring from densities:
- **SDF identical, density differs** ⇒ fault is in `FSdfCarveOp`.
- **SDF differs** ⇒ fault is in `FLatticeCorridorSource` (edge set or capsule fold).
It also reports the split across all 126 mismatches, and dumps the first one with raw hex plus the
verbatim edge count — so if the edge SETS differ (a cache-key bug) that shows up as a count mismatch
immediately.
**UNVERIFIED:** the instrumentation.
**Next single action:** rebuild, read `FIRST B-vs-C MISMATCH`. It names the file to open.
---
## 2026-07-27 — LOCALISED to the carve. Testing the right variable this time.
**The diagnostic pinned it exactly:**
```
SDF stack -1.76393199 [0xBFE1C886] verbatim -1.76393199 [0xBFE1C886] IDENTICAL
MC stack 7.83939362 [0x40FADC50] verbatim 7.83939266 [0x40FADC4E] 2 ULP apart
across all mismatches: SDF differs 0, SDF identical but density differs 126
```
So the lattice, the hashes, the edge set and `VoxelSDF::Capsule` are all **exactly right** — 126 of
126. The entire difference is in `FSdfCarveOp`, whose expression is character-identical to the
original and whose inputs (`Sdf`, `Blend` 2.0, `BaseDensity` 8) are bit-identical.
**Identical inputs + identical expression + different output ⇒ the arithmetic is being *evaluated*
differently.** And `SmoothStep01` is `x * x * (3.0f - 2.0f * x)``3.0f - 2.0f*x` is exactly the
shape MSVC fuses into an FMA, which is one rounding instead of two: **~1 ULP.**
**Why the three-way missed it — worth recording, because it is a reasoning error, not a coding one.**
`A` (GetMazeDensity) and `C` (the verbatim copy) are both straight-line, inlined code. `B` goes
through a **virtual** `IVoxelDensityOp` call, so `FSdfCarveOp::Eval` is compiled out-of-line and can
get a different contraction decision. The three-way tested *"does the translation-unit boundary
change the result?"* — it does not — but the real variable is *"does the optimisation context change
the result?"*. **I designed a clean experiment for the wrong variable, and then believed its answer.**
Hypothesis 3 was not wrong about the mechanism (`/fp:fast` contraction); it was wrong about the test.
**The experiment now added** isolates exactly that variable: the same carve expression, in the same
translation unit, once `FORCEINLINE` and once `FORCENOINLINE`.
- **inlined != FORCENOINLINE** ⇒ FP contraction confirmed. **The port has no bug** — the operator
stack is arithmetically correct and the residue is unavoidable wherever an op is a virtual call.
Then: correct the docs with the *real* reason, accept the ULP floor, move on to step 3.
- **inlined == FORCENOINLINE** ⇒ contraction is not it, and there is a real logic bug in
`FSdfCarveOp` that has now survived four readings.
**UNVERIFIED:** the experiment.
**Next single action:** rebuild, read `INLINING EXPERIMENT`. Either way the answer is final — the
inputs are proven bit-identical, so only the evaluation can differ.
---
## 2026-07-27 — the variable is COMPILE-TIME CONSTANT vs RUNTIME DATA. One line left to confirm.
**The inlining experiment partitioned the measurements perfectly, just not the way it was framed:**
```
inlined carve != FORCENOINLINE carve : 0 <-- inlining is NOT the variable
FORCENOINLINE == operator stack : 5000/5000 <-- test-TU carve == other-TU op, ALWAYS
inlined == verbatim : 4874/5000 <-- 126 differ, IN THE SAME TU
```
The test-TU carve matches the operator stack **in a different TU** perfectly, yet disagrees with the
verbatim **in its own TU**. So neither the TU boundary nor inlining is the variable. Sorting the five
implementations by the one remaining difference:
| Implementation | `Blend` is | Group |
|---|---|---|
| `GetMazeDensity` (A) | `const float Blend = 2.0f` | **compile-time constant** |
| `MazeCoreVerbatim` (C) | `const float Blend = 2.0f` | **compile-time constant** |
| `FSdfCarveOp` (B) | a class member | **runtime data** |
| `CarveInlined` | a parameter | **runtime data** |
| `CarveNoInline` | a parameter | **runtime data** |
A == C. B == CarveInlined == CarveNoInline. The two groups differ. **Every single observation from
today fits that split, and nothing else does.**
**Mechanism:** under `/fp:fast`, folding `Blend * 2.0f` to the literal `4.0f` at compile time enables
a contraction in `SmoothStep01`'s `3.0f - 2.0f*x` — one rounding instead of two — that the runtime
form cannot get. ~1 ULP.
**Why this matters far beyond the bug:** an operator's parameters are **data by design** — that is
the entire point of the refactor. They can never go back to being compile-time literals. So this
ULP-level difference is **inherent and permanent** for every archetype port, and no amount of care in
transcription will remove it. That is the real, precise reason bit-identity is unachievable here —
not the vague `/fp:fast` hand-wave I put in the docs earlier, which happened to name the right
compiler flag for the wrong reason.
**Confirming line added:** `CarveConstBlend` — identical to `CarveInlined` except `Blend` is a
compile-time constant. Predicted: matches the verbatim 5000/5000, differs from the runtime form on
exactly 126.
**UNVERIFIED:** that prediction.
**Next single action:** rebuild, read `CARVE VARIABLE ISOLATION`. If it lands as predicted: correct
`OPSTACK-PLAN §2.6`, `AUDIT §C9` and the test's INFO text with the real reason, delete the diagnostic
scaffolding, and **resume Phase 1 step 3** — the port is proven correct (SDF exact on 126/126, only
the final rounding differs, 0 isosurface crossings).
---
## 2026-07-27 — hypothesis 5 dead too. One unambiguous check left, then I stop chasing.
```
CONST-Blend == verbatim : 4874 / 5000
CONST-Blend != runtime-Blend : 0
```
`Blend`'s constness is **not** the variable: the const and runtime carve forms are bit-identical to
each other, and both miss the verbatim on the same 126. Five hypotheses, five dead.
**Worse, one of the numbers I reasoned from was circular.** `FORCENOINLINE == operator stack :
5000/5000` cannot fail by construction — it feeds `S.Sdf` to a carve and compares against the density
the stack computed *from that same `S.Sdf`*. It measures nothing. I read it as corroboration.
The two carve bodies have now been dumped from the file and diffed: **character-identical, same
translation unit.** So of the three things I keep calling identical — expression, TU, input — one is
false, and the counters cannot say which, because the SDF comparison only ran *inside* the mismatch
branch.
**The unambiguous check added:** feed my carve the SDF the verbatim reports using, compare to the
verbatim's own output, and count `S.Sdf != VerbSdf` **directly, with no enclosing condition**.
- `Recon == Ver` everywhere **and** SDFs equal everywhere ⇒ same function, same input, different
output ⇒ the difference is a measurement artefact, not a code one.
- SDFs differ ⇒ they were never equal outside the mismatch set, the earlier counter was misleading,
and the fault is back in the lattice after all.
### Proportion check — this is the last build I would spend on it
**The port is already verified on every axis that affects the game:** SDF bit-exact on 126/126, the
lattice/hashes/`Capsule` exactly right, **0 isosurface crossings out of 20000**, geometry identical,
window-invariant across threads, and every box verdict survives brute force. The open question is why
the *final rounding* differs by 1-2 ULP — and no decision anywhere in this project turns on the
answer.
**So: if this check doesn't resolve it, accept and move to Phase 1 step 3.** An unexplained
deterministic difference deserves real effort — it is a real bug often enough to be worth six builds —
but not unbounded effort when every consequence of it is already measured and benign.
**UNVERIFIED:** the check.
**Next single action:** rebuild, read `UNAMBIGUOUS DISCRIMINATOR`. Then either fix or accept, and in
both cases correct `OPSTACK-PLAN §2.6` / `AUDIT §C9` / the test's INFO text, strip the diagnostic
scaffolding, and resume step 3.
---
## 2026-07-27 — ULP residue PARKED by decision. Phase 1 closed. Moving to step 3.
**Jahni's call, and the right one:** pin it and move on. Six builds spent; the information stopped
being worth the cost.
**The final run did close it as far as it can be closed:**
```
my carve(verbatim's own SDF) == verbatim : 4874 / 5000
stack SDF != verbatim SDF : 0 (counted directly, no condition)
```
SDFs identical **everywhere**, not just among mismatches. Two character-identical carve
implementations, in the **same translation unit**, fed a **provably identical** input, differ by
1 ULP on 126/5000. For deterministic code that is only possible if they compile to different
instruction sequences — which is exactly what `/fp:fast` permits based on surrounding context, with
no single isolable axis. **Hypothesis 3 was right about the mechanism and wrong about every clean
variable I proposed for it**, which is why four carefully-designed isolation tests all came back
negative.
The one experiment that would settle it (`FPSemantics = Precise` on this module) is blocked behind
the shared-PCH / IWYU debt. Worth clearing on its own terms; not worth clearing to chase 1 ULP.
**Recorded as `AUDIT-2026-07.md §C10`** with the full refutation table, so the next context cannot
re-derive the same five hypotheses at a build each. `OPSTACK-PLAN §2.6` and `§C9` corrected — my
earlier `/fp:fast`-across-TUs explanation was wrong and is gone.
**Scaffolding stripped** from `MazeEquivalence`: the verbatim copy, the three-way, the bisect, the
inlining/constness experiments and the worst-point dump are all out. What remains is the permanent
value: the equivalence check with ULP grading, the window-invariance check, and the box-verdict
brute force.
### Phase 1 is closed. What it proved
- Maze decomposes into **seven** ops with no contortion; three are already shared with other archetypes.
- The **SDF is bit-exact** — lattice, hashes, `Capsule` all correct.
- **Zero isosurface crossings** — geometrically identical to the original.
- **Window-invariant** across query order and worker threads.
- **Every box verdict survives brute force**, and **23 of 60 tiles prove uniform** where
`ClassifyTile` proves zero for any cave archetype. That is the perf case, measured.
**Next single action:** Phase 1 step 3 — wire the stack into `GetDensityAt` behind a per-strate
opt-in, so a Maze strate can be A/B-switched in the editor and judged on a screenshot (§2.6's bar).
---
## 2026-07-27 — Phase 1 step 3: the stack is WIRED IN, behind a per-strate opt-in.
**`OPSTACK-PLAN §4` Phase 1 step 3 done.** `GetDensityAt` gains exactly one branch, as the plan
specified, and both systems now coexist.
**Files:**
- `VoxelStrateDefinition.h``bool bUseOperatorStack` (EditAnywhere, "Use Operator Stack
(experimental)"). The A/B switch §2.6's acceptance bar needs: flip it, regenerate, judge the
screenshot.
- `VoxelStrateManager.{h,cpp}``UsesOperatorStackForChunk()`. **The ported-archetype list lives
here and nowhere else**, so an unported archetype ignores the flag and falls back to the switch.
Ticking the box on any strate is therefore harmless today; only `Maze` changes behaviour.
- `VoxelGenerator.cpp``CP_OpStack` / `CP_UseOpStack` built in the SAME refetch block as the
params (so the chunk+`LayoutVersion` key already covers it, no new invalidation logic), plus one
`if (CP_UseOpStack)` on the dispatch.
**Cost on the hot path: one bool test per voxel.** The stack is built per chunk, never per voxel —
the same cadence as the existing param refetch. `ApplyDisturbances` and the diff layer are
deliberately left OUTSIDE the stack and run once for both paths, exactly as before, so the tail of
the pipeline is untouched.
**Defensive choice worth noting:** if `UsesOperatorStackForChunk` ever returns true for an archetype
with no builder, the code clears the flag and falls back to the switch rather than generating an
empty stack. A world that is *unported* is recoverable; a world that is *wrong* is not.
**UNVERIFIED:** not compiled. Likely error spots: the `else switch` form in `GetDensityAt`,
`FVoxelOpStack` as a `thread_local` (it is move-only — move-assign from a temporary is used to
reset it), and the new include in `VoxelGenerator.cpp`.
**What to look at in the editor:** set a Maze strate's `bUseOperatorStack`, regenerate, and compare
against the same seed with it off. **Pass = recognisably the same maze** — same corridor scale, same
connectivity, same feel. That is §2.6's bar, and it is the last thing Phase 1 needs.
**Next single action:** build, then the visual A/B. After that, Phase 2 — the port order in
`OPSTACK-DECOMPOSITION §10.4` starts with FlatPlain + CrystalChamber collapsing into one op.
---
## 2026-07-27 — session handoff written. 39 UE skills installed.
**`OPSTACK-HANDOFF.md`** added at the plugin root: a pasteable resume prompt for a fresh session.
Read order, exact current state, the immediate next action, the hard rules, the open items, and the
method lesson from this session.
**39 Unreal skills installed.** Jahni dropped a UE skills library into `.claude/skills/`, but nested
as `.claude/skills/core/<name>/SKILL.md` — two levels deep, where Claude Code discovers skills one
level deep at `.claude/skills/<name>/SKILL.md`. None were being loaded. Flattened (39 SKILL.md, 124
reference files, all frontmatter valid, folder names already matched `name:`); `core/category.md`
left in place as documentation. Confirmed loading.
⚠️ **They are untracked**: `.gitignore` starts with `*`, and git cannot re-include a file whose parent
directory is excluded, so nothing under `.claude/` can be tracked without un-ignoring the directory
itself. Same shape as AUDIT P1. Flagged to Jahni, not actioned — vendoring a reference library into
the plugin repo is his call.
**Immediately relevant to open work:** `module-and-build-system` documents `PCHUsage` / shared PCH /
IWYU — i.e. the exact mechanism that blocked §C10's settling experiment. That skill was in the repo,
undiscovered, while it was worked out the slow way. Worth reading before clearing the IWYU debt.
**Next single action unchanged:** build Phase 1 step 3, then the visual A/B on a Maze strate.
---
## 2026-07-27 — fresh context: two gaps closed in the step-3 wiring BEFORE the build.
Resumed from `OPSTACK-HANDOFF.md`. Read the step-3 diff against the test path instead of taking
"not compiled" as "nothing to check first" — the symbols all line up (`BuildMazeStack`'s five params
match, `Seed`/`OriginSpineRadius` are generator members, `FVoxelOpContext` comes in transitively via
`VoxelDensityOp.h`), but **the production path and the test path did not agree on two things.**
**1. `PrepareChunk` was never called in production.** The test calls it (`…MazeTest.cpp:133`);
`GetDensityAt` did not. All seven concrete `PrepareChunk` bodies are empty today, so this changes
**nothing** now — which is exactly why it was worth fixing before it could bite. The first op that
hoists real per-chunk work would have been **green in test and silently wrong in game**, and that
class of bug is expensive to find precisely because the test says yes. `GetDensityAt` now builds an
`FVoxelOpContext` (chunk, seed, layout version, strate Z bounds) in the same refetch block and calls
`PrepareChunk` on it. `Step` stays 1 — `GetDensityAt` genuinely does not know the mesher's sampling
step (T2.b contract); noted rather than guessed.
**2. The degenerate-strate early-out had no counterpart.** `GetMazeDensity` opens with
`if (StrateHeight <= 0.0f) return 1.0f;` — air. The stack has no such early-out **by design** (the
test asserts this and refuses to run on a degenerate strate). Unguarded, a zero-height Maze strate
would give **air on one path and whatever the spine/seal ops make of a zero-height band on the
other**. `GetDensityAt` now falls back to the `switch` in that case, so the reference behaviour is
the behaviour. Reachability is not the point — the archetype guard exists, so its port needs one.
**Docs corrected, since both were now actively false:**
- `VoxelDensityOpStack.h`'s banner still said "NOTHING HERE FEEDS THE GAME". It does feed the game
now, behind the opt-in. Rewritten to say exactly what is wired (`GetDensityAt`) and what is not
(`ClassifyTile` — still hand-written guards, that is Phase 2), plus the §C10 "never compare the
two paths" rule at the point of use.
- `CODEMAP §3.2d` had the same stale claim; `§3` gained rows for `UsesOperatorStackForChunk` and
`bUseOperatorStack`, and the `BuildMazeStack` row now carries the degenerate-strate precondition.
**UNVERIFIED:** still not compiled — that is Jahni's call and it is the immediate next action.
Error spots unchanged, plus one: `FVoxelOpContext` is aggregate-initialised field-by-field, so a
field rename would show up here.
**Next single action unchanged:** build, then the visual A/B on a Maze strate (§2.6's bar —
recognisably the same maze, judged on a screenshot). Then Phase 2, starting with the §3.1 question.
---
## 2026-07-27 — PHASE 1 CLOSED (visual A/B passed). Phase 2 opened: the slab collapse.
**Phase 1's acceptance bar is met.** Jahni built step 3, ticked `bUseOperatorStack` on the Maze
strate and compared: *"it's hard to see with our current maze (which is simple in architecture) but
seems like it's pretty similar, if not entirely similar."* That is §2.6's bar — recognisably the
same maze — and it is worth being precise about how much weight it carries.
**The screenshot is the weakest evidence Phase 1 has, and that is fine, because it was never
carrying the argument.** A simple maze is a poor visual discriminator: "hard to tell apart" is
*exactly* what the measurements already predicted, since **0 of 20 000 samples cross the
isosurface** — no triangle can move. The A/B's job was to catch the class of error the numbers
cannot see (wrong params reaching the stack, wrong strate, wrong wiring), and it did that. The
geometric claim rests on the numbers, and always did.
### Phase 2, first port: FlatPlain + CrystalChamber → ONE op
**§3.1 answered by Jahni: the Z term can go.** So it is gone, and this is the change that makes the
rest worth doing.
**Two separate changes landed together, deliberately, and the test is what keeps them attributable:**
1. **The design change**`GetSlabDensity`'s floor and ceiling noise lost their Z terms
(`WorldZ * FF * 0.05f``0.0f`; `WorldZ * CF * 0.08f + 3000.0f``3000.0f`, keeping the
decorrelation offset). The world **re-tunes once**: a different slice of the noise field means a
different floor/ceiling shape. Not a degradation — a different draw.
2. **The refactor** — the now-XY-pure function ported to `FSlabVoidSource` + `FGridColumnMod`,
plus the three structural ops. Five ops.
`SlabEquivalence` compares the stack against `GetSlabDensity` **as it is now**, so: green ⇒ the port
is a pure refactor ⇒ **any visual delta is attributable to the Z-term removal and nothing else.**
That is why both could go in one build without losing the ability to say which one caused what —
the attribution comes from the test, not from the build order.
**Why this port matters more than its size:** `BuildSlabStack` has **no branch on archetype**,
because `GetSlabDensity` never had one either — CrystalChamber IS FlatPlain with a bigger
`CeilingRoughness`. The test runs the identical battery on both slots, so "two archetypes are one
op" is demonstrated rather than asserted. **8 archetypes → 7.**
**And the perf claim, which is what §3.1 was really about:** `FSlabVoidSource::ClassifyBox` is
**exact and needs no sampling**. `VoxelNoise::FBM` is contractually `[-1,1]`, so both surfaces live
in Z bands with known bounds — a tile below `FloorZ - FloorAmp` is provably solid, a tile strictly
between the bands is provably air. A slab strate is mostly solid rock below its floor, so this
should prove a large fraction of tiles. `ClassifyTile` proves **zero** today. The test prints the
count per archetype; that number is the whole return on the Z term.
`FGridColumnMod` returning `Identity` when no column reaches the box is what lets the source's
`AllAir` survive the fold — otherwise columns would kill every air verdict in the strate.
**UNVERIFIED: none of this is compiled.** Likely error spots, in order:
- `VoxelForgeOpStackSlabTest.cpp` is new — check it is picked up by the module's build.
- The lambda `RunForSlot` captures `World`/`Gen` by reference and calls `AddError`/`TestEqual` on
the test instance; `TestEqual`'s name argument is built with `*FString::Printf(...)`.
- `FGridColumnMod::GetCells` returns a reference to a `thread_local` — intentional (same pattern as
`FLatticeCorridorSource::GetCellEdges`), but it is `const` while mutating the thread_local.
- `static constexpr float ColBlend` used inside `FMath::Max`/comparisons — may need a definition
under older MSVC ODR rules if it is ever odr-used.
- `FSlabGenerationParams` must be complete in `VoxelDensityOpStack.h` (it comes via
`VoxelStrateTypes.h`, already included).
**Next single action:** build, run `VoxelForge.OpStack.SlabEquivalence`, and **read the two
"proved uniform" numbers** — they are the measured payoff of §3.1. Then tick `bUseOperatorStack` on
a FlatPlain or CrystalChamber strate for the visual A/B. Expect the floor/ceiling shape to have
changed from the Z-term removal; the question is whether it still reads as the same *kind* of place.
---
## 2026-07-27 — SlabEquivalence GREEN. The §3.1 payoff is measured: 36 and 40 of 60 tiles.
```
FlatPlain box verdicts over 60 tiles: 36 proved uniform, 24 Mixed
CrystalChamber box verdicts over 60 tiles: 40 proved uniform, 20 Mixed
both: 52 of 20000 samples differ, ALL at ULP scale, 0 cross the isosurface
```
**This is the number the whole §3.1 question was about, and it is better than Maze's.** Maze proved
23 of 60; the slab archetypes prove **36 and 40** — 60-67 % of tiles, where `ClassifyTile` proves
**zero** for these two today. The reason is structural rather than lucky: a slab strate is mostly
solid rock below its floor, and now that both surfaces are XY-pure their Z bands are known exactly,
so "this tile is entirely below the floor band" is a comparison rather than a sample.
**The ULP residue is the known C10 floor** — 52/20000, 0 isosurface crossings, worst delta 2⁻¹⁸
(exactly 1 ULP at a density magnitude of ~32). **Not investigated, on purpose.** Same shape as C10:
deterministic, ULP-scale, zero consequence, and six builds were already spent proving that shape is
not worth chasing.
### The output exposed a real weakness in my own test — fixed
Both archetypes reported **the same 52 and the same worst delta**. The explanation is in the
fixture: `FTestWorld::Build` sets only `GeneratorType`, so **FlatPlain and CrystalChamber both get
DEFAULT `FSlabGenerationParams`.** They are the same configuration at two depths.
So the test's claim #2 was overstated. It demonstrated "the slab stack works at two depths", not
"one op serves two archetypes with different defaults" — **`CeilingRoughness`, the one field that
actually distinguishes CrystalChamber, was never varied.** The differing tile counts (36 vs 40)
come from the two slots' Z ranges, not from the archetypes differing.
**A third pass added: `CrystalChamber(tuned)`**`CeilingRoughness` 6 → 20, plus a rougher floor
and 3× the columns. It varies what actually matters, and it is deliberately the **worst case for
`ClassifyBox`**: a large `CeilingRoughness` widens the ceiling band and makes the
`Max(CeilZ - noise, FloorSurface + 2)` clamp far more likely to bind, which is exactly where a
false verdict — a HOLE — would appear. The default params were too gentle to stress that bound.
**UNVERIFIED:** the third pass. Its box-verdict brute force is the part that matters; expect fewer
tiles proved uniform than the gentle passes (wider bands ⇒ more Mixed), and **zero** unsound
verdicts. If `NumUnsound > 0` here, the `CeilHi` bound in `FSlabVoidSource::ClassifyBox` is the
first suspect, not the noise contract.
**Next single action:** rebuild, confirm the tuned pass is green, then the visual A/B on a FlatPlain
or CrystalChamber strate. The floor/ceiling shape WILL differ from before (the Z-term removal); the
question is whether it still reads as the same kind of place. Then `SurfaceWorld`
(OPSTACK-DECOMPOSITION §5) — biggest payoff, most care.
---
## 2026-07-27 — the tuned pass cried wolf. The TEST was wrong, not the port.
```
CrystalChamber(tuned): 121/20000 differ, 6 exceed the bound, worst |delta| 1.71661377e-05
0 cross the isosurface; box verdicts 32/60 proved, 0 unsound
```
**The port is fine. The yardstick was wrong, and it was wrong in a way that only shows up under
large amplitudes — which is precisely why the tuned pass was worth adding.**
The old bound was `16 · max(|Old|, 1) · FLT_EPSILON` — ULPs measured on the **output density**.
But the density is `min(Z - Floor, Ceil - Z)`, so **near the isosurface the output tends to 0 while
the intermediates (surfaces, world Z, noise amplitudes) are in the HUNDREDS.** A rounding born at
scale ~400 was being judged against a yardstick of scale 1: 400× too tight, and tightest exactly
where the test looks hardest.
**Measured, not assumed:**
| | roughness | worst \|delta\| | in ULP of \|Z\| |
|---|---|---|---|
| gentle passes | ceil 6, floor 4 | 3.81e-06 | ~0.08 |
| tuned pass | ceil 20, floor 9 | 1.72e-05 | **0.345** |
Amplitudes went up ×2.253.33, the deltas went up **×4.5**, and the worst one is **sub-ULP at the
scale it is born in**. Error proportional to amplitude is the signature of ordinary rounding, not of
a wrong transcription — a wrong offset or a missing `abs()` would move the surface by **voxels**,
four orders of magnitude above this, not by a factor of four.
**Fixed:** the bound now scales with the magnitude the error is born in (`max(|Old|, |Z|, strate Z
bounds)`), and — more importantly — **the warning now prints the discriminator instead of just the
alarm**: the density at the offending sample and the delta expressed in ULPs of the working scale.
A few ULP with a near-zero density is cancellation; thousands of ULP is drift. The next context
reads that off the message instead of re-deriving it at a build apiece.
The test stays discriminating: real drift is 4 orders of magnitude above the new bound.
### The box verdicts held under the worst case — that is the result that mattered
32 of 60 proved uniform with **0 unsound**, under tripled ceiling roughness and 3× the columns.
That was the specific thing the tuned pass existed to attack (a wide ceiling band makes the
`Max(CeilZ - noise, FloorSurface + 2)` clamp bind, which is where a false verdict would be a HOLE),
and the bound survived it. Fewer tiles proved than the gentle passes (32 vs 36/40), which is correct
— wider bands mean more genuinely Mixed tiles.
### Visual A/B — and a content finding worth more than the A/B
Jahni: *"visually, crystal and plain are identical, same for when opting on or off from the
opstack."*
**Opt-in on/off identical = the port is confirmed a pure refactor.** Note this is the expected
result and my earlier framing was sloppy: **both** paths compute the post-§3.1 function, so the
Z-term change is invisible in the A/B by construction. It is only visible against the world as it
looked *before* this build.
**FlatPlain and CrystalChamber rendering identical is the real finding.** They share
`FSlabGenerationParams` and nothing in the content distinguishes them — **the enum promised a
difference the data never delivered**, in the shipped world exactly as in the test fixture. So the
merge loses no distinction; it reveals there was none. Recorded in `OPSTACK-DECOMPOSITION §3`.
Making a crystal chamber look like one is now a **params** job (`CeilingRoughness` 6 → ~20), which
is precisely the outcome the refactor is for.
**UNVERIFIED:** the corrected bound and the new warning text.
**Next single action:** rebuild; the tuned pass should drop to INFO with all three passes reporting
sub-ULP-of-scale deltas. Then `SurfaceWorld` (§5) — biggest payoff, most care: the T1.a column cache
and the exact-lattice `ClassifyTile` bound must both survive the port.
---
## 2026-07-27 — the acceptance bar changed, and it invalidated the audit's own fix for C1.
**Jahni:** *"I do not need your work to be identical or near identical to what I had before, only
having it 99.99% at worst reproducible if two people share the same seed, since everyone rebuilds it
on multiplayer."*
The bar is **peer agreement in the present**, not fidelity to the past. Recorded as
`OPSTACK-PLAN §2.6.1`, which supersedes §2.6's "recognisably the same place".
**Four consequences, all recorded where they will be found:**
1. **`§C10` closed permanently, not parked.** It measures old-path vs new-path agreement, and the
two paths never coexist in a shipped world. No requirement depends on it.
2. **The equivalence tests keep their value for a different reason** — they are **port-correctness**
checks (a transcription slip is a real bug), not fidelity checks. Hard-fail on isosurface
crossings stays; the ULP grading is now diagnostic only.
3. **`§C9` promoted to the top open risk.** "Two people share a seed" is exactly what `/fp:fast`
weakens across toolchains, and a **Linux dedicated server** generating collision/nav against
Windows clients compiles the same density code under *opposite* float models. The fix
(`FPSemantics = Precise`) is blocked behind the IWYU debt, which now has a real justification
rather than a tidiness one.
4. **`§C1` is unblocked** — it was deferred *only* because it re-rolls the world's noise.
### ⛔ And then C1's documented fix turned out to be wrong
Before applying the one-liner the audit has carried since it was written, I did its arithmetic:
```
proposed: SeedF bounded to 16383, multiplier * 97.7 KEPT
max coord term = 1.6e6 -> ULP = 0.19 -> 9.5x the ~0.02/voxel step
```
**It bounds the seed but not the offset, and the multiplier is where the magnitude comes from.** The
fix would have made the bug less catastrophic while leaving it live for mid-range seeds — and, worse,
**closed the ticket**. Cost to discover after applying: one build plus a world re-roll.
**The real fix is to delete the multipliers.** The `· 7.3f … · 97.7f` factors exist only to
decorrelate the ~40 noise sites from each other; that is a hashing job, and hashing does it better:
`VoxelHash::SeedOffset(Seed, Site)` returns a **site-salted, bounded, final-units** offset. Bounding
`SeedF` alone would also funnel every seed through 16384 offsets *shared by all sites*, so two
colliding seeds would give identical noise everywhere; per-site salting means two seeds must collide
at **all ~40 sites**, which is never.
**And the op stack has already inherited the bug**`FSlabVoidSource::SeedF` is a faithful
transcription of `const float SeedF = (float)Seed`. It must be fixed in **both** paths in one change
or the equivalence tests will correctly scream. **Every further archetype port copies it again**,
which is the argument for doing it before `SurfaceWorld` rather than after.
Corrected in `AUDIT §C1` with the arithmetic, so the wrong fix cannot be applied later from the doc.
**UNVERIFIED:** nothing new in code this entry — docs only. The C1 fix itself is NOT written; it
needs Jahni's go-ahead on scope (~40 sites, re-rolls every world, one build).
**Next single action — Jahni's call between two:**
- **`§C1`** (recommended first): ~40 mechanical sites + the op stack, one build, kills the
highest-severity latent bug before more ports copy it.
- **`SurfaceWorld`** (`OPSTACK-DECOMPOSITION §5`): the biggest op-stack payoff, most care.
---
## 2026-07-27 — C9 FIXED AT THE CAUSE: FPSemantics = Precise, IWYU debt cleared.
**Jahni's call, and the right one:** *"I believe we could put FPSemantics to precise? Clearing up
anything that makes us not use it, then skip on the cross-platform same result for now and continue
on the rework of the opstack."* Fix the cause rather than measure the symptom — if both toolchains
compile IEEE-754 with no contraction, they agree **by construction** and there is nothing left to
compare.
**Verified in the UE 5.7 source rather than assumed** (`E:\Program Files\Epic Games\UE_5.7`):
| | `Default` | `Precise` |
|---|---|---|
| `VCToolChain.cs:1264` (Windows/MSVC) | `/fp:fast` | **`/fp:precise`** |
| `ClangToolChain.cs:712` (Linux/Mac) | `-ffp-contract=off` | **`-ffp-contract=off`** |
So `Default` really did mean **opposite** float models per platform, and `Precise` collapses them
onto the same one. `FPSemantics` is a `ModuleRules` property (`ModuleRules.cs:777`), so per-module
is the right granularity.
### The IWYU debt, cleared
Losing the shared PCH is what the debt was hiding behind. All uses turned out to be pointers,
`TWeakObjectPtr` or `TSubclassOf` parameters, so **forward declarations suffice** — only the
templates and macros needed real includes:
| Header | Added |
|---|---|
| `VoxelBiomeDefinition.h` | `class UMaterialInterface;` |
| `VoxelSettings.h` | `class UMaterialInterface;` |
| `VoxelStrateDefinition.h` | `Templates/SubclassOf.h` + `UMaterialInterface`, `USoundBase`, `AActor` |
| `VoxelStrateTypes.h` | `Templates/SubclassOf.h` + `AActor` |
| `VoxelContentManager.h` | `Templates/SubclassOf.h` + `AActor` |
| `VoxelAtmosphereManager.h` | `class AActor;` |
| `VoxelDensityVolume.h` | **`DrawDebugHelpers.h`** + `class AActor;` |
**`VoxelDensityVolume.h` was the one worth catching.** It uses `ENABLE_DRAW_DEBUG` in an `#if`, and
an **undefined macro in an `#if` is silently 0** — so without the include the debug block would have
vanished without a single warning, rather than failing the build. Include paths verified against the
engine tree (`Engine/Public/DrawDebugHelpers.h`, `CoreUObject/Public/Templates/SubclassOf.h`), not
guessed.
**Expect a residual tail.** The shared PCH hid these for years and only a real build enumerates them
all. Any further "undefined type" error from this build is IWYU, **not** the float setting — the fix
is to add the include, never to revert `FPSemantics`. That instruction is now written into
`VoxelForge.Build.cs` where the next person will hit it.
### Also landed: the cross-platform instrument (kept, not blocking)
`VoxelForge.Determinism.CrossPlatformDigest` — two FNV-1a digests over a fixed integer grid (no RNG,
so the sample set cannot itself diverge):
- **SHAPE** — the sign of density only. This is all the mesher reads, so it *is* the world: same
shape digest ⇒ same cavities, same walls, same collision. Jahni's "99.99% reproducible", literally.
- **FIELD** — every float bit. Differing while SHAPE matches ⇒ sub-voxel vertex wobble, harmless.
Plus `NearIso`: how many samples sit within 1e-4 of the isosurface, i.e. how many could *possibly*
flip sign under a float-model change. That **bounds** the risk instead of assuming it. Digests are
reported, not asserted, until pinned — pinning before the platforms agree would just carve the
divergence into the test. Per Jahni, the cross-platform comparison is deferred; the test costs
nothing to leave in and becomes a permanent regression guard the day someone runs it on Linux.
**UNVERIFIED:** none of this is compiled. **Expect a perf regression**`/fp:precise` forbids the
reassociation and FMA contraction `/fp:fast` allowed, on a noise-heavy hot path. Worth measuring
against `ARCHITECTURE §8.10` rather than assuming it is small.
**Next single action:** build. Then, per Jahni, back to the opstack — `SurfaceWorld`
(`OPSTACK-DECOMPOSITION §5`), the biggest payoff and the most care: the T1.a column cache and the
exact-lattice `ClassifyTile` bound must both survive the port. `§C1` (bounded seed offsets) stays
open and still wants doing before too many more archetypes copy it.
---
## 2026-07-27 — the IWYU tail was exactly one site.
`VoxelWorld.cpp:526``PC->GetPawn()->GetActorLocation()` needs **`APawn` complete**, and `Casts.h`
only forward-declares it. Added `GameFramework/Pawn.h`, plus `GameFramework/PlayerController.h`
which was only complete *transitively* — the same fragility this whole change exists to remove.
**My scan missed it because I only audited `Public/`.** The shared PCH served `.cpp` files too, and
`APawn` was named in `Build.cs`'s original error list — I read that list and still scanned only the
headers. Everything else in the module compiled, so this is the whole tail: one site, one build.
**UNVERIFIED:** the fix.
**Next single action:** rebuild. Then watch the perf number — `/fp:precise` on a noise-heavy hot
path, checked against `ARCHITECTURE §8.10`. Then back to the opstack: `SurfaceWorld` (§5).
---
## 2026-07-27 — ✅ C10 SOLVED (it WAS /fp:fast). SurfaceWorld opened: height space needed its own family.
**Maze and Slab are now BIT-IDENTICAL to their originals.** `FPSemantics = Precise`, set for
cross-platform play, dissolved the ULP residue.
**So hypothesis 3 had the right mechanism all along, and every experiment built on it was doomed by
construction.** Under `/fp:fast` the compiler reassociates and contracts *by surrounding context*,
with **no single isolable axis** — which is exactly why five carefully-designed one-variable tests
all came back negative while the difference stayed. There was no variable to find. Removing the
*permission* removed the difference.
**The lesson is not the one I expected.** Nobody solved C10; C9 got fixed for an unrelated
requirement and C10 fell out of it. Six more builds of bisecting source would have found nothing,
because the answer was a **build setting nobody was looking at**. Parking a question whose every
consequence is measured and benign was right on its own terms *and* right in hindsight — the
information was not obtainable along the path I was on, at any price. Recorded in `AUDIT §C10`.
**Immediate consequence:** the equivalence tests are now much sharper instruments. Any diff at all is
a real finding rather than noise to grade. The ULP machinery stays as a float-model regression alarm.
### SurfaceWorld, step 1 of 2 — and it forced an architectural decision
`§5` says the height ops *"operate on Z values in the column, not on density"* and then lists them as
children of `FHeightfieldSource`. Writing them made the consequence unavoidable: **they do not fit
`IVoxelDensityOp` at all.** No input Z (they produce one), XY-pure per column rather than per voxel,
and they write neither density nor SDF. The two ways to force them in were a per-voxel third channel
for what is a **column** property, or one opaque op — `§2.5`'s named failure mode.
**So height space got its own contract:** `VoxelHeightOp.h``FVoxelHeightSample` (`Height` +
`Relief`), `IVoxelHeightOp`, `FVoxelHeightStack`, and five ops in `VoxelHeightOpStack.cpp`.
`Relief` is the original's `M`: produced by the structural source, consumed by the terrace gate.
Same shape of lesson as `§0.1` — that one found density needed a second *channel*; this found
terrain needs a second *space*.
**A property the type system now gives for free:** a height stack **cannot** hold Z-dependent data,
because there is no Z in the signature to put there. `AUDIT §6.3` warns that Z-dependent data
smuggled into `FSurfaceColumn` silently corrupts every chunk in the vertical stack and that
`ValidateDeterminism` would not catch it. That hazard is now a type error instead of a convention.
**Deliberately staged.** This step touches **nothing** on the density path — no `FHeightfieldSource`
adapter, no `FSkyCapSource`, no `FOverhangShelfMod`, no column cache, no wiring. If height space had
not decomposed cleanly, that would show up here for the price of one test rather than after building
the adapter, the cache integration and the dispatch on top of it. Same method Phase 1 used on
density, applied to the question Phase 2 actually raised.
**The test runs twice, and the second pass is the one that matters:** the F20 terrain ops are **off
by default**, so a defaults-only run would exercise the structural source and leave all four
modifiers — i.e. everything new — untested. The second pass turns them all on. It also brute-forces
`MaxDisplacement` against observed movement, because a false bound would later be a hole.
`WaterLevelRelative` must be set for that pass or `FBeachHeightMod` early-outs and the fifth op is
never touched — a green test that measured nothing.
**Also:** `ComputeSurfaceTerrainZ` moved from private to public on `UVoxelGenerator` (same
justification as `GetSlabDensity` / `GetMazeDensity` — exposed for isolated tests). Its old private
declaration was removed; two declarations of one member would not compile.
**UNVERIFIED:** none of this is compiled. Likely error spots: the new `.cpp`/`.h` pair being picked
up; `FVoxelHeightStack` as a move-only local; the non-owning `const IVoxelHeightOp**` out-param in
`MakeStructuralHeightSource` and its `static_cast` back down in `MakeCliffHeightMod`; `FVector2D`
members being double in UE5 (cast at every use); and the `ComputeSurfaceTerrainZ` access move.
**Next single action:** build + run `SurfaceHeightEquivalence`. If green, step 2 — `FHeightfieldSource`
(the adapter that turns a column into density, preserving the T1.a cache), `FSkyCapSource`,
`FOverhangShelfMod` (the one genuinely 3D op here), then biome blending and the wiring. `§C1`
(bounded seed offsets) still open.
---
## 2026-07-27 — height stack GREEN (bit-identical, both passes). Step 2a + a real bug in my own §3.1 work.
```
SurfaceWorld(defaults): bit-identical across 20000 samples
SurfaceWorld(all terrain ops on): bit-identical across 20000 samples
MaxDisplacement: claims 16.300, worst observed 4.441 (27% of claim)
```
**The second op family was the right call** — height space decomposes as cleanly as density did,
including with all four F20 terrain ops on. `MaxDisplacement` is loose (27% used) because it sums
each op's independent maximum and they never peak at the same XY. **Left loose deliberately:** a
loose bound only costs CPU when the heightfield eventually gets a `ClassifyBox`; a tight-but-wrong
one is a hole in the world.
### ⚠️ `FSlabVoidSource::IsXYPure()` was `true`, and that was WRONG
Found while writing `FSurfaceColumnSource` and having to decide the same flag.
The contract is *"**`Eval`** does not depend on Z"*. `FSlabVoidSource::Eval` computes
`min(Z - floor, ceil - Z)` — Z-dependent in the most direct way possible. **§3.1 made the SURFACES
XY-pure; the DENSITY never was and cannot be** — it is a distance to a surface. I conflated the two
while writing the very operator that quotes the warning against doing so.
**Latent only because nothing reads the flag yet** — and step 2b is exactly where it would have gone
live: a generic T1.a column cache keyed on (XY box, StrateKey, Seed) with **no ChunkZ** would have
shared one density value down the entire vertical chunk stack. `AUDIT §6.3` says this corrupts every
chunk silently and that `ValidateDeterminism` would not catch it, because it samples along an X
boundary. Fixed, with the distinction written at the site.
**This is the clearest argument yet for the height-space split:** the thing that is XY-pure is the
HEIGHT, and in `VoxelHeightOp.h` it lives in a type with no Z to get wrong. The bug is unrepresentable
there.
### Step 2a — the bridge into density space
- `FSkyCapHeightSource` — the ceiling is an **altitude**, so it belongs in height space, not in
density space as `§5` had it. Same category slip as the terrain ops; the subtraction happens later,
in the combine. It gets tested window-invariance and type-enforced XY purity for free.
- `FSurfaceColumnSource` — consumes both height stacks, produces
`Density = max(TerrainZ - Z, Z - CeilSurf)`. `IsXYPure() = false`, correctly this time.
- `BuildSurfaceStack` — source + 3 structural. **No per-column memo inside the op, on purpose:**
T1.a already exists one level up in `GetDensityAt`, and a second cache key is a second thing to get
wrong in exactly the way described above. Step 2b reuses the existing cache rather than inventing a
second one.
**What step 2a does NOT cover, and the test now says so at the top:** the **overhang** (found while
reading: `GetSurfaceDensity` passes `OverhangAmp = 0`, so it computes none — the only reference is
the *cached* path, `ComputeSurfaceColumn`) and **biome blending** (weight 0 here; it is the `Mask`
combiner and `§5`'s Phase 3 prototype). **Do not wire SurfaceWorld into a world with biomes or
overhangs until 2b**, because nothing currently in the tests would say it is wrong.
**UNVERIFIED:** step 2a is not compiled. Likely spots: `FVoxelHeightStack` as a member of
`FSurfaceColumnSource` (move-only member ⇒ the enclosing op is move-only too, which is fine since it
lives behind `TUniquePtr`); the new `VoxelHeightOp.h` include in `VoxelDensityOpStack.cpp`; and
`Gen->GetSurfaceDensity` taking two param refs plus a weight.
**Next single action:** build. Then step 2b — the overhang op (per-column gate + per-voxel union,
referenced against the cached path), biome blending as the `Mask` combiner, and the wiring that
reuses `GSurfColCache`. `§C1` still open.
---
## 2026-07-27 — step 2b: the overhang, the one op that could NOT live in height space.
Step 2a came back green on all four counts, including the density-side bridge
(`FSurfaceColumnSource` bit-identical to `GetSurfaceDensity` over 20 000 samples).
**`FOverhangShelfMod` is the boundary case that justifies where the two spaces were split.** Its
uphill reach *grows with altitude* (`Frac = (Z - TerrainZ) / OverhangHeight`), so it is essentially
Z-dependent — it is the only SurfaceWorld op that could not have gone into `VoxelHeightOp.h`. The
line between the two spaces falls where the code changes nature, not where it was convenient.
**The per-column problem, and how it is solved.** The overhang needs `TerrainZ` plus a per-column
gate (`OverhangAmp`, `DirX`, `DirY`) that the source computes. Three ways to get it, two bad:
- recompute the height stack per voxel — correct but pays the cliff's four resamples per lip voxel;
- add a third channel to `FVoxelOpSample` — a per-voxel slot for a **column** property, and
archetype-specific pollution of a shared contract (`§11` has this open, unresolved);
- **chosen:** the source memoises the column and the overhang reads it, same as `cliff → structural`.
**The memo key is the part worth getting right.** Keyed on `(InstanceId, X, Y)` where `InstanceId`
comes from a monotonic atomic counter — **not** on `this`. A freed stack and a newly allocated one
can share an address; a counter that never goes backwards cannot collide. Since the stack evaluates
every Z of a column at the same XY, the hit rate is ~1, so this also recovers the per-column reuse
without inventing a second cross-chunk cache.
**Two functions exposed** (`ComputeSurfaceColumn`, `SurfaceDensityFromColumn`) because they are the
**only** oracle for the overhang — `GetSurfaceDensity` passes `OverhangAmp = 0` and computes none.
Their private declarations were removed; two declarations of one member will not compile.
**The test's third pass is written to avoid measuring nothing.** A uniform Z draw over the whole
strate would almost never land in the overhang window, so the test would pass green having never run
the op — the same trap as `WaterLevelRelative` in the height pass. Half the samples are now placed
*inside* the window deliberately, the count is reported, and it warns if it is zero.
**Still missing before wiring: BIOME BLENDING.** The ground is evaluated for the dominant biome and
lerped toward the neighbour — the `Mask` combiner, and `§5` calls it the Phase 3 prototype. **Do not
tick `bUseOperatorStack` on a SurfaceWorld strate with biomes until then.**
**UNVERIFIED:** step 2b is not compiled. Likely spots: `std::atomic` include; `HFractal3D` newly
added to the density TU; `MakeUnique<FSurfaceColumnSource>` then `MoveTemp` into the stack while
keeping a raw pointer; and the two newly-public generator methods.
**Next single action:** build. Then biome blending as the `Mask` combiner + the wiring that reuses
`GSurfColCache`. `§C1` still open.
---
## 2026-07-27 — SurfaceWorld WIRED (no biomes yet). And a perf trap caught before it shipped.
All six checks green, **9399 samples deliberately inside the overhang window** — the op was genuinely
exercised rather than skipped.
### ⚠️ The one-entry column memo would have been a disaster, not a slowdown
`FSurfaceColumnSource`'s memo held a single entry. That is correct **only if the caller walks a whole
Z column before changing XY** — and the mesher promises nothing of the sort. If it iterates X first
within a Z slice, *every* voxel misses and the full height stack re-runs per voxel, **including the
cliff's four structural resamples**. On the most expensive archetype in the plugin that is an order
of magnitude, not a few percent.
It would also have been invisible in the tests: they sample random XY, where a one-entry memo and a
256-entry one behave identically. **The tests could not have caught this; only reading the access
pattern could.**
Replaced with a **direct-mapped 256-entry table**, `thread_local`, hashed on the XY bit patterns,
with the **full key compared on hit** — a collision can only cost a recompute, never return the
wrong column. Robust to any iteration order the mesher chooses.
### Wired, with the biome guard in one place
`UsesOperatorStackForChunk` now returns true for `SurfaceWorld` **only when the strate has no
biomes**. The original evaluates the dominant biome and interpolates the *heights* toward the
neighbour across the border band; the stack evaluates one param set. Without the `Mask` combiner a
biome strate would not shift subtly — it would get **a hard seam at every biome border**.
The guard lives in `UsesOperatorStackForChunk`, beside the archetype list, so "can this strate take
the stack?" stays one question asked in one place. `GetDensityAt` carries a second, defensive check
on `CP_BiomeCtx.IsValid()`: if the two ever disagree it falls back to the `switch`, because an
unported world is recoverable and a wrong one is not.
**UNVERIFIED:** not compiled. Likely spots: the `FSlot` struct + `thread_local` array inside a const
method; `return S.C` (the previous `return C` referred to a name now scoped inside the `if`); the
new `SurfaceWorld` case in `GetDensityAt`'s op-stack switch.
**What to try in the editor after the build:** tick `bUseOperatorStack` on a **biome-less**
SurfaceWorld strate and compare. Both paths compute the same function, so this is a wiring check,
not a look change — expect it identical. **A biome strate will silently ignore the flag**, by design.
**Next single action:** build, then the visual A/B. After that the remaining SurfaceWorld work is
the `Mask` combiner (biome blending, §5's Phase 3 prototype) and integrating `GSurfColCache` so the
stack path reuses the existing box cache rather than only its own table. `§C1` still open.
---
## 2026-07-27 — AUDIT §C1 FIXED. 85 noise sites, mechanically, in both paths at once.
SurfaceWorld's wiring came back fine, so I took `§C1` next — **not a detour any more**: the op stack
had already inherited the bug three times, and every remaining port (`VerticalShafts`,
`FloatingIslands`, `TunnelNetwork`) would copy it again. Fixing it now means those get written
correctly instead of needing a follow-up pass.
**The fix, and why the documented one was wrong.** The audit proposed bounding `SeedF` to 16383
while keeping the `· 97.7f` multiplier — which still reaches 1.6e6, where the ULP is 0.19, **9.5× the
per-voxel step**. Less spectacular, still broken, ticket closed. The multiplier is the problem.
`VoxelHash::SeedOffset(Seed, SiteKey)` inverts the roles: **the multiplier no longer decorrelates by
amplifying — it IDENTIFIES the site, and the hash decorrelates.** Output is already in final units,
bounded to [0, 16383], so the ULP is 0.002 = 10 % of a voxel step. Site-salted, so two seeds must
collide at **all ~50 sites** to give the same world, rather than sharing one global bucket.
**Why the 85-site edit was safe to do without compiling:** the transformation is a pure regex —
`SeedF * K.Kf``VoxelHash::SeedOffset(SeedU, K.Kf)` — and **the literal stays visible at the call
site**, so every line can still be eye-checked against the original. Applied to all three files in
one pass, so the archetype `switch` and the ported ops changed *identically*; if they had not, the
three equivalence tests would say so loudly. 62 + 7 + 16 sites, 0 left behind, plus 2 bare `+ SeedF`
worm sites handled by hand (site key `1.0f`).
### ⚠️ The new test exists because the equivalence tests are structurally blind here
`VoxelForge.Determinism.LargeSeedSurvives` — seeds 1337, 1e5, 1e7, 2e9; asserts the heightfield still
produces ≥ 50 distinct heights over 400 samples.
**The equivalence tests could never have caught C1.** They compare the op stack against the archetype
switch, and both read the *same* faulty expression — so at a large seed both collapse **identically**:
bit-identical, green, and both perfectly flat. An oracle that shares the implementation's bug cannot
see the bug. This test compares nothing to nothing; it asserts a **property** — the terrain must vary.
That distinction is worth keeping in mind for the ports still to come.
The fixture's own comment said the small seed was a *workaround* for C1; corrected, since the reason
is now just message comparability.
**UNVERIFIED:** not compiled. Likely spots: a missed `SeedU` declaration in one of the 9 functions
(the awk sweep found none, but it is a heuristic); `VoxelCaveMorphology.h` newly included in
`VoxelHeightOpStack.cpp`; and the `uint32`/`float` swap on two op members.
**⚠️ EXPECT EVERY WORLD TO LOOK DIFFERENT.** This re-rolls every noise offset in the plugin. That is
the intended consequence and it is covered by §2.6.1 — nothing depends on the old shapes any more.
The three equivalence tests should stay green (both paths changed together); the visual is new.
**Next single action:** build, run the full `VoxelForge` filter — six tests now. Then back to the
op stack: the `Mask` combiner (biome blending) to finish SurfaceWorld, or `VerticalShafts` next.
---
## 2026-07-27 — all 9 tests green. C1 PROVEN fixed. And C9 is only HALF fixed.
Full `VoxelForge` filter, exported run `Saved/Automation/Automation2026.07.27-16.35.18.csv`:
```
ClassifyTileSoundness · CrossPlatformDigest · DensityPurity · DiffLayerContention
LargeSeedSurvives · LiveEditInvalidation · BoxVerdictFold · MazeEquivalence
SlabEquivalence · SurfaceHeightEquivalence — all Success
```
**`§C1` is proven fixed, by property rather than by comparison:**
```
Seed 1337 : 400 distinct heights / 400 samples, range 20.95 voxels
Seed 100 000 : 399 / 400, range 23.46
Seed 10 000 000 : 399 / 400, range 22.89
Seed 2 000 000 000 : 400 / 400, range 23.42
```
Seed 2e9 — what `FMath::Rand()` produces — now generates a live world. Before the fix that seed
gave a *constant* field. Note the three equivalence tests stayed green through an 85-site rewrite,
which is exactly the port-correctness value they were kept for.
### ⚠️ The digest warning was right for the WRONG reason — and I nearly dismissed it
`NearIso: 2 of 115000 samples within 1e-4 of the isosurface` fired a warning whose text blamed the
`/fp:fast`-vs-precise split. That split is fixed, so my first instinct was "stale warning, soften the
text". **I checked instead of assuming, and the risk is real via a different mechanism.**
**`sinf`/`cosf` are not specified by IEEE-754.** `FPSemantics = Precise` makes MSVC and Clang agree on
*expression evaluation*; it says nothing about the math library. MSVC's CRT and glibc's libm may
legitimately differ by ~1 ULP — and `FMath::Sin`/`Cos` are all over the density path: layer lines
(`VoxelGenerator.cpp:2275`, `VoxelHeightOpStack.cpp:232`), ribs, room placement, rotations.
**So `§C9` is half closed:** the compiler half by construction, the **library half still open**. No
build flag can fix the second — two libm implementations cannot be made to agree by a compiler
setting. The fix, if it is ever needed, is a deterministic in-house `sin`/`cos` in the density path
(one more world re-tune).
**The measurement was also over-stating things ~100×.** A single 1e-4 band is far too wide for a
libm-scale delta (~1e-6 absolute on densities of magnitude ~10). Replaced with a three-band profile
(1e-4 / 1e-5 / 1e-6); only the tight band raises a warning, because only it corresponds to a delta
that could actually flip a sign. Recorded in `AUDIT §C9`.
**UNVERIFIED:** the reworded digest test.
**Next single action:** build (quick — one test file changed). Then, per Jahni: finish SurfaceWorld
(the `Mask` combiner = biome blending, `§5`'s Phase 3 prototype), then `VerticalShafts` (`§6`).
---
## 2026-07-27 — the `Mask` combiner (biome blending) in height space. §5's Phase 3 prototype.
All 10 tests green on the previous build, and the reworded digest confirms the point that mattered:
```
NearIso profile over 115000 samples: 2 within 1e-4, 0 within 1e-5, 0 within 1e-6
```
**Zero in the tight band** — no sampled voxel sits close enough to the isosurface for a libm
difference to flip its side. The residual `§C9` library half is real in principle and, on this grid,
carries no measured risk. The wide band's "2" was the ~100× over-statement, exactly as predicted.
### The design decision worth recording: `IVoxelBiomeField`
Biome blending needs to ask "which biome is at this XY?", and the real answer is a warped Voronoi
with a per-chunk cache living on `UVoxelGenerator`. **The op must not hold a `UVoxelGenerator*`**
Phase 3 wants operators to become *assets*, and an op that owns a generator pointer never can.
So the op depends on `IVoxelBiomeField`, a two-line interface returning `(dominant, neighbour,
weight)`. The adapter that knows the generator stays on the generator's side. This is the same move
as `cliff → structural source`: depend on the *capability*, not on the owner.
### The combiner itself
`FBiomeBlendHeightSource`**one complete height stack per biome**, heights lerped in the border
band. Each biome's stack computes its **own** relief `M` and gates its **own** terrace with it,
which is exactly what the original does (two independent full `ComputeSurfaceTerrainZ` calls, only
the OUTPUTS blended). Blending *heights* rather than *params* is what keeps borders continuous
across any param difference — interpolating params would drag a terrace through intermediate states
that mean nothing.
**The ceiling SELECTS instead of blending**, because the original takes the dominant biome's ceiling
alone. Reproduced as-is rather than "improved": a blended sky cap would change the world's
silhouette, and a port is not where that gets decided.
### Tested against a synthetic field, on purpose
The real Voronoi resolver has its own coverage; a synthetic field lets the weight sweep 0 → 1
**continuously**, which is where an inverted lerp (`1-w` for `w`) hides — it looks right in the
middle and wrong only at the ends. Two deliberately dissimilar biomes, five weights × 400 points,
bit-exact against `FMath::Lerp` of the two full stacks. Plus a check that the ceiling still returns
the dominant's at weight 1.0, which is the case a "blend everything" refactor would silently break.
**UNVERIFIED:** not compiled. Likely spots: the local `class FFixedWeightField` declared inside a
function body and used as an interface; `TArray<FVoxelHeightStack>` (move-only element type — needs
`MoveTemp` on insert, which it has).
**Next single action:** build. Then the density-side integration — `FSurfaceColumnSource` takes the
per-biome params + field, blends the overhang amp (`Lerp(Amp(PD), Amp(PN), W)` with slope from PD),
and the generator-side adapter wraps `ResolveBiomeSampleAt`. That drops the biome guard in
`UsesOperatorStackForChunk` and finishes SurfaceWorld. Then `VerticalShafts` (§6), which reuses
`FConstantRockSource` + `FSdfRoughnessMod` + `FSdfCarve` and should be the cheapest port yet.
---
## 2026-07-27 — SurfaceWorld COMPLETE (biomes included). 5 of 8 archetypes ported.
All 10 tests green again. **But the two new biome checks printed nothing** — because I wrote them to
report only on failure. **That is the exact flaw I flagged twice in this session** (the
`WaterLevelRelative` early-out, the overhang window count) and then committed myself: a silent pass
is indistinguishable from a check that never ran. Both now `AddInfo` their coverage, so the next run
shows the blend actually executed over 2000 (weight, point) pairs.
### Step 2c — the density side, and SurfaceWorld is closed
- **`FSurfaceColumnSource` takes per-biome params + an owned `IVoxelBiomeField`.** Empty params ⇒
the original path, bit-for-bit unchanged (which is why the existing tests should stay green).
- **The field is OWNED by the stack, not borrowed.** The real adapter points at `GetDensityAt`'s
`thread_local` `CP_BiomeCtx` / `CP_BiomeCache`; the stack is itself `thread_local` and rebuilt in
the *same* refetch block, so all three are born and die together on one thread. Making ownership
structural beats leaving survival to a convention the next reader has to infer.
- **The overhang amp now blends across biomes** — `Lerp(Amp(PD), Amp(PN), W)` with the slope and
threshold from the **dominant** only, exactly as `ComputeSurfaceColumn` does. Interpolating the
*slope* would be meaningless: it is a measurement of the terrain, not a setting.
- **`FGeneratorBiomeField` lives in `VoxelGenerator.cpp`,** on the side that knows the generator.
That is the whole point of the interface — the op sees a *capability*, never an owner, which is
what lets it become an asset in Phase 3.
- **The biome guard is gone** from `UsesOperatorStackForChunk`.
Also cleaned up while there: the two constructors now **delegate to one body with one id counter**
instead of each initialising separately (two init paths is two places to forget a member — the first
draft already had two competing counters, one of them tagged with a high bit to avoid collision,
which is a smell rather than a design).
**5 of 8 archetypes ported:** Maze · FlatPlain · CrystalChamber · SurfaceWorld.
**UNVERIFIED:** not compiled. Likely spots: the delegating constructor; `TUniquePtr<IVoxelBiomeField>`
as a defaulted parameter in the public header; `VoxelHeightOp.h` newly included by
`VoxelDensityOpStack.h` (a public→public include); the `case` block now needing braces for its local
declarations; and `FGeneratorBiomeField` being defined before `UVoxelGenerator`'s member functions
while calling `ResolveBiomeSampleAt`.
**What to try after the build:** tick `bUseOperatorStack` on a SurfaceWorld strate **with biomes**
now. Both paths compute the same function, so expect it identical — biome borders included, which is
the case that was guarded off until now.
**Next single action:** build. Then `VerticalShafts` (§6) — it reuses `FConstantRockSource`,
`FSdfRoughnessMod` and `FSdfCarve` unchanged from Maze, so it should be the cheapest of the eight.
---
## 2026-07-27 — SurfaceWorld verified visually. And Jahni measured the thing I had only flagged.
All 10 green, biome checks now visible and correct (blend bit-exact across the whole weight sweep;
ceiling selects rather than blends at weight 1.0). Jahni: *"the opstack looks similar if not
identical to the old terrain"* — the biome case included, which is what step 2c added.
**And: *"it took a bit more time generating with the opstack."*** That is a real regression, it was
predictable, and I had flagged it as "pending" rather than fixed. Diagnosed, two compounding causes:
**1. The memo key invalidated on every chunk.** It was keyed on `InstanceId`, which changes on every
stack rebuild — i.e. every chunk. `GSurfColCache`, the path it replaced, is keyed on
`(XY box, StrateKey, Seed, LayoutVersion)` with **no ChunkZ**, deliberately *"shared down the whole
vertical strate stack"*. So a 4-chunk-tall strate recomputed **every column four times**, cliff
resamples included — and the cliff costs four extra structural samples per column.
**2. The table could not hold one chunk.** A chunk is `CHUNK_SIZE²` = 1024 columns; the table had
**256** entries. It thrashed against itself *within a single tile*, before any cross-chunk question.
**Fixed:** `PrepareChunk` now derives a **shared** `ColumnKey` from `(StrateBottomWorldZ,
LayoutVersion, Seed)` — the same identity `GSurfColCache` uses — and the table is 4096 entries
(~150 KB/worker, in line with `GSurfColCache`'s 6 × 59 KB). The memo is `thread_local`, so it
already survived stack rebuilds; **only the key was throwing the contents away.**
**Why sharing across chunk Z is sound:** heights are XY-pure *by type* (the whole point of
`VoxelHeightOp.h` — there is no Z in the signature), and the biome field is documented
Z-independent (*"ZERO Z dependence: the climate/Voronoi fields are pure-XY"*). That is precisely the
justification `GSurfColCache` already rests on.
**Un-prepared safety:** `ColumnKey` starts at `InstanceId` rather than 0, because slots initialise to
`Key = 0` and a zero key would falsely hit the pristine slot at (0,0). Without `PrepareChunk` you
get per-instance caching — less sharing, still correct. Degrade, never lie.
**⚠️ This may not close the gap entirely, and I am not claiming it does.** Virtual dispatch (5 ops
per voxel) and the hashed lookup versus the original's direct-indexed box both remain. Those are
smaller than a 4× column recompute, but "smaller" is a guess until measured. **The next generation
timing is the measurement** — if it is still slower, the remaining suspects in order are: the box
cache's direct indexing vs. my hash, then per-voxel virtual calls.
**UNVERIFIED:** not compiled.
**Next single action:** build, regenerate, and compare generation time against the switch path with
the flag off. Then `VerticalShafts` (§6).
---
## 2026-07-27 — VerticalShafts ported. 5 of 8, and operator reuse is now MEASURED.
**The port that tests the thesis rather than the fidelity.** Every previous port asked "does the
decomposition reproduce the original?". This one asks **"do operators actually get reused across
archetypes?"** — which is `OPSTACK-PLAN §2.5`'s claim and the only reason to do this refactor rather
than tidy the `switch`.
**Three of the five ops are Maze's, reused without a line changed:** `ConstantRock`, `SdfRoughness`,
`SdfCarve`. In the `switch`, `GetMazeDensity` and `GetVerticalShaftDensity` are two ~100-line
functions with nothing visibly in common. As operators they are the **same three ops with a
different source and different tuning** — frequency 0.1 instead of 0.12, window `rough + 4` instead
of `R + rough + 2`. If the test is bit-identical, reuse stops being an intention and becomes a
measurement.
**Two new ops:** `FShaftFieldSource` (infinite cylinders + hash-gated connectors → SDF) and
`FShaftLedgeMod` (banded shelves on the +X/+Y half only, so the shaft stays climbable).
**Deviation from `§6`, stated:** it suggested splitting the source in two (XY-pure cylinders +
connectors) so the cylinder half could get an exact XY box verdict. Kept as one op, because the
connectors derive from the *same* 3×3 roll as the shafts and the ledge mod needs the shaft list
anyway — splitting would mean rolling twice or sharing a cache between two ops. What is forfeited is
the exact verdict on the cylinder half alone; what is kept is a conservative `EffectOverBox` that
tests circles *and* connector reach. Revisit if the profile says it matters.
**One subtlety worth flagging:** `FShaftLedgeMod` gates on `InOut.Sdf < 0` — the SDF **after**
roughness, as the pile left it. Re-deriving the SDF there would give the pre-roughness value and
shift every ledge. The op reads the channel rather than recomputing, which is exactly what the
two-channel `FVoxelOpSample` is for.
**Compile fix on the way in:** `FCells` was declared at the bottom of the class but returned by
functions above it. Member *bodies* are deferred; *return types* are not — C4430 plus an unreadable
cascade from a trivial cause. Moved up beside `FShaft`/`FConn`, with a note.
**Ported: Maze · FlatPlain · CrystalChamber · SurfaceWorld (biomes incl.) · VerticalShafts — 5 of 8.**
Remaining: `FloatingIslands` (§7), `Underwater` (§8, TunnelNetwork + a flag), and `TunnelNetwork`
(§2) **last**, because it owns `BuildChunkCache`'s two-region window-invariance discipline (§8.4),
the most delicate code in the plugin.
**UNVERIFIED:** the shaft port is not compiled past the `FCells` fix.
**Next single action:** build, run the `VoxelForge` filter — 11 tests now, the new one is
`VoxelForge.OpStack.VerticalShaftEquivalence`. Then `FloatingIslands`. **Perf is deliberately parked
until the transition is complete** (Jahni's call); the open item is that the op path is slower, with
virtual dispatch and the hashed column lookup as the remaining suspects.
---
## 2026-07-27 — VerticalShafts bit-identical. And the perf fix had introduced a REAL bug.
**`VerticalShaftEquivalence`: bit-identical over 20 000 samples, 966 inside a shaft.** So the
cylinders, connectors, roughness, carve and ledges all ran, and **operator reuse across archetypes is
now measured rather than intended** — three of the five ops are Maze's, unchanged.
### ⚠️ `SurfaceHeightEquivalence` FAILED, and it was my own perf fix
```
Overhang: 69 of 20000 samples differ (largest |delta| 5.84); 1 crosses the isosurface
```
**Cause: the `ColumnKey` I introduced in `f3faa3b` did not include the params.** It was
`hash(StrateBottomWorldZ, LayoutVersion, Seed)`. The test builds two stacks in the **same** strate,
same layout version, same seed, differing only in overhang settings — identical keys, so the second
stack read the first's cached columns, which had been computed with `OverhangAmp = 0`. The overhang
silently vanished wherever a column was already cached.
**This is not a test artefact.** It is precisely the weakness the codebase already documents for
`GSurfColCache` (the extended `AUDIT §C2` note in `VoxelGenerator.cpp`): *"StrateKey is
round(StrateBottomWorldZ), so a live edit that changes terrain params WITHOUT moving the strate
leaves the key unchanged and serves stale columns."* Production merely **masks** it, because
`RebuildStrates` bumps `LayoutVersion`. My key inherited the same hole, and the test found it in one
build.
**Fixed** by folding a `FCrc::MemCrc32` fingerprint of the params (plus every per-biome param set)
into the key. `FSurfaceGenerationParams` is verified pure POD — no `TArray`, `FString` or pointer —
so a memory CRC **cannot** produce a false hit; at worst padding causes a false *miss*, i.e. a
recompute. Erring toward CPU cost rather than toward a wrong column.
**Worth stating plainly: a perf optimisation introduced a correctness bug, and the test suite caught
it the same day.** That is the clearest answer yet to "what are all these tests for" — the failure
was invisible to inspection, produced *plausible* terrain, and only one of 20 000 samples actually
crossed the isosurface.
### Known, not fixed: VerticalShafts proves 0 of 60 tiles
Maze proves 23, the slabs 36-40, shafts **zero**. Expected from the choice recorded last commit: my
`EffectOverBox` returns `CarveOnly` if *any* shaft exists within a `Spacing*1.6` halo, rather than
testing the actual connector capsules. At default `ShaftDensity` almost every box has a shaft in that
halo, so `AllSolid` is always killed and nothing is provable. **Correct but pessimistic** — a false
verdict would be a hole, this is only lost CPU. The improvement is to test real connector capsules
instead of "a shaft exists nearby", which is `§6`'s split argument arriving through the back door.
**UNVERIFIED:** the fingerprint fix.
**Next single action:** build, confirm `SurfaceHeightEquivalence` is green again (all 11 tests).
Then `FloatingIslands` (§7) → `Underwater` (§8) → `TunnelNetwork` (§2, **last**). Perf still parked.
---
## 2026-07-27 — END OF DAY 2. All 11 tests green. 5 of 8 archetypes ported.
The params fingerprint fixed the overhang regression; the full `VoxelForge` filter is green.
**Ported and wired, all bit-identical to their originals:** Maze · FlatPlain · CrystalChamber ·
SurfaceWorld (biomes included) · VerticalShafts.
**Remaining ports:** `FloatingIslands` (§7) → `Underwater` (§8) → `TunnelNetwork` (§2, **last**).
**Open items, none blocking:**
- **Perf** — op path is slower; parked by Jahni until the transition is complete. One cause fixed.
- **`§C9` library half** — `sinf`/`cosf` not IEEE-754; 0 samples measured at risk. Run the digest on
Linux and pin it when the platforms agree.
- **VerticalShafts proves 0 of 60 tiles** — pessimistic `EffectOverBox`, not a hole.
- **`ClassifyTile` still hand-written** — `ClassifyBox` is verified but unconsumed. This is where the
measured tile-skipping becomes actual frames, and it is arguably the biggest win left.
`OPSTACK-HANDOFF.md` rewritten for a fresh context: current state, what is left in order, the two
things Phase 2 invented (height space, `IVoxelBiomeField`), and the method lessons that were paid for
in build cycles.
---
## 2026-07-28 — FloatingIslands ported. 6 of 8. The stack runs BACKWARDS, and §C1 was NOT closed.
**The portage that tests the AXIS, not the fidelity.** `VerticalShafts` measured reuse *by identity*
— three of Maze's ops, not a line changed. This one measures something stronger and riskier for the
abstraction: **reuse by INVERSION**.
The four archetypes ported so far all start from ROC and CARVE. FloatingIslands starts from the VOID
and FILLS. If the abstract axis chosen back in §0.1 — the *sign* of the internal density — is the
right one, then both ends of the pile must be the same operators negated:
```
FConstantFieldSource(+Base) ←→ FConstantFieldSource(-Base) ClassifyBox: AllSolid ←→ AllAir
FSdfConvertOp(Sign = -1) ←→ FSdfConvertOp(Sign = +1) carve ←→ fill
FSdfRoughnessMod ←→ FSdfRoughnessMod 4ᵉ archétype, inchangé
```
And it holds: **the only new operator in this port is the island blob.** 7 ops total.
Two classes were merged rather than duplicated (`FConstantRockSource``FConstantFieldSource`,
`FSdfCarveOp``FSdfConvertOp`), each with two factories so the *authoring* vocabulary keeps saying
"rock"/"void" and "carve"/"fill". Multiplying by ±1 is exact in IEEE-754, so the three green ports
are bit-for-bit untouched — that claim is load-bearing and the next run tests it.
### `ClassifyBox` can say **AllAir** for the first time
No cave archetype has ever proved "all air"; `FConstantVoidSource` can, trivially and exactly, and a
floating-island strate is *by construction* mostly empty. That is `OPSTACK-DECOMPOSITION §7`'s claim,
and the test counts AllSolid and AllAir **separately** — an aggregate "N proved" would have hidden
precisely the number that matters. If AllAir comes back 0, the stack is still sound and the perf
argument simply did not fire (the VerticalShafts situation); the test says so out loud rather than
looking green.
### ⚠️ The bound is ONE-SIDED, and assuming otherwise would have been a hole
`Sdf ≥ WorldZ TopSurf` bounds an island from above. **There is no bound from below:** under an
island the SDF degenerates to ≈ `DistXY`, so a hairline thread of matter hangs down the axis to the
strate floor. Rejecting a box for sitting below an island would be a hole in the original's own
geometry. Only the top rejects.
Second trap, caught by doing the arithmetic rather than eyeballing it: the domain warp displaces X
and Y with **two independent** noise samples, so the point moves along the diagonal — the pad needs
`WarpAmp·√2`, not `WarpAmp`. A 1× pad is wrong by 41 % exactly where both noises saturate together:
rare, plausible-looking, and effectively unreachable by random testing.
### ⚠️⚠️ `AUDIT §C1` was reported closed on 2026-07-27. It was one site short.
Found by reading `GetFloatingIslandDensity` line by line to port it:
```cpp
const float WX = WorldX + FractalNoise3D(FVector(WorldX * 0.04f + (float)S * 0.0007f, ...));
```
**The sweep matched `SeedF * K`; this site spells it `(float)S * K`.** A textual sweep finds a
spelling, not a bug. And the property test could not compensate — `LargeSeedSurvives` asserts the
*heightfield* still varies, while this site perturbs an *island outline*: at seed 2e9 the term hits
~1.4e6, ULP 0.125 against a 0.04 voxel step, so the warp flattens and every island snaps back to a
perfect circle. Cosmetic, not catastrophic, which is exactly why nothing screamed for two days.
Fixed in **both** paths in one pass so `FloatingIslandEquivalence` stays a valid oracle. Recorded in
`AUDIT §C1` with the sharp edge that came with it: `SeedOffset` quantises its site key by ×100, so
`0.0007f` lands on site **0** — unique today (every other key is ≥ 0.19), silently collidable
tomorrow.
**This is the third time this session that a confident premise failed a check.** C1's *documented*
fix was wrong; "C9 is gone after FPSemantics" was wrong; now "C1 is closed, 0 left behind" was wrong.
The pattern is stable enough to plan around: **a claim about the code is evidence about whatever was
actually examined, and nothing else.**
### Deviation from `§7`, stated
`§7` sketched a `FRAME IslandWarp` wrapping the source. The warp stays **inside** the op. Frames are
worth building at the *second* real user, and two of the three (TunnelNetwork's cave warp, its tunnel
warp) are not ported. Designing an abstraction against one example is what this refactor has avoided
throughout — `IVoxelBiomeField` exists because a second, concrete need appeared. Revisit at
TunnelNetwork.
Also carried over from the shaft port: the 3×3 memo key includes `BoundarySealThickness`, **which the
original omits** although `SpreadZ` reads it. Same family as `§C2` and as the overhang regression of
2026-07-27. Adding a field to a cache key can only cost a recompute; leaving one out costs a wrong
world, invisibly.
**Ported: Maze · FlatPlain · CrystalChamber · SurfaceWorld (biomes incl.) · VerticalShafts ·
FloatingIslands — 6 of 8.** The two remaining are really one: `Underwater` *is* TunnelNetwork plus
`WaterLevelRelative` (§8), so the `switch` loses both cases in a single port.
**UNVERIFIED:** nothing here is compiled. Likely spots, in order: the two class renames
(`FConstantRockSource` / `FSdfCarveOp` no longer exist — every reference should go through a factory,
but a missed one is a clean C2065); `FFloatingIslandParams` reaching `VoxelDensityOpStack.cpp`
(it comes via `VoxelStrateTypes.h`, already included, so this should be free); the nested `FCells`
declared before its returning functions (the `FShaftFieldSource` C4430 trap, avoided deliberately);
and `MakeUnique<FIslandBlobSource>` being called from the factory namespace, which is fine only
because the class sits above the end-of-anonymous-namespace line.
**Next single action:** build, run the `VoxelForge` filter — **12 tests** now, the new one is
`VoxelForge.OpStack.FloatingIslandEquivalence`. Watch three numbers in its output: samples inside
island rock (0 ⇒ the equivalence proved only that two voids agree), the AllAir verdict count (0 ⇒ the
perf argument did not fire), and of course the diff count.
**⚠️ EXPECT ISLAND SILHOUETTES TO CHANGE** wherever the seed is large — that is the C1 fix, it is
intended, and §2.6.1 covers it.
Then `Underwater` + `TunnelNetwork` (§8 / §2, **last**, with §8.4's window-invariance discipline).
Perf still parked by Jahni until the transition is complete.
---
## 2026-07-28 — 12 tests green. TunnelNetwork STAGE A (of three) written.
Jahni: *"everything's green."* FloatingIslands is bit-identical and wired; 6 of 8 ported.
**Worth asking for explicitly next run:** the three coverage numbers that test prints — samples
inside island rock, the AllAir verdict count, and the diff count. Green with zero samples inside
island rock would mean the equivalence proved that two empty voids agree.
### The last archetype, staged deliberately
`GetDensityWithParams` is ~1080 lines, 13 detail modifiers, a two-region cache and a per-room op
override. Porting all of it before anything can be verified would be ~600 unverified lines on top of
~200 unverified ones — the `AUDIT §P3` pattern this refactor has dodged six times.
**The way in:** every detail modifier is amplitude-gated, and `FStrateGenerationParams` already
leaves all of them at **zero** by default (`BuildParamsFromDefinition` stopped merging them globally
— they arrive as per-room ops). One exception, `SurfaceRoughness = 5`. So zeroing those amplitudes
sends the ORIGINAL down exactly the path stage A ported, and stage A is verifiable **today**, bit for
bit, against the real function. Same discipline as the height-stack test's "defaults, then all ops
ON", taken in the other direction.
- **Stage A (this commit):** vertical scale · base rock · cave warp · room graph (+ pits + chimneys)
· carve · worms · structural post. 6 ops.
- **Stage B:** the 13 detail modifiers, gated on `Sdf < SDFBlendRadius·3`.
- **Stage C:** the per-room op override (`§2`'s option (a)), the `Underwater` water flag, and only
then does `UsesOperatorStackForChunk` return true for either.
### ⚠️ The one decision that matters: the op CALLS `BuildChunkCache`, it does not transcribe it
Every other port is a literal transcription. This one must not be. `BuildChunkCache` carries the
two-region window-invariance discipline (`ARCHITECTURE §8.4`) and is the most delicate code in the
plugin; transcribing it would **fork** it — two copies of one invariant, drifting, with the copy
"validated" by a test that compares it to the original. What *is* transcribed is the ~60 lines of
glue around it (strate-index memo, search-box key, warp, pit/chimney loops).
### `FRAME` ops are retired, and porting is what retired them
`§2` described two nested frames (`VerticalScale`, `CaveWarp`) and `§7` a third (the island warp).
Porting all three dissolved all three:
- **`CaveWarp` wraps exactly ONE operator.** Pits and chimneys explicitly read *unwarped* coords.
A transform whose scope is one op is a local variable, not a frame.
- **`VerticalScale` is `Z / Scale`** — a pure function of a scalar and a param, one line wherever it
is needed. A frame would add a channel to avoid a division.
- **The island warp** was already kept local for the same reason.
**Zero frames out of three candidates.** It was never missing infrastructure; it was the same thing
seen three times from a distance. Recorded in the builder rather than left as a permanent TODO.
Also found: TunnelNetwork's carve divides by `Max(SDFBlendRadius·2, 1)` where Maze/Shafts/Islands
divide by `Blend·2`. The formulas diverge once `Blend·2 < 1`, so `FSdfConvertOp` gained an explicit
`MinDivisor` instead of letting the two look interchangeable. `Max(x, 0) == x` for positive Blend, so
the green ports are untouched.
### ⚠️ A test that compares against the original would have measured the ORIGINAL'S bug
Check 3 (two stacks, different params, evaluated A/B/A/B at the same point) was written comparing
both against `GetDensityWithParams`. **It would have failed** — and not because of the port. The
original's SDF cache key is `(XY box, strate, seed)` with **no params**, so under interleaving it
serves B the rooms it built for A. Comparing to it there measures its staleness, not my operator.
Rewritten to compare each stack against **itself evaluated alone**: an oracle that does not share the
defect under test.
That in turn raises a **suspicion, filed as a suspicion**: `GetGenerationParams` blends params across
Gradient transitions, so two chunk Zs inside one strate can hold different params with the same XY
box, strate index and seed ⇒ no rebuild ⇒ the lower chunk gets the upper chunk's rooms. In
production, with no live edit needed. Recorded in `AUDIT §C2` **with the check that would confirm
it**, because the whole chain rests on "Gradient blending actually varies within a strate", which I
have not verified. The port does not inherit it — the params fingerprint forces the rebuild.
### What stage A deliberately does NOT prove
Box verdicts: **zero proved, and the test asserts zero.** `FRoomGraphSource` answers `Both` (its
bounds are in the SDF cache, which it would have to build for the queried box — worth doing only
once `ClassifyTile` actually consumes `ClassifyBox`), and `FWormFieldSource` answers `CarveOnly`
*everywhere*, because a fielded-noise carve has no spatial bound. That is `§0.2`'s point, and the
amplitude cap that fixes it (`t ∈ [0,1]`, `Mask ∈ [0,1]` ⇒ at most `WormStrength` toward air) now has
a home in `FWormFieldSource::MaxCarveAmplitude()`, waiting for a fold that carries numbers.
**UNVERIFIED:** not compiled. Likely spots: `FStrateTerrainOpEntry` / `UVoxelStrateDefinition` newly
reachable from `VoxelDensityOpStack.cpp` (added the include); `MakeSdfCarve`'s new defaulted third
parameter (declared in the header, so the three existing call sites still compile); `FCrc` needing
`Misc/Crc.h` (it comes via `CoreMinimal.h`, and `FSurfaceColumnSource` already uses it in this file);
and the test's `GetGenerationParams` signature.
**Next single action:** build, run the filter — **13 tests**, the new one is
`VoxelForge.OpStack.TunnelNetworkSpineEquivalence`. Then stage B (the 13 modifiers).
---
## 2026-07-28 — Stage A is bit-identical. And its own counters say the test was thin.
```
TunnelNetwork STAGE A: bit-identical across 6000 samples in 24 chunks
(65 in open cave, 5636 in rock)
Params-fingerprint : 3 of 400 probe points genuinely differ, 0 served wrong
Box verdicts : 0 proved, 40 Mixed ← asserted, expected
Op count : expected 6, got 7 ← FAIL
```
**The port is correct.** Vertical scale, cave warp, the shared `BuildChunkCache`, pits and chimneys
at unwarped coords, the floored carve divisor and the worm network mask all reproduce the original
bit for bit. The only failure was **my arithmetic in the assertion**: 4 ops + 3 structural = 7, and I
wrote 6. Fixed.
### The two numbers that matter are the ones that passed
**65 of 6000 samples in open cave — 1.1 %.** Green, and mostly comparing solid rock to solid rock:
the carve, the pits, the chimneys and the worm carve only run near the network. At `RoomSpacing = 80`
/ `RoomDensity = 0.35` a 32-voxel chunk usually contains no cave at all.
**3 of 400 probe points differ between the two param sets — 0.75 %.** So the stale-cache check asked
its question three times and answered "not stale" 397 times about points that could never have
revealed staleness.
Both counters exist precisely to say this, and both said it. **Both guards, however, only fired at
ZERO** — so the run was green with the coverage of a much smaller test. That is the same lesson as
"a test that prints nothing on success is indistinguishable from one that never ran", one notch
finer: *a coverage guard that only trips at zero does not measure coverage, it only notices absence.*
Both are now **fraction thresholds** (≥ 10 % cave, ≥ 5 % differing probes) and both print a
percentage.
Fixes applied: `RoomSpacing 80 → 42`, `RoomDensity 0.35 → 0.85`.
### New check 3b — did the pits and chimneys actually fire?
`§2` calls those two loops the fiddliest thing in the whole decomposition (unwarped coords
`SmoothMin`'d into the warped room SDF). Setting `PitDensity = 0.55` proves nothing: `SDFCache.Pits`
can still come back empty and the test stays green. So the new check builds the same stack with
`PitDensity = ChimneyDensity = 0` and counts the points that MOVE. Zero ⇒ those loops contributed
nothing and the equivalence says nothing about them, whatever colour the test is.
Same move as the FloatingIslands "samples inside island rock" counter, applied to a sub-feature
instead of a whole archetype: **enabling a feature is not evidence it fired.**
**UNVERIFIED:** the strengthened test is not compiled. Only the test file changed; the operators are
untouched and already proven bit-identical.
**Next single action:** re-run the filter (13 tests). Expect stage A green with a cave fraction in
the tens of percent, a fingerprint check in the double digits, and a non-zero pit/chimney count.
Then stage B — the 13 detail modifiers.
---
## 2026-07-28 — the coverage fixes landed, and check 3b caught a real gap on its first run.
```
Cave coverage : 21.0 % (was 1.1 %) ✅
Params-fingerprint : 383/400 = 95.8 % differ (was 0.75 %) ✅
Equivalence : bit-identical, 6000 samples, 1259 in open cave ✅
Box verdicts : 0 proved, 40 Mixed (asserted)
Pit/chimney : 0 of 1500 points move ❌ ← the new check, earning its keep immediately
```
### Pits and chimneys never ran, and the params said they would
`BuildChunkCache` bakes them inside `if (!CR.RoomOp) continue;`, then reads **`OpParams`** — a
*fresh* `FStrateGenerationParams` with only that room's terrain op applied. So
`FStrateGenerationParams::PitDensity`, which the test set to 0.55, **is never read by the bake at
all**. Pits, chimneys and columns exist *only* through a `UVoxelTerrainOpDefinition` rolled per room,
and the fixture's definitions have an empty `TerrainOperations`. Zero pits, every run.
**Not a product bug, and worth stating so I don't "fix" it later:** those fields carry no `UPROPERTY`
on `FStrateGenerationParams`. It is a transport struct written by `ApplyTo`, not a settings surface —
nothing in the editor offers Jahni a `PitDensity` that quietly does nothing. My reading was wrong,
not the code.
**Consequence for stage C:** the per-room op override is not an optional refinement. It is the *only*
path by which pits, chimneys and columns exist at all.
### The fix took three attempts, and the second one was the instructive failure
1. ~~Zero `PitDensity` on the params~~ — the fields nothing reads.
2. ~~Build a second stack with an empty op pool and diff the densities~~ — **would have silently
lied.** The op pool is *not* in the SDF cache key (in production `LayoutVersion` covers pool
edits, which is why the original gets away with it), so two stacks differing only by the pool
would share the same `thread_local` cache and produce identical output — reported as "pits
contribute nothing", for the second time, for a third reason.
3. **Ask the bake what it baked.** The suspected cause was an empty `SDFCache.Pits`; that is a
structure the test can simply *look at*. `BuildChunkCache` over six search boxes, then assert
rooms > 0, pits > 0, chimneys > 0. No cache key involved, no ordering to respect, and it fails
for exactly one reason.
The test now attaches a real two-entry op pool (Pit 0.5 / Chimney 0.5, so every room draws one).
**Safe at stage A on a non-obvious ground:** the per-room override applies the op to a copy of the
params that drives the 13 unported detail modifiers — but `ApplyTo(Pit)` writes only the four pit
fields, so no modifier wakes. A `Terrace` op there *would* break stage A, which is precisely what
stage B will add. There is also a guard that errors out if the bake ever produces **columns**, since
stage A has not ported `STEP 4d`.
### And a message that lied for a whole run
The green equivalence line said *"Exercised: … pits and chimneys at UNWARPED coords"*. It was false —
zero pits existed. A success message that asserts coverage instead of reporting it is the same
failure as a guard that only trips at zero: it reads as evidence while measuring nothing. Reworded to
point at the bake-coverage number rather than claim the coverage.
**UNVERIFIED:** test file only; operators untouched and already proven bit-identical.
**Next single action:** re-run. Expect non-zero pits and chimneys, zero columns, and the equivalence
still bit-identical — *that* run is the one where stage A is genuinely covered. Then stage B.
---
## 2026-07-28 — STAGE A GENUINELY COVERED. 13 tests green. Handoff rewritten.
```
TunnelNetworkSpineEquivalence ......................... Success
Equivalence : bit-identical, 6000 samples, 1431 in open cave
Cave coverage : 23.9 % (floor 10 %)
Bake coverage : 49 rooms, 56 pits, 28 chimneys, 0 columns
Fingerprint : 383/400 = 95.8 % differ, 0 served wrong
Box verdicts : 0 proved, 40 Mixed (asserted — §0.2's worm bound)
```
**This is the run where stage A means something.** The previous green one did not: it compared
mostly solid rock to solid rock, its stale-cache check asked its question three times out of 400, and
its pit/chimney loops ran on an empty list while the success message claimed otherwise. Same code,
same colour, three different strengths of evidence — which is the whole argument for printing
coverage numbers instead of pass/fail.
56 pits and 28 chimneys now exist in the bake, so the two loops `§2` calls the fiddliest thing in the
decomposition (unwarped coords `SmoothMin`'d into the warped room SDF) were exercised by 6000
bit-identical samples. 0 columns confirms `STEP 4d` stayed dormant, as stage A requires.
`OPSTACK-HANDOFF.md` rewritten for a fresh context: the three-stage TunnelNetwork plan and why stage
A is verifiable while incomplete, the `FRoomGraphSource` calls-not-transcribes rule, `FRAME` ops
recorded as retired, and the method lessons regrouped — notably the three coverage traps and
"enabling a feature is not evidence it fired".
**Next single action:** stage B — the 13 detail modifiers of `STEP 4b4h`, inserted between the carve
and the worms. The test's op-count assertion moves, and its `EnableTunnelFeatures` starts turning the
amplitudes back ON one group at a time (a `Terrace` op in the room pool will be the first thing to
break stage A's equivalence, by design).
---
## 2026-07-28 — **8 OF 8.** TunnelNetwork + Underwater ported. Unattended run, 8 commits, zero builds.
Jahni started this session and left. Rule #1 held: nothing was compiled, nothing was launched,
nothing was pushed. **Everything below is written and committed but UNVERIFIED.** He reverts by sha,
which is why the one-commit-per-group rule was the one thing not bent.
```
6e29cbf B1 surface roughness (4b) + VoxelNoise::Cellular3D shared out of VoxelGenerator.cpp
ab1a996 B2 Terrace · LayerLines · Ribbing (4c) + FRoomGraphSource::FState hoisted out of Eval
6ec6009 B3 cave Overhang · Cliff · Scallop · Arch (4c)
b063d43 B4 Columns (4d) · Domes (4g) · Pinch (4h) · FloorBias + Column op in the test pool
8a303cc B5 the gate itself — test only, proves a voxel outside it is bit-identically untouched
9591088 C1 per-room op override — eleven detail ops read FRoomGraphSource::LocalParams()
e479fcd C2 Underwater wired to the same builder + its own equivalence check
(this) C3 UsesOperatorStackForChunk returns true for both — 8 of 8 — + CODEMAP / PLAN / PROGRESS
```
**Test filter: `VoxelForge`. Still 13 tests** — no test file was added; the TunnelNetwork test grew
five new checks. `VoxelForge.OpStack.TunnelNetworkSpineEquivalence` is the one to read.
### What the archetype switch looks like now
`ConstantRock → RoomGraph(warp + pits + chimneys) → SdfCarve → Roughness → Terrace → LayerLines →
Ribbing → Overhang → Cliff → Scallop → Arch → RoomColumn → Dome → Pinch → FloorBias → Worms →
[spine → seal → passage]`**19 ops, one builder, two archetypes.** Every one of the eight
archetypes now has an operator-stack twin, per-strate opt-in, each equivalence-tested bit for bit
against its original function.
⚠️ `UsesOperatorStackForChunk` returning true for TunnelNetwork/Underwater **changes nothing by
itself** — it still requires `bUseOperatorStack` ticked on a strate asset, which is Jahni's call and
was deliberately not done. What *has* changed: the flag is no longer a no-op anywhere. Ticking it on
any strate now really switches that strate onto the operator stack.
### Five things found by reading the original, ported as-is, and worth knowing
Each of these looked like a bug or an oversight. None was fixed, because with no test feedback a
"cleanup" is an unfalsifiable guess — and three of them are load-bearing for the look of the world.
1. **There are TWELVE detail modifiers, not thirteen** — and only **ELEVEN** read the per-room param
copy. The `const FStrateGenerationParams& Params = LocalTerrainParams;` shadow is declared *inside*
the `if (bNearCaveSurface)` block, which begins **after** STEP 4b, so **surface roughness reads
strate params**. Both numbers were carried wrong in the handoff and both matter for C1.
2. **The cliff modifier's comment describes code that does not exist.** It promises "sample density at
Z±1 and compute the vertical gradient"; the code samples nothing and uses a Perlin with 3× Z
frequency as a proxy it calls `VertGrad`. Multiplying by `CaveSDF` still gives it the right sign
either side of the surface, which is why it produces steeper faces at all.
3. **Terrace's two SDF gradient probes query unwarped X/Y and raw Z**, while the field they probe was
evaluated at warped coordinates and effective Z — and they exclude pits and chimneys. The probe
does not sample the field whose slope it measures.
4. **`ColumnDensity` on `FStrateGenerationParams` is read by nothing.** Neither the bake (which reads
a fresh `OpParams` with only the room's op applied) nor the per-voxel loop. Columns exist *only*
through a `Column` terrain-op asset in the strate's pool. Same shape as the `PitDensity` finding,
and the reason B4's coverage had to be an assertion about the bake rather than a params probe.
5. **The per-voxel ~74-field param copy** of the override is transcribed as-is. Real perf item.
### Two decisions this run made that are not reversible by taste
**B5 — the gate is a repeated early-out per operator, not a scoping container.** The stack is a flat
list that `ClassifyBox` folds op by op; a container would have to re-implement `VF_FoldOp` and would
hide its children from the fold, and an op that only exists inside a container cannot become a Phase-3
asset. The cost is stated rather than hidden: the original tests once and skips twelve, the stack
tests twelve times. Predictable branches, but measure before optimising — that is the §C10 lesson.
**C1 — one op owns the shared state, eleven read it.** `DECOMPOSITION §2` called the per-room override
"the piece with no clean home" and proposed a per-modifier scoping predicate. The difficulty came
entirely from assuming each modifier must *own* its params. `FRoomGraphSource::LocalParams()` publishes
the strate params with the nearest room's op applied, memoised once per voxel (invalidated at the top
of every `Eval`, before any early-out), and the modifiers read it. **No scoping predicate was
invented** — this is the third use of a pattern already in the file (`FOverhangShelfMod`
`FSurfaceColumnSource`, `FShaftLedgeMod``FShaftFieldSource`). Same shape as the pit/chimney
resolution: the problem was in the framing, not the code.
### ⚠️ One thing C1 broke that nothing consumes yet — fix before `ClassifyTile` does
`EffectOverBox` still answers from **strate** params, because a box spans many rooms and a per-voxel
copy has no meaning there. But `ApplyTo` writes the op's value even where the strate's was `0`, so a
room op can **enable a modifier the strate had switched off**. On a strate with a terrain-op pool, a
box verdict can therefore be too optimistic. Harmless today — nothing consumes `ClassifyBox` in
production — and **it must be fixed before `ClassifyTile` starts to.** Noted at
`FLayerLineMod::EffectOverBox`, which is where a reader would land.
### The coverage discipline, extended — and why the checks grew faster than the code
Every group added an operator *and* a way to be told it did nothing. The test now reports:
- **group coverage** (check 1b): rebuild the stack with one group off, count moved samples. **Zero is
an error, not a warning.** Legitimate as a stack-diff here — unlike for the op pool — because these
are params, and the params CRC *is* in the SDF cache key.
- **the noise switch** (1c): all four roughness noise types × warp on/off. The main equivalence only
ever takes the FBM branch; three of four cases were otherwise untested.
- **the gate** (1d): outside-gate samples must be bit-identical to a modifier-free stack, *and* the
inside-gate move count must be non-zero — otherwise "nothing leaked" only means "nothing happened".
- **the bake** (3b): rooms, pits, chimneys and now **columns**, which is the only sound way to cover an
operator that has no parameter. The stage-A guard that errored on `TotalColumns > 0` is inverted.
- **the override** (3c): how many baked rooms drew a `Terrace` op, and how many samples land inside
one of those rooms. Both non-zero, or check 1's green means only that two paths agree where the
override never applies — which is what stage B already proved.
- **Underwater** (5): 2000 samples in the Underwater *slot*, which is a different strate index, hence
a different bake seed and a different entry in the strate-index memo — something six chunks of slot
0 cannot exercise.
That is the pit lesson applied a fourth and fifth time: **enabling a feature is not evidence it
fired, and the check that proves it must be able to fail for exactly one reason.**
### What breaks first, per group, if a group is wrong
- **B1** — diffs concentrated in open cave within `|SDF| < SurfaceRoughness·2`. If instead only check
1c fails, exactly one branch of the noise switch is implicated and nothing else.
- **B2** — diffs near horizontal surfaces (terrace) or in thin Z bands (lines/ribs). If the *whole*
equivalence collapses everywhere, suspect the `FState` hoist, not the three new ops.
- **B3** — a band `|SDF| < range`, or inside open cave for the arch.
- **B4** — dome/pinch near room centres and perimeters; a column error is a ring of diffs at fixed XY
through a whole room height.
- **C1** — diffs confined to the subset of rooms that drew `Terrace`, with terrace-shaped Z banding,
while check 3c still reports non-zero coverage.
- **C2** — if the TunnelNetwork equivalence is green and only this fails, **the operators are fine**
and the finding is a real density difference between Underwater and TunnelNetwork. That would
contradict `DECOMPOSITION §8` and is worth more written down than patched.
### Likely compile-error spots, in order
1. `VoxelNoise::Cellular3D` in `VoxelCaveMorphology.h` — it reopens `namespace VoxelNoise` in a header
that does not include `VoxelNoise.h`. Legal, but it is the newest structural change.
2. `#include "VoxelTerrainOpDefinition.h"` newly needed in `VoxelDensityOpStack.cpp` for `ApplyTo`
(added). `FCachedRoom::RoomOp` was only forward-declared before.
3. The five modifier constructors that gained a `const FRoomGraphSource*` parameter — a missed call
site is a clean argument-count error at `BuildTunnelNetworkStack`.
4. `FRoomGraphSource::FState` is declared *after* `PrepareChunk` and used inside `Eval` further down;
in-class member functions see the whole class, so this is fine, but it is the `C4430` shape that
bit the shaft port.
5. The test's `EVoxelTerrainOpType::Terrace` / `Column` and `FTestWorld::SlotUnderwater` — all exist,
all newly referenced from this file.
### Next single action
Build, run the `VoxelForge` filter, and read three numbers before anything else: the **group-coverage
percentages** (1b), the **Terrace-room sample count** (3c), and the **inside-gate move count** (1d).
A green run with a zero in any of those is a green run that proved much less than it looks.
Then, in the order agreed: settle `AUDIT §C2`'s suspected item (answerable by reading
`GetGenerationParams` and `FStrateGenerationParams::Lerp` — no build needed), the worm amplitude cap
(`DECOMPOSITION §0.2`, its own commit, extends `BoxVerdictFold`), and then make `ClassifyTile` consume
`ClassifyBox`. Perf stays parked until Jahni unparks it.
---
## 2026-07-28 — after 8 of 8: the fold carries numbers, and `ClassifyTile` finally consumes `ClassifyBox`
Same unattended run, continued past the transition. Four more commits, still zero builds.
```
64d0e11 AUDIT §C2's suspected item settled — CONFIRMED, and it is the default config (docs only)
353d504 the box fold carries NUMBERS — amplitude bounds alongside EVoxelOpEffect (§0.2)
239c037 the twelve detail modifiers inherit the room source's box verdict
6a8390f ClassifyTile consumes ClassifyBox for cave archetypes — the T1.d prize
```
**Test filter: `VoxelForge`. Now 14 tests** — the new one is
`VoxelForge.OpStack.ClassifyTileSoundness`.
### `AUDIT §C2` — the suspicion was right, and it needed no build to settle
Four links, each read in the source rather than inferred: `Alpha` depends on chunk Z *inside* a
slot; `Lerp` blends every room-placement field; `Gradient` + `TransitionBlendChunks = 2` are the
**defaults**; and the strate index is per-slot, so the cache key cannot notice. A worker that builds
`(X,Y,Z1)` then `(X,Y,Z2)` in one strate evaluates the second chunk against the first chunk's rooms.
The part that matters more than the seam: **the result depends on which chunk that worker happened
to build first** — a window-invariance break, and in multiplayer two peers can generate different
geometry from the same seed. Not fixed here: the fix is a params CRC in the *original* path's SDF
cache key, a live-generation change that wants a build in front of it.
**Why nothing caught it:** the fixture sets `TransitionType = Hard` on every strate on purpose. The
one configuration the tests never build is the default one.
### The numeric fold, and the thing `§0.2` does not say out loud
`§0.2` frames the worm as *the* blocker: a fielded carve has no spatial bound, so it answers
`CarveOnly` on every box and kills `AllSolid` everywhere. True — and the amplitude fix is exactly as
described (`t ∈ [0,1]`, `Mask ∈ [0,1]` ⇒ at most `WormStrength`). But porting it surfaced a second
half that was missing:
1. **The subtraction had no first term.** Nothing declared *how solid* the rock was. Hence
`ForcedMarginOverBox`, which `FConstantFieldSource` answers exactly (`|Value|`).
2. **The twelve detail modifiers were the bigger drag, and not because of their amplitudes.** They
are all gated on the SDF the room source writes, so where the source proves no cave reaches the
box they are **Identity**, not "bounded" — yet each declared `Both`/`FillOnly`/`CarveOnly` and
killed the hypothesis just as hard. They already held the pointer since C1; they simply were not
asking. `VF_NoCaveOverBox` fixes twelve declarations in one place.
Backwards compatibility is structural, not promised: `FLT_MAX` and `0` are the defaults, so an op
that overrides nothing subtracts `FLT_MAX` from a margin of `0` and dies exactly as before.
### `ClassifyTile` — the prize, and the only change on this run that could make a hole
Where it used to `return Mixed` without a call for cave archetypes, it now builds the strate's stack
and folds `ClassifyBox`. **SurfaceWorld and bedrock gaps keep their hand-written proofs** — an
exact-lattice column test beats any box bound, so the stack has nothing to offer there.
**The load-bearing decision:** `GetDensityAt`'s build switch was extracted into
`VF_BuildOpStackForChunk` and both callers now use it. A second copy would be the worst bug
available in this file — a tile skipped on the verdict of a stack that is not the one producing its
density *is* a hole. A "keep these in sync" comment would not have been enough.
Six guards, all failing to `Mixed`. The one that matters is **params bit-identical across every
chunk coord the box touches**, and it exists *because of* the `§C2` finding two commits earlier: a
tile straddling a blend band genuinely cannot be represented by one stack. The others: opt-in true on
every chunk (not just the triggering one), one cave slot per tile, no mixed cave/surface/gap tile, a
27-chunk-coord cap, and the disturbances folded by hand since `§10.2` leaves them outside the stack.
**A safety property worth recording, because it was checked rather than assumed:** no box query in
the whole operator library touches per-worker memo state — `EffectOverBox` and `ClassifyBox` are
pure in every op. So the classifier building and folding its own stack cannot clobber the caches the
density path depends on.
**What it buys:** Maze, FlatPlain/CrystalChamber, VerticalShafts and FloatingIslands can prove tiles
in production. TunnelNetwork and Underwater still prove nothing — their chain dies at
`FRoomGraphSource`, which answers `Both`. Making it answer spatially means building the SDF cache for
the queried box, which is *now worth it*: a skipped tile saves 30k+ density evaluations, and both
the amplitude fold and the modifiers' Identity inheritance are already in place to receive it.
**That is the single highest-value thing left.**
### Fixture: a cross-test hazard that was hidden by an accident
`FTestWorld::Build` gains `bUseOperatorStack` (default false), and every test world now gets a
**process-unique `LayoutVersion`**. `PassagesVersion` is per-instance and starts at 0, so two
`FTestWorld`s both reported `1` — and `GetDensityAt`'s per-chunk caches are keyed on
`(ChunkCoord, LayoutVersion)`. One world could be served the previous world's params *and its
`CP_UseOpStack` flag*. Invisible for as long as every world agreed the flag was false; the first
world that ticks it removes the coincidence, in both directions.
### The reviewer pass, done as the last step
Re-read every ported operator against the original block, as a reviewer rather than the author.
**No transcription error found.** What the pass did confirm, and what is worth knowing:
- operator order in `BuildTunnelNetworkStack` matches the original **line for line** (roughness →
terrace → lines → ribs → overhang → cliff → scallop → arch → columns → domes → pinch → floor bias);
- `EffectiveZ` is recomputed per operator instead of once — same expression, same operands, so
bit-identical;
- the terrace's `SDFBlendRadius` comes from strate params in the port and from the *shadowed* copy in
the original, which are equal because no `ApplyTo` writes that field;
- `LocalParams()` is materialised lazily by the first modifier that asks, which reproduces the
original's cost profile (one struct copy per voxel **inside the gate**, none in deep rock).
### Next single action
Unchanged: build, run the `VoxelForge` filter, read the coverage numbers before the colours. Then
the two numbers new to this batch — `VoxelForge.OpStack.ClassifyTileSoundness`'s **count of tiles
actually brute-forced** (zero would mean it proved nothing about the new wiring, and it errors on
that), and the verdict counts compared against `VoxelForge.Determinism.ClassifyTileSoundness`, whose
difference *is* the T1.d gain.
After that: make `FRoomGraphSource::EffectOverBox` answer spatially. Everything else is now waiting
on it, and the two commits above were built to receive it.
---
## 2026-07-28 — **IT ALL COMPILES AND IT IS ALL GREEN.** 14 tests. 8 of 8, verified.
Jahni built it. Everything written across the unattended run is now verified except one number.
```
TunnelNetwork STAGE A+B ... bit-identical, 6000 samples (1002 open cave, 4699 rock)
Cave coverage ............ 16.7 % (floor 10 %)
Group coverage ........... roughness 64.8 % · terrace 60.2 % · layer lines 25.7 %
ribbing 13.4 % · overhang 39.4 % · cliff 26.2 %
scallop 1.6 % · arches 3.4 % · domes 1.6 % · pinch 1.0 %
floor bias 18.2 % ← ALL TWELVE NON-ZERO
Roughness noise sweep .... 8/8 variants bit-identical over 1500 samples
Gate check (B5) .......... 979 outside the gate, 0 leaked; 4880 inside move
Params fingerprint ....... 395/400 = 98.8 % differ, 0 served wrong
Bake coverage ............ 49 rooms, 26 pits, 30 chimneys, 39 columns
Per-room override (C1) ... 51/51 rooms carry an op, 10 Terrace, 1119 samples inside them
Box verdicts ............. 0 proved, 40 Mixed (asserted)
Underwater (C2) .......... bit-identical, 2000 samples — 0 in open cave ⚠️
```
**Every claim this run made about correctness now has a measurement behind it.** The twelve
modifiers are transcribed correctly *and* each demonstrably fired; the four noise branches are
covered, not just the one; the gate suppresses everything outside it with zero leaks; and C1 is
proved by the only check that could prove it — 10 rooms drawing a `Terrace` op whose params
overwrite the strate's, with 1119 samples inside them, still bit-identical.
### The one number that is a warning, and it is the test working
`Underwater: 0 of 2000 samples in open cave.` **A green bit-identity over 2000 samples of solid rock
is not evidence** — it is exactly what two agreeing voids look like. Same failure as stage A's 1.1 %
run, in a different slot, caught by a counter written for precisely that.
**A real bug surfaced while diagnosing it.** The sampled chunk-Z range used `Z / CHUNK_SIZE`, and
C++ integer division **truncates toward zero**. TunnelNetwork sits at the top of the layout in
positive Z, where truncation and floor agree — so the bug could not show there. Underwater sits at
the **bottom, in negative Z**: `-1 / 32` is `0` truncated and `-1` floored, so the upper chunk bound
starts a notch too high and the `Clamp` piles the excess onto the strate's last voxel, inside the
top seal band, i.e. solid rock. Fixed in both point builders (`FloorDivChunk`). Same family as the
`DivideAndRoundDown` lesson already in the project notes.
That is a **candidate**, not a conclusion, so the fix does not stand alone: new **check 5b** gives
each of the three possible causes its own number — rooms baked for the Underwater strate index (the
bake), samples landing inside the seal-free interior (the Z range), and both non-zero (the XY
spread) — and prints how to read them. Sampling also widens 8 → 24 clusters. **Unverified: this
commit has not been built.**
### What the green run settles, beyond the tests
- `AUDIT §C10` stays closed: eight archetypes now compare **bit for bit** against their originals
across every detail path, including four noise types and a domain warp.
- The `FRoomGraphSource::FState` hoist, the lazy `LocalParams()` memo and the whole C1 mechanism are
compiled and exercised — the pattern "one op owns the state, the rest read it" now has three
users, all green.
- The extraction of `VF_BuildOpStackForChunk` did not disturb `GetDensityAt`: the equivalence tests
that route through it are unchanged.
- `0 proved, 40 Mixed` remains **asserted** for TunnelNetwork. That is not a defect and not stale:
the chain still dies at `FRoomGraphSource::EffectOverBox`, which is the next piece of work.
### Next single action
Re-run the filter and read **one line**: `Underwater diagnosis`. It names which of the three causes
produced the 0 %, and the fix follows from that rather than from a guess.
Then the one thing everything else is now waiting on: **make `FRoomGraphSource::EffectOverBox`
answer spatially.** Its room and tunnel bounds are already in the SDF cache; what it costs is
building that cache for the queried box, and that is now clearly worth it — a skipped tile saves
30 000+ density evaluations, `ClassifyTile` consumes `ClassifyBox` in production, the numeric
amplitude fold is in place, and the twelve modifiers already inherit the source's verdict. Every
piece is built to receive it and nothing else moves until it lands.
## 2026-07-28 — the Underwater 0% is EXPLAINED, and `EffectOverBox` answers spatially. **UNBUILT.**
Two things, in the order the handoff asked for them. The first cost one file read; the second is the
piece everything has been waiting on since stage A.
### 1. The one number — read, not inferred
`Saved/Automation/Automation2026.07.28-14.30.37.csv`, the run Jahni had already made:
```
Underwater (C2) ..... bit-identical, 6000 samples in 24 chunks — 94 in open cave (1.6%) was 0.0%
Underwater diagnosis strate index 7, voxel Z [-1440, -1313], seal-free interior (-1436, -1316);
5600 of 6000 samples inside that interior;
bake: 54 rooms, 28 pits, 17 chimneys, 25 columns over 6 search boxes
```
**The truncation WAS the cause.** All three counters of check 5b came back the way the "it's the Z
range" hypothesis predicts: the bake is alive (54 rooms — never the problem), and the interior count
is now 93 % where the whole point of the bug was that `Z / CHUNK_SIZE` truncating toward zero in
negative Z shoved samples into the top seal band. `FloorDivChunk` moved them back. **Section closed.**
One honest caveat, because the number deserves to be read rather than celebrated: **1.6 % is not
16.7 %.** TunnelNetwork's cave coverage carries a 10 % floor; Underwater's guard trips only at
**zero** (`if (UWInCave == 0)`), which is the exact shape the project's own lesson warns about — *a
guard that only trips at zero notices absence, it does not measure coverage.* 94 samples genuinely
exercise the carve, so the bit-identity now means something; it means about a tenth as much as
TunnelNetwork's. Per the diagnosis line's own reading key (rooms > 0, interior high) the residual
thinness is the **XY spread**. Not chased — the criterion for closing was non-zero, and it is a
counter to tighten when someone is next in this file, not a bug.
### 2. `FRoomGraphSource::EffectOverBox` answers spatially — the debt is paid
The chain no longer dies at the room source. The criterion is the **per-voxel cull lifted from point
to box**, which is the one formulation that can fail for exactly one reason:
> `Eval` starts at `MinSDF = FLT_MAX` and only lowers it through a primitive that survives its own
> cull. So if no cached primitive can survive its cull *anywhere in the box*, `Sdf` stays `FLT_MAX`
> across the whole box — the source is the identity, and so is everything behind it.
"Everything behind it" is not a figure of speech: `FSdfConvertOp` already returns `Identity` ("la
source a répondu pour la paire") and the twelve detail modifiers already inherit through
`VF_NoCaveOverBox`. **One function learned to answer and fourteen operators became provable.** That
is what the C1 wiring was for, and it is the first time the "one place, not thirteen" bet has paid.
What it does, in order: reject boxes spanning two strates or two op pools (`Both`); memoise the
verdict (all twelve modifiers ask the *same* question about the *same* box — without the memo a tile
would cost thirteen `BuildChunkCache` instead of one); build the cache for the queried box into a
**second per-worker cache**, never `FState::Cache`, so classification cannot disturb a live
generation; then test rooms and tunnels as spheres against the warp-dilated box, pits and chimneys
against the **undilated** box (they are queried in real coordinates — dilating them would be merely
cautious, *not* dilating them would be wrong), and columns as infinite cylinders.
Three things chosen deliberately, all erring toward CPU:
- **`|Perlin3D| <= 2`, not `<= 1`.** The header says "~[-1,1] (typically [-0.7,0.7])" — that `~` is an
observation, and a box verdict resting on an observation is the hole this file spends its life
avoiding. What is *provable* from reading `GradDot`: it returns `ru + rv` with both in `[-1,1]`, and
a trilinear lerp never leaves the hull of its inputs. So the warp dilation uses 2, roughly 3x the
real displacement. Costs a wider search box; cannot cost a verdict.
- **The op pool is passed to `BuildChunkCache`, not `nullptr`.** Tempting to skip it "since room
geometry doesn't depend on it" — and false. The bake reads `OpParams` to place **pits and
chimneys**. Passing `nullptr` would under-bound the cache and could return `Identity` over a real
pit. That is precisely a hole, and it is the same trap as `ColumnDensity = 0` not disabling columns.
- **The search box is wider than `Eval`'s**, which yields a *superset* of primitives — so "nothing
reaches the box here" implies "nothing reaches it there".
### 3. `AUDIT §C2` fixed on the `switch` path — and the obvious fix was the wrong one
`GetDensityWithParams` now takes **required** `ParamsFingerprint` + `LayoutVersion`. Details and the
reasoning are in `AUDIT §C2`; the part worth repeating here is why the alternative that section used
to recommend ("just add chunk Z to the key") is both insufficient and destructive: `Interleaved`
makes `Alpha` depend on chunk **XY** as well, and chunk XY is deliberately *not* in the key — the box
outlives the chunk so `WorldX ± 1` gradient probes don't thrash it (`§8.10`). Pinning XY to fix params
would have traded a determinism bug for a perf regression. The CRC is taken **once per chunk** where
the params memo already lives, so the per-voxel cost is two integer compares.
The three test call sites now pass `VF_FP(P)`, so the oracle stops sharing the defect it tests.
### 4. The other debt, `FLayerLineMod` / `LocalParams` — **dormant, and the premise was slightly off**
The handoff said this had to be paid before `EffectOverBox` landed, because a per-room op can raise a
modifier's amplitude above what the strate params claim. Checked before paying it, and the check
reverses the conclusion — the fifth time this refactor that has happened:
- when the source proves `Identity`, all twelve modifiers return `Identity` **soundly**: their
per-voxel gate is `bNearCaveSurface`, which is false everywhere in the box no matter what any room
op says. The room op cannot enable a gate that never opens.
- when the source answers `Both`, it supplies **no** `MaxCarveOverBox`, so the default `FLT_MAX`
removes the whole margin and nothing is provable regardless of what the modifiers claim.
So there is no reachable path today where an over-optimistic modifier bound changes a verdict. **The
debt goes live the day `FRoomGraphSource` gains a `MaxCarveOverBox`** — bounding the converter's
`2·BaseDensity` would make the modifiers' own numbers matter for the first time. That is now written
at the site rather than in a handoff.
### Ready to build. Likely compile-error spots, worst first
1. `GetDensityWithParams` signature — 2 production call sites + 3 in `VoxelForgeOpStackTunnelTest.cpp`
are updated; **any other caller I missed will fail to compile, which is the intent.**
2. `FCrc::MemCrc32` in `VoxelGenerator.cpp` and the test — reachable transitively in
`VoxelDensityOpStack.cpp` today, so it should resolve, but `#include "Misc/Crc.h"` is the fix.
3. `FBoxState` / `BoxState()` / `PerlinAbsBound` are new members of `FRoomGraphSource`, inserted
INSIDE the class (not near the FACTORIES banner — the mistake this file warns about twice).
4. `FBox::operator==` on `B.KeyBox == VoxelBox`.
5. The rewritten check 4 uses `EVoxelTileClass::AllSolid` / `AllAir` and a triple `float` loop.
### What to read in the results
- **`Box verdicts over 40 TunnelNetwork tiles`** — this line no longer asserts `0 proved`. It asserts
that **no proved tile is wrong under brute force**, and reports the count. A non-zero proved count
is the T1.d prize arriving; a zero count now emits a *warning* saying the check verified nothing,
because a soundness check with no verdicts to contradict is vacuous.
- Everything else should be **unchanged and green**. The §C2 fix changes generated terrain only
inside transition bands on the `switch` path (where it was previously order-dependent, i.e. not
well-defined), so an equivalence test that moves is a real signal, not expected noise.
## 2026-07-28 — GREEN, and `0 proved of 40`. The blocker was the **worm**, not the room source.
The build was green and every number in the previous entry held. One line did not:
```
Box verdicts over 40 TunnelNetwork tiles: 0 proved (0 AllSolid, 0 AllAir), 40 Mixed
-- brute-forced over 0 voxels, 0 violations.
WARNING: No TunnelNetwork tile was proved ... this check verified nothing.
```
**The warning I wrote for exactly this case fired, and then it was not good enough.** It offered two
candidate causes — "the tiles genuinely straddle cave" or "the source isn't reaching its `Identity`
branch" — and **both were wrong**. The real cause was a third operator that neither candidate
mentioned. That is the failure worth recording, more than the bug itself.
### The cause, found by reading two default values
`FWormFieldSource::EffectOverBox` answered `CarveOnly` unconditionally, with a *provably correct*
amplitude bound of `WormStrength`. And in `VoxelStrateTypes.h`:
```
float BaseDensity = 8.0f;
float WormStrength = 10.0f; // "Must exceed BaseDensity to create air." <- the field's own comment
```
So `SolidMargin = 8 10 = 2 < 0`, on **every tile of every strate with worms on**, before the fold
ever reached anything the room source had proved. The worm's bound is not loose by accident — the
defaults *require* it to exceed `BaseDensity`, or worms could never carve. A numerically correct
bound that is structurally always fatal.
### The fix was already written in the worm's own `Eval`, three lines up
```cpp
if (CaveSDF >= P.WormNetworkRange) // vrai aussi quand il n'y a pas de réseau (FLT_MAX)
{ NetworkMask = 0.0f; }
...
if (NetworkMask <= 0.0f) { return; }
```
**The worm IS spatially bounded** — not by a bound of its own, but by the room source's, exactly like
the twelve detail modifiers. Where `FRoomGraphSource` proves `Identity`, `Sdf` stays `FLT_MAX`
(verified: `FVoxelOpSample::Sdf = FLT_MAX` is the initialiser), so `NetworkMask` is 0 at every voxel
and `Eval` returns before touching `Density`. The worm is the *identity* there, not "a bounded
carve". It simply never asked the question.
So it now inherits the verdict — thirteen inheritors instead of twelve. Two details:
- **Deliberately NOT `VF_NoCaveOverBox`.** That helper returns `true` when `Rooms == nullptr`, which
is right for the twelve modifiers (they only ever exist in a stack where the room source is the
sole SDF writer) and **wrong** for the worm, which a future assembly could place behind a different
SDF writer — `FLatticeCorridorSource` writes that channel too. No room source ⇒ we don't know ⇒
`CarveOnly`. Not knowing must cost CPU, never a hole.
- `MaxCarveOverBox` now calls `EffectOverBox` rather than re-testing the condition, so the two cannot
drift. The room source's verdict memo makes the second call free.
### The lesson, and the instrument that came out of it
**A diagnostic that lists candidate causes without measuring them is still a guess** — it just looks
like rigour. My warning named two causes and had a number for neither, so a green run with a real
defect in it produced a message that sent the reader to the wrong two places.
`FVoxelOpStack::ClassifyBoxAttributed` now exists: the same fold, verdict-identical to `ClassifyBox`
(same loop, same early-out — a diagnostic that takes a different path than the thing it explains is
worse than none), reporting the **index of the first operator that kills each hypothesis**.
`IVoxelDensityOp::DebugName()` gives them readable names; it touches no cache key and no generation
decision, so it cannot change the world. Check 4 prints:
```
AllSolid killed by: <op> x<count>, <op> x<count>, ...
```
Always printed, not only on failure — when tiles *are* proved, that line is what says why the rest
are not. And the `0 proved` warning now says **"do not re-derive the cause, read the attribution
line"**, because the next person's guess would be as good as mine was.
### Ready to build. Likely compile-error spots
1. `FWormFieldSource` gained a third ctor arg (defaulted) and a `Rooms` member; its single
construction site in `BuildTunnelNetworkStack` passes `RoomPtr`, which is already in scope there.
2. `IVoxelDensityOp::DebugName()` is a new virtual with a default — six overrides added
(`ConstantFieldSource`, `RoomGraphSource`, `SdfConvertOp`, `OriginSpineOp`, `BoundarySealOp`,
`PassageCarveOp`, `WormFieldSource`). Everything else inherits `"(unnamed op)"` and reports by index.
3. `ClassifyBoxAttributed` / `GetOpDebugName` are new inline methods on `FVoxelOpStack` (header).
4. The test uses `TMap<FString,int32>::ValueSort` and `FindOrAdd` on a `const TCHAR*` key.
### What to read, in order
1. **`AllSolid killed by:`** — the new line. If it is empty ("AllSolid survived every tile") the
prize has landed. If it names `RoomGraphSource`, the tiles genuinely straddle cave and the sampler
is what to look at. If it names anything else, that operator's box answer is more pessimistic than
its `Eval`, and it is now named rather than guessed at.
2. **`Box verdicts over 40`** — proved count is a measurement; the assertion is only that no proved
tile is *wrong*.
3. Everything else should be unchanged. The worm change cannot alter density: `EffectOverBox` and
`MaxCarveOverBox` are box-verdict methods, and `Eval` is untouched.
## 2026-07-28 — the worm fix WORKED. `AllSolid killed by: RoomGraphSource x40` — and that is not an answer.
The attribution line did its job on its first run:
```
AllSolid killed by: RoomGraphSource x40
```
`WormFieldSource` is gone from that list, so the previous entry's fix landed exactly as reasoned. The
blocker moved one operator upstream, to the room source itself.
**And my own warning is now the thing to distrust.** It said: *"If it names RoomGraphSource, the tiles
genuinely straddle cave."* That is a **hypothesis wearing the costume of a conclusion** — the same
mistake as the previous warning, one level down. `RoomGraphSource` has FOUR primitive classes behind
it (rooms, tunnels, pits, chimneys) whose bounds differ enormously in quality, and "the tiles straddle
cave" is only one of the things a `Both` from it can mean. So: no third guess. Two changes, one of
them proved, and an instrument for the rest.
### Proved, not guessed: the columns test is gone
The first version treated columns as **infinite cylinders in Z** (the cache gives them no vertical
bound), so a box hundreds of voxels below the owning room answered `Both` because it shared an XY
circle with a column. That was the loosest test in the function — and it was **redundant**, not
conservative:
```cpp
void FRoomColumnMod::Eval(...) const
{
if (!VF_NearCaveSurface(InOut.Sdf, P.SDFBlendRadius)) { return; } // <- the only consumer
```
Columns are read by exactly one operator, and it gates on `Sdf` being near a cave surface. In a box
no room, tunnel, pit or chimney reaches, `Sdf` stays `FLT_MAX` at every voxel, so **no column can
execute regardless of where it sits in XY**. Removing the test tightens the verdict without touching
its correctness. That is a proof, not a relaxation.
### The instrument: which primitive class actually reaches the box
`EffectOverBox` no longer early-outs on the first hit. It **counts all four classes**, because
stopping at the first gives the right verdict and no information — which is precisely why
`RoomGraphSource x40` told us nothing actionable. The cost is nil at the scale that matters: we have
just run `BuildChunkCache`, which dwarfs a walk over ~100 structs, and the verdict is memoised so the
walk happens once per box rather than thirteen times.
`VoxelDensityOps::GetLastRoomBoxDiagnostic()` exposes it. **Deliberately a read-back of what the
operator computed, not a re-derivation in the test** — the test has everything needed to replay the
criterion, and replaying it would create a second definition that drifts from the real one and lies
on the day it is believed. Same reason `VF_BuildOpStackForChunk` exists.
The report now prints, per killed tile, how many rooms and tunnels of those in the cache actually
reach the box. **The hypothesis it is built to kill or confirm:** a tunnel is culled per voxel by its
**bounding sphere**, and for a long thin capsule that sphere is an enormous over-estimate, while a
room's cull sphere is a fair fit for a roughly spherical room. If tunnels ≫ rooms, the box test is
losing to capsule bounding spheres rather than to real cave, and the fix is a segment-vs-box distance
— nothing to do with the sampler or with cave density.
### What was deliberately NOT done, and why
Tightening tunnels to a real capsule test is **not** free correctness: the per-voxel cull *is* the
bounding sphere, so a capsule test would be tighter than the cull and would break the stated criterion
("no primitive survives its cull"). Making it sound needs the stronger criterion — *no primitive can
bring `Sdf` below `max(Blend, SDFBlendRadius·3, WormNetworkRange)`* — which in turn needs a bound on
how far `SmoothMin` of N primitives can dip below `min`. That is real §0.2 design work, and doing it
blind, in the same build, before knowing whether tunnels are even the problem, is the §C10 mistake
verbatim. **Measure, then tighten what the numbers name.**
### Ready to build. Likely compile-error spots
1. `VoxelDensityOps::FRoomBoxDiagnostic` + `GetLastRoomBoxDiagnostic()` — new declaration in the
header, defined in the .cpp *after* the anonymous namespace closes (it reads
`FRoomGraphSource::BoxState()`, whose type lives in that namespace; legal, since the type does not
appear in the function's signature).
2. `FBoxState` gained eight `int32` counters.
3. The test accumulates into new locals and calls `VoxelDensityOps::GetLastRoomBoxDiagnostic()`.
### What to read
1. **`...and when RoomGraphSource is the killer`** — the new second line. `tunnels 40 / rooms 3` says
capsule bounding spheres; `rooms 40` says the tiles really are near rooms and the sampler is what
to look at; pits or chimneys leading would be a surprise worth stopping on.
2. Whether removing the columns test alone moved `proved` off zero. If it did, that number is the
first real T1.d saving in the plugin.
## 2026-07-28 — the breakdown refuted MY hypothesis, and found the sampler was the bug
```
rooms 40, tunnels 40, pits 14, chimneys 0
Averages per killed tile: 4.9 of 7.2 rooms reach, 69.2 of 80.4 tunnels reach
```
I predicted "tunnels ≫ rooms ⇒ capsule bounding spheres". **Wrong, or rather insufficient:** tunnels
*are* wildly over-counted (69 of 80), but **rooms hit all 40 tiles too**, so fixing tunnels alone
would have moved the number by exactly zero. Fourth hypothesis this refactor has reversed on contact
with a measurement. The instrument paid for itself on its first run.
### The real finding: the tile sampler never left the (0,0) spine
```cpp
const int32 Extent = Step * Cells; // 1 * 8 = 8
Rng.RandRange(-4, 4) * Extent // XY ∈ [-32, +32] voxels
```
Against the defaults:
| | |
|---|---|
| `RoomSpacing` | **80** — ±32 does not cover half of one room cell |
| `OriginRoomRadius` | **20**, guaranteed at (0,0); cull radius `max(20·1.5, 8) + 3·4` = **42** |
| the (0,0) spine | descends exactly there |
So all 40 tiles sat inside the origin room's cull sphere, in the most cave-riddled cubic
metre of the entire world. `4.9 of 7.2 rooms reach` was not measuring the world's cave density — it
was measuring the spine hub. **Every conclusion about "is deep rock provable" drawn from that sample
was answering a different question.**
Widened to ±320 voxels (4 × `RoomSpacing`), and the report now **prints its own sampling extent** plus
how many tiles landed clear of the spine — because a sampler whose extent you cannot quote is one
nobody is watching. The brute-force soundness assertion is untouched: this changes what the
measurement *looks at*, never what it *demands*.
### Why rooms can't be tightened and tunnels can — the arithmetic, before writing any code
With `SDFBlendRadius K = 4`, `WormNetworkRange = 24`, mods gating at `3K = 12`, the strongest useful
threshold is `T = max(Blend, 3K, WormNetworkRange) = 24`.
`SmoothMin(A,B,K) = min(A,B) H³K/6`, `H = max(K|AB|,0)/K`. Two consequences worth writing down:
the penalty is **exactly zero** once `|AB| ≥ K`, and the running minimum therefore **saturates at
`K` below the true minimum** — so `Sdf ≥ min_i(SDF_i) K` for **any** number of primitives. That is
the bound that makes an "`Sdf ≥ T`" criterion possible at all.
Now compare the two possible criteria per primitive:
- **rooms** — cull rejects at `dist > Rmax + 3K` (= 57 worst case). The `Sdf ≥ T+K` criterion rejects
only at `dist ≥ Rmax + T + K` (= 73). Since `T + K = 28 > 3K = 12`, **the cull is strictly the
better test for rooms.** Nothing to gain; leave them alone.
- **tunnels** — the cull is the capsule's **bounding sphere**, and with `MaxTunnelLength = 200` that
sphere has radius up to ~107 for a tube of radius 7. The `Sdf ≥ T+K` criterion uses the real
distance to the segment, so it rejects at `dist(box, segment) ≥ 7 + 28 = 35`. **Order of magnitude
tighter**, and sound because `TaperedCapsule` is a genuine distance function
(`Dist(P, ClosestOnSegment) Lerp(Ra,Rb,T)`), verified rather than assumed.
So the eventual fix is a **per-primitive disjunction** — a primitive cannot matter if it fails its
cull *or* its own SDF stays ≥ `T + K` over the box — applied **only to tunnels**, where the SDF is
exact. Mixing is sound: primitives failing the cull contribute nothing, the rest are all ≥ `T+K`, so
the fold is ≥ `T`, and every consumer (converter at `Blend`, twelve mods at `3K`, worm at
`WormNetworkRange`) is identity at `Sdf ≥ T`.
### NOT done in this build, deliberately
The tunnel disjunction changes what `Identity` *means* here — from "`Sdf` stays `FLT_MAX`" to
"`Sdf ≥ T`" — and that is only sound if **every** consumer's threshold is ≤ `T`. Three premises still
need reading rather than assuming: `VF_NearCaveSurface`'s exact constant, the `Blend` actually passed
to `FSdfConvertOp` by `BuildTunnelNetworkStack`, and whether any modifier re-probes the SDF *outside*
the box (`FCaveTerraceMod` samples Z±1) before its gate. Any one of those wrong is a hole with no
collision behind it, and this session has already produced four confident chains that reversed on
checking. **Fix the measurement first; it costs nothing and it is wrong today.**
### The prediction, stated so the next run can refute it
With the widened sampler: **rooms should stop hitting every tile** (cull spheres of mean radius ~42
on an 80-lattice cover roughly 60 % of space, so ~40 % of tiles should be clear of all rooms), and
**tunnels should now be the binding constraint on nearly all of them.** If instead tunnels stay at
~100 % *and* rooms drop, the tunnel disjunction above is the whole remaining job. If rooms also stay
at 100 %, my model of the room lattice is wrong and that is the thing to look at next — not the
tunnels.
### Ready to build. Compile-error spots
`SpanCells` / `SpanVoxelsReported` / `NumTilesAwayFromSpine` are new locals hoisted **outside** the
tile loop (the report line needs them); `P.OriginRoomRadius` and `P.RoomSpacing` are read in check 4.
## 2026-07-28 — the sampler fix worked, the prediction failed, and the fixture is the reason
The widened sampler did exactly what it was meant to: **39 of 40 tiles now land clear of the (0,0)
spine.** And the verdict did not move.
```
[before] rooms 40, tunnels 40 | 4.9 of 7.2 rooms, 69.2 of 80.4 tunnels (XY ±32)
[after ] rooms 40, tunnels 40 | 6.3 of 8.3 rooms, 78.0 of 87.7 tunnels (XY ±320, 39/40 off-spine)
```
My stated prediction was *"rooms should stop hitting every tile."* **They did not.** By my own
pre-registered rule that means the room-lattice model was wrong — and it was, for a reason printed in
the report I had been reading past for three runs:
> `= 7.6 x RoomSpacing 42`
**`RoomSpacing` is 42 here, not the 80 I computed with.** I read the `UPROPERTY` default out of the
header instead of the fixture, which overrides it. Fifth premise this refactor that reversed on being
checked, and the plainest one: *the number was on screen.*
### Why the fixture cannot ever prove a tile — arithmetic, not opinion
`EnableTunnelFeatures` densifies **on purpose**, and says so: `RoomSpacing` 80 → 42,
`RoomDensity` 0.35 → 0.85, because at the defaults the first run had **1.1 %** of samples in open
cave and the equivalence was comparing solid rock to solid rock. That densification is what makes
check 1 mean anything.
It is also exactly antagonistic to provability:
```
room cull radius = max(R·1.5, R·HeightRatio) + 3·SDFBlendRadius
= 1.5R + 12 for R ∈ [10,30] ⇒ 27 … 57, mean ≈ 42
lattice spacing = 42, occupancy 0.85
```
**The mean cull radius equals the lattice spacing.** Sphere volume over cell volume gives ~3.6×
redundant coverage — *no box in that world can be outside all room cull spheres.* `6.3 of 8.3 rooms
reach` is not a symptom of a loose test; it is the world being saturated. No sampler change and no
tightening of tunnels can move it, which is why widening the sampler correctly changed nothing.
**So `0 proved` on this fixture is the right answer**, and it is informative: the tile-skipping prize
shrinks to nothing as caves saturate. What it is *not* is an answer about production.
### Both worlds, one criterion
Check 4 is now a lambda run twice — **parameterised, not copy-pasted**, because two copies of the
criterion would drift and the second one would lie:
- **`[dense fixture]`** — as before. Expected to keep reporting ~0, and that is now written down as
the correct result rather than read as a failure.
- **`[production defaults]`** — the same 40 tiles against `RoomSpacing 80 / RoomDensity 0.35`, the
actual `UVoxelStrateDefinition` defaults, which is the world the question "how many tiles can we
skip" is really about. At that spacing, cull spheres of mean radius 42 on an 80-lattice at 35 %
occupancy cover roughly a fifth of space.
Both are brute-forced voxel by voxel. This is **not** "widen it until it passes": the dense run stays
in the report and must stay ~0, and a false verdict in either run still fails the assertion. The two
stacks deliberately share `FRoomGraphSource`'s `thread_local` caches — the params fingerprint in the
key is what keeps them apart, so this run and check 3 now watch each other.
### The lesson, and it is not the one I expected to write
Three runs in a row I reasoned from `RoomSpacing = 80` while the test printed 42 at me. The
instrument was right, the arithmetic done on it was right, and the *input to the arithmetic* was
taken from the wrong file. **"Verify the premise" has to include the premises you are confident
enough about not to look up** — especially a default, when a fixture exists whose entire job is to
override defaults.
### Ready to build. Compile-error spots
1. Check 4 is now `auto RunTileScan = [&](const FVoxelOpStack& S, const FStrateGenerationParams& TP,
const FVoxelOpContext& TCtx, const TCHAR* Label)`, called twice. Verified mechanically: zero bare
`Stack` / `P.` / `Ctx` references survive inside the lambda body, braces and parens balance.
2. Five `FString::Printf` sites gained a leading `%s` **and** the matching `Label` argument — the
mismatch was caught and fixed before commit; worth re-checking if the log looks scrambled.
3. `SparseStack` is a second `FVoxelOpStack` built by `BuildTunnelNetworkStack` and `PrepareChunk`ed.
### What to read
**`[production defaults] Box verdicts`** — the only new number that matters. Non-zero is the first
real T1.d saving in the plugin. If it is *also* zero, then the per-class line under it says whether
rooms still saturate at 80 spacing (my model is wrong again, look there) or whether tunnels are
finally alone (the segment-vs-box disjunction from the previous entry is then the whole remaining
job, and its three unverified premises are listed there).
## 2026-07-28 — ✅ **THE PRIZE LANDED.** 6 of 40 tiles proved, 7986 voxels brute-forced, 0 violations.
```
[production defaults] Box verdicts over 40 tiles: 6 proved (6 AllSolid, 0 AllAir), 34 Mixed
-- brute-forced over 7986 voxels, 0 violations
[dense fixture] 0 proved, 40 Mixed <- correct, and predicted
```
**This is the first real T1.d saving in the plugin.** 15 % of tiles skip `GenerateMesh` entirely —
one `BuildChunkCache` traded against 30 000+ density evaluations each — and not one of those verdicts
is asserted: all 7986 voxels were re-evaluated through the stack and every single one came back on
the claimed side. The two-world split also did its job: the dense fixture reported 0, exactly as the
arithmetic said it must, so the number above is a measurement of *production*, not of a test that was
widened until it agreed.
Five runs to get here, and the honest summary of them is that **every single one of my hypotheses was
wrong and every single instrument was right**:
| run | my hypothesis | what the instrument said |
|---|---|---|
| 1 | "the tiles straddle cave" / "Identity unreachable" | **the worm**, unbounded `CarveOnly`, `8 < 10` by default |
| 2 | "tunnels ≫ rooms ⇒ bounding spheres" | rooms hit 40/40 too — fixing tunnels alone changes nothing |
| 3 | "widen the sampler and rooms will drop" | they did not — `RoomSpacing` was **42**, not the 80 I computed with |
| 4 | — | the fixture is *deliberately* saturated; `0 proved` is its correct answer |
| 5 | "production defaults will prove tiles" | **6 of 40**, and now tunnels really are alone (32 vs 21) |
### The tunnel disjunction — the deferred piece, now justified by its own number
The breakdown finally isolated it: of the 34 unproved tiles, **32 were blocked by a tunnel and 21 by
a room**, so ≥13 were blocked by tunnels *alone*. That is what made the deferred work worth doing, and
it is why it was deferred until now rather than guessed at three runs ago.
A primitive now fails to matter under a **disjunction**:
> it misses its cull entirely **OR** its own SDF stays ≥ `T + K` over the whole box.
Each branch wins on a different class, and the arithmetic says which:
- **rooms** — cull rejects at `Rmax + 3K`, threshold only at `Rmax + T + K`. Cull is strictly tighter,
so rooms keep the cull alone. Not an omission; a calculation.
- **tunnels** — the cull is the capsule's *bounding sphere*: radius ~107 for a 200-long tube of
radius 7. The threshold uses the true distance to the segment. Order of magnitude.
**The bound is exact, not cautious.** `TaperedCapsule` was *read* (`Dist(P, ClosestOnSegment)
Lerp(Ra,Rb,t)`), so `SDF ≥ dist(P, segment) max(Ra,Rb)`; and `dist(box, segment) ≥ dist(centre,
segment) half-diagonal` by triangle inequality. `VF_DistPointSegment` is written locally rather than
taken from `FMath` — five lines, and "I think that function does that" is not good enough under a
correctness bound.
### ⚠️ `Identity` CHANGED MEANING, and that is the one real debt this creates
It used to mean "`Sdf` stays `FLT_MAX`". It now means "`Sdf ≥ T`", with
`T = max(3·SDFBlendRadius, WormNetworkRange)`. That is only sound because all three consumers of the
SDF channel were **read one by one**, not assumed:
| consumer | threshold | verified at |
|---|---|---|
| `FSdfConvertOp::Eval` | `Sdf >= Blend`, and `BuildTunnelNetworkStack` passes `MakeSdfCarve(P.SDFBlendRadius, …)` ⇒ **K** | call site |
| the twelve modifiers | `VF_NearCaveSurface` ⇒ **3K** | its body |
| `FWormFieldSource::Eval` | `CaveSDF >= WormNetworkRange` ⇒ **WormNetworkRange** | its body |
Plus the one that could have bitten: `FCaveTerraceMod` re-probes the SDF at **Z±1, outside the box** —
but its `VF_NearCaveSurface` gate is line 13 and the probes are lines 2829. Gated first, so a gate
that is false everywhere never emits a probe. Checked, not assumed.
**Any new consumer of the `Sdf` channel must have a threshold ≤ `T` or be added to that `max`.** An
operator reading `Sdf < 100` would see false `Identity` verdicts, i.e. tiles with no geometry **and no
collision**. That warning is written at the site, in the function you land in when you add one.
### Why ` K` is enough for any number of primitives
`SmoothMin(A,B,K) = min(A,B) H³K/6` with `H = max(K|AB|,0)/K`. The penalty is **exactly zero**
once `|AB| ≥ K`, so the running minimum saturates at `K` below the smallest term — it cannot descend
further, because at that distance `H = 0` and subsequent folds return it unchanged. Hence
`Sdf ≥ min_i(SDF_i) K` for **any** N, not ` N·K/6`. Without that observation the slack would scale
with the ~88 tunnels in the cache and the criterion would be worthless.
### Ready to build. Compile-error spots
1. `VF_DistPointSegment` — new helper in the anonymous namespace, next to `VF_NearCaveSurface`
(i.e. well above the end-of-namespace marker).
2. `K` / `T` / `TunnelClear` / `QCenter` / `BoxHalfDiag` are new locals in `EffectOverBox`; the tunnel
loop variable is `Tn` **specifically so it does not shadow `T`**. Braces and parens verified balanced.
3. `(QMax - QMin).Size()` returns a double under UE5's `FVector` — cast to float.
### What to read
`[production defaults] Box verdicts` — **6 is the number to beat.** The disjunction should raise it;
≥13 tiles were tunnel-only blocked, so somewhere near 19 of 40 is the expectation. And
`[dense fixture]` **must stay at 0** — if the dense world starts proving tiles, the disjunction is
wrong somewhere and the brute force is the thing that will say so.
## 2026-07-29 — Jahni: "are you in a loop?" **Yes.** The dominant term was a constant I set on day one and never measured.
11 of 40 proved, 0 violations — real, but Jahni called it: *still wrong, re-read the original and how
you're doing it.* He was right, and the re-read found it in one line.
### The loop
`PerlinAbsBound = 2.0`, chosen in the very first commit of the spatial `EffectOverBox` and never
revisited. The warp dilation is `CaveWarpStrength · VOXEL_NOISE_SCALE · PerlinAbsBound`, and
**`CaveWarpStrength = 8.0` by default** (the fixture does not override it):
```
dilation = 8 × 1.25 × 2.0 = 20 voxels, applied ±, on every axis
tile = 10 voxels → query box 50 voxels per side → 125× the tile's volume
BoxHalfDiag = 43.3
```
So the tunnel test I described as "an order of magnitude tighter" actually required
`DistToAxis ≥ 28 + 43.3 + 7 = 78`, against a bounding-sphere cull that rejects at ~107. **27 %
tighter, not 10×** — and the run reported exactly that: tunnels reaching went 78.0 → 58.5, a 25 %
cut. The instrument told me the truth and I credited it to the tunnels.
Four rounds of work — the worm, the columns, the sampler, the tunnel disjunction — every one of them
tightening a term while **the term nobody measured stayed 125× too big**. Each round was individually
correct and the loop was still real: *I kept instrumenting the thing I had just changed, and never
instrumented the thing I had assumed.*
### What re-reading the original actually showed
`BuildChunkCache` is called — in `GetDensityWithParams`, in `FRoomGraphSource::Eval`, everywhere —
with `Expansion = CaveWarpStrength + 2.0f`. That is the shipped, working, years-old code, and it is
only correct if `|Perlin3D| · VOXEL_NOISE_SCALE ≤ CaveWarpStrength`, i.e. **`|Perlin3D| ≤ 0.8`**.
**The whole plugin has always bet on 0.8. I chose 2.0 — 2.5× more conservative than the assumption
the cache's own correctness already rests on — and then optimised around my own choice for three
builds.** That is the answer to "are you in a loop", and it is not a subtle one.
### The corrected bound, derived rather than guessed
Read out of `GradDot`: `u` and `v` are always **two distinct** components of the corner offset —
checked on all four hash branches, not assumed. Then per axis, splitting the eight corners by `i`:
```
Σ_c w_c·|dx_c| = (1su)·fx + su·(1fx) su = Fade(fx)
≤ 0.5 (max at fx = 0.5; 0.302 at fx = 0.25 and 0.75)
|Perlin3D| ≤ Σ_c w_c(|a_c|+|b_c|) ≤ S_x + S_y + S_z ≤ 1.5
```
**1.5, provable, no case analysis on the hashes.** (The true max is lower still — only two of three
axes appear per corner — and the classical `√3/2 ≈ 0.87` depends on the gradient set, so it is not
leaned on.) Dilation drops 20 → 15 voxels. That is a 25 % cut in the dominant term, honestly
obtained, and it is *not* the end of the story.
### The instrument that should have existed from the first commit
`EffectOverBox` now also computes every room/tunnel test **with the warp dilation set to zero**, and
reports both. `HitRooms HitRoomsNoWarp` is exactly the blocking caused by my own box rather than by
geometry. The line reads:
```
WARP SHARE: the query box is dilated by ±N voxels per axis; with that dilation set to ZERO
the same tests would keep only X rooms and Y tunnels.
```
If that gap dominates, the next move is the warp bound — **not** the primitives. That sentence is in
the output so the next person (me, next run) cannot repeat this.
### The other thing that was wrong: the diagnostics had become narrative
The report was printing **hardcoded numbers from previous runs** next to live ones — "the first build
reported 0 proved of 40", "32 of 34 tiles vs 21 for rooms" — while the live figures said 21 rooms of
28 tiles. A reader cannot tell which is which, and I wrote every line of it. Diagnostics now report
**this run only**; the history lives here, in the log, where it belongs.
### Ready to build. Compile-error spots
1. `PerlinAbsBound` 2.0 → 1.5 (one constant, big blast radius on the numbers, none on compilation).
2. `FBoxState` gained `HitRoomsNoWarp` / `HitTunnelsNoWarp` / `WarpDilation`; mirrored on
`VoxelDensityOps::FRoomBoxDiagnostic` and copied in `GetLastRoomBoxDiagnostic`.
3. `NWMin` / `NWMax` / `NoWarpHalfDiag` are new locals in `EffectOverBox`, declared before use
(verified mechanically, along with brace/paren balance).
4. The per-class report line went from 10 to 13 format specifiers and 13 arguments — counted, and the
previous round's arg-list mismatch is exactly why it was counted.
### What to read, and in this order
1. **`WARP SHARE`** — the number that has been invisible all along. `rooms 1.1 → 0.4` would mean most
of the remaining block is my dilation and the warp bound is the whole job; `1.1 → 1.0` would mean
the geometry really is that dense and the dilation was never the point after the 1.5 fix.
2. `[production defaults] Box verdicts` — **11 is the number to beat.**
3. `[dense fixture]` must still be 0.
## 2026-07-29 — WARP SHARE measured: **over half the blocking is my box.** And two corrections.
The instrument that should have existed from commit one, on its first run:
```
[production defaults] 0.9 of 1.1 rooms reach, 2.4 of 3.3 tunnels reach
WARP SHARE: dilated ±15.0 voxels/axis; with dilation ZERO
the same tests keep only 0.4 rooms and 1.1 tunnels
[dense fixture] 5.0 rooms / 46.3 tunnels → 2.1 / 22.4 with zero dilation
```
**Rooms 0.9 → 0.4, tunnels 2.4 → 1.1.** Better than half of everything still blocking a verdict is
caused by the query-box dilation rather than by cave geometry. Hypothesis confirmed, and the number
is now permanent output.
### ✋ CORRECTION — the justification I gave last entry was wrong
I wrote that `BuildChunkCache`'s `Expansion = CaveWarpStrength + 2` means *"the whole plugin has
always bet on `|Perlin3D| ≤ 0.8`"*, and used that to argue 2.0 was needlessly conservative. **That is
false, and I should have read six lines further:**
```cpp
const bool bNeedRebuild = … || WarpedX < CachedSMinX || WarpedX > CachedSMaxX || …;
```
The cache **rebuilds when the warped query leaves the box.** So that expansion is a
*rebuild-frequency heuristic*, not a correctness bound — nothing anywhere bets on 0.8, and the cache
is self-correcting no matter how far the warp reaches. The 2.0 → 1.5 change stands on its own
derivation and needs no support from that claim. Sixth premise this refactor that reversed on being
checked, and this one I asserted **in the same entry where I diagnosed the habit.**
### ✋ NEGATIVE RESULT — "shift the box by the local warp" does **not** work here
The obvious next move is: the warp is smooth and low-frequency (`WF = 0.015`, wavelength ~67 voxels)
and the tile is only 10 voxels, so evaluate the warp at the box centre, **shift** the box by it, and
dilate only by the *variation* across the box. Worked through before writing it, and it loses:
A rigorous per-axis Lipschitz bound for this `Perlin3D`:
```
∂V/∂fx = [Σ_c ∂W_c/∂su · G_c]·Fade'(fx) + Σ_c W_c·∂G_c/∂fx
|Fade'| ≤ 15/8 = 1.875
|Σ ∂W/∂su · G| ≤ 4 (difference of two convex combinations of values in [2,2])
|∂G/∂fx| ≤ 1 (u and v are distinct axes, so at most one of them is x)
⇒ |∂V/∂fx| ≤ 4·1.875 + 1 = 8.5 per unit noise cell
```
Half-box in noise units: `WF·5 = 0.075` for x and y, `WF·5/1.35 = 0.056` for z ⇒ 0.206 total.
So the local variation bound is `8.5 × 0.206 = 1.75`, against a **global** range bound of **1.5**.
**The local bound is worse than the global one.** The crude Lipschitz constant times the box's extent
in noise space already exceeds the whole range of the function. Recorded so nobody spends a build
discovering it — the idea is sound in principle and simply does not pay at `WF = 0.015` with a
Lipschitz constant this loose.
### Where that leaves the warp term
The measured ceiling for *all* remaining warp work is "about half the current blocking", i.e.
somewhere around 11 → 16-20 tiles of 40. The only route left is tightening the sup of `|Perlin3D|`
from the proved 1.5 toward its true value:
```
|Perlin| ≤ S_x + S_y + S_z Σ_c w_c·min(|dx_c|,|dy_c|,|dz_c|)
spot values: 1.000 at (0.5,0.5,0.5) · 1.003 at (0.9,0.5,0.5) · 0.614 at (0.9,0.9,0.5)
```
so the true sup looks like **≈ 1.01.1**, worth ~27 % off the dilation. But "spot-checked on a grid"
is not a proof, and a sup proved wrong here is a tile with no geometry and **no collision**. That is
analysis work with real hole-risk for a partial share of a bounded prize — a judgement call about
appetite, not a technical unknown, so it goes to Jahni rather than getting done on my own initiative.
### State: T1.d is delivered and verified
**11 of 40 tiles (27.5 %) proved AllSolid at production defaults, 14641 voxels brute-forced,
0 violations.** The dense fixture correctly proves nothing. Every verdict is checked voxel by voxel,
so the risk of the whole feature is bounded by a test that runs on every build.
## 2026-07-29 — T1.d banked at 11/40. VerticalShafts: the connector branch never tested a connector.
Jahni's call: bank the tunnel result and take VerticalShafts rather than squeeze the Perlin sup.
Right call — the shaft fix is a clearly-scoped defect with no hole-risk maths, on an archetype that
was getting nothing.
### The defect, and it is stated in its own comment
`FShaftFieldSource::EffectOverBox` ends with a "connectors" branch that never looks at a connector:
```cpp
const FBox ConnBox = VoxelBox.ExpandBy(Spacing * 1.6f + Pad);
for (cells in ConnBox)
if (RollShaft(cx, cy, Sh)) { return EVoxelOpEffect::CarveOnly; } // prudent
```
It returns `CarveOnly` because a shaft **exists** somewhere in the neighbourhood. Against the
defaults — `ShaftSpacing = 55`, `ShaftDensity = 0.6`, `Pad ≈ 11 + ExtraReach` — that expanded box
spans roughly 4×4 cells and therefore ~10 shafts. **The condition is true essentially everywhere**,
which is exactly the reported `0 proved of 60`. Conservative, never wrong, and completely sterile —
the same shape as the worm's unconditional `CarveOnly`, one archetype over.
### The fix: rebuild the connectors the way `GetCells` does, and test the real capsule
Two things had to be right, and both were checked in the source rather than assumed:
- **The enumeration is a superset.** `Eval` reads connectors from the 3×3 neighbourhood of *its
query's* cell. So any pair visible from a point in the box has both shafts inside the 3×3 of some
cell the box touches ⇒ both lie in `[box cells] ± 1`, which is precisely the range swept. Pairs
that no query ever sees may be produced — extra `CarveOnly`, never a hole.
- **The pair order matches, so the hash matches.** `VoxelHash::Pair(A…, B…)` is fed in insertion
order, and `GetCells` inserts over `(dy, dx)` — row-major. The sweep here is `(cy, cx)`, the same
order, and row-major order restricted to a sub-grid preserves the relative order of any two cells.
So the same pair gets the same hash **without assuming `Pair()` is symmetric** — which was never
verified and now does not need to be.
The capsule test splits the axes because a connector is a **horizontal** capsule at height `Zc`:
Z is exact (`RMinZ > Zc + Reach || RMaxZ < Zc - Reach` ⇒ skip), XY uses point-to-segment from the box
centre minus the XY half-diagonal. Far tighter than a 3-D half-diagonal, which is what the tunnel
version had to settle for.
### And the same sampler trap, caught this time before the build
The shaft test drew tile XY from `RandRange(-6,6)*8` = **±48 voxels, against `ShaftSpacing = 55`** —
less than one period of the pattern. Identical in kind to the ±32-vs-80 bug that cost the tunnel test
three runs. Widened to ±440 (8 periods), and the report now prints its own extent in units of
`ShaftSpacing`, so it cannot go unnoticed again.
**The safety net was already there and is untouched:** this test brute-forces the full lattice of
every proved tile against `EvalMC`, both hypotheses, and `AddError`s on the first violation. So the
fix is checked by construction — if the superset argument or the hash-order argument is wrong, the
existing assertion fails rather than a player falling through the floor.
### Ready to build. Compile-error spots
1. `VF_DistPointSegment2D` — new helper next to `VF_NearCaveSurface` in the anonymous namespace.
2. Both new helpers now use `KINDA_SMALL_NUMBER`, matching the ~6 existing uses in the plugin; they
were briefly written with the `UE_`-prefixed spelling, which nothing else here uses.
3. The shaft test hoists `SpanCells` / `SpanVoxels` **outside** the tile loop — the report needs
them, and `Extent` is loop-local. (That exact scoping slip happened in the tunnel test two rounds
ago; caught here before the build rather than after.)
4. Format string is 6 specifiers / 6 arguments — counted.
### What to read
`Box verdicts over 60 VerticalShafts tiles` — **0 is the number to beat**, and `%d violations` must
stay 0. If it is still 0 proved, the warning now says what to check *first*: `ExtraReach` inflates
both remaining tests, so its value against `ShaftMaxRadius` is the thing to look at before touching
either test — not a re-derivation from scratch.
## 2026-07-29 — clean stopping point. Codex joins; the production-observability gap is named.
### The finding that ends this session
Jahni built a world, looked at it, and said: *"I don't know if it dropped any meshing? but it looks
alright by the eye."*
That sentence is the honest state of this entire refactor. **Everything proved so far was proved in
an automation harness, on 40 sampled tiles.** In the running game, tile-skipping is **unobservable**:
- `grep INC_DWORD_STAT Source/` → **nothing**. The plugin has zero stat counters.
- *Skipped correctly* and *skipped nothing* render identically, so no visual check can separate them.
- And `bUseOperatorStack` defaults **false** with no strate ticked, so unless Jahni ticked one, the
honest answer is "nothing new was dropped, by construction".
This is the same failure this session spent six rounds learning to avoid — *coverage is a number, not
a boolean* — applied to production instead of to a test. The tests got that discipline; the game
never did.
**Interim answer that needs no code:** the trace scopes `VoxelForge_ClassifyTile` and
`VoxelForge_GenerateMesh` already exist at the site (`AVoxelWorld::GenerateTileResult`,
VoxelWorld.cpp ~1501). In Unreal Insights, a skipped tile is a `ClassifyTile` with no `GenerateMesh`
after it. ⚠️ But the comment there records that **~84 % of tiles were already being rejected** by the
hand-written SurfaceWorld/bedrock paths long before the op stack existed, so surface skips drown the
cave ones — the number only means something **underground in an opted-in `TunnelNetwork` strate**.
### `CODEX-TASK-001-tile-skip-stats.md`
Written and committed. A `stat VoxelForge` group with `TilesClassified / TilesSkippedAllSolid /
TilesSkippedAllAir / TilesMeshed`. Solid and air are split deliberately: **cave archetypes prove
`AllSolid`**, so that counter is the one that answers whether the op-stack work did anything real.
Its deliverable is a before/after that is **the production proof of T1.d**, which does not exist
today: no strate opted in ⇒ `TilesSkippedAllSolid` underground reads 0; tick `bUseOperatorStack` on
one `TunnelNetwork` strate, fly the same route ⇒ non-zero.
The spec carries the invariants rather than just the task, because that is the whole point of a spec
here: `bTrivialEmpty` decides whether a tile has **collision**, the five-clause gate is load-bearing,
and `GenerateTileResult` runs on **worker threads** — so a plain `static int32++` is a data race and
`INC_DWORD_STAT` is not.
### Working arrangement, from now on
**Codex (Model Luna, xHigh) handles most of the coding; I orchestrate** — read the code, decide what
to do, write precise specs, review what comes back **against the code rather than its description**,
and own the docs and the measurements. Recorded in the handoff (`## How we work now`) and in memory.
### Git
`experimental` is now **pushed and tracked** (`origin/experimental`, Jahni's Gitea). `main` stays
pinned at the known-good commit. The old flat "never push" rule was corrected in both places that
stated it — with the caveat that matters: **a pushed commit is not a "verified green" marker**, since
this branch carries unbuilt work by design. `OPSTACK-PROGRESS.md` remains the only record of what was
actually built.
### Still open, in priority order
1. **`CODEX-TASK-001`** — the production observability gap. Priority, because every claim this
refactor makes is currently harness-only.
2. **Build `e002bd4`** (VerticalShafts connector capsules) and read
`Box verdicts over 60 VerticalShafts tiles`. 0 was the number for the project's whole life.
3. **PERF** — the op path is measurably slower; suspects listed in the handoff. Measure first.
4. The warp squeeze stays **parked** with its ceiling measured and a negative result recorded.
## 2026-08-16 — reviewing CODEX-TASK-001 against the code reversed its acceptance bar
Orchestrator pass over the spec before handing it to Codex, per the rule that a spec here must carry
the invariant and not just the task. Reading `UVoxelGenerator::ClassifyTile` end to end found the
spec's deliverable unmeasurable as written.
### The defect
The spec's baseline was *"with no strate opted in, `TilesSkippedAllSolid` underground stays 0"*.
It cannot. `ClassifyTile` reaches a non-`Mixed` verdict by **two independent routes**:
- **hand-written, predates all of this** — a chunk in a bedrock gap sets `bCanAir = false`
(VoxelGenerator.cpp ~2835); absent a passage or the origin spine the tile resolves **`AllSolid`**.
Plus the SurfaceWorld column scan. Both fire with **no strate ticked at all** — this is precisely
the "~84 % already rejected" note at the call site, which the spec quoted as a warning and then
contradicted in its own acceptance section;
- **operator stack** — the `if (bAnyCave)` block, the only thing T1.d added.
A single lumped counter mixes them, so the before/after Jahni is meant to read would have had a
large non-zero "before" and a slightly larger "after". Unreadable — and the exact failure mode this
project already has a lesson for: *when a zero has several possible causes, give each one its own
number.*
### The fix — site B
Second site: the exit of the `bAnyCave` block (~3009), counters `TilesOpStackSolid` /
`TilesOpStackAir`. The property that makes them the right instrument is stronger than the one they
replace: the cave branch returns `Mixed` outright at `UsesOperatorStackForChunk` (~2809, and again
per chunk of the box at ~2914), so **without an opted-in strate these are zero by construction, not
by observation**. A non-zero reading cannot come from anywhere else.
Also written into the spec: `TilesOpStackSolid ≤ TilesSkippedAllSolid` as a cross-check, the
warning that a swapped solid/air pair reads as a plausible result while proving the wrong thing, and
the note that `ClassifyTile` is `const` on the same workers as site A so the `INC_DWORD_STAT` rule
covers both sites.
`6bd5589`, pushed. **Nothing was built or run** — this is a documentation commit.
### Unchanged and still open
1. `CODEX-TASK-001` — now correct, ready to hand to Codex.
2. `e002bd4` (VerticalShafts connector capsules) — written, **not yet built**. Read
`Box verdicts over 60 VerticalShafts tiles`; 0 is the number to beat, `violations` must stay 0.
3. PERF on the op path; the warp squeeze stays parked.
## 2026-08-16 (b) — PERF: the suspects don't share an archetype, and the A/B needs no code
Orchestration pass on the parked PERF item. Three findings, none of them measured yet — they are
written here so the measurement that follows is aimed rather than exploratory.
### 1. The perf A/B requires NO new code — but it requires task 001 to be readable
`VoxelForge_ClassifyTile` and `VoxelForge_GenerateMesh` already exist at the call site. Since the
world is deterministic, the same seed and the same flight path generate the same tiles, so **two
Insights traces — `bUseOperatorStack` off, then on — are a clean A/B.** No instrument needed.
**But the totals cannot be compared directly.** With the stack on, tiles are *skipped*, so
`GenerateMesh` runs fewer times: a total-time comparison conflates "cheaper per tile" with "fewer
tiles", and those two point in opposite directions. **`TilesMeshed` from `CODEX-TASK-001` is the
denominator that separates them.**
⇒ **001 is a PREREQUISITE for the perf work, not a parallel item.** The handoff listed them as
independent (1 and 3); they are sequential. Order is now 001 → traces → attribution → fix.
### 2. "The op path is slower" is one number over two unrelated cost structures
Per archetype the suspects do not overlap, so a single figure cannot be acted on — the same lumping
error as the tile-skip counter, one level up:
- **SurfaceWorld** — the hashed column memo (below). Nothing to do with dispatch.
- **TunnelNetwork** — **19 virtual calls per voxel**: 16 from `BuildTunnelNetworkStack` (rock, room
graph, carve, the twelve detail mods, the worm) plus 3 from `AppendStructuralPost` (spine, seal,
passage carve). None inlinable, against one monolithic `GetDensityWithParams` the compiler can
inline and vectorise. Plus the known 12× gate re-test (stage B5's deliberate trade).
The measurement must therefore be taken **per archetype, one strate ticked at a time.**
### 3. The column memo hypothesis — `CODEX-TASK-002` written to test it
`FSurfaceColumnSource::GetColumn` uses a **direct-mapped 4096-entry hashed table**; `GSurfColCache`
uses a **direct-indexed box** with a `Computed[]` flag and therefore has no collisions at all.
The premise that makes the difference bite was checked rather than assumed: the mesher pre-samples
with **Z OUTERMOST** (`VoxelMarchingCubesMesher.cpp` ~226), sweeping a full XY plane per Z level, so
every column is revisited ~34 times per tile.
The table's own comment sized it against this and concluded 4096 entries "covers four chunks". **The
gap in that reasoning: a direct-mapped table evicts on collision, not on fullness.** ~1156 columns
in 4096 slots is a load factor of 0.28, at which ~285 columns (~25 %) still share a slot with
another. Those recompute the entire height stack — cliff's four structural resamples included — on
*every* Z plane. ~1156 column computations on the original path vs ~10 000 on the op path: **~9×**,
on the most expensive archetype in the plugin.
**This is a derivation, not a measurement.** Six confident chains in this project have reversed on
an unchecked premise, so `CODEX-TASK-002` adds `ColumnMemoHit` / `ColumnMemoMiss` and **fixes
nothing** — deliberately, so the instrument and the fix cannot land in the same build and make each
other unreadable. A negative result is specified as a real result.
`CODEX-TASK-002-column-memo-thrash.md`. **Nothing was built or run.**
## 2026-08-16 (c) — Codex delivered 001 + 002. Reviewed against the code. READY TO BUILD.
First tandem round actually executed by me rather than handed over: Codex (`gpt-5.6-luna`, xhigh)
ran both specs, I reviewed the diffs against the source rather than against its reports.
### What landed
`stat VoxelForge` — the plugin's **first stat group ever** (`grep INC_DWORD_STAT` used to return
nothing). New `Public/VoxelStats.h` + `Private/VoxelStats.cpp`, eight `DWORD_COUNTER`s:
| counter | site |
|---|---|
| `TilesClassified` / `TilesSkippedAllSolid` / `TilesSkippedAllAir` / `TilesMeshed` | `AVoxelWorld::GenerateTileResult` |
| `TilesOpStackSolid` / `TilesOpStackAir` | `UVoxelGenerator::ClassifyTile`, the `bAnyCave` exit |
| `ColumnMemoHit` / `ColumnMemoMiss` | `FSurfaceColumnSource::GetColumn` |
### The review — what was checked, not what was reported
- **Site A's verdict refactor is contract-identical.** `bTrivialEmpty = (Verdict != Mixed)` where
`Verdict` is the stored return. The gate's five clauses are untouched. That bool decides whether a
tile has **collision**, so this was checked character by character rather than read.
- **Site B increments AFTER the `bCanSolid == bCanAir` bail-out**, and the pair follows `bCanSolid`
the same way the returned enum does — the swap that would compile, look plausible, and prove the
wrong thing did not happen.
- **`GetColumn` is instrumented and NOT fixed**, which was the whole point of specifying 002 that
way. Table size, hash and the full key comparison are byte-identical; miss inside the `if`, hit in
the `else`, neither derived by subtracting from the other.
- **Every `INC_DWORD_STAT` body is braced.** The macro expands to a braced block, so an unbraced
`if` would have left a stray `;` and broken the following `else`.
### The macro arities were VERIFIED, not assumed
This is the plugin's first use of the stats system, so there was no in-repo precedent to match and
the usual "likely compile spot" would have been a guess. Read out of
`UE_5.7/Engine/Source/Runtime/Core/Public/Stats/Stats.h` instead:
- `DECLARE_STATS_GROUP(GroupDesc, GroupId, GroupCat)` — 3 args ✔
- `DECLARE_DWORD_COUNTER_STAT_EXTERN(CounterName, StatId, GroupId, API)` — 4 args ✔
- `DEFINE_STAT(Stat)` ✔ · `INC_DWORD_STAT(Stat)` ✔
- `INC_DWORD_STAT` → **`FThreadStats::AddMessage`**, i.e. per-thread stat packets ⇒ **the
worker-thread invariant holds by mechanism**, not by hope. Both sites are on workers.
- `Stats/Stats.h` includes `CoreGlobals.h` + `CoreTypes.h` itself ⇒ `VoxelStats.h` is self-
sufficient and IWYU-clean as a first include (it is, in `VoxelStats.cpp`).
- `VOXELFORGE_API` needs no include — UBT defines it on the command line.
- `VoxelForge.Build.cs` does not enumerate sources, so no build-script change is needed.
### READY TO BUILD — and what to read afterwards
Remaining compile risk is low and concentrated in `VoxelStats.h`/`.cpp` (identifier mismatch between
the eight declarations and the eight definitions — checked by eye, they match).
Three readings, and **the order matters**:
1. **Baseline, nothing ticked, underground:** `TilesOpStackSolid` / `TilesOpStackAir` must be **0**.
They are zero by construction (the cave branch returns `Mixed` at `UsesOperatorStackForChunk`),
but observe it anyway or the next step proves nothing. `TilesSkippedAllSolid` **will** be non-zero
here — that is the pre-existing bedrock/surface skipping, not a bug.
2. **Tick `bUseOperatorStack` on ONE `TunnelNetwork` strate, same route:** `TilesOpStackSolid` goes
**non-zero**. ← the production proof of T1.d, which has never existed.
3. **A `SurfaceWorld` strate ticked:** report `ColumnMemoHit` **and** `ColumnMemoMiss`, both numbers.
Miss ≈ once per distinct column ⇒ my ~9× thrash derivation is **wrong**, the table is fine, and
the perf cost is the 19-virtual-calls suspect instead. Miss stuck at 2030 % of lookups ⇒
confirmed, and the fix gets its own task with this run as its before.
Also still unbuilt and riding along: `e002bd4` (VerticalShafts connector capsules) —
`Box verdicts over 60 VerticalShafts tiles`, **0 is the number to beat**, `violations` must stay 0.
## 2026-08-16 (d) — while Jahni was away: one bug closed on paper, one prediction sharpened
No build, no measurement available, so this was a reading session. Three results.
### 1. The last compile risk in `eb317d9` is mechanically gone
The one spot I could not rule out by eye was an identifier mismatch between the eight declarations in
`VoxelStats.h` and the eight `DEFINE_STAT`s in `VoxelStats.cpp`. Diffed the extracted symbol lists:
**identical**, and all eight use sites resolve to declared names. That risk is retired, not estimated.
### 2. ⛔ `AUDIT §C2` IS FULLY CLOSED — I was one step from spec'ing a fix for a solved bug
The handoff, the audit and my own memory all listed the live-edit half (`OC_Chunk`, `BM_Chunk`,
`FChunkBiomeCache`) as open. It is not, and has not been for a while. Every per-chunk cache carries
the layout version — `CP_Version`, `OC_Version`, `BM_Version`, `TC_SeenVersion` — and
`FChunkBiomeCache::Invalidate()` exists precisely because the validity box says nothing about the
`FBiomeContext` its cells were classified against; all four `thread_local` instances call it on a
version change. The only two other instances in the tree are **function-local**, built per task, so
they cannot go stale.
**Seventh time a confident premise reversed on reading.** The difference is that this one was checked
*before* the work rather than after: it cost a doc edit instead of a Codex run and a build cycle.
Recorded at `AUDIT-2026-07.md §C2` and struck from the handoff's open list.
### 3. The column-memo prediction is now arithmetic, not a band
`CODEX-TASK-002`'s acceptance said "2030 % of lookups", which was a guess wearing a number. The
inputs are all statically knowable, so they were read instead: `CHUNK_SIZE = 32`, `CellsPerAxis = 32`,
`GridDim = 33`, pre-sample loops over `g ∈ [-1, GridDim]` ⇒ **1225 columns per tile over 35 Z
planes** (the mesher's own "35³ floats" buffer comment confirms the dimension).
1225 keys in 4096 slots = load factor **0.299**; expected single-occupancy slots `4096·np(1-p)^(n-1)`
≈ 908, so **~317 columns (25.9 %) collide** and evict each other every plane. Op path ≈
`1225 + 34×317` ≈ **12 000** column computations per tile against the box's **1225**: **≈ 9.8×**.
⚠️ And the reading instruction changed with it: **compare the ratio of the two hypotheses, not an
absolute percentage.** The overhang and cliff mods call `GetColumn` again at the same XY, which adds
**hits only** — it drags the miss *rate* down without touching the verdict. The ~10× gap between
"confirmed" and "wrong" is what survives that.
### 4. `REVIEW_FINDINGS.md` reassessed — two items are now counter-productive, not merely optional
- `GetDensityWithParams` and `BuildChunkCache` splits: **don't.** The operator stack is *replacing*
the first archetype by archetype, so the split gets deleted and churns the eight bit-for-bit
equivalence tests; and `FRoomGraphSource` deliberately **calls** `BuildChunkCache` rather than
transcribing it, so restructuring forks the thing kept unforked on purpose. Both also sit on the
`§8.10` invariants, where a "behaviour-preserving" change once silently deleted the overhang.
- `EVoxelPassageType` vs `EVoxelPassageStyle`: **judged, leave them.** Not duplicates — one is the
global inter-strate bore shape, the other per-strate descent styling on `FStratePassageConfig`,
with different owners and value sets. Both are `UMETA`-tagged and therefore **serialised into
Jahni's authored strate assets**, so merging them rewrites saved content for cosmetic tidiness.
Recommend closing the item rather than acting on it.
- `GetGenerationParams` is flagged as the safest of the six if any is ever wanted.
Nothing here needs a decision from Jahni; it all needs the same next build.
## 2026-08-16 (e) — ⛔ A REAL BUG: three box-verdict reaches used a bound this file had already refuted
Found by auditing the invariant the handoff calls "the one that can delete collision", extended to
the archetypes it does *not* cover. `CODEX-TASK-003`, written, executed by Codex, reviewed. **Not a
perf change — a correctness change to a box verdict.**
### The finding
This file derives `|Perlin3D| ≤ 1.5` rigorously (the `GradDot` two-distinct-axes form and the
per-axis weighted bound of 0.5), exposes it as `PerlinAbsBound`, uses it correctly for the tunnel
warp dilation — and states outright that the noise header's "~[-1,1]" is an observation, not a
theorem, and that a box verdict resting on one is a hole.
**Three `ExtraReach` formulas in the same file assumed `sup|FBM| ≤ 1.0`**, each with the comment
"FBM ∈ [-1,1]" — the exact claim disproved 1800 lines above. And `VoxelNoise::FBM` **normalises**
(`return Total / MaxValue`, `VoxelNoise.h` ~272), so `sup|FBM| = sup|Perlin3D|` **exactly**: the
octave sum neither amplifies nor attenuates it. The bound was simply wrong, by 1.5×.
`Identity` from these sources means `Sdf ≥ ExtraReach` over the box; roughness then does
`Sdf += FBM · VOXEL_NOISE_SCALE · Strength`, so soundness needs
`ExtraReach B·1.25·|Rough| ≥ (carve/fill threshold)`:
| archetype | `ExtraReach` (defaults) | threshold | needs `B ≤` | at proved `B = 1.5` |
|---|---|---|---|---|
| **VerticalShafts** (`Rough 3.0`) | 6.75 | carve 2.0 | 1.27 | ⛔ **UNSOUND**, margin 0.875 |
| **Maze** (`Rough 2.0`) | 5.5 | carve 2.0 | 1.40 | ⛔ **UNSOUND**, margin 0.25 |
| **FloatingIslands** (`Rough 4.0`, `K 5.0`) | 16.0 | fill 5.0 + `K/6` | 2.03 | ✅ sound — **by parameter luck**, not construction |
Break-even roughness for the two carve archetypes is `1.6`; they ship at **3.0** and **2.0**.
### How alarmed to be — stated honestly, because overstating this would be its own failure
**Nothing in the running game was ever affected**: no strate has `bUseOperatorStack` ticked. The
brute-force tile scans report 0 violations, but they **sample**, and for shafts they were sampling a
source that proved **zero** tiles until `e002bd4` — so that path had never been exercised at all.
The empirical sup of this Perlin is ~1.01.1, *below* the 1.27 the shafts needed, which is why
nothing surfaced. **The defect is that the verdict rested on an unproved bound** — precisely the
standard this codebase already adopted and wrote down.
⚠️ And note what made it findable: `e002bd4` (still unbuilt) is what first lets the shaft source
return `Identity` at all. It converted a latent unsoundness into a reachable one.
### The fix, and why it is safe
`PerlinAbsBound` hoisted to file scope as `VF_PerlinAbsBound` (**one** definition, not a class-static
plus three implicit `1.0`s — the same "one definition, not two kept in sync" rule that produced
`VF_BuildOpStackForChunk`), and multiplied into all three reaches. The three lying comments now state
the real justification.
**It cannot change density by one bit.** `ExtraReach` is read *only* inside `EffectOverBox` — every
other occurrence in the file is a comment or the `float ExtraReach;` member declaration. Verified
mechanically after the edit: no `Eval` / `GetCells` / `GetCellsAt` line appears in the diff. The
eight bit-for-bit equivalence tests are therefore unaffected by construction, not by hope.
The direction is strictly conservative: larger reach ⇒ more `CarveOnly`, fewer `Identity` ⇒ *fewer*
tiles proved. It can only cost CPU.
New values at defaults: shafts **6.75 → 8.625** (+28 %), maze **5.5 → 6.75** (+23 %), islands
**16.0 → 18.5** (+16 %).
### ⚠️ What to expect in the build, so a correct result is not misread as a regression
**The Maze and VerticalShafts box-verdict lines may prove FEWER tiles than before. That is the
correct outcome and the price of a sound bound — do not "fix" it.** Record the before/after.
`violations` must stay 0, as always. The eight equivalence tests must stay green; if any of them
moves, the change touched density and the diff is wrong.
### Process note — Codex refused the first run, correctly
The first attempt stopped without editing: my spec wrote `VF_PerlinAbsBound` in its code snippet and
`PerlinAbsBound` in its prose, and it asked which was intended rather than picking one. That is
exactly the behaviour the spec template asks for ("if the code contradicts the spec, stop and say so
rather than guessing"), and it is worth recording that it works — the cost was one clarification
instead of a plausible-looking wrong rename.
## 2026-08-16 (f) — the box-verdict audit, finished: 28 ops swept, a second class of bug found
Having found the `ExtraReach` bug by auditing one archetype, I swept **all 28 `EffectOverBox`
implementations** rather than stopping at the one that paid. Result: one more real defect, three
false alarms worth recording so nobody re-chases them, and a clean bill for the rest.
### ⛔ FOUND — `Min > Max` radius envelope (`CODEX-TASK-004`)
Three ops roll a primitive radius as `FMath::Lerp(MinRadius, MaxRadius, hash01)`. `Lerp` with
`t ∈ [0,1]` lands in `[min(A,B), max(A,B)]` — it does **not** require `A ≤ B`. Each op's
`EffectOverBox` then sweeps a range of lattice cells padded by the largest radius a cell could hold;
a pad below the true maximum means cells are **never examined**, so the op reports `Identity` for a
box its own `Eval` will fill or carve.
| op | pad | |
|---|---|---|
| `FGridColumnMod` ~1086 | `Max(MaxRadius, 0)` | ⛔ exposed |
| `FShaftFieldSource` ~1436 | `Max(ShaftMaxRadius, ConnectorRadius)` | ⛔ exposed |
| `FIslandBlobSource` ~1803 | `FMath::Max(IslandMinRadius, IslandMaxRadius)` | ✅ **already correct** |
**The third is the whole argument.** Someone hit this exact concern writing the island source and
guarded it; the other two shipped without. Fixed to `FMath::Max3(...)` — a spelling already used in
this file (~2481) and in `VoxelCaveMorphology.cpp`.
**Honest severity:** shipped defaults are correctly ordered (`2/5`, `2/7`, `5/11`), so nothing is
broken out of the box, and **this fix is a no-op at defaults — that is its acceptance signal.** It
needs a mis-ordered asset value, which nothing prevents: `ClampMin` is a per-property floor and
Unreal cannot express "≤ that other property". `ColumnMinRadius` is also settable per-room via
`UVoxelTerrainOpDefinition`. What makes five lines worth it is the failure *mode*: `Eval` still draws
the fat column perfectly, so every meshed tile looks right and only the **skipped** ones are missing
geometry and collision — invisible, and maddening to diagnose.
⚠️ Recorded in the spec because it is the tempting wrong fix: **do not normalise the params.**
Swapping the endpoints maps the same hash to a different radius, changing generated geometry and
breaking the eight equivalence tests. Only the *bound* may become conservative; `Eval` stays
byte-identical. Same rule as task 003.
### ✅ CHECKED AND SOUND — do not re-audit these
- **`FPassageCarveOp`** — its `Eval` reads `EvaluateModifierSDF` while its box guard asks
`AnyPassageNearBox`; the differing nouns look like a set mismatch and are not — **both iterate the
same `Passages` array**, "Modifier" is a legacy name. And the guard is the *same function* the
hand-written `ClassifyTile` uses, so there is one definition, not two.
- **The passage bounding sphere** — `BoundRadius = maxDistFromCentre + Radius + 4.0f`, and that `+4`
**is** `PASSAGE_BLEND_RADIUS`, so "the bound already includes the blend" is true rather than
assumed. Its per-point radii are `RadiusAt(t) = Lerp(Mouth, Mid, Sin(t·π))` with `Sin ∈ [0,1]`, so
they never exceed `Passage.Radius = max(Mouth, Mid)` — the one place the `Min > Max` class *could*
have bitten a shipped default, and it does not.
- **`FOriginSpineOp` / `FBoundarySealOp`** — Z-band plus XY-circle tests against named reaches; both
fail safe to `CarveOnly`/`FillOnly`.
- **`FShaftLedgeMod`** — returns `FillOnly` unconditionally whenever ledges are configured, and its
`Identity` case is exactly the early-out its `Eval` takes. Conservative by construction.
- **`FCaveArchMod` / `FDomeMod`** — they *also* roll `Lerp(Min, Max, hash)` radii, but their
`Identity` comes only from `VF_NoCaveOverBox` (cave-surface proximity) or the feature being off.
**No radius envelope in their verdict ⇒ not exposed.** Checked precisely because they matched the
pattern on a grep.
- `FConstantFieldSource`, `FSlabVoidSource`, `FSurfaceColumnSource` — never return `Identity` at all.
### The shape of both bugs, worth naming
Task 003 and task 004 are the same mistake twice: **a box verdict's bound was derived from the
parameter that reads like the maximum rather than from the actual supremum of what `Eval` produces.**
Once it was `sup|FBM|` (assumed 1.0, proved 1.5); once it was `max radius` (assumed `MaxRadius`,
actually `max(Min, Max)`). Both times a correct instance of the same reasoning existed **elsewhere in
the same file** — `VF_PerlinAbsBound` for the first, `FIslandBlobSource` for the second.
⇒ When adding a bound, the question is not "what is the max parameter" but **"what is the supremum
of the thing `Eval` can actually produce, and where in this file has that already been worked out?"**
## 2026-08-16 (g) — the sampler bug was fixed twice and never as a CLASS. Three more instances.
`CODEX-TASK-005`. Test-only; no non-test file changed.
The handoff records: *"A sampler must cover at least one period of what it samples"* — found in the
tunnel test (±32 vs `RoomSpacing 80`) and again in the shaft test (±48 vs `ShaftSpacing 55`). Both
were widened to ±440 and taught to print their extent in periods. **The fix was applied where the
bug was noticed, never to the class.** Three box-verdict tile scans still ran the original
`RandRange(-6, 6) * Extent` with `Step 1, Cells 8` ⇒ **±48 voxels**:
| test | half-extent | period (default — verified NOT overridden by the fixture) | coverage |
|---|---|---|---|
| Island | ±48 | `IslandSpacing` **95** | **0.51 periods** — worse than either bug already fixed |
| Slab | ±48 | `ColumnSpacing` **60** | **0.80 periods** |
| Maze | ±48 | `CellSize` **40** | 1.20 periods (marginal) |
**Why this mattered *today* rather than in general:** the two commits immediately before it changed
the very verdicts these tests guard — `7dbdf51` touches **islands** and maze, `eaa44bf` touches
**slab columns**. The tests that would catch a mistake in those fixes were sampling half a lattice
cell. Widening them before the build is what makes that build's green mean something.
### The rule the two fixed tests already encoded, now written down
`Extent = Step · Cells = 8`, so `SpanVoxels = SpanCells · 8`. For **8 periods** of half-extent:
**`SpanCells` = the lattice spacing.** That is where the shaft test's `55` comes from — it equals
`ShaftSpacing`, and it is not a coincidence. Applied: Island `95`, Slab `60`, Maze `40`, each giving
exactly 8.0 periods, each printing the ratio **computed live from the params struct** so a future
narrowing is visible instead of silent.
### What was deliberately NOT done
- **No assertion, tolerance, `AddError` or brute-force loop touched.** Every proved tile is still
verified voxel by voxel; a wider sampler producing a false verdict fails exactly as before. This
changes what the measurement *looks at*, never what it *demands*.
- **No seed, `Step`, `Cells` or tile count changed** — a moved seed makes the before/after
incomparable, which is the only reason to do this now rather than later.
- The ratio is never a hardcoded "8.0" in a format string. That would be a success message asserting
coverage while measuring nothing — a named failure mode here.
### What to read after the build
The proved counts **will move**; that is the point, not a regression. **`NumUnsound` / `violations`
must stay 0 in all three.** If one goes non-zero, the wider sampler has found a real hole the narrow
one was hiding — in which case this task paid for itself on its first run, and the number must be
reported rather than tuned away.
## 2026-08-16 (h) — ⚠️ CORRECTION: the flag is ON in the game. Today's two fixes were LIVE, not latent.
Jahni, on being told the op stack was "switched off": **"the data assets in game have the switch on."**
The handoff said *"No strate asset has the box ticked — that is my call and I still haven't made it."*
That sentence was stale, and I reasoned from it all session. **Every severity assessment written
today under "nothing in the running game is affected" is wrong**, including two commit messages that
say so in their body (`7dbdf51`, `eaa44bf`). Those are pushed and stay as written; this entry is the
correction of record.
### Corrected severity
| fix | as filed today | actually |
|---|---|---|
| `7dbdf51``ExtraReach` used `sup\|FBM\| = 1.0`, proved bound 1.5 | "latent, nothing affected" | **live in the shipped game.** VerticalShafts needed `B ≤ 1.27`, Maze `B ≤ 1.40`. Whether a hole ever *manifested* rests on this Perlin's true sup (~1.01.1 empirically) — i.e. **it did not fail because the empirical sup happened to stay under the margin**, not because the code was right. |
| `eaa44bf` — cell sweep padded by `MaxRadius`, envelope is `max(Min, Max)` | "no-op at defaults" | **still a no-op at correctly ordered values — but now worth CHECKING the real assets.** If any strate has `ColumnMinRadius > ColumnMaxRadius` or `ShaftMinRadius > ShaftMaxRadius`, that asset **was** producing tiles with no geometry and no collision. |
**ACTION FOR JAHNI, before or with the next build:** open the strate assets and confirm
`ColumnMinRadius ≤ ColumnMaxRadius` and `ShaftMinRadius ≤ ShaftMaxRadius`. If either is inverted,
`eaa44bf` fixed a live hole and the before/after is visible. `IslandMinRadius/MaxRadius` was already
guarded and is not at risk.
Also re-prioritised by this: **the perf regression is a shipped regression**, not a lab curiosity.
`CODEX-TASK-001` / `-002` stop being "prove the refactor worked" and become "diagnose a live
slowdown". Same build, higher stakes.
### The process failure worth keeping
The flag's state lives in **`.uasset` binary data**, which cannot be grepped from here. So the docs
carried a hand-written claim about it, nobody re-checked it, and it silently went stale — the exact
shape of `AUDIT §C2`'s staleness class, but in prose instead of a cache key.
**Rule added to the handoff: never state the flag's state from memory. Ask, or read it in the
editor.** More generally: *a fact that lives outside the repo cannot be maintained inside the repo*
docs may record what it was **and when it was checked**, never assert it as current.
## 2026-08-16 (i) — Sol-High audit: one real find (fixed), one overstated citation, eight leads
Jahni asked for a second opinion: *"maybe put Codex on Sol High to do an evaluation of our code
cleanliness and efficiency?"* Run read-only as `gpt-5.6-sol` / high, output at
`AUDIT-2026-08-CODEX.md`. It touched nothing else, as instructed.
**Prompting note that mattered:** the real failure mode for a fresh model here is confidently
re-proposing decisions this project already made and documented. The prompt handed it the rejected
list up front (splitting `GetDensityWithParams`, "simplifying" the `FVector` round-trip, collapsing
the twelve-times gate, merging the passage enums, tightening "over-conservative" box bounds, the
§8.10 invariants, the deliberate old/new duplication). **It re-proposed none of them.**
### ✅ VF-05 is real, and it is the worst instance of the class I found twice today
`VoxelCaveMorphology::BuildChunkCache` derives `MaxInfluence`, `RoomZBuffer` and `EvaluateSDF`'s
`Margin` from `MaxRoomRadius` / `TunnelMaxRadius`, while the radii themselves are
`Lerp(Min, Max, hash)` — which yields up to `max(Min, Max)`. Same defect as `CODEX-TASK-004`, and
worse on three counts:
1. it is **TunnelNetwork**, the largest archetype;
2. `BuildChunkCache` is called by **both** density paths (`FRoomGraphSource` calls it rather than
transcribing it), so **this was never an operator-stack bug — it is in the shipped original
code and always has been**;
3. its failure mode is a **window-invariance break** (`ARCHITECTURE §8.4`): whether a room exists
depends on which chunk you queried from. In multiplayer, two peers generate different geometry
from the same seed.
Fixed in `CODEX-TASK-006`, verified against the code before specifying and after: four bound sites
now use `RoomRadiusEnvelope` / `TunnelRadiusEnvelope`, the two duplicated copies of the formula still
match, and **no `Lerp`, placement, hash or `bStore` line changed** — with correctly ordered params
`max(Min,Max) == Max`, so it is bit-identical. **A no-op at correct values is the acceptance signal.**
Notably, Sol independently derived the same *"do not reorder the `Lerp` endpoints, it changes
deterministic assignment"* caveat I had written into task 004. Convergent reasoning on the danger,
from a model that had not seen that task.
### ⚠️ VF-03's corroboration does not exist — and that is the finding about the audit
VF-03 claims *"the test fixture explicitly documents observed cross-world contamination."*
It does not. `VoxelForgeTestFixture.h` documents that the `thread_local` caches exist and flags an
unrelated `TSoftObjectPtr` risk. The underlying concern (caches keyed on chunk/seed/layout but not on
*which generator owns them*) may still be valid — but it must be judged on its own merits.
An audit whose job is to catch unchecked premises contained one. Every row is self-labelled "Verified
by reading"; that label is the author's claim, not an independent check. A reviewer's header saying
so now sits at the top of the file, listing exactly which rows I verified (VF-05 ✅, VF-02 ✅ premise,
VF-03 ⚠️) and which are unverified leads (VF-01, VF-04, VF-06…VF-10).
### VF-02 is worth a deliberate decision from Jahni, not a silent fix
`VoxelWorld.cpp:327` reads *"Timeout after 3 seconds to avoid hanging the editor"*, and `EndPlay`
proceeds when it expires while tasks still hold a raw `this`. **`CLAUDE.md` states the invariant more
strongly than the code implements it** ("EndPlay blocks on `ActiveTaskCount → 0`" — it blocks *with a
deadline*). That is a design trade someone made on purpose (never hang the editor) and it should be
re-affirmed or changed deliberately, not patched by an agent while he is away.
## 2026-08-16 (j) — VF-01 CONFIRMED (crash class). And the deliberate decision to STOP writing code.
Jahni: *"with those new finds, anything you feel you should act on right now?"* Two verifications,
and then a judgement call that the answer is **no more code today**.
### ⛔ VF-01 is real, and it is the most serious thing found today
`UVoxelStrateManager::Initialize` does `StrateLayout.Empty()` **and** `Passages.Empty()` followed by
`Passages.Add()` — it frees and reallocates both arrays. **There is no lock, no barrier and no worker
drain anywhere in that file.** Meanwhile these are read *on mesher workers*:
- `AnyPassageNearBox` (:460) — range-for over `Passages`
- `EvaluateModifierSDF` — indexes `Passages[...]`
- `FindSlotIndexForChunkZ` — iterates `StrateLayout`
all reached from `GetDensityAt` / `ClassifyTile`. And `RegenerateAllChunks()` — which bumps the epoch
— runs **after** `Initialize`, so previous-epoch workers are still live *during* the mutation. The
epoch rejects a finished result; it cannot make a concurrent read of a freed allocation safe.
**This exact class has already bitten this project once and been fixed once:** `DiffLayer.ChunkMods`
is read on mesher workers and written on the game thread, and all access now holds `ModsLock` after a
carve-vs-stream access violation. `StrateLayout` / `Passages` are the same shape with no such guard.
Four `Initialize` call sites, and the dangerous one is **`OnObjectModifiedInEditor` (:309)** — it
fires *automatically* when a strate asset is edited while the world is streaming. That is a routine
action here, not an exotic one.
⚠️ **If Jahni has seen unexplained editor crashes while tweaking strate assets, this is a prime
suspect.** Worth asking before assuming it has never fired.
### VF-10 confirmed real — but Sol missed the conclusion that matters
The ~74-field `FStrateGenerationParams` copy per near-surface sample is real (74 fields counted).
**But it is inherited from the original path**`GetDensityWithParams` does the same copy — so both
paths pay it equally and **it does not explain the op-stack perf regression.** The op stack actually
*improved* it, memoising so eleven detail ops don't each repeat it, and the site's own comment
already records it as a known perf item. A genuine future optimisation for both paths; not an answer
to "why is the op path slower". Sol reported the cost correctly and drew no comparison.
### ⛔ THE DECISION: stop adding code. This is the action, not an absence of one.
Four code changes are stacked unbuilt, and **three of them have "nothing should move" as their
acceptance signal**: `eaa44bf` and `cab8e8f` must be exact no-ops at correctly ordered params, and
`7dbdf51` must move exactly two numbers in one known direction. That is a delicate attribution setup
and it is only readable while it stays clean.
Adding a fifth change — to **streaming lifecycle**, the most timing-sensitive code in the plugin —
would make an unexpected build result unattributable. **The value of a clean, readable build now
exceeds the value of one more fix.**
VF-01's fix is also genuinely Jahni's call, not an agent's:
- **Minimal option** — stop submissions, drain to `ActiveTaskCount == 0`, then `Initialize`, then bump
the epoch, then resume. Reuses the drain `EndPlay` already has. Cost: an editor hitch on every
asset edit, proportional to the in-flight tile queue.
- **Sol's option** — an immutable generation snapshot (seed + layout + passages + resolved defs), one
atomic publish, each task holding a strong ref. Correct and hitch-free; a real refactor.
- **Narrow option** — an `FRWLock` over just `StrateLayout`/`Passages`, exactly the `ModsLock`
precedent. Smallest diff, adds a per-voxel read lock on the hot path — which is precisely the kind
of cost this project insists on measuring before accepting.
Trading a possible crash for a guaranteed hitch, or for hot-path lock traffic, is a product decision.
It waits for him.
## 2026-08-16 (k) — VF-01 FIXED: the drain, chosen over the lock and the snapshot
Jahni: *"well, you select it, I trust you and Sol."* `CODEX-TASK-007`.
**First, a position I updated rather than defended.** An hour earlier I argued for adding no more
code before the build, to keep the three "nothing should move" fixes attributable. That argument was
about the **geometry** numbers — and a streaming-lifecycle change cannot reach an equivalence test or
a box verdict, which are computed from params and hashes, not from *when* `Initialize` runs. The risk
I cited did not apply to this particular change. A lifecycle bug shows up as a hang or a hitch, which
is trivially attributable because nothing else in the stack touches lifecycle.
### Why the drain, and not the other two
| option | verdict |
|---|---|
| **`FRWLock` on the two arrays** (the `ModsLock` shape — smallest diff, exact in-repo precedent) | ⛔ **rejected on timing.** It puts a read lock on the **per-voxel hot path**: `EvaluateModifierSDF` and `FindSlotIndexForChunkZ` run ~43k times per tile. There is an open, unmeasured perf regression and two tasks queued to measure it — adding hot-path lock traffic now would **contaminate the exact measurement they exist to take.** Correct, worst possible timing. |
| **Immutable generation snapshot** (Sol's proposal) | ⛔ **right long-term, too large to improvise.** It refactors `UVoxelStrateManager`'s whole API surface. That is a design conversation, not an agent's unprompted call. |
| **Drain before mutating** | ✅ **chosen.** Zero hot-path cost. Reuses machinery already proven in `EndPlay`. Its only cost — a brief stall — lands exclusively on **human-initiated editor actions** (asset edit, rebuild, seed change) and never during play. |
### What landed
`FScopedGenerationPause` (RAII, in `VoxelWorld.cpp`) raises a new `bGenerationPaused` — **deliberately
not `bShuttingDown`**, which means "tearing down" and would have misled the next reader — then drains
**both** reader populations: `ActiveTaskCount` for chunk tasks and, via a new
`UVoxelContentManager::WaitForDecorationTasks`, the decoration tasks counted by `GActiveDecoTasks`.
Chunk tasks alone were never the whole reader set.
⚠️ **The line that makes it a fix rather than a narrowing: on timeout it does NOT mutate.** It logs an
`Error` naming the function and returns, leaving the world in its previous consistent state. A
timeout that proceeds anyway is exactly what the bug already does. The three dangerous sites —
`RebuildStrates`, `OnObjectModifiedInEditor`, `ChangeSeed` — are structured so `Initialize` is
*unreachable* without the pause. `BeginPlay` (the fourth site) is untouched: no tasks exist yet.
### Verified after the fact, not taken on report
- Four files only; **no density or mesher file in the diff**, so generated terrain cannot move.
- `ShouldAbortWork()` **replaced** three existing `bShuttingDown` checks — 3 replacements, 0 new
check points. `bShuttingDown`'s own semantics are unchanged.
- `EndPlay` and `BeginPlay` untouched (VF-02's 3-second timeout is a separate deliberate decision and
stays Jahni's call).
- The deadline passed to `WaitForDecorationTasks` is **absolute**, documented as such in the header —
and the refactor **removed** the duplicate spin loop from `NotifyShutdown` rather than adding a
second one.
- Deadlock check, which the spec required Codex to perform and report: chunk and decoration tasks
read the generator and `Enqueue` to MPSC queues; **no worker path waits on the game thread**, so a
game-thread drain is bounded. Raising the pause also makes in-flight tasks bail early, so the drain
gets *faster*, not slower.
### Compile-risk spots for the build
1. `WaitForDecorationTasks(double)` declaration / definition / call-site agreement.
2. `FScopedGenerationPause`'s forward declaration and `friend class` access in `VoxelWorld.h`.
3. `ShouldAbortWork()` visibility from inside the `UE::Tasks::Launch` lambda.
### What to check in the editor
Edit a strate asset while the world is streaming. Expect a **brief stall, then the edit applies, and
no crash.** If the `Error` log about a timed-out pause ever appears, the drain deadline (5 s) is too
short for the in-flight queue and that is worth knowing rather than guessing.
## 2026-08-16 (l) — BUILD RESULTS, and the column-memo fix they justified
### The build: 14/14 green, 0 violations anywhere
- **VerticalShafts box verdicts: 0 → 30 of 60**, 0 violations. **Zero was the number for this
project's entire life.** `e002bd4`'s connector capsules delivered — *after* `7dbdf51` widened that
archetype's reach by 28%, which pushed the other way.
- **`[production defaults]` TunnelNetwork: 11 proved, 14641 voxels, 0 violations — byte-identical to
the pre-change run.** That line was set up as the isolated diagnostic for `cab8e8f`: nothing else
touched TunnelNetwork, and the fix is a guaranteed no-op iff room/tunnel `Min ≤ Max`. It did not
move ⇒ **Jahni's assets have correctly ordered ranges; that hole was never being hit.**
- **All eight equivalence tests bit-identical** ⇒ the "bounds only, cannot reach density" claim on
`7dbdf51` / `eaa44bf` / `cab8e8f` is now **tested, not asserted**.
- Maze 27/60, FlatPlain 45/60, CrystalChamber 43/60 (tuned 32/60), Islands 9 AllAir — all at the new
8.0× samplers, so not comparable to earlier runs by design.
### ✅ The column-memo thrash: predicted 14.0%, measured 14.7%
```
Column Memo Hits avg 102,300.84
Column Memo Misses avg 17,683.60 → 14.74%
```
Predicted **14.0%** if thrashing, **1.4%** if not. Second confirmation from a different statistic:
17,683 misses ÷ 2.13 tiles meshed ≈ **8,300 recomputes per tile** against a healthy ~1,225 = **6.8×**
(derived 9.8×; the gap is expected since some tiles are cave and have no columns).
**This is the first quantitative confirmation of a perf hypothesis in this project made *before* the
measurement.** Fixed in `CODEX-TASK-008` Part A: the 4096-slot direct-mapped hashed table is replaced
by a **direct-indexed box**, the scheme `GSurfColCache` has always used — no hash, no collisions,
every column computed once. `ParamsFingerprint` is retained in the box key (its absence was the
shipped bug that silently deleted the overhang; `GSurfColCache` itself omits it, and that part was
deliberately **not** copied).
⚠️ Open hypothesis, falsifiable next build: **`Tiles Meshed` averages only 2.13/frame, which is
plausibly why LOD rings take so long to update.** If misses drop ~10× *and* LOD updates visibly
speed up, the memo was the throughput ceiling. If misses drop and LODs stay slow, they are separate
problems — and we will have cleanly separated them.
### ⚠️ T1.d still does NOT fire in production — and now we will know why
`Tiles Operator Stack Solid` / `Air` **never appeared** in `stat VoxelForge`. Every row that *did*
appear has `Min 1.00`, not `0.00` — rows only render in frames where the counter fires — so site B is
genuinely never reached. The `TilesSkippedAllSolid` of 1.18/frame (~45% of classified tiles) is the
**pre-existing** bedrock/surface skipping, not the prize.
The likely reason that sample missed it: 102,300 column-memo hits are `FSurfaceColumnSource`, i.e.
**SurfaceWorld-dominated flight**. But the cave branch has 13 `return Mixed` paths and guessing which
one fires is exactly the mistake this project keeps paying for. Part B adds six attribution counters
`CaveBailNotOpStack / MixedContent / Params / StackVerdict / Disturbance / NoStack` — one per bail
category. **One underground flight now names the cause outright instead of offering candidates.**
Verified in review: Part B's only edits are brace-expansions of one-line `if`s so a counter fits
before the existing `return`. **Every condition and every returned value is byte-identical** — the
control flow of `ClassifyTile` is untouched, which is the invariant that matters there.
### No crashes while editing assets
Good sign for `49a9959`, not proof — it was a race, and races hide. The stronger signal is the
absence of any `generation pause timed out` log: the drain completes well inside its 5 s deadline.
## 2026-08-16 (m) — ⚠️ T1.d's cause NAMED in one flight. And a retraction about the memo.
### ⛔ RETRACTION: "the column-memo fix made it worse" was a bad read
I compared 14.7% (one flight) against 23.3% (a different flight) and called the fix a regression.
**Invalid.** Jahni: *"it was already like that when I first told you about it, it just is very
random, being between 10 and I'd say at least 30% sometimes, not much the fix Codex just did."*
**The miss rate is strongly location-dependent — roughly 1030% depending on where you fly — and it
was in that band BEFORE the direct-indexed-box port.** Comparing single-flight averages from
different routes measures the route, not the change. The very error this project has a lesson for,
committed against my own instrument.
**The memo port is neither vindicated nor implicated.** A real before/after needs the *same seed
and the same route*, which is exactly the discipline used for the box-verdict numbers and which I
skipped here because a single average looked conclusive. **Do not re-litigate it from these
screenshots.** Parked until it can be measured properly; not urgent.
### 📌 Noted while diagnosing, worth keeping: the port is incomplete vs its reference
`GSurfColCache` is **not one box — it is a 6-box LRU** (`NumBoxes = 6`, `Acquire()` picks/evicts by
`LastUse`), each `Dim = 2·(CHUNK_SIZE+8)+1 = 81` square. My spec said "a direct-indexed box" and
Codex faithfully implemented **one**. Consequence: on a single box, *any* recentre (leaving the XY
footprint, or a `ColumnKey` change) wipes all 6561 cells, where the old hashed table lost exactly one
entry per key mismatch. With several strates or regions interleaved on one worker, that is a
plausible thrash source — **plausible, not measured.** If the memo is revisited, port the LRU too.
### ✅ THE REAL RESULT — Part B named T1.d's blocker on its first flight
Flying over the caves:
```
Tiles Classified 1.77 Tiles Meshed 1.77 ← nothing skipped at all
Cave Bail Not Op Stack 1.42 ← Cave Bail Stack Verdict 0.32
Cave Bail Params 0.03 Cave Bail Mixed Content (never fired)
```
**`CaveBailNotOpStack` accounts for ~80% of cave-branch attempts.** That counter fires on exactly one
condition: `UVoxelStrateManager::UsesOperatorStackForChunk(CC)` returned **false** — either the
initial check or the per-chunk sweep over every chunk the tile's box touches.
⚠️ **So T1.d is blocked by CONFIGURATION, not by code.** All 8 archetypes are in the ported list, so a
`false` means either `bUseOperatorStack` is not ticked on the strate being flown over, or a chunk in
the box belongs to an adjacent strate that has not ticked it — the sweep requires **every** chunk in
the box to agree, so one un-ticked neighbour kills boundary tiles.
That `Cave Bail Stack Verdict = 0.32` is non-zero is the corroborating detail: for those tiles the
flag *was* on for the whole box and the stack built fine — the verdict itself came back `Mixed`.
So the machinery works; it is mostly not being reached.
**Next step is Jahni's, and it is not a code change:** confirm which strate assets actually have
`bUseOperatorStack` ticked. This is the second time today that a fact living in `.uasset` data
drove hours of work in the wrong direction — see 2026-08-16 (h). **The counter did in one flight what
six hours of reasoning could not: it named the cause instead of listing candidates.**
## 2026-08-16 (n) — Sol investigation: it refuted MY hypothesis and caught MY false accusation
Sol High ran as investigator with authority to spawn Luna xHigh workers; two approaches implemented
in isolated git worktrees (`../VF-approach-A`, `../VF-approach-B`). Main tree untouched at `4ba53f2`.
Report: `INVESTIGATION-2026-08-SOL.md`.
### ⛔ Two corrections to things I asserted, both found by reading
**1. My "one un-ticked neighbouring strate kills boundary tiles" hypothesis is WRONG.**
`UsesOperatorStackForChunk` depends **only on `ChunkCoord.Z`**. `ClassifyTile`'s earlier Z loop has
already required the predicate for every cave Z it sampled and required all cave samples to be in one
layout slot — within one slot the flag cannot vary with X or Y. **The later XYZ sweep is redundant,
not over-strict.** Removing it would unlock nothing: boundary tiles still fail the independent
one-slot / generator-type / bit-identical-params guards, which is correct and necessary.
**2. I falsely accused the earlier audit of fabricating evidence.** I wrote in the reviewer header
that VF-03's fixture citation did not exist. **It does**`VoxelForgeTestFixture.h` ~134/146
explicitly documents `CP_UseOpStack` contamination between worlds. I had read only the file's 30-line
header comment and asserted a negative from a partial read. Header corrected.
**VF-03's core claim is CONFIRMED, not plausible:** `GetDensityAt` keys its `thread_local CP_*`
state by `(ChunkCoord, LayoutVersion)` with **no generator/world identity**, and every manager's
version starts at the same value, so a second world on the same worker can inherit the first's
params, `CP_UseOpStack` and stack. Its *breadth* (the `OC_*` / `BM_*` / passage / biome / diff
caches) is still unproven and should be one owner-identity task, not assumed from the old row.
### ⚠️ The bail counter I specced is AMBIGUOUS — my 80% reading was over-confident
`Cave Bail Not Op Stack` is incremented by the **first** predicate check, which runs *before* the
different-slot attribution. So a boundary tile that reaches an un-ticked adjacent cave slot is
counted as `NotOpStack` even though mixed content would have rejected it anyway. **The measured
1.42/1.77 therefore does NOT distinguish "the flown slot is un-ticked" from "a boundary tile met an
un-ticked slot first."** Terrain correctness is unaffected; only the diagnosis is. The asset state
still has to be read in the editor. *A counter that can fire for two reasons is the same defect this
project fixed for `TilesSkippedAllSolid` — and I reintroduced it one layer down.*
### The column memo, restated honestly
The one-box port losing 6561 cells per recenter is **verified structurally**. That it *materially
causes* the 1030% in-game band is **unproven**, and the retracted different-route comparison cannot
support it. Same seed, same route, or no claim.
### Two approaches, both built, recommendation A
| | **A — explicit opt-in + six-box LRU** | **B — code-enforced cutover + 4-way associative memo** |
|---|---|---|
| T1.d | init-time warnings naming every disabled slot; human fixes the asset. No silent behaviour change. | ignores the serialized flag; routes all 8 ported archetypes through the stack. |
| memo | ports the reference 6-box spatial LRU (5 boxes stay warm on a miss) | 1024 sets × 4 ways, exact X/Y+key compare, no bulk clear |
| cost | **~0.79 MiB TLS per worker** (vs ~0.13 now) | ~160 KiB TLS per worker |
| risk | per-worker memory multiplication | **destroys the A/B lever** — the flag can no longer select paths in production |
**Sol recommends A**, and I agree with its reason: the code does **not** justify weakening
`ClassifyTile`, and the flag is the only same-route A/B lever we have — B would delete the very
instrument needed to prove the refactor. Caveat to apply before adopting A: its warning currently
enumerates SurfaceWorld slots too, whose exact-lattice T1.d path does not depend on the flag; narrow
or reword that first.
**Neither approach solves VF-03's owner identity, boundary params conservatism, or the memo's
unproven benefit.** Stated plainly by Sol rather than glossed.
## 2026-08-17 (o) — Approach A + unambiguous bails + CP owner key LANDED on `experimental`
Jahni explicitly authorised writes, commits, and a push to the real `experimental` tree. The three
code changes below were landed there as separate commits. **Nothing in this entry was built,
compiled, run in the editor, automation-tested, or measured.** All checks were static source/diff
inspection plus `git diff --check`. The tree is ready for Jahni's build, not claimed green.
### `8295f6e` — six-box surface-column LRU + cave-only opt-in diagnostic
- Landed Approach A's six 81×81 direct-indexed boxes in
`FSurfaceColumnSource::GetColumn`. An acquisition miss recentres and clears one LRU victim; the
other five working sets stay warm. Exact `uint64 ColumnKey`, exact XY coverage, per-cell computed
flags, and fractional-XY direct computation are retained.
- Deliberately kept the recommended six boxes despite the estimated **~0.79 MiB TLS per worker**
versus ~0.13 MiB for one box. There is no same-route measurement supporting an arbitrary smaller
count; silently choosing one would trade unknown miss behaviour for memory without evidence.
- Applied the report's caveat before landing: `UVoxelStrateManager::Initialize` counts and lists
disabled **cave** slots only. `SurfaceWorld` is explicitly excluded because its exact-lattice
T1.d proof does not depend on `bUseOperatorStack`. The flag remains a real per-asset A/B switch;
no forced cutover from Approach B was taken.
- Likely compile-error/watch spots for this commit: MSVC/UE function-local TLS for the large nested
`FColumnCache`; aggregate initialization of six `FColumnBox` values; range-for/member lookup in
the local `Acquire`; `FMemory::Memzero` on the selected `Computed` array; the new `UE_LOG` format
arguments and direct `Slot.Definition->GeneratorType` access.
### `1e02c63` — the old `Cave Bail Not Op Stack` no longer means two things
- Replaced the ambiguous counter with four named sites: `Sole Slot` means the complete sampled tile
Z range is proved inside the disabled slot; `Boundary Tile` means that range crosses its bounds;
`No Layout` means the failed chunk has no resolvable slot; `Recheck` means the later exhaustive
XYZ guard failed after the Z pass had already accepted the cave slot.
- This is instrumentation only. The existing `UsesOperatorStackForChunk` predicate is still tested
at the same point, and every original `ClassifyTile` return point and return value is unchanged.
Slot bounds are queried only after that predicate has already failed, solely to choose a counter.
- Likely compile-error/watch spots for this commit: declaration/definition spelling for all four UE
stats; the long stat display names; `FloorDivC` visibility inside the diagnostic branch; and the
`GetStrateChunkZBounds(ChunkZ, Top, Bottom)` argument order. A source diff confirms no terrain
predicate or `EVoxelTileClass` return changed, but only a build can validate the stat macros.
### `f90c4e5` — VF-03's proved `CP_*` cache now has an owner identity
- Each `UVoxelGenerator` receives a monotonic process-unique `DensityCacheOwnerId` from a relaxed
atomic at construction. `GetDensityAt` now keys its function-static `thread_local CP_*` state by
`(DensityCacheOwnerId, ChunkCoord, LayoutVersion)`. A world/generator change therefore refetches
params, generator type, disturbances, biome context, `CP_UseOpStack`, and the built op stack, and
invalidates `CP_BiomeCache` even when chunk/version values happen to match.
- Hot-path cost is one additional `uint64` comparison per voxel; atomic work happens only once per
generator construction. A monotonic ID was chosen instead of a raw owner pointer so a later
UObject address reuse cannot resurrect stale TLS state.
- Scope was deliberately restricted to the proved `CP_*` path. No claim was made and no key was
added to `OC_*`, `BM_*`, passage, biome, diff, or op-local caches. The test fixture's process-unique
layout-version bumps remain as conservative isolation for those unaudited caches, with its stale
CP-specific explanation corrected.
- Likely compile-error/watch spots for this commit: UHT/generated-constructor compatibility with the
explicit `UVoxelGenerator()` declaration; MSVC/UE support for `<atomic>`, `std::atomic<uint64>`,
`fetch_add`, and `std::memory_order_relaxed`; and initialization/access of the new private,
non-UPROPERTY `DensityCacheOwnerId` from the `.cpp` constructor and const hot path.
### Deliberately still open
- Read the actual `.uasset` opt-in state in the editor and fly the same underground route. The new
`Sole Slot` versus `Boundary Tile` counters make that result interpretable; source cannot answer
asset state.
- Measure the six-box memo on the same seed/route/settings/warm-up and record both miss rate per tile
and process memory at the real worker count. The verified structural improvement is **not** yet a
measured performance win.
- Audit owner identity for the other TLS caches separately. This change does not promote VF-03's
unproved breadth into fact.
- Boundary slot/params conservatism and every `ClassifyTile` safety guard remain unchanged.
### Candidate worktree cleanup
The dirty candidate diffs were preserved as named, recoverable stashes before cleanup:
- `c5e067c3` (currently `stash@{1}`) — `archive VF approach A before worktree cleanup 2026-08-17`
- `3be0b558` (currently `stash@{0}`) — `archive VF approach B before worktree cleanup 2026-08-17`
`../VF-approach-A` and `../VF-approach-B` were then removed from Git's worktree list and their
directories removed. Stashing was a cleanup-safety deviation only; it did not alter the landed code.
## 2026-08-16 (o) — build break in the six-box LRU: `Box` used after its scope closed
`VoxelDensityOpStack.cpp(832): error C2065: 'Box' : identificateur non déclaré`
Sol's single-box → six-box refactor changed the acquisition to
`FColumnBox& Box = Cache.Acquire(...)` **inside** `if (bIntegerXY)`, but the `Computed` flag is only
set *after* the column is computed, further down and **outside** that block. The old single-box
version had `Box` at function scope, so the write-back compiled; the reference did not.
Fixed by hoisting `FColumnBox* AcquiredBox = nullptr;` beside `MemoColumn` and writing back through
it. The guard is now `if (AcquiredBox)` rather than `if (bIntegerXY)`**non-null ⇔ integer path**,
so the pointer proves its own safety instead of relying on two conditions staying in agreement.
Swept the file: the only other `Box.` uses (761763) are inside the `if` and in scope. Not a logic
error and not a caching change — a scope slip, invisible in review because the diff showed both
halves separately. Still unbuilt beyond this compile fix.