diff --git a/Source/VoxelForge/Private/Tests/VoxelForgeCrossPlatformTest.cpp b/Source/VoxelForge/Private/Tests/VoxelForgeCrossPlatformTest.cpp index cff985b..c218669 100644 --- a/Source/VoxelForge/Private/Tests/VoxelForgeCrossPlatformTest.cpp +++ b/Source/VoxelForge/Private/Tests/VoxelForgeCrossPlatformTest.cpp @@ -163,15 +163,10 @@ bool FVoxelForgeCrossPlatformTest::RunTest(const FString& Parameters) // que ça rendait cette mesure caduque. **C'est faux, et il a fallu vérifier plutôt que // supposer.** // - // Il reste `FMath::Sin` / `FMath::Cos`, présents partout dans le chemin de densité (lignes de - // strates, nervures, placement des salles, rotations). **`sinf`/`cosf` ne sont PAS spécifiés - // par IEEE-754** : le CRT de MSVC et la libm de la glibc ont parfaitement le droit de rendre - // des résultats différents (typiquement ≤ 1 ULP, mais différents). `FPSemantics` a donc fermé - // la moitié COMPILATEUR de §C9 et laissé ouverte la moitié BIBLIOTHÈQUE. - // - // FPSemantics = Precise removed the float-MODEL difference, but sinf/cosf are not IEEE-754 - // specified, so MSVC's CRT and glibc's libm may still differ. The compiler half of C9 is closed; - // the library half is not. + // The world path now owns its sine/cosine range reduction and polynomial in VoxelMath. Before + // that replacement, `sinf`/`cosf` were not specified by IEEE-754: MSVC's CRT and glibc's libm + // could return different results (typically <= 1 ULP). `FPSemantics` closed the compiler half + // of §C9; DetSin/DetCos close the library half as well. // // D'où trois bandes au lieu d'une : une bande unique à 1e-4 est **100× trop large** pour un // écart de libm (~1e-6 en absolu sur des densités de magnitude ~10), donc elle sur-estime @@ -223,37 +218,30 @@ bool FVoxelForgeCrossPlatformTest::RunTest(const FString& Parameters) AddInfo(FString::Printf( TEXT("CROSS-PLATFORM DIGEST (seed %d, %d samples, step %d/%d)\n") TEXT(" SHAPE digest : 0x%016llX <- must match across Windows/Linux. This is the world.\n") - TEXT(" FIELD digest : 0x%016llX <- bit-for-bit. May differ; see NearIso below.\n") + TEXT(" FIELD digest : 0x%016llX <- expected bit-for-bit after DetSin/DetCos.\n") TEXT(" solid %d / air %d / NaN %d"), World.Settings->Seed, NumSamples, XYStep, ZStep, ShapeDigest, FieldDigest, NumSolid, NumSamples - NumSolid, NumNaN)); - // Le PROFIL de proximité à l'isosurface, plutôt qu'un seul seuil binaire. + // Le PROFIL de proximité à l'isosurface reste utile pour visualiser la marge géométrique, + // mais il ne mesure plus une divergence CRT : the world trig path is deterministic now. AddInfo(FString::Printf( TEXT("NearIso profile over %d samples: %d within 1e-4, %d within 1e-5, %d within 1e-6. ") - TEXT("Only the LAST number is the cross-platform risk: FPSemantics = Precise removed the ") - TEXT("float-MODEL difference, so what remains is that sinf/cosf are not IEEE-754 specified ") - TEXT("and MSVC's CRT may differ from glibc's libm by ~1 ULP. On densities of magnitude ~10 ") - TEXT("that is ~1e-6 absolute, which is why the wide band over-states the risk ~100x."), + TEXT("This is a geometric margin profile; DetSin/DetCos removes the prior CRT/libm source ") + TEXT("of cross-platform variation."), NumSamples, NumNearWide, NumNearMid, NumNearTight)); if (NumNearTight > 0) { - AddWarning(FString::Printf( - TEXT("%d of %d samples sit within 1e-6 of the isosurface -- tight enough that a libm ") - TEXT("difference between MSVC and glibc could flip their SIGN, i.e. one voxel solid for ") - TEXT("a Windows host and air for a Linux client. FMath::Sin/Cos are used throughout the ") - TEXT("density path (layer lines, ribs, room placement, rotations), so this is the ") - TEXT("REMAINING half of AUDIT C9 -- the compiler half is fixed, the library half is not. ") - TEXT("If this must be zero, the fix is a deterministic in-house sin/cos in the density ") - TEXT("path (one more world re-tune), not another build flag."), + AddInfo(FString::Printf( + TEXT("%d of %d samples sit within 1e-6 of the isosurface. The proximity is worth ") + TEXT("recording, but it is no longer a CRT/libm cross-platform risk because the production ") + TEXT("world path uses deterministic VoxelMath::DetSin/DetCos."), NumNearTight, NumSamples)); } else { - AddInfo(TEXT("No sample sits within 1e-6 of the isosurface, so no sampled voxel is close ") - TEXT("enough for a libm difference to flip its side. Evidence, not proof: it covers ") - TEXT("this grid, not every voxel of a world.")); + AddInfo(TEXT("No sample sits within 1e-6 of the isosurface on this grid.")); } TestEqual(TEXT("no sample produced NaN"), NumNaN, 0); diff --git a/Source/VoxelForge/Private/Tests/VoxelForgeDetSinCosTest.cpp b/Source/VoxelForge/Private/Tests/VoxelForgeDetSinCosTest.cpp new file mode 100644 index 0000000..d473df3 --- /dev/null +++ b/Source/VoxelForge/Private/Tests/VoxelForgeDetSinCosTest.cpp @@ -0,0 +1,102 @@ +// Pinned contract for VoxelMath::DetSin/DetCos/DetSinCos. + +#include "Misc/AutomationTest.h" +#include "VoxelTypes.h" + +#include + +namespace +{ + struct FDetSinCosGolden + { + float Input; + uint32 SinBits; + uint32 CosBits; + }; + + uint32 FloatBits(float Value) + { + uint32 Bits = 0; + FMemory::Memcpy(&Bits, &Value, sizeof(Bits)); + return Bits; + } +} + +IMPLEMENT_SIMPLE_AUTOMATION_TEST( + FVoxelForgeDetSinCosTest, + "VoxelForge.Determinism.DetSinCos", + EAutomationTestFlags_ApplicationContextMask | EAutomationTestFlags::EngineFilter) + +bool FVoxelForgeDetSinCosTest::RunTest(const FString& Parameters) +{ + (void)Parameters; + + const float Pi = 3.1415926535897932384626433832795f; + const float TwoPi = 6.283185307179586476925286766559f; + // The expected values are deliberately bit patterns rather than tolerances. This corpus is + // the pinned implementation contract; update it only with an intentional world re-tune. + static const FDetSinCosGolden Corpus[] = + { + { 0.0f, 0x00000000u, 0x3F800000u }, + {-0.0f, 0x80000000u, 0x3F800000u }, + { Pi, 0xB3BBBD2Eu, 0xBF800000u }, + {-Pi, 0x33BBBD2Eu, 0xBF800000u }, + { TwoPi, 0x343BBD2Eu, 0x3F800000u }, + {-TwoPi, 0xB43BBD2Eu, 0x3F800000u }, + { 0.001f, 0x3A83126Du, 0x3F7FFFF8u }, + {-0.001f, 0xBA83126Du, 0x3F7FFFF8u }, + { 123456.789f, 0xBF7FA83Du, 0x3D53E806u }, + {-123456.789f, 0x3F7FA83Du, 0x3D53E806u }, + { 1000000.25f, 0xBDDBDDEBu, 0x3F7E853Du }, + {-1000000.25f, 0x3DDBDDEBu, 0x3F7E853Du }, + }; + + for (const FDetSinCosGolden& Sample : Corpus) + { + float ActualSin = 0.0f; + float ActualCos = 0.0f; + VoxelMath::DetSinCos(ActualSin, ActualCos, Sample.Input); + AddInfo(FString::Printf( + TEXT("DetSinCos input %.9g -> sin=0x%08X cos=0x%08X"), + Sample.Input, FloatBits(ActualSin), FloatBits(ActualCos))); + TestEqual( + FString::Printf(TEXT("sin bits for %.9g"), Sample.Input), + FloatBits(ActualSin), Sample.SinBits); + TestEqual( + FString::Printf(TEXT("cos bits for %.9g"), Sample.Input), + FloatBits(ActualCos), Sample.CosBits); + TestEqual( + FString::Printf(TEXT("DetSin agrees with paired sin for %.9g"), Sample.Input), + FloatBits(VoxelMath::DetSin(Sample.Input)), FloatBits(ActualSin)); + TestEqual( + FString::Printf(TEXT("DetCos agrees with paired cos for %.9g"), Sample.Input), + FloatBits(VoxelMath::DetCos(Sample.Input)), FloatBits(ActualCos)); + } + + // The canonical export measured |argument| <= 6.19255877. Sweep a small margin around that + // envelope; the large corpus above separately exercises the double range reduction. + constexpr float MeasuredWorldArgumentMax = 6.25f; + constexpr int32 SweepSamples = 200001; + float MaxSinError = 0.0f; + float MaxCosError = 0.0f; + for (int32 Index = 0; Index < SweepSamples; ++Index) + { + const float Alpha = -MeasuredWorldArgumentMax + + (2.0f * MeasuredWorldArgumentMax) + * static_cast(Index) / static_cast(SweepSamples - 1); + float ActualSin = 0.0f; + float ActualCos = 0.0f; + VoxelMath::DetSinCos(ActualSin, ActualCos, Alpha); + MaxSinError = FMath::Max(MaxSinError, FMath::Abs(ActualSin - ::sinf(Alpha))); + MaxCosError = FMath::Max(MaxCosError, FMath::Abs(ActualCos - ::cosf(Alpha))); + } + AddInfo(FString::Printf( + TEXT("DetSinCos max abs error vs sinf/cosf over [-%.1f,+%.1f], %d samples: " + "sin %.9g, cos %.9g"), + MeasuredWorldArgumentMax, MeasuredWorldArgumentMax, SweepSamples, + MaxSinError, MaxCosError)); + TestTrue(TEXT("DetSin max error over measured world range <= 2e-6"), MaxSinError <= 2.0e-6f); + TestTrue(TEXT("DetCos max error over measured world range <= 2e-6"), MaxCosError <= 2.0e-6f); + + return true; +} diff --git a/Source/VoxelForge/Private/Tests/VoxelForgePassageOpenSpaceTest.cpp b/Source/VoxelForge/Private/Tests/VoxelForgePassageOpenSpaceTest.cpp index 6d24a6e..028db64 100644 --- a/Source/VoxelForge/Private/Tests/VoxelForgePassageOpenSpaceTest.cpp +++ b/Source/VoxelForge/Private/Tests/VoxelForgePassageOpenSpaceTest.cpp @@ -963,8 +963,10 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters) for (int32 RingSample = 0; RingSample < RingSampleCount; ++RingSample) { const float Angle = 2.0f * PI * (float)RingSample / (float)RingSampleCount; - const float SampleX = Passage.LowerPoint.X + FMath::Cos(Angle) * RingRadius; - const float SampleY = Passage.LowerPoint.Y + FMath::Sin(Angle) * RingRadius; + float SinAngle = 0.0f, CosAngle = 0.0f; + VoxelMath::DetSinCos(SinAngle, CosAngle, Angle); + const float SampleX = Passage.LowerPoint.X + CosAngle * RingRadius; + const float SampleY = Passage.LowerPoint.Y + SinAngle * RingRadius; const float Density = World.Generator->GetDensityAt( SampleX, SampleY, @@ -1115,7 +1117,7 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters) LandingGradient = MaxGradient; WorstLandingFloorGradient = FMath::Max(WorstLandingFloorGradient, MaxGradient); const float NormalZ = 1.0f / FMath::Sqrt(1.0f + MaxGradient * MaxGradient); - bFloorPassed = NormalZ + KINDA_SMALL_NUMBER >= FMath::Cos( + bFloorPassed = NormalZ + KINDA_SMALL_NUMBER >= VoxelMath::DetCos( FMath::DegreesToRadians(LandingWalkableAngleDegrees)); } diff --git a/Source/VoxelForge/Private/Tests/VoxelForgeScaleDiagnosisTest.cpp b/Source/VoxelForge/Private/Tests/VoxelForgeScaleDiagnosisTest.cpp index 5434119..e18a894 100644 --- a/Source/VoxelForge/Private/Tests/VoxelForgeScaleDiagnosisTest.cpp +++ b/Source/VoxelForge/Private/Tests/VoxelForgeScaleDiagnosisTest.cpp @@ -990,7 +990,7 @@ bool FVoxelForgeScaleDiagnosisTest::RunTest(const FString& Parameters) return false; } - const float MinimumWalkableNormalZ = FMath::Cos(FMath::DegreesToRadians( + const float MinimumWalkableNormalZ = VoxelMath::DetCos(FMath::DegreesToRadians( CharacterFitSettings.PlayerWalkableFloorAngleDegrees)); const int32 SupportFootprintColumns = VF_CountSupportFootprintColumns( CharacterFitSettings.PlayerCapsuleRadiusVoxels); diff --git a/Source/VoxelForge/Private/VoxelCaveMorphology.cpp b/Source/VoxelForge/Private/VoxelCaveMorphology.cpp index 65d4e15..59ee5a4 100644 --- a/Source/VoxelForge/Private/VoxelCaveMorphology.cpp +++ b/Source/VoxelForge/Private/VoxelCaveMorphology.cpp @@ -2928,7 +2928,7 @@ namespace const int32 MaxDownwardSearchCells = FMath::CeilToInt( static_cast(MaxStepHeightVoxelsReal)); const float MaxStepHeightVoxels = static_cast(MaxStepHeightVoxelsReal); - const float MinimumWalkableNormalZ = FMath::Cos(FMath::DegreesToRadians( + const float MinimumWalkableNormalZ = VoxelMath::DetCos(FMath::DegreesToRadians( Settings.PlayerWalkableFloorAngleDegrees)); TArray SupportHeights; @@ -3263,7 +3263,9 @@ namespace const float DirectionAngle = VoxelHash::ToFloat01( VoxelHash::Mix(Site.Hash ^ 0xCAFEBABEu)) * 2.0f * PI; const float StretchDistance = Site.RadiusXY * 0.7f; - const FVector Direction(FMath::Cos(DirectionAngle), FMath::Sin(DirectionAngle), 0.0f); + float SinDirection = 0.0f, CosDirection = 0.0f; + VoxelMath::DetSinCos(SinDirection, CosDirection, DirectionAngle); + const FVector Direction(CosDirection, SinDirection, 0.0f); RoomSDF = VoxelSDF::Capsule( Position, Site.Center + Direction * StretchDistance, @@ -6823,7 +6825,7 @@ void VoxelCaveMorphology::BuildChunkCache( { const float T = static_cast(ControlIndex) / static_cast(WanderSegments); - const float Envelope = FMath::Sin(T * PI); + const float Envelope = VoxelMath::DetSin(T * PI); FVector WorldControlPoint = FMath::Lerp(WorldEndA, WorldEndB, T); const float RadiusBase = FMath::Lerp(RadA, RadB, T); float ControlRadius = RadiusBase; @@ -6840,7 +6842,7 @@ void VoxelCaveMorphology::BuildChunkCache( // serpentine route without moving either mouth. const float Side = RawSide * 0.65f + PreviousSide * 0.35f; const float WindWave = bNeedsWind - ? FMath::Sin(static_cast(WindWaves) * PI * T) + ? VoxelMath::DetSin(static_cast(WindWaves) * PI * T) * WindAmplitude * Envelope * ((LedgeChoiceHash & 1u) != 0u ? 1.0f : -1.0f) : 0.0f; @@ -7364,7 +7366,9 @@ void VoxelCaveMorphology::BuildChunkCache( CR.ShapeType = 2; const float DirAngle = VoxelHash::ToFloat01(VoxelHash::Mix(CR.Hash ^ 0xCAFEBABEu)) * 2.0f * PI; const float StretchDist = CR.RadiusXY * 0.7f; - const FVector Dir(FMath::Cos(DirAngle), FMath::Sin(DirAngle), 0.0f); + float SinDir = 0.0f, CosDir = 0.0f; + VoxelMath::DetSinCos(SinDir, CosDir, DirAngle); + const FVector Dir(CosDir, SinDir, 0.0f); CR.ShapeA = CR.Center + Dir * StretchDist; CR.ShapeB = CR.Center - Dir * StretchDist; CR.ShapeR = FMath::Min(CR.RadiusXY * 0.6f, CR.RadiusZ); @@ -7484,8 +7488,8 @@ void VoxelCaveMorphology::BuildChunkCache( const float Angle = VoxelHash::ToFloat01(AH4) * PI; const float HalfSpan = CR.RadiusXY * (0.5f + VoxelHash::ToFloat01(VoxelHash::Mix(AH4)) * 0.35f); - const float CosA = FMath::Cos(Angle); - const float SinA = FMath::Sin(Angle); + float SinA = 0.0f, CosA = 0.0f; + VoxelMath::DetSinCos(SinA, CosA, Angle); FCachedArch& Arch = CR.Arches[i]; Arch.bActive = true; Arch.EndpointA = FVector(ArcCX - CosA * HalfSpan, @@ -7517,8 +7521,7 @@ void VoxelCaveMorphology::BuildChunkCache( Pinch.CenterX = PnX; Pinch.CenterY = PnY; Pinch.CenterZ = PnZ; - Pinch.CosAngle = FMath::Cos(PnAngle); - Pinch.SinAngle = FMath::Sin(PnAngle); + VoxelMath::DetSinCos(Pinch.SinAngle, Pinch.CosAngle, PnAngle); Pinch.MaxExtent = FMath::Max( FeatureParams.PinchLength, FeatureParams.PinchStrength) + 5.0f; Pinch.HalfLength = FeatureParams.PinchLength * 0.5f; diff --git a/Source/VoxelForge/Private/VoxelContentManager.cpp b/Source/VoxelForge/Private/VoxelContentManager.cpp index 1bd6adc..26913ba 100644 --- a/Source/VoxelForge/Private/VoxelContentManager.cpp +++ b/Source/VoxelForge/Private/VoxelContentManager.cpp @@ -501,9 +501,9 @@ void UVoxelContentManager::BuildCellSpawns(const UVoxelGenerator* Gen, const FTr for (int32 e = 0; e < Entries.Num(); ++e) { CosMaxSlope[e] = (Entries[e].Profile.MaxSlopeAngle < 89.99f) - ? FMath::Cos(FMath::DegreesToRadians(Entries[e].Profile.MaxSlopeAngle)) : -1.0f; + ? VoxelMath::DetCos(FMath::DegreesToRadians(Entries[e].Profile.MaxSlopeAngle)) : -1.0f; CosMinSlope[e] = (Entries[e].Profile.MinSlopeAngle > 0.01f) - ? FMath::Cos(FMath::DegreesToRadians(Entries[e].Profile.MinSlopeAngle)) : -1.0f; + ? VoxelMath::DetCos(FMath::DegreesToRadians(Entries[e].Profile.MinSlopeAngle)) : -1.0f; } // Per-COLUMN biome cache: ResolveBiomeSampleAt's noise-heavy cell classification is box-validated @@ -647,8 +647,10 @@ void UVoxelContentManager::BuildCellSpawns(const UVoxelGenerator* Gen, const FTr const float Ang = VoxelHash::ToFloat01(IH) * 2.0f * PI; const float RadVox = FMath::Lerp(Comp.RadiusMinVox, Comp.RadiusMaxVox, VoxelHash::ToFloat01(VoxelHash::Mix(IH ^ 0x77u))); - const float SatVX = VX + FMath::Cos(Ang) * RadVox; // voxel, actor-local - const float SatVY = VY + FMath::Sin(Ang) * RadVox; + float SinAng = 0.0f, CosAng = 0.0f; + VoxelMath::DetSinCos(SinAng, CosAng, Ang); + const float SatVX = VX + CosAng * RadVox; // voxel, actor-local + const float SatVY = VY + SinAng * RadVox; // Optional per-satellite gating (relief/moisture/biome-border at ITS own XY). if (Comp.Profile.Conditions.Num() > 0 && @@ -693,8 +695,10 @@ void UVoxelContentManager::BuildCellSpawns(const UVoxelGenerator* Gen, const FTr const float SAng = VoxelHash::ToFloat01(JH) * 2.0f * PI; const float SRadVox = FMath::Lerp(Sub.RadiusMinVox, Sub.RadiusMaxVox, VoxelHash::ToFloat01(VoxelHash::Mix(JH ^ 0x77u))); - const float SubVX = SatVX + FMath::Cos(SAng) * SRadVox; - const float SubVY = SatVY + FMath::Sin(SAng) * SRadVox; + float SinSAng = 0.0f, CosSAng = 0.0f; + VoxelMath::DetSinCos(SinSAng, CosSAng, SAng); + const float SubVX = SatVX + CosSAng * SRadVox; + const float SubVY = SatVY + SinSAng * SRadVox; if (Sub.Profile.Conditions.Num() > 0 && !Gen->EvaluateTerrainConditions(Sub.Profile.Conditions, SubVX, SubVY, Ctx.BiomeCtx)) continue; @@ -1230,9 +1234,9 @@ bool UVoxelContentManager::SpawnFromProfile(const FPlacementProfile& P, uint32 H // Surface-tilt gates (acos(|N.Z|); guarded so defaults cost no trig). if (P.MaxSlopeAngle < 89.99f && - FMath::Abs(N.Z) < FMath::Cos(FMath::DegreesToRadians(P.MaxSlopeAngle))) return false; + FMath::Abs(N.Z) < VoxelMath::DetCos(FMath::DegreesToRadians(P.MaxSlopeAngle))) return false; if (P.MinSlopeAngle > 0.01f && - FMath::Abs(N.Z) > FMath::Cos(FMath::DegreesToRadians(P.MinSlopeAngle))) return false; + FMath::Abs(N.Z) > VoxelMath::DetCos(FMath::DegreesToRadians(P.MinSlopeAngle))) return false; const FVector LocalPos(LocalX, LocalY, ZC * VOXEL_SIZE); if (P.bRequireWaterRelative && Ctx.bHasWater) diff --git a/Source/VoxelForge/Private/VoxelDensityOpStack.cpp b/Source/VoxelForge/Private/VoxelDensityOpStack.cpp index c547905..0b43b28 100644 --- a/Source/VoxelForge/Private/VoxelDensityOpStack.cpp +++ b/Source/VoxelForge/Private/VoxelDensityOpStack.cpp @@ -7504,7 +7504,7 @@ namespace if (!(DistFromSurface < LineRange)) { return; } const float LinePhase = WorldZ * (2.0f * PI) / LP.LayerLineSpacing; - float LineValue = FMath::Sin(LinePhase); + float LineValue = VoxelMath::DetSin(LinePhase); LineValue = FMath::Max(LineValue, 0.0f); LineValue = LineValue * LineValue * LineValue; // affûtage cubique @@ -7606,7 +7606,7 @@ namespace if (!(DistFromSurface < RibRange)) { return; } const float RibPhase = WorldZ * (2.0f * PI) / LP.RibbingSpacing + PI * 0.5f; - float RibValue = FMath::Sin(RibPhase); + float RibValue = VoxelMath::DetSin(RibPhase); RibValue = FMath::Max(RibValue, 0.0f); RibValue = RibValue * RibValue; // profil de bosse arrondi diff --git a/Source/VoxelForge/Private/VoxelForgeModule.cpp b/Source/VoxelForge/Private/VoxelForgeModule.cpp index 68c774b..e341c6c 100644 --- a/Source/VoxelForge/Private/VoxelForgeModule.cpp +++ b/Source/VoxelForge/Private/VoxelForgeModule.cpp @@ -3,6 +3,14 @@ #include "VoxelForgeModule.h" #include "VoxelStackSampler.h" +#include "VoxelGenerator.h" +#include "VoxelMarchingCubesMesher.h" +#include "Misc/CommandLine.h" +#include "Misc/Parse.h" + +#if PLATFORM_WINDOWS +#include +#endif // This macro registers our module with Unreal // "VoxelForge" must match the module name in VoxelForge.Build.cs and .uplugin @@ -11,7 +19,30 @@ IMPLEMENT_MODULE(FVoxelForgeModule, VoxelForge) void FVoxelForgeModule::StartupModule() { // Called when plugin loads - // We don't need to do anything here for now + // Keep this diagnostic before any plugin-owned generation can begin. The UCRT may select + // its FMA3 math implementations during process startup; this switch lets the canonical export + // compare that path with the non-FMA3 implementation on the same machine. +#if PLATFORM_WINDOWS + const int32 Fma3Before = _get_FMA3_enable(); + int32 RequestedFma3 = 0; + const bool bHasFma3Override = FParse::Value( + FCommandLine::Get(), TEXT("voxel.CrtFma3="), RequestedFma3); + if (bHasFma3Override) + { + _set_FMA3_enable(RequestedFma3 != 0 ? 1 : 0); + } + const int32 Fma3After = _get_FMA3_enable(); + UE_LOG(LogTemp, Log, + TEXT("VoxelForge CRT FMA3 diagnostic: before=%d override=%s requested=%d after=%d"), + Fma3Before, + bHasFma3Override ? TEXT("yes") : TEXT("no"), + RequestedFma3, + Fma3After); +#endif + // These parsers used to be entered lazily by worker-side density/mesher calls. Resolve the + // command line once while the module is starting, before any generation task is submitted. + VoxelGenLOD::InitializeConsoleSwitches(); + UVoxelMarchingCubesMesher::InitializeConsoleSwitches(); UE_LOG(LogTemp, Log, TEXT("VoxelForge module started!")); } diff --git a/Source/VoxelForge/Private/VoxelGenerator.cpp b/Source/VoxelForge/Private/VoxelGenerator.cpp index 1bf0060..d823049 100644 --- a/Source/VoxelForge/Private/VoxelGenerator.cpp +++ b/Source/VoxelForge/Private/VoxelGenerator.cpp @@ -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 diff --git a/Source/VoxelForge/Private/VoxelHeightOpStack.cpp b/Source/VoxelForge/Private/VoxelHeightOpStack.cpp index c434829..2e4bd61 100644 --- a/Source/VoxelForge/Private/VoxelHeightOpStack.cpp +++ b/Source/VoxelForge/Private/VoxelHeightOpStack.cpp @@ -229,7 +229,7 @@ namespace if (P.LayerLineDepth <= 0.0f || P.LayerLineSpacing <= 0.0f) { return; } const float Phase = InOut.Height * (2.0f * PI / P.LayerLineSpacing); - InOut.Height -= FMath::Sin(Phase) * P.LayerLineDepth; + InOut.Height -= VoxelMath::DetSin(Phase) * P.LayerLineDepth; } // `sin` ∈ [-1,1] ⇒ borne exacte. diff --git a/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp b/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp index 9aa4464..a5f241d 100644 --- a/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp +++ b/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp @@ -82,6 +82,12 @@ namespace } } +void UVoxelMarchingCubesMesher::InitializeConsoleSwitches() +{ + VF_ParseOperatorBlockSwitch(); + VF_ParseTilePostReachSwitches(); +} + //============================================================================= // MAIN ALGORITHM //============================================================================= @@ -94,8 +100,6 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels, int64* OutDensitySampleCount) { TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_MesherGenerateMesh); - VF_ParseOperatorBlockSwitch(); - VF_ParseTilePostReachSwitches(); FVoxelMeshData MeshData; if (OutCaptureGrid) { OutCaptureGrid->Reset(); } if (OutDensitySampleCount) { *OutDensitySampleCount = 0; } diff --git a/Source/VoxelForge/Private/VoxelStrateManager.cpp b/Source/VoxelForge/Private/VoxelStrateManager.cpp index 62c6da8..a00ce29 100644 --- a/Source/VoxelForge/Private/VoxelStrateManager.cpp +++ b/Source/VoxelForge/Private/VoxelStrateManager.cpp @@ -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); diff --git a/Source/VoxelForge/Private/VoxelStrateMeasure.cpp b/Source/VoxelForge/Private/VoxelStrateMeasure.cpp index 3df59a2..85b7ac5 100644 --- a/Source/VoxelForge/Private/VoxelStrateMeasure.cpp +++ b/Source/VoxelForge/Private/VoxelStrateMeasure.cpp @@ -1405,7 +1405,7 @@ namespace VoxelStrateMeasurePrivate const int32 MaxDownwardSearchCells = static_cast( FMath::CeilToDouble(MaxStepHeightVoxelsReal)); const float MaxStepHeightVoxels = static_cast(MaxStepHeightVoxelsReal); - const float MinimumWalkableNormalZ = FMath::Cos(FMath::DegreesToRadians( + const float MinimumWalkableNormalZ = VoxelMath::DetCos(FMath::DegreesToRadians( Settings.PlayerWalkableFloorAngleDegrees)); TArray SupportOffsets; diff --git a/Source/VoxelForge/Public/VoxelGenerator.h b/Source/VoxelForge/Public/VoxelGenerator.h index 9d0b9f7..5699a4e 100644 --- a/Source/VoxelForge/Public/VoxelGenerator.h +++ b/Source/VoxelForge/Public/VoxelGenerator.h @@ -9,6 +9,7 @@ #pragma once #include "CoreMinimal.h" +#include "UObject/Object.h" #include "VoxelTypes.h" #include "VoxelStrateTypes.h" #include "VoxelBiomeTypes.h" @@ -73,6 +74,8 @@ namespace VoxelGenLOD VOXELFORGE_API void SetThreadOctaveBias(int32 Value); VOXELFORGE_API int32 GetThreadSampleStep(); VOXELFORGE_API void SetThreadSampleStep(int32 Value); + // Called by the module's game-thread startup before any worker can enter GetDensityAt. + VOXELFORGE_API void InitializeConsoleSwitches(); VOXELFORGE_API bool IsTileCacheWindowEnabled(bool bFusedPath = true); VOXELFORGE_API bool GetThreadTileCacheWindow( FIntVector& OutOriginVoxels, int32& OutStep, int32& OutCellsPerAxis); diff --git a/Source/VoxelForge/Public/VoxelMarchingCubesMesher.h b/Source/VoxelForge/Public/VoxelMarchingCubesMesher.h index 1c493aa..098644e 100644 --- a/Source/VoxelForge/Public/VoxelMarchingCubesMesher.h +++ b/Source/VoxelForge/Public/VoxelMarchingCubesMesher.h @@ -44,6 +44,10 @@ class VOXELFORGE_API UVoxelMarchingCubesMesher : public UObject GENERATED_BODY() public: + // Parse command-line switches on the module's game-thread startup. GenerateMesh is a worker + // entry point and therefore only reads the resulting values. + static void InitializeConsoleSwitches(); + /** * Génère le mesh d'une TUILE de clipmap (voir FVoxelTileKey). * diff --git a/Source/VoxelForge/Public/VoxelTypes.h b/Source/VoxelForge/Public/VoxelTypes.h index 98f7ebe..98a4e3f 100644 --- a/Source/VoxelForge/Public/VoxelTypes.h +++ b/Source/VoxelForge/Public/VoxelTypes.h @@ -51,6 +51,148 @@ namespace VoxelMath { return GFastIsFinite != 0 ? IsFiniteFast(Value) : FMath::IsFinite(Value); } + + // Deterministic trigonometry for world decisions. + // + // The implementation deliberately owns both the range reduction and the polynomial. It + // does not call the CRT, FMath, or a platform vector intrinsic: the same finite float is + // reduced by the same constants and evaluated by the same scalar arithmetic on every host. + // The reduction is done in double so world-coordinate phases do not lose all low bits before + // the period is removed. The polynomial itself is float arithmetic, matching the values + // consumed by the density field and keeping the hot path small. + FORCEINLINE void DetSinCos(float& OutSin, float& OutCos, float Value) + { + if (!IsFiniteFast(Value)) + { + // Non-finite values are outside the world contract. Preserve the input rather than + // entering an undefined integer conversion during range reduction. + OutSin = Value; + OutCos = Value; + return; + } + + constexpr double TwoPi = 6.283185307179586476925286766559; + constexpr double HalfPi = 1.57079632679489661923132169164; + + // Round the number of complete periods toward the nearest integer. All measured world + // phases are many orders of magnitude below INT64_MAX. For a pathological finite input + // whose period count does not fit in int64, use the exact integer significand from the + // float representation and reduce it by fixed binary doubling instead of converting an + // out-of-range quotient. + const double PeriodsReal = static_cast(Value) / TwoPi; + constexpr double MaxSafeInt64 = 9223372036854774784.0; + double Reduced = 0.0; + if (PeriodsReal >= MaxSafeInt64 || PeriodsReal <= -MaxSafeInt64) + { + uint32 ValueBits = 0; + FMemory::Memcpy(&ValueBits, &Value, sizeof(ValueBits)); + const uint32 MagnitudeBits = ValueBits & 0x7FFFFFFFu; + const uint32 ExponentBits = (MagnitudeBits >> 23) & 0xFFu; + const uint32 Significand = (MagnitudeBits & 0x007FFFFFu) | 0x00800000u; + const int32 BinaryExponent = static_cast(ExponentBits) - 127 - 23; + + double ReducedInteger = 0.0; + for (int32 Bit = 23; Bit >= 0; --Bit) + { + ReducedInteger *= 2.0; + ReducedInteger += static_cast((Significand >> Bit) & 1u); + if (ReducedInteger >= TwoPi) + { + ReducedInteger -= TwoPi; + } + } + for (int32 Shift = 0; Shift < BinaryExponent; ++Shift) + { + ReducedInteger *= 2.0; + if (ReducedInteger >= TwoPi) + { + ReducedInteger -= TwoPi; + } + } + Reduced = (ValueBits & 0x80000000u) != 0u ? -ReducedInteger : ReducedInteger; + } + else + { + int64 Periods = 0; + if (PeriodsReal >= 0.0) + { + Periods = static_cast(PeriodsReal + 0.5); + } + else + { + Periods = static_cast(PeriodsReal - 0.5); + } + + Reduced = static_cast(Value) + - static_cast(Periods) * TwoPi; + } + + // Reduce the remaining angle to [-pi/4, pi/4]. Both reduction branches above keep + // Reduced bounded by one period, so this quadrant conversion is always in int32 range. + const double QuadrantReal = Reduced / HalfPi; + int32 Quadrant = 0; + if (QuadrantReal >= 0.0) + { + Quadrant = static_cast(QuadrantReal + 0.5); + } + else + { + Quadrant = static_cast(QuadrantReal - 0.5); + } + Reduced -= static_cast(Quadrant) * HalfPi; + const float Y = static_cast(Reduced); + const float Y2 = Y * Y; + + // Fixed minimax polynomials, the same degree as UE's scalar SinCos implementation. The + // quarter-period reduction gives them a smaller interval than UE's [-pi/2, pi/2] path. + const float SinP = (((((-2.3889859e-08f * Y2 + 2.7525562e-06f) * Y2 + - 0.00019840874f) * Y2 + 0.0083333310f) * Y2 + - 0.16666667f) * Y2 + 1.0f) * Y; + const float CosP = (((((-2.6051615e-07f * Y2 + 2.4760495e-05f) * Y2 + - 0.0013888378f) * Y2 + 0.041666638f) * Y2 + - 0.5f) * Y2 + 1.0f); + + int32 QuadrantMod = Quadrant % 4; + if (QuadrantMod < 0) + { + QuadrantMod += 4; + } + switch (QuadrantMod) + { + case 0: + OutSin = SinP; + OutCos = CosP; + break; + case 1: + OutSin = CosP; + OutCos = -SinP; + break; + case 2: + OutSin = -SinP; + OutCos = -CosP; + break; + default: + OutSin = -CosP; + OutCos = SinP; + break; + } + } + + FORCEINLINE float DetSin(float Value) + { + float SinValue = 0.0f; + float CosValue = 0.0f; + DetSinCos(SinValue, CosValue, Value); + return SinValue; + } + + FORCEINLINE float DetCos(float Value) + { + float SinValue = 0.0f; + float CosValue = 0.0f; + DetSinCos(SinValue, CosValue, Value); + return CosValue; + } } //=============================================================================