fix(opstack): the worm inherits the room source's box verdict -- the actual blocker
First build of the spatial EffectOverBox came back green with 0 tiles proved of 40. The warning written for exactly that case fired, and then was not good enough: it offered two candidate causes and BOTH were wrong. The real one was a third operator neither candidate mentioned. FWormFieldSource answered CarveOnly unconditionally with a provably correct amplitude bound of WormStrength. But BaseDensity = 8 and WormStrength = 10 are the DEFAULTS -- the field's own comment requires the worm to exceed BaseDensity or it could never carve air. So SolidMargin = 8 - 10 < 0 on every tile of every strate with worms on, before the fold reached anything the room source proved. A numerically correct bound that is structurally always fatal. The fix was already written three lines up in the worm's own Eval: NetworkMask is 0 when CaveSDF >= WormNetworkRange, which FLT_MAX always satisfies. The worm IS spatially bounded -- by the room source's bound, exactly like the twelve detail modifiers -- so where the source proves Identity it does not execute at all. Thirteen inheritors instead of twelve. Verified FVoxelOpSample::Sdf really does initialise to FLT_MAX rather than assuming it; the inheritance inverts into a hole otherwise. Deliberately NOT VF_NoCaveOverBox: that helper answers "identity" for a null Rooms, correct for the twelve modifiers and wrong for an op a future assembly could place behind a different SDF writer. No room source means we do not know, which must cost CPU rather than a hole. And the instrument, because the guess is the thing that cost a build: a diagnostic that lists candidate causes without measuring them is still a guess wearing rigour. FVoxelOpStack::ClassifyBoxAttributed reports the index of the first op that kills each hypothesis -- same loop, same early-out, verdict identical to ClassifyBox, because a diagnostic that takes a different path than the thing it explains sends you hunting in the wrong place. IVoxelDensityOp::DebugName gives them names and touches no cache key, so it cannot change the world. Check 4 now always prints "AllSolid killed by: <op> xN", and the zero-proved warning says to read it instead of re-deriving. Unbuilt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -155,7 +155,7 @@ bit. They are port-correctness oracles, not fidelity checks: the acceptance bar
|
|||||||
| `VoxelDensityOps::BuildSurfaceStack` | — | SurfaceWorld, complete: column + overhang + 3 structural, plus biome blending when `PerBiomeParams` is non-empty. Takes ownership of an `IVoxelBiomeField`. |
|
| `VoxelDensityOps::BuildSurfaceStack` | — | SurfaceWorld, complete: column + overhang + 3 structural, plus biome blending when `PerBiomeParams` is non-empty. Takes ownership of an `IVoxelBiomeField`. |
|
||||||
| `VoxelDensityOps::BuildVerticalShaftStack` | — | 8 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`). The measured proof of `OPSTACK-PLAN §2.5`'s reuse claim. |
|
| `VoxelDensityOps::BuildVerticalShaftStack` | — | 8 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`). 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: it builds the cache for the queried box into a *second* per-worker cache (never `FState::Cache`), then lifts each primitive's per-voxel cull from point to box — rooms/tunnels as spheres vs the warp-dilated box, pits/chimneys vs the **undilated** box (real coords), columns as infinite cylinders. `Identity` ⇒ `Sdf` stays `FLT_MAX` ⇒ `FSdfConvertOp` **and all twelve detail modifiers** go identity with it. Verdict is memoised per box (the twelve all ask the same question). Warp dilation uses a **provable** `\|Perlin3D\| ≤ 2`, not the header's observed `~[-1,1]`. |
|
| `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: it builds the cache for the queried box into a *second* per-worker cache (never `FState::Cache`), then lifts each primitive's per-voxel cull from point to box — rooms/tunnels as spheres vs the warp-dilated box, pits/chimneys vs the **undilated** box (real coords), columns as infinite cylinders. `Identity` ⇒ `Sdf` stays `FLT_MAX` ⇒ `FSdfConvertOp` **and all twelve detail modifiers** go identity with it. Verdict is memoised per box (the twelve all ask the same question). Warp dilation uses a **provable** `\|Perlin3D\| ≤ 2`, not the header's observed `~[-1,1]`. |
|
||||||
| `FWormFieldSource` (internal) | 1 | Fielded 3D-noise threshold carve, masked by distance to the room network (reads `InOut.Sdf` *after* pits/chimneys). `EffectOverBox` → **`CarveOnly` everywhere** — no spatial bound, so it kills `AllSolid` on every tile of every strate with worms on. `MaxCarveAmplitude()` holds the bound from DECOMPOSITION §0.2 that would recover it, waiting for a fold that carries numbers. |
|
| `FWormFieldSource` (internal) | 1 | Fielded 3D-noise threshold carve, masked by distance to the room network (reads `InOut.Sdf` *after* pits/chimneys). **`EffectOverBox` INHERITS `FRoomGraphSource`'s verdict** since 2026-07-28 — its `Eval` sets `NetworkMask = 0` when `CaveSDF >= WormNetworkRange`, which `FLT_MAX` always satisfies, so where the room source proves `Identity` the worm doesn't execute at all. ⚠️ This was **the** blocker: `BaseDensity = 8` < `WormStrength = 10` **by default** (the field comment requires it), so an unconditional `CarveOnly` drove `SolidMargin` negative on every tile in the world and no room-source proof could survive behind it. Deliberately **not** `VF_NoCaveOverBox` — that helper answers "identity" for a null `Rooms`, which is wrong for an op that could sit behind a different SDF writer. |
|
||||||
| `VoxelDensityOps::BuildTunnelNetworkStack` | — | **COMPLETE, 19 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). 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). |
|
| `VoxelDensityOps::BuildTunnelNetworkStack` | — | **COMPLETE, 19 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). 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). |
|
||||||
| `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. |
|
||||||
|
|||||||
@@ -2583,3 +2583,99 @@ at the site rather than in a handoff.
|
|||||||
- Everything else should be **unchanged and green**. The §C2 fix changes generated terrain only
|
- Everything else should be **unchanged and green**. The §C2 fix changes generated terrain only
|
||||||
inside transition bands on the `switch` path (where it was previously order-dependent, i.e. not
|
inside transition bands on the `switch` path (where it was previously order-dependent, i.e. not
|
||||||
well-defined), so an equivalence test that moves is a real signal, not expected noise.
|
well-defined), so an equivalence test that moves is a real signal, not expected noise.
|
||||||
|
|
||||||
|
## 2026-07-28 — GREEN, and `0 proved of 40`. The blocker was the **worm**, not the room source.
|
||||||
|
|
||||||
|
The build was green and every number in the previous entry held. One line did not:
|
||||||
|
|
||||||
|
```
|
||||||
|
Box verdicts over 40 TunnelNetwork tiles: 0 proved (0 AllSolid, 0 AllAir), 40 Mixed
|
||||||
|
-- brute-forced over 0 voxels, 0 violations.
|
||||||
|
WARNING: No TunnelNetwork tile was proved ... this check verified nothing.
|
||||||
|
```
|
||||||
|
|
||||||
|
**The warning I wrote for exactly this case fired, and then it was not good enough.** It offered two
|
||||||
|
candidate causes — "the tiles genuinely straddle cave" or "the source isn't reaching its `Identity`
|
||||||
|
branch" — and **both were wrong**. The real cause was a third operator that neither candidate
|
||||||
|
mentioned. That is the failure worth recording, more than the bug itself.
|
||||||
|
|
||||||
|
### The cause, found by reading two default values
|
||||||
|
|
||||||
|
`FWormFieldSource::EffectOverBox` answered `CarveOnly` unconditionally, with a *provably correct*
|
||||||
|
amplitude bound of `WormStrength`. And in `VoxelStrateTypes.h`:
|
||||||
|
|
||||||
|
```
|
||||||
|
float BaseDensity = 8.0f;
|
||||||
|
float WormStrength = 10.0f; // "Must exceed BaseDensity to create air." <- the field's own comment
|
||||||
|
```
|
||||||
|
|
||||||
|
So `SolidMargin = 8 − 10 = −2 < 0`, on **every tile of every strate with worms on**, before the fold
|
||||||
|
ever reached anything the room source had proved. The worm's bound is not loose by accident — the
|
||||||
|
defaults *require* it to exceed `BaseDensity`, or worms could never carve. A numerically correct
|
||||||
|
bound that is structurally always fatal.
|
||||||
|
|
||||||
|
### The fix was already written in the worm's own `Eval`, three lines up
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
if (CaveSDF >= P.WormNetworkRange) // vrai aussi quand il n'y a pas de réseau (FLT_MAX)
|
||||||
|
{ NetworkMask = 0.0f; }
|
||||||
|
...
|
||||||
|
if (NetworkMask <= 0.0f) { return; }
|
||||||
|
```
|
||||||
|
|
||||||
|
**The worm IS spatially bounded** — not by a bound of its own, but by the room source's, exactly like
|
||||||
|
the twelve detail modifiers. Where `FRoomGraphSource` proves `Identity`, `Sdf` stays `FLT_MAX`
|
||||||
|
(verified: `FVoxelOpSample::Sdf = FLT_MAX` is the initialiser), so `NetworkMask` is 0 at every voxel
|
||||||
|
and `Eval` returns before touching `Density`. The worm is the *identity* there, not "a bounded
|
||||||
|
carve". It simply never asked the question.
|
||||||
|
|
||||||
|
So it now inherits the verdict — thirteen inheritors instead of twelve. Two details:
|
||||||
|
|
||||||
|
- **Deliberately NOT `VF_NoCaveOverBox`.** That helper returns `true` when `Rooms == nullptr`, which
|
||||||
|
is right for the twelve modifiers (they only ever exist in a stack where the room source is the
|
||||||
|
sole SDF writer) and **wrong** for the worm, which a future assembly could place behind a different
|
||||||
|
SDF writer — `FLatticeCorridorSource` writes that channel too. No room source ⇒ we don't know ⇒
|
||||||
|
`CarveOnly`. Not knowing must cost CPU, never a hole.
|
||||||
|
- `MaxCarveOverBox` now calls `EffectOverBox` rather than re-testing the condition, so the two cannot
|
||||||
|
drift. The room source's verdict memo makes the second call free.
|
||||||
|
|
||||||
|
### The lesson, and the instrument that came out of it
|
||||||
|
|
||||||
|
**A diagnostic that lists candidate causes without measuring them is still a guess** — it just looks
|
||||||
|
like rigour. My warning named two causes and had a number for neither, so a green run with a real
|
||||||
|
defect in it produced a message that sent the reader to the wrong two places.
|
||||||
|
|
||||||
|
`FVoxelOpStack::ClassifyBoxAttributed` now exists: the same fold, verdict-identical to `ClassifyBox`
|
||||||
|
(same loop, same early-out — a diagnostic that takes a different path than the thing it explains is
|
||||||
|
worse than none), reporting the **index of the first operator that kills each hypothesis**.
|
||||||
|
`IVoxelDensityOp::DebugName()` gives them readable names; it touches no cache key and no generation
|
||||||
|
decision, so it cannot change the world. Check 4 prints:
|
||||||
|
|
||||||
|
```
|
||||||
|
AllSolid killed by: <op> x<count>, <op> x<count>, ...
|
||||||
|
```
|
||||||
|
|
||||||
|
Always printed, not only on failure — when tiles *are* proved, that line is what says why the rest
|
||||||
|
are not. And the `0 proved` warning now says **"do not re-derive the cause, read the attribution
|
||||||
|
line"**, because the next person's guess would be as good as mine was.
|
||||||
|
|
||||||
|
### Ready to build. Likely compile-error spots
|
||||||
|
|
||||||
|
1. `FWormFieldSource` gained a third ctor arg (defaulted) and a `Rooms` member; its single
|
||||||
|
construction site in `BuildTunnelNetworkStack` passes `RoomPtr`, which is already in scope there.
|
||||||
|
2. `IVoxelDensityOp::DebugName()` is a new virtual with a default — six overrides added
|
||||||
|
(`ConstantFieldSource`, `RoomGraphSource`, `SdfConvertOp`, `OriginSpineOp`, `BoundarySealOp`,
|
||||||
|
`PassageCarveOp`, `WormFieldSource`). Everything else inherits `"(unnamed op)"` and reports by index.
|
||||||
|
3. `ClassifyBoxAttributed` / `GetOpDebugName` are new inline methods on `FVoxelOpStack` (header).
|
||||||
|
4. The test uses `TMap<FString,int32>::ValueSort` and `FindOrAdd` on a `const TCHAR*` key.
|
||||||
|
|
||||||
|
### What to read, in order
|
||||||
|
|
||||||
|
1. **`AllSolid killed by:`** — the new line. If it is empty ("AllSolid survived every tile") the
|
||||||
|
prize has landed. If it names `RoomGraphSource`, the tiles genuinely straddle cave and the sampler
|
||||||
|
is what to look at. If it names anything else, that operator's box answer is more pessimistic than
|
||||||
|
its `Eval`, and it is now named rather than guessed at.
|
||||||
|
2. **`Box verdicts over 40`** — proved count is a measurement; the assertion is only that no proved
|
||||||
|
tile is *wrong*.
|
||||||
|
3. Everything else should be unchanged. The worm change cannot alter density: `EffectOverBox` and
|
||||||
|
`MaxCarveOverBox` are box-verdict methods, and `Eval` is untouched.
|
||||||
|
|||||||
@@ -1106,6 +1106,7 @@ bool FVoxelForgeOpStackTunnelTest::RunTest(const FString& Parameters)
|
|||||||
int32 NumProved = 0, NumMixed = 0, NumSolid = 0, NumAir = 0;
|
int32 NumProved = 0, NumMixed = 0, NumSolid = 0, NumAir = 0;
|
||||||
int32 NumBruteSamples = 0, NumViolations = 0;
|
int32 NumBruteSamples = 0, NumViolations = 0;
|
||||||
float WorstViolation = 0.0f;
|
float WorstViolation = 0.0f;
|
||||||
|
TMap<FString, int32> SolidKillerCounts;
|
||||||
|
|
||||||
FRandomStream Rng(97531);
|
FRandomStream Rng(97531);
|
||||||
for (int32 t = 0; t < 40; ++t)
|
for (int32 t = 0; t < 40; ++t)
|
||||||
@@ -1121,7 +1122,18 @@ bool FVoxelForgeOpStackTunnelTest::RunTest(const FString& Parameters)
|
|||||||
FVector(Origin.X - Step, Origin.Y - Step, Origin.Z - Step),
|
FVector(Origin.X - Step, Origin.Y - Step, Origin.Z - Step),
|
||||||
FVector(Origin.X + GridDim * Step, Origin.Y + GridDim * Step, Origin.Z + GridDim * Step));
|
FVector(Origin.X + GridDim * Step, Origin.Y + GridDim * Step, Origin.Z + GridDim * Step));
|
||||||
|
|
||||||
const EVoxelTileClass Verdict = Stack.ClassifyBox(Box, Ctx);
|
// ATTRIBUTION — le même pliage, mais il dit QUI tue chaque hypothèse. Le premier build
|
||||||
|
// de l'`EffectOverBox` spatial est revenu vert avec 0 tuile prouvée, et le rapport ne
|
||||||
|
// savait nommer aucun coupable : les deux causes que la mise en garde proposait étaient
|
||||||
|
// toutes les deux fausses, la vraie étant un troisième opérateur. On ne redevine pas.
|
||||||
|
int32 SolidKiller = INDEX_NONE, AirKiller = INDEX_NONE;
|
||||||
|
const EVoxelTileClass Verdict = Stack.ClassifyBoxAttributed(Box, Ctx, SolidKiller, AirKiller);
|
||||||
|
|
||||||
|
if (SolidKiller != INDEX_NONE)
|
||||||
|
{
|
||||||
|
SolidKillerCounts.FindOrAdd(Stack.GetOpDebugName(SolidKiller))++;
|
||||||
|
}
|
||||||
|
|
||||||
if (Verdict == EVoxelTileClass::Mixed) { ++NumMixed; continue; }
|
if (Verdict == EVoxelTileClass::Mixed) { ++NumMixed; continue; }
|
||||||
|
|
||||||
++NumProved;
|
++NumProved;
|
||||||
@@ -1158,13 +1170,37 @@ bool FVoxelForgeOpStackTunnelTest::RunTest(const FString& Parameters)
|
|||||||
TEXT("verdict leaves no geometry and no collision behind it."),
|
TEXT("verdict leaves no geometry and no collision behind it."),
|
||||||
NumProved, NumSolid, NumAir, NumMixed, NumBruteSamples, NumViolations));
|
NumProved, NumSolid, NumAir, NumMixed, NumBruteSamples, NumViolations));
|
||||||
|
|
||||||
|
// QUI TUE `AllSolid`, ET COMBIEN DE FOIS. Toujours imprimé, pas seulement en cas d'échec :
|
||||||
|
// c'est aussi la ligne qui dit, quand des tuiles SONT prouvées, ce qui bloque les autres.
|
||||||
|
{
|
||||||
|
SolidKillerCounts.ValueSort([](int32 A, int32 B) { return A > B; });
|
||||||
|
FString Breakdown;
|
||||||
|
for (const TPair<FString, int32>& Kv : SolidKillerCounts)
|
||||||
|
{
|
||||||
|
if (!Breakdown.IsEmpty()) { Breakdown += TEXT(", "); }
|
||||||
|
Breakdown += FString::Printf(TEXT("%s x%d"), *Kv.Key, Kv.Value);
|
||||||
|
}
|
||||||
|
if (Breakdown.IsEmpty()) { Breakdown = TEXT("nothing -- AllSolid survived every tile"); }
|
||||||
|
|
||||||
|
AddInfo(FString::Printf(
|
||||||
|
TEXT("AllSolid killed by: %s. This is the line that replaced a guess. The first ")
|
||||||
|
TEXT("build of the spatial EffectOverBox reported 0 proved of 40, and the warning ")
|
||||||
|
TEXT("offered two candidate causes -- BOTH WRONG. The real one was a third operator ")
|
||||||
|
TEXT("nobody was looking at: FWormFieldSource answered CarveOnly everywhere, and ")
|
||||||
|
TEXT("since BaseDensity=8 < WormStrength=10 BY DEFAULT, its provable amplitude bound ")
|
||||||
|
TEXT("alone drove SolidMargin negative on every tile in the world. Attribution is ")
|
||||||
|
TEXT("cheap; a second wrong guess is not."),
|
||||||
|
*Breakdown));
|
||||||
|
}
|
||||||
|
|
||||||
if (NumProved == 0)
|
if (NumProved == 0)
|
||||||
{
|
{
|
||||||
AddWarning(TEXT("No TunnelNetwork tile was proved. That is not a failure, but it means ")
|
AddWarning(TEXT("No TunnelNetwork tile was proved, so the brute force below verified ")
|
||||||
TEXT("this check verified nothing: the brute force below has no verdict to ")
|
TEXT("nothing -- it has no verdict to contradict. Do NOT re-derive the cause: ")
|
||||||
TEXT("contradict. Either the sampled tiles all genuinely straddle cave, or ")
|
TEXT("read the 'AllSolid killed by' line above, which names the operator and ")
|
||||||
TEXT("the spatial EffectOverBox is not reaching its Identity branch -- the ")
|
TEXT("counts how often. If it names RoomGraphSource, the tiles genuinely ")
|
||||||
TEXT("bake-coverage line of check 5b is the one that tells those apart."));
|
TEXT("straddle cave (check the bake-coverage line of 5b); anything else is an ")
|
||||||
|
TEXT("operator whose box answer is more pessimistic than its Eval."));
|
||||||
}
|
}
|
||||||
|
|
||||||
TestEqual(FString::Printf(
|
TestEqual(FString::Printf(
|
||||||
|
|||||||
@@ -144,6 +144,8 @@ namespace
|
|||||||
return FMath::Abs(Value);
|
return FMath::Abs(Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const TCHAR* DebugName() const override { return TEXT("ConstantFieldSource"); }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
float Value;
|
float Value;
|
||||||
};
|
};
|
||||||
@@ -1119,6 +1121,8 @@ namespace
|
|||||||
return EVoxelOpEffect::Identity; // la source a répondu pour la paire
|
return EVoxelOpEffect::Identity; // la source a répondu pour la paire
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const TCHAR* DebugName() const override { return TEXT("SdfConvertOp"); }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
float Blend, BaseDensity, Sign, MinDivisor;
|
float Blend, BaseDensity, Sign, MinDivisor;
|
||||||
};
|
};
|
||||||
@@ -1159,6 +1163,8 @@ namespace
|
|||||||
return EVoxelOpEffect::CarveOnly;
|
return EVoxelOpEffect::CarveOnly;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const TCHAR* DebugName() const override { return TEXT("OriginSpineOp"); }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
float TopZ, BotZ, Seal, Base, Radius;
|
float TopZ, BotZ, Seal, Base, Radius;
|
||||||
};
|
};
|
||||||
@@ -1226,6 +1232,8 @@ namespace
|
|||||||
return EVoxelOpEffect::FillOnly;
|
return EVoxelOpEffect::FillOnly;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const TCHAR* DebugName() const override { return TEXT("BoundarySealOp"); }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
float TopZ, BotZ, Thickness, Base;
|
float TopZ, BotZ, Thickness, Base;
|
||||||
};
|
};
|
||||||
@@ -1258,6 +1266,8 @@ namespace
|
|||||||
? EVoxelOpEffect::CarveOnly : EVoxelOpEffect::Identity;
|
? EVoxelOpEffect::CarveOnly : EVoxelOpEffect::Identity;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const TCHAR* DebugName() const override { return TEXT("PassageCarveOp"); }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
const UVoxelStrateManager* Manager;
|
const UVoxelStrateManager* Manager;
|
||||||
float Base, Seal;
|
float Base, Seal;
|
||||||
@@ -2393,6 +2403,8 @@ namespace
|
|||||||
return B.Verdict;
|
return B.Verdict;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const TCHAR* DebugName() const override { return TEXT("RoomGraphSource"); }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
FStrateGenerationParams P;
|
FStrateGenerationParams P;
|
||||||
int32 Seed;
|
int32 Seed;
|
||||||
@@ -3505,8 +3517,12 @@ namespace
|
|||||||
class FWormFieldSource final : public IVoxelDensityOp
|
class FWormFieldSource final : public IVoxelDensityOp
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
FWormFieldSource(const FStrateGenerationParams& InP, int32 Seed)
|
/** @param InRooms ⚠️ UNIQUEMENT pour `EffectOverBox` / `MaxCarveOverBox`. `Eval` lit le
|
||||||
: P(InP), SeedU((uint32)Seed) {}
|
* canal SDF de `InOut`, pas ce pointeur — le ver n'interroge jamais la
|
||||||
|
* source directement, il consomme ce qu'elle a écrit. Peut être nullptr. */
|
||||||
|
FWormFieldSource(const FStrateGenerationParams& InP, int32 Seed,
|
||||||
|
const FRoomGraphSource* InRooms = nullptr)
|
||||||
|
: P(InP), SeedU((uint32)Seed), Rooms(InRooms) {}
|
||||||
|
|
||||||
EVoxelOpRole GetRole() const override { return EVoxelOpRole::FieldSource; }
|
EVoxelOpRole GetRole() const override { return EVoxelOpRole::FieldSource; }
|
||||||
void PrepareChunk(const FVoxelOpContext&) override {}
|
void PrepareChunk(const FVoxelOpContext&) override {}
|
||||||
@@ -3572,10 +3588,57 @@ namespace
|
|||||||
* est solide de plus que la somme des carves restants » redevient prouvable — et c'est le
|
* est solide de plus que la somme des carves restants » redevient prouvable — et c'est le
|
||||||
* plus gros poste de perf du plan. Noté ici, au point exact où la borne manque.
|
* plus gros poste de perf du plan. Noté ici, au point exact où la borne manque.
|
||||||
*/
|
*/
|
||||||
EVoxelOpEffect EffectOverBox(const FBox&, const FVoxelOpContext&) const override
|
/**
|
||||||
|
* ✅ **LE VER HÉRITE DU VERDICT DE LA SOURCE DE SALLES — ET C'EST CE QUI DÉBLOQUE TOUT.**
|
||||||
|
*
|
||||||
|
* La note ci-dessus (« aucune borne spatiale, donc il tue `AllSolid` sur CHAQUE tuile »)
|
||||||
|
* était vraie, et pourtant elle passait à côté de ce que son propre `Eval` fait trois
|
||||||
|
* lignes plus haut :
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* if (CaveSDF >= P.WormNetworkRange) // vrai aussi quand il n'y a pas de réseau (FLT_MAX)
|
||||||
|
* { NetworkMask = 0.0f; }
|
||||||
|
* ...
|
||||||
|
* if (NetworkMask <= 0.0f) { return; }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* **Le ver EST spatialement borné** — pas par une borne à lui, mais par celle de la source
|
||||||
|
* de salles, exactement comme les douze modificateurs de détail. Là où `FRoomGraphSource`
|
||||||
|
* prouve `Identity`, `Sdf` reste `FLT_MAX` sur toute la boîte, donc `NetworkMask` vaut 0
|
||||||
|
* partout, donc ce `return` est pris à chaque voxel. Le ver est l'identité, pas « un carve
|
||||||
|
* borné » : il ne s'exécute pas.
|
||||||
|
*
|
||||||
|
* ⚠️ POURQUOI CE CONTRÔLE COMPTAIT AUTANT. `BaseDensity = 8` et `WormStrength = 10` sont
|
||||||
|
* les DÉFAUTS, et le commentaire de `WormStrength` dit pourquoi (« must exceed BaseDensity
|
||||||
|
* to create air »). Donc `SolidMargin = 8 − 10 < 0` : tant que le ver rendait `CarveOnly`
|
||||||
|
* partout, il tuait `AllSolid` sur **toutes** les tuiles, et la réponse spatiale de la
|
||||||
|
* source de salles ne pouvait rien prouver derrière lui. Le premier build l'a montré —
|
||||||
|
* 0 tuile prouvée sur 40, la source ayant pourtant appris à répondre.
|
||||||
|
*
|
||||||
|
* ⚠️ ET POURQUOI ON N'UTILISE **PAS** `VF_NoCaveOverBox` ICI. Cet assistant rend `true`
|
||||||
|
* quand `Rooms == nullptr` — correct pour les douze modificateurs, qui n'existent que dans
|
||||||
|
* une pile où la source de salles est le seul écrivain du canal SDF. Le ver, lui, est un
|
||||||
|
* opérateur dont un futur assemblage pourrait le placer derrière un AUTRE écrivain de SDF
|
||||||
|
* (`FLatticeCorridorSource` en écrit un). Sans source de salles, on ne sait pas : on rend
|
||||||
|
* `CarveOnly`. Ne pas savoir doit coûter du CPU, jamais un trou.
|
||||||
|
*
|
||||||
|
* The worm IS spatially bounded — by the room source's bound, not one of its own, exactly
|
||||||
|
* like the twelve detail modifiers. Where the room source proves Identity, Sdf stays
|
||||||
|
* FLT_MAX, NetworkMask is 0 everywhere and Eval returns immediately. This mattered because
|
||||||
|
* BaseDensity=8 < WormStrength=10 BY DEFAULT, so an unconditional CarveOnly killed AllSolid
|
||||||
|
* on every tile. Deliberately not VF_NoCaveOverBox: its null-Rooms case answers "identity",
|
||||||
|
* which is wrong for an op that could sit behind a different SDF writer.
|
||||||
|
*/
|
||||||
|
EVoxelOpEffect EffectOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx) const override
|
||||||
{
|
{
|
||||||
return (P.WormStrength > 0.0f && P.WormThreshold > 0.0f) ? EVoxelOpEffect::CarveOnly
|
if (!(P.WormStrength > 0.0f && P.WormThreshold > 0.0f)) { return EVoxelOpEffect::Identity; }
|
||||||
: EVoxelOpEffect::Identity;
|
|
||||||
|
if (P.WormNetworkRange > 0.0f && Rooms != nullptr
|
||||||
|
&& Rooms->EffectOverBox(VoxelBox, Ctx) == EVoxelOpEffect::Identity)
|
||||||
|
{
|
||||||
|
return EVoxelOpEffect::Identity;
|
||||||
|
}
|
||||||
|
return EVoxelOpEffect::CarveOnly;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -3588,8 +3651,12 @@ namespace
|
|||||||
* la seule sorte qui ait le droit d'être ici : sur-estimer coûte du CPU, sous-estimer fait
|
* la seule sorte qui ait le droit d'être ici : sur-estimer coûte du CPU, sous-estimer fait
|
||||||
* un trou.
|
* un trou.
|
||||||
*/
|
*/
|
||||||
float MaxCarveOverBox(const FBox&, const FVoxelOpContext&) const override
|
float MaxCarveOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx) const override
|
||||||
{
|
{
|
||||||
|
// Cohérent avec `EffectOverBox` PAR CONSTRUCTION plutôt que par relecture : deux
|
||||||
|
// conditions écrites deux fois finiraient par diverger. Le mémo de verdict de
|
||||||
|
// `FRoomGraphSource` rend ce second appel gratuit.
|
||||||
|
if (EffectOverBox(VoxelBox, Ctx) == EVoxelOpEffect::Identity) { return 0.0f; }
|
||||||
return MaxCarveAmplitude();
|
return MaxCarveAmplitude();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3599,15 +3666,19 @@ namespace
|
|||||||
return 0.0f;
|
return 0.0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** L'amplitude max de carve, en unités de densité. */
|
/** L'amplitude max de carve, en unités de densité. Borne BRUTE : elle ignore la portée du
|
||||||
|
* réseau, c'est `MaxCarveOverBox` qui l'applique. */
|
||||||
float MaxCarveAmplitude() const
|
float MaxCarveAmplitude() const
|
||||||
{
|
{
|
||||||
return (P.WormStrength > 0.0f && P.WormThreshold > 0.0f) ? P.WormStrength : 0.0f;
|
return (P.WormStrength > 0.0f && P.WormThreshold > 0.0f) ? P.WormStrength : 0.0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const TCHAR* DebugName() const override { return TEXT("WormFieldSource"); }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
FStrateGenerationParams P;
|
FStrateGenerationParams P;
|
||||||
uint32 SeedU;
|
uint32 SeedU;
|
||||||
|
const FRoomGraphSource* Rooms; // NON possédant — peut être nullptr (voir EffectOverBox)
|
||||||
};
|
};
|
||||||
|
|
||||||
} // ⚠️ FIN DU NAMESPACE ANONYME — TOUT NOUVEL OPÉRATEUR SE MET AU-DESSUS DE CETTE LIGNE.
|
} // ⚠️ FIN DU NAMESPACE ANONYME — TOUT NOUVEL OPÉRATEUR SE MET AU-DESSUS DE CETTE LIGNE.
|
||||||
@@ -3819,7 +3890,11 @@ namespace VoxelDensityOps
|
|||||||
OutStack.Add(MakeUnique<FDomeMod>(P, RoomPtr)); // 4g — dômes
|
OutStack.Add(MakeUnique<FDomeMod>(P, RoomPtr)); // 4g — dômes
|
||||||
OutStack.Add(MakeUnique<FPinchMod>(P, RoomPtr)); // 4h — pincement
|
OutStack.Add(MakeUnique<FPinchMod>(P, RoomPtr)); // 4h — pincement
|
||||||
OutStack.Add(MakeUnique<FFloorBiasMod>(P, RoomPtr)); // fin 4h — biais de sol
|
OutStack.Add(MakeUnique<FFloorBiasMod>(P, RoomPtr)); // fin 4h — biais de sol
|
||||||
OutStack.Add(MakeUnique<FWormFieldSource>(P, Seed));
|
// ⚠️ `RoomPtr` N'EST PAS DÉCORATIF ICI. Le ver hérite du verdict de boîte de la source de
|
||||||
|
// salles, faute de quoi il rend `CarveOnly` partout et tue `AllSolid` sur chaque tuile —
|
||||||
|
// avec les défauts (`BaseDensity = 8`, `WormStrength = 10`) la marge part négative, donc
|
||||||
|
// aucune tuile n'est prouvable, quoi que la source de salles ait réussi à prouver.
|
||||||
|
OutStack.Add(MakeUnique<FWormFieldSource>(P, Seed, RoomPtr));
|
||||||
|
|
||||||
OutStack.AppendStructuralPost(P.StrateTopWorldZ, P.StrateBottomWorldZ,
|
OutStack.AppendStructuralPost(P.StrateTopWorldZ, P.StrateBottomWorldZ,
|
||||||
P.BoundarySealThickness, P.BaseDensity, SpineRadius, StrateManager);
|
P.BoundarySealThickness, P.BaseDensity, SpineRadius, StrateManager);
|
||||||
|
|||||||
@@ -415,6 +415,27 @@ public:
|
|||||||
* ValidateDeterminism — qui échantillonne le long d'une frontière en X — ne le verrait pas.
|
* ValidateDeterminism — qui échantillonne le long d'une frontière en X — ne le verrait pas.
|
||||||
*/
|
*/
|
||||||
virtual bool IsXYPure() const { return false; }
|
virtual bool IsXYPure() const { return false; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DIAGNOSTIC UNIQUEMENT — le nom que les rapports de test impriment pour cet opérateur.
|
||||||
|
*
|
||||||
|
* ⚠️ POURQUOI CETTE MÉTHODE EXISTE, ET CE QU'ELLE A COÛTÉ DE NE PAS AVOIR. Le premier build de
|
||||||
|
* l'`EffectOverBox` spatial est revenu **vert avec 0 tuile prouvée sur 40**, et la seule chose
|
||||||
|
* que le rapport pouvait dire était « ou bien les tuiles traversent toutes une grotte, ou bien
|
||||||
|
* la source n'atteint pas sa branche `Identity` ». Deux causes, zéro nombre pour les
|
||||||
|
* départager — exactement le piège que ce projet a déjà payé plusieurs fois. La vraie cause
|
||||||
|
* était un TROISIÈME opérateur (le ver, qui rendait `CarveOnly` partout). Avec un nom par
|
||||||
|
* opérateur, `ClassifyBoxAttributed` répond « c'est celui-là » au lieu de laisser deviner.
|
||||||
|
*
|
||||||
|
* N'entre dans AUCUNE clé de cache, dans aucun hash, dans aucune décision de génération : le
|
||||||
|
* changer ne peut pas changer le monde. Le défaut est volontairement laconique — un opérateur
|
||||||
|
* sans nom se repère à son index, ce qui suffit à savoir où regarder.
|
||||||
|
*
|
||||||
|
* Diagnostic only: the first build of the spatial EffectOverBox came back green with 0 tiles
|
||||||
|
* proved, and the report could not name which operator was killing the hypothesis. It was a
|
||||||
|
* third one nobody was looking at. Never part of a cache key or any generation decision.
|
||||||
|
*/
|
||||||
|
virtual const TCHAR* DebugName() const { return TEXT("(unnamed op)"); }
|
||||||
};
|
};
|
||||||
|
|
||||||
//=============================================================================
|
//=============================================================================
|
||||||
|
|||||||
@@ -135,6 +135,50 @@ public:
|
|||||||
return H.Resolve();
|
return H.Resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LE MÊME PLIAGE, MAIS QUI DIT **QUI** A TUÉ CHAQUE HYPOTHÈSE. Diagnostic, réservé aux tests.
|
||||||
|
*
|
||||||
|
* ⚠️ IL DOIT RENDRE EXACTEMENT LE MÊME VERDICT QUE `ClassifyBox` — même boucle, même early-out,
|
||||||
|
* même ordre. Un diagnostic qui emprunte un chemin légèrement différent de celui qu'il explique
|
||||||
|
* est pire que pas de diagnostic : il envoie chercher le bug ailleurs. Si l'un des deux change,
|
||||||
|
* l'autre change avec lui.
|
||||||
|
*
|
||||||
|
* `OutSolidKiller` / `OutAirKiller` reçoivent l'INDEX du premier opérateur qui fait passer
|
||||||
|
* l'hypothèse correspondante de vraie à fausse, ou `INDEX_NONE` si elle a survécu. Le nom
|
||||||
|
* lisible s'obtient par `GetOpDebugName(index)`.
|
||||||
|
*
|
||||||
|
* Same fold, but it reports WHICH op killed each hypothesis. Must stay verdict-identical to
|
||||||
|
* ClassifyBox — a diagnostic that takes a slightly different path sends you hunting in the
|
||||||
|
* wrong place.
|
||||||
|
*/
|
||||||
|
EVoxelTileClass ClassifyBoxAttributed(const FBox& VoxelBox, const FVoxelOpContext& Ctx,
|
||||||
|
int32& OutSolidKiller, int32& OutAirKiller) const
|
||||||
|
{
|
||||||
|
OutSolidKiller = INDEX_NONE;
|
||||||
|
OutAirKiller = INDEX_NONE;
|
||||||
|
|
||||||
|
FVoxelBoxHypotheses H;
|
||||||
|
for (int32 i = 0; i < Ops.Num(); ++i)
|
||||||
|
{
|
||||||
|
const bool bSolidBefore = H.bCanBeAllSolid;
|
||||||
|
const bool bAirBefore = H.bCanBeAllAir;
|
||||||
|
|
||||||
|
VF_FoldOp(H, *Ops[i], VoxelBox, Ctx);
|
||||||
|
|
||||||
|
if (bSolidBefore && !H.bCanBeAllSolid && OutSolidKiller == INDEX_NONE) { OutSolidKiller = i; }
|
||||||
|
if (bAirBefore && !H.bCanBeAllAir && OutAirKiller == INDEX_NONE) { OutAirKiller = i; }
|
||||||
|
|
||||||
|
if (H.IsDead()) { return EVoxelTileClass::Mixed; }
|
||||||
|
}
|
||||||
|
return H.Resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Nom lisible d'un opérateur, pour les rapports de test. Voir `IVoxelDensityOp::DebugName`. */
|
||||||
|
const TCHAR* GetOpDebugName(int32 Index) const
|
||||||
|
{
|
||||||
|
return Ops.IsValidIndex(Index) ? Ops[Index]->DebugName() : TEXT("(none)");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* RÔLE 4 — ajoute les invariants de monde, dans l'ordre fixe, à la fin de la pile.
|
* RÔLE 4 — ajoute les invariants de monde, dans l'ordre fixe, à la fin de la pile.
|
||||||
* spine (0,0) → seal de frontière → carve de passage.
|
* spine (0,0) → seal de frontière → carve de passage.
|
||||||
|
|||||||
Reference in New Issue
Block a user