CORRECTNESS on a box verdict, not perf. This file derives |Perlin3D| <= 1.5
rigorously, exposes it as PerlinAbsBound, uses it for the tunnel warp, and says
outright that the header's "~[-1,1]" is an observation and that a box verdict
resting on one is a hole. Three ExtraReach formulas in the same file assumed
sup|FBM| <= 1.0 -- and VoxelNoise::FBM normalises (Total / MaxValue), so
sup|FBM| = sup|Perlin3D| exactly. The bound was wrong by 1.5x.
At shipped defaults, soundness needed B <= 1.27 (VerticalShafts, Rough 3.0) and
B <= 1.40 (Maze, Rough 2.0); both are violated at B = 1.5. Break-even roughness
is 1.6. FloatingIslands survives only because its SDFBlendRadius is 5 -- sound
by parameter luck, not construction.
Nothing in the running game was affected (no strate has bUseOperatorStack
ticked) and the empirical Perlin sup ~1.0-1.1 is why nothing surfaced. But the
tile scans SAMPLE, and for shafts they sampled a source that proved zero tiles
until e002bd4 -- which is what makes this reachable rather than latent.
Fix: PerlinAbsBound hoisted to file scope as VF_PerlinAbsBound (one definition,
not a class-static plus three implicit 1.0s) and multiplied into all three
reaches; the three comments now state the real justification instead of the
refuted one.
Cannot change density: ExtraReach is read only inside EffectOverBox, verified
mechanically -- no Eval/GetCells line appears in the diff. Direction is strictly
conservative, so it can only cost CPU. Expect FEWER proved tiles for Maze and
VerticalShafts; that is the correct outcome, not a regression.
Not built -- Jahni builds.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
7.0 KiB
Codex task 003 — three ExtraReach formulas use an FBM bound this file already proved wrong
Owner: Codex (Model Luna, xHigh) · Orchestrator: Claude · Branch: experimental
Status: specified, not started
Kind: ⚠️ correctness of a box verdict — the class of bug that deletes collision. Not a perf task.
Why this exists
VoxelDensityOpStack.cpp contains a rigorous, written derivation that |Perlin3D| ≤ 1.5, exposes it
as PerlinAbsBound (line ~2336), and uses it correctly for the tunnel warp dilation (2452). The
comment there is explicit that the loose "[-1,1]" figure from the noise header is not to be
relied on, and OPSTACK-HANDOFF.md records the standard: a bound in a box verdict must be PROVED,
not observed — over-estimating costs CPU, under-estimating deletes collision.
Three ExtraReach formulas in the same file silently assume sup|FBM| ≤ 1.0. Each carries the
comment "FBM ∈ [-1,1]", which is exactly the claim the file disproves 1800 lines earlier.
And VoxelNoise::FBM is normalised — it returns Total / MaxValue where MaxValue = Σ Amp
(VoxelNoise.h ~272). So sup|FBM| = sup|Perlin3D| exactly: 1.5, not 1.0. The octave sum
neither amplifies nor attenuates the bound.
What that costs, per archetype, at the shipped defaults
Identity from these sources means "no primitive within ExtraReach of the box", i.e. Sdf ≥ ExtraReach throughout. Roughness then does Sdf += FBM · VOXEL_NOISE_SCALE · Strength
(FSdfRoughnessMod::Eval), so worst case Sdf' ≥ ExtraReach − B·1.25·|Roughness|. Soundness
requires Sdf' to stay at or above the downstream carve/fill threshold.
| archetype | ExtraReach at defaults |
downstream threshold | needs B ≤ |
verdict at B = 1.5 |
|---|---|---|---|---|
VerticalShafts (Rough 3.0) |
1.25·3 + 2 + 1 = 6.75 |
carve blend 2.0 | 1.27 | ⛔ UNSOUND (margin −0.875) |
Maze (Rough 2.0) |
1.25·2 + 2 + 1 = 5.5 |
carve blend 2.0 | 1.40 | ⛔ UNSOUND (margin −0.25) |
FloatingIslands (Rough 4.0, K 5.0) |
1.25·4 + 2·5 + 1 = 16.0 |
fill K 5.0 (+ K/6 SmoothMin dip) |
2.03 | ✅ sound — but only because K is large. Sound by parameter luck, not by construction. |
Break-even roughness for the two carve archetypes is |Rough| ≤ 1.6; they ship at 3.0 and 2.0.
How alarmed to be, stated honestly. No strate has bUseOperatorStack ticked, so nothing in the
running game is affected today. The brute-force tile scans report 0 violations — but they sample,
and they were sampling against a shaft source that proved zero tiles until e002bd4, so the
shaft path has never been exercised at all. The empirical sup of this Perlin is estimated at
~1.0–1.1, which is below the 1.27 the shafts need — which is why nothing has been seen yet, and
also why the margin is uncomfortably thin. The bug is that the verdict rests on an unproved bound,
which is the thing this codebase has already decided it does not do.
The fix
-
Hoist
PerlinAbsBoundto file scope and rename itVF_PerlinAbsBound, so there is one definition rather than a class-static plus three implicit1.0s. Keep the existing derivation comment with it — it is the justification, not decoration.Naming, resolved: the file-scope helpers in this file are all
VF_-prefixed (VF_NearCaveSurface,VF_DistPointSegment,VF_NoCaveOverBox), so a file-scope constant takes the same prefix. That means this is a rename, not just a move:- delete the
static constexpr float PerlinAbsBound = 1.5f;class-static insideFRoomGraphSource(~2336), moving its whole derivation comment with it; - update
FRoomGraphSource's own use at ~2452 (P.CaveWarpStrength * VOXEL_NOISE_SCALE * PerlinAbsBound) to the new name. This is the one place where the warp dilation is computed and it must keep computing the identical value — the rename must not change its arithmetic. - after the edit,
grep -n "PerlinAbsBound"must show onlyVF_PerlinAbsBoundoccurrences.
- delete the
-
Multiply the roughness term by it in all three
ExtraReachformulas (~4106 VerticalShafts, ~4215 FloatingIslands, ~4247 Maze):// before FMath::Abs(P.SurfaceRoughness) * VOXEL_NOISE_SCALE // after FMath::Abs(P.SurfaceRoughness) * VOXEL_NOISE_SCALE * VF_PerlinAbsBound -
Fix the three comments. Each says "FBM ∈ [-1,1]". Replace with the real statement:
FBMis normalised (Total / MaxValue), sosup|FBM| = sup|Perlin3D| =the provedPerlinAbsBound. A comment that states a refuted bound is how this happened in the first place.
⚠️ Invariants
- This must not change density by one bit.
ExtraReachis read only insideEffectOverBox(verified: every other occurrence is a comment or thefloat ExtraReach;member declaration — noEval, noGetCells). The eight equivalence tests compareEvalbit for bit and must stay green. If you find yourself editing anEval, stop — you have the wrong site. - The change direction is strictly conservative: larger
ExtraReach⇒ moreCarveOnly, fewerIdentity⇒ fewer tiles proved uniform. It can only cost CPU, never open a hole. Do not "balance" it by tightening something else in the same edit. - Anonymous-namespace placement. Put the hoisted constant above the labelled end of the anonymous namespace, not anchored on the FACTORIES banner — anchoring there puts it outside and the brace added with it closes nothing. This mistake has been made twice in this file and the file says so.
FRoomGraphSource's warp dilation changes NAME ONLY. It already uses the bound correctly and is the reference implementation for this fix; the value it computes must be bit-identical after the rename. Do not alter its formula, its√2factor, or anything else in that function.- Comments are French + English; match the surrounding file.
Acceptance
git diff --statshows one file:Source/VoxelForge/Private/VoxelDensityOpStack.cpp.- All three
ExtraReachdefinitions include the bound; no fourth site exists (BuildTunnelNetworkStackhas noExtraReach— it usesPerlinAbsBounddirectly for the warp). - No
Evalbody changed. - After the build: the eight equivalence tests stay green (density unchanged), and the box-verdict
lines for Maze and VerticalShafts may report fewer proved tiles than before. A drop
there is the expected, correct outcome, not a regression — it is the cost of a sound bound.
Record the before/after in
OPSTACK-PROGRESS.md.
Notes for the reviewer (Claude)
- Confirm the constant is genuinely at file scope inside the anonymous namespace and that the class-static is gone, not shadowed — two definitions that can drift is the failure this fixes.
- Confirm all three call sites got it. Two out of three is worse than none, because it looks done.
- Confirm no
Eval,GetCells, orGetCellsAtbody appears in the diff.