test: the Maze residue is /fp:fast, not port drift — encode the real bar
The bisect settled it. The difference survives every stage removal down to
"corridors + carve ONLY", which is character-for-character transcribed code, so
it is not in anything the decomposition added.
Cause, read out of the engine rather than assumed (VCToolChain.cs):
case FPSemanticsMode.Default: // Default is imprecise FP semantics.
case FPSemanticsMode.Imprecise: Arguments.Add("/fp:fast"); break;
with UBT's own doc: "the compiler is allowed to transform math expressions in
ways that might result in differently rounded results". Identical source in two
translation units may reassociate differently, worth ~1 ULP. It shows up on
exactly the ~2% of samples inside the SDF blend shell, where Blend - Sdf
catastrophically cancels; outside it Carve is exactly 0 or 1 and both agree.
So MazeEquivalence now grades what it can actually assert:
- hard fail : any isosurface crossing (geometry moves)
- info : differences at ULP scale (the unavoidable floor)
- warn : anything larger, which IS port drift, and runs the bisect
A test that warns on every port would get ignored by the port that matters.
Recorded in OPSTACK-PLAN 2.6, and as AUDIT C9 for the part that outlives this
refactor: ARCHITECTURE 9.1's "every peer regenerates identically" holds only
between bit-identical binaries under /fp:fast. Fine for one build on one
platform; a real desync source for a Linux server plus Windows clients both
regenerating authoritative geometry. The FPSemantics::Precise knob exists but
must not be turned speculatively -- it blocks the vectorisation T2.a was chasing,
on the hot loop, for an unmeasured cost.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -247,6 +247,59 @@ reachable only with a very large origin room *and* short tunnels. Flagging it be
|
||||
|
||||
---
|
||||
|
||||
### C9 — The build uses `/fp:fast`, and the MP plan assumes bit-reproducible terrain ⚠️ **added 2026-07-27, measured not assumed**
|
||||
|
||||
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
|
||||
pointed at the toolchain rather than the port. It is:
|
||||
|
||||
```csharp
|
||||
// UnrealBuildTool/Platform/Windows/VCToolChain.cs
|
||||
case FPSemanticsMode.Default: // Default is imprecise FP semantics.
|
||||
case FPSemanticsMode.Imprecise: Arguments.Add("/fp:fast"); break;
|
||||
```
|
||||
|
||||
and UBT's own doc for that mode: *"FP math isn't IEEE-754 compliant: the compiler is allowed to
|
||||
transform math expressions in ways that might result in differently rounded results from what
|
||||
IEEE-754 requires."* The plugin sets no `FPSemantics` override, so it gets the default.
|
||||
|
||||
**Two consequences, one benign and one not:**
|
||||
|
||||
**Benign — refactors cannot be bit-identical.** The same expression compiled into two translation
|
||||
units may reassociate differently, worth ~1 ULP. So "the port reproduces the original exactly" is
|
||||
not an achievable bar for the op-stack work, and `OPSTACK-PLAN §2.6`'s bar (recognisably the same
|
||||
*place*, judged on a screenshot) was the right call for reasons beyond the ones it gave. The
|
||||
acceptance criterion is now encoded in `VoxelForge.OpStack.MazeEquivalence`: **hard-fail on any
|
||||
isosurface crossing, tolerate ULP-scale deltas, warn on anything larger.**
|
||||
|
||||
**Not benign — `ARCHITECTURE §9.1`'s multiplayer model rests on this.** The plan is "replicate the
|
||||
seed + layout + diff, never the geometry; every peer regenerates identically." That guarantee is
|
||||
only as strong as the floating-point reproducibility of the density path, and under `/fp:fast` it
|
||||
holds **only between bit-identical binaries**. Same build, same platform: fine — `DensityPurity`
|
||||
proves the field is pure across threads and query order. But a different compiler version, a
|
||||
different optimisation level (Debug vs Shipping), or a different platform's toolchain may produce a
|
||||
subtly different world from the same seed.
|
||||
|
||||
The docs already reject GPU density partly because *"cross-GPU float determinism is fatal for
|
||||
replicate-the-seed"*. The same argument applies to the CPU across build variants, and had not been
|
||||
stated.
|
||||
|
||||
**How much it matters depends on a design question that is Jahni's, not the audit's:**
|
||||
|
||||
- **If listen-server only, one binary per platform, and clients never regenerate authoritative
|
||||
geometry** — this is a non-issue. Ship as is.
|
||||
- **If a dedicated server on Linux and Windows clients both regenerate terrain and compare** — this
|
||||
is a real desync source, and it will present as rare, unreproducible, geometry-only divergence:
|
||||
approximately the worst bug class to diagnose.
|
||||
|
||||
**The knob, if it turns out to matter:** `ModuleRules.FPSemantics = FPSemanticsMode.Precise` in
|
||||
`VoxelForge.Build.cs` restores IEEE semantics for this module only. **Do not do this speculatively** —
|
||||
the density path is the plugin's hot loop, `/fp:precise` blocks exactly the vectorisation and
|
||||
contraction that T2.a's SIMD noise work was chasing, and the cost is unmeasured. It is a decision to
|
||||
take with a profile in hand and a confirmed cross-platform requirement, not a tidy-up.
|
||||
|
||||
---
|
||||
|
||||
### Threading — what's *right*, for the record
|
||||
|
||||
Worth stating plainly, because it's the part that's easy to get wrong and this doesn't:
|
||||
|
||||
Reference in New Issue
Block a user