wip(standalone): GPU step 22 -- startup multi-batch pump, pool before handoff: cached 1.12-1.20 s

Per-batch GPU timeline (submit, timestamps, query, consume). Startup Draws submit
several ready batches per pump (80 ms startup budget, 16 ms per batch); the output
pool is reserved before the warm-up-pending deferral.
My runs outside the sandbox (Step22.exe, marker removed first, interleaved):
  CPU 1.295 / 1.314 / 1.284 / 1.342 s
  GPU cached (marker match, -Timeline): 1.197 / 1.121 / 1.133 s
      (1,124 / 1,380 / 1,415 GPU tiles; output pool at 655-680 ms in all three)
  GPU marker miss: 1.446 (-Timeline), 1.447, 1.312 s (no -Timeline). The 1.447 run's
      shape compile ended at 776 ms vs 634-643 ms in its neighbours, so this is
      startup variance, not a clear regression; to be watched.
Known tradeoff: startup pumps can stall the main thread 65-108 ms (submit calls run
past the budget); fine before the first view, to check in an interactive viewer.
Audits: hashes w1/w8, walk 5,642/5,642, near 960 / far 1,114 0 diffs vs CPU and
WARP, thin 353, survival ~6.2 GiB, ten captures as step 18.

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 00:00:06 +02:00
1 parent 38f6009200
commit 052b6de354
7 files changed
+274 -43

No files matched your search

@@ -64,7 +64,8 @@ namespace VoxelForgeGpuFarViewer
const std::filesystem::path& shaderPath,ID3D11Query** completionEvent,
GpuTimingQueries* timingQueries,std::string& error);
bool ResolveGpuTiming(ID3D11DeviceContext* context,GpuTimingQueries& queries,
double& dispatchMs,std::uint64_t* timestampFrequency=nullptr);
double& dispatchMs,std::uint64_t* timestampFrequency=nullptr,
std::uint64_t* startTimestamp=nullptr,std::uint64_t* endTimestamp=nullptr);
bool ResolveGpuTimestampPair(ID3D11DeviceContext* context,GpuTimingQueries& queries,
std::uint64_t timestampFrequency,double& dispatchMs);
}
@@ -3512,10 +3512,13 @@ namespace VoxelForgeGpuFarViewer
}
bool ResolveGpuTiming(ID3D11DeviceContext* context,GpuTimingQueries& queries,double& dispatchMs,
std::uint64_t* timestampFrequency)
std::uint64_t* timestampFrequency,std::uint64_t* startTimestamp,
std::uint64_t* endTimestamp)
{
dispatchMs=0.0;
if(timestampFrequency)*timestampFrequency=0;
if(startTimestamp)*startTimestamp=0;
if(endTimestamp)*endTimestamp=0;
if(!context||!queries.start||!queries.end||!queries.disjoint)return true;
D3D11_QUERY_DATA_TIMESTAMP_DISJOINT disjoint{};UINT64 start=0,end=0;
HRESULT hr=context->GetData(queries.disjoint,&disjoint,sizeof(disjoint),D3D11_ASYNC_GETDATA_DONOTFLUSH);
@@ -3528,6 +3531,11 @@ namespace VoxelForgeGpuFarViewer
if(hr==S_FALSE)return false;
Check(hr,"GetData(async end timestamp)");
if(timestampFrequency&&!disjoint.Disjoint)*timestampFrequency=disjoint.Frequency;
if(!disjoint.Disjoint)
{
if(startTimestamp)*startTimestamp=start;
if(endTimestamp)*endTimestamp=end;
}
if(!disjoint.Disjoint&&disjoint.Frequency)
dispatchMs=1000.0*static_cast<double>(end-start)/static_cast<double>(disjoint.Frequency);
queries.start->Release();queries.start=nullptr;
@@ -42,3 +42,17 @@ The numeric capture comparison reports are [Step 19](step19_final_capture_compar
Step 21 moves GPU input export onto the existing eight-worker streaming pool. Each worker takes up to three queued GPU input tasks while CPU tile jobs are pending, then one CPU tile job, so GPU inputs start first while CPU meshing retains service. The no-parity-change path still keeps candidates on CPU until warm-up completes on a first-use marker miss. In the takeover diagnostic, output-pool completion was 1,447.248 ms, input workers started 1.576 ms after queueing, the first input was ready 27.7 ms later, and input export finished in 203.286 ms while CPU meshing continued. The run submitted 1,418 GPU tiles and reached whole-view-ready in 2.120 s including cold shader warm-up. The slowest measured tile was `(-2,-13,-4,lod0)` at 58.072 ms; it was a 32-cell near-detail input with no protected/thin samples, skirts, or connectors. The stage-level cause is still unknown.
Step 21 passed the CPU W1/W8 reference checks, whole-near and whole-far CPU/GPU/WARP topology checks, GPU survival audit, and ten-view capture comparison. The capture background counts match the Step 18 baseline (0 in eight views, 2 in each of `remote_yaw180` and `remote_yaw270`); peak measured private plus DXGI-local memory across the capture run was 6.30 GB. Forced-cached whole-view times were 2.068 and 2.066 s, but both used CPU fallback, so the ≤1.0 s cached-GPU target remains unverified in this sandbox. See [STEP21_REPORT.md](STEP21_REPORT.md) for the timeline, exact audit results, and limits.
## Step 22
- `Research\OwnerShapeWorld\gpu_field_port\BuildGpuFieldViewer.ps1 -OutputName VoxelForgeGpuFarStep22.exe` — builds the viewer with batch timestamps and early output-pool reservation.
- `Research\OwnerShapeWorld\gpu_field_port\BuildGpuFieldPort.ps1 -OutputName VoxelForgeGpuFarAgreementStep22.exe` — builds the hardware/WARP agreement runner.
- `Research\OwnerShapeWorld\gpu_field_port\RunGpuFarViewer.ps1 -Mode gpu -WarmupPolicy cold -NoColdDrain -Timeline -ExecutableName VoxelForgeGpuFarStep22.exe -LogStem step22_forced_cold` — checks the first-use CPU route; expect the CPU whole view before the deferred warm-up handoff and zero GPU submissions. Its output-pool reservation happens after the first CPU view by design.
- `Research\OwnerShapeWorld\gpu_field_port\RunGpuFarViewer.ps1 -Mode gpu -WarmupBeforeWorkers -NoColdDrain -Timeline -ExecutableName VoxelForgeGpuFarStep22.exe -LogStem step22_batch_timeline` — records each batch's CPU readiness and submit interval, GPU timestamp ticks/start/end, completion observation, and consumption. The warm-up-before-workers route is a GPU-throughput diagnostic, not a cached-launch timing.
- `Research\OwnerShapeWorld\gpu_field_port\RunGpuFarViewer.ps1 -Mode gpu -WarmupPolicy auto -NoColdDrain -Timeline -ExecutableName VoxelForgeGpuFarStep22.exe -LogStem step22_auto_owner` — use outside the sandbox for the owner's cached-path measurement. Check `cache_marker=match`, nonzero GPU submissions, the batch timeline, and the whole-view marker; a marker match alone does not mean the driver's warm-up queue was ready before CPU workers claimed the candidates.
- `Research\OwnerShapeWorld\gpu_field_port\RunGpuFarViewer.ps1 -Mode audit -Timeline -ExecutableName VoxelForgeGpuFarStep22.exe -LogStem step22_gpu_survival` — runs the full-residency GPU survival audit with required field readback. Pass when `[GpuFarSurvivalAudit]` and `[FarSurvivalAudit]` report `PASS`, `[InitialClipmapReady]` appears, exit is 0, and private plus DXGI-local memory stays under 8 GB. This is the route verified for Step 22.
- `Research\OwnerShapeWorld\gpu_field_port\RunGpuFarAgreement.ps1 -ExecutableName VoxelForgeGpuFarAgreementStep22.exe -WholeNear -LogStem step22_whole_near` (repeat with `-WholeFar`) — compares CPU, hardware GPU, and WARP; require zero sign/topology, protected-cell, and thin-feature differences.
- `Research\OwnerShapeWorld\gpu_field_port\RunStep15CpuReference.ps1 -ExecutableName VoxelForgeGpuFarStep22.exe -LogPrefix step22_cpu_reference` — checks the W1/W8 hashes and 5,642/5,642 walkability.
- `Research\OwnerShapeWorld\gpu_field_port\RunStep15CaptureMatrix.ps1 -ExecutableName VoxelForgeGpuFarStep22.exe -Prefix <run> -GpuWarmupBeforeWorkers` then `py -3 Research\OwnerShapeWorld\gpu_field_port\CompareStep15Captures.py --prefix <run>` — captures ten full-residency CPU/GPU pairs and compares numeric RGB arrays. Do not open the full-size captures in an image viewer. Step 22's numeric result is [step22_final_capture_comparison.md](step22_final_capture_comparison.md).
Step 22's batch trace showed that event-query completion was followed by resident consumption within 0.012–0.120 ms, with no output copy/upload. In the final diagnostic trace, batches 0–1 were GPU-contiguous, followed by one 38.742 ms GPU gap; batch 2 became CPU-ready at 1662.063 ms and submitted at 1680.422 ms, then batches 2–7 ran contiguously. The batch-level rows and remaining wait are documented in [STEP22_REPORT.md](STEP22_REPORT.md). The output pool now reserves all candidate slots before the warm-up-pending branch defers GPU ownership, removing the variable warm-up-handoff delay that previously moved pool start by about 190 ms.
@@ -0,0 +1,55 @@
# GPU field port, Step 22: keep the GPU busy once it has work
## Changes
The viewer now logs a per-batch timeline with CPU batch-ready and submit begin/end times, raw GPU timestamp ticks and frequency, GPU start/end relative to the first batch, query completion observation, and result consumption. Batch results are marked complete and resident as soon as the event query signals; direct GPU output buffers need no copy or readback upload. The batched path no longer emits redundant per-tile timeline records.
Startup Draws can submit multiple ready batches in one pump, with an 80 ms frame-wide startup budget and a 16 ms build budget per batch. The output pool is reserved before the warm-up-pending route defers GPU ownership, so its allocation no longer waits for shader warm-up handoff.
Changed code: `Source/StandaloneViewer.cpp`, `Research/OwnerShapeWorld/gpu_field_port/GpuFarViewer.h`, and `Research/OwnerShapeWorld/gpu_field_port/GpuFieldPort.cpp`. No `Core/` source changed.
## What the batch trace found
This trace is from the final Step 22 viewer using `-WarmupBeforeWorkers -NoColdDrain -Timeline`. Times are milliseconds after process start. GPU start/end values are relative to the first GPU timestamp; the reported timestamp frequency was 100 MHz.
| Batch | Tiles | CPU submit begin–end | GPU start–end | GPU gap before | Query observed | Consumed | Query-to-consumed |
|---:|---:|---:|---:|---:|---:|---:|---:|
| 0 | 32 | 1510.173–1557.855 | 0.000–37.317 | — | 1602.524 | 1602.536 | 0.012 ms |
| 1 | 134 | 1575.168–1602.503 | 37.318–78.629 | 0.001 ms | 1640.770 | 1640.789 | 0.019 ms |
| 2 | 155 | 1662.068–1680.422 | 117.371–148.418 | 38.742 ms | 1706.998 | 1707.016 | 0.018 ms |
| 3 | 256 | 1686.487–1689.651 | 148.419–187.394 | 0.001 ms | 1750.469 | 1750.504 | 0.034 ms |
| 4 | 256 | 1694.524–1695.752 | 187.394–214.600 | 0.001 ms | 1780.902 | 1780.932 | 0.030 ms |
| 5 | 256 | 1700.083–1701.387 | 214.601–268.206 | 0.000 ms | 1826.074 | 1826.194 | 0.120 ms |
| 6 | 256 | 1704.914–1705.746 | 268.207–331.883 | 0.001 ms | 1889.422 | 1889.448 | 0.027 ms |
| 7 | 89 | 1706.686–1706.984 | 331.883–353.331 | 0.001 ms | 1904.366 | 1904.378 | 0.012 ms |
Total GPU timestamps were 314.570 ms across a 353.331 ms timestamp span. The one 38.742 ms GPU gap was between batches 1 and 2. Batch 1 ended at GPU offset 78.629 ms; batch 2 was not CPU-ready until 1662.063 ms, then its submit call ran for 18.355 ms. That places the gap before the batch was available and during its D3D submit, rather than after an event-query completion. The trace does not isolate how much of the pre-ready portion came from shared-worker input production versus main-thread scheduling.
The CPU submit calls for batches 0 and 1 took 47.682 and 27.335 ms; later calls took 18.355 ms or less. The viewer's startup GPU pump summary recorded 8 calls, a maximum pump gap of 85.482 ms, and a maximum pump body of 63.712 ms. The corresponding Draws spent 108.374 ms and 65.806 ms in GPU pumping despite the nominal 80 ms outer budget, because submit calls can run past the budget. This buys several submissions per Draw but is a startup main-thread stall and remains a tuning tradeoff.
The first GPU input became ready at 1490.378 ms. Input export ran from 1459.112 to 1667.703 ms (208.591 ms wall, 762.960 ms worker sum). The GPU's last batch was consumed at 1904.378 ms; near tiles were all ready at 1997.847 ms and the full 2,226-tile view at 2029.414 ms. The remaining post-GPU interval includes CPU fallback meshing and the final residency/upload work.
## Output-pool start
The variable late pool start came from the warm-up-pending branch returning before output-slot reservation. Step 22 moves pool reservation ahead of that deferral while still keeping candidate ownership on CPU until shader warm-up is complete.
On the auto-route check, the warm-up marker matched and 1,434 output candidates were found at 683.997 ms with warm-up handoff pending. Pool reservation began 25.373 ms later at 709.370 ms and ended at 724.535 ms, with all 1,434 slots recorded in `[GpuFarPrepDeferred]` before handoff. That replaces the owner's previous roughly 181–196 ms candidate-to-pool-start delay. The warm-up queue then took 1,139.870 ms; CPU workers claimed all candidates first, so that check had zero GPU submissions. It confirms the pool no longer waits for handoff, but it is not a cached-GPU timing.
## Slow tiles and timing target
The final throughput trace's slowest dispatch was `(-2,-14,-4,lod0)` at 33.893 ms. The remaining logged slow tiles were `(-1,-15,-1,lod0)` at 6.557 ms, `(0,-15,0,lod0)` at 13.511 ms, and `(2,-6,-5,lod1)` at 13.844 ms. These are ordinary near-detail tiles; the outlier key changes between runs, and this trace has no per-shader-stage timing to justify a targeted math change.
The `-WarmupBeforeWorkers` diagnostic reached full residency in 2.029 s, including startup work. The auto-route check reached it in 1.937 s with CPU fallback and no GPU submissions. Neither is a valid cached-GPU launch measurement, so the ≤1.0 s target remains unverified here. The owner's external Step 21 cached run reached 1.216 s; this change does not claim that it crosses the target.
The final forced first-use route reached the CPU whole view in 1.325 s, with all 2,226 tiles on CPU and zero GPU submissions. That matches the final CPU capture startup (1.333 s) and the owner's Step 21 CPU runs (1.277–1.356 s), so this path did not regress. On this route, output-pool reservation at 1,367 ms happened after the CPU view at 1,325 ms by design: the first-use route deliberately leaves GPU candidates CPU-owned until that first view is presented. This is separate from the matched-marker warm-up-pending route above, where reserving the pool before handoff removed the late GPU-path allocation.
## Correctness and survival
- CPU W1 and W8 each generated all 2,226 tiles and matched density `0xdb2991c73215af4c`, mesh `0x8236c5edd7fc8e4f`, payload `0x4c48fa75a543d58e`, and topology `0xbea22e76b34a4dfc`. Walkability passed at 5,642/5,642.
- WholeNear tested 960 tiles. Hardware and WARP topology differences were zero; all 229,376 protected exception cells matched on both.
- WholeFar tested 1,114 eligible tiles and skipped 152 near-transition tiles. Hardware and WARP topology differences were zero; all 127,401 protected exception cells and 353 thin-feature cells matched. Repeated GPU outputs were bit-identical.
- The existing auxiliary high-LOD vertex-distance bound diagnostic still has known outliers, as recorded in prior step reports; the Step 22 sign/topology, protected-cell, and thin-feature checks all pass.
- Full-residency GPU survival passed with required field readback. It verified 7,200/7,200 shell boundary surfaces and 10,976/10,976 far connectors. Peak private plus sampled DXGI-local memory was about 6.2 GiB.
- All ten full-residency CPU/GPU capture pairs compared numerically. CPU-rock-to-GPU-background counts were zero in eight views and two in each of `remote_yaw180` and `remote_yaw270`, matching the prior baseline. No full-size capture was opened in an image viewer; see [step22_final_capture_comparison.md](step22_final_capture_comparison.md).
The startup pump budget needs checking in an interactive viewer because its submit calls can exceed the nominal frame budget. No cache was cleared or manually altered and no driver setting was changed. The auto route recorded its normal warm-up state as part of the application run. No commit, push, or stash was made.
@@ -0,0 +1,18 @@
# Step22 full-residency capture comparison
All ten CPU/GPU pairs were captured after `[InitialClipmapReady]`. The files were compared numerically as RGB arrays and were not opened in an image viewer.
`CPU rock to GPU background` isolates the prior missing-coverage signature. Captures were loaded as RGB arrays only; no full-size image was opened in a viewer.
| View | Size | RGB MAE | RGB RMSE | Changed pixels | Pixels max channel >32 | Max channel | CPU rock to GPU background | GPU rock to CPU background | Missing-pixel bounds |
|---|---:|---:|---:|---:|---:|---:|---:|---:|---|
| chamber | 1280x800 | 0.6075 | 1.8638 | 42.6368% | 0.0183% | 123 | 0 | 0 | none |
| tunnel | 1280x800 | 0.6383 | 3.3799 | 31.7335% | 0.2772% | 130 | 0 | 0 | none |
| passage | 1280x800 | 1.6092 | 3.6878 | 50.4982% | 0.1733% | 52 | 0 | 0 | none |
| far | 1280x800 | 0.7910 | 2.1578 | 44.8325% | 0.0198% | 128 | 0 | 0 | none |
| passage_floor_closeup | 1280x800 | 0.9314 | 2.4800 | 43.0219% | 0.0500% | 100 | 0 | 0 | none |
| tunnel_bend | 1280x800 | 0.8129 | 2.1599 | 40.9629% | 0.0708% | 55 | 0 | 0 | none |
| remote_yaw0 | 1280x800 | 0.0403 | 1.0654 | 2.7550% | 0.0305% | 90 | 0 | 0 | none |
| remote_yaw90 | 1280x800 | 0.0100 | 0.4323 | 0.9746% | 0.0035% | 94 | 0 | 0 | none |
| remote_yaw180 | 1280x800 | 0.0470 | 1.3091 | 2.9463% | 0.0311% | 102 | 2 | 0 | (415,172)-(834,173) |
| remote_yaw270 | 1280x800 | 0.0812 | 1.6857 | 5.0511% | 0.0580% | 107 | 2 | 0 | (835,204)-(886,205) |
@@ -0,0 +1,18 @@
# Step22_Final full-residency capture comparison
All ten CPU/GPU pairs were captured after `[InitialClipmapReady]`. The files were compared numerically as RGB arrays and were not opened in an image viewer.
`CPU rock to GPU background` isolates the prior missing-coverage signature. Captures were loaded as RGB arrays only; no full-size image was opened in a viewer.
| View | Size | RGB MAE | RGB RMSE | Changed pixels | Pixels max channel >32 | Max channel | CPU rock to GPU background | GPU rock to CPU background | Missing-pixel bounds |
|---|---:|---:|---:|---:|---:|---:|---:|---:|---|
| chamber | 1280x800 | 0.6075 | 1.8638 | 42.6368% | 0.0183% | 123 | 0 | 0 | none |
| tunnel | 1280x800 | 0.6383 | 3.3799 | 31.7335% | 0.2772% | 130 | 0 | 0 | none |
| passage | 1280x800 | 1.6092 | 3.6878 | 50.4982% | 0.1733% | 52 | 0 | 0 | none |
| far | 1280x800 | 0.7910 | 2.1578 | 44.8325% | 0.0198% | 128 | 0 | 0 | none |
| passage_floor_closeup | 1280x800 | 0.9314 | 2.4800 | 43.0219% | 0.0500% | 100 | 0 | 0 | none |
| tunnel_bend | 1280x800 | 0.8129 | 2.1599 | 40.9629% | 0.0708% | 55 | 0 | 0 | none |
| remote_yaw0 | 1280x800 | 0.0715 | 1.7193 | 2.8492% | 0.0806% | 90 | 0 | 0 | none |
| remote_yaw90 | 1280x800 | 0.0100 | 0.4323 | 0.9746% | 0.0035% | 94 | 0 | 0 | none |
| remote_yaw180 | 1280x800 | 0.0470 | 1.3091 | 2.9463% | 0.0311% | 102 | 2 | 0 | (415,172)-(834,173) |
| remote_yaw270 | 1280x800 | 0.0826 | 1.6874 | 5.1077% | 0.0580% | 107 | 2 | 0 | (835,204)-(886,205) |
+158 -41
View File
@@ -1393,10 +1393,13 @@ namespace
uint32 ResidentCount=0;
std::vector<FVoxelTileKey> Keys;
uint64 TimestampFrequency=0;
uint64 GpuStartTimestamp=0,GpuEndTimestamp=0;
double GpuDispatchMs=0.0;
std::chrono::steady_clock::time_point PreparedAt{};
std::chrono::steady_clock::time_point SubmitStartedAt{};
std::chrono::steady_clock::time_point SubmittedAt{};
std::chrono::steady_clock::time_point CompletedAt{};
std::chrono::steady_clock::time_point ConsumedAt{};
bool bGpuComplete=false,bTimingResolved=false;
};
@@ -1559,6 +1562,7 @@ namespace
std::atomic<uint64> GpuFarPreparationGeneration{0};
std::atomic<bool> bGpuFarPreparationFinished{false};
bool bGpuFarPreparationStarted=false,bGpuFarInputsFinalized=false;
bool bGpuFarPreparationDeferredReported=false;
bool bGpuFarFirstPreparedInputReported=false;
bool bGpuWarmupCompleted=false,bGpuFarTimelineEnabled=false;
bool bGpuWarmupRoutePending=false;
@@ -1605,6 +1609,7 @@ namespace
uint64 GpuFarSubmissionBatches=0;
uint32 GpuFarMaxTilesPerSubmissionBatch=0;
uint32 GpuFarOutputCapacitySlots=0,GpuFarNextOutputSlot=0;
uint32 GpuFarOutputPoolReservedSlots=0;
std::vector<uint32> GpuFarFreeOutputSlots;
std::unordered_map<FVoxelTileKey,VoxelForgeCore::FarFieldTileInput,FStandaloneTileKeyHash> GpuFarPreparedInputs;
std::unordered_map<FVoxelTileKey,std::string,FStandaloneTileKeyHash> GpuFarPreparationErrors;
@@ -1756,6 +1761,7 @@ namespace
GpuFarCandidateTiles.clear();GpuFarBaseCpuFallbackTiles.clear();
GpuFarCpuFallbackTiles.clear();GpuFarNeedPreparation.clear();
bGpuFarPreparationStarted=false;bGpuFarInputsFinalized=false;
bGpuFarPreparationDeferredReported=false;
bGpuFarPreparationFinished.store(false,std::memory_order_relaxed);
bGpuWarmupRoutePending=false;
GpuFarWarmupCpuClaimedTiles=0;GpuFarWarmupGpuTakeoverTiles=0;
@@ -1778,6 +1784,7 @@ namespace
GpuSkirtDrawAuditSumIndices=GpuSkirtDrawAuditMaxIndices=0;
GpuFarPoolReserveMs=0.0;
GpuFarSubmissionBatches=0;GpuFarMaxTilesPerSubmissionBatch=0;
GpuFarOutputPoolReservedSlots=0;
DirtyBatchSet.clear();DesiredTiles.clear();DesiredTileSet.clear();CrossingWork.clear();
DeferredLeaverTiles.clear();
for(int32 I=0;I<=MaxLevels;++I)
@@ -2668,7 +2675,7 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
{
if(Tile.CompletedAt.time_since_epoch().count()!=0)return;
Tile.CompletedAt=std::chrono::steady_clock::now();
if(!bGpuFarTimelineEnabled)return;
if(!bGpuFarTimelineEnabled||BatchIndex!=UINT32_MAX)return;
std::printf("[GpuTileTimeline] event=completed key=(%d,%d,%d,lod%d) batch=%u at_ms=%.3f frame=%llu submitted_ms=%.3f\n",
Key.Coord.X,Key.Coord.Y,Key.Coord.Z,Key.Level,BatchIndex,
ColdTimelineMilliseconds(Tile.CompletedAt),
@@ -2685,7 +2692,7 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
if(Tile.bColdTimelineResidentReported)return;
Tile.bColdTimelineResidentReported=true;
const auto ResidentAt=std::chrono::steady_clock::now();
if(bGpuFarTimelineEnabled)
if(bGpuFarTimelineEnabled&&Tile.BatchIndex==UINT32_MAX)
std::printf("[GpuTileTimeline] event=resident key=(%d,%d,%d,lod%d) batch=%u at_ms=%.3f frame=%llu submitted_ms=%.3f completed_ms=%.3f\n",
Key.Coord.X,Key.Coord.Y,Key.Coord.Z,Key.Level,Tile.BatchIndex,
ColdTimelineMilliseconds(ResidentAt),
@@ -2696,10 +2703,69 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
FGpuFarBatch& Batch=GpuFarBatches[Tile.BatchIndex];
++Batch.ResidentCount;
if(bGpuFarTimelineEnabled&&Batch.ResidentCount==Batch.TileCount)
{
Batch.ConsumedAt=ResidentAt;
std::printf("[GpuBatchTimeline] event=resident batch=%u at_ms=%.3f frame=%llu tile_count=%u submitted_ms=%.3f completed_ms=%.3f\n",
Tile.BatchIndex,ColdTimelineMilliseconds(ResidentAt),
static_cast<unsigned long long>(ColdTimelineFrame()),Batch.TileCount,
ColdTimelineMilliseconds(Batch.SubmittedAt),ColdTimelineMilliseconds(Batch.CompletedAt));
std::printf("[GpuBatchTimeline] event=consumed batch=%u at_ms=%.3f frame=%llu tile_count=%u submit_to_gpu_complete_ms=%.3f gpu_complete_to_consumed_ms=%.3f output=direct_gpu_buffers\n",
Tile.BatchIndex,ColdTimelineMilliseconds(ResidentAt),
static_cast<unsigned long long>(ColdTimelineFrame()),Batch.TileCount,
ColdTimelineMilliseconds(Batch.CompletedAt)-ColdTimelineMilliseconds(Batch.SubmittedAt),
ColdTimelineMilliseconds(ResidentAt)-ColdTimelineMilliseconds(Batch.CompletedAt));
}
}
}
void ReportGpuBatchTimelineSummary()
{
if(!bGpuFarTimelineEnabled)return;
uint64 FirstGpuStart=0,CommonFrequency=0;
for(const FGpuFarBatch& Batch:GpuFarBatches)
{
if(Batch.bTimingResolved&&Batch.TimestampFrequency&&Batch.GpuStartTimestamp)
{
FirstGpuStart=Batch.GpuStartTimestamp;
CommonFrequency=Batch.TimestampFrequency;
break;
}
}
uint64 PreviousGpuEnd=0,PreviousFrequency=0;
for(std::size_t I=0;I<GpuFarBatches.size();++I)
{
const FGpuFarBatch& Batch=GpuFarBatches[I];
const auto AtMs=[this](const std::chrono::steady_clock::time_point& At)
{
return At.time_since_epoch().count()!=0
?ColdTimelineMilliseconds(At):-1.0;
};
double GpuStartFromFirstMs=-1.0,GpuEndFromFirstMs=-1.0,GpuGapBeforeMs=-1.0;
if(Batch.bTimingResolved&&Batch.TimestampFrequency&&Batch.GpuStartTimestamp
&&CommonFrequency==Batch.TimestampFrequency&&FirstGpuStart)
{
GpuStartFromFirstMs=(static_cast<double>(Batch.GpuStartTimestamp)
-static_cast<double>(FirstGpuStart))*1000.0/Batch.TimestampFrequency;
GpuEndFromFirstMs=(static_cast<double>(Batch.GpuEndTimestamp)
-static_cast<double>(FirstGpuStart))*1000.0/Batch.TimestampFrequency;
if(PreviousGpuEnd&&PreviousFrequency==Batch.TimestampFrequency)
GpuGapBeforeMs=(static_cast<double>(Batch.GpuStartTimestamp)
-static_cast<double>(PreviousGpuEnd))*1000.0/Batch.TimestampFrequency;
}
std::printf("[GpuBatchTimeline] event=summary batch=%zu tiles=%u cpu_batch_ready_ms=%.3f cpu_submit_begin_ms=%.3f cpu_submit_end_ms=%.3f gpu_start_tick=%llu gpu_end_tick=%llu timestamp_frequency_hz=%llu gpu_start_from_first_ms=%.3f gpu_end_from_first_ms=%.3f gpu_gap_from_previous_end_ms=%.3f gpu_busy_ms=%.3f completion_observed_ms=%.3f consumed_ms=%.3f submit_to_completion_observed_ms=%.3f completion_to_consumed_ms=%.3f timing_resolved=%s\n",
I,Batch.TileCount,AtMs(Batch.PreparedAt),AtMs(Batch.SubmitStartedAt),
AtMs(Batch.SubmittedAt),static_cast<unsigned long long>(Batch.GpuStartTimestamp),
static_cast<unsigned long long>(Batch.GpuEndTimestamp),
static_cast<unsigned long long>(Batch.TimestampFrequency),GpuStartFromFirstMs,
GpuEndFromFirstMs,GpuGapBeforeMs,Batch.GpuDispatchMs,AtMs(Batch.CompletedAt),
AtMs(Batch.ConsumedAt),Batch.CompletedAt.time_since_epoch().count()!=0
?AtMs(Batch.CompletedAt)-AtMs(Batch.SubmittedAt):-1.0,
Batch.ConsumedAt.time_since_epoch().count()!=0
&&Batch.CompletedAt.time_since_epoch().count()!=0
?AtMs(Batch.ConsumedAt)-AtMs(Batch.CompletedAt):-1.0,
Batch.bTimingResolved?"yes":"no");
if(Batch.bTimingResolved&&Batch.TimestampFrequency&&Batch.GpuEndTimestamp)
{PreviousGpuEnd=Batch.GpuEndTimestamp;PreviousFrequency=Batch.TimestampFrequency;}
}
}
@@ -2741,13 +2807,16 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
{
BatchTileIt->second.bGpuComplete=true;
ReportGpuTileCompletion(BatchKey,BatchTileIt->second,Tile.BatchIndex);
RecordGpuCrossingTileReady(BatchKey);
ReportGpuTileResident(BatchKey,BatchTileIt->second);
}
}
}
if(Batch.bGpuComplete&&!Batch.bTimingResolved)
{
if(VoxelForgeGpuFarViewer::ResolveGpuTiming(Context,Batch.TimingQueries,
Batch.GpuDispatchMs,&Batch.TimestampFrequency))
Batch.GpuDispatchMs,&Batch.TimestampFrequency,
&Batch.GpuStartTimestamp,&Batch.GpuEndTimestamp))
{
Batch.bTimingResolved=true;
GpuFarSubmissionSpanMs+=Batch.GpuDispatchMs;
@@ -2933,6 +3002,7 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
GpuFarCandidateTiles.clear();GpuFarBaseCpuFallbackTiles.clear();
GpuFarCpuFallbackTiles.clear();GpuFarNeedPreparation.clear();
bGpuFarPreparationStarted=false;bGpuFarInputsFinalized=false;
bGpuFarPreparationDeferredReported=false;
bGpuFarFirstPreparedInputReported=false;
bGpuFarPreparationFinished.store(false,std::memory_order_relaxed);
bGpuWarmupRoutePending=false;
@@ -3006,6 +3076,11 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
bGpuWarmupRoutePending=bGpuFarShaderPrewarm&&!bGpuFarAuditEnabled
&&!bGpuWarmupCompleted&&!bGpuFarWarmupBeforeWorkers
&&!GpuFarCandidateTiles.empty();
if(bGpuFarTimelineEnabled)
std::printf("[ColdTimeline] event=gpu_output_pool_candidates at_ms=%.3f slots=%u candidates=%zu warmup_handoff_pending=%s\n",
ColdTimelineMilliseconds(std::chrono::steady_clock::now()),
GpuFarOutputCapacitySlots,GpuFarCandidateTiles.size(),
bGpuWarmupRoutePending?"yes":"no");
if(bGpuWarmupRoutePending)
{
GpuFarCpuFallbackTiles.reserve(GpuFarCpuFallbackTiles.size()+GpuFarCandidateTiles.size());
@@ -3050,40 +3125,53 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
void StartGpuFarPreparation()
{
if(!bGpuFarEnabled||!ShapeWorld||!ShapeWorld->hasFarWorld()||bGpuFarPreparationStarted)return;
if(bGpuWarmupDeferredUntilFirstView&&!bGpuWarmupDeferredStarted)return;
if(GpuFarCandidateTiles.empty())
{
bGpuFarPreparationStarted=true;
bGpuFarInputsFinalized=true;
bGpuFarPreparationFinished.store(true,std::memory_order_relaxed);
return;
}
if(GpuFarOutputPoolReservedSlots<GpuFarOutputCapacitySlots)
{
const auto ReserveStart=std::chrono::steady_clock::now();
std::printf("[ColdTimeline] stage=gpu_output_pool begin_ms=%.3f slots=%u reason=reserve_before_warmup_handoff\n",
ColdTimelineMilliseconds(ReserveStart),GpuFarOutputCapacitySlots);
std::string PoolError;
if(!VoxelForgeGpuFarViewer::ReserveFarOutputPool(Device,Context,GpuFarShaderPath,
GpuFarOutputCapacitySlots,static_cast<uint32>(World->CoarseTileCells),PoolError))
{
std::fprintf(stderr,"[GpuFarPoolError] %s\n",PoolError.c_str());
bGpuFarEnabled=false;bGpuFarPreparationStarted=true;
GpuFarOutputCapacitySlots=0;GpuFarOutputPoolReservedSlots=0;
for(auto& Pair:GpuFarTiles)ReleaseGpuFarTile(Pair.second);
GpuFarTiles.clear();GpuFarCandidateTiles.clear();GpuFarQueue.clear();GpuFarQueued.clear();
GpuFarNeedPreparation.clear();GpuFarEligibleTiles.clear();
AddGpuFarCpuFallbackTiles(DesiredTiles);
bGpuFarInputsFinalized=true;
bGpuFarPreparationFinished.store(true,std::memory_order_relaxed);
return;
}
const double ReserveMs=std::chrono::duration<double,std::milli>(
std::chrono::steady_clock::now()-ReserveStart).count();
GpuFarPoolReserveMs+=ReserveMs;
GpuFarOutputPoolReservedSlots=GpuFarOutputCapacitySlots;
std::printf("[ColdTimeline] stage=gpu_output_pool end_ms=%.3f duration_ms=%.3f reserved_slots=%u\n",
ColdTimelineMilliseconds(std::chrono::steady_clock::now()),ReserveMs,
GpuFarOutputPoolReservedSlots);
}
if(bGpuWarmupRoutePending&&!bGpuWarmupCompleted)
{
std::printf("[GpuFarPrepDeferred] reason=shader_warmup_cpu_tiles_own_candidates tiles=%zu\n",
GpuFarCandidateTiles.size());
if(!bGpuFarPreparationDeferredReported)
{
bGpuFarPreparationDeferredReported=true;
std::printf("[GpuFarPrepDeferred] reason=shader_warmup_cpu_tiles_own_candidates tiles=%zu output_pool_reserved_slots=%u\n",
GpuFarCandidateTiles.size(),GpuFarOutputPoolReservedSlots);
}
return;
}
bGpuFarPreparationStarted=true;
if(GpuFarCandidateTiles.empty())
{
bGpuFarInputsFinalized=true;
bGpuFarPreparationFinished.store(true,std::memory_order_relaxed);
return;
}
const auto ReserveStart=std::chrono::steady_clock::now();
std::printf("[ColdTimeline] stage=gpu_output_pool begin_ms=%.3f slots=%u\n",
ColdTimelineMilliseconds(ReserveStart),GpuFarOutputCapacitySlots);
std::string PoolError;
if(!VoxelForgeGpuFarViewer::ReserveFarOutputPool(Device,Context,GpuFarShaderPath,
GpuFarOutputCapacitySlots,static_cast<uint32>(World->CoarseTileCells),PoolError))
{
std::fprintf(stderr,"[GpuFarPoolError] %s\n",PoolError.c_str());
bGpuFarEnabled=false;GpuFarOutputCapacitySlots=0;
for(auto& Pair:GpuFarTiles)ReleaseGpuFarTile(Pair.second);
GpuFarTiles.clear();GpuFarCandidateTiles.clear();GpuFarQueue.clear();GpuFarQueued.clear();
GpuFarNeedPreparation.clear();GpuFarEligibleTiles.clear();
AddGpuFarCpuFallbackTiles(DesiredTiles);
bGpuFarInputsFinalized=true;
bGpuFarPreparationFinished.store(true,std::memory_order_relaxed);
return;
}
GpuFarPoolReserveMs+=std::chrono::duration<double,std::milli>(
std::chrono::steady_clock::now()-ReserveStart).count();
std::printf("[ColdTimeline] stage=gpu_output_pool end_ms=%.3f duration_ms=%.3f\n",
ColdTimelineMilliseconds(std::chrono::steady_clock::now()),GpuFarPoolReserveMs);
const auto PoolEnd=std::chrono::steady_clock::now();
const auto MemorySampleStart=std::chrono::steady_clock::now();
SampleGpuFarMemory();
@@ -3317,7 +3405,7 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
return true;
}
void PumpGpuFarGeneration()
void PumpGpuFarGeneration(double MaxPumpBudgetMilliseconds=8.0)
{
if(!bGpuFarEnabled||!ShapeWorld||!ShapeWorld->hasFarWorld()||!Device||!Context)return;
if(bGpuWarmupDeferredUntilFirstView&&!bGpuWarmupDeferredStarted)
@@ -3341,6 +3429,7 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
std::string PipelineError;
if(!EnsureGpuFarPipeline(PipelineError))
{bGpuFarEnabled=false;AddGpuFarCpuFallbackTiles(DesiredTiles);return;}
StartGpuFarPreparation();
if(!CompleteGpuShaderWarmupAndRoute())return;
StartGpuFarPreparation();
if(!bGpuFarEnabled||!FinalizeGpuFarPreparation()||GpuFarQueue.empty())return;
@@ -3348,18 +3437,18 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
&&!bDisableGpuColdStartDrain&&bGpuFarWarmupBeforeWorkers;
constexpr std::size_t StartupFirstGpuBatchTarget=32;
constexpr std::size_t StartupGpuBatchTarget=256;
constexpr double StartupGpuPumpBudgetMilliseconds=8.0;
constexpr double StartupGpuPumpBudgetMilliseconds=16.0;
const bool bWaitForStreamingGpuBatch=!bColdDrainBeforeWholeView
&&!bGpuFarAuditEnabled&&!bInitialClipmapReported;
const std::size_t GpuPumpTileLimit=bWaitForStreamingGpuBatch
?(GpuFarSubmissionBatches==0?StartupFirstGpuBatchTarget:StartupGpuBatchTarget):32;
const double GpuPumpBudgetMilliseconds=bWaitForStreamingGpuBatch
?StartupGpuPumpBudgetMilliseconds:4.0;
const double GpuPumpBudgetMilliseconds=std::min(MaxPumpBudgetMilliseconds,
bWaitForStreamingGpuBatch?StartupGpuPumpBudgetMilliseconds:4.0);
if(bWaitForStreamingGpuBatch
&&!bGpuFarPreparationFinished.load(std::memory_order_acquire)
&&GpuFarQueue.size()<StartupFirstGpuBatchTarget)return;
if(bWaitForStreamingGpuBatch&&GpuFarSubmissionBatches==0)
std::printf("[GpuFarStreamingPolicy] first_batch_tiles=%zu start_threshold=%zu later_batch_limit=%zu pump_budget_ms=%.1f order=lod_then_camera_distance\n",
std::printf("[GpuFarStreamingPolicy] first_batch_tiles=%zu start_threshold=%zu later_batch_limit=%zu per_draw_budget_ms=80.0 per_batch_build_budget_ms=%.1f submit_batches_per_draw=until_budget_or_input_starvation order=lod_then_camera_distance\n",
GpuFarQueue.size(),StartupFirstGpuBatchTarget,StartupGpuBatchTarget,
StartupGpuPumpBudgetMilliseconds);
const auto PumpStart=std::chrono::steady_clock::now();
@@ -3911,6 +4000,7 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
BatchIndex,ColdTimelineMilliseconds(Batch.PreparedAt),
static_cast<unsigned long long>(ColdTimelineFrame()),Batch.TileCount);
std::string BatchError;
Batch.SubmitStartedAt=std::chrono::steady_clock::now();
if(!VoxelForgeGpuFarViewer::SubmitFarTileBatch(Device,Context,GpuFarShaderPath,
&Batch.CompletionEvent,&Batch.TimingQueries,BatchError))
{
@@ -3932,16 +4022,23 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
{
It->second.BatchIndex=BatchIndex;
It->second.SubmittedAt=Batch.SubmittedAt;
if(bGpuFarTimelineEnabled)std::printf("[GpuTileTimeline] event=submitted key=(%d,%d,%d,lod%d) batch=%u at_ms=%.3f frame=%llu prepared_ms=%.3f\n",
if(bGpuFarTimelineEnabled&&!bDeferredBatch)std::printf("[GpuTileTimeline] event=submitted key=(%d,%d,%d,lod%d) batch=%u at_ms=%.3f frame=%llu prepared_ms=%.3f\n",
Key.Coord.X,Key.Coord.Y,Key.Coord.Z,Key.Level,BatchIndex,
ColdTimelineMilliseconds(Batch.SubmittedAt),
static_cast<unsigned long long>(ColdTimelineFrame()),
ColdTimelineMilliseconds(It->second.PreparedAt));
}
}
if(bGpuFarTimelineEnabled)std::printf("[GpuBatchTimeline] event=submitted batch=%u at_ms=%.3f frame=%llu tile_count=%u\n",
BatchIndex,ColdTimelineMilliseconds(Batch.SubmittedAt),
if(bGpuFarTimelineEnabled)std::printf("[GpuBatchTimeline] event=cpu_submit batch=%u begin_ms=%.3f end_ms=%.3f cpu_submit_ms=%.3f frame=%llu tile_count=%u\n",
BatchIndex,ColdTimelineMilliseconds(Batch.SubmitStartedAt),
ColdTimelineMilliseconds(Batch.SubmittedAt),
std::chrono::duration<double,std::milli>(
Batch.SubmittedAt-Batch.SubmitStartedAt).count(),
static_cast<unsigned long long>(ColdTimelineFrame()),Batch.TileCount);
if(bGpuFarTimelineEnabled)std::printf("[GpuBatchTimeline] event=submitted batch=%u at_ms=%.3f frame=%llu tile_count=%u cpu_submit_begin_ms=%.3f\n",
BatchIndex,ColdTimelineMilliseconds(Batch.SubmittedAt),
static_cast<unsigned long long>(ColdTimelineFrame()),Batch.TileCount,
ColdTimelineMilliseconds(Batch.SubmitStartedAt));
++GpuFarSubmissionBatches;
GpuFarMaxTilesPerSubmissionBatch=std::max<uint32>(GpuFarMaxTilesPerSubmissionBatch,
static_cast<uint32>(Built));
@@ -4699,6 +4796,7 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
std::printf("[GpuFarMemory] dxgi_local_sampled_peak_mb=%.1f dxgi_local_budget_mb=%.1f\n",
GpuFarLocalMemoryPeakBytes/(1024.0*1024.0),
GpuFarLocalMemoryBudgetBytes/(1024.0*1024.0));
ReportGpuBatchTimelineSummary();
}
else
{
@@ -4784,7 +4882,26 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
const auto CpuPumpEnd=std::chrono::steady_clock::now();
const bool bDeferGpuUntilCpuFrame=bGpuFarEnabled&&bPresentCpuFrameBeforeGpu
&&!bCpuReadyFramePresentedBeforeGpu;
if(!bDeferGpuUntilCpuFrame)PumpGpuFarGeneration();
if(!bDeferGpuUntilCpuFrame)
{
// Startup holds the last CPU front buffer while the GPU route takes over.
// Allow several ready batches to be submitted in one draw before resuming it.
constexpr double StartupGpuPumpFrameBudgetMilliseconds=80.0;
const auto GpuPumpFrameStart=std::chrono::steady_clock::now();
uint64 PreviousSubmissionCount=GpuFarSubmissionBatches;
for(;;)
{
const double ElapsedGpuPumpMs=std::chrono::duration<double,std::milli>(
std::chrono::steady_clock::now()-GpuPumpFrameStart).count();
const double RemainingGpuPumpMs=StartupGpuPumpFrameBudgetMilliseconds
-ElapsedGpuPumpMs;
if(RemainingGpuPumpMs<=0.0)break;
PumpGpuFarGeneration(RemainingGpuPumpMs);
if(bGpuFarAuditEnabled||bInitialClipmapReported
||GpuFarSubmissionBatches==PreviousSubmissionCount)break;
PreviousSubmissionCount=GpuFarSubmissionBatches;
}
}
const auto GpuPumpEnd=std::chrono::steady_clock::now();
const auto Now=std::chrono::steady_clock::now();
if(FlightTargetCrossings>0&&bFullDetailReported&&!bFlightStarted)