1e02c6314f070149841d8c153be3a69784cf3134
70 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
588f9e0294 |
docs: Sol investigation -- refutes my boundary-tile hypothesis, corrects my false accusation
Sol High investigated with Luna xHigh sub-agents; two approaches built in isolated worktrees, main tree untouched. Two corrections to my own assertions: 1. "One un-ticked neighbouring strate kills boundary tiles" is WRONG. UsesOperatorStackForChunk depends only on ChunkCoord.Z, and ClassifyTile's earlier Z loop already required the predicate for every sampled cave Z within one slot -- so the later XYZ sweep is redundant, not over-strict. Removing it unlocks nothing; boundary tiles still fail the one-slot/type/params guards, correctly. 2. I falsely wrote that VF-03's fixture citation was fabricated. It exists (VoxelForgeTestFixture.h ~134/146, explicit about CP_UseOpStack contamination). I had read only the 30-line header and asserted a negative from a partial read. Header corrected. VF-03's core claim is now CONFIRMED: GetDensityAt keys thread_local CP_* by (ChunkCoord, LayoutVersion) with no world identity, and every manager's version starts equal. Also: the Cave Bail Not Op Stack counter I specced is ambiguous -- it fires before the different-slot attribution, so it cannot distinguish "flown slot un-ticked" from "boundary tile met an un-ticked slot first". My 80% reading was over-confident. Same defect I fixed for TilesSkippedAllSolid, reintroduced one layer down. Approach A recommended (explicit opt-in + six-box LRU) over B (code-enforced cutover): the code does not justify weakening ClassifyTile, and B would delete the flag, which is the only same-route A/B lever available to prove the refactor. Narrow A's warning to cave archetypes before adopting. Nothing built. Worktrees left in place for review. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
4ba53f2ede |
docs: T1.d is blocked by CONFIG not code (Cave Bail Not Op Stack = 80%); retract memo claim
Part B paid for itself on its first flight. Over the caves, Tiles Classified == Tiles Meshed (nothing skipped at all) and CaveBailNotOpStack accounts for ~80% of cave-branch attempts. That counter has exactly one cause: UsesOperatorStackForChunk returned false -- so either the strate flown over has not ticked bUseOperatorStack, or a chunk in the tile's box belongs to an adjacent strate that has not, since the per-chunk sweep requires every chunk to agree. CaveBailStackVerdict being non-zero (0.32) corroborates: where the flag did hold for the whole box, the stack built fine and merely returned Mixed. The machinery works; it is mostly not being reached. RETRACTION: I called the column-memo port a regression after comparing 14.7% from one flight against 23.3% from another. Invalid -- the miss rate is strongly location-dependent (~10-30%) and was in that band before the port. Comparing single-flight averages from different routes measures the route. A real before/after needs the same seed and route; parked until then, and not urgent. Kept while diagnosing: GSurfColCache is a 6-box LRU, not one box. My spec said "a direct-indexed box" and Codex implemented one faithfully, so any recentre now wipes all 6561 cells where the old table lost one entry. Plausible thrash source, unmeasured. Port the LRU if the memo is revisited. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
3ad2720d0a |
perf(opstack): direct-indexed column box (measured 14.7% miss); + T1.d bail attribution
BUILD RESULTS FIRST: 14/14 tests green, 0 violations. VerticalShafts box verdicts went 0 -> 30 of 60 (zero was the number for the project's whole life). TunnelNetwork production held at 11 proved / 14641 voxels, byte-identical -- that line was the isolated diagnostic for |
||
|
|
49a9959aed |
fix(world): drain workers before mutating strate layout/passages (VF-01)
Initialize does StrateLayout.Empty() and Passages.Empty()/Add() -- freeing and
reallocating both -- with no guard, while mesher workers read them through
AnyPassageNearBox, EvaluateModifierSDF and FindSlotIndexForChunkZ. The epoch is
bumped AFTER, so previous-epoch workers are live during the mutation; the epoch
rejects a finished result, it cannot make a read of a freed allocation safe.
Same class as the DiffLayer.ChunkMods carve-vs-stream AV that ModsLock fixed.
Chose the drain over the two alternatives:
- FRWLock on the arrays: correct, exact in-repo precedent, but a read lock on
the per-voxel hot path (~43k/tile) would contaminate the perf measurement
CODEX-TASK-001/002 are queued to take. Worst possible timing.
- Immutable generation snapshot (Sol's proposal): right long-term, refactors
the whole StrateManager API surface. A design conversation, not an agent's
unprompted call.
- Drain: zero hot-path cost, reuses the machinery EndPlay already proved, and
its stall lands only on human-initiated editor actions, never during play.
FScopedGenerationPause raises a new bGenerationPaused (deliberately NOT
bShuttingDown, which means teardown) and drains both reader populations: chunk
tasks via ActiveTaskCount and decoration tasks via a new
WaitForDecorationTasks, whose refactor also removed NotifyShutdown's duplicate
spin loop. On timeout it does NOT mutate -- logs an Error and leaves the world
consistent. A timeout that proceeds is what the bug already does.
RebuildStrates, OnObjectModifiedInEditor and ChangeSeed cannot reach Initialize
without the pause. BeginPlay untouched (no tasks yet). EndPlay untouched --
VF-02's 3s timeout is a separate deliberate decision.
Verified: four files, no density/mesher file, so terrain cannot move.
ShouldAbortWork replaced three existing checks, adding none. No worker path
waits on the game thread, so the drain is bounded.
Not built -- Jahni builds.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
bc0bf83c64 |
docs: VF-01 confirmed (worker race on rebuild) + VF-10 triaged; hold further code
VF-01 CONFIRMED and it is the most serious find of the day. Initialize does StrateLayout.Empty() and Passages.Empty()/Add() -- freeing and reallocating both -- with no lock, barrier or drain anywhere in that file, while worker threads read the same arrays through AnyPassageNearBox (:460), EvaluateModifierSDF and FindSlotIndexForChunkZ, reached from GetDensityAt/ClassifyTile. RegenerateAllChunks bumps the epoch AFTER the mutation, so previous-epoch workers are live during it; the epoch rejects a finished result, it cannot make a read of a freed allocation safe. Same class already fixed once here: DiffLayer.ChunkMods got ModsLock after a carve-vs-stream AV. The dangerous call site is OnObjectModifiedInEditor, which fires automatically on a strate asset edit while streaming -- routine here. VF-10 confirmed real (74 fields, per near-surface sample) but Sol missed the conclusion: the copy is INHERITED from GetDensityWithParams, so both paths pay it and it does NOT explain the op-stack regression. The op stack improved it by memoising across eleven detail ops. Future optimisation, not the answer. Deliberately NOT fixing VF-01 now. Four code changes are stacked unbuilt and three have "nothing should move" as their acceptance signal; a fifth change to streaming lifecycle would make an odd build result unattributable. The three fix options (drain / snapshot / RWLock) trade a possible crash for an editor hitch or hot-path lock traffic -- a product decision, not an agent's. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
cab8e8fe55 |
fix(morphology): BuildChunkCache under-bounds room/tunnel collection when Min > Max
Third instance of the same class, and the worst one -- found by the Sol-High
read-only audit (VF-05) and verified against the code before acting.
MaxInfluence, RoomZBuffer and EvaluateSDF's Margin all derive from
MaxRoomRadius / TunnelMaxRadius, while the radii are Lerp(Min, Max, hash), which
yields up to max(Min, Max). A room able to reach a chunk can therefore sit in a
cell the collect region never visited.
Worse than the two fixed earlier today because:
- it is TunnelNetwork, the largest archetype;
- BuildChunkCache is called by BOTH density paths (FRoomGraphSource calls it
rather than transcribing it), so this was never an op-stack bug -- it is in
the shipped original code and always has been;
- the failure mode is a window-invariance break (ARCHITECTURE 8.4): whether a
room exists depends on which chunk you queried from, which in multiplayer
means two peers generate different geometry from the same seed.
Four bound sites now use RoomRadiusEnvelope / TunnelRadiusEnvelope; the two
duplicated copies of the formula still compute the identical expression. No
Lerp, placement, hash or bStore line changed -- with correctly ordered params
max(Min,Max) == Max, so this is bit-identical. A no-op at correct values is the
acceptance signal.
Also adds a reviewer's header to AUDIT-2026-08-CODEX.md marking which findings I
verified (VF-05 confirmed, VF-02 premise confirmed, VF-03 evidence overstated --
its cited fixture corroboration does not exist) and which are unverified leads.
Not built -- Jahni builds.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
8ef4d7dc09 |
docs: CORRECTION -- bUseOperatorStack is ON in the game's data assets
The handoff said "No strate asset has the box ticked -- that is my call and I still haven't made it." Stale, and I reasoned from it all session. Jahni: "the data assets in game have the switch on." Everything downstream flips. The operator stack is the PRODUCTION density path, so the measured perf regression is one players feel, and any unsound EffectOverBox is a live hole rather than a latent one. The two correctness fixes committed today ( |
||
|
|
f737488d88 |
test(opstack): three tile scans sampled <1 lattice period; fix the CLASS this time
The "a sampler must cover at least one period" bug was found in the tunnel test (+/-32 vs RoomSpacing 80) and again in the shaft test (+/-48 vs ShaftSpacing 55). Both were fixed. The class was not: three box-verdict scans still ran the original RandRange(-6,6)*Extent at Step 1 / Cells 8, i.e. +/-48 voxels. Island +/-48 vs IslandSpacing 95 = 0.51 periods (worse than either fix) Slab +/-48 vs ColumnSpacing 60 = 0.80 periods Maze +/-48 vs CellSize 40 = 1.20 periods Verified the fixture does not override any of those spacings, so the header defaults are what these tests really ran against. This matters now specifically: |
||
|
|
eaa44bf0c0 |
fix(opstack): two cell-sweep pads assumed MinRadius <= MaxRadius; a third already didn't
Second defect from the full 28-op EffectOverBox audit. Three ops roll a radius as Lerp(MinRadius, MaxRadius, hash01), which lands in [min(A,B), max(A,B)] -- Lerp does not require A <= B. Their EffectOverBox sweeps lattice cells padded by the largest radius a cell could hold, so a pad below the true maximum means the cells are never examined and the op reports Identity for a box its own Eval will fill or carve: no geometry, no collision. FGridColumnMod ~1086 Max(MaxRadius, 0) exposed FShaftFieldSource ~1436 Max(ShaftMaxRadius, ConnectorRadius) exposed FIslandBlobSource ~1803 Max(IslandMinRadius, IslandMaxRadius) already correct The third one is the argument: the concern was met and guarded once in this same file, and the other two shipped without it. Fixed with FMath::Max3, a spelling already used here (~2481) and in VoxelCaveMorphology.cpp. Shipped defaults are ordered correctly (2/5, 2/7, 5/11), so this is a NO-OP at defaults -- that is its acceptance signal, and any moved number means the diff did more than intended. It needs a mis-ordered asset value, which nothing prevents: ClampMin is a per-property floor and UE cannot express "<= that other property". ColumnMinRadius is also settable per-room via UVoxelTerrainOpDefinition. Deliberately NOT fixed by normalising the params: swapping the Lerp endpoints maps the same hash to a different radius, changing geometry and breaking the eight equivalence tests. Only the bound becomes conservative; Eval is byte- identical -- verified, no Eval/Roll/GetCells line in the diff. Also audited and found sound (recorded so they are not re-chased): FPassageCarveOp and the passage bounding sphere, FOriginSpineOp, FBoundarySealOp, FShaftLedgeMod, FCaveArchMod, FDomeMod. Not built -- Jahni builds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
7dbdf51b44 |
fix(opstack): three ExtraReach box verdicts used sup|FBM| = 1.0; the proved bound is 1.5
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
|
||
|
|
b5294b2d5b |
docs: close C2, sharpen the column-memo prediction, reassess REVIEW_FINDINGS
Reading session -- no build available, so: verify things instead of writing code. - Cross-checked the eight stat declarations against their definitions and use sites. Identical. The one compile risk I had only eyeballed is now retired. - REVIEW_FINDINGS: the GetDensityWithParams and BuildChunkCache splits are now counter-productive rather than optional -- the op stack is replacing the first, and FRoomGraphSource deliberately CALLS the second so a restructure forks what was kept unforked. The two passage enums are not duplicates and are UMETA-serialised into authored assets; merging them rewrites content for tidiness. Judged: leave, and close the item. - CODEX-TASK-002's acceptance said "20-30% of lookups", a guess wearing a number. Replaced with arithmetic from the real grid: 1225 columns/tile over 35 Z planes, load factor 0.299, ~317 colliding columns, ~12000 vs 1225 computations = ~9.8x. Plus the reading rule that matters -- the overhang and cliff mods re-query the same XY and add HITS only, so compare the ratio of the two hypotheses, not an absolute percentage. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
eb317d9933 |
feat(stats): stat VoxelForge -- make tile skipping and the column memo observable
The plugin had ZERO stat counters, so every claim this refactor makes was harness-only: "skipped correctly" and "skipped nothing" render identically. CODEX-TASK-001 and -002, executed by Codex (gpt-5.6-luna xhigh), reviewed against the source. Eight DWORD counters in a new stat group: GenerateTileResult -- TilesClassified / SkippedAllSolid / SkippedAllAir / Meshed ClassifyTile bAnyCave exit -- TilesOpStackSolid / TilesOpStackAir FSurfaceColumnSource::GetColumn -- ColumnMemoHit / ColumnMemoMiss The op-stack counters are separate from the lumped skip counters on purpose: ClassifyTile also proves AllSolid on its hand-written bedrock-gap path with no strate opted in, so the lumped number cannot show a before/after. The op-stack pair is zero BY CONSTRUCTION until a strate ticks the flag. GetColumn is instrumented and deliberately NOT fixed -- the instrument and the fix in one build would make each other unreadable. Verified against UE_5.7 Stats.h rather than assumed (first stats use in this plugin, no in-repo precedent): all four macro arities, and INC_DWORD_STAT -> FThreadStats::AddMessage, so the worker-thread safety both sites need holds by mechanism. Not built -- Jahni builds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
7313201e91 |
docs: spec CODEX-TASK-002 (column-memo thrash) and aim the PERF work
Three orchestration findings on the parked PERF item, none measured yet: 1. The perf A/B needs no new code -- two Insights traces on a deterministic world are a clean before/after. But tile skipping changes how many times GenerateMesh runs, so totals conflate "cheaper per tile" with "fewer tiles". TilesMeshed from task 001 is the denominator. 001 is therefore a PREREQUISITE for the perf work, not a parallel item as the handoff had it. 2. The suspects don't share an archetype: SurfaceWorld is the column memo, TunnelNetwork is 19 virtual calls per voxel (16 + 3 structural post). One lumped number can't be acted on -- measure one strate at a time. 3. GetColumn is a direct-mapped hashed table where GSurfColCache is a direct-indexed box, and the mesher pre-samples Z-OUTERMOST (verified, VoxelMarchingCubesMesher.cpp ~226) so every column is revisited ~34x per tile. The table's sizing comment reasons about fullness; a direct-mapped table evicts on collision. At load factor 0.28 that is still ~25% of columns recomputing the full height stack every Z plane -- derived ~9x. Task 002 measures that and fixes nothing, so the instrument and the fix cannot land in the same build and make each other unreadable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
57002b35cf |
docs(opstack): log the CODEX-TASK-001 acceptance-bar correction
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
251a1288e0 |
docs: spec CODEX-TASK-001 (tile-skip stats) and refresh the handoff for the Codex tandem
Jahni built a world 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 refactor:
everything proved so far was proved in an automation harness on 40 sampled
tiles, and in the running game tile-skipping is unobservable.
grep INC_DWORD_STAT over Source/ returns nothing -- the plugin has zero stat
counters -- and "skipped correctly" renders identically to "skipped nothing", so
no visual check can separate them. The tests got the "coverage is a number, not
a boolean" discipline this session; the game never did.
CODEX-TASK-001-tile-skip-stats.md specs 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 says whether the op-stack work did anything real. Its deliverable
is the before/after that constitutes the PRODUCTION proof of T1.d, which does not
exist today.
The spec carries the invariants rather than just the task, which is the 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 while INC_DWORD_STAT is not.
Handoff updated: a "How we work now" section (Codex Model Luna xHigh writes most
code, Claude orchestrates -- hand over the INVARIANT, review against the code and
not the description), first actions split into the Codex task and the pending
|
||
|
|
e002bd4e2e |
feat(opstack): VerticalShafts' connector branch never tested a connector -- test the real capsules
Jahni's call: bank T1.d at 11/40 and take VerticalShafts rather than squeeze the Perlin sup. Right call -- a clearly-scoped defect with no hole-risk maths, on an archetype that was getting nothing. The defect is stated in its own comment. FShaftFieldSource::EffectOverBox ended with a "connectors" branch that never looks at a connector: it returns CarveOnly because a shaft EXISTS within Spacing*1.6 + Pad. At ShaftSpacing 55 and ShaftDensity 0.6 that box spans ~4x4 cells and ~10 shafts, so the condition is true essentially everywhere -- exactly the reported 0 proved of 60. Conservative, never wrong, completely sterile; the same shape as the worm's unconditional CarveOnly one archetype over. It now rebuilds the connectors the way GetCells does and tests the real capsule. Two things had to be right and both were read in the source rather than assumed: - The enumeration is a superset. Eval reads connectors from the 3x3 of ITS query's cell, so any pair visible from a point in the box has both shafts in the 3x3 of some cell the box touches, i.e. in [box cells] +- 1 -- the range swept here. Pairs no query ever sees may be produced: extra CarveOnly, never a hole. - The pair order matches, so the hash matches. VoxelHash::Pair is fed in insertion order and GetCells inserts over (dy, dx), row-major; this sweeps (cy, cx), and row-major order restricted to a sub-grid preserves the relative order of two cells. So the same pair gets the same hash WITHOUT assuming Pair() is symmetric -- which was never verified and now need not be. The capsule test splits the axes because a connector is HORIZONTAL at height Zc: Z is exact, XY is point-to-segment from the box centre minus the XY half diagonal. Tighter than the 3-D half-diagonal the tunnel version had to settle for. Also the same sampler trap, caught before the build this time: tile XY was drawn from +-48 voxels against ShaftSpacing 55 -- under one period of the pattern, identical in kind to the +-32-vs-80 bug that cost the tunnel test three runs. Widened to +-440, and the report prints its own extent in units of ShaftSpacing. The safety net was already there and is untouched: this test brute-forces the full lattice of every proved tile, both hypotheses, and AddErrors on the first violation. If the superset or hash-order argument is wrong, the assertion fails rather than a player falling through the floor. Unbuilt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
861dc8e109 |
docs(opstack): WARP SHARE measured -- half the blocking is the box; plus two corrections
The instrument landed on its first run. At production defaults, rooms reaching go 0.9 -> 0.4 and tunnels 2.4 -> 1.1 when the warp dilation is set to zero: more than half of all remaining blocking is the query box rather than cave geometry. Hypothesis confirmed, and the number is permanent output now. CORRECTION 1. Last entry I claimed BuildChunkCache's Expansion = CaveWarpStrength + 2 means "the whole plugin bets on |Perlin3D| <= 0.8", and used it to argue 2.0 was needlessly conservative. False. Six lines further down, bNeedRebuild tests WarpedX < CachedSMinX || WarpedX > CachedSMaxX -- the cache REBUILDS when the warped query leaves the box, so that expansion is a rebuild-frequency heuristic and nothing bets on 0.8. The 2.0 -> 1.5 change stands on its own derivation. Sixth premise this refactor that reversed on checking, and I asserted this one in the same entry where I diagnosed the habit. CORRECTION 2 / NEGATIVE RESULT. The obvious next move -- evaluate the warp at the box centre, SHIFT the box, dilate only by the variation across it -- does not pay here, worked out before writing it. A rigorous per-axis Lipschitz bound for this Perlin3D is |dV/dfx| <= 4*1.875 + 1 = 8.5 per unit cell (fade derivative times the spread of GradDot, plus GradDot's own linear term, u and v being distinct axes). The half-box is 0.206 in noise units, so the local variation bound is 1.75 against a GLOBAL range bound of 1.5. The local bound is worse than the global one. Recorded so nobody spends a build rediscovering it. That leaves one route for the warp term: tightening the sup of |Perlin3D| from the proved 1.5 toward its apparent true value ~1.0-1.1, worth ~27% of the dilation. Spot-checking a grid is not a proof, and a sup proved wrong is a tile with no geometry and no collision -- so that is a judgement call about appetite, not a technical unknown. State: T1.d 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. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
b2938d38f1 |
fix(opstack): the warp bound was the dominant term all along -- 2.0 -> 1.5, and MEASURE it
Jahni asked whether I was in a loop. I was, and re-reading the original found it
in one line.
PerlinAbsBound was set to 2.0 in the first commit of the spatial EffectOverBox
and never revisited. The dilation is CaveWarpStrength * VOXEL_NOISE_SCALE *
PerlinAbsBound, and CaveWarpStrength is 8.0 by default:
8 * 1.25 * 2.0 = 20 voxels, applied +/- on every axis
tile 10 voxels -> query box 50 per side -> 125x the tile volume
So the tunnel test I called "an order of magnitude tighter" actually needed
DistToAxis >= 78 against a cull rejecting at ~107. 27% tighter, not 10x -- and
the run said so: tunnels reaching went 78.0 -> 58.5, a 25% cut. The instrument
was right and I credited the tunnels.
Four rounds -- worm, columns, sampler, tunnel disjunction -- each individually
correct, every one of them tightening around a term nobody had measured. I kept
instrumenting what I had just changed and never instrumented what I had assumed.
What re-reading showed: BuildChunkCache is called everywhere with
Expansion = CaveWarpStrength + 2.0f, which is only correct if |Perlin3D| <= 0.8.
The whole plugin has always bet on 0.8. I picked 2.0 -- 2.5x more conservative
than the assumption the cache's own correctness already rests on.
The corrected bound is derived, not guessed. GradDot returns +-u +-v with u and
v two DISTINCT components (checked on all four hash branches). Splitting the
eight corners by i gives, per axis, sum w*|dx| = (1-su)*fx + su*(1-fx) <= 0.5
(max at fx = 0.5). Hence |Perlin3D| <= S_x + S_y + S_z <= 1.5, with no case
analysis on the hashes. Dilation 20 -> 15.
And the instrument that should have existed from the start: EffectOverBox now
also runs every room/tunnel test with the warp dilation set to ZERO and reports
both, so Hit* - Hit*NoWarp is exactly the blocking caused by my box rather than
by geometry. The report says, in the output, that if that gap dominates the next
move is the warp bound and not the primitives.
Separately: the diagnostics had turned into narrative, printing hardcoded numbers
from previous runs next to live ones ("32 of 34 tiles vs 21 for rooms" while the
live figures said 21 of 28). Unreadable, and I wrote all of it. Diagnostics now
report THIS run; the history stays in OPSTACK-PROGRESS.
Unbuilt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
9733179723 |
feat(opstack): tunnels get a threshold test -- T1.d is measured at 6 of 40 tiles, 0 violations
THE PRIZE LANDED. At production defaults: 6 of 40 tiles proved AllSolid, 7986 voxels brute-forced, 0 violations. 15% of tiles skip GenerateMesh entirely, one BuildChunkCache traded against 30000+ density evaluations each, and not one verdict is asserted -- every voxel was re-evaluated and came back on the claimed side. The dense fixture still reports 0, exactly as the arithmetic said it must, so this is a measurement of production rather than of a widened test. The breakdown finally isolated the tunnels: of 34 unproved tiles, 32 were blocked by a tunnel and 21 by a room, so >=13 were tunnel-only. That is what justified the deferred disjunction, and why it was deferred rather than guessed. A primitive no longer matters if it misses its cull OR its own SDF stays >= T+K over the box. Each branch wins on a different class, by calculation: for rooms the cull (Rmax+3K) is tighter than the threshold (Rmax+T+K), so rooms keep the cull alone; for tunnels the cull is a capsule BOUNDING SPHERE, radius ~107 for a 200-long tube of radius 7, against a true segment distance. Order of magnitude. The bound is exact, not cautious. TaperedCapsule was read, not assumed -- 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, from "Sdf stays FLT_MAX" to "Sdf >= T" with T = max(3*SDFBlendRadius, WormNetworkRange). Sound only because all three consumers of the SDF channel were read one by one: FSdfConvertOp's Blend is MakeSdfCarve(P.SDFBlendRadius, ...) => K; the twelve modifiers gate at 3K; FWormFieldSource at WormNetworkRange. Plus the one that could have bitten -- FCaveTerraceMod re-probes the SDF at Z+-1, OUTSIDE the box, but its gate is line 13 and the probes are lines 28-29, so a gate false everywhere never emits one. ANY NEW CONSUMER OF THE Sdf CHANNEL MUST HAVE A THRESHOLD <= T OR JOIN THAT MAX, or it gets tiles with no geometry and no collision. Written at the site. Why -K suffices for any N: SmoothMin's penalty is exactly zero once |A-B| >= K, so the running minimum saturates at K below the smallest term and cannot descend further. Sdf >= min_i(SDF_i) - K for ANY number of primitives, not - N*K/6 -- without which the slack would scale with the ~88 tunnels and be worthless. Unbuilt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
87a0b996ec |
test(opstack): measure the box verdict on BOTH densities -- the fixture cannot prove a tile
The widened sampler worked: 39 of 40 tiles now land clear of the (0,0) spine.
The verdict did not move -- rooms still hit 40 of 40. By my own pre-registered
rule that means the room-lattice model was wrong, and it was, for a reason the
report had been printing at me for three runs: "= 7.6 x RoomSpacing 42".
RoomSpacing is 42 here, not the 80 I did the arithmetic with. I read the
UPROPERTY default out of the header instead of the fixture that overrides it.
Fifth premise this refactor that reversed on being checked, and the plainest:
the number was on screen.
EnableTunnelFeatures densifies on purpose -- RoomSpacing 80 -> 42, RoomDensity
0.35 -> 0.85 -- because at the defaults only 1.1% of samples were in open cave
and the equivalence compared solid rock to solid rock. That densification is what
makes check 1 mean anything, and it is exactly antagonistic to provability:
room cull radius = max(R*1.5, R*HeightRatio) + 3*SDFBlendRadius
= 1.5R + 12 for R in [10,30] => 27..57, mean ~42
lattice spacing = 42 at 85% occupancy
The mean cull radius equals the lattice spacing, so cull spheres cover that world
~3.6x over and NO box can be outside all of them. "6.3 of 8.3 rooms reach" is not
a loose test, it is a saturated world. No sampler change and no tunnel tightening
can move it -- which is why widening the sampler correctly changed nothing.
So 0 proved on this fixture is the RIGHT answer, and informative: the prize
shrinks to nothing as caves saturate. It is simply not an answer about
production.
Check 4 is now a lambda run twice -- parameterised rather than copy-pasted,
because two copies of the criterion would drift and the second would lie:
[dense fixture] as before, expected to stay ~0 and now documented as correct;
[production defaults] the same 40 tiles against RoomSpacing 80 / RoomDensity
0.35, the real UVoxelStrateDefinition defaults. Both brute-forced voxel by voxel.
Not "widen it until it passes": the dense run stays in the report, must stay ~0,
and a false verdict in either still fails. The two stacks deliberately share
FRoomGraphSource's thread_local caches, so this run and check 3 now watch each
other through the params fingerprint in the key.
Unbuilt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
f2fefade4c |
test(opstack): the tile sampler never left the (0,0) spine -- widen it to 4x RoomSpacing
The per-class breakdown refuted my own hypothesis on its first run. I predicted "tunnels >> rooms, so it is capsule bounding spheres". Tunnels ARE wildly over-counted (69.2 of 80.4 reach), but rooms hit all 40 tiles too, so fixing tunnels alone would have moved the number by exactly zero. Fourth confident chain this refactor that reversed on contact with a measurement. The real finding is that the measurement was invalid. The sampler drew tile XY from RandRange(-4,4)*8, i.e. +/-32 voxels, against RoomSpacing = 80 and a guaranteed OriginRoomRadius = 20 room at (0,0) whose cull radius is 42, with the (0,0) spine descending exactly there. All 40 tiles sat inside the origin room's cull sphere, in the most cave-riddled spot in the world. "4.9 of 7.2 rooms reach" was measuring the spine hub, not the world's cave density -- every conclusion about whether deep rock is provable was answering another question. Widened to +/-320 voxels (4x RoomSpacing). The report now prints its own sampling extent and how many tiles landed clear of the spine, because a sampler whose extent you cannot quote is one nobody is watching. This changes what the measurement LOOKS AT, never what it demands: every verdict is still brute-forced voxel by voxel, so a wider sampler that produced a false verdict still fails. Also worked out, before writing any code, why rooms cannot be tightened and tunnels can. SmoothMin's penalty is exactly zero once |A-B| >= K, so the running minimum saturates at K below the true minimum and Sdf >= min_i(SDF_i) - K for ANY number of primitives. With K=4, WormNetworkRange=24, mods gating at 3K=12, the threshold T is 24. For rooms the cull rejects at Rmax+3K=57 while an "Sdf >= T+K" test rejects only at Rmax+T+K=73 -- the cull is strictly better. For tunnels the cull is a capsule BOUNDING SPHERE, radius up to ~107 for a 200-long tube of radius 7, while the real segment distance rejects at 35. An order of magnitude, and sound because TaperedCapsule is a genuine distance function (verified, not assumed). That disjunction is NOT in this build on purpose: it changes what Identity means here, from "Sdf stays FLT_MAX" to "Sdf >= T", which is only sound if every consumer threshold is <= T. Three premises still need reading rather than assuming -- VF_NearCaveSurface's constant, the Blend passed to FSdfConvertOp, and whether any modifier re-probes the SDF outside the box before its gate. Fix the measurement first; it costs nothing and it is wrong today. Unbuilt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
8043a613c3 |
perf(opstack): drop the redundant column test, and measure WHICH primitive class blocks the box
The worm fix worked -- attribution now reads "AllSolid killed by: RoomGraphSource x40", with WormFieldSource gone from the list. The blocker moved one operator upstream. And my own warning is now the thing to distrust. It said "if it names RoomGraphSource, the tiles genuinely straddle cave", which 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 whose bounds differ enormously in quality. No third guess. PROVED, not guessed: the columns test is removed. It 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 over a shared XY circle. It was REDUNDANT rather than conservative: columns are read by exactly one operator, FRoomColumnMod, whose Eval opens with VF_NearCaveSurface(InOut.Sdf, ...). In a box no room/tunnel/pit/chimney reaches, Sdf stays FLT_MAX everywhere, so no column can execute wherever it sits in XY. Removing it tightens the verdict without touching correctness. THE INSTRUMENT: EffectOverBox no longer early-outs on the first hit, it counts all four classes -- stopping at the first gives the right verdict and no information, which is exactly why "RoomGraphSource x40" was unactionable. Free at the scale that matters: BuildChunkCache has just run and dwarfs a walk over ~100 structs, and the verdict is memoised so the walk happens once per box. GetLastRoomBoxDiagnostic reads back what the operator computed rather than letting the test re-derive the criterion. The test could replay it -- and that second definition would drift from the real one and lie on the day it was believed. Same reason VF_BuildOpStackForChunk exists. The hypothesis it exists to kill or confirm: a tunnel is culled per voxel by its BOUNDING SPHERE, an enormous over-estimate for a long thin capsule, while a room's cull sphere is a fair fit. Tunnels >> rooms would mean the box test is losing to capsule bounding spheres rather than to real cave. NOT done deliberately: tightening tunnels to a true 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. Making it sound needs "no primitive can bring Sdf below max(Blend, SDFBlendRadius*3, WormNetworkRange)", which needs a bound on how far SmoothMin of N primitives dips below min. Real 0.2 design work, and doing it blind before knowing whether tunnels are even the problem is the C10 mistake verbatim. Unbuilt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
f537648867 |
fix(opstack): the worm inherits the room source's box verdict -- the actual blocker
First build of the spatial EffectOverBox came back green with 0 tiles proved of 40. The warning written for exactly that case fired, and then was not good enough: it offered two candidate causes and BOTH were wrong. The real one was a third operator neither candidate mentioned. FWormFieldSource answered CarveOnly unconditionally with a provably correct amplitude bound of WormStrength. But BaseDensity = 8 and WormStrength = 10 are the DEFAULTS -- the field's own comment requires the worm to exceed BaseDensity or it could never carve air. So SolidMargin = 8 - 10 < 0 on every tile of every strate with worms on, before the fold reached anything the room source proved. A numerically correct bound that is structurally always fatal. The fix was already written three lines up in the worm's own Eval: NetworkMask is 0 when CaveSDF >= WormNetworkRange, which FLT_MAX always satisfies. The worm IS spatially bounded -- by the room source's bound, exactly like the twelve detail modifiers -- so where the source proves Identity it does not execute at all. Thirteen inheritors instead of twelve. Verified FVoxelOpSample::Sdf really does initialise to FLT_MAX rather than assuming it; the inheritance inverts into a hole otherwise. Deliberately NOT VF_NoCaveOverBox: that helper answers "identity" for a null Rooms, correct for the twelve modifiers and wrong for an op a future assembly could place behind a different SDF writer. No room source means we do not know, which must cost CPU rather than a hole. And the instrument, because the guess is the thing that cost a build: a diagnostic that lists candidate causes without measuring them is still a guess wearing rigour. FVoxelOpStack::ClassifyBoxAttributed reports the index of the first op that kills each hypothesis -- same loop, same early-out, verdict identical to ClassifyBox, because a diagnostic that takes a different path than the thing it explains sends you hunting in the wrong place. IVoxelDensityOp::DebugName gives them names and touches no cache key, so it cannot change the world. Check 4 now always prints "AllSolid killed by: <op> xN", and the zero-proved warning says to read it instead of re-deriving. Unbuilt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
03ddcde334 |
feat(opstack): FRoomGraphSource::EffectOverBox answers spatially -- and AUDIT C2 is fixed
The chain no longer dies at the room source. The criterion is the per-voxel cull
lifted from point to box, which 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 survives its cull anywhere in the box, Sdf stays
FLT_MAX across the whole box.
FSdfConvertOp already returned Identity ("la source a repondu pour la paire") and
the twelve detail modifiers already inherited through VF_NoCaveOverBox. One
function learned to answer and fourteen operators became provable -- what the C1
wiring was built for.
Three deliberate choices, all erring toward CPU rather than toward a hole:
- |Perlin3D| <= 2, derived from GradDot + the convex hull of a trilinear lerp,
instead of the header's observed "~[-1,1]". A verdict resting on an observation
is the hole this file spends its life avoiding.
- the op pool is passed to BuildChunkCache, not nullptr: the bake reads OpParams
to place pits and chimneys, so nullptr would under-bound the cache and could
return Identity over a real pit.
- the search box is wider than Eval's, giving a superset of primitives.
The verdict is memoised per box (all twelve modifiers ask the same question), and
the cache is a SECOND per-worker cache so classification cannot disturb a live
generation's hot cache.
AUDIT C2, confirmed 2026-07-28, is fixed on the switch path in the same breath:
GetDensityWithParams now takes required ParamsFingerprint + LayoutVersion. The
alternative this audit section used to recommend -- add chunk Z to the key -- is
insufficient (Interleaved makes Alpha depend on chunk XY too) and destructive
(chunk XY is deliberately absent so gradient probes don't thrash the box, ARCH
8.10). 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 pass it
too, so the oracle stops sharing the defect it tests.
Check 4 of the tunnel test no longer asserts "0 proved" -- that assertion would
now forbid the gain. It brute-forces every proved tile voxel by voxel instead and
reports the count, because a false verdict leaves no geometry and no collision
behind it.
The second debt (per-room ops can raise a modifier's amplitude above the strate
params a box bound reads) turned out to be DORMANT, not live: where the source
proves Identity the modifiers' gate never opens, and where it answers Both it
supplies no MaxCarveOverBox so nothing is provable anyway. It goes live the day
the source gains one. Written at the site.
Unbuilt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
b533294f5d |
docs(opstack): record the green build and rewrite the handoff for a fresh context
OPSTACK-PROGRESS gets the measured entry: every number the run produced, what each one settles, and the single open warning. OPSTACK-PLAN's status header goes from UNVERIFIED to BUILT AND GREEN. OPSTACK-HANDOFF is rewritten end to end. It no longer describes a transition in progress but a completed one, and it leads with the two things a cold context needs: the Underwater 0%-coverage warning (with the truncation-vs-floor finding behind it, and the reminder that a green bit-identity over solid rock is not evidence), and the one task everything else now waits on -- making FRoomGraphSource::EffectOverBox answer spatially. It also carries forward the two debts that must be paid BEFORE that lands rather than after: box bounds computed from strate params can be too optimistic once a per-room op raises them, and AUDIT C2 is confirmed but unfixed on the switch path. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
46b507a261 |
docs(opstack): close the unattended run -- the post-8/8 batch and the reviewer pass
Four commits after 8 of 8, logged in OPSTACK-PROGRESS with the same shape as the first entry: what each touches, what breaks first, and the numbers to read before the colours. Records three things that only became visible while doing the work: 1. DECOMPOSITION 0.2 frames the worm as THE blocker for tile skipping. It is one of three. The subtraction also had no first term (nothing declared how solid the rock was), and the twelve detail modifiers were the bigger drag -- not through their amplitudes but because they declared a direction where they are provably Identity, being gated on the SDF the room source writes. 2. A safety property that was checked rather than assumed: no box query anywhere in the operator library touches per-worker memo state. That is what makes it safe for ClassifyTile to build and fold its own stack without clobbering the caches the density path depends on. 3. The reviewer pass over every ported operator found no transcription error. It did confirm the four things a future reader would otherwise have to re-derive: the operator order matches the original line for line, EffectiveZ is recomputed per op but bit-identically, the terrace's SDFBlendRadius is equal on both paths because no ApplyTo writes it, and LocalParams() is lazy so it reproduces the original's cost profile rather than merely its value. Next action is now one thing rather than a queue: make FRoomGraphSource::EffectOverBox answer spatially. Both of this batch's mechanism commits were built to receive it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
10dbe64b06 |
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 <noreply@anthropic.com> |
||
|
|
b888b86729 |
docs: stage A verified with real coverage; rewrite the handoff
The run that matters: bit-identical over 6000 samples with 23.9% of them in open cave, and a bake carrying 49 rooms / 56 pits / 28 chimneys / 0 columns — so the pit and chimney loops finally ran on real data, and 0 columns confirms STEP 4d stayed dormant as stage A requires. Same code and same colour as the previous green run, three different strengths of evidence. That is the argument for printing coverage numbers rather than pass/fail. Handoff rewritten for a fresh context: the three-stage TunnelNetwork plan and why stage A is verifiable while incomplete, the calls-not-transcribes rule for BuildChunkCache, FRAME ops recorded as retired (0 of 3 candidates needed one), the per-room override reclassified as load-bearing rather than polish, and the method lessons regrouped around the three coverage traps. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
68e43238bd |
test: pits and chimneys never ran — ask the bake, not the density
Check 3b earned its keep on its first run: 0 of 1500 points moved when PitDensity was zeroed, because BuildChunkCache bakes pits inside `if (!CR.RoomOp) continue;` and then reads a FRESH param struct with only that room's terrain op applied. FStrateGenerationParams::PitDensity is never read by the bake. Pits, chimneys and columns exist only through a per-room UVoxelTerrainOpDefinition, and the fixture had none. Not a product bug: those fields carry no UPROPERTY, so no editor surface offers a setting that quietly does nothing. My reading was wrong. Three attempts, and the second is the instructive one. Zeroing the params tests fields nothing reads. Building a second stack with an empty op pool would have LIED — the op pool is not in the SDF cache key (LayoutVersion covers pool edits in production), so both stacks share the thread_local cache and report "no contribution" for a third wrong reason. So: ask the bake what it baked. Rooms > 0, pits > 0, chimneys > 0, columns == 0. The test now attaches a real Pit/Chimney op pool. Safe at stage A because ApplyTo(Pit) writes only pit fields, so none of the 13 unported detail modifiers wake up — a Terrace op there would break it, which is exactly what stage B adds. Also reworded the green equivalence message, which claimed pits and chimneys were exercised while zero existed. A success message that asserts coverage instead of reporting it reads as evidence while measuring nothing. Cave coverage 1.1% -> 21%, fingerprint 0.75% -> 95.8%. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
5bf61c1815 |
test: stage A is bit-identical — strengthen the test its own counters indicted
The port is correct across 6000 samples. The only failure was arithmetic in the assertion: 4 ops + 3 structural = 7, and I wrote 6. But two numbers that PASSED are the real finding. 65 of 6000 samples landed in open cave (1.1%), so the equivalence mostly compared solid rock to solid rock — the carve, pits, chimneys and worm carve only run near the network. And 3 of 400 probe points genuinely differed between the two param sets, so the stale-cache check asked its question three times and said "fine" 397 times about points that could never have answered it. Both counters were written to say exactly this, and did. Both guards only fired at ZERO, so the run went green with the coverage of a much smaller test. A coverage guard that only trips at zero does not measure coverage, it notices absence. Both are now fraction thresholds that print a percent. RoomSpacing 80 -> 42, RoomDensity 0.35 -> 0.85. New check 3b: rebuild the stack with PitDensity = ChimneyDensity = 0 and count the points that move. Enabling a feature in the params is not evidence it fired — SDFCache.Pits can come back empty and the test stays green — and these are the two loops DECOMPOSITION §2 calls the fiddliest in the plugin. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
ef5bda3d8a |
feat: TunnelNetwork stage A — the SDF spine, wrapping BuildChunkCache
The last archetype is ~1080 lines with 13 detail modifiers, a two-region cache and a per-room op override. Porting it whole before anything can be verified is ~600 unverified lines on top of ~200 — the pattern this refactor has dodged six times. So: three stages. Stage A = vertical scale, base rock, cave warp, room graph (+ pits and chimneys), carve, worms, structural post. 6 ops. It is verifiable NOW because every detail modifier is amplitude-gated and FStrateGenerationParams already defaults them all to zero — zeroing SurfaceRoughness sends the ORIGINAL down exactly the path stage A ported. TunnelNetwork stays OFF in UsesOperatorStackForChunk until stage C. The decision that matters: FRoomGraphSource CALLS BuildChunkCache and EvaluateSDFCached rather than transcribing them. That is where §8.4's two-region window-invariance discipline lives; a transcription would fork it, and the fork would be "validated" by a test comparing it to the original. Only the ~60 lines of glue are transcribed. FRAME ops are retired. All three candidates are now ported and none needed one: CaveWarp's scope is exactly one operator (pits/chimneys read unwarped coords), VerticalScale is a one-line pure function, and the island warp was already local. Not missing infrastructure — one idea seen three times from a distance. Also: check 3 was going to compare two interleaved param sets against the original, which would have FAILED — the original's SDF cache key has no params, so it serves B the rooms it built for A. Comparing there measures its bug, not the port. Rewritten against each stack evaluated alone. The same reasoning suggests a live production staleness across Gradient transitions; filed in AUDIT §C2 as SUSPECTED with the check that would confirm it, since it rests on a premise I have not verified. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
96e75abe57 |
feat: port FloatingIslands — the stack that runs backwards
6 of 8 archetypes ported. This one starts from VOID and FILLS where the other four start from ROCK and CARVE, which is what it was worth doing: neither end of the pile needed a new operator, only the opposite sign. FConstantRockSource -> FConstantFieldSource(+/-Base) AllSolid <-> AllAir FSdfCarveOp -> FSdfConvertOp(Sign = +/-1) carve <-> fill FSdfRoughnessMod 4th archetype, unchanged Only the island blob source is new. Multiplying by +/-1 is exact in IEEE-754, so the three already-green ports are bit-for-bit untouched. ClassifyBox can return AllAir for the first time in the plugin, and an island strate is by construction mostly empty — the test counts AllSolid and AllAir separately so an aggregate cannot hide whether that fired. Two bounds that would have been holes if assumed rather than derived: the island bound is one-sided (a hairline thread of matter hangs below each island down its axis, so only the TOP may reject), and the domain warp displaces X and Y independently, so the pad needs WarpAmp*sqrt(2). Also: AUDIT C1 was NOT closed. The 2026-07-27 sweep matched `SeedF * K` and this archetype's warp spells it `(float)S * K`, so one site survived — at seed 2e9 the warp flattens and every island snaps back to a perfect circle. Fixed in both paths in one pass so the equivalence test stays a valid oracle. Expect island silhouettes to change at large seeds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
f5d5a03ad1 |
docs: end-of-day handoff — 5 of 8 ported, 11 tests green
Rewrites OPSTACK-HANDOFF.md for a fresh context: the port table, what is left in order, the two things Phase 2 invented that were not in the original design (height space as a second operator family, and IVoxelBiomeField so ops depend on a capability rather than the generator), and the method lessons that cost build cycles to learn. Open items, none blocking: perf (parked by Jahni), C9's library half (no measured risk), VerticalShafts' pessimistic box verdicts, and ClassifyTile still using hand-written guards while ClassifyBox sits verified but unconsumed — which is where the measured tile-skipping would actually become frames. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
d0a9ce3018 |
fix: the column key omitted the params — my perf fix broke the overhang
VerticalShaftEquivalence is bit-identical (966 samples inside a shaft), so operator
reuse across archetypes is measured now, not intended.
But SurfaceHeightEquivalence failed: 69/20000 overhang samples differ, 1 crossing the
isosurface. Cause is the ColumnKey from
|
||
|
|
3acb3fbc6b |
feat: port VerticalShafts — three ops reused from Maze unchanged
The port that tests the thesis rather than the fidelity. Previous ports asked whether the decomposition reproduces the original; this one asks whether operators actually get reused across archetypes, which is section 2.5's claim and the only reason to do this refactor instead of tidying the switch. ConstantRock, SdfRoughness and SdfCarve are Maze's, reused without a line changed. 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 (freq 0.1 vs 0.12, window rough+4 vs R+rough+2). New: FShaftFieldSource (infinite cylinders + hash-gated connectors into the SDF channel) and FShaftLedgeMod (banded shelves on the +X/+Y half so the shaft stays climbable). Deviation from section 6, stated: it suggested splitting the source so the XY-pure cylinder half could get an exact box verdict. Kept as one op because the connectors derive from the same 3x3 roll and the ledge mod needs the shaft list anyway, so splitting means rolling twice or sharing a cache between two ops. Forfeited: the exact verdict on the cylinder half. Kept: a conservative EffectOverBox testing circles and connector reach. FShaftLedgeMod gates on the POST-roughness Sdf as the stack left it; re-deriving it would use the pre-roughness value and shift every ledge. Reading the channel rather than recomputing is what the two-channel sample is for. Compile fix: FCells was declared below the functions returning it. Member bodies are deferred, return types are not. Ported: Maze, FlatPlain, CrystalChamber, SurfaceWorld (biomes included), VerticalShafts — 5 of 8. UNVERIFIED: not compiled past the FCells fix. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
f3faa3b5c2 |
perf: the column memo threw itself away every chunk
Jahni measured what I had only flagged: generation is slower on the op-stack path. Two compounding causes. The memo was keyed on InstanceId, which changes on every stack rebuild — every chunk. GSurfColCache, the cache this path replaced, is keyed on (XY box, StrateKey, Seed, LayoutVersion) with no ChunkZ, deliberately shared down the whole vertical strate stack. So a 4-chunk strate recomputed every column four times, including the cliff's four extra structural samples per column. And the table held 256 entries where a chunk is CHUNK_SIZE^2 = 1024 columns, so it thrashed against itself within a single tile before any cross-chunk question arose. 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 x 59 KB). The memo is thread_local so it already survived rebuilds; only the key was discarding the contents. Sharing across chunk Z is sound because heights are XY-pure by type and the biome field is documented Z-independent — the same justification GSurfColCache rests on. ColumnKey starts at InstanceId rather than 0: slots initialise to Key = 0, so a zero key would falsely hit the pristine slot at (0,0). Without PrepareChunk you get per-instance caching, which is less sharing but still correct. This may not close the gap entirely and I am not claiming it does. Virtual dispatch and the hashed lookup vs a direct-indexed box both remain; they are smaller than a 4x column recompute, but "smaller" is a guess until measured. UNVERIFIED: not compiled. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
c277931a08 |
feat: SurfaceWorld complete — biome blending wired, guard removed
The two biome checks added last commit printed nothing on success, so a passing run was indistinguishable from a block that never executed — the exact flaw I flagged twice this session and then wrote myself. Both now report their coverage. Step 2c closes SurfaceWorld: - FSurfaceColumnSource takes per-biome params and an OWNED IVoxelBiomeField. Empty params leaves the original path bit-for-bit unchanged. - The field is owned by the stack rather than borrowed: the adapter points at GetDensityAt's thread_local biome context and cache, and the stack is itself thread_local rebuilt in the same refetch block, so all three live and die together. Structural ownership beats a convention the next reader has to infer. - The overhang amp blends across biomes — Lerp(Amp(PD), Amp(PN), W) with slope and threshold from the dominant only, as ComputeSurfaceColumn does. Interpolating the slope would be meaningless; it measures the terrain rather than configuring it. - FGeneratorBiomeField lives in VoxelGenerator.cpp, on the side that knows the generator. The op sees a capability, never an owner — which is what lets it become an asset in Phase 3. - The no-biome guard is removed from UsesOperatorStackForChunk. Also: the two constructors now delegate to one body with one id counter. The first draft had two competing counters, one 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. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
6ad9f37a65 |
feat: the Mask combiner — biome blending in height space
Biome blending needs to ask which biome is at an XY, and the real answer is a warped Voronoi with a per-chunk cache on UVoxelGenerator. The op must not hold a generator pointer — Phase 3 wants ops to become assets, and one that owns a generator never can. So it depends on IVoxelBiomeField, a two-line interface returning (dominant, neighbour, weight), and the adapter that knows the generator stays on the generator's side. Same move as cliff -> structural: depend on the capability, not the owner. FBiomeBlendHeightSource holds one complete height stack per biome and lerps the HEIGHTS in the border band. Each biome's stack computes its own relief and gates its own terrace, exactly as the original makes two independent full calls and blends only the outputs. Blending heights rather than params is what keeps borders continuous across any param difference. The ceiling SELECTS the dominant instead of blending, because that is what the original does. Reproduced as-is rather than improved — a blended sky cap changes the world's silhouette and a port is not where that gets decided. Tested against a synthetic field rather than the real resolver: the resolver has its own coverage, while a synthetic field sweeps the weight 0 -> 1 continuously, which is where an inverted lerp hides. Five weights x 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. UNVERIFIED: not compiled. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
353c023dfd |
test: C9 is only half fixed — the libm half survives FPSemantics
All 9 tests green. LargeSeedSurvives proves C1 fixed by property rather than by comparison: seed 2e9 (what FMath::Rand produces) now yields 400 distinct heights over 400 samples where it previously gave a constant field. The three equivalence tests stayed green through an 85-site rewrite. The digest's NearIso warning fired at 2/115000 and its text blamed the /fp:fast vs precise split, which is fixed. First instinct was "stale warning, soften it". Checked instead, and the risk is real by a different mechanism: sinf/cosf are not specified by IEEE-754. FPSemantics = Precise makes MSVC and Clang agree on expression evaluation and says nothing about the math library; MSVC's CRT and glibc's libm may differ by ~1 ULP. FMath::Sin/Cos are used throughout the density path — layer lines, ribs, room placement, rotations. So C9's compiler half is closed by construction and its library half is not, and no build flag can close it. The measurement was also over-stating by ~100x: a single 1e-4 band is far too wide for a libm-scale delta (~1e-6 absolute at densities of magnitude ~10). Replaced with a three-band profile; only the tight band warns. UNVERIFIED: the reworded test. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
cd4cf216f5 |
fix: AUDIT C1 — bounded, site-salted seed offsets across all 85 noise sites
The op stack had already inherited this three times and every remaining port would copy it again, so fixing it now is cheaper than after. The audit's documented fix was wrong: bounding SeedF while keeping the * 97.7f multiplier still reaches 1.6e6, where the ULP is 0.19 — 9.5x the per-voxel step. Less spectacular, still broken, ticket closed. VoxelHash::SeedOffset(Seed, SiteKey) inverts the roles: the multiplier no longer decorrelates by amplifying, it IDENTIFIES the site, and the hash decorrelates. Output is in final units, bounded to [0, 16383], so the ULP is 10% of a voxel step. Site-salted, so two seeds must collide at all ~50 sites rather than sharing one global bucket. Safe to apply without compiling because the transformation is a pure regex and the literal stays visible at the call site, so each line remains eye-checkable against the original. Applied to all three files in one pass so the archetype switch and the ported ops changed identically — had they not, the three equivalence tests would say so. 62 + 7 + 16 sites, none left, plus two bare `+ SeedF` worm sites by hand. New test VoxelForge.Determinism.LargeSeedSurvives (seeds up to 2e9) because the equivalence tests are structurally blind to this: they compare the stack against the switch, both read the same faulty expression, so at a large seed both collapse identically — bit-identical, green, and both flat. An oracle that shares the bug cannot see it. This test asserts a property instead of a comparison. EXPECT EVERY WORLD TO LOOK DIFFERENT: this re-rolls every noise offset in the plugin. Intended, and covered by OPSTACK-PLAN 2.6.1. UNVERIFIED: not compiled. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
f7ed9407bf |
feat: wire SurfaceWorld (biome-less) + fix a column-memo perf trap
All six checks green, 9399 samples inside the overhang window. The single-entry column memo was correct only if the caller walks a Z column before changing XY, which the mesher does not promise. Iterating X first within a Z slice would miss on every voxel and re-run the whole height stack per voxel, cliff resamples included — an order of magnitude on the plugin's most expensive archetype. The tests could not have caught it: they sample random XY, where a one-entry memo and a 256-entry one behave identically. Only reading the access pattern finds this. Replaced with a direct-mapped 256-entry thread_local table hashed on the XY bit patterns, full key compared on hit, so a collision costs a recompute and never returns the wrong column. Wiring: UsesOperatorStackForChunk returns true for SurfaceWorld only when the strate has no biomes. The original blends heights toward the neighbouring biome across the border band; the stack evaluates one param set, so a biome strate would get a hard seam at every border rather than a subtle shift. The guard sits beside the archetype list so "can this strate take the stack?" stays one question in one place, and GetDensityAt keeps a defensive CP_BiomeCtx check that falls back if the two disagree. UNVERIFIED: not compiled. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
4dc55b1af3 |
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> |
||
|
|
2b2afacd9e |
feat: SurfaceWorld step 2a — the bridge to density space; fix IsXYPure on the slab
Height stack is green: bit-identical to ComputeSurfaceTerrainZ on both passes, including all four F20 terrain ops on. MaxDisplacement is loose (27% used) and left that way — loose only costs CPU, tight-but-wrong is a hole. FIX: FSlabVoidSource::IsXYPure() returned true and that was wrong. The contract is "Eval does not depend on Z", and Eval computes min(Z - floor, ceil - Z). Section 3.1 made the SURFACES XY-pure; the density is a distance to them and never can be. I conflated the two while writing the operator that quotes the warning against it. Latent only because nothing reads the flag yet — and step 2b is where it would have gone live, since a generic T1.a column cache keyed without ChunkZ would have shared one density down the whole vertical chunk stack. AUDIT 6.3 says that corrupts every chunk silently and ValidateDeterminism would not catch it. That is also the clearest argument for the height-space split: what is XY-pure is the HEIGHT, and in VoxelHeightOp.h it lives in a type with no Z to get wrong. Step 2a: - FSkyCapHeightSource: the ceiling is an altitude, so it belongs in height space rather than density space as section 5 had it — same category slip as the terrain ops. The subtraction happens later, in the combine. - FSurfaceColumnSource: consumes both height stacks, IsXYPure false. - BuildSurfaceStack: source + 3 structural, no per-column memo inside the op since T1.a already exists one level up and a second cache key is a second thing to get wrong. NOT covered, and the test header now says so: the overhang (GetSurfaceDensity passes OverhangAmp = 0, so only the cached path computes it) and biome blending. Both are step 2b; do not wire SurfaceWorld into a biome or overhang world before then. UNVERIFIED: not compiled. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
921a9fb666 |
feat: height-space operator family — SurfaceWorld step 1, and C10 is solved
Maze and Slab now report BIT-IDENTICAL: FPSemantics = Precise, set for cross-platform play, dissolved the ULP residue. Hypothesis 3 had the right mechanism all along — under /fp:fast the compiler transforms by surrounding context with no isolable axis, which is exactly why five one-variable experiments all came back negative. Removing the permission removed the difference. Nobody solved C10; C9 got fixed for an unrelated reason and C10 fell out of it. SurfaceWorld step 1 forced an architectural decision. DECOMPOSITION section 5 notes the height ops operate on Z values rather than density, then lists them as children of FHeightfieldSource. Writing them made the consequence unavoidable: they do not fit IVoxelDensityOp. No input Z (they produce one), XY-pure per column rather than per voxel, and they write neither channel. Forcing them in would need a per-voxel channel for a column property, or one opaque op — section 2.5's failure mode. So height space gets its own contract: VoxelHeightOp.h (FVoxelHeightSample with Height + Relief, IVoxelHeightOp, FVoxelHeightStack) and five ops. Relief is the original's M — produced by the structural source, consumed by the terrace gate. Section 0.1 found density needed a second channel; this found terrain needs a second space. The type system now forbids for free what AUDIT 6.3 warns about: a height stack cannot hold Z-dependent data because there is no Z in the signature. Deliberately staged — this touches nothing on the density path. If height space had not decomposed cleanly, it shows up here for one test rather than after building the adapter, the column cache integration and the dispatch on top. The test runs twice; the second pass is load-bearing because the F20 terrain ops are off by default, so a defaults-only run leaves all four modifiers untested. It also brute-forces MaxDisplacement, since a false bound would later be a hole. ComputeSurfaceTerrainZ moved private -> public for the test, same justification as GetSlabDensity. Old declaration removed. UNVERIFIED: not compiled. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
25df4fceff |
build: complete the IWYU tail — APawn in VoxelWorld.cpp
VoxelWorld.cpp:526 dereferences the pawn, so APawn must be complete; Casts.h only forward-declares it. Adds GameFramework/Pawn.h, and PlayerController.h which was complete transitively only — the same fragility this change removes. My earlier scan covered Public/ only. The shared PCH served .cpp files too, and APawn was named in Build.cs's own error list. Everything else in the module compiled, so this is the entire tail. UNVERIFIED: not compiled. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
6a60732c98 |
build: FPSemantics = Precise + clear the IWYU debt that blocked it
Jahni: cross-platform play (Linux and Windows, either side hosting) is a product requirement, so fix C9 at the cause instead of measuring the symptom. Verified in UE 5.7 source rather than assumed: VCToolChain.cs:1264 Default -> /fp:fast Precise -> /fp:precise ClangToolChain.cs:712 Default -> -ffp-contract=off Precise -> -ffp-contract=off Default really did mean opposite float models per platform; Precise collapses them onto the same one, so a Windows host and a Linux client agree by construction. Losing the shared PCH is what the IWYU debt hid behind. Every use turned out to be a pointer, TWeakObjectPtr or TSubclassOf parameter, so forward declarations suffice; only the templates and macros needed real includes. Seven headers fixed. VoxelDensityVolume.h was the one worth catching: it tests ENABLE_DRAW_DEBUG in an #if, and an undefined macro there is silently 0 — the debug block would have vanished without a warning rather than failing the build. Include paths verified against the engine tree, not guessed. Expect a residual tail; the shared PCH hid these for years and only a build enumerates them all. Build.cs now says so, and says the fix is to add the include rather than revert FPSemantics. Also adds VoxelForge.Determinism.CrossPlatformDigest: SHAPE digest (sign of density = the world) and FIELD digest (bit-for-bit) over a fixed integer grid, plus NearIso to bound how many samples could flip sign at all. Reports rather than asserts until pinned. The cross-platform comparison itself is deferred per Jahni. Expect a perf regression from losing reassociation and contraction on a noise-heavy hot path — measure against ARCHITECTURE 8.10. UNVERIFIED: not compiled. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
f1fd1e0b05 |
docs: new acceptance bar (peer agreement, not fidelity) — and C1's documented fix is wrong
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." Recorded as OPSTACK-PLAN 2.6.1, superseding 2.6's "recognisably the same place". Consequences, each recorded where it will be found: - C10 closed permanently rather than parked: it measures old-path vs new-path agreement and the two never coexist in a shipped world. - The equivalence tests keep their value as PORT-CORRECTNESS checks, not fidelity checks. Isosurface hard-fail stays; ULP grading is diagnostic only. - C9 promoted to top open risk. "Two people share a seed" is exactly what /fp:fast weakens across toolchains, and a Linux dedicated server against Windows clients compiles the density path under opposite float models. FPSemantics = Precise is the fix and the IWYU debt now blocks something that matters. - C1 unblocked: it was deferred only because it re-rolls the world's noise. Then, doing C1's arithmetic before applying its documented one-liner: THE FIX IS WRONG. It bounds SeedF but keeps the * 97.7 multiplier, so the coordinate term still reaches 1.6e6 where the ULP is 0.19 — 9.5x the ~0.02/voxel step. It would have left the bug live for mid-range seeds while closing the ticket. The real fix deletes the multipliers: they only decorrelate the ~40 noise sites, which is a hashing job. VoxelHash::SeedOffset(Seed, Site) gives a site-salted, bounded, final-units offset. Bounding SeedF alone would also funnel every seed through 16384 offsets shared by all sites; per-site salting requires a collision at all ~40 sites instead. The op stack has already inherited the bug via FSlabVoidSource, so it must land in both paths at once — and every further port copies it again. Docs only; the C1 fix is not written. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
51d8db842b |
test: fix the ULP yardstick — it measured output density, not where the error is born
The tuned pass warned: 121/20000 differ, 6 over the bound, worst 1.72e-05, still 0 isosurface crossings. The port is fine; the bound was wrong. It was 16 * max(|Old|, 1) * FLT_EPSILON — ULPs on the OUTPUT density. But density is min(Z - Floor, Ceil - Z), so near the isosurface the output tends to 0 while the intermediates are in the hundreds. Rounding born at scale ~400 judged against a yardstick of scale 1: 400x too tight, and tightest exactly where the test looks hardest. Large amplitudes are what expose it, which is why the tuned pass earned its place immediately. Measured rather than assumed: amplitudes rose x2.25-3.33 and the deltas rose x4.5, with the worst delta at 0.345 ULP of |Z| — sub-ULP at the scale it is born in. Error proportional to amplitude is ordinary rounding. A wrong noise offset or a missing abs() would move the surface by voxels, four orders of magnitude above this. The bound now scales with max(|Old|, |Z|, strate Z bounds), and the warning prints the discriminator instead of just the alarm: the density at the offending sample and the delta in ULPs of the working scale. A few ULP at near-zero density is cancellation; thousands is drift. That distinction is now readable rather than re-derivable at a build apiece. The box verdicts held under the worst case: 32/60 proved uniform, 0 unsound, under tripled ceiling roughness and 3x the columns — exactly the case that stresses the Max(CeilZ - noise, FloorSurface + 2) clamp. Also recorded in DECOMPOSITION section 3: FlatPlain and CrystalChamber render identical in the live world because nothing in the content distinguishes them. The merge loses no distinction; it reveals there was none. UNVERIFIED: the corrected bound. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
85993199fb |
test: the slab test proved less than it claimed — add the pass that varies CeilingRoughness
SlabEquivalence came back green (FlatPlain 36/60 and CrystalChamber 40/60 tiles proved uniform, vs zero for today's ClassifyTile; 52/20000 ULP-scale diffs, 0 isosurface crossings). But both archetypes reported the SAME 52 and the same worst delta, which pointed at the fixture: FTestWorld::Build sets only GeneratorType, so both slots carry DEFAULT slab params. So the two passes were the same configuration at two depths. The test claimed to demonstrate "one op, two archetypes" while never varying CeilingRoughness — the only field that actually distinguishes CrystalChamber. The differing tile counts come from the slots' Z ranges, not from the archetypes. Third pass added: CrystalChamber(tuned), CeilingRoughness 6 -> 20, rougher floor, 3x the columns. It varies what matters and doubles as the worst case for the ClassifyBox amplitude bounds — a large CeilingRoughness widens the ceiling band and makes the FloorSurface + 2 clamp far more likely to bind, which is precisely where a false verdict would be a hole. The default params were too gentle to stress it. The ULP residue is left alone: deterministic, 0 isosurface crossings, and the same shape C10 already cost six builds to prove not worth chasing. UNVERIFIED: the third pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
644339def5 |
feat: Phase 2 first port — FlatPlain + CrystalChamber collapse into one op
Jahni closed OPSTACK-DECOMPOSITION 3.1: the slab noise Z term was not intentional character. Phase 1 also closed — the visual A/B on Maze passed. Two changes, deliberately together, kept attributable by the test: 1. Design: GetSlabDensity's floor and ceiling noise lose their Z terms. A floor height no longer depends on the altitude you sample it from. The ceiling keeps its + 3000.0f, which is a decorrelation offset, not a Z term. The world re-tunes once — a different slice of the noise field, not a worse one. 2. Refactor: the now-XY-pure function ports to FSlabVoidSource + FGridColumnMod plus the three structural ops. BuildSlabStack has NO branch on archetype because GetSlabDensity never had one — CrystalChamber is FlatPlain with a bigger CeilingRoughness. 8 archetypes -> 7. SlabEquivalence compares against the reference AS IT IS NOW and runs the whole battery on both slots, so green means the port is a pure refactor and any visual delta is attributable to the Z-term removal alone. The attribution comes from the test, not from splitting it across two builds. The payoff 3.1 was actually about: FSlabVoidSource::ClassifyBox is exact and needs no sampling. FBM is contractually [-1,1], so both surfaces live in Z bands with known bounds — a tile below the floor band is provably solid, a tile between the bands provably air. ClassifyTile proves zero tiles for these archetypes today. FGridColumnMod answers Identity when no column reaches the box, which is what lets the source's AllAir verdict survive the fold. UNVERIFIED: not compiled. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |