refactor(opstack): the operator stack is the only density path

Every strate now generates through its operator stack: authored
definitions, cooked-season entries (recipe or native) and editor
composer candidates alike. The per-strate opt-in and the archetype
switch it guarded are gone.

- Remove UVoxelStrateDefinition::bUseOperatorStack and the season
  wiring that set it; UsesOperatorStackForChunk now only says whether
  the slot has a known archetype.
- GetDensityAt always builds a stack (composer, then recipe, then the
  native archetype stack); a degenerate zero-height strate stays air.
- Remove the startup opt-in warning, the operator_stack startup-trace
  field and the four "Cave Bail Not Op Stack" stats (the remaining
  bail is counted by Cave Bail No Stack).
- Explore commandlet: drop -opstack and the operator_stack fields;
  VoxelForgeTest.ps1 no longer passes -opstack=1.
- Tests: FTestWorld::Build loses its bool parameter; legacy-vs-stack
  comparisons (MazeSeam, VerticalShaftSeam, TerrainDetailLiveness,
  WorldEdgeSeal, TilePostReach) collapse to the single stack path.

This changes terrain for any strate asset that did not already have
the opt-in ticked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01URDGLsu7c4nunqNFPJEFPU
This commit is contained in:
Claude committed 2026-09-28 15:56:19 +00:00
1 parent 63fb71fe6a
commit 8117d118eb
29 files changed
+249 -654

No files matched your search

@@ -39,12 +39,8 @@
#include "Misc/AutomationTest.h" #include "Misc/AutomationTest.h"
#include "VoxelForgeTestFixture.h" #include "VoxelForgeTestFixture.h"
// Inclus ici DÉLIBÉRÉMENT : VoxelDensityOp.h n'est encore inclus par aucun .cpp, donc le // Le fold défini par VoxelDensityOp.h prétend reproduire ClassifyTile : ce fichier le teste.
// compilateur ne le verrait jamais. Le fold qu'il définit prétend reproduire ClassifyTile — ce // The fold defined in VoxelDensityOp.h claims to reproduce ClassifyTile; this file tests it.
// fichier est l'endroit naturel pour que cette prétention soit à la fois COMPILÉE et TESTÉE.
// Deliberately included here: VoxelDensityOp.h is not yet included by any .cpp, so the compiler
// would never see it. Its fold claims to reproduce ClassifyTile, so this is the natural place for
// that claim to be both compiled and tested.
#include "VoxelDensityOp.h" #include "VoxelDensityOp.h"
IMPLEMENT_SIMPLE_AUTOMATION_TEST( IMPLEMENT_SIMPLE_AUTOMATION_TEST(
@@ -153,7 +149,7 @@ bool FVoxelForgeClassifyTileTest::RunTest(const FString& Parameters)
AddInfo(FString::Printf( AddInfo(FString::Printf(
TEXT("ClassifyTile verdicts over %d scanned tiles: Mixed %d, AllSolid %d, AllAir %d ") TEXT("ClassifyTile verdicts over %d scanned tiles: Mixed %d, AllSolid %d, AllAir %d ")
TEXT("(brute-forcing %d of them). Cave archetypes now use the opt-in operator-stack ") TEXT("(brute-forcing %d of them). Cave archetypes use the operator-stack ")
TEXT("ClassifyBox fold; the global XY edge proof also skips proven outer-shell tiles. A low ") TEXT("ClassifyBox fold; the global XY edge proof also skips proven outer-shell tiles. A low ")
TEXT("non-Mixed count is expected: every uncertain case remains Mixed for safety."), TEXT("non-Mixed count is expected: every uncertain case remains Mixed for safety."),
NumTilesScanned, NumMixed, NumAllSolid, NumAllAir, ToVerify.Num())); NumTilesScanned, NumMixed, NumAllSolid, NumAllAir, ToVerify.Num()));
@@ -237,25 +233,24 @@ bool FVoxelForgeClassifyTileTest::RunTest(const FString& Parameters)
} }
//============================================================================= //=============================================================================
// LE CHEMIN PILE D'OPÉRATEURS DE ClassifyTile — MÊME FORCE BRUTE, MONDE OPT-IN // LE CHEMIN PILE D'OPÉRATEURS DE ClassifyTile — MÊME FORCE BRUTE, STRATES DE CAVE
//============================================================================= //=============================================================================
// `ClassifyTile` rendait `Mixed` sans appel pour tout archétype de CAVE. Il consulte désormais // Pour un slot de CAVE, `ClassifyTile` consulte `FVoxelOpStack::ClassifyBox` — donc **la pile peut
// `FVoxelOpStack::ClassifyBox` quand la strate a coché `bUseOperatorStack` — donc **un tout nouveau // faire sauter le maillage d'une tuile**, et son erreur est un TROU : pas de triangles, pas de
// chemin peut faire sauter le maillage d'une tuile**, et son erreur est un TROU : pas de triangles, // collision, invisible jusqu'à ce qu'un joueur tombe au travers.
// pas de collision, invisible jusqu'à ce qu'un joueur tombe au travers.
// //
// Ce test est le même oracle par force brute que `ClassifyTileSoundness`, sur un monde dont TOUTES // Ce test est le même oracle par force brute que `ClassifyTileSoundness`, avec un tirage uniforme
// les strates ont coché la case. Il ne vérifie pas le pliage (c'est `BoxVerdictFold`) ni les // sur tout le layout. Il ne vérifie pas le pliage (c'est `BoxVerdictFold`) ni les opérateurs : il
// opérateurs (ce sont les huit tests d'équivalence) : il vérifie le **câblage** — que la pile // vérifie le **câblage** — que la pile interrogée par le classifieur est bien celle qui produit la
// interrogée par le classifieur est bien celle qui produit la densité, params, drapeau et // densité, params et disturbances compris.
// disturbances compris.
// //
// ⚠️ LE COMPTEUR À LIRE EN PREMIER est le nombre de tuiles réellement brute-forcées. Un run vert // ⚠️ LE COMPTEUR À LIRE EN PREMIER est le nombre de tuiles réellement brute-forcées. Un run vert
// avec zéro verdict non-Mixed ne prouverait RIEN — exactement le piège que ce fichier documente // avec zéro verdict non-Mixed ne prouverait RIEN — exactement le piège que ce fichier documente
// depuis sa première version, et la raison pour laquelle l'absence de verdict est une ERREUR ici. // depuis sa première version, et la raison pour laquelle l'absence de verdict est une ERREUR ici.
// //
// Same brute-force oracle as ClassifyTileSoundness, on a world where every strate has opted in. // Same brute-force oracle as ClassifyTileSoundness, drawn uniformly over the whole layout so cave
// It checks the WIRING, not the fold and not the operators. // slots (which classify through their stack) dominate. It checks the WIRING, not the fold and not
// the operators.
IMPLEMENT_SIMPLE_AUTOMATION_TEST( IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FVoxelForgeOpStackClassifyTileTest, FVoxelForgeOpStackClassifyTileTest,
@@ -266,11 +261,10 @@ bool FVoxelForgeOpStackClassifyTileTest::RunTest(const FString& Parameters)
{ {
using namespace VoxelForgeTest; using namespace VoxelForgeTest;
// ⚠️ `bUseOperatorStack = true` sur toutes les strates : c'est LE point du test. La fixture // La fixture donne à ce monde une `LayoutVersion` unique dans le processus, sans quoi les caches
// donne à ce monde une `LayoutVersion` unique dans le processus, sans quoi les caches par chunk // par chunk de `GetDensityAt` — dont `CP_UseOpStack` — pourraient encore porter ceux d'un autre test.
// de `GetDensityAt` — dont `CP_UseOpStack` — pourraient encore porter ceux d'un autre test.
FTestWorld World; FTestWorld World;
World.Build(/*Seed*/1337, /*GapChunks*/2, /*bUseOperatorStack*/true); World.Build(/*Seed*/1337, /*GapChunks*/2);
if (!World.IsValid()) if (!World.IsValid())
{ {
AddError(World.WhyInvalid()); AddError(World.WhyInvalid());
@@ -287,8 +281,8 @@ bool FVoxelForgeOpStackClassifyTileTest::RunTest(const FString& Parameters)
const int32 BottomVoxelZ = World.BottomChunkZ * CHUNK_SIZE; const int32 BottomVoxelZ = World.BottomChunkZ * CHUNK_SIZE;
// Tirage uniforme sur tout le layout, PAS biaisé vers SurfaceWorld comme l'autre test : ici ce // Tirage uniforme sur tout le layout, PAS biaisé vers SurfaceWorld comme l'autre test : ici ce
// sont précisément les strates de cave qui intéressent, puisque ce sont elles qui passent par le // sont précisément les strates de cave qui intéressent, puisque ce sont elles que ClassifyTile
// nouveau chemin. SurfaceWorld continue d'être prouvé par le code écrit à la main. // juge par leur pile. SurfaceWorld est prouvé par le test colonne écrit à la main.
for (int32 t = 0; t < NumTilesScanned; ++t) for (int32 t = 0; t < NumTilesScanned; ++t)
{ {
FTileSpec Spec; FTileSpec Spec;
@@ -316,20 +310,17 @@ bool FVoxelForgeOpStackClassifyTileTest::RunTest(const FString& Parameters)
AddInfo(FString::Printf( AddInfo(FString::Printf(
TEXT("ClassifyTile ON THE OPERATOR-STACK PATH, %d scanned tiles: Mixed %d, AllSolid %d, ") TEXT("ClassifyTile ON THE OPERATOR-STACK PATH, %d scanned tiles: Mixed %d, AllSolid %d, ")
TEXT("AllAir %d (brute-forcing %d). Compare with VoxelForge.Determinism.ClassifyTileSoundness, ") TEXT("AllAir %d (brute-forcing %d). Every cave-slot verdict is a tile the mesher skips on ")
TEXT("which runs the SAME scan on a world that has NOT opted in: every verdict beyond what ") TEXT("the strength of the stack's ClassifyBox fold -- and a hole if the wiring is wrong, ")
TEXT("that test reports is a tile the mesher now skips and did not before. That difference ") TEXT("which is what the brute force below is for."),
TEXT("IS the T1.d prize OPSTACK-PLAN has been aiming at -- and every one of those tiles is a ")
TEXT("hole if the wiring is wrong, which is what the brute force below is for."),
NumTilesScanned, NumMixed, NumAllSolid, NumAllAir, ToVerify.Num())); NumTilesScanned, NumMixed, NumAllSolid, NumAllAir, ToVerify.Num()));
if (ToVerify.Num() == 0) if (ToVerify.Num() == 0)
{ {
AddError(TEXT("VACUOUS: not one tile got a non-Mixed verdict on the operator-stack path, so ") AddError(TEXT("VACUOUS: not one tile got a non-Mixed verdict on the operator-stack path, so ")
TEXT("this test verified NOTHING about the new wiring. Either no strate actually ") TEXT("this test verified NOTHING about the wiring. Every guard in the cave branch ")
TEXT("opted in (check FTestWorld::Build's bUseOperatorStack), or every guard in the ") TEXT("of ClassifyTile bailed to Mixed -- the params-identical check and the ")
TEXT("cave branch of ClassifyTile bailed to Mixed -- the params-identical check and ") TEXT("27-chunk-coord cap are the likeliest. Do NOT read a green run as proof."));
TEXT("the 27-chunk-coord cap are the likeliest. Do NOT read a green run as proof."));
return false; return false;
} }
@@ -356,7 +356,7 @@ bool FVoxelForgeComposerCorpusFreeTest::RunTest(const FString& Parameters)
Corpus.GetEntries().Num(), 12); Corpus.GetEntries().Num(), 12);
FTestWorld World; FTestWorld World;
World.Build(1337, 2, true); World.Build(1337, 2);
if (!World.IsValid()) if (!World.IsValid())
{ {
AddError(World.WhyInvalid()); AddError(World.WhyInvalid());
@@ -955,7 +955,7 @@ bool FVoxelForgeComposerParameterRollTest::RunTest(const FString& Parameters)
} }
FTestWorld World; FTestWorld World;
World.Build(1337, 2, true); World.Build(1337, 2);
if (!World.IsValid()) if (!World.IsValid())
{ {
AddError(World.WhyInvalid()); AddError(World.WhyInvalid());
@@ -1117,7 +1117,6 @@ bool FVoxelForgeComposerParameterRollTest::RunTest(const FString& Parameters)
CandidateDefinition->SurfaceParams = Roll.ArchetypeParams.SurfaceParams; CandidateDefinition->SurfaceParams = Roll.ArchetypeParams.SurfaceParams;
CandidateDefinition->VerticalShaftParams = Roll.ArchetypeParams.VerticalShaftParams; CandidateDefinition->VerticalShaftParams = Roll.ArchetypeParams.VerticalShaftParams;
CandidateDefinition->FloatingIslandParams = Roll.ArchetypeParams.FloatingIslandParams; CandidateDefinition->FloatingIslandParams = Roll.ArchetypeParams.FloatingIslandParams;
CandidateDefinition->bUseOperatorStack = true;
CandidateDefinition->TransitionType = EVoxelStrateTransition::Hard; CandidateDefinition->TransitionType = EVoxelStrateTransition::Hard;
World.Reinitialize(); World.Reinitialize();
@@ -1342,15 +1341,13 @@ bool FVoxelForgeTerrainDetailLivenessTest::RunTest(const FString& Parameters)
constexpr int32 ExpectedLatticeSamples = 16 * 16 * 16; constexpr int32 ExpectedLatticeSamples = 16 * 16 * 16;
FString Table = TEXT( FString Table = TEXT(
"TERRAIN DETAIL LIVENESS (same 4096-point lattice; only one group changed per row)\n" "TERRAIN DETAIL LIVENESS (same 4096-point lattice; only one group changed per row)\n"
"path | group | samples | baseline digest | variant digest | baseline mean | variant mean | " "group | samples | baseline digest | variant digest | baseline mean | variant mean | "
"changed | sign changes | sum abs delta | max abs delta | verdict\n"); "changed | sign changes | sum abs delta | max abs delta | verdict\n");
int32 TotalRows = 0; int32 TotalRows = 0;
int32 FailedRows = 0; int32 FailedRows = 0;
for (const bool bUseOperatorStack : { false, true })
{
FTestWorld World; FTestWorld World;
World.Build(1337, 2, bUseOperatorStack); World.Build(1337, 2);
if (!World.IsValid()) if (!World.IsValid())
{ {
AddError(World.WhyInvalid()); AddError(World.WhyInvalid());
@@ -1365,17 +1362,11 @@ bool FVoxelForgeTerrainDetailLivenessTest::RunTest(const FString& Parameters)
return false; return false;
} }
Definition->TerrainOperations.Reset(); Definition->TerrainOperations.Reset();
TestEqual( TestEqual(TEXT("fixture has no terrain-op asset pool"), Definition->TerrainOperations.Num(), 0);
FString::Printf(TEXT("%s fixture has no terrain-op asset pool"),
bUseOperatorStack ? TEXT("operator-stack") : TEXT("legacy")),
Definition->TerrainOperations.Num(), 0);
TArray<FVector> Points; TArray<FVector> Points;
VF_BuildTerrainDetailLattice(World, Points); VF_BuildTerrainDetailLattice(World, Points);
TestEqual( TestEqual(TEXT("experiment uses the fixed lattice"), Points.Num(), ExpectedLatticeSamples);
FString::Printf(TEXT("%s experiment uses the fixed lattice"),
bUseOperatorStack ? TEXT("operator-stack") : TEXT("legacy")),
Points.Num(), ExpectedLatticeSamples);
FStrateGenerationParams BaselineParams = Definition->GenerationParams; FStrateGenerationParams BaselineParams = Definition->GenerationParams;
VF_ClearTerrainDetailFields(BaselineParams); VF_ClearTerrainDetailFields(BaselineParams);
@@ -1403,9 +1394,7 @@ bool FVoxelForgeTerrainDetailLivenessTest::RunTest(const FString& Parameters)
bSameBaseline = BitEqual(FirstBaseline[SampleIndex], BaselineSamples[SampleIndex]); bSameBaseline = BitEqual(FirstBaseline[SampleIndex], BaselineSamples[SampleIndex]);
} }
TestTrue( TestTrue(
FString::Printf(TEXT("%s baseline is stable before %s probe"), FString::Printf(TEXT("baseline is stable before %s probe"), Spec.Name),
bUseOperatorStack ? TEXT("operator-stack") : TEXT("legacy"),
Spec.Name),
bSameBaseline); bSameBaseline);
} }
@@ -1428,14 +1417,11 @@ bool FVoxelForgeTerrainDetailLivenessTest::RunTest(const FString& Parameters)
++FailedRows; ++FailedRows;
} }
TestTrue( TestTrue(
FString::Printf(TEXT("%s %s field group has the measured liveness verdict"), FString::Printf(TEXT("%s field group has the measured liveness verdict"), Spec.Name),
bUseOperatorStack ? TEXT("operator-stack") : TEXT("legacy"),
Spec.Name),
bVerdictMatches); bVerdictMatches);
Table += FString::Printf( Table += FString::Printf(
TEXT("%s | %s | %d | 0x%08x | 0x%08x | %.9g | %.9g | %d | %d | %.9g | %.9g | %s\n"), TEXT("%s | %d | 0x%08x | 0x%08x | %.9g | %.9g | %d | %d | %.9g | %.9g | %s\n"),
bUseOperatorStack ? TEXT("operator-stack") : TEXT("legacy"),
Spec.Name, Spec.Name,
Points.Num(), Points.Num(),
BaselineSummary.Digest, BaselineSummary.Digest,
@@ -1448,17 +1434,16 @@ bool FVoxelForgeTerrainDetailLivenessTest::RunTest(const FString& Parameters)
Delta.MaxAbs, Delta.MaxAbs,
bObservedLive ? TEXT("LIVE") : TEXT("DEAD")); bObservedLive ? TEXT("LIVE") : TEXT("DEAD"));
} }
}
AddInfo(Table); AddInfo(Table);
AddInfo(FString::Printf( AddInfo(FString::Printf(
TEXT("Terrain detail liveness: %d group/path rows measured, %d mismatches; LIVE groups " TEXT("Terrain detail liveness: %d group rows measured, %d mismatches; LIVE groups "
"must have at least one bit-changed density and DEAD groups must have none. No " "must have at least one bit-changed density and DEAD groups must have none. No "
"terrain-operation assets were present in the fixture pool."), "terrain-operation assets were present in the fixture pool."),
TotalRows, FailedRows)); TotalRows, FailedRows));
TestEqual(TEXT("all terrain detail liveness rows matched their measured verdict"), FailedRows, 0); TestEqual(TEXT("all terrain detail liveness rows matched their measured verdict"), FailedRows, 0);
TestEqual(TEXT("both density paths measured every terrain detail group"), TotalRows, TestEqual(TEXT("every terrain detail group was measured"), TotalRows,
static_cast<int32>(2 * UE_ARRAY_COUNT(GTerrainDetailProbeSpecs))); static_cast<int32>(UE_ARRAY_COUNT(GTerrainDetailProbeSpecs)));
return FailedRows == 0; return FailedRows == 0;
} }
@@ -394,7 +394,7 @@ bool FVoxelForgeComposerPromotionTest::RunTest(const FString& Parameters)
} }
FTestWorld World; FTestWorld World;
World.Build(1337, 2, true); World.Build(1337, 2);
if (!World.IsValid()) if (!World.IsValid())
{ {
AddError(World.WhyInvalid()); AddError(World.WhyInvalid());
@@ -1590,7 +1590,7 @@ bool FVoxelForgeComposerShowcaseTest::RunTest(const FString& Parameters)
FTestWorld World; FTestWorld World;
// The shared fixture defaults to a bounded 4-chunk volume for fast density tests. The // The shared fixture defaults to a bounded 4-chunk volume for fast density tests. The
// owner-facing scale audit must exercise the production/default 8-chunk envelope. // owner-facing scale audit must exercise the production/default 8-chunk envelope.
World.Build(BaseComposerSeed, 2, true, 8); World.Build(BaseComposerSeed, 2, 8);
if (!World.IsValid()) if (!World.IsValid())
{ {
AddError(World.WhyInvalid()); AddError(World.WhyInvalid());
@@ -262,7 +262,7 @@ bool FVoxelForgeComposerStructureRollTest::RunTest(const FString& Parameters)
} }
FTestWorld World; FTestWorld World;
World.Build(1337, 2, true); World.Build(1337, 2);
if (!World.IsValid()) if (!World.IsValid())
{ {
AddError(World.WhyInvalid()); AddError(World.WhyInvalid());
@@ -132,7 +132,7 @@ IMPLEMENT_SIMPLE_AUTOMATION_TEST(
bool FVoxelForgeTileCacheContextDeterminismTest::RunTest(const FString& Parameters) bool FVoxelForgeTileCacheContextDeterminismTest::RunTest(const FString& Parameters)
{ {
FTestWorld World; FTestWorld World;
World.Build(/*Seed*/1337, /*GapChunks*/2, /*bUseOperatorStack*/true); World.Build(/*Seed*/1337, /*GapChunks*/2);
if (!World.IsValid()) if (!World.IsValid())
{ {
AddError(World.WhyInvalid()); AddError(World.WhyInvalid());
@@ -208,8 +208,8 @@ bool FVoxelForgeTileCacheContextDeterminismTest::RunTest(const FString& Paramete
// level-0 tile at the same origin is compared with a fresh world's level-0 output. // level-0 tile at the same origin is compared with a fresh world's level-0 output.
FTestWorld WarmWorld; FTestWorld WarmWorld;
FTestWorld FreshWorld; FTestWorld FreshWorld;
WarmWorld.Build(/*Seed*/1337, /*GapChunks*/2, /*bUseOperatorStack*/true); WarmWorld.Build(/*Seed*/1337, /*GapChunks*/2);
FreshWorld.Build(/*Seed*/1337, /*GapChunks*/2, /*bUseOperatorStack*/true); FreshWorld.Build(/*Seed*/1337, /*GapChunks*/2);
if (!WarmWorld.IsValid() || !FreshWorld.IsValid()) if (!WarmWorld.IsValid() || !FreshWorld.IsValid())
{ {
AddError(WarmWorld.IsValid() && FreshWorld.IsValid() AddError(WarmWorld.IsValid() && FreshWorld.IsValid()
@@ -348,7 +348,7 @@ bool FVoxelForgeDensityBlockEquivalenceTest::RunTest(const FString& Parameters)
for (int32 SeedIndex = 0; SeedIndex < UE_ARRAY_COUNT(Seeds); ++SeedIndex) for (int32 SeedIndex = 0; SeedIndex < UE_ARRAY_COUNT(Seeds); ++SeedIndex)
{ {
VoxelForgeTest::FTestWorld World; VoxelForgeTest::FTestWorld World;
World.Build(Seeds[SeedIndex], /*gap chunks=*/2, /*operator stack=*/true); World.Build(Seeds[SeedIndex], /*gap chunks=*/2);
if (!World.IsValid()) if (!World.IsValid())
{ {
AddError(World.WhyInvalid()); AddError(World.WhyInvalid());
@@ -470,8 +470,8 @@ bool FVoxelForgeDensityBlockEquivalenceTest::RunTest(const FString& Parameters)
// No-edit grids. In the two canonical network archetypes this enters the actual worker // No-edit grids. In the two canonical network archetypes this enters the actual worker
// block session, which partitions each mesher grid by resolved chunk before running the // block session, which partitions each mesher grid by resolved chunk before running the
// active operator list. The six legacy density routes still run their production scalar // active operator list. The six other archetypes run the scalar stack evaluator, as they
// evaluator, as they do outside the block-safe network branch. // do in production outside the block-safe network branch.
for (int32 GridIndex = 0; GridIndex < GridSetCount; ++GridIndex) for (int32 GridIndex = 0; GridIndex < GridSetCount; ++GridIndex)
{ {
BaseResults[GridIndex] = CompareGeneratorBlockWithScalar( BaseResults[GridIndex] = CompareGeneratorBlockWithScalar(
@@ -761,14 +761,13 @@ namespace
const float SimulatedCarvedFinalDensity = -SimulatedCarvedInternalDensity; const float SimulatedCarvedFinalDensity = -SimulatedCarvedInternalDensity;
UE_LOG(LogTemp, Display, UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeDescentWriterContext] seed=0 sample=(%.3f,%.3f,%.3f) " TEXT("[VoxelForgeDescentWriterContext] seed=0 sample=(%.3f,%.3f,%.3f) "
"chunk=(%d,%d,%d) archetype=%s asset_stack_flag=%d effective_op_stack=%d " "chunk=(%d,%d,%d) archetype=%s effective_op_stack=%d "
"passage_sdf=%.6f simulated_passage_carve_density=%.6f " "passage_sdf=%.6f simulated_passage_carve_density=%.6f "
"terrain_ops='%s' terrain_op_paths='%s' " "terrain_ops='%s' terrain_op_paths='%s' "
"base=%.3f tunnel_columns=(%.3f,%.3f,%.3f) slab_columns=(%.3f,%.3f,%.3f)"), "base=%.3f tunnel_columns=(%.3f,%.3f,%.3f) slab_columns=(%.3f,%.3f,%.3f)"),
OutSamples.FirstSolidPosition.X, OutSamples.FirstSolidPosition.Y, OutSamples.FirstSolidPosition.X, OutSamples.FirstSolidPosition.Y,
OutSamples.FirstSolidPosition.Z, ChunkCoord.X, ChunkCoord.Y, ChunkCoord.Z, OutSamples.FirstSolidPosition.Z, ChunkCoord.X, ChunkCoord.Y, ChunkCoord.Z,
*GeneratorName, Definition != nullptr && Definition->bUseOperatorStack ? 1 : 0, *GeneratorName, bUsesOperatorStack ? 1 : 0, PassageSDF, SimulatedCarvedFinalDensity,
bUsesOperatorStack ? 1 : 0, PassageSDF, SimulatedCarvedFinalDensity,
*TerrainOps, *TerrainOpPaths, Params.BaseDensity, Params.ColumnDensity, *TerrainOps, *TerrainOpPaths, Params.BaseDensity, Params.ColumnDensity,
Params.ColumnMinRadius, Params.ColumnMaxRadius, Params.ColumnMinRadius, Params.ColumnMaxRadius,
Definition != nullptr ? Definition->SlabParams.ColumnDensity : -1.0f, Definition != nullptr ? Definition->SlabParams.ColumnDensity : -1.0f,
@@ -549,7 +549,7 @@ bool FVoxelForgeLateralRegionsTest::RunTest(const FString& Parameters)
} }
FTestWorld World; FTestWorld World;
World.Build(Seed, 2, false); World.Build(Seed, 2);
int32 TopWorldZ = 0; int32 TopWorldZ = 0;
int32 BottomWorldZ = 0; int32 BottomWorldZ = 0;
FVoxelStrateRegionManifest Manifest; FVoxelStrateRegionManifest Manifest;
@@ -612,7 +612,7 @@ bool FVoxelForgeLateralRegionsTest::RunTest(const FString& Parameters)
} }
FTestWorld World; FTestWorld World;
World.Build(Seed, 2, false); World.Build(Seed, 2);
int32 TopWorldZ = 0; int32 TopWorldZ = 0;
int32 BottomWorldZ = 0; int32 BottomWorldZ = 0;
FVoxelStrateRegionManifest Manifest; FVoxelStrateRegionManifest Manifest;
@@ -661,7 +661,7 @@ bool FVoxelForgeLateralRegionsTest::RunTest(const FString& Parameters)
TestTrue(TEXT("found a single-region recipe identity case"), bRecipeIdentityChecked); TestTrue(TEXT("found a single-region recipe identity case"), bRecipeIdentityChecked);
FTestWorld World; FTestWorld World;
World.Build(MultiSeed, 2, false); World.Build(MultiSeed, 2);
if (!World.IsValid()) if (!World.IsValid())
{ {
AddError(World.WhyInvalid()); AddError(World.WhyInvalid());
@@ -955,7 +955,7 @@ bool FVoxelForgeLateralRegionsTest::RunTest(const FString& Parameters)
} }
FTestWorld LawWorld; FTestWorld LawWorld;
LawWorld.Build(Seed, 2, false); LawWorld.Build(Seed, 2);
FVoxelStrateRegionManifest LawManifest; FVoxelStrateRegionManifest LawManifest;
FVoxelOpStack LawStack; FVoxelOpStack LawStack;
FVoxelOpContext LawContext; FVoxelOpContext LawContext;
@@ -100,65 +100,44 @@ bool FVoxelForgeMazeSeamTest::RunTest(const FString& Parameters)
{ {
using namespace VoxelForgeTest; using namespace VoxelForgeTest;
FTestWorld LegacyWorld; FTestWorld World;
LegacyWorld.Build(7331, 2, false, 4); World.Build(7331, 2, 4);
FTestWorld StackWorld; if (!World.IsValid())
StackWorld.Build(7331, 2, true, 4);
if (!LegacyWorld.IsValid() || !StackWorld.IsValid())
{ {
AddError(LegacyWorld.IsValid() ? StackWorld.WhyInvalid() : LegacyWorld.WhyInvalid()); AddError(World.WhyInvalid());
return false; return false;
} }
int32 LegacyTop = 0, LegacyBottom = 0; int32 Top = 0, Bottom = 0;
int32 StackTop = 0, StackBottom = 0; if (!World.GetSlotVoxelZRange(FTestWorld::SlotMaze, Top, Bottom))
if (!LegacyWorld.GetSlotVoxelZRange(FTestWorld::SlotMaze, LegacyTop, LegacyBottom)
|| !StackWorld.GetSlotVoxelZRange(FTestWorld::SlotMaze, StackTop, StackBottom))
{ {
AddError(TEXT("The seam fixture has no Maze slot.")); AddError(TEXT("The seam fixture has no Maze slot."));
return false; return false;
} }
const int32 LegacyMidChunkZ = ((LegacyTop + LegacyBottom) / 2) / CHUNK_SIZE; const int32 MidChunkZ = ((Top + Bottom) / 2) / CHUNK_SIZE;
const int32 StackMidChunkZ = ((StackTop + StackBottom) / 2) / CHUNK_SIZE; const FMazeGenerationParams Params = World.StrateManager->GetMazeParamsForChunk(
const FMazeGenerationParams LegacyParams = LegacyWorld.StrateManager->GetMazeParamsForChunk( FIntVector(0, 0, MidChunkZ));
FIntVector(0, 0, LegacyMidChunkZ));
const FMazeGenerationParams StackParams = StackWorld.StrateManager->GetMazeParamsForChunk(
FIntVector(0, 0, StackMidChunkZ));
TArray<FVector> LegacyProbes; TArray<FVector> Probes;
TArray<FVector> StackProbes; AddMazeBoundaryProbes(Params, Bottom, Top, Probes);
AddMazeBoundaryProbes(LegacyParams, LegacyBottom, LegacyTop, LegacyProbes);
AddMazeBoundaryProbes(StackParams, StackBottom, StackTop, StackProbes);
TestEqual(TEXT("legacy and operator seam tests have the same probe count"),
LegacyProbes.Num(), StackProbes.Num());
int32 LegacyFirst = INDEX_NONE, StackFirst = INDEX_NONE; int32 First = INDEX_NONE;
float LegacyBefore = 0.0f, LegacyAfter = 0.0f; float Before = 0.0f, After = 0.0f;
float StackBefore = 0.0f, StackAfter = 0.0f; const int32 Mismatches = CountContextMismatches(World, Probes, First, Before, After);
const int32 LegacyMismatches = CountContextMismatches(
LegacyWorld, LegacyProbes, LegacyFirst, LegacyBefore, LegacyAfter);
const int32 StackMismatches = CountContextMismatches(
StackWorld, StackProbes, StackFirst, StackBefore, StackAfter);
TestEqual(TEXT("legacy Maze has no chunk-context seam mismatches"), LegacyMismatches, 0); TestEqual(TEXT("Maze has no chunk-context seam mismatches"), Mismatches, 0);
TestEqual(TEXT("operator-stack Maze has no chunk-context seam mismatches"), StackMismatches, 0); TestTrue(TEXT("Maze seam test sampled cell-boundary probes"), Probes.Num() > 0);
TestTrue(TEXT("Maze seam test sampled cell-boundary probes"), LegacyProbes.Num() > 0);
if (LegacyFirst != INDEX_NONE) if (First != INDEX_NONE)
{ {
AddError(FString::Printf(TEXT("Legacy Maze seam mismatch at probe %d: %.9g -> %.9g"), AddError(FString::Printf(TEXT("Maze seam mismatch at probe %d: %.9g -> %.9g"),
LegacyFirst, LegacyBefore, LegacyAfter)); First, Before, After));
}
if (StackFirst != INDEX_NONE)
{
AddError(FString::Printf(TEXT("Operator Maze seam mismatch at probe %d: %.9g -> %.9g"),
StackFirst, StackBefore, StackAfter));
} }
AddInfo(FString::Printf( AddInfo(FString::Printf(
TEXT("Maze seam freedom: %d boundary probes, legacy mismatches=%d, operator mismatches=%d; " TEXT("Maze seam freedom: %d boundary probes, mismatches=%d; "
"local window=2x2x2 child nodes ({-1,0}^3), no wide collect."), "local window=2x2x2 child nodes ({-1,0}^3), no wide collect."),
LegacyProbes.Num(), LegacyMismatches, StackMismatches)); Probes.Num(), Mismatches));
return true; return true;
} }
@@ -175,7 +154,7 @@ bool FVoxelForgeCaveWarpTileLodSeamTest::RunTest(const FString& Parameters)
using namespace VoxelForgeTest; using namespace VoxelForgeTest;
FTestWorld World; FTestWorld World;
World.Build(1337, 2, true, 4); World.Build(1337, 2, 4);
if (!World.IsValid()) if (!World.IsValid())
{ {
AddError(World.WhyInvalid()); AddError(World.WhyInvalid());
@@ -90,9 +90,8 @@ bool FVoxelForgeOpStackChannelTest::RunTest(const FString& Parameters)
TestTrue(*FString::Printf(TEXT("%s shipping stack has a legal channel DAG"), Name), bValid); TestTrue(*FString::Printf(TEXT("%s shipping stack has a legal channel DAG"), Name), bValid);
}; };
// These are the eight builders currently shipped behind bUseOperatorStack. Keep the // These are the eight shipped archetype builders. Keep the archetype list explicit so adding
// archetype list explicit so adding a builder without adding its validator assertion is // a builder without adding its validator assertion is visible in this test.
// visible in this test.
int32 MidChunkZ = 0; int32 MidChunkZ = 0;
if (MidChunkForSlot(FTestWorld::SlotTunnelNetwork, TEXT("TunnelNetwork"), MidChunkZ)) if (MidChunkForSlot(FTestWorld::SlotTunnelNetwork, TEXT("TunnelNetwork"), MidChunkZ))
@@ -140,7 +140,6 @@ namespace
Definition->SurfaceParams = Candidate.ArchetypeParams.SurfaceParams; Definition->SurfaceParams = Candidate.ArchetypeParams.SurfaceParams;
Definition->VerticalShaftParams = Candidate.ArchetypeParams.VerticalShaftParams; Definition->VerticalShaftParams = Candidate.ArchetypeParams.VerticalShaftParams;
Definition->FloatingIslandParams = Candidate.ArchetypeParams.FloatingIslandParams; Definition->FloatingIslandParams = Candidate.ArchetypeParams.FloatingIslandParams;
Definition->bUseOperatorStack = true;
Definition->TransitionType = EVoxelStrateTransition::Hard; Definition->TransitionType = EVoxelStrateTransition::Hard;
World.Reinitialize(); World.Reinitialize();
} }
@@ -869,7 +868,7 @@ bool FVoxelForgeScaleDiagnosisTest::RunTest(const FString& Parameters)
// Resolve every probe from the current corpus. This keeps the diagnosis attached to real // Resolve every probe from the current corpus. This keeps the diagnosis attached to real
// single-region candidates without making a corpus asset edit look like a geometry failure. // single-region candidates without making a corpus asset edit look like a geometry failure.
FTestWorld World; FTestWorld World;
World.Build(DiagnosisSeed, 2, true, 8); World.Build(DiagnosisSeed, 2, 8);
if (!World.IsValid()) if (!World.IsValid())
{ {
AddError(World.WhyInvalid()); AddError(World.WhyInvalid());
@@ -64,8 +64,7 @@ bool FVoxelForgeSealedSolidProofTest::RunTest(const FString& Parameters)
// Forty voxels makes a complete 32-cell level-0 tile fit inside a bottom boundary seal. The // Forty voxels makes a complete 32-cell level-0 tile fit inside a bottom boundary seal. The
// production asset remains untouched; this is a transient proof-only fixture configuration. // production asset remains untouched; this is a transient proof-only fixture configuration.
FTestWorld World; FTestWorld World;
World.Build(/*Seed*/1337, /*GapChunks*/2, /*bUseOperatorStack*/false, World.Build(/*Seed*/1337, /*GapChunks*/2, /*InStrateHeightInChunks*/8);
/*InStrateHeightInChunks*/8);
if (!World.IsValid()) if (!World.IsValid())
{ {
AddError(World.WhyInvalid()); AddError(World.WhyInvalid());
@@ -1325,20 +1325,12 @@ bool FVoxelForgeVerticalShaftSeamTest::RunTest(const FString& Parameters)
{ {
using namespace VoxelForgeTest; using namespace VoxelForgeTest;
bool bAllChecksPassed = true;
FString Summary = TEXT("VerticalShafts seam-freedom check (cell-boundary probes with two "
"neighbouring chunk-context orders):\n");
for (const bool bUseOperatorStack : { false, true })
{
FTestWorld World; FTestWorld World;
World.Build(/*InSeed=*/1337, /*InGapChunks=*/2, bUseOperatorStack); World.Build(/*InSeed=*/1337, /*InGapChunks=*/2);
if (!World.IsValid()) if (!World.IsValid())
{ {
AddError(FString::Printf( AddError(TEXT("HARD FAILURE: could not build the seam-test fixture."));
TEXT("HARD FAILURE: could not build the %s seam-test fixture."), return false;
bUseOperatorStack ? TEXT("operator-stack") : TEXT("legacy")));
bAllChecksPassed = false;
continue;
} }
int32 Checked = 0; int32 Checked = 0;
@@ -1346,33 +1338,25 @@ bool FVoxelForgeVerticalShaftSeamTest::RunTest(const FString& Parameters)
FString FirstMismatch; FString FirstMismatch;
if (!CheckVerticalShaftSeams(World, Checked, Mismatches, FirstMismatch)) if (!CheckVerticalShaftSeams(World, Checked, Mismatches, FirstMismatch))
{ {
AddError(FString::Printf( AddError(TEXT("HARD FAILURE: the seam test sampled no VerticalShafts positions."));
TEXT("HARD FAILURE: the %s seam test sampled no VerticalShafts positions."), return false;
bUseOperatorStack ? TEXT("operator-stack") : TEXT("legacy")));
bAllChecksPassed = false;
continue;
} }
Summary += FString::Printf( AddInfo(FString::Printf(
TEXT(" %s path: %d same-position re-evaluations, %d bit mismatches%s%s.\n"), TEXT("VerticalShafts seam-freedom check (cell-boundary probes with two neighbouring "
bUseOperatorStack ? TEXT("operator-stack") : TEXT("legacy"), "chunk-context orders): %d same-position re-evaluations, %d bit mismatches%s%s."),
Checked, Checked,
Mismatches, Mismatches,
Mismatches > 0 ? TEXT("; first ") : TEXT(""), Mismatches > 0 ? TEXT("; first ") : TEXT(""),
Mismatches > 0 ? *FirstMismatch : TEXT("")); Mismatches > 0 ? *FirstMismatch : TEXT("")));
if (Mismatches != 0) if (Mismatches != 0)
{ {
AddError(FString::Printf( AddError(FString::Printf(
TEXT("SEAM: %s VerticalShafts density changed after different chunk-context " TEXT("SEAM: VerticalShafts density changed after different chunk-context orders: %s"),
"orders: %s"),
bUseOperatorStack ? TEXT("operator-stack") : TEXT("legacy"),
*FirstMismatch)); *FirstMismatch));
bAllChecksPassed = false; return false;
} }
} return true;
AddInfo(Summary);
return bAllChecksPassed;
} }
bool FVoxelForgeStrateConnectivityTest::RunTest(const FString& Parameters) bool FVoxelForgeStrateConnectivityTest::RunTest(const FString& Parameters)
@@ -6,9 +6,9 @@
// --------------- // ---------------
// The interesting invariants (density purity across worker threads, ClassifyTile soundness) // The interesting invariants (density purity across worker threads, ClassifyTile soundness)
// only fire on the REAL path — UVoxelGenerator::GetDensityAt — because that is where the // only fire on the REAL path — UVoxelGenerator::GetDensityAt — because that is where the
// thread_local per-chunk caches live (CP_*, GSurfColCache, the diff slots, the SDF cache). // thread_local per-chunk caches live (CP_*, the prepared stacks, the diff slots, the SDF cache).
// Calling GetSurfaceDensity / GetMazeDensity directly bypasses every one of them and would // Evaluating a stack directly bypasses every one of them and would test almost nothing.
// test almost nothing. GetDensityAt in turn needs a live UVoxelStrateManager, whose only // GetDensityAt in turn needs a live UVoxelStrateManager, whose only
// entry point is Initialize(UVoxelSettings*, int32) reading TSoftObjectPtr pools. // entry point is Initialize(UVoxelSettings*, int32) reading TSoftObjectPtr pools.
// //
// So the fixture builds a whole synthetic world in memory: transient strate definitions → // So the fixture builds a whole synthetic world in memory: transient strate definitions →
@@ -46,7 +46,7 @@ namespace VoxelForgeTest
* generator + diff layer. No AActor, no UWorld, no PIE. * generator + diff layer. No AActor, no UWorld, no PIE.
* *
* The default layout stacks one strate of EVERY archetype (in ECaveGeneratorType order), * The default layout stacks one strate of EVERY archetype (in ECaveGeneratorType order),
* so a single fixture exercises all eight density functions and their per-chunk caches, * so a single fixture exercises all eight archetype stacks and their per-chunk caches,
* plus the gap-bedrock path when InterStrateGapChunks > 0. * plus the gap-bedrock path when InterStrateGapChunks > 0.
*/ */
struct FTestWorld struct FTestWorld
@@ -73,8 +73,7 @@ namespace VoxelForgeTest
* dangerous — and `VoxelForge.Determinism.LargeSeedSurvives` deliberately passes big ones * dangerous — and `VoxelForge.Determinism.LargeSeedSurvives` deliberately passes big ones
* (up to 2e9) to prove it stays that way. * (up to 2e9) to prove it stays that way.
*/ */
void Build(int32 InSeed = 1337, int32 InGapChunks = 2, bool bUseOperatorStack = false, void Build(int32 InSeed = 1337, int32 InGapChunks = 2, int32 InStrateHeightInChunks = 4)
int32 InStrateHeightInChunks = 4)
{ {
Settings = TStrongObjectPtr<UVoxelSettings>( Settings = TStrongObjectPtr<UVoxelSettings>(
NewObject<UVoxelSettings>(GetTransientPackage(), NAME_None, RF_Transient)); NewObject<UVoxelSettings>(GetTransientPackage(), NAME_None, RF_Transient));
@@ -111,10 +110,6 @@ namespace VoxelForgeTest
// refinement and roll tests use the smaller synthetic volume as a bounded memory // refinement and roll tests use the smaller synthetic volume as a bounded memory
// control; the owner-facing showcase opts into 8 chunks below. // control; the owner-facing showcase opts into 8 chunks below.
Def->StrateHeightInChunks = FMath::Max(1, InStrateHeightInChunks); Def->StrateHeightInChunks = FMath::Max(1, InStrateHeightInChunks);
// L'OPT-IN de la pile d'opérateurs. Faux par défaut : les treize tests existants
// doivent continuer à exercer le `switch`, qui reste le comportement de référence.
// Seul le test de solidité de ClassifyTie côté pile le passe à vrai.
Def->bUseOperatorStack = bUseOperatorStack;
// Hard transitions: param blending across a boundary would make "which archetype // Hard transitions: param blending across a boundary would make "which archetype
// owns this chunk" ambiguous, and these tests want an unambiguous mapping. // owns this chunk" ambiguous, and these tests want an unambiguous mapping.
Def->TransitionType = EVoxelStrateTransition::Hard; Def->TransitionType = EVoxelStrateTransition::Hard;
@@ -154,72 +154,50 @@ bool FVoxelForgeTilePostReachProofTest::RunTest(const FString& Parameters)
return false; return false;
} }
FTestWorld NativeWorld; FTestWorld World;
NativeWorld.Build(/*Seed*/1337, /*GapChunks*/2, /*bUseOperatorStack*/false); World.Build(/*Seed*/1337, /*GapChunks*/2);
FTestWorld FusedWorld; if (!World.IsValid())
FusedWorld.Build(/*Seed*/1337, /*GapChunks*/2, /*bUseOperatorStack*/true);
if (!NativeWorld.IsValid() || !FusedWorld.IsValid())
{ {
AddError(NativeWorld.IsValid() && FusedWorld.IsValid() AddError(World.WhyInvalid());
? TEXT("Unexpected invalid reach-proof fixture")
: (!NativeWorld.IsValid() ? NativeWorld.WhyInvalid() : FusedWorld.WhyInvalid()));
return false; return false;
} }
TArray<FIntVector> NativeOrigins; TArray<FIntVector> Origins;
TArray<FIntVector> FusedOrigins; AppendWorldReachTiles(World, Origins);
AppendWorldReachTiles(NativeWorld, NativeOrigins); TestTrue(TEXT("the representative tile set is non-empty"), Origins.Num() > 0);
AppendWorldReachTiles(FusedWorld, FusedOrigins);
TestTrue(TEXT("the representative native tile set is non-empty"), NativeOrigins.Num() > 0);
TestTrue(TEXT("the representative fused tile set is non-empty"), FusedOrigins.Num() > 0);
TStrongObjectPtr<UVoxelMarchingCubesMesher> NativeMesher( TStrongObjectPtr<UVoxelMarchingCubesMesher> Mesher(
NewObject<UVoxelMarchingCubesMesher>(GetTransientPackage(), NAME_None, RF_Transient)); NewObject<UVoxelMarchingCubesMesher>(GetTransientPackage(), NAME_None, RF_Transient));
TStrongObjectPtr<UVoxelMarchingCubesMesher> FusedMesher( Mesher->SetGenerator(World.Generator.Get());
NewObject<UVoxelMarchingCubesMesher>(GetTransientPackage(), NAME_None, RF_Transient));
NativeMesher->SetGenerator(NativeWorld.Generator.Get());
FusedMesher->SetGenerator(FusedWorld.Generator.Get());
VoxelGenLOD::ResetTilePostReachDiagnostics(); VoxelGenLOD::ResetTilePostReachDiagnostics();
CVarGuard.SetScale(1.0f); CVarGuard.SetScale(1.0f);
const FReachSnapshot NativeFull = RunReachTiles(*NativeMesher, NativeOrigins); const FReachSnapshot Full = RunReachTiles(*Mesher, Origins);
const FReachSnapshot FusedFull = RunReachTiles(*FusedMesher, FusedOrigins);
const uint64 FullFinalComparisons = NativeFull.FinalComparisons + FusedFull.FinalComparisons;
const uint64 FullFinalDifferences = NativeFull.FinalDifferences + FusedFull.FinalDifferences;
TestTrue(TEXT("normal reach compares final fields on the representative tiles"), TestTrue(TEXT("normal reach compares final fields on the representative tiles"),
FullFinalComparisons > 0); Full.FinalComparisons > 0);
TestEqual(TEXT("normal reach has no final-field differences"), FullFinalDifferences, 0ull); TestEqual(TEXT("normal reach has no final-field differences"), Full.FinalDifferences, 0ull);
TestEqual(TEXT("normal reach has no skipped-post differences"), TestEqual(TEXT("normal reach has no skipped-post differences"), Full.AllDifferences, 0ull);
NativeFull.AllDifferences + FusedFull.AllDifferences, 0ull);
VoxelGenLOD::ResetTilePostReachDiagnostics(); VoxelGenLOD::ResetTilePostReachDiagnostics();
// Use a deliberately tiny but valid scale so at least one envelope is rejected while the same // Use a deliberately tiny but valid scale so at least one envelope is rejected while the same
// final-field samples remain exercised; this is the negative control, not a supported setting. // final-field samples remain exercised; this is the negative control, not a supported setting.
CVarGuard.SetScale(0.01f); CVarGuard.SetScale(0.01f);
const FReachSnapshot NativeShrunk = RunReachTiles(*NativeMesher, NativeOrigins); const FReachSnapshot Shrunk = RunReachTiles(*Mesher, Origins);
const FReachSnapshot FusedShrunk = RunReachTiles(*FusedMesher, FusedOrigins);
const uint64 ShrunkFinalComparisons =
NativeShrunk.FinalComparisons + FusedShrunk.FinalComparisons;
const uint64 ShrunkFinalDifferences =
NativeShrunk.FinalDifferences + FusedShrunk.FinalDifferences;
TestTrue(TEXT("shrunk reach still executes final-field comparisons"), TestTrue(TEXT("shrunk reach still executes final-field comparisons"),
ShrunkFinalComparisons > 0); Shrunk.FinalComparisons > 0);
TestTrue(TEXT("shrunk reach demonstrably fails the final-field proof"), TestTrue(TEXT("shrunk reach demonstrably fails the final-field proof"),
ShrunkFinalDifferences > 0); Shrunk.FinalDifferences > 0);
AddInfo(FString::Printf( AddInfo(FString::Printf(
TEXT("tile_post_reach_proof: native_tiles=%llu fused_tiles=%llu full_final=%llu/%llu " TEXT("tile_post_reach_proof: tiles=%llu full_final=%llu/%llu shrunk_final=%llu/%llu "
"shrunk_final=%llu/%llu native_samples=%lld/%lld fused_samples=%lld/%lld."), "samples=%lld/%lld."),
static_cast<unsigned long long>(NativeFull.Tiles), static_cast<unsigned long long>(Full.Tiles),
static_cast<unsigned long long>(FusedFull.Tiles), static_cast<unsigned long long>(Full.FinalComparisons),
static_cast<unsigned long long>(FullFinalComparisons), static_cast<unsigned long long>(Full.FinalDifferences),
static_cast<unsigned long long>(FullFinalDifferences), static_cast<unsigned long long>(Shrunk.FinalComparisons),
static_cast<unsigned long long>(ShrunkFinalComparisons), static_cast<unsigned long long>(Shrunk.FinalDifferences),
static_cast<unsigned long long>(ShrunkFinalDifferences), static_cast<long long>(Full.DensitySamples),
static_cast<long long>(NativeFull.DensitySamples), static_cast<long long>(Shrunk.DensitySamples)));
static_cast<long long>(NativeShrunk.DensitySamples),
static_cast<long long>(FusedFull.DensitySamples),
static_cast<long long>(FusedShrunk.DensitySamples)));
return true; return true;
} }
@@ -67,26 +67,20 @@ bool FVoxelForgeWorldEdgeSealTest::RunTest(const FString& Parameters)
FailureKinds += Kind; FailureKinds += Kind;
}; };
// The ordinary fixture deliberately uses the requested default, so existing op-stack and // The ordinary fixture deliberately uses the requested default, so the other tests exercise
// legacy tests exercise the setting without having to know about the new post. // the setting without having to know about the new post.
FTestWorld Legacy; FTestWorld Bounded;
Legacy.Build(/*Seed*/1337, /*GapChunks*/2, /*bUseOperatorStack*/false); Bounded.Build(/*Seed*/1337, /*GapChunks*/2);
FTestWorld StackWorld;
StackWorld.Build(/*Seed*/1337, /*GapChunks*/2, /*bUseOperatorStack*/true);
FTestWorld Unbounded; FTestWorld Unbounded;
Unbounded.Build(/*Seed*/1337, /*GapChunks*/2, /*bUseOperatorStack*/false); Unbounded.Build(/*Seed*/1337, /*GapChunks*/2);
if (!Legacy.IsValid() || !StackWorld.IsValid() || !Unbounded.IsValid()) if (!Bounded.IsValid() || !Unbounded.IsValid())
{ {
AddError(Legacy.IsValid() && StackWorld.IsValid() && Unbounded.IsValid() AddError(!Bounded.IsValid() ? Bounded.WhyInvalid() : Unbounded.WhyInvalid());
? TEXT("Unexpected edge-seal fixture state")
: (!Legacy.IsValid() ? Legacy.WhyInvalid()
: (!StackWorld.IsValid() ? StackWorld.WhyInvalid() : Unbounded.WhyInvalid())));
return false; return false;
} }
SetEdgeSettings(Legacy, TestWorldRadius); SetEdgeSettings(Bounded, TestWorldRadius);
SetEdgeSettings(StackWorld, TestWorldRadius);
SetEdgeSettings(Unbounded, 0.0f); SetEdgeSettings(Unbounded, 0.0f);
//========================================================================== //==========================================================================
@@ -103,12 +97,12 @@ bool FVoxelForgeWorldEdgeSealTest::RunTest(const FString& Parameters)
for (int32 Slot = 0; Slot < 8; ++Slot) for (int32 Slot = 0; Slot < 8; ++Slot)
{ {
int32 TopZ = 0, BottomZ = 0; int32 TopZ = 0, BottomZ = 0;
if (Legacy.GetSlotVoxelZRange(Slot, TopZ, BottomZ)) if (Bounded.GetSlotVoxelZRange(Slot, TopZ, BottomZ))
{ {
StrateZ.Add((float)((TopZ + BottomZ) / 2)); StrateZ.Add((float)((TopZ + BottomZ) / 2));
} }
} }
StrateZ.Add(Legacy.MidVoxelZ()); StrateZ.Add(Bounded.MidVoxelZ());
int32 NumPositiveSamples = 0; int32 NumPositiveSamples = 0;
for (const FVector& XY : FarXY) for (const FVector& XY : FarXY)
@@ -117,8 +111,7 @@ bool FVoxelForgeWorldEdgeSealTest::RunTest(const FString& Parameters)
{ {
const FVector P(XY.X, XY.Y, Z); const FVector P(XY.X, XY.Y, Z);
++NumPositiveSamples; ++NumPositiveSamples;
if (!IsSolidMC(*Legacy.Generator, P)) Fail(TEXT("positive-legacy")); if (!IsSolidMC(*Bounded.Generator, P)) Fail(TEXT("positive"));
if (!IsSolidMC(*StackWorld.Generator, P)) Fail(TEXT("positive-opstack"));
} }
} }
@@ -138,7 +131,7 @@ bool FVoxelForgeWorldEdgeSealTest::RunTest(const FString& Parameters)
for (const float Z : StrateZ) for (const float Z : StrateZ)
{ {
const FVector P(XY.X, XY.Y, Z); const FVector P(XY.X, XY.Y, Z);
const float BoundedD = Legacy.Generator->GetDensityAt(P.X, P.Y, P.Z); const float BoundedD = Bounded.Generator->GetDensityAt(P.X, P.Y, P.Z);
const float UnboundedD = Unbounded.Generator->GetDensityAt(P.X, P.Y, P.Z); const float UnboundedD = Unbounded.Generator->GetDensityAt(P.X, P.Y, P.Z);
++NumInteriorComparisons; ++NumInteriorComparisons;
if (!BitEqual(BoundedD, UnboundedD)) Fail(TEXT("interior-not-no-op")); if (!BitEqual(BoundedD, UnboundedD)) Fail(TEXT("interior-not-no-op"));
@@ -193,7 +186,7 @@ bool FVoxelForgeWorldEdgeSealTest::RunTest(const FString& Parameters)
// CLASSIFYBOX — force boxes in the ramp and beyond the radius, then brute-force every voxel. // CLASSIFYBOX — force boxes in the ramp and beyond the radius, then brute-force every voxel.
//========================================================================== //==========================================================================
int32 TopShaftZ = 0, BottomShaftZ = 0; int32 TopShaftZ = 0, BottomShaftZ = 0;
if (!StackWorld.GetSlotVoxelZRange(FTestWorld::SlotVerticalShafts, TopShaftZ, BottomShaftZ)) if (!Bounded.GetSlotVoxelZRange(FTestWorld::SlotVerticalShafts, TopShaftZ, BottomShaftZ))
{ {
Fail(TEXT("missing-shaft-slot")); Fail(TEXT("missing-shaft-slot"));
} }
@@ -218,17 +211,17 @@ bool FVoxelForgeWorldEdgeSealTest::RunTest(const FString& Parameters)
}; };
const int32 MidChunkZ = FMath::FloorToInt((BoxZ0 + BoxZ1) * 0.5f / CHUNK_SIZE); const int32 MidChunkZ = FMath::FloorToInt((BoxZ0 + BoxZ1) * 0.5f / CHUNK_SIZE);
const FVerticalShaftParams ShaftParams = StackWorld.StrateManager->GetVerticalShaftParamsForChunk( const FVerticalShaftParams ShaftParams = Bounded.StrateManager->GetVerticalShaftParamsForChunk(
FIntVector(0, 0, MidChunkZ)); FIntVector(0, 0, MidChunkZ));
FVoxelOpStack Stack; FVoxelOpStack Stack;
VoxelDensityOps::BuildVerticalShaftStack( VoxelDensityOps::BuildVerticalShaftStack(
Stack, ShaftParams, StackWorld.Settings->Seed, Stack, ShaftParams, Bounded.Settings->Seed,
StackWorld.Generator->OriginSpineRadius, StackWorld.StrateManager.Get()); Bounded.Generator->OriginSpineRadius, Bounded.StrateManager.Get());
FVoxelOpContext Ctx; FVoxelOpContext Ctx;
Ctx.ChunkCoord = FIntVector(0, 0, MidChunkZ); Ctx.ChunkCoord = FIntVector(0, 0, MidChunkZ);
Ctx.Seed = (uint32)StackWorld.Settings->Seed; Ctx.Seed = (uint32)Bounded.Settings->Seed;
Ctx.LayoutVersion = StackWorld.StrateManager->GetLayoutVersion(); Ctx.LayoutVersion = Bounded.StrateManager->GetLayoutVersion();
Ctx.StrateTopWorldZ = ShaftParams.StrateTopWorldZ; Ctx.StrateTopWorldZ = ShaftParams.StrateTopWorldZ;
Ctx.StrateBottomWorldZ = ShaftParams.StrateBottomWorldZ; Ctx.StrateBottomWorldZ = ShaftParams.StrateBottomWorldZ;
Ctx.WorldRadiusVoxels = TestWorldRadius; Ctx.WorldRadiusVoxels = TestWorldRadius;
@@ -255,14 +248,14 @@ bool FVoxelForgeWorldEdgeSealTest::RunTest(const FString& Parameters)
for (int32 X = X0; X <= X1; ++X) for (int32 X = X0; X <= X1; ++X)
{ {
++NumVoxelsChecked; ++NumVoxelsChecked;
if (!IsSolidMC(*StackWorld.Generator, FVector((float)X, (float)Y, (float)Z))) if (!IsSolidMC(*Bounded.Generator, FVector((float)X, (float)Y, (float)Z)))
Fail(TEXT("classifybox-false-allsolid")); Fail(TEXT("classifybox-false-allsolid"));
} }
} }
} }
// The T1.d path must be able to skip an edge tile without knowing which Z category it hits. // The T1.d path must be able to skip an edge tile without knowing which Z category it hits.
const EVoxelTileClass EdgeTile = StackWorld.Generator->ClassifyTile( const EVoxelTileClass EdgeTile = Bounded.Generator->ClassifyTile(
FIntVector(320, 0, BottomShaftZ + 8), 1, 8); FIntVector(320, 0, BottomShaftZ + 8), 1, 8);
int32 NumTilesProved = (EdgeTile == EVoxelTileClass::AllSolid) ? 1 : 0; int32 NumTilesProved = (EdgeTile == EVoxelTileClass::AllSolid) ? 1 : 0;
if (EdgeTile != EVoxelTileClass::AllSolid) Fail(TEXT("classifytile-missed-edge")); if (EdgeTile != EVoxelTileClass::AllSolid) Fail(TEXT("classifytile-missed-edge"));
@@ -283,9 +276,7 @@ bool FVoxelForgeWorldEdgeSealTest::RunTest(const FString& Parameters)
// coverage only: the synthetic helper case above is the hard assertion even if landing-site // coverage only: the synthetic helper case above is the hard assertion even if landing-site
// adjustment moves every generated mouth out of this small test rim. // adjustment moves every generated mouth out of this small test rim.
FTestWorld GeneratedPassageWorld; FTestWorld GeneratedPassageWorld;
GeneratedPassageWorld.Build(/*Seed*/1337, /*GapChunks*/2, /*bUseOperatorStack*/false); GeneratedPassageWorld.Build(/*Seed*/1337, /*GapChunks*/2);
FTestWorld GeneratedPassageStackWorld;
GeneratedPassageStackWorld.Build(/*Seed*/1337, /*GapChunks*/2, /*bUseOperatorStack*/true);
auto CheckGeneratedPassages = [&](FTestWorld& World, const TCHAR* FailureKind) auto CheckGeneratedPassages = [&](FTestWorld& World, const TCHAR* FailureKind)
{ {
if (!World.IsValid()) if (!World.IsValid())
@@ -310,7 +301,6 @@ bool FVoxelForgeWorldEdgeSealTest::RunTest(const FString& Parameters)
} }
}; };
CheckGeneratedPassages(GeneratedPassageWorld, TEXT("generated-passage-breaches-rim")); CheckGeneratedPassages(GeneratedPassageWorld, TEXT("generated-passage-breaches-rim"));
CheckGeneratedPassages(GeneratedPassageStackWorld, TEXT("generated-opstack-passage-breaches-rim"));
// One sorted summary block; individual samples intentionally never log. // One sorted summary block; individual samples intentionally never log.
AddInfo(FString::Printf( AddInfo(FString::Printf(
@@ -319,7 +309,7 @@ bool FVoxelForgeWorldEdgeSealTest::RunTest(const FString& Parameters)
TEXT(" classify_tile_proved=%d\n") TEXT(" classify_tile_proved=%d\n")
TEXT(" interior_comparisons=%d\n") TEXT(" interior_comparisons=%d\n")
TEXT(" near_rim_passage_checks=%d\n") TEXT(" near_rim_passage_checks=%d\n")
TEXT(" positive_sample_points=%d (checked in both paths)\n") TEXT(" positive_sample_points=%d\n")
TEXT(" rim_directions=%d\n") TEXT(" rim_directions=%d\n")
TEXT(" rim_max_adjacent_solidness_jump=%.6f (threshold=%.6f)\n") TEXT(" rim_max_adjacent_solidness_jump=%.6f (threshold=%.6f)\n")
TEXT(" voxels_checked=%lld\n") TEXT(" voxels_checked=%lld\n")
+58 -191
View File
@@ -16,7 +16,7 @@
#include "VoxelWormField.h" #include "VoxelWormField.h"
#include "VoxelDensityAblation.h" // fingerprinted development-only stage measurements #include "VoxelDensityAblation.h" // fingerprinted development-only stage measurements
#include "VoxelDensityPrimitives.h" // spine / seals / passage — shared with the operator stack #include "VoxelDensityPrimitives.h" // spine / seals / passage — shared with the operator stack
#include "VoxelDensityOpStack.h" // the opt-in per-strate operator stack #include "VoxelDensityOpStack.h" // the per-strate operator stack
#include "VoxelHeightOp.h" // IVoxelBiomeField — the adapter below implements it #include "VoxelHeightOp.h" // IVoxelBiomeField — the adapter below implements it
#include "VoxelStats.h" #include "VoxelStats.h"
#include "VoxelDensityProfile.h" #include "VoxelDensityProfile.h"
@@ -669,9 +669,9 @@ struct FVoxelDensityBlockSession
return; return;
} }
// Allocate lazily on the first operator-stack sample. Legacy archetypes still use the // Allocate lazily on the first operator-stack sample. Chunks that never reach a stack
// ordinary scalar generator and should not pay for a block scratch lattice merely because // (gaps, out-of-layout, degenerate strates) use the ordinary scalar path and should not
// the mesher opened the scoped hand-off. // pay for a block scratch lattice merely because the mesher opened the scoped hand-off.
BlockScratch.Reset(); BlockScratch.Reset();
} }
@@ -2549,12 +2549,11 @@ namespace
/** /**
* Construit la pile de cet archétype dans `OutStack` et remplit les bornes Z de `OutCtx`. * Construit la pile de cet archétype dans `OutStack` et remplit les bornes Z de `OutCtx`.
* *
* @return false quand la pile NE DOIT PAS être utilisée — archétype non porté, params absents, * @return false quand la pile NE DOIT PAS être utilisée — archétype inconnu, params absents,
* ou **strate dégénérée**. Ce dernier cas n'est pas de la prudence : cinq fonctions * ou **strate dégénérée** (hauteur nulle). La pile n'a pas d'early-out pour ce dernier
* d'archétype court-circuitent sur `return 1.0f` (= air) quand la hauteur est nulle, * cas, par conception : l'appelant rend de l'air (`GetDensityAt`) ou refuse de prouver
* et la pile n'a pas cet early-out, par conception. L'appelant retombe sur le `switch`, * la tuile (`ClassifyTile`). (`GetDensityWithParams` n'a aucun early-out de ce genre :
* qui EST le comportement de référence. (`GetDensityWithParams`, lui, n'a aucun * TunnelNetwork/Underwater n'ont donc pas cette garde.)
* early-out de ce genre : TunnelNetwork/Underwater n'ont donc pas cette garde.)
*/ */
bool VF_BuildOpStackForChunk(ECaveGeneratorType Type, FVoxelStackParamRefs& Refs, bool VF_BuildOpStackForChunk(ECaveGeneratorType Type, FVoxelStackParamRefs& Refs,
int32 Seed, float SpineRadius, const UVoxelStrateManager* SM, int32 Seed, float SpineRadius, const UVoxelStrateManager* SM,
@@ -2572,8 +2571,8 @@ namespace
case ECaveGeneratorType::FlatPlain: case ECaveGeneratorType::FlatPlain:
case ECaveGeneratorType::CrystalChamber: case ECaveGeneratorType::CrystalChamber:
// UN SEUL cas pour les deux, comme le `switch` de production : `GetSlabDensity` ne les // UN SEUL cas pour les deux : ils ne diffèrent que par leurs valeurs par défaut.
// distingue pas non plus. Voir BuildSlabStack. // Voir BuildSlabStack.
if (!Refs.Slab) { return false; } if (!Refs.Slab) { return false; }
if (Refs.Slab->StrateTopWorldZ - Refs.Slab->StrateBottomWorldZ <= 0.0f) { return false; } if (Refs.Slab->StrateTopWorldZ - Refs.Slab->StrateBottomWorldZ <= 0.0f) { return false; }
OutCtx.StrateTopWorldZ = Refs.Slab->StrateTopWorldZ; OutCtx.StrateTopWorldZ = Refs.Slab->StrateTopWorldZ;
@@ -2621,9 +2620,8 @@ namespace
return true; return true;
default: default:
// `UsesOperatorStackForChunk` ne rend true que pour les archétypes portés (les 8), donc // `UsesOperatorStackForChunk` ne rend true que pour les 8 archétypes, donc on ne devrait
// on ne devrait jamais arriver ici. Si ça arrive : retomber sur le `switch` plutôt que // jamais arriver ici (valeur d'enum invalide).
// générer du vide — un monde faux est pire qu'un monde non porté.
return false; return false;
} }
} }
@@ -2842,11 +2840,6 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
thread_local FBiomeContext CP_BiomeCtx; thread_local FBiomeContext CP_BiomeCtx;
thread_local FChunkBiomeCache CP_BiomeCache; thread_local FChunkBiomeCache CP_BiomeCache;
thread_local TArray<FSurfaceGenerationParams> CP_SurfaceBiomeParams; thread_local TArray<FSurfaceGenerationParams> CP_SurfaceBiomeParams;
// T1.a per-column surface cache: GSurfColCache (file-scope, shared with ClassifyTile).
// Discriminates the surface cache by strate: same strate ⇒ identical heightfield params ⇒ columns
// are shareable across the whole vertical chunk stack. Taken from the params themselves
// (StrateBottomWorldZ is unique per stacked strate) so the key can never disagree with CP_Surface.
thread_local int32 CP_StrateKey = MIN_int32;
// AUDIT C2 — la clé DOIT contenir la version de layout, pas seulement le chunk. Après un // AUDIT C2 — la clé DOIT contenir la version de layout, pas seulement le chunk. Après un
// RebuildStrates / une édition à chaud, StrateManager reconstruit le layout et bumpe la // RebuildStrates / une édition à chaud, StrateManager reconstruit le layout et bumpe la
// version ; un worker dont CP_Chunk vaut encore ce chunk sauterait le refetch et // version ; un worker dont CP_Chunk vaut encore ce chunk sauterait le refetch et
@@ -2860,8 +2853,8 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
// when the other context misses, this bit must force the rebuild guard below to run. // when the other context misses, this bit must force the rebuild guard below to run.
thread_local bool CP_UsesTileCacheWindow = false; thread_local bool CP_UsesTileCacheWindow = false;
// OPSTACK — la pile d'opérateurs, construite dans le MÊME bloc de refetch que les // OPSTACK — la pile d'opérateurs, construite dans le MÊME bloc de refetch que les
// params (donc même clé owner+chunk+version, aucune logique d'invalidation en plus). Vide tant que // params (donc même clé owner+chunk+version, aucune logique d'invalidation en plus). Vide
// la strate n'a pas coché `bUseOperatorStack` ET que son archétype n'est pas porté. // seulement pour une strate dégénérée (hauteur nulle).
thread_local FVoxelOpStack CP_OpStack; thread_local FVoxelOpStack CP_OpStack;
thread_local bool CP_UseOpStack = false; thread_local bool CP_UseOpStack = false;
thread_local uint64 CP_ManagerLifetimeId = 0; thread_local uint64 CP_ManagerLifetimeId = 0;
@@ -2890,8 +2883,8 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
// A coarse tile samples thousands of exact XY chunk keys, but those keys describe the // A coarse tile samples thousands of exact XY chunk keys, but those keys describe the
// same immutable stack whenever the selected archetype is operator-stack backed. Share // same immutable stack whenever the selected archetype is operator-stack backed. Share
// that prepared state by Z; the stack still queries the actual world position, and its // that prepared state by Z; the stack still queries the actual world position, and its
// room-graph cache remains keyed by the real XY search window. Legacy tunnel states keep // room-graph cache remains keyed by the real XY search window. Chunks outside the strate
// the full XYZ key because their native cache is built for one XY window. // layout (constant air) keep the full XYZ key.
const ECaveGeneratorType QueryGeneratorType = bUseChunkRoutingCache const ECaveGeneratorType QueryGeneratorType = bUseChunkRoutingCache
? CP_RouteGeneratorType ? CP_RouteGeneratorType
: StrateManager->GetGeneratorTypeForChunk(ChunkCoord); : StrateManager->GetGeneratorTypeForChunk(ChunkCoord);
@@ -3066,7 +3059,6 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
CP_Maze = StrateManager->GetMazeParamsForChunk(ChunkCoord); break; CP_Maze = StrateManager->GetMazeParamsForChunk(ChunkCoord); break;
case ECaveGeneratorType::SurfaceWorld: case ECaveGeneratorType::SurfaceWorld:
ResolveSurfaceChunkParams(ChunkCoord, CP_Surface, CP_BiomeCtx, CP_SurfaceBiomeParams); ResolveSurfaceChunkParams(ChunkCoord, CP_Surface, CP_BiomeCtx, CP_SurfaceBiomeParams);
CP_StrateKey = FMath::RoundToInt(CP_Surface.StrateBottomWorldZ);
break; break;
case ECaveGeneratorType::VerticalShafts: case ECaveGeneratorType::VerticalShafts:
CP_Vert = StrateManager->GetVerticalShaftParamsForChunk(ChunkCoord); break; CP_Vert = StrateManager->GetVerticalShaftParamsForChunk(ChunkCoord); break;
@@ -3085,28 +3077,17 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
} }
CP_Dist = StrateManager->GetDisturbanceParamsForChunk(ChunkCoord); CP_Dist = StrateManager->GetDisturbanceParamsForChunk(ChunkCoord);
// ── OPSTACK : (re)construire la pile si cette strate l'a demandée. ── // ── OPSTACK : (re)construire la pile de ce chunk. ──
// Une seule branche ajoutée au chemin densité, et elle est FROIDE : la construction est // Toute strate est générée par une pile d'opérateurs. La construction est par chunk
// par chunk (comme le refetch de params juste au-dessus), jamais par voxel. // (comme le refetch de params juste au-dessus), jamais par voxel.
CP_UseOpStack = bUseChunkRoutingCache // Every strate is generated by an operator stack. An editor composer candidate or a
? CP_RouteUsesOperatorStack // cooked-season recipe supplies its own stack; otherwise — or if that stack cannot be
: StrateManager->UsesOperatorStackForChunk(ChunkCoord); // built — the archetype's native stack is used.
CP_UseOpStack = CP_UseOpStack || CP_UseCustomRecipe;
#if WITH_EDITOR
// A composer override is already an explicit, validated stack description. It must
// be evaluated even when the authored definition had the normal operator-stack opt-in
// disabled; otherwise a multi-region candidate would be installed in the manager but
// silently fall back to the old one-archetype switch.
CP_UseOpStack = CP_UseOpStack || CP_UseComposerRegions;
#endif
if (CP_UseOpStack)
{
CP_OpStack = FVoxelOpStack(); // move-assign : libère l'ancienne pile CP_OpStack = FVoxelOpStack(); // move-assign : libère l'ancienne pile
bool bBuiltSpecialStack = false; CP_UseOpStack = false;
#if WITH_EDITOR #if WITH_EDITOR
if (CP_UseComposerRegions) if (CP_UseComposerRegions)
{ {
bBuiltSpecialStack = true;
FVoxelOpContext RegionContext; FVoxelOpContext RegionContext;
if (VF_BuildStrateRegionStack( if (VF_BuildStrateRegionStack(
CP_ComposerRegions, OriginSpineRadius, StrateManager, CP_ComposerRegions, OriginSpineRadius, StrateManager,
@@ -3118,16 +3099,16 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
RegionContext.WorldRadiusVoxels = WorldRadiusVoxels; RegionContext.WorldRadiusVoxels = WorldRadiusVoxels;
RegionContext.EdgeSealThickness = EdgeSealThickness; RegionContext.EdgeSealThickness = EdgeSealThickness;
CP_OpStack.PrepareChunk(RegionContext); CP_OpStack.PrepareChunk(RegionContext);
CP_UseOpStack = true;
} }
else else
{ {
CP_UseOpStack = false; CP_OpStack = FVoxelOpStack();
CP_UseComposerRegions = false;
} }
} }
#endif #endif
if (!bBuiltSpecialStack) if (!CP_UseOpStack && CP_UseCustomRecipe)
{
if (CP_UseCustomRecipe)
{ {
FVoxelOpContext RecipeContext; FVoxelOpContext RecipeContext;
if (VF_BuildStackFromRecipe( if (VF_BuildStackFromRecipe(
@@ -3142,15 +3123,17 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
RecipeContext.WorldRadiusVoxels = WorldRadiusVoxels; RecipeContext.WorldRadiusVoxels = WorldRadiusVoxels;
RecipeContext.EdgeSealThickness = EdgeSealThickness; RecipeContext.EdgeSealThickness = EdgeSealThickness;
CP_OpStack.PrepareChunk(RecipeContext); CP_OpStack.PrepareChunk(RecipeContext);
CP_UseOpStack = true;
} }
else else
{ {
// Import/load validates recipes before a season becomes active. Keep // Import/load validates recipes before a season becomes active. If a future
// the fallback hole-safe if a future schema changes underneath it. // schema still fails here, fall back to the archetype's native stack below.
CP_UseOpStack = false; CP_OpStack = FVoxelOpStack();
CP_UseCustomRecipe = false;
} }
} }
else if (!CP_UseOpStack)
{ {
FVoxelOpContext OpCtx; FVoxelOpContext OpCtx;
OpCtx.ChunkCoord = ChunkCoord; OpCtx.ChunkCoord = ChunkCoord;
@@ -3190,26 +3173,16 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
CP_GenType, Refs, Seed, OriginSpineRadius, CP_GenType, Refs, Seed, OriginSpineRadius,
StrateManager, CP_OpStack, OpCtx); StrateManager, CP_OpStack, OpCtx);
// Le test appelle PrepareChunk, pas la production : c'est exactement la divergence // PrepareChunk runs here exactly as in the tests, so an op that hoists real
// qui rend un opérateur vert en test et faux en jeu. Les sept `PrepareChunk` // per-chunk work behaves the same in game. `Step` reste 1 : GetDensityAt ne connaît
// actuels sont vides, donc ceci ne change RIEN aujourd'hui — c'est le point : le // pas le pas d'échantillonnage du mesher (voir le contrat T2.b dans VoxelDensityOp.h).
// premier opérateur qui hisse vraiment du travail par chunk doit trouver l'appel
// déjà là. `Step` reste 1 : GetDensityAt ne connaît pas le pas d'échantillonnage
// du mesher (voir le contrat T2.b dans VoxelDensityOp.h).
// The test calls PrepareChunk and production did not — the exact divergence that
// makes an op green in test and wrong in game. All seven bodies are empty today,
// which is the point: the first op that hoists real per-chunk work must find the
// call already here.
if (CP_UseOpStack) { CP_OpStack.PrepareChunk(OpCtx); } if (CP_UseOpStack) { CP_OpStack.PrepareChunk(OpCtx); }
} }
}
}
// The legacy path needs its prepared native tunnel-core cache. An operator-stack // An operator-stack tunnel source publishes the world-space core after EvalMC, so
// tunnel source publishes the same world-space result after EvalMC, so preparing a // preparing a second full graph/core cache here would only duplicate the work that made
// second full graph/core cache here would only duplicate the work that made coarse // coarse streaming expensive in the first place. Only an editor composer region stack,
// streaming expensive in the first place. Custom/composer paths retain the legacy // which does not publish that hand-off, still gets the prepared native core.
// cache unless their own stack supplies the hand-off.
const bool bNativeTunnelCore = !CP_UseCustomRecipe const bool bNativeTunnelCore = !CP_UseCustomRecipe
&& (CP_GenType == ECaveGeneratorType::TunnelNetwork && (CP_GenType == ECaveGeneratorType::TunnelNetwork
|| CP_GenType == ECaveGeneratorType::Underwater); || CP_GenType == ECaveGeneratorType::Underwater);
@@ -3294,12 +3267,9 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
FTunnelCoreWorldEvaluation FusedTunnelCore; FTunnelCoreWorldEvaluation FusedTunnelCore;
bool bRoomOwnsBottom = false; bool bRoomOwnsBottom = false;
bool bRoomOwnsBottomValid = false; bool bRoomOwnsBottomValid = false;
FVector NativeWarpedPosition = FVector::ZeroVector;
bool bNativeWarpedPositionValid = false;
// Le seul point d'entrée de la pile dans le chemin de production. Elle rend la convention // Le seul point d'entrée de la pile dans le chemin de production. Elle rend la convention
// MC (négatif = solide) comme les fonctions d'archétype, donc les disturbances et la couche // MC (négatif = solide), que les disturbances et la couche de diff qui suivent attendent.
// de diff qui suivent ne voient aucune différence.
if (CP_UseOpStack) if (CP_UseOpStack)
{ {
const bool bCanUseFusedEvaluator = const bool bCanUseFusedEvaluator =
@@ -3383,66 +3353,11 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
} }
} }
} }
else switch (CP_GenType)
{
case ECaveGeneratorType::FlatPlain:
case ECaveGeneratorType::CrystalChamber:
Result = GetSlabDensity(WorldX, WorldY, WorldZ, CP_Slab); break;
case ECaveGeneratorType::Maze:
Result = GetMazeDensity(WorldX, WorldY, WorldZ, CP_Maze); break;
case ECaveGeneratorType::SurfaceWorld:
{
// Integer XY (the density grid) → reuse the column down its whole Z extent (T1.a).
// Fractional XY (gradient-normal samples) → compute directly (no cache key).
if (WorldX == FMath::FloorToFloat(WorldX) && WorldY == FMath::FloorToFloat(WorldY))
{
const int32 IX = (int32)WorldX, IY = (int32)WorldY;
// XY-keyed LRU box (shared down the whole vertical strate stack). Acquire centres a box on
// the first sample so the rest of the chunk's queries — incl. the ±Step margin ring — hit.
FSurfaceColumnBox& Box = GSurfColCache.Acquire(
IX, IY, CP_StrateKey, Seed, LayoutVersion,
DensityCacheOwnerId, ManagerLifetimeId);
const int32 CI = (IY - Box.BaseY) * FSurfaceColumnBox::Dim + (IX - Box.BaseX);
if (!Box.Computed[CI])
{
ComputeSurfaceColumn(WorldX, WorldY, ChunkCoord.Z, CP_Surface, CP_BiomeCtx,
CP_SurfaceBiomeParams, CP_BiomeCache,
Box.Cols[CI].TerrainZ, Box.Cols[CI].CeilSurf,
Box.Cols[CI].OverhangAmp, Box.Cols[CI].DirX, Box.Cols[CI].DirY);
Box.Computed[CI] = true;
}
const FSurfaceColumn& Col = Box.Cols[CI];
Result = SurfaceDensityFromColumn(WorldX, WorldY, WorldZ,
Col.TerrainZ, Col.CeilSurf, Col.OverhangAmp,
Col.DirX, Col.DirY, CP_Surface);
}
else else
{ {
float TerrainZ, CeilSurf, OverhangAmp, DirX, DirY; // Only a degenerate strate (zero height) has no stack. It is open air, like the
ComputeSurfaceColumn(WorldX, WorldY, ChunkCoord.Z, CP_Surface, CP_BiomeCtx, // archetype functions the stack replaced.
CP_SurfaceBiomeParams, CP_BiomeCache, TerrainZ, CeilSurf, Result = 1.0f;
OverhangAmp, DirX, DirY);
Result = SurfaceDensityFromColumn(WorldX, WorldY, WorldZ,
TerrainZ, CeilSurf, OverhangAmp, DirX, DirY, CP_Surface);
}
break;
}
case ECaveGeneratorType::VerticalShafts:
Result = GetVerticalShaftDensity(WorldX, WorldY, WorldZ, CP_Vert); break;
case ECaveGeneratorType::FloatingIslands:
Result = GetFloatingIslandDensity(WorldX, WorldY, WorldZ, CP_Float); break;
case ECaveGeneratorType::Underwater:
case ECaveGeneratorType::TunnelNetwork:
default:
// Underwater shares tunnel rock (water table is a render-side overlay).
Result = GetDensityWithParams(WorldX, WorldY, WorldZ, CP_Tunnel,
CP_TunnelFP, LayoutVersion,
/*bApplyLegacyStructuralPosts=*/false,
nullptr, false, &bRoomOwnsBottom,
&NativeWarpedPosition);
bRoomOwnsBottomValid = true;
bNativeWarpedPositionValid = true;
break;
} }
DensityCoreTimer.End(); DensityCoreTimer.End();
@@ -3482,21 +3397,13 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
{ {
VoxelDensityProfile::FScopedTimer ProfileTimer( VoxelDensityProfile::FScopedTimer ProfileTimer(
VoxelDensityProfile::EBucket::TunnelCorePosts); VoxelDensityProfile::EBucket::TunnelCorePosts);
FVector RoomQueryPosition; FVector RoomQueryPosition(WorldX, WorldY, WorldZ);
if (bNativeWarpedPositionValid)
{
RoomQueryPosition = NativeWarpedPosition;
}
else
{
RoomQueryPosition = FVector(WorldX, WorldY, WorldZ);
if (CP_Tunnel.VerticalScale != 1.0f && CP_Tunnel.VerticalScale > 0.0f) if (CP_Tunnel.VerticalScale != 1.0f && CP_Tunnel.VerticalScale > 0.0f)
{ {
RoomQueryPosition.Z = WorldZ / CP_Tunnel.VerticalScale; RoomQueryPosition.Z = WorldZ / CP_Tunnel.VerticalScale;
} }
RoomQueryPosition = VoxelCaveMorphology::ApplyCaveWarp( RoomQueryPosition = VoxelCaveMorphology::ApplyCaveWarp(
RoomQueryPosition, CP_Tunnel, static_cast<uint32>(Seed)); RoomQueryPosition, CP_Tunnel, static_cast<uint32>(Seed));
}
PreDisturbanceTunnelCore = VoxelCaveMorphology::EvaluateTunnelCoreWorld( PreDisturbanceTunnelCore = VoxelCaveMorphology::EvaluateTunnelCoreWorld(
WorldX, WorldY, WorldZ, ActiveTunnelCoreCache->Cache, nullptr, WorldX, WorldY, WorldZ, ActiveTunnelCoreCache->Cache, nullptr,
VoxelGenLOD::ShouldUseSpatialIndex( VoxelGenLOD::ShouldUseSpatialIndex(
@@ -3520,7 +3427,7 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
// A disturbance is allowed to add visual detail, but it must not refill the landing's // A disturbance is allowed to add visual detail, but it must not refill the landing's
// measured air volume or turn its support slab back into a hole. Reassert the landing // measured air volume or turn its support slab back into a hole. Reassert the landing
// air first, then the structural floor, in MC space before the final XY seal. The two // air first, then the structural floor, in MC space before the final XY seal. The two
// calls use the same landing geometry already evaluated by the legacy and op-stack post. // calls use the same landing geometry already evaluated by the op-stack structural post.
VoxelDensityProfile::FScopedTimer DensityStructuralPostsTimer( VoxelDensityProfile::FScopedTimer DensityStructuralPostsTimer(
VoxelDensityProfile::EBucket::DensityStructuralPosts); VoxelDensityProfile::EBucket::DensityStructuralPosts);
VoxelDensityProfile::FScopedTimer StructuralTailTimer( VoxelDensityProfile::FScopedTimer StructuralTailTimer(
@@ -3695,21 +3602,13 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
{ {
VoxelDensityProfile::FScopedTimer ProfileTimer( VoxelDensityProfile::FScopedTimer ProfileTimer(
VoxelDensityProfile::EBucket::TunnelCorePosts); VoxelDensityProfile::EBucket::TunnelCorePosts);
FVector RoomQueryPosition; FVector RoomQueryPosition(WorldX, WorldY, WorldZ);
if (bNativeWarpedPositionValid)
{
RoomQueryPosition = NativeWarpedPosition;
}
else
{
RoomQueryPosition = FVector(WorldX, WorldY, WorldZ);
if (CP_Tunnel.VerticalScale != 1.0f && CP_Tunnel.VerticalScale > 0.0f) if (CP_Tunnel.VerticalScale != 1.0f && CP_Tunnel.VerticalScale > 0.0f)
{ {
RoomQueryPosition.Z = WorldZ / CP_Tunnel.VerticalScale; RoomQueryPosition.Z = WorldZ / CP_Tunnel.VerticalScale;
} }
RoomQueryPosition = VoxelCaveMorphology::ApplyCaveWarp( RoomQueryPosition = VoxelCaveMorphology::ApplyCaveWarp(
RoomQueryPosition, CP_Tunnel, static_cast<uint32>(Seed)); RoomQueryPosition, CP_Tunnel, static_cast<uint32>(Seed));
}
TunnelCore = VoxelCaveMorphology::EvaluateTunnelCoreWorld( TunnelCore = VoxelCaveMorphology::EvaluateTunnelCoreWorld(
WorldX, WorldY, WorldZ, ActiveTunnelCoreCache->Cache, WorldX, WorldY, WorldZ, ActiveTunnelCoreCache->Cache,
nullptr, nullptr,
@@ -6823,7 +6722,7 @@ EVoxelTileClass UVoxelGenerator::ClassifyTileUncached(
bool bCanSolid = true; // "tout le treillis est solide" encore prouvable bool bCanSolid = true; // "tout le treillis est solide" encore prouvable
bool bCanAir = true; // "tout le treillis est air" encore prouvable bool bCanAir = true; // "tout le treillis est air" encore prouvable
// ── Catégorisation par Z du treillis : gap bedrock = solide ; hors layout = air constant ; // ── Catégorisation par Z du treillis : gap bedrock = solide ; hors layout = air constant ;
// SurfaceWorld = test colonne ; un slot cave opt-in = verdict de pile sur sa sous-boîte. ── // SurfaceWorld = test colonne ; un slot cave = verdict de pile sur sa sous-boîte. ──
struct FSurfSlot struct FSurfSlot
{ {
int32 BotChunkZ = INT32_MAX; // identité du slot (borne basse de la strate, en chunks) int32 BotChunkZ = INT32_MAX; // identité du slot (borne basse de la strate, en chunks)
@@ -6904,7 +6803,7 @@ EVoxelTileClass UVoxelGenerator::ClassifyTileUncached(
// EST FAUX) et `IsGapChunk` rend false au-dessus du sommet (« open air, NOT a gap »). // EST FAUX) et `IsGapChunk` rend false au-dessus du sommet (« open air, NOT a gap »).
// Résultat : chaque tuile touchant l'air libre au-dessus du monde entrait dans la // Résultat : chaque tuile touchant l'air libre au-dessus du monde entrait dans la
// BRANCHE DE CAVE, n'y trouvait aucun slot, et abandonnait — mesuré en jeu à 83 % des // BRANCHE DE CAVE, n'y trouvait aucun slot, et abandonnait — mesuré en jeu à 83 % des
// tuiles classées (`Cave Bail Not Op Stack No Layout` = 1.58 / 1.90). // tuiles classées.
// //
// La vérité est dans `GetGenerationParams` : hors layout il rend `BaseDensity = -1`, // La vérité est dans `GetGenerationParams` : hors layout il rend `BaseDensity = -1`,
// `RoomDensity = 0`, `WormStrength = 0` — un champ CONSTANT, donc de l'air, sans salle // `RoomDensity = 0`, `WormStrength = 0` — un champ CONSTANT, donc de l'air, sans salle
@@ -6987,38 +6886,11 @@ EVoxelTileClass UVoxelGenerator::ClassifyTileUncached(
// le pliage générique de la pile — mais SEULEMENT sous des conditions vérifiables, // le pliage générique de la pile — mais SEULEMENT sous des conditions vérifiables,
// parce qu'un faux verdict ici est un trou (pas de géométrie, pas de collision). // parce qu'un faux verdict ici est un trou (pas de géométrie, pas de collision).
// //
// Condition 1 : la strate doit RÉELLEMENT être générée par la pile. Sinon on // Condition 1 : le slot doit avoir une pile (une définition d'archétype connue).
// classerait un champ que le mesher ne produira pas. C'est le même drapeau, lu au // Sinon on classerait un champ que le mesher ne produira pas.
// même endroit, que `GetDensityAt`.
if (!StrateManager->UsesOperatorStackForChunk(CC)) if (!StrateManager->UsesOperatorStackForChunk(CC))
{ {
// Attribution DIAGNOSTIQUE uniquement : distingue une strate cave entièrement INC_DWORD_STAT(STAT_VoxelForgeCaveBailNoStack);
// désactivée d'une tuile de frontière qui a rencontré un slot désactivé avant
// la garde « slot différent » ci-dessous.
// On résout les bornes APRÈS l'échec du même prédicat ; elles ne changent ni
// la condition, ni le point de retour, ni le verdict.
// Diagnostic attribution only: separates a wholly disabled cave slot from a
// boundary tile that met a disabled slot before the different-slot guard
// below. Resolve bounds only after the same predicate fails; classification
// control flow and return value stay unchanged.
int32 FailedTopCZ = 0, FailedBotCZ = 0;
if (!StrateManager->GetStrateChunkZBounds(ChunkZ, FailedTopCZ, FailedBotCZ))
{
INC_DWORD_STAT(STAT_VoxelForgeCaveBailNotOpStackNoLayout);
}
else
{
const int32 TileMinCZ = FloorDivC(MinZ, CHUNK_SIZE);
const int32 TileMaxCZ = FloorDivC(MaxZ, CHUNK_SIZE);
if (TileMinCZ >= FailedBotCZ && TileMaxCZ <= FailedTopCZ)
{
INC_DWORD_STAT(STAT_VoxelForgeCaveBailNotOpStackSoleSlot);
}
else
{
INC_DWORD_STAT(STAT_VoxelForgeCaveBailNotOpStackBoundaryTile);
}
}
return EVoxelTileClass::Mixed; return EVoxelTileClass::Mixed;
} }
@@ -7223,16 +7095,11 @@ EVoxelTileClass UVoxelGenerator::ClassifyTileUncached(
return EVoxelTileClass::Mixed; // la boîte déborde sur un autre archétype return EVoxelTileClass::Mixed; // la boîte déborde sur un autre archétype
} }
// Le drapeau doit tenir sur TOUS les chunks de la boîte, pas seulement sur celui qui a // Every chunk of the box must have a stack, not only the one that triggered the
// déclenché la tentative : un seul chunk hors pile invaliderait le verdict. // attempt (redundant with the Z pass today, kept hole-safe).
if (!StrateManager->UsesOperatorStackForChunk(CC)) if (!StrateManager->UsesOperatorStackForChunk(CC))
{ {
// Le passage Z précédent a déjà accepté l'unique slot cave. Avec le layout actuel INC_DWORD_STAT(STAT_VoxelForgeCaveBailNoStack);
// (prédicat indépendant de X/Y), ce recheck est redondant ; un hit nomme donc
// précisément cette garde tardive au lieu d'être agrégé aux opt-ins désactivés.
// The prior Z pass already accepted the sole cave slot. With the current X/Y-
// independent predicate this recheck is redundant, so attribute it separately.
INC_DWORD_STAT(STAT_VoxelForgeCaveBailNotOpStackRecheck);
return EVoxelTileClass::Mixed; return EVoxelTileClass::Mixed;
} }
@@ -7331,8 +7198,8 @@ EVoxelTileClass UVoxelGenerator::ClassifyTileUncached(
if (!VF_BuildOpStackForChunk(CaveType, Refs, Seed, OriginSpineRadius, if (!VF_BuildOpStackForChunk(CaveType, Refs, Seed, OriginSpineRadius,
StrateManager, TileStack, OpCtx)) StrateManager, TileStack, OpCtx))
{ {
// Strate dégénérée ou archétype non porté : `GetDensityAt` retomberait sur le `switch`, // Strate dégénérée : `GetDensityAt` y rend de l'air sans pile, donc la pile ne décrit
// donc la pile ne décrit pas ce que le mesher verra. Aucun verdict. // pas ce que le mesher verra. Aucun verdict.
INC_DWORD_STAT(STAT_VoxelForgeCaveBailNoStack); INC_DWORD_STAT(STAT_VoxelForgeCaveBailNoStack);
return EVoxelTileClass::Mixed; return EVoxelTileClass::Mixed;
} }
-4
View File
@@ -10,10 +10,6 @@ DEFINE_STAT(STAT_VoxelForgeTilesSkippedAllAir);
DEFINE_STAT(STAT_VoxelForgeTilesMeshed); DEFINE_STAT(STAT_VoxelForgeTilesMeshed);
DEFINE_STAT(STAT_VoxelForgeTilesOpStackSolid); DEFINE_STAT(STAT_VoxelForgeTilesOpStackSolid);
DEFINE_STAT(STAT_VoxelForgeTilesOpStackAir); DEFINE_STAT(STAT_VoxelForgeTilesOpStackAir);
DEFINE_STAT(STAT_VoxelForgeCaveBailNotOpStackSoleSlot);
DEFINE_STAT(STAT_VoxelForgeCaveBailNotOpStackBoundaryTile);
DEFINE_STAT(STAT_VoxelForgeCaveBailNotOpStackNoLayout);
DEFINE_STAT(STAT_VoxelForgeCaveBailNotOpStackRecheck);
DEFINE_STAT(STAT_VoxelForgeCaveMixOutOfLayout); DEFINE_STAT(STAT_VoxelForgeCaveMixOutOfLayout);
DEFINE_STAT(STAT_VoxelForgeCaveMixGap); DEFINE_STAT(STAT_VoxelForgeCaveMixGap);
DEFINE_STAT(STAT_VoxelForgeCaveMixSurfaceWorld); DEFINE_STAT(STAT_VoxelForgeCaveMixSurfaceWorld);
@@ -2,7 +2,6 @@
// Runtime strate layout generation and queries. // Runtime strate layout generation and queries.
#include "VoxelStrateManager.h" #include "VoxelStrateManager.h"
#include "CoreGlobals.h" // GIsAutomationTesting — the opt-in diagnostic stays quiet under tests
#include "VoxelSettings.h" #include "VoxelSettings.h"
#include "VoxelSeasonAsset.h" #include "VoxelSeasonAsset.h"
#include "VoxelTypes.h" // For CHUNK_SIZE, VOXEL_SIZE, WorldToChunkCoord #include "VoxelTypes.h" // For CHUNK_SIZE, VOXEL_SIZE, WorldToChunkCoord
@@ -1334,7 +1333,6 @@ bool UVoxelStrateManager::Initialize(UVoxelSettings* Settings, int32 WorldSeed)
} }
Slot.Definition->GeneratorType = SeasonStrate.Archetype; Slot.Definition->GeneratorType = SeasonStrate.Archetype;
Slot.Definition->bUseOperatorStack = SeasonStrate.bUsesRecipe;
Slot.Definition->StrateHeightInChunks = SeasonStrate.HeightInChunks; Slot.Definition->StrateHeightInChunks = SeasonStrate.HeightInChunks;
Slot.Definition->GenerationParams = SeasonStrate.Params.TunnelNetworkParams; Slot.Definition->GenerationParams = SeasonStrate.Params.TunnelNetworkParams;
Slot.Definition->SlabParams = SeasonStrate.Params.SlabParams; Slot.Definition->SlabParams = SeasonStrate.Params.SlabParams;
@@ -1401,10 +1399,9 @@ bool UVoxelStrateManager::Initialize(UVoxelSettings* Settings, int32 WorldSeed)
: FString::Printf(TEXT("Value_%d"), static_cast<int32>(Slot.Definition->GeneratorType)); : FString::Printf(TEXT("Value_%d"), static_cast<int32>(Slot.Definition->GeneratorType));
VoxelForgeStartupTrace::RecordEvent(TEXT("strate_slot"), FString::Printf( VoxelForgeStartupTrace::RecordEvent(TEXT("strate_slot"), FString::Printf(
TEXT("\"index\":%d,\"definition\":\"%s\",\"archetype\":\"%s\",\"top_chunk_z\":%d," TEXT("\"index\":%d,\"definition\":\"%s\",\"archetype\":\"%s\",\"top_chunk_z\":%d,"
"\"bottom_chunk_z\":%d,\"height_chunks\":%d,\"operator_stack\":%d"), "\"bottom_chunk_z\":%d,\"height_chunks\":%d"),
Slot.StrateIndex, *DefinitionPath, *ArchetypeName, Slot.StrateIndex, *DefinitionPath, *ArchetypeName,
Slot.TopChunkZ, Slot.BottomChunkZ, Slot.HeightInChunks, Slot.TopChunkZ, Slot.BottomChunkZ, Slot.HeightInChunks));
Slot.Definition->bUseOperatorStack ? 1 : 0));
} }
UE_LOG(LogTemp, Log, TEXT("[StrateManager] Strate %d: '%s' | Z chunks [%d to %d] | %d chunks tall"), UE_LOG(LogTemp, Log, TEXT("[StrateManager] Strate %d: '%s' | Z chunks [%d to %d] | %d chunks tall"),
@@ -1415,79 +1412,6 @@ bool UVoxelStrateManager::Initialize(UVoxelSettings* Settings, int32 WorldSeed)
Slot.HeightInChunks); Slot.HeightInChunks);
} }
// Diagnostic de configuration, une seule fois par construction de layout. SurfaceWorld est
// volontairement exclu : son chemin T1.d exact-lattice ne dépend pas de ce drapeau.
// Configuration diagnostic once per layout build. SurfaceWorld is deliberately excluded:
// its exact-lattice T1.d path does not depend on this flag.
int32 NumCaveSlots = 0;
int32 NumOperatorStackDisabledCaves = 0;
for (const FStrateSlot& Slot : StrateLayout)
{
const ECaveGeneratorType SlotArchetype = bUseSeason
? SeasonStrates[Slot.StrateIndex].Archetype : Slot.Definition->GeneratorType;
if (!Slot.Definition || SlotArchetype == ECaveGeneratorType::SurfaceWorld)
{
continue;
}
++NumCaveSlots;
const bool bUsesStack = bUseSeason
? SeasonStrates[Slot.StrateIndex].bUsesRecipe : Slot.Definition->bUseOperatorStack;
if (!bUsesStack)
{
++NumOperatorStackDisabledCaves;
}
}
// ⚠️ WARNING EN ÉDITEUR/JEU, JAMAIS EN TEST. Les tests `Determinism.*` construisent
// DÉLIBÉRÉMENT un monde non opt-in — c'est leur oracle de comparaison — et le framework
// d'automatisation compte un Warning comme un échec. Un diagnostic ne doit pas casser la suite
// qu'il est censé éclairer. Le message reste écrit UNE fois : seule la verbosité change.
// Warning in editor/game where it is actionable, never in tests: the Determinism.* tests build
// a non-opted-in world ON PURPOSE as their comparison oracle, and the automation framework
// treats a Warning as a failure. One message, two verbosities.
const bool bQuietDiagnostic = GIsAutomationTesting;
if (NumOperatorStackDisabledCaves > 0)
{
const FString Summary = FString::Printf(
TEXT("[StrateManager] Operator-stack opt-in: %d/%d cave layout slots have Use Operator Stack disabled. These slots cannot use operator-stack ClassifyBox/T1.d; enable the asset setting on the listed definitions if that is intended."),
NumOperatorStackDisabledCaves, NumCaveSlots);
if (bQuietDiagnostic) { UE_LOG(LogTemp, Verbose, TEXT("%s"), *Summary); }
else { UE_LOG(LogTemp, Warning, TEXT("%s"), *Summary); }
}
else
{
UE_LOG(LogTemp, Log,
TEXT("[StrateManager] Operator-stack opt-in: all %d cave layout slots have Use Operator Stack enabled."),
NumCaveSlots);
}
for (const FStrateSlot& Slot : StrateLayout)
{
const bool bSeasonStack = bUseSeason
&& SeasonStrates[Slot.StrateIndex].bUsesRecipe;
const ECaveGeneratorType SlotArchetype = bUseSeason
? SeasonStrates[Slot.StrateIndex].Archetype : Slot.Definition->GeneratorType;
if (!Slot.Definition
|| SlotArchetype == ECaveGeneratorType::SurfaceWorld
|| (bUseSeason ? bSeasonStack : Slot.Definition->bUseOperatorStack))
{
continue;
}
const FString Line = FString::Printf(
TEXT("[StrateManager] cave slot=%d name='%s' Z chunks=[%d to %d] bUseOperatorStack=false"),
Slot.StrateIndex,
*Slot.Definition->StrateName.ToString(),
Slot.TopChunkZ,
Slot.BottomChunkZ);
if (bQuietDiagnostic) { UE_LOG(LogTemp, Verbose, TEXT("%s"), *Line); }
else { UE_LOG(LogTemp, Warning, TEXT("%s"), *Line); }
}
CachedSeed = WorldSeed; CachedSeed = WorldSeed;
bOpenSurfaceEntry = Settings->bOpenSurfaceEntry; bOpenSurfaceEntry = Settings->bOpenSurfaceEntry;
OriginSpineRadius = bUseSeason OriginSpineRadius = bUseSeason
@@ -6281,30 +6205,13 @@ bool UVoxelStrateManager::UsesOperatorStackForChunk(const FIntVector& ChunkCoord
const int32 SlotIdx = FindSlotIndexForChunkZ(ChunkCoord.Z); const int32 SlotIdx = FindSlotIndexForChunkZ(ChunkCoord.Z);
if (SlotIdx < 0 || !StrateLayout[SlotIdx].Definition) { return false; } if (SlotIdx < 0 || !StrateLayout[SlotIdx].Definition) { return false; }
if (SeasonStrates.IsValidIndex(SlotIdx)) // Every strate is generated by the operator stack: authored definitions, cooked-season entries
{ // (recipe or native) and editor composer candidates alike. The slot definition carries the
// A recipe is an explicit stack opt-in. Native fixed entries retain the authored/switch // archetype in every case.
// route while still reading their season parameter vector.
return SeasonStrates[SlotIdx].bUsesRecipe;
}
#if WITH_EDITOR
if (FindComposerOverride(StrateLayout[SlotIdx].StrateIndex) != nullptr)
{
// Temporary composer candidates are measured through the stack path. Do not inherit an
// unrelated asset flag from the slot being replaced.
return true;
}
#endif
const UVoxelStrateDefinition* Def = StrateLayout[SlotIdx].Definition; const UVoxelStrateDefinition* Def = StrateLayout[SlotIdx].Definition;
if (!Def->bUseOperatorStack) { return false; }
// LA LISTE DES ARCHÉTYPES PORTÉS — le seul endroit où elle est écrite. Un archétype non porté // LA LISTE DES ARCHÉTYPES — le seul endroit où elle est écrite.
// ignore le drapeau et retombe sur le `switch`, pour qu'on puisse cocher la case sur n'importe // THE ARCHETYPE LIST, written down exactly once.
// quelle strate sans rien casser en attendant son portage.
// THE PORTED-ARCHETYPE LIST, written down exactly once. An unported archetype ignores the flag
// and falls back to the switch, so the box can be ticked anywhere without breaking anything.
switch (Def->GeneratorType) switch (Def->GeneratorType)
{ {
case ECaveGeneratorType::Maze: return true; case ECaveGeneratorType::Maze: return true;
@@ -6327,16 +6234,13 @@ bool UVoxelStrateManager::UsesOperatorStackForChunk(const FIntVector& ChunkCoord
case ECaveGeneratorType::Underwater: case ECaveGeneratorType::Underwater:
// ⚠️ AUCUNE PILE À ELLE : `Underwater` EST `TunnelNetwork` plus un drapeau d'eau consommé // ⚠️ AUCUNE PILE À ELLE : `Underwater` EST `TunnelNetwork` plus un drapeau d'eau consommé
// côté rendu. `GetDensityAt` les met dans le même `case`, et `WaterLevelRelative` n'est lu // côté rendu. `VF_BuildOpStackForChunk` les met dans le même `case`, et `WaterLevelRelative` n'est lu
// que par `GetWaterLevel*` de ce manager — jamais par la densité (vérifié, pas supposé). // que par `GetWaterLevel*` de ce manager — jamais par la densité (vérifié, pas supposé).
case ECaveGeneratorType::TunnelNetwork: case ECaveGeneratorType::TunnelNetwork:
// Le plus gros : squelette SDF → douze modificateurs de détail → override d'op par salle, // Le plus gros : squelette SDF → douze modificateurs de détail → override d'op par salle,
// 19 opérateurs, dont `FRoomGraphSource` qui **APPELLE** `BuildChunkCache`/`EvaluateSDFCached` // 19 opérateurs, dont `FRoomGraphSource` qui **APPELLE** `BuildChunkCache`/`EvaluateSDFCached`
// au lieu de les transcrire — c'est là que vit la discipline d'invariance de fenêtre // au lieu de les transcrire — c'est là que vit la discipline d'invariance de fenêtre
// d'ARCHITECTURE §8.4 ; en forker une copie la casserait. // d'ARCHITECTURE §8.4 ; en forker une copie la casserait.
//
// **8 SUR 8.** Le `switch` d'archétypes a un jumeau en pile d'opérateurs, opt-in par
// strate, chacun vérifié par un test d'équivalence bit à bit contre sa fonction d'origine.
return true; return true;
default: return false; default: return false;
+20 -33
View File
@@ -1,34 +1,32 @@
// VoxelDensityOp.h // VoxelDensityOp.h
// LE CONTRAT de la pile d'opérateurs de densité / THE density operator stack CONTRACT. // LE CONTRAT de la pile d'opérateurs de densité / THE density operator stack CONTRACT.
// Design : Docs/archive/OPSTACK-PLAN.md. The contract is implemented by the opt-in stack path in // Design : Docs/archive/OPSTACK-PLAN.md. The contract is implemented by the stack path in
// UVoxelGenerator::GetDensityAt; the legacy archetype switch remains for non-opt-in strates. // UVoxelGenerator::GetDensityAt, which generates every strate.
// //
// ───────────────────────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────────────────────
// POURQUOI / WHY // POURQUOI / WHY
// ───────────────────────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────────────────────
// UVoxelGenerator::GetDensityAt est aujourd'hui un `switch` sur 8 ECaveGeneratorType, chacun // Chaque archétype (ECaveGeneratorType) est une composition d'opérateurs, pas une fonction de
// possédant sa fonction de densité et son struct de params. Conséquences : une nouvelle idée de // densité monolithique qui posséderait le voxel entier. Une idée de monde s'exprime en empilant
// monde coûte ~6 sites d'édition, et surtout LES IDÉES NE PEUVENT PAS SE COMBINER — un archétype // sources, combinateurs et modificateurs — et les idées peuvent donc SE COMBINER : « une strate de
// possède le voxel entier. On ne peut pas écrire « une strate de surface dont les montagnes // surface dont les montagnes contiennent un réseau de salles, avec des îles flottantes au-dessus ».
// contiennent un réseau de salles, avec des îles flottantes dans le vide au-dessus », à aucun prix.
// //
// GetDensityAt is today a `switch` over 8 ECaveGeneratorType, each owning a bespoke density // Each archetype (ECaveGeneratorType) is a composition of operators, not a monolithic density
// function and param struct. A new world idea costs ~6 edit sites, and — the real problem — // function that owns the whole voxel. A world idea is expressed by stacking sources, combiners and
// IDEAS CANNOT COMBINE: one archetype owns the whole voxel. // modifiers — so ideas can COMBINE.
// //
// ───────────────────────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────────────────────
// ⚠️ CE N'EST PAS L'ANCIEN SYSTÈME DE « ROOM OPERATIONS » / THIS IS NOT THE OLD ROOM-OPS SYSTEM // ⚠️ CE N'EST PAS L'ANCIEN SYSTÈME DE « ROOM OPERATIONS » / THIS IS NOT THE OLD ROOM-OPS SYSTEM
// ───────────────────────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────────────────────
// UVoxelTerrainOpDefinition (Terrace, Ribbing, Cliff, Scallop, Overhang, Arch, Column, Pit…) ne // UVoxelTerrainOpDefinition (Terrace, Ribbing, Cliff, Scallop, Overhang, Arch, Column, Pit…) ne
// sait que PERTURBER une densité près d'une surface qui existe déjà. Il ne décide jamais ce que le // sait que PERTURBER une densité près d'une surface qui existe déjà. Il ne décide jamais ce que le
// champ EST — cette décision vit dans le `switch`. Une pile d'opérateurs qui se contenterait de // champ EST — c'est le rôle des sources de champ ci-dessous.
// cela aurait reconstruit le switch avec des étapes en plus.
// //
// D'où QUATRE RÔLES, dont l'ancien système n'occupait que le troisième : // D'où QUATRE RÔLES, dont l'ancien système n'occupait que le troisième :
// //
// 1. FIELD SOURCE — fabrique un champ À PARTIR DE RIEN. C'est ce qui fait qu'une grotte est // 1. FIELD SOURCE — fabrique un champ À PARTIR DE RIEN. C'est ce qui fait qu'une grotte est
// une grotte et qu'un monde ouvert est un monde ouvert. Chaque archétype // une grotte et qu'un monde ouvert est un monde ouvert. Chaque archétype
// d'aujourd'hui est fondamentalement l'une de ces sources. // est fondamentalement l'une de ces sources.
// 2. COMBINER — comment deux champs fusionnent (min/max/smooth/mask). C'EST le rôle qui // 2. COMBINER — comment deux champs fusionnent (min/max/smooth/mask). C'EST le rôle qui
// achète la composition ; sans lui il n'y a pas de refactor. // achète la composition ; sans lui il n'y a pas de refactor.
// 3. DETAIL MODIFIER — l'ancien système, rétrogradé à un rôle sur quatre. Inchangé. // 3. DETAIL MODIFIER — l'ancien système, rétrogradé à un rôle sur quatre. Inchangé.
@@ -56,9 +54,8 @@
// An SDF-only writer reports an SDF interval; a later converter folds that interval through its own // 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. // 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 // Presque tout opérateur est UNIDIRECTIONNEL : il ne fait que creuser, ou que remplir. La direction
// existant est UNIDIRECTIONNEL : il ne fait que creuser, ou que remplir. Cela suffit à reproduire // suffit donc à exprimer GÉNÉRIQUEMENT les gardes de ClassifyTile :
// 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
@@ -71,26 +68,16 @@
// it never lets an SDF writer impersonate a downstream neighbour. // it never lets an SDF writer impersonate a downstream neighbour.
// //
// ───────────────────────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────────────────────
// LE GROS LOT PERF : les strates de grotte ne sautent AUCUNE tuile aujourd'hui // CLASSIFYTILE
// THE PERF PRIZE: cave strates skip ZERO tiles today
// ───────────────────────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────────────────────
// ClassifyTile conserve ses preuves exactes pour les gaps de bedrock et SurfaceWorld ; les // ClassifyTile garde ses preuves exactes pour les gaps de bedrock et SurfaceWorld ; les slots de
// archétypes de cave opt-in passent maintenant par le même pliage `ClassifyBox`. Le post XY fournit // cave passent par le pliage `ClassifyBox` de la pile même qui produit leur densité. Le post XY
// en plus la première preuve globale de coque : une tuile entièrement dans la bande forcée peut être // fournit en plus une preuve globale de coque : une tuile entièrement dans la bande forcée peut être
// sautée sans connaître l'archétype ou le Z. // sautée sans connaître l'archétype ou le Z.
// //
// **Traiter cela comme un livrable explicite de chaque portage, pas comme un effet de bord.** // ClassifyTile keeps its exact proofs for bedrock gaps and SurfaceWorld; cave slots go through the
// // `ClassifyBox` fold of the same stack that produces their density. The XY post adds a global
// ───────────────────────────────────────────────────────────────────────────────────────── // shell proof: a tile entirely inside the forced band is skipped without knowing archetype or Z.
// ÉTAT / STATUS
// ─────────────────────────────────────────────────────────────────────────────────────────
// Les huit archétypes ont maintenant une pile derrière l'opt-in `bUseOperatorStack`, et ClassifyTile
// utilise cette pile pour les slots de cave opt-in. Le chemin legacy reste disponible pour les
// strates non opt-in; les tests d'équivalence servent de garde de portage.
//
// All eight archetypes now have an operator-stack path behind `bUseOperatorStack`, and ClassifyTile
// uses that stack for opt-in cave slots. The legacy switch remains available for opt-out strates;
// equivalence tests are port-correctness guards.
// //
// NOTE sur les UENUM : ces types sont volontairement du C++ nu (pas d'UHT, pas de .generated.h). // NOTE sur les UENUM : ces types sont volontairement du C++ nu (pas d'UHT, pas de .generated.h).
// Ils deviendront UENUM/USTRUCT en Phase 3, quand les opérateurs deviendront des data assets et // Ils deviendront UENUM/USTRUCT en Phase 3, quand les opérateurs deviendront des data assets et
+9 -21
View File
@@ -2,33 +2,21 @@
// La PILE : un conteneur ordonné d'opérateurs, plus les fabriques d'opérateurs concrets. // La PILE : un conteneur ordonné d'opérateurs, plus les fabriques d'opérateurs concrets.
// The STACK: an ordered container of operators, plus the concrete-operator factories. // The STACK: an ordered container of operators, plus the concrete-operator factories.
// //
// ⚠️ CECI ALIMENTE LE JEU, MAIS SEULEMENT SUR OPT-IN. // ⚠️ CECI EST LE CHEMIN DE GÉNÉRATION DU JEU.
// `UVoxelGenerator::GetDensityAt` construit la pile par chunk et l'évalue à la place du `switch` // `UVoxelGenerator::GetDensityAt` construit la pile par chunk pour chaque strate :
// UNIQUEMENT quand `UVoxelStrateManager::UsesOperatorStackForChunk` rend true — c.-à-d. quand la
// strate a coché `bUseOperatorStack` ET que son archétype figure dans la liste des portés :
// **Maze, FlatPlain, CrystalChamber, SurfaceWorld, VerticalShafts, FloatingIslands, TunnelNetwork, // **Maze, FlatPlain, CrystalChamber, SurfaceWorld, VerticalShafts, FloatingIslands, TunnelNetwork,
// Underwater (8 sur 8)**. // Underwater (8 sur 8)**. `ClassifyTile` construit la même pile pour les archétypes de cave et plie
// Toute autre strate passe encore par le `switch`, inchangé. // `ClassifyBox`; les gaps et SurfaceWorld gardent leurs preuves exactes dédiées.
// `ClassifyTile` est branché pour les archétypes de cave opt-in : il construit la même pile et
// plie `ClassifyBox`; les gaps et SurfaceWorld gardent leurs preuves exactes dédiées.
// //
// THIS FEEDS THE GAME, BUT ONLY BEHIND AN OPT-IN. GetDensityAt builds the stack per chunk and // THIS IS THE GAME'S GENERATION PATH. GetDensityAt builds the stack per chunk for every strate.
// evaluates it instead of the switch only when UsesOperatorStackForChunk returns true (strate ticked // ClassifyTile uses the same stack for cave archetypes; gaps and SurfaceWorld retain their exact
// bUseOperatorStack AND its archetype is ported — all 8). ClassifyTile uses the same stack for cave // hand-written proofs.
// archetypes; gaps and SurfaceWorld retain their exact hand-written proofs.
//
// ⛔ NE JAMAIS faire tourner les deux chemins dans le même monde.
// ⚠️ EN REVANCHE, LES COMPARER EST LÉGITIME. `AUDIT §C10` (le résidu ~1 ULP) est clos par
// `FPSemantics = Precise` : les cinq tests d'équivalence comparent bit à bit. Ce ne sont pas des
// contrôles de FIDÉLITÉ (la barre `§2.6.1` n'exige aucune ressemblance avec l'ancien monde) mais
// des oracles de CORRECTION DE PORTAGE — une faute de transcription reste un vrai bug, et l'ancienne fonction est
// le moyen le moins cher de l'attraper.
// //
// POURQUOI CETTE FORME / WHY THIS SHAPE // POURQUOI CETTE FORME / WHY THIS SHAPE
// La question à laquelle cette forme répond n'est pas « est-ce que ça marche ? » mais // La question à laquelle cette forme répond n'est pas « est-ce que ça marche ? » mais
// **« est-ce que la séparation source / modifier tombe naturellement du code existant ? »** // **« est-ce que la séparation source / modifier tombe naturellement du code existant ? »**
// (OPSTACK-PLAN §4). Chaque archétype porté est comparé bit à bit à l'original : la réponse est // (OPSTACK-PLAN §4). Chaque archétype a été porté puis comparé bit à bit à sa fonction
// MESURÉE plutôt qu'une opinion. // d'origine avant qu'elle ne soit retirée : la réponse a été MESURÉE plutôt qu'une opinion.
#pragma once #pragma once
+1 -1
View File
@@ -414,7 +414,7 @@ public:
* *
* GAP (bedrock), hors-layout (air constant) et SurfaceWorld plient leurs hypothèses par Z ; * GAP (bedrock), hors-layout (air constant) et SurfaceWorld plient leurs hypothèses par Z ;
* SurfaceWorld évalue ses colonnes terrain/plafond avec le MÊME ComputeSurfaceColumn que le * SurfaceWorld évalue ses colonnes terrain/plafond avec le MÊME ComputeSurfaceColumn que le
* chemin densité. Un unique slot cave opt-in peut ajouter le verdict conservatif de sa pile, * chemin densité. Un unique slot cave peut ajouter le verdict conservatif de sa pile,
* sous gardes d'archétype et de params bit-identiques. Spine/passages/disturbances/diff restent * sous gardes d'archétype et de params bit-identiques. Spine/passages/disturbances/diff restent
* des gardes conservatrices. Worker-safe (lecture seule + caches thread_local partagés avec * des gardes conservatrices. Worker-safe (lecture seule + caches thread_local partagés avec
* GetDensityAt — un verdict Mixed laisse les colonnes chaudes pour la génération qui suit). * GetDensityAt — un verdict Mixed laisse les colonnes chaudes pour la génération qui suit).
-4
View File
@@ -14,10 +14,6 @@ DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Tiles Skipped All Air"), STAT_VoxelForge
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Tiles Meshed"), STAT_VoxelForgeTilesMeshed, STATGROUP_VoxelForge, VOXELFORGE_API); DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Tiles Meshed"), STAT_VoxelForgeTilesMeshed, STATGROUP_VoxelForge, VOXELFORGE_API);
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Tiles Operator Stack Solid"), STAT_VoxelForgeTilesOpStackSolid, STATGROUP_VoxelForge, VOXELFORGE_API); DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Tiles Operator Stack Solid"), STAT_VoxelForgeTilesOpStackSolid, STATGROUP_VoxelForge, VOXELFORGE_API);
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Tiles Operator Stack Air"), STAT_VoxelForgeTilesOpStackAir, STATGROUP_VoxelForge, VOXELFORGE_API); DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Tiles Operator Stack Air"), STAT_VoxelForgeTilesOpStackAir, STATGROUP_VoxelForge, VOXELFORGE_API);
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Cave Bail Not Op Stack Sole Slot"), STAT_VoxelForgeCaveBailNotOpStackSoleSlot, STATGROUP_VoxelForge, VOXELFORGE_API);
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Cave Bail Not Op Stack Boundary Tile"), STAT_VoxelForgeCaveBailNotOpStackBoundaryTile, STATGROUP_VoxelForge, VOXELFORGE_API);
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Cave Bail Not Op Stack No Layout"), STAT_VoxelForgeCaveBailNotOpStackNoLayout, STATGROUP_VoxelForge, VOXELFORGE_API);
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Cave Bail Not Op Stack Recheck"), STAT_VoxelForgeCaveBailNotOpStackRecheck, STATGROUP_VoxelForge, VOXELFORGE_API);
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Cave Mix Out Of Layout"), STAT_VoxelForgeCaveMixOutOfLayout, STATGROUP_VoxelForge, VOXELFORGE_API); DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Cave Mix Out Of Layout"), STAT_VoxelForgeCaveMixOutOfLayout, STATGROUP_VoxelForge, VOXELFORGE_API);
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Cave Mix Gap"), STAT_VoxelForgeCaveMixGap, STATGROUP_VoxelForge, VOXELFORGE_API); DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Cave Mix Gap"), STAT_VoxelForgeCaveMixGap, STATGROUP_VoxelForge, VOXELFORGE_API);
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Cave Mix Surface World"), STAT_VoxelForgeCaveMixSurfaceWorld, STATGROUP_VoxelForge, VOXELFORGE_API); DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Cave Mix Surface World"), STAT_VoxelForgeCaveMixSurfaceWorld, STATGROUP_VoxelForge, VOXELFORGE_API);
@@ -115,29 +115,6 @@ public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Generation") UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Generation")
ECaveGeneratorType GeneratorType = ECaveGeneratorType::TunnelNetwork; ECaveGeneratorType GeneratorType = ECaveGeneratorType::TunnelNetwork;
/**
* OPERATOR STACK (experimental) — generate this strate through the composable density operator
* stack instead of the hardcoded archetype switch. Same world, different machinery.
*
* This is the A/B switch for `OPSTACK-PLAN §2.6`'s acceptance bar: flip it, regenerate, and
* judge on a screenshot that it is recognisably the same place. Both systems coexist
* indefinitely — the switch is not going away until every archetype is ported.
*
* ⚠️ ONLY `Maze` IS PORTED SO FAR. On any other GeneratorType this flag is ignored and the
* switch runs as before, so setting it is harmless but does nothing yet.
*
* ⚠️ Do NOT flip this on a strate mid-session and expect the old and new geometry to agree to
* the bit — they differ by ~1-2 ULP with ZERO isosurface crossings, so the shape is identical
* but the floats are not (`Docs/archive/AUDIT-2026-07.md §C10`). Regenerate the world after changing it
* rather than letting old and new tiles sit side by side.
*
* Pile d'opérateurs (expérimental) : génère cette strate via la pile composable au lieu du
* `switch` d'archétype. Seul `Maze` est porté ; ailleurs le drapeau est ignoré.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Generation",
meta = (DisplayName = "Use Operator Stack (experimental)"))
bool bUseOperatorStack = false;
/** /**
* Authoritative per-strate worm switch. Existing assets default to enabled. The manager * Authoritative per-strate worm switch. Existing assets default to enabled. The manager
* resolves a disabled strate to WormStrength == 0 before any fused, stack, composer, or * resolves a disabled strate to WormStrength == 0 before any fused, stack, composer, or
@@ -294,7 +294,7 @@ public:
* *
* Returns TunnelNetwork if the chunk is outside all strates or if the * Returns TunnelNetwork if the chunk is outside all strates or if the
* strate definition is null. The generator uses this to pick which * strate definition is null. The generator uses this to pick which
* density function to call (GetDensityWithParams vs GetSlabDensity). * operator stack to build for the chunk.
* *
* @param ChunkCoord - The chunk position * @param ChunkCoord - The chunk position
* @return The ECaveGeneratorType for the strate containing this chunk * @return The ECaveGeneratorType for the strate containing this chunk
@@ -302,12 +302,9 @@ public:
ECaveGeneratorType GetGeneratorTypeForChunk(const FIntVector& ChunkCoord) const; ECaveGeneratorType GetGeneratorTypeForChunk(const FIntVector& ChunkCoord) const;
/** /**
* True when this chunk's strate opts into the density OPERATOR STACK instead of the hardcoded * True when this chunk lies in a strate slot whose archetype has an operator stack (all eight
* archetype switch (`UVoxelStrateDefinition::bUseOperatorStack`). * do). False outside the layout or for a slot without a definition. This predicate is the ONLY
* * place the archetype list is written down.
* Returns false for archetypes that have no port yet, so the flag can be set on any strate
* without changing its output until that archetype lands. Only `Maze` is ported today — this
* predicate is where that list grows, and it is deliberately the ONLY place it is written down.
*/ */
bool UsesOperatorStackForChunk(const FIntVector& ChunkCoord) const; bool UsesOperatorStackForChunk(const FIntVector& ChunkCoord) const;
@@ -314,7 +314,6 @@ struct FExploreArguments
bool bRender = true; bool bRender = true;
bool bWalk = true; bool bWalk = true;
bool bExport = true; bool bExport = true;
bool bUseOperatorStack = true;
bool bWormsEnabledOverride = false; bool bWormsEnabledOverride = false;
bool bWormsEnabled = true; bool bWormsEnabled = true;
bool bWormStrengthOverride = false; bool bWormStrengthOverride = false;
@@ -431,9 +430,6 @@ bool ParseArguments(const FString& Params, FExploreArguments& OutArguments, FStr
} }
ModesText.TrimQuotesInline(); ModesText.TrimQuotesInline();
} }
int32 UseOperatorStack = 1;
FParse::Value(*Params, TEXT("opstack="), UseOperatorStack);
OutArguments.bUseOperatorStack = UseOperatorStack != 0;
int32 WormsEnabled = 1; int32 WormsEnabled = 1;
if (FParse::Value(*Params, TEXT("wormsenabled="), WormsEnabled)) if (FParse::Value(*Params, TEXT("wormsenabled="), WormsEnabled))
{ {
@@ -1218,9 +1214,6 @@ struct FExploreWorld
Definition->GeneratorType = Index == Arguments.Slot Definition->GeneratorType = Index == Arguments.Slot
? Arguments.Archetype ? Arguments.Archetype
: ECaveGeneratorType::TunnelNetwork; : ECaveGeneratorType::TunnelNetwork;
// Explicitly select the production switch so the same synthetic layout, seed and
// mesher invocation can be measured through both density branches.
Definition->bUseOperatorStack = Arguments.bUseOperatorStack;
if (Arguments.bWormsEnabledOverride) if (Arguments.bWormsEnabledOverride)
{ {
Definition->bEnableWorms = Arguments.bWormsEnabled; Definition->bEnableWorms = Arguments.bWormsEnabled;
@@ -1376,7 +1369,7 @@ struct FExploreWorld
const FStrateGenerationParams& ReportParams = Target.Definition->GenerationParams; const FStrateGenerationParams& ReportParams = Target.Definition->GenerationParams;
UE_LOG(LogTemp, Display, UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeExplore][OwnerAssets] strate=%s settings=%s slot=%d " TEXT("[VoxelForgeExplore][OwnerAssets] strate=%s settings=%s slot=%d "
"seed=%d height_chunks=%d archetype=%s opstack=%d " "seed=%d height_chunks=%d archetype=%s "
"floor_switch_ignored=1 floor_terracing=%d floor_step=%.9g max_ledge=%.9g " "floor_switch_ignored=1 floor_terracing=%d floor_step=%.9g max_ledge=%.9g "
"gentle_slope_degrees=%.9g gentle_gradient=%.9g route_cap=%.9g " "gentle_slope_degrees=%.9g gentle_gradient=%.9g route_cap=%.9g "
"wind_wave_cap=%d " "wind_wave_cap=%d "
@@ -1385,7 +1378,7 @@ struct FExploreWorld
bUseAuthoredStrate ? *Arguments.StrateReference : TEXT("synthetic"), bUseAuthoredStrate ? *Arguments.StrateReference : TEXT("synthetic"),
bUseAuthoredSettings ? *Arguments.SettingsReference : TEXT("synthetic"), bUseAuthoredSettings ? *Arguments.SettingsReference : TEXT("synthetic"),
Arguments.Slot, Settings->Seed, Target.Definition->StrateHeightInChunks, Arguments.Slot, Settings->Seed, Target.Definition->StrateHeightInChunks,
ArchetypeName(TargetArchetype), Target.Definition->bUseOperatorStack ? 1 : 0, ArchetypeName(TargetArchetype),
ReportParams.bTunnelFloorTerracingEnabled ? 1 : 0, ReportParams.bTunnelFloorTerracingEnabled ? 1 : 0,
ReportParams.TunnelFloorTerraceStepHeight, ReportParams.TunnelFloorTerraceStepHeight,
ReportParams.TunnelFloorMaxLedgeHeight, ReportParams.TunnelFloorMaxLedgeHeight,
@@ -4453,7 +4446,6 @@ FString BuildManifestJson(
Writer->WriteValue(TEXT("seed"), Arguments.Seed); Writer->WriteValue(TEXT("seed"), Arguments.Seed);
Writer->WriteValue(TEXT("archetype"), ArchetypeName(Arguments.Archetype)); Writer->WriteValue(TEXT("archetype"), ArchetypeName(Arguments.Archetype));
Writer->WriteValue(TEXT("slot"), Arguments.Slot); Writer->WriteValue(TEXT("slot"), Arguments.Slot);
Writer->WriteValue(TEXT("operator_stack"), Arguments.bUseOperatorStack);
Writer->WriteValue(TEXT("worms_enabled_override"), Arguments.bWormsEnabledOverride); Writer->WriteValue(TEXT("worms_enabled_override"), Arguments.bWormsEnabledOverride);
if (Arguments.bWormsEnabledOverride) if (Arguments.bWormsEnabledOverride)
{ {
@@ -6135,7 +6127,6 @@ FString BuildExploreJson(
Writer->WriteValue(TEXT("seed"), Arguments.Seed); Writer->WriteValue(TEXT("seed"), Arguments.Seed);
Writer->WriteValue(TEXT("archetype"), ArchetypeName(Arguments.Archetype)); Writer->WriteValue(TEXT("archetype"), ArchetypeName(Arguments.Archetype));
Writer->WriteValue(TEXT("slot"), Arguments.Slot); Writer->WriteValue(TEXT("slot"), Arguments.Slot);
Writer->WriteValue(TEXT("operator_stack"), Arguments.bUseOperatorStack);
Writer->WriteValue(TEXT("worms_enabled_override"), Arguments.bWormsEnabledOverride); Writer->WriteValue(TEXT("worms_enabled_override"), Arguments.bWormsEnabledOverride);
if (Arguments.bWormsEnabledOverride) if (Arguments.bWormsEnabledOverride)
{ {
@@ -6216,12 +6207,11 @@ FString BuildExploreJson(
Writer->WriteValue(TEXT("out_directory"), Arguments.OutDirectory); Writer->WriteValue(TEXT("out_directory"), Arguments.OutDirectory);
Writer->WriteValue(TEXT("canonical_invocation"), FString::Printf( Writer->WriteValue(TEXT("canonical_invocation"), FString::Printf(
TEXT("UnrealEditor-Cmd VoxelM.uproject -run=VoxelForgeExplore -seed=%d -archetype=%s " TEXT("UnrealEditor-Cmd VoxelM.uproject -run=VoxelForgeExplore -seed=%d -archetype=%s "
"-slot=%d -opstack=%d -modes=%s -blockearlyout=%d " "-slot=%d -modes=%s -blockearlyout=%d "
"-out=<ABSOLUTE_PLUGIN_SAVED_PATH>"), "-out=<ABSOLUTE_PLUGIN_SAVED_PATH>"),
Arguments.Seed, Arguments.Seed,
ArchetypeName(Arguments.Archetype), ArchetypeName(Arguments.Archetype),
Arguments.Slot, Arguments.Slot,
Arguments.bUseOperatorStack ? 1 : 0,
*Arguments.CanonicalModes(), *Arguments.CanonicalModes(),
Arguments.bBlockEarlyOut ? 1 : 0)); Arguments.bBlockEarlyOut ? 1 : 0));
Writer->WriteObjectEnd(); Writer->WriteObjectEnd();
@@ -7203,7 +7193,6 @@ bool WriteExploreOpBoundsReport(
Writer->WriteValue(TEXT("seed"), Arguments.Seed); Writer->WriteValue(TEXT("seed"), Arguments.Seed);
Writer->WriteValue(TEXT("archetype"), ArchetypeName(Arguments.Archetype)); Writer->WriteValue(TEXT("archetype"), ArchetypeName(Arguments.Archetype));
Writer->WriteValue(TEXT("slot"), Arguments.Slot); Writer->WriteValue(TEXT("slot"), Arguments.Slot);
Writer->WriteValue(TEXT("operator_stack"), Arguments.bUseOperatorStack);
Writer->WriteValue(TEXT("sample_step"), 1); Writer->WriteValue(TEXT("sample_step"), 1);
Writer->WriteValue(TEXT("block_cells"), 8); Writer->WriteValue(TEXT("block_cells"), 8);
Writer->WriteValue(TEXT("classifier_blocks_tested"), BlocksTested); Writer->WriteValue(TEXT("classifier_blocks_tested"), BlocksTested);
@@ -7311,10 +7300,9 @@ bool RunOpBounds(
FExploreBudget& Budget, FExploreBudget& Budget,
FString& OutError) FString& OutError)
{ {
if (Arguments.Archetype != ECaveGeneratorType::TunnelNetwork if (Arguments.Archetype != ECaveGeneratorType::TunnelNetwork)
|| !Arguments.bUseOperatorStack)
{ {
OutError = TEXT("opbounds requires archetype=TunnelNetwork and opstack=1."); OutError = TEXT("opbounds requires archetype=TunnelNetwork.");
return false; return false;
} }
@@ -8262,8 +8250,6 @@ bool BuildBatchCaseParams(
FString Modes = TEXT("export"); FString Modes = TEXT("export");
JsonString(Case, TEXT("modes"), Modes); JsonString(Case, TEXT("modes"), Modes);
bool bOperatorStack = true;
JsonBool(Case, TEXT("operator_stack"), bOperatorStack);
bool bProfileDensity = false; bool bProfileDensity = false;
JsonBool(Case, TEXT("profile_density"), bProfileDensity); JsonBool(Case, TEXT("profile_density"), bProfileDensity);
bool bProfileDensityFull = false; bool bProfileDensityFull = false;
@@ -8293,11 +8279,11 @@ bool BuildBatchCaseParams(
Case, TEXT("tunnel_floor_terracing"), bTunnelFloorTerracing); Case, TEXT("tunnel_floor_terracing"), bTunnelFloorTerracing);
OutParams = FString::Printf( OutParams = FString::Printf(
TEXT("-seed=%d -archetype=%s -slot=%d -modes=%s -opstack=%d " TEXT("-seed=%d -archetype=%s -slot=%d -modes=%s "
"-exportsize=%d -exportstep=%d -densitygridreuse=%d " "-exportsize=%d -exportstep=%d -densitygridreuse=%d "
"-blockearlyout=%d -meshminbatch=%d " "-blockearlyout=%d -meshminbatch=%d "
"-failurefocus=%d -out=\"%s\""), "-failurefocus=%d -out=\"%s\""),
Seed, *Archetype, Slot, *Modes, bOperatorStack ? 1 : 0, Seed, *Archetype, Slot, *Modes,
ExportSize, ExportStep, bReuseDensityGrid ? 1 : 0, ExportSize, ExportStep, bReuseDensityGrid ? 1 : 0,
bBlockEarlyOut ? 1 : 0, MeshMinBatchSize, bBlockEarlyOut ? 1 : 0, MeshMinBatchSize,
bFailureFocus ? 1 : 0, *CaseOutDirectory); bFailureFocus ? 1 : 0, *CaseOutDirectory);
-1
View File
@@ -633,7 +633,6 @@ function New-CommandletArguments([string]$CommandletOut, [string]$Modes, [bool]$
[void]$args.Add('-seed=0') [void]$args.Add('-seed=0')
[void]$args.Add('-archetype=TunnelNetwork') [void]$args.Add('-archetype=TunnelNetwork')
[void]$args.Add("-slot=$Slot") [void]$args.Add("-slot=$Slot")
[void]$args.Add('-opstack=1')
[void]$args.Add(('-modes="{0}"' -f $Modes)) [void]$args.Add(('-modes="{0}"' -f $Modes))
[void]$args.Add('-blockearlyout=0') [void]$args.Add('-blockearlyout=0')
[void]$args.Add('-densitygridreuse=1') [void]$args.Add('-densitygridreuse=1')