Files
VoxelForge/Source/VoxelForge/Private/Tests/VoxelForgeOpStackMazeTest.cpp
T
Fr0zkaandClaude Opus 5 8e346a3054 feat(opstack): EffectOverBox correct in ISOLATION -- the blocker is closed
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
2026-09-04 06:20:40 +02:00

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