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:
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);
|
||||
|
||||
Reference in new issue
Block a user