test: C9 is only half fixed — the libm half survives FPSemantics
All 9 tests green. LargeSeedSurvives proves C1 fixed by property rather than by comparison: seed 2e9 (what FMath::Rand produces) now yields 400 distinct heights over 400 samples where it previously gave a constant field. The three equivalence tests stayed green through an 85-site rewrite. The digest's NearIso warning fired at 2/115000 and its text blamed the /fp:fast vs precise split, which is fixed. First instinct was "stale warning, soften it". Checked instead, and the risk is real by a different mechanism: sinf/cosf are not specified by IEEE-754. FPSemantics = Precise makes MSVC and Clang agree on expression evaluation and says nothing about the math library; MSVC's CRT and glibc's libm may differ by ~1 ULP. FMath::Sin/Cos are used throughout the density path — layer lines, ribs, room placement, rotations. So C9's compiler half is closed by construction and its library half is not, and no build flag can close it. The measurement was also over-stating by ~100x: a single 1e-4 band is far too wide for a libm-scale delta (~1e-6 absolute at densities of magnitude ~10). Replaced with a three-band profile; only the tight band warns. UNVERIFIED: the reworded test. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+23
-3
@@ -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.
|
> (`ClangToolChain` → precise, `VCToolChain` → `/fp:fast`). That is not a theoretical divergence.
|
||||||
> Same-platform peers on the same build are fine.
|
> 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
|
> ### ✅ HALF FIXED 2026-07-27 — and the other half is a different mechanism
|
||||||
> cleared first (seven headers compile only via the shared PCH). That debt is now worth clearing on
|
>
|
||||||
> *this* justification rather than on tidiness.
|
> **`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
|
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
|
difference survived a full bisect down to code that is character-for-character identical, which
|
||||||
|
|||||||
@@ -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.
|
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`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|||||||
@@ -149,13 +149,40 @@ bool FVoxelForgeCrossPlatformTest::RunTest(const FString& Parameters)
|
|||||||
uint64 ShapeDigest = FnvOffsetBasis;
|
uint64 ShapeDigest = FnvOffsetBasis;
|
||||||
uint64 FieldDigest = 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
|
// `NearIso` — À QUELLE DISTANCE DE ZÉRO UN ÉCHANTILLON PEUT-IL CHANGER DE SIGNE ?
|
||||||
// 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é.
|
// ⚠️ CORRIGÉ 2026-07-27, ET LA CORRECTION EST LE POINT INTÉRESSANT.
|
||||||
constexpr float NearIsoBand = 1.0e-4f; // 10× la pire divergence observée : marge délibérée
|
//
|
||||||
|
// 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)
|
for (int32 Z = BottomVoxelZ; Z <= TopVoxelZ; Z += ZStep)
|
||||||
{
|
{
|
||||||
@@ -178,7 +205,13 @@ bool FVoxelForgeCrossPlatformTest::RunTest(const FString& Parameters)
|
|||||||
// CHAMP : tous les bits.
|
// CHAMP : tous les bits.
|
||||||
FnvAccumU32(FieldDigest, 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;
|
++NumSamples;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -195,27 +228,32 @@ bool FVoxelForgeCrossPlatformTest::RunTest(const FString& Parameters)
|
|||||||
World.Settings->Seed, NumSamples, XYStep, ZStep,
|
World.Settings->Seed, NumSamples, XYStep, ZStep,
|
||||||
ShapeDigest, FieldDigest, NumSolid, NumSamples - NumSolid, NumNaN));
|
ShapeDigest, FieldDigest, NumSolid, NumSamples - NumSolid, NumNaN));
|
||||||
|
|
||||||
// LE chiffre qui borne le risque, plutôt que de le supposer.
|
// Le PROFIL de proximité à l'isosurface, plutôt qu'un seul seuil binaire.
|
||||||
if (NumNearIso == 0)
|
|
||||||
{
|
|
||||||
AddInfo(FString::Printf(
|
AddInfo(FString::Printf(
|
||||||
TEXT("NearIso: 0 of %d samples sit within %.1e of the isosurface. No sample is close ")
|
TEXT("NearIso profile over %d samples: %d within 1e-4, %d within 1e-5, %d within 1e-6. ")
|
||||||
TEXT("enough for a float-model difference (~1e-5 worst observed) to flip its SIGN, so ")
|
TEXT("Only the LAST number is the cross-platform risk: FPSemantics = Precise removed the ")
|
||||||
TEXT("the SHAPE digest is robust to the /fp:fast-vs-precise split by a 10x margin at ")
|
TEXT("float-MODEL difference, so what remains is that sinf/cosf are not IEEE-754 specified ")
|
||||||
TEXT("these sample points. That is evidence, not proof -- it covers this grid, not ")
|
TEXT("and MSVC's CRT may differ from glibc's libm by ~1 ULP. On densities of magnitude ~10 ")
|
||||||
TEXT("every voxel in a world."),
|
TEXT("that is ~1e-6 absolute, which is why the wide band over-states the risk ~100x."),
|
||||||
NumSamples, NearIsoBand));
|
NumSamples, NumNearWide, NumNearMid, NumNearTight));
|
||||||
|
|
||||||
|
if (NumNearTight > 0)
|
||||||
|
{
|
||||||
|
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
|
else
|
||||||
{
|
{
|
||||||
AddWarning(FString::Printf(
|
AddInfo(TEXT("No sample sits within 1e-6 of the isosurface, so no sampled voxel is close ")
|
||||||
TEXT("NearIso: %d of %d samples sit within %.1e of the isosurface -- close enough that ")
|
TEXT("enough for a libm difference to flip its side. Evidence, not proof: it covers ")
|
||||||
TEXT("a float-model difference could flip their SIGN, which is a solid-vs-air ")
|
TEXT("this grid, not every voxel of a world."));
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
TestEqual(TEXT("no sample produced NaN"), NumNaN, 0);
|
TestEqual(TEXT("no sample produced NaN"), NumNaN, 0);
|
||||||
|
|||||||
Reference in New Issue
Block a user