test: bisect the residual Maze difference instead of guessing at it again

All six tests are green as of the 12:02 run, so Phase 0.5's gate is met.

MazeEquivalence still reports 454 differing samples, the same max delta, at the
same coordinate as before -- byte for byte the previous result. So the FVector
float->double->float hypothesis from the last commit is dead: that detour is a
no-op, exactly as /fp:precise says it should be. It stays (harmless, and it
documents the original's shape) but it explains nothing.

Rather than propose a third guess, MazeEquivalence now bisects: it re-runs the
comparison with roughness, then seal, then spine, then passages disabled ON BOTH
SIDES, and reports which stage's removal makes it bit-exact. One run answers what
two hypotheses failed to.

Standing hypothesis for the bisect to confirm or kill: compiler float
contraction across translation units under /fp:fast, worth ~1 ULP. It fits the
~2% hit rate -- only voxels inside the narrow SDF blend shell have an unsaturated
carve factor; everywhere else Carve is exactly 0 or exactly 1 and both paths
agree bit for bit. If confirmed, bit-identity is not achievable in principle for
these ports and the bar for every later archetype is "zero isosurface
crossings", which is what OPSTACK-PLAN 2.6 asked for anyway.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-27 14:07:14 +02:00
parent 826a8c99dc
commit 23605d5350
2 changed files with 124 additions and 0 deletions
@@ -172,6 +172,86 @@ bool FVoxelForgeOpStackMazeTest::RunTest(const FString& Parameters)
WorstIdx >= 0 ? Points[WorstIdx].Y : 0.0f,
WorstIdx >= 0 ? Points[WorstIdx].Z : 0.0f,
NumSolidDisagreements));
//=====================================================================
// LE BISECT — quelle ÉTAPE introduit l'écart ?
//=====================================================================
// Deviner a déjà échoué une fois : l'hypothèse « aller-retour float→double par FVector »
// prédisait 0 écart et le run suivant a rendu EXACTEMENT les mêmes 454 échantillons, le
// même delta, la même coordonnée. Donc on arrête de deviner et on MESURE.
//
// On rejoue la comparaison en désactivant les étages un par un, DES DEUX CÔTÉS pour que la
// comparaison reste honnête. La première variante bit-exacte désigne l'étage fautif :
// celui qui vient d'être retiré.
//
// Guessing already failed once — the FVector hypothesis predicted 0 and the next run
// returned the exact same 454 samples, delta and coordinate. So: measure. Each variant
// disables one more stage ON BOTH SIDES; the first bit-exact variant names the culprit.
{
struct FVariant
{
const TCHAR* Name;
bool bNoRoughness, bNoSeal, bNoSpine, bNoPassages;
};
static const FVariant Variants[] = {
{ TEXT("roughness off"), true, false, false, false },
{ TEXT("roughness + seal off"), true, true, false, false },
{ TEXT("roughness + seal + spine off"), true, true, true, false },
{ TEXT("corridors + carve ONLY"), true, true, true, true },
};
// Ces deux-là vivent sur le GÉNÉRATEUR, pas dans les params, donc pour les faire varier
// des deux côtés il faut les muter puis les restaurer.
UVoxelGenerator* MutableGen = World.Generator.Get();
const float SavedSpineRadius = MutableGen->OriginSpineRadius;
const UVoxelStrateManager* SavedManager = MutableGen->StrateManager;
const int32 BisectSamples = FMath::Min(NumMazeSamples, 5000);
FString Report;
for (const FVariant& V : Variants)
{
FMazeGenerationParams P = MazeParams;
if (V.bNoRoughness) { P.SurfaceRoughness = 0.0f; }
if (V.bNoSeal) { P.BoundarySealThickness = 0.0f; }
const float SpineR = V.bNoSpine ? 0.0f : SavedSpineRadius;
const UVoxelStrateManager* Mgr = V.bNoPassages ? nullptr : SavedManager;
MutableGen->OriginSpineRadius = SpineR;
MutableGen->SetStrateManager(Mgr);
FVoxelOpStack VarStack;
VoxelDensityOps::BuildMazeStack(VarStack, P, World.Settings->Seed, SpineR, Mgr);
int32 VarDiff = 0;
float VarWorst = 0.0f;
for (int32 i = 0; i < BisectSamples; ++i)
{
const float X = (float)Points[i].X, Y = (float)Points[i].Y, Z = (float)Points[i].Z;
const float A = MutableGen->GetMazeDensity(X, Y, Z, P);
const float B = VarStack.EvalMC(X, Y, Z);
if (!BitEqual(A, B)) { ++VarDiff; VarWorst = FMath::Max(VarWorst, FMath::Abs(A - B)); }
}
Report += FString::Printf(TEXT("\n %-34s -> %5d / %d differ (max |delta| %.9g)"),
V.Name, VarDiff, BisectSamples, VarWorst);
}
MutableGen->OriginSpineRadius = SavedSpineRadius;
MutableGen->SetStrateManager(SavedManager);
AddInfo(FString::Printf(
TEXT("BISECT of the residual difference (each row disables one MORE stage, on both ")
TEXT("sides; the first row reading 0 names the stage removed just before it):%s")
TEXT("\n If even \"corridors + carve ONLY\" differs, the residue is in the lattice/")
TEXT("capsule/carve core -- and since that code is a literal transcription, the cause ")
TEXT("is the COMPILER, not the port: same expressions in two translation units are ")
TEXT("free to contract/reassociate differently under /fp:fast, which is worth about ")
TEXT("1 ULP. That would also explain why only ~2%% of samples differ: only voxels ")
TEXT("inside the narrow SDF blend shell have an unsaturated carve factor. Everywhere ")
TEXT("else Carve is exactly 0 or exactly 1 and both paths agree bit for bit."),
*Report));
}
}
// Un désaccord de côté d'iso EST une différence de géométrie. C'est la seule chose ici qui