fix(determinism): own sin/cos on the world path -- same world on every CPU, 3.5% faster

On Win64, FMath::Sin/Cos resolve to CRT sinf/cosf (GenericPlatformMath.h:521/530; no platform
override), and the UCRT picks FMA3 or non-FMA3 implementations at startup by CPU. /fp:precise
closed only the compiler half, so the same build and seed could build a slightly different world
on two Windows PCs, and on Linux.

- VoxelMath::DetSin/DetCos/DetSinCos (VoxelTypes.h): fixed double-precision period reduction to
  a quarter period, then fixed float minimax polynomials; no CRT, FMath or CPU-dependent
  intrinsic. Measured max error vs sinf 5.96e-8 over the used range (|arg| <= 6.19).
- Every world-deciding FMath::Sin/Cos replaced (generator, op stacks, cave morphology, strate
  manager, content placement, strate measure); none remain in runtime code.
- Pinned test VoxelForge.Determinism.DetSinCos (golden bit patterns + error bound).
- Diagnostic start-up switch -voxel.CrtFma3=0 (Windows) forces the CRT's non-FMA3 path.
- Parse-once console switches now initialise in StartupModule on the game thread; workers only
  read them (was an unsynchronised first parse inside GetDensityAt).
- Corrected the CRC comment that claimed a CRC32 cannot give a false positive.

Proof (Codex round; orchestrator re-checked):
- Before the fix, FMA3 on vs off was identical on this Ryzen 7 5800X3D: the hole exists but was
  not observed in this world. After the fix, the canonical exports are byte-identical with
  FMA3 on and off.
- The world changed once, as expected. New canonical export SHA-256 CBB8AE18...906F (geometry
  B9EF84AC; was B3E5F4C3...377); worms region 9242C093 (was 311CA42C), walk OBJ 20CCF7BD....
  Triangle counts unchanged in every tile. Order-independent nearest-vertex distance, worms
  region: max 8.7 micrometres (the reported "11.4 m" was a line-order artefact of a line-by-line
  OBJ diff); export region max 3e-5 m.
- Capability exact: player-fit 20,830, walk-reachable 10,909, connected. TilePostReachProof,
  sealed-proof, cross-platform and strate-crossing tests pass.
- Performance, headless game path, S/M/S/M vs the committed-tip DLL: static worker 27.41 s vs
  28.44 s (-3.6%), moving 59.59 s vs 61.74 s (-3.5%); LOD0 p50/p95 not worse.

From Sol's code review (DESIGN-SOL-2026-09-15-CODE.md, section 4.1), branch sol/deterministic-math.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TaFdXhTdYMkW1xrV6ApT6Y
This commit is contained in:
Fr0zkaandClaude Opus 5 committed 2026-09-16 00:55:04 +02:00
1 parent cda082fa10
commit 10fd0b6e77
16 files changed
+393 -85

No files matched your search

@@ -1827,8 +1827,10 @@ void UVoxelStrateManager::GeneratePassages()
// surface shaft, while this passage opens a separate room in the strate network.
const float Angle = PassageRandom01(PassageSaltAngle) * (2.0f * PI);
const float Distance = PassageRandomRange(PlacementLo, PlacementHi, PassageSaltDistance);
const float PX = FMath::Cos(Angle) * Distance;
const float PY = FMath::Sin(Angle) * Distance;
float SinAngle = 0.0f, CosAngle = 0.0f;
VoxelMath::DetSinCos(SinAngle, CosAngle, Angle);
const float PX = CosAngle * Distance;
const float PY = SinAngle * Distance;
++TotalPassages;
@@ -1990,7 +1992,7 @@ void UVoxelStrateManager::GeneratePassages()
Passage.ControlRadii.Reserve(Segments + 1);
// WIDTH profile: mouth radius at the ends, mid radius in the centre (taper/bulge).
auto RadiusAt = [&](float t) { return FMath::Lerp(Cfg.MouthRadius, Cfg.MidRadius, FMath::Sin(t * PI)); };
auto RadiusAt = [&](float t) { return FMath::Lerp(Cfg.MouthRadius, Cfg.MidRadius, VoxelMath::DetSin(t * PI)); };
// Per-passage shape seeds.
const float WormFreq = PassageRandomRange(0.8f, 1.8f, PassageSaltWormFreq); // (vertical wobble only)
@@ -2005,7 +2007,7 @@ void UVoxelStrateManager::GeneratePassages()
{
const float T = (float)s / (float)Segments;
float Z = FMath::Lerp(TopZ, BottomZ, T);
const float Env = FMath::Sin(T * PI); // 0 at both ends → mouths stay anchored
const float Env = VoxelMath::DetSin(T * PI); // 0 at both ends → mouths stay anchored
float OX = 0.0f, OY = 0.0f;
switch (Cfg.Style)
@@ -2018,8 +2020,10 @@ void UVoxelStrateManager::GeneratePassages()
case EVoxelPassageStyle::Spiral:
{
const float Ang = PhaseA + T * Cfg.SpiralTurns * 2.0f * PI;
OX = FMath::Cos(Ang) * Cfg.SpiralRadius * Env;
OY = FMath::Sin(Ang) * Cfg.SpiralRadius * Env;
float SinAng = 0.0f, CosAng = 0.0f;
VoxelMath::DetSinCos(SinAng, CosAng, Ang);
OX = CosAng * Cfg.SpiralRadius * Env;
OY = SinAng * Cfg.SpiralRadius * Env;
break;
}
@@ -2029,8 +2033,10 @@ void UVoxelStrateManager::GeneratePassages()
const int32 Steps = FMath::Clamp(Cfg.CascadeSteps, 1, 16);
const int32 Idx = FMath::Min((int32)(T * Steps), Steps - 1);
const float SA = PhaseA + (float)Idx * 2.39996f; // golden-angle spread
OX = FMath::Cos(SA) * Cfg.CascadeLedge * Env;
OY = FMath::Sin(SA) * Cfg.CascadeLedge * Env;
float SinSA = 0.0f, CosSA = 0.0f;
VoxelMath::DetSinCos(SinSA, CosSA, SA);
OX = CosSA * Cfg.CascadeLedge * Env;
OY = SinSA * Cfg.CascadeLedge * Env;
break;
}
@@ -2042,7 +2048,7 @@ void UVoxelStrateManager::GeneratePassages()
// 2D organic wander — it curls and meanders "here and there" rather than
// oscillating along one line (zig-zag) or orbiting the axis (spiral).
// Flat-top envelope keeps full motion along the length but anchors the mouths.
const float WormEnv = FMath::Clamp(FMath::Sin(T * PI) * 3.0f, 0.0f, 1.0f);
const float WormEnv = FMath::Clamp(VoxelMath::DetSin(T * PI) * 3.0f, 0.0f, 1.0f);
OX = PassageFBM(T * BendFreq, NSeedX) * VOXEL_NOISE_SCALE * Cfg.Wander * WormEnv;
OY = PassageFBM(T * BendFreq, NSeedY + 53.0f) * VOXEL_NOISE_SCALE * Cfg.Wander * WormEnv;
break;
@@ -2084,7 +2090,9 @@ void UVoxelStrateManager::GeneratePassages()
FVector Direction(Delta.X, Delta.Y, 0.0f);
if (!Direction.Normalize())
{
Direction = FVector(FMath::Cos(Angle), FMath::Sin(Angle), 0.0f);
float SinAngle = 0.0f, CosAngle = 0.0f;
VoxelMath::DetSinCos(SinAngle, CosAngle, Angle);
Direction = FVector(CosAngle, SinAngle, 0.0f);
if (!Direction.Normalize())
{
Direction = FVector(1.0f, 0.0f, 0.0f);
@@ -2138,8 +2146,9 @@ void UVoxelStrateManager::GeneratePassages()
// This is deliberately a construction rule, not a seed-dependent observation.
const float SwitchbackAngle =
PassageRandom01(PassageSaltUpperDoor) * 2.0f * PI;
FVector2D SwitchbackDirection(
FMath::Cos(SwitchbackAngle), FMath::Sin(SwitchbackAngle));
float SinSwitchback = 0.0f, CosSwitchback = 0.0f;
VoxelMath::DetSinCos(SinSwitchback, CosSwitchback, SwitchbackAngle);
FVector2D SwitchbackDirection(CosSwitchback, SinSwitchback);
if (!SwitchbackDirection.Normalize())
{
SwitchbackDirection = FVector2D(1.0f, 0.0f);