feat(opstack): EffectOverBox correct in ISOLATION -- the blocker is closed
COMPOSER-NOTES §6.1, titled THE BLOCKER, gated everything after it. A source writing only Sdf did not touch density, so it answered the box query ON BEHALF OF the converter that followed it -- fine when a human wrote and verified the pair, unsafe the moment the composer assembles a combination nobody checked. And the failure is the silent one: a tile wrongly proved uniform means no geometry, no collision, no error, and a player falls through the floor. FVoxelBoxHypotheses now carries FVoxelBoxSdfInterval. SDF writers publish their own [Min, Max]; replacement writers replace it, accumulating writers MinWith it; converters and detail ops consume the prior interval through a state-aware EffectOverBox. Each op answers only for itself and the fold composes them. Unknown, invalid, non-finite or unimplemented bounds force Mixed -- when in doubt the answer is Mixed, because a missed skip costs milliseconds and a false uniform costs a player. Bounds are SUPREMA of what Eval can produce, never the param named "max": FBM 1.5 (not 1.0), max(Min,Max) radius envelopes, warp sqrt(2), SmoothMin slack. [FLT_MAX, FLT_MAX] is the exact no-surface identity. ⭐ THE PROOF IS THE NOVEL STACK. Built a combination no shipping builder produces: ConstantVoid -> LatticeCorridorSource -> SdfRoughness -> SdfFill brute-forced 332,750 voxels across 256 boxes: 250 proved AllAir, 0 VIOLATIONS. If isolation is real an unverified stack must still be sound, and it is. That test -- not the refactor -- is the deliverable. Every archetype re-proved by brute force, 0 violations everywhere: Maze 112/256 · CrystalChamber 38/60 · Underwater 33/40 · FlatPlain 32/60 VerticalShafts 28/60 · FloatingIslands 15/60 · TunnelNetwork prod 9/40 TunnelNetwork dense 0/40 (correctly -- it really is all mixed) COST, reported not hidden: isolation gives up some skipping. TunnelNetwork production 12->9, VerticalShafts 32->28. Answering for yourself alone is strictly weaker than answering for a verified pair. The classifier stays materially useful and the trade buys safe composition, which is the point. Eval is untouched; equivalence tests bit-identical; no per-voxel box query. SurfaceWorld keeps its separate exact height/lattice classifier. Also: ⛔ WITHDREW the "corrective ops must be fused" claim from §3.2. FFloorBiasMod does NOT undo FCaveRoughnessMod -- it adds an independent NormZ^2 x FloorBias term to the ACCUMULATED density and never reads TotalRough. Not a correction, no fuse, no "corrects" dependency. The claim came from reading a name and a comment; one grep would have shown the ten Density-writing ops in between before the paragraph was written. Build clean, 22 succeeded / 0 failed, verified in VoxelM.log. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VJiTcTSDNkKBtnNdiBtdaU
This commit is contained in:
1 parent
7b982ad5ae
commit
8e346a3054
11 files changed
+1193
-890
No files matched your search
@@ -233,7 +233,7 @@ bool FVoxelForgeOpStackIslandTest::RunTest(const FString& Parameters)
|
||||
// 3. LE VERDICT DE BOÎTE — et la première preuve « AllAir » du plugin
|
||||
//=========================================================================
|
||||
{
|
||||
int32 NumProvedSolid = 0, NumProvedAir = 0, NumMixed = 0, NumUnsound = 0;
|
||||
int32 NumProvedSolid = 0, NumProvedAir = 0, NumMixed = 0, NumUnsound = 0, NumBruteSamples = 0;
|
||||
FRandomStream Rng(24680);
|
||||
// Hors de la boucle : la ligne de rapport en a besoin. Une étendue d'échantillonnage qu'on
|
||||
// ne peut pas citer dans le rapport est une étendue que personne ne surveille.
|
||||
@@ -268,6 +268,7 @@ bool FVoxelForgeOpStackIslandTest::RunTest(const FString& Parameters)
|
||||
const float Y = (float)(Origin.Y + gy * Step);
|
||||
const float Z = (float)(Origin.Z + gz * Step);
|
||||
const float D = Stack.EvalMC(X, Y, Z);
|
||||
++NumBruteSamples;
|
||||
if (bClaimsSolid ? (D >= 0.0f) : (D < 0.0f))
|
||||
{
|
||||
if (NumUnsound == 0)
|
||||
@@ -275,7 +276,7 @@ bool FVoxelForgeOpStackIslandTest::RunTest(const FString& Parameters)
|
||||
AddError(FString::Printf(
|
||||
TEXT("HOLE: the island stack claimed %s for the box at (%d,%d,%d) but ")
|
||||
TEXT("EvalMC(%.0f, %.0f, %.0f) = %.6g is on the %s side. Suspects, in ")
|
||||
TEXT("order: the blob source's Pad (does it cover the WARP amplitude ")
|
||||
TEXT("order: the blob source's propagated SDF interval (does it cover the WARP amplitude ")
|
||||
TEXT("AND the roughness AND the fill blend AND the SmoothMin dip?), ")
|
||||
TEXT("then the Z bound -- note there is NO lower bound, a thin thread ")
|
||||
TEXT("of matter hangs below each island down the axis, so only the ")
|
||||
@@ -295,11 +296,11 @@ bool FVoxelForgeOpStackIslandTest::RunTest(const FString& Parameters)
|
||||
AddInfo(FString::Printf(
|
||||
TEXT("Box verdicts over 60 FloatingIslands tiles (XY sampled from +/- %d voxels = %.1f x ")
|
||||
TEXT("IslandSpacing %.0f): %d proved AllSolid, %d proved AllAir, ")
|
||||
TEXT("%d Mixed. Today's ClassifyTile proves ZERO of these. The AllAir count is the new ")
|
||||
TEXT("%d Mixed, %d voxels checked, %d violations. Today's ClassifyTile proves ZERO of these. The AllAir count is the new ")
|
||||
TEXT("thing: no cave archetype has ever been able to prove 'all air', and a floating-")
|
||||
TEXT("island strate is mostly exactly that (OPSTACK-DECOMPOSITION 7)."),
|
||||
SpanVoxels, (float)SpanVoxels / FMath::Max(P.IslandSpacing, 1.0f), P.IslandSpacing,
|
||||
NumProvedSolid, NumProvedAir, NumMixed));
|
||||
NumProvedSolid, NumProvedAir, NumMixed, NumBruteSamples, NumUnsound));
|
||||
|
||||
if (NumProvedAir == 0)
|
||||
{
|
||||
|
||||
@@ -87,6 +87,7 @@ IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
namespace
|
||||
{
|
||||
constexpr int32 NumMazeSamples = 20000;
|
||||
constexpr int32 NumMazeBoxTests = 256;
|
||||
|
||||
/** Les params Maze de la strate Maze de la fixture, bornes Z de runtime comprises. */
|
||||
bool ResolveMazeParams(const VoxelForgeTest::FTestWorld& World, FMazeGenerationParams& Out,
|
||||
@@ -265,14 +266,14 @@ bool FVoxelForgeOpStackMazeTest::RunTest(const FString& Parameters)
|
||||
// VerticalShafts, FloatingIslands, FlatPlain, CrystalChamber et Underwater ne captent RIEN du
|
||||
// gain T1.d. Tout nombre > 0 ici est du saut de tuile que Maze n'a jamais eu.
|
||||
{
|
||||
int32 NumProved = 0, NumMixed = 0, NumUnsound = 0;
|
||||
int32 NumProved = 0, NumMixed = 0, NumUnsound = 0, NumBruteSamples = 0;
|
||||
FRandomStream Rng(24680);
|
||||
// Hors de la boucle : la ligne de rapport en a besoin. Une étendue d'échantillonnage qu'on
|
||||
// ne peut pas citer dans le rapport est une étendue que personne ne surveille.
|
||||
const int32 SpanCells = 40;
|
||||
const int32 SpanVoxels = SpanCells * 8; // Extent = Step * Cells = 1 * 8
|
||||
|
||||
for (int32 t = 0; t < 60; ++t)
|
||||
for (int32 t = 0; t < NumMazeBoxTests; ++t)
|
||||
{
|
||||
const int32 Step = 1, Cells = 8; // petites tuiles : force brute tenable
|
||||
const int32 Extent = Step * Cells;
|
||||
@@ -299,6 +300,7 @@ bool FVoxelForgeOpStackMazeTest::RunTest(const FString& Parameters)
|
||||
const float Y = (float)(Origin.Y + gy * Step);
|
||||
const float Z = (float)(Origin.Z + gz * Step);
|
||||
const float D = Stack.EvalMC(X, Y, Z);
|
||||
++NumBruteSamples;
|
||||
if (bClaimsSolid ? (D >= 0.0f) : (D < 0.0f))
|
||||
{
|
||||
if (NumUnsound == 0)
|
||||
@@ -307,8 +309,8 @@ bool FVoxelForgeOpStackMazeTest::RunTest(const FString& Parameters)
|
||||
TEXT("HOLE: the op stack claimed %s for the box at (%d,%d,%d) but ")
|
||||
TEXT("EvalMC(%.0f, %.0f, %.0f) = %.6g is on the %s side. One of the ops' ")
|
||||
TEXT("EffectOverBox/ClassifyBox is not conservative. Suspects, in order: ")
|
||||
TEXT("the lattice source's ExtraReach (does it cover the roughness ")
|
||||
TEXT("amplitude AND the carve blend?), then the seal's forcing verdict."),
|
||||
TEXT("the source interval, the SDF roughness interval, the converter's ")
|
||||
TEXT("threshold fold, then the seal's forcing verdict."),
|
||||
bClaimsSolid ? TEXT("AllSolid") : TEXT("AllAir"),
|
||||
Origin.X, Origin.Y, Origin.Z, X, Y, Z, D,
|
||||
(D >= 0.0f) ? TEXT("AIR") : TEXT("SOLID")));
|
||||
@@ -323,12 +325,12 @@ bool FVoxelForgeOpStackMazeTest::RunTest(const FString& Parameters)
|
||||
NumUnsound, 0);
|
||||
|
||||
AddInfo(FString::Printf(
|
||||
TEXT("Box verdicts over 60 Maze tiles (XY sampled from +/- %d voxels = %.1f x ")
|
||||
TEXT("CellSize %.0f): %d proved uniform, %d Mixed. Today's ClassifyTile ")
|
||||
TEXT("Box verdicts over %d Maze tiles (XY sampled from +/- %d voxels = %.1f x ")
|
||||
TEXT("CellSize %.0f): %d proved uniform, %d Mixed, %d voxels checked, %d violations. Today's ClassifyTile ")
|
||||
TEXT("proves ZERO of these -- every cave archetype falls through to \"pas prouvable en ")
|
||||
TEXT("v1\". Any number above zero here is tile-skipping Maze has never had."),
|
||||
SpanVoxels, (float)SpanVoxels / FMath::Max(MazeParams.CellSize, 1.0f), MazeParams.CellSize,
|
||||
NumProved, NumMixed));
|
||||
NumMazeBoxTests, SpanVoxels, (float)SpanVoxels / FMath::Max(MazeParams.CellSize, 1.0f), MazeParams.CellSize,
|
||||
NumProved, NumMixed, NumBruteSamples, NumUnsound));
|
||||
|
||||
if (NumProved == 0)
|
||||
{
|
||||
@@ -338,6 +340,86 @@ bool FVoxelForgeOpStackMazeTest::RunTest(const FString& Parameters)
|
||||
}
|
||||
}
|
||||
|
||||
//==========================================================================
|
||||
// ISOLATION — a source with a converter it was never authored with
|
||||
//==========================================================================
|
||||
// Maze's hand-written stack uses ConstantRock -> Lattice -> SdfRoughness -> SdfCarve.
|
||||
// This stack deliberately uses a VOID root and SdfFill instead. The only way it may
|
||||
// claim AllAir is for the propagated lattice/roughness interval to prove that the fill
|
||||
// converter is inactive over the whole box. A source that still answered for its old
|
||||
// carve neighbour would not be a valid implementation of this test.
|
||||
{
|
||||
FMazeGenerationParams NovelParams = MazeParams;
|
||||
NovelParams.BranchProbability = 0.12f;
|
||||
NovelParams.Verticality = 0.08f;
|
||||
NovelParams.CorridorRadius = 1.0f;
|
||||
|
||||
FVoxelOpStack NovelStack;
|
||||
NovelStack.Add(VoxelDensityOps::MakeConstantVoidSource(NovelParams.BaseDensity));
|
||||
NovelStack.Add(VoxelDensityOps::MakeLatticeCorridorSource(NovelParams, World.Settings->Seed));
|
||||
NovelStack.Add(VoxelDensityOps::MakeSdfRoughnessMod(
|
||||
0.35f, 0.12f, 3, NovelParams.CorridorRadius + 2.0f));
|
||||
NovelStack.Add(VoxelDensityOps::MakeSdfFill(1.25f, NovelParams.BaseDensity));
|
||||
NovelStack.PrepareChunk(Ctx);
|
||||
|
||||
FString ChannelError;
|
||||
TestTrue(TEXT("the unverified source/converter stack has a valid channel order"),
|
||||
NovelStack.ValidateChannelOrder(&ChannelError));
|
||||
if (!ChannelError.IsEmpty()) { AddError(ChannelError); }
|
||||
|
||||
constexpr int32 NumNovelBoxTests = 256;
|
||||
const int32 NovelSpanCells = 160;
|
||||
const int32 NovelSpanVoxels = NovelSpanCells * 8;
|
||||
int32 NumNovelSolid = 0, NumNovelAir = 0, NumNovelMixed = 0;
|
||||
int32 NumNovelVoxels = 0, NumNovelViolations = 0;
|
||||
FRandomStream NovelRng(86420);
|
||||
|
||||
for (int32 t = 0; t < NumNovelBoxTests; ++t)
|
||||
{
|
||||
const int32 Step = 1, Cells = 8;
|
||||
const int32 Extent = Step * Cells;
|
||||
const FIntVector Origin(
|
||||
NovelRng.RandRange(-NovelSpanCells, NovelSpanCells) * Extent,
|
||||
NovelRng.RandRange(-NovelSpanCells, NovelSpanCells) * Extent,
|
||||
FMath::Clamp(NovelRng.RandRange(BottomVoxelZ / Extent, TopVoxelZ / Extent), -4096, 4096) * Extent);
|
||||
const int32 GridDim = Cells + 1;
|
||||
const FBox Box(
|
||||
FVector(Origin.X - Step, Origin.Y - Step, Origin.Z - Step),
|
||||
FVector(Origin.X + GridDim * Step, Origin.Y + GridDim * Step, Origin.Z + GridDim * Step));
|
||||
|
||||
const EVoxelTileClass Verdict = NovelStack.ClassifyBox(Box, Ctx);
|
||||
if (Verdict == EVoxelTileClass::Mixed)
|
||||
{
|
||||
++NumNovelMixed;
|
||||
continue;
|
||||
}
|
||||
|
||||
const bool bClaimsSolid = (Verdict == EVoxelTileClass::AllSolid);
|
||||
if (bClaimsSolid) { ++NumNovelSolid; } else { ++NumNovelAir; }
|
||||
|
||||
// No sampled subset: every lattice point in every proved box is checked.
|
||||
for (int32 gz = -1; gz <= GridDim; ++gz)
|
||||
for (int32 gy = -1; gy <= GridDim; ++gy)
|
||||
for (int32 gx = -1; gx <= GridDim; ++gx)
|
||||
{
|
||||
const float X = (float)(Origin.X + gx * Step);
|
||||
const float Y = (float)(Origin.Y + gy * Step);
|
||||
const float Z = (float)(Origin.Z + gz * Step);
|
||||
const float D = NovelStack.EvalMC(X, Y, Z);
|
||||
++NumNovelVoxels;
|
||||
if (bClaimsSolid ? (D >= 0.0f) : (D < 0.0f)) { ++NumNovelViolations; }
|
||||
}
|
||||
}
|
||||
|
||||
TestEqual(TEXT("the unverified source/converter stack has zero box-verdict violations"),
|
||||
NumNovelViolations, 0);
|
||||
AddInfo(FString::Printf(
|
||||
TEXT("Novel lattice+roughness+FILL stack: %d boxes proved (%d AllSolid, %d AllAir), " )
|
||||
TEXT("%d Mixed; %d voxels checked, %d violations; XY sampled from +/- %d voxels."),
|
||||
NumNovelSolid + NumNovelAir, NumNovelSolid, NumNovelAir, NumNovelMixed,
|
||||
NumNovelVoxels, NumNovelViolations, NovelSpanVoxels));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -252,7 +252,7 @@ bool FVoxelForgeOpStackShaftTest::RunTest(const FString& Parameters)
|
||||
// 3. LE VERDICT DE BOÎTE
|
||||
//=========================================================================
|
||||
{
|
||||
int32 NumProved = 0, NumMixed = 0, NumUnsound = 0;
|
||||
int32 NumProved = 0, NumMixed = 0, NumUnsound = 0, NumBruteSamples = 0;
|
||||
FRandomStream Rng(13579);
|
||||
// Hors de la boucle : la ligne de rapport en a besoin. Une étendue d'échantillonnage qu'on
|
||||
// ne peut pas citer dans le rapport est une étendue que personne ne surveille.
|
||||
@@ -292,6 +292,7 @@ bool FVoxelForgeOpStackShaftTest::RunTest(const FString& Parameters)
|
||||
const float Y = (float)(Origin.Y + gy * Step);
|
||||
const float Z = (float)(Origin.Z + gz * Step);
|
||||
const float D = Stack.EvalMC(X, Y, Z);
|
||||
++NumBruteSamples;
|
||||
if (bClaimsSolid ? (D >= 0.0f) : (D < 0.0f))
|
||||
{
|
||||
if (NumUnsound == 0)
|
||||
@@ -299,8 +300,8 @@ bool FVoxelForgeOpStackShaftTest::RunTest(const FString& Parameters)
|
||||
AddError(FString::Printf(
|
||||
TEXT("HOLE: the shaft stack claimed %s for the box at (%d,%d,%d) but ")
|
||||
TEXT("EvalMC(%.0f, %.0f, %.0f) = %.6g is on the %s side. Suspects, in ")
|
||||
TEXT("order: the shaft source's ExtraReach (does it cover the roughness ")
|
||||
TEXT("amplitude AND the carve blend?), then the connector sweep (a ")
|
||||
TEXT("order: the shaft source's SDF interval (does it cover all shaft ")
|
||||
TEXT("and connector geometry?), then the connector sweep (a ")
|
||||
TEXT("connector can reach Spacing*1.6 beyond its cell), then the ledge ")
|
||||
TEXT("op's FillOnly."),
|
||||
bClaimsSolid ? TEXT("AllSolid") : TEXT("AllAir"),
|
||||
@@ -317,22 +318,22 @@ bool FVoxelForgeOpStackShaftTest::RunTest(const FString& Parameters)
|
||||
|
||||
AddInfo(FString::Printf(
|
||||
TEXT("Box verdicts over 60 VerticalShafts tiles (XY sampled from +/-%d voxels = %.1f x ")
|
||||
TEXT("ShaftSpacing %.0f): %d proved uniform, %d Mixed, brute-forced with %d violations. ")
|
||||
TEXT("ShaftSpacing %.0f): %d proved uniform, %d Mixed, %d voxels checked, %d violations. ")
|
||||
TEXT("This was 0 proved for as long as the connector branch bailed on mere shaft ")
|
||||
TEXT("EXISTENCE within Spacing*1.6 -- true almost everywhere at ShaftDensity 0.6, so it ")
|
||||
TEXT("was conservative AND sterile. It now tests the real connector capsules. Read the ")
|
||||
TEXT("proved count as a measurement; what is ASSERTED is that none of them is wrong, ")
|
||||
TEXT("because a false verdict here leaves no geometry and no collision."),
|
||||
SpanVoxels, (float)SpanVoxels / FMath::Max(P.ShaftSpacing, 1.0f), P.ShaftSpacing,
|
||||
NumProved, NumMixed, NumUnsound));
|
||||
NumProved, NumMixed, NumBruteSamples, NumUnsound));
|
||||
|
||||
if (NumProved == 0)
|
||||
{
|
||||
AddWarning(TEXT("No VerticalShafts tile was proved, so the brute force above verified ")
|
||||
TEXT("nothing. Before hypothesising: the shaft CIRCLE test and the connector ")
|
||||
TEXT("CAPSULE test are the only two things that can return CarveOnly here, ")
|
||||
TEXT("and ExtraReach inflates both -- check its value against ShaftMaxRadius ")
|
||||
TEXT("before touching either test."));
|
||||
TEXT("and the interval must cover both the shaft and connector capsules before ")
|
||||
TEXT("either test is relaxed."));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -278,7 +278,7 @@ bool FVoxelForgeOpStackSlabTest::RunTest(const FString& Parameters)
|
||||
// 3. LE VERDICT DE BOÎTE — ce que §3.1 a acheté
|
||||
//=====================================================================
|
||||
{
|
||||
int32 NumProved = 0, NumMixed = 0, NumUnsound = 0;
|
||||
int32 NumProved = 0, NumMixed = 0, NumUnsound = 0, NumBruteSamples = 0;
|
||||
FRandomStream Rng(24680 + SlotIndex);
|
||||
// Hors de la boucle : la ligne de rapport en a besoin. Une étendue d'échantillonnage qu'on
|
||||
// ne peut pas citer dans le rapport est une étendue que personne ne surveille.
|
||||
@@ -312,6 +312,7 @@ bool FVoxelForgeOpStackSlabTest::RunTest(const FString& Parameters)
|
||||
const float Y = (float)(Origin.Y + gy * Step);
|
||||
const float Z = (float)(Origin.Z + gz * Step);
|
||||
const float D = Stack.EvalMC(X, Y, Z);
|
||||
++NumBruteSamples;
|
||||
if (bClaimsSolid ? (D >= 0.0f) : (D < 0.0f))
|
||||
{
|
||||
if (NumUnsound == 0)
|
||||
@@ -320,7 +321,7 @@ bool FVoxelForgeOpStackSlabTest::RunTest(const FString& Parameters)
|
||||
TEXT("HOLE: %s claimed %s for the box at (%d,%d,%d) but ")
|
||||
TEXT("EvalMC(%.0f, %.0f, %.0f) = %.6g is on the %s side. One of the ")
|
||||
TEXT("ops is not conservative. Suspects, in order: the slab source's ")
|
||||
TEXT("noise amplitude bounds (does FBM really honour [-1,1]?), the ")
|
||||
TEXT("noise amplitude bounds (the proved FBM supremum is 1.5, not 1.0), the ")
|
||||
TEXT("ceiling clamp raising CeilSurface above CeilZ, then the column ")
|
||||
TEXT("mod's reach (MaxRadius + blend)."),
|
||||
SlotName, bClaimsSolid ? TEXT("AllSolid") : TEXT("AllAir"),
|
||||
@@ -340,12 +341,12 @@ bool FVoxelForgeOpStackSlabTest::RunTest(const FString& Parameters)
|
||||
|
||||
AddInfo(FString::Printf(
|
||||
TEXT("%s box verdicts over %d tiles (XY sampled from +/- %d voxels = %.1f x ")
|
||||
TEXT("ColumnSpacing %.0f): %d proved uniform, %d Mixed. Today's ")
|
||||
TEXT("ColumnSpacing %.0f): %d proved uniform, %d Mixed, %d voxels checked, %d violations. Today's ")
|
||||
TEXT("ClassifyTile proves ZERO of these. This number is the whole point of making ")
|
||||
TEXT("the slab surfaces XY-pure (OPSTACK-DECOMPOSITION 3.1)."),
|
||||
SlotName, NumSlabTiles, SpanVoxels,
|
||||
(float)SpanVoxels / FMath::Max(SlabParams.ColumnSpacing, 1.0f), SlabParams.ColumnSpacing,
|
||||
NumProved, NumMixed));
|
||||
NumProved, NumMixed, NumBruteSamples, NumUnsound));
|
||||
|
||||
if (NumProved == 0)
|
||||
{
|
||||
|
||||
@@ -1086,8 +1086,8 @@ bool FVoxelForgeOpStackTunnelTest::RunTest(const FString& Parameters)
|
||||
// 4. LE VERDICT DE BOÎTE — plus attendu nul, et CHAQUE VERDICT EST BRUTE-FORCÉ
|
||||
//=========================================================================
|
||||
// ⚠️ CE BLOC A CHANGÉ DE NATURE LE 2026-07-28, ET IL FAUT SAVOIR POURQUOI.
|
||||
// Il ASSERTAIT `NumProved == 0`. C'était juste tant que `FRoomGraphSource::EffectOverBox`
|
||||
// rendait `Both` inconditionnellement : « zéro » était alors une description honnête de l'état
|
||||
// Il ASSERTAIT `NumProved == 0`. C'était juste tant que la source de salles répondait
|
||||
// `Both` inconditionnellement : « zéro » était alors une description honnête de l'état
|
||||
// du portage. Depuis que la source répond SPATIALEMENT, asserter zéro reviendrait à interdire
|
||||
// le gain qu'on vient de construire — et pire, ça transformerait le test en gardien du bug.
|
||||
//
|
||||
@@ -1239,7 +1239,7 @@ bool FVoxelForgeOpStackTunnelTest::RunTest(const FString& Parameters)
|
||||
AddInfo(FString::Printf(
|
||||
TEXT("[%s] Box verdicts over 40 TunnelNetwork tiles: %d proved (%d AllSolid, %d AllAir), ")
|
||||
TEXT("%d Mixed -- brute-forced over %d voxels, %d violations. This number was 0 proved / ")
|
||||
TEXT("40 Mixed until FRoomGraphSource::EffectOverBox learned to answer spatially, and it ")
|
||||
TEXT("40 Mixed until the SDF source interval and state-aware fold learned to answer spatially, and it ")
|
||||
TEXT("is the single largest perf item of the whole plan (OPSTACK-DECOMPOSITION 0.2): a ")
|
||||
TEXT("proved tile skips GenerateMesh entirely, so it trades one BuildChunkCache against ")
|
||||
TEXT("30000+ density evaluations. Read the PROVED count as a measurement, never as a ")
|
||||
@@ -1408,6 +1408,11 @@ bool FVoxelForgeOpStackTunnelTest::RunTest(const FString& Parameters)
|
||||
UWCtx.StrateBottomWorldZ = UP.StrateBottomWorldZ;
|
||||
UWStack.PrepareChunk(UWCtx);
|
||||
|
||||
// Underwater shares the tunnel builder, but it is a separate production archetype. Keep
|
||||
// its box proof in the report so a future change cannot silently make only this slot
|
||||
// unsound or sterile.
|
||||
RunTileScan(UWStack, UP, UWCtx, TEXT("Underwater"), /*bZeroProvedIsExpected*/ false);
|
||||
|
||||
TestEqual(TEXT("the Underwater stack is the tunnel stack: same 20 ops"), UWStack.Num(), 20);
|
||||
|
||||
// 8 → 24 chunks, comme le balayage principal. Le premier run vert a rapporté **0 de 2000
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -39,22 +39,35 @@
|
||||
// the descent structure intact no matter what an author assembles.
|
||||
//
|
||||
// ─────────────────────────────────────────────────────────────────────────────────────────
|
||||
// LA CLÉ DE VOÛTE : EffectOverBox — direction, pas intervalle / THE KEYSTONE: direction, not intervals
|
||||
// LA CLÉ DE VOÛTE : EffectOverBox + intervalle SDF / THE KEYSTONE: EffectOverBox + SDF interval
|
||||
// ─────────────────────────────────────────────────────────────────────────────────────────
|
||||
// La version « complète » d'une borne rendrait un intervalle numérique. NE PAS COMMENCER LÀ.
|
||||
// Presque tout opérateur existant est UNIDIRECTIONNEL : il ne fait que creuser, ou que remplir.
|
||||
// Cela suffit à reproduire GÉNÉRIQUEMENT chaque garde écrite à la main dans ClassifyTile :
|
||||
// Un verdict de boîte doit être une preuve. Rendre `Both` coûte du CPU ; rendre `Identity` ou une
|
||||
// direction fausse supprime de la géométrie et peut laisser un joueur tomber au travers du monde.
|
||||
// En cas de doute, `Mixed`.
|
||||
//
|
||||
// Un opérateur qui écrit seulement le canal SDF ne peut pas honnêtement répondre pour le convertisseur
|
||||
// qui viendra peut-être après lui : il doit propager un intervalle SDF, et le convertisseur doit plier
|
||||
// cet intervalle avec sa propre formule. Chaque opérateur répond ainsi pour lui-même, et la composition
|
||||
// reste sûre même pour une combinaison que personne n'a écrite à la main auparavant.
|
||||
//
|
||||
// A box verdict is a proof. `Both` costs CPU; a false `Identity` or direction removes geometry and can
|
||||
// let a player fall through the world. When in doubt, return `Mixed`.
|
||||
// An SDF-only writer reports an SDF interval; a later converter folds that interval through its own
|
||||
// response. Each operator answers for itself, so an unverified composition remains sound.
|
||||
//
|
||||
// La première version de cette interface n'avait qu'une direction numérique. Presque tout opérateur
|
||||
// existant est UNIDIRECTIONNEL : il ne fait que creuser, ou que remplir. Cela suffit à reproduire
|
||||
// GÉNÉRIQUEMENT chaque garde écrite à la main dans ClassifyTile :
|
||||
//
|
||||
// « passages ⇒ bCanSolid = false » EST CarveOnly
|
||||
// « ponts/arêtes ⇒ bCanAir = false » EST FillOnly
|
||||
// « aucun passage près de cette boîte » EST Identity
|
||||
//
|
||||
// Donc la Phase 1 n'a besoin d'AUCUNE borne numérique et obtient déjà toute la propriété de
|
||||
// sûreté. Les intervalles sont un resserrement ultérieur pour le coût de génération, pas un
|
||||
// prérequis de correction. C'est ce qui rend le premier pas petit.
|
||||
// La direction reste le repli conservateur par défaut pour les opérateurs qui ne connaissent pas
|
||||
// d'intervalle ; elle ne permet jamais à un écrivain SDF d'usurper la réponse d'un voisin.
|
||||
//
|
||||
// Phase 1 needs NO numeric bounds and already gets the whole safety property. Intervals are a
|
||||
// later tightening for gen cost, not a correctness prerequisite.
|
||||
// Direction remains the conservative fallback for operators without an interval implementation;
|
||||
// it never lets an SDF writer impersonate a downstream neighbour.
|
||||
//
|
||||
// ─────────────────────────────────────────────────────────────────────────────────────────
|
||||
// LE GROS LOT PERF : les strates de grotte ne sautent AUCUNE tuile aujourd'hui
|
||||
@@ -283,6 +296,64 @@ struct FVoxelOpSample
|
||||
float Sdf = FLT_MAX;
|
||||
};
|
||||
|
||||
/**
|
||||
* Intervalle conservateur du canal SDF sur une boîte.
|
||||
*
|
||||
* `Min` et `Max` sont les bornes SUPRÊMES de toutes les valeurs que l'opérateur peut produire dans
|
||||
* la boîte. `[FLT_MAX, FLT_MAX]` est l'identité exacte : aucune surface connue. Un intervalle inconnu
|
||||
* est volontairement large ; il force le fold à rester `Mixed` plutôt que de risquer un faux tile
|
||||
* uniforme. Les setters refusent toute borne non finie ou inversée : un opérateur qui ne peut pas
|
||||
* prouver son intervalle perd le skip, jamais la géométrie.
|
||||
*
|
||||
* Conservative SDF interval over a box. `Min`/`Max` bound every value the operator can produce.
|
||||
* `[FLT_MAX, FLT_MAX]` is the exact "no surface" identity. Unknown is deliberately wide: it costs
|
||||
* a skip, never a hole. Invalid bounds become unknown instead of being guessed.
|
||||
*/
|
||||
struct FVoxelBoxSdfInterval
|
||||
{
|
||||
bool bKnown = true;
|
||||
float Min = FLT_MAX;
|
||||
float Max = FLT_MAX;
|
||||
|
||||
bool IsKnown() const { return bKnown; }
|
||||
|
||||
void SetUnknown()
|
||||
{
|
||||
bKnown = false;
|
||||
Min = -FLT_MAX;
|
||||
Max = FLT_MAX;
|
||||
}
|
||||
|
||||
void Set(float InMin, float InMax)
|
||||
{
|
||||
if (!FMath::IsFinite(InMin) || !FMath::IsFinite(InMax) || InMin > InMax)
|
||||
{
|
||||
SetUnknown();
|
||||
return;
|
||||
}
|
||||
bKnown = true;
|
||||
Min = InMin;
|
||||
Max = InMax;
|
||||
}
|
||||
|
||||
/** Output of `min(Input, Other)` when both intervals are known. */
|
||||
void MinWith(const FVoxelBoxSdfInterval& Other)
|
||||
{
|
||||
if (!bKnown || !Other.bKnown)
|
||||
{
|
||||
SetUnknown();
|
||||
return;
|
||||
}
|
||||
Min = FMath::Min(Min, Other.Min);
|
||||
Max = FMath::Min(Max, Other.Max);
|
||||
}
|
||||
};
|
||||
|
||||
// Forward declaration: the interval-aware EffectOverBox overloads consume the hypotheses that
|
||||
// have already been folded, while the hypotheses themselves are declared immediately below the
|
||||
// interface.
|
||||
struct FVoxelBoxHypotheses;
|
||||
|
||||
//=============================================================================
|
||||
// L'INTERFACE / THE INTERFACE
|
||||
//=============================================================================
|
||||
@@ -344,8 +415,9 @@ public:
|
||||
virtual void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const = 0;
|
||||
|
||||
/**
|
||||
* CONSERVATIF. Phase 1 : direction seule. Phase 3 : surcharge avec intervalle numérique.
|
||||
* Rendre Both est toujours sûr ; rendre le mauvais est un trou.
|
||||
* CONSERVATIF. The two-argument method is the intrinsic fallback. The state-aware overload
|
||||
* below is what the stack calls: it sees the SDF interval already produced by earlier ops.
|
||||
* Returning Both is always safe; returning the wrong direction is a hole.
|
||||
*
|
||||
* Le contrat est « conservatif », pas « forme close » : un opérateur A LE DROIT
|
||||
* D'ÉCHANTILLONNER pour répondre. C'est exactement ce que fait ClassifyTile aujourd'hui pour
|
||||
@@ -354,20 +426,20 @@ public:
|
||||
*
|
||||
* The contract is "conservative", not "closed-form": an op MAY sample to answer.
|
||||
*
|
||||
* ⚠️ SIMPLIFICATION DE PHASE 1, à connaître : une source qui n'écrit QUE le canal SDF ne touche
|
||||
* pas la densité par elle-même — c'est l'opérateur de conversion (`FSdfCarve`/`FSdfFill`) qui le
|
||||
* fait. Répondre honnêtement demanderait de propager un INTERVALLE de SDF à travers la requête
|
||||
* de boîte, exactement comme `Eval` propage une valeur de SDF. En attendant, **la source répond
|
||||
* pour la paire** (elle rend `CarveOnly`/`FillOnly` quand une primitive atteint la boîte,
|
||||
* `Identity` sinon) et la conversion rend `Identity`. Conservatif et correct ; à remplacer par
|
||||
* une requête de boîte à deux canaux quand les intervalles numériques arriveront (Phase 3).
|
||||
*
|
||||
* PHASE 1 SIMPLIFICATION: an SDF-only source answers for itself AND its conversion op; the
|
||||
* conversion returns Identity. Answering honestly needs an SDF INTERVAL threaded through the box
|
||||
* query, mirroring how Eval threads an SDF value. Conservative and correct meanwhile.
|
||||
* An SDF-only source MUST NOT answer for a converter that happens to follow it. It reports its
|
||||
* own interval through `PropagateSdfOverBox`; the converter's state-aware overload then folds
|
||||
* the interval through its own formula. This is the isolation rule that makes free composition
|
||||
* safe.
|
||||
*/
|
||||
virtual EVoxelOpEffect EffectOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx) const = 0;
|
||||
|
||||
/** State-aware effect. The default preserves old custom operators' conservative direction. */
|
||||
virtual EVoxelOpEffect EffectOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx,
|
||||
const FVoxelBoxHypotheses&) const
|
||||
{
|
||||
return EffectOverBox(VoxelBox, Ctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* ⚠️ LE PLIAGE QUI PORTE DES NOMBRES — `OPSTACK-DECOMPOSITION §0.2`, et le plus gros poste de
|
||||
* perf du plan.
|
||||
@@ -406,11 +478,38 @@ public:
|
||||
return FLT_MAX;
|
||||
}
|
||||
|
||||
/** State-aware amplitude. The default is the old, conservative amplitude. */
|
||||
virtual float MaxCarveOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx,
|
||||
const FVoxelBoxHypotheses&) const
|
||||
{
|
||||
return MaxCarveOverBox(VoxelBox, Ctx);
|
||||
}
|
||||
|
||||
virtual float MaxFillOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx) const
|
||||
{
|
||||
return FLT_MAX;
|
||||
}
|
||||
|
||||
/** State-aware amplitude. The default is the old, conservative amplitude. */
|
||||
virtual float MaxFillOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx,
|
||||
const FVoxelBoxHypotheses&) const
|
||||
{
|
||||
return MaxFillOverBox(VoxelBox, Ctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* Propagate this operator's own SDF result through a box query. Only call this for an operator
|
||||
* whose cached channel declaration writes `Sdf`. The default is unknown by construction: a new
|
||||
* SDF writer cannot accidentally make a false uniform verdict merely because it forgot this
|
||||
* method. Implementations that assign SDF replace the interval; min/accumulating writers use
|
||||
* `FVoxelBoxSdfInterval::MinWith`.
|
||||
*/
|
||||
virtual void PropagateSdfOverBox(FVoxelBoxSdfInterval& InOut, const FBox& VoxelBox,
|
||||
const FVoxelOpContext&) const
|
||||
{
|
||||
InOut.SetUnknown();
|
||||
}
|
||||
|
||||
/**
|
||||
* Pour un opérateur FORÇANT (celui dont `ClassifyBox` rend autre chose que `Mixed`) : de combien
|
||||
* la densité est-elle garantie du bon côté de zéro, PARTOUT dans la boîte ?
|
||||
@@ -537,6 +636,9 @@ struct FVoxelBoxHypotheses
|
||||
float SolidMargin = 0.0f;
|
||||
float AirMargin = 0.0f;
|
||||
|
||||
/** SDF interval after all SDF writers folded so far. It starts at the exact no-surface identity. */
|
||||
FVoxelBoxSdfInterval Sdf;
|
||||
|
||||
bool IsDead() const { return !bCanBeAllSolid && !bCanBeAllAir; }
|
||||
|
||||
/** Verdict final : exactement une hypothèse doit survivre. Égalité = prudence ⇒ Mixed. */
|
||||
@@ -626,14 +728,29 @@ FORCEINLINE void VF_FoldEffect(FVoxelBoxHypotheses& H, EVoxelOpEffect Effect,
|
||||
* pure FillOnly would have thrown away, while still letting a passage take it back.
|
||||
*/
|
||||
FORCEINLINE void VF_FoldOp(FVoxelBoxHypotheses& H, const IVoxelDensityOp& Op,
|
||||
const FBox& VoxelBox, const FVoxelOpContext& Ctx)
|
||||
const FBox& VoxelBox, const FVoxelOpContext& Ctx,
|
||||
bool bWritesSdf)
|
||||
{
|
||||
// Soundness bias: an unknown SDF interval is allowed to kill a skip, never to manufacture a
|
||||
// uniform tile. The interval is propagated only after this op's own effect has been folded, so
|
||||
// the next op sees exactly the SDF version that Eval would expose at this point in the stack.
|
||||
const EVoxelTileClass Forced = Op.ClassifyBox(VoxelBox, Ctx);
|
||||
if (Forced != EVoxelTileClass::Mixed)
|
||||
{
|
||||
VF_ForceHypotheses(H, Forced, Op.ForcedMarginOverBox(VoxelBox, Ctx));
|
||||
if (bWritesSdf) { Op.PropagateSdfOverBox(H.Sdf, VoxelBox, Ctx); }
|
||||
return;
|
||||
}
|
||||
VF_FoldEffect(H, Op.EffectOverBox(VoxelBox, Ctx),
|
||||
Op.MaxCarveOverBox(VoxelBox, Ctx), Op.MaxFillOverBox(VoxelBox, Ctx));
|
||||
VF_FoldEffect(H, Op.EffectOverBox(VoxelBox, Ctx, H),
|
||||
Op.MaxCarveOverBox(VoxelBox, Ctx, H),
|
||||
Op.MaxFillOverBox(VoxelBox, Ctx, H));
|
||||
if (bWritesSdf) { Op.PropagateSdfOverBox(H.Sdf, VoxelBox, Ctx); }
|
||||
}
|
||||
|
||||
/** Backward-compatible helper for unit tests and callers with no cached declaration metadata. */
|
||||
FORCEINLINE void VF_FoldOp(FVoxelBoxHypotheses& H, const IVoxelDensityOp& Op,
|
||||
const FBox& VoxelBox, const FVoxelOpContext& Ctx)
|
||||
{
|
||||
VF_FoldOp(H, Op, VoxelBox, Ctx,
|
||||
(Op.ChannelWrites() & VoxelOpChannels::Sdf) != 0);
|
||||
}
|
||||
@@ -154,7 +154,8 @@ public:
|
||||
FVoxelBoxHypotheses H;
|
||||
for (const FOpEntry& Entry : Ops)
|
||||
{
|
||||
VF_FoldOp(H, *Entry.Op, VoxelBox, Ctx);
|
||||
VF_FoldOp(H, *Entry.Op, VoxelBox, Ctx,
|
||||
(Entry.Writes & VoxelOpChannels::Sdf) != 0);
|
||||
// Do not early-out on a dead hypothesis: a later forcing structural post may
|
||||
// deliberately overwrite it (the XY edge seal is appended after passage carving).
|
||||
}
|
||||
@@ -189,7 +190,8 @@ public:
|
||||
const bool bSolidBefore = H.bCanBeAllSolid;
|
||||
const bool bAirBefore = H.bCanBeAllAir;
|
||||
|
||||
VF_FoldOp(H, *Ops[i].Op, VoxelBox, Ctx);
|
||||
VF_FoldOp(H, *Ops[i].Op, VoxelBox, Ctx,
|
||||
(Ops[i].Writes & VoxelOpChannels::Sdf) != 0);
|
||||
|
||||
if (bSolidBefore && !H.bCanBeAllSolid && OutSolidKiller == INDEX_NONE) { OutSolidKiller = i; }
|
||||
if (bAirBefore && !H.bCanBeAllAir && OutAirKiller == INDEX_NONE) { OutAirKiller = i; }
|
||||
@@ -270,14 +272,11 @@ namespace VoxelDensityOps
|
||||
* Écrit le canal SDF uniquement. Identité d'arête = hash(nœud inférieur, axe), donc deux
|
||||
* chunks adjacents NE PEUVENT PAS être en désaccord : pas de cache de chunk, pas de région
|
||||
* COLLECT, zéro risque de couture (AUDIT §6.4 — le motif à préférer). */
|
||||
/* `ExtraReach` = tout ce qui peut eLARGIR la portée du couloir en aval (amplitude de rugosité
|
||||
* rayon de blend du carve). La source répond pour la paire source+conversion dans
|
||||
* `EffectOverBox` (voir la note « SIMPLIFICATION DE PHASE 1 » dans VoxelDensityOp.h), donc elle
|
||||
* doit connaître cette marge, sinon sa réponse `Identity` serait un MENSONGE — c'est-à-dire un
|
||||
* trou. / The source answers for the source+conversion pair, so it must know the downstream
|
||||
* margin: an Identity that is wrong is a hole. */
|
||||
/** The source answers only for its own SDF field. Its box query publishes an SDF interval;
|
||||
* each later converter or modifier consumes that interval through the generic fold. It does
|
||||
* not know, and must not answer for, any downstream operator. */
|
||||
VOXELFORGE_API TUniquePtr<IVoxelDensityOp> MakeLatticeCorridorSource(const FMazeGenerationParams& P,
|
||||
int32 Seed, float ExtraReach);
|
||||
int32 Seed);
|
||||
|
||||
/** Rôle 3 — rugosité de paroi appliquée au canal SDF (variante Maze/Shafts/Islands).
|
||||
* `Frequency` est codée en dur au site d'appel aujourd'hui (0.12 pour Maze) ; l'exposer est
|
||||
@@ -302,7 +301,8 @@ namespace VoxelDensityOps
|
||||
/** Rôle 1 — la dalle : surface de sol + surface de plafond → champ de vide. **XY-PUR** depuis
|
||||
* OPSTACK-DECOMPOSITION §3.1 (le terme en Z des deux bruits est parti), ce qui lui donne un
|
||||
* `ClassifyBox` EXACT sans échantillonnage : les deux surfaces vivent dans des bandes en Z
|
||||
* bornées par le contrat [-1,1] de FBM. Sert FlatPlain **et** CrystalChamber. */
|
||||
* bornées par le supremum prouvé de FBM (1.5, not the nominal parameter label). Sert
|
||||
* FlatPlain **et** CrystalChamber. */
|
||||
VOXELFORGE_API TUniquePtr<IVoxelDensityOp> MakeSlabVoidSource(const FSlabGenerationParams& P, int32 Seed);
|
||||
|
||||
/** Rôle 3 — cylindres de hauteur infinie sur une grille monde. N'ajoute que du solide ⇒
|
||||
@@ -380,8 +380,9 @@ namespace VoxelDensityOps
|
||||
const UVoxelStrateManager* StrateManager);
|
||||
|
||||
/**
|
||||
* DIAGNOSTIC — la ventilation par CLASSE DE PRIMITIVE du dernier `FRoomGraphSource::EffectOverBox`
|
||||
* évalué sur ce thread. **Tests uniquement. N'entre dans aucune décision de génération.**
|
||||
* DIAGNOSTIC — la ventilation par CLASSE DE PRIMITIVE de la dernière propagation d'intervalle
|
||||
* de `FRoomGraphSource` évaluée sur ce thread. **Tests uniquement. N'entre dans aucune décision
|
||||
* de génération.**
|
||||
*
|
||||
* ⚠️ POURQUOI ÇA EXISTE PLUTÔT QUE D'ÊTRE REFAIT DANS LE TEST. Le test a déjà tout ce qu'il faut
|
||||
* pour rejouer le critère — il appelle `BuildChunkCache` ailleurs. Le rejouer serait une
|
||||
|
||||
Reference in new issue
Block a user