From 23605d5350a1a10777ffdb2f2f8b2c18a103a54d Mon Sep 17 00:00:00 2001 From: Fr0zka Date: Mon, 27 Jul 2026 14:07:14 +0200 Subject: [PATCH] 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 --- OPSTACK-PROGRESS.md | 44 ++++++++++ .../Tests/VoxelForgeOpStackMazeTest.cpp | 80 +++++++++++++++++++ 2 files changed, 124 insertions(+) diff --git a/OPSTACK-PROGRESS.md b/OPSTACK-PROGRESS.md index 2ea374c..b294d37 100644 --- a/OPSTACK-PROGRESS.md +++ b/OPSTACK-PROGRESS.md @@ -308,3 +308,47 @@ samples. Then wire the stack into `GetDensityAt` behind a per-strate opt-in — was deliberately held back until the build went green. It now has. --- + +## 2026-07-27 (afternoon) — ALL SIX TESTS GREEN. One hypothesis killed. + +Run at 12:02 today (in `Saved/Logs/VoxelM.log`; the CSV export was taken later). + +| Test | Result | +|---|---| +| `ClassifyTileSoundness` | ✅ | +| `DensityPurity` | ✅ | +| `DiffLayerContention` | ✅ **now passes** — the assertion fix was right; 33.3M read rounds | +| `LiveEditInvalidation` | ✅ 64/64 probes moved | +| `BoxVerdictFold` | ✅ | +| `MazeEquivalence` | ✅ (same warning) | + +**Phase 0.5's gate is met.** All three original tests plus the two op-stack ones are green on the +current code, so the op stack is no longer being built on unverified ground. + +### The `FVector` rounding hypothesis was WRONG — and the way it was wrong is informative + +The previous entry predicted the fix would take `MazeEquivalence` from 454 differing samples to 0. +The re-run returned **exactly 454 samples, exactly `1.90734863e-06`, at exactly `(-23, 55, -660)`** — +bit-for-bit the same result. The `float → double → float` detour is a no-op, which is what +`/fp:precise` semantics say it should be. Hypothesis eliminated cleanly; the detour is harmless and +stays (it costs nothing and documents the original's shape), but it is **not** the cause. + +**Stopped guessing, added a bisect** to `MazeEquivalence`: it re-runs the comparison four times, +disabling roughness → seal → spine → passages, on **both sides**, and reports which stage's removal +makes it bit-exact. One run now answers a question two guesses failed to. + +**Standing hypothesis, to be confirmed or killed by that bisect:** the residue is compiler +float-contraction across translation units (`/fp:fast` lets the same expression reassociate +differently in `VoxelGenerator.cpp` and `VoxelDensityOpStack.cpp`), 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 that is +confirmed, **bit-identity is not achievable in principle for these ports**, and the standard for +every later archetype becomes "zero isosurface crossings", not "zero differing floats". That is a +conclusion worth having explicitly rather than re-deriving per port. + +**UNVERIFIED:** the bisect itself. + +**Next single action:** re-run `MazeEquivalence` and read the bisect table. Then Phase 1 step 3 — +wire the stack into `GetDensityAt` behind a per-strate opt-in. + +--- diff --git a/Source/VoxelForge/Private/Tests/VoxelForgeOpStackMazeTest.cpp b/Source/VoxelForge/Private/Tests/VoxelForgeOpStackMazeTest.cpp index 708cf34..4eeef6d 100644 --- a/Source/VoxelForge/Private/Tests/VoxelForgeOpStackMazeTest.cpp +++ b/Source/VoxelForge/Private/Tests/VoxelForgeOpStackMazeTest.cpp @@ -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