From 10dbe64b06459d530b2a5808d664ac3b065a1d07 Mon Sep 17 00:00:00 2001 From: Fr0zka Date: Tue, 28 Jul 2026 03:46:17 +0200 Subject: [PATCH] feat(opstack C3): TunnelNetwork + Underwater in the ported list -- 8 of 8 UsesOperatorStackForChunk now returns true for both, so the archetype switch has a complete operator-stack twin: per-strate opt-in, every archetype equivalence-tested bit for bit against its original function. This changes nothing by itself. The flag still requires bUseOperatorStack ticked on a strate asset, which is Jahni's call and was deliberately NOT done. What HAS changed is that the flag is no longer a no-op anywhere: ticking it on any strate now really switches that strate onto the stack. Not done, and it is the next real prize: ClassifyTile still uses hand-written guards and does not consume ClassifyBox. That is where measured tile-skipping becomes frames. DOCS - CODEMAP 3.2d: ported list 6 of 8 -> 8 of 8; the BuildTunnelNetworkStack row rewritten (19 ops, one builder for two archetypes); six new rows for the detail modifiers, each carrying the thing a reader would otherwise have to rediscover -- roughness reads STRATE params, terrace re-queries the SDF, the cliff's comment disagrees with its code, columns have no strate parameter at all, and LocalParams() is the override whose EffectOverBox is too optimistic on a strate with an op pool. Also corrected the stale "never compare the two paths" line: C10 is closed and all eight equivalence tests compare bit for bit. - CODEMAP 3.3 UsesOperatorStackForChunk row: same list, plus the warning that the flag is now a real switch rather than a harmless tick. - OPSTACK-PLAN: status header and the Phase 2 order both updated; the three-stage TunnelNetwork breakdown and the calls-not-transcribes rule recorded there rather than only in the code. - OPSTACK-PROGRESS: the closing entry for this unattended run -- every commit in order, the five original-code findings ported as-is, the two decisions that are not reversible by taste, the ClassifyBox optimism C1 introduced and that must be fixed before ClassifyTile consumes it, what breaks first per group, and the likely compile-error spots. Co-Authored-By: Claude Opus 5 --- CODEMAP.md | 25 ++- OPSTACK-PLAN.md | 42 +++-- OPSTACK-PROGRESS.md | 144 ++++++++++++++++++ .../VoxelForge/Private/VoxelStrateManager.cpp | 18 ++- 4 files changed, 211 insertions(+), 18 deletions(-) diff --git a/CODEMAP.md b/CODEMAP.md index 0556b4b..f55b270 100644 --- a/CODEMAP.md +++ b/CODEMAP.md @@ -121,11 +121,15 @@ stack share ONE copy. `VoxelGenerator.cpp` keeps same-named `static FORCEINLINE` ⚠️ **Feeds the game, behind a per-strate opt-in** (Phase 1 step 3). `GetDensityAt` builds the stack in its per-chunk refetch block and evaluates it *instead of* the `switch` only when `UVoxelStrateManager::UsesOperatorStackForChunk` says so — strate ticked `bUseOperatorStack` **and** -archetype in the ported list (**Maze, FlatPlain, CrystalChamber, SurfaceWorld, VerticalShafts, -FloatingIslands** — 6 of 8). Everything else still takes the `switch`, unchanged. -**`ClassifyTile` is NOT wired** — still hand-written guards, not `ClassifyBox`. That is Phase 2. -⛔ Never run both paths in one world, and never compare them for equality: the ~1 ULP residue is -inherent (AUDIT §C10). The acceptance bar is visual (OPSTACK-PLAN §2.6). +archetype in the ported list, which is now **all 8 of 8**: Maze, FlatPlain, CrystalChamber, +SurfaceWorld, VerticalShafts, FloatingIslands, TunnelNetwork, Underwater. A strate that has not +ticked the box still takes the `switch`, unchanged — the flag is the only thing that switches paths. +**`ClassifyTile` is NOT wired** — still hand-written guards, not `ClassifyBox`. That is the next +piece, and it is where the measured tile-skipping turns into frames. +⛔ Never run both paths in one world. **Comparing them IS legitimate now** — the ~1 ULP residue of +AUDIT §C10 is gone since `FPSemantics = Precise`, and all eight equivalence tests compare bit for +bit. They are port-correctness oracles, not fidelity checks: the acceptance bar is §2.6.1 (same seed +⇒ same world on every peer), which does not require resembling the pre-refactor world. | Symbol | Role | Notes | |--------|------|-------| @@ -145,7 +149,14 @@ inherent (AUDIT §C10). The acceptance bar is visual (OPSTACK-PLAN §2.6). | `VoxelDensityOps::BuildVerticalShaftStack` | — | 8 ops, and **three are Maze's reused unchanged** (`ConstantRock`, `SdfRoughness`, `SdfCarve`) with different tuning (freq 0.1 vs 0.12, window `rough+4` vs `R+rough+2`). The measured proof of `OPSTACK-PLAN §2.5`'s reuse claim. | | `FRoomGraphSource` (internal) | 1 | TunnelNetwork's SDF spine. **CALLS `BuildChunkCache`/`EvaluateSDFCached` — does not transcribe them**: that is where §8.4's two-region discipline lives and a copy would fork it. Owns the cave warp (scope = this op alone; pits/chimneys read *unwarped* coords, which is why no FRAME op was needed). Its cache key adds a **params CRC + LayoutVersion** that the original lacks — see the suspected staleness note in AUDIT §C2. `EffectOverBox` → `Both` for now (a real answer means building the cache for the queried box; only pays once `ClassifyTile` consumes `ClassifyBox`). | | `FWormFieldSource` (internal) | 1 | Fielded 3D-noise threshold carve, masked by distance to the room network (reads `InOut.Sdf` *after* pits/chimneys). `EffectOverBox` → **`CarveOnly` everywhere** — no spatial bound, so it kills `AllSolid` on every tile of every strate with worms on. `MaxCarveAmplitude()` holds the bound from DECOMPOSITION §0.2 that would recover it, waiting for a fold that carries numbers. | -| `VoxelDensityOps::BuildTunnelNetworkStack` | — | **STAGE A of three — incomplete on purpose.** 6 ops: rock → room graph → carve → worms → structural ×3. The 13 detail modifiers (stage B) and the per-room op override (stage C) are missing, which is why TunnelNetwork is still **off** in `UsesOperatorStackForChunk`. Testable now because every detail modifier is amplitude-gated and defaults to 0. | +| `VoxelDensityOps::BuildTunnelNetworkStack` | — | **COMPLETE, 19 ops** — the biggest port in the plugin (~1080 lines), done in three stages: SDF spine (A) → the twelve detail modifiers of 4b–4h (B) → the per-room op override (C). Serves **TunnelNetwork and Underwater** from one builder. Operator order is the original's, line for line, and it is load-bearing (`FFloorBiasMod` exists to undo what `FCaveRoughnessMod` did to floors). | +| `FCaveRoughnessMod` (internal) | 3 | STEP 4b, **density space** — a different op from `MakeSdfRoughnessMod`: two octave sets, optional domain warp, four noise types, an anti-fill clamp inside definite air, quadratic fade. ⚠️ **Reads STRATE params, not the per-room copy** — the original's shadow is declared *after* step 4b. Eleven of twelve modifiers read the room copy; this one does not. | +| `FCaveTerraceMod` (internal) | 3 | STEP 4c. The only modifier that **re-queries the SDF** (Z±1, through `FRoomGraphSource::ProbeSdfUnwarped`) for its horizontality gate — which is why the room source's cache is exposed at all. ⚠️ Those probes use unwarped X/Y and raw Z although the field was evaluated warped: transcribed as-is, see OPSTACK-PROGRESS. | +| `FLayerLineMod` / `FRibbingMod` (internal) | 3 | The same sine along Z: cubed and subtracted (grooves) vs quarter-phase-shifted, squared and added (ribs). `CarveOnly` / `FillOnly` — two of the few detail modifiers that keep a usable direction for the fold. | +| `FCaveOverhangMod` / `FCaveCliffMod` / `FScallopMod` (internal) | 3 | STEP 4c. ⚠️ The cliff's own comment promises a sampled vertical gradient; the **code** uses a Z-stretched Perlin as a proxy and samples nothing. Ported as written — fixing it would change the world. | +| `FCaveArchMod` / `FDomeMod` / `FPinchMod` / `FFloorBiasMod` (internal) | 3 | Room-relative: they read `FRoomGraphSource::GetNearestRoomIdx()` and the cached room. Their gate is `SDF < SDFBlendRadius`, **not** the shared `·3` one — they live in the cave's void, not its wall. | +| `FRoomColumnMod` (internal) | 3 | STEP 4d, and **not** `MakeGridColumnMod`: it walks `SDFCache.Columns`, pre-baked per room. ⚠️ It has **no strate parameter at all** — neither the bake nor the loop reads `FStrateGenerationParams::ColumnDensity`. Columns exist only through a `Column` terrain-op asset in the strate's pool, and the only way to prove they fired is to look at `SDFCache.Columns.Num()`. | +| `FRoomGraphSource::LocalParams()` | — | **The per-room op override** (DECOMPOSITION §2's "no clean home"). Strate params + the nearest room's `UVoxelTerrainOpDefinition::ApplyTo`, memoised once per voxel and read by eleven modifiers. One op owns the shared state, the rest read it — the same pattern as `FOverhangShelfMod` ← `FSurfaceColumnSource`. ⚠️ `EffectOverBox` still answers from STRATE params, so a box verdict can be **too optimistic** on a strate with a terrain-op pool; harmless until `ClassifyTile` consumes `ClassifyBox`, and it must be fixed before that. | | `FIslandBlobSource` (internal) | 1 | Hash-placed tapered flat-top blobs, `SmoothMin`'d, in a **domain-warped XY frame** (the warp stays inside the op — see the deviation note vs DECOMPOSITION §7). SDF channel only. `EffectOverBox` → `FillOnly` when a blob reaches the box, `Identity` otherwise; its pad must cover warp·**√2** (two independent noise axes), roughness, fill blend and the `SmoothMin` dip. **No lower Z bound exists** — a hairline thread of matter hangs below each island down its axis, so only the TOP may reject. | | `VoxelDensityOps::BuildFloatingIslandStack` | — | 7 ops, and **the stack runs backwards**: void source + fill instead of rock source + carve, using the *same* classes with the opposite sign. Only the blob source is new. Reuse by **inversion** — a stronger result than reuse by identity, since it says the abstract axis (the density sign) is the right one. | | `VoxelDensityOps::BuildMazeStack` | — | The 7-op Maze stack. If this ever becomes one op, the refactor failed its own test (§2.5). Callers must skip it on a **degenerate strate** (top−bottom ≤ 0): `GetMazeDensity` early-outs to air there and the stack has no such early-out by design — `GetDensityAt` falls back to the `switch`. | @@ -327,7 +338,7 @@ Maps depth→strate at runtime; owns passages. | `GetLayoutVersion` | h:161 (inline) | Layout/passage generation counter (= `PassagesVersion`, bumped by every `Initialize`). Hot-path callers key `thread_local` memos on it (strate-index memo in `GetDensityWithParams`, passage shortlist) so editor rebuilds never serve stale data. | | `GetStrateForChunk` | 466 | Chunk → definition. | | `GetGeneratorTypeForChunk` | 476 | Chunk → generator type. | -| `UsesOperatorStackForChunk` | 559 | Chunk → should `GetDensityAt` take the operator stack? `bUseOperatorStack` on the definition **AND** archetype in the ported list (Maze, FlatPlain, CrystalChamber, SurfaceWorld, VerticalShafts, FloatingIslands — 6 of 8; missing: TunnelNetwork, Underwater). **That list is written down here and nowhere else** — an unported archetype ignores the flag, so ticking the box anywhere is harmless. Add a row here when you port one. | +| `UsesOperatorStackForChunk` | 559 | Chunk → should `GetDensityAt` take the operator stack? `bUseOperatorStack` on the definition **AND** archetype in the ported list — **now all 8 of 8** (Maze, FlatPlain, CrystalChamber, SurfaceWorld, VerticalShafts, FloatingIslands, TunnelNetwork, Underwater). **That list is written down here and nowhere else.** With every archetype ported the flag is now the *only* thing that decides the path, so ticking the box is no longer a no-op anywhere — it is a real switch onto the operator stack for that strate. | | `GetSlabParamsForChunk` | 490 | Slab params with runtime Z bounds (no blend — slabs use Hard). | | `GetBiomeContextForChunk` | — | Flatten the strate's `Biomes[]` + `BiomeMapParams` into a POD `FBiomeContext` for the biome field. Empty ⇒ biomes disabled. §8.14. | | `GetGenerationParams` | 515 | **Blended** TunnelNetwork params (handles Gradient/Hard/Interleaved transitions). | diff --git a/OPSTACK-PLAN.md b/OPSTACK-PLAN.md index 38d035a..6422f03 100644 --- a/OPSTACK-PLAN.md +++ b/OPSTACK-PLAN.md @@ -4,11 +4,18 @@ > *composable density pipeline*, so new world ideas become authoring instead of C++. Written > 2026-07-26 as a handoff for a future context — read this instead of re-deriving it. > -> **Status (2026-07-27):** **Phases 0.5 and 1 DONE and verified. Phase 2 is 5 of 8 archetypes in**, -> all bit-identical to their originals and all wired behind `bUseOperatorStack`: -> **Maze · FlatPlain · CrystalChamber · SurfaceWorld (biomes included) · VerticalShafts.** -> Remaining: `FloatingIslands`, `TunnelNetwork` (**last** — it owns the §8.4 window-invariance -> discipline), `Underwater` (TunnelNetwork + a flag). +> **Status (2026-07-28):** **Phases 0.5, 1 and 2 CODE-COMPLETE — 8 of 8 archetypes ported**, each +> bit-identical to its original in an equivalence test, all wired behind `bUseOperatorStack`: +> **Maze · FlatPlain · CrystalChamber · SurfaceWorld (biomes included) · VerticalShafts · +> FloatingIslands · TunnelNetwork · Underwater.** The archetype `switch` now has a complete +> operator-stack twin, opt-in per strate. +> +> ⚠️ **The last three groups (stage B, C1, C2, C3) are UNVERIFIED — written, committed, never +> compiled.** Run the `VoxelForge` filter; `OPSTACK-PROGRESS.md`'s last entry lists every commit and +> what breaks first if one is wrong. +> +> **Not done, and it is the next real prize:** `ClassifyTile` still uses hand-written guards and does +> not consume `ClassifyBox`. That is where measured tile-skipping becomes frames. > > Two things came out of Phase 2 that were not in the original design: **height space** > (`VoxelHeightOp.h`, a second operator family — some things are not another channel but another @@ -406,12 +413,27 @@ care: the T1.a column cache and the exact-lattice `ClassifyTile` bound both surv ✅ `VerticalShafts` (**done**, 3 ops reused from Maze unchanged) → ✅ `FloatingIslands` (**done**, `BuildFloatingIslandStack` — the stack that runs **backwards**: void source + fill instead of rock source + carve, the *same* classes with the opposite sign; only the -blob source is new) → `Underwater` (TunnelNetwork + a flag) → `TunnelNetwork` -(**last** — it owns `BuildChunkCache`'s two-region window-invariance discipline, §8.4, the most delicate -code in the plugin). +blob source is new) → ✅ `TunnelNetwork` + `Underwater` (**done**, one builder for both — +`BuildTunnelNetworkStack`, 19 ops). -**6 of 8 ported.** The two that remain are really one: `Underwater` *is* TunnelNetwork plus -`WaterLevelRelative` (§8), so the switch loses its last two cases in a single port. +**8 of 8 ported.** The last two were really one: `Underwater` *is* TunnelNetwork plus +`WaterLevelRelative` (§8, re-verified before relying on it), so the switch lost its last two cases in +a single port. + +TunnelNetwork was ~1080 lines and was taken in **three stages, each verifiable on its own** rather +than as ~600 unverified lines on top of ~200 (the `AUDIT §P3` pattern): +* **A** — SDF spine: vertical scale, base rock, cave warp, room graph (+ pits + chimneys), carve, + worms, structural post. Verifiable *while incomplete* because every detail modifier is + amplitude-gated and defaults to zero, so zeroing them sends the ORIGINAL down exactly stage A's path. +* **B** — the twelve detail modifiers of `STEP 4b–4h`, one group per commit, each with a coverage + probe that proves the group actually moved something (`B1` roughness, `B2` terrace/lines/ribs, + `B3` overhang/cliff/scallop/arch, `B4` columns/domes/pinch/floor-bias, `B5` the gate itself). +* **C** — the per-room op override (`§2`'s option (a), and it needed no scoping predicate: one op + owns the state, eleven read it), `Underwater`, and the flag flip. + +⚠️ **`FRoomGraphSource` CALLS `BuildChunkCache`/`EvaluateSDFCached`; it does not transcribe them.** +That is where §8.4's two-region window-invariance discipline lives, and a copy would fork it — with +the fork "validated" by a test that compares it to the original. Along the way, `FStrateGenerationParams`' 74 fields decompose into per-op structs, which retires the `VF_STRATE_PARAM_FIELDS` X-macro drift problem for free. diff --git a/OPSTACK-PROGRESS.md b/OPSTACK-PROGRESS.md index 1490995..e1cd16c 100644 --- a/OPSTACK-PROGRESS.md +++ b/OPSTACK-PROGRESS.md @@ -2139,3 +2139,147 @@ amplitudes back ON one group at a time (a `Terrace` op in the room pool will be 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. diff --git a/Source/VoxelForge/Private/VoxelStrateManager.cpp b/Source/VoxelForge/Private/VoxelStrateManager.cpp index 59bf322..ecbb7af 100644 --- a/Source/VoxelForge/Private/VoxelStrateManager.cpp +++ b/Source/VoxelForge/Private/VoxelStrateManager.cpp @@ -587,7 +587,23 @@ bool UVoxelStrateManager::UsesOperatorStackForChunk(const FIntVector& ChunkCoord case ECaveGeneratorType::FloatingIslands: // Phase 2 — la pile qui tourne à l'ENVERS : source de VIDE + fill, au lieu de source de ROC - // + carve, avec les MÊMES opérateurs au signe près. 6 des 8 portés. + // + carve, avec les MÊMES opérateurs au signe près. + return true; + + case ECaveGeneratorType::Underwater: + // ⚠️ AUCUNE PILE À ELLE : `Underwater` EST `TunnelNetwork` plus un drapeau d'eau consommé + // côté rendu. `GetDensityAt` les met dans le même `case`, et `WaterLevelRelative` n'est lu + // que par `GetWaterLevel*` de ce manager — jamais par la densité (vérifié, pas supposé). + case ECaveGeneratorType::TunnelNetwork: + // Phase 2, LE DERNIER, et le plus gros : ~1080 lignes portées en trois étapes (squelette + // SDF → douze modificateurs de détail → override d'op par salle), 19 opérateurs, dont + // `FRoomGraphSource` qui **APPELLE** `BuildChunkCache`/`EvaluateSDFCached` au lieu de les + // transcrire — c'est là que vit la discipline d'invariance de fenêtre d'ARCHITECTURE §8.4, + // et en forker une copie aurait été le pire résultat possible de ce refactor. + // + // **8 SUR 8.** Le `switch` d'archétypes a désormais un jumeau en pile d'opérateurs, opt-in + // par strate, chacun vérifié par un test d'équivalence bit à bit contre sa fonction + // d'origine. Ce qui n'est PAS fait : `ClassifyTile` n'utilise toujours pas `ClassifyBox`. return true; default: return false;