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

@@ -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 // que ça rendait cette mesure caduque. **C'est faux, et il a fallu vérifier plutôt que
// supposer.** // supposer.**
// //
// Il reste `FMath::Sin` / `FMath::Cos`, présents partout dans le chemin de densité (lignes de // The world path now owns its sine/cosine range reduction and polynomial in VoxelMath. Before
// strates, nervures, placement des salles, rotations). **`sinf`/`cosf` ne sont PAS spécifiés // that replacement, `sinf`/`cosf` were not specified by IEEE-754: MSVC's CRT and glibc's libm
// par IEEE-754** : le CRT de MSVC et la libm de la glibc ont parfaitement le droit de rendre // could return different results (typically <= 1 ULP). `FPSemantics` closed the compiler half
// des résultats différents (typiquement ≤ 1 ULP, mais différents). `FPSemantics` a donc fermé // of §C9; DetSin/DetCos close the library half as well.
// 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.
// //
// D'où trois bandes au lieu d'une : une bande unique à 1e-4 est **100× trop large** pour un // 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 // é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( AddInfo(FString::Printf(
TEXT("CROSS-PLATFORM DIGEST (seed %d, %d samples, step %d/%d)\n") 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(" 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"), TEXT(" solid %d / air %d / NaN %d"),
World.Settings->Seed, NumSamples, XYStep, ZStep, World.Settings->Seed, NumSamples, XYStep, ZStep,
ShapeDigest, FieldDigest, NumSolid, NumSamples - NumSolid, NumNaN)); 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( AddInfo(FString::Printf(
TEXT("NearIso profile over %d samples: %d within 1e-4, %d within 1e-5, %d within 1e-6. ") 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("This is a geometric margin profile; DetSin/DetCos removes the prior CRT/libm source ")
TEXT("float-MODEL difference, so what remains is that sinf/cosf are not IEEE-754 specified ") TEXT("of cross-platform variation."),
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."),
NumSamples, NumNearWide, NumNearMid, NumNearTight)); NumSamples, NumNearWide, NumNearMid, NumNearTight));
if (NumNearTight > 0) if (NumNearTight > 0)
{ {
AddWarning(FString::Printf( AddInfo(FString::Printf(
TEXT("%d of %d samples sit within 1e-6 of the isosurface -- tight enough that a libm ") TEXT("%d of %d samples sit within 1e-6 of the isosurface. The proximity is worth ")
TEXT("difference between MSVC and glibc could flip their SIGN, i.e. one voxel solid for ") TEXT("recording, but it is no longer a CRT/libm cross-platform risk because the production ")
TEXT("a Windows host and air for a Linux client. FMath::Sin/Cos are used throughout the ") TEXT("world path uses deterministic VoxelMath::DetSin/DetCos."),
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."),
NumNearTight, NumSamples)); NumNearTight, NumSamples));
} }
else else
{ {
AddInfo(TEXT("No sample sits within 1e-6 of the isosurface, so no sampled voxel is close ") AddInfo(TEXT("No sample sits within 1e-6 of the isosurface on this grid."));
TEXT("enough for a libm difference to flip its side. Evidence, not proof: it covers ")
TEXT("this grid, not every voxel of a world."));
} }
TestEqual(TEXT("no sample produced NaN"), NumNaN, 0); TestEqual(TEXT("no sample produced NaN"), NumNaN, 0);
@@ -0,0 +1,102 @@
// Pinned contract for VoxelMath::DetSin/DetCos/DetSinCos.
#include "Misc/AutomationTest.h"
#include "VoxelTypes.h"
#include <math.h>
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<float>(Index) / static_cast<float>(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;
}
@@ -963,8 +963,10 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
for (int32 RingSample = 0; RingSample < RingSampleCount; ++RingSample) for (int32 RingSample = 0; RingSample < RingSampleCount; ++RingSample)
{ {
const float Angle = 2.0f * PI * (float)RingSample / (float)RingSampleCount; const float Angle = 2.0f * PI * (float)RingSample / (float)RingSampleCount;
const float SampleX = Passage.LowerPoint.X + FMath::Cos(Angle) * RingRadius; float SinAngle = 0.0f, CosAngle = 0.0f;
const float SampleY = Passage.LowerPoint.Y + FMath::Sin(Angle) * RingRadius; 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( const float Density = World.Generator->GetDensityAt(
SampleX, SampleX,
SampleY, SampleY,
@@ -1115,7 +1117,7 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
LandingGradient = MaxGradient; LandingGradient = MaxGradient;
WorstLandingFloorGradient = FMath::Max(WorstLandingFloorGradient, MaxGradient); WorstLandingFloorGradient = FMath::Max(WorstLandingFloorGradient, MaxGradient);
const float NormalZ = 1.0f / FMath::Sqrt(1.0f + MaxGradient * 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)); FMath::DegreesToRadians(LandingWalkableAngleDegrees));
} }
@@ -990,7 +990,7 @@ bool FVoxelForgeScaleDiagnosisTest::RunTest(const FString& Parameters)
return false; return false;
} }
const float MinimumWalkableNormalZ = FMath::Cos(FMath::DegreesToRadians( const float MinimumWalkableNormalZ = VoxelMath::DetCos(FMath::DegreesToRadians(
CharacterFitSettings.PlayerWalkableFloorAngleDegrees)); CharacterFitSettings.PlayerWalkableFloorAngleDegrees));
const int32 SupportFootprintColumns = VF_CountSupportFootprintColumns( const int32 SupportFootprintColumns = VF_CountSupportFootprintColumns(
CharacterFitSettings.PlayerCapsuleRadiusVoxels); CharacterFitSettings.PlayerCapsuleRadiusVoxels);
@@ -2928,7 +2928,7 @@ namespace
const int32 MaxDownwardSearchCells = FMath::CeilToInt( const int32 MaxDownwardSearchCells = FMath::CeilToInt(
static_cast<float>(MaxStepHeightVoxelsReal)); static_cast<float>(MaxStepHeightVoxelsReal));
const float MaxStepHeightVoxels = static_cast<float>(MaxStepHeightVoxelsReal); const float MaxStepHeightVoxels = static_cast<float>(MaxStepHeightVoxelsReal);
const float MinimumWalkableNormalZ = FMath::Cos(FMath::DegreesToRadians( const float MinimumWalkableNormalZ = VoxelMath::DetCos(FMath::DegreesToRadians(
Settings.PlayerWalkableFloorAngleDegrees)); Settings.PlayerWalkableFloorAngleDegrees));
TArray<float> SupportHeights; TArray<float> SupportHeights;
@@ -3263,7 +3263,9 @@ namespace
const float DirectionAngle = VoxelHash::ToFloat01( const float DirectionAngle = VoxelHash::ToFloat01(
VoxelHash::Mix(Site.Hash ^ 0xCAFEBABEu)) * 2.0f * PI; VoxelHash::Mix(Site.Hash ^ 0xCAFEBABEu)) * 2.0f * PI;
const float StretchDistance = Site.RadiusXY * 0.7f; 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( RoomSDF = VoxelSDF::Capsule(
Position, Position,
Site.Center + Direction * StretchDistance, Site.Center + Direction * StretchDistance,
@@ -6823,7 +6825,7 @@ void VoxelCaveMorphology::BuildChunkCache(
{ {
const float T = static_cast<float>(ControlIndex) const float T = static_cast<float>(ControlIndex)
/ static_cast<float>(WanderSegments); / static_cast<float>(WanderSegments);
const float Envelope = FMath::Sin(T * PI); const float Envelope = VoxelMath::DetSin(T * PI);
FVector WorldControlPoint = FMath::Lerp(WorldEndA, WorldEndB, T); FVector WorldControlPoint = FMath::Lerp(WorldEndA, WorldEndB, T);
const float RadiusBase = FMath::Lerp(RadA, RadB, T); const float RadiusBase = FMath::Lerp(RadA, RadB, T);
float ControlRadius = RadiusBase; float ControlRadius = RadiusBase;
@@ -6840,7 +6842,7 @@ void VoxelCaveMorphology::BuildChunkCache(
// serpentine route without moving either mouth. // serpentine route without moving either mouth.
const float Side = RawSide * 0.65f + PreviousSide * 0.35f; const float Side = RawSide * 0.65f + PreviousSide * 0.35f;
const float WindWave = bNeedsWind const float WindWave = bNeedsWind
? FMath::Sin(static_cast<float>(WindWaves) * PI * T) ? VoxelMath::DetSin(static_cast<float>(WindWaves) * PI * T)
* WindAmplitude * Envelope * WindAmplitude * Envelope
* ((LedgeChoiceHash & 1u) != 0u ? 1.0f : -1.0f) * ((LedgeChoiceHash & 1u) != 0u ? 1.0f : -1.0f)
: 0.0f; : 0.0f;
@@ -7364,7 +7366,9 @@ void VoxelCaveMorphology::BuildChunkCache(
CR.ShapeType = 2; CR.ShapeType = 2;
const float DirAngle = VoxelHash::ToFloat01(VoxelHash::Mix(CR.Hash ^ 0xCAFEBABEu)) * 2.0f * PI; const float DirAngle = VoxelHash::ToFloat01(VoxelHash::Mix(CR.Hash ^ 0xCAFEBABEu)) * 2.0f * PI;
const float StretchDist = CR.RadiusXY * 0.7f; 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.ShapeA = CR.Center + Dir * StretchDist;
CR.ShapeB = CR.Center - Dir * StretchDist; CR.ShapeB = CR.Center - Dir * StretchDist;
CR.ShapeR = FMath::Min(CR.RadiusXY * 0.6f, CR.RadiusZ); 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 Angle = VoxelHash::ToFloat01(AH4) * PI;
const float HalfSpan = CR.RadiusXY const float HalfSpan = CR.RadiusXY
* (0.5f + VoxelHash::ToFloat01(VoxelHash::Mix(AH4)) * 0.35f); * (0.5f + VoxelHash::ToFloat01(VoxelHash::Mix(AH4)) * 0.35f);
const float CosA = FMath::Cos(Angle); float SinA = 0.0f, CosA = 0.0f;
const float SinA = FMath::Sin(Angle); VoxelMath::DetSinCos(SinA, CosA, Angle);
FCachedArch& Arch = CR.Arches[i]; FCachedArch& Arch = CR.Arches[i];
Arch.bActive = true; Arch.bActive = true;
Arch.EndpointA = FVector(ArcCX - CosA * HalfSpan, Arch.EndpointA = FVector(ArcCX - CosA * HalfSpan,
@@ -7517,8 +7521,7 @@ void VoxelCaveMorphology::BuildChunkCache(
Pinch.CenterX = PnX; Pinch.CenterX = PnX;
Pinch.CenterY = PnY; Pinch.CenterY = PnY;
Pinch.CenterZ = PnZ; Pinch.CenterZ = PnZ;
Pinch.CosAngle = FMath::Cos(PnAngle); VoxelMath::DetSinCos(Pinch.SinAngle, Pinch.CosAngle, PnAngle);
Pinch.SinAngle = FMath::Sin(PnAngle);
Pinch.MaxExtent = FMath::Max( Pinch.MaxExtent = FMath::Max(
FeatureParams.PinchLength, FeatureParams.PinchStrength) + 5.0f; FeatureParams.PinchLength, FeatureParams.PinchStrength) + 5.0f;
Pinch.HalfLength = FeatureParams.PinchLength * 0.5f; Pinch.HalfLength = FeatureParams.PinchLength * 0.5f;
@@ -501,9 +501,9 @@ void UVoxelContentManager::BuildCellSpawns(const UVoxelGenerator* Gen, const FTr
for (int32 e = 0; e < Entries.Num(); ++e) for (int32 e = 0; e < Entries.Num(); ++e)
{ {
CosMaxSlope[e] = (Entries[e].Profile.MaxSlopeAngle < 89.99f) 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) 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 // 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 Ang = VoxelHash::ToFloat01(IH) * 2.0f * PI;
const float RadVox = FMath::Lerp(Comp.RadiusMinVox, Comp.RadiusMaxVox, const float RadVox = FMath::Lerp(Comp.RadiusMinVox, Comp.RadiusMaxVox,
VoxelHash::ToFloat01(VoxelHash::Mix(IH ^ 0x77u))); VoxelHash::ToFloat01(VoxelHash::Mix(IH ^ 0x77u)));
const float SatVX = VX + FMath::Cos(Ang) * RadVox; // voxel, actor-local float SinAng = 0.0f, CosAng = 0.0f;
const float SatVY = VY + FMath::Sin(Ang) * RadVox; 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). // Optional per-satellite gating (relief/moisture/biome-border at ITS own XY).
if (Comp.Profile.Conditions.Num() > 0 && 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 SAng = VoxelHash::ToFloat01(JH) * 2.0f * PI;
const float SRadVox = FMath::Lerp(Sub.RadiusMinVox, Sub.RadiusMaxVox, const float SRadVox = FMath::Lerp(Sub.RadiusMinVox, Sub.RadiusMaxVox,
VoxelHash::ToFloat01(VoxelHash::Mix(JH ^ 0x77u))); VoxelHash::ToFloat01(VoxelHash::Mix(JH ^ 0x77u)));
const float SubVX = SatVX + FMath::Cos(SAng) * SRadVox; float SinSAng = 0.0f, CosSAng = 0.0f;
const float SubVY = SatVY + FMath::Sin(SAng) * SRadVox; VoxelMath::DetSinCos(SinSAng, CosSAng, SAng);
const float SubVX = SatVX + CosSAng * SRadVox;
const float SubVY = SatVY + SinSAng * SRadVox;
if (Sub.Profile.Conditions.Num() > 0 && if (Sub.Profile.Conditions.Num() > 0 &&
!Gen->EvaluateTerrainConditions(Sub.Profile.Conditions, SubVX, SubVY, Ctx.BiomeCtx)) continue; !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). // Surface-tilt gates (acos(|N.Z|); guarded so defaults cost no trig).
if (P.MaxSlopeAngle < 89.99f && 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 && 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); const FVector LocalPos(LocalX, LocalY, ZC * VOXEL_SIZE);
if (P.bRequireWaterRelative && Ctx.bHasWater) if (P.bRequireWaterRelative && Ctx.bHasWater)
@@ -7504,7 +7504,7 @@ namespace
if (!(DistFromSurface < LineRange)) { return; } if (!(DistFromSurface < LineRange)) { return; }
const float LinePhase = WorldZ * (2.0f * PI) / LP.LayerLineSpacing; 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 = FMath::Max(LineValue, 0.0f);
LineValue = LineValue * LineValue * LineValue; // affûtage cubique LineValue = LineValue * LineValue * LineValue; // affûtage cubique
@@ -7606,7 +7606,7 @@ namespace
if (!(DistFromSurface < RibRange)) { return; } if (!(DistFromSurface < RibRange)) { return; }
const float RibPhase = WorldZ * (2.0f * PI) / LP.RibbingSpacing + PI * 0.5f; 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 = FMath::Max(RibValue, 0.0f);
RibValue = RibValue * RibValue; // profil de bosse arrondi RibValue = RibValue * RibValue; // profil de bosse arrondi
+32 -1
View File
@@ -3,6 +3,14 @@
#include "VoxelForgeModule.h" #include "VoxelForgeModule.h"
#include "VoxelStackSampler.h" #include "VoxelStackSampler.h"
#include "VoxelGenerator.h"
#include "VoxelMarchingCubesMesher.h"
#include "Misc/CommandLine.h"
#include "Misc/Parse.h"
#if PLATFORM_WINDOWS
#include <math.h>
#endif
// This macro registers our module with Unreal // This macro registers our module with Unreal
// "VoxelForge" must match the module name in VoxelForge.Build.cs and .uplugin // "VoxelForge" must match the module name in VoxelForge.Build.cs and .uplugin
@@ -11,7 +19,30 @@ IMPLEMENT_MODULE(FVoxelForgeModule, VoxelForge)
void FVoxelForgeModule::StartupModule() void FVoxelForgeModule::StartupModule()
{ {
// Called when plugin loads // 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!")); UE_LOG(LogTemp, Log, TEXT("VoxelForge module started!"));
} }
+35 -19
View File
@@ -857,9 +857,18 @@ void VoxelGenLOD::SetThreadSampleStep(int32 Value)
SampleStep = Value; SampleStep = Value;
} }
void VoxelGenLOD::InitializeConsoleSwitches()
{
VF_ParseFastIsFiniteSwitch();
VF_ParseFusedEvaluatorSwitch();
VF_ParseWormBlockSkipSwitch();
VF_ParseTileCacheWindowSwitch();
VF_ParseSpatialIndexSwitch();
VF_ParseChunkRoutingCacheSwitch();
}
bool VoxelGenLOD::IsTileCacheWindowEnabled(bool bFusedPath) bool VoxelGenLOD::IsTileCacheWindowEnabled(bool bFusedPath)
{ {
VF_ParseTileCacheWindowSwitch();
if (GVoxelForgeTileCacheWindow == 0) if (GVoxelForgeTileCacheWindow == 0)
{ {
return false; return false;
@@ -900,7 +909,6 @@ void VoxelGenLOD::SetThreadTileContext(
bool VoxelGenLOD::ShouldUseSpatialIndex(bool bFusedPath) bool VoxelGenLOD::ShouldUseSpatialIndex(bool bFusedPath)
{ {
VF_ParseSpatialIndexSwitch();
if (GVoxelForgeSpatialIndex >= 0) if (GVoxelForgeSpatialIndex >= 0)
{ {
return 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, const float zc = FMath::Lerp(InnerBot + 8.0f, InnerTop - 8.0f,
VoxelHash::ToFloat01(VoxelHash::Mix(h ^ 0xB1u))); VoxelHash::ToFloat01(VoxelHash::Mix(h ^ 0xB1u)));
const float ang = VoxelHash::ToFloat01(VoxelHash::Mix(h ^ 0xB2u)) * PI; 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 bx = (nx + 0.5f) * Sp, by = (ny + 0.5f) * Sp;
const float half = Sp * 0.6f; const float half = Sp * 0.6f;
Bridges.Add({ FVector(bx - dxu * half, by - dyu * half, zc), 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); const uint32 h = VoxelHash::Cell(nx, ny, Seed ^ 0x52470001u);
if (VoxelHash::ToFloat01(h) > D.RidgeDensity) continue; if (VoxelHash::ToFloat01(h) > D.RidgeDensity) continue;
const float ang = VoxelHash::ToFloat01(VoxelHash::Mix(h ^ 0x9001u)) * PI; 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 bx = (nx + 0.5f) * Sp, by = (ny + 0.5f) * Sp;
const float half = Sp * 0.45f; const float half = Sp * 0.45f;
Ridges.Add({ bx - dxu * half, by - dyu * half, bx + dxu * half, by + dyu * half }); 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, InnerBot + 8.0f, InnerTop - 8.0f,
VoxelHash::ToFloat01(VoxelHash::Mix(H ^ 0xB1u))); VoxelHash::ToFloat01(VoxelHash::Mix(H ^ 0xB1u)));
const float Angle = VoxelHash::ToFloat01(VoxelHash::Mix(H ^ 0xB2u)) * PI; 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 BX = (NX + 0.5f) * Spacing, BY = (NY + 0.5f) * Spacing;
const float Half = Spacing * 0.6f; const float Half = Spacing * 0.6f;
const float HorizontalSq = VF_DistanceSquaredToXYSegment( const float HorizontalSq = VF_DistanceSquaredToXYSegment(
@@ -1453,7 +1467,9 @@ static bool VF_AnyRidgeCanTouchLattice(
continue; continue;
} }
const float Angle = VoxelHash::ToFloat01(VoxelHash::Mix(H ^ 0x9001u)) * PI; 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 BX = (NX + 0.5f) * Spacing, BY = (NY + 0.5f) * Spacing;
const float Half = Spacing * 0.45f; const float Half = Spacing * 0.45f;
if (VF_DistanceSquaredToXYSegment( if (VF_DistanceSquaredToXYSegment(
@@ -1554,7 +1570,9 @@ static bool VF_AnyBridgeCanTouchBox(
InnerBot + 8.0f, InnerTop - 8.0f, InnerBot + 8.0f, InnerTop - 8.0f,
VoxelHash::ToFloat01(VoxelHash::Mix(H ^ 0xB1u))); VoxelHash::ToFloat01(VoxelHash::Mix(H ^ 0xB1u)));
const float Angle = VoxelHash::ToFloat01(VoxelHash::Mix(H ^ 0xB2u)) * PI; 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 BX = (NX + 0.5f) * Spacing, BY = (NY + 0.5f) * Spacing;
const float Half = Spacing * 0.6f; const float Half = Spacing * 0.6f;
const FVector A(BX - DX * Half, BY - DY * Half, ZC); const FVector A(BX - DX * Half, BY - DY * Half, ZC);
@@ -1609,7 +1627,9 @@ static bool VF_AnyRidgeCanTouchBox(
continue; continue;
} }
const float Angle = VoxelHash::ToFloat01(VoxelHash::Mix(H ^ 0x9001u)) * PI; 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 BX = (NX + 0.5f) * Spacing, BY = (NY + 0.5f) * Spacing;
const float Half = Spacing * 0.45f; const float Half = Spacing * 0.45f;
const FVector A(BX - DX * Half, BY - DY * Half, InnerBot); const FVector A(BX - DX * Half, BY - DY * Half, InnerBot);
@@ -2776,7 +2796,6 @@ UVoxelGenerator::UVoxelGenerator()
void UVoxelGenerator::InitializeSettings(const UVoxelSettings* Settings) 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 // 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. // déréférencer l'asset de settings.
Seed = Settings ? Settings->GetEffectiveWorldSeed() : 0; Seed = Settings ? Settings->GetEffectiveWorldSeed() : 0;
@@ -2817,10 +2836,6 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
VoxelGenLOD::bTilePostReachBypass = PreviousBypass; VoxelGenLOD::bTilePostReachBypass = PreviousBypass;
} }
TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_GetDensityAt); TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_GetDensityAt);
VF_ParseFastIsFiniteSwitch();
VF_ParseFusedEvaluatorSwitch();
VF_ParseWormBlockSkipSwitch();
VF_ParseChunkRoutingCacheSwitch();
VoxelDensityProfile::FScopedTimer DensityProfileTimer( VoxelDensityProfile::FScopedTimer DensityProfileTimer(
VoxelDensityProfile::EBucket::GetDensityAt); VoxelDensityProfile::EBucket::GetDensityAt);
VoxelDensityProfile::FScopedTimer DensityPrologueTimer( VoxelDensityProfile::FScopedTimer DensityPrologueTimer(
@@ -3164,9 +3179,10 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
CP_Tunnel = StrateManager->GetGenerationParams(ChunkCoord); CP_Tunnel = StrateManager->GetGenerationParams(ChunkCoord);
// AUDIT §C2 — l'empreinte est calculée ICI, une fois par chunk, au seul endroit où // 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 / // 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 // FString / pointeur), donc une CRC mémoire peut donner un FAUX POSITIF par
// pire un octet de padding donne un faux MANQUE, c'est-à-dire une reconstruction de // collision CRC32 ; au pire un octet de padding donne aussi un faux MANQUE, c'est-
// cache. On se trompe du côté du CPU, jamais du côté d'une salle fausse. // à-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)); CP_TunnelFP = FCrc::MemCrc32(&CP_Tunnel, sizeof(CP_Tunnel));
break; break;
} }
@@ -4705,7 +4721,7 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
{ {
// Sine wave along Z: peaks at each line position // Sine wave along Z: peaks at each line position
float LinePhase = WorldZ * (2.0f * PI) / Params.LayerLineSpacing; 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. // 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 // 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 // Sine wave along Z, offset by half-period from layer lines
float RibPhase = WorldZ * (2.0f * PI) / Params.RibbingSpacing + PI * 0.5f; 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 // Half-wave rectify (only positive → ribs, not grooves) then smooth
RibValue = FMath::Max(RibValue, 0.0f); 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) if (Params.LayerLineDepth > 0.0f && Params.LayerLineSpacing > 0.0f)
{ {
const float Phase = Terrain * (2.0f * PI / Params.LayerLineSpacing); 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 // Beach: flatten terrain toward the water line within BeachWidth. (Water level is
@@ -229,7 +229,7 @@ namespace
if (P.LayerLineDepth <= 0.0f || P.LayerLineSpacing <= 0.0f) { return; } if (P.LayerLineDepth <= 0.0f || P.LayerLineSpacing <= 0.0f) { return; }
const float Phase = InOut.Height * (2.0f * PI / P.LayerLineSpacing); 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. // `sin` ∈ [-1,1] ⇒ borne exacte.
@@ -82,6 +82,12 @@ namespace
} }
} }
void UVoxelMarchingCubesMesher::InitializeConsoleSwitches()
{
VF_ParseOperatorBlockSwitch();
VF_ParseTilePostReachSwitches();
}
//============================================================================= //=============================================================================
// MAIN ALGORITHM // MAIN ALGORITHM
//============================================================================= //=============================================================================
@@ -94,8 +100,6 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
int64* OutDensitySampleCount) int64* OutDensitySampleCount)
{ {
TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_MesherGenerateMesh); TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_MesherGenerateMesh);
VF_ParseOperatorBlockSwitch();
VF_ParseTilePostReachSwitches();
FVoxelMeshData MeshData; FVoxelMeshData MeshData;
if (OutCaptureGrid) { OutCaptureGrid->Reset(); } if (OutCaptureGrid) { OutCaptureGrid->Reset(); }
if (OutDensitySampleCount) { *OutDensitySampleCount = 0; } if (OutDensitySampleCount) { *OutDensitySampleCount = 0; }
@@ -1827,8 +1827,10 @@ void UVoxelStrateManager::GeneratePassages()
// surface shaft, while this passage opens a separate room in the strate network. // surface shaft, while this passage opens a separate room in the strate network.
const float Angle = PassageRandom01(PassageSaltAngle) * (2.0f * PI); const float Angle = PassageRandom01(PassageSaltAngle) * (2.0f * PI);
const float Distance = PassageRandomRange(PlacementLo, PlacementHi, PassageSaltDistance); const float Distance = PassageRandomRange(PlacementLo, PlacementHi, PassageSaltDistance);
const float PX = FMath::Cos(Angle) * Distance; float SinAngle = 0.0f, CosAngle = 0.0f;
const float PY = FMath::Sin(Angle) * Distance; VoxelMath::DetSinCos(SinAngle, CosAngle, Angle);
const float PX = CosAngle * Distance;
const float PY = SinAngle * Distance;
++TotalPassages; ++TotalPassages;
@@ -1990,7 +1992,7 @@ void UVoxelStrateManager::GeneratePassages()
Passage.ControlRadii.Reserve(Segments + 1); Passage.ControlRadii.Reserve(Segments + 1);
// WIDTH profile: mouth radius at the ends, mid radius in the centre (taper/bulge). // 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. // Per-passage shape seeds.
const float WormFreq = PassageRandomRange(0.8f, 1.8f, PassageSaltWormFreq); // (vertical wobble only) 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; const float T = (float)s / (float)Segments;
float Z = FMath::Lerp(TopZ, BottomZ, T); 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; float OX = 0.0f, OY = 0.0f;
switch (Cfg.Style) switch (Cfg.Style)
@@ -2018,8 +2020,10 @@ void UVoxelStrateManager::GeneratePassages()
case EVoxelPassageStyle::Spiral: case EVoxelPassageStyle::Spiral:
{ {
const float Ang = PhaseA + T * Cfg.SpiralTurns * 2.0f * PI; const float Ang = PhaseA + T * Cfg.SpiralTurns * 2.0f * PI;
OX = FMath::Cos(Ang) * Cfg.SpiralRadius * Env; float SinAng = 0.0f, CosAng = 0.0f;
OY = FMath::Sin(Ang) * Cfg.SpiralRadius * Env; VoxelMath::DetSinCos(SinAng, CosAng, Ang);
OX = CosAng * Cfg.SpiralRadius * Env;
OY = SinAng * Cfg.SpiralRadius * Env;
break; break;
} }
@@ -2029,8 +2033,10 @@ void UVoxelStrateManager::GeneratePassages()
const int32 Steps = FMath::Clamp(Cfg.CascadeSteps, 1, 16); const int32 Steps = FMath::Clamp(Cfg.CascadeSteps, 1, 16);
const int32 Idx = FMath::Min((int32)(T * Steps), Steps - 1); const int32 Idx = FMath::Min((int32)(T * Steps), Steps - 1);
const float SA = PhaseA + (float)Idx * 2.39996f; // golden-angle spread const float SA = PhaseA + (float)Idx * 2.39996f; // golden-angle spread
OX = FMath::Cos(SA) * Cfg.CascadeLedge * Env; float SinSA = 0.0f, CosSA = 0.0f;
OY = FMath::Sin(SA) * Cfg.CascadeLedge * Env; VoxelMath::DetSinCos(SinSA, CosSA, SA);
OX = CosSA * Cfg.CascadeLedge * Env;
OY = SinSA * Cfg.CascadeLedge * Env;
break; break;
} }
@@ -2042,7 +2048,7 @@ void UVoxelStrateManager::GeneratePassages()
// 2D organic wander — it curls and meanders "here and there" rather than // 2D organic wander — it curls and meanders "here and there" rather than
// oscillating along one line (zig-zag) or orbiting the axis (spiral). // oscillating along one line (zig-zag) or orbiting the axis (spiral).
// Flat-top envelope keeps full motion along the length but anchors the mouths. // 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; OX = PassageFBM(T * BendFreq, NSeedX) * VOXEL_NOISE_SCALE * Cfg.Wander * WormEnv;
OY = PassageFBM(T * BendFreq, NSeedY + 53.0f) * VOXEL_NOISE_SCALE * Cfg.Wander * WormEnv; OY = PassageFBM(T * BendFreq, NSeedY + 53.0f) * VOXEL_NOISE_SCALE * Cfg.Wander * WormEnv;
break; break;
@@ -2084,7 +2090,9 @@ void UVoxelStrateManager::GeneratePassages()
FVector Direction(Delta.X, Delta.Y, 0.0f); FVector Direction(Delta.X, Delta.Y, 0.0f);
if (!Direction.Normalize()) 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()) if (!Direction.Normalize())
{ {
Direction = FVector(1.0f, 0.0f, 0.0f); 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. // This is deliberately a construction rule, not a seed-dependent observation.
const float SwitchbackAngle = const float SwitchbackAngle =
PassageRandom01(PassageSaltUpperDoor) * 2.0f * PI; PassageRandom01(PassageSaltUpperDoor) * 2.0f * PI;
FVector2D SwitchbackDirection( float SinSwitchback = 0.0f, CosSwitchback = 0.0f;
FMath::Cos(SwitchbackAngle), FMath::Sin(SwitchbackAngle)); VoxelMath::DetSinCos(SinSwitchback, CosSwitchback, SwitchbackAngle);
FVector2D SwitchbackDirection(CosSwitchback, SinSwitchback);
if (!SwitchbackDirection.Normalize()) if (!SwitchbackDirection.Normalize())
{ {
SwitchbackDirection = FVector2D(1.0f, 0.0f); SwitchbackDirection = FVector2D(1.0f, 0.0f);
@@ -1405,7 +1405,7 @@ namespace VoxelStrateMeasurePrivate
const int32 MaxDownwardSearchCells = static_cast<int32>( const int32 MaxDownwardSearchCells = static_cast<int32>(
FMath::CeilToDouble(MaxStepHeightVoxelsReal)); FMath::CeilToDouble(MaxStepHeightVoxelsReal));
const float MaxStepHeightVoxels = static_cast<float>(MaxStepHeightVoxelsReal); const float MaxStepHeightVoxels = static_cast<float>(MaxStepHeightVoxelsReal);
const float MinimumWalkableNormalZ = FMath::Cos(FMath::DegreesToRadians( const float MinimumWalkableNormalZ = VoxelMath::DetCos(FMath::DegreesToRadians(
Settings.PlayerWalkableFloorAngleDegrees)); Settings.PlayerWalkableFloorAngleDegrees));
TArray<FIntPoint> SupportOffsets; TArray<FIntPoint> SupportOffsets;
@@ -9,6 +9,7 @@
#pragma once #pragma once
#include "CoreMinimal.h" #include "CoreMinimal.h"
#include "UObject/Object.h"
#include "VoxelTypes.h" #include "VoxelTypes.h"
#include "VoxelStrateTypes.h" #include "VoxelStrateTypes.h"
#include "VoxelBiomeTypes.h" #include "VoxelBiomeTypes.h"
@@ -73,6 +74,8 @@ namespace VoxelGenLOD
VOXELFORGE_API void SetThreadOctaveBias(int32 Value); VOXELFORGE_API void SetThreadOctaveBias(int32 Value);
VOXELFORGE_API int32 GetThreadSampleStep(); VOXELFORGE_API int32 GetThreadSampleStep();
VOXELFORGE_API void SetThreadSampleStep(int32 Value); 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 IsTileCacheWindowEnabled(bool bFusedPath = true);
VOXELFORGE_API bool GetThreadTileCacheWindow( VOXELFORGE_API bool GetThreadTileCacheWindow(
FIntVector& OutOriginVoxels, int32& OutStep, int32& OutCellsPerAxis); FIntVector& OutOriginVoxels, int32& OutStep, int32& OutCellsPerAxis);
@@ -44,6 +44,10 @@ class VOXELFORGE_API UVoxelMarchingCubesMesher : public UObject
GENERATED_BODY() GENERATED_BODY()
public: 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). * Génère le mesh d'une TUILE de clipmap (voir FVoxelTileKey).
* *
+142
View File
@@ -51,6 +51,148 @@ namespace VoxelMath
{ {
return GFastIsFinite != 0 ? IsFiniteFast(Value) : FMath::IsFinite(Value); 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<double>(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<int32>(ExponentBits) - 127 - 23;
double ReducedInteger = 0.0;
for (int32 Bit = 23; Bit >= 0; --Bit)
{
ReducedInteger *= 2.0;
ReducedInteger += static_cast<double>((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<int64>(PeriodsReal + 0.5);
}
else
{
Periods = static_cast<int64>(PeriodsReal - 0.5);
}
Reduced = static_cast<double>(Value)
- static_cast<double>(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<int32>(QuadrantReal + 0.5);
}
else
{
Quadrant = static_cast<int32>(QuadrantReal - 0.5);
}
Reduced -= static_cast<double>(Quadrant) * HalfPi;
const float Y = static_cast<float>(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;
}
} }
//============================================================================= //=============================================================================