feat: SurfaceWorld step 2b — the overhang shelf, the one op that is genuinely 3D

Step 2a green on all four counts, including FSurfaceColumnSource bit-identical to
GetSurfaceDensity over 20000 samples.

FOverhangShelfMod is the 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 and could not have lived in VoxelHeightOp.h. The boundary
falls where the code changes nature.

It needs TerrainZ plus a per-column gate (amp, uphill dir) the source computes.
Recomputing per voxel would pay the cliff's four resamples per lip voxel; adding a
third channel to FVoxelOpSample would put a COLUMN property in a per-voxel slot and
pollute a shared contract (section 11 has that open). Instead the source memoises
the column and the overhang reads it — the same shape as cliff -> structural.

The memo is keyed on (InstanceId, X, Y) with InstanceId from a monotonic atomic
counter, not on `this`: a freed stack and a newly allocated one can share an address,
a never-decreasing counter cannot collide. The stack evaluates every Z of a column at
one XY, so the hit rate is ~1 and this recovers per-column reuse without inventing a
second cross-chunk cache.

ComputeSurfaceColumn and SurfaceDensityFromColumn are now public: they are the only
oracle for the overhang, since GetSurfaceDensity passes OverhangAmp = 0. Private
declarations removed.

The test's third pass places half its samples inside the overhang window on purpose —
a uniform Z draw would almost never hit it and the test would pass having never run
the op, the same trap as WaterLevelRelative in the height pass. The in-window count is
reported and warns at zero.

Still missing before wiring: biome blending (the Mask combiner, section 5's Phase 3
prototype). Do not tick bUseOperatorStack on a SurfaceWorld strate with biomes yet.

UNVERIFIED: not compiled.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-27 16:23:27 +02:00
parent 7cd2bed237
commit 4dc55b1af3
4 changed files with 368 additions and 37 deletions
+45
View File
@@ -1337,3 +1337,48 @@ referenced against the cached path), biome blending as the `Mask` combiner, and
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.
---