wip(standalone): GPU step 6 -- near field on GPU, exact; cold start still slower than CPU

Near LOD0/1 (detail band, relief, root refinement, normals) now evaluated and
meshed on the GPU. 960 near tiles: zero sign/topology differences vs CPU and
WARP, vertex max 0.0049 mm (was 15 cm); far survival audit passes; CPU hashes
44/44 unchanged. Far connector-panel outlier (64.56 cm, LOD8) remains a
tessellation difference, and remote captures show missing far geometry on GPU.

Cold readiness (viewer clock, my interleaved runs): CPU 2.51/2.49/2.40 s,
GPU 5.05/2.68/2.80 s. The ~2.1 s Present stall hit only the first GPU launch.
Viewer spends ~1.13 s before shape compile starts (startup checks), which is
the gap between the 0.95 s benchmark and ~2.4 s on-screen.

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-06 20:03:10 +02:00
1 parent 0e730cdbba
commit b0a14262b1
50 files changed
+1681 -236

No files matched your search

+53 -66
View File
@@ -526,9 +526,20 @@ public:
auto Data=std::make_shared<VoxelForgeCore::FarFieldWorldInput>();
Data->spatialBinSize=static_cast<uint32>(ActiveSpatialBinSize);
Data->spatialBinMask=static_cast<uint32>(TableCapacity-1);
Data->fieldSeed=static_cast<uint32>(Owner?Owner->Seed:0);
for(int32 Axis=0;Axis<3;++Axis)
{Data->detailMinVoxel[Axis]=Min[Axis];Data->detailMaxVoxel[Axis]=Max[Axis];}
Data->shapes.reserve(SharedFieldPrimitives.size());
for(const VF_FieldPrimitive& Shape:SharedFieldPrimitives)
Data->shapes.push_back(ToFarFieldGpuPrimitive(Shape));
Data->detailLayers.reserve(LayerFields.size());
for(const FLayerField& Field:LayerFields)
{
const FStrateGenerationParams& Params=Field.Params;
Data->detailLayers.push_back({Field.Lo,Field.Hi,Params.SurfaceRoughness,
Params.RoughnessFrequency,Params.VerticalScale,Params.DomainWarpStrength,
Params.DomainWarpFrequency,static_cast<uint32>(Params.RoughnessNoiseType)});
}
Data->spatialBins.resize(TableCapacity);
size_t ReferenceCount=0;
for(const auto& Bin:SpatialBins)ReferenceCount+=Bin.second.size();
@@ -551,20 +562,22 @@ public:
const double UploadMiB=(G.shapes.size()*sizeof(VoxelForgeCore::FarFieldPrimitiveGpu)
+G.spatialBins.size()*sizeof(VoxelForgeCore::FarFieldSpatialBin)
+G.spatialBinShapeIds.size()*sizeof(uint32))/1048576.0;
std::printf("[FarGpuWorldData] shapes=%zu spatial_bins=%zu hash_slots=%zu bin_shape_ids=%zu bin_size=%d bytes=%.3fMiB upload_scope=once_per_world\n",
std::printf("[FarGpuWorldData] shapes=%zu spatial_bins=%zu hash_slots=%zu bin_shape_ids=%zu bin_size=%d detail_min=(%d,%d,%d) detail_max=(%d,%d,%d) bytes=%.3fMiB upload_scope=once_per_world\n",
G.shapes.size(),SpatialBins.size(),G.spatialBins.size(),G.spatialBinShapeIds.size(),
ActiveSpatialBinSize,UploadMiB);
ActiveSpatialBinSize,G.detailMinVoxel[0],G.detailMinVoxel[1],G.detailMinVoxel[2],
G.detailMaxVoxel[0],G.detailMaxVoxel[1],G.detailMaxVoxel[2],UploadMiB);
return CachedFarFieldGpuInput;
}
bool ExportFarFieldTileInput(const FIntVector& Origin,int32 Cells,int32 Step,
VoxelForgeCore::FarFieldTileInput& Out,std::string& Error) const
bool bNearDetail,VoxelForgeCore::FarFieldTileInput& Out,std::string& Error) const
{
if(!FarBuilt||Cells<=0||Cells>32||Step<=0)
{Error="far field export requires a built far world and a bounded coarse tile";return false;}
const int32 Dim=Cells+3;
const size_t SampleCount=static_cast<size_t>(Dim)*Dim*Dim;
Out={};Error.clear();Out.origin={Origin.X,Origin.Y,Origin.Z};Out.cells=Cells;Out.step=Step;Out.sampleDim=Dim;
Out.nearDetailEnabled=bNearDetail;
Out.worldInput=GetFarFieldWorldInput(Error);
if(!Out.worldInput)return false;
if(LayerRanges.empty())
@@ -596,6 +609,8 @@ public:
Out.protectedAir.assign((SampleCount+31u)/32u,0u);
Out.protectedSolid.assign((SampleCount+31u)/32u,0u);
Out.protectedThin.assign((SampleCount+31u)/32u,0u);
if(bNearDetail)
Out.nearPackedProxy.assign(SampleCount,std::numeric_limits<uint32>::max());
const int32 LatticeMin[3]={Origin.X-Step,Origin.Y-Step,Origin.Z-Step};
const int32 LatticeMax[3]={Origin.X+(Cells+1)*Step,Origin.Y+(Cells+1)*Step,
@@ -723,7 +738,34 @@ public:
for(int32 X=-1;X<=Cells+1;++X,++SampleIndex)
{
const int32 WX=Origin.X+X*Step,WY=Origin.Y+Y*Step,WZ=Origin.Z+Z*Step;
if(WX>=Min[0]&&WX<=Max[0]&&WY>=Min[1]&&WY<=Max[1]&&WZ>=Min[2]&&WZ<=Max[2])
if(bNearDetail&&!bAnalyticOnly&&Contains(WX,WY,WZ))
{
const size_t Column=static_cast<size_t>(WY-Min[1])*Size[0]
+static_cast<size_t>(WX-Min[0]);
const size_t Zi=static_cast<size_t>(WZ-Min[2]);
const size_t Word=Column*WordsPerZ+(Zi>>6);
const uint64 Mask=uint64{1}<<(Zi&63u);
if((CandidateWords[Word]&Mask)==0)
Out.nearPackedProxy[SampleIndex]=0u;
else
{
const size_t Node=Zi*static_cast<size_t>(Size[1])*Size[0]+Column;
const int32 Q=std::abs(static_cast<int32>(SurfaceDistanceQ[Node]));
const uint32 Magnitude=static_cast<uint32>(std::clamp(Q,1,32767));
if(Magnitude==1u)
{
// At the minimum quantization step a one-ULP GPU sign flip
// can change an entire marching-cubes cell. Keep this sparse
// ambiguous boundary decision aligned with the packed cache.
const bool bRock=(RockWords[Word]&Mask)!=0;
Out.nearPackedProxy[SampleIndex]=0x40000000u
|(bRock?0x80000000u:0u)|Magnitude;
}
else Out.nearPackedProxy[SampleIndex]=Magnitude;
}
}
if(!bNearDetail&&WX>=Min[0]&&WX<=Max[0]&&WY>=Min[1]&&WY<=Max[1]
&&WZ>=Min[2]&&WZ<=Max[2])
{Error="tile lattice intersects the near analytic/detail region; this export is far-field only";return false;}
if(InsideCritical(WX,WY,WZ))
{
@@ -1746,6 +1788,7 @@ private:
if(P.bCurvedFloorClip)Out.flags|=VF_FIELD_PRIMITIVE_CURVED_FLOOR;
if(P.bCarve)Out.flags|=VF_FIELD_PRIMITIVE_CARVE;
if(P.bCritical)Out.flags|=VF_FIELD_PRIMITIVE_CRITICAL;
if(P.bPassageSurface)Out.flags|=VF_FIELD_PRIMITIVE_PASSAGE;
for(int32 A=0;A<3;++A)
{
Out.minVoxel[A]=P.Min[A];Out.maxVoxel[A]=P.Max[A];
@@ -4081,68 +4124,12 @@ private:
float OwnerCaveRoughnessCore(double X,double Y,double Z,
const FStrateGenerationParams& Params) const
{
const uint32 Seed=static_cast<uint32>(Owner->Seed);
const float RF=Params.RoughnessFrequency;
const float NoiseX=static_cast<float>(X);
const float NoiseY=static_cast<float>(Y);
const float EffectiveZ=(Params.VerticalScale>0.0f&&Params.VerticalScale!=1.0f)
?static_cast<float>(Z)/Params.VerticalScale:static_cast<float>(Z);
FVector3f Main(NoiseX*RF+VoxelHash::SeedOffset(Seed,11.3f),
NoiseY*RF+VoxelHash::SeedOffset(Seed,13.7f),
EffectiveZ*RF+VoxelHash::SeedOffset(Seed,17.1f));
FVector3f Fine(NoiseX*RF*3.0f+VoxelHash::SeedOffset(Seed,19.1f)+2000.0f,
NoiseY*RF*3.0f+VoxelHash::SeedOffset(Seed,23.7f)+2500.0f,
EffectiveZ*RF*3.0f+VoxelHash::SeedOffset(Seed,29.3f)+3000.0f);
if(Params.DomainWarpStrength>0.0f)
{
const float WF=Params.DomainWarpFrequency,WS=Params.DomainWarpStrength;
float WX=0.0f,WY=0.0f,WZ=0.0f;
VoxelNoise::Perlin3D_x3(
NoiseX*WF+VoxelHash::SeedOffset(Seed,5.2f),
NoiseY*WF+VoxelHash::SeedOffset(Seed,1.3f),
EffectiveZ*WF+VoxelHash::SeedOffset(Seed,9.7f),
NoiseX*WF+100.0f+VoxelHash::SeedOffset(Seed,7.7f),
NoiseY*WF+200.0f+VoxelHash::SeedOffset(Seed,3.1f),
EffectiveZ*WF+300.0f,
NoiseX*WF+400.0f,
NoiseY*WF+500.0f+VoxelHash::SeedOffset(Seed,11.9f),
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:
{
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:
{
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:
{
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;
// Keep CPU prediction and the near-field HLSL on one scalar source. The helper
// preserves the legacy seed offsets, octave order, and roughness families.
return VF_FieldOwnerRoughnessCore(static_cast<float>(X),static_cast<float>(Y),
static_cast<float>(Z),static_cast<uint32>(Owner->Seed),Params.SurfaceRoughness,
Params.RoughnessFrequency,Params.VerticalScale,Params.DomainWarpStrength,
Params.DomainWarpFrequency,static_cast<uint32>(Params.RoughnessNoiseType));
}
float OwnerCaveRoughness(double X,double Y,double Z,float SurfaceSDF,