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
+1192
-889
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
|
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
|
enter `Eval`'s per-voxel loop. If `FVoxelOpSample` gains a field, add its channel bit and update
|
||||||
every operator declaration and the validator together.
|
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
|
- **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
|
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
|
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`. |
|
| `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. |
|
| `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. |
|
| `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. |
|
| `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. |
|
| `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`
|
### 3.2c Structural primitives — `Public/VoxelDensityPrimitives.h`
|
||||||
`VF_ApplyOriginSpine` · `VF_ApplyBoundarySeal` · `VF_ApplyPassageCarving` · `VF_ApplyXYEdgeSeal` —
|
`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. |
|
| `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::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::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::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::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::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. |
|
| `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. |
|
| `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. |
|
| `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::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. |
|
| `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`. |
|
| `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 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. |
|
| `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, 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. |
|
| `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. |
|
| `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. |
|
| `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. |
|
| `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. |
|
| `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. |
|
| `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()`. |
|
| `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. |
|
| `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. `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. |
|
| `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::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`. |
|
| `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. |
|
| `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. |
|
| `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. |
|
| `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. |
|
| `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. |
|
| `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`). |
|
| `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
|
**void** source with a **fill** instead of a rock source with a carve — and you get islands instead of
|
||||||
tunnels.
|
tunnels.
|
||||||
|
|
||||||
**Corrective ops must be FUSED, not ordered.** `FFloorBiasMod` exists only to undo what
|
⛔ **"Corrective ops must be FUSED" — WITHDRAWN 2026-09-04. The premise was false.**
|
||||||
`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.)
|
|
||||||
|
|
||||||
**Current implementation finding:** the fuse is not landed. Ten operators intervene between 4b and
|
This section used to assert: *"`FFloorBiasMod` exists only to undo what `FCaveRoughnessMod` did to
|
||||||
4h — `FCaveTerraceMod`, `FLayerLineMod`, `FRibbingMod`, `FCaveOverhangMod`, `FCaveCliffMod`,
|
floors... fuse them into one op with two internal phases."* **It does not, and they must not be fused.**
|
||||||
`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
|
Read the math rather than the names. `FCaveRoughnessMod` adds its own displacement:
|
||||||
density updates; the required bit-identical gate cannot be assumed. The current 20-op stack remains
|
`D += TotalRough`. Eleven ops later, `FFloorBiasMod` computes an **independent** term
|
||||||
in its original order until a fusion that preserves that order and math is available.
|
`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
|
**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).
|
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.
|
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**
|
The source+converter coupling is removed. `FVoxelBoxHypotheses` now carries an
|
||||||
(Phase 1 simplification) so it must be told the downstream `ExtraReach`."* The op interface says the
|
`FVoxelBoxSdfInterval`; each SDF writer publishes only the interval for its own field, and each
|
||||||
same in its own comment: a source writing only `Sdf` does not touch density itself, so it answers on
|
converter/detail operator folds that interval through its own formula. `FVoxelOpStack::VF_FoldOp`
|
||||||
behalf of the converter that follows it.
|
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
|
The interval is deliberately a proof, not a likelihood: invalid, non-finite, or unimplemented
|
||||||
verified**, and the failure is the silent one: a tile wrongly proved uniform means no geometry, no
|
bounds become unknown and therefore cost a skip rather than manufacture `AllSolid`/`AllAir`.
|
||||||
collision, no error, and a player falls through the floor.
|
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
|
The isolation deliverable is an unverified combination: `ConstantVoid → LatticeCorridorSource →
|
||||||
ISOLATION**, not correct-given-its-neighbours. That means propagating an **SDF interval** through the
|
SdfRoughness → SdfFill`, which no shipping builder emits. Across 256 boxes it proved 250 `AllAir`
|
||||||
box query instead of letting the source answer for the pair — which the interface comment already names
|
and 6 `Mixed`; **332,750 voxels checked, 0 violations**. The shipping archetype scans likewise
|
||||||
as the Phase 3 version of the contract. Not huge, but load-bearing, and it must land first.
|
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
|
### ⚠️ 6.2 Box-verdict bounds are proved against HAND-SET params
|
||||||
|
|
||||||
@@ -509,10 +525,12 @@ they became "findings" (§13).
|
|||||||
2026-09-04).
|
2026-09-04).
|
||||||
- ⏸ **Fuse corrective ops** (`FFloorBiasMod` into `FCaveRoughnessMod`) — blocked by the ten
|
- ⏸ **Fuse corrective ops** (`FFloorBiasMod` into `FCaveRoughnessMod`) — blocked by the ten
|
||||||
intervening `Density` readers/writers under the bit-identity requirement; see §3.2.
|
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).
|
- **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
|
### Tier 4 — the composer proper
|
||||||
Structure roll (§3.2) · parameter roll (§3.3) · reject-and-resample driven by Tier 2 (§3.5) · the
|
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
|
// 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);
|
FRandomStream Rng(24680);
|
||||||
// Hors de la boucle : la ligne de rapport en a besoin. Une étendue d'échantillonnage qu'on
|
// 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.
|
// 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 Y = (float)(Origin.Y + gy * Step);
|
||||||
const float Z = (float)(Origin.Z + gz * Step);
|
const float Z = (float)(Origin.Z + gz * Step);
|
||||||
const float D = Stack.EvalMC(X, Y, Z);
|
const float D = Stack.EvalMC(X, Y, Z);
|
||||||
|
++NumBruteSamples;
|
||||||
if (bClaimsSolid ? (D >= 0.0f) : (D < 0.0f))
|
if (bClaimsSolid ? (D >= 0.0f) : (D < 0.0f))
|
||||||
{
|
{
|
||||||
if (NumUnsound == 0)
|
if (NumUnsound == 0)
|
||||||
@@ -275,7 +276,7 @@ bool FVoxelForgeOpStackIslandTest::RunTest(const FString& Parameters)
|
|||||||
AddError(FString::Printf(
|
AddError(FString::Printf(
|
||||||
TEXT("HOLE: the island stack claimed %s for the box at (%d,%d,%d) but ")
|
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("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("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("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 ")
|
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(
|
AddInfo(FString::Printf(
|
||||||
TEXT("Box verdicts over 60 FloatingIslands tiles (XY sampled from +/- %d voxels = %.1f x ")
|
TEXT("Box verdicts over 60 FloatingIslands tiles (XY sampled from +/- %d voxels = %.1f x ")
|
||||||
TEXT("IslandSpacing %.0f): %d proved AllSolid, %d proved AllAir, ")
|
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("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)."),
|
TEXT("island strate is mostly exactly that (OPSTACK-DECOMPOSITION 7)."),
|
||||||
SpanVoxels, (float)SpanVoxels / FMath::Max(P.IslandSpacing, 1.0f), P.IslandSpacing,
|
SpanVoxels, (float)SpanVoxels / FMath::Max(P.IslandSpacing, 1.0f), P.IslandSpacing,
|
||||||
NumProvedSolid, NumProvedAir, NumMixed));
|
NumProvedSolid, NumProvedAir, NumMixed, NumBruteSamples, NumUnsound));
|
||||||
|
|
||||||
if (NumProvedAir == 0)
|
if (NumProvedAir == 0)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
|||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
constexpr int32 NumMazeSamples = 20000;
|
constexpr int32 NumMazeSamples = 20000;
|
||||||
|
constexpr int32 NumMazeBoxTests = 256;
|
||||||
|
|
||||||
/** Les params Maze de la strate Maze de la fixture, bornes Z de runtime comprises. */
|
/** Les params Maze de la strate Maze de la fixture, bornes Z de runtime comprises. */
|
||||||
bool ResolveMazeParams(const VoxelForgeTest::FTestWorld& World, FMazeGenerationParams& Out,
|
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
|
// 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.
|
// 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);
|
FRandomStream Rng(24680);
|
||||||
// Hors de la boucle : la ligne de rapport en a besoin. Une étendue d'échantillonnage qu'on
|
// 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.
|
// ne peut pas citer dans le rapport est une étendue que personne ne surveille.
|
||||||
const int32 SpanCells = 40;
|
const int32 SpanCells = 40;
|
||||||
const int32 SpanVoxels = SpanCells * 8; // Extent = Step * Cells = 1 * 8
|
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 Step = 1, Cells = 8; // petites tuiles : force brute tenable
|
||||||
const int32 Extent = Step * Cells;
|
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 Y = (float)(Origin.Y + gy * Step);
|
||||||
const float Z = (float)(Origin.Z + gz * Step);
|
const float Z = (float)(Origin.Z + gz * Step);
|
||||||
const float D = Stack.EvalMC(X, Y, Z);
|
const float D = Stack.EvalMC(X, Y, Z);
|
||||||
|
++NumBruteSamples;
|
||||||
if (bClaimsSolid ? (D >= 0.0f) : (D < 0.0f))
|
if (bClaimsSolid ? (D >= 0.0f) : (D < 0.0f))
|
||||||
{
|
{
|
||||||
if (NumUnsound == 0)
|
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("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("EvalMC(%.0f, %.0f, %.0f) = %.6g is on the %s side. One of the ops' ")
|
||||||
TEXT("EffectOverBox/ClassifyBox is not conservative. Suspects, in order: ")
|
TEXT("EffectOverBox/ClassifyBox is not conservative. Suspects, in order: ")
|
||||||
TEXT("the lattice source's ExtraReach (does it cover the roughness ")
|
TEXT("the source interval, the SDF roughness interval, the converter's ")
|
||||||
TEXT("amplitude AND the carve blend?), then the seal's forcing verdict."),
|
TEXT("threshold fold, then the seal's forcing verdict."),
|
||||||
bClaimsSolid ? TEXT("AllSolid") : TEXT("AllAir"),
|
bClaimsSolid ? TEXT("AllSolid") : TEXT("AllAir"),
|
||||||
Origin.X, Origin.Y, Origin.Z, X, Y, Z, D,
|
Origin.X, Origin.Y, Origin.Z, X, Y, Z, D,
|
||||||
(D >= 0.0f) ? TEXT("AIR") : TEXT("SOLID")));
|
(D >= 0.0f) ? TEXT("AIR") : TEXT("SOLID")));
|
||||||
@@ -323,12 +325,12 @@ bool FVoxelForgeOpStackMazeTest::RunTest(const FString& Parameters)
|
|||||||
NumUnsound, 0);
|
NumUnsound, 0);
|
||||||
|
|
||||||
AddInfo(FString::Printf(
|
AddInfo(FString::Printf(
|
||||||
TEXT("Box verdicts over 60 Maze tiles (XY sampled from +/- %d voxels = %.1f x ")
|
TEXT("Box verdicts over %d Maze tiles (XY sampled from +/- %d voxels = %.1f x ")
|
||||||
TEXT("CellSize %.0f): %d proved uniform, %d Mixed. Today's ClassifyTile ")
|
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("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."),
|
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,
|
NumMazeBoxTests, SpanVoxels, (float)SpanVoxels / FMath::Max(MazeParams.CellSize, 1.0f), MazeParams.CellSize,
|
||||||
NumProved, NumMixed));
|
NumProved, NumMixed, NumBruteSamples, NumUnsound));
|
||||||
|
|
||||||
if (NumProved == 0)
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -252,7 +252,7 @@ bool FVoxelForgeOpStackShaftTest::RunTest(const FString& Parameters)
|
|||||||
// 3. LE VERDICT DE BOÎTE
|
// 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);
|
FRandomStream Rng(13579);
|
||||||
// Hors de la boucle : la ligne de rapport en a besoin. Une étendue d'échantillonnage qu'on
|
// 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.
|
// 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 Y = (float)(Origin.Y + gy * Step);
|
||||||
const float Z = (float)(Origin.Z + gz * Step);
|
const float Z = (float)(Origin.Z + gz * Step);
|
||||||
const float D = Stack.EvalMC(X, Y, Z);
|
const float D = Stack.EvalMC(X, Y, Z);
|
||||||
|
++NumBruteSamples;
|
||||||
if (bClaimsSolid ? (D >= 0.0f) : (D < 0.0f))
|
if (bClaimsSolid ? (D >= 0.0f) : (D < 0.0f))
|
||||||
{
|
{
|
||||||
if (NumUnsound == 0)
|
if (NumUnsound == 0)
|
||||||
@@ -299,8 +300,8 @@ bool FVoxelForgeOpStackShaftTest::RunTest(const FString& Parameters)
|
|||||||
AddError(FString::Printf(
|
AddError(FString::Printf(
|
||||||
TEXT("HOLE: the shaft stack claimed %s for the box at (%d,%d,%d) but ")
|
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("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("order: the shaft source's SDF interval (does it cover all shaft ")
|
||||||
TEXT("amplitude AND the carve blend?), then the connector sweep (a ")
|
TEXT("and connector geometry?), then the connector sweep (a ")
|
||||||
TEXT("connector can reach Spacing*1.6 beyond its cell), then the ledge ")
|
TEXT("connector can reach Spacing*1.6 beyond its cell), then the ledge ")
|
||||||
TEXT("op's FillOnly."),
|
TEXT("op's FillOnly."),
|
||||||
bClaimsSolid ? TEXT("AllSolid") : TEXT("AllAir"),
|
bClaimsSolid ? TEXT("AllSolid") : TEXT("AllAir"),
|
||||||
@@ -317,22 +318,22 @@ bool FVoxelForgeOpStackShaftTest::RunTest(const FString& Parameters)
|
|||||||
|
|
||||||
AddInfo(FString::Printf(
|
AddInfo(FString::Printf(
|
||||||
TEXT("Box verdicts over 60 VerticalShafts tiles (XY sampled from +/-%d voxels = %.1f x ")
|
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("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("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("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("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."),
|
TEXT("because a false verdict here leaves no geometry and no collision."),
|
||||||
SpanVoxels, (float)SpanVoxels / FMath::Max(P.ShaftSpacing, 1.0f), P.ShaftSpacing,
|
SpanVoxels, (float)SpanVoxels / FMath::Max(P.ShaftSpacing, 1.0f), P.ShaftSpacing,
|
||||||
NumProved, NumMixed, NumUnsound));
|
NumProved, NumMixed, NumBruteSamples, NumUnsound));
|
||||||
|
|
||||||
if (NumProved == 0)
|
if (NumProved == 0)
|
||||||
{
|
{
|
||||||
AddWarning(TEXT("No VerticalShafts tile was proved, so the brute force above verified ")
|
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("nothing. Before hypothesising: the shaft CIRCLE test and the connector ")
|
||||||
TEXT("CAPSULE test are the only two things that can return CarveOnly here, ")
|
TEXT("CAPSULE test are the only two things that can return CarveOnly here, ")
|
||||||
TEXT("and ExtraReach inflates both -- check its value against ShaftMaxRadius ")
|
TEXT("and the interval must cover both the shaft and connector capsules before ")
|
||||||
TEXT("before touching either test."));
|
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é
|
// 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);
|
FRandomStream Rng(24680 + SlotIndex);
|
||||||
// Hors de la boucle : la ligne de rapport en a besoin. Une étendue d'échantillonnage qu'on
|
// 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.
|
// 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 Y = (float)(Origin.Y + gy * Step);
|
||||||
const float Z = (float)(Origin.Z + gz * Step);
|
const float Z = (float)(Origin.Z + gz * Step);
|
||||||
const float D = Stack.EvalMC(X, Y, Z);
|
const float D = Stack.EvalMC(X, Y, Z);
|
||||||
|
++NumBruteSamples;
|
||||||
if (bClaimsSolid ? (D >= 0.0f) : (D < 0.0f))
|
if (bClaimsSolid ? (D >= 0.0f) : (D < 0.0f))
|
||||||
{
|
{
|
||||||
if (NumUnsound == 0)
|
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("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("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("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("ceiling clamp raising CeilSurface above CeilZ, then the column ")
|
||||||
TEXT("mod's reach (MaxRadius + blend)."),
|
TEXT("mod's reach (MaxRadius + blend)."),
|
||||||
SlotName, bClaimsSolid ? TEXT("AllSolid") : TEXT("AllAir"),
|
SlotName, bClaimsSolid ? TEXT("AllSolid") : TEXT("AllAir"),
|
||||||
@@ -340,12 +341,12 @@ bool FVoxelForgeOpStackSlabTest::RunTest(const FString& Parameters)
|
|||||||
|
|
||||||
AddInfo(FString::Printf(
|
AddInfo(FString::Printf(
|
||||||
TEXT("%s box verdicts over %d tiles (XY sampled from +/- %d voxels = %.1f x ")
|
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("ClassifyTile proves ZERO of these. This number is the whole point of making ")
|
||||||
TEXT("the slab surfaces XY-pure (OPSTACK-DECOMPOSITION 3.1)."),
|
TEXT("the slab surfaces XY-pure (OPSTACK-DECOMPOSITION 3.1)."),
|
||||||
SlotName, NumSlabTiles, SpanVoxels,
|
SlotName, NumSlabTiles, SpanVoxels,
|
||||||
(float)SpanVoxels / FMath::Max(SlabParams.ColumnSpacing, 1.0f), SlabParams.ColumnSpacing,
|
(float)SpanVoxels / FMath::Max(SlabParams.ColumnSpacing, 1.0f), SlabParams.ColumnSpacing,
|
||||||
NumProved, NumMixed));
|
NumProved, NumMixed, NumBruteSamples, NumUnsound));
|
||||||
|
|
||||||
if (NumProved == 0)
|
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É
|
// 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.
|
// ⚠️ 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`
|
// Il ASSERTAIT `NumProved == 0`. C'était juste tant que la source de salles répondait
|
||||||
// rendait `Both` inconditionnellement : « zéro » était alors une description honnête de l'état
|
// `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
|
// 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.
|
// 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(
|
AddInfo(FString::Printf(
|
||||||
TEXT("[%s] Box verdicts over 40 TunnelNetwork tiles: %d proved (%d AllSolid, %d AllAir), ")
|
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("%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("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("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 ")
|
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;
|
UWCtx.StrateBottomWorldZ = UP.StrateBottomWorldZ;
|
||||||
UWStack.PrepareChunk(UWCtx);
|
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);
|
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
|
// 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.
|
// 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À.
|
// Un verdict de boîte doit être une preuve. Rendre `Both` coûte du CPU ; rendre `Identity` ou une
|
||||||
// Presque tout opérateur existant est UNIDIRECTIONNEL : il ne fait que creuser, ou que remplir.
|
// direction fausse supprime de la géométrie et peut laisser un joueur tomber au travers du monde.
|
||||||
// Cela suffit à reproduire GÉNÉRIQUEMENT chaque garde écrite à la main dans ClassifyTile :
|
// 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
|
// « passages ⇒ bCanSolid = false » EST CarveOnly
|
||||||
// « ponts/arêtes ⇒ bCanAir = false » EST FillOnly
|
// « ponts/arêtes ⇒ bCanAir = false » EST FillOnly
|
||||||
// « aucun passage près de cette boîte » EST Identity
|
// « 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
|
// La direction reste le repli conservateur par défaut pour les opérateurs qui ne connaissent pas
|
||||||
// sûreté. Les intervalles sont un resserrement ultérieur pour le coût de génération, pas un
|
// d'intervalle ; elle ne permet jamais à un écrivain SDF d'usurper la réponse d'un voisin.
|
||||||
// prérequis de correction. C'est ce qui rend le premier pas petit.
|
|
||||||
//
|
//
|
||||||
// Phase 1 needs NO numeric bounds and already gets the whole safety property. Intervals are a
|
// Direction remains the conservative fallback for operators without an interval implementation;
|
||||||
// later tightening for gen cost, not a correctness prerequisite.
|
// it never lets an SDF writer impersonate a downstream neighbour.
|
||||||
//
|
//
|
||||||
// ─────────────────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────────────────
|
||||||
// LE GROS LOT PERF : les strates de grotte ne sautent AUCUNE tuile aujourd'hui
|
// LE GROS LOT PERF : les strates de grotte ne sautent AUCUNE tuile aujourd'hui
|
||||||
@@ -283,6 +296,64 @@ struct FVoxelOpSample
|
|||||||
float Sdf = FLT_MAX;
|
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
|
// L'INTERFACE / THE INTERFACE
|
||||||
//=============================================================================
|
//=============================================================================
|
||||||
@@ -344,8 +415,9 @@ public:
|
|||||||
virtual void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const = 0;
|
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.
|
* CONSERVATIF. The two-argument method is the intrinsic fallback. The state-aware overload
|
||||||
* Rendre Both est toujours sûr ; rendre le mauvais est un trou.
|
* 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
|
* 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
|
* 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.
|
* 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
|
* An SDF-only source MUST NOT answer for a converter that happens to follow it. It reports its
|
||||||
* pas la densité par elle-même — c'est l'opérateur de conversion (`FSdfCarve`/`FSdfFill`) qui le
|
* own interval through `PropagateSdfOverBox`; the converter's state-aware overload then folds
|
||||||
* fait. Répondre honnêtement demanderait de propager un INTERVALLE de SDF à travers la requête
|
* the interval through its own formula. This is the isolation rule that makes free composition
|
||||||
* de boîte, exactement comme `Eval` propage une valeur de SDF. En attendant, **la source répond
|
* safe.
|
||||||
* 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.
|
|
||||||
*/
|
*/
|
||||||
virtual EVoxelOpEffect EffectOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx) const = 0;
|
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
|
* ⚠️ LE PLIAGE QUI PORTE DES NOMBRES — `OPSTACK-DECOMPOSITION §0.2`, et le plus gros poste de
|
||||||
* perf du plan.
|
* perf du plan.
|
||||||
@@ -406,11 +478,38 @@ public:
|
|||||||
return FLT_MAX;
|
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
|
virtual float MaxFillOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx) const
|
||||||
{
|
{
|
||||||
return FLT_MAX;
|
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
|
* 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 ?
|
* 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 SolidMargin = 0.0f;
|
||||||
float AirMargin = 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; }
|
bool IsDead() const { return !bCanBeAllSolid && !bCanBeAllAir; }
|
||||||
|
|
||||||
/** Verdict final : exactement une hypothèse doit survivre. Égalité = prudence ⇒ Mixed. */
|
/** 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.
|
* pure FillOnly would have thrown away, while still letting a passage take it back.
|
||||||
*/
|
*/
|
||||||
FORCEINLINE void VF_FoldOp(FVoxelBoxHypotheses& H, const IVoxelDensityOp& Op,
|
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);
|
const EVoxelTileClass Forced = Op.ClassifyBox(VoxelBox, Ctx);
|
||||||
if (Forced != EVoxelTileClass::Mixed)
|
if (Forced != EVoxelTileClass::Mixed)
|
||||||
{
|
{
|
||||||
VF_ForceHypotheses(H, Forced, Op.ForcedMarginOverBox(VoxelBox, Ctx));
|
VF_ForceHypotheses(H, Forced, Op.ForcedMarginOverBox(VoxelBox, Ctx));
|
||||||
|
if (bWritesSdf) { Op.PropagateSdfOverBox(H.Sdf, VoxelBox, Ctx); }
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
VF_FoldEffect(H, Op.EffectOverBox(VoxelBox, Ctx),
|
VF_FoldEffect(H, Op.EffectOverBox(VoxelBox, Ctx, H),
|
||||||
Op.MaxCarveOverBox(VoxelBox, Ctx), Op.MaxFillOverBox(VoxelBox, Ctx));
|
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;
|
FVoxelBoxHypotheses H;
|
||||||
for (const FOpEntry& Entry : Ops)
|
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
|
// 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).
|
// deliberately overwrite it (the XY edge seal is appended after passage carving).
|
||||||
}
|
}
|
||||||
@@ -189,7 +190,8 @@ public:
|
|||||||
const bool bSolidBefore = H.bCanBeAllSolid;
|
const bool bSolidBefore = H.bCanBeAllSolid;
|
||||||
const bool bAirBefore = H.bCanBeAllAir;
|
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 (bSolidBefore && !H.bCanBeAllSolid && OutSolidKiller == INDEX_NONE) { OutSolidKiller = i; }
|
||||||
if (bAirBefore && !H.bCanBeAllAir && OutAirKiller == INDEX_NONE) { OutAirKiller = 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
|
* É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
|
* 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). */
|
* 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é
|
/** The source answers only for its own SDF field. Its box query publishes an SDF interval;
|
||||||
* rayon de blend du carve). La source répond pour la paire source+conversion dans
|
* each later converter or modifier consumes that interval through the generic fold. It does
|
||||||
* `EffectOverBox` (voir la note « SIMPLIFICATION DE PHASE 1 » dans VoxelDensityOp.h), donc elle
|
* not know, and must not answer for, any downstream operator. */
|
||||||
* 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. */
|
|
||||||
VOXELFORGE_API TUniquePtr<IVoxelDensityOp> MakeLatticeCorridorSource(const FMazeGenerationParams& P,
|
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).
|
/** 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
|
* `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
|
/** 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
|
* 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
|
* `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);
|
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 ⇒
|
/** 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);
|
const UVoxelStrateManager* StrateManager);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DIAGNOSTIC — la ventilation par CLASSE DE PRIMITIVE du dernier `FRoomGraphSource::EffectOverBox`
|
* DIAGNOSTIC — la ventilation par CLASSE DE PRIMITIVE de la dernière propagation d'intervalle
|
||||||
* évalué sur ce thread. **Tests uniquement. N'entre dans aucune décision de génération.**
|
* 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
|
* ⚠️ 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
|
* pour rejouer le critère — il appelle `BuildChunkCache` ailleurs. Le rejouer serait une
|
||||||
|
|||||||
Reference in new issue
Block a user