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
@@ -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
|
||||
|
||||
Reference in new issue
Block a user