diff --git a/OPSTACK-PROGRESS.md b/OPSTACK-PROGRESS.md index f58ad6d..393f78e 100644 --- a/OPSTACK-PROGRESS.md +++ b/OPSTACK-PROGRESS.md @@ -537,3 +537,55 @@ diagnostic-only and comes out once the answer is in. the THREE-WAY block. Phase 1 step 3 still paused. --- + +## 2026-07-27 — THREE-WAY VERDICT: the fault is MINE, in the operator stack. + +``` +A generator TU vs B opstack TU : 126 differ +A generator TU vs C test TU : 0 differ <-- identical source, different TU, SAME result +B opstack TU vs C test TU : 126 differ +``` + +**A == C settles it: the source is stable across translation units.** So the compiler was never the +cause, and the operator stack differs for a **logic** reason. Fourth hypothesis dead — but this one +points at code I own, which is the first time the answer has been actionable. + +**Correction to walk back in the docs** (not yet done — do it once the cause is known, so it is +corrected with the right explanation rather than twice): +- `OPSTACK-PLAN §2.6`'s green note claims bit-identity is unachievable because of `/fp:fast`. + **False.** `A == C` proves identical source reproduces exactly across TUs here. +- `AUDIT-2026-07.md §C9`'s *first* consequence ("refactors cannot be bit-identical") is likewise + false and must go. **C9's second half stands** — UBT's FP default genuinely differs by toolchain, + read straight out of `VCToolChain.cs` / `ClangToolChain.cs`, and the MP model does assume + bit-reproducible terrain. That half was never inferred from this test. +- The test's own INFO text ("this is the expected floor... /fp:fast") is wrong for the same reason + and gets rewritten with the real cause. + +**Also learned, and worth keeping:** setting `FPSemantics` on VoxelForge costs the module the +engine's shared PCH and exposes ~30 missing includes across seven files. Recorded in `Build.cs`. + +### Where the fault is NOT + +Read line by line against the verbatim copy, all identical: the ctor's `FMath::Max` clamps, the +cell `FloorToInt`, `NodeCenter`, `EdgeOpen`'s hashes and salts, the `{-1,0}³` sweep and its add +order, the capsule loop, the `FMath::Min` fold, the carve's clamp/smoothstep/subtract, and the four +structural-post no-ops. Three readings said "identical" and the measurement disagrees, so **reading +is not going to find it** — hence more instrument, less staring. + +### The instrument now in place + +`MazeCoreVerbatim` optionally returns its **SDF** and edge count, and the three-way compares the SDF +channels directly instead of inferring from densities: + +- **SDF identical, density differs** ⇒ fault is in `FSdfCarveOp`. +- **SDF differs** ⇒ fault is in `FLatticeCorridorSource` (edge set or capsule fold). + +It also reports the split across all 126 mismatches, and dumps the first one with raw hex plus the +verbatim edge count — so if the edge SETS differ (a cache-key bug) that shows up as a count mismatch +immediately. + +**UNVERIFIED:** the instrumentation. + +**Next single action:** rebuild, read `FIRST B-vs-C MISMATCH`. It names the file to open. + +--- diff --git a/Source/VoxelForge/Private/Tests/VoxelForgeOpStackMazeTest.cpp b/Source/VoxelForge/Private/Tests/VoxelForgeOpStackMazeTest.cpp index 924024b..fd73610 100644 --- a/Source/VoxelForge/Private/Tests/VoxelForgeOpStackMazeTest.cpp +++ b/Source/VoxelForge/Private/Tests/VoxelForgeOpStackMazeTest.cpp @@ -68,7 +68,8 @@ namespace * passages omis), parce que c'est là que le bisect a montré l'écart survivre. */ float MazeCoreVerbatim(float WorldX, float WorldY, float WorldZ, - const FMazeGenerationParams& Params, int32 Seed) + const FMazeGenerationParams& Params, int32 Seed, + float* OutSdf = nullptr, int32* OutNumEdges = nullptr) { const float CS = FMath::Max(Params.CellSize, 1.0f); const FVector Pos(WorldX, WorldY, WorldZ); @@ -116,6 +117,9 @@ namespace MazeSDF = FMath::Min(MazeSDF, VoxelSDF::Capsule(Pos, E.A, E.B, R)); } + if (OutSdf) { *OutSdf = MazeSDF; } + if (OutNumEdges) { *OutNumEdges = Edges.Num(); } + // Rugosité omise volontairement (variante du bisect). const float Blend = 2.0f; if (MazeSDF < Blend) @@ -336,15 +340,50 @@ bool FVoxelForgeOpStackMazeTest::RunTest(const FString& Parameters) const int32 N = FMath::Min(NumMazeSamples, 5000); int32 DiffAB = 0, DiffAC = 0, DiffBC = 0; + int32 SdfDiffers = 0, SdfSame_DensityDiffers = 0, FirstBad = -1; for (int32 i = 0; i < N; ++i) { const float X = (float)Points[i].X, Y = (float)Points[i].Y, Z = (float)Points[i].Z; + + float VerbSdf = 0.0f; int32 VerbEdges = 0; const float A = MutableGen->GetMazeDensity(X, Y, Z, Core); - const float B = CoreStack.EvalMC(X, Y, Z); - const float C = MazeCoreVerbatim(X, Y, Z, Core, World.Settings->Seed); + const FVoxelOpSample BS = CoreStack.EvalSample(X, Y, Z); + const float B = -BS.Density; + const float C = MazeCoreVerbatim(X, Y, Z, Core, World.Settings->Seed, &VerbSdf, &VerbEdges); + if (!BitEqual(A, B)) { ++DiffAB; } if (!BitEqual(A, C)) { ++DiffAC; } - if (!BitEqual(B, C)) { ++DiffBC; } + if (!BitEqual(B, C)) + { + ++DiffBC; + if (FirstBad < 0) { FirstBad = i; } + // LA question, posée directement au lieu d'être déduite d'une densité : + // les deux SDF sont-ils identiques ? Si oui, la faute est dans le carve. + if (BitEqual(BS.Sdf, VerbSdf)) { ++SdfSame_DensityDiffers; } else { ++SdfDiffers; } + } + } + + if (FirstBad >= 0) + { + const float X = (float)Points[FirstBad].X, Y = (float)Points[FirstBad].Y, Z = (float)Points[FirstBad].Z; + float VerbSdf = 0.0f; int32 VerbEdges = 0; + const float C = MazeCoreVerbatim(X, Y, Z, Core, World.Settings->Seed, &VerbSdf, &VerbEdges); + const FVoxelOpSample BS = CoreStack.EvalSample(X, Y, Z); + const float BMC = -BS.Density; + auto Bits = [](float V) { return *reinterpret_cast(&V); }; + AddInfo(FString::Printf( + TEXT("FIRST B-vs-C MISMATCH at (%.0f, %.0f, %.0f):\n") + TEXT(" SDF stack %.9g [0x%08X] verbatim %.9g [0x%08X] %s\n") + TEXT(" MC stack %.9g [0x%08X] verbatim %.9g [0x%08X]\n") + TEXT(" verbatim edge count %d - CellSize %.9g - CorridorRadius %.9g - BaseDensity %.9g\n") + TEXT(" across all mismatches: SDF differs %d, SDF identical but density differs %d"), + X, Y, Z, + BS.Sdf, Bits(BS.Sdf), VerbSdf, Bits(VerbSdf), + BitEqual(BS.Sdf, VerbSdf) ? TEXT("<- SDF IDENTICAL, fault is in the CARVE") + : TEXT("<- SDF DIFFERS, fault is in the lattice/capsule"), + BMC, Bits(BMC), C, Bits(C), + VerbEdges, Core.CellSize, Core.CorridorRadius, Core.BaseDensity, + SdfDiffers, SdfSame_DensityDiffers)); } MutableGen->OriginSpineRadius = SavedSpine;