feat(opstack): EffectOverBox correct in ISOLATION -- the blocker is closed
COMPOSER-NOTES §6.1, titled THE BLOCKER, gated everything after it. A source writing only Sdf did not touch density, so it answered the box query ON BEHALF OF the converter that followed it -- fine when a human wrote and verified the pair, unsafe the moment the composer assembles a combination nobody checked. And the failure is the silent one: a tile wrongly proved uniform means no geometry, no collision, no error, and a player falls through the floor. FVoxelBoxHypotheses now carries FVoxelBoxSdfInterval. SDF writers publish their own [Min, Max]; replacement writers replace it, accumulating writers MinWith it; converters and detail ops consume the prior interval through a state-aware EffectOverBox. Each op answers only for itself and the fold composes them. Unknown, invalid, non-finite or unimplemented bounds force Mixed -- when in doubt the answer is Mixed, because a missed skip costs milliseconds and a false uniform costs a player. Bounds are SUPREMA of what Eval can produce, never the param named "max": FBM 1.5 (not 1.0), max(Min,Max) radius envelopes, warp sqrt(2), SmoothMin slack. [FLT_MAX, FLT_MAX] is the exact no-surface identity. ⭐ THE PROOF IS THE NOVEL STACK. Built a combination no shipping builder produces: ConstantVoid -> LatticeCorridorSource -> SdfRoughness -> SdfFill brute-forced 332,750 voxels across 256 boxes: 250 proved AllAir, 0 VIOLATIONS. If isolation is real an unverified stack must still be sound, and it is. That test -- not the refactor -- is the deliverable. Every archetype re-proved by brute force, 0 violations everywhere: Maze 112/256 · CrystalChamber 38/60 · Underwater 33/40 · FlatPlain 32/60 VerticalShafts 28/60 · FloatingIslands 15/60 · TunnelNetwork prod 9/40 TunnelNetwork dense 0/40 (correctly -- it really is all mixed) COST, reported not hidden: isolation gives up some skipping. TunnelNetwork production 12->9, VerticalShafts 32->28. Answering for yourself alone is strictly weaker than answering for a verified pair. The classifier stays materially useful and the trade buys safe composition, which is the point. Eval is untouched; equivalence tests bit-identical; no per-voxel box query. SurfaceWorld keeps its separate exact height/lattice classifier. Also: ⛔ WITHDREW the "corrective ops must be fused" claim from §3.2. FFloorBiasMod does NOT undo FCaveRoughnessMod -- it adds an independent NormZ^2 x FloorBias term to the ACCUMULATED density and never reads TotalRough. Not a correction, no fuse, no "corrects" dependency. The claim came from reading a name and a comment; one grep would have shown the ten Density-writing ops in between before the paragraph was written. Build clean, 22 succeeded / 0 failed, verified in VoxelM.log. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VJiTcTSDNkKBtnNdiBtdaU
This commit is contained in:
1 parent
7b982ad5ae
commit
8e346a3054
11 files changed
+1193
-890
No files matched your search
@@ -383,6 +383,11 @@ driven by `EditorBrush*` props.
|
||||
the stack entry. `ValidateChannelOrder` is assembly/diagnostic-only; no declaration virtual may
|
||||
enter `Eval`'s per-voxel loop. If `FVoxelOpSample` gains a field, add its channel bit and update
|
||||
every operator declaration and the validator together.
|
||||
- **Isolated box proofs** (`FVoxelBoxHypotheses` + `FVoxelBoxSdfInterval`): the box fold carries
|
||||
the SDF interval produced so far. An SDF-only source publishes only its own interval; a later
|
||||
converter or detail op applies its own state-aware response. Unknown or invalid bounds force the
|
||||
fold toward `Mixed`: a missed skip costs CPU, while a false uniform verdict removes geometry and
|
||||
collision. Every new SDF writer must implement `PropagateSdfOverBox`; the default is unknown.
|
||||
- **VerticalShafts field cache**: shaft rolls and connector decisions, including the structural
|
||||
tree and structural-spine connector endpoint, are rebuilt only when the thread-local centre cell
|
||||
or a geometry-affecting parameter changes. Each rebuild rolls a wider direct-indexed geometric
|
||||
|
||||
+11
-11
@@ -106,10 +106,10 @@ reference path for opt-out strates. See [OPSTACK-PLAN.md](OPSTACK-PLAN.md) for t
|
||||
| `EVoxelOpCombine` | `Replace`/`Union`(min)/`Subtract`(max)/`SmoothUnion`/`SmoothSubtract`/`Add`/`Mask`. Sign reminder: negative = solid, so "add solid" is `min`. |
|
||||
| `EVoxelOpEffect` | `Identity`/`CarveOnly`/`FillOnly`/`Both`. Conservative: `Both` is always safe, the wrong one is a hole. |
|
||||
| `FVoxelOpContext` | Chunk-constant inputs. Carries `LayoutVersion` plus the copied global `WorldRadiusVoxels`/`EdgeSealThickness`, so a new op cannot forget them. |
|
||||
| `IVoxelDensityOp` | `ChannelReads` / `ChannelWrites` / `IsAdditive` plus `PrepareChunk` / `Eval` / `EffectOverBox` / `ClassifyBox` / `IsXYPure`. The three declarations are pure virtual and are snapshotted during stack assembly; none is called per voxel. |
|
||||
| `IVoxelDensityOp` | `ChannelReads` / `ChannelWrites` / `IsAdditive` plus `PrepareChunk` / `Eval` / intrinsic and state-aware `EffectOverBox` / `MaxCarveOverBox` / `MaxFillOverBox` / `PropagateSdfOverBox` / `ClassifyBox` / `IsXYPure`. Box-query methods are never called per voxel; channel declarations are snapshotted during stack assembly. |
|
||||
| `IVoxelDensityOp::ClassifyBox` | ⚠️ **not source-only.** Forcing ops (the vertical seal and XY edge seal inside their proven bands) overwrite the input entirely, which pure direction cannot express. |
|
||||
| `FVoxelOpSample` | The state threaded through the stack: **two** channels, `Density` (INTERNAL convention, **positive = SOLID**, negated to MC once by the caller) and `Sdf` (standard SDF, negative = inside). ⚠️ `min()` therefore means opposite things on the two channels. |
|
||||
| `FVoxelBoxHypotheses` + `VF_ForceHypotheses` / `VF_FoldEffect` / `VF_FoldOp` | The fold that turns a stack into an `EVoxelTileClass`. Reproduces today's hand-written `ClassifyTile` line for line — the mapping is written out in the header. |
|
||||
| `FVoxelBoxHypotheses` + `FVoxelBoxSdfInterval` + `VF_ForceHypotheses` / `VF_FoldEffect` / `VF_FoldOp` | The fold that turns a stack into an `EVoxelTileClass`, carrying the SDF interval between independent writers and consumers. Unknown is biased toward `Mixed`; the mapping and soundness rule are written out in the header. |
|
||||
|
||||
### 3.2c Structural primitives — `Public/VoxelDensityPrimitives.h`
|
||||
`VF_ApplyOriginSpine` · `VF_ApplyBoundarySeal` · `VF_ApplyPassageCarving` · `VF_ApplyXYEdgeSeal` —
|
||||
@@ -148,28 +148,28 @@ bit. They are port-correctness oracles, not fidelity checks: the acceptance bar
|
||||
| `FVoxelOpStack::AppendStructuralPost` | 4 | Appends spine → vertical seal → passage → XY edge seal **in that fixed order**. An author cannot omit or reorder them. The diff layer is NOT here yet — it still lives in `GetDensityAt` after the MC negate, with disturbances. |
|
||||
| `VoxelDensityOps::MakeConstantRockSource` | 1 | `Density = BaseDensity`. `ClassifyBox` → **AllSolid**, exact and free. Shared by TunnelNetwork, Maze, VerticalShafts and bedrock gaps. Class is `FConstantFieldSource` (one class, two factories). |
|
||||
| `VoxelDensityOps::MakeConstantVoidSource` | 1 | The **same class, negated**: `Density = -BaseDensity`, and `ClassifyBox` → **AllAir** — the first source in the plugin that can prove it. FloatingIslands' root; that verdict is what makes a mostly-empty island strate skippable. |
|
||||
| `VoxelDensityOps::MakeLatticeCorridorSource` | 1 | Maze corridors, SDF channel. Edge identity = `hash(lower node, axis)` ⇒ adjacent chunks cannot disagree (AUDIT §6.4's preferred pattern). Its `EffectOverBox` answers for the source+carve **pair** (Phase 1 simplification) so it must be told the downstream `ExtraReach`. |
|
||||
| `VoxelDensityOps::MakeLatticeCorridorSource` | 1 | Maze corridors, SDF channel. Edge identity = `hash(lower node, axis)` ⇒ adjacent chunks cannot disagree (AUDIT §6.4's preferred pattern). The source reports its own conservative SDF interval; a later converter supplies its own carve/fill response, so the source is independent of the chosen consumer. |
|
||||
| `VoxelDensityOps::MakeSdfRoughnessMod` | 3 | Wall roughness in **SDF** space (Maze/Shafts/Islands variant). TunnelNetwork's density-space roughness is a **different op** — see OPSTACK-DECOMPOSITION §1. |
|
||||
| `VoxelDensityOps::MakeSdfCarve` | 2 | SDF → density carve. The same six lines currently copied in three archetypes. Class is `FSdfConvertOp(Sign = -1)`. |
|
||||
| `VoxelDensityOps::MakeSdfFill` | 2 | The same op with `Sign = +1` — FloatingIslands' `Density += Fill·Base·2`. ±1 multiplication is exact in IEEE-754, so the carve path is bit-for-bit unchanged by the generalisation. |
|
||||
| `VoxelDensityOps::MakeSlabVoidSource` | 1 | Floor surface + ceiling surface → void field. **XY-pure** since §3.1, which is what gives it an **exact `ClassifyBox` with no sampling**: FBM's `[-1,1]` contract bounds both surfaces into known Z bands. Serves FlatPlain **and** CrystalChamber. |
|
||||
| `VoxelDensityOps::MakeSlabVoidSource` | 1 | Floor surface + ceiling surface → void field. **XY-pure** since §3.1, which is what gives it an **exact `ClassifyBox` with no sampling**: the proved FBM supremum `1.5` bounds both surfaces into known Z bands. Serves FlatPlain **and** CrystalChamber. |
|
||||
| `VoxelDensityOps::MakeGridColumnMod` | 3 | Infinite-height cylinders on a world grid, 3×3 cell memo. Adds solid only ⇒ `FillOnly` when a column reaches the box, `Identity` otherwise — and that `Identity` is what lets the source's `AllAir` verdict survive. |
|
||||
| `FShaftFieldSource` (internal) | 1 | VerticalShafts' memoised inner 3×3 shaft/capsule source. Each rebuild uses a direct-indexed wider halo (default 9×9 tree-emission / 15×15 roll) so every emitted shaft has its complete 5×5 parent window plus capsule reach; the nearest strictly more-central shaft in ±2 cells is the deterministic parent, with a deterministic lower-origin ±3 fallback and a direct `(0,0)` spine fallback. Thus every shaft has a provable path to the spine. Existing `VoxelHash::Pair`/`Spacing*1.6` links remain additive texture/loops. Tree and spine capsules use a radius strictly above `sup|FBM|=1.5`, and tree Z is chosen in a ledge-free band; the structural post still owns the column and the ledge modifier keeps its mathematical axis boundary open. `EffectOverBox` mirrors both tree and random capsules conservatively. |
|
||||
| `FShaftFieldSource` (internal) | 1 | VerticalShafts' memoised inner 3×3 shaft/capsule source. Each rebuild uses a direct-indexed wider halo (default 9×9 tree-emission / 15×15 roll) so every emitted shaft has its complete 5×5 parent window plus capsule reach; the nearest strictly more-central shaft in ±2 cells is the deterministic parent, with a deterministic lower-origin ±3 fallback and a direct `(0,0)` spine fallback. Thus every shaft has a provable path to the spine. Existing `VoxelHash::Pair`/`Spacing*1.6` links remain additive texture/loops. Tree and spine capsules use a radius strictly above `sup|FBM|=1.5`, and tree Z is chosen in a ledge-free band; the structural post still owns the column and the ledge modifier keeps its mathematical axis boundary open. The source publishes a conservative interval covering both shafts and connector capsules; it does not answer for the ledge or converter. |
|
||||
| `VoxelDensityOps::BuildSlabStack` | — | 6 ops, **no branch on archetype**: FlatPlain and CrystalChamber differ only in defaults, exactly as `GetSlabDensity` already had it. 8 archetypes → 7. |
|
||||
| `FSurfaceColumnSource` (internal) | 1 | The bridge between the two spaces: consumes the ground + sky-cap **height** stacks and produces density. `IsXYPure()` **false** — the heights are XY-pure, a distance to them never is. Owns a **six-box spatial LRU** of direct-indexed per-column cells, keyed by `PrepareChunk` on `(StrateBottomWorldZ, LayoutVersion, Seed, ParamsFingerprint)` so it is **shared down the whole vertical strate stack**, exactly like `GSurfColCache`. Six 81×81 boxes preserve hot columns across interleaved regions at roughly 0.79 MiB TLS before padding (more memory, fewer whole-cache recenter/recompute misses). Fractional XY remains direct-compute. |
|
||||
| `VoxelDensityOps::BuildSurfaceStack` | — | SurfaceWorld, complete: column + overhang + 4 structural, plus biome blending when `PerBiomeParams` is non-empty. Takes ownership of an `IVoxelBiomeField`. |
|
||||
| `VoxelDensityOps::BuildVerticalShaftStack` | — | 9 ops, and **three are Maze's reused unchanged** (`ConstantRock`, `SdfRoughness`, `SdfCarve`) with different tuning (freq 0.1 vs 0.12, window `rough+4` vs `R+rough+2`). Passes the runtime spine radius into `FShaftFieldSource`, which mirrors the generator's connector-only origin endpoint and roughness-safe spine capsule. The measured proof of `OPSTACK-PLAN §2.5`'s reuse claim. |
|
||||
| `FRoomGraphSource` (internal) | 1 | TunnelNetwork's SDF spine. **CALLS `BuildChunkCache`/`EvaluateSDFCached` — does not transcribe them**: that is where §8.4's two-region discipline lives and a copy would fork it. Owns the cave warp (scope = this op alone; pits/chimneys read *unwarped* coords, which is why no FRAME op was needed). Its cache key adds a **params CRC + LayoutVersion**; the original lacked them until AUDIT §C2 was fixed (2026-07-28) and now carries them too. **`EffectOverBox` ANSWERS SPATIALLY** since 2026-07-28 — this is the T1.d switch (measured: **6 of 40 tiles proved AllSolid at production defaults, 7986 voxels brute-forced, 0 violations**). It builds the cache for the queried box into a *second* per-worker cache (never `FState::Cache`), then applies a **disjunction** per primitive: it doesn't matter if it **fails its cull** *or* if **its own SDF stays ≥ `T+K`** over the box. Cull wins for rooms (`Rmax+3K` < `Rmax+T+K`); the threshold wins hugely for tunnels, whose cull is a capsule *bounding sphere* (~107 radius for a 200-long tube of radius 7). ⚠️ **`Identity` therefore means `Sdf ≥ T`, not `Sdf == FLT_MAX`**, with `T = max(3·SDFBlendRadius, WormNetworkRange)` — **any new consumer of the `Sdf` channel must have a threshold ≤ T or be added to that max**, or it gets tiles with no geometry and no collision. The `−K` slack covers any number of primitives because `SmoothMin`'s penalty is exactly 0 once `\|A−B\| ≥ K`. Pits/chimneys use the cull only; columns are **not** tested (their sole consumer gates on `Sdf`, so the test was redundant). Verdict memoised per box; warp dilation uses a **provable** `\|Perlin3D\| ≤ 2`. |
|
||||
| `FWormFieldSource` (internal) | 1 | Fielded 3D-noise threshold carve, masked by distance to the room network (reads `InOut.Sdf` *after* pits/chimneys). **`EffectOverBox` INHERITS `FRoomGraphSource`'s verdict** since 2026-07-28 — its `Eval` sets `NetworkMask = 0` when `CaveSDF >= WormNetworkRange`, which `FLT_MAX` always satisfies, so where the room source proves `Identity` the worm doesn't execute at all. ⚠️ This was **the** blocker: `BaseDensity = 8` < `WormStrength = 10` **by default** (the field comment requires it), so an unconditional `CarveOnly` drove `SolidMargin` negative on every tile in the world and no room-source proof could survive behind it. Deliberately **not** `VF_NoCaveOverBox` — that helper answers "identity" for a null `Rooms`, which is wrong for an op that could sit behind a different SDF writer. |
|
||||
| `VoxelDensityOps::BuildTunnelNetworkStack` | — | **COMPLETE, 20 ops** — the biggest port in the plugin (~1080 lines), done in three stages: SDF spine (A) → the twelve detail modifiers of 4b–4h (B) → the per-room op override (C) → 4 structural posts. Serves **TunnelNetwork and Underwater** from one builder. Operator order is the original's, line for line, and it is load-bearing (`FFloorBiasMod` exists to undo what `FCaveRoughnessMod` did to floors). The requested 4b→4h fuse is pending: all 10 intervening ops touch `Density`, so moving the corrective phase fails the bit-identity precondition. |
|
||||
| `FRoomGraphSource` (internal) | 1 | TunnelNetwork's SDF spine. **CALLS `BuildChunkCache`/`EvaluateSDFCached` — does not transcribe them**: that is where §8.4's two-region discipline lives and a copy would fork it. Owns the cave warp (scope = this op alone; pits/chimneys read *unwarped* coords, which is why no FRAME op was needed). Its cache key adds a **params CRC + LayoutVersion**; the original lacked them until AUDIT §C2 was fixed (2026-07-28) and now carries them too. The source now publishes a conservative SDF interval through the box fold; it does not answer for the carve or detail stack. It builds the cache for the queried box into a *second* per-worker cache (never `FState::Cache`), and the interval uses the same cull/threshold disjunction as the per-voxel source: a primitive is harmless when it fails its cull or its own SDF stays ≥ `T+K`; `T = max(3·SDFBlendRadius, WormNetworkRange)`, with `−K` slack for the final `SmoothMin`. Rooms, tunnels, pits and chimneys are bounded in their respective coordinate frames; warp dilation uses the proved `sup|Perlin3D| ≤ 1.5` envelope. |
|
||||
| `FWormFieldSource` (internal) | 1 | Fielded 3D-noise threshold carve, masked by the propagated SDF interval (reads `InOut.Sdf` *after* pits/chimneys). Its state-aware box response is `Identity` only when the preceding interval proves `Sdf ≥ WormNetworkRange`; otherwise it reports its own `CarveOnly` direction and bound. With no preceding SDF source it remains conservative, so it cannot inherit a verdict from an unrelated future writer. |
|
||||
| `VoxelDensityOps::BuildTunnelNetworkStack` | — | **COMPLETE, 20 ops** — the biggest port in the plugin (~1080 lines), done in three stages: SDF spine (A) → the twelve detail modifiers of 4b–4h (B) → the per-room op override (C) → 4 structural posts. Serves **TunnelNetwork and Underwater** from one builder. Operator order is the original's, line for line. `FFloorBiasMod` is an independent additive term on the density accumulated before it; it is kept late because it does not undo the roughness delta. The requested 4b→4h fuse is not semantically justified. |
|
||||
| `FCaveRoughnessMod` (internal) | 3 | STEP 4b, **density space** — a different op from `MakeSdfRoughnessMod`: two octave sets, optional domain warp, four noise types, an anti-fill clamp inside definite air, quadratic fade. ⚠️ **Reads STRATE params, not the per-room copy** — the original's shadow is declared *after* step 4b. Eleven of twelve modifiers read the room copy; this one does not. |
|
||||
| `FCaveTerraceMod` (internal) | 3 | STEP 4c. The only modifier that **re-queries the SDF** (Z±1, through `FRoomGraphSource::ProbeSdfUnwarped`) for its horizontality gate — which is why the room source's cache is exposed at all. ⚠️ Those probes use unwarped X/Y and raw Z although the field was evaluated warped: transcribed as-is, see OPSTACK-PROGRESS. |
|
||||
| `FLayerLineMod` / `FRibbingMod` (internal) | 3 | The same sine along Z: cubed and subtracted (grooves) vs quarter-phase-shifted, squared and added (ribs). `CarveOnly` / `FillOnly` — two of the few detail modifiers that keep a usable direction for the fold. |
|
||||
| `FCaveOverhangMod` / `FCaveCliffMod` / `FScallopMod` (internal) | 3 | STEP 4c. ⚠️ The cliff's own comment promises a sampled vertical gradient; the **code** uses a Z-stretched Perlin as a proxy and samples nothing. Ported as written — fixing it would change the world. |
|
||||
| `FCaveArchMod` / `FDomeMod` / `FPinchMod` / `FFloorBiasMod` (internal) | 3 | Room-relative: they read `FRoomGraphSource::GetNearestRoomIdx()` and the cached room. Their gate is `SDF < SDFBlendRadius`, **not** the shared `·3` one — they live in the cave's void, not its wall. |
|
||||
| `FRoomColumnMod` (internal) | 3 | STEP 4d, and **not** `MakeGridColumnMod`: it walks `SDFCache.Columns`, pre-baked per room. ⚠️ It has **no strate parameter at all** — neither the bake nor the loop reads `FStrateGenerationParams::ColumnDensity`. Columns exist only through a `Column` terrain-op asset in the strate's pool, and the only way to prove they fired is to look at `SDFCache.Columns.Num()`. |
|
||||
| `FRoomGraphSource::LocalParams()` | — | **The per-room op override** (DECOMPOSITION §2's "no clean home"). Strate params + the nearest room's `UVoxelTerrainOpDefinition::ApplyTo`, memoised once per voxel and read by eleven modifiers. One op owns the shared state, the rest read it — the same pattern as `FOverhangShelfMod` ← `FSurfaceColumnSource`. ⚠️ `EffectOverBox` still answers from STRATE params, so a box verdict can be **too optimistic** on a strate with a terrain-op pool; the cave classifier remains conservative by bailing Mixed on any unresolved cave guard. |
|
||||
| `FIslandBlobSource` (internal) | 1 | Hash-placed tapered flat-top blobs, `SmoothMin`'d, in a **domain-warped XY frame** (the warp stays inside the op — see the deviation note vs DECOMPOSITION §7). SDF channel only. `EffectOverBox` → `FillOnly` when a blob reaches the box, `Identity` otherwise; its pad must cover warp·**√2** (two independent noise axes), roughness, fill blend and the `SmoothMin` dip. **No lower Z bound exists** — a hairline thread of matter hangs below each island down its axis, so only the TOP may reject. |
|
||||
| `FRoomGraphSource::LocalParams()` | — | **The per-room op override** (DECOMPOSITION §2's "no clean home"). Strate params + the nearest room's `UVoxelTerrainOpDefinition::ApplyTo`, memoised once per voxel and read by eleven modifiers. One op owns the shared state, the rest read it — the same pattern as `FOverhangShelfMod` ← `FSurfaceColumnSource`. The detail consumers use the propagated interval for their shared near/far gate; unknown remains conservative, and the per-room parameter override is never used to claim an unproved box identity. |
|
||||
| `FIslandBlobSource` (internal) | 1 | Hash-placed tapered flat-top blobs, `SmoothMin`'d, in a **domain-warped XY frame** (the warp stays inside the op — see the deviation note vs DECOMPOSITION §7). SDF channel only. The source publishes an interval covering the full authored radius envelope (`max(MinRadius, MaxRadius)`), the two-axis warp·**√2** envelope, top surface and SmoothMin dip; the fill converter makes its own decision. **No lower Z bound exists** — a hairline thread of matter hangs below each island down its axis, so only the TOP may reject. |
|
||||
| `VoxelDensityOps::BuildFloatingIslandStack` | — | 8 ops, and **the stack runs backwards**: void source + fill instead of rock source + carve, using the *same* classes with the opposite sign. Only the blob source is new. Reuse by **inversion** — a stronger result than reuse by identity, since it says the abstract axis (the density sign) is the right one. |
|
||||
| `VoxelDensityOps::BuildMazeStack` | — | The 8-op Maze stack (4 structural posts included). If this ever becomes one op, the refactor failed its own test (§2.5). Callers must skip it on a **degenerate strate** (top−bottom ≤ 0): `GetMazeDensity` early-outs to air there and the stack has no such early-out by design — `GetDensityAt` falls back to the `switch`. |
|
||||
|
||||
@@ -497,7 +497,7 @@ The plugin's first tests (`OPSTACK-PLAN.md` Phase 0.5). Run them from the editor
|
||||
| `VoxelForgeCrossPlatformTest.cpp` | `VoxelForge.Determinism.CrossPlatformDigest` | SHAPE digest (sign of density = the world) + FIELD digest (bit-for-bit) over a fixed integer grid, plus `NearIso` bounding how many samples could flip sign. Reports rather than asserts until pinned. Run on Windows and Linux and compare. |
|
||||
| `VoxelForgeOpStackSlabTest.cpp` | `VoxelForge.OpStack.SlabEquivalence` | **Phase 2's first port.** The same 6-op slab stack vs `GetSlabDensity` over 20k points, run twice — FlatPlain **and** CrystalChamber — which is what demonstrates the two archetypes really are one op. Plus window-invariance and box-verdict brute force. Compares against the reference **as it is now** (post Z-term removal), so green = pure refactor and any visual delta is attributable to §3.1 alone. |
|
||||
| `VoxelForgeOpStackTunnelTest.cpp` | `VoxelForge.OpStack.TunnelNetworkSpineEquivalence` | **Stage A of the last port.** Zeroes the 13 detail-op amplitudes so the *original* takes the path stage A ported — that is what makes an incomplete stack verifiable now. Samples in **clusters** (24 chunks × 250 points), because the SDF cache rebuilds when a query leaves its box and uniform sampling would rebuild per point on both paths. Check 3 (two param sets, A/B interleaved) compares each stack **to itself alone, never to the original** — the original would fail it, see AUDIT §C2. Asserts **zero** box verdicts, which is the honest stage-A result. |
|
||||
| `VoxelForgeOpStackShaftTest.cpp` | `VoxelForge.OpStack.VerticalShaftEquivalence` | The port that tests **reuse**, not fidelity: three of the five ops are Maze's, unchanged. Forces connectors + ledges on, because both are off or negligible at defaults and a resting param is an untested operator. The source and mirror now share a deterministic inner-3×3-emitted drainage tree whose parents come from rebuild-only ±2/±3 windows in a wider direct-indexed halo (default 9×9 emit / 15×15 roll), plus the old random links. Tree connector Z is exact and ledge-free; tree and random capsules are spatially culled from the cached rebuild. **Fixed 2026-07-29:** its `EffectOverBox` used to return `CarveOnly` because a shaft merely *existed* within a `Spacing*1.6` halo — true almost everywhere at `ShaftSpacing 55 / ShaftDensity 0.6`, hence **0 of 60** tiles. It now tests tree and random capsules (same row-major cell order ⇒ same `VoxelHash::Pair`, so symmetry of `Pair()` is not assumed), with **Z exact** and XY conservative. The sampler was also widened from ±48 voxels to ±440. Every proved tile is brute-forced over its full lattice, and the test reports rebuild-vs-hot cache cost. |
|
||||
| `VoxelForgeOpStackShaftTest.cpp` | `VoxelForge.OpStack.VerticalShaftEquivalence` | The port that tests **reuse**, not fidelity: three of the five ops are Maze's, unchanged. Forces connectors + ledges on, because both are off or negligible at defaults and a resting param is an untested operator. The source and mirror now share a deterministic inner-3×3-emitted drainage tree whose parents come from rebuild-only ±2/±3 windows in a wider direct-indexed halo (default 9×9 emit / 15×15 roll), plus the old random links. Tree connector Z is exact and ledge-free; tree and random capsules are spatially culled from the cached rebuild. **Fixed 2026-07-29:** its box query used to return `CarveOnly` because a shaft merely *existed* within a `Spacing*1.6` halo — true almost everywhere at `ShaftSpacing 55 / ShaftDensity 0.6`, hence **0 of 60** tiles. It now publishes an isolated interval for tree and random capsules (same row-major cell order ⇒ same `VoxelHash::Pair`, so symmetry of `Pair()` is not assumed), with **Z exact** and XY conservative; the ledge and converter decide their own effects. The sampler was also widened from ±48 voxels to ±440. Every proved tile is brute-forced over its full lattice, and the test reports rebuild-vs-hot cache cost. |
|
||||
| `VoxelForgeOpStackIslandTest.cpp` | `VoxelForge.OpStack.FloatingIslandEquivalence` | The port that runs the stack **backwards** — void + fill vs rock + carve, same classes with the opposite sign. Counts interior-solid and open-void samples separately (on this archetype an aggregate "N solid" is dominated by the seal bands and says nothing about the islands). Counts `AllSolid` and `AllAir` verdicts **separately** too: `AllAir` is the one no cave archetype could ever prove, and it is the entire perf argument here. |
|
||||
| `VoxelForgeOpStackMazeTest.cpp` | `VoxelForge.OpStack.MazeEquivalence` | **Phase 1's load-bearing test.** The 7-op Maze stack vs `GetMazeDensity` over 20k points (aiming for bit-identity; a side-of-iso disagreement is the hard fail), plus purity across workers and brute force on every box verdict the stack emits. Reports how many tiles the stack can prove uniform — today's `ClassifyTile` proves **zero** for any cave archetype. |
|
||||
| `VoxelForgeStrateParamCoverageTest.cpp` | `VoxelForge.Determinism.StrateParamBlendCoverage` | **The X-macro guard** (added 2026-08-17). `FStrateGenerationParams::Lerp` blends the hand-written `VF_STRATE_PARAM_FIELDS` list, **not** the struct — so a field added to one and not the other compiles, tests green, and silently takes its **default** inside every Gradient/Interleaved transition band. This expands the X-macro a **third** way (after LERP and SNAP), into a name list, and diffs it against the struct's UObject reflection. Pure shape test: no fixture, no world, instant. `GExemptFieldNames` is **empty** — every reflected field is covered today, and any exemption must be written down as a decision. Stakes rise with the world composer, which intends to invent parameter sets through this same `Lerp` (`COMPOSER-NOTES.md`). |
|
||||
|
||||
+44
-26
@@ -118,19 +118,32 @@ succeeded, 0 failed, 0 not run**.
|
||||
**void** source with a **fill** instead of a rock source with a carve — and you get islands instead of
|
||||
tunnels.
|
||||
|
||||
**Corrective ops must be FUSED, not ordered.** `FFloorBiasMod` exists only to undo what
|
||||
`FCaveRoughnessMod` did to floors. Both are density-space, so channel rules permit either order.
|
||||
**An operator whose only purpose is to correct another operator is not a separate operator** — fuse them
|
||||
into one op with two internal phases. That removes the invalid state instead of annotating it. Fifteen
|
||||
ops that compose in any legal order beat eighteen with a footnote. (Fusing with identical order and
|
||||
math leaves the field bit-identical.)
|
||||
⛔ **"Corrective ops must be FUSED" — WITHDRAWN 2026-09-04. The premise was false.**
|
||||
|
||||
**Current implementation finding:** the fuse is not landed. Ten operators intervene between 4b and
|
||||
4h — `FCaveTerraceMod`, `FLayerLineMod`, `FRibbingMod`, `FCaveOverhangMod`, `FCaveCliffMod`,
|
||||
`FScallopMod`, `FCaveArchMod`, `FRoomColumnMod`, `FDomeMod`, and `FPinchMod` — and every one reads
|
||||
and writes `Density`. Moving the floor phase next to roughness would therefore reorder the live
|
||||
density updates; the required bit-identical gate cannot be assumed. The current 20-op stack remains
|
||||
in its original order until a fusion that preserves that order and math is available.
|
||||
This section used to assert: *"`FFloorBiasMod` exists only to undo what `FCaveRoughnessMod` did to
|
||||
floors... fuse them into one op with two internal phases."* **It does not, and they must not be fused.**
|
||||
|
||||
Read the math rather than the names. `FCaveRoughnessMod` adds its own displacement:
|
||||
`D += TotalRough`. Eleven ops later, `FFloorBiasMod` computes an **independent** term
|
||||
`B = NormZ² × FloorBias` from `Sdf`, room geometry and `WorldZ`, and adds it: `D += B`.
|
||||
**It never reads or subtracts `TotalRough`.** It is not a correction to roughness at all — it acts on
|
||||
the accumulated density and belongs late in the stack, exactly where it is.
|
||||
|
||||
⇒ **No fuse. No `corrects` dependency either** — there is nothing being corrected. The general
|
||||
principle ("an operator whose only purpose is to correct another is not a separate operator") stands
|
||||
as a rule for ops that genuinely are corrective; this simply was not an instance of it.
|
||||
|
||||
**Two lessons, both cheap here and expensive elsewhere:**
|
||||
- The claim came from reading a *name* and a *comment*. Ten Density-writing ops sit between the two
|
||||
(`FCaveTerraceMod`, `FLayerLineMod`, `FRibbingMod`, `FCaveOverhangMod`, `FCaveCliffMod`,
|
||||
`FScallopMod`, `FCaveArchMod`, `FRoomColumnMod`, `FDomeMod`, `FPinchMod`) — a fact one grep would
|
||||
have shown before the design paragraph was written.
|
||||
- The old parenthetical *"fusing with identical order and math leaves the field bit-identical"* was
|
||||
also wrong: reordering across ten accumulating ops cannot be bit-identical, because IEEE-754
|
||||
addition is not commutative. ⚠️ But do not let that argument decide anything on its own —
|
||||
**bit-identity across builds was never required** (see §8: peer determinism is *same build, all
|
||||
machines*). Had the fuse been semantically right, a slightly changed field would have been an
|
||||
acceptable price. It was the *semantics* that killed it, not the bits.
|
||||
|
||||
**One declaration covers the rest:** an op is **additive** (small displacement, commutes freely — shuffle
|
||||
at will) or **transformative** (clamps, multiplies, gates — position matters, needs explicit placement).
|
||||
@@ -335,21 +348,24 @@ still a gradient, and gradients are what Jahni rejected.
|
||||
|
||||
All verified against the source on 2026-08-17 unless marked otherwise.
|
||||
|
||||
### ⚠️ 6.1 THE BLOCKER — box verdicts are only correct in the stack they ship in
|
||||
### ✅ 6.1 THE BLOCKER — `EffectOverBox` correct in isolation (resolved 2026-09-04)
|
||||
|
||||
CODEMAP, on the lattice corridor source: *"Its `EffectOverBox` answers for the source+carve **pair**
|
||||
(Phase 1 simplification) so it must be told the downstream `ExtraReach`."* The op interface says the
|
||||
same in its own comment: a source writing only `Sdf` does not touch density itself, so it answers on
|
||||
behalf of the converter that follows it.
|
||||
The source+converter coupling is removed. `FVoxelBoxHypotheses` now carries an
|
||||
`FVoxelBoxSdfInterval`; each SDF writer publishes only the interval for its own field, and each
|
||||
converter/detail operator folds that interval through its own formula. `FVoxelOpStack::VF_FoldOp`
|
||||
propagates the interval in stack order, so an operator cannot borrow a neighbour's reach or verdict.
|
||||
|
||||
Fine when a human wrote the pair. **It breaks the moment the composer assembles a combination nobody
|
||||
verified**, and the failure is the silent one: a tile wrongly proved uniform means no geometry, no
|
||||
collision, no error, and a player falls through the floor.
|
||||
The interval is deliberately a proof, not a likelihood: invalid, non-finite, or unimplemented
|
||||
bounds become unknown and therefore cost a skip rather than manufacture `AllSolid`/`AllAir`.
|
||||
Bounds use the supremum of `Eval` over the authored parameter range, including `sup|FBM| = 1.5`,
|
||||
the `max(Min, Max)` radius envelope, warp·√2 where two axes are independent, and the SmoothMin
|
||||
slack. The existing fold carries the interval; no parallel box-query path was needed.
|
||||
|
||||
⇒ **Prerequisite before free composition is safe: every operator's `EffectOverBox` must be correct in
|
||||
ISOLATION**, not correct-given-its-neighbours. That means propagating an **SDF interval** through the
|
||||
box query instead of letting the source answer for the pair — which the interface comment already names
|
||||
as the Phase 3 version of the contract. Not huge, but load-bearing, and it must land first.
|
||||
The isolation deliverable is an unverified combination: `ConstantVoid → LatticeCorridorSource →
|
||||
SdfRoughness → SdfFill`, which no shipping builder emits. Across 256 boxes it proved 250 `AllAir`
|
||||
and 6 `Mixed`; **332,750 voxels checked, 0 violations**. The shipping archetype scans likewise
|
||||
reported zero violations. This resolves §6.1 for the current operators, with one ongoing interface
|
||||
rule: every future SDF writer must implement interval propagation or accept `Mixed` by default.
|
||||
|
||||
### ⚠️ 6.2 Box-verdict bounds are proved against HAND-SET params
|
||||
|
||||
@@ -509,10 +525,12 @@ they became "findings" (§13).
|
||||
2026-09-04).
|
||||
- ⏸ **Fuse corrective ops** (`FFloorBiasMod` into `FCaveRoughnessMod`) — blocked by the ten
|
||||
intervening `Density` readers/writers under the bit-identity requirement; see §3.2.
|
||||
- ⚠️ **`EffectOverBox` correct in ISOLATION** (§6.1) — the gate on everything after.
|
||||
- ✅ **`EffectOverBox` correct in ISOLATION** (§6.1; built and brute-force verified 2026-09-04).
|
||||
- **The XY edge seal** (§6.7).
|
||||
|
||||
*Standalone value:* the fuse and the isolation fix are both correctness improvements to shipping code.
|
||||
*Standalone value:* the isolation fix is a correctness improvement to shipping code. The fuse remains
|
||||
an open semantic decision; the Part 0 analysis above concludes that FloorBias is not a correction of
|
||||
roughness and should stay late in the stack.
|
||||
|
||||
### Tier 4 — the composer proper
|
||||
Structure roll (§3.2) · parameter roll (§3.3) · reject-and-resample driven by Tier 2 (§3.5) · the
|
||||
|
||||
@@ -233,7 +233,7 @@ bool FVoxelForgeOpStackIslandTest::RunTest(const FString& Parameters)
|
||||
// 3. LE VERDICT DE BOÎTE — et la première preuve « AllAir » du plugin
|
||||
//=========================================================================
|
||||
{
|
||||
int32 NumProvedSolid = 0, NumProvedAir = 0, NumMixed = 0, NumUnsound = 0;
|
||||
int32 NumProvedSolid = 0, NumProvedAir = 0, NumMixed = 0, NumUnsound = 0, NumBruteSamples = 0;
|
||||
FRandomStream Rng(24680);
|
||||
// Hors de la boucle : la ligne de rapport en a besoin. Une étendue d'échantillonnage qu'on
|
||||
// ne peut pas citer dans le rapport est une étendue que personne ne surveille.
|
||||
@@ -268,6 +268,7 @@ bool FVoxelForgeOpStackIslandTest::RunTest(const FString& Parameters)
|
||||
const float Y = (float)(Origin.Y + gy * Step);
|
||||
const float Z = (float)(Origin.Z + gz * Step);
|
||||
const float D = Stack.EvalMC(X, Y, Z);
|
||||
++NumBruteSamples;
|
||||
if (bClaimsSolid ? (D >= 0.0f) : (D < 0.0f))
|
||||
{
|
||||
if (NumUnsound == 0)
|
||||
@@ -275,7 +276,7 @@ bool FVoxelForgeOpStackIslandTest::RunTest(const FString& Parameters)
|
||||
AddError(FString::Printf(
|
||||
TEXT("HOLE: the island stack claimed %s for the box at (%d,%d,%d) but ")
|
||||
TEXT("EvalMC(%.0f, %.0f, %.0f) = %.6g is on the %s side. Suspects, in ")
|
||||
TEXT("order: the blob source's Pad (does it cover the WARP amplitude ")
|
||||
TEXT("order: the blob source's propagated SDF interval (does it cover the WARP amplitude ")
|
||||
TEXT("AND the roughness AND the fill blend AND the SmoothMin dip?), ")
|
||||
TEXT("then the Z bound -- note there is NO lower bound, a thin thread ")
|
||||
TEXT("of matter hangs below each island down the axis, so only the ")
|
||||
@@ -295,11 +296,11 @@ bool FVoxelForgeOpStackIslandTest::RunTest(const FString& Parameters)
|
||||
AddInfo(FString::Printf(
|
||||
TEXT("Box verdicts over 60 FloatingIslands tiles (XY sampled from +/- %d voxels = %.1f x ")
|
||||
TEXT("IslandSpacing %.0f): %d proved AllSolid, %d proved AllAir, ")
|
||||
TEXT("%d Mixed. Today's ClassifyTile proves ZERO of these. The AllAir count is the new ")
|
||||
TEXT("%d Mixed, %d voxels checked, %d violations. Today's ClassifyTile proves ZERO of these. The AllAir count is the new ")
|
||||
TEXT("thing: no cave archetype has ever been able to prove 'all air', and a floating-")
|
||||
TEXT("island strate is mostly exactly that (OPSTACK-DECOMPOSITION 7)."),
|
||||
SpanVoxels, (float)SpanVoxels / FMath::Max(P.IslandSpacing, 1.0f), P.IslandSpacing,
|
||||
NumProvedSolid, NumProvedAir, NumMixed));
|
||||
NumProvedSolid, NumProvedAir, NumMixed, NumBruteSamples, NumUnsound));
|
||||
|
||||
if (NumProvedAir == 0)
|
||||
{
|
||||
|
||||
@@ -87,6 +87,7 @@ IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
namespace
|
||||
{
|
||||
constexpr int32 NumMazeSamples = 20000;
|
||||
constexpr int32 NumMazeBoxTests = 256;
|
||||
|
||||
/** Les params Maze de la strate Maze de la fixture, bornes Z de runtime comprises. */
|
||||
bool ResolveMazeParams(const VoxelForgeTest::FTestWorld& World, FMazeGenerationParams& Out,
|
||||
@@ -265,14 +266,14 @@ bool FVoxelForgeOpStackMazeTest::RunTest(const FString& Parameters)
|
||||
// VerticalShafts, FloatingIslands, FlatPlain, CrystalChamber et Underwater ne captent RIEN du
|
||||
// gain T1.d. Tout nombre > 0 ici est du saut de tuile que Maze n'a jamais eu.
|
||||
{
|
||||
int32 NumProved = 0, NumMixed = 0, NumUnsound = 0;
|
||||
int32 NumProved = 0, NumMixed = 0, NumUnsound = 0, NumBruteSamples = 0;
|
||||
FRandomStream Rng(24680);
|
||||
// Hors de la boucle : la ligne de rapport en a besoin. Une étendue d'échantillonnage qu'on
|
||||
// ne peut pas citer dans le rapport est une étendue que personne ne surveille.
|
||||
const int32 SpanCells = 40;
|
||||
const int32 SpanVoxels = SpanCells * 8; // Extent = Step * Cells = 1 * 8
|
||||
|
||||
for (int32 t = 0; t < 60; ++t)
|
||||
for (int32 t = 0; t < NumMazeBoxTests; ++t)
|
||||
{
|
||||
const int32 Step = 1, Cells = 8; // petites tuiles : force brute tenable
|
||||
const int32 Extent = Step * Cells;
|
||||
@@ -299,6 +300,7 @@ bool FVoxelForgeOpStackMazeTest::RunTest(const FString& Parameters)
|
||||
const float Y = (float)(Origin.Y + gy * Step);
|
||||
const float Z = (float)(Origin.Z + gz * Step);
|
||||
const float D = Stack.EvalMC(X, Y, Z);
|
||||
++NumBruteSamples;
|
||||
if (bClaimsSolid ? (D >= 0.0f) : (D < 0.0f))
|
||||
{
|
||||
if (NumUnsound == 0)
|
||||
@@ -307,8 +309,8 @@ bool FVoxelForgeOpStackMazeTest::RunTest(const FString& Parameters)
|
||||
TEXT("HOLE: the op stack claimed %s for the box at (%d,%d,%d) but ")
|
||||
TEXT("EvalMC(%.0f, %.0f, %.0f) = %.6g is on the %s side. One of the ops' ")
|
||||
TEXT("EffectOverBox/ClassifyBox is not conservative. Suspects, in order: ")
|
||||
TEXT("the lattice source's ExtraReach (does it cover the roughness ")
|
||||
TEXT("amplitude AND the carve blend?), then the seal's forcing verdict."),
|
||||
TEXT("the source interval, the SDF roughness interval, the converter's ")
|
||||
TEXT("threshold fold, then the seal's forcing verdict."),
|
||||
bClaimsSolid ? TEXT("AllSolid") : TEXT("AllAir"),
|
||||
Origin.X, Origin.Y, Origin.Z, X, Y, Z, D,
|
||||
(D >= 0.0f) ? TEXT("AIR") : TEXT("SOLID")));
|
||||
@@ -323,12 +325,12 @@ bool FVoxelForgeOpStackMazeTest::RunTest(const FString& Parameters)
|
||||
NumUnsound, 0);
|
||||
|
||||
AddInfo(FString::Printf(
|
||||
TEXT("Box verdicts over 60 Maze tiles (XY sampled from +/- %d voxels = %.1f x ")
|
||||
TEXT("CellSize %.0f): %d proved uniform, %d Mixed. Today's ClassifyTile ")
|
||||
TEXT("Box verdicts over %d Maze tiles (XY sampled from +/- %d voxels = %.1f x ")
|
||||
TEXT("CellSize %.0f): %d proved uniform, %d Mixed, %d voxels checked, %d violations. Today's ClassifyTile ")
|
||||
TEXT("proves ZERO of these -- every cave archetype falls through to \"pas prouvable en ")
|
||||
TEXT("v1\". Any number above zero here is tile-skipping Maze has never had."),
|
||||
SpanVoxels, (float)SpanVoxels / FMath::Max(MazeParams.CellSize, 1.0f), MazeParams.CellSize,
|
||||
NumProved, NumMixed));
|
||||
NumMazeBoxTests, SpanVoxels, (float)SpanVoxels / FMath::Max(MazeParams.CellSize, 1.0f), MazeParams.CellSize,
|
||||
NumProved, NumMixed, NumBruteSamples, NumUnsound));
|
||||
|
||||
if (NumProved == 0)
|
||||
{
|
||||
@@ -338,6 +340,86 @@ bool FVoxelForgeOpStackMazeTest::RunTest(const FString& Parameters)
|
||||
}
|
||||
}
|
||||
|
||||
//==========================================================================
|
||||
// ISOLATION — a source with a converter it was never authored with
|
||||
//==========================================================================
|
||||
// Maze's hand-written stack uses ConstantRock -> Lattice -> SdfRoughness -> SdfCarve.
|
||||
// This stack deliberately uses a VOID root and SdfFill instead. The only way it may
|
||||
// claim AllAir is for the propagated lattice/roughness interval to prove that the fill
|
||||
// converter is inactive over the whole box. A source that still answered for its old
|
||||
// carve neighbour would not be a valid implementation of this test.
|
||||
{
|
||||
FMazeGenerationParams NovelParams = MazeParams;
|
||||
NovelParams.BranchProbability = 0.12f;
|
||||
NovelParams.Verticality = 0.08f;
|
||||
NovelParams.CorridorRadius = 1.0f;
|
||||
|
||||
FVoxelOpStack NovelStack;
|
||||
NovelStack.Add(VoxelDensityOps::MakeConstantVoidSource(NovelParams.BaseDensity));
|
||||
NovelStack.Add(VoxelDensityOps::MakeLatticeCorridorSource(NovelParams, World.Settings->Seed));
|
||||
NovelStack.Add(VoxelDensityOps::MakeSdfRoughnessMod(
|
||||
0.35f, 0.12f, 3, NovelParams.CorridorRadius + 2.0f));
|
||||
NovelStack.Add(VoxelDensityOps::MakeSdfFill(1.25f, NovelParams.BaseDensity));
|
||||
NovelStack.PrepareChunk(Ctx);
|
||||
|
||||
FString ChannelError;
|
||||
TestTrue(TEXT("the unverified source/converter stack has a valid channel order"),
|
||||
NovelStack.ValidateChannelOrder(&ChannelError));
|
||||
if (!ChannelError.IsEmpty()) { AddError(ChannelError); }
|
||||
|
||||
constexpr int32 NumNovelBoxTests = 256;
|
||||
const int32 NovelSpanCells = 160;
|
||||
const int32 NovelSpanVoxels = NovelSpanCells * 8;
|
||||
int32 NumNovelSolid = 0, NumNovelAir = 0, NumNovelMixed = 0;
|
||||
int32 NumNovelVoxels = 0, NumNovelViolations = 0;
|
||||
FRandomStream NovelRng(86420);
|
||||
|
||||
for (int32 t = 0; t < NumNovelBoxTests; ++t)
|
||||
{
|
||||
const int32 Step = 1, Cells = 8;
|
||||
const int32 Extent = Step * Cells;
|
||||
const FIntVector Origin(
|
||||
NovelRng.RandRange(-NovelSpanCells, NovelSpanCells) * Extent,
|
||||
NovelRng.RandRange(-NovelSpanCells, NovelSpanCells) * Extent,
|
||||
FMath::Clamp(NovelRng.RandRange(BottomVoxelZ / Extent, TopVoxelZ / Extent), -4096, 4096) * Extent);
|
||||
const int32 GridDim = Cells + 1;
|
||||
const FBox Box(
|
||||
FVector(Origin.X - Step, Origin.Y - Step, Origin.Z - Step),
|
||||
FVector(Origin.X + GridDim * Step, Origin.Y + GridDim * Step, Origin.Z + GridDim * Step));
|
||||
|
||||
const EVoxelTileClass Verdict = NovelStack.ClassifyBox(Box, Ctx);
|
||||
if (Verdict == EVoxelTileClass::Mixed)
|
||||
{
|
||||
++NumNovelMixed;
|
||||
continue;
|
||||
}
|
||||
|
||||
const bool bClaimsSolid = (Verdict == EVoxelTileClass::AllSolid);
|
||||
if (bClaimsSolid) { ++NumNovelSolid; } else { ++NumNovelAir; }
|
||||
|
||||
// No sampled subset: every lattice point in every proved box is checked.
|
||||
for (int32 gz = -1; gz <= GridDim; ++gz)
|
||||
for (int32 gy = -1; gy <= GridDim; ++gy)
|
||||
for (int32 gx = -1; gx <= GridDim; ++gx)
|
||||
{
|
||||
const float X = (float)(Origin.X + gx * Step);
|
||||
const float Y = (float)(Origin.Y + gy * Step);
|
||||
const float Z = (float)(Origin.Z + gz * Step);
|
||||
const float D = NovelStack.EvalMC(X, Y, Z);
|
||||
++NumNovelVoxels;
|
||||
if (bClaimsSolid ? (D >= 0.0f) : (D < 0.0f)) { ++NumNovelViolations; }
|
||||
}
|
||||
}
|
||||
|
||||
TestEqual(TEXT("the unverified source/converter stack has zero box-verdict violations"),
|
||||
NumNovelViolations, 0);
|
||||
AddInfo(FString::Printf(
|
||||
TEXT("Novel lattice+roughness+FILL stack: %d boxes proved (%d AllSolid, %d AllAir), " )
|
||||
TEXT("%d Mixed; %d voxels checked, %d violations; XY sampled from +/- %d voxels."),
|
||||
NumNovelSolid + NumNovelAir, NumNovelSolid, NumNovelAir, NumNovelMixed,
|
||||
NumNovelVoxels, NumNovelViolations, NovelSpanVoxels));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -252,7 +252,7 @@ bool FVoxelForgeOpStackShaftTest::RunTest(const FString& Parameters)
|
||||
// 3. LE VERDICT DE BOÎTE
|
||||
//=========================================================================
|
||||
{
|
||||
int32 NumProved = 0, NumMixed = 0, NumUnsound = 0;
|
||||
int32 NumProved = 0, NumMixed = 0, NumUnsound = 0, NumBruteSamples = 0;
|
||||
FRandomStream Rng(13579);
|
||||
// Hors de la boucle : la ligne de rapport en a besoin. Une étendue d'échantillonnage qu'on
|
||||
// ne peut pas citer dans le rapport est une étendue que personne ne surveille.
|
||||
@@ -292,6 +292,7 @@ bool FVoxelForgeOpStackShaftTest::RunTest(const FString& Parameters)
|
||||
const float Y = (float)(Origin.Y + gy * Step);
|
||||
const float Z = (float)(Origin.Z + gz * Step);
|
||||
const float D = Stack.EvalMC(X, Y, Z);
|
||||
++NumBruteSamples;
|
||||
if (bClaimsSolid ? (D >= 0.0f) : (D < 0.0f))
|
||||
{
|
||||
if (NumUnsound == 0)
|
||||
@@ -299,8 +300,8 @@ bool FVoxelForgeOpStackShaftTest::RunTest(const FString& Parameters)
|
||||
AddError(FString::Printf(
|
||||
TEXT("HOLE: the shaft stack claimed %s for the box at (%d,%d,%d) but ")
|
||||
TEXT("EvalMC(%.0f, %.0f, %.0f) = %.6g is on the %s side. Suspects, in ")
|
||||
TEXT("order: the shaft source's ExtraReach (does it cover the roughness ")
|
||||
TEXT("amplitude AND the carve blend?), then the connector sweep (a ")
|
||||
TEXT("order: the shaft source's SDF interval (does it cover all shaft ")
|
||||
TEXT("and connector geometry?), then the connector sweep (a ")
|
||||
TEXT("connector can reach Spacing*1.6 beyond its cell), then the ledge ")
|
||||
TEXT("op's FillOnly."),
|
||||
bClaimsSolid ? TEXT("AllSolid") : TEXT("AllAir"),
|
||||
@@ -317,22 +318,22 @@ bool FVoxelForgeOpStackShaftTest::RunTest(const FString& Parameters)
|
||||
|
||||
AddInfo(FString::Printf(
|
||||
TEXT("Box verdicts over 60 VerticalShafts tiles (XY sampled from +/-%d voxels = %.1f x ")
|
||||
TEXT("ShaftSpacing %.0f): %d proved uniform, %d Mixed, brute-forced with %d violations. ")
|
||||
TEXT("ShaftSpacing %.0f): %d proved uniform, %d Mixed, %d voxels checked, %d violations. ")
|
||||
TEXT("This was 0 proved for as long as the connector branch bailed on mere shaft ")
|
||||
TEXT("EXISTENCE within Spacing*1.6 -- true almost everywhere at ShaftDensity 0.6, so it ")
|
||||
TEXT("was conservative AND sterile. It now tests the real connector capsules. Read the ")
|
||||
TEXT("proved count as a measurement; what is ASSERTED is that none of them is wrong, ")
|
||||
TEXT("because a false verdict here leaves no geometry and no collision."),
|
||||
SpanVoxels, (float)SpanVoxels / FMath::Max(P.ShaftSpacing, 1.0f), P.ShaftSpacing,
|
||||
NumProved, NumMixed, NumUnsound));
|
||||
NumProved, NumMixed, NumBruteSamples, NumUnsound));
|
||||
|
||||
if (NumProved == 0)
|
||||
{
|
||||
AddWarning(TEXT("No VerticalShafts tile was proved, so the brute force above verified ")
|
||||
TEXT("nothing. Before hypothesising: the shaft CIRCLE test and the connector ")
|
||||
TEXT("CAPSULE test are the only two things that can return CarveOnly here, ")
|
||||
TEXT("and ExtraReach inflates both -- check its value against ShaftMaxRadius ")
|
||||
TEXT("before touching either test."));
|
||||
TEXT("and the interval must cover both the shaft and connector capsules before ")
|
||||
TEXT("either test is relaxed."));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -278,7 +278,7 @@ bool FVoxelForgeOpStackSlabTest::RunTest(const FString& Parameters)
|
||||
// 3. LE VERDICT DE BOÎTE — ce que §3.1 a acheté
|
||||
//=====================================================================
|
||||
{
|
||||
int32 NumProved = 0, NumMixed = 0, NumUnsound = 0;
|
||||
int32 NumProved = 0, NumMixed = 0, NumUnsound = 0, NumBruteSamples = 0;
|
||||
FRandomStream Rng(24680 + SlotIndex);
|
||||
// Hors de la boucle : la ligne de rapport en a besoin. Une étendue d'échantillonnage qu'on
|
||||
// ne peut pas citer dans le rapport est une étendue que personne ne surveille.
|
||||
@@ -312,6 +312,7 @@ bool FVoxelForgeOpStackSlabTest::RunTest(const FString& Parameters)
|
||||
const float Y = (float)(Origin.Y + gy * Step);
|
||||
const float Z = (float)(Origin.Z + gz * Step);
|
||||
const float D = Stack.EvalMC(X, Y, Z);
|
||||
++NumBruteSamples;
|
||||
if (bClaimsSolid ? (D >= 0.0f) : (D < 0.0f))
|
||||
{
|
||||
if (NumUnsound == 0)
|
||||
@@ -320,7 +321,7 @@ bool FVoxelForgeOpStackSlabTest::RunTest(const FString& Parameters)
|
||||
TEXT("HOLE: %s claimed %s for the box at (%d,%d,%d) but ")
|
||||
TEXT("EvalMC(%.0f, %.0f, %.0f) = %.6g is on the %s side. One of the ")
|
||||
TEXT("ops is not conservative. Suspects, in order: the slab source's ")
|
||||
TEXT("noise amplitude bounds (does FBM really honour [-1,1]?), the ")
|
||||
TEXT("noise amplitude bounds (the proved FBM supremum is 1.5, not 1.0), the ")
|
||||
TEXT("ceiling clamp raising CeilSurface above CeilZ, then the column ")
|
||||
TEXT("mod's reach (MaxRadius + blend)."),
|
||||
SlotName, bClaimsSolid ? TEXT("AllSolid") : TEXT("AllAir"),
|
||||
@@ -340,12 +341,12 @@ bool FVoxelForgeOpStackSlabTest::RunTest(const FString& Parameters)
|
||||
|
||||
AddInfo(FString::Printf(
|
||||
TEXT("%s box verdicts over %d tiles (XY sampled from +/- %d voxels = %.1f x ")
|
||||
TEXT("ColumnSpacing %.0f): %d proved uniform, %d Mixed. Today's ")
|
||||
TEXT("ColumnSpacing %.0f): %d proved uniform, %d Mixed, %d voxels checked, %d violations. Today's ")
|
||||
TEXT("ClassifyTile proves ZERO of these. This number is the whole point of making ")
|
||||
TEXT("the slab surfaces XY-pure (OPSTACK-DECOMPOSITION 3.1)."),
|
||||
SlotName, NumSlabTiles, SpanVoxels,
|
||||
(float)SpanVoxels / FMath::Max(SlabParams.ColumnSpacing, 1.0f), SlabParams.ColumnSpacing,
|
||||
NumProved, NumMixed));
|
||||
NumProved, NumMixed, NumBruteSamples, NumUnsound));
|
||||
|
||||
if (NumProved == 0)
|
||||
{
|
||||
|
||||
@@ -1086,8 +1086,8 @@ bool FVoxelForgeOpStackTunnelTest::RunTest(const FString& Parameters)
|
||||
// 4. LE VERDICT DE BOÎTE — plus attendu nul, et CHAQUE VERDICT EST BRUTE-FORCÉ
|
||||
//=========================================================================
|
||||
// ⚠️ CE BLOC A CHANGÉ DE NATURE LE 2026-07-28, ET IL FAUT SAVOIR POURQUOI.
|
||||
// Il ASSERTAIT `NumProved == 0`. C'était juste tant que `FRoomGraphSource::EffectOverBox`
|
||||
// rendait `Both` inconditionnellement : « zéro » était alors une description honnête de l'état
|
||||
// Il ASSERTAIT `NumProved == 0`. C'était juste tant que la source de salles répondait
|
||||
// `Both` inconditionnellement : « zéro » était alors une description honnête de l'état
|
||||
// du portage. Depuis que la source répond SPATIALEMENT, asserter zéro reviendrait à interdire
|
||||
// le gain qu'on vient de construire — et pire, ça transformerait le test en gardien du bug.
|
||||
//
|
||||
@@ -1239,7 +1239,7 @@ bool FVoxelForgeOpStackTunnelTest::RunTest(const FString& Parameters)
|
||||
AddInfo(FString::Printf(
|
||||
TEXT("[%s] Box verdicts over 40 TunnelNetwork tiles: %d proved (%d AllSolid, %d AllAir), ")
|
||||
TEXT("%d Mixed -- brute-forced over %d voxels, %d violations. This number was 0 proved / ")
|
||||
TEXT("40 Mixed until FRoomGraphSource::EffectOverBox learned to answer spatially, and it ")
|
||||
TEXT("40 Mixed until the SDF source interval and state-aware fold learned to answer spatially, and it ")
|
||||
TEXT("is the single largest perf item of the whole plan (OPSTACK-DECOMPOSITION 0.2): a ")
|
||||
TEXT("proved tile skips GenerateMesh entirely, so it trades one BuildChunkCache against ")
|
||||
TEXT("30000+ density evaluations. Read the PROVED count as a measurement, never as a ")
|
||||
@@ -1408,6 +1408,11 @@ bool FVoxelForgeOpStackTunnelTest::RunTest(const FString& Parameters)
|
||||
UWCtx.StrateBottomWorldZ = UP.StrateBottomWorldZ;
|
||||
UWStack.PrepareChunk(UWCtx);
|
||||
|
||||
// Underwater shares the tunnel builder, but it is a separate production archetype. Keep
|
||||
// its box proof in the report so a future change cannot silently make only this slot
|
||||
// unsound or sterile.
|
||||
RunTileScan(UWStack, UP, UWCtx, TEXT("Underwater"), /*bZeroProvedIsExpected*/ false);
|
||||
|
||||
TestEqual(TEXT("the Underwater stack is the tunnel stack: same 20 ops"), UWStack.Num(), 20);
|
||||
|
||||
// 8 → 24 chunks, comme le balayage principal. Le premier run vert a rapporté **0 de 2000
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -39,22 +39,35 @@
|
||||
// the descent structure intact no matter what an author assembles.
|
||||
//
|
||||
// ─────────────────────────────────────────────────────────────────────────────────────────
|
||||
// LA CLÉ DE VOÛTE : EffectOverBox — direction, pas intervalle / THE KEYSTONE: direction, not intervals
|
||||
// LA CLÉ DE VOÛTE : EffectOverBox + intervalle SDF / THE KEYSTONE: EffectOverBox + SDF interval
|
||||
// ─────────────────────────────────────────────────────────────────────────────────────────
|
||||
// La version « complète » d'une borne rendrait un intervalle numérique. NE PAS COMMENCER LÀ.
|
||||
// Presque tout opérateur existant est UNIDIRECTIONNEL : il ne fait que creuser, ou que remplir.
|
||||
// Cela suffit à reproduire GÉNÉRIQUEMENT chaque garde écrite à la main dans ClassifyTile :
|
||||
// Un verdict de boîte doit être une preuve. Rendre `Both` coûte du CPU ; rendre `Identity` ou une
|
||||
// direction fausse supprime de la géométrie et peut laisser un joueur tomber au travers du monde.
|
||||
// En cas de doute, `Mixed`.
|
||||
//
|
||||
// Un opérateur qui écrit seulement le canal SDF ne peut pas honnêtement répondre pour le convertisseur
|
||||
// qui viendra peut-être après lui : il doit propager un intervalle SDF, et le convertisseur doit plier
|
||||
// cet intervalle avec sa propre formule. Chaque opérateur répond ainsi pour lui-même, et la composition
|
||||
// reste sûre même pour une combinaison que personne n'a écrite à la main auparavant.
|
||||
//
|
||||
// A box verdict is a proof. `Both` costs CPU; a false `Identity` or direction removes geometry and can
|
||||
// let a player fall through the world. When in doubt, return `Mixed`.
|
||||
// An SDF-only writer reports an SDF interval; a later converter folds that interval through its own
|
||||
// response. Each operator answers for itself, so an unverified composition remains sound.
|
||||
//
|
||||
// La première version de cette interface n'avait qu'une direction numérique. Presque tout opérateur
|
||||
// existant est UNIDIRECTIONNEL : il ne fait que creuser, ou que remplir. Cela suffit à reproduire
|
||||
// GÉNÉRIQUEMENT chaque garde écrite à la main dans ClassifyTile :
|
||||
//
|
||||
// « passages ⇒ bCanSolid = false » EST CarveOnly
|
||||
// « ponts/arêtes ⇒ bCanAir = false » EST FillOnly
|
||||
// « aucun passage près de cette boîte » EST Identity
|
||||
//
|
||||
// Donc la Phase 1 n'a besoin d'AUCUNE borne numérique et obtient déjà toute la propriété de
|
||||
// sûreté. Les intervalles sont un resserrement ultérieur pour le coût de génération, pas un
|
||||
// prérequis de correction. C'est ce qui rend le premier pas petit.
|
||||
// La direction reste le repli conservateur par défaut pour les opérateurs qui ne connaissent pas
|
||||
// d'intervalle ; elle ne permet jamais à un écrivain SDF d'usurper la réponse d'un voisin.
|
||||
//
|
||||
// Phase 1 needs NO numeric bounds and already gets the whole safety property. Intervals are a
|
||||
// later tightening for gen cost, not a correctness prerequisite.
|
||||
// Direction remains the conservative fallback for operators without an interval implementation;
|
||||
// it never lets an SDF writer impersonate a downstream neighbour.
|
||||
//
|
||||
// ─────────────────────────────────────────────────────────────────────────────────────────
|
||||
// LE GROS LOT PERF : les strates de grotte ne sautent AUCUNE tuile aujourd'hui
|
||||
@@ -283,6 +296,64 @@ struct FVoxelOpSample
|
||||
float Sdf = FLT_MAX;
|
||||
};
|
||||
|
||||
/**
|
||||
* Intervalle conservateur du canal SDF sur une boîte.
|
||||
*
|
||||
* `Min` et `Max` sont les bornes SUPRÊMES de toutes les valeurs que l'opérateur peut produire dans
|
||||
* la boîte. `[FLT_MAX, FLT_MAX]` est l'identité exacte : aucune surface connue. Un intervalle inconnu
|
||||
* est volontairement large ; il force le fold à rester `Mixed` plutôt que de risquer un faux tile
|
||||
* uniforme. Les setters refusent toute borne non finie ou inversée : un opérateur qui ne peut pas
|
||||
* prouver son intervalle perd le skip, jamais la géométrie.
|
||||
*
|
||||
* Conservative SDF interval over a box. `Min`/`Max` bound every value the operator can produce.
|
||||
* `[FLT_MAX, FLT_MAX]` is the exact "no surface" identity. Unknown is deliberately wide: it costs
|
||||
* a skip, never a hole. Invalid bounds become unknown instead of being guessed.
|
||||
*/
|
||||
struct FVoxelBoxSdfInterval
|
||||
{
|
||||
bool bKnown = true;
|
||||
float Min = FLT_MAX;
|
||||
float Max = FLT_MAX;
|
||||
|
||||
bool IsKnown() const { return bKnown; }
|
||||
|
||||
void SetUnknown()
|
||||
{
|
||||
bKnown = false;
|
||||
Min = -FLT_MAX;
|
||||
Max = FLT_MAX;
|
||||
}
|
||||
|
||||
void Set(float InMin, float InMax)
|
||||
{
|
||||
if (!FMath::IsFinite(InMin) || !FMath::IsFinite(InMax) || InMin > InMax)
|
||||
{
|
||||
SetUnknown();
|
||||
return;
|
||||
}
|
||||
bKnown = true;
|
||||
Min = InMin;
|
||||
Max = InMax;
|
||||
}
|
||||
|
||||
/** Output of `min(Input, Other)` when both intervals are known. */
|
||||
void MinWith(const FVoxelBoxSdfInterval& Other)
|
||||
{
|
||||
if (!bKnown || !Other.bKnown)
|
||||
{
|
||||
SetUnknown();
|
||||
return;
|
||||
}
|
||||
Min = FMath::Min(Min, Other.Min);
|
||||
Max = FMath::Min(Max, Other.Max);
|
||||
}
|
||||
};
|
||||
|
||||
// Forward declaration: the interval-aware EffectOverBox overloads consume the hypotheses that
|
||||
// have already been folded, while the hypotheses themselves are declared immediately below the
|
||||
// interface.
|
||||
struct FVoxelBoxHypotheses;
|
||||
|
||||
//=============================================================================
|
||||
// L'INTERFACE / THE INTERFACE
|
||||
//=============================================================================
|
||||
@@ -344,8 +415,9 @@ public:
|
||||
virtual void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const = 0;
|
||||
|
||||
/**
|
||||
* CONSERVATIF. Phase 1 : direction seule. Phase 3 : surcharge avec intervalle numérique.
|
||||
* Rendre Both est toujours sûr ; rendre le mauvais est un trou.
|
||||
* CONSERVATIF. The two-argument method is the intrinsic fallback. The state-aware overload
|
||||
* below is what the stack calls: it sees the SDF interval already produced by earlier ops.
|
||||
* Returning Both is always safe; returning the wrong direction is a hole.
|
||||
*
|
||||
* Le contrat est « conservatif », pas « forme close » : un opérateur A LE DROIT
|
||||
* D'ÉCHANTILLONNER pour répondre. C'est exactement ce que fait ClassifyTile aujourd'hui pour
|
||||
@@ -354,20 +426,20 @@ public:
|
||||
*
|
||||
* The contract is "conservative", not "closed-form": an op MAY sample to answer.
|
||||
*
|
||||
* ⚠️ SIMPLIFICATION DE PHASE 1, à connaître : une source qui n'écrit QUE le canal SDF ne touche
|
||||
* pas la densité par elle-même — c'est l'opérateur de conversion (`FSdfCarve`/`FSdfFill`) qui le
|
||||
* fait. Répondre honnêtement demanderait de propager un INTERVALLE de SDF à travers la requête
|
||||
* de boîte, exactement comme `Eval` propage une valeur de SDF. En attendant, **la source répond
|
||||
* pour la paire** (elle rend `CarveOnly`/`FillOnly` quand une primitive atteint la boîte,
|
||||
* `Identity` sinon) et la conversion rend `Identity`. Conservatif et correct ; à remplacer par
|
||||
* une requête de boîte à deux canaux quand les intervalles numériques arriveront (Phase 3).
|
||||
*
|
||||
* PHASE 1 SIMPLIFICATION: an SDF-only source answers for itself AND its conversion op; the
|
||||
* conversion returns Identity. Answering honestly needs an SDF INTERVAL threaded through the box
|
||||
* query, mirroring how Eval threads an SDF value. Conservative and correct meanwhile.
|
||||
* An SDF-only source MUST NOT answer for a converter that happens to follow it. It reports its
|
||||
* own interval through `PropagateSdfOverBox`; the converter's state-aware overload then folds
|
||||
* the interval through its own formula. This is the isolation rule that makes free composition
|
||||
* safe.
|
||||
*/
|
||||
virtual EVoxelOpEffect EffectOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx) const = 0;
|
||||
|
||||
/** State-aware effect. The default preserves old custom operators' conservative direction. */
|
||||
virtual EVoxelOpEffect EffectOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx,
|
||||
const FVoxelBoxHypotheses&) const
|
||||
{
|
||||
return EffectOverBox(VoxelBox, Ctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* ⚠️ LE PLIAGE QUI PORTE DES NOMBRES — `OPSTACK-DECOMPOSITION §0.2`, et le plus gros poste de
|
||||
* perf du plan.
|
||||
@@ -406,11 +478,38 @@ public:
|
||||
return FLT_MAX;
|
||||
}
|
||||
|
||||
/** State-aware amplitude. The default is the old, conservative amplitude. */
|
||||
virtual float MaxCarveOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx,
|
||||
const FVoxelBoxHypotheses&) const
|
||||
{
|
||||
return MaxCarveOverBox(VoxelBox, Ctx);
|
||||
}
|
||||
|
||||
virtual float MaxFillOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx) const
|
||||
{
|
||||
return FLT_MAX;
|
||||
}
|
||||
|
||||
/** State-aware amplitude. The default is the old, conservative amplitude. */
|
||||
virtual float MaxFillOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx,
|
||||
const FVoxelBoxHypotheses&) const
|
||||
{
|
||||
return MaxFillOverBox(VoxelBox, Ctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* Propagate this operator's own SDF result through a box query. Only call this for an operator
|
||||
* whose cached channel declaration writes `Sdf`. The default is unknown by construction: a new
|
||||
* SDF writer cannot accidentally make a false uniform verdict merely because it forgot this
|
||||
* method. Implementations that assign SDF replace the interval; min/accumulating writers use
|
||||
* `FVoxelBoxSdfInterval::MinWith`.
|
||||
*/
|
||||
virtual void PropagateSdfOverBox(FVoxelBoxSdfInterval& InOut, const FBox& VoxelBox,
|
||||
const FVoxelOpContext&) const
|
||||
{
|
||||
InOut.SetUnknown();
|
||||
}
|
||||
|
||||
/**
|
||||
* Pour un opérateur FORÇANT (celui dont `ClassifyBox` rend autre chose que `Mixed`) : de combien
|
||||
* la densité est-elle garantie du bon côté de zéro, PARTOUT dans la boîte ?
|
||||
@@ -537,6 +636,9 @@ struct FVoxelBoxHypotheses
|
||||
float SolidMargin = 0.0f;
|
||||
float AirMargin = 0.0f;
|
||||
|
||||
/** SDF interval after all SDF writers folded so far. It starts at the exact no-surface identity. */
|
||||
FVoxelBoxSdfInterval Sdf;
|
||||
|
||||
bool IsDead() const { return !bCanBeAllSolid && !bCanBeAllAir; }
|
||||
|
||||
/** Verdict final : exactement une hypothèse doit survivre. Égalité = prudence ⇒ Mixed. */
|
||||
@@ -626,14 +728,29 @@ FORCEINLINE void VF_FoldEffect(FVoxelBoxHypotheses& H, EVoxelOpEffect Effect,
|
||||
* pure FillOnly would have thrown away, while still letting a passage take it back.
|
||||
*/
|
||||
FORCEINLINE void VF_FoldOp(FVoxelBoxHypotheses& H, const IVoxelDensityOp& Op,
|
||||
const FBox& VoxelBox, const FVoxelOpContext& Ctx)
|
||||
const FBox& VoxelBox, const FVoxelOpContext& Ctx,
|
||||
bool bWritesSdf)
|
||||
{
|
||||
// Soundness bias: an unknown SDF interval is allowed to kill a skip, never to manufacture a
|
||||
// uniform tile. The interval is propagated only after this op's own effect has been folded, so
|
||||
// the next op sees exactly the SDF version that Eval would expose at this point in the stack.
|
||||
const EVoxelTileClass Forced = Op.ClassifyBox(VoxelBox, Ctx);
|
||||
if (Forced != EVoxelTileClass::Mixed)
|
||||
{
|
||||
VF_ForceHypotheses(H, Forced, Op.ForcedMarginOverBox(VoxelBox, Ctx));
|
||||
if (bWritesSdf) { Op.PropagateSdfOverBox(H.Sdf, VoxelBox, Ctx); }
|
||||
return;
|
||||
}
|
||||
VF_FoldEffect(H, Op.EffectOverBox(VoxelBox, Ctx),
|
||||
Op.MaxCarveOverBox(VoxelBox, Ctx), Op.MaxFillOverBox(VoxelBox, Ctx));
|
||||
VF_FoldEffect(H, Op.EffectOverBox(VoxelBox, Ctx, H),
|
||||
Op.MaxCarveOverBox(VoxelBox, Ctx, H),
|
||||
Op.MaxFillOverBox(VoxelBox, Ctx, H));
|
||||
if (bWritesSdf) { Op.PropagateSdfOverBox(H.Sdf, VoxelBox, Ctx); }
|
||||
}
|
||||
|
||||
/** Backward-compatible helper for unit tests and callers with no cached declaration metadata. */
|
||||
FORCEINLINE void VF_FoldOp(FVoxelBoxHypotheses& H, const IVoxelDensityOp& Op,
|
||||
const FBox& VoxelBox, const FVoxelOpContext& Ctx)
|
||||
{
|
||||
VF_FoldOp(H, Op, VoxelBox, Ctx,
|
||||
(Op.ChannelWrites() & VoxelOpChannels::Sdf) != 0);
|
||||
}
|
||||
@@ -154,7 +154,8 @@ public:
|
||||
FVoxelBoxHypotheses H;
|
||||
for (const FOpEntry& Entry : Ops)
|
||||
{
|
||||
VF_FoldOp(H, *Entry.Op, VoxelBox, Ctx);
|
||||
VF_FoldOp(H, *Entry.Op, VoxelBox, Ctx,
|
||||
(Entry.Writes & VoxelOpChannels::Sdf) != 0);
|
||||
// Do not early-out on a dead hypothesis: a later forcing structural post may
|
||||
// deliberately overwrite it (the XY edge seal is appended after passage carving).
|
||||
}
|
||||
@@ -189,7 +190,8 @@ public:
|
||||
const bool bSolidBefore = H.bCanBeAllSolid;
|
||||
const bool bAirBefore = H.bCanBeAllAir;
|
||||
|
||||
VF_FoldOp(H, *Ops[i].Op, VoxelBox, Ctx);
|
||||
VF_FoldOp(H, *Ops[i].Op, VoxelBox, Ctx,
|
||||
(Ops[i].Writes & VoxelOpChannels::Sdf) != 0);
|
||||
|
||||
if (bSolidBefore && !H.bCanBeAllSolid && OutSolidKiller == INDEX_NONE) { OutSolidKiller = i; }
|
||||
if (bAirBefore && !H.bCanBeAllAir && OutAirKiller == INDEX_NONE) { OutAirKiller = i; }
|
||||
@@ -270,14 +272,11 @@ namespace VoxelDensityOps
|
||||
* Écrit le canal SDF uniquement. Identité d'arête = hash(nœud inférieur, axe), donc deux
|
||||
* chunks adjacents NE PEUVENT PAS être en désaccord : pas de cache de chunk, pas de région
|
||||
* COLLECT, zéro risque de couture (AUDIT §6.4 — le motif à préférer). */
|
||||
/* `ExtraReach` = tout ce qui peut eLARGIR la portée du couloir en aval (amplitude de rugosité
|
||||
* rayon de blend du carve). La source répond pour la paire source+conversion dans
|
||||
* `EffectOverBox` (voir la note « SIMPLIFICATION DE PHASE 1 » dans VoxelDensityOp.h), donc elle
|
||||
* doit connaître cette marge, sinon sa réponse `Identity` serait un MENSONGE — c'est-à-dire un
|
||||
* trou. / The source answers for the source+conversion pair, so it must know the downstream
|
||||
* margin: an Identity that is wrong is a hole. */
|
||||
/** The source answers only for its own SDF field. Its box query publishes an SDF interval;
|
||||
* each later converter or modifier consumes that interval through the generic fold. It does
|
||||
* not know, and must not answer for, any downstream operator. */
|
||||
VOXELFORGE_API TUniquePtr<IVoxelDensityOp> MakeLatticeCorridorSource(const FMazeGenerationParams& P,
|
||||
int32 Seed, float ExtraReach);
|
||||
int32 Seed);
|
||||
|
||||
/** Rôle 3 — rugosité de paroi appliquée au canal SDF (variante Maze/Shafts/Islands).
|
||||
* `Frequency` est codée en dur au site d'appel aujourd'hui (0.12 pour Maze) ; l'exposer est
|
||||
@@ -302,7 +301,8 @@ namespace VoxelDensityOps
|
||||
/** Rôle 1 — la dalle : surface de sol + surface de plafond → champ de vide. **XY-PUR** depuis
|
||||
* OPSTACK-DECOMPOSITION §3.1 (le terme en Z des deux bruits est parti), ce qui lui donne un
|
||||
* `ClassifyBox` EXACT sans échantillonnage : les deux surfaces vivent dans des bandes en Z
|
||||
* bornées par le contrat [-1,1] de FBM. Sert FlatPlain **et** CrystalChamber. */
|
||||
* bornées par le supremum prouvé de FBM (1.5, not the nominal parameter label). Sert
|
||||
* FlatPlain **et** CrystalChamber. */
|
||||
VOXELFORGE_API TUniquePtr<IVoxelDensityOp> MakeSlabVoidSource(const FSlabGenerationParams& P, int32 Seed);
|
||||
|
||||
/** Rôle 3 — cylindres de hauteur infinie sur une grille monde. N'ajoute que du solide ⇒
|
||||
@@ -380,8 +380,9 @@ namespace VoxelDensityOps
|
||||
const UVoxelStrateManager* StrateManager);
|
||||
|
||||
/**
|
||||
* DIAGNOSTIC — la ventilation par CLASSE DE PRIMITIVE du dernier `FRoomGraphSource::EffectOverBox`
|
||||
* évalué sur ce thread. **Tests uniquement. N'entre dans aucune décision de génération.**
|
||||
* DIAGNOSTIC — la ventilation par CLASSE DE PRIMITIVE de la dernière propagation d'intervalle
|
||||
* de `FRoomGraphSource` évaluée sur ce thread. **Tests uniquement. N'entre dans aucune décision
|
||||
* de génération.**
|
||||
*
|
||||
* ⚠️ POURQUOI ÇA EXISTE PLUTÔT QUE D'ÊTRE REFAIT DANS LE TEST. Le test a déjà tout ce qu'il faut
|
||||
* pour rejouer le critère — il appelle `BuildChunkCache` ailleurs. Le rejouer serait une
|
||||
|
||||
Reference in new issue
Block a user