9d2324879db604167dd955b049fd324f5624fcea
102
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
9d2324879d |
feat(world): the XY edge seal -- the fourth structural invariant
VF_ApplyBoundarySeal was purely VERTICAL: it sealed each strate's ceiling
and floor and there was no horizontal equivalent, because until the depths
were bounded the world had no sides. The GDD now says "bounded, but
gigantic", so a player who walked far enough reached unterminated
generation running off into nothing.
Built as a FORCING op, appended automatically and not author-omittable, so
an invented strate cannot forget its own edge. Because a forcing op proves
AllSolid in its band, the outer shell also becomes free to skip in tile
classification -- correctness and a perf win from one change.
order spine -> vertical seal -> passage carving -> XY EDGE SEAL
-> disturbances -> diff
A near-rim passage may pierce a vertical seal; the XY seal runs after and
wins. A passage must never punch a hole through the world's edge.
ClassifyBox/ForcedMarginOverBox proved via the closest AABB radius with a
one-voxel margin: 8 boxes proved, 1 tile proved, 600 voxels brute-forced,
0 violations. FXYEdgeSealOp is IsXYPure. Max rim discontinuity .187 against
a 1.0 threshold -- a ramp, not a cylinder wall.
⚠️ WorldRadiusVoxels DEFAULTS TO 0, and that is deliberate. Codex flagged
that DA_Settings and BP_VoxelSettings predate the property and so do not
serialize it -- they inherit the C++ default silently. A default of 8192
would have walled in every existing world on next editor open with nothing
in the asset to explain it. 0 is a bitwise no-op (45 interior comparisons
against the unbounded path prove it). The GDD's intended ~8192 is a value
to set in the asset, deliberately, once. Documented at the declaration.
Codex found four spec errors, three of them mine: VF_ApplyBoundarySeal
lives in VoxelDensityPrimitives.h not VoxelCaveMorphology; the asset
default trap above; and my "density increases through the band" wording
contradicts the MC sign convention (solid is more NEGATIVE) -- the test
measures increasing solidness.
Build clean, 21 succeeded / 0 failed, run and verified by me.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VJiTcTSDNkKBtnNdiBtdaU
|
||
|
|
44938f3f98 |
fix(measure): a blocked route is not a disconnected pair
VF_AreConnected took the shortest coarse BFS route, re-walked it at full resolution, and on hitting a wall returned false and gave up -- never looking for another way round. That is a false NEGATIVE, and it was inflating the failure count of a bug we were actively trying to fix. On refutation it now excludes that specific coarse step and re-searches, up to a bounded budget (16). Outcomes are distinguished honestly: Connected a route survived full-resolution verification NotConnectedAtThisResolution no coarse route remains after exclusions CoarseLiedBudgetExhausted UNKNOWN -- routes kept being refuted The third is deliberately NOT folded into either other verdict. Collapsing "unknown" into "disconnected" is precisely what misled us. arrival->departure 7/16 -> 14/16 All 7 former COARSE_LIED became CONNECTED. arrival->spine 15/16 connected departure->spine 15/16 connected per-archetype law: 6/6 applicable routes pass Retries needed are small -- max 3 per query, 0 budget hits -- so 16 is generous rather than load-bearing. Runtime slightly IMPROVED (45.3 -> 44.9 s on the 16-seed sweep) because a satisfied query stops early. The false-POSITIVE guard is untouched and is now stronger in meaning: every Connected still requires a fully re-walked, air-verified route, and it now says "this specific route is open" rather than "the first route I tried was open". The controlled wall still returns BudgetExhausted after exactly 16 retries, and a repeat query is bit-identical in verdict, snap flags and retry count. ⚠️ REMAINING: seeds 6 and 14 are genuinely NOT_CONNECTED. Real or a step-2 resolution limit is not yet settled -- refine before touching generation again, per COMPOSER-NOTES §13. No generation, placement or parameter changed. Verifier only. Build clean, 20 succeeded / 0 warnings / 0 failed, confirmed in VoxelM.log. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VJiTcTSDNkKBtnNdiBtdaU |
||
|
|
9d5633ad54 |
fix(passages): aim the UPPER mouth too -- Tier 1 was only half built
VF_SuggestLandingPoint was called exactly once per passage, for the LOWER
mouth. The upper mouth fell out of the control-point chain with no landing
query at all: Tier 1 fixed where a player ARRIVES and never touched where
they DEPART. That predicted the measurement exactly -- departure->spine was
0 connected across all 16 seeds.
The ring proxy could never have caught this. It checks each mouth for open
space individually, and an unaimed mouth still has open space around it,
because the passage tube carved some. It took a connectivity measurement.
Each mouth now queries ITS OWN strate independently, which is what keeps
the layout a set rather than a sequence -- passage i never reads passage
i-1, so LayoutOrderIndependence stays green. Seed contracts kept unmixed;
SurfaceWorld still refuses; a declined query still preserves today's
placement rather than degrading it.
departure->spine 0 -> 9 connected of 16
arrival->departure 4 -> 7 of 16
aiming coverage upper 6/7 and lower 6/7 mouths now aimed at
generator-confirmed open space
⚠️ ACCEPTANCE STILL FAILS at 7/16, and the breakdown says why it is not
what it looks like:
arrival->departure: CONNECTED 7, NOT_CONNECTED 2, COARSE_LIED 7
Only TWO seeds are genuinely disconnected. Seven are COARSE_LIED -- the
coarse BFS found a route and the full-resolution walk hit solid on it.
VF_AreConnected checks that ONE route and gives up, so a blocked candidate
is reported as failure even when another route exists. That is a false
NEGATIVE in the verifier, and it means this acceptance number probably
understates the world. Next task fixes the measurement, not the world.
Codex was killed mid-flight by a session teardown, so the edits arrived
unvalidated; I built and ran the suite myself rather than assume.
Build clean, 20 succeeded / 0 warnings / 0 failed, verified in VoxelM.log.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VJiTcTSDNkKBtnNdiBtdaU
|
||
|
|
7a20eeed0d |
feat(shafts): a drainage tree -- the field goes from 22% to 97% connected
The plateau was the tell. Roughness changed the component COUNT (it
manufactures noise pockets) but the giant component sat at ~0.22 in every
variant, and widening connectors past the proven roughness bound barely
moved it. That is a random graph below its percolation threshold: 0.6
sites, 0.35 bonds, and a 1.6-cell pair cutoff that drops many neighbour
pairs once jitter spreads them 1.7 (orthogonal) to 2.4 (diagonal) cells
apart.
So the lever was topology. Every shaft now emits a deterministic connector
to the nearest shaft STRICTLY closer to the origin, so chains drain inward
and terminate at the spine. The probabilistic connectors stay as texture
and loops -- the tree guarantees you can get out, the noise keeps it from
reading as a diagram.
Deliberately NOT tuned: raising CrossConnectChance/ShaftDensity/the cutoff
toward the percolation threshold buys a world connected at one seed and
shattered at the next, which is exactly the 1-in-16 behaviour we had. A
phase transition is not a knob.
seed 1337 components 30 -> 17, largest share .220 -> .975
16 seeds spine is the largest component in 16/16 (was 8/16)
largest share .58 - .9999
Seam safety, because a parent computed from a 3x3 window would differ
between chunks and the connector would exist in one and not the other:
collect over 7x7, emit only for the inner 3x3, whose 5x5 candidate windows
are then fully contained. 154 seam probes per path, 0 mismatches.
Box verdicts re-checked against the new air: 27 proved, 33 mixed,
0 violations. Stack mirror bit-identical over 20,000 samples.
Perf: 1.634 us forced rebuild, 0.100 us hot call -- the 49 rolls happen
only on cache rebuild, never per voxel.
⚠️ ACCEPTANCE STILL FAILS: arrival->departure 4/16 (was 1/16). Reported,
not tuned. With 97% of the air in one component, two mouths that both land
IN the field would essentially always connect -- so the remaining failure
is placement, not topology. Next.
Build clean, 20 succeeded / 0 warnings / 0 failed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bp7FxoQcSoiuhxjuLr6aBM
|
||
|
|
58e61cdefa |
feat(shafts): the spine joins its neighbours -- necessary, not sufficient
Jahni chose: make the (0,0) column join the shafts it passes, so VerticalShafts behaves like the other seven archetypes. Built, and it works as far as it goes -- the spine is now the LARGEST air component in 13 of 16 seeds (share up to 0.81, was 0.096 and never largest). ⚠️ IT DOES NOT MEET THE ACCEPTANCE CRITERION. arrival->departure is still 1/16. Reported as a failure rather than dressed up, because the criterion was the measured law, not "the code was written". The test now prints ACCEPTANCE arrival->departure: FAIL (1/16 seeds pass) so it cannot be quietly forgotten. Why it is insufficient, and it is structural: the spine only joins its own 3x3 cell neighbourhood, while passage mouths land 60-200 voxels from (0,0). The departure network is unreachable from the spine in all 16 seeds. Joining the hub to its neighbours does not help if the rest of the graph never reaches the hub. Part A diagnosis (seed 1337, margin 0, step 2, radius 192): baseline 30 components, largest .220 roughness 0 17 components, largest .226 connector radius 8 29 components, largest .229 H1 is real but partial: roughness fragments the space (13 of the 30 components are noise pockets), yet widening connectors past the proven roughness bound barely moves it. So roughness is NOT what severs the network -- the largest component sits at ~0.22 in every variant, which is the signature of a graph below its percolation threshold. The lever is topology, not radius. H2 (the hardcoded 1.6 pair cutoff) went untested. Implementation notes: the spine is added ONLY as a connector endpoint, not as a carving shaft -- VF_ApplyOriginSpine remains sole owner -- and is excluded from ledge placement. Spine connectors use radius 6.625, above the proven roughness supremum of 5.625 (sup|FBM| = 1.5, not 1.0). SpineRadius is threaded through BuildVerticalShaftStack, not hardcoded. Op-stack mirror bit-identical over 20,000 samples, 0 box-verdict violations. LayoutOrderIndependence green. Build clean, 19 succeeded / 0 warnings / 0 failed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bp7FxoQcSoiuhxjuLr6aBM |
||
|
|
5519b323c8 |
test(measure): the spine is the backbone in 7 of 8 strates -- and not in
VerticalShafts Diagnosis only. No generation or placement changed. The hypothesis was that the (0,0) spine (OriginSpineRadius 14) is the universal connector, so "can this mouth reach the spine" would be the real primordial-law test. Measured at margin 0, step 2, radius 192: strate spine share largest? arr->spine dep->spine TunnelNetwork 0.979 YES CONNECTED not connected FlatPlain 1.000 YES CONNECTED CONNECTED CrystalChamber 1.000 YES CONNECTED CONNECTED Maze 0.997 YES coarse-lied CONNECTED SurfaceWorld 1.000 YES CONNECTED CONNECTED VerticalShafts 0.096 NO not conn. not connected FloatingIslands 1.000 YES CONNECTED CONNECTED Underwater 0.999 YES CONNECTED n/a The spine forms one uninterrupted open interior run in 8/8 strates, and holds essentially ALL the air in 7 of them. VerticalShafts is the lone anomaly: the spine punches through as an isolated column holding 9.6% of the air and is NOT the largest component. So the archetype has three mutually unreachable things -- the spine, the arrival network, the departure network. It has no lateral connectivity at all. Over 16 seeds: spine open 16/16, spine largest only 8/16, arrival->spine 1 connected, departure->spine 0 connected. Neither extreme was right. The spine is not a universal connector, but it is far from irrelevant: it is the de-facto backbone everywhere except here, which reframes the bug as an ANOMALY to bring in line rather than a missing feature to invent. Ordering hazard confirmed on the record: making passage i depend on passage i-1 would break LayoutOrderIndependence. Both candidate fixes avoid it, because the spine is a fixed deterministic anchor that two mouths can target independently. Build clean, 19 succeeded / 0 warnings / 0 failed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bp7FxoQcSoiuhxjuLr6aBM |
||
|
|
0bbfdeb7a7 |
test(measure): VerticalShafts breaks the primordial law in 15 of 16 seeds
The margin is exonerated. Every earlier run excluded 8 voxels at each strate boundary, and voxels 4-8 of those are ordinary interior, not seal -- the same window mistake that produced "FlatPlain is 99.95% void" at margin 32. So the query was re-run at margin 0, 2, 4 and 8. All four stay negative; margin 0 includes the seal itself and still reports negative. (The implementation had clamped explicit margins to >= 1, which made the margin-0 check impossible; Codex found and fixed that.) Diagnosis -- this is not a near-miss wall: arrival LowerPoint (65.8, 95.8, -1047.25) departure UpperPoint (88.4, -104.6, -991.58) XY separation 201.7 voxels (~50 m), Z delta 55.7 arrival component 22,531 cells (22.0% of air) departure component 21,621 cells (21.1% of air) nearest approach between the two components: 135-179 voxels Two large, healthy, entirely separate shaft networks half a strate apart. Nothing to breach; the archetype has no lateral connection at all. 16-seed sweep: 1 pass, 15 fail. Not one unlucky world -- structural. So: descend into a VerticalShafts strate and you probably cannot reach its exit. This is the "live hole in the shipping game" COMPOSER-NOTES Tier 1 predicted, now measured instead of assumed. Stated precisely, because it names the fix: VF_SuggestLandingPoint makes each mouth land in open space, but NOTHING makes the two mouths of one strate land in the SAME open space. New: VF_DiagnoseConnectivity returns component sizes and the spatial separation between the two mouths' components, so a near-miss and a never-meant-to-connect read differently instead of both being "false". No generation change, no threshold tuning. The fix is a design decision. Build clean, 19 succeeded / 0 warnings / 0 failed. Sweeps 3.4 s + 6.9 s. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bp7FxoQcSoiuhxjuLr6aBM |
||
|
|
cd31dec869 |
test(measure): the VerticalShafts disconnection is real, not a coarse lie
Our guard was one-directional. FullResolutionPathIsAir catches the false POSITIVE -- coarse says joined, reality says walled -- which is the dangerous direction for safety, so we built it first. The opposite error was unguarded: a corridor narrower than SampleStep falls between sample points and vanishes, so connected regions read as separate. That is exactly what a network of narrow vertical shafts would fake at step 4. Refinement sweep, radius held constant for the controls: step 4 / r256 NOT_CONNECTED 57 comp largest .119 458,752 cells step 4 / r192 NOT_CONNECTED 32 comp largest .220 258,048 step 2 / r192 NOT_CONNECTED 27 comp largest .220 2,064,384 step 4 / r128 NOT_CONNECTED 20 comp largest .291 114,688 step 1 / r128 NOT_CONNECTED 11 comp largest .303 7,340,032 Both fixed-radius pairs stayed negative, so neither the step nor the radius explains it. At one-voxel sampling the arrival and departure mouths are still in different air components. VerticalShafts is a live primordial-law violation: a player descends into it and cannot reach its exit. NotConnected is renamed NotConnectedAtThisResolution and documents its own limit at the declaration, so a bare negative can never again be read as proof. CoarseLied names one direction; this names the other. Sweep split into VoxelForge.Generation.StrateConnectivityRefinement so the fast test stays fast. 4.4 s. The MaxCells refusal path was exercised and still refuses. Build clean, 19 succeeded / 0 warnings / 0 failed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bp7FxoQcSoiuhxjuLr6aBM |
||
|
|
efcc8fcb65 |
fix(measure): ask the primordial law the right question
Three defects, two of them mine. 1. The anchor was a guess. LargestComponentAnchor() returned a hardcoded point near the (0,0) spine and the comment admitted it, so NOT_CONNECTED meant "not connected to a guessed point". The flood fill already knows the answer; it now returns LargestComponentPoint (tie- broken by lowest cell index, so it stays deterministic), LargestComponentCells and NumComponentsAtLeast1Pct. 2. "Connected to the largest component" is NOT the primordial law. The law is that a player must not descend into a place they cannot leave -- a question about MOUTHS. A strate can be 57 disconnected shafts and be fine, provided the shaft you arrive in is the shaft you leave from. The test now asks arrival -> departure per strate: the LowerPoint of the passage arriving, to the UpperPoint of the passage departing. 3. A false verdict and a true one were indistinguishable. VF_AreConnected returned false both when no route existed and when the endpoint's own coarse cell sampled solid -- and a passage mouth is a carved tube whose cell centre lands in adjacent rock easily. It now returns EVoxelConnectivityResult and snaps to an adjacent air cell first, reporting the repair. RESULT: the earlier "5 of 15 endpoints disconnected" was an ARTIFACT of the guessed anchor plus the wrong question. Corrected: 13 of 15 connected, 0 StartCellSolid, 0 GoalCellSolid. One real finding survives. arrival -> departure passes for 5 of 6 applicable strates; VerticalShafts FAILS -- its arrival mouth is in the largest component, its departure mouth is not. NOT yet reported as a bug: a coarse grid can falsely SEPARATE as well as falsely join, and a connecting corridor thinner than SampleStep vanishes. Next task settles it at a finer step. Also noted: GetPassages() contains a self-referential surface entry (0 -> 0), excluded from the chain law. Build clean, 18 succeeded / 0 warnings / 0 failed, verified in VoxelM.log. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bp7FxoQcSoiuhxjuLr6aBM |
||
|
|
13399529b5 |
fix(measure): measure the strate, not the band above its floor
The window excluded a whole chunk (32 voxels) at each end to avoid a 4-voxel seal band -- 8x too much. On 4-chunk strates that discarded half of every strate, floor included, so the pass was measuring the open air above the ground and calling it the strate. The margin now derives from the seal that actually exists: ceil(2 x BoundarySealThickness), clamped to [1, height/4], overridable via InteriorMarginVoxels. The fixture resolves to 8 voxels. FlatPlain/CrystalChamber walkable fraction: 0.00044 -> 0.0578, ~132x. The "99.95% empty void" reading was the window, not the world. Metrics now carry ResolvedMarginVoxels, SampledMinZ and SampledMaxZ, and the test prints them. A measurement that does not state the volume it measured is exactly how the previous defect survived review. VerticalShafts largest-component share rose only 0.074 -> 0.119 across 57 components: the window explained part of the swiss-cheese signal, not all of it. That one is at least partly real. Coarse-lie guard still load-bearing: 24 routes checked, 3 refuted at full resolution (was 21 / 2). Build clean, 18 succeeded / 0 warnings / 0 failed, confirmed in Saved/Logs/VoxelM.log rather than from the agent's summary. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bp7FxoQcSoiuhxjuLr6aBM |
||
|
|
6988e5ace1 |
feat(measure): one grid, one flood fill -- the strate measurement pass
Tier 2 slice 1 from COMPOSER-NOTES.md 3.4. A read-only, deterministic, headless measurement of a built strate: sample a coarse grid once, flood-fill the air once, derive every metric from that same grid. What it answers today: air fraction, air component count and largest component share (the swiss-cheese signal), walkable fraction, median feature scale, median vertical clearance -- and whether two points are actually connected. The connectivity verdict is the point. Coarse sampling LIES about it in the dangerous direction: a wall thinner than the sample step vanishes and two sealed caverns read as joined. So VF_AreConnected recovers the actual BFS route and re-walks that one corridor at one-voxel spacing; a true verdict is never returned on coarse evidence alone. It fired: 2 of 21 routes were coarse false positives. The test carries a negative control, because a green suite that cannot fail is what hid the last defect. Two of them, in fact: a direct probe of the known-solid inter-strate gap (AirFraction 0.00042), and -- Codex's improvement on the spec -- a diff-layer box filled solid and measured through VF_MeasureStrate itself, so an inverted sign is caught end to end rather than around the function under test. Build clean, 18 succeeded / 0 warnings / 0 failed. KNOWN, fix in flight: the interior window excludes a whole chunk (32 voxels) at each end to avoid a 4-voxel seal band -- 8x too much. On the fixture's 4-chunk strates that discards the floor, so the reported FlatPlain/CrystalChamber air fractions of 0.999 describe the open band above the floor, not the strate. The engine is right; the window is not. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bp7FxoQcSoiuhxjuLr6aBM |
||
|
|
8cd5face9b |
fix(strate): aim at the room's centre, not beside it
Commit
|
||
|
|
1ab8c0cae4 |
feat(strate): passages aim at open space -- for half the archetypes, measurably
Tier 1. GeneratePassages placed every passage at a random angle, distance and reach, never consulting the destination strate. The carved tube is air by construction, so nothing looked wrong -- but whether the lower mouth joined the destination's connected cave space was luck. A player could descend and arrive in a sealed pocket. The primordial law was a hope. VF_SuggestOpenPointZ is a pure free function of (archetype, params, seed, XY): no operator stack, no StrateManager, no cache. That is deliberate. This runs inside Initialize, where the layout is half-built and LayoutVersion is in flight, so anything holding a manager pointer there is a re-entrancy trap. Coverage is four archetypes of eight, and the number is the deliverable: 7 passages, 3 aimed at generator-confirmed open space with 48/48 ring samples air, 4 falling back. The hole is not closed. It is measured, where this morning it was invisible. Two corrections from Codex, both mine and both would have shipped confident nonsense: room placement is seeded by MakeStrateSeed, not the passage salt, so my version would have queried a cave graph that does not exist; and slabs need the world seed because that is what GetSlabDensity uses. And one of mine caught in review: the first test sampled GetDensityAt at the mouth and asserted air, which is true by construction because the passage carve is a structural-post invariant. It could not fail. Replaced with a 16-point ring outside the carve radius requiring 8/16 air -- a proxy for connectivity, not a proof. The proof is a flood fill, which is Tier 2. Logging: my first spec asked for a per-passage warning on fallback. It fired 572 times and turned 14 clean tests yellow, which is precisely the anti-pattern OPSTACK-HANDOFF.md warns about -- a warning that always means "this is fine" teaches people to ignore warnings. Now one sorted Log line per layout build. Built clean, suite green: 17 succeeded, 0 with warnings, 0 failed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DPUuCtn5zTLui6WybLbkNb |
||
|
|
10a8bbc677 |
feat(strate): the layout is a set, not a sequence -- and the suite is green
Tier 0. Reordering Settings->StratePool in the editor used to change every world in the game, because Initialize Fisher-Yates shuffled the array in editor order. The pool is now sorted by stable soft-asset path before the shuffle, so the layout is a pure function of (seed, pool contents as a SET). The selection algorithm itself is untouched -- same Fisher-Yates, same cycling, same FixedStrates handling, same Z ranges. Sorting by pointer would have looked right and been load-order dependent, which is the trap here. GeneratePassages walked one sequential FRandomStream across every strate, so changing an early connection count shifted every later passage. Each draw is now a salted VoxelHash of (seed, boundary index, connection index), with the distributions preserved exactly -- ToFloat01 returns [0,1), so Lerp(Min,Max,t) reproduces FRandRange precisely. Codex implemented it and caught a real flaw in my spec: I asked for a test that permutes the pool and asserts the passages match, but once the layout is order-independent that test cannot fail whether or not the passage fix works -- the layout comes out identical, so even the old sequential stream would agree. It added the perturbation that actually bites: change an early connection count and verify every later boundary keeps its own values. It also contradicted me on passage type being randomly drawn. It isn't, and it preserved that. Built VoxelMEditor clean, then ran the suite headless: 16 succeeded, 0 failed, 0 not run. That also closes the thread OPSTACK-HANDOFF.md left open -- |
||
|
|
302cf16681 |
fix(world): the voxel field is in ACTOR space -- make every entry point agree
The city's hole is not at world (0,0), so the AVoxelWorld actor has to move.
It couldn't: the plugin was only HALF actor-relative, and had been for a long
time without symptoms because the actor always sat at the origin.
Already correct: GetBiomeAtWorldLocation, GetVoxelSurfaceHeightAt,
UpdateDecorations, UpdateLandmarks, UpdateWater, UVoxelDensityVolume::Update,
the passage debug draw, the clipmap material.
Not correct: the streaming centre, the streaming anchors, all six carve/fill
entry points, GetStrateAtPosition, and UVoxelAtmosphereManager::UpdateForPlayer
-- whose own comment documented the assumption out loud ("actor at
origin/identity"). Carving would have landed offset; the strate lookup would
have been quietly wrong by the actor's Z.
The trap worth recording: because the content managers already convert
internally, converting PlayerLastPos once at the source would have DOUBLE-
converted four call sites. The fix has to be per-boundary. That was only
visible after reading each consumer -- the estimate before that read was wrong
in both directions.
WorldToLocalCm / WorldToLocalVoxel / LocalVoxelToWorld are now declared as the
only sanctioned boundary between Unreal coordinates and voxel coordinates, and
the real deliverable is the invariant next to them: no `/ VOXEL_SIZE` applied
to a parameter named `World*` outside those three functions. A grep can check
that. The risk was never a missed site among the ones found -- it is the next
one nobody notices.
Also adds a translation-only guard in BeginPlay. Tiles, ClassifyBox verdicts,
the clipmap window, culling and streaming distance all assume world-axis-
aligned boxes; a rotation or non-unit scale breaks that structurally rather
than gracefully, so it logs an Error naming what was set.
With the actor at the origin every conversion is identity, so this is a no-op
until the actor actually moves. Built and confirmed working.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DPUuCtn5zTLui6WybLbkNb
|
||
|
|
dfa1d5f5b1 |
test(strate): the X-macro guard -- a warning in prose does not fail CI
FStrateGenerationParams::Lerp blends the hand-written VF_STRATE_PARAM_FIELDS list, not the struct. Add a field to one and not the other and it compiles, tests green, and silently takes its default inside every Gradient/Interleaved transition band -- terrain that changes shape at the boundary and nowhere else. CODEMAP has warned about this in prose for a month. The test expands the X-macro a third way (after LERP and SNAP), into a list of names, and diffs it against the struct's UObject reflection. No fixture, no world -- a shape test, instant. GExemptFieldNames is empty on purpose: every reflected field is covered today, so an exemption has to be written down as a decision rather than happening by omission. The stakes rise with the world composer, which intends to invent parameter sets through this same Lerp: a missing field would then be constant across every invented strate, with nothing to notice it by. (Its CODEMAP §3.12 row lands in the following commit, which also touches that file for an unrelated reason.) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DPUuCtn5zTLui6WybLbkNb |
||
|
|
91585ea493 |
test(tunnel): a warning that always means "this is fine" is noise -- demote it
Jahni asked whether a "warning stage" he had been seeing for several sessions meant something was broken. It did not: the only Warning in that run is the tunnel test's "[dense fixture] No tile was proved", whose own text explains that 0 proved is the EXPECTED and correct result there -- cull spheres cover the dense fixture ~3.6x, so no box can be outside all of them. Test passes, density bit-identical, all coverage non-zero, violations 0, and production defaults reports 11 proved / 14641 voxels / 0 violations right below it. But a warning that fires every run and always means "fine" trains the reader to ignore warnings, and this one quietly worried him across several sessions. RunTileScan now takes bZeroProvedIsExpected: AddInfo on the dense fixture where zero is the only possible answer, AddWarning on production defaults where zero would be a real regression from 11. Same message, severity now carries meaning. Same defect as the lumped bail counter one layer out: a signal that always says the same thing measures nothing. Test-only. Not built. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
4d33321bfa | feat(classify): fold cave boundary categories safely | ||
|
|
871ca190af |
fix(classify): use the public GetStrateChunkZBounds; the index accessor is protected
VoxelGenerator.cpp(2796): C2248 -- FindSlotIndexForChunkZ is protected. I claimed it was public "verified" in the previous commit. The grep printed the declaration line, not the access specifier above it. Same shape as the partial read that produced a false accusation earlier today. GetStrateChunkZBounds is public (VoxelStrateManager.h:177), returns false for exactly the no-slot case, and ClassifyTile already calls it three times for the same question. Wrapped in a VF_ChunkZHasSlot lambda so the condition reads as the predicate it is. No accessor promoted, no API widened. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
a2c5e02713 |
fix(classify): out-of-layout chunks are constant AIR, not TunnelNetwork -- unblocks T1.d
Measured in game: Cave Bail Not Op Stack No Layout = 1.58 of 1.90 classified tiles (83%), with SoleSlot and BoundaryTile never firing. The assets were correctly ticked all along. Cause: GetGeneratorTypeForChunk returns TunnelNetwork for any chunk outside the strate stack, commenting "the fallback density path will produce solid rock anyway". That comment is FALSE. GetGenerationParams for SlotIdx < 0 returns BaseDensity = -1, RoomDensity = 0, WormStrength = 0 -- a CONSTANT AIR field. IsGapChunk's "open air, NOT a gap" was the correct description. So every tile touching the open air above the world looked like a cave archetype, entered the cave branch, found no layout slot, and bailed. T1.d was never failing -- it was unreachable, behind a routing mistake in the archetype lookup that had nothing to do with the operator stack, the box verdicts, or the flags. Fix: ClassifyTile gains a fourth Z category for no-slot chunks. It sets bAnyNonCave (never enters the cave branch) and bCanSolid = false (AllAir survives), and bails to Mixed if a disturbance could add rock there -- chasms only carve, so they cannot threaten an air verdict. A tile entirely above the stack now resolves AllAir and is skipped: the majority of a surface flight, a class T1.d has never been able to prove. Classifier only -- no density value changes. Equivalences must stay bit-identical and violations must stay 0. Not built -- Jahni builds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
4ae9e6e72f |
fix(strate): opt-in diagnostic goes Verbose under automation, not Log
The verbosity fix stopped the diagnostic failing the suite, but it still printed at Log on every Initialize -- which the fixture calls dozens of times per run, burying the actual test output under hundreds of identical lines. Under GIsAutomationTesting it now logs at Verbose (off by default, still available with -LogCmds). Editor and game keep Warning, where it is actionable and fires once per rebuild. The information was worthless in tests anyway: the fixture's opt-in state is known by construction, and the one test that needs the opted-in variant (OpStack.ClassifyTileSoundness) already reports "all 7 cave layout slots have Use Operator Stack enabled" on its own. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
9740d117e0 |
fix(strate): opt-in diagnostic must not fail the automation suite
Sol's opt-in warning logs at Warning. UE's automation framework counts a Warning as a failure, and the Determinism.* tests deliberately build a NON-opted-in world -- that world is their comparison oracle, the baseline every OpStack.* number is measured against. The diagnostic was failing the suite by correctly reporting an intentional configuration. Verbosity is now Log under GIsAutomationTesting and Warning otherwise. The message is written once and only the verbosity branches (Printf then log %s): four copies of a long format string across two sites would have been exactly the "one definition, not two kept in sync" failure this project has rules about. CoreGlobals.h included explicitly for GIsAutomationTesting -- IWYU, no shared PCH here. Note for the record: the tests must NOT be switched to the op stack. The non-opted-in fixture is the oracle, not an oversight. The diagnostic also did its job: 7/7 cave slots report disabled in the fixture (slot 4 absent = the SurfaceWorld narrowing working). The same line read from the GAME log is now the ground truth for whether T1.d has ever been enabled in production. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
45c61dd00e |
fix(opstack): hoist the acquired column box out of the integer-XY scope
VoxelDensityOpStack.cpp(832): C2065 'Box' undeclared. The six-box LRU refactor moved acquisition to `FColumnBox& Box = Cache.Acquire(...)` inside `if (bIntegerXY)`, but the Computed flag is set after the column is computed, outside that block. The previous single-box version had Box at function scope so the write-back compiled. Hoists `FColumnBox* AcquiredBox` beside MemoColumn and writes back through it. The guard becomes `if (AcquiredBox)` instead of `if (bIntegerXY)`: non-null implies the integer path, so the pointer proves its own safety rather than relying on two conditions staying in agreement. No caching or logic change. Other Box. uses (761-763) are in scope, verified. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
f90c4e56c3 |
fix(generator): key CP cache by world owner
Function-static thread_local CP state previously trusted only chunk coordinates and each manager's locally-reused layout version, allowing a worker to carry params, biome context, CP_UseOpStack, and the built stack into another world. Allocate each generator a monotonic owner ID and add one uint64 comparison to the hot key. This intentionally fixes only the proved CP_* path; other TLS caches remain outside this change. |
||
|
|
1e02c6314f |
diagnostics(opstack): separate disabled-slot bail causes
The old Not Op Stack counter fired before slot attribution, so interior disabled strates and boundary encounters produced the same number. Split the instrumentation into sole-slot, boundary-tile, unresolved-layout, and late-recheck counters while preserving every ClassifyTile condition, return point, and verdict. |
||
|
|
8295f6e76b |
perf(opstack): retain six surface column regions
The single direct-indexed box discarded all 6,561 computed columns whenever spatial or column identity moved. Port the reference six-box LRU so interleaved regions keep five warm working sets, accepting the documented ~0.79 MiB TLS cost per worker to preserve the recommended and measurable A/B path. Also report disabled cave opt-ins at layout initialization; SurfaceWorld is excluded because its exact-lattice tile proof does not use that flag. |
||
|
|
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>
|
||
|
|
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>
|
||
|
|
21f62c4a70 |
docs(opstack): two comments still described a half-finished port that is finished
Both said the refactor was mid-flight and both contradicted the code around them. 1. BuildTunnelNetworkStack: "stage A of three, the twelve detail modifiers and the per-room override are not ported yet, which is why UsesOperatorStackForChunk returns false for TunnelNetwork." All three stages are done, the twelve modifiers are added a dozen lines below the comment, and that function returns true. 2. FIslandBlobSource: "two of the three frame users are not ported yet; revisit when TunnelNetwork brings the second real use." TunnelNetwork is ported, and BuildTunnelNetworkStack already records the verdict: CaveWarp wraps exactly one operator and VerticalScale is a pure scalar function, so ZERO frames out of three candidates. The question was answered; only this comment still asked it, and it would have sent the next reader off to build frame infrastructure that was explicitly decided against. A comment that contradicts the code beneath it is the "read the code, not the comment" trap in reverse. This file already carries the forward version of that lesson, at the cliff modifier whose comment promises a gradient it never samples. Comments only -- no code changed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
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
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
ce409e7bb1 |
test(opstack C2): floor-divide the sampled chunk range, and DIAGNOSE the Underwater 0% instead of guessing
The build is green: 14 tests, TunnelNetwork A+B bit-identical over 6000 samples, all twelve group probes non-zero, all 8 roughness variants identical, 0 gate leaks, C1 proved (10 Terrace rooms, 1119 samples inside them). One warning fired, and it is the one the test exists to fire: Underwater (stage C2): 2000 samples, 0 of them in open cave (0.0%) A GREEN BIT-IDENTITY OVER 2000 SAMPLES OF SOLID ROCK IS NOT EVIDENCE. It is exactly what two agreeing voids look like -- the 1.1% run of stage A, again, in a different slot. A REAL BUG FOUND WHILE DIAGNOSING IT The sampled chunk-Z range used Z / CHUNK_SIZE, and C++ integer division TRUNCATES TOWARD ZERO. TunnelNetwork sits at the top of the layout in positive Z, where truncation and floor agree, so it could not show there. Underwater sits at the BOTTOM, in NEGATIVE Z: -1 / 32 is 0 by truncation and -1 by floor, so the upper chunk bound starts one notch too high and the Clamp that follows piles the excess onto the strate's very last voxel -- inside the top seal band, i.e. solid rock. Fixed in both point builders via FloorDivChunk. Same family as the DivideAndRoundDown lesson already in the project notes: truncation costs a build cycle every time it is assumed to be a floor. That is a CANDIDATE cause, not a conclusion, so the commit does not stop there. NEW CHECK 5b -- ASK, DO NOT INFER Three causes produce "no sample in open cave" and they are fixed differently, so each now has its own number: rooms baked for the Underwater strate index (cause: the bake), samples landing inside the seal-free interior (cause: the sampled Z range), and the two together (cause: the XY spread). The info line says explicitly how to read them. Sampling also widens from 8 clusters to 24, matching the main scan. Fourth application of the rule this archetype keeps teaching: the check that explains a zero must be able to fail for exactly one reason. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
6a8390f11f |
feat(opstack): ClassifyTile consumes ClassifyBox for cave archetypes -- the T1.d prize, behind the same opt-in
⚠️ THIS IS THE ONE COMMIT OF THIS RUN WHOSE FAILURE MODE IS A HOLE, NOT A REGRESSION. A non-Mixed verdict makes the world skip GenerateMesh entirely: no triangles, no collision, invisible until a player falls through. Read VoxelForge.OpStack.ClassifyTileSoundness before trusting it. WHAT CHANGED ClassifyTile used to `return Mixed` without a call for every cave archetype ("archétype cave [...] pas prouvable en v1"). It now builds that strate's stack and folds ClassifyBox. SurfaceWorld and bedrock gaps keep their hand-written proofs untouched -- the exact-lattice column test is strictly better than any box bound, so the stack has nothing to add there. ONE DEFINITION OF THE STACK MAPPING, and that is the load-bearing part GetDensityAt's ~90-line build switch is extracted into VF_BuildOpStackForChunk and both callers now use it. A second copy would be the worst bug available here: a tile skipped on the verdict of a stack that is not the one producing its density is precisely a hole. A "keep these in sync" comment would not have been enough; there had to be only one. Params are passed in by pointer, never fetched inside, because both callers already have them. SIX GUARDS, ALL FAILING TO MIXED, none giving the benefit of the doubt 1. The strate must actually opt in -- on EVERY chunk coord the box touches, not just the one that triggered the attempt. Otherwise the classifier judges a field the mesher will not produce. 2. One cave slot per tile. Two slots means two param sets, and a stack answers only for its strate. 3. No mixed cave/surface/gap tile: the cave stack's box would cover Z belonging to another strate. 4. The params must be BIT-IDENTICAL across every chunk coord the box touches. This is the guard that matters, and it exists because of the finding committed earlier today: GetGenerationParams blends params inside a strate (Alpha depends on chunk Z for Gradient, and on chunk XY as well for Interleaved), so one stack genuinely cannot represent a tile that straddles a transition band. Memcmp on POD: differing padding can only produce a false MISMATCH, i.e. one Mixed too many. 5. A 27-chunk-coord cap, so a very wide tile does not pay for the check. We give up the gain, never the safety. 6. The disturbances are folded by hand (chasm ⇒ CarveOnly, bridge/ridge ⇒ FillOnly), because DECOMPOSITION 10.2 leaves them OUTSIDE the stack -- GetDensityAt applies them after the negate. A verdict that ignored them would be wrong exactly where they act. Same inequalities the SurfaceWorld branch already uses. The diff layer needs no new guard: ClassifyTile already returns Mixed for any tile with player mods in range, before any of this. TEST: VoxelForge.OpStack.ClassifyTileSoundness -- the same brute-force oracle as the existing ClassifyTileSoundness, on a world where every strate opted in. It does not check the fold (that is BoxVerdictFold) or the operators (those are the eight equivalence tests); it checks the WIRING. The number to read first is the count of tiles actually brute-forced: zero non-Mixed verdicts would mean the test proved nothing, so that case is an ERROR rather than a quiet pass. Its failure message lists the four suspects in the order worth checking. FIXTURE: FTestWorld::Build gains a bUseOperatorStack parameter (default false, so the thirteen existing tests still exercise the switch), and every test world now gets a PROCESS-UNIQUE LayoutVersion. That second change fixes a real cross-test hazard that was only ever hidden by an accident: PassagesVersion is per-instance and starts at 0, so two FTestWorlds both reported 1, and GetDensityAt's per-chunk caches are keyed on (ChunkCoord, LayoutVersion) -- one world could be served the previous world's params AND its CP_UseOpStack flag. Invisible while every world agreed the flag was false. The first world that ticks it removes that coincidence, in both directions. WHAT THIS BUYS TODAY: Maze, FlatPlain/CrystalChamber, VerticalShafts and FloatingIslands can now prove tiles in production, which is where the measured skipping (Maze 23/60, slabs 36-40/60) turns into frames. TunnelNetwork and Underwater still prove nothing: their chain dies at FRoomGraphSource, which answers Both with unknown amplitude. Making it answer spatially means building the SDF cache for the queried box -- now worth doing, since a skipped tile saves 30k+ density evaluations, and the amplitude fold plus the modifiers' Identity inheritance are already in place to receive it. That is the next piece. And nothing here changes Jahni's current world: no strate asset has bUseOperatorStack ticked, so every guard above is unreachable in his project until he ticks one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
239c037f35 |
feat(opstack): the twelve detail modifiers inherit the room source's box verdict
Follow-on to the numeric fold, and the reason that commit does not yet pay off on TunnelNetwork. Separate commit because it is a separate mechanism with a separate revert story: the fold change was about AMPLITUDES, this one is about IDENTITY. THE OBSERVATION Every one of the twelve detail modifiers is gated on bNearCaveSurface, i.e. on Sdf < BlendRadius*3, and Sdf only becomes finite if the room graph wrote something. So wherever the source proves that no room and no tunnel reaches the box, Sdf stays FLT_MAX across the whole box and all twelve are IDENTITY -- not merely bounded, not merely CarveOnly-with-a-small-number. Identity. Each of them already knew this per voxel (it is their first `if`) and still declared Both / FillOnly / CarveOnly per box, which killed deep bedrock's AllSolid hypothesis exactly as hard as an operator that genuinely acts there. Twelve over-cautious declarations, one cause: they were not asking the source they depend on, although since C1 they already hold a pointer to it. VF_NoCaveOverBox(Rooms, Box, Ctx) short-circuits each EffectOverBox to Identity when the source itself answers Identity. Strictly conservative: it never returns Identity on its own authority, only where the source already did. A null pointer also means Identity, and that is correct rather than convenient -- with no room source in the stack, Sdf is FLT_MAX everywhere and the gate never opens. FCaveRoughnessMod gains the pointer purely for this. It is named RoomsForBox and documented as NOT for Eval, because that op deliberately reads STRATE params rather than LocalParams() and mixing the two up is the exact mistake C1's note exists to prevent. ⚠️ WHAT THIS BUYS TODAY: almost nothing, and that is worth stating rather than implying. FRoomGraphSource::EffectOverBox still answers Identity only when RoomDensity <= 0. The short-circuit becomes the deep-bedrock switch on the day the source answers SPATIALLY -- its room and tunnel bounds are already in the SDF cache; what it costs is building that cache for the queried box, which only pays once ClassifyTile actually consumes ClassifyBox. The wiring is put in now so that day touches ONE place instead of thirteen. No test change: this can only turn Both/FillOnly/CarveOnly into Identity where the source already returned Identity, so no existing verdict can move. The TunnelNetwork test still asserts 0 proved verdicts over 40 tiles, and it should still hold. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
353d504169 |
feat(opstack): the box fold carries NUMBERS -- amplitude bounds alongside EVoxelOpEffect (DECOMPOSITION 0.2)
Its own commit, nothing else in it, because it changes the contract all thirteen tests rest on. VoxelForge.OpStack.BoxVerdictFold is extended in the same commit, as required. THE PROBLEM, restated because the fix only makes sense against it Direction alone can never recover a FIELDED carve. A noise-threshold carve (TunnelNetwork's worms, wall roughness) has no spatial bound: it answers CarveOnly on every box of every strate that enables it, and that answer is TRUE. So it kills AllSolid everywhere and the archetype skips zero tiles. No refinement of EffectOverBox can fix that. But the AMPLITUDE is bounded, and for the worm it is trivial: the block only runs below the threshold, so t = 1 - WormValue/WormThreshold is in [0,1] and NetworkMask is in [0,1], hence the carve is at most WormStrength. "The rock is solid by more than the sum of every remaining carve" is therefore provable. THE CONTRACT - IVoxelDensityOp gains MaxCarveOverBox / MaxFillOverBox (density units, FLT_MAX = unknown) and ForcedMarginOverBox (how far the density is guaranteed from zero, 0 = unknown). - FVoxelBoxHypotheses gains SolidMargin / AirMargin. A forcing op sets one; each carve subtracts its amplitude; the hypothesis dies when the margin is no longer strictly positive. - VF_ForceHypotheses and VF_FoldEffect take the new values as DEFAULTED parameters. BACKWARDS COMPATIBILITY IS THE POINT, and it is structural rather than promised: the defaults are FLT_MAX and 0, so an op that overrides nothing subtracts FLT_MAX from a margin of 0 and kills the hypothesis exactly as the purely directional fold did. Not one existing verdict moves. The float arithmetic is deliberately unguarded: 0 - FLT_MAX is -FLT_MAX, -FLT_MAX - FLT_MAX saturates to -inf, -inf > 0 is false, and no NaN is reachable because both terms share a sign. BOUNDS DECLARED (each proved from the code, not estimated -- over-estimating costs CPU, under-estimating is a hole) - FConstantFieldSource::ForcedMarginOverBox = |Value|. This is the missing FIRST TERM: without a source that states how solid the rock is, there is nothing for a bounded carve to be subtracted from, and every bound would still kill the hypothesis. - FWormFieldSource: MaxCarve = WormStrength (the bound that had been written and unused since stage A), MaxFill = 0. - FCaveRoughnessMod: 1.4 * SurfaceRoughness * VOXEL_NOISE_SCALE both ways. - FLayerLineMod (LayerLineDepth), FRibbingMod (RibbingDepth), FScallopMod (ScallopStrength), FCaveOverhangMod (SCALE * Depth * Strength). WHAT THIS DOES *NOT* DO YET, said plainly rather than implied TunnelNetwork still proves ZERO tiles. The chain dies at FRoomGraphSource, which answers Both with unknown amplitude, before any of the bounded ops are reached. Making it answer spatially means building the SDF cache for the queried box, which only pays once ClassifyTile actually consumes ClassifyBox -- so it belongs with that work, not here. The one case that changes today is a tunnel strate with RoomDensity <= 0: the room source returns Identity and the bounded worm + roughness can now leave AllSolid standing. Seven ops keep the FLT_MAX default (terrace, cliff, arch, column, dome, pinch, floor bias). Their amplitudes depend on room-relative data, and bounding them would change no verdict while the room source is unbounded. Writing bounds nobody can consume is how a bound goes stale unnoticed. ONE PRE-EXISTING DEBT MADE SHARPER, noted at every site: these bounds are computed from STRATE params, and a per-room terrain op can write a LARGER amplitude (ApplyTo overwrites even where the strate had 0). So a bound can be too small -- the dangerous direction. Same root as the too-optimistic EffectOverBox flagged in C1, same fix, and it MUST land before ClassifyTile consumes ClassifyBox. TEST: eight new blocks in BoxVerdictFold. The first two are the ones to read -- they assert that the defaults reproduce the old fold exactly, and that "unknown" is not "zero". Also asserted: carve amplitudes accumulate; equality loses the tile (the > is strict on purpose, since zero counts as air at the mesher); a bounded Both no longer kills a margin it cannot cross; and nothing bounded can resurrect an unprovable box. 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> |
||
|
|
e479fcdf7d |
feat(opstack C2): Underwater reaches the tunnel stack from a second case + its own equivalence check
STAGE C group 2. No operator, no parameter, no new stack: Underwater IS TunnelNetwork.
PREMISE VERIFIED BEFORE BUILDING ON IT (the lesson this refactor has paid for four times)
OPSTACK-DECOMPOSITION §8 claims there is no density difference at all. Both halves were re-read
rather than assumed:
- GetDensityAt already puts `case Underwater:` and `case TunnelNetwork:` in the SAME arm, both
calling GetDensityWithParams (VoxelGenerator.cpp, the production switch);
- a repo-wide search for WaterLevelRelative shows FStrateGenerationParams' copy is read only by
UVoxelStrateManager's water-level query (render side) and by the SurfaceWorld beach op through
a DIFFERENT struct. GetDensityWithParams never reads it.
So the wiring is one more `case` above the existing one, sharing CP_Tunnel.
ONE REAL DIFFERENCE FROM THE FIVE CASES ABOVE IT, stated in the code
No degenerate-strate guard. The other five archetypes have one because their density functions
early-out to `return 1.0f` on zero height and the stack has no such early-out by design.
GetDensityWithParams has no early-out at all -- read line by line, not assumed. Adding a guard here
would make the stack diverge from the switch on degenerate strates, in exactly the direction the
guard exists elsewhere to prevent.
TEST (same commit): new check 5 samples 2000 points in the Underwater SLOT, with the same room-op
pool attached to that definition before anything is evaluated. This is not tautological even
though both paths call the same function: the Underwater slot is a different strate, hence a
different StrateIndex, hence a different bake seed and a different entry in the strate-index memo.
It exercises what six chunks of slot 0 cannot -- that the stack follows the right room set when
two strates of the same archetype coexist in one world.
The failure message says explicitly that if the TunnelNetwork equivalence is green and only this
one fails, the finding is a real density difference between the two archetypes, which contradicts
§8 and is worth more written down than patched.
UsesOperatorStackForChunk still returns false for both. That is C3.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
9591088d34 |
feat(opstack C1): per-room op override -- eleven detail ops read LocalParams() from the room source
STAGE C group 1. No operator added: nineteen ops before, nineteen after. What changed is what ELEVEN of them READ. THE MECHANISM, AND WHY §2's "NO CLEAN HOME" PROBLEM DISSOLVED FRoomGraphSource gains LocalParams(): the strate params with the nearest room's hash-rolled UVoxelTerrainOpDefinition applied on top, exactly as the original's `FStrateGenerationParams LocalTerrainParams = Params; ... NR.RoomOp->ApplyTo(...)` shadow does. Every detail op that read the shadowed copy now reads that. OPSTACK-DECOMPOSITION §2 called this the piece with no clean home and proposed giving each modifier an "only inside room N" predicate. The difficulty came entirely from assuming each modifier must OWN its params. One op owns the shared state, the rest read it -- and that pattern was already established here twice (FOverhangShelfMod <- FSurfaceColumnSource, FShaftLedgeMod <- FShaftFieldSource). No scoping predicate was invented. Same shape as the pit/chimney resolution. ELEVEN, NOT TWELVE, AND THAT IS READ FROM THE CODE Surface roughness (4b) does NOT read the per-room copy: the original's `const FStrateGenerationParams& Params = LocalTerrainParams;` is declared INSIDE the `if (bNearCaveSurface)` block, which begins after step 4b. Ported that way and flagged in the op so it does not get "uniformised" later. MEMOISED PER VOXEL, AND WHY THAT IS FIDELITY RATHER THAN OPTIMISATION The original builds the copy ONCE per voxel inside the gate. Eleven ops calling LocalParams() would build it eleven times. So FState carries a valid-flag that FRoomGraphSource::Eval clears at the top of every voxel (before any early-out, so nothing can read the previous voxel's room), and the first modifier that asks pays for it. Deep rock pays nothing, exactly as before. The ~74-field copy itself is TRANSCRIBED AS-IS. It is a real per-voxel cost; recorded in OPSTACK-PROGRESS as a perf item, not "improved" here. ONE THING THIS EXPOSES, WRITTEN DOWN BEFORE IT BITES EffectOverBox still answers from the STRATE params, because a box spans many rooms and a per-voxel copy has no meaning there. Since ApplyTo writes the op's value even where the strate's was 0, a room op can ENABLE a modifier the strate had off -- so a box verdict can now be too optimistic on a strate that has a terrain-op pool. Harmless today (nothing consumes ClassifyBox in production) and it MUST be fixed before ClassifyTile does. Noted at FLayerLineMod::EffectOverBox and in OPSTACK-PROGRESS. TEST (same commit) -- this is the acceptance stage B could not make - The pool gains a Terrace op with values (3.0 / 0.95 / 1.4) deliberately far from the strate's (6.0 / 0.6 / 0.5). A stack that ignored the override now CANNOT be bit-identical. - New check 3c asks the structure, not the density, because there is still no params probe for a pool (the pool is not in the SDF cache key): how many baked rooms carry a Terrace op, and how many samples fall inside one of those rooms' influence radius. Both must be non-zero, or check 1's green means only that two paths agree where the override never applies -- which is what stage B already proved. Fourth application of the pit lesson. IF THIS GROUP IS WRONG: check 1 fails with diffs clustered inside a subset of rooms (those that drew Terrace) and terrace-shaped Z banding, while check 3c still reports non-zero coverage. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
8a303cc085 |
test(opstack B5): prove the bNearCaveSurface gate -- a voxel outside it is bit-identically untouched
STAGE B group 5 of 5, and the only one that adds no operator. Stage B is complete: 19 ops, all
twelve detail modifiers ported.
THE DECISION, restated where it is paid for
The gate is a repeated early-out inside each operator (VF_NearCaveSurface), not a scoping
container. Reasons live in the code at that function; the short form is that the stack is a flat
list ClassifyBox folds op by op, a container would have to re-implement VF_FoldOp and would hide
its children from the fold, and an op that only exists inside a container cannot become a Phase-3
asset. What that choice buys in composability it pays for in risk: TWELVE places to forget the
gate instead of one. Hence a check aimed at exactly that.
NEW CHECK 1d
Classify each sample by its final SDF (nothing downstream of the room source writes that channel),
then compare the full stack against one with the eleven controllable amplitudes zeroed:
- outside the gate the two must be BIT-IDENTICAL -- asserted, and the leak count is printed;
- inside the gate they must differ often -- printed, and ZERO is an error, because "nothing
leaked" is worthless if the answer is "nothing happened anywhere". Same trap as a coverage
guard that only fires at zero, one level up: a check can be vacuous as well as a counter.
- the outside-gate sample count is printed and warns below 10%, since deep rock is the common
case in production and a sample set that never leaves the cave does not exercise the gate.
WHAT THIS CHECK CANNOT COVER, stated in the output rather than left implicit: the column operator
(STEP 4d) has no amplitude to zero, so it cannot appear in the zeroed stack. Its gate rides on
check 1's bit-identity against the original instead. That is sound but invisible, which is why the
info line says so.
Also corrected in the test header: the modifier count is TWELVE, not the thirteen that had been
carried in the notes, and only ELEVEN of them read the per-room param copy -- surface roughness
(4b) sits before the shadow declaration and reads strate params. Both numbers matter for C1.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|