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:
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")
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in new issue
Block a user