e55f12a9de634b572340d234e2e863ab97b5b631
15 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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> |
||
|
|
b063d437c1 |
feat(opstack B4): port Columns (4d), Domes (4g), Pinch (4h), FloorBias + Column op in the test pool
STAGE B group 4 of 5 -- and with it, ALL TWELVE detail modifiers are ported. 19 ops:
ConstantRock -> RoomGraph -> SdfCarve -> Roughness -> Terrace -> LayerLines -> Ribbing
-> Overhang -> Cliff -> Scallop -> Arch -> RoomColumn -> Dome -> Pinch -> FloorBias
-> Worms -> [structural x3]
Still NOT wired: UsesOperatorStackForChunk returns false for TunnelNetwork. What is missing is C1,
the per-room op override -- which adds NO operator, it changes what eleven of them READ.
TRANSCRIBED
- FRoomColumnMod (4d): walks SDFCache.Columns. Not FGridColumnMod -- that one rolls cylinders on a
world grid for FlatPlain/CrystalChamber; this one iterates a per-room bake.
- FDomeMod (4g): upward half-ellipsoid, up to 2 per room, capped at 0.85 * room radius.
- FPinchMod (4h): perimeter-anchored ellipsoid with the SideFactor term that leaves the passage
axis clear.
- FFloorBiasMod: quadratic fill below room centre, only in definite air. LAST in the chain and not
interchangeable -- it exists to undo what roughness (4b) did to floors. That is why the operator
order in BuildTunnelNetworkStack is the original's order line for line.
THE TRAP IN THIS GROUP, WHICH IS THE PIT LESSON A THIRD TIME
Columns have NO strate parameter. The per-voxel loop has no `if (ColumnDensity > 0)`, and the bake
reads OpParams (a FRESH struct with only the room's op applied), so FStrateGenerationParams::
ColumnDensity is read by nothing. Consequences, all three of which shaped this commit:
1. The `ColumnDensity = 0` line in the old DisableStageBModifiers was inert. It was never what
kept columns off; the absence of a Column op in the pool was.
2. No FeatureProbes entry can cover columns -- there is no param to switch off. And a second
stack built on a pool without the Column op would SHARE the thread_local SDF cache (the pool
is not in its key) and return identical densities, i.e. it would lie. Exactly the trap that
was documented for pits.
3. So the stage-A guard that ERRORED on TotalColumns > 0 is inverted in this same commit: it now
REQUIRES TotalColumns > 0. Asking the bake what it baked is the only sound check here.
DisableStageBModifiers is deleted rather than left empty: its emptiness is the measure of stage B
being complete, and an empty function still being called is how a step gets forgotten.
TEST (same commit): op count 15 -> 19; MakeShaftOpPool renamed MakeRoomOpPool and given a third
entry (Column, Probability 1.0 on all three so every room draws one of the three evenly); three
more param probes (dome/pinch/floor bias); the column guard inverted.
⚠️ THE POOL MUST STAY Pit/Chimney/Column UNTIL C1. Eleven of the twelve modifiers read the per-room
param copy in the original and the strate params here; those agree only while no room carries a
DETAIL-type op, and ApplyTo(Pit/Chimney/Column) writes only pit/chimney/column fields. A Terrace
entry would break the equivalence -- which is precisely the test C1 adds, and the only possible
proof that the override works.
IF THIS GROUP IS WRONG: dome/pinch/arch diffs land inside open cave near room centres and
perimeters; a column error shows up as a ring of diffs at fixed XY through the whole room height.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
6ec60099d6 |
feat(opstack B3): port cave Overhang, Cliff, Scallop, Arch (STEP 4c)
STAGE B group 3 of 5. The stack is now 15 ops: ConstantRock -> RoomGraph -> SdfCarve -> Roughness -> Terrace -> LayerLines -> Ribbing -> Overhang -> Cliff -> Scallop -> Arch -> Worms -> [structural x3] Still NOT wired: UsesOperatorStackForChunk returns false for TunnelNetwork. TRANSCRIBED - FCaveOverhangMod: fBM with Z frequency at 0.15x of XY, positive lobe only (rock extends INTO the cave, never away), quadratic fade. Note this is the THIRD unrelated thing called "overhang" in the plugin -- FOverhangShelfMod (SurfaceWorld) borrows the uphill terrain height and shares nothing with it but the name. - FScallopMod: cellular noise, positive lobe carved out. Second consumer of VoxelNoise::Cellular3D, which is the second reason B1 shared that body instead of copying it. - FCaveArchMod: the first ROOM-RELATIVE modifier -- it needs the room's hash, centre and radii, so it is what makes FRoomGraphSource::GetNearestRoomIdx() necessary. Its gate is NOT the shared one: CaveSDF < SDFBlendRadius (inside the cave), not < SDFBlendRadius*3. PORTED AS-IS THOUGH THE ORIGINAL'S OWN COMMENT DISAGREES WITH ITS CODE FCaveCliffMod's comment promises "sample density at Z+1 and Z-1, compute vertical gradient". The code samples nothing: it draws a Perlin whose Z frequency is 3x its XY frequency and names the result VertGrad. It is a gradient-shaped PROXY, decorrelated from the wall's actual slope; the multiply by CaveSDF still gives it the right SIGN either side of the surface, which is why it produces steeper faces at all. Fixing it would change the world, and saying nothing would leave it to be "fixed" later by someone reading the comment instead of the code. Ported, documented in the op, recorded in OPSTACK-PROGRESS. ONE DELIBERATE ADDITION FCaveArchMod bounds-checks NearestRoomIdx with IsValidIndex before indexing; the original tests only >= 0. The index comes from EvaluateSDFCached so it is valid by construction, which is exactly why this can never change output -- only prevent a crash if that invariant ever breaks. Same class of decision as the params fingerprint in the cache key: err on cost, never on result. Overhang is FillOnly, Scallop is CarveOnly, Arch is FillOnly; only Cliff needs Both. Four of the eight modifiers ported so far keep a usable direction for the fold, which matters for the DECOMPOSITION 0.2 work later. TEST (same commit): op count 11 -> 15, four params groups move into EnableTunnelFeatures, four more per-operator coverage probes. ArchDensity is set to 0.9 rather than its 0.1 default and the reason is written down: at 0.1 a room draws ~0.3 arches and the probe would report zero with nothing wrong. Test coverage settings are not production values. IF THIS GROUP IS WRONG: diffs concentrate inside open cave (arch) or in a band |SDF| < range (the other three), and the roughness/terrace probes stay green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
ab1a996cc5 |
feat(opstack B2): port Terrace, LayerLines, Ribbing (STEP 4c) + hoist the room source's per-worker state
STAGE B group 2 of 5. The stack is now 11 ops: ConstantRock -> RoomGraph -> SdfCarve -> CaveRoughness -> Terrace -> LayerLines -> Ribbing -> Worms -> [structural x3] Still NOT wired: UsesOperatorStackForChunk returns false for TunnelNetwork. STRUCTURAL CHANGE THIS GROUP FORCED FRoomGraphSource's thread_local SDF cache moved out of Eval into a `static FState& State()` accessor, because terracing re-queries the cache at Z+-1 and (from B4 on) columns walk it and the room-relative ops need NearestRoomIdx. Kept `static`, i.e. SHARED BETWEEN INSTANCES, on purpose: that is what the original does, and the test's stale-cache check (check 3) rests on two stacks sharing this cache while the params CRC in the key keeps them from serving each other's rooms. Making it a member would have turned that check green for the wrong reason. This is the established pattern in this file, not a new one: FOverhangShelfMod reads FSurfaceColumnSource, FShaftLedgeMod reads FShaftFieldSource, both via a non-owning pointer handed over at build time. Terrace now reads FRoomGraphSource the same way. TRANSCRIBED - FCaveTerraceMod: the Z-only SDF gradient orientation gate, the optional noise displacement of Z before the staircase, the Edge = lerp(0.45, 0.02, Hardness) stair profile, the quadratic fade. - FLayerLineMod: sine along Z, half-wave rectified, CUBED, subtracted. - FRibbingMod: same sine shifted by PI/2, rectified, SQUARED, added. THINGS THAT LOOK WRONG AND WERE PORTED AS-IS - Terrace's two gradient probes use UNWARPED X/Y and RAW Z, while the field they probe was evaluated at WARPED coords and effective Z, and they exclude pits/chimneys. So the probe does not sample exactly the field whose slope it measures. Real, in the original, load-bearing for the look; noted in OPSTACK-PROGRESS rather than fixed. - Terrace/LayerLines/Ribbing use raw WorldZ while roughness uses EffectiveZ. Deliberate in the original (geology stays horizontal whatever the strate's vertical scale). - `&& CaveSDF < FLT_MAX` is redundant under the bNearCaveSurface gate; kept. - NearestRoom is now reset to -1 unconditionally, which the original does not do (its thread_local keeps the previous voxel's value when RoomDensity <= 0). Unobservable there because no consumer runs; here the consumers are separate objects, and a stale value crossing an operator boundary is not something you find later. Ribbing and LayerLines are FillOnly / CarveOnly rather than Both -- two detail modifiers that keep a usable direction for the fold, which is more than the roughness or the terrace can say. TEST (same commit): op count 8 -> 11; the three params move from DisableStageBModifiers into EnableTunnelFeatures with spacings deliberately small against the sample window; three MORE coverage probes, one per operator rather than one per group -- a single "B2" probe would stay green while two of the three were wrong. IF THIS GROUP IS WRONG: diffs cluster where |SDF| < spacing*1.5 near horizontal surfaces (terrace) or in thin Z bands (lines/ribs). If instead the whole equivalence collapses everywhere, suspect the FState hoist, not the three new ops. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
6e29cbfe4c |
feat(opstack B1): port TunnelNetwork surface roughness (STEP 4b) + share the cellular noise
STAGE B of TunnelNetwork, group 1 of 5. The stack is now
ConstantRock -> RoomGraph -> SdfCarve -> CaveRoughness(4b) -> Worms -> [structural x3]
8 ops (was 7). Still NOT wired: UsesOperatorStackForChunk returns false for TunnelNetwork.
TRANSCRIBED
- FCaveRoughnessMod (VoxelDensityOpStack.cpp): STEP 4b literally -- two octave sets
(main + fine x3 with the +2000/+2500/+3000 offsets), the optional domain warp, the four-way
noise switch, the min(TotalRough, 0) anti-fill clamp inside definite air, and the quadratic
fade by distance from surface. Reads EffectiveZ, not WorldZ.
- VoxelNoise::Cellular3D moved verbatim from VoxelGenerator.cpp's `static CellularNoise3D` into
VoxelCaveMorphology.h; the generator keeps a one-line forwarder, exactly as FractalNoise3D and
RidgedNoise3D already do since T2.a. It lands in the cave header rather than VoxelNoise.h
because it needs VoxelHash, and VoxelNoise.h must not depend on the cave header. Forking a pure
function is how AUDIT C1 happened.
- HRidged3D added next to HFractal3D (same FVector round-trip, deliberately).
TWO THINGS THAT LOOKED WRONG AND WERE PORTED AS-IS
- The domain warp computes ONE offset and adds it to BOTH noise positions, so the main and fine
octave sets are warped identically. Two independent warps would be tidier and a different world.
- Roughness reads the STRATE params, NOT the per-room override. The original's
`const FStrateGenerationParams& Params = LocalTerrainParams;` shadow is declared INSIDE the
`if (bNearCaveSurface)` block that begins after step 4b. So eleven of the twelve detail
modifiers read the room copy and this one does not -- flagged in the op so C1 does not
"uniformise" it. (The handoff's "all 13 modifiers read the shadowed copy" is off on both counts:
it is twelve modifiers, and one of them is outside the shadow.)
STAGE B5 DECIDED HERE, WITH REASONS (VF_NearCaveSurface)
The gate is a repeated early-out per op, NOT a scoping container: the stack is a flat list that
ClassifyBox folds op by op, a container would have to re-implement VF_FoldOp and would hide its
children from the fold, and an op that only exists inside a container is not a Phase-3 asset.
Cost stated rather than hidden: the original tests once and skips twelve, the stack tests twelve
times. Measured before optimised -- that is the C10 lesson.
TEST (same commit)
- Op count 7 -> 8.
- SurfaceRoughness/DomainWarp moved from DisableStageBModifiers into EnableTunnelFeatures.
- NEW check 1b, group coverage: rebuild the stack with the group OFF and count moved samples.
Zero is an ERROR, not a warning -- a ported group that never executes is exactly how a bad
transcription survives a whole green run (the PitDensity lesson). This diff-of-stacks is sound
here although it lied for the op pool, because these are params and the params CRC IS in the
SDF cache key.
- NEW check 1c: all 4 noise types x {warp off, warp on} compared to the original over 1500 points.
The main equivalence only ever takes the FBM branch; three of four cases were untested.
IF THIS GROUP IS WRONG, what breaks first: the main equivalence reports diffs concentrated in
open cave and within |SDF| < SurfaceRoughness*2; if instead only check 1c fails, the fault is
inside one branch of the noise switch and nothing else is implicated.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
ef5bda3d8a |
feat: TunnelNetwork stage A — the SDF spine, wrapping BuildChunkCache
The last archetype is ~1080 lines with 13 detail modifiers, a two-region cache and a per-room op override. Porting it whole before anything can be verified is ~600 unverified lines on top of ~200 — the pattern this refactor has dodged six times. So: three stages. Stage A = vertical scale, base rock, cave warp, room graph (+ pits and chimneys), carve, worms, structural post. 6 ops. It is verifiable NOW because every detail modifier is amplitude-gated and FStrateGenerationParams already defaults them all to zero — zeroing SurfaceRoughness sends the ORIGINAL down exactly the path stage A ported. TunnelNetwork stays OFF in UsesOperatorStackForChunk until stage C. The decision that matters: FRoomGraphSource CALLS BuildChunkCache and EvaluateSDFCached rather than transcribing them. That is where §8.4's two-region window-invariance discipline lives; a transcription would fork it, and the fork would be "validated" by a test comparing it to the original. Only the ~60 lines of glue are transcribed. FRAME ops are retired. All three candidates are now ported and none needed one: CaveWarp's scope is exactly one operator (pits/chimneys read unwarped coords), VerticalScale is a one-line pure function, and the island warp was already local. Not missing infrastructure — one idea seen three times from a distance. Also: check 3 was going to compare two interleaved param sets against the original, which would have FAILED — the original's SDF cache key has no params, so it serves B the rooms it built for A. Comparing there measures its bug, not the port. Rewritten against each stack evaluated alone. The same reasoning suggests a live production staleness across Gradient transitions; filed in AUDIT §C2 as SUSPECTED with the check that would confirm it, since it rests on a premise I have not verified. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
96e75abe57 |
feat: port FloatingIslands — the stack that runs backwards
6 of 8 archetypes ported. This one starts from VOID and FILLS where the other four start from ROCK and CARVE, which is what it was worth doing: neither end of the pile needed a new operator, only the opposite sign. FConstantRockSource -> FConstantFieldSource(+/-Base) AllSolid <-> AllAir FSdfCarveOp -> FSdfConvertOp(Sign = +/-1) carve <-> fill FSdfRoughnessMod 4th archetype, unchanged Only the island blob source is new. Multiplying by +/-1 is exact in IEEE-754, so the three already-green ports are bit-for-bit untouched. ClassifyBox can return AllAir for the first time in the plugin, and an island strate is by construction mostly empty — the test counts AllSolid and AllAir separately so an aggregate cannot hide whether that fired. Two bounds that would have been holes if assumed rather than derived: the island bound is one-sided (a hairline thread of matter hangs below each island down its axis, so only the TOP may reject), and the domain warp displaces X and Y independently, so the pad needs WarpAmp*sqrt(2). Also: AUDIT C1 was NOT closed. The 2026-07-27 sweep matched `SeedF * K` and this archetype's warp spells it `(float)S * K`, so one site survived — at seed 2e9 the warp flattens and every island snaps back to a perfect circle. Fixed in both paths in one pass so the equivalence test stays a valid oracle. Expect island silhouettes to change at large seeds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
3acb3fbc6b |
feat: port VerticalShafts — three ops reused from Maze unchanged
The port that tests the thesis rather than the fidelity. Previous ports asked whether the decomposition reproduces the original; this one asks whether operators actually get reused across archetypes, which is section 2.5's claim and the only reason to do this refactor instead of tidying the switch. ConstantRock, SdfRoughness and SdfCarve are Maze's, reused without a line changed. In the switch, GetMazeDensity and GetVerticalShaftDensity are two ~100-line functions with nothing visibly in common; as operators they are the same three ops with a different source and different tuning (freq 0.1 vs 0.12, window rough+4 vs R+rough+2). New: FShaftFieldSource (infinite cylinders + hash-gated connectors into the SDF channel) and FShaftLedgeMod (banded shelves on the +X/+Y half so the shaft stays climbable). Deviation from section 6, stated: it suggested splitting the source so the XY-pure cylinder half could get an exact box verdict. Kept as one op because the connectors derive from the same 3x3 roll and the ledge mod needs the shaft list anyway, so splitting means rolling twice or sharing a cache between two ops. Forfeited: the exact verdict on the cylinder half. Kept: a conservative EffectOverBox testing circles and connector reach. FShaftLedgeMod gates on the POST-roughness Sdf as the stack left it; re-deriving it would use the pre-roughness value and shift every ledge. Reading the channel rather than recomputing is what the two-channel sample is for. Compile fix: FCells was declared below the functions returning it. Member bodies are deferred, return types are not. Ported: Maze, FlatPlain, CrystalChamber, SurfaceWorld (biomes included), VerticalShafts — 5 of 8. UNVERIFIED: not compiled past the FCells fix. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
c277931a08 |
feat: SurfaceWorld complete — biome blending wired, guard removed
The two biome checks added last commit printed nothing on success, so a passing run was indistinguishable from a block that never executed — the exact flaw I flagged twice this session and then wrote myself. Both now report their coverage. Step 2c closes SurfaceWorld: - FSurfaceColumnSource takes per-biome params and an OWNED IVoxelBiomeField. Empty params leaves the original path bit-for-bit unchanged. - The field is owned by the stack rather than borrowed: the adapter points at GetDensityAt's thread_local biome context and cache, and the stack is itself thread_local rebuilt in the same refetch block, so all three live and die together. Structural ownership beats a convention the next reader has to infer. - The overhang amp blends across biomes — Lerp(Amp(PD), Amp(PN), W) with slope and threshold from the dominant only, as ComputeSurfaceColumn does. Interpolating the slope would be meaningless; it measures the terrain rather than configuring it. - FGeneratorBiomeField lives in VoxelGenerator.cpp, on the side that knows the generator. The op sees a capability, never an owner — which is what lets it become an asset in Phase 3. - The no-biome guard is removed from UsesOperatorStackForChunk. Also: the two constructors now delegate to one body with one id counter. The first draft had two competing counters, one tagged with a high bit to avoid collision, which is a smell rather than a design. 5 of 8 archetypes ported: Maze, FlatPlain, CrystalChamber, SurfaceWorld. UNVERIFIED: not compiled. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
2b2afacd9e |
feat: SurfaceWorld step 2a — the bridge to density space; fix IsXYPure on the slab
Height stack is green: bit-identical to ComputeSurfaceTerrainZ on both passes, including all four F20 terrain ops on. MaxDisplacement is loose (27% used) and left that way — loose only costs CPU, tight-but-wrong is a hole. FIX: FSlabVoidSource::IsXYPure() returned true and that was wrong. The contract is "Eval does not depend on Z", and Eval computes min(Z - floor, ceil - Z). Section 3.1 made the SURFACES XY-pure; the density is a distance to them and never can be. I conflated the two while writing the operator that quotes the warning against it. Latent only because nothing reads the flag yet — and step 2b is where it would have gone live, since a generic T1.a column cache keyed without ChunkZ would have shared one density down the whole vertical chunk stack. AUDIT 6.3 says that corrupts every chunk silently and ValidateDeterminism would not catch it. That is also the clearest argument for the height-space split: what is XY-pure is the HEIGHT, and in VoxelHeightOp.h it lives in a type with no Z to get wrong. Step 2a: - FSkyCapHeightSource: the ceiling is an altitude, so it belongs in height space rather than density space as section 5 had it — same category slip as the terrain ops. The subtraction happens later, in the combine. - FSurfaceColumnSource: consumes both height stacks, IsXYPure false. - BuildSurfaceStack: source + 3 structural, no per-column memo inside the op since T1.a already exists one level up and a second cache key is a second thing to get wrong. NOT covered, and the test header now says so: the overhang (GetSurfaceDensity passes OverhangAmp = 0, so only the cached path computes it) and biome blending. Both are step 2b; do not wire SurfaceWorld into a biome or overhang world before then. UNVERIFIED: not compiled. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
644339def5 |
feat: Phase 2 first port — FlatPlain + CrystalChamber collapse into one op
Jahni closed OPSTACK-DECOMPOSITION 3.1: the slab noise Z term was not intentional character. Phase 1 also closed — the visual A/B on Maze passed. Two changes, deliberately together, kept attributable by the test: 1. Design: GetSlabDensity's floor and ceiling noise lose their Z terms. A floor height no longer depends on the altitude you sample it from. The ceiling keeps its + 3000.0f, which is a decorrelation offset, not a Z term. The world re-tunes once — a different slice of the noise field, not a worse one. 2. Refactor: the now-XY-pure function ports to FSlabVoidSource + FGridColumnMod plus the three structural ops. BuildSlabStack has NO branch on archetype because GetSlabDensity never had one — CrystalChamber is FlatPlain with a bigger CeilingRoughness. 8 archetypes -> 7. SlabEquivalence compares against the reference AS IT IS NOW and runs the whole battery on both slots, so green means the port is a pure refactor and any visual delta is attributable to the Z-term removal alone. The attribution comes from the test, not from splitting it across two builds. The payoff 3.1 was actually about: FSlabVoidSource::ClassifyBox is exact and needs no sampling. FBM is contractually [-1,1], so both surfaces live in Z bands with known bounds — a tile below the floor band is provably solid, a tile between the bands provably air. ClassifyTile proves zero tiles for these archetypes today. FGridColumnMod answers Identity when no column reaches the box, which is what lets the source's AllAir verdict survive the fold. UNVERIFIED: not compiled. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
974e795b66 |
fix: call PrepareChunk and guard the degenerate strate on the wired path
Read the step-3 wiring against the equivalence test before spending a build. The symbols all line up; the two PATHS did not. - GetDensityAt never called FVoxelOpStack::PrepareChunk, though the test does. All seven concrete bodies are empty today so behaviour is unchanged — which is the reason to fix it now: the first op to hoist real per-chunk work would have been green in test and silently wrong in game. Builds an FVoxelOpContext in the same refetch block (chunk, seed, layout version, strate Z bounds). Step stays 1; GetDensityAt does not know the mesher's sampling step (T2.b). - GetMazeDensity early-outs to air on a degenerate strate (height <= 0) and the stack has no such early-out by design. Unguarded that is air on one path and spine/seal-of-a-zero-height-band on the other, so the wired path now falls back to the switch there — the reference behaviour is the behaviour. Docs: VoxelDensityOpStack.h's banner still claimed nothing here feeds the game, and CODEMAP 3.2d repeated it. Both now state what is wired (GetDensityAt) and what is not (ClassifyTile, hand-written guards, Phase 2), with C10's never-compare rule at the point of use. CODEMAP gains UsesOperatorStackForChunk and bUseOperatorStack rows, and BuildMazeStack's degenerate-strate precondition. UNVERIFIED: not compiled. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
cca9e83182 |
test: /fp:precise reproduced the residue byte for byte — instrument instead of guessing
An /fp:precise build returned the identical 454 samples, identical max delta, identical coordinate. A different float model producing byte-identical output is proof that rounding is not the cause, so the /fp:fast explanation is dead. That is three failed hypotheses on one discrepancy (FVector round-trip, then "check the roughness window", then /fp:fast), each reasoned from plausibility and each costing a build. So: stop reasoning, print. FVoxelOpStack::EvalSample exposes the full sample, and MazeEquivalence now dumps the worst point in raw hex -- both densities, the stack's internal SDF, and the carve factor reconstructed from each side. The recovered carve localises it: identical carve + differing density means the fault is after the conversion; differing carve means it is in the SDF (lattice edges or VoxelSDF::Capsule) or in SmoothStep01. Note for whoever reads the docs next: AUDIT C9 and OPSTACK-PLAN 2.6 currently assert the /fp:fast story as the explanation for THIS residue. That specific claim is falsified and needs walking back once the dump identifies the real cause. C9's other half -- that UBT's FP default differs by toolchain and the MP model assumes bit-reproducible terrain -- stands independently; it was read out of VCToolChain.cs and ClangToolChain.cs, not inferred from this test. Phase 1 step 3 (wiring the stack into GetDensityAt) is paused until this is understood. Small unexplained numeric differences do not get smaller when you build on top of them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
62e3d5a933 |
fix(build): FVoxelOpStack is move-only, so it must not be a dllexported class
MSVC C2280 on TArray<TUniquePtr<IVoxelDensityOp>>'s copy path. Putting VOXELFORGE_API on the class forces the compiler to instantiate every implicit member, including copy-assignment -- which cannot exist for a move-only element type. The export moves to AppendStructuralPost, the only out-of-line method. Also declares the move-only-ness explicitly rather than leaving it implied. That is the right semantics independently of the compiler: a stack uniquely OWNS its operators, and copying one would mean cloning polymorphic ops, which is meaningless here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
4c53d3bbed |
feat: Phase 1 — Maze ported to the operator stack, OFF the hot path
Maze decomposes into seven ops with no contortion:
ConstantRockSource -> LatticeCorridorSource -> SdfRoughnessMod -> SdfCarve
-> OriginSpine -> BoundarySeal -> PassageCarve
That is the answer to Phase 1's actual question (OPSTACK-PLAN section 4's
stop-trigger: "does the source/modifier split fall out naturally?"). It does.
Three of those ops are already shared: ConstantRockSource is the first line of
TunnelNetwork, Maze AND VerticalShafts; SdfCarve is the same six lines in all
three; the structural post is identical across all six density functions.
GetDensityAt and ClassifyTile are NOT touched. The archetype switch is still the
only path feeding the game, so nothing in a running world can change. The port
is validated instead by VoxelForge.OpStack.MazeEquivalence, which compares the
stack against GetMazeDensity over 20k points, re-checks purity across worker
threads, and brute-forces every box verdict the stack emits.
Two contract decisions, delegated and taken:
1. Eval is now two-channel (FVoxelOpSample { Density, Sdf }). Maze forces it:
its roughness perturbs the SDF, not the density, and on density the same
noise scales with the local gradient and is a visibly different effect. It is
also what lets two different sources SmoothMin together later, which is the
difference between a composed idea belonging somewhere and being punched into
it.
2. The stack's density channel is INTERNAL convention (positive = solid),
negated once by the caller. This REVERSES what the header said yesterday.
Every archetype body is already written that way, so each port becomes a
literal transcription instead of a sign-flip of every line -- on the plugin's
documented #1 source of confusion. The SDF channel keeps standard SDF
convention, so min() means opposite things on the two channels; the header
says so loudly.
Also extracts spine/seal/passage from VoxelGenerator.cpp into
Public/VoxelDensityPrimitives.h so the generator and the ops share ONE copy of
three world invariants. Forwarders keep the local names, so not one of the ~20
call sites changes; bodies are byte-identical.
One thing found while writing the seal's ClassifyBox and NOT silently fixed: at
the inner edge of a seal band, 1 - Dist/Thickness can round to exactly 0.0f, so
SealFactor*BaseDensity is 0, internal density lands on 0, and the mesher counts
that as AIR. Claiming AllSolid there would be a hole. The new op keeps a 1-voxel
safety margin before it forces. Today's ClassifyTile has no such margin -- the
window is hairline and needs the archetype to produce air at exactly that z, but
it is real. Reported rather than patched, since Phase 1 does not touch that path.
UNVERIFIED: not compiled.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|