COMPOSER-NOTES §6.1, titled THE BLOCKER, gated everything after it. A source writing only Sdf did not touch density, so it answered the box query ON BEHALF OF the converter that followed it -- fine when a human wrote and verified the pair, unsafe the moment the composer assembles a combination nobody checked. And the failure is the silent one: a tile wrongly proved uniform means no geometry, no collision, no error, and a player falls through the floor. FVoxelBoxHypotheses now carries FVoxelBoxSdfInterval. SDF writers publish their own [Min, Max]; replacement writers replace it, accumulating writers MinWith it; converters and detail ops consume the prior interval through a state-aware EffectOverBox. Each op answers only for itself and the fold composes them. Unknown, invalid, non-finite or unimplemented bounds force Mixed -- when in doubt the answer is Mixed, because a missed skip costs milliseconds and a false uniform costs a player. Bounds are SUPREMA of what Eval can produce, never the param named "max": FBM 1.5 (not 1.0), max(Min,Max) radius envelopes, warp sqrt(2), SmoothMin slack. [FLT_MAX, FLT_MAX] is the exact no-surface identity. ⭐ THE PROOF IS THE NOVEL STACK. Built a combination no shipping builder produces: ConstantVoid -> LatticeCorridorSource -> SdfRoughness -> SdfFill brute-forced 332,750 voxels across 256 boxes: 250 proved AllAir, 0 VIOLATIONS. If isolation is real an unverified stack must still be sound, and it is. That test -- not the refactor -- is the deliverable. Every archetype re-proved by brute force, 0 violations everywhere: Maze 112/256 · CrystalChamber 38/60 · Underwater 33/40 · FlatPlain 32/60 VerticalShafts 28/60 · FloatingIslands 15/60 · TunnelNetwork prod 9/40 TunnelNetwork dense 0/40 (correctly -- it really is all mixed) COST, reported not hidden: isolation gives up some skipping. TunnelNetwork production 12->9, VerticalShafts 32->28. Answering for yourself alone is strictly weaker than answering for a verified pair. The classifier stays materially useful and the trade buys safe composition, which is the point. Eval is untouched; equivalence tests bit-identical; no per-voxel box query. SurfaceWorld keeps its separate exact height/lattice classifier. Also: ⛔ WITHDREW the "corrective ops must be fused" claim from §3.2. FFloorBiasMod does NOT undo FCaveRoughnessMod -- it adds an independent NormZ^2 x FloorBias term to the ACCUMULATED density and never reads TotalRough. Not a correction, no fuse, no "corrects" dependency. The claim came from reading a name and a comment; one grep would have shown the ten Density-writing ops in between before the paragraph was written. Build clean, 22 succeeded / 0 failed, verified in VoxelM.log. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VJiTcTSDNkKBtnNdiBtdaU
427 lines
23 KiB
C++
427 lines
23 KiB
C++
// 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.
|
|
//
|
|
// 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 sept 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
|
|
// ─────────────────────────────────────────────────────────────────────────────────────────
|
|
// La pile reproduit `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
|
|
|
|
#include "Misc/AutomationTest.h"
|
|
#include "Async/ParallelFor.h"
|
|
#include "HAL/PlatformMisc.h"
|
|
|
|
#include "VoxelForgeTestFixture.h"
|
|
#include "VoxelDensityOpStack.h"
|
|
|
|
#include <atomic>
|
|
|
|
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
|
FVoxelForgeOpStackMazeTest,
|
|
"VoxelForge.OpStack.MazeEquivalence",
|
|
EAutomationTestFlags_ApplicationContextMask | EAutomationTestFlags::EngineFilter)
|
|
|
|
namespace
|
|
{
|
|
constexpr int32 NumMazeSamples = 20000;
|
|
constexpr int32 NumMazeBoxTests = 256;
|
|
|
|
/** Les params Maze de la strate Maze de la fixture, bornes Z de runtime comprises. */
|
|
bool ResolveMazeParams(const VoxelForgeTest::FTestWorld& World, FMazeGenerationParams& Out,
|
|
int32& OutTopVoxelZ, int32& OutBottomVoxelZ)
|
|
{
|
|
using namespace VoxelForgeTest;
|
|
if (!World.GetSlotVoxelZRange(FTestWorld::SlotMaze, OutTopVoxelZ, OutBottomVoxelZ)) { return false; }
|
|
const int32 MidChunkZ = ((OutTopVoxelZ + OutBottomVoxelZ) / 2) / CHUNK_SIZE;
|
|
Out = World.StrateManager->GetMazeParamsForChunk(FIntVector(0, 0, MidChunkZ));
|
|
return true;
|
|
}
|
|
}
|
|
|
|
bool FVoxelForgeOpStackMazeTest::RunTest(const FString& Parameters)
|
|
{
|
|
using namespace VoxelForgeTest;
|
|
|
|
FTestWorld World;
|
|
World.Build();
|
|
if (!World.IsValid())
|
|
{
|
|
AddError(World.WhyInvalid());
|
|
return false;
|
|
}
|
|
|
|
FMazeGenerationParams MazeParams;
|
|
int32 TopVoxelZ = 0, BottomVoxelZ = 0;
|
|
if (!ResolveMazeParams(World, MazeParams, TopVoxelZ, BottomVoxelZ))
|
|
{
|
|
AddError(TEXT("The fixture layout has no Maze slot. Check FTestWorld::Build's Archetypes[] ")
|
|
TEXT("against FTestWorld::SlotMaze."));
|
|
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.
|
|
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."),
|
|
MazeParams.StrateTopWorldZ, MazeParams.StrateBottomWorldZ));
|
|
return false;
|
|
}
|
|
|
|
const UVoxelGenerator* Gen = World.Generator.Get();
|
|
|
|
FVoxelOpStack Stack;
|
|
VoxelDensityOps::BuildMazeStack(Stack, MazeParams, World.Settings->Seed,
|
|
Gen->OriginSpineRadius, World.StrateManager.Get());
|
|
|
|
// La décomposition doit être une DÉCOMPOSITION. Un `FMazeOp` monolithique passerait tous les
|
|
// tests numériques ci-dessous et aurait pourtant raté l'objet entier du refactor (§2.5).
|
|
TestEqual(TEXT("the Maze stack is decomposed, not wrapped (rock + corridors + roughness + carve + 4 structural)"),
|
|
Stack.Num(), 8);
|
|
|
|
FVoxelOpContext Ctx;
|
|
Ctx.Seed = (uint32)World.Settings->Seed;
|
|
Ctx.LayoutVersion = World.StrateManager->GetLayoutVersion();
|
|
Ctx.StrateTopWorldZ = MazeParams.StrateTopWorldZ;
|
|
Ctx.StrateBottomWorldZ = MazeParams.StrateBottomWorldZ;
|
|
Stack.PrepareChunk(Ctx);
|
|
|
|
TArray<FVector> Points;
|
|
Points.Reserve(NumMazeSamples);
|
|
{
|
|
FRandomStream Rng(31337);
|
|
for (int32 i = 0; i < NumMazeSamples; ++i)
|
|
{
|
|
Points.Add(FVector(
|
|
(float)Rng.RandRange(-3 * CHUNK_SIZE, 3 * CHUNK_SIZE),
|
|
(float)Rng.RandRange(-3 * CHUNK_SIZE, 3 * CHUNK_SIZE),
|
|
(float)Rng.RandRange(BottomVoxelZ, TopVoxelZ)));
|
|
}
|
|
}
|
|
|
|
//=========================================================================
|
|
// É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.
|
|
//=========================================================================
|
|
// Le cache par cellule de la source de couloirs est `thread_local` : c'est exactement le genre
|
|
// d'endroit où une clé incomplète produit une couture (cf. AUDIT C2).
|
|
{
|
|
std::atomic<int32> Impure{ 0 };
|
|
const int32 NumBlocks = FMath::Max(4, FMath::Min(16, FPlatformMisc::NumberOfCores()));
|
|
|
|
TArray<float> Ref;
|
|
Ref.SetNumUninitialized(NumMazeSamples);
|
|
for (int32 i = 0; i < NumMazeSamples; ++i)
|
|
{
|
|
Ref[i] = Stack.EvalMC((float)Points[i].X, (float)Points[i].Y, (float)Points[i].Z);
|
|
}
|
|
|
|
ParallelFor(NumBlocks, [&](int32 Block)
|
|
{
|
|
TArray<int32> LocalOrder;
|
|
BuildShuffledOrder(NumMazeSamples, 500 + Block, LocalOrder);
|
|
for (const int32 i : LocalOrder)
|
|
{
|
|
const float V = Stack.EvalMC((float)Points[i].X, (float)Points[i].Y, (float)Points[i].Z);
|
|
if (!BitEqual(V, Ref[i])) { Impure.fetch_add(1, std::memory_order_relaxed); }
|
|
}
|
|
});
|
|
|
|
TestEqual(TEXT("the op stack is window-invariant across query order and worker threads"),
|
|
Impure.load(), 0);
|
|
}
|
|
|
|
//=========================================================================
|
|
// LE VERDICT DE BOÎTE — le vrai prix perf : Maze n'a JAMAIS su sauter une tuile.
|
|
//=========================================================================
|
|
// 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 du saut de tuile que Maze n'a jamais eu.
|
|
{
|
|
int32 NumProved = 0, NumMixed = 0, NumUnsound = 0, NumBruteSamples = 0;
|
|
FRandomStream Rng(24680);
|
|
// Hors de la boucle : la ligne de rapport en a besoin. Une étendue d'échantillonnage qu'on
|
|
// ne peut pas citer dans le rapport est une étendue que personne ne surveille.
|
|
const int32 SpanCells = 40;
|
|
const int32 SpanVoxels = SpanCells * 8; // Extent = Step * Cells = 1 * 8
|
|
|
|
for (int32 t = 0; t < NumMazeBoxTests; ++t)
|
|
{
|
|
const int32 Step = 1, Cells = 8; // petites tuiles : force brute tenable
|
|
const int32 Extent = Step * Cells;
|
|
const FIntVector Origin(
|
|
Rng.RandRange(-SpanCells, SpanCells) * Extent,
|
|
Rng.RandRange(-SpanCells, SpanCells) * Extent,
|
|
FMath::Clamp(Rng.RandRange(BottomVoxelZ / Extent, TopVoxelZ / Extent), -4096, 4096) * Extent);
|
|
|
|
const int32 GridDim = Cells + 1; // le MÊME treillis que le mesher, marge ±1 comprise
|
|
const FBox Box(
|
|
FVector(Origin.X - Step, Origin.Y - Step, Origin.Z - Step),
|
|
FVector(Origin.X + GridDim * Step, Origin.Y + GridDim * Step, Origin.Z + GridDim * Step));
|
|
|
|
const EVoxelTileClass Verdict = Stack.ClassifyBox(Box, Ctx);
|
|
if (Verdict == EVoxelTileClass::Mixed) { ++NumMixed; continue; }
|
|
++NumProved;
|
|
|
|
const bool bClaimsSolid = (Verdict == EVoxelTileClass::AllSolid);
|
|
for (int32 gz = -1; gz <= GridDim; ++gz)
|
|
for (int32 gy = -1; gy <= GridDim; ++gy)
|
|
for (int32 gx = -1; gx <= GridDim; ++gx)
|
|
{
|
|
const float X = (float)(Origin.X + gx * Step);
|
|
const float Y = (float)(Origin.Y + gy * Step);
|
|
const float Z = (float)(Origin.Z + gz * Step);
|
|
const float D = Stack.EvalMC(X, Y, Z);
|
|
++NumBruteSamples;
|
|
if (bClaimsSolid ? (D >= 0.0f) : (D < 0.0f))
|
|
{
|
|
if (NumUnsound == 0)
|
|
{
|
|
AddError(FString::Printf(
|
|
TEXT("HOLE: the op stack claimed %s for the box at (%d,%d,%d) but ")
|
|
TEXT("EvalMC(%.0f, %.0f, %.0f) = %.6g is on the %s side. One of the ops' ")
|
|
TEXT("EffectOverBox/ClassifyBox is not conservative. Suspects, in order: ")
|
|
TEXT("the source interval, the SDF roughness interval, the converter's ")
|
|
TEXT("threshold fold, then the seal's forcing verdict."),
|
|
bClaimsSolid ? TEXT("AllSolid") : TEXT("AllAir"),
|
|
Origin.X, Origin.Y, Origin.Z, X, Y, Z, D,
|
|
(D >= 0.0f) ? TEXT("AIR") : TEXT("SOLID")));
|
|
}
|
|
++NumUnsound;
|
|
gz = gy = gx = GridDim + 1; // ce verdict est déjà mort, tuile suivante
|
|
}
|
|
}
|
|
}
|
|
|
|
TestEqual(TEXT("every box verdict the stack emits survives brute force (a false verdict is a hole)"),
|
|
NumUnsound, 0);
|
|
|
|
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."),
|
|
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."));
|
|
}
|
|
}
|
|
|
|
//==========================================================================
|
|
// ISOLATION — a source with a converter it was never authored with
|
|
//==========================================================================
|
|
// Maze's hand-written stack uses ConstantRock -> Lattice -> SdfRoughness -> SdfCarve.
|
|
// This stack deliberately uses a VOID root and SdfFill instead. The only way it may
|
|
// claim AllAir is for the propagated lattice/roughness interval to prove that the fill
|
|
// converter is inactive over the whole box. A source that still answered for its old
|
|
// carve neighbour would not be a valid implementation of this test.
|
|
{
|
|
FMazeGenerationParams NovelParams = MazeParams;
|
|
NovelParams.BranchProbability = 0.12f;
|
|
NovelParams.Verticality = 0.08f;
|
|
NovelParams.CorridorRadius = 1.0f;
|
|
|
|
FVoxelOpStack NovelStack;
|
|
NovelStack.Add(VoxelDensityOps::MakeConstantVoidSource(NovelParams.BaseDensity));
|
|
NovelStack.Add(VoxelDensityOps::MakeLatticeCorridorSource(NovelParams, World.Settings->Seed));
|
|
NovelStack.Add(VoxelDensityOps::MakeSdfRoughnessMod(
|
|
0.35f, 0.12f, 3, NovelParams.CorridorRadius + 2.0f));
|
|
NovelStack.Add(VoxelDensityOps::MakeSdfFill(1.25f, NovelParams.BaseDensity));
|
|
NovelStack.PrepareChunk(Ctx);
|
|
|
|
FString ChannelError;
|
|
TestTrue(TEXT("the unverified source/converter stack has a valid channel order"),
|
|
NovelStack.ValidateChannelOrder(&ChannelError));
|
|
if (!ChannelError.IsEmpty()) { AddError(ChannelError); }
|
|
|
|
constexpr int32 NumNovelBoxTests = 256;
|
|
const int32 NovelSpanCells = 160;
|
|
const int32 NovelSpanVoxels = NovelSpanCells * 8;
|
|
int32 NumNovelSolid = 0, NumNovelAir = 0, NumNovelMixed = 0;
|
|
int32 NumNovelVoxels = 0, NumNovelViolations = 0;
|
|
FRandomStream NovelRng(86420);
|
|
|
|
for (int32 t = 0; t < NumNovelBoxTests; ++t)
|
|
{
|
|
const int32 Step = 1, Cells = 8;
|
|
const int32 Extent = Step * Cells;
|
|
const FIntVector Origin(
|
|
NovelRng.RandRange(-NovelSpanCells, NovelSpanCells) * Extent,
|
|
NovelRng.RandRange(-NovelSpanCells, NovelSpanCells) * Extent,
|
|
FMath::Clamp(NovelRng.RandRange(BottomVoxelZ / Extent, TopVoxelZ / Extent), -4096, 4096) * Extent);
|
|
const int32 GridDim = Cells + 1;
|
|
const FBox Box(
|
|
FVector(Origin.X - Step, Origin.Y - Step, Origin.Z - Step),
|
|
FVector(Origin.X + GridDim * Step, Origin.Y + GridDim * Step, Origin.Z + GridDim * Step));
|
|
|
|
const EVoxelTileClass Verdict = NovelStack.ClassifyBox(Box, Ctx);
|
|
if (Verdict == EVoxelTileClass::Mixed)
|
|
{
|
|
++NumNovelMixed;
|
|
continue;
|
|
}
|
|
|
|
const bool bClaimsSolid = (Verdict == EVoxelTileClass::AllSolid);
|
|
if (bClaimsSolid) { ++NumNovelSolid; } else { ++NumNovelAir; }
|
|
|
|
// No sampled subset: every lattice point in every proved box is checked.
|
|
for (int32 gz = -1; gz <= GridDim; ++gz)
|
|
for (int32 gy = -1; gy <= GridDim; ++gy)
|
|
for (int32 gx = -1; gx <= GridDim; ++gx)
|
|
{
|
|
const float X = (float)(Origin.X + gx * Step);
|
|
const float Y = (float)(Origin.Y + gy * Step);
|
|
const float Z = (float)(Origin.Z + gz * Step);
|
|
const float D = NovelStack.EvalMC(X, Y, Z);
|
|
++NumNovelVoxels;
|
|
if (bClaimsSolid ? (D >= 0.0f) : (D < 0.0f)) { ++NumNovelViolations; }
|
|
}
|
|
}
|
|
|
|
TestEqual(TEXT("the unverified source/converter stack has zero box-verdict violations"),
|
|
NumNovelViolations, 0);
|
|
AddInfo(FString::Printf(
|
|
TEXT("Novel lattice+roughness+FILL stack: %d boxes proved (%d AllSolid, %d AllAir), " )
|
|
TEXT("%d Mixed; %d voxels checked, %d violations; XY sampled from +/- %d voxels."),
|
|
NumNovelSolid + NumNovelAir, NumNovelSolid, NumNovelAir, NumNovelMixed,
|
|
NumNovelVoxels, NumNovelViolations, NovelSpanVoxels));
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
#endif // WITH_DEV_AUTOMATION_TESTS
|