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)"),
NumHoles, 0);
// ── Stabilité du verdict : ClassifyTile partage GSurfColCache avec GetDensityAt, donc le
// brute-force ci-dessus a réchauffé les caches. Re-classifier doit rendre le MÊME verdict.
// Verdict stability: ClassifyTile shares GSurfColCache with GetDensityAt, so the brute force
// above warmed the caches. Re-classifying must yield the SAME verdict.
// ── Stabilité du verdict : le premier ClassifyTile a réchauffé son cache de colonnes et le
// brute-force ci-dessus les caches de GetDensityAt. Re-classifier doit rendre le MÊME verdict.
// Verdict stability: the first ClassifyTile warmed its column cache and the brute force
// above warmed GetDensityAt's caches. Re-classifying must yield the SAME verdict.
for (const FTileSpec& Spec : ToVerify)
{
const EVoxelTileClass A = Gen->ClassifyTile(Spec.Origin, Spec.Step, Spec.Cells);
@@ -13,8 +13,8 @@
// seam, and in multiplayer a world divergence.
//
// POURQUOI CE TEST EXISTE / WHY THIS TEST EXISTS:
// ~30 caches thread_local à clé manuelle vivent sous GetDensityAt (CP_*, GSurfColCache, les
// slots de diff, le cache SDF). Chacun est correct exactement tant que sa CLÉ contient toutes
// ~30 caches thread_local à clé manuelle vivent sous GetDensityAt (CP_*, les mémos par cellule
// 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 :
// 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
@@ -351,7 +351,7 @@ bool FVoxelForgeLiveEditInvalidationTest::RunTest(const FString& Parameters)
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("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("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
// 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
// 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.
// D'où une seconde famille, `VoxelHeightOp.h`, mesurée ici contre `ComputeSurfaceTerrainZ`.
//
// ⚠️ CE QUE CE TEST COUVRE
// ✅ 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.
//
// The old GetSurfaceDensity helper remains a differential diagnostic only: it does not carry the
// current common structural post. The assertions target the height stack and the density stack
// 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.
// LA BARRE : **bit à bit** pour les opérations en espace hauteur.
// The bar is bit-identity for height-space operations.
#if WITH_DEV_AUTOMATION_TESTS
@@ -285,18 +277,9 @@ bool FVoxelForgeHeightStackTest::RunTest(const FString& Parameters)
// ÉTAPE 2a — LE PONT VERS L'ESPACE 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**
// (il passe `OverhangAmp = 0`) et **sans biomes** (ParamsD == ParamsN, poids 0) — donc la
// 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.
// Cette passe est la variante **sans overhang** et **sans biomes** ; l'overhang a sa propre
// passe juste en dessous. / No-overhang, no-biome variant; the overhang has its own pass below.
{
// ⚠️ `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;
P.OverhangStrength = 0.0f;
@@ -318,8 +301,7 @@ bool FVoxelForgeHeightStackTest::RunTest(const FString& Parameters)
Ctx.StrateBottomWorldZ = P.StrateBottomWorldZ;
Stack.PrepareChunk(Ctx);
int32 NumDiff = 0, NumSideDisagree = 0, WorstIdx = -1, NumNonFinite = 0;
float WorstDelta = 0.0f;
int32 NumNonFinite = 0;
FRandomStream Rng(5150);
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 Y = (float)Rng.RandRange(-4 * CHUNK_SIZE, 4 * CHUNK_SIZE);
const float Z = (float)Rng.RandRange(BottomVoxelZ, TopVoxelZ);
// 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))
if (!FMath::IsFinite(Stack.EvalMC(X, Y, Z)))
{
++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"),
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` (qui résout le gate et la direction amont par colonne) suivi de
// `SurfaceDensityFromColumn` (qui applique l'union par voxel). Les deux viennent d'être
// exposées pour ça.
//
// 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.
// `ComputeSurfaceColumn` résout le gate et la direction amont par colonne ; la moitié des
// échantillons est placée DANS la fenêtre d'overhang qu'il rapporte, pour que l'op overhang de
// la pile soit réellement exercé. / Half the samples are placed inside the overhang window that
// ComputeSurfaceColumn reports, so the stack's overhang op is actually exercised.
{
FSurfaceGenerationParams P = AllOps;
P.OverhangStrength = 0.8f;
@@ -391,8 +352,7 @@ bool FVoxelForgeHeightStackTest::RunTest(const FString& Parameters)
TArray<FSurfaceGenerationParams> NoBiomeParams;
FChunkBiomeCache BiomeCache;
int32 NumDiff = 0, NumSideDisagree = 0, NumInWindow = 0, NumNonFinite = 0;
float WorstDelta = 0.0f;
int32 NumInWindow = 0, NumNonFinite = 0;
FRandomStream Rng(1337);
for (int32 i = 0; i < NumHeightSamples; ++i)
@@ -418,27 +378,16 @@ bool FVoxelForgeHeightStackTest::RunTest(const FString& Parameters)
Z = (float)Rng.RandRange(BottomVoxelZ, TopVoxelZ);
}
const float Old = Gen->SurfaceDensityFromColumn(X, Y, Z, TerrainZ, CeilSurf,
Amp, DirX, DirY, P);
const float New = Stack.EvalMC(X, Y, Z);
if (!FMath::IsFinite(New))
if (!FMath::IsFinite(Stack.EvalMC(X, Y, Z)))
{
++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(
TEXT("Overhang legacy diagnostic: %d of %d samples differ from "
"SurfaceDensityFromColumn (largest |delta| %.9g), %d side disagreements, "
"%d deliberately inside the overhang window; current stack non-finite samples=%d."),
NumDiff, NumHeightSamples, WorstDelta, NumSideDisagree, NumInWindow, NumNonFinite));
TEXT("Overhang: %d of %d samples deliberately inside the overhang window; current "
"stack non-finite samples=%d."),
NumInWindow, NumHeightSamples, NumNonFinite));
TestEqual(TEXT("overhang: current density stack has no non-finite samples"),
NumNonFinite, 0);
@@ -108,36 +108,37 @@ bool FVoxelForgeMazeSeamTest::RunTest(const FString& Parameters)
return false;
}
int32 Top = 0, Bottom = 0;
if (!World.GetSlotVoxelZRange(FTestWorld::SlotMaze, Top, Bottom))
int32 MazeTopZ = 0, MazeBottomZ = 0;
if (!World.GetSlotVoxelZRange(FTestWorld::SlotMaze, MazeTopZ, MazeBottomZ))
{
AddError(TEXT("The seam fixture has no Maze slot."));
return false;
}
const int32 MidChunkZ = ((Top + Bottom) / 2) / CHUNK_SIZE;
const FMazeGenerationParams Params = World.StrateManager->GetMazeParamsForChunk(
FIntVector(0, 0, MidChunkZ));
const int32 MazeMidChunkZ = ((MazeTopZ + MazeBottomZ) / 2) / CHUNK_SIZE;
const FMazeGenerationParams MazeParams = World.StrateManager->GetMazeParamsForChunk(
FIntVector(0, 0, MazeMidChunkZ));
TArray<FVector> Probes;
AddMazeBoundaryProbes(Params, Bottom, Top, Probes);
TArray<FVector> SeamProbes;
AddMazeBoundaryProbes(MazeParams, MazeBottomZ, MazeTopZ, SeamProbes);
int32 First = INDEX_NONE;
float Before = 0.0f, After = 0.0f;
const int32 Mismatches = CountContextMismatches(World, Probes, First, Before, After);
int32 FirstProbe = INDEX_NONE;
float FirstBefore = 0.0f, FirstAfter = 0.0f;
const int32 NumMismatches = CountContextMismatches(
World, SeamProbes, FirstProbe, FirstBefore, FirstAfter);
TestEqual(TEXT("Maze has no chunk-context seam mismatches"), Mismatches, 0);
TestTrue(TEXT("Maze seam test sampled cell-boundary probes"), Probes.Num() > 0);
TestEqual(TEXT("Maze has no chunk-context seam mismatches"), NumMismatches, 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"),
First, Before, After));
FirstProbe, FirstBefore, FirstAfter));
}
AddInfo(FString::Printf(
TEXT("Maze seam freedom: %d boundary probes, mismatches=%d; "
"local window=2x2x2 child nodes ({-1,0}^3), no wide collect."),
Probes.Num(), Mismatches));
SeamProbes.Num(), NumMismatches));
return true;
}
@@ -1,30 +1,18 @@
// VoxelForgeOpStackIslandTest.cpp
// FloatingIslands — le portage qui fait tourner la pile À L'ENVERS.
// FloatingIslands — the port that runs the stack BACKWARDS.
// FloatingIslands — la pile qui tourne À L'ENVERS / the stack that runs BACKWARDS.
//
// CE QUE CELUI-CI PROUVE EN PLUS DES AUTRES
// `VerticalShaftEquivalence` a mesuré la réutilisation À L'IDENTIQUE : trois opérateurs de Maze
// 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 :
// Les autres archétypes partent de ROC et CREUSENT ; FloatingIslands part du VIDE et REMPLIT, avec
// les MÊMES opérateurs au signe près (convention interne positif = solide) :
//
// FConstantFieldSource(+Base) ←→ FConstantFieldSource(-Base)
// 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
// réutilise en s'INVERSANT est une preuve plus forte qu'un archétype qui se réutilise à l'identique
// — le premier dit que l'abstraction a trouvé le bon axe, le second seulement que deux archétypes
// se ressemblaient.
// Le seul opérateur propre à l'archétype est le blob d'île. / The only archetype-specific op is the
// island blob; the rest is Maze's rock + carve with the opposite sign.
//
// ET LE VERDICT DE BOÎTE : c'est ici que `ClassifyBox` peut rendre **AllAir** pour la première fois
// de tout le plugin. Une strate d'îles flottantes est, par construction, surtout vide ; aucun
// archétype de grotte n'a jamais su prouver « tout air » (`OPSTACK-DECOMPOSITION §7`). Le test
// 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).
// ET LE VERDICT DE BOÎTE : une strate d'îles flottantes est surtout vide, donc `ClassifyBox` doit
// savoir rendre **AllAir** (`OPSTACK-DECOMPOSITION §7`). Le test compte AllSolid et AllAir
// SÉPARÉMENT, parce qu'un total agrégé masquerait exactement ce verdict-là.
#if WITH_DEV_AUTOMATION_TESTS
@@ -92,8 +80,8 @@ bool FVoxelForgeOpStackIslandTest::RunTest(const FString& Parameters)
if (P.StrateTopWorldZ - P.StrateBottomWorldZ <= 0.0f)
{
AddError(TEXT("The FloatingIslands strate has degenerate Z bounds, which sends ")
TEXT("GetFloatingIslandDensity down its early-out. The op stack has none by design."));
AddError(TEXT("The FloatingIslands strate has degenerate Z bounds; GetDensityAt builds no ")
TEXT("stack for it. The op stack has no degenerate-strate early-out by design."));
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
// 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 InnerTop = P.StrateTopWorldZ - P.BoundarySealThickness;
int32 NumDiff = 0, NumSideDisagree = 0, WorstIdx = -1;
int32 NumInsideIsland = 0, NumOpenVoid = 0;
float WorstDelta = 0.0f;
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 Old = Gen->GetFloatingIslandDensity(X, Y, Z, P);
const float New = Stack.EvalMC(X, Y, Z);
const bool bInterior = (Z > InnerBot && Z < InnerTop);
if (bInterior && Old < 0.0f) { ++NumInsideIsland; } // solide loin des seals ⇒ une île
if (bInterior && Old >= 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; }
if (bInterior && New < 0.0f) { ++NumInsideIsland; } // solide loin des seals ⇒ une île
if (bInterior && New >= 0.0f) { ++NumOpenVoid; }
}
if (NumDiff == 0)
{
AddInfo(FString::Printf(
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);
AddInfo(FString::Printf(
TEXT("FloatingIslands: %d of %d samples inside island rock away from the seal bands, %d in ")
TEXT("open void."),
NumInsideIsland, NumIslandSamples, NumOpenVoid));
if (NumInsideIsland == 0)
{
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("above then only proves that two empty voids agree. Raise IslandDensity or ")
TEXT("IslandMaxRadius."));
TEXT("source and the fill were never meaningfully exercised. Raise IslandDensity ")
TEXT("or IslandMaxRadius."));
}
//=========================================================================
// 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é
// inclut délibérément `BoundarySealThickness`, que l'original omet alors que `SpreadZ` le lit
// (voir la note dans FIslandBlobSource::GetCells).
// inclut délibérément `BoundarySealThickness`, que `SpreadZ` lit (voir la note dans
// FIslandBlobSource::GetCells).
{
std::atomic<int32> Impure{ 0 };
const int32 NumBlocks = FMath::Max(4, FMath::Min(16, FPlatformMisc::NumberOfCores()));
@@ -296,9 +244,8 @@ bool FVoxelForgeOpStackIslandTest::RunTest(const FString& Parameters)
AddInfo(FString::Printf(
TEXT("Box verdicts over 60 FloatingIslands tiles (XY sampled from +/- %d voxels = %.1f x ")
TEXT("IslandSpacing %.0f): %d proved AllSolid, %d proved AllAir, ")
TEXT("%d Mixed, %d voxels checked, %d violations. Today's ClassifyTile proves ZERO of these. The AllAir count is the new ")
TEXT("thing: no cave archetype has ever been able to prove 'all air', and a floating-")
TEXT("island strate is mostly exactly that (OPSTACK-DECOMPOSITION 7)."),
TEXT("%d Mixed, %d voxels checked, %d violations. The AllAir count matters most: a ")
TEXT("floating-island strate is mostly open air (OPSTACK-DECOMPOSITION 7)."),
SpanVoxels, (float)SpanVoxels / FMath::Max(P.IslandSpacing, 1.0f), P.IslandSpacing,
NumProvedSolid, NumProvedAir, NumMixed, NumBruteSamples, NumUnsound));
@@ -1,73 +1,17 @@
// VoxelForgeOpStackMazeTest.cpp
// PHASE 1, LE TEST QUI COMPTE — la pile d'opérateurs Maze contre GetMazeDensity.
// PHASE 1'S LOAD-BEARING TEST — the Maze operator stack against GetMazeDensity.
// La pile d'opérateurs Maze / The Maze operator stack.
//
// CE QUE LA PHASE 1 DEVAIT PROUVER / WHAT PHASE 1 HAD TO PROVE
// Le déclencheur d'arrêt de `OPSTACK-PLAN §4` : **« est-ce que la séparation source / modifier tombe
// naturellement du code existant ? »** Réponse mesurée : oui. Maze se décompose en huit opérateurs
// sans contorsion, le SDF est reproduit BIT POUR BIT, et aucun échantillon ne change de côté de
// l'isosurface.
//
// ═════════════════════════════════════════════════════════════════════════════════════════
// ✅ MISE À JOUR 2026-07-27 : LE PLANCHER ULP N'EXISTE PLUS. C'ÉTAIT `/fp:fast`.
// ═════════════════════════════════════════════════════════════════════════════════════════
// `FPSemantics = Precise` sur le module (AUDIT §C9, posé pour le cross-play Linux/Windows) fait
// passer ce test à **BIT-IDENTIQUE**. La section ci-dessous décrit un état RÉVOLU ; elle est gardée
// 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.
// Ce que ce test vérifie / What this test checks:
// • TOPOLOGIE — le graphe parent orienté vers l'origine est un arbre couvrant (N-1 arêtes, zéro
// cellule déconnectée), les boucles optionnelles restent bornées, et le graphe est déterministe
// sur 64 seeds. / The origin-directed parent graph is a spanning tree, deterministic per seed.
// • 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
// 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
// trou), y compris pour une pile source/convertisseur jamais écrite à la main.
// Every uniform box verdict survives brute force, including for a novel source/converter stack.
#if WITH_DEV_AUTOMATION_TESTS
@@ -355,13 +299,13 @@ bool FVoxelForgeOpStackMazeTest::RunTest(const FString& Parameters)
return false;
}
// GetMazeDensity court-circuite sur une strate dégénérée (`return 1.0f`). Cette garde appartient
// à la fonction d'archétype, pas à un opérateur ; la pile suppose une strate valide.
// La pile suppose une strate valide ; `GetDensityAt` rend de l'air sans pile sur une strate
// 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)
{
AddError(FString::Printf(
TEXT("The Maze strate has degenerate Z bounds (top %.1f, bottom %.1f), which sends ")
TEXT("GetMazeDensity down its early-out. The op stack has no such early-out by design."),
TEXT("The Maze strate has degenerate Z bounds (top %.1f, bottom %.1f); GetDensityAt ")
TEXT("builds no stack for it. The op stack has no degenerate-strate early-out by design."),
MazeParams.StrateTopWorldZ, MazeParams.StrateBottomWorldZ));
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.
//=========================================================================
@@ -624,10 +504,9 @@ bool FVoxelForgeOpStackMazeTest::RunTest(const FString& Parameters)
//=========================================================================
// 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,
// VerticalShafts, FloatingIslands, FlatPlain, CrystalChamber et Underwater ne captent RIEN du
// gain T1.d. Tout nombre > 0 ici est le saut de tuile propre à la pile, pas une décision du
// classifieur historique.
// Pour un slot de cave, ClassifyTile plie `ClassifyBox` de cette pile : tout nombre > 0 ici est
// une tuile que le mesher saute. / For a cave slot ClassifyTile folds this stack's ClassifyBox:
// every proved box here is a tile the mesher skips.
{
int32 NumProved = 0, NumMixed = 0, NumUnsound = 0, NumBruteSamples = 0;
FRandomStream Rng(24680);
@@ -689,17 +568,14 @@ bool FVoxelForgeOpStackMazeTest::RunTest(const FString& Parameters)
AddInfo(FString::Printf(
TEXT("Box verdicts over %d Maze tiles (XY sampled from +/- %d voxels = %.1f x ")
TEXT("CellSize %.0f): %d proved uniform, %d Mixed, %d voxels checked, %d violations. Today's ClassifyTile ")
TEXT("proves ZERO of these -- every cave archetype falls through to \"pas prouvable en ")
TEXT("v1\". Any number above zero here is tile-skipping Maze has never had."),
TEXT("CellSize %.0f): %d proved uniform, %d Mixed, %d voxels checked, %d violations."),
NumMazeBoxTests, SpanVoxels, (float)SpanVoxels / FMath::Max(MazeParams.CellSize, 1.0f), MazeParams.CellSize,
NumProved, NumMixed, NumBruteSamples, NumUnsound));
if (NumProved == 0)
{
AddWarning(TEXT("The stack proved no tile uniform, so it is not yet better than today's ")
TEXT("classifier for Maze. Not a correctness problem, but the perf case for ")
TEXT("the port rests on this number."));
AddWarning(TEXT("The stack proved no tile uniform, so ClassifyTile skips no Maze tile. ")
TEXT("Not a correctness problem, but a perf regression."));
}
}
@@ -1,21 +1,15 @@
// VoxelForgeOpStackShaftTest.cpp
// VerticalShafts — le portage qui teste la RÉUTILISATION et le contrat de l'opérateur courant.
// VerticalShafts — the port that tests REUSE and the current operator contract.
// VerticalShafts — la RÉUTILISATION d'opérateurs et le contrat de la pile.
// VerticalShafts — operator REUSE and the stack's contract.
//
// CE QUE CELUI-CI PROUVE EN PLUS DES AUTRES
// Les portages précédents demandaient « la décomposition reproduit-elle l'original ? ». Celui-ci
// demande **« les opérateurs se RÉUTILISENT-ils vraiment entre archétypes ? »**, qui est la thèse
// de `OPSTACK-PLAN §2.5` et la seule raison de faire ce refactor plutôt que de nettoyer le `switch`.
// Trois des cinq opérateurs de VerticalShafts sont ceux de Maze, repris tels quels :
// `ConstantRock`, `SdfRoughness`, `SdfCarve` — mêmes ops, autre source et autres réglages
// (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` : 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
// changement** : `ConstantRock`, `SdfRoughness`, `SdfCarve`. Dans le `switch`, `GetMazeDensity` et
// `GetVerticalShaftDensity` 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 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.
// Three of the five VerticalShafts ops are Maze's, reused unchanged. The assertions below target
// the stack used in-game: channel count/order, feature liveness, window invariance, cache cost and
// box-proof soundness.
#if WITH_DEV_AUTOMATION_TESTS
@@ -93,8 +87,8 @@ bool FVoxelForgeOpStackShaftTest::RunTest(const FString& Parameters)
if (P.StrateTopWorldZ - P.StrateBottomWorldZ <= 0.0f)
{
AddError(TEXT("The VerticalShafts strate has degenerate Z bounds, which sends ")
TEXT("GetVerticalShaftDensity down its early-out. The op stack has none by design."));
AddError(TEXT("The VerticalShafts strate has degenerate Z bounds; GetDensityAt builds no ")
TEXT("stack for it. The op stack has no degenerate-strate early-out by design."));
return false;
}
@@ -128,49 +122,17 @@ bool FVoxelForgeOpStackShaftTest::RunTest(const FString& Parameters)
}
//=========================================================================
// 1. CURRENT-OWNER DIAGNOSTIC
// 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.
// 1. LIVENESS — des échantillons tombent dans l'air des puits / samples land in shaft air
//=========================================================================
int32 NumDiff = 0, NumSideDisagree = 0, WorstIdx = -1, NumInsideShaft = 0;
float WorstDelta = 0.0f;
int32 NumInsideShaft = 0;
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 Old = Gen->GetVerticalShaftDensity(X, Y, Z, P);
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; }
// air ⇒ dans un puits/connecteur/étagère / air ⇒ inside a shaft, connector or ledge
if (EvalStackMCForTest(Stack, X, Y, Z) >= 0.0f) { ++NumInsideShaft; }
}
if (NumDiff == 0)
{
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));
AddInfo(FString::Printf(TEXT("VerticalShafts: %d of %d samples are air in the stack."),
NumInsideShaft, NumShaftSamples));
TestTrue(TEXT("shaft feature sampling exercised the operator stack"), NumInsideShaft > 0);
if (NumInsideShaft == 0)
@@ -268,14 +230,14 @@ bool FVoxelForgeOpStackShaftTest::RunTest(const FString& Parameters)
const int32 CellY = (i / 16) - 8;
LastX = (static_cast<float>(CellX) + 0.37f) * 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 HotStart = FPlatformTime::Seconds();
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;
@@ -1,32 +1,20 @@
// VoxelForgeOpStackSlabTest.cpp
// PHASE 2, PREMIER PORTAGE — la pile Slab courante, sur LES DEUX archétypes.
// PHASE 2'S FIRST PORT — the current Slab operator stack, on BOTH archetypes.
// La pile Slab, sur LES DEUX archétypes / The Slab operator stack, on BOTH archetypes.
//
// 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
// guard the path used by the game. `GetSlabDensity` is retained as a legacy diagnostic only:
// it still owns the former full passage/support tail, while the operator stack is followed by
// the current common MC post.
// 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.
// 1. LE CHEMIN DU JEU — la pile rend une densité finie, invariante à la fenêtre (ordre et
// threads), et ses verdicts de boîte sont conservatifs.
// The stack's density is finite and window-invariant, and its box verdicts are conservative.
// 2. UN OPÉRATEUR, DEUX ARCHÉTYPES — la MÊME pile est vérifiée pour FlatPlain ET CrystalChamber.
// ⚠️ 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
// 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
// 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
// FlatPlain et CrystalChamber aujourd'hui. Depuis que les deux surfaces sont XY-PURES, leurs
// bornes en Z sont connues exactement (contrat [-1,1] de FBM), donc toute tuile entièrement
// 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.
// 3. LE VERDICT DE BOÎTE — les deux surfaces sont XY-PURES, donc leurs bornes en Z sont connues
// exactement (contrat [-1,1] de FBM) : toute tuile entièrement sous le sol ou entre les deux
// bandes se prouve SANS échantillonner.
#if WITH_DEV_AUTOMATION_TESTS
@@ -73,14 +61,13 @@ bool FVoxelForgeOpStackSlabTest::RunTest(const FString& Parameters)
int32 TopVoxelZ, int32 BottomVoxelZ,
int32 SlotIndex, const TCHAR* SlotName)
{
// `GetSlabDensity` court-circuite sur une strate dégénérée (`return 1.0f`). Cette garde
// appartient à la fonction d'archétype, pas à un opérateur ; la pile suppose une strate
// valide, et `GetDensityAt` retombe sur le `switch` dans ce cas.
// La pile suppose une strate valide ; `GetDensityAt` rend de l'air sans pile sur une strate
// dégénérée. / The stack assumes a valid strate; GetDensityAt returns air for a degenerate one.
if (SlabParams.StrateTopWorldZ - SlabParams.StrateBottomWorldZ <= 0.0f)
{
AddError(FString::Printf(
TEXT("%s has degenerate Z bounds (top %.1f, bottom %.1f), which sends GetSlabDensity ")
TEXT("down its early-out. The op stack has no such early-out by design."),
TEXT("%s has degenerate Z bounds (top %.1f, bottom %.1f); GetDensityAt builds no ")
TEXT("stack for it. The op stack has no degenerate-strate early-out by design."),
SlotName, SlabParams.StrateTopWorldZ, SlabParams.StrateBottomWorldZ));
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;
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)
{
const float X = (float)Points[i].X, Y = (float)Points[i].Y, Z = (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))
if (!FMath::IsFinite(Stack.EvalMC((float)Points[i].X, (float)Points[i].Y, (float)Points[i].Z)))
{
++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),
NumNonFinite, 0);
@@ -300,9 +213,9 @@ bool FVoxelForgeOpStackSlabTest::RunTest(const FString& Parameters)
AddInfo(FString::Printf(
TEXT("%s box verdicts over %d tiles (XY sampled from +/- %d voxels = %.1f x ")
TEXT("ColumnSpacing %.0f): %d proved uniform, %d Mixed, %d voxels checked, %d violations. Today's ")
TEXT("ClassifyTile proves ZERO of these. This number is the whole point of making ")
TEXT("the slab surfaces XY-pure (OPSTACK-DECOMPOSITION 3.1)."),
TEXT("ColumnSpacing %.0f): %d proved uniform, %d Mixed, %d voxels checked, %d violations. ")
TEXT("This number is the whole point of making the slab surfaces XY-pure ")
TEXT("(OPSTACK-DECOMPOSITION 3.1)."),
SlotName, NumSlabTiles, SpanVoxels,
(float)SpanVoxels / FMath::Max(SlabParams.ColumnSpacing, 1.0f), SlabParams.ColumnSpacing,
NumProved, NumMixed, NumBruteSamples, NumUnsound));
@@ -8,6 +8,7 @@
#include "Misc/AutomationTest.h"
#include "VoxelCaveMorphology.h"
#include "VoxelDensityOpStack.h"
#include "VoxelDensityPrimitives.h"
#include "VoxelForgeTestFixture.h"
#include "VoxelPassageGeometry.h"
@@ -900,11 +901,27 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
++NumFootingChecked;
// 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
// source itself: air at the landing and solid immediately below it.
TStrongObjectPtr<UVoxelGenerator> SourceOnlyGenerator(
NewObject<UVoxelGenerator>(GetTransientPackage(), NAME_None, RF_Transient));
SourceOnlyGenerator->InitializeSettings(World.Settings.Get());
// already known to be passage-carved, so the archetype's own stack, built without a
// strate manager (hence without passage carving), checks the source itself: air at
// the landing and solid immediately below it.
const UVoxelGenerator& LiveGenerator = *World.Generator;
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 FootingDensity = 0.0f;
@@ -915,13 +932,16 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
FMazeGenerationParams P = Definition->MazeParams;
P.StrateTopWorldZ = StrateTopZ;
P.StrateBottomWorldZ = StrateBottomZ;
LandingDensity = SourceOnlyGenerator->GetMazeDensity(
SuggestedPoint.X, SuggestedPoint.Y, SuggestedPoint.Z, P);
VoxelDensityOps::BuildMazeStack(SourceStack, P, LiveGenerator.Seed,
LiveGenerator.OriginSpineRadius, /*StrateManager*/nullptr);
SourceStack.PrepareChunk(SourceContext);
LandingDensity = EvalSource(
SuggestedPoint.X, SuggestedPoint.Y, SuggestedPoint.Z);
const float FloorProbeZ = SuggestedPoint.Z
- FMath::Max(P.CorridorRadius, 0.5f)
- P.SurfaceRoughness * VOXEL_NOISE_SCALE * 1.5f - 3.0f;
FootingDensity = SourceOnlyGenerator->GetMazeDensity(
SuggestedPoint.X, SuggestedPoint.Y, FloorProbeZ, P);
FootingDensity = EvalSource(
SuggestedPoint.X, SuggestedPoint.Y, FloorProbeZ);
break;
}
@@ -930,11 +950,14 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
FVerticalShaftParams P = Definition->VerticalShaftParams;
P.StrateTopWorldZ = StrateTopZ;
P.StrateBottomWorldZ = StrateBottomZ;
LandingDensity = SourceOnlyGenerator->GetVerticalShaftDensity(
SuggestedPoint.X, SuggestedPoint.Y, SuggestedPoint.Z, P);
VoxelDensityOps::BuildVerticalShaftStack(SourceStack, P, LiveGenerator.Seed,
LiveGenerator.OriginSpineRadius, /*StrateManager*/nullptr);
SourceStack.PrepareChunk(SourceContext);
LandingDensity = EvalSource(
SuggestedPoint.X, SuggestedPoint.Y, SuggestedPoint.Z);
const float FloorProbeZ = StrateBottomZ + P.BoundarySealThickness * 0.5f;
FootingDensity = SourceOnlyGenerator->GetVerticalShaftDensity(
SuggestedPoint.X, SuggestedPoint.Y, FloorProbeZ, P);
FootingDensity = EvalSource(
SuggestedPoint.X, SuggestedPoint.Y, FloorProbeZ);
break;
}
@@ -943,10 +966,13 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
FFloatingIslandParams P = Definition->FloatingIslandParams;
P.StrateTopWorldZ = StrateTopZ;
P.StrateBottomWorldZ = StrateBottomZ;
LandingDensity = SourceOnlyGenerator->GetFloatingIslandDensity(
SuggestedPoint.X, SuggestedPoint.Y, SuggestedPoint.Z, P);
FootingDensity = SourceOnlyGenerator->GetFloatingIslandDensity(
SuggestedPoint.X, SuggestedPoint.Y, SuggestedPoint.Z - 1.0f, P);
VoxelDensityOps::BuildFloatingIslandStack(SourceStack, P, LiveGenerator.Seed,
LiveGenerator.OriginSpineRadius, /*StrateManager*/nullptr);
SourceStack.PrepareChunk(SourceContext);
LandingDensity = EvalSource(
SuggestedPoint.X, SuggestedPoint.Y, SuggestedPoint.Z);
FootingDensity = EvalSource(
SuggestedPoint.X, SuggestedPoint.Y, SuggestedPoint.Z - 1.0f);
break;
}
@@ -230,8 +230,8 @@ namespace VoxelForgeTest
/**
* 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
* missing key input produces a wrong answer. Integer XY on purpose: that is the branch
* GetDensityAt's T1.a column cache actually takes (fractional XY bypasses the cache).
* missing key input produces a wrong answer. Integer XY on purpose: that is the lattice the
* mesher samples, and the one the per-column memos are keyed on.
*/
inline void BuildSamplePoints(const FTestWorld& World, int32 Count, int32 Seed,
TArray<FVector>& OutPoints)
@@ -5303,9 +5303,9 @@ namespace
}
// 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
// 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(
const FSlabGenerationParams& Params,
int32 Seed,
@@ -5444,7 +5444,8 @@ namespace
// 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
// 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(
const FMazeGenerationParams& Params,
int32 Seed,
@@ -5879,7 +5880,7 @@ namespace
};
// 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
// 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.
@@ -5916,7 +5917,7 @@ namespace
for (int32 IslandIndex = 0; IslandIndex < NumIslands; ++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
// can be found, but those distant blobs must not influence the returned footing.
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.
// 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
// Chaque corps ci-dessous est une transcription LITTÉRALE du bloc correspondant de
// `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.
// Convention de signe INTERNE : positif = solide ; la pile négative une fois pour le MC.
// Internal sign convention: positive = solid; the stack negates once for MC.
#include "VoxelDensityOpStack.h"
#include "VoxelStrateComposer.h"
@@ -674,7 +664,7 @@ namespace
, CorridorRadius(FMath::Max(P.CorridorRadius, 0.5f))
, BranchProbability(P.BranchProbability)
, Verticality(P.Verticality)
, Salt((uint32)Seed ^ 0x4D617A65u) // 'Maze' — identique à GetMazeDensity
, Salt((uint32)Seed ^ 0x4D617A65u) // 'Maze'
{}
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
* une course. La reconstruction seule inspecte le petit voisinage ; Eval ne refait aucune
* décision de parent ou de boucle.
@@ -829,16 +819,13 @@ namespace
//=========================================================================
// RÔLE 1 — SOURCE : DALLE / SLAB VOID (FlatPlain ET CrystalChamber)
//=========================================================================
// Transcription littérale des ÉTAPES 1-3 de `GetSlabDensity` : surface de sol, surface de
// plafond, puis `Density = -min(distAuSol, distAuPlafond)`.
// Surface de sol, surface de plafond, puis `Density = -min(distAuSol, distAuPlafond)`.
//
// DEUX archétypes, UN opérateur. `GetSlabDensity` est appelé pour FlatPlain et
// CrystalChamber sans le moindre branchement sur le type — CrystalChamber n'est rien d'autre
// que FlatPlain avec un `CeilingRoughness` plus grand. C'est le premier vrai gain du refactor
// (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.
// DEUX archétypes, UN opérateur, sans le moindre branchement sur le type — CrystalChamber
// n'est rien d'autre que FlatPlain avec un `CeilingRoughness` plus grand (OPSTACK-PLAN §4) :
// la différence entre eux est 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.
//
// 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)
//=========================================================================
// 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 = 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
//=========================================================================
// É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à
// 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.
//
// Le cache 3×3 par cellule est repris tel quel (il était déjà `thread_local` dans l'original,
// et c'est exactement ce que la note de threading de VoxelDensityOp.h autorise). Sa clé
// Le cache 3×3 par cellule est `thread_local`, ce que la note de threading de VoxelDensityOp.h
// autorise. Sa clé
// 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
// des colonnes identiques (cf. AUDIT C2 — la clé est complète, pas seulement le coord).
@@ -1657,7 +1644,7 @@ namespace
private:
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,
* 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`.
//
// 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
// que le `switch` ne pouvait pas montrer : les deux blocs y sont à 900 lignes l'un de l'autre.
// opérateur au signe près, exactement comme la source constante au-dessus.
//
// ⚠️ `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
@@ -2267,7 +2253,7 @@ namespace
{
public:
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) {}
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
* 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
* qui influent (comme l'original) : stable à travers les reconstructions de pile, ce qui
* est la leçon retenue du mémo de colonne de SurfaceWorld. */
* 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 qui influent : stable à travers les
* 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 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
{
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
// l'a laissé. C'est bien `InOut.Sdf` ici, pas une re-évaluation : re-calculer donnerait
// le SDF SANS rugosité et déplacerait les étagères.
// ⚠️ La garde est `Sdf < 0` — le SDF APRÈS rugosité, tel que la pile l'a laissé. C'est
// bien `InOut.Sdf` ici, pas une re-évaluation : re-calculer donnerait le SDF SANS
// rugosité et déplacerait les étagères.
if (InOut.Sdf >= 0.0f) { return; }
const float Phase = FMath::Frac((WorldZ - P.StrateBottomWorldZ) / P.LedgeSpacing);
@@ -2886,7 +2872,7 @@ namespace
{
public:
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; }
@@ -2962,8 +2948,8 @@ namespace
* • 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
* 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 »
* serait un TROU, et c'est pourquoi seule la borne HAUTE est testée.
* l'axe. Le confondre avec « rien en dessous » serait un TROU, et c'est pourquoi seule
* la borne HAUTE est testée.
* • 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
* opérateurs dans le pliage, chacun avec son contrat isolé.
@@ -3059,8 +3045,7 @@ namespace
private:
/** 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
* `GetFloatingIslandDensity`. */
* peuvent pas voir des îles différentes. */
bool RollIsland(int32 nx, int32 ny, FIsland& Out) const
{
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
* (`H·0.5 − max(TopHalf, UnderDepth) − Seal`), donc dans `GetFloatingIslandDensity` une
* édition à chaud qui ne change QUE l'épaisseur de seal sert des îles périmées. Même famille
* que `AUDIT §C2` : une clé de cache incomplète ne se voit pas, elle produit du terrain
* plausible. Ajouter le champ ne coûte
* qu'un recalcul, jamais une valeur différente — donc l'égalité binaire tient.
* ⚠️ LA CLÉ INCLUT `BoundarySealThickness` : `SpreadZ` s'en sert
* (`H·0.5 − max(TopHalf, UnderDepth) − Seal`), donc sans lui une édition à chaud qui ne
* change QUE l'épaisseur de seal servirait des îles périmées. Même famille que `AUDIT §C2` :
* une clé de cache incomplète ne se voit pas, elle produit du terrain plausible.
*
* The key includes BoundarySealThickness, which the original omits although SpreadZ reads it.
* Adding it can only cost a recompute, never change a value — bit-equality is unaffected.
* The key includes BoundarySealThickness because SpreadZ reads it; without it a live edit
* of the seal thickness alone would serve stale islands.
*/
const FCells& GetCells(float WorldX, float WorldY) const
{
@@ -9326,8 +9309,8 @@ namespace VoxelDensityOps
TUniquePtr<IVoxelDensityOp> MakeConstantVoidSource(float BaseDensity)
{
// `float Density = -Params.BaseDensity; // start as open air (void)` — la négation unaire
// est exacte, donc c'est littéralement la première ligne de GetFloatingIslandDensity.
// `Density = -BaseDensity` : on part du vide. La négation unaire est exacte.
// Start as open air; unary negation is exact.
return MakeUnique<FConstantFieldSource>(-BaseDensity);
}
@@ -9391,10 +9374,8 @@ namespace VoxelDensityOps
int32 Seed, float SpineRadius, const UVoxelStrateManager* StrateManager,
bool bAppendStructuralPosts)
{
// DEUX archétypes entrent ici, aucun branchement ne les distingue — parce que
// `GetSlabDensity` n'en fait aucun non plus. FlatPlain et CrystalChamber ne diffèrent que
// par leurs valeurs par défaut, et c'est maintenant visible dans le code plutôt que dans
// un commentaire. 8 archétypes → 7.
// DEUX archétypes entrent ici, aucun branchement ne les distingue : FlatPlain et
// CrystalChamber ne diffèrent que par leurs valeurs par défaut.
OutStack.Add(MakeSlabVoidSource(P, Seed));
OutStack.Add(MakeGridColumnMod(P, Seed));
@@ -9410,15 +9391,10 @@ namespace VoxelDensityOps
int32 Seed, float SpineRadius, const UVoxelStrateManager* StrateManager,
bool bAppendStructuralPosts)
{
// ⚠️ LA PREUVE QUE L'ABSTRACTION EST RÉELLE, et elle vaut d'être dite : TROIS des cinq
// opérateurs ci-dessous sont ceux de Maze, **repris sans une ligne de changement** —
// `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.
// TROIS des cinq opérateurs ci-dessous sont ceux de Maze, repris tels quels —
// `ConstantRock`, `SdfRoughness`, `SdfCarve` — avec une source différente (`§2.5`).
//
// THREE of the five ops below are Maze's, reused without a line changed. In the switch,
// Maze and VerticalShafts are two unrelated ~100-line functions; as operators they are the
// same three ops with a different source.
// THREE of the five ops below are Maze's, reused unchanged, with a different source.
constexpr float CarveBlend = 2.0f;
TUniquePtr<FShaftFieldSource> ShaftSource =
@@ -9427,9 +9403,8 @@ namespace VoxelDensityOps
OutStack.Add(MakeConstantRockSource(P.BaseDensity));
OutStack.Add(MoveTemp(ShaftSource));
// Fréquence 0.1 et fenêtre `SurfaceRoughness + 4` — les constantes de
// `GetVerticalShaftDensity`, PAS celles de Maze (0.12 / `R + rough + 2`). Même opérateur,
// réglages différents : c'est le point.
// Fréquence 0.1 et fenêtre `SurfaceRoughness + 4` — PAS celles de Maze
// (0.12 / `R + rough + 2`). Même opérateur, réglages différents.
OutStack.Add(MakeSdfRoughnessMod(P.SurfaceRoughness, 0.1f, 3, P.SurfaceRoughness + 4.0f));
OutStack.Add(MakeSdfCarve(CarveBlend, P.BaseDensity));
OutStack.Add(MakeUnique<FShaftLedgeMod>(P, ShaftPtr));
@@ -9544,8 +9519,8 @@ namespace VoxelDensityOps
OutStack.Add(MakeConstantVoidSource(P.BaseDensity));
OutStack.Add(MakeUnique<FIslandBlobSource>(P, Seed));
// Fréquence 0.08 et 4 octaves — les constantes de `GetFloatingIslandDensity`. Quatrième
// archétype à réutiliser cet opérateur (Maze 0.12/3, VerticalShafts 0.1/3).
// Fréquence 0.08 et 4 octaves — les constantes de FloatingIslands (Maze 0.12/3,
// VerticalShafts 0.1/3).
OutStack.Add(MakeSdfRoughnessMod(P.SurfaceRoughness, 0.08f, 4,
P.SurfaceRoughness + BlendK + 2.0f));
OutStack.Add(MakeSdfFill(BlendK, P.BaseDensity));
@@ -9562,15 +9537,14 @@ namespace VoxelDensityOps
int32 Seed, float SpineRadius, const UVoxelStrateManager* StrateManager,
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 RoughFrequency = 0.12f;
constexpr int32 RoughOctaves = 3;
const float R = FMath::Max(P.CorridorRadius, 0.5f);
// Fenêtre d'application de la rugosité : `MazeSDF < R + SurfaceRoughness + 2.0f` dans
// l'original. Reproduite à l'identique pour que l'égalité binaire tienne.
// Fenêtre d'application de la rugosité : `MazeSDF < R + SurfaceRoughness + 2.0f`.
const float RoughApplyWithin = R + P.SurfaceRoughness + 2.0f;
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;
}
/** L'état COMPLET (densité + SDF) après toute la pile. Diagnostic : quand une comparaison
* avec l'ancien chemin diverge, c'est le canal SDF qui dit si l'écart naît avant ou après
* la conversion. / The full state after the stack — the SDF channel is what says whether a
* divergence is born before or after the carve. */
/** L'état COMPLET (densité + SDF) après toute la pile. Diagnostic : quand deux évaluations
* divergent, c'est le canal SDF qui dit si l'écart naît avant ou après la conversion.
* / The full state after the stack — the SDF channel is what says whether a divergence is
* born before or after the carve. */
FVoxelOpSample EvalSample(float WorldX, float WorldY, float WorldZ) const
{
FVoxelOpSample S;
@@ -578,9 +578,8 @@ namespace VoxelDensityOps
* FlatPlain ET CrystalChamber — la même pile, **sans branchement sur le type** :
* SlabVoidSource → GridColumnMod → [structural post ×4]
*
* C'est le premier vrai gain du refactor (OPSTACK-PLAN §4) : deux des huit archétypes
* disparaissent dans un opérateur, et leur différence redevient ce qu'elle était déjà dans
* `GetSlabDensity` — un jeu de valeurs par défaut, pas du code.
* Deux des huit archétypes tiennent dans un opérateur (OPSTACK-PLAN §4) : leur différence est
* un jeu de valeurs par défaut, pas du code.
*/
VOXELFORGE_API void BuildSlabStack(FVoxelOpStack& OutStack, const FSlabGenerationParams& P,
int32 Seed, float SpineRadius,
@@ -592,10 +591,8 @@ namespace VoxelDensityOps
* ConstantRock → ShaftField → SdfRoughness → SdfCarve → ShaftLedge
* → ShaftConnectorAir → ShaftConnectorFloor → [structural post ×4]
*
* C'est la démonstration que `§2.5` promettait : dans le `switch`, Maze et VerticalShafts sont
* deux fonctions de ~100 lignes sans rien de commun à l'œil ; 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`).
* Trois de ces ops sont ceux de Maze (`§2.5`), 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,
int32 Seed, float SpineRadius,
+13 -73
View File
@@ -3,8 +3,8 @@
//
// Pipeline density-only: plus de grille de blocs, plus de surface heightfield.
// Le mesher appelle GetDensityAt() par voxel pour reconstruire la géométrie.
// Toute la logique de caves vit dans GetDensityWithParams / GetSlabDensity,
// pilotée par les params de strate récupérés auprès du StrateManager.
// Chaque strate est générée par sa pile d'opérateurs (VoxelDensityOpStack.h), pilotée par les
// params de strate récupérés auprès du StrateManager.
#pragma once
@@ -202,7 +202,7 @@ public:
/**
* 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.
*
* ⚠️ `ParamsFingerprint` ET `LayoutVersion` SONT OBLIGATOIRES (`AUDIT §C2`). Le cache SDF
@@ -242,56 +242,8 @@ public:
bool bCollectFusedDiagnostics = false,
// Optional same-sample hand-off from EvaluateSDFCached. This is
// only a room-shape ownership bit; it does not alter the scalar
// result and avoids a duplicate room-index query in native paths.
bool* OutRoomOwnsBottom = nullptr,
// 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;
// result and avoids a duplicate room-index query.
bool* OutRoomOwnsBottom = nullptr) const;
//=========================================================================
// 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 →
* plage. Rend une ALTITUDE monde en voxels, pas une densité.
*
* PUBLIQUE pour la même raison que `GetSlabDensity` / `GetMazeDensity` : permettre un test
* isolé. C'est la référence de `VoxelForge.OpStack.SurfaceHeightEquivalence`, qui compare la
* pile d'opérateurs de hauteur (`VoxelHeightOp.h`) à cette fonction point par point.
* Public so the height-op stack can be measured against it — same reason as GetSlabDensity.
* PUBLIQUE pour permettre un test isolé : c'est la référence de
* `VoxelForge.OpStack.SurfaceHeightEquivalence`, qui compare la pile d'opérateurs de hauteur
* (`VoxelHeightOp.h`) à cette fonction point par point.
* Public so the height-op stack can be measured against it.
*/
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
* (amplitude + direction amont). PUBLIQUES toutes deux pour la même raison que ci-dessus :
* c'est le seul chemin qui calcule l'overhang — `GetSurfaceDensity` passe `OverhangAmp = 0` —
* donc c'est la seule référence possible pour `FOverhangShelfMod`.
* Public because this is the ONLY path that computes the overhang (GetSurfaceDensity passes 0),
* so it is the only possible reference for the ported op.
* (amplitude + direction amont). C'est la colonne que `ClassifyTile` teste pour SurfaceWorld ;
* publique pour que les tests puissent placer des échantillons dans la fenêtre d'overhang.
* The surface column ClassifyTile tests for SurfaceWorld; public so tests can aim samples at
* the overhang window.
*/
void ComputeSurfaceColumn(float WorldX, float WorldY, int32 ChunkZ,
const FSurfaceGenerationParams& BaseSurface, const FBiomeContext& BiomeCtx,
@@ -329,12 +280,6 @@ public:
float& OutTerrainZ, float& OutCeilSurf,
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.
*/
@@ -467,11 +412,6 @@ private:
FSurfaceGenerationParams& OutSurface, FBiomeContext& OutBiomeCtx,
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. */
void RebuildBiomeGrid(int32 ChunkX, int32 ChunkY, int32 ChunkZ,
const FBiomeContext& Ctx, FChunkBiomeCache& Cache,