wip(standalone): GPU step 28 -- skirted near tiles on GPU (correct, but SLOWER); 16 shape workers

Luna: near skirts were CPU-only by implementation gap, not correctness. Ported with
near-resolution refinement and overflow fallback; near-probe preparation split out so
the skirt kernels no longer inline the near field (first attempt: two fxc /O3 compiles
ran 10.5 h and were stopped). Shape compile proven identical at W1/W8/W16.
-SkipCpuDetailBand retired (154/640 skirt audit failures, +174 ms planning).
Audits: hashes W1/W8/W16, walk 5,642/5,642, whole-near 960 tiles + 640 skirt faces
0 sign/topology diffs (one 0.001 cm vertex-key delta), far/thin 0 diffs, both
survivals pass with 2,074 candidates at 7.00 / 7.02 GiB (was ~6.2), ten captures pass.

My runs outside the sandbox (Step28.exe). NOTE the machine was ~25% slower than last
night for every build (step 27 CPU full machine 0.97-1.02 s at night, 1.22-1.29 s now):
  0xFF00: CPU 1.337 / 1.291 / 1.295 s; GPU miss 1.337 s;
          GPU cached 1.663 / 1.424 / 1.617 s (GPU tiles 1,427 / 2,072 / 1,222)
  0xFFFF: CPU 1.221 / 1.231 / 1.236 s (step 27 same session 1.285 / 1.222 s)
          GPU cached 1.582 / 1.550 / 1.416, 1.635 / 1.441 / 1.470 / 1.445 s
          (GPU tiles vary 593-2,074; timestamps up to ~470 ms)
=> GPU route now slower than the CPU route measured alongside it. Step 27 GPU control
   was not clean (both exes load the same precompiled .cso files).

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-08 17:07:40 +02:00
1 parent 7d1808fdf2
commit 5aa285c3ef
26 files changed
+1536 -400

No files matched your search

+9 -157
View File
@@ -66,7 +66,6 @@ public:
uint64 LocalBandSkips=0;
uint64 GpuVisualCandidates=0,CpuVisualCandidates=0,GpuOnlyCandidates=0;
uint64 GpuVisualDetailEvaluations=0,GpuOnlyDetailEvaluations=0;
uint64 GpuOnlySkippedCandidateMasks=0;
uint64 DistanceSamples=0,DistanceSampleNanoseconds=0;
uint64 DetailSamples=0,DetailSampleNanoseconds=0;
uint64 ReliefSampleNanoseconds=0,LayerLookupSampleNanoseconds=0;
@@ -282,18 +281,14 @@ public:
bool bInRoundTunnels=false,bool bInProfileDetailBand=false,
bool bValidatePassageRoutes=true,
const std::vector<VoxelForgeCore::DetailBandTileRegion>& InGpuVisualDetailTiles={},
const std::vector<VoxelForgeCore::DetailBandTileRegion>& InCpuVisualDetailTiles={},
bool bInSkipCpuDetailBandForGpuVisuals=false)
const std::vector<VoxelForgeCore::DetailBandTileRegion>& InCpuVisualDetailTiles={})
{
using Clock=std::chrono::steady_clock;
const auto TotalStart=Clock::now();
Owner=&World;WorkerCount=std::clamp(Workers,1,8);DetailVariant=Variant;
Owner=&World;WorkerCount=std::clamp(Workers,1,16);DetailVariant=Variant;
Scheduler=InScheduler;RoundTunnelsEnabled=bInRoundTunnels;
bAnalyticOnly=false;
bProfileDetailBand=bInProfileDetailBand;
bSkipCpuDetailBandForGpuVisuals=bInSkipCpuDetailBandForGpuVisuals;
if(bSkipCpuDetailBandForGpuVisuals&&InGpuVisualDetailTiles.empty())
{Error="GPU visual detail-band skipping requires at least one GPU near tile region";return false;}
for(int32 A=0;A<3;++A)
{
Min[A]=(&MinVoxels.X)[A]-2;
@@ -333,9 +328,6 @@ public:
Stats.CandidateMergeSeconds);
if(!BuildGpuVisualDetailNodeSets(InGpuVisualDetailTiles,InCpuVisualDetailTiles,Error))
return false;
GpuVisualProxyValidWords.clear();
if(bSkipCpuDetailBandForGpuVisuals)
GpuVisualProxyValidWords.assign(CandidateWords.size(),0u);
Stats.IntervalSeconds=std::chrono::duration<double>(Clock::now()-Stage).count();
Stage=Clock::now();
@@ -777,14 +769,6 @@ public:
const uint64 Mask=uint64{1}<<(Zi&63u);
if((CandidateWords[Word]&Mask)==0)
Out.nearPackedProxy[SampleIndex]=0u;
else if(IsSkippedGpuVisualDetailNode(WX,WY,WZ)
&&(GpuVisualProxyValidWords[Word]&Mask)==0)
{
// UINT_MAX asks ApplyNearFieldDetailCS to use its own exact
// GPU roughness result instead of a CPU packed proxy.
Out.nearPackedProxy[SampleIndex]=std::numeric_limits<uint32>::max();
++Out.nearPackedSkippedSampleCount;
}
else
{
++Out.nearPackedCandidateSampleCount;
@@ -933,8 +917,6 @@ public:
{
int32 IX=0,IY=0,IZ=0;
const bool bInteger=NearInteger(X,IX)&&NearInteger(Y,IY)&&NearInteger(Z,IZ);
if(bInteger&&IsSkippedGpuVisualDetailNode(IX,IY,IZ))
return GpuSkippedDetailDensityAt(IX,IY,IZ,X,Y,Z);
const bool bPacked=!bAnalyticOnly&&bInteger&&Contains(IX,IY,IZ);
if(bPacked)
return PackedDensityAt(IX,IY,IZ,IsRock(IX,IY,IZ),X,Y,Z);
@@ -943,9 +925,6 @@ public:
float DensityAtLattice(int32 X,int32 Y,int32 Z) const
{
if(IsSkippedGpuVisualDetailNode(X,Y,Z))
return GpuSkippedDetailDensityAt(X,Y,Z,static_cast<float>(X),
static_cast<float>(Y),static_cast<float>(Z));
const bool bPacked=!bAnalyticOnly&&Contains(X,Y,Z);
if(bPacked)
return PackedDensityAt(X,Y,Z,IsRock(X,Y,Z),static_cast<float>(X),
@@ -959,39 +938,6 @@ public:
return AnalyticDensityAt(X,Y,Z,false);
}
float GpuSkippedDetailDensityAt(int32 X,int32 Y,int32 Z,
float FX,float FY,float FZ) const
{
const size_t Column=static_cast<size_t>(Y-Min[1])*Size[0]
+static_cast<size_t>(X-Min[0]);
const size_t Zi=static_cast<size_t>(Z-Min[2]);
const size_t Word=Column*WordsPerZ+(Zi>>6);
const uint64 Mask=uint64{1}<<(Zi&63u);
if(!GpuVisualProxyValidWords.empty()&&(GpuVisualProxyValidWords[Word]&Mask))
return PackedDensityAt(X,Y,Z,IsRock(X,Y,Z),FX,FY,FZ);
if(CandidateWords[Word]&Mask)
{
// The GPU-only compile omitted this packed cache entry. Reconstruct the
// CPU reference's un-clamped detail density and the same signed Q8.8
// proxy when an exceptional CPU mesh or field query needs the sample.
const float ExactDensity=AnalyticDensityAt(FX,FY,FZ,false,
nullptr,nullptr,nullptr,false);
int32 Q=static_cast<int32>(std::lround(ExactDensity*256.0f));
Q=std::clamp(Q,-32767,32767);
if(Q==0)Q=ExactDensity<0.0f?-1:1;
float Density=static_cast<float>(Q)*(1.0f/256.0f);
const bool bRock=ExactDensity<=0.0f;
if((Density<0.0f)!=bRock)
Density=(bRock?-1.0f:1.0f)*std::max(std::abs(Density),1.0f/256.0f);
return ApplyEditBrushes(Density,FX,FY,FZ);
}
const bool bRock=((RockWords[Word]&Mask)!=0);
constexpr float ProxyBandEdge=DetailSupremum+0.125f;
return ApplyEditBrushes(bRock?-ProxyBandEdge:ProxyBandEdge,FX,FY,FZ);
}
VoxelForgeCore::DetailFieldSample DetailSampleAt(float X,float Y,float Z) const
{
VoxelForgeCore::DetailFieldSample Sample;
@@ -1018,18 +964,6 @@ public:
OutDensity[Lane]=DensityAtLattice(XStart+Lane*XStep,Y,Z);
return;
}
if(bSkipCpuDetailBandForGpuVisuals)
{
bool bContainsSkippedNode=false;
for(int32 Lane=0;Lane<Count;++Lane)
bContainsSkippedNode|=IsSkippedGpuVisualDetailNode(XStart+Lane*XStep,Y,Z);
if(bContainsSkippedNode)
{
for(int32 Lane=0;Lane<Count;++Lane)
OutDensity[Lane]=DensityAtLattice(XStart+Lane*XStep,Y,Z);
return;
}
}
bool bFast=EditBrushes.empty()&&Y>=Min[1]&&Y<=Max[1]
&&Z>=Min[2]&&Z<=Max[2]
&&XStart>=Min[0]
@@ -1185,9 +1119,6 @@ public:
bool IsRock(int32 X,int32 Y,int32 Z) const
{
if(IsSkippedGpuVisualDetailNode(X,Y,Z))
return AnalyticDensityAt(static_cast<float>(X),static_cast<float>(Y),
static_cast<float>(Z))<=0.0f;
if(!Contains(X,Y,Z))return EvaluateFast(X,Y,Z);
const size_t Line=static_cast<size_t>(Y-Min[1])*Size[0]
+static_cast<size_t>(X-Min[0]);
@@ -3342,17 +3273,14 @@ private:
if(GpuTiles.empty()&&CpuTiles.empty())
{
GpuVisualDetailWords.clear();CpuVisualDetailWords.clear();
CpuVisualDetailGuardWords.clear();
if(bProfileDetailBand)
{
GpuVisualDetailWords.assign(CandidateWords.size(),0u);
CpuVisualDetailWords.assign(CandidateWords.size(),0u);
CpuVisualDetailGuardWords.assign(CandidateWords.size(),0u);
}
GpuVisualDetailTileCount=CpuVisualDetailTileCount=0;
GpuVisualDetailGridSamples=CpuVisualDetailGridSamples=0;
GpuVisualDetailNodeCount=CpuVisualDetailNodeCount=0;
CpuVisualDetailGuardNodeCount=0;
return true;
}
const auto BuildSet=[&](const std::vector<VoxelForgeCore::DetailBandTileRegion>& Tiles,
@@ -3403,55 +3331,16 @@ private:
GpuVisualDetailNodeCount=0;CpuVisualDetailNodeCount=0;
for(const uint64 Bits:GpuVisualDetailWords)GpuVisualDetailNodeCount+=std::popcount(Bits);
for(const uint64 Bits:CpuVisualDetailWords)CpuVisualDetailNodeCount+=std::popcount(Bits);
CpuVisualDetailGuardWords.assign(CandidateWords.size(),0u);
if(bSkipCpuDetailBandForGpuVisuals&&!CpuTiles.empty())
{
constexpr int32 AxisOffsets[6][3]={{-1,0,0},{1,0,0},{0,-1,0},
{0,1,0},{0,0,-1},{0,0,1}};
for(size_t Word=0;Word<CpuVisualDetailWords.size();++Word)
{
uint64 Bits=CpuVisualDetailWords[Word];
const size_t Column=Word/WordsPerZ;
const int32 X=Min[0]+static_cast<int32>(Column%Size[0]);
const int32 Y=Min[1]+static_cast<int32>(Column/Size[0]);
const size_t ZiBase=(Word%WordsPerZ)*64u;
while(Bits)
{
const uint32 Bit=std::countr_zero(Bits);Bits&=Bits-1;
const size_t Zi=ZiBase+Bit;
if(Zi>=static_cast<size_t>(Size[2]))continue;
const int32 Z=Min[2]+static_cast<int32>(Zi);
for(const auto& Offset:AxisOffsets)
{
const int32 GX=X+Offset[0],GY=Y+Offset[1],GZ=Z+Offset[2];
if(GX<Min[0]||GX>Max[0]||GY<Min[1]||GY>Max[1]
||GZ<Min[2]||GZ>Max[2])continue;
const size_t GuardColumn=static_cast<size_t>(GY-Min[1])*Size[0]
+static_cast<size_t>(GX-Min[0]);
const size_t GuardZi=static_cast<size_t>(GZ-Min[2]);
const size_t GuardWord=GuardColumn*WordsPerZ+(GuardZi>>6);
const uint64 GuardMask=uint64{1}<<(GuardZi&63u);
if(CandidateWords[GuardWord]&GuardMask)
CpuVisualDetailGuardWords[GuardWord]|=GuardMask;
}
}
}
}
CpuVisualDetailGuardNodeCount=0;
for(const uint64 Bits:CpuVisualDetailGuardWords)
CpuVisualDetailGuardNodeCount+=std::popcount(Bits);
GpuVisualDetailTileCount=GpuTiles.size();CpuVisualDetailTileCount=CpuTiles.size();
if(!GpuTiles.empty()||!CpuTiles.empty())
{
std::printf("[GpuVisualDetailBandPlan] gpu_tiles=%llu gpu_sample_nodes=%llu gpu_unique_candidate_nodes=%llu cpu_near_tiles=%llu cpu_sample_nodes=%llu cpu_unique_candidate_nodes=%llu cpu_boundary_guard_candidate_nodes=%llu skip_enabled=%d\n",
std::printf("[GpuVisualDetailBandPlan] gpu_tiles=%llu gpu_sample_nodes=%llu gpu_unique_candidate_nodes=%llu cpu_near_tiles=%llu cpu_sample_nodes=%llu cpu_unique_candidate_nodes=%llu\n",
static_cast<unsigned long long>(GpuVisualDetailTileCount),
static_cast<unsigned long long>(GpuVisualDetailGridSamples),
static_cast<unsigned long long>(GpuVisualDetailNodeCount),
static_cast<unsigned long long>(CpuVisualDetailTileCount),
static_cast<unsigned long long>(CpuVisualDetailGridSamples),
static_cast<unsigned long long>(CpuVisualDetailNodeCount),
static_cast<unsigned long long>(CpuVisualDetailGuardNodeCount),
bSkipCpuDetailBandForGpuVisuals?1:0);
static_cast<unsigned long long>(CpuVisualDetailNodeCount));
}
return true;
}
@@ -3476,25 +3365,6 @@ private:
&(uint64{1}<<(Zi&63u)))!=0;
}
bool IsSkippedGpuVisualDetailWord(size_t Word,uint64 Mask) const
{
return bSkipCpuDetailBandForGpuVisuals
&&(GpuVisualDetailWords[Word]&Mask)!=0
&&(CpuVisualDetailWords[Word]&Mask)==0
&&(CpuVisualDetailGuardWords[Word]&Mask)==0;
}
bool IsSkippedGpuVisualDetailNode(int32 X,int32 Y,int32 Z) const
{
if(!bSkipCpuDetailBandForGpuVisuals||!Contains(X,Y,Z)
||GpuVisualDetailWords.empty())return false;
const size_t Column=static_cast<size_t>(Y-Min[1])*Size[0]
+static_cast<size_t>(X-Min[0]);
const size_t Zi=static_cast<size_t>(Z-Min[2]);
return IsSkippedGpuVisualDetailWord(Column*WordsPerZ+(Zi>>6),
uint64{1}<<(Zi&63u));
}
void ApplyDetailBand(FParallelTiming& Timing)
{
using Clock=std::chrono::steady_clock;
@@ -3566,25 +3436,16 @@ private:
if(std::abs(SDF)>DetailSupremum+0.125f)continue;
const size_t VisualWordIndex=WordBase+(Zi>>6);
const uint64 VisualMask=uint64{1}<<(Zi&63u);
const bool bGpuVisualNode=(bProfile||bSkipCpuDetailBandForGpuVisuals)
const bool bGpuVisualNode=bProfile
&&(GpuVisualDetailWords[VisualWordIndex]&VisualMask)!=0;
const bool bCpuVisualNode=(bProfile||bSkipCpuDetailBandForGpuVisuals)
const bool bCpuVisualNode=bProfile
&&(CpuVisualDetailWords[VisualWordIndex]&VisualMask)!=0;
const bool bSkipDetail=bSkipCpuDetailBandForGpuVisuals&&bGpuVisualNode
&&!bCpuVisualNode
&&(CpuVisualDetailGuardWords[VisualWordIndex]&VisualMask)==0;
if(bProfile)
{
if(bGpuVisualNode)++Profile.GpuVisualCandidates;
if(bCpuVisualNode)++Profile.CpuVisualCandidates;
if(bGpuVisualNode&&!bCpuVisualNode)++Profile.GpuOnlyCandidates;
}
if(bSkipDetail)
{
ValidBits|=uint64{1}<<(Zi&63u);
if(bProfile)++Profile.GpuOnlySkippedCandidateMasks;
continue;
}
const float SurfaceSDF=Sample.SurfaceSDF;
const float CaveInfluence=Sample.CaveInfluence;
const float FloorInfluence=Sample.FloorInfluence;
@@ -3681,7 +3542,7 @@ private:
uint64 LayerLookupSampleNs=0,OwnerSampleNs=0,OwnerFieldCalls=0,OwnerActiveCalls=0;
uint64 OwnerCacheHits=0,OwnerCacheMisses=0;
uint64 GpuVisualCandidates=0,CpuVisualCandidates=0,GpuOnlyCandidates=0;
uint64 GpuVisualDetailCalls=0,GpuOnlyDetailCalls=0,GpuOnlySkippedMasks=0;
uint64 GpuVisualDetailCalls=0,GpuOnlyDetailCalls=0;
for(int32 Worker=0;Worker<WorkerCount;++Worker)
{
const FDetailWorkerProfile& P=WorkerProfile[static_cast<size_t>(Worker)];
@@ -3701,7 +3562,6 @@ private:
GpuOnlyCandidates+=P.GpuOnlyCandidates;
GpuVisualDetailCalls+=P.GpuVisualDetailEvaluations;
GpuOnlyDetailCalls+=P.GpuOnlyDetailEvaluations;
GpuOnlySkippedMasks+=P.GpuOnlySkippedCandidateMasks;
}
const double DistanceAverageNs=DistanceSamples?static_cast<double>(DistanceSampleNs)/DistanceSamples:0.0;
const double DetailAverageNs=DetailSamples?static_cast<double>(DetailSampleNs)/DetailSamples:0.0;
@@ -3726,14 +3586,12 @@ private:
static_cast<unsigned long long>(OwnerCacheHits),
static_cast<unsigned long long>(OwnerCacheMisses),
static_cast<unsigned long long>(LocalBandSkips));
std::printf("[GpuDetailConsumerProfile] gpu_near_proxy_nodes=%llu cpu_near_mesh_nodes=%llu gpu_only_nodes=%llu gpu_near_detail_noise_evaluations=%llu gpu_only_detail_noise_evaluations=%llu gpu_only_skipped_candidate_masks=%llu skip_enabled=%d\n",
std::printf("[GpuDetailConsumerProfile] gpu_near_proxy_nodes=%llu cpu_near_mesh_nodes=%llu gpu_only_nodes=%llu gpu_near_detail_noise_evaluations=%llu gpu_only_detail_noise_evaluations=%llu\n",
static_cast<unsigned long long>(GpuVisualCandidates),
static_cast<unsigned long long>(CpuVisualCandidates),
static_cast<unsigned long long>(GpuOnlyCandidates),
static_cast<unsigned long long>(GpuVisualDetailCalls),
static_cast<unsigned long long>(GpuOnlyDetailCalls),
static_cast<unsigned long long>(GpuOnlySkippedMasks),
bSkipCpuDetailBandForGpuVisuals?1:0);
static_cast<unsigned long long>(GpuOnlyDetailCalls));
for(int32 Worker=0;Worker<WorkerCount;++Worker)
{
const FDetailWorkerProfile& P=WorkerProfile[static_cast<size_t>(Worker)];
@@ -3769,8 +3627,6 @@ private:
+static_cast<size_t>(X-Min[0]);
StoreSurfaceDistance(Node,Density);
CandidateWords[Word]|=Mask;
if(IsSkippedGpuVisualDetailNode(X,Y,Z))
GpuVisualProxyValidWords[Word]|=Mask;
++AddedThisPass;
const bool bExactRock=Density<0.0f;
@@ -4821,14 +4677,10 @@ private:
std::unique_ptr<int16[]> SurfaceDistanceQ;
std::array<std::vector<uint64>,3> CandidateAxisWords;
std::vector<uint64> CandidateWords;
bool bSkipCpuDetailBandForGpuVisuals=false;
std::vector<uint64> GpuVisualDetailWords,CpuVisualDetailWords;
std::vector<uint64> CpuVisualDetailGuardWords;
uint64 GpuVisualDetailTileCount=0,CpuVisualDetailTileCount=0;
uint64 GpuVisualDetailGridSamples=0,CpuVisualDetailGridSamples=0;
uint64 GpuVisualDetailNodeCount=0,CpuVisualDetailNodeCount=0;
uint64 CpuVisualDetailGuardNodeCount=0;
std::vector<uint64> GpuVisualProxyValidWords;
FBuildStats Stats;
bool bAnalyticOnly=false;
};