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
+412 -817

No files matched your search

@@ -39,12 +39,8 @@
#include "Misc/AutomationTest.h"
#include "VoxelForgeTestFixture.h"
// Inclus ici DÉLIBÉRÉMENT : VoxelDensityOp.h n'est encore inclus par aucun .cpp, donc le
// compilateur ne le verrait jamais. Le fold qu'il définit prétend reproduire ClassifyTile — ce
// 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.
// Le fold défini par VoxelDensityOp.h prétend reproduire ClassifyTile : ce fichier le teste.
// The fold defined in VoxelDensityOp.h claims to reproduce ClassifyTile; this file tests it.
#include "VoxelDensityOp.h"
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
@@ -153,7 +149,7 @@ bool FVoxelForgeClassifyTileTest::RunTest(const FString& Parameters)
AddInfo(FString::Printf(
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("non-Mixed count is expected: every uncertain case remains Mixed for safety."),
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
// `FVoxelOpStack::ClassifyBox` quand la strate a coché `bUseOperatorStack` — donc **un tout nouveau
// chemin peut faire sauter le maillage d'une tuile**, et son erreur est un TROU : pas de triangles,
// pas de collision, invisible jusqu'à ce qu'un joueur tombe au travers.
// Pour un slot de CAVE, `ClassifyTile` consulte `FVoxelOpStack::ClassifyBox` — donc **la pile peut
// faire sauter le maillage d'une tuile**, et son erreur est un TROU : pas de triangles, 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
// les strates ont coché la case. Il ne vérifie pas le pliage (c'est `BoxVerdictFold`) ni les
// opérateurs (ce sont les huit tests d'équivalence) : il vérifie le **câblage** — que la pile
// interrogée par le classifieur est bien celle qui produit la densité, params, drapeau et
// disturbances compris.
// Ce test est le même oracle par force brute que `ClassifyTileSoundness`, avec un tirage uniforme
// sur tout le layout. Il ne vérifie pas le pliage (c'est `BoxVerdictFold`) ni les opérateurs : il
// vérifie le **câblage** — que la pile interrogée par le classifieur est bien celle qui produit la
// densité, params et disturbances compris.
//
// ⚠️ 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
// 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.
// It checks the WIRING, not the fold and not the operators.
// Same brute-force oracle as ClassifyTileSoundness, drawn uniformly over the whole layout so cave
// slots (which classify through their stack) dominate. It checks the WIRING, not the fold and not
// the operators.
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FVoxelForgeOpStackClassifyTileTest,
@@ -266,11 +261,10 @@ bool FVoxelForgeOpStackClassifyTileTest::RunTest(const FString& Parameters)
{
using namespace VoxelForgeTest;
// ⚠️ `bUseOperatorStack = true` sur toutes les strates : c'est LE point du test. La fixture
// donne à ce monde une `LayoutVersion` unique dans le processus, sans quoi les caches par chunk
// de `GetDensityAt` — dont `CP_UseOpStack` — pourraient encore porter ceux d'un autre test.
// La fixture donne à ce monde une `LayoutVersion` unique dans le processus, sans quoi les caches
// par chunk de `GetDensityAt` — dont `CP_UseOpStack` — pourraient encore porter ceux d'un autre test.
FTestWorld World;
World.Build(/*Seed*/1337, /*GapChunks*/2, /*bUseOperatorStack*/true);
World.Build(/*Seed*/1337, /*GapChunks*/2);
if (!World.IsValid())
{
AddError(World.WhyInvalid());
@@ -287,8 +281,8 @@ bool FVoxelForgeOpStackClassifyTileTest::RunTest(const FString& Parameters)
const int32 BottomVoxelZ = World.BottomChunkZ * CHUNK_SIZE;
// 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
// nouveau chemin. SurfaceWorld continue d'être prouvé par le code écrit à la main.
// sont précisément les strates de cave qui intéressent, puisque ce sont elles que ClassifyTile
// juge par leur pile. SurfaceWorld est prouvé par le test colonne écrit à la main.
for (int32 t = 0; t < NumTilesScanned; ++t)
{
FTileSpec Spec;
@@ -316,20 +310,17 @@ bool FVoxelForgeOpStackClassifyTileTest::RunTest(const FString& Parameters)
AddInfo(FString::Printf(
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("which runs the SAME scan on a world that has NOT opted in: every verdict beyond what ")
TEXT("that test reports is a tile the mesher now skips and did not before. That difference ")
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."),
TEXT("AllAir %d (brute-forcing %d). Every cave-slot verdict is a tile the mesher skips on ")
TEXT("the strength of the stack's ClassifyBox fold -- and a hole if the wiring is wrong, ")
TEXT("which is what the brute force below is for."),
NumTilesScanned, NumMixed, NumAllSolid, NumAllAir, ToVerify.Num()));
if (ToVerify.Num() == 0)
{
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("opted in (check FTestWorld::Build's bUseOperatorStack), or every guard in the ")
TEXT("cave branch of ClassifyTile bailed to Mixed -- the params-identical check and ")
TEXT("the 27-chunk-coord cap are the likeliest. Do NOT read a green run as proof."));
TEXT("this test verified NOTHING about the wiring. Every guard in the cave branch ")
TEXT("of ClassifyTile bailed to Mixed -- the params-identical check and the ")
TEXT("27-chunk-coord cap are the likeliest. Do NOT read a green run as proof."));
return false;
}
@@ -356,7 +356,7 @@ bool FVoxelForgeComposerCorpusFreeTest::RunTest(const FString& Parameters)
Corpus.GetEntries().Num(), 12);
FTestWorld World;
World.Build(1337, 2, true);
World.Build(1337, 2);
if (!World.IsValid())
{
AddError(World.WhyInvalid());
@@ -955,7 +955,7 @@ bool FVoxelForgeComposerParameterRollTest::RunTest(const FString& Parameters)
}
FTestWorld World;
World.Build(1337, 2, true);
World.Build(1337, 2);
if (!World.IsValid())
{
AddError(World.WhyInvalid());
@@ -1117,7 +1117,6 @@ bool FVoxelForgeComposerParameterRollTest::RunTest(const FString& Parameters)
CandidateDefinition->SurfaceParams = Roll.ArchetypeParams.SurfaceParams;
CandidateDefinition->VerticalShaftParams = Roll.ArchetypeParams.VerticalShaftParams;
CandidateDefinition->FloatingIslandParams = Roll.ArchetypeParams.FloatingIslandParams;
CandidateDefinition->bUseOperatorStack = true;
CandidateDefinition->TransitionType = EVoxelStrateTransition::Hard;
World.Reinitialize();
@@ -1342,123 +1341,109 @@ bool FVoxelForgeTerrainDetailLivenessTest::RunTest(const FString& Parameters)
constexpr int32 ExpectedLatticeSamples = 16 * 16 * 16;
FString Table = TEXT(
"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");
int32 TotalRows = 0;
int32 FailedRows = 0;
for (const bool bUseOperatorStack : { false, true })
FTestWorld World;
World.Build(1337, 2);
if (!World.IsValid())
{
FTestWorld World;
World.Build(1337, 2, bUseOperatorStack);
if (!World.IsValid())
AddError(World.WhyInvalid());
return false;
}
UVoxelStrateDefinition* Definition =
World.Definitions[FTestWorld::SlotTunnelNetwork].Get();
if (Definition == nullptr)
{
AddError(TEXT("The fixture has no TunnelNetwork definition for the liveness experiment."));
return false;
}
Definition->TerrainOperations.Reset();
TestEqual(TEXT("fixture has no terrain-op asset pool"), Definition->TerrainOperations.Num(), 0);
TArray<FVector> Points;
VF_BuildTerrainDetailLattice(World, Points);
TestEqual(TEXT("experiment uses the fixed lattice"), Points.Num(), ExpectedLatticeSamples);
FStrateGenerationParams BaselineParams = Definition->GenerationParams;
VF_ClearTerrainDetailFields(BaselineParams);
TArray<float> FirstBaseline;
bool bHaveFirstBaseline = false;
for (const FTerrainDetailProbeSpec& Spec : GTerrainDetailProbeSpecs)
{
Definition->GenerationParams = BaselineParams;
World.Reinitialize();
TArray<float> BaselineSamples;
const FTerrainDetailDensitySummary BaselineSummary =
VF_SampleTerrainDetailLattice(World, Points, BaselineSamples);
if (!bHaveFirstBaseline)
{
AddError(World.WhyInvalid());
return false;
FirstBaseline = BaselineSamples;
bHaveFirstBaseline = true;
}
UVoxelStrateDefinition* Definition =
World.Definitions[FTestWorld::SlotTunnelNetwork].Get();
if (Definition == nullptr)
else
{
AddError(TEXT("The fixture has no TunnelNetwork definition for the liveness experiment."));
return false;
}
Definition->TerrainOperations.Reset();
TestEqual(
FString::Printf(TEXT("%s fixture has no terrain-op asset pool"),
bUseOperatorStack ? TEXT("operator-stack") : TEXT("legacy")),
Definition->TerrainOperations.Num(), 0);
TArray<FVector> Points;
VF_BuildTerrainDetailLattice(World, Points);
TestEqual(
FString::Printf(TEXT("%s experiment uses the fixed lattice"),
bUseOperatorStack ? TEXT("operator-stack") : TEXT("legacy")),
Points.Num(), ExpectedLatticeSamples);
FStrateGenerationParams BaselineParams = Definition->GenerationParams;
VF_ClearTerrainDetailFields(BaselineParams);
TArray<float> FirstBaseline;
bool bHaveFirstBaseline = false;
for (const FTerrainDetailProbeSpec& Spec : GTerrainDetailProbeSpecs)
{
Definition->GenerationParams = BaselineParams;
World.Reinitialize();
TArray<float> BaselineSamples;
const FTerrainDetailDensitySummary BaselineSummary =
VF_SampleTerrainDetailLattice(World, Points, BaselineSamples);
if (!bHaveFirstBaseline)
bool bSameBaseline = FirstBaseline.Num() == BaselineSamples.Num();
for (int32 SampleIndex = 0; bSameBaseline && SampleIndex < FirstBaseline.Num(); ++SampleIndex)
{
FirstBaseline = BaselineSamples;
bHaveFirstBaseline = true;
}
else
{
bool bSameBaseline = FirstBaseline.Num() == BaselineSamples.Num();
for (int32 SampleIndex = 0; bSameBaseline && SampleIndex < FirstBaseline.Num(); ++SampleIndex)
{
bSameBaseline = BitEqual(FirstBaseline[SampleIndex], BaselineSamples[SampleIndex]);
}
TestTrue(
FString::Printf(TEXT("%s baseline is stable before %s probe"),
bUseOperatorStack ? TEXT("operator-stack") : TEXT("legacy"),
Spec.Name),
bSameBaseline);
}
FStrateGenerationParams VariantParams = BaselineParams;
VF_SetTerrainDetailProbe(VariantParams, Spec.Group);
Definition->GenerationParams = VariantParams;
World.Reinitialize();
TArray<float> VariantSamples;
const FTerrainDetailDensitySummary VariantSummary =
VF_SampleTerrainDetailLattice(World, Points, VariantSamples);
const FTerrainDetailDensityDelta Delta =
VF_CompareTerrainDetailSamples(BaselineSamples, VariantSamples);
const bool bObservedLive = Delta.Changed > 0;
const bool bVerdictMatches = bObservedLive == Spec.bExpectedLive;
++TotalRows;
if (!bVerdictMatches)
{
++FailedRows;
bSameBaseline = BitEqual(FirstBaseline[SampleIndex], BaselineSamples[SampleIndex]);
}
TestTrue(
FString::Printf(TEXT("%s %s field group has the measured liveness verdict"),
bUseOperatorStack ? TEXT("operator-stack") : TEXT("legacy"),
Spec.Name),
bVerdictMatches);
Table += FString::Printf(
TEXT("%s | %s | %d | 0x%08x | 0x%08x | %.9g | %.9g | %d | %d | %.9g | %.9g | %s\n"),
bUseOperatorStack ? TEXT("operator-stack") : TEXT("legacy"),
Spec.Name,
Points.Num(),
BaselineSummary.Digest,
VariantSummary.Digest,
BaselineSummary.Mean,
VariantSummary.Mean,
Delta.Changed,
Delta.SignChanged,
Delta.SumAbs,
Delta.MaxAbs,
bObservedLive ? TEXT("LIVE") : TEXT("DEAD"));
FString::Printf(TEXT("baseline is stable before %s probe"), Spec.Name),
bSameBaseline);
}
FStrateGenerationParams VariantParams = BaselineParams;
VF_SetTerrainDetailProbe(VariantParams, Spec.Group);
Definition->GenerationParams = VariantParams;
World.Reinitialize();
TArray<float> VariantSamples;
const FTerrainDetailDensitySummary VariantSummary =
VF_SampleTerrainDetailLattice(World, Points, VariantSamples);
const FTerrainDetailDensityDelta Delta =
VF_CompareTerrainDetailSamples(BaselineSamples, VariantSamples);
const bool bObservedLive = Delta.Changed > 0;
const bool bVerdictMatches = bObservedLive == Spec.bExpectedLive;
++TotalRows;
if (!bVerdictMatches)
{
++FailedRows;
}
TestTrue(
FString::Printf(TEXT("%s field group has the measured liveness verdict"), Spec.Name),
bVerdictMatches);
Table += FString::Printf(
TEXT("%s | %d | 0x%08x | 0x%08x | %.9g | %.9g | %d | %d | %.9g | %.9g | %s\n"),
Spec.Name,
Points.Num(),
BaselineSummary.Digest,
VariantSummary.Digest,
BaselineSummary.Mean,
VariantSummary.Mean,
Delta.Changed,
Delta.SignChanged,
Delta.SumAbs,
Delta.MaxAbs,
bObservedLive ? TEXT("LIVE") : TEXT("DEAD"));
}
AddInfo(Table);
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 "
"terrain-operation assets were present in the fixture pool."),
TotalRows, FailedRows));
TestEqual(TEXT("all terrain detail liveness rows matched their measured verdict"), FailedRows, 0);
TestEqual(TEXT("both density paths measured every terrain detail group"), TotalRows,
static_cast<int32>(2 * UE_ARRAY_COUNT(GTerrainDetailProbeSpecs)));
TestEqual(TEXT("every terrain detail group was measured"), TotalRows,
static_cast<int32>(UE_ARRAY_COUNT(GTerrainDetailProbeSpecs)));
return FailedRows == 0;
}
@@ -394,7 +394,7 @@ bool FVoxelForgeComposerPromotionTest::RunTest(const FString& Parameters)
}
FTestWorld World;
World.Build(1337, 2, true);
World.Build(1337, 2);
if (!World.IsValid())
{
AddError(World.WhyInvalid());
@@ -1590,7 +1590,7 @@ bool FVoxelForgeComposerShowcaseTest::RunTest(const FString& Parameters)
FTestWorld World;
// 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.
World.Build(BaseComposerSeed, 2, true, 8);
World.Build(BaseComposerSeed, 2, 8);
if (!World.IsValid())
{
AddError(World.WhyInvalid());
@@ -262,7 +262,7 @@ bool FVoxelForgeComposerStructureRollTest::RunTest(const FString& Parameters)
}
FTestWorld World;
World.Build(1337, 2, true);
World.Build(1337, 2);
if (!World.IsValid())
{
AddError(World.WhyInvalid());
@@ -132,7 +132,7 @@ IMPLEMENT_SIMPLE_AUTOMATION_TEST(
bool FVoxelForgeTileCacheContextDeterminismTest::RunTest(const FString& Parameters)
{
FTestWorld World;
World.Build(/*Seed*/1337, /*GapChunks*/2, /*bUseOperatorStack*/true);
World.Build(/*Seed*/1337, /*GapChunks*/2);
if (!World.IsValid())
{
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.
FTestWorld WarmWorld;
FTestWorld FreshWorld;
WarmWorld.Build(/*Seed*/1337, /*GapChunks*/2, /*bUseOperatorStack*/true);
FreshWorld.Build(/*Seed*/1337, /*GapChunks*/2, /*bUseOperatorStack*/true);
WarmWorld.Build(/*Seed*/1337, /*GapChunks*/2);
FreshWorld.Build(/*Seed*/1337, /*GapChunks*/2);
if (!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)
{
VoxelForgeTest::FTestWorld World;
World.Build(Seeds[SeedIndex], /*gap chunks=*/2, /*operator stack=*/true);
World.Build(Seeds[SeedIndex], /*gap chunks=*/2);
if (!World.IsValid())
{
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
// 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
// evaluator, as they do outside the block-safe network branch.
// active operator list. The six other archetypes run the scalar stack evaluator, as they
// do in production outside the block-safe network branch.
for (int32 GridIndex = 0; GridIndex < GridSetCount; ++GridIndex)
{
BaseResults[GridIndex] = CompareGeneratorBlockWithScalar(
@@ -761,14 +761,13 @@ namespace
const float SimulatedCarvedFinalDensity = -SimulatedCarvedInternalDensity;
UE_LOG(LogTemp, Display,
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 "
"terrain_ops='%s' terrain_op_paths='%s' "
"base=%.3f tunnel_columns=(%.3f,%.3f,%.3f) slab_columns=(%.3f,%.3f,%.3f)"),
OutSamples.FirstSolidPosition.X, OutSamples.FirstSolidPosition.Y,
OutSamples.FirstSolidPosition.Z, ChunkCoord.X, ChunkCoord.Y, ChunkCoord.Z,
*GeneratorName, Definition != nullptr && Definition->bUseOperatorStack ? 1 : 0,
bUsesOperatorStack ? 1 : 0, PassageSDF, SimulatedCarvedFinalDensity,
*GeneratorName, bUsesOperatorStack ? 1 : 0, PassageSDF, SimulatedCarvedFinalDensity,
*TerrainOps, *TerrainOpPaths, Params.BaseDensity, Params.ColumnDensity,
Params.ColumnMinRadius, Params.ColumnMaxRadius,
Definition != nullptr ? Definition->SlabParams.ColumnDensity : -1.0f,
@@ -549,7 +549,7 @@ bool FVoxelForgeLateralRegionsTest::RunTest(const FString& Parameters)
}
FTestWorld World;
World.Build(Seed, 2, false);
World.Build(Seed, 2);
int32 TopWorldZ = 0;
int32 BottomWorldZ = 0;
FVoxelStrateRegionManifest Manifest;
@@ -612,7 +612,7 @@ bool FVoxelForgeLateralRegionsTest::RunTest(const FString& Parameters)
}
FTestWorld World;
World.Build(Seed, 2, false);
World.Build(Seed, 2);
int32 TopWorldZ = 0;
int32 BottomWorldZ = 0;
FVoxelStrateRegionManifest Manifest;
@@ -661,7 +661,7 @@ bool FVoxelForgeLateralRegionsTest::RunTest(const FString& Parameters)
TestTrue(TEXT("found a single-region recipe identity case"), bRecipeIdentityChecked);
FTestWorld World;
World.Build(MultiSeed, 2, false);
World.Build(MultiSeed, 2);
if (!World.IsValid())
{
AddError(World.WhyInvalid());
@@ -955,7 +955,7 @@ bool FVoxelForgeLateralRegionsTest::RunTest(const FString& Parameters)
}
FTestWorld LawWorld;
LawWorld.Build(Seed, 2, false);
LawWorld.Build(Seed, 2);
FVoxelStrateRegionManifest LawManifest;
FVoxelOpStack LawStack;
FVoxelOpContext LawContext;
@@ -100,65 +100,44 @@ bool FVoxelForgeMazeSeamTest::RunTest(const FString& Parameters)
{
using namespace VoxelForgeTest;
FTestWorld LegacyWorld;
LegacyWorld.Build(7331, 2, false, 4);
FTestWorld StackWorld;
StackWorld.Build(7331, 2, true, 4);
if (!LegacyWorld.IsValid() || !StackWorld.IsValid())
FTestWorld World;
World.Build(7331, 2, 4);
if (!World.IsValid())
{
AddError(LegacyWorld.IsValid() ? StackWorld.WhyInvalid() : LegacyWorld.WhyInvalid());
AddError(World.WhyInvalid());
return false;
}
int32 LegacyTop = 0, LegacyBottom = 0;
int32 StackTop = 0, StackBottom = 0;
if (!LegacyWorld.GetSlotVoxelZRange(FTestWorld::SlotMaze, LegacyTop, LegacyBottom)
|| !StackWorld.GetSlotVoxelZRange(FTestWorld::SlotMaze, StackTop, StackBottom))
int32 Top = 0, Bottom = 0;
if (!World.GetSlotVoxelZRange(FTestWorld::SlotMaze, Top, Bottom))
{
AddError(TEXT("The seam fixture has no Maze slot."));
return false;
}
const int32 LegacyMidChunkZ = ((LegacyTop + LegacyBottom) / 2) / CHUNK_SIZE;
const int32 StackMidChunkZ = ((StackTop + StackBottom) / 2) / CHUNK_SIZE;
const FMazeGenerationParams LegacyParams = LegacyWorld.StrateManager->GetMazeParamsForChunk(
FIntVector(0, 0, LegacyMidChunkZ));
const FMazeGenerationParams StackParams = StackWorld.StrateManager->GetMazeParamsForChunk(
FIntVector(0, 0, StackMidChunkZ));
const int32 MidChunkZ = ((Top + Bottom) / 2) / CHUNK_SIZE;
const FMazeGenerationParams Params = World.StrateManager->GetMazeParamsForChunk(
FIntVector(0, 0, MidChunkZ));
TArray<FVector> LegacyProbes;
TArray<FVector> StackProbes;
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());
TArray<FVector> Probes;
AddMazeBoundaryProbes(Params, Bottom, Top, Probes);
int32 LegacyFirst = INDEX_NONE, StackFirst = INDEX_NONE;
float LegacyBefore = 0.0f, LegacyAfter = 0.0f;
float StackBefore = 0.0f, StackAfter = 0.0f;
const int32 LegacyMismatches = CountContextMismatches(
LegacyWorld, LegacyProbes, LegacyFirst, LegacyBefore, LegacyAfter);
const int32 StackMismatches = CountContextMismatches(
StackWorld, StackProbes, StackFirst, StackBefore, StackAfter);
int32 First = INDEX_NONE;
float Before = 0.0f, After = 0.0f;
const int32 Mismatches = CountContextMismatches(World, Probes, First, Before, After);
TestEqual(TEXT("legacy Maze has no chunk-context seam mismatches"), LegacyMismatches, 0);
TestEqual(TEXT("operator-stack Maze has no chunk-context seam mismatches"), StackMismatches, 0);
TestTrue(TEXT("Maze seam test sampled cell-boundary probes"), LegacyProbes.Num() > 0);
TestEqual(TEXT("Maze has no chunk-context seam mismatches"), Mismatches, 0);
TestTrue(TEXT("Maze seam test sampled cell-boundary probes"), Probes.Num() > 0);
if (LegacyFirst != INDEX_NONE)
if (First != INDEX_NONE)
{
AddError(FString::Printf(TEXT("Legacy Maze seam mismatch at probe %d: %.9g -> %.9g"),
LegacyFirst, LegacyBefore, LegacyAfter));
}
if (StackFirst != INDEX_NONE)
{
AddError(FString::Printf(TEXT("Operator Maze seam mismatch at probe %d: %.9g -> %.9g"),
StackFirst, StackBefore, StackAfter));
AddError(FString::Printf(TEXT("Maze seam mismatch at probe %d: %.9g -> %.9g"),
First, Before, After));
}
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."),
LegacyProbes.Num(), LegacyMismatches, StackMismatches));
Probes.Num(), Mismatches));
return true;
}
@@ -175,7 +154,7 @@ bool FVoxelForgeCaveWarpTileLodSeamTest::RunTest(const FString& Parameters)
using namespace VoxelForgeTest;
FTestWorld World;
World.Build(1337, 2, true, 4);
World.Build(1337, 2, 4);
if (!World.IsValid())
{
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);
};
// These are the eight builders currently shipped behind bUseOperatorStack. Keep the
// archetype list explicit so adding a builder without adding its validator assertion is
// visible in this test.
// These are the eight shipped archetype builders. Keep the archetype list explicit so adding
// a builder without adding its validator assertion is visible in this test.
int32 MidChunkZ = 0;
if (MidChunkForSlot(FTestWorld::SlotTunnelNetwork, TEXT("TunnelNetwork"), MidChunkZ))
@@ -140,7 +140,6 @@ namespace
Definition->SurfaceParams = Candidate.ArchetypeParams.SurfaceParams;
Definition->VerticalShaftParams = Candidate.ArchetypeParams.VerticalShaftParams;
Definition->FloatingIslandParams = Candidate.ArchetypeParams.FloatingIslandParams;
Definition->bUseOperatorStack = true;
Definition->TransitionType = EVoxelStrateTransition::Hard;
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
// single-region candidates without making a corpus asset edit look like a geometry failure.
FTestWorld World;
World.Build(DiagnosisSeed, 2, true, 8);
World.Build(DiagnosisSeed, 2, 8);
if (!World.IsValid())
{
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
// production asset remains untouched; this is a transient proof-only fixture configuration.
FTestWorld World;
World.Build(/*Seed*/1337, /*GapChunks*/2, /*bUseOperatorStack*/false,
/*InStrateHeightInChunks*/8);
World.Build(/*Seed*/1337, /*GapChunks*/2, /*InStrateHeightInChunks*/8);
if (!World.IsValid())
{
AddError(World.WhyInvalid());
@@ -1325,54 +1325,38 @@ bool FVoxelForgeVerticalShaftSeamTest::RunTest(const FString& Parameters)
{
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;
World.Build(/*InSeed=*/1337, /*InGapChunks=*/2);
if (!World.IsValid())
{
FTestWorld World;
World.Build(/*InSeed=*/1337, /*InGapChunks=*/2, bUseOperatorStack);
if (!World.IsValid())
{
AddError(FString::Printf(
TEXT("HARD FAILURE: could not build the %s seam-test fixture."),
bUseOperatorStack ? TEXT("operator-stack") : TEXT("legacy")));
bAllChecksPassed = false;
continue;
}
int32 Checked = 0;
int32 Mismatches = 0;
FString FirstMismatch;
if (!CheckVerticalShaftSeams(World, Checked, Mismatches, FirstMismatch))
{
AddError(FString::Printf(
TEXT("HARD FAILURE: the %s seam test sampled no VerticalShafts positions."),
bUseOperatorStack ? TEXT("operator-stack") : TEXT("legacy")));
bAllChecksPassed = false;
continue;
}
Summary += FString::Printf(
TEXT(" %s path: %d same-position re-evaluations, %d bit mismatches%s%s.\n"),
bUseOperatorStack ? TEXT("operator-stack") : TEXT("legacy"),
Checked,
Mismatches,
Mismatches > 0 ? TEXT("; first ") : TEXT(""),
Mismatches > 0 ? *FirstMismatch : TEXT(""));
if (Mismatches != 0)
{
AddError(FString::Printf(
TEXT("SEAM: %s VerticalShafts density changed after different chunk-context "
"orders: %s"),
bUseOperatorStack ? TEXT("operator-stack") : TEXT("legacy"),
*FirstMismatch));
bAllChecksPassed = false;
}
AddError(TEXT("HARD FAILURE: could not build the seam-test fixture."));
return false;
}
AddInfo(Summary);
return bAllChecksPassed;
int32 Checked = 0;
int32 Mismatches = 0;
FString FirstMismatch;
if (!CheckVerticalShaftSeams(World, Checked, Mismatches, FirstMismatch))
{
AddError(TEXT("HARD FAILURE: the seam test sampled no VerticalShafts positions."));
return false;
}
AddInfo(FString::Printf(
TEXT("VerticalShafts seam-freedom check (cell-boundary probes with two neighbouring "
"chunk-context orders): %d same-position re-evaluations, %d bit mismatches%s%s."),
Checked,
Mismatches,
Mismatches > 0 ? TEXT("; first ") : TEXT(""),
Mismatches > 0 ? *FirstMismatch : TEXT("")));
if (Mismatches != 0)
{
AddError(FString::Printf(
TEXT("SEAM: VerticalShafts density changed after different chunk-context orders: %s"),
*FirstMismatch));
return false;
}
return true;
}
bool FVoxelForgeStrateConnectivityTest::RunTest(const FString& Parameters)
@@ -6,9 +6,9 @@
// ---------------
// The interesting invariants (density purity across worker threads, ClassifyTile soundness)
// 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).
// Calling GetSurfaceDensity / GetMazeDensity directly bypasses every one of them and would
// test almost nothing. GetDensityAt in turn needs a live UVoxelStrateManager, whose only
// thread_local per-chunk caches live (CP_*, the prepared stacks, the diff slots, the SDF cache).
// Evaluating a stack directly bypasses every one of them and would test almost nothing.
// GetDensityAt in turn needs a live UVoxelStrateManager, whose only
// entry point is Initialize(UVoxelSettings*, int32) reading TSoftObjectPtr pools.
//
// 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.
*
* 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.
*/
struct FTestWorld
@@ -73,8 +73,7 @@ namespace VoxelForgeTest
* dangerous — and `VoxelForge.Determinism.LargeSeedSurvives` deliberately passes big ones
* (up to 2e9) to prove it stays that way.
*/
void Build(int32 InSeed = 1337, int32 InGapChunks = 2, bool bUseOperatorStack = false,
int32 InStrateHeightInChunks = 4)
void Build(int32 InSeed = 1337, int32 InGapChunks = 2, int32 InStrateHeightInChunks = 4)
{
Settings = TStrongObjectPtr<UVoxelSettings>(
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
// control; the owner-facing showcase opts into 8 chunks below.
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
// owns this chunk" ambiguous, and these tests want an unambiguous mapping.
Def->TransitionType = EVoxelStrateTransition::Hard;
@@ -154,72 +154,50 @@ bool FVoxelForgeTilePostReachProofTest::RunTest(const FString& Parameters)
return false;
}
FTestWorld NativeWorld;
NativeWorld.Build(/*Seed*/1337, /*GapChunks*/2, /*bUseOperatorStack*/false);
FTestWorld FusedWorld;
FusedWorld.Build(/*Seed*/1337, /*GapChunks*/2, /*bUseOperatorStack*/true);
if (!NativeWorld.IsValid() || !FusedWorld.IsValid())
FTestWorld World;
World.Build(/*Seed*/1337, /*GapChunks*/2);
if (!World.IsValid())
{
AddError(NativeWorld.IsValid() && FusedWorld.IsValid()
? TEXT("Unexpected invalid reach-proof fixture")
: (!NativeWorld.IsValid() ? NativeWorld.WhyInvalid() : FusedWorld.WhyInvalid()));
AddError(World.WhyInvalid());
return false;
}
TArray<FIntVector> NativeOrigins;
TArray<FIntVector> FusedOrigins;
AppendWorldReachTiles(NativeWorld, NativeOrigins);
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);
TArray<FIntVector> Origins;
AppendWorldReachTiles(World, Origins);
TestTrue(TEXT("the representative tile set is non-empty"), Origins.Num() > 0);
TStrongObjectPtr<UVoxelMarchingCubesMesher> NativeMesher(
TStrongObjectPtr<UVoxelMarchingCubesMesher> Mesher(
NewObject<UVoxelMarchingCubesMesher>(GetTransientPackage(), NAME_None, RF_Transient));
TStrongObjectPtr<UVoxelMarchingCubesMesher> FusedMesher(
NewObject<UVoxelMarchingCubesMesher>(GetTransientPackage(), NAME_None, RF_Transient));
NativeMesher->SetGenerator(NativeWorld.Generator.Get());
FusedMesher->SetGenerator(FusedWorld.Generator.Get());
Mesher->SetGenerator(World.Generator.Get());
VoxelGenLOD::ResetTilePostReachDiagnostics();
CVarGuard.SetScale(1.0f);
const FReachSnapshot NativeFull = RunReachTiles(*NativeMesher, NativeOrigins);
const FReachSnapshot FusedFull = RunReachTiles(*FusedMesher, FusedOrigins);
const uint64 FullFinalComparisons = NativeFull.FinalComparisons + FusedFull.FinalComparisons;
const uint64 FullFinalDifferences = NativeFull.FinalDifferences + FusedFull.FinalDifferences;
const FReachSnapshot Full = RunReachTiles(*Mesher, Origins);
TestTrue(TEXT("normal reach compares final fields on the representative tiles"),
FullFinalComparisons > 0);
TestEqual(TEXT("normal reach has no final-field differences"), FullFinalDifferences, 0ull);
TestEqual(TEXT("normal reach has no skipped-post differences"),
NativeFull.AllDifferences + FusedFull.AllDifferences, 0ull);
Full.FinalComparisons > 0);
TestEqual(TEXT("normal reach has no final-field differences"), Full.FinalDifferences, 0ull);
TestEqual(TEXT("normal reach has no skipped-post differences"), Full.AllDifferences, 0ull);
VoxelGenLOD::ResetTilePostReachDiagnostics();
// 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.
CVarGuard.SetScale(0.01f);
const FReachSnapshot NativeShrunk = RunReachTiles(*NativeMesher, NativeOrigins);
const FReachSnapshot FusedShrunk = RunReachTiles(*FusedMesher, FusedOrigins);
const uint64 ShrunkFinalComparisons =
NativeShrunk.FinalComparisons + FusedShrunk.FinalComparisons;
const uint64 ShrunkFinalDifferences =
NativeShrunk.FinalDifferences + FusedShrunk.FinalDifferences;
const FReachSnapshot Shrunk = RunReachTiles(*Mesher, Origins);
TestTrue(TEXT("shrunk reach still executes final-field comparisons"),
ShrunkFinalComparisons > 0);
Shrunk.FinalComparisons > 0);
TestTrue(TEXT("shrunk reach demonstrably fails the final-field proof"),
ShrunkFinalDifferences > 0);
Shrunk.FinalDifferences > 0);
AddInfo(FString::Printf(
TEXT("tile_post_reach_proof: native_tiles=%llu fused_tiles=%llu full_final=%llu/%llu "
"shrunk_final=%llu/%llu native_samples=%lld/%lld fused_samples=%lld/%lld."),
static_cast<unsigned long long>(NativeFull.Tiles),
static_cast<unsigned long long>(FusedFull.Tiles),
static_cast<unsigned long long>(FullFinalComparisons),
static_cast<unsigned long long>(FullFinalDifferences),
static_cast<unsigned long long>(ShrunkFinalComparisons),
static_cast<unsigned long long>(ShrunkFinalDifferences),
static_cast<long long>(NativeFull.DensitySamples),
static_cast<long long>(NativeShrunk.DensitySamples),
static_cast<long long>(FusedFull.DensitySamples),
static_cast<long long>(FusedShrunk.DensitySamples)));
TEXT("tile_post_reach_proof: tiles=%llu full_final=%llu/%llu shrunk_final=%llu/%llu "
"samples=%lld/%lld."),
static_cast<unsigned long long>(Full.Tiles),
static_cast<unsigned long long>(Full.FinalComparisons),
static_cast<unsigned long long>(Full.FinalDifferences),
static_cast<unsigned long long>(Shrunk.FinalComparisons),
static_cast<unsigned long long>(Shrunk.FinalDifferences),
static_cast<long long>(Full.DensitySamples),
static_cast<long long>(Shrunk.DensitySamples)));
return true;
}
@@ -67,26 +67,20 @@ bool FVoxelForgeWorldEdgeSealTest::RunTest(const FString& Parameters)
FailureKinds += Kind;
};
// The ordinary fixture deliberately uses the requested default, so existing op-stack and
// legacy tests exercise the setting without having to know about the new post.
FTestWorld Legacy;
Legacy.Build(/*Seed*/1337, /*GapChunks*/2, /*bUseOperatorStack*/false);
FTestWorld StackWorld;
StackWorld.Build(/*Seed*/1337, /*GapChunks*/2, /*bUseOperatorStack*/true);
// The ordinary fixture deliberately uses the requested default, so the other tests exercise
// the setting without having to know about the new post.
FTestWorld Bounded;
Bounded.Build(/*Seed*/1337, /*GapChunks*/2);
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()
? TEXT("Unexpected edge-seal fixture state")
: (!Legacy.IsValid() ? Legacy.WhyInvalid()
: (!StackWorld.IsValid() ? StackWorld.WhyInvalid() : Unbounded.WhyInvalid())));
AddError(!Bounded.IsValid() ? Bounded.WhyInvalid() : Unbounded.WhyInvalid());
return false;
}
SetEdgeSettings(Legacy, TestWorldRadius);
SetEdgeSettings(StackWorld, TestWorldRadius);
SetEdgeSettings(Bounded, TestWorldRadius);
SetEdgeSettings(Unbounded, 0.0f);
//==========================================================================
@@ -103,12 +97,12 @@ bool FVoxelForgeWorldEdgeSealTest::RunTest(const FString& Parameters)
for (int32 Slot = 0; Slot < 8; ++Slot)
{
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(Legacy.MidVoxelZ());
StrateZ.Add(Bounded.MidVoxelZ());
int32 NumPositiveSamples = 0;
for (const FVector& XY : FarXY)
@@ -117,8 +111,7 @@ bool FVoxelForgeWorldEdgeSealTest::RunTest(const FString& Parameters)
{
const FVector P(XY.X, XY.Y, Z);
++NumPositiveSamples;
if (!IsSolidMC(*Legacy.Generator, P)) Fail(TEXT("positive-legacy"));
if (!IsSolidMC(*StackWorld.Generator, P)) Fail(TEXT("positive-opstack"));
if (!IsSolidMC(*Bounded.Generator, P)) Fail(TEXT("positive"));
}
}
@@ -138,7 +131,7 @@ bool FVoxelForgeWorldEdgeSealTest::RunTest(const FString& Parameters)
for (const float Z : StrateZ)
{
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);
++NumInteriorComparisons;
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.
//==========================================================================
int32 TopShaftZ = 0, BottomShaftZ = 0;
if (!StackWorld.GetSlotVoxelZRange(FTestWorld::SlotVerticalShafts, TopShaftZ, BottomShaftZ))
if (!Bounded.GetSlotVoxelZRange(FTestWorld::SlotVerticalShafts, TopShaftZ, BottomShaftZ))
{
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 FVerticalShaftParams ShaftParams = StackWorld.StrateManager->GetVerticalShaftParamsForChunk(
const FVerticalShaftParams ShaftParams = Bounded.StrateManager->GetVerticalShaftParamsForChunk(
FIntVector(0, 0, MidChunkZ));
FVoxelOpStack Stack;
VoxelDensityOps::BuildVerticalShaftStack(
Stack, ShaftParams, StackWorld.Settings->Seed,
StackWorld.Generator->OriginSpineRadius, StackWorld.StrateManager.Get());
Stack, ShaftParams, Bounded.Settings->Seed,
Bounded.Generator->OriginSpineRadius, Bounded.StrateManager.Get());
FVoxelOpContext Ctx;
Ctx.ChunkCoord = FIntVector(0, 0, MidChunkZ);
Ctx.Seed = (uint32)StackWorld.Settings->Seed;
Ctx.LayoutVersion = StackWorld.StrateManager->GetLayoutVersion();
Ctx.Seed = (uint32)Bounded.Settings->Seed;
Ctx.LayoutVersion = Bounded.StrateManager->GetLayoutVersion();
Ctx.StrateTopWorldZ = ShaftParams.StrateTopWorldZ;
Ctx.StrateBottomWorldZ = ShaftParams.StrateBottomWorldZ;
Ctx.WorldRadiusVoxels = TestWorldRadius;
@@ -255,14 +248,14 @@ bool FVoxelForgeWorldEdgeSealTest::RunTest(const FString& Parameters)
for (int32 X = X0; X <= X1; ++X)
{
++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"));
}
}
}
// 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);
int32 NumTilesProved = (EdgeTile == EVoxelTileClass::AllSolid) ? 1 : 0;
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
// adjustment moves every generated mouth out of this small test rim.
FTestWorld GeneratedPassageWorld;
GeneratedPassageWorld.Build(/*Seed*/1337, /*GapChunks*/2, /*bUseOperatorStack*/false);
FTestWorld GeneratedPassageStackWorld;
GeneratedPassageStackWorld.Build(/*Seed*/1337, /*GapChunks*/2, /*bUseOperatorStack*/true);
GeneratedPassageWorld.Build(/*Seed*/1337, /*GapChunks*/2);
auto CheckGeneratedPassages = [&](FTestWorld& World, const TCHAR* FailureKind)
{
if (!World.IsValid())
@@ -310,7 +301,6 @@ bool FVoxelForgeWorldEdgeSealTest::RunTest(const FString& Parameters)
}
};
CheckGeneratedPassages(GeneratedPassageWorld, TEXT("generated-passage-breaches-rim"));
CheckGeneratedPassages(GeneratedPassageStackWorld, TEXT("generated-opstack-passage-breaches-rim"));
// One sorted summary block; individual samples intentionally never log.
AddInfo(FString::Printf(
@@ -319,7 +309,7 @@ bool FVoxelForgeWorldEdgeSealTest::RunTest(const FString& Parameters)
TEXT(" classify_tile_proved=%d\n")
TEXT(" interior_comparisons=%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_max_adjacent_solidness_jump=%.6f (threshold=%.6f)\n")
TEXT(" voxels_checked=%lld\n")
+131 -264
View File
@@ -16,7 +16,7 @@
#include "VoxelWormField.h"
#include "VoxelDensityAblation.h" // fingerprinted development-only stage measurements
#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 "VoxelStats.h"
#include "VoxelDensityProfile.h"
@@ -669,9 +669,9 @@ struct FVoxelDensityBlockSession
return;
}
// Allocate lazily on the first operator-stack sample. Legacy archetypes still use the
// ordinary scalar generator and should not pay for a block scratch lattice merely because
// the mesher opened the scoped hand-off.
// Allocate lazily on the first operator-stack sample. Chunks that never reach a stack
// (gaps, out-of-layout, degenerate strates) use the ordinary scalar path and should not
// pay for a block scratch lattice merely because the mesher opened the scoped hand-off.
BlockScratch.Reset();
}
@@ -2549,12 +2549,11 @@ namespace
/**
* 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,
* ou **strate dégénérée**. Ce dernier cas n'est pas de la prudence : cinq fonctions
* d'archétype court-circuitent sur `return 1.0f` (= air) quand la hauteur est nulle,
* et la pile n'a pas cet early-out, par conception. L'appelant retombe sur le `switch`,
* qui EST le comportement de référence. (`GetDensityWithParams`, lui, n'a aucun
* early-out de ce genre : TunnelNetwork/Underwater n'ont donc pas cette garde.)
* @return false quand la pile NE DOIT PAS être utilisée — archétype inconnu, params absents,
* ou **strate dégénérée** (hauteur nulle). La pile n'a pas d'early-out pour ce dernier
* cas, par conception : l'appelant rend de l'air (`GetDensityAt`) ou refuse de prouver
* la tuile (`ClassifyTile`). (`GetDensityWithParams` n'a aucun early-out de ce genre :
* TunnelNetwork/Underwater n'ont donc pas cette garde.)
*/
bool VF_BuildOpStackForChunk(ECaveGeneratorType Type, FVoxelStackParamRefs& Refs,
int32 Seed, float SpineRadius, const UVoxelStrateManager* SM,
@@ -2572,8 +2571,8 @@ namespace
case ECaveGeneratorType::FlatPlain:
case ECaveGeneratorType::CrystalChamber:
// UN SEUL cas pour les deux, comme le `switch` de production : `GetSlabDensity` ne les
// distingue pas non plus. Voir BuildSlabStack.
// UN SEUL cas pour les deux : ils ne diffèrent que par leurs valeurs par défaut.
// Voir BuildSlabStack.
if (!Refs.Slab) { return false; }
if (Refs.Slab->StrateTopWorldZ - Refs.Slab->StrateBottomWorldZ <= 0.0f) { return false; }
OutCtx.StrateTopWorldZ = Refs.Slab->StrateTopWorldZ;
@@ -2621,9 +2620,8 @@ namespace
return true;
default:
// `UsesOperatorStackForChunk` ne rend true que pour les archétypes portés (les 8), donc
// on ne devrait jamais arriver ici. Si ça arrive : retomber sur le `switch` plutôt que
// générer du vide — un monde faux est pire qu'un monde non porté.
// `UsesOperatorStackForChunk` ne rend true que pour les 8 archétypes, donc on ne devrait
// jamais arriver ici (valeur d'enum invalide).
return false;
}
}
@@ -2842,11 +2840,6 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
thread_local FBiomeContext CP_BiomeCtx;
thread_local FChunkBiomeCache CP_BiomeCache;
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
// 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
@@ -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.
thread_local bool CP_UsesTileCacheWindow = false;
// 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
// la strate n'a pas coché `bUseOperatorStack` ET que son archétype n'est pas porté.
// params (donc même clé owner+chunk+version, aucune logique d'invalidation en plus). Vide
// seulement pour une strate dégénérée (hauteur nulle).
thread_local FVoxelOpStack CP_OpStack;
thread_local bool CP_UseOpStack = false;
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
// 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
// room-graph cache remains keyed by the real XY search window. Legacy tunnel states keep
// the full XYZ key because their native cache is built for one XY window.
// room-graph cache remains keyed by the real XY search window. Chunks outside the strate
// layout (constant air) keep the full XYZ key.
const ECaveGeneratorType QueryGeneratorType = bUseChunkRoutingCache
? CP_RouteGeneratorType
: StrateManager->GetGeneratorTypeForChunk(ChunkCoord);
@@ -3066,7 +3059,6 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
CP_Maze = StrateManager->GetMazeParamsForChunk(ChunkCoord); break;
case ECaveGeneratorType::SurfaceWorld:
ResolveSurfaceChunkParams(ChunkCoord, CP_Surface, CP_BiomeCtx, CP_SurfaceBiomeParams);
CP_StrateKey = FMath::RoundToInt(CP_Surface.StrateBottomWorldZ);
break;
case ECaveGeneratorType::VerticalShafts:
CP_Vert = StrateManager->GetVerticalShaftParamsForChunk(ChunkCoord); break;
@@ -3085,131 +3077,112 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
}
CP_Dist = StrateManager->GetDisturbanceParamsForChunk(ChunkCoord);
// ── OPSTACK : (re)construire la pile si cette strate l'a demandée. ──
// Une seule branche ajoutée au chemin densité, et elle est FROIDE : la construction est
// par chunk (comme le refetch de params juste au-dessus), jamais par voxel.
CP_UseOpStack = bUseChunkRoutingCache
? CP_RouteUsesOperatorStack
: StrateManager->UsesOperatorStackForChunk(ChunkCoord);
CP_UseOpStack = CP_UseOpStack || CP_UseCustomRecipe;
// ── OPSTACK : (re)construire la pile de ce chunk. ──
// Toute strate est générée par une pile d'opérateurs. La construction est par chunk
// (comme le refetch de params juste au-dessus), jamais par voxel.
// Every strate is generated by an operator stack. An editor composer candidate or a
// cooked-season recipe supplies its own stack; otherwise — or if that stack cannot be
// built — the archetype's native stack is used.
CP_OpStack = FVoxelOpStack(); // move-assign : libère l'ancienne pile
CP_UseOpStack = false;
#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)
if (CP_UseComposerRegions)
{
CP_OpStack = FVoxelOpStack(); // move-assign : libère l'ancienne pile
bool bBuiltSpecialStack = false;
#if WITH_EDITOR
if (CP_UseComposerRegions)
FVoxelOpContext RegionContext;
if (VF_BuildStrateRegionStack(
CP_ComposerRegions, OriginSpineRadius, StrateManager,
CP_OpStack, RegionContext, nullptr))
{
bBuiltSpecialStack = true;
FVoxelOpContext RegionContext;
if (VF_BuildStrateRegionStack(
CP_ComposerRegions, OriginSpineRadius, StrateManager,
CP_OpStack, RegionContext, nullptr))
{
RegionContext.ChunkCoord = ChunkCoord;
RegionContext.Step = 1;
RegionContext.LayoutVersion = LayoutVersion;
RegionContext.WorldRadiusVoxels = WorldRadiusVoxels;
RegionContext.EdgeSealThickness = EdgeSealThickness;
CP_OpStack.PrepareChunk(RegionContext);
}
else
{
CP_UseOpStack = false;
}
RegionContext.ChunkCoord = ChunkCoord;
RegionContext.Step = 1;
RegionContext.LayoutVersion = LayoutVersion;
RegionContext.WorldRadiusVoxels = WorldRadiusVoxels;
RegionContext.EdgeSealThickness = EdgeSealThickness;
CP_OpStack.PrepareChunk(RegionContext);
CP_UseOpStack = true;
}
#endif
if (!bBuiltSpecialStack)
else
{
if (CP_UseCustomRecipe)
{
FVoxelOpContext RecipeContext;
if (VF_BuildStackFromRecipe(
CP_CustomRecipe, CP_CustomParams, CP_CustomRecipeSeed,
OriginSpineRadius, StrateManager, CP_OpStack, RecipeContext, nullptr))
{
RecipeContext.ChunkCoord = ChunkCoord;
RecipeContext.Step = 1;
RecipeContext.LayoutVersion = LayoutVersion;
// The recipe builder supplies the candidate's vertical seal to the
// structural post. The XY edge seal is global and uses live settings.
RecipeContext.WorldRadiusVoxels = WorldRadiusVoxels;
RecipeContext.EdgeSealThickness = EdgeSealThickness;
CP_OpStack.PrepareChunk(RecipeContext);
}
else
{
// Import/load validates recipes before a season becomes active. Keep
// the fallback hole-safe if a future schema changes underneath it.
CP_UseOpStack = false;
}
}
else
{
FVoxelOpContext OpCtx;
OpCtx.ChunkCoord = ChunkCoord;
OpCtx.Seed = (uint32)Seed;
OpCtx.LayoutVersion = LayoutVersion;
OpCtx.WorldRadiusVoxels = WorldRadiusVoxels;
OpCtx.EdgeSealThickness = EdgeSealThickness;
CP_OpStack = FVoxelOpStack();
CP_UseComposerRegions = false;
}
}
#endif
if (!CP_UseOpStack && CP_UseCustomRecipe)
{
FVoxelOpContext RecipeContext;
if (VF_BuildStackFromRecipe(
CP_CustomRecipe, CP_CustomParams, CP_CustomRecipeSeed,
OriginSpineRadius, StrateManager, CP_OpStack, RecipeContext, nullptr))
{
RecipeContext.ChunkCoord = ChunkCoord;
RecipeContext.Step = 1;
RecipeContext.LayoutVersion = LayoutVersion;
// The recipe builder supplies the candidate's vertical seal to the
// structural post. The XY edge seal is global and uses live settings.
RecipeContext.WorldRadiusVoxels = WorldRadiusVoxels;
RecipeContext.EdgeSealThickness = EdgeSealThickness;
CP_OpStack.PrepareChunk(RecipeContext);
CP_UseOpStack = true;
}
else
{
// Import/load validates recipes before a season becomes active. If a future
// schema still fails here, fall back to the archetype's native stack below.
CP_OpStack = FVoxelOpStack();
CP_UseCustomRecipe = false;
}
}
if (!CP_UseOpStack)
{
FVoxelOpContext OpCtx;
OpCtx.ChunkCoord = ChunkCoord;
OpCtx.Seed = (uint32)Seed;
OpCtx.LayoutVersion = LayoutVersion;
OpCtx.WorldRadiusVoxels = WorldRadiusVoxels;
OpCtx.EdgeSealThickness = EdgeSealThickness;
// ⚠️ LE MAPPING VIT DANS `VF_BuildOpStackForChunk` (haut de ce fichier) ET NULLE
// PART AILLEURS — `ClassifyTile` appelle la MÊME fabrique. Un verdict de tuile issu
// d'une pile construite autrement serait un trou. Ici on ne fait que fournir les
// params déjà cherchés juste au-dessus.
FVoxelStackParamRefs Refs;
Refs.Slab = &CP_Slab;
Refs.Maze = &CP_Maze;
Refs.Vert = &CP_Vert;
Refs.Float = &CP_Float;
Refs.Tunnel = &CP_Tunnel;
FVoxelStackParamRefs Refs;
Refs.Slab = &CP_Slab;
Refs.Maze = &CP_Maze;
Refs.Vert = &CP_Vert;
Refs.Float = &CP_Float;
Refs.Tunnel = &CP_Tunnel;
// SurfaceWorld : le champ de biomes est fabriqué ICI, du côté qui connaît le
// générateur, et TRANSFÉRÉ à la pile. L'opérateur ne voit qu'une `IVoxelBiomeField`,
// ce qui lui permet de devenir un asset en Phase 3 sans traîner le générateur.
TArray<FSurfaceGenerationParams> PerBiome;
if (CP_GenType == ECaveGeneratorType::SurfaceWorld)
{
Refs.Surface = &CP_Surface;
if (CP_BiomeCtx.IsValid() && CP_SurfaceBiomeParams.Num() > 0)
{
PerBiome = CP_SurfaceBiomeParams;
Refs.SurfaceBiomeParams = &PerBiome;
Refs.BiomeField = MakeUnique<FGeneratorBiomeField>(
this, &CP_BiomeCtx, &CP_BiomeCache, ChunkCoord.Z);
}
}
CP_UseOpStack = VF_BuildOpStackForChunk(
CP_GenType, Refs, Seed, OriginSpineRadius,
StrateManager, CP_OpStack, OpCtx);
// Le test appelle PrepareChunk, pas la production : c'est exactement la divergence
// qui rend un opérateur vert en test et faux en jeu. Les sept `PrepareChunk`
// actuels sont vides, donc ceci ne change RIEN aujourd'hui — c'est le point : le
// 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); }
TArray<FSurfaceGenerationParams> PerBiome;
if (CP_GenType == ECaveGeneratorType::SurfaceWorld)
{
Refs.Surface = &CP_Surface;
if (CP_BiomeCtx.IsValid() && CP_SurfaceBiomeParams.Num() > 0)
{
PerBiome = CP_SurfaceBiomeParams;
Refs.SurfaceBiomeParams = &PerBiome;
Refs.BiomeField = MakeUnique<FGeneratorBiomeField>(
this, &CP_BiomeCtx, &CP_BiomeCache, ChunkCoord.Z);
}
}
CP_UseOpStack = VF_BuildOpStackForChunk(
CP_GenType, Refs, Seed, OriginSpineRadius,
StrateManager, CP_OpStack, OpCtx);
// PrepareChunk runs here exactly as in the tests, so an op that hoists real
// per-chunk work behaves the same in game. `Step` reste 1 : GetDensityAt ne connaît
// pas le pas d'échantillonnage du mesher (voir le contrat T2.b dans VoxelDensityOp.h).
if (CP_UseOpStack) { CP_OpStack.PrepareChunk(OpCtx); }
}
// The legacy path needs its prepared native tunnel-core cache. An operator-stack
// tunnel source publishes the same world-space result after EvalMC, so preparing a
// second full graph/core cache here would only duplicate the work that made coarse
// streaming expensive in the first place. Custom/composer paths retain the legacy
// cache unless their own stack supplies the hand-off.
// An operator-stack tunnel source publishes the world-space core after EvalMC, so
// preparing a second full graph/core cache here would only duplicate the work that made
// coarse streaming expensive in the first place. Only an editor composer region stack,
// which does not publish that hand-off, still gets the prepared native core.
const bool bNativeTunnelCore = !CP_UseCustomRecipe
&& (CP_GenType == ECaveGeneratorType::TunnelNetwork
|| CP_GenType == ECaveGeneratorType::Underwater);
@@ -3294,12 +3267,9 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
FTunnelCoreWorldEvaluation FusedTunnelCore;
bool bRoomOwnsBottom = 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
// MC (négatif = solide) comme les fonctions d'archétype, donc les disturbances et la couche
// de diff qui suivent ne voient aucune différence.
// MC (négatif = solide), que les disturbances et la couche de diff qui suivent attendent.
if (CP_UseOpStack)
{
const bool bCanUseFusedEvaluator =
@@ -3383,66 +3353,11 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
}
}
}
else switch (CP_GenType)
else
{
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
{
float TerrainZ, CeilSurf, OverhangAmp, DirX, DirY;
ComputeSurfaceColumn(WorldX, WorldY, ChunkCoord.Z, CP_Surface, CP_BiomeCtx,
CP_SurfaceBiomeParams, CP_BiomeCache, TerrainZ, CeilSurf,
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;
// Only a degenerate strate (zero height) has no stack. It is open air, like the
// archetype functions the stack replaced.
Result = 1.0f;
}
DensityCoreTimer.End();
@@ -3482,21 +3397,13 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
{
VoxelDensityProfile::FScopedTimer ProfileTimer(
VoxelDensityProfile::EBucket::TunnelCorePosts);
FVector RoomQueryPosition;
if (bNativeWarpedPositionValid)
FVector RoomQueryPosition(WorldX, WorldY, WorldZ);
if (CP_Tunnel.VerticalScale != 1.0f && CP_Tunnel.VerticalScale > 0.0f)
{
RoomQueryPosition = NativeWarpedPosition;
}
else
{
RoomQueryPosition = FVector(WorldX, WorldY, WorldZ);
if (CP_Tunnel.VerticalScale != 1.0f && CP_Tunnel.VerticalScale > 0.0f)
{
RoomQueryPosition.Z = WorldZ / CP_Tunnel.VerticalScale;
}
RoomQueryPosition = VoxelCaveMorphology::ApplyCaveWarp(
RoomQueryPosition, CP_Tunnel, static_cast<uint32>(Seed));
RoomQueryPosition.Z = WorldZ / CP_Tunnel.VerticalScale;
}
RoomQueryPosition = VoxelCaveMorphology::ApplyCaveWarp(
RoomQueryPosition, CP_Tunnel, static_cast<uint32>(Seed));
PreDisturbanceTunnelCore = VoxelCaveMorphology::EvaluateTunnelCoreWorld(
WorldX, WorldY, WorldZ, ActiveTunnelCoreCache->Cache, nullptr,
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
// 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
// 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::EBucket::DensityStructuralPosts);
VoxelDensityProfile::FScopedTimer StructuralTailTimer(
@@ -3695,21 +3602,13 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
{
VoxelDensityProfile::FScopedTimer ProfileTimer(
VoxelDensityProfile::EBucket::TunnelCorePosts);
FVector RoomQueryPosition;
if (bNativeWarpedPositionValid)
FVector RoomQueryPosition(WorldX, WorldY, WorldZ);
if (CP_Tunnel.VerticalScale != 1.0f && CP_Tunnel.VerticalScale > 0.0f)
{
RoomQueryPosition = NativeWarpedPosition;
}
else
{
RoomQueryPosition = FVector(WorldX, WorldY, WorldZ);
if (CP_Tunnel.VerticalScale != 1.0f && CP_Tunnel.VerticalScale > 0.0f)
{
RoomQueryPosition.Z = WorldZ / CP_Tunnel.VerticalScale;
}
RoomQueryPosition = VoxelCaveMorphology::ApplyCaveWarp(
RoomQueryPosition, CP_Tunnel, static_cast<uint32>(Seed));
RoomQueryPosition.Z = WorldZ / CP_Tunnel.VerticalScale;
}
RoomQueryPosition = VoxelCaveMorphology::ApplyCaveWarp(
RoomQueryPosition, CP_Tunnel, static_cast<uint32>(Seed));
TunnelCore = VoxelCaveMorphology::EvaluateTunnelCoreWorld(
WorldX, WorldY, WorldZ, ActiveTunnelCoreCache->Cache,
nullptr,
@@ -6823,7 +6722,7 @@ EVoxelTileClass UVoxelGenerator::ClassifyTileUncached(
bool bCanSolid = true; // "tout le treillis est solide" 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 ;
// 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
{
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 »).
// 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
// 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`,
// `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,
// 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
// classerait un champ que le mesher ne produira pas. C'est le même drapeau, lu au
// même endroit, que `GetDensityAt`.
// Condition 1 : le slot doit avoir une pile (une définition d'archétype connue).
// Sinon on classerait un champ que le mesher ne produira pas.
if (!StrateManager->UsesOperatorStackForChunk(CC))
{
// Attribution DIAGNOSTIQUE uniquement : distingue une strate cave entièrement
// 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);
}
}
INC_DWORD_STAT(STAT_VoxelForgeCaveBailNoStack);
return EVoxelTileClass::Mixed;
}
@@ -7223,16 +7095,11 @@ EVoxelTileClass UVoxelGenerator::ClassifyTileUncached(
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
// déclenché la tentative : un seul chunk hors pile invaliderait le verdict.
// Every chunk of the box must have a stack, not only the one that triggered the
// attempt (redundant with the Z pass today, kept hole-safe).
if (!StrateManager->UsesOperatorStackForChunk(CC))
{
// Le passage Z précédent a déjà accepté l'unique slot cave. Avec le layout actuel
// (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);
INC_DWORD_STAT(STAT_VoxelForgeCaveBailNoStack);
return EVoxelTileClass::Mixed;
}
@@ -7331,8 +7198,8 @@ EVoxelTileClass UVoxelGenerator::ClassifyTileUncached(
if (!VF_BuildOpStackForChunk(CaveType, Refs, Seed, OriginSpineRadius,
StrateManager, TileStack, OpCtx))
{
// Strate dégénérée ou archétype non porté : `GetDensityAt` retomberait sur le `switch`,
// donc la pile ne décrit pas ce que le mesher verra. Aucun verdict.
// Strate dégénérée : `GetDensityAt` y rend de l'air sans pile, donc la pile ne décrit
// pas ce que le mesher verra. Aucun verdict.
INC_DWORD_STAT(STAT_VoxelForgeCaveBailNoStack);
return EVoxelTileClass::Mixed;
}
-4
View File
@@ -10,10 +10,6 @@ DEFINE_STAT(STAT_VoxelForgeTilesSkippedAllAir);
DEFINE_STAT(STAT_VoxelForgeTilesMeshed);
DEFINE_STAT(STAT_VoxelForgeTilesOpStackSolid);
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_VoxelForgeCaveMixGap);
DEFINE_STAT(STAT_VoxelForgeCaveMixSurfaceWorld);
@@ -2,7 +2,6 @@
// Runtime strate layout generation and queries.
#include "VoxelStrateManager.h"
#include "CoreGlobals.h" // GIsAutomationTesting — the opt-in diagnostic stays quiet under tests
#include "VoxelSettings.h"
#include "VoxelSeasonAsset.h"
#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->bUseOperatorStack = SeasonStrate.bUsesRecipe;
Slot.Definition->StrateHeightInChunks = SeasonStrate.HeightInChunks;
Slot.Definition->GenerationParams = SeasonStrate.Params.TunnelNetworkParams;
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));
VoxelForgeStartupTrace::RecordEvent(TEXT("strate_slot"), FString::Printf(
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.TopChunkZ, Slot.BottomChunkZ, Slot.HeightInChunks,
Slot.Definition->bUseOperatorStack ? 1 : 0));
Slot.TopChunkZ, Slot.BottomChunkZ, Slot.HeightInChunks));
}
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);
}
// 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;
bOpenSurfaceEntry = Settings->bOpenSurfaceEntry;
OriginSpineRadius = bUseSeason
@@ -6281,30 +6205,13 @@ bool UVoxelStrateManager::UsesOperatorStackForChunk(const FIntVector& ChunkCoord
const int32 SlotIdx = FindSlotIndexForChunkZ(ChunkCoord.Z);
if (SlotIdx < 0 || !StrateLayout[SlotIdx].Definition) { return false; }
if (SeasonStrates.IsValidIndex(SlotIdx))
{
// A recipe is an explicit stack opt-in. Native fixed entries retain the authored/switch
// 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
// 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
// archetype in every case.
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é
// ignore le drapeau et retombe sur le `switch`, pour qu'on puisse cocher la case sur n'importe
// 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.
// LA LISTE DES ARCHÉTYPES — le seul endroit où elle est écrite.
// THE ARCHETYPE LIST, written down exactly once.
switch (Def->GeneratorType)
{
case ECaveGeneratorType::Maze: return true;
@@ -6327,16 +6234,13 @@ bool UVoxelStrateManager::UsesOperatorStackForChunk(const FIntVector& ChunkCoord
case ECaveGeneratorType::Underwater:
// ⚠️ 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é).
case ECaveGeneratorType::TunnelNetwork:
// Le plus gros : squelette SDF → douze modificateurs de détail → override d'op par salle,
// 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
// 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;
default: return false;
+20 -33
View File
@@ -1,34 +1,32 @@
// VoxelDensityOp.h
// 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
// UVoxelGenerator::GetDensityAt; the legacy archetype switch remains for non-opt-in strates.
// Design : Docs/archive/OPSTACK-PLAN.md. The contract is implemented by the stack path in
// UVoxelGenerator::GetDensityAt, which generates every strate.
//
// ─────────────────────────────────────────────────────────────────────────────────────────
// POURQUOI / WHY
// ─────────────────────────────────────────────────────────────────────────────────────────
// UVoxelGenerator::GetDensityAt est aujourd'hui un `switch` sur 8 ECaveGeneratorType, chacun
// possédant sa fonction de densité et son struct de params. Conséquences : une nouvelle idée de
// monde coûte ~6 sites d'édition, et surtout LES IDÉES NE PEUVENT PAS SE COMBINER — un archétype
// possède le voxel entier. On ne peut pas écrire « une strate de surface dont les montagnes
// contiennent un réseau de salles, avec des îles flottantes dans le vide au-dessus », à aucun prix.
// Chaque archétype (ECaveGeneratorType) est une composition d'opérateurs, pas une fonction de
// densité monolithique qui posséderait le voxel entier. Une idée de monde s'exprime en empilant
// sources, combinateurs et modificateurs — et les idées peuvent donc SE COMBINER : « une strate de
// surface dont les montagnes contiennent un réseau de salles, avec des îles flottantes au-dessus ».
//
// GetDensityAt is today a `switch` over 8 ECaveGeneratorType, each owning a bespoke density
// function and param struct. A new world idea costs ~6 edit sites, and — the real problem —
// IDEAS CANNOT COMBINE: one archetype owns the whole voxel.
// Each archetype (ECaveGeneratorType) is a composition of operators, not a monolithic density
// function that owns the whole voxel. A world idea is expressed by stacking sources, combiners and
// modifiers — so ideas can COMBINE.
//
// ─────────────────────────────────────────────────────────────────────────────────────────
// ⚠️ 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
// 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
// cela aurait reconstruit le switch avec des étapes en plus.
// champ EST — c'est le rôle des sources de champ ci-dessous.
//
// 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
// 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
// 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é.
@@ -56,9 +54,8 @@
// An SDF-only writer reports an SDF interval; a later converter folds that interval through its own
// response. Each operator answers for itself, so an unverified composition remains sound.
//
// La première version de cette interface n'avait qu'une direction numérique. Presque tout opérateur
// existant est UNIDIRECTIONNEL : il ne fait que creuser, ou que remplir. Cela suffit à reproduire
// GÉNÉRIQUEMENT chaque garde écrite à la main dans ClassifyTile :
// Presque tout opérateur est UNIDIRECTIONNEL : il ne fait que creuser, ou que remplir. La direction
// suffit donc à exprimer GÉNÉRIQUEMENT les gardes de ClassifyTile :
//
// « passages ⇒ bCanSolid = false » EST CarveOnly
// « ponts/arêtes ⇒ bCanAir = false » EST FillOnly
@@ -71,26 +68,16 @@
// it never lets an SDF writer impersonate a downstream neighbour.
//
// ─────────────────────────────────────────────────────────────────────────────────────────
// LE GROS LOT PERF : les strates de grotte ne sautent AUCUNE tuile aujourd'hui
// THE PERF PRIZE: cave strates skip ZERO tiles today
// CLASSIFYTILE
// ─────────────────────────────────────────────────────────────────────────────────────────
// ClassifyTile conserve ses preuves exactes pour les gaps de bedrock et SurfaceWorld ; les
// archétypes de cave opt-in passent maintenant par le même pliage `ClassifyBox`. Le post XY fournit
// en plus la première preuve globale de coque : une tuile entièrement dans la bande forcée peut être
// ClassifyTile garde ses preuves exactes pour les gaps de bedrock et SurfaceWorld ; les slots de
// cave passent par le pliage `ClassifyBox` de la pile même qui produit leur densité. Le post XY
// 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.
//
// **Traiter cela comme un livrable explicite de chaque portage, pas comme un effet de bord.**
//
// ─────────────────────────────────────────────────────────────────────────────────────────
// É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.
// 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.
//
// 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
+9 -21
View File
@@ -2,33 +2,21 @@
// 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.
//
// ⚠️ CECI ALIMENTE LE JEU, MAIS SEULEMENT SUR OPT-IN.
// `UVoxelGenerator::GetDensityAt` construit la pile par chunk et l'évalue à la place du `switch`
// 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 :
// ⚠️ CECI EST LE CHEMIN DE GÉNÉRATION DU JEU.
// `UVoxelGenerator::GetDensityAt` construit la pile par chunk pour chaque strate :
// **Maze, FlatPlain, CrystalChamber, SurfaceWorld, VerticalShafts, FloatingIslands, TunnelNetwork,
// Underwater (8 sur 8)**.
// Toute autre strate passe encore par le `switch`, inchangé.
// `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.
// Underwater (8 sur 8)**. `ClassifyTile` construit la même pile pour les archétypes de cave 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
// evaluates it instead of the switch only when UsesOperatorStackForChunk returns true (strate ticked
// bUseOperatorStack AND its archetype is ported — all 8). ClassifyTile uses the same stack for cave
// 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.
// THIS IS THE GAME'S GENERATION PATH. GetDensityAt builds the stack per chunk for every strate.
// ClassifyTile uses the same stack for cave archetypes; gaps and SurfaceWorld retain their exact
// hand-written proofs.
//
// POURQUOI CETTE FORME / WHY THIS SHAPE
// 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 ? »**
// (OPSTACK-PLAN §4). Chaque archétype porté est comparé bit à bit à l'original : la réponse est
// MESURÉE plutôt qu'une opinion.
// (OPSTACK-PLAN §4). Chaque archétype a été porté puis comparé bit à bit à sa fonction
// d'origine avant qu'elle ne soit retirée : la réponse a été MESURÉE plutôt qu'une opinion.
#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 ;
* 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
* 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).
-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 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("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 Gap"), STAT_VoxelForgeCaveMixGap, 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")
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
* 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
* 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
* @return The ECaveGeneratorType for the strate containing this chunk
@@ -302,12 +302,9 @@ public:
ECaveGeneratorType GetGeneratorTypeForChunk(const FIntVector& ChunkCoord) const;
/**
* True when this chunk's strate opts into the density OPERATOR STACK instead of the hardcoded
* archetype switch (`UVoxelStrateDefinition::bUseOperatorStack`).
*
* 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.
* True when this chunk lies in a strate slot whose archetype has an operator stack (all eight
* do). False outside the layout or for a slot without a definition. This predicate is the ONLY
* place the archetype list is written down.
*/
bool UsesOperatorStackForChunk(const FIntVector& ChunkCoord) const;