wip(standalone): GPU step 5 -- far GPU work 646 -> 112 ms; hybrid whole view equal to CPU

Whole far field on the RX 7900 XTX: 112 ms total (connector rings 61 ms). Cold hybrid 2.303 s vs
all-CPU 2.314 s (within noise): the far field is no longer on the wall-clock critical path.
0 sign/topology differences vs CPU and WARP incl. 127,401 protected and 353 thin-feature cells;
repeat GPU runs identical; far survival passes on GPU meshes; CPU hashes unchanged.
Open: CPU/GPU vertex distance mean 1.15 mm but max 64.56 cm, 247 tiles over the quantisation bound.

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 13:50:07 +02:00
1 parent 247682a8e0
commit 0e730cdbba
9 files changed
+697 -260

No files matched your search

+56 -47
View File
@@ -20,6 +20,7 @@
#include <dxgi.h>
#include <windowsx.h>
#include <atomic>
#include <bit>
#include <complex>
#include <cstdio>
@@ -35,6 +36,7 @@
#include <numeric>
#include <optional>
#include <sstream>
#include <thread>
#include <unordered_map>
#include <unordered_set>
@@ -2227,6 +2229,55 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
return ResidentTiles.contains(Key);
}
void PrepareGpuFarInputs(std::vector<FVoxelTileKey> NeedPreparation)
{
if(NeedPreparation.empty())return;
const auto PrepareStart=std::chrono::steady_clock::now();
std::vector<std::optional<VoxelForgeCore::FarFieldTileInput>> Inputs(NeedPreparation.size());
std::vector<std::string> Errors(NeedPreparation.size());
const std::size_t ThreadCount=std::min<std::size_t>(8,NeedPreparation.size());
std::atomic<std::size_t> Next{0};
std::vector<double> WorkerElapsed(ThreadCount,0.0);
std::vector<std::thread> Workers;Workers.reserve(ThreadCount);
for(std::size_t Worker=0;Worker<ThreadCount;++Worker)
{
Workers.emplace_back([&,Worker]
{
const auto WorkerStart=std::chrono::steady_clock::now();
for(;;)
{
const std::size_t Index=Next.fetch_add(1,std::memory_order_relaxed);
if(Index>=NeedPreparation.size())break;
const FVoxelTileKey& Key=NeedPreparation[Index];
const VoxelForgeCore::TileKey CoreKey{{Key.Coord.X,Key.Coord.Y,Key.Coord.Z},Key.Level};
try { Inputs[Index]=ShapeWorld->exportFarFieldTile(CoreKey,Errors[Index]); }
catch(const std::exception& Error) { Errors[Index]=Error.what(); }
}
WorkerElapsed[Worker]=std::chrono::duration<double,std::milli>(
std::chrono::steady_clock::now()-WorkerStart).count();
});
}
for(std::thread& Worker:Workers)Worker.join();
const double PreparationMs=std::chrono::duration<double,std::milli>(
std::chrono::steady_clock::now()-PrepareStart).count();
double WorkerSumMs=0.0;for(const double Elapsed:WorkerElapsed)WorkerSumMs+=Elapsed;
GpuFarPreparationWallMs+=PreparationMs;GpuFarPreparationWorkerMs+=WorkerSumMs;
std::size_t Failed=0;
for(std::size_t Index=0;Index<NeedPreparation.size();++Index)
{
if(Inputs[Index])GpuFarPreparedInputs.emplace(NeedPreparation[Index],std::move(*Inputs[Index]));
else
{
++Failed;
GpuFarPreparationErrors.emplace(NeedPreparation[Index],Errors[Index].empty()
?"far-field input export failed":Errors[Index]);
}
}
std::printf("[GpuFarInputPrepare] tiles=%zu workers=%zu wall_ms=%.3f worker_sum_ms=%.3f failed=%zu\n",
NeedPreparation.size(),ThreadCount,PreparationMs,WorkerSumMs,Failed);
std::fflush(stdout);
}
void ScheduleGpuFarTiles(const std::vector<FVoxelTileKey>& Keys)
{
GpuFarQueue.clear();GpuFarQueued.clear();GpuFarEligibleTiles.clear();
@@ -2270,52 +2321,7 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
for(const FVoxelTileKey& Key:Eligible)
if(!GpuFarTiles.contains(Key))NeedPreparation.push_back(Key);
if(!NeedPreparation.empty())
{
const auto PrepareStart=std::chrono::steady_clock::now();
std::vector<std::optional<VoxelForgeCore::FarFieldTileInput>> Inputs(NeedPreparation.size());
std::vector<std::string> Errors(NeedPreparation.size());
const std::size_t ThreadCount=std::min<std::size_t>(8,NeedPreparation.size());
std::atomic<std::size_t> Next{0};
std::vector<double> WorkerElapsed(ThreadCount,0.0);
std::vector<std::thread> Workers;Workers.reserve(ThreadCount);
for(std::size_t Worker=0;Worker<ThreadCount;++Worker)
{
Workers.emplace_back([&,Worker]
{
const auto WorkerStart=std::chrono::steady_clock::now();
for(;;)
{
const std::size_t Index=Next.fetch_add(1,std::memory_order_relaxed);
if(Index>=NeedPreparation.size())break;
const FVoxelTileKey& Key=NeedPreparation[Index];
const VoxelForgeCore::TileKey CoreKey{{Key.Coord.X,Key.Coord.Y,Key.Coord.Z},Key.Level};
try { Inputs[Index]=ShapeWorld->exportFarFieldTile(CoreKey,Errors[Index]); }
catch(const std::exception& Error) { Errors[Index]=Error.what(); }
}
WorkerElapsed[Worker]=std::chrono::duration<double,std::milli>(
std::chrono::steady_clock::now()-WorkerStart).count();
});
}
for(std::thread& Worker:Workers)Worker.join();
const double PreparationMs=std::chrono::duration<double,std::milli>(
std::chrono::steady_clock::now()-PrepareStart).count();
double WorkerSumMs=0.0;for(const double Elapsed:WorkerElapsed)WorkerSumMs+=Elapsed;
GpuFarPreparationWallMs+=PreparationMs;GpuFarPreparationWorkerMs+=WorkerSumMs;
std::size_t Failed=0;
for(std::size_t Index=0;Index<NeedPreparation.size();++Index)
{
if(Inputs[Index])GpuFarPreparedInputs.emplace(NeedPreparation[Index],std::move(*Inputs[Index]));
else
{
++Failed;
GpuFarPreparationErrors.emplace(NeedPreparation[Index],Errors[Index].empty()
?"far-field input export failed":Errors[Index]);
}
}
std::printf("[GpuFarInputPrepare] tiles=%zu workers=%zu wall_ms=%.3f worker_sum_ms=%.3f failed=%zu\n",
NeedPreparation.size(),ThreadCount,PreparationMs,WorkerSumMs,Failed);
std::fflush(stdout);
}
PrepareGpuFarInputs(std::move(NeedPreparation));
for(const FVoxelTileKey& Key:Eligible)
{
GpuFarEligibleTiles.insert(Key);
@@ -3170,7 +3176,10 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
const UINT Stride=sizeof(FGpuVertex),Offset=0;Context->IASetVertexBuffers(0,1,&L.VertexBuffer,&Stride,&Offset);
Context->IASetIndexBuffer(L.IndexBuffer,DXGI_FORMAT_R32_UINT,0);Context->DrawIndexed(L.IndexCount,0,0);
}
if(bGpuFarEnabled&&ShapeWorld&&ShapeWorld->hasFarWorld())
// Avoid issuing hundreds of tiny indirect draw calls while the
// initial far batches are still being built. Once the clipmap is
// ready the pooled GPU meshes draw directly as before.
if(bGpuFarEnabled&&bInitialClipmapReported&&ShapeWorld&&ShapeWorld->hasFarWorld())
{
C.Padding[0]=0.01f;
Context->UpdateSubresource(Constants,0,nullptr,&C,0,0);