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
+446 -135

No files matched your search

@@ -1,5 +1,68 @@
# Shape world speed round — final half-load verification
## Round 8 — canonical AVX2 shape arithmetic (2026-10-05)
The AVX2 path plus factored corner hashing cuts the direct cold W8 process mean from **1.456 s to 1.149 s** (21.1%). It misses the requested **under-1-second** target by 149 ms at the mean; the faster new run was 1.146 s. The full 110.90 MiB payload remains enabled, all 2,226 requested tiles were generated, and every direct run recorded zero mesh-cache hits. The field passes the shape and far-survival audits, the 5,642 route samples remain open, all 44 W1/W8 hash fields match, and the six fixed captures are pixel-identical to Round 7.
### Arithmetic and profile
`VoxelNoise::Perlin3D_x8` evaluates eight independent samples with AVX2 integer hashing and gradient/interpolation arithmetic. `FractalPair` packs the main and fine octave coordinates into the same batch and shares each sample across fBM and ridged accumulation. Domain-warp channels also use the 8-lane path. The cave detail line reuses its spatial-bin items across consecutive sorted Z samples, and coarse rock-distance lookup remembers the previous bin per thread. The hash keeps its existing three mix rounds. For each Perlin lattice, the three corner-coordinate products are now computed once; adjacent corner products use wrapping integer adds, preserving all hash outputs while removing repeated vector multiplies. A reduced-round hash probe changed the field and did not improve time, so it was discarded.
This is the standalone harness's canonical path, with no runtime CPU dispatch. The host requires AVX2. Intrinsics issue multiply and add separately; the build uses `/fp:precise` to prevent contraction. The AVX2 kernel uses no CRT transcendental functions. Build settings also include `/arch:AVX2` and `/MP8`.
| W8 profile measure | Round 7 scalar path | Round 8 final | Change |
| --- | ---: | ---: | ---: |
| Shape stage | 690.9 ms | 478.9 ms | -30.7% |
| Surface detail band | 448.3 ms | 245.1 ms | -45.3% |
| Detail evaluation sample | 792.7 ns | 328.0 ns | -58.6% |
| Owner roughness sample | 572.7 ns | 107.2 ns | -81.3% |
| Detail evaluations | 3,527,283 | 3,527,283 | same workload |
The profile starts at [Round 8 before](round8_profile_before_w8.stdout.log), passes through [AVX2](round8_profile_after_avx2_w8.stdout.log), [shared noise](round8_profile_after_opt_w8.stdout.log), and [paired octaves](round8_profile_after_pair_w8.stdout.log), and ends at the current [factored-hash profile](round8_hash_factored_profile_w8.stdout.log). The distance sample moved from 128.4 ns to 117.3 ns in the final profile; most of the gain is in owner noise. The hash-factor probe's alternating direct runs averaged 1.196 s for the preceding AVX2 executable and 1.145 s for the factored probe: [base A](round8_hash_factored_base_a_w8.stdout.log), [probe A](round8_hash_factored_probe_a_w8.stdout.log), [base B](round8_hash_factored_base_b_w8.stdout.log), [probe B](round8_hash_factored_probe_b_w8.stdout.log).
An isolated W8 LOD8 mesher profile processed 306 tiles / 2.10 M samples. Its [tile profile](round8_tile_profile_lod8_w8.stdout.log) attributes 125.8 ms to density-grid construction within a 403 ms serial tile-benchmark run. This diagnostic exercises the base mesher and does not include the streamer's source-aware repair panels, so it is not a replacement for the full-payload direct timing. It confirms density sampling is material; no separate mesher rewrite was retained this round.
### Required cold direct head-to-head
Both executables were launched in fresh processes with `--stream-benchmark --shape-world --force-generate --workers=8 --clip-radius=3`. The old executable is `VoxelForgeShapeSurvivalSearch.exe`; the new one is `VoxelForgeShapeRound8Final.exe`. The wrapper held BelowNormal priority and affinity `0xFF00`. Each run generated the full payload with `cache_hits=0` and `payload_alloc=110.90MiB`.
| Pair | Executable | Process wall | Shape stage | Full clipmap | Cache hits |
| --- | --- | ---: | ---: | ---: | ---: |
| A | Round 7 | 1.453 s | 698.2 ms | 648 ms | 0 / 2,226 |
| A | Round 8 | 1.146 s | 453.5 ms | 585 ms | 0 / 2,226 |
| B | Round 7 | 1.459 s | 698.2 ms | 638 ms | 0 / 2,226 |
| B | Round 8 | 1.152 s | 472.7 ms | 574 ms | 0 / 2,226 |
Logs: [old A](round8_final_ab_old_a_w8.stdout.log), [new A](round8_final_ab_new_a_w8.stdout.log), [old B](round8_final_ab_old_b_w8.stdout.log), [new B](round8_final_ab_new_b_w8.stdout.log). The new mean is 1.149 s versus 1.456 s old. The matching shape-only profiles with attribution are separate runs and are not included in these wall times.
### Determinism and audits
The new executable's `--stream-hash` run at W1 and W8 matched density, mesh, payload, and topology for full detail, whole view, and each LOD0–8: **44/44 fields pass**. The [comparison output](round8_final/hash_compare_run.log) also confirms all 44 final fields equal the preceding AVX2 executable; it is generated by [compare_hash_runs.py](round8_final/compare_hash_runs.py). Full-detail hashes are density `0x67609e3f9aa3a0b0`, mesh `0xd98eb46b92a59917`, payload `0x8f148240ca4a4a8f`, topology `0x954e28f6fd44967f`. Whole-view hashes are density `0xa78b0985b1b8fefd`, mesh `0xb44e0c998152d1f5`, payload `0x1eb6b48127fd8e26`, topology `0x8b2557006b5ce3c8`. Raw runs: [W1](round8_final_hash_w1.stdout.log) and [W8](round8_final_hash_w8.stdout.log).
The [shape audit](round8_final_shape_audit_w8.stdout.log) passes walkability (**5,642/5,642**), room-floor join, floor slope and profile, tunnel radius, flat surface, grid stair, thin sheet, surface-band family, flat patch, and detail-scale checks. The [far-survival audit](round8_final_far_audit_w8.stdout.log) passes all 7,200 boundary surfaces and 10,976 connectors across 178,351 connector sections: zero missing, holes, or unsampled connectors. The audit also records 47,858 extra mesh ray hits. The injected [bad-shell control](round8_final_bad_shell_w8.stdout.log) removed 512 triangles, detected 16 missing surfaces and 16 holes, and exited 14 as expected.
### Captures
All six fixed captures compare pixel-identically with Round 7. The four X=500 m remote views have only small pixel differences. The [comparison log](round8_final/capture_compare.log) compares all ten images; the images are in [round8_final](round8_final/).
| View | Changed pixels | Mean channel delta | Max channel delta |
| --- | ---: | ---: | ---: |
| Fixed chamber / tunnel / passage / far / floor close-up / tunnel bend | 0 / 1,024,000 each | 0 | 0 |
| Remote yaw 0 | 0 / 1,024,000 | 0.000000 | 0 |
| Remote yaw 90 | 1 / 1,024,000 | 0.000000 | 1 |
| Remote yaw 180 | 0 / 1,024,000 | 0.000000 | 0 |
| Remote yaw 270 | 0 / 1,024,000 | 0.000000 | 0 |
### Failed and discarded attempts
- The first AVX2 build invocation returned shell exit 1 because its relative `Tee-Object` destination was resolved after `Build.ps1` changed directories. The compiler had already produced the executable. Retrying with an absolute log path succeeded: [build log](round8_avx2_build.log). The hash-factor probe build succeeded: [probe build](round8_hash_factored_build.log), followed by the final `/MP8` build: [final build log](round8_hashfactored_final_build.log). Builds emit existing C4838 narrowing warnings and a `vswhere.exe`-not-recognized diagnostic; neither stops compilation. No process reported `0xC0000142`.
- The first chamber capture used an absolute output path containing spaces; argument splitting made the executable treat the remainder as its owner-data path, and that run exited 2. Using a Standalone-relative capture path succeeded: [failed invocation stderr](round8_capture_fixed_chamber_w8.stderr.log), [retry](round8_capture_fixed_chamber_retry_w8.stdout.log). The other nine captures succeeded on their first invocation.
- The reduced-round hash experiment built and ran, but changed candidate counts/field output without a timing gain; its source was reverted. Evidence: [probe build](round8_hash_build.log), [profile](round8_profile_after_hash_w8.stdout.log).
- A far-ray prune probe completed but did not improve direct time and was reverted: [build](round8_prune_build.log), [direct probe](round8_prune_direct_probe_w8.stdout.log).
- The bad-shell control is the intentional failing audit described above (exit 14). No other Round 8 benchmark, audit, or capture run failed.
All build and executable runs were constrained to BelowNormal priority and affinity `0xFF00`; builds used `/MP8`. No viewer that this work did not start was closed. Changes and artifacts are confined to `Standalone\`; no commit, push, or stash was made.
## Round 7 — sparse far repairs and single shape compile (2026-10-05)
Round 7 cut the LOD8 survival build from **276.74 MiB** to **110.90 MiB** of full mesh-payload allocation, a 59.9% reduction from Round 6. The final CPU follow-up keeps that payload and trims about 0.28 seconds from summed worker time and 0.04 seconds from stream wall. The result remains **18.29 MiB above** the no-shell 92.61 MiB baseline, and the process-wall target below 1.3 seconds was not reached.
+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(
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)
)) * VOXEL_NOISE_SCALE * WS;
const float WarpY = VoxelNoise::Perlin3D(FVector3f(
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
)) * VOXEL_NOISE_SCALE * WS;
const float WarpZ = VoxelNoise::Perlin3D(FVector3f(
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)
)) * VOXEL_NOISE_SCALE * WS;
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(
// 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)
)) * VOXEL_NOISE_SCALE * WS;
float WarpY = VoxelNoise::Perlin3D(FVector3f(
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
)) * VOXEL_NOISE_SCALE * WS;
float WarpZ = VoxelNoise::Perlin3D(FVector3f(
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)
)) * VOXEL_NOISE_SCALE * WS;
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])
+248 -72
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
// 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
#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
#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