refactor(generator): delete the pre-operator-stack archetype functions

With the operator stack as the only density path, the per-archetype
density functions have no caller left in the game:

- Delete UVoxelGenerator::GetSlabDensity, GetMazeDensity,
  GetSurfaceDensity, SurfaceDensityFromColumn, GetVerticalShaftDensity
  and GetFloatingIslandDensity (~1000 lines) with their declarations.
- Drop GetDensityWithParams' OutWarpedPosition out-parameter; only
  the native GetDensityAt branch removed in the previous commit used it.
- OpStack Slab/Maze/VerticalShaft/FloatingIsland and SurfaceHeight
  tests: remove the stack-vs-legacy comparisons; keep decomposition,
  finiteness, liveness, window-invariance, cache-cost and box-verdict
  checks (Shaft cache timing now measures the stack source).
- PassageLandsInOpenSpace: the source-only footing probe evaluates
  the archetype stack built without a strate manager instead of the
  deleted functions.
- Comments that cited the deleted functions now cite the operators
  (FSlabVoidSource, FLatticeCorridorSource, FIslandBlobSource, ...).
  GSurfColCache is documented as ClassifyTile's column cache.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01URDGLsu7c4nunqNFPJEFPU
This commit is contained in:
Claude committed 2026-09-28 16:04:51 +00:00
1 parent 8117d118eb
commit f83c4bd8c1
15 files changed
+253 -1698

No files matched your search

@@ -211,10 +211,10 @@ bool FVoxelForgeClassifyTileTest::RunTest(const FString& Parameters)
TestEqual(TEXT("no tile was classified uniform while containing a surface (a false verdict is a hole)"), TestEqual(TEXT("no tile was classified uniform while containing a surface (a false verdict is a hole)"),
NumHoles, 0); NumHoles, 0);
// ── Stabilité du verdict : ClassifyTile partage GSurfColCache avec GetDensityAt, donc le // ── Stabilité du verdict : le premier ClassifyTile a réchauffé son cache de colonnes et le
// brute-force ci-dessus a réchauffé les caches. Re-classifier doit rendre le MÊME verdict. // brute-force ci-dessus les caches de GetDensityAt. Re-classifier doit rendre le MÊME verdict.
// Verdict stability: ClassifyTile shares GSurfColCache with GetDensityAt, so the brute force // Verdict stability: the first ClassifyTile warmed its column cache and the brute force
// above warmed the caches. Re-classifying must yield the SAME verdict. // above warmed GetDensityAt's caches. Re-classifying must yield the SAME verdict.
for (const FTileSpec& Spec : ToVerify) for (const FTileSpec& Spec : ToVerify)
{ {
const EVoxelTileClass A = Gen->ClassifyTile(Spec.Origin, Spec.Step, Spec.Cells); const EVoxelTileClass A = Gen->ClassifyTile(Spec.Origin, Spec.Step, Spec.Cells);
@@ -13,8 +13,8 @@
// seam, and in multiplayer a world divergence. // seam, and in multiplayer a world divergence.
// //
// POURQUOI CE TEST EXISTE / WHY THIS TEST EXISTS: // POURQUOI CE TEST EXISTE / WHY THIS TEST EXISTS:
// ~30 caches thread_local à clé manuelle vivent sous GetDensityAt (CP_*, GSurfColCache, les // ~30 caches thread_local à clé manuelle vivent sous GetDensityAt (CP_*, les mémos par cellule
// slots de diff, le cache SDF). Chacun est correct exactement tant que sa CLÉ contient toutes // et par colonne des piles, les slots de diff, le cache SDF). Chacun est correct exactement tant que sa CLÉ contient toutes
// les entrées dont dépend la valeur cachée. Une entrée oubliée ne casse rien tout de suite : // les entrées dont dépend la valeur cachée. Une entrée oubliée ne casse rien tout de suite :
// elle produit une mauvaise valeur seulement quand le cache est chaud pour une AUTRE entrée — // elle produit une mauvaise valeur seulement quand le cache est chaud pour une AUTRE entrée —
// c'est-à-dire de façon intermittente, dépendante de l'ordre, et invisible en jeu jusqu'à ce // c'est-à-dire de façon intermittente, dépendante de l'ordre, et invisible en jeu jusqu'à ce
@@ -351,7 +351,7 @@ bool FVoxelForgeLiveEditInvalidationTest::RunTest(const FString& Parameters)
TEXT("STALE PARAMS (AUDIT C2): the SurfaceWorld heightfield params were tripled and the ") TEXT("STALE PARAMS (AUDIT C2): the SurfaceWorld heightfield params were tripled and the ")
TEXT("layout rebuilt, yet all %d probe densities are bit-identical. A per-chunk cache is ") TEXT("layout rebuilt, yet all %d probe densities are bit-identical. A per-chunk cache is ")
TEXT("still keyed on ChunkCoord alone and skipped its refetch. Suspects, in order: ") TEXT("still keyed on ChunkCoord alone and skipped its refetch. Suspects, in order: ")
TEXT("CP_Chunk/CP_Version in GetDensityAt, the GSurfColCache box key, and the ") TEXT("CP_Chunk/CP_Version in GetDensityAt, the FSurfaceColumnSource column memo key, and the ")
TEXT("FChunkBiomeCache validity box (which says nothing about the FBiomeContext its ") TEXT("FChunkBiomeCache validity box (which says nothing about the FBiomeContext its ")
TEXT("cells were classified against)."), Probes.Num())); TEXT("cells were classified against)."), Probes.Num()));
} }
@@ -8,23 +8,15 @@
// voudrait dire soit un canal par-voxel pour une propriété de COLONNE, soit un seul opérateur // voudrait dire soit un canal par-voxel pour une propriété de COLONNE, soit un seul opérateur
// opaque — ce que `OPSTACK-PLAN §2.5` appelle exactement l'échec du refactor. // opaque — ce que `OPSTACK-PLAN §2.5` appelle exactement l'échec du refactor.
// //
// D'où une seconde famille, `VoxelHeightOp.h`. **Ce test est ce qui dit si elle était une bonne // D'où une seconde famille, `VoxelHeightOp.h`, mesurée ici contre `ComputeSurfaceTerrainZ`.
// idée** — la même méthode que la Phase 1 a appliquée à la densité : décomposer, puis MESURER
// contre l'original, avant de construire par-dessus.
// //
// ⚠️ CE QUE CE TEST COUVRE // ⚠️ CE QUE CE TEST COUVRE
// ✅ la pile de HAUTEUR du sol, contre `ComputeSurfaceTerrainZ` (défauts puis tous les ops F20) ; // ✅ la pile de HAUTEUR du sol, contre `ComputeSurfaceTerrainZ` (défauts puis tous les ops F20) ;
// ✅ les canaux de densité courants : ordre, fenêtre et overhang ; // ✅ la pile de densité SurfaceWorld : décomposition, ordre des canaux, densité finie, overhang ;
// ✅ le mélange synthétique à poids zéro. // ✅ le mélange synthétique à poids zéro.
// //
// The old GetSurfaceDensity helper remains a differential diagnostic only: it does not carry the // LA BARRE : **bit à bit** pour les opérations en espace hauteur.
// current common structural post. The assertions target the height stack and the density stack // The bar is bit-identity for height-space operations.
// used by the owner path, so room ownership and the authored-floor removal are not mistaken for
// a failed height port.
//
// LA BARRE : **bit à bit** remains the rule for height-space operations. Density comparisons to
// the retired direct helper are reported, not asserted, because room ownership and the common
// MC post intentionally make those paths different.
#if WITH_DEV_AUTOMATION_TESTS #if WITH_DEV_AUTOMATION_TESTS
@@ -285,18 +277,9 @@ bool FVoxelForgeHeightStackTest::RunTest(const FString& Parameters)
// ÉTAPE 2a — LE PONT VERS L'ESPACE DENSITÉ // ÉTAPE 2a — LE PONT VERS L'ESPACE DENSITÉ
//========================================================================= //=========================================================================
// `FSurfaceColumnSource` consomme les DEUX piles de hauteur (sol + voûte) et rend une densité. // `FSurfaceColumnSource` consomme les DEUX piles de hauteur (sol + voûte) et rend une densité.
// La référence est `GetSurfaceDensity`, qui est exactement la variante **sans overhang** // Cette passe est la variante **sans overhang** et **sans biomes** ; l'overhang a sa propre
// (il passe `OverhangAmp = 0`) et **sans biomes** (ParamsD == ParamsN, poids 0) — donc la // passe juste en dessous. / No-overhang, no-biome variant; the overhang has its own pass below.
// comparaison est nette plutôt qu'approximative.
//
// ⚠️ Ce que ce bloc NE teste PAS, et qu'il ne faut pas croire testé : l'overhang et le mélange
// de biomes. Tous deux arrivent à l'étape 2b, avec le chemin CACHÉ pour référence — c'est le
// seul qui les calcule.
{ {
// ⚠️ `OverhangStrength = 0` EXPLICITEMENT : `GetSurfaceDensity` passe `OverhangAmp = 0`,
// donc il n'en calcule aucun. Comparer une pile qui en produit à une référence qui n'en
// produit pas ferait échouer le test pour la seule raison que la référence est incomplète.
// L'overhang a sa propre passe juste en dessous, avec le bon oracle.
FSurfaceGenerationParams P = AllOps; FSurfaceGenerationParams P = AllOps;
P.OverhangStrength = 0.0f; P.OverhangStrength = 0.0f;
@@ -318,8 +301,7 @@ bool FVoxelForgeHeightStackTest::RunTest(const FString& Parameters)
Ctx.StrateBottomWorldZ = P.StrateBottomWorldZ; Ctx.StrateBottomWorldZ = P.StrateBottomWorldZ;
Stack.PrepareChunk(Ctx); Stack.PrepareChunk(Ctx);
int32 NumDiff = 0, NumSideDisagree = 0, WorstIdx = -1, NumNonFinite = 0; int32 NumNonFinite = 0;
float WorstDelta = 0.0f;
FRandomStream Rng(5150); FRandomStream Rng(5150);
for (int32 i = 0; i < NumHeightSamples; ++i) for (int32 i = 0; i < NumHeightSamples; ++i)
@@ -327,44 +309,23 @@ bool FVoxelForgeHeightStackTest::RunTest(const FString& Parameters)
const float X = (float)Rng.RandRange(-4 * CHUNK_SIZE, 4 * CHUNK_SIZE); const float X = (float)Rng.RandRange(-4 * CHUNK_SIZE, 4 * CHUNK_SIZE);
const float Y = (float)Rng.RandRange(-4 * CHUNK_SIZE, 4 * CHUNK_SIZE); const float Y = (float)Rng.RandRange(-4 * CHUNK_SIZE, 4 * CHUNK_SIZE);
const float Z = (float)Rng.RandRange(BottomVoxelZ, TopVoxelZ); const float Z = (float)Rng.RandRange(BottomVoxelZ, TopVoxelZ);
if (!FMath::IsFinite(Stack.EvalMC(X, Y, Z)))
// ParamsD == ParamsN, poids 0 ⇒ une seule évaluation, pas de biomes.
const float Old = Gen->GetSurfaceDensity(X, Y, Z, P, P, 0.0f);
const float New = Stack.EvalMC(X, Y, Z);
if (!FMath::IsFinite(New))
{ {
++NumNonFinite; ++NumNonFinite;
} }
if (!BitEqual(Old, New))
{
++NumDiff;
const float Delta = FMath::Abs(Old - New);
if (Delta > WorstDelta) { WorstDelta = Delta; WorstIdx = i; }
}
if ((Old >= 0.0f) != (New >= 0.0f)) { ++NumSideDisagree; }
} }
AddInfo(FString::Printf(
TEXT("SurfaceWorld density legacy diagnostic: %d of %d samples differ from "
"GetSurfaceDensity (largest |delta| %.9g), %d side disagreements; "
"current stack non-finite samples=%d."),
NumDiff, NumHeightSamples, WorstDelta, NumSideDisagree, NumNonFinite));
TestEqual(TEXT("surface: current density stack has no non-finite samples"), TestEqual(TEXT("surface: current density stack has no non-finite samples"),
NumNonFinite, 0); NumNonFinite, 0);
} }
//========================================================================= //=========================================================================
// ÉTAPE 2b — L'OVERHANG, contre le SEUL oracle qui le calcule // ÉTAPE 2b — L'OVERHANG
//========================================================================= //=========================================================================
// `GetSurfaceDensity` passe `OverhangAmp = 0`. La seule référence est donc le chemin caché : // `ComputeSurfaceColumn` résout le gate et la direction amont par colonne ; la moitié des
// `ComputeSurfaceColumn` (qui résout le gate et la direction amont par colonne) suivi de // échantillons est placée DANS la fenêtre d'overhang qu'il rapporte, pour que l'op overhang de
// `SurfaceDensityFromColumn` (qui applique l'union par voxel). Les deux viennent d'être // la pile soit réellement exercé. / Half the samples are placed inside the overhang window that
// exposées pour ça. // ComputeSurfaceColumn reports, so the stack's overhang op is actually exercised.
//
// C'est aussi la passe qui vérifie le MÉMO DE COLONNE de `FSurfaceColumnSource` : l'op overhang
// lit la colonne produite par la source, et s'ils divergeaient d'un XY, l'union se ferait au
// mauvais endroit. Un mémo mal clé se verrait ici.
{ {
FSurfaceGenerationParams P = AllOps; FSurfaceGenerationParams P = AllOps;
P.OverhangStrength = 0.8f; P.OverhangStrength = 0.8f;
@@ -391,8 +352,7 @@ bool FVoxelForgeHeightStackTest::RunTest(const FString& Parameters)
TArray<FSurfaceGenerationParams> NoBiomeParams; TArray<FSurfaceGenerationParams> NoBiomeParams;
FChunkBiomeCache BiomeCache; FChunkBiomeCache BiomeCache;
int32 NumDiff = 0, NumSideDisagree = 0, NumInWindow = 0, NumNonFinite = 0; int32 NumInWindow = 0, NumNonFinite = 0;
float WorstDelta = 0.0f;
FRandomStream Rng(1337); FRandomStream Rng(1337);
for (int32 i = 0; i < NumHeightSamples; ++i) for (int32 i = 0; i < NumHeightSamples; ++i)
@@ -418,27 +378,16 @@ bool FVoxelForgeHeightStackTest::RunTest(const FString& Parameters)
Z = (float)Rng.RandRange(BottomVoxelZ, TopVoxelZ); Z = (float)Rng.RandRange(BottomVoxelZ, TopVoxelZ);
} }
const float Old = Gen->SurfaceDensityFromColumn(X, Y, Z, TerrainZ, CeilSurf, if (!FMath::IsFinite(Stack.EvalMC(X, Y, Z)))
Amp, DirX, DirY, P);
const float New = Stack.EvalMC(X, Y, Z);
if (!FMath::IsFinite(New))
{ {
++NumNonFinite; ++NumNonFinite;
} }
if (!BitEqual(Old, New))
{
++NumDiff;
WorstDelta = FMath::Max(WorstDelta, FMath::Abs(Old - New));
}
if ((Old >= 0.0f) != (New >= 0.0f)) { ++NumSideDisagree; }
} }
AddInfo(FString::Printf( AddInfo(FString::Printf(
TEXT("Overhang legacy diagnostic: %d of %d samples differ from " TEXT("Overhang: %d of %d samples deliberately inside the overhang window; current "
"SurfaceDensityFromColumn (largest |delta| %.9g), %d side disagreements, " "stack non-finite samples=%d."),
"%d deliberately inside the overhang window; current stack non-finite samples=%d."), NumInWindow, NumHeightSamples, NumNonFinite));
NumDiff, NumHeightSamples, WorstDelta, NumSideDisagree, NumInWindow, NumNonFinite));
TestEqual(TEXT("overhang: current density stack has no non-finite samples"), TestEqual(TEXT("overhang: current density stack has no non-finite samples"),
NumNonFinite, 0); NumNonFinite, 0);
@@ -108,36 +108,37 @@ bool FVoxelForgeMazeSeamTest::RunTest(const FString& Parameters)
return false; return false;
} }
int32 Top = 0, Bottom = 0; int32 MazeTopZ = 0, MazeBottomZ = 0;
if (!World.GetSlotVoxelZRange(FTestWorld::SlotMaze, Top, Bottom)) if (!World.GetSlotVoxelZRange(FTestWorld::SlotMaze, MazeTopZ, MazeBottomZ))
{ {
AddError(TEXT("The seam fixture has no Maze slot.")); AddError(TEXT("The seam fixture has no Maze slot."));
return false; return false;
} }
const int32 MidChunkZ = ((Top + Bottom) / 2) / CHUNK_SIZE; const int32 MazeMidChunkZ = ((MazeTopZ + MazeBottomZ) / 2) / CHUNK_SIZE;
const FMazeGenerationParams Params = World.StrateManager->GetMazeParamsForChunk( const FMazeGenerationParams MazeParams = World.StrateManager->GetMazeParamsForChunk(
FIntVector(0, 0, MidChunkZ)); FIntVector(0, 0, MazeMidChunkZ));
TArray<FVector> Probes; TArray<FVector> SeamProbes;
AddMazeBoundaryProbes(Params, Bottom, Top, Probes); AddMazeBoundaryProbes(MazeParams, MazeBottomZ, MazeTopZ, SeamProbes);
int32 First = INDEX_NONE; int32 FirstProbe = INDEX_NONE;
float Before = 0.0f, After = 0.0f; float FirstBefore = 0.0f, FirstAfter = 0.0f;
const int32 Mismatches = CountContextMismatches(World, Probes, First, Before, After); const int32 NumMismatches = CountContextMismatches(
World, SeamProbes, FirstProbe, FirstBefore, FirstAfter);
TestEqual(TEXT("Maze has no chunk-context seam mismatches"), Mismatches, 0); TestEqual(TEXT("Maze has no chunk-context seam mismatches"), NumMismatches, 0);
TestTrue(TEXT("Maze seam test sampled cell-boundary probes"), Probes.Num() > 0); TestTrue(TEXT("Maze seam test sampled cell-boundary probes"), SeamProbes.Num() > 0);
if (First != INDEX_NONE) if (FirstProbe != INDEX_NONE)
{ {
AddError(FString::Printf(TEXT("Maze seam mismatch at probe %d: %.9g -> %.9g"), AddError(FString::Printf(TEXT("Maze seam mismatch at probe %d: %.9g -> %.9g"),
First, Before, After)); FirstProbe, FirstBefore, FirstAfter));
} }
AddInfo(FString::Printf( AddInfo(FString::Printf(
TEXT("Maze seam freedom: %d boundary probes, mismatches=%d; " TEXT("Maze seam freedom: %d boundary probes, mismatches=%d; "
"local window=2x2x2 child nodes ({-1,0}^3), no wide collect."), "local window=2x2x2 child nodes ({-1,0}^3), no wide collect."),
Probes.Num(), Mismatches)); SeamProbes.Num(), NumMismatches));
return true; return true;
} }
@@ -1,30 +1,18 @@
// VoxelForgeOpStackIslandTest.cpp // VoxelForgeOpStackIslandTest.cpp
// FloatingIslands — le portage qui fait tourner la pile À L'ENVERS. // FloatingIslands — la pile qui tourne À L'ENVERS / the stack that runs BACKWARDS.
// FloatingIslands — the port that runs the stack BACKWARDS.
// //
// CE QUE CELUI-CI PROUVE EN PLUS DES AUTRES // Les autres archétypes partent de ROC et CREUSENT ; FloatingIslands part du VIDE et REMPLIT, avec
// `VerticalShaftEquivalence` a mesuré la réutilisation À L'IDENTIQUE : trois opérateurs de Maze // les MÊMES opérateurs au signe près (convention interne positif = solide) :
// repris sans une ligne de changement. Celui-ci mesure quelque chose de plus fort, et de plus
// risqué pour l'abstraction : **la réutilisation PAR INVERSION**.
//
// Les quatre archétypes déjà portés partent tous de ROC et CREUSENT. FloatingIslands part du VIDE
// et REMPLIT. Si l'axe abstrait choisi (le SIGNE de la densité, convention interne positif = solide)
// est le bon, alors les deux extrémités de la pile doivent être les MÊMES opérateurs au signe près :
// //
// FConstantFieldSource(+Base) ←→ FConstantFieldSource(-Base) // FConstantFieldSource(+Base) ←→ FConstantFieldSource(-Base)
// FSdfConvertOp(Sign = -1) ←→ FSdfConvertOp(Sign = +1) // FSdfConvertOp(Sign = -1) ←→ FSdfConvertOp(Sign = +1)
// //
// Et c'est le cas : le seul opérateur neuf de ce portage est le blob d'île. Un archétype qui se // Le seul opérateur propre à l'archétype est le blob d'île. / The only archetype-specific op is the
// réutilise en s'INVERSANT est une preuve plus forte qu'un archétype qui se réutilise à l'identique // island blob; the rest is Maze's rock + carve with the opposite sign.
// — le premier dit que l'abstraction a trouvé le bon axe, le second seulement que deux archétypes
// se ressemblaient.
// //
// ET LE VERDICT DE BOÎTE : c'est ici que `ClassifyBox` peut rendre **AllAir** pour la première fois // ET LE VERDICT DE BOÎTE : une strate d'îles flottantes est surtout vide, donc `ClassifyBox` doit
// de tout le plugin. Une strate d'îles flottantes est, par construction, surtout vide ; aucun // savoir rendre **AllAir** (`OPSTACK-DECOMPOSITION §7`). Le test compte AllSolid et AllAir
// archétype de grotte n'a jamais su prouver « tout air » (`OPSTACK-DECOMPOSITION §7`). Le test // SÉPARÉMENT, parce qu'un total agrégé masquerait exactement ce verdict-là.
// compte les deux verdicts SÉPARÉMENT, parce qu'un total agrégé masquerait exactement ce gain-là.
//
// LA BARRE : bit à bit, comme les autres depuis `FPSemantics = Precise` (AUDIT §C9/§C10).
#if WITH_DEV_AUTOMATION_TESTS #if WITH_DEV_AUTOMATION_TESTS
@@ -92,8 +80,8 @@ bool FVoxelForgeOpStackIslandTest::RunTest(const FString& Parameters)
if (P.StrateTopWorldZ - P.StrateBottomWorldZ <= 0.0f) if (P.StrateTopWorldZ - P.StrateBottomWorldZ <= 0.0f)
{ {
AddError(TEXT("The FloatingIslands strate has degenerate Z bounds, which sends ") AddError(TEXT("The FloatingIslands strate has degenerate Z bounds; GetDensityAt builds no ")
TEXT("GetFloatingIslandDensity down its early-out. The op stack has none by design.")); TEXT("stack for it. The op stack has no degenerate-strate early-out by design."));
return false; return false;
} }
@@ -127,7 +115,7 @@ bool FVoxelForgeOpStackIslandTest::RunTest(const FString& Parameters)
} }
//========================================================================= //=========================================================================
// 1. ÉQUIVALENCE // 1. LIVENESS — roche d'île et vide ouvert / island rock and open void
//========================================================================= //=========================================================================
// On compte SÉPARÉMENT le solide d'intérieur et le solide de seal : sur cet archétype la // On compte SÉPARÉMENT le solide d'intérieur et le solide de seal : sur cet archétype la
// quasi-totalité du volume est de l'air, donc un « N solides » agrégé serait dominé par les // quasi-totalité du volume est de l'air, donc un « N solides » agrégé serait dominé par les
@@ -135,74 +123,34 @@ bool FVoxelForgeOpStackIslandTest::RunTest(const FString& Parameters)
const float InnerBot = P.StrateBottomWorldZ + P.BoundarySealThickness; const float InnerBot = P.StrateBottomWorldZ + P.BoundarySealThickness;
const float InnerTop = P.StrateTopWorldZ - P.BoundarySealThickness; const float InnerTop = P.StrateTopWorldZ - P.BoundarySealThickness;
int32 NumDiff = 0, NumSideDisagree = 0, WorstIdx = -1;
int32 NumInsideIsland = 0, NumOpenVoid = 0; int32 NumInsideIsland = 0, NumOpenVoid = 0;
float WorstDelta = 0.0f;
for (int32 i = 0; i < NumIslandSamples; ++i) for (int32 i = 0; i < NumIslandSamples; ++i)
{ {
const float X = (float)Points[i].X, Y = (float)Points[i].Y, Z = (float)Points[i].Z; const float X = (float)Points[i].X, Y = (float)Points[i].Y, Z = (float)Points[i].Z;
const float Old = Gen->GetFloatingIslandDensity(X, Y, Z, P);
const float New = Stack.EvalMC(X, Y, Z); const float New = Stack.EvalMC(X, Y, Z);
const bool bInterior = (Z > InnerBot && Z < InnerTop); const bool bInterior = (Z > InnerBot && Z < InnerTop);
if (bInterior && Old < 0.0f) { ++NumInsideIsland; } // solide loin des seals ⇒ une île if (bInterior && New < 0.0f) { ++NumInsideIsland; } // solide loin des seals ⇒ une île
if (bInterior && Old >= 0.0f) { ++NumOpenVoid; } if (bInterior && New >= 0.0f) { ++NumOpenVoid; }
if (!BitEqual(Old, New))
{
++NumDiff;
const float D = FMath::Abs(Old - New);
if (D > WorstDelta) { WorstDelta = D; WorstIdx = i; }
}
if ((Old >= 0.0f) != (New >= 0.0f)) { ++NumSideDisagree; }
} }
AddInfo(FString::Printf(
if (NumDiff == 0) TEXT("FloatingIslands: %d of %d samples inside island rock away from the seal bands, %d in ")
{ TEXT("open void."),
AddInfo(FString::Printf( NumInsideIsland, NumIslandSamples, NumOpenVoid));
TEXT("FloatingIslands: bit-identical across %d samples (%d inside island rock away from ")
TEXT("the seal bands, %d in open void, so the void source, the blobs, the roughness and ")
TEXT("the fill were all exercised). The stack runs BACKWARDS -- void source + fill ")
TEXT("instead of rock source + carve -- using the SAME operators with the opposite ")
TEXT("sign. Only the blob source is new (OPSTACK-PLAN 2.5)."),
NumIslandSamples, NumInsideIsland, NumOpenVoid));
}
else
{
AddError(FString::Printf(
TEXT("FloatingIslands: %d of %d samples differ (largest |delta| %.9g at (%.0f, %.0f, ")
TEXT("%.0f)); %d cross the isosurface. Since /fp:precise the bar is bit-identity, so ")
TEXT("this is a real port error. Check, in order: the C1 warp fix (BOTH paths must now ")
TEXT("use VoxelHash::SeedOffset(S, 0.0007f) -- if only one was changed, EVERY warped ")
TEXT("sample differs), then the SdfConvert SIGN (+1 fills, -1 carves), then the 'Isld' ")
TEXT("salt (0x49736C64), the roughness frequency (0.08 / 4 octaves here, NOT Maze's ")
TEXT("0.12 / 3), the per-island TaperEnd and TopFlatten dome branch, and the ")
TEXT("SmoothMin blend K = max(SDFBlendRadius, 0.01)."),
NumDiff, NumIslandSamples, WorstDelta,
WorstIdx >= 0 ? Points[WorstIdx].X : 0.0f,
WorstIdx >= 0 ? Points[WorstIdx].Y : 0.0f,
WorstIdx >= 0 ? Points[WorstIdx].Z : 0.0f,
NumSideDisagree));
}
TestEqual(TEXT("no sample lands on the opposite side of the isosurface"), NumSideDisagree, 0);
if (NumInsideIsland == 0) if (NumInsideIsland == 0)
{ {
AddWarning(TEXT("No sample landed inside island rock away from the seal bands, so the blob ") AddWarning(TEXT("No sample landed inside island rock away from the seal bands, so the blob ")
TEXT("source and the fill were never meaningfully exercised -- the equivalence ") TEXT("source and the fill were never meaningfully exercised. Raise IslandDensity ")
TEXT("above then only proves that two empty voids agree. Raise IslandDensity or ") TEXT("or IslandMaxRadius."));
TEXT("IslandMaxRadius."));
} }
//========================================================================= //=========================================================================
// 2. INVARIANCE DE FENÊTRE // 2. INVARIANCE DE FENÊTRE
//========================================================================= //=========================================================================
// La source garde un cache 3×3 `thread_local` dont la clé est le jeu de params — et cette clé // La source garde un cache 3×3 `thread_local` dont la clé est le jeu de params — et cette clé
// inclut délibérément `BoundarySealThickness`, que l'original omet alors que `SpreadZ` le lit // inclut délibérément `BoundarySealThickness`, que `SpreadZ` lit (voir la note dans
// (voir la note dans FIslandBlobSource::GetCells). // FIslandBlobSource::GetCells).
{ {
std::atomic<int32> Impure{ 0 }; std::atomic<int32> Impure{ 0 };
const int32 NumBlocks = FMath::Max(4, FMath::Min(16, FPlatformMisc::NumberOfCores())); const int32 NumBlocks = FMath::Max(4, FMath::Min(16, FPlatformMisc::NumberOfCores()));
@@ -296,9 +244,8 @@ bool FVoxelForgeOpStackIslandTest::RunTest(const FString& Parameters)
AddInfo(FString::Printf( AddInfo(FString::Printf(
TEXT("Box verdicts over 60 FloatingIslands tiles (XY sampled from +/- %d voxels = %.1f x ") TEXT("Box verdicts over 60 FloatingIslands tiles (XY sampled from +/- %d voxels = %.1f x ")
TEXT("IslandSpacing %.0f): %d proved AllSolid, %d proved AllAir, ") TEXT("IslandSpacing %.0f): %d proved AllSolid, %d proved AllAir, ")
TEXT("%d Mixed, %d voxels checked, %d violations. Today's ClassifyTile proves ZERO of these. The AllAir count is the new ") TEXT("%d Mixed, %d voxels checked, %d violations. The AllAir count matters most: a ")
TEXT("thing: no cave archetype has ever been able to prove 'all air', and a floating-") TEXT("floating-island strate is mostly open air (OPSTACK-DECOMPOSITION 7)."),
TEXT("island strate is mostly exactly that (OPSTACK-DECOMPOSITION 7)."),
SpanVoxels, (float)SpanVoxels / FMath::Max(P.IslandSpacing, 1.0f), P.IslandSpacing, SpanVoxels, (float)SpanVoxels / FMath::Max(P.IslandSpacing, 1.0f), P.IslandSpacing,
NumProvedSolid, NumProvedAir, NumMixed, NumBruteSamples, NumUnsound)); NumProvedSolid, NumProvedAir, NumMixed, NumBruteSamples, NumUnsound));
@@ -1,73 +1,17 @@
// VoxelForgeOpStackMazeTest.cpp // VoxelForgeOpStackMazeTest.cpp
// PHASE 1, LE TEST QUI COMPTE — la pile d'opérateurs Maze contre GetMazeDensity. // La pile d'opérateurs Maze / The Maze operator stack.
// PHASE 1'S LOAD-BEARING TEST — the Maze operator stack against GetMazeDensity.
// //
// CE QUE LA PHASE 1 DEVAIT PROUVER / WHAT PHASE 1 HAD TO PROVE // Ce que ce test vérifie / What this test checks:
// Le déclencheur d'arrêt de `OPSTACK-PLAN §4` : **« est-ce que la séparation source / modifier tombe // • TOPOLOGIE — le graphe parent orienté vers l'origine est un arbre couvrant (N-1 arêtes, zéro
// naturellement du code existant ? »** Réponse mesurée : oui. Maze se décompose en huit opérateurs // cellule déconnectée), les boucles optionnelles restent bornées, et le graphe est déterministe
// sans contorsion, le SDF est reproduit BIT POUR BIT, et aucun échantillon ne change de côté de // sur 64 seeds. / The origin-directed parent graph is a spanning tree, deterministic per seed.
// l'isosurface. // • DÉCOMPOSITION — huit opérateurs (roche + couloirs + rugosité + carve + 4 structurels), pas un
// // `FMazeOp` monolithique. / Eight ops, not one wrapped monolith.
// ═════════════════════════════════════════════════════════════════════════════════════════ // • INVARIANCE DE FENÊTRE — le cache par cellule est `thread_local` : bit-identique quel que soit
// ✅ MISE À JOUR 2026-07-27 : LE PLANCHER ULP N'EXISTE PLUS. C'ÉTAIT `/fp:fast`. // l'ordre des requêtes et le thread. / Bit-identical across query order and worker threads.
// ═════════════════════════════════════════════════════════════════════════════════════════ // • VERDICTS DE BOÎTE — chaque verdict uniforme survit à la force brute (un faux verdict est un
// `FPSemantics = Precise` sur le module (AUDIT §C9, posé pour le cross-play Linux/Windows) fait // trou), y compris pour une pile source/convertisseur jamais écrite à la main.
// passer ce test à **BIT-IDENTIQUE**. La section ci-dessous décrit un état RÉVOLU ; elle est gardée // Every uniform box verdict survives brute force, including for a novel source/converter stack.
// parce qu'elle explique pourquoi les cinq expériences d'isolation avaient toutes échoué (sous
// `/fp:fast` le compilateur transforme selon le CONTEXTE — il n'y avait aucune variable à isoler)
// et parce qu'elle dit quoi regarder si la bit-identité régresse un jour.
//
// **Conséquence pratique : ce test est maintenant un instrument BEAUCOUP plus fin.** Le moindre
// écart est désormais une vraie trouvaille, pas du bruit à noter. La machinerie de gradation ULP
// est conservée exprès — c'est elle qui signalerait une régression du modèle flottant.
//
// UPDATE: the ULP floor is GONE — FPSemantics = Precise makes this test bit-identical. The section
// below describes a past state, kept because it explains why five isolation experiments all failed
// (under /fp:fast the compiler transforms by CONTEXT — there was no variable to isolate) and what to
// look at if bit-identity ever regresses.
//
// ─────────────────────────────────────────────────────────────────────────────────────────
// ⚠️ LE PLANCHER ULP (HISTORIQUE) — lire ceci avant de « corriger » un écart résiduel
// ─────────────────────────────────────────────────────────────────────────────────────────
// (HISTORIQUE, avant `FPSemantics = Precise`) la pile reproduisait `GetMazeDensity` à ~1-2 ULP près
// sur ~2 % des échantillons (ceux qui tombent
// dans la coquille de blend du SDF, où `Blend - Sdf` annule catastrophiquement et amplifie le
// dernier arrondi). **Zéro échantillon ne traverse l'isosurface**, donc pas un triangle ne bouge.
//
// L'origine exacte de ce dernier arrondi n'a PAS été identifiée, après six cycles de build et cinq
// hypothèses toutes réfutées par la mesure (aller-retour FVector · fenêtre de rugosité · `/fp:fast`
// entre unités de compilation · contexte d'inlining · constante de compilation vs donnée
// d'exécution). Ce qui EST établi par la mesure :
//
// • le SDF est bit-identique sur 126/126 des écarts — le treillis, les hashs, l'ensemble d'arêtes
// et `VoxelSDF::Capsule` sont donc exacts ;
// • l'écart naît entièrement dans la conversion SDF→densité, au dernier arrondi ;
// • il est DÉTERMINISTE (mêmes échantillons, même delta, même coordonnée à chaque run) ;
// • il ne dépend ni de l'unité de compilation, ni de l'inlining, ni du modèle flottant.
//
// **Décision (Jahni, 2026-07-27) : on l'accepte et on avance.** Aucune décision du projet ne dépend
// de la réponse, et la chasse coûtait plus que l'information. Consigné comme point ouvert dans
// `AUDIT-2026-07.md §C10`.
//
// ⚠️ LA RÈGLE QUI EN DÉCOULE, ELLE, EST IMPORTANTE :
// **ne jamais faire tourner les deux chemins (switch d'archétype et pile d'opérateurs) dans le même
// monde, et ne jamais comparer leurs sorties pour égalité.** Ce n'est PAS un risque de désync entre
// clients — dans un même binaire le champ est prouvé pur (`VoxelForge.Determinism.DensityPurity`,
// bit-identique entre threads et ordres de requête) et tous les pairs exécutent le même chemin. Mais
// une strate à moitié migrée produirait une couture. Le vrai sujet multijoueur est ailleurs :
// `AUDIT §C9` (le défaut FP d'UBT diffère selon la toolchain).
//
// Never run both paths in one world and never compare their outputs for equality. This is NOT a
// client-desync risk — within one binary the field is proven pure and every peer runs the same path —
// but a half-migrated strate would produce a seam.
//
// ─────────────────────────────────────────────────────────────────────────────────────────
// LA BARRE D'ACCEPTATION, ENCODÉE CI-DESSOUS / THE ACCEPTANCE BAR, ENCODED BELOW
// ─────────────────────────────────────────────────────────────────────────────────────────
// • ÉCHEC DUR : un seul échantillon qui change de côté de l'isosurface (la géométrie bouge).
// • INFO : des écarts à l'échelle de l'ULP (le plancher, attendu).
// • WARN : un écart plus grand — ÇA, c'est une vraie dérive de portage, et il faut chercher.
// Un test qui avertit à chaque portage serait ignoré par le portage qui compte.
#if WITH_DEV_AUTOMATION_TESTS #if WITH_DEV_AUTOMATION_TESTS
@@ -355,13 +299,13 @@ bool FVoxelForgeOpStackMazeTest::RunTest(const FString& Parameters)
return false; return false;
} }
// GetMazeDensity court-circuite sur une strate dégénérée (`return 1.0f`). Cette garde appartient // La pile suppose une strate valide ; `GetDensityAt` rend de l'air sans pile sur une strate
// à la fonction d'archétype, pas à un opérateur ; la pile suppose une strate valide. // dégénérée. / The stack assumes a valid strate; GetDensityAt returns air for a degenerate one.
if (MazeParams.StrateTopWorldZ - MazeParams.StrateBottomWorldZ <= 0.0f) if (MazeParams.StrateTopWorldZ - MazeParams.StrateBottomWorldZ <= 0.0f)
{ {
AddError(FString::Printf( AddError(FString::Printf(
TEXT("The Maze strate has degenerate Z bounds (top %.1f, bottom %.1f), which sends ") TEXT("The Maze strate has degenerate Z bounds (top %.1f, bottom %.1f); GetDensityAt ")
TEXT("GetMazeDensity down its early-out. The op stack has no such early-out by design."), TEXT("builds no stack for it. The op stack has no degenerate-strate early-out by design."),
MazeParams.StrateTopWorldZ, MazeParams.StrateBottomWorldZ)); MazeParams.StrateTopWorldZ, MazeParams.StrateBottomWorldZ));
return false; return false;
} }
@@ -485,70 +429,6 @@ bool FVoxelForgeOpStackMazeTest::RunTest(const FString& Parameters)
} }
} }
//=========================================================================
// ÉQUIVALENCE — géométrie d'abord, bits ensuite.
//=========================================================================
int32 NumDiff = 0, WorstIdx = -1, NumBeyondUlpNoise = 0, NumSolidDisagreements = 0;
float WorstDelta = 0.0f;
for (int32 i = 0; i < NumMazeSamples; ++i)
{
const float X = (float)Points[i].X, Y = (float)Points[i].Y, Z = (float)Points[i].Z;
const float Old = Gen->GetMazeDensity(X, Y, Z, MazeParams); // MC : négatif = solide
const float New = Stack.EvalMC(X, Y, Z);
if (!BitEqual(Old, New))
{
++NumDiff;
const float Delta = FMath::Abs(Old - New);
if (Delta > WorstDelta) { WorstDelta = Delta; WorstIdx = i; }
// `Blend - Sdf` annule catastrophiquement au bord de la coquille de blend, donc un
// écart d'ULP sur le SDF ressort amplifié sur la densité : marge généreuse, mais bornée.
const float UlpNoise = 16.0f * FMath::Max(FMath::Abs(Old), 1.0f) * FLT_EPSILON;
if (Delta > UlpNoise) { ++NumBeyondUlpNoise; }
}
// Le mesher ne lit que le SIGNE (D >= IsoLevel ⇒ air). Un désaccord de CÔTÉ bouge la géométrie.
if ((Old >= 0.0f) != (New >= 0.0f)) { ++NumSolidDisagreements; }
}
if (NumDiff == 0)
{
AddInfo(FString::Printf(TEXT("Bit-identical across %d samples."), NumMazeSamples));
}
else if (NumBeyondUlpNoise == 0)
{
AddInfo(FString::Printf(
TEXT("%d of %d samples differ, ALL at ULP scale (largest |delta| %.9g at (%.0f, %.0f, ")
TEXT("%.0f)), and 0 cross the isosurface -- not one triangle would move. This is the ")
TEXT("accepted floor; see the header comment and AUDIT-2026-07.md C10. The SDF itself is ")
TEXT("reproduced BIT FOR BIT, so the lattice, the hashes and VoxelSDF::Capsule are exact; ")
TEXT("only the final SDF->density rounding differs. Do not go hunting this again without ")
TEXT("reading C10 first -- five hypotheses have already been measured and refuted."),
NumDiff, NumMazeSamples, WorstDelta,
WorstIdx >= 0 ? Points[WorstIdx].X : 0.0f,
WorstIdx >= 0 ? Points[WorstIdx].Y : 0.0f,
WorstIdx >= 0 ? Points[WorstIdx].Z : 0.0f));
}
else
{
AddWarning(FString::Printf(
TEXT("%d of %d samples differ and %d are TOO LARGE to be the accepted ULP floor (largest ")
TEXT("|delta| %.9g at (%.0f, %.0f, %.0f)); %d cross the isosurface. THIS one is real port ")
TEXT("drift, not the known floor. Check, in order: the roughness apply-window ")
TEXT("(R + SurfaceRoughness + 2), the carve blend (2.0), the noise frequency (0.12) and ")
TEXT("octave count (3), and the order of the structural post ops."),
NumDiff, NumMazeSamples, NumBeyondUlpNoise, WorstDelta,
WorstIdx >= 0 ? Points[WorstIdx].X : 0.0f,
WorstIdx >= 0 ? Points[WorstIdx].Y : 0.0f,
WorstIdx >= 0 ? Points[WorstIdx].Z : 0.0f,
NumSolidDisagreements));
}
// Le SEUL échec dur : un désaccord de côté d'iso EST une différence de géométrie.
TestEqual(TEXT("no sample lands on the opposite side of the isosurface from the original"),
NumSolidDisagreements, 0);
//========================================================================= //=========================================================================
// INVARIANCE DE FENÊTRE — la pile doit tenir les mêmes règles que le générateur. // INVARIANCE DE FENÊTRE — la pile doit tenir les mêmes règles que le générateur.
//========================================================================= //=========================================================================
@@ -624,10 +504,9 @@ bool FVoxelForgeOpStackMazeTest::RunTest(const FString& Parameters)
//========================================================================= //=========================================================================
// LE VERDICT DE BOÎTE — le prix perf de l'intervalle SDF conservatif. // LE VERDICT DE BOÎTE — le prix perf de l'intervalle SDF conservatif.
//========================================================================= //=========================================================================
// ClassifyTile renvoie Mixed pour tout archétype de grotte, donc TunnelNetwork, Maze, // Pour un slot de cave, ClassifyTile plie `ClassifyBox` de cette pile : tout nombre > 0 ici est
// VerticalShafts, FloatingIslands, FlatPlain, CrystalChamber et Underwater ne captent RIEN du // une tuile que le mesher saute. / For a cave slot ClassifyTile folds this stack's ClassifyBox:
// gain T1.d. Tout nombre > 0 ici est le saut de tuile propre à la pile, pas une décision du // every proved box here is a tile the mesher skips.
// classifieur historique.
{ {
int32 NumProved = 0, NumMixed = 0, NumUnsound = 0, NumBruteSamples = 0; int32 NumProved = 0, NumMixed = 0, NumUnsound = 0, NumBruteSamples = 0;
FRandomStream Rng(24680); FRandomStream Rng(24680);
@@ -689,17 +568,14 @@ bool FVoxelForgeOpStackMazeTest::RunTest(const FString& Parameters)
AddInfo(FString::Printf( AddInfo(FString::Printf(
TEXT("Box verdicts over %d Maze tiles (XY sampled from +/- %d voxels = %.1f x ") TEXT("Box verdicts over %d Maze tiles (XY sampled from +/- %d voxels = %.1f x ")
TEXT("CellSize %.0f): %d proved uniform, %d Mixed, %d voxels checked, %d violations. Today's ClassifyTile ") TEXT("CellSize %.0f): %d proved uniform, %d Mixed, %d voxels checked, %d violations."),
TEXT("proves ZERO of these -- every cave archetype falls through to \"pas prouvable en ")
TEXT("v1\". Any number above zero here is tile-skipping Maze has never had."),
NumMazeBoxTests, SpanVoxels, (float)SpanVoxels / FMath::Max(MazeParams.CellSize, 1.0f), MazeParams.CellSize, NumMazeBoxTests, SpanVoxels, (float)SpanVoxels / FMath::Max(MazeParams.CellSize, 1.0f), MazeParams.CellSize,
NumProved, NumMixed, NumBruteSamples, NumUnsound)); NumProved, NumMixed, NumBruteSamples, NumUnsound));
if (NumProved == 0) if (NumProved == 0)
{ {
AddWarning(TEXT("The stack proved no tile uniform, so it is not yet better than today's ") AddWarning(TEXT("The stack proved no tile uniform, so ClassifyTile skips no Maze tile. ")
TEXT("classifier for Maze. Not a correctness problem, but the perf case for ") TEXT("Not a correctness problem, but a perf regression."));
TEXT("the port rests on this number."));
} }
} }
@@ -1,21 +1,15 @@
// VoxelForgeOpStackShaftTest.cpp // VoxelForgeOpStackShaftTest.cpp
// VerticalShafts — le portage qui teste la RÉUTILISATION et le contrat de l'opérateur courant. // VerticalShafts — la RÉUTILISATION d'opérateurs et le contrat de la pile.
// VerticalShafts — the port that tests REUSE and the current operator contract. // VerticalShafts — operator REUSE and the stack's contract.
// //
// CE QUE CELUI-CI PROUVE EN PLUS DES AUTRES // Trois des cinq opérateurs de VerticalShafts sont ceux de Maze, repris tels quels :
// Les portages précédents demandaient « la décomposition reproduit-elle l'original ? ». Celui-ci // `ConstantRock`, `SdfRoughness`, `SdfCarve` — mêmes ops, autre source et autres réglages
// demande **« les opérateurs se RÉUTILISENT-ils vraiment entre archétypes ? »**, qui est la thèse // (fréquence 0.1 au lieu de 0.12, fenêtre `rough + 4` au lieu de `R + rough + 2`). C'est la thèse
// de `OPSTACK-PLAN §2.5` et la seule raison de faire ce refactor plutôt que de nettoyer le `switch`. // de `OPSTACK-PLAN §2.5` : les opérateurs se réutilisent entre archétypes.
// //
// Trois des cinq opérateurs de VerticalShafts sont ceux de Maze, **repris sans une ligne de // Three of the five VerticalShafts ops are Maze's, reused unchanged. The assertions below target
// changement** : `ConstantRock`, `SdfRoughness`, `SdfCarve`. Dans le `switch`, `GetMazeDensity` et // the stack used in-game: channel count/order, feature liveness, window invariance, cache cost and
// `GetVerticalShaftDensity` sont deux fonctions de ~100 lignes qui n'ont rien en commun à l'œil. // box-proof soundness.
// En opérateurs, ce sont les mêmes trois ops avec une source différente et d'autres réglages
// (fréquence 0.1 au lieu de 0.12, fenêtre `rough + 4` au lieu de `R + rough + 2`).
//
// The direct helper remains useful migration telemetry, but it is not the owner path after the
// structural-post split. The assertions below target the operator stack actually used in-game:
// current channel count/order, feature liveness, cache isolation and box-proof soundness.
#if WITH_DEV_AUTOMATION_TESTS #if WITH_DEV_AUTOMATION_TESTS
@@ -93,8 +87,8 @@ bool FVoxelForgeOpStackShaftTest::RunTest(const FString& Parameters)
if (P.StrateTopWorldZ - P.StrateBottomWorldZ <= 0.0f) if (P.StrateTopWorldZ - P.StrateBottomWorldZ <= 0.0f)
{ {
AddError(TEXT("The VerticalShafts strate has degenerate Z bounds, which sends ") AddError(TEXT("The VerticalShafts strate has degenerate Z bounds; GetDensityAt builds no ")
TEXT("GetVerticalShaftDensity down its early-out. The op stack has none by design.")); TEXT("stack for it. The op stack has no degenerate-strate early-out by design."));
return false; return false;
} }
@@ -128,49 +122,17 @@ bool FVoxelForgeOpStackShaftTest::RunTest(const FString& Parameters)
} }
//========================================================================= //=========================================================================
// 1. CURRENT-OWNER DIAGNOSTIC // 1. LIVENESS — des échantillons tombent dans l'air des puits / samples land in shaft air
// GetVerticalShaftDensity is the retired direct path. Its delta is reported so a future
// port can be audited, but it is not an owner-path failure: the game evaluates Stack plus
// the common MC tail.
//========================================================================= //=========================================================================
int32 NumDiff = 0, NumSideDisagree = 0, WorstIdx = -1, NumInsideShaft = 0; int32 NumInsideShaft = 0;
float WorstDelta = 0.0f;
for (int32 i = 0; i < NumShaftSamples; ++i) for (int32 i = 0; i < NumShaftSamples; ++i)
{ {
const float X = (float)Points[i].X, Y = (float)Points[i].Y, Z = (float)Points[i].Z; const float X = (float)Points[i].X, Y = (float)Points[i].Y, Z = (float)Points[i].Z;
// air ⇒ dans un puits/connecteur/étagère / air ⇒ inside a shaft, connector or ledge
const float Old = Gen->GetVerticalShaftDensity(X, Y, Z, P); if (EvalStackMCForTest(Stack, X, Y, Z) >= 0.0f) { ++NumInsideShaft; }
const float New = EvalStackMCForTest(Stack, X, Y, Z);
if (Old >= 0.0f) { ++NumInsideShaft; } // air ⇒ dans un puits/connecteur/étagère
if (!BitEqual(Old, New))
{
++NumDiff;
const float D = FMath::Abs(Old - New);
if (D > WorstDelta) { WorstDelta = D; WorstIdx = i; }
}
if ((Old >= 0.0f) != (New >= 0.0f)) { ++NumSideDisagree; }
} }
AddInfo(FString::Printf(TEXT("VerticalShafts: %d of %d samples are air in the stack."),
if (NumDiff == 0) NumInsideShaft, NumShaftSamples));
{
AddInfo(FString::Printf(
TEXT("VerticalShafts: bit-identical across %d samples (%d of them inside a shaft, so ")
TEXT("the cylinders, connectors, roughness, carve and ledges were all exercised). ")
TEXT("THREE of the five ops here are Maze's, reused unchanged -- operator reuse across ")
TEXT("archetypes is now measured rather than intended (OPSTACK-PLAN 2.5)."),
NumShaftSamples, NumInsideShaft));
}
AddInfo(FString::Printf(
TEXT("VerticalShafts legacy diagnostic: %d of %d samples differ (largest |delta| %.9g at "
"(%.0f, %.0f, %.0f)); %d cross the isosurface; %d samples are air in the stack."),
NumDiff, NumShaftSamples, WorstDelta,
WorstIdx >= 0 ? Points[WorstIdx].X : 0.0f,
WorstIdx >= 0 ? Points[WorstIdx].Y : 0.0f,
WorstIdx >= 0 ? Points[WorstIdx].Z : 0.0f,
NumSideDisagree, NumInsideShaft));
TestTrue(TEXT("shaft feature sampling exercised the operator stack"), NumInsideShaft > 0); TestTrue(TEXT("shaft feature sampling exercised the operator stack"), NumInsideShaft > 0);
if (NumInsideShaft == 0) if (NumInsideShaft == 0)
@@ -268,14 +230,14 @@ bool FVoxelForgeOpStackShaftTest::RunTest(const FString& Parameters)
const int32 CellY = (i / 16) - 8; const int32 CellY = (i / 16) - 8;
LastX = (static_cast<float>(CellX) + 0.37f) * PerfSpacing; LastX = (static_cast<float>(CellX) + 0.37f) * PerfSpacing;
LastY = (static_cast<float>(CellY) + 0.61f) * PerfSpacing; LastY = (static_cast<float>(CellY) + 0.61f) * PerfSpacing;
Sink += Gen->GetVerticalShaftDensity(LastX, LastY, PerfZ, P); Sink += EvalStackMCForTest(Stack, LastX, LastY, PerfZ);
} }
const double RebuildSeconds = FPlatformTime::Seconds() - RebuildStart; const double RebuildSeconds = FPlatformTime::Seconds() - RebuildStart;
const double HotStart = FPlatformTime::Seconds(); const double HotStart = FPlatformTime::Seconds();
for (int32 i = 0; i < NumHotSamples; ++i) for (int32 i = 0; i < NumHotSamples; ++i)
{ {
Sink += Gen->GetVerticalShaftDensity(LastX, LastY, PerfZ, P); Sink += EvalStackMCForTest(Stack, LastX, LastY, PerfZ);
} }
const double HotSeconds = FPlatformTime::Seconds() - HotStart; const double HotSeconds = FPlatformTime::Seconds() - HotStart;
@@ -1,32 +1,20 @@
// VoxelForgeOpStackSlabTest.cpp // VoxelForgeOpStackSlabTest.cpp
// PHASE 2, PREMIER PORTAGE — la pile Slab courante, sur LES DEUX archétypes. // La pile Slab, sur LES DEUX archétypes / The Slab operator stack, on BOTH archetypes.
// PHASE 2'S FIRST PORT — the current Slab operator stack, on BOTH archetypes.
// //
// CE QUE CE TEST DOIT PROUVER / WHAT THIS TEST HAS TO PROVE // CE QUE CE TEST DOIT PROUVER / WHAT THIS TEST HAS TO PROVE
// Trois choses, et la troisième est la raison d'être du portage :
// //
// 1. CURRENT OWNER — the stack's channels, window invariance and conservative box verdicts // 1. LE CHEMIN DU JEU — la pile rend une densité finie, invariante à la fenêtre (ordre et
// guard the path used by the game. `GetSlabDensity` is retained as a legacy diagnostic only: // threads), et ses verdicts de boîte sont conservatifs.
// it still owns the former full passage/support tail, while the operator stack is followed by // The stack's density is finite and window-invariant, and its box verdicts are conservative.
// the current common MC post. // 2. UN OPÉRATEUR, DEUX ARCHÉTYPES — la MÊME pile est vérifiée pour FlatPlain ET CrystalChamber.
// 2. UN OPÉRATEUR, DEUX ARCHÉTYPES — la MÊME pile est vérifiée pour FlatPlain ET
// CrystalChamber. The legacy helper is sampled for migration telemetry, not used as the
// owner oracle; if the current stack becomes non-finite or loses its channel contract, this
// test still fails.
// ⚠️ La fixture ne règle que `GeneratorType`, donc les deux slots portent des params PAR // ⚠️ La fixture ne règle que `GeneratorType`, donc les deux slots portent des params PAR
// DÉFAUT : à eux seuls ils exécutent la même configuration à deux profondeurs. C'est la // DÉFAUT : à eux seuls ils exécutent la même configuration à deux profondeurs. C'est la
// TROISIÈME passe (`CrystalChamber(tuned)`, `CeilingRoughness` 6 → 20) qui fait réellement // TROISIÈME passe (`CrystalChamber(tuned)`, `CeilingRoughness` 6 → 20) qui fait réellement
// varier ce qui distingue les deux archétypes — et qui sert en même temps de pire cas aux // varier ce qui distingue les deux archétypes — et qui sert en même temps de pire cas aux
// bornes d'amplitude de `ClassifyBox`. Voir le bloc en bas de fichier. // bornes d'amplitude de `ClassifyBox`. Voir le bloc en bas de fichier.
// 3. LE VERDICT DE BOÎTE — et c'est ici que §3.1 se paie. `ClassifyTile` prouve ZÉRO tuile pour // 3. LE VERDICT DE BOÎTE — les deux surfaces sont XY-PURES, donc leurs bornes en Z sont connues
// FlatPlain et CrystalChamber aujourd'hui. Depuis que les deux surfaces sont XY-PURES, leurs // exactement (contrat [-1,1] de FBM) : toute tuile entièrement sous le sol ou entre les deux
// bornes en Z sont connues exactement (contrat [-1,1] de FBM), donc toute tuile entièrement // bandes se prouve SANS échantillonner.
// sous le sol ou entre les deux bandes se prouve SANS échantillonner.
//
// ─────────────────────────────────────────────────────────────────────────────────────────
// The old direct helper contains the removed support-floor/room tail. Room ownership and the
// authored-floor removal deliberately split that structural post from the operator stack, so its
// differences remain visible as diagnostics while the owner assertions below cover the live path.
#if WITH_DEV_AUTOMATION_TESTS #if WITH_DEV_AUTOMATION_TESTS
@@ -73,14 +61,13 @@ bool FVoxelForgeOpStackSlabTest::RunTest(const FString& Parameters)
int32 TopVoxelZ, int32 BottomVoxelZ, int32 TopVoxelZ, int32 BottomVoxelZ,
int32 SlotIndex, const TCHAR* SlotName) int32 SlotIndex, const TCHAR* SlotName)
{ {
// `GetSlabDensity` court-circuite sur une strate dégénérée (`return 1.0f`). Cette garde // La pile suppose une strate valide ; `GetDensityAt` rend de l'air sans pile sur une strate
// appartient à la fonction d'archétype, pas à un opérateur ; la pile suppose une strate // dégénérée. / The stack assumes a valid strate; GetDensityAt returns air for a degenerate one.
// valide, et `GetDensityAt` retombe sur le `switch` dans ce cas.
if (SlabParams.StrateTopWorldZ - SlabParams.StrateBottomWorldZ <= 0.0f) if (SlabParams.StrateTopWorldZ - SlabParams.StrateBottomWorldZ <= 0.0f)
{ {
AddError(FString::Printf( AddError(FString::Printf(
TEXT("%s has degenerate Z bounds (top %.1f, bottom %.1f), which sends GetSlabDensity ") TEXT("%s has degenerate Z bounds (top %.1f, bottom %.1f); GetDensityAt builds no ")
TEXT("down its early-out. The op stack has no such early-out by design."), TEXT("stack for it. The op stack has no degenerate-strate early-out by design."),
SlotName, SlabParams.StrateTopWorldZ, SlabParams.StrateBottomWorldZ)); SlotName, SlabParams.StrateTopWorldZ, SlabParams.StrateBottomWorldZ));
return; return;
} }
@@ -114,90 +101,16 @@ bool FVoxelForgeOpStackSlabTest::RunTest(const FString& Parameters)
} }
//===================================================================== //=====================================================================
// 1. ÉQUIVALENCE — géométrie d'abord, bits ensuite. // 1. DENSITÉ FINIE / FINITE DENSITY
//===================================================================== //=====================================================================
// ─────────────────────────────────────────────────────────────────────
// LE BON MÈTRE — corrigé 2026-07-27 après que la passe `tuned` a crié au loup
// ─────────────────────────────────────────────────────────────────────
// Première version : `16 · max(|Old|, 1) · FLT_EPSILON`, c.-à-d. l'ULP mesuré sur la
// DENSITÉ DE SORTIE. C'est le mauvais mètre, et il se trompe exactement là où le test
// regarde le plus : la densité vaut `min(Z - Sol, Plafond - Z)`, donc PRÈS DE L'ISOSURFACE
// la sortie tend vers 0 pendant que les intermédiaires (surfaces, Z monde, amplitudes de
// bruit) valent des CENTAINES. Un arrondi né à l'échelle 400 était jugé contre un mètre
// à l'échelle 1 — 400× trop serré.
//
// Mesuré : la passe `tuned` (rugosités ×2.25 et ×3.33) a vu ses écarts croître ×4.5, et
// son pire écart valait **0.345 ULP de |Z|**. Sous-ULP à l'échelle où l'erreur naît.
// L'erreur est donc proportionnelle à l'AMPLITUDE, ce qui est la signature d'un arrondi
// ordinaire, pas d'une transcription fausse.
//
// Le mètre correct est la magnitude des quantités D'OÙ VIENT l'erreur. Le test reste
// discriminant : une vraie dérive de portage (offset de bruit faux, `abs()` manquant,
// clamp oublié) déplace la surface de plusieurs VOXELS — 4 ordres de grandeur au-dessus
// de ce seuil, pas 4 fois.
//
// The first yardstick measured ULPs on the OUTPUT density, which tends to 0 near the
// isosurface while the intermediates are in the hundreds. Rounding born at scale ~400 was
// judged against a yardstick of scale 1. Real port drift moves the surface by voxels —
// four orders of magnitude above this bound, so the test stays discriminating.
const float SurfaceScale = FMath::Max(FMath::Abs(SlabParams.StrateTopWorldZ),
FMath::Abs(SlabParams.StrateBottomWorldZ));
int32 NumDiff = 0, WorstIdx = -1, NumBeyondUlpNoise = 0, NumSolidDisagreements = 0;
int32 NumNonFinite = 0; int32 NumNonFinite = 0;
float WorstDelta = 0.0f, WorstOld = 0.0f, WorstUlpsOfScale = 0.0f;
// Le pire cas PARMI LES DÉPASSEMENTS — c'est lui qui dit si un WARN est du bruit ou une dérive.
float WorstOutlierDelta = 0.0f, WorstOutlierOld = 0.0f, WorstOutlierUlps = 0.0f;
for (int32 i = 0; i < NumSlabSamples; ++i) for (int32 i = 0; i < NumSlabSamples; ++i)
{ {
const float X = (float)Points[i].X, Y = (float)Points[i].Y, Z = (float)Points[i].Z; if (!FMath::IsFinite(Stack.EvalMC((float)Points[i].X, (float)Points[i].Y, (float)Points[i].Z)))
const float Old = Gen->GetSlabDensity(X, Y, Z, SlabParams); // MC : négatif = solide
const float New = Stack.EvalMC(X, Y, Z);
if (!FMath::IsFinite(New))
{ {
++NumNonFinite; ++NumNonFinite;
} }
if (!BitEqual(Old, New))
{
++NumDiff;
const float Delta = FMath::Abs(Old - New);
// L'échelle à laquelle CET échantillon calcule : la sortie, sa propre altitude, et
// les bornes de la strate. C'est le plus grand des trois qui porte l'arrondi.
const float Scale = FMath::Max3(FMath::Abs(Old), FMath::Abs(Z),
FMath::Max(SurfaceScale, 1.0f));
const float Ulps = Delta / (Scale * FLT_EPSILON);
if (Delta > WorstDelta)
{
WorstDelta = Delta; WorstIdx = i; WorstOld = Old; WorstUlpsOfScale = Ulps;
}
if (Delta > 16.0f * Scale * FLT_EPSILON)
{
++NumBeyondUlpNoise;
if (Delta > WorstOutlierDelta)
{
WorstOutlierDelta = Delta; WorstOutlierOld = Old; WorstOutlierUlps = Ulps;
}
}
}
// Le mesher ne lit que le SIGNE. Un désaccord de CÔTÉ bouge la géométrie.
if ((Old >= 0.0f) != (New >= 0.0f)) { ++NumSolidDisagreements; }
} }
AddInfo(FString::Printf(
TEXT("%s legacy slab diagnostic: %d of %d samples differ, %d exceed the old ULP "
"bound, largest |delta| %.9g at (%.0f,%.0f,%.0f), %d side disagreements; "
"current stack non-finite samples=%d."),
SlotName, NumDiff, NumSlabSamples, NumBeyondUlpNoise, WorstDelta,
WorstIdx >= 0 ? Points[WorstIdx].X : 0.0f,
WorstIdx >= 0 ? Points[WorstIdx].Y : 0.0f,
WorstIdx >= 0 ? Points[WorstIdx].Z : 0.0f,
NumSolidDisagreements, NumNonFinite));
TestEqual(*FString::Printf(TEXT("%s: current stack has no non-finite density"), SlotName), TestEqual(*FString::Printf(TEXT("%s: current stack has no non-finite density"), SlotName),
NumNonFinite, 0); NumNonFinite, 0);
@@ -300,9 +213,9 @@ bool FVoxelForgeOpStackSlabTest::RunTest(const FString& Parameters)
AddInfo(FString::Printf( AddInfo(FString::Printf(
TEXT("%s box verdicts over %d tiles (XY sampled from +/- %d voxels = %.1f x ") TEXT("%s box verdicts over %d tiles (XY sampled from +/- %d voxels = %.1f x ")
TEXT("ColumnSpacing %.0f): %d proved uniform, %d Mixed, %d voxels checked, %d violations. Today's ") TEXT("ColumnSpacing %.0f): %d proved uniform, %d Mixed, %d voxels checked, %d violations. ")
TEXT("ClassifyTile proves ZERO of these. This number is the whole point of making ") TEXT("This number is the whole point of making the slab surfaces XY-pure ")
TEXT("the slab surfaces XY-pure (OPSTACK-DECOMPOSITION 3.1)."), TEXT("(OPSTACK-DECOMPOSITION 3.1)."),
SlotName, NumSlabTiles, SpanVoxels, SlotName, NumSlabTiles, SpanVoxels,
(float)SpanVoxels / FMath::Max(SlabParams.ColumnSpacing, 1.0f), SlabParams.ColumnSpacing, (float)SpanVoxels / FMath::Max(SlabParams.ColumnSpacing, 1.0f), SlabParams.ColumnSpacing,
NumProved, NumMixed, NumBruteSamples, NumUnsound)); NumProved, NumMixed, NumBruteSamples, NumUnsound));
@@ -8,6 +8,7 @@
#include "Misc/AutomationTest.h" #include "Misc/AutomationTest.h"
#include "VoxelCaveMorphology.h" #include "VoxelCaveMorphology.h"
#include "VoxelDensityOpStack.h"
#include "VoxelDensityPrimitives.h" #include "VoxelDensityPrimitives.h"
#include "VoxelForgeTestFixture.h" #include "VoxelForgeTestFixture.h"
#include "VoxelPassageGeometry.h" #include "VoxelPassageGeometry.h"
@@ -900,11 +901,27 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
++NumFootingChecked; ++NumFootingChecked;
// The live world's passage modifier is intentionally absent here. The endpoint is // The live world's passage modifier is intentionally absent here. The endpoint is
// already known to be passage-carved, so this second generator checks the archetype // already known to be passage-carved, so the archetype's own stack, built without a
// source itself: air at the landing and solid immediately below it. // strate manager (hence without passage carving), checks the source itself: air at
TStrongObjectPtr<UVoxelGenerator> SourceOnlyGenerator( // the landing and solid immediately below it.
NewObject<UVoxelGenerator>(GetTransientPackage(), NAME_None, RF_Transient)); const UVoxelGenerator& LiveGenerator = *World.Generator;
SourceOnlyGenerator->InitializeSettings(World.Settings.Get()); FVoxelOpStack SourceStack;
FVoxelOpContext SourceContext;
SourceContext.Seed = (uint32)LiveGenerator.Seed;
SourceContext.LayoutVersion = World.StrateManager->GetLayoutVersion();
SourceContext.StrateTopWorldZ = StrateTopZ;
SourceContext.StrateBottomWorldZ = StrateBottomZ;
SourceContext.WorldRadiusVoxels = LiveGenerator.WorldRadiusVoxels;
SourceContext.EdgeSealThickness = LiveGenerator.EdgeSealThickness;
// The generator resets the shaft-connector hand-off around every density query; a
// direct stack evaluation must model that boundary too.
auto EvalSource = [&SourceStack](float QueryX, float QueryY, float QueryZ)
{
VoxelPassageGeometry::ResetVerticalShaftConnectorAirMarker();
const float SourceDensity = SourceStack.EvalMC(QueryX, QueryY, QueryZ);
VoxelPassageGeometry::ResetVerticalShaftConnectorAirMarker();
return SourceDensity;
};
float LandingDensity = 0.0f; float LandingDensity = 0.0f;
float FootingDensity = 0.0f; float FootingDensity = 0.0f;
@@ -915,13 +932,16 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
FMazeGenerationParams P = Definition->MazeParams; FMazeGenerationParams P = Definition->MazeParams;
P.StrateTopWorldZ = StrateTopZ; P.StrateTopWorldZ = StrateTopZ;
P.StrateBottomWorldZ = StrateBottomZ; P.StrateBottomWorldZ = StrateBottomZ;
LandingDensity = SourceOnlyGenerator->GetMazeDensity( VoxelDensityOps::BuildMazeStack(SourceStack, P, LiveGenerator.Seed,
SuggestedPoint.X, SuggestedPoint.Y, SuggestedPoint.Z, P); LiveGenerator.OriginSpineRadius, /*StrateManager*/nullptr);
SourceStack.PrepareChunk(SourceContext);
LandingDensity = EvalSource(
SuggestedPoint.X, SuggestedPoint.Y, SuggestedPoint.Z);
const float FloorProbeZ = SuggestedPoint.Z const float FloorProbeZ = SuggestedPoint.Z
- FMath::Max(P.CorridorRadius, 0.5f) - FMath::Max(P.CorridorRadius, 0.5f)
- P.SurfaceRoughness * VOXEL_NOISE_SCALE * 1.5f - 3.0f; - P.SurfaceRoughness * VOXEL_NOISE_SCALE * 1.5f - 3.0f;
FootingDensity = SourceOnlyGenerator->GetMazeDensity( FootingDensity = EvalSource(
SuggestedPoint.X, SuggestedPoint.Y, FloorProbeZ, P); SuggestedPoint.X, SuggestedPoint.Y, FloorProbeZ);
break; break;
} }
@@ -930,11 +950,14 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
FVerticalShaftParams P = Definition->VerticalShaftParams; FVerticalShaftParams P = Definition->VerticalShaftParams;
P.StrateTopWorldZ = StrateTopZ; P.StrateTopWorldZ = StrateTopZ;
P.StrateBottomWorldZ = StrateBottomZ; P.StrateBottomWorldZ = StrateBottomZ;
LandingDensity = SourceOnlyGenerator->GetVerticalShaftDensity( VoxelDensityOps::BuildVerticalShaftStack(SourceStack, P, LiveGenerator.Seed,
SuggestedPoint.X, SuggestedPoint.Y, SuggestedPoint.Z, P); LiveGenerator.OriginSpineRadius, /*StrateManager*/nullptr);
SourceStack.PrepareChunk(SourceContext);
LandingDensity = EvalSource(
SuggestedPoint.X, SuggestedPoint.Y, SuggestedPoint.Z);
const float FloorProbeZ = StrateBottomZ + P.BoundarySealThickness * 0.5f; const float FloorProbeZ = StrateBottomZ + P.BoundarySealThickness * 0.5f;
FootingDensity = SourceOnlyGenerator->GetVerticalShaftDensity( FootingDensity = EvalSource(
SuggestedPoint.X, SuggestedPoint.Y, FloorProbeZ, P); SuggestedPoint.X, SuggestedPoint.Y, FloorProbeZ);
break; break;
} }
@@ -943,10 +966,13 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
FFloatingIslandParams P = Definition->FloatingIslandParams; FFloatingIslandParams P = Definition->FloatingIslandParams;
P.StrateTopWorldZ = StrateTopZ; P.StrateTopWorldZ = StrateTopZ;
P.StrateBottomWorldZ = StrateBottomZ; P.StrateBottomWorldZ = StrateBottomZ;
LandingDensity = SourceOnlyGenerator->GetFloatingIslandDensity( VoxelDensityOps::BuildFloatingIslandStack(SourceStack, P, LiveGenerator.Seed,
SuggestedPoint.X, SuggestedPoint.Y, SuggestedPoint.Z, P); LiveGenerator.OriginSpineRadius, /*StrateManager*/nullptr);
FootingDensity = SourceOnlyGenerator->GetFloatingIslandDensity( SourceStack.PrepareChunk(SourceContext);
SuggestedPoint.X, SuggestedPoint.Y, SuggestedPoint.Z - 1.0f, P); LandingDensity = EvalSource(
SuggestedPoint.X, SuggestedPoint.Y, SuggestedPoint.Z);
FootingDensity = EvalSource(
SuggestedPoint.X, SuggestedPoint.Y, SuggestedPoint.Z - 1.0f);
break; break;
} }
@@ -230,8 +230,8 @@ namespace VoxelForgeTest
/** /**
* A spread of world sample points that deliberately crosses chunk boundaries, strate * A spread of world sample points that deliberately crosses chunk boundaries, strate
* boundaries and bedrock gaps — the exact conditions under which a per-chunk cache with a * boundaries and bedrock gaps — the exact conditions under which a per-chunk cache with a
* missing key input produces a wrong answer. Integer XY on purpose: that is the branch * missing key input produces a wrong answer. Integer XY on purpose: that is the lattice the
* GetDensityAt's T1.a column cache actually takes (fractional XY bypasses the cache). * mesher samples, and the one the per-column memos are keyed on.
*/ */
inline void BuildSamplePoints(const FTestWorld& World, int32 Count, int32 Seed, inline void BuildSamplePoints(const FTestWorld& World, int32 Count, int32 Seed,
TArray<FVector>& OutPoints) TArray<FVector>& OutPoints)
@@ -5303,9 +5303,9 @@ namespace
} }
// The slab source is an XY height band. Evaluate the same two pure height fields as // The slab source is an XY height band. Evaluate the same two pure height fields as
// GetSlabDensity, then choose their midpoint after intersecting the seal-free interior. // FSlabVoidSource, then choose their midpoint after intersecting the seal-free interior.
// La source slab est une bande de hauteurs XY : on recalcule les deux champs purs comme // La source slab est une bande de hauteurs XY : on recalcule les deux champs purs comme
// GetSlabDensity, puis on prend leur milieu après intersection avec l'intérieur sans seal. // FSlabVoidSource, puis on prend leur milieu après intersection avec l'intérieur sans seal.
bool VF_SuggestSlabLandingPoint( bool VF_SuggestSlabLandingPoint(
const FSlabGenerationParams& Params, const FSlabGenerationParams& Params,
int32 Seed, int32 Seed,
@@ -5444,7 +5444,8 @@ namespace
// Maze corridors are lattice edges. Only a horizontal edge is a useful landing source: a // Maze corridors are lattice edges. Only a horizontal edge is a useful landing source: a
// vertical edge can prove air, but cannot prove a place to stand. The edge set and Z levels // vertical edge can prove air, but cannot prove a place to stand. The edge set and Z levels
// below use the same origin-directed tree + loop contract as GetMazeDensity, with no cache. // below use the same origin-directed tree + loop contract as FLatticeCorridorSource, with no
// cache.
bool VF_SuggestMazeLandingPoint( bool VF_SuggestMazeLandingPoint(
const FMazeGenerationParams& Params, const FMazeGenerationParams& Params,
int32 Seed, int32 Seed,
@@ -5879,7 +5880,7 @@ namespace
}; };
// Evaluate only the island source, in the same internal-density convention as // Evaluate only the island source, in the same internal-density convention as
// GetFloatingIslandDensity (positive = solid, negative = void). This is intentionally a // FIslandBlobSource (positive = solid, negative = void). This is intentionally a
// local pure evaluator: it lets the query bracket the actual blob top instead of treating a // local pure evaluator: it lets the query bracket the actual blob top instead of treating a
// nominal island centre as footing. Full-octave noise is used because this query has no LOD // nominal island centre as footing. Full-octave noise is used because this query has no LOD
// state by design and GeneratePassages runs before a tile's LOD is selected. // state by design and GeneratePassages runs before a tile's LOD is selected.
@@ -5916,7 +5917,7 @@ namespace
for (int32 IslandIndex = 0; IslandIndex < NumIslands; ++IslandIndex) for (int32 IslandIndex = 0; IslandIndex < NumIslands; ++IslandIndex)
{ {
const FVFIslandSite& Island = Islands[IslandIndex]; const FVFIslandSite& Island = Islands[IslandIndex];
// Production GetFloatingIslandDensity evaluates exactly this 3x3 cell neighbourhood // Production FIslandBlobSource evaluates exactly this 3x3 cell neighbourhood
// for the queried XY. The outer search may collect more candidates so a lateral snap // for the queried XY. The outer search may collect more candidates so a lateral snap
// can be found, but those distant blobs must not influence the returned footing. // can be found, but those distant blobs must not influence the returned footing.
if (FMath::Abs(Island.CellX - BaseCellX) > 1 if (FMath::Abs(Island.CellX - BaseCellX) > 1
@@ -2,21 +2,11 @@
// Les opérateurs concrets : les décompositions d'archétypes + le post-traitement structurel. // Les opérateurs concrets : les décompositions d'archétypes + le post-traitement structurel.
// The concrete operators: the archetype decompositions + the structural post-process. // The concrete operators: the archetype decompositions + the structural post-process.
// //
// Ces opérateurs alimentent le jeu derrière l'opt-in décrit dans l'en-tête de VoxelDensityOpStack.h. // Ces opérateurs génèrent chaque strate du jeu (voir l'en-tête de VoxelDensityOpStack.h).
// These operators generate every strate in the game (see the VoxelDensityOpStack.h header).
// //
// FIDÉLITÉ / FIDELITY // Convention de signe INTERNE : positif = solide ; la pile négative une fois pour le MC.
// Chaque corps ci-dessous est une transcription LITTÉRALE du bloc correspondant de // Internal sign convention: positive = solid; the stack negates once for MC.
// `UVoxelGenerator::GetMazeDensity` — mêmes hashes, mêmes constantes, même ordre d'opérations
// flottantes, même convention de signe (INTERNE : positif = solide). L'objectif est
// l'égalité BIT à BIT, vérifiée par `VoxelForge.OpStack.MazeEquivalence`.
//
// `OPSTACK-PLAN §2.6` n'EXIGE pas l'identité binaire avec l'ancien système — mais Maze se
// décompose si proprement qu'on peut l'obtenir, et quand on peut l'obtenir il faut la prendre :
// une égalité binaire transforme « je crois que la décomposition est correcte » en preuve.
//
// §2.6 does not REQUIRE bit-identity with the old system — but Maze decomposes cleanly enough that
// it is achievable, and when it is achievable it should be taken: bit-equality turns "I believe the
// decomposition is right" into a proof.
#include "VoxelDensityOpStack.h" #include "VoxelDensityOpStack.h"
#include "VoxelStrateComposer.h" #include "VoxelStrateComposer.h"
@@ -674,7 +664,7 @@ namespace
, CorridorRadius(FMath::Max(P.CorridorRadius, 0.5f)) , CorridorRadius(FMath::Max(P.CorridorRadius, 0.5f))
, BranchProbability(P.BranchProbability) , BranchProbability(P.BranchProbability)
, Verticality(P.Verticality) , Verticality(P.Verticality)
, Salt((uint32)Seed ^ 0x4D617A65u) // 'Maze' — identique à GetMazeDensity , Salt((uint32)Seed ^ 0x4D617A65u) // 'Maze'
{} {}
EVoxelOpRole GetRole() const override { return EVoxelOpRole::FieldSource; } EVoxelOpRole GetRole() const override { return EVoxelOpRole::FieldSource; }
@@ -765,7 +755,7 @@ namespace
} }
/** /**
* Le cache par CELLULE, partagé avec le même contrat que GetMazeDensity. Il est `thread_local` et non * Le cache par CELLULE. Il est `thread_local` et non
* membre parce que la pile est PARTAGÉE entre workers en lecture — un membre mutable serait * membre parce que la pile est PARTAGÉE entre workers en lecture — un membre mutable serait
* une course. La reconstruction seule inspecte le petit voisinage ; Eval ne refait aucune * une course. La reconstruction seule inspecte le petit voisinage ; Eval ne refait aucune
* décision de parent ou de boucle. * décision de parent ou de boucle.
@@ -829,16 +819,13 @@ namespace
//========================================================================= //=========================================================================
// RÔLE 1 — SOURCE : DALLE / SLAB VOID (FlatPlain ET CrystalChamber) // RÔLE 1 — SOURCE : DALLE / SLAB VOID (FlatPlain ET CrystalChamber)
//========================================================================= //=========================================================================
// Transcription littérale des ÉTAPES 1-3 de `GetSlabDensity` : surface de sol, surface de // Surface de sol, surface de plafond, puis `Density = -min(distAuSol, distAuPlafond)`.
// plafond, puis `Density = -min(distAuSol, distAuPlafond)`.
// //
// DEUX archétypes, UN opérateur. `GetSlabDensity` est appelé pour FlatPlain et // DEUX archétypes, UN opérateur, sans le moindre branchement sur le type — CrystalChamber
// CrystalChamber sans le moindre branchement sur le type — CrystalChamber n'est rien d'autre // n'est rien d'autre que FlatPlain avec un `CeilingRoughness` plus grand (OPSTACK-PLAN §4) :
// que FlatPlain avec un `CeilingRoughness` plus grand. C'est le premier vrai gain du refactor // la différence entre eux est un jeu de valeurs par défaut.
// (OPSTACK-PLAN §4) : deux des huit archétypes disparaissent dans un seul opérateur, et la
// différence entre eux redevient ce qu'elle a toujours été — un jeu de valeurs par défaut.
// //
// Two archetypes, ONE op: GetSlabDensity is called for both with no branch on the type. // Two archetypes, ONE op, with no branch on the type.
// CrystalChamber IS FlatPlain with a bigger CeilingRoughness. // CrystalChamber IS FlatPlain with a bigger CeilingRoughness.
// //
// XY-PUR depuis §3.1 (le terme en Z des deux bruits est parti). C'est ce qui rend // XY-PUR depuis §3.1 (le terme en Z des deux bruits est parti). C'est ce qui rend
@@ -993,7 +980,7 @@ namespace
// RÔLE 1 — SOURCE : COLONNE DE SURFACE / SURFACE COLUMN (SurfaceWorld) // RÔLE 1 — SOURCE : COLONNE DE SURFACE / SURFACE COLUMN (SurfaceWorld)
//========================================================================= //=========================================================================
// Le pont entre les deux espaces : consomme DEUX piles de hauteur (sol et voûte) et en fait une // Le pont entre les deux espaces : consomme DEUX piles de hauteur (sol et voûte) et en fait une
// densité. C'est tout le combine de `SurfaceDensityFromColumn` : // densité :
// //
// Density = TerrainZ - Z ← solide sous le sol // Density = TerrainZ - Z ← solide sous le sol
// Density = max(Density, Z - CeilSurf) ← solide au-dessus de la voûte // Density = max(Density, Z - CeilSurf) ← solide au-dessus de la voûte
@@ -1564,13 +1551,13 @@ namespace
//========================================================================= //=========================================================================
// RÔLE 3 — MODIFIER : COLONNES SUR GRILLE MONDE / WORLD-GRID COLUMNS // RÔLE 3 — MODIFIER : COLONNES SUR GRILLE MONDE / WORLD-GRID COLUMNS
//========================================================================= //=========================================================================
// ÉTAPE 4 de `GetSlabDensity`. Des cylindres de hauteur infinie posés sur une grille de // Des cylindres de hauteur infinie posés sur une grille de
// `ColumnSpacing`, un tirage d'existence et un jitter par cellule. Le champ de vide décide déjà // `ColumnSpacing`, un tirage d'existence et un jitter par cellule. Le champ de vide décide déjà
// où est le solide, donc la colonne n'a qu'à AJOUTER de la densité le long de son XY — elle // où est le solide, donc la colonne n'a qu'à AJOUTER de la densité le long de son XY — elle
// n'est visible que là où le vide avait creusé autour d'elle. // n'est visible que là où le vide avait creusé autour d'elle.
// //
// Le cache 3×3 par cellule est repris tel quel (il était déjà `thread_local` dans l'original, // Le cache 3×3 par cellule est `thread_local`, ce que la note de threading de VoxelDensityOp.h
// et c'est exactement ce que la note de threading de VoxelDensityOp.h autorise). Sa clé // autorise. Sa clé
// contient tous les paramètres qui influent sur le résultat + le seed, donc un changement de // contient tous les paramètres qui influent sur le résultat + le seed, donc un changement de
// layout qui change un param invalide bien ; un changement qui n'en touche aucun produirait // layout qui change un param invalide bien ; un changement qui n'en touche aucun produirait
// des colonnes identiques (cf. AUDIT C2 — la clé est complète, pas seulement le coord). // des colonnes identiques (cf. AUDIT C2 — la clé est complète, pas seulement le coord).
@@ -1657,7 +1644,7 @@ namespace
private: private:
struct FSlabColumn { float X, Y, R; }; struct FSlabColumn { float X, Y, R; };
static constexpr float ColBlend = 2.0f; // identique à GetSlabDensity static constexpr float ColBlend = 2.0f;
/** Le tirage d'une cellule : existence, jitter, rayon. Fonction PURE de (cellule, seed, /** Le tirage d'une cellule : existence, jitter, rayon. Fonction PURE de (cellule, seed,
* params) — donc `Eval` et `EffectOverBox` voient forcément la même colonne. */ * params) — donc `Eval` et `EffectOverBox` voient forcément la même colonne. */
@@ -1714,8 +1701,7 @@ namespace
// SEUL changement est `Density += Fill·Base·2` au lieu de `Density -= Carve·Base·2`. // SEUL changement est `Density += Fill·Base·2` au lieu de `Density -= Carve·Base·2`.
// //
// Un archétype qui CREUSE dans du roc et un archétype qui REMPLIT du vide sont donc le même // Un archétype qui CREUSE dans du roc et un archétype qui REMPLIT du vide sont donc le même
// opérateur au signe près, exactement comme la source constante au-dessus. C'est la symétrie // opérateur au signe près, exactement comme la source constante au-dessus.
// que le `switch` ne pouvait pas montrer : les deux blocs y sont à 900 lignes l'un de l'autre.
// //
// ⚠️ `Sign` vaut ±1.0f et rien d'autre. La multiplication par ±1 est EXACTE en IEEE-754, donc // ⚠️ `Sign` vaut ±1.0f et rien d'autre. La multiplication par ±1 est EXACTE en IEEE-754, donc
// `D += (-1·F)·B·2` rend bit pour bit ce que `D -= F·B·2` rendait — l'égalité binaire des trois // `D += (-1·F)·B·2` rend bit pour bit ce que `D -= F·B·2` rendait — l'égalité binaire des trois
@@ -2267,7 +2253,7 @@ namespace
{ {
public: public:
FShaftFieldSource(const FVerticalShaftParams& InP, int32 Seed, float InSpineRadius) FShaftFieldSource(const FVerticalShaftParams& InP, int32 Seed, float InSpineRadius)
: P(InP), Salt((uint32)Seed ^ 0x53686674u) // 'Shft' — identique à GetVerticalShaftDensity : P(InP), Salt((uint32)Seed ^ 0x53686674u) // 'Shft'
, SpineRadius(InSpineRadius) {} , SpineRadius(InSpineRadius) {}
EVoxelOpRole GetRole() const override { return EVoxelOpRole::FieldSource; } EVoxelOpRole GetRole() const override { return EVoxelOpRole::FieldSource; }
@@ -2427,9 +2413,9 @@ namespace
} }
/** Le voisinage inner 3×3 + ses connecteurs, mémoïsés par worker. Le rebuild collecte un /** Le voisinage inner 3×3 + ses connecteurs, mémoïsés par worker. Le rebuild collecte un
* halo 9×9 émetteur / 15×15 total pour résoudre les fenêtres parent 5×5 et le fallback fixe ±3. Clé = cellule + tous les params * halo 9×9 émetteur / 15×15 total pour résoudre les fenêtres parent 5×5 et le fallback
* qui influent (comme l'original) : stable à travers les reconstructions de pile, ce qui * fixe ±3. Clé = cellule + tous les params qui influent : stable à travers les
* est la leçon retenue du mémo de colonne de SurfaceWorld. */ * reconstructions de pile, ce qui est la leçon retenue du mémo de colonne de SurfaceWorld. */
const FCells& GetCells(float WorldX, float WorldY) const const FCells& GetCells(float WorldX, float WorldY) const
{ {
const float Spacing = FMath::Max(P.ShaftSpacing, 1.0f); const float Spacing = FMath::Max(P.ShaftSpacing, 1.0f);
@@ -2723,9 +2709,9 @@ namespace
void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override
{ {
if (P.LedgeSpacing <= 0.0f || P.LedgeDepth <= 0.0f || Field == nullptr) { return; } if (P.LedgeSpacing <= 0.0f || P.LedgeDepth <= 0.0f || Field == nullptr) { return; }
// ⚠️ La garde de l'original est `CaveSDF < 0` — le SDF APRÈS rugosité, tel que la pile // ⚠️ La garde est `Sdf < 0` — le SDF APRÈS rugosité, tel que la pile l'a laissé. C'est
// l'a laissé. C'est bien `InOut.Sdf` ici, pas une re-évaluation : re-calculer donnerait // bien `InOut.Sdf` ici, pas une re-évaluation : re-calculer donnerait le SDF SANS
// le SDF SANS rugosité et déplacerait les étagères. // rugosité et déplacerait les étagères.
if (InOut.Sdf >= 0.0f) { return; } if (InOut.Sdf >= 0.0f) { return; }
const float Phase = FMath::Frac((WorldZ - P.StrateBottomWorldZ) / P.LedgeSpacing); const float Phase = FMath::Frac((WorldZ - P.StrateBottomWorldZ) / P.LedgeSpacing);
@@ -2886,7 +2872,7 @@ namespace
{ {
public: public:
FIslandBlobSource(const FFloatingIslandParams& InP, int32 Seed) FIslandBlobSource(const FFloatingIslandParams& InP, int32 Seed)
: P(InP), Salt((uint32)Seed ^ 0x49736C64u) // 'Isld' — identique à GetFloatingIslandDensity : P(InP), Salt((uint32)Seed ^ 0x49736C64u) // 'Isld'
{} {}
EVoxelOpRole GetRole() const override { return EVoxelOpRole::FieldSource; } EVoxelOpRole GetRole() const override { return EVoxelOpRole::FieldSource; }
@@ -2962,8 +2948,8 @@ namespace
* • en Z, `Sdf ≥ WorldZ − TopSurf ≥ WorldZ − TopZ`, donc au-dessus du sommet + marge il * • en Z, `Sdf ≥ WorldZ − TopSurf ≥ WorldZ − TopZ`, donc au-dessus du sommet + marge il
* n'y a plus rien à faire. **En dessous, il n'y a PAS de borne** : sous une île, le SDF * n'y a plus rien à faire. **En dessous, il n'y a PAS de borne** : sous une île, le SDF
* vaut ≈ `DistXY` à toute profondeur, donc un mince fil de matière descend le long de * vaut ≈ `DistXY` à toute profondeur, donc un mince fil de matière descend le long de
* l'axe. C'est le comportement de l'original ; le confondre avec « rien en dessous » * l'axe. Le confondre avec « rien en dessous » serait un TROU, et c'est pourquoi seule
* serait un TROU, et c'est pourquoi seule la borne HAUTE est testée. * la borne HAUTE est testée.
* • cette source ne couvre pas l'aval : elle publie seulement son propre intervalle SDF. * • cette source ne couvre pas l'aval : elle publie seulement son propre intervalle SDF.
* La rugosité, le blend du fill et le creux du SmoothMin sont bornés par leurs propres * La rugosité, le blend du fill et le creux du SmoothMin sont bornés par leurs propres
* opérateurs dans le pliage, chacun avec son contrat isolé. * opérateurs dans le pliage, chacun avec son contrat isolé.
@@ -3059,8 +3045,7 @@ namespace
private: private:
/** Tirage d'une cellule. PURE en (cellule, seed, params) ⇒ `Eval` et `EffectOverBox` ne /** Tirage d'une cellule. PURE en (cellule, seed, params) ⇒ `Eval` et `EffectOverBox` ne
* peuvent pas voir des îles différentes. Transcription littérale du bloc de cuisson de * peuvent pas voir des îles différentes. */
* `GetFloatingIslandDensity`. */
bool RollIsland(int32 nx, int32 ny, FIsland& Out) const bool RollIsland(int32 nx, int32 ny, FIsland& Out) const
{ {
const float H = P.StrateTopWorldZ - P.StrateBottomWorldZ; const float H = P.StrateTopWorldZ - P.StrateBottomWorldZ;
@@ -3094,17 +3079,15 @@ namespace
} }
/** /**
* Le voisinage 3×3, mémoïsé par worker — la même cuisson `thread_local` que l'original. * Le voisinage 3×3, mémoïsé par worker (`thread_local`).
* *
* ⚠️ LA CLÉ INCLUT `BoundarySealThickness`, QUE L'ORIGINAL OMET. `SpreadZ` s'en sert * ⚠️ LA CLÉ INCLUT `BoundarySealThickness` : `SpreadZ` s'en sert
* (`H·0.5 − max(TopHalf, UnderDepth) − Seal`), donc dans `GetFloatingIslandDensity` une * (`H·0.5 − max(TopHalf, UnderDepth) − Seal`), donc sans lui une édition à chaud qui ne
* édition à chaud qui ne change QUE l'épaisseur de seal sert des îles périmées. Même famille * change QUE l'épaisseur de seal servirait des îles périmées. Même famille que `AUDIT §C2` :
* que `AUDIT §C2` : une clé de cache incomplète ne se voit pas, elle produit du terrain * une clé de cache incomplète ne se voit pas, elle produit du terrain plausible.
* plausible. Ajouter le champ ne coûte
* qu'un recalcul, jamais une valeur différente — donc l'égalité binaire tient.
* *
* The key includes BoundarySealThickness, which the original omits although SpreadZ reads it. * The key includes BoundarySealThickness because SpreadZ reads it; without it a live edit
* Adding it can only cost a recompute, never change a value — bit-equality is unaffected. * of the seal thickness alone would serve stale islands.
*/ */
const FCells& GetCells(float WorldX, float WorldY) const const FCells& GetCells(float WorldX, float WorldY) const
{ {
@@ -9326,8 +9309,8 @@ namespace VoxelDensityOps
TUniquePtr<IVoxelDensityOp> MakeConstantVoidSource(float BaseDensity) TUniquePtr<IVoxelDensityOp> MakeConstantVoidSource(float BaseDensity)
{ {
// `float Density = -Params.BaseDensity; // start as open air (void)` — la négation unaire // `Density = -BaseDensity` : on part du vide. La négation unaire est exacte.
// est exacte, donc c'est littéralement la première ligne de GetFloatingIslandDensity. // Start as open air; unary negation is exact.
return MakeUnique<FConstantFieldSource>(-BaseDensity); return MakeUnique<FConstantFieldSource>(-BaseDensity);
} }
@@ -9391,10 +9374,8 @@ namespace VoxelDensityOps
int32 Seed, float SpineRadius, const UVoxelStrateManager* StrateManager, int32 Seed, float SpineRadius, const UVoxelStrateManager* StrateManager,
bool bAppendStructuralPosts) bool bAppendStructuralPosts)
{ {
// DEUX archétypes entrent ici, aucun branchement ne les distingue — parce que // DEUX archétypes entrent ici, aucun branchement ne les distingue : FlatPlain et
// `GetSlabDensity` n'en fait aucun non plus. FlatPlain et CrystalChamber ne diffèrent que // CrystalChamber ne diffèrent que par leurs valeurs par défaut.
// par leurs valeurs par défaut, et c'est maintenant visible dans le code plutôt que dans
// un commentaire. 8 archétypes → 7.
OutStack.Add(MakeSlabVoidSource(P, Seed)); OutStack.Add(MakeSlabVoidSource(P, Seed));
OutStack.Add(MakeGridColumnMod(P, Seed)); OutStack.Add(MakeGridColumnMod(P, Seed));
@@ -9410,15 +9391,10 @@ namespace VoxelDensityOps
int32 Seed, float SpineRadius, const UVoxelStrateManager* StrateManager, int32 Seed, float SpineRadius, const UVoxelStrateManager* StrateManager,
bool bAppendStructuralPosts) bool bAppendStructuralPosts)
{ {
// ⚠️ LA PREUVE QUE L'ABSTRACTION EST RÉELLE, et elle vaut d'être dite : TROIS des cinq // TROIS des cinq opérateurs ci-dessous sont ceux de Maze, repris tels quels —
// opérateurs ci-dessous sont ceux de Maze, **repris sans une ligne de changement** — // `ConstantRock`, `SdfRoughness`, `SdfCarve` — avec une source différente (`§2.5`).
// `ConstantRock`, `SdfRoughness`, `SdfCarve`. Dans le `switch`, Maze et VerticalShafts sont
// deux fonctions de ~100 lignes qui n'ont rien en commun à l'œil ; en opérateurs, ce sont
// les MÊMES trois ops avec une source différente. C'est exactement ce que `§2.5` prédisait.
// //
// THREE of the five ops below are Maze's, reused without a line changed. In the switch, // THREE of the five ops below are Maze's, reused unchanged, with a different source.
// Maze and VerticalShafts are two unrelated ~100-line functions; as operators they are the
// same three ops with a different source.
constexpr float CarveBlend = 2.0f; constexpr float CarveBlend = 2.0f;
TUniquePtr<FShaftFieldSource> ShaftSource = TUniquePtr<FShaftFieldSource> ShaftSource =
@@ -9427,9 +9403,8 @@ namespace VoxelDensityOps
OutStack.Add(MakeConstantRockSource(P.BaseDensity)); OutStack.Add(MakeConstantRockSource(P.BaseDensity));
OutStack.Add(MoveTemp(ShaftSource)); OutStack.Add(MoveTemp(ShaftSource));
// Fréquence 0.1 et fenêtre `SurfaceRoughness + 4` — les constantes de // Fréquence 0.1 et fenêtre `SurfaceRoughness + 4` — PAS celles de Maze
// `GetVerticalShaftDensity`, PAS celles de Maze (0.12 / `R + rough + 2`). Même opérateur, // (0.12 / `R + rough + 2`). Même opérateur, réglages différents.
// réglages différents : c'est le point.
OutStack.Add(MakeSdfRoughnessMod(P.SurfaceRoughness, 0.1f, 3, P.SurfaceRoughness + 4.0f)); OutStack.Add(MakeSdfRoughnessMod(P.SurfaceRoughness, 0.1f, 3, P.SurfaceRoughness + 4.0f));
OutStack.Add(MakeSdfCarve(CarveBlend, P.BaseDensity)); OutStack.Add(MakeSdfCarve(CarveBlend, P.BaseDensity));
OutStack.Add(MakeUnique<FShaftLedgeMod>(P, ShaftPtr)); OutStack.Add(MakeUnique<FShaftLedgeMod>(P, ShaftPtr));
@@ -9544,8 +9519,8 @@ namespace VoxelDensityOps
OutStack.Add(MakeConstantVoidSource(P.BaseDensity)); OutStack.Add(MakeConstantVoidSource(P.BaseDensity));
OutStack.Add(MakeUnique<FIslandBlobSource>(P, Seed)); OutStack.Add(MakeUnique<FIslandBlobSource>(P, Seed));
// Fréquence 0.08 et 4 octaves — les constantes de `GetFloatingIslandDensity`. Quatrième // Fréquence 0.08 et 4 octaves — les constantes de FloatingIslands (Maze 0.12/3,
// archétype à réutiliser cet opérateur (Maze 0.12/3, VerticalShafts 0.1/3). // VerticalShafts 0.1/3).
OutStack.Add(MakeSdfRoughnessMod(P.SurfaceRoughness, 0.08f, 4, OutStack.Add(MakeSdfRoughnessMod(P.SurfaceRoughness, 0.08f, 4,
P.SurfaceRoughness + BlendK + 2.0f)); P.SurfaceRoughness + BlendK + 2.0f));
OutStack.Add(MakeSdfFill(BlendK, P.BaseDensity)); OutStack.Add(MakeSdfFill(BlendK, P.BaseDensity));
@@ -9562,15 +9537,14 @@ namespace VoxelDensityOps
int32 Seed, float SpineRadius, const UVoxelStrateManager* StrateManager, int32 Seed, float SpineRadius, const UVoxelStrateManager* StrateManager,
bool bAppendStructuralPosts) bool bAppendStructuralPosts)
{ {
// Les constantes viennent telles quelles de GetMazeDensity — elles y étaient codées en dur. // Constantes Maze, codées en dur. / Maze's hard-coded constants.
constexpr float CarveBlend = 2.0f; constexpr float CarveBlend = 2.0f;
constexpr float RoughFrequency = 0.12f; constexpr float RoughFrequency = 0.12f;
constexpr int32 RoughOctaves = 3; constexpr int32 RoughOctaves = 3;
const float R = FMath::Max(P.CorridorRadius, 0.5f); const float R = FMath::Max(P.CorridorRadius, 0.5f);
// Fenêtre d'application de la rugosité : `MazeSDF < R + SurfaceRoughness + 2.0f` dans // Fenêtre d'application de la rugosité : `MazeSDF < R + SurfaceRoughness + 2.0f`.
// l'original. Reproduite à l'identique pour que l'égalité binaire tienne.
const float RoughApplyWithin = R + P.SurfaceRoughness + 2.0f; const float RoughApplyWithin = R + P.SurfaceRoughness + 2.0f;
OutStack.Add(MakeConstantRockSource(P.BaseDensity)); OutStack.Add(MakeConstantRockSource(P.BaseDensity));
File diff suppressed because it is too large. Load diff
+8 -11
View File
@@ -190,10 +190,10 @@ public:
return EvalSample(WorldX, WorldY, WorldZ).Density; return EvalSample(WorldX, WorldY, WorldZ).Density;
} }
/** L'état COMPLET (densité + SDF) après toute la pile. Diagnostic : quand une comparaison /** L'état COMPLET (densité + SDF) après toute la pile. Diagnostic : quand deux évaluations
* avec l'ancien chemin diverge, c'est le canal SDF qui dit si l'écart naît avant ou après * divergent, c'est le canal SDF qui dit si l'écart naît avant ou après la conversion.
* la conversion. / The full state after the stack — the SDF channel is what says whether a * / The full state after the stack — the SDF channel is what says whether a divergence is
* divergence is born before or after the carve. */ * born before or after the carve. */
FVoxelOpSample EvalSample(float WorldX, float WorldY, float WorldZ) const FVoxelOpSample EvalSample(float WorldX, float WorldY, float WorldZ) const
{ {
FVoxelOpSample S; FVoxelOpSample S;
@@ -578,9 +578,8 @@ namespace VoxelDensityOps
* FlatPlain ET CrystalChamber — la même pile, **sans branchement sur le type** : * FlatPlain ET CrystalChamber — la même pile, **sans branchement sur le type** :
* SlabVoidSource → GridColumnMod → [structural post ×4] * SlabVoidSource → GridColumnMod → [structural post ×4]
* *
* C'est le premier vrai gain du refactor (OPSTACK-PLAN §4) : deux des huit archétypes * Deux des huit archétypes tiennent dans un opérateur (OPSTACK-PLAN §4) : leur différence est
* disparaissent dans un opérateur, et leur différence redevient ce qu'elle était déjà dans * un jeu de valeurs par défaut, pas du code.
* `GetSlabDensity` — un jeu de valeurs par défaut, pas du code.
*/ */
VOXELFORGE_API void BuildSlabStack(FVoxelOpStack& OutStack, const FSlabGenerationParams& P, VOXELFORGE_API void BuildSlabStack(FVoxelOpStack& OutStack, const FSlabGenerationParams& P,
int32 Seed, float SpineRadius, int32 Seed, float SpineRadius,
@@ -592,10 +591,8 @@ namespace VoxelDensityOps
* ConstantRock → ShaftField → SdfRoughness → SdfCarve → ShaftLedge * ConstantRock → ShaftField → SdfRoughness → SdfCarve → ShaftLedge
* → ShaftConnectorAir → ShaftConnectorFloor → [structural post ×4] * → ShaftConnectorAir → ShaftConnectorFloor → [structural post ×4]
* *
* C'est la démonstration que `§2.5` promettait : dans le `switch`, Maze et VerticalShafts sont * Trois de ces ops sont ceux de Maze (`§2.5`), avec une source différente et d'autres réglages
* deux fonctions de ~100 lignes sans rien de commun à l'œil ; en opérateurs, ce sont les mêmes * (fréquence 0.1 au lieu de 0.12, fenêtre `rough + 4` au lieu de `R + rough + 2`).
* trois ops avec une source différente et d'autres réglages (fréquence 0.1 au lieu de 0.12,
* fenêtre `rough + 4` au lieu de `R + rough + 2`).
*/ */
VOXELFORGE_API void BuildVerticalShaftStack(FVoxelOpStack& OutStack, const FVerticalShaftParams& P, VOXELFORGE_API void BuildVerticalShaftStack(FVoxelOpStack& OutStack, const FVerticalShaftParams& P,
int32 Seed, float SpineRadius, int32 Seed, float SpineRadius,
+13 -73
View File
@@ -3,8 +3,8 @@
// //
// Pipeline density-only: plus de grille de blocs, plus de surface heightfield. // Pipeline density-only: plus de grille de blocs, plus de surface heightfield.
// Le mesher appelle GetDensityAt() par voxel pour reconstruire la géométrie. // Le mesher appelle GetDensityAt() par voxel pour reconstruire la géométrie.
// Toute la logique de caves vit dans GetDensityWithParams / GetSlabDensity, // Chaque strate est générée par sa pile d'opérateurs (VoxelDensityOpStack.h), pilotée par les
// pilotée par les params de strate récupérés auprès du StrateManager. // params de strate récupérés auprès du StrateManager.
#pragma once #pragma once
@@ -202,7 +202,7 @@ public:
/** /**
* Densité pour une strate TunnelNetwork (rooms + tunnels + worm noise). * Densité pour une strate TunnelNetwork (rooms + tunnels + worm noise).
* Utilisée en interne par GetDensityAt quand la strate est de ce type. * C'est l'évaluateur fusionné de la pile TunnelNetwork/Underwater dans GetDensityAt.
* Exposée pour permettre des tests isolés avec des params custom. * Exposée pour permettre des tests isolés avec des params custom.
* *
* ⚠️ `ParamsFingerprint` ET `LayoutVersion` SONT OBLIGATOIRES (`AUDIT §C2`). Le cache SDF * ⚠️ `ParamsFingerprint` ET `LayoutVersion` SONT OBLIGATOIRES (`AUDIT §C2`). Le cache SDF
@@ -242,56 +242,8 @@ public:
bool bCollectFusedDiagnostics = false, bool bCollectFusedDiagnostics = false,
// Optional same-sample hand-off from EvaluateSDFCached. This is // Optional same-sample hand-off from EvaluateSDFCached. This is
// only a room-shape ownership bit; it does not alter the scalar // only a room-shape ownership bit; it does not alter the scalar
// result and avoids a duplicate room-index query in native paths. // result and avoids a duplicate room-index query.
bool* OutRoomOwnsBottom = nullptr, bool* OutRoomOwnsBottom = nullptr) const;
// Optional same-sample warped position. Native GetDensityAt uses
// this to feed the core post without applying the cave warp twice.
FVector* OutWarpedPosition = nullptr) const;
/**
* Densité pour une strate Slab (FlatPlain / CrystalChamber).
* Vide horizontal entre un sol bruité et un plafond bruité.
*/
float GetSlabDensity(float WorldX, float WorldY, float WorldZ,
const FSlabGenerationParams& Params) const;
/**
* Densité pour une strate Maze — couloirs sur un treillis 3D déterministe, avec les décisions
* parent/boucle reconstruites par cellule dans un cache thread-local; le hot path ne parcourt
* que les capsules déjà émises.
*/
float GetMazeDensity(float WorldX, float WorldY, float WorldZ,
const FMazeGenerationParams& Params) const;
/**
* Densité pour une strate SurfaceWorld — terrain à ciel ouvert (collines,
* montagnes, plages) sous un plafond solide, avec nappe d'eau optionnelle.
*
* Biome output-blend: the heightfield is evaluated with ParamsD (the voxel's dominant
* biome) and, when NeighborWeight > 0, also with ParamsN (its nearest neighbour); the
* two surface HEIGHTS are lerped. This stays seamless across ANY param difference
* (frequencies included). With no biomes, pass ParamsD == ParamsN and weight 0 →
* bit-identical to the single-param terrain. Structural fields (Z bounds, seal, base,
* water level) must be equal in both (forced from the strate).
*/
float GetSurfaceDensity(float WorldX, float WorldY, float WorldZ,
const FSurfaceGenerationParams& ParamsD,
const FSurfaceGenerationParams& ParamsN,
float NeighborWeight) const;
/**
* Densité pour une strate VerticalShafts — puits verticaux pleine hauteur,
* vires horizontales, et connecteurs horizontaux occasionnels.
*/
float GetVerticalShaftDensity(float WorldX, float WorldY, float WorldZ,
const FVerticalShaftParams& Params) const;
/**
* Densité pour une strate FloatingIslands — masses de terre suspendues dans
* un grand vide ouvert (îles flottantes), sommets aplatis, dessous rugueux.
*/
float GetFloatingIslandDensity(float WorldX, float WorldY, float WorldZ,
const FFloatingIslandParams& Params) const;
//========================================================================= //=========================================================================
// CLIMATE & BIOME FIELDS (pure XY, deterministic, window-invariant) // CLIMATE & BIOME FIELDS (pure XY, deterministic, window-invariant)
@@ -308,20 +260,19 @@ public:
* La chaîne de hauteur complète de SurfaceWorld : structural → cliff → terrace → layer lines → * La chaîne de hauteur complète de SurfaceWorld : structural → cliff → terrace → layer lines →
* plage. Rend une ALTITUDE monde en voxels, pas une densité. * plage. Rend une ALTITUDE monde en voxels, pas une densité.
* *
* PUBLIQUE pour la même raison que `GetSlabDensity` / `GetMazeDensity` : permettre un test * PUBLIQUE pour permettre un test isolé : c'est la référence de
* isolé. C'est la référence de `VoxelForge.OpStack.SurfaceHeightEquivalence`, qui compare la * `VoxelForge.OpStack.SurfaceHeightEquivalence`, qui compare la pile d'opérateurs de hauteur
* pile d'opérateurs de hauteur (`VoxelHeightOp.h`) à cette fonction point par point. * (`VoxelHeightOp.h`) à cette fonction point par point.
* Public so the height-op stack can be measured against it — same reason as GetSlabDensity. * Public so the height-op stack can be measured against it.
*/ */
float ComputeSurfaceTerrainZ(float WorldX, float WorldY, const FSurfaceGenerationParams& Params) const; float ComputeSurfaceTerrainZ(float WorldX, float WorldY, const FSurfaceGenerationParams& Params) const;
/** /**
* La colonne de surface : terrain Z, plafond, et le gate d'OVERHANG résolu par colonne * La colonne de surface : terrain Z, plafond, et le gate d'OVERHANG résolu par colonne
* (amplitude + direction amont). PUBLIQUES toutes deux pour la même raison que ci-dessus : * (amplitude + direction amont). C'est la colonne que `ClassifyTile` teste pour SurfaceWorld ;
* c'est le seul chemin qui calcule l'overhang — `GetSurfaceDensity` passe `OverhangAmp = 0` — * publique pour que les tests puissent placer des échantillons dans la fenêtre d'overhang.
* donc c'est la seule référence possible pour `FOverhangShelfMod`. * The surface column ClassifyTile tests for SurfaceWorld; public so tests can aim samples at
* Public because this is the ONLY path that computes the overhang (GetSurfaceDensity passes 0), * the overhang window.
* so it is the only possible reference for the ported op.
*/ */
void ComputeSurfaceColumn(float WorldX, float WorldY, int32 ChunkZ, void ComputeSurfaceColumn(float WorldX, float WorldY, int32 ChunkZ,
const FSurfaceGenerationParams& BaseSurface, const FBiomeContext& BiomeCtx, const FSurfaceGenerationParams& BaseSurface, const FBiomeContext& BiomeCtx,
@@ -329,12 +280,6 @@ public:
float& OutTerrainZ, float& OutCeilSurf, float& OutTerrainZ, float& OutCeilSurf,
float& OutOverhangAmp, float& OutDirX, float& OutDirY) const; float& OutOverhangAmp, float& OutDirX, float& OutDirY) const;
/** Le combine par voxel : colonne → densité, overhang compris, puis le post structurel. */
float SurfaceDensityFromColumn(float WorldX, float WorldY, float WorldZ,
float TerrainZ, float CeilSurf,
float OverhangAmp, float DirX, float DirY,
const FSurfaceGenerationParams& S) const;
/** /**
* Moisture field at a world XY → [0,1]. The second climate axis for biome placement. * Moisture field at a world XY → [0,1]. The second climate axis for biome placement.
*/ */
@@ -467,11 +412,6 @@ private:
FSurfaceGenerationParams& OutSurface, FBiomeContext& OutBiomeCtx, FSurfaceGenerationParams& OutSurface, FBiomeContext& OutBiomeCtx,
TArray<FSurfaceGenerationParams>& OutBiomeParams) const; TArray<FSurfaceGenerationParams>& OutBiomeParams) const;
// ComputeSurfaceColumn et SurfaceDensityFromColumn ont été DÉPLACÉES en `public` (voir plus
// haut) : c'est le seul chemin qui calcule l'overhang, donc la seule référence possible pour
// VoxelForge.OpStack.SurfaceHeightEquivalence. Une seule déclaration chacune.
// Moved to public above — the only path that computes the overhang, hence the only oracle.
/** (Re)build the per-chunk biome cell grid covering chunk (X,Y) footprint + margin. */ /** (Re)build the per-chunk biome cell grid covering chunk (X,Y) footprint + margin. */
void RebuildBiomeGrid(int32 ChunkX, int32 ChunkY, int32 ChunkZ, void RebuildBiomeGrid(int32 ChunkX, int32 ChunkY, int32 ChunkZ,
const FBiomeContext& Ctx, FChunkBiomeCache& Cache, const FBiomeContext& Ctx, FChunkBiomeCache& Cache,