diff --git a/AUDIT-2026-07.md b/AUDIT-2026-07.md index 9670d39..4de5aa2 100644 --- a/AUDIT-2026-07.md +++ b/AUDIT-2026-07.md @@ -302,9 +302,29 @@ reachable only with a very large origin room *and* short tunnels. Flagging it be > (`ClangToolChain` → precise, `VCToolChain` → `/fp:fast`). That is not a theoretical divergence. > Same-platform peers on the same build are fine. > -> **The fix is known and blocked:** `FPSemantics = Precise` on this module, which needs the IWYU debt -> cleared first (seven headers compile only via the shared PCH). That debt is now worth clearing on -> *this* justification rather than on tidiness. +> ### ✅ HALF FIXED 2026-07-27 — and the other half is a different mechanism +> +> **`FPSemantics = Precise` is set** (IWYU debt cleared to allow it). MSVC now gets `/fp:precise`, +> Clang gets `-ffp-contract=off`; both are IEEE-754 with no contraction, so **the compiler half is +> closed by construction.** It also dissolved `§C10` entirely. +> +> **⚠️ THE LIBRARY HALF IS STILL OPEN, and I nearly missed it by assuming.** After setting Precise I +> reasoned the cross-platform risk was gone. It is not: +> +> **`sinf`/`cosf` are not specified by IEEE-754.** MSVC's CRT and glibc's libm may legitimately +> return different results (typically ≤ 1 ULP). And `FMath::Sin`/`FMath::Cos` are used throughout the +> density path — layer lines (`VoxelGenerator.cpp:2275`, `VoxelHeightOpStack.cpp:232`), ribs (`:1357`), +> room placement (`:1520`), rotations (`:362`, `:408`, `:1663`). So a Windows host and a Linux client +> can still disagree, just by ~1 ULP instead of by a whole reassociation. +> +> **Measured, not assumed:** `CrossPlatformDigest` now reports a *profile* — samples within 1e-4 / +> 1e-5 / 1e-6 of the isosurface. Only the tight band matters (a libm delta is ~1e-6 absolute on +> densities of magnitude ~10), and the first run showed **2 / 115 000 within 1e-4** — i.e. the wide +> band, which over-states the risk by ~100×. The tight-band number is the one to watch. +> +> **If it must be zero:** a deterministic in-house `sin`/`cos` in the density path (a polynomial with +> defined rounding), which costs one more world re-tune. **Not another build flag** — no compiler +> setting can make two different libm implementations agree. Found while chasing a 1-ULP difference between `GetMazeDensity` and its operator-stack port. The difference survived a full bisect down to code that is character-for-character identical, which diff --git a/OPSTACK-PROGRESS.md b/OPSTACK-PROGRESS.md index 42349e5..ce14c63 100644 --- a/OPSTACK-PROGRESS.md +++ b/OPSTACK-PROGRESS.md @@ -1479,3 +1479,54 @@ The three equivalence tests should stay green (both paths changed together); the op stack: the `Mask` combiner (biome blending) to finish SurfaceWorld, or `VerticalShafts` next. --- + +## 2026-07-27 — all 9 tests green. C1 PROVEN fixed. And C9 is only HALF fixed. + +Full `VoxelForge` filter, exported run `Saved/Automation/Automation2026.07.27-16.35.18.csv`: + +``` +ClassifyTileSoundness · CrossPlatformDigest · DensityPurity · DiffLayerContention +LargeSeedSurvives · LiveEditInvalidation · BoxVerdictFold · MazeEquivalence +SlabEquivalence · SurfaceHeightEquivalence — all Success +``` + +**`§C1` is proven fixed, by property rather than by comparison:** + +``` +Seed 1337 : 400 distinct heights / 400 samples, range 20.95 voxels +Seed 100 000 : 399 / 400, range 23.46 +Seed 10 000 000 : 399 / 400, range 22.89 +Seed 2 000 000 000 : 400 / 400, range 23.42 +``` + +Seed 2e9 — what `FMath::Rand()` produces — now generates a live world. Before the fix that seed +gave a *constant* field. Note the three equivalence tests stayed green through an 85-site rewrite, +which is exactly the port-correctness value they were kept for. + +### ⚠️ The digest warning was right for the WRONG reason — and I nearly dismissed it + +`NearIso: 2 of 115000 samples within 1e-4 of the isosurface` fired a warning whose text blamed the +`/fp:fast`-vs-precise split. That split is fixed, so my first instinct was "stale warning, soften the +text". **I checked instead of assuming, and the risk is real via a different mechanism.** + +**`sinf`/`cosf` are not specified by IEEE-754.** `FPSemantics = Precise` makes MSVC and Clang agree on +*expression evaluation*; it says nothing about the math library. MSVC's CRT and glibc's libm may +legitimately differ by ~1 ULP — and `FMath::Sin`/`Cos` are all over the density path: layer lines +(`VoxelGenerator.cpp:2275`, `VoxelHeightOpStack.cpp:232`), ribs, room placement, rotations. + +**So `§C9` is half closed:** the compiler half by construction, the **library half still open**. No +build flag can fix the second — two libm implementations cannot be made to agree by a compiler +setting. The fix, if it is ever needed, is a deterministic in-house `sin`/`cos` in the density path +(one more world re-tune). + +**The measurement was also over-stating things ~100×.** A single 1e-4 band is far too wide for a +libm-scale delta (~1e-6 absolute on densities of magnitude ~10). Replaced with a three-band profile +(1e-4 / 1e-5 / 1e-6); only the tight band raises a warning, because only it corresponds to a delta +that could actually flip a sign. Recorded in `AUDIT §C9`. + +**UNVERIFIED:** the reworded digest test. + +**Next single action:** build (quick — one test file changed). Then, per Jahni: finish SurfaceWorld +(the `Mask` combiner = biome blending, `§5`'s Phase 3 prototype), then `VerticalShafts` (`§6`). + +--- diff --git a/Source/VoxelForge/Private/Tests/VoxelForgeCrossPlatformTest.cpp b/Source/VoxelForge/Private/Tests/VoxelForgeCrossPlatformTest.cpp index f67f532..cff985b 100644 --- a/Source/VoxelForge/Private/Tests/VoxelForgeCrossPlatformTest.cpp +++ b/Source/VoxelForge/Private/Tests/VoxelForgeCrossPlatformTest.cpp @@ -149,13 +149,40 @@ bool FVoxelForgeCrossPlatformTest::RunTest(const FString& Parameters) uint64 ShapeDigest = FnvOffsetBasis; uint64 FieldDigest = FnvOffsetBasis; - int32 NumSamples = 0, NumSolid = 0, NumNaN = 0, NumNearIso = 0; + int32 NumSamples = 0, NumSolid = 0, NumNaN = 0; + int32 NumNearWide = 0, NumNearMid = 0, NumNearTight = 0; - // `NearIso` : à quelle distance de zéro un échantillon doit-il être pour qu'une différence - // d'ULP puisse faire basculer son SIGNE ? Les écarts mesurés entre le `switch` et la pile - // valent ~1e-5 au pire (AUDIT §C10) ; un écart entre modèles flottants est du même ordre. - // Tout échantillon plus loin que ça de l'isosurface ne peut PAS changer de côté. - constexpr float NearIsoBand = 1.0e-4f; // 10× la pire divergence observée : marge délibérée + // ───────────────────────────────────────────────────────────────────────── + // `NearIso` — À QUELLE DISTANCE DE ZÉRO UN ÉCHANTILLON PEUT-IL CHANGER DE SIGNE ? + // ───────────────────────────────────────────────────────────────────────── + // ⚠️ CORRIGÉ 2026-07-27, ET LA CORRECTION EST LE POINT INTÉRESSANT. + // + // Version d'origine : une seule bande à 1e-4, justifiée par « une différence de MODÈLE + // FLOTTANT ». Depuis `FPSemantics = Precise` (§C9), il n'y a plus de différence de modèle + // flottant : MSVC et Clang compilent tous deux en IEEE-754 sans contraction. J'ai d'abord cru + // que ça rendait cette mesure caduque. **C'est faux, et il a fallu vérifier plutôt que + // supposer.** + // + // Il reste `FMath::Sin` / `FMath::Cos`, présents partout dans le chemin de densité (lignes de + // strates, nervures, placement des salles, rotations). **`sinf`/`cosf` ne sont PAS spécifiés + // par IEEE-754** : le CRT de MSVC et la libm de la glibc ont parfaitement le droit de rendre + // des résultats différents (typiquement ≤ 1 ULP, mais différents). `FPSemantics` a donc fermé + // la moitié COMPILATEUR de §C9 et laissé ouverte la moitié BIBLIOTHÈQUE. + // + // FPSemantics = Precise removed the float-MODEL difference, but sinf/cosf are not IEEE-754 + // specified, so MSVC's CRT and glibc's libm may still differ. The compiler half of C9 is closed; + // the library half is not. + // + // D'où trois bandes au lieu d'une : une bande unique à 1e-4 est **100× trop large** pour un + // écart de libm (~1e-6 en absolu sur des densités de magnitude ~10), donc elle sur-estime + // grossièrement le risque et crie au loup. Mesurer trois échelles donne un vrai profil, et + // seule la plus serrée — celle qui correspond réellement à un écart de libm — déclenche + // l'alerte. + // Three bands, not one: 1e-4 over-estimates a libm-scale delta by ~100x. Only the tight band, + // which actually matches a libm difference, raises a warning. + constexpr float NearIsoWide = 1.0e-4f; // profil : large, informatif + constexpr float NearIsoMid = 1.0e-5f; // profil + constexpr float NearIsoTight = 1.0e-6f; // ≈ l'échelle d'un écart libm ⇒ LE chiffre du risque for (int32 Z = BottomVoxelZ; Z <= TopVoxelZ; Z += ZStep) { @@ -178,7 +205,13 @@ bool FVoxelForgeCrossPlatformTest::RunTest(const FString& Parameters) // CHAMP : tous les bits. FnvAccumU32(FieldDigest, Bits); - if (!bWasNaN && FMath::Abs(D) < NearIsoBand) { ++NumNearIso; } + if (!bWasNaN) + { + const float A = FMath::Abs(D); + if (A < NearIsoWide) { ++NumNearWide; } + if (A < NearIsoMid) { ++NumNearMid; } + if (A < NearIsoTight) { ++NumNearTight; } + } ++NumSamples; } } @@ -195,27 +228,32 @@ bool FVoxelForgeCrossPlatformTest::RunTest(const FString& Parameters) World.Settings->Seed, NumSamples, XYStep, ZStep, ShapeDigest, FieldDigest, NumSolid, NumSamples - NumSolid, NumNaN)); - // LE chiffre qui borne le risque, plutôt que de le supposer. - if (NumNearIso == 0) + // Le PROFIL de proximité à l'isosurface, plutôt qu'un seul seuil binaire. + AddInfo(FString::Printf( + TEXT("NearIso profile over %d samples: %d within 1e-4, %d within 1e-5, %d within 1e-6. ") + TEXT("Only the LAST number is the cross-platform risk: FPSemantics = Precise removed the ") + TEXT("float-MODEL difference, so what remains is that sinf/cosf are not IEEE-754 specified ") + TEXT("and MSVC's CRT may differ from glibc's libm by ~1 ULP. On densities of magnitude ~10 ") + TEXT("that is ~1e-6 absolute, which is why the wide band over-states the risk ~100x."), + NumSamples, NumNearWide, NumNearMid, NumNearTight)); + + if (NumNearTight > 0) { - AddInfo(FString::Printf( - TEXT("NearIso: 0 of %d samples sit within %.1e of the isosurface. No sample is close ") - TEXT("enough for a float-model difference (~1e-5 worst observed) to flip its SIGN, so ") - TEXT("the SHAPE digest is robust to the /fp:fast-vs-precise split by a 10x margin at ") - TEXT("these sample points. That is evidence, not proof -- it covers this grid, not ") - TEXT("every voxel in a world."), - NumSamples, NearIsoBand)); + AddWarning(FString::Printf( + TEXT("%d of %d samples sit within 1e-6 of the isosurface -- tight enough that a libm ") + TEXT("difference between MSVC and glibc could flip their SIGN, i.e. one voxel solid for ") + TEXT("a Windows host and air for a Linux client. FMath::Sin/Cos are used throughout the ") + TEXT("density path (layer lines, ribs, room placement, rotations), so this is the ") + TEXT("REMAINING half of AUDIT C9 -- the compiler half is fixed, the library half is not. ") + TEXT("If this must be zero, the fix is a deterministic in-house sin/cos in the density ") + TEXT("path (one more world re-tune), not another build flag."), + NumNearTight, NumSamples)); } else { - AddWarning(FString::Printf( - TEXT("NearIso: %d of %d samples sit within %.1e of the isosurface -- close enough that ") - TEXT("a float-model difference could flip their SIGN, which is a solid-vs-air ") - TEXT("disagreement between a Windows host and a Linux client. This is the concrete ") - TEXT("mechanism behind AUDIT C9, and the count is roughly how many voxels per %d are at ") - TEXT("risk. It does not mean they DO differ -- run this on both platforms and compare ") - TEXT("the SHAPE digest to find out."), - NumNearIso, NumSamples, NearIsoBand, NumSamples)); + AddInfo(TEXT("No sample sits within 1e-6 of the isosurface, so no sampled voxel is close ") + TEXT("enough for a libm difference to flip its side. Evidence, not proof: it covers ") + TEXT("this grid, not every voxel of a world.")); } TestEqual(TEXT("no sample produced NaN"), NumNaN, 0);