standalone: canonical AVX2 noise + factored lattice hashes -- whole view 1.40 -> 1.09 s, world unchanged

- Octave noise evaluated in AVX2 batches (one canonical path), fBM and ridged share samples, lattice
  hash products factored; the outputs are preserved: W1/W8 and pre-change hashes equal across 44
  fields, six fixed captures pixel-identical, remote views differ by at most one pixel.
- Routes 5642/5642, shape and far-survival audits pass, bad-shell control still fails.
Verified by me, interleaved head to head (8 workers, cold, BelowNormal 0xFF00): 1.092 / 1.098 s
vs 1.408 / 1.396 s for round 7.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
This commit is contained in:
Fr0zkaandClaude Opus 5.5 committed 2026-10-05 02:58:46 +02:00
1 parent d4fa89204d
commit 16612acba4
5 files changed
+459 -148

No files matched your search

+71 -22
View File
@@ -2500,6 +2500,10 @@ private:
const int32 Y=Min[1]+static_cast<int32>(Line/Size[0]);
FDetailNoiseLineContext NoiseLine(static_cast<float>(X),static_cast<float>(Y),
static_cast<uint32>(Owner->Seed));
const int32 QueryBinX=FloorDiv(X,ActiveSpatialBinSize);
const int32 QueryBinY=FloorDiv(Y,ActiveSpatialBinSize);
int32 CachedQueryBinZ=std::numeric_limits<int32>::min();
const FBinItems* CachedQueryBinItems=nullptr;
FDetailWorkerProfile& Profile=WorkerProfile[static_cast<size_t>(Worker)];
if(bProfile)++Profile.Lines;
uint64 LocalCandidates=0,LocalProxies=0,LocalFlips=0;
@@ -2514,12 +2518,20 @@ private:
const size_t Zi=W*64+Bit;if(Zi>=static_cast<size_t>(Size[2]))continue;
const int32 Z=Min[2]+static_cast<int32>(Zi);
++LocalCandidates;
const int32 QueryBinZ=FloorDiv(Z,ActiveSpatialBinSize);
if(QueryBinZ!=CachedQueryBinZ)
{
const auto Bin=SpatialBins.find({QueryBinX,QueryBinY,QueryBinZ});
CachedQueryBinItems=Bin==SpatialBins.end()?nullptr:&Bin->second;
CachedQueryBinZ=QueryBinZ;
}
const bool bTimeDistance=bProfile&&((Profile.Candidates++&255u)==0);
const auto DistanceStart=bTimeDistance?Clock::now():Clock::time_point{};
ESurfaceFamily SurfaceFamily=ESurfaceFamily::Strata;float SurfaceSDF=100000.0f;
bool bFloorSurface=false,bCriticalAir=false;float CaveInfluence=0.0f,FloorInfluence=0.0f;
const float SDF=EffectiveRockDistance(X,Y,Z,&SurfaceFamily,&SurfaceSDF,
&bFloorSurface,&CaveInfluence,&FloorInfluence,&bCriticalAir);
&bFloorSurface,&CaveInfluence,&FloorInfluence,&bCriticalAir,
CachedQueryBinItems,true);
if(bTimeDistance)
{
Profile.DistanceSampleNanoseconds+=static_cast<uint64>(
@@ -2768,17 +2780,24 @@ private:
bool* OutFloorSurface=nullptr,
float* OutCaveInfluence=nullptr,
float* OutFloorInfluence=nullptr,
bool* OutCriticalAir=nullptr) const
bool* OutCriticalAir=nullptr,
const FBinItems* KnownBinItems=nullptr,
bool bKnownBin=false) const
{
const float Base=StrataDistance(Z);
float Cave=100000.0f;
float Overlay=-100000.0f;
float CaveFloorInfluence=0.0f;
bool bCriticalAir=false;
const auto Bin=SpatialBins.find(PointBin(X,Y,Z));
if(Bin!=SpatialBins.end())
const FBinItems* BinItems=KnownBinItems;
if(!bKnownBin)
{
for(uint32 Id:Bin->second)
const auto Bin=SpatialBins.find(PointBin(X,Y,Z));
if(Bin!=SpatialBins.end())BinItems=&Bin->second;
}
if(BinItems)
{
for(uint32 Id:*BinItems)
{
const FPrimitive& P=Primitives[Id];
if(P.bCarve)
@@ -2839,10 +2858,26 @@ private:
const float Base=StrataDistance(Z);
float Cave=100000.0f,Overlay=-100000.0f;
bool bCriticalAir=false;
const auto Bin=SpatialBins.find(PointBin(X,Y,Z));
if(Bin!=SpatialBins.end())
struct FCoarseBinCache
{
for(uint32 Id:Bin->second)
const FStandaloneShapeWorld* Owner=nullptr;
FBinKey Key{};
const FBinItems* Items=nullptr;
bool Valid=false;
};
static thread_local FCoarseBinCache LastQuery;
const FBinKey QueryKey=PointBin(X,Y,Z);
if(!LastQuery.Valid||LastQuery.Owner!=this||!(LastQuery.Key==QueryKey))
{
const auto Bin=SpatialBins.find(QueryKey);
LastQuery.Owner=this;
LastQuery.Key=QueryKey;
LastQuery.Items=Bin==SpatialBins.end()?nullptr:&Bin->second;
LastQuery.Valid=true;
}
if(LastQuery.Items)
{
for(uint32 Id:*LastQuery.Items)
{
const FPrimitive& P=Primitives[Id];
if(P.bCarve)
@@ -2895,37 +2930,51 @@ private:
if(Params.DomainWarpStrength>0.0f)
{
const float WF=Params.DomainWarpFrequency,WS=Params.DomainWarpStrength;
const float WX=VoxelNoise::Perlin3D(FVector3f(
float WX=0.0f,WY=0.0f,WZ=0.0f;
VoxelNoise::Perlin3D_x3(
static_cast<float>(X)*WF+VoxelHash::SeedOffset(Seed,5.2f),
static_cast<float>(Y)*WF+VoxelHash::SeedOffset(Seed,1.3f),
EffectiveZ*WF+VoxelHash::SeedOffset(Seed,9.7f)))*VOXEL_NOISE_SCALE*WS;
const float WY=VoxelNoise::Perlin3D(FVector3f(
EffectiveZ*WF+VoxelHash::SeedOffset(Seed,9.7f),
static_cast<float>(X)*WF+100.0f+VoxelHash::SeedOffset(Seed,7.7f),
static_cast<float>(Y)*WF+200.0f+VoxelHash::SeedOffset(Seed,3.1f),
EffectiveZ*WF+300.0f))*VOXEL_NOISE_SCALE*WS;
const float WZ=VoxelNoise::Perlin3D(FVector3f(
EffectiveZ*WF+300.0f,
static_cast<float>(X)*WF+400.0f,
static_cast<float>(Y)*WF+500.0f+VoxelHash::SeedOffset(Seed,11.9f),
EffectiveZ*WF+600.0f+VoxelHash::SeedOffset(Seed,13.3f)))*VOXEL_NOISE_SCALE*WS;
EffectiveZ*WF+600.0f+VoxelHash::SeedOffset(Seed,13.3f),WX,WY,WZ);
WX*=VOXEL_NOISE_SCALE*WS;WY*=VOXEL_NOISE_SCALE*WS;WZ*=VOXEL_NOISE_SCALE*WS;
Main+=FVector3f(WX,WY,WZ);Fine+=FVector3f(WX,WY,WZ);
}
float Rough=0.0f,FineValue=0.0f;
switch(Params.RoughnessNoiseType)
{
case EVoxelNoiseType::Ridged:
Rough=VoxelNoise::Ridged(Main.X,Main.Y,Main.Z,3);
FineValue=VoxelNoise::Ridged(Fine.X,Fine.Y,Fine.Z,2);break;
{
float MainFBM=0.0f,MainRidged=0.0f,FineFBM=0.0f,FineRidged=0.0f;
VoxelNoise::FractalPair<false,true>(Main.X,Main.Y,Main.Z,3,
Fine.X,Fine.Y,Fine.Z,2,MainFBM,MainRidged,FineFBM,FineRidged);
Rough=MainRidged;FineValue=FineRidged;
break;
}
case EVoxelNoiseType::Mixed:
Rough=0.5f*(VoxelNoise::FBM(Main.X,Main.Y,Main.Z,3)
+VoxelNoise::Ridged(Main.X,Main.Y,Main.Z,3));
FineValue=0.5f*(VoxelNoise::FBM(Fine.X,Fine.Y,Fine.Z,2)
+VoxelNoise::Ridged(Fine.X,Fine.Y,Fine.Z,2));break;
{
float MainFBM=0.0f,MainRidged=0.0f,FineFBM=0.0f,FineRidged=0.0f;
VoxelNoise::FractalPair<true,true>(Main.X,Main.Y,Main.Z,3,
Fine.X,Fine.Y,Fine.Z,2,MainFBM,MainRidged,FineFBM,FineRidged);
Rough=MainFBM*0.5f+MainRidged*0.5f;
FineValue=FineFBM*0.5f+FineRidged*0.5f;
break;
}
case EVoxelNoiseType::Cellular:
Rough=VoxelNoise::Cellular3D(Main);FineValue=VoxelNoise::Cellular3D(Fine);break;
case EVoxelNoiseType::FBM:
default:
Rough=VoxelNoise::FBM(Main.X,Main.Y,Main.Z,3);
FineValue=VoxelNoise::FBM(Fine.X,Fine.Y,Fine.Z,2);break;
{
float MainFBM=0.0f,MainRidged=0.0f,FineFBM=0.0f,FineRidged=0.0f;
VoxelNoise::FractalPair<true,false>(Main.X,Main.Y,Main.Z,3,
Fine.X,Fine.Y,Fine.Z,2,MainFBM,MainRidged,FineFBM,FineRidged);
Rough=MainFBM;FineValue=FineFBM;
break;
}
}
return (Rough+FineValue*0.4f)*VOXEL_NOISE_SCALE*Params.SurfaceRoughness;
}
@@ -6830,24 +6830,21 @@ namespace
{
const float WF = P.DomainWarpFrequency;
const float WS = P.DomainWarpStrength;
const float WarpX = VoxelNoise::Perlin3D(FVector3f(
WorldX * WF + VoxelHash::SeedOffset(SeedU, 5.2f),
WorldY * WF + VoxelHash::SeedOffset(SeedU, 1.3f),
EffectiveZ * WF + VoxelHash::SeedOffset(SeedU, 9.7f)
)) * VOXEL_NOISE_SCALE * WS;
const float WarpY = VoxelNoise::Perlin3D(FVector3f(
WorldX * WF + 100.0f + VoxelHash::SeedOffset(SeedU, 7.7f),
WorldY * WF + 200.0f + VoxelHash::SeedOffset(SeedU, 3.1f),
EffectiveZ * WF + 300.0f
)) * VOXEL_NOISE_SCALE * WS;
const float WarpZ = VoxelNoise::Perlin3D(FVector3f(
WorldX * WF + 400.0f,
WorldY * WF + 500.0f + VoxelHash::SeedOffset(SeedU, 11.9f),
EffectiveZ * WF + 600.0f + VoxelHash::SeedOffset(SeedU, 13.3f)
)) * VOXEL_NOISE_SCALE * WS;
float WarpX=0.0f,WarpY=0.0f,WarpZ=0.0f;
VoxelNoise::Perlin3D_x3(
WorldX*WF+VoxelHash::SeedOffset(SeedU,5.2f),
WorldY*WF+VoxelHash::SeedOffset(SeedU,1.3f),
EffectiveZ*WF+VoxelHash::SeedOffset(SeedU,9.7f),
WorldX*WF+100.0f+VoxelHash::SeedOffset(SeedU,7.7f),
WorldY*WF+200.0f+VoxelHash::SeedOffset(SeedU,3.1f),
EffectiveZ*WF+300.0f,
WorldX*WF+400.0f,
WorldY*WF+500.0f+VoxelHash::SeedOffset(SeedU,11.9f),
EffectiveZ*WF+600.0f+VoxelHash::SeedOffset(SeedU,13.3f),
WarpX,WarpY,WarpZ);
WarpX*=VOXEL_NOISE_SCALE*WS;
WarpY*=VOXEL_NOISE_SCALE*WS;
WarpZ*=VOXEL_NOISE_SCALE*WS;
const FVector3f WarpOffset(WarpX, WarpY, WarpZ);
MainPos += WarpOffset;
@@ -6863,16 +6860,25 @@ namespace
switch (P.RoughnessNoiseType)
{
case EVoxelNoiseType::Ridged:
RoughNoise = HRidged3D(MainPos, Oct3);
FineNoise = HRidged3D(FinePos, Oct2);
{
float MainFBM=0.0f,MainRidged=0.0f,FineFBM=0.0f,FineRidged=0.0f;
VoxelNoise::FractalPair<false,true>(MainPos.X,MainPos.Y,MainPos.Z,Oct3,
FinePos.X,FinePos.Y,FinePos.Z,Oct2,
MainFBM,MainRidged,FineFBM,FineRidged);
RoughNoise=MainRidged;FineNoise=FineRidged;
break;
}
case EVoxelNoiseType::Mixed:
RoughNoise = HFractal3D(MainPos, Oct3) * 0.5f
+ HRidged3D(MainPos, Oct3) * 0.5f;
FineNoise = HFractal3D(FinePos, Oct2) * 0.5f
+ HRidged3D(FinePos, Oct2) * 0.5f;
{
float MainFBM=0.0f,MainRidged=0.0f,FineFBM=0.0f,FineRidged=0.0f;
VoxelNoise::FractalPair<true,true>(MainPos.X,MainPos.Y,MainPos.Z,Oct3,
FinePos.X,FinePos.Y,FinePos.Z,Oct2,
MainFBM,MainRidged,FineFBM,FineRidged);
RoughNoise = MainFBM * 0.5f + MainRidged * 0.5f;
FineNoise = FineFBM * 0.5f + FineRidged * 0.5f;
break;
}
case EVoxelNoiseType::Cellular:
RoughNoise = VoxelNoise::Cellular3D(MainPos);
@@ -6881,10 +6887,15 @@ namespace
case EVoxelNoiseType::FBM:
default:
RoughNoise = HFractal3D(MainPos, Oct3);
FineNoise = HFractal3D(FinePos, Oct2);
{
float MainFBM=0.0f,MainRidged=0.0f,FineFBM=0.0f,FineRidged=0.0f;
VoxelNoise::FractalPair<true,false>(MainPos.X,MainPos.Y,MainPos.Z,Oct3,
FinePos.X,FinePos.Y,FinePos.Z,Oct2,
MainFBM,MainRidged,FineFBM,FineRidged);
RoughNoise=MainFBM;FineNoise=FineFBM;
break;
}
}
RoughNoise *= VOXEL_NOISE_SCALE;
FineNoise *= VOXEL_NOISE_SCALE;
@@ -988,7 +988,7 @@ bool VoxelGenLOD::ShouldUseSpatialIndex(bool bFusedPath)
}
// NOTE (T2.a): the fBm/Ridged bodies moved to VoxelNoise.h, where octaves are evaluated
// 4-wide via SSE (Perlin3D_x4). These thin wrappers keep every call site unchanged. They
// 8-wide via AVX2 (Perlin3D_x8). These thin wrappers keep every call site unchanged. They
// sample a DIFFERENT (float hash-gradient) noise field than the old FMath::PerlinNoise3D,
// so worlds re-tune once — but the fBm/Ridged math/contracts ([-1,1]) are identical.
static float FractalNoise3D(const FVector3f& Position, int32 Octaves = 4,
@@ -4763,24 +4763,22 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
float WF = Params.DomainWarpFrequency;
float WS = Params.DomainWarpStrength;
// Sample three independent noise fields for X, Y, Z warp
float WarpX = VoxelNoise::Perlin3D(FVector3f(
WorldX * WF + VoxelHash::SeedOffset(SeedU, 5.2f),
WorldY * WF + VoxelHash::SeedOffset(SeedU, 1.3f),
EffectiveZ * WF + VoxelHash::SeedOffset(SeedU, 9.7f)
)) * VOXEL_NOISE_SCALE * WS;
float WarpY = VoxelNoise::Perlin3D(FVector3f(
WorldX * WF + 100.0f + VoxelHash::SeedOffset(SeedU, 7.7f),
WorldY * WF + 200.0f + VoxelHash::SeedOffset(SeedU, 3.1f),
EffectiveZ * WF + 300.0f
)) * VOXEL_NOISE_SCALE * WS;
float WarpZ = VoxelNoise::Perlin3D(FVector3f(
WorldX * WF + 400.0f,
WorldY * WF + 500.0f + VoxelHash::SeedOffset(SeedU, 11.9f),
EffectiveZ * WF + 600.0f + VoxelHash::SeedOffset(SeedU, 13.3f)
)) * VOXEL_NOISE_SCALE * WS;
// Sample three independent warp fields together through AVX2.
float WarpX=0.0f,WarpY=0.0f,WarpZ=0.0f;
VoxelNoise::Perlin3D_x3(
WorldX*WF+VoxelHash::SeedOffset(SeedU,5.2f),
WorldY*WF+VoxelHash::SeedOffset(SeedU,1.3f),
EffectiveZ*WF+VoxelHash::SeedOffset(SeedU,9.7f),
WorldX*WF+100.0f+VoxelHash::SeedOffset(SeedU,7.7f),
WorldY*WF+200.0f+VoxelHash::SeedOffset(SeedU,3.1f),
EffectiveZ*WF+300.0f,
WorldX*WF+400.0f,
WorldY*WF+500.0f+VoxelHash::SeedOffset(SeedU,11.9f),
EffectiveZ*WF+600.0f+VoxelHash::SeedOffset(SeedU,13.3f),
WarpX,WarpY,WarpZ);
WarpX*=VOXEL_NOISE_SCALE*WS;
WarpY*=VOXEL_NOISE_SCALE*WS;
WarpZ*=VOXEL_NOISE_SCALE*WS;
// Apply warp to both noise positions
FVector3f WarpOffset(WarpX, WarpY, WarpZ);
@@ -4799,17 +4797,26 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
{
case EVoxelNoiseType::Ridged:
// Ridged multifractal: sharp, craggy features
RoughNoise = RidgedNoise3D(MainPos, Oct3);
FineNoise = RidgedNoise3D(FinePos, Oct2);
{
float MainFBM=0.0f,MainRidged=0.0f,FineFBM=0.0f,FineRidged=0.0f;
VoxelNoise::FractalPair<false,true>(MainPos.X,MainPos.Y,MainPos.Z,Oct3,
FinePos.X,FinePos.Y,FinePos.Z,Oct2,
MainFBM,MainRidged,FineFBM,FineRidged);
RoughNoise=MainRidged;FineNoise=FineRidged;
break;
}
case EVoxelNoiseType::Mixed:
// Blend: ridged structure softened by fBM
RoughNoise = FractalNoise3D(MainPos, Oct3) * 0.5f
+ RidgedNoise3D(MainPos, Oct3) * 0.5f;
FineNoise = FractalNoise3D(FinePos, Oct2) * 0.5f
+ RidgedNoise3D(FinePos, Oct2) * 0.5f;
{
float MainFBM=0.0f,MainRidged=0.0f,FineFBM=0.0f,FineRidged=0.0f;
VoxelNoise::FractalPair<true,true>(MainPos.X,MainPos.Y,MainPos.Z,Oct3,
FinePos.X,FinePos.Y,FinePos.Z,Oct2,
MainFBM,MainRidged,FineFBM,FineRidged);
RoughNoise = MainFBM * 0.5f + MainRidged * 0.5f;
FineNoise = FineFBM * 0.5f + FineRidged * 0.5f;
break;
}
case EVoxelNoiseType::Cellular:
// Worley/cellular: grotto, bubble-like patterns
@@ -4821,10 +4828,15 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
case EVoxelNoiseType::FBM:
default:
// Standard fBM: smooth, organic
RoughNoise = FractalNoise3D(MainPos, Oct3);
FineNoise = FractalNoise3D(FinePos, Oct2);
{
float MainFBM=0.0f,MainRidged=0.0f,FineFBM=0.0f,FineRidged=0.0f;
VoxelNoise::FractalPair<true,false>(MainPos.X,MainPos.Y,MainPos.Z,Oct3,
FinePos.X,FinePos.Y,FinePos.Z,Oct2,
MainFBM,MainRidged,FineFBM,FineRidged);
RoughNoise=MainFBM;FineNoise=FineFBM;
break;
}
}
// Scale by VOXEL_NOISE_SCALE (all noise functions return ~[-1,1] but UE
// Perlin internally returns ~[-0.8,0.8])
+249 -73
View File
@@ -1,55 +1,27 @@
// VoxelNoise.h
// Float, SIMD-batched gradient-noise core (T2.a). Replaces UE's double-precision
// FMath::PerlinNoise3D on the density hot path (~6.6 ms/chunk was all noise math).
// Float hash-gradient Perlin noise for the standalone harness. Batched octave
// coordinates and the three domain-warp channels use the canonical 8-lane AVX2
// path. Standalone builds require AVX2; there is no runtime CPU dispatch.
//
// WHY THIS EXISTS
// FMath::PerlinNoise3D is double-precision with a permutation-table lookup. The
// density field calls it >1M times per surface chunk (fBm octaves + domain warps).
// This core is:
// - FLOAT (no double math),
// - table-free hash-gradient Perlin (pure arithmetic → vectorizes cleanly),
// - SIMD-BATCHED across fBm octaves: one FractalNoise/Ridged call evaluates up to
// 4 octaves' Perlin samples in one 4-wide SSE pass.
//
// It is a DIFFERENT noise field than FMath's, so worlds re-tune ONCE (accepted).
//
// DETERMINISM / CACHES
// Perlin3D is a pure function of (x,y,z) exactly like the old call — every box-validity
// cache (SDF §8.10, biome, surface column) stays valid. No invariant changes.
//
// SCALAR vs SIMD
// Perlin3D (scalar) and Perlin3D_x4 (SSE) use the IDENTICAL formula, op-for-op, so on
// x86 (SSE math == scalar-float math, same IEEE rounding) they produce bit-identical
// results. The SSE path is therefore a pure speedup with no second re-tune. If it ever
// fails to build on a given toolchain, force the scalar fallback with one line:
// #define VF_NOISE_USE_SIMD 0 // (before including this header, or here)
// The scalar path alone is still a large win over the old double-precision core.
//
// REQUIRES: SSE4.1 for the SIMD path (UE5 x64 baseline is SSE4.2 → always available).
// The AVX2 batches preserve the scalar Perlin interpolation and hash operations.
// Intrinsics spell out multiply and add separately, and Build.ps1 uses /fp:precise.
#pragma once
#include "CoreMinimal.h"
#include "VoxelTypes.h"
#if !defined(VF_NOISE_USE_SIMD)
#if PLATFORM_ENABLE_VECTORINTRINSICS && PLATFORM_CPU_X86_FAMILY
#define VF_NOISE_USE_SIMD 1
#else
#define VF_NOISE_USE_SIMD 0
#endif
#endif
#if VF_NOISE_USE_SIMD
#include <immintrin.h> // SSE4.1: _mm_floor_ps / _mm_mullo_epi32 / _mm_blendv_ps
#endif
// The standalone harness is built with /arch:AVX2 on the required AVX2 host.
// Keep one canonical arithmetic path here; there is no runtime or platform dispatch.
#define VF_NOISE_USE_SIMD 1
#include <immintrin.h>
namespace VoxelNoise
{
namespace Detail
{
// Table-free integer hash of a lattice corner → gradient selector. Pure mul/xor/shift
// so it vectorizes 1:1 (see the SSE Hash lambda below — must stay in lock-step).
// Table-free integer hash of a lattice corner → gradient selector. Keep it in
// lock-step with the AVX2 Hash lambda below.
FORCEINLINE uint32 HashCorner(int32 ix, int32 iy, int32 iz)
{
uint32 h = (uint32)ix * 0x9E3779B1u
@@ -61,7 +33,7 @@ namespace Detail
return h;
}
// Quintic fade 6t^5-15t^4+10t^3, factored as t^3 * (t*(6t-15)+10) so the SSE twin
// Quintic fade 6t^5-15t^4+10t^3, factored as t^3 * (t*(6t-15)+10) so the AVX2 twin
// can mirror the exact grouping.
FORCEINLINE float Fade(float t)
{
@@ -71,7 +43,7 @@ namespace Detail
}
// Ken Perlin's 12-gradient dot (hash&15 picks the gradient). Branchy form here;
// the SSE twin reproduces it with selects.
// the AVX2 twin reproduces it with selects.
FORCEINLINE float GradDot(uint32 hash, float x, float y, float z)
{
const uint32 h = hash & 15u;
@@ -357,52 +329,136 @@ 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.
// Eight independent gradient-noise samples in AVX2 lanes. Arithmetic order mirrors the
// scalar Perlin3D implementation; explicit mul/add intrinsics keep contraction disabled.
FORCEINLINE void Perlin3D_x8(const float* Xs, const float* Ys, const float* Zs, float* Out)
{
const __m256 x = _mm256_loadu_ps(Xs);
const __m256 y = _mm256_loadu_ps(Ys);
const __m256 z = _mm256_loadu_ps(Zs);
const __m256 xf = _mm256_floor_ps(x);
const __m256 yf = _mm256_floor_ps(y);
const __m256 zf = _mm256_floor_ps(z);
const __m256i X = _mm256_cvttps_epi32(xf);
const __m256i Y = _mm256_cvttps_epi32(yf);
const __m256i Z = _mm256_cvttps_epi32(zf);
const __m256 fx = _mm256_sub_ps(x, xf);
const __m256 fy = _mm256_sub_ps(y, yf);
const __m256 fz = _mm256_sub_ps(z, zf);
const __m256 c6 = _mm256_set1_ps(6.0f);
const __m256 c15 = _mm256_set1_ps(15.0f);
const __m256 c10 = _mm256_set1_ps(10.0f);
const auto Fade = [&](const __m256 t)
{
const __m256 inner = _mm256_add_ps(
_mm256_mul_ps(t, _mm256_sub_ps(_mm256_mul_ps(t, c6), c15)), c10);
const __m256 t3 = _mm256_mul_ps(_mm256_mul_ps(t, t), t);
return _mm256_mul_ps(t3, inner);
};
const __m256 su = Fade(fx), sv = Fade(fy), sw = Fade(fz);
const __m256i k1 = _mm256_set1_epi32(static_cast<int32>(0x9E3779B1u));
const __m256i k2 = _mm256_set1_epi32(static_cast<int32>(0x85EBCA77u));
const __m256i k3 = _mm256_set1_epi32(static_cast<int32>(0xC2B2AE3Du));
const __m256i m1 = _mm256_set1_epi32(static_cast<int32>(0x2C1B3C6Du));
const __m256i m2 = _mm256_set1_epi32(static_cast<int32>(0x297A2D39u));
const __m256i px0 = _mm256_mullo_epi32(X, k1);
const __m256i py0 = _mm256_mullo_epi32(Y, k2);
const __m256i pz0 = _mm256_mullo_epi32(Z, k3);
const __m256i px1 = _mm256_add_epi32(px0, k1);
const __m256i py1 = _mm256_add_epi32(py0, k2);
const __m256i pz1 = _mm256_add_epi32(pz0, k3);
const auto HashProducts = [&](const __m256i px, const __m256i py, const __m256i pz)
{
__m256i h = _mm256_xor_si256(_mm256_xor_si256(px, py), pz);
h = _mm256_xor_si256(h, _mm256_srli_epi32(h, 15)); h = _mm256_mullo_epi32(h, m1);
h = _mm256_xor_si256(h, _mm256_srli_epi32(h, 12)); h = _mm256_mullo_epi32(h, m2);
return _mm256_xor_si256(h, _mm256_srli_epi32(h, 15));
};
const __m256 fx1 = _mm256_sub_ps(fx, _mm256_set1_ps(1.0f));
const __m256 fy1 = _mm256_sub_ps(fy, _mm256_set1_ps(1.0f));
const __m256 fz1 = _mm256_sub_ps(fz, _mm256_set1_ps(1.0f));
const __m256i i8 = _mm256_set1_epi32(8), i12 = _mm256_set1_epi32(12);
const __m256i i13 = _mm256_set1_epi32(13), i15 = _mm256_set1_epi32(15);
const __m256i i1 = _mm256_set1_epi32(1), i2 = _mm256_set1_epi32(2);
const __m256i izero = _mm256_setzero_si256();
const __m256 sign = _mm256_set1_ps(-0.0f);
const auto Grad = [&](const __m256i hash, const __m256 gx, const __m256 gy, const __m256 gz)
{
const __m256i h = _mm256_and_si256(hash, i15);
const __m256 mU = _mm256_castsi256_ps(
_mm256_cmpeq_epi32(_mm256_and_si256(h, i8), izero));
const __m256 u = _mm256_blendv_ps(gy, gx, mU);
const __m256 mLt4 = _mm256_castsi256_ps(
_mm256_cmpeq_epi32(_mm256_and_si256(h, i12), izero));
const __m256 m1214 = _mm256_castsi256_ps(
_mm256_cmpeq_epi32(_mm256_and_si256(h, i13), i12));
const __m256 vTmp = _mm256_blendv_ps(gz, gx, m1214);
const __m256 v = _mm256_blendv_ps(vTmp, gy, mLt4);
const __m256 mNegU = _mm256_castsi256_ps(
_mm256_cmpeq_epi32(_mm256_and_si256(h, i1), i1));
const __m256 mNegV = _mm256_castsi256_ps(
_mm256_cmpeq_epi32(_mm256_and_si256(h, i2), i2));
const __m256 ru = _mm256_blendv_ps(u, _mm256_xor_ps(u, sign), mNegU);
const __m256 rv = _mm256_blendv_ps(v, _mm256_xor_ps(v, sign), mNegV);
return _mm256_add_ps(ru, rv);
};
const __m256 n000 = Grad(HashProducts(px0, py0, pz0), fx, fy, fz);
const __m256 n100 = Grad(HashProducts(px1, py0, pz0), fx1, fy, fz);
const __m256 n010 = Grad(HashProducts(px0, py1, pz0), fx, fy1, fz);
const __m256 n110 = Grad(HashProducts(px1, py1, pz0), fx1, fy1, fz);
const __m256 n001 = Grad(HashProducts(px0, py0, pz1), fx, fy, fz1);
const __m256 n101 = Grad(HashProducts(px1, py0, pz1), fx1, fy, fz1);
const __m256 n011 = Grad(HashProducts(px0, py1, pz1), fx, fy1, fz1);
const __m256 n111 = Grad(HashProducts(px1, py1, pz1), fx1, fy1, fz1);
const auto Lerp = [&](const __m256 a, const __m256 b, const __m256 t)
{
return _mm256_add_ps(a, _mm256_mul_ps(t, _mm256_sub_ps(b, a)));
};
const __m256 x00 = Lerp(n000, n100, su);
const __m256 x10 = Lerp(n010, n110, su);
const __m256 x01 = Lerp(n001, n101, su);
const __m256 x11 = Lerp(n011, n111, su);
const __m256 y0 = Lerp(x00, x10, sv);
const __m256 y1 = Lerp(x01, x11, sv);
_mm256_storeu_ps(Out, Lerp(y0, y1, sw));
}
// Three independent channels are the hot cave-warp shape. Pack them into the canonical AVX2 path.
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
const float Xs[8] = { X0, X1, X2, X0, X0, X0, X0, X0 };
const float Ys[8] = { Y0, Y1, Y2, Y0, Y0, Y0, Y0, Y0 };
const float Zs[8] = { Z0, Z1, Z2, Z0, Z0, Z0, Z0, Z0 };
float Out[8];
Perlin3D_x8(Xs, Ys, Zs, Out);
Out0 = Out[0]; Out1 = Out[1]; Out2 = Out[2];
}
//=============================================================================
// fBm / Ridged — octaves evaluated 4 at a time through Perlin3D_x4.
// Accumulation stays scalar in octave order, so the result is independent of
// whether the SIMD or scalar Perlin3D_x4 is used (bit-identical either way).
// fBm / Ridged — independent octave coordinates occupy AVX2 lanes. Accumulation
// remains scalar and ordered by octave, with explicit float operations.
//=============================================================================
FORCEINLINE float FBM(float x, float y, float z,
int32 Octaves = 4, float Lacunarity = 2.0f, float Persistence = 0.5f)
{
float Total = 0.0f, MaxValue = 0.0f, Freq = 1.0f, Amp = 1.0f;
for (int32 o = 0; o < Octaves; )
while (Octaves > 0)
{
const int32 N = FMath::Min(4, Octaves - o);
float Xs[4] = {}, Ys[4] = {}, Zs[4] = {}, Out[4];
const int32 N = FMath::Min(8, Octaves);
float Xs[8] = {}, Ys[8] = {}, Zs[8] = {}, Out[8];
float f = Freq;
for (int32 i = 0; i < N; ++i) { Xs[i] = x * f; Ys[i] = y * f; Zs[i] = z * f; f *= Lacunarity; }
Perlin3D_x4(Xs, Ys, Zs, Out);
Perlin3D_x8(Xs, Ys, Zs, Out);
for (int32 i = 0; i < N; ++i) { Total += Out[i] * Amp; MaxValue += Amp; Amp *= Persistence; }
Freq = f;
o += N;
Octaves -= N;
}
return Total / MaxValue;
}
@@ -413,13 +469,13 @@ FORCEINLINE float Ridged(float x, float y, float z,
// Matches the original RidgedNoise3D fold exactly (NS scale, square, weight feedback).
static constexpr float NS = 1.25f;
float Total = 0.0f, MaxValue = 0.0f, Freq = 1.0f, Amp = 1.0f, Weight = 1.0f;
for (int32 o = 0; o < Octaves; )
while (Octaves > 0)
{
const int32 N = FMath::Min(4, Octaves - o);
float Xs[4] = {}, Ys[4] = {}, Zs[4] = {}, Out[4];
const int32 N = FMath::Min(8, Octaves);
float Xs[8] = {}, Ys[8] = {}, Zs[8] = {}, Out[8];
float f = Freq;
for (int32 i = 0; i < N; ++i) { Xs[i] = x * f; Ys[i] = y * f; Zs[i] = z * f; f *= Lacunarity; }
Perlin3D_x4(Xs, Ys, Zs, Out);
Perlin3D_x8(Xs, Ys, Zs, Out);
for (int32 i = 0; i < N; ++i)
{
float Nn = Out[i] * NS;
@@ -430,9 +486,129 @@ FORCEINLINE float Ridged(float x, float y, float z,
Total += Nn * Amp; MaxValue += Amp; Amp *= Persistence;
}
Freq = f;
o += N;
Octaves -= N;
}
return (Total / MaxValue) * 2.0f - 1.0f;
}
// Mixed roughness consumes fBm and ridged folds of the same octave samples. Share
// the expensive AVX2 Perlin batch while retaining each accumulator's original order.
FORCEINLINE void FBMAndRidged(float x, float y, float z, int32 Octaves,
float& OutFBM, float& OutRidged,
float Lacunarity = 2.0f, float Persistence = 0.5f)
{
static constexpr float NS = 1.25f;
float FbmTotal = 0.0f, FbmMax = 0.0f;
float RidgeTotal = 0.0f, RidgeMax = 0.0f;
float Freq = 1.0f, Amp = 1.0f, Weight = 1.0f;
while (Octaves > 0)
{
const int32 N = FMath::Min(8, Octaves);
float Xs[8] = {}, Ys[8] = {}, Zs[8] = {}, Noise[8];
float f = Freq;
for (int32 i = 0; i < N; ++i)
{
Xs[i] = x * f; Ys[i] = y * f; Zs[i] = z * f;
f *= Lacunarity;
}
Perlin3D_x8(Xs, Ys, Zs, Noise);
for (int32 i = 0; i < N; ++i)
{
FbmTotal += Noise[i] * Amp;
FbmMax += Amp;
float Ridge = Noise[i] * NS;
Ridge = 1.0f - FMath::Abs(Ridge);
Ridge = Ridge * Ridge;
Ridge *= Weight;
Weight = FMath::Clamp(Ridge * 2.0f, 0.0f, 1.0f);
RidgeTotal += Ridge * Amp;
RidgeMax += Amp;
Amp *= Persistence;
}
Freq = f;
Octaves -= N;
}
OutFBM = FbmTotal / FbmMax;
OutRidged = (RidgeTotal / RidgeMax) * 2.0f - 1.0f;
}
template<bool bComputeFBM, bool bComputeRidged>
FORCEINLINE void FractalPair(float X0, float Y0, float Z0, int32 Octaves0,
float X1, float Y1, float Z1, int32 Octaves1,
float& OutFBM0, float& OutRidged0,
float& OutFBM1, float& OutRidged1,
float Lacunarity = 2.0f, float Persistence = 0.5f)
{
OutFBM0 = OutRidged0 = OutFBM1 = OutRidged1 = 0.0f;
if (Octaves0 + Octaves1 > 8)
{
if constexpr (bComputeFBM)
{
OutFBM0 = FBM(X0,Y0,Z0,Octaves0,Lacunarity,Persistence);
OutFBM1 = FBM(X1,Y1,Z1,Octaves1,Lacunarity,Persistence);
}
if constexpr (bComputeRidged)
{
OutRidged0 = Ridged(X0,Y0,Z0,Octaves0,Lacunarity,Persistence);
OutRidged1 = Ridged(X1,Y1,Z1,Octaves1,Lacunarity,Persistence);
}
return;
}
float Xs[8] = {}, Ys[8] = {}, Zs[8] = {}, Noise[8];
float Frequency = 1.0f;
for (int32 I = 0; I < Octaves0; ++I)
{
Xs[I] = X0 * Frequency; Ys[I] = Y0 * Frequency; Zs[I] = Z0 * Frequency;
Frequency *= Lacunarity;
}
Frequency = 1.0f;
for (int32 I = 0; I < Octaves1; ++I)
{
const int32 Lane = Octaves0 + I;
Xs[Lane] = X1 * Frequency; Ys[Lane] = Y1 * Frequency; Zs[Lane] = Z1 * Frequency;
Frequency *= Lacunarity;
}
Perlin3D_x8(Xs,Ys,Zs,Noise);
float FbmTotal[2] = {}, FbmMax[2] = {};
float RidgeTotal[2] = {}, RidgeMax[2] = {};
float Amplitude[2] = {1.0f,1.0f}, Weight[2] = {1.0f,1.0f};
const int32 Octaves[2] = {Octaves0,Octaves1};
for (int32 Group = 0; Group < 2; ++Group)
{
for (int32 I = 0; I < Octaves[Group]; ++I)
{
const float N = Noise[(Group==0?0:Octaves0)+I];
const float Amp = Amplitude[Group];
if constexpr (bComputeFBM)
{
FbmTotal[Group] += N * Amp;
FbmMax[Group] += Amp;
}
if constexpr (bComputeRidged)
{
float Ridge = N * 1.25f;
Ridge = 1.0f - FMath::Abs(Ridge);
Ridge = Ridge * Ridge;
Ridge *= Weight[Group];
Weight[Group] = FMath::Clamp(Ridge * 2.0f,0.0f,1.0f);
RidgeTotal[Group] += Ridge * Amp;
RidgeMax[Group] += Amp;
}
Amplitude[Group] *= Persistence;
}
}
if constexpr (bComputeFBM)
{
OutFBM0 = FbmTotal[0] / FbmMax[0];
OutFBM1 = FbmTotal[1] / FbmMax[1];
}
if constexpr (bComputeRidged)
{
OutRidged0 = (RidgeTotal[0] / RidgeMax[0]) * 2.0f - 1.0f;
OutRidged1 = (RidgeTotal[1] / RidgeMax[1]) * 2.0f - 1.0f;
}
}
} // namespace VoxelNoise