From 1d824aaab2067ee5ec871d6087c562307d6bc5ed Mon Sep 17 00:00:00 2001 From: Fr0zka Date: Tue, 15 Sep 2026 08:41:59 +0200 Subject: [PATCH] perf(noise): batch the three cave-warp channels through the SIMD evaluator -- exact, ~4% faster The remove-one ablation, re-confirmed on the current build, puts the cave warp at 11.0% static / 11.8% moving of worker time: three independent Perlin3D calls per sample. VoxelNoise::Perlin3D_x3 evaluates the three warp channels together through the existing SIMD evaluator (Codex; reviewed by me). It is wired into the generator, op-stack, classifier and room/player-fit unwarp paths. Exact: the field is byte-identical. 0/841 tiles changed, 0 triangle delta, 0 voxels of maximum surface displacement; canonical export 07C14005 / b3e5f4c3..9b377; capability 20,830 / 10,909; strate-crossing and sealed-proof tests pass. The seam test (VoxelForgeMazeSeamTest, extended) has 48 neighbouring-tile and 24 cross-LOD comparisons, 0 mismatches. Before/after renders from a camera aimed at the real arrival tunnel are byte-identical. Cross-machine determinism: Perlin3D_x3 adds no new intrinsics, no AVX, no FMA and no runtime CPU dispatch. It reuses the evaluator behind the compile-time gate `#if PLATFORM_ENABLE_VECTORINTRINSICS && PLATFORM_CPU_X86_FAMILY`, so every x64 machine runs the same instruction path; its output matches the scalar path bit for bit. Final DEFAULT build vs the committed-tip DLL (two interleaved runs each): static worker 30.83 -> 29.47 s (-4.4%) LOD0 generation p50/p95 56.3/99.3 -> 53.3/95.3 ms moving worker 67.28 -> 64.56 s (-4.0%) LOD0 generation p50/p95 55.9/99.0 -> 53.4/96.4 ms Ledger: every launch passed; two harness false negatives (the seam test on the game-only validator, and a trace ended by intentional forced termination) were rerun. No crash folders. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01TaFdXhTdYMkW1xrV6ApT6Y --- .../Private/Tests/VoxelForgeMazeSeamTest.cpp | 169 ++++++++++++++++++ .../Private/VoxelCaveMorphology.cpp | 18 +- .../Private/VoxelDensityOpStack.cpp | 31 ++-- Source/VoxelForge/Private/VoxelGenerator.cpp | 21 ++- Source/VoxelForge/Public/VoxelNoise.h | 27 +++ 5 files changed, 235 insertions(+), 31 deletions(-) diff --git a/Source/VoxelForge/Private/Tests/VoxelForgeMazeSeamTest.cpp b/Source/VoxelForge/Private/Tests/VoxelForgeMazeSeamTest.cpp index a9789c5..86e8819 100644 --- a/Source/VoxelForge/Private/Tests/VoxelForgeMazeSeamTest.cpp +++ b/Source/VoxelForge/Private/Tests/VoxelForgeMazeSeamTest.cpp @@ -162,4 +162,173 @@ bool FVoxelForgeMazeSeamTest::RunTest(const FString& Parameters) return true; } +// The cave-warp optimization is deliberately tile/LOD agnostic: its three channel inputs are +// world coordinates only. Exercise the production density path nevertheless, because a cache +// window or an LOD TLS leak could still make an otherwise pure warp look like a seam. +IMPLEMENT_SIMPLE_AUTOMATION_TEST( + FVoxelForgeCaveWarpTileLodSeamTest, + "VoxelForge.Generation.CaveWarpTileLodSeam", + EAutomationTestFlags_ApplicationContextMask | EAutomationTestFlags::EngineFilter) + +bool FVoxelForgeCaveWarpTileLodSeamTest::RunTest(const FString& Parameters) +{ + using namespace VoxelForgeTest; + + FTestWorld World; + World.Build(1337, 2, true, 4); + if (!World.IsValid()) + { + AddError(World.WhyInvalid()); + return false; + } + + int32 TunnelTopZ = 0, TunnelBottomZ = 0; + if (!World.GetSlotVoxelZRange(FTestWorld::SlotTunnelNetwork, TunnelTopZ, TunnelBottomZ)) + { + AddError(TEXT("The seam fixture has no TunnelNetwork slot.")); + return false; + } + + const float TunnelMidZ = 0.5f * (static_cast(TunnelTopZ) + + static_cast(TunnelBottomZ)); + const int32 ProbeZMid = FMath::Clamp( + FMath::RoundToInt(TunnelMidZ), TunnelBottomZ + 1, TunnelTopZ - 1); + + const int32 PreviousStep = VoxelGenLOD::GetThreadSampleStep(); + FIntVector PreviousOrigin = FIntVector::ZeroValue; + int32 PreviousCells = 0; + int32 PreviousContextStep = PreviousStep; + VoxelGenLOD::GetThreadTileCacheWindow( + PreviousOrigin, PreviousContextStep, PreviousCells); + + int32 PairMismatches = 0; + int32 LodMismatches = 0; + int32 NumPairSamples = 0; + FString FirstMismatch; + auto Compare = [&](const TCHAR* Label, const FVector& Point, float A, float B, + int32& Counter) + { + if (!BitEqual(A, B)) + { + ++Counter; + if (FirstMismatch.IsEmpty()) + { + FirstMismatch = FString::Printf( + TEXT("%s at (%.0f, %.0f, %.0f): %.9g [0x%08X] vs %.9g [0x%08X]"), + Label, Point.X, Point.Y, Point.Z, + A, *reinterpret_cast(&A), + B, *reinterpret_cast(&B)); + } + } + }; + + auto Sample = [&](int32 Step, const FIntVector& TileOrigin, const FVector& Point) + { + VoxelGenLOD::SetThreadSampleStep(Step); + VoxelGenLOD::SetThreadTileContext(TileOrigin, CHUNK_SIZE); + return World.Generator->GetDensityAt(Point.X, Point.Y, Point.Z); + }; + + // Common points are deliberately on the shared face of adjacent production tiles. The + // LOD1 pair is 64 voxels wide (Step=2, Cells=32); its boundary is also a LOD0 tile boundary. + const int32 CommonOffsets[] = { 4, 12, 20, 28 }; + const int32 ZOffsets[] = { -8, 0, 8 }; + const int32 LodSteps[] = { 1, 2 }; + for (const int32 Step : LodSteps) + { + const int32 Extent = CHUNK_SIZE * Step; + const int32 ZOrigin = FMath::FloorToInt( + TunnelMidZ / static_cast(Extent)) * Extent; + const int32 L0Boundary = Extent == CHUNK_SIZE ? CHUNK_SIZE : Extent; + const FIntVector XLeftOrigin( + L0Boundary - Extent, 0, ZOrigin); + const FIntVector XRightOrigin( + L0Boundary, 0, ZOrigin); + const FIntVector YLowerOrigin( + 0, L0Boundary - Extent, ZOrigin); + const FIntVector YUpperOrigin( + 0, L0Boundary, ZOrigin); + + for (const int32 Offset : CommonOffsets) + { + for (const int32 ZOffset : ZOffsets) + { + const float ProbeZ = FMath::Clamp( + static_cast(ProbeZMid + ZOffset), + static_cast(TunnelBottomZ + 1), + static_cast(TunnelTopZ - 1)); + + const FVector XPoint(static_cast(L0Boundary), + static_cast(Offset), ProbeZ); + const float XLeft = Sample(Step, XLeftOrigin, XPoint); + const float XRight = Sample(Step, XRightOrigin, XPoint); + Compare(Step == 1 ? TEXT("LOD0 X tile pair") : TEXT("LOD1 X tile pair"), + XPoint, XLeft, XRight, PairMismatches); + + const FVector YPoint(static_cast(Offset), + static_cast(L0Boundary), ProbeZ); + const float YLower = Sample(Step, YLowerOrigin, YPoint); + const float YUpper = Sample(Step, YUpperOrigin, YPoint); + Compare(Step == 1 ? TEXT("LOD0 Y tile pair") : TEXT("LOD1 Y tile pair"), + YPoint, YLower, YUpper, PairMismatches); + NumPairSamples += 2; + } + } + } + + // Compare the same world points through the two LOD contexts. The default LODOctaveDrop is + // zero, so LOD changes sampling density, not the value of a shared world sample. + const int32 CrossLodBoundary = CHUNK_SIZE * 2; + const int32 L0ZOrigin = FMath::FloorToInt( + TunnelMidZ / static_cast(CHUNK_SIZE)) * CHUNK_SIZE; + const int32 L1ZOrigin = FMath::FloorToInt( + TunnelMidZ / static_cast(CrossLodBoundary)) * CrossLodBoundary; + for (const int32 Offset : CommonOffsets) + { + for (const int32 ZOffset : ZOffsets) + { + const float ProbeZ = FMath::Clamp( + static_cast(ProbeZMid + ZOffset), + static_cast(TunnelBottomZ + 1), + static_cast(TunnelTopZ - 1)); + + const FVector XPoint(static_cast(CrossLodBoundary), + static_cast(Offset), ProbeZ); + const float L0X = Sample( + 1, FIntVector(CHUNK_SIZE, 0, L0ZOrigin), XPoint); + const float L1X = Sample( + 2, FIntVector(0, 0, L1ZOrigin), XPoint); + Compare(TEXT("same X world point across LOD0/LOD1"), + XPoint, L0X, L1X, LodMismatches); + + const FVector YPoint(static_cast(Offset), + static_cast(CrossLodBoundary), ProbeZ); + const float L0Y = Sample( + 1, FIntVector(0, CHUNK_SIZE, L0ZOrigin), YPoint); + const float L1Y = Sample( + 2, FIntVector(0, 0, L1ZOrigin), YPoint); + Compare(TEXT("same Y world point across LOD0/LOD1"), + YPoint, L0Y, L1Y, LodMismatches); + } + } + + VoxelGenLOD::SetThreadSampleStep(PreviousContextStep); + VoxelGenLOD::SetThreadTileContext(PreviousOrigin, PreviousCells); + + TestTrue(TEXT("sampled shared boundary points"), NumPairSamples > 0); + TestEqual(TEXT("LOD0 and LOD1 neighbouring tile boundaries are bit-identical"), + PairMismatches, 0); + TestEqual(TEXT("same world boundary points are bit-identical across LOD0 and LOD1"), + LodMismatches, 0); + if (!FirstMismatch.IsEmpty()) + { + AddError(FirstMismatch); + } + AddInfo(FString::Printf( + TEXT("Cave warp tile/LOD seam proof: %d same-LOD pair samples, pair mismatches=%d, " + "cross-LOD mismatches=%d; shared points sampled from X and Y boundaries."), + NumPairSamples, PairMismatches, LodMismatches)); + return true; +} + #endif // WITH_DEV_AUTOMATION_TESTS diff --git a/Source/VoxelForge/Private/VoxelCaveMorphology.cpp b/Source/VoxelForge/Private/VoxelCaveMorphology.cpp index d05f149..66bab2e 100644 --- a/Source/VoxelForge/Private/VoxelCaveMorphology.cpp +++ b/Source/VoxelForge/Private/VoxelCaveMorphology.cpp @@ -3110,21 +3110,21 @@ namespace const float Frequency = Params.CaveWarpFrequency; const float Strength = Params.CaveWarpStrength; - Warped.X += VoxelNoise::Perlin3D( + float WarpX, WarpY, WarpZ; + VoxelNoise::Perlin3D_x3( WorldPoint.X * Frequency + VoxelHash::SeedOffset(Seed, 0.37f), WorldPoint.Y * Frequency + 1.3f, - EffectiveZ * Frequency + 5.7f) - * VOXEL_NOISE_SCALE * Strength; - Warped.Y += VoxelNoise::Perlin3D( + EffectiveZ * Frequency + 5.7f, WorldPoint.X * Frequency + 7.1f, WorldPoint.Y * Frequency + VoxelHash::SeedOffset(Seed, 0.59f), - EffectiveZ * Frequency + 2.3f) - * VOXEL_NOISE_SCALE * Strength; - Warped.Z += VoxelNoise::Perlin3D( + EffectiveZ * Frequency + 2.3f, WorldPoint.X * Frequency + 11.3f, WorldPoint.Y * Frequency + 9.7f, - EffectiveZ * Frequency + VoxelHash::SeedOffset(Seed, 0.41f)) - * VOXEL_NOISE_SCALE * Strength; + EffectiveZ * Frequency + VoxelHash::SeedOffset(Seed, 0.41f), + WarpX, WarpY, WarpZ); + Warped.X += WarpX * VOXEL_NOISE_SCALE * Strength; + Warped.Y += WarpY * VOXEL_NOISE_SCALE * Strength; + Warped.Z += WarpZ * VOXEL_NOISE_SCALE * Strength; return Warped; } diff --git a/Source/VoxelForge/Private/VoxelDensityOpStack.cpp b/Source/VoxelForge/Private/VoxelDensityOpStack.cpp index 0a8b05a..c547905 100644 --- a/Source/VoxelForge/Private/VoxelDensityOpStack.cpp +++ b/Source/VoxelForge/Private/VoxelDensityOpStack.cpp @@ -4121,18 +4121,21 @@ namespace { const float WF = P.CaveWarpFrequency; const float WS = P.CaveWarpStrength; - WarpedX += VoxelNoise::Perlin3D(FVector3f( + float WarpX, WarpY, WarpZ; + VoxelNoise::Perlin3D_x3( WorldX * WF + VoxelHash::SeedOffset(SeedU, 0.37f), WorldY * WF + 1.3f, - EffectiveZ * WF + 5.7f)) * VOXEL_NOISE_SCALE * WS; - WarpedY += VoxelNoise::Perlin3D(FVector3f( + EffectiveZ * WF + 5.7f, WorldX * WF + 7.1f, WorldY * WF + VoxelHash::SeedOffset(SeedU, 0.59f), - EffectiveZ * WF + 2.3f)) * VOXEL_NOISE_SCALE * WS; - WarpedZ += VoxelNoise::Perlin3D(FVector3f( + EffectiveZ * WF + 2.3f, WorldX * WF + 11.3f, WorldY * WF + 9.7f, - EffectiveZ * WF + VoxelHash::SeedOffset(SeedU, 0.41f))) * VOXEL_NOISE_SCALE * WS; + EffectiveZ * WF + VoxelHash::SeedOffset(SeedU, 0.41f), + WarpX, WarpY, WarpZ); + WarpedX += WarpX * VOXEL_NOISE_SCALE * WS; + WarpedY += WarpY * VOXEL_NOISE_SCALE * WS; + WarpedZ += WarpZ * VOXEL_NOISE_SCALE * WS; } S.LastWarpedPosition = FVector(WarpedX, WarpedY, WarpedZ); @@ -5475,19 +5478,21 @@ namespace if (!VoxelDensityAblation::IsCaveWarpOff() && P.CaveWarpStrength > 0.0f) { const float Frequency = P.CaveWarpFrequency; - Query.X += VoxelNoise::Perlin3D( + float WarpX, WarpY, WarpZ; + VoxelNoise::Perlin3D_x3( WorldX * Frequency + VoxelHash::SeedOffset(SeedU, 0.37f), WorldY * Frequency + 1.3f, - EffectiveZ * Frequency + 5.7f) * WarpAmplitude; - Query.Y += VoxelNoise::Perlin3D( + EffectiveZ * Frequency + 5.7f, WorldX * Frequency + 7.1f, WorldY * Frequency + VoxelHash::SeedOffset(SeedU, 0.59f), - EffectiveZ * Frequency + 2.3f) * WarpAmplitude; - Query.Z += VoxelNoise::Perlin3D( + EffectiveZ * Frequency + 2.3f, WorldX * Frequency + 11.3f, WorldY * Frequency + 9.7f, - EffectiveZ * Frequency + VoxelHash::SeedOffset(SeedU, 0.41f)) - * WarpAmplitude; + EffectiveZ * Frequency + VoxelHash::SeedOffset(SeedU, 0.41f), + WarpX, WarpY, WarpZ); + Query.X += WarpX * WarpAmplitude; + Query.Y += WarpY * WarpAmplitude; + Query.Z += WarpZ * WarpAmplitude; } return Query; }; diff --git a/Source/VoxelForge/Private/VoxelGenerator.cpp b/Source/VoxelForge/Private/VoxelGenerator.cpp index 800f2be..4017227 100644 --- a/Source/VoxelForge/Private/VoxelGenerator.cpp +++ b/Source/VoxelForge/Private/VoxelGenerator.cpp @@ -3841,21 +3841,24 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo const float WF = Params.CaveWarpFrequency; const float WS = Params.CaveWarpStrength; - // Three independent Perlin fields offset by irrational-ish numbers - // so the X/Y/Z warp channels don't correlate with each other. - // Single octave to keep per-voxel cost low (3 Perlin calls total). - WarpedX += VoxelNoise::Perlin3D(FVector3f( + // Three independent Perlin fields offset by irrational-ish numbers keep the X/Y/Z warp + // channels decorrelated. Perlin3D_x3 evaluates these same three scalar channels in one + // SIMD batch on x86; its non-SIMD fallback is the original three-call expression. + float WarpX, WarpY, WarpZ; + VoxelNoise::Perlin3D_x3( WorldX * WF + VoxelHash::SeedOffset(SeedU, 0.37f), WorldY * WF + 1.3f, - EffectiveZ * WF + 5.7f)) * VOXEL_NOISE_SCALE * WS; - WarpedY += VoxelNoise::Perlin3D(FVector3f( + EffectiveZ * WF + 5.7f, WorldX * WF + 7.1f, WorldY * WF + VoxelHash::SeedOffset(SeedU, 0.59f), - EffectiveZ * WF + 2.3f)) * VOXEL_NOISE_SCALE * WS; - WarpedZ += VoxelNoise::Perlin3D(FVector3f( + EffectiveZ * WF + 2.3f, WorldX * WF + 11.3f, WorldY * WF + 9.7f, - EffectiveZ * WF + VoxelHash::SeedOffset(SeedU, 0.41f))) * VOXEL_NOISE_SCALE * WS; + EffectiveZ * WF + VoxelHash::SeedOffset(SeedU, 0.41f), + WarpX, WarpY, WarpZ); + WarpedX += WarpX * VOXEL_NOISE_SCALE * WS; + WarpedY += WarpY * VOXEL_NOISE_SCALE * WS; + WarpedZ += WarpZ * VOXEL_NOISE_SCALE * WS; } //========================================================================= diff --git a/Source/VoxelForge/Public/VoxelNoise.h b/Source/VoxelForge/Public/VoxelNoise.h index ecaaa3a..1ef9a08 100644 --- a/Source/VoxelForge/Public/VoxelNoise.h +++ b/Source/VoxelForge/Public/VoxelNoise.h @@ -357,6 +357,33 @@ FORCEINLINE void Perlin3D_x4(const float* Xs, const float* Ys, const float* Zs, } #endif +// Three independent channels are the hot cave-warp shape. On x86, evaluate them as three lanes +// of the existing four-wide implementation; the unused fourth lane is deliberate. The scalar +// fallback keeps the exact three-call behavior on platforms without the SIMD path. This helper is +// the owner of the cave-warp channel evaluation so generator, op-stack, and room-fit callers cannot +// drift into different noise formulas or channel offsets. +FORCEINLINE void Perlin3D_x3( + float X0, float Y0, float Z0, + float X1, float Y1, float Z1, + float X2, float Y2, float Z2, + float& Out0, float& Out1, float& Out2) +{ +#if VF_NOISE_USE_SIMD + const float Xs[4] = { X0, X1, X2, X0 }; + const float Ys[4] = { Y0, Y1, Y2, Y0 }; + const float Zs[4] = { Z0, Z1, Z2, Z0 }; + float Out[4]; + Perlin3D_x4(Xs, Ys, Zs, Out); + Out0 = Out[0]; + Out1 = Out[1]; + Out2 = Out[2]; +#else + Out0 = Perlin3D(X0, Y0, Z0); + Out1 = Perlin3D(X1, Y1, Z1); + Out2 = Perlin3D(X2, Y2, Z2); +#endif +} + //============================================================================= // fBm / Ridged — octaves evaluated 4 at a time through Perlin3D_x4. // Accumulation stays scalar in octave order, so the result is independent of