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:
2026-07-27 14:15:43 +02:00
parent 23605d5350
commit af5f2103b3
4 changed files with 184 additions and 8 deletions
+53
View File
@@ -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: