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:
@@ -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.
|
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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|||||||
@@ -172,6 +172,86 @@ bool FVoxelForgeOpStackMazeTest::RunTest(const FString& Parameters)
|
|||||||
WorstIdx >= 0 ? Points[WorstIdx].Y : 0.0f,
|
WorstIdx >= 0 ? Points[WorstIdx].Y : 0.0f,
|
||||||
WorstIdx >= 0 ? Points[WorstIdx].Z : 0.0f,
|
WorstIdx >= 0 ? Points[WorstIdx].Z : 0.0f,
|
||||||
NumSolidDisagreements));
|
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
|
// Un désaccord de côté d'iso EST une différence de géométrie. C'est la seule chose ici qui
|
||||||
|
|||||||
Reference in New Issue
Block a user