wip(standalone): GPU step 15 -- cold-start drain: hybrid whole view 3.2 -> 1.25-1.68 s

Timeline showed the post-export window was frame-paced: the 4-batch / 2 ms
per-frame upload budget held CPU-fallback tiles up to 755 ms after they
finished, and 35 GPU batches drained one per frame. Before the first whole
view, GPU work now drains as one batch and CPU fallback starts with GPU input
preparation (--gpu-far-no-cold-drain = paced control).

Luna's race: CPU 1.393 s vs GPU 1.677 s. My interleaved runs (Step15Timeline,
RunGpuFarViewer): GPU 2.902 (first launch) / 1.252 / 1.209 s; CPU 2.196 /
2.250 / 2.061 s. The CPU viewer still uses the paced upload budget at cold
start (stream 674 -> 2250 ms in my runs vs 741 -> 1422 in Luna's), so the
race is not yet fair. Audits: hashes, walk 5,642/5,642, 0 diffs vs CPU/WARP.

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-07 08:47:36 +02:00
1 parent f322b1c089
commit c152708a67
16 files changed
+3378 -86

No files matched your search

+47 -3
View File
@@ -6,6 +6,7 @@
#include <condition_variable>
#include <algorithm>
#include <array>
#include <atomic>
#include <bit>
#include <chrono>
#include <cstdint>
@@ -41,6 +42,8 @@ struct FStandaloneCpuTile
uint32 SkirtTriangleCount=0;
uint64 DensitySamples=0;
double WorkerSeconds=0.0;
std::chrono::steady_clock::time_point QueuedAt{};
std::chrono::steady_clock::time_point StartedAt{};
std::chrono::steady_clock::time_point CompletedAt{};
uint64 DensityHash=14695981039346656037ull;
bool bHasDensityHash=false;
@@ -340,6 +343,8 @@ struct FStandaloneTileJob
int32 BandZMax=MAX_int32;
uint64 RequestedCrossing=0;
size_t PriorityRank=0;
std::chrono::steady_clock::time_point QueuedAt{};
std::chrono::steady_clock::time_point StartedAt{};
};
struct FStandaloneTileJobHeapOrder
@@ -421,6 +426,14 @@ public:
FStandaloneStreamingGenerator& operator=(const FStandaloneStreamingGenerator&)=delete;
~FStandaloneStreamingGenerator(){Stop();}
void EnableColdTimelineTrace(std::chrono::steady_clock::time_point Origin,
const std::atomic<uint64>* FrameOrdinal)
{
TimelineOrigin=Origin;
TimelineFrameOrdinal=FrameOrdinal;
bColdTimelineTrace=true;
}
FStandaloneStreamingUpdate Start(const FVector& CameraPositionVoxels,
const std::vector<FVoxelTileKey>& ResidentKeys={},
uint64 CrossingIndex=0)
@@ -540,6 +553,7 @@ private:
auto QueueEnd=std::remove_if(Queue.begin(),Queue.end(),[&](const FStandaloneTileJob& Job)
{
if(Desired.contains(Job.Key))return false;
if(bColdTimelineTrace)TraceTileEvent("cancelled",Job.Key,FClock::now(),Job.QueuedAt);
Outstanding.erase(Job.Key);++Stats.CancelledQueuedJobs;
++CancelledByCrossing[Job.RequestedCrossing];return true;
});
@@ -568,7 +582,12 @@ private:
for(const FVoxelTileKey& Key:NewDesired.Tiles)
{
if(Resident.contains(Key)||Outstanding.contains(Key))continue;
Queue.push_back({Key,BandMin,BandMax,CrossingIndex,PriorityRanks.at(Key)});
FStandaloneTileJob Job;
Job.Key=Key;Job.BandZMin=BandMin;Job.BandZMax=BandMax;
Job.RequestedCrossing=CrossingIndex;Job.PriorityRank=PriorityRanks.at(Key);
Job.QueuedAt=FClock::now();
Queue.push_back(Job);
if(bColdTimelineTrace)TraceTileEvent("queued",Key,Job.QueuedAt);
Outstanding.insert(Key);++Stats.NewJobs;
}
CurrentDesired=std::move(NewDesired);
@@ -578,8 +597,9 @@ private:
}
FStandaloneCpuTile Generate(const FStandaloneTileJob& Job)
{
const auto StartTime=FClock::now();
const auto StartTime=Job.StartedAt;
FStandaloneCpuTile Out;Out.Key=Job.Key;Out.RequestedCrossing=Job.RequestedCrossing;
Out.QueuedAt=Job.QueuedAt;Out.StartedAt=Job.StartedAt;
if(MeshCache)
{
if(const FStandaloneCpuTile* Cached=MeshCache->Find(Job.Key))
@@ -718,9 +738,12 @@ private:
Wake.wait(Lock,[&](){return bStop||!Queue.empty();});
if(bStop)return;
std::pop_heap(Queue.begin(),Queue.end(),FStandaloneTileJobHeapOrder{});
Job=Queue.back();Queue.pop_back();
Job=Queue.back();Queue.pop_back();Job.StartedAt=FClock::now();
}
if(bColdTimelineTrace)TraceTileEvent("started",Job.Key,Job.StartedAt,Job.QueuedAt,Job.StartedAt);
FStandaloneCpuTile Tile=Generate(Job);
if(bColdTimelineTrace)TraceTileEvent("finished",Tile.Key,Tile.CompletedAt,
Tile.QueuedAt,Tile.StartedAt,Tile.CompletedAt,Tile.WorkerSeconds*1000.0);
{
std::lock_guard<std::mutex> Lock(Mutex);
if(bStop)return;
@@ -730,6 +753,24 @@ private:
}
using FClock=std::chrono::steady_clock;
double TimelineMilliseconds(FClock::time_point At)const
{
return At.time_since_epoch().count()==0?-1.0:
std::chrono::duration<double,std::milli>(At-TimelineOrigin).count();
}
uint64 TimelineFrame()const
{
return TimelineFrameOrdinal?TimelineFrameOrdinal->load(std::memory_order_relaxed):0;
}
void TraceTileEvent(const char* Event,const FVoxelTileKey& Key,FClock::time_point At,
FClock::time_point Queued={},FClock::time_point Started={},FClock::time_point Finished={},
double WorkerMs=-1.0)const
{
std::printf("[CpuTileTimeline] event=%s key=(%d,%d,%d,lod%d) at_ms=%.3f frame=%llu queued_ms=%.3f started_ms=%.3f finished_ms=%.3f worker_ms=%.3f\n",
Event,Key.Coord.X,Key.Coord.Y,Key.Coord.Z,Key.Level,TimelineMilliseconds(At),
static_cast<unsigned long long>(TimelineFrame()),TimelineMilliseconds(Queued),
TimelineMilliseconds(Started),TimelineMilliseconds(Finished),WorkerMs);
}
FStandaloneOwnerWorld* World=nullptr;
int32 WorkerCount=8;
bool bHashDensity=false;
@@ -753,4 +794,7 @@ private:
bool bUseTileFilter=false;
FStandaloneDesiredTileSet CurrentDesired;
int32 FullResTileCount=0;
bool bColdTimelineTrace=false;
FClock::time_point TimelineOrigin{};
const std::atomic<uint64>* TimelineFrameOrdinal=nullptr;
};