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

+35 -19
View File
@@ -857,9 +857,18 @@ void VoxelGenLOD::SetThreadSampleStep(int32 Value)
SampleStep = Value;
}
void VoxelGenLOD::InitializeConsoleSwitches()
{
VF_ParseFastIsFiniteSwitch();
VF_ParseFusedEvaluatorSwitch();
VF_ParseWormBlockSkipSwitch();
VF_ParseTileCacheWindowSwitch();
VF_ParseSpatialIndexSwitch();
VF_ParseChunkRoutingCacheSwitch();
}
bool VoxelGenLOD::IsTileCacheWindowEnabled(bool bFusedPath)
{
VF_ParseTileCacheWindowSwitch();
if (GVoxelForgeTileCacheWindow == 0)
{
return false;
@@ -900,7 +909,6 @@ void VoxelGenLOD::SetThreadTileContext(
bool VoxelGenLOD::ShouldUseSpatialIndex(bool bFusedPath)
{
VF_ParseSpatialIndexSwitch();
if (GVoxelForgeSpatialIndex >= 0)
{
return GVoxelForgeSpatialIndex != 0;
@@ -1126,7 +1134,9 @@ static void ApplyDisturbances(float& MC, float X, float Y, float Z,
const float zc = FMath::Lerp(InnerBot + 8.0f, InnerTop - 8.0f,
VoxelHash::ToFloat01(VoxelHash::Mix(h ^ 0xB1u)));
const float ang = VoxelHash::ToFloat01(VoxelHash::Mix(h ^ 0xB2u)) * PI;
const float dxu = FMath::Cos(ang), dyu = FMath::Sin(ang);
float sinAng = 0.0f, cosAng = 0.0f;
VoxelMath::DetSinCos(sinAng, cosAng, ang);
const float dxu = cosAng, dyu = sinAng;
const float bx = (nx + 0.5f) * Sp, by = (ny + 0.5f) * Sp;
const float half = Sp * 0.6f;
Bridges.Add({ FVector(bx - dxu * half, by - dyu * half, zc),
@@ -1175,7 +1185,9 @@ static void ApplyDisturbances(float& MC, float X, float Y, float Z,
const uint32 h = VoxelHash::Cell(nx, ny, Seed ^ 0x52470001u);
if (VoxelHash::ToFloat01(h) > D.RidgeDensity) continue;
const float ang = VoxelHash::ToFloat01(VoxelHash::Mix(h ^ 0x9001u)) * PI;
const float dxu = FMath::Cos(ang), dyu = FMath::Sin(ang);
float sinAng = 0.0f, cosAng = 0.0f;
VoxelMath::DetSinCos(sinAng, cosAng, ang);
const float dxu = cosAng, dyu = sinAng;
const float bx = (nx + 0.5f) * Sp, by = (ny + 0.5f) * Sp;
const float half = Sp * 0.45f;
Ridges.Add({ bx - dxu * half, by - dyu * half, bx + dxu * half, by + dyu * half });
@@ -1374,7 +1386,9 @@ static bool VF_AnyBridgeCanTouchLattice(
InnerBot + 8.0f, InnerTop - 8.0f,
VoxelHash::ToFloat01(VoxelHash::Mix(H ^ 0xB1u)));
const float Angle = VoxelHash::ToFloat01(VoxelHash::Mix(H ^ 0xB2u)) * PI;
const float DXU = FMath::Cos(Angle), DYU = FMath::Sin(Angle);
float SinAngle = 0.0f, CosAngle = 0.0f;
VoxelMath::DetSinCos(SinAngle, CosAngle, Angle);
const float DXU = CosAngle, DYU = SinAngle;
const float BX = (NX + 0.5f) * Spacing, BY = (NY + 0.5f) * Spacing;
const float Half = Spacing * 0.6f;
const float HorizontalSq = VF_DistanceSquaredToXYSegment(
@@ -1453,7 +1467,9 @@ static bool VF_AnyRidgeCanTouchLattice(
continue;
}
const float Angle = VoxelHash::ToFloat01(VoxelHash::Mix(H ^ 0x9001u)) * PI;
const float DXU = FMath::Cos(Angle), DYU = FMath::Sin(Angle);
float SinAngle = 0.0f, CosAngle = 0.0f;
VoxelMath::DetSinCos(SinAngle, CosAngle, Angle);
const float DXU = CosAngle, DYU = SinAngle;
const float BX = (NX + 0.5f) * Spacing, BY = (NY + 0.5f) * Spacing;
const float Half = Spacing * 0.45f;
if (VF_DistanceSquaredToXYSegment(
@@ -1554,7 +1570,9 @@ static bool VF_AnyBridgeCanTouchBox(
InnerBot + 8.0f, InnerTop - 8.0f,
VoxelHash::ToFloat01(VoxelHash::Mix(H ^ 0xB1u)));
const float Angle = VoxelHash::ToFloat01(VoxelHash::Mix(H ^ 0xB2u)) * PI;
const float DX = FMath::Cos(Angle), DY = FMath::Sin(Angle);
float SinAngle = 0.0f, CosAngle = 0.0f;
VoxelMath::DetSinCos(SinAngle, CosAngle, Angle);
const float DX = CosAngle, DY = SinAngle;
const float BX = (NX + 0.5f) * Spacing, BY = (NY + 0.5f) * Spacing;
const float Half = Spacing * 0.6f;
const FVector A(BX - DX * Half, BY - DY * Half, ZC);
@@ -1609,7 +1627,9 @@ static bool VF_AnyRidgeCanTouchBox(
continue;
}
const float Angle = VoxelHash::ToFloat01(VoxelHash::Mix(H ^ 0x9001u)) * PI;
const float DX = FMath::Cos(Angle), DY = FMath::Sin(Angle);
float SinAngle = 0.0f, CosAngle = 0.0f;
VoxelMath::DetSinCos(SinAngle, CosAngle, Angle);
const float DX = CosAngle, DY = SinAngle;
const float BX = (NX + 0.5f) * Spacing, BY = (NY + 0.5f) * Spacing;
const float Half = Spacing * 0.45f;
const FVector A(BX - DX * Half, BY - DY * Half, InnerBot);
@@ -2776,7 +2796,6 @@ UVoxelGenerator::UVoxelGenerator()
void UVoxelGenerator::InitializeSettings(const UVoxelSettings* Settings)
{
VF_ParseFastIsFiniteSwitch();
// Les paramètres globaux sont copiés ici une seule fois : le chemin voxel ne doit pas
// déréférencer l'asset de settings.
Seed = Settings ? Settings->GetEffectiveWorldSeed() : 0;
@@ -2817,10 +2836,6 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
VoxelGenLOD::bTilePostReachBypass = PreviousBypass;
}
TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_GetDensityAt);
VF_ParseFastIsFiniteSwitch();
VF_ParseFusedEvaluatorSwitch();
VF_ParseWormBlockSkipSwitch();
VF_ParseChunkRoutingCacheSwitch();
VoxelDensityProfile::FScopedTimer DensityProfileTimer(
VoxelDensityProfile::EBucket::GetDensityAt);
VoxelDensityProfile::FScopedTimer DensityPrologueTimer(
@@ -3164,9 +3179,10 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
CP_Tunnel = StrateManager->GetGenerationParams(ChunkCoord);
// AUDIT §C2 — l'empreinte est calculée ICI, une fois par chunk, au seul endroit où
// les params changent. `FStrateGenerationParams` est du POD pur (aucun TArray /
// FString / pointeur), donc une CRC mémoire ne peut pas donner de FAUX POSITIF ; au
// pire un octet de padding donne un faux MANQUE, c'est-à-dire une reconstruction de
// cache. On se trompe du côté du CPU, jamais du côté d'une salle fausse.
// FString / pointeur), donc une CRC mémoire peut donner un FAUX POSITIF par
// collision CRC32 ; au pire un octet de padding donne aussi un faux MANQUE, c'est-
// à-dire une reconstruction de cache. Le risque pratique est une collision, jamais
// une salle fausse créée par la seule différence de padding.
CP_TunnelFP = FCrc::MemCrc32(&CP_Tunnel, sizeof(CP_Tunnel));
break;
}
@@ -4705,7 +4721,7 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
{
// Sine wave along Z: peaks at each line position
float LinePhase = WorldZ * (2.0f * PI) / Params.LayerLineSpacing;
float LineValue = FMath::Sin(LinePhase);
float LineValue = VoxelMath::DetSin(LinePhase);
// Sharpen to thin grooves: only carve where sine > 0, then cube it.
// sin → max(sin, 0) → pow(_, 3) turns broad sine humps into thin spikes
@@ -4735,7 +4751,7 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
{
// Sine wave along Z, offset by half-period from layer lines
float RibPhase = WorldZ * (2.0f * PI) / Params.RibbingSpacing + PI * 0.5f;
float RibValue = FMath::Sin(RibPhase);
float RibValue = VoxelMath::DetSin(RibPhase);
// Half-wave rectify (only positive → ribs, not grooves) then smooth
RibValue = FMath::Max(RibValue, 0.0f);
@@ -5720,7 +5736,7 @@ float UVoxelGenerator::ComputeSurfaceTerrainZ(float WorldX, float WorldY,
if (Params.LayerLineDepth > 0.0f && Params.LayerLineSpacing > 0.0f)
{
const float Phase = Terrain * (2.0f * PI / Params.LayerLineSpacing);
Terrain -= FMath::Sin(Phase) * Params.LayerLineDepth;
Terrain -= VoxelMath::DetSin(Phase) * Params.LayerLineDepth;
}
// Beach: flatten terrain toward the water line within BeachWidth. (Water level is