wip(standalone): GPU step 21 -- GPU input export shares the 8 workers (3:1 priority); no wall gain

Export tasks interleave with CPU tile jobs on the existing pool. Slow-dispatch keys
are logged (>5 ms, with -Timeline).
My runs outside the sandbox (Step21.exe, marker removed first, interleaved, GPU with
-Timeline):
  CPU 1.356 / 1.321 / 1.277 s
  GPU 1.453 (marker miss, 0 GPU tiles) / 1.249 / 1.216 / 1.217 s (match)
Export now starts at 686 ms in run c (was ~850), but the whole view is unchanged
(1.216 s): from first input (721 ms) to ready (1,216 ms) the GPU path spends ~500 ms
wall for ~255 ms of GPU timestamps. In runs b/d the output pool itself began late
(~860-875 ms). Slow tiles: (0,-6,-5,lod1) 28.5 ms, (-4,-15,-3,lod0) 11.3 ms,
(-3,-13,-2,lod0) 8.2 ms, all ordinary near-detail tiles.
Audits: hashes w1/w8, walk 5,642/5,642, near 960 / far 1,114 0 diffs vs CPU and
WARP, thin 353, survival 6.13 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-07 23:20:42 +02:00
1 parent 2cb3344e1b
commit 38f6009200
5 files changed
+308 -119

No files matched your search

@@ -24,3 +24,21 @@ Step 19's apparent 698-850 ms idle gap was an instrumentation gap. Its `ChunkCro
The no-prewarm diagnostic took 2.058 s to whole view and started GPU submission at 1.047 s. Its slowest tile was `(-3,-14,-5,lod0)` at 25.901 ms. Step 19 run b's 15.254 ms tile cannot be identified from its old log because it did not retain the tile key. Step 20 records that key for future runs.
The numeric capture comparison reports are [Step 19](step19_final_capture_comparison.md) and [Step 20](step20final_capture_comparison.md). The sandbox forced routes exercise both startup paths but do not establish that the external driver's queue is cached; the forced-cached control still had a cold driver queue and zero GPU submissions. Use the auto command for the owner's cached-path verification. See [STEP20_REPORT.md](STEP20_REPORT.md) for the full findings and audit results.
## Step 21
- `Research\OwnerShapeWorld\gpu_field_port\BuildGpuFieldViewer.ps1 -OutputName VoxelForgeGpuFarStep21.exe` — builds the shared-worker scheduler viewer; pass when build and Core link safety checks exit 0.
- `Research\OwnerShapeWorld\gpu_field_port\BuildGpuFieldPort.ps1 -OutputName VoxelForgeGpuFarAgreementStep21.exe` — builds the agreement runner for the unchanged CPU reference and GPU/WARP checks.
- `Research\OwnerShapeWorld\gpu_field_port\RunGpuFarViewer.ps1 -Mode gpu -WarmupPolicy cold -NoColdDrain -Timeline -ExecutableName VoxelForgeGpuFarStep21.exe -LogStem step21_forced_cold` — forces warm-up after the CPU view; verifies the first-use CPU path and deferred warm-up handoff.
- `Research\OwnerShapeWorld\gpu_field_port\RunGpuFarViewer.ps1 -Mode gpu -WarmupPolicy cached -NoColdDrain -Timeline -ExecutableName VoxelForgeGpuFarStep21.exe -LogStem step21_forced_cached` — forces startup-overlap selection. This does not establish a warm driver queue: both Step 21 sandbox runs reported an application marker match, a 1.15–1.26 s warm-up queue, zero GPU submissions, and CPU fallback.
- `Research\OwnerShapeWorld\gpu_field_port\RunGpuFarViewer.ps1 -Mode gpu -WarmupBeforeWorkers -NoColdDrain -Timeline -ExecutableName VoxelForgeGpuFarStep21.exe -LogStem step21_gpu_takeover` — measures GPU takeover and shared-worker overlap after shader warm-up. Check that `[GpuFarInputPrepare]` has `failed=0`, GPU submissions are nonzero, and the shared-worker start follows queueing promptly. This includes cold warm-up and is not cached-launch timing.
- `Research\OwnerShapeWorld\gpu_field_port\RunGpuFarViewer.ps1 -Mode gpu -WarmupPolicy auto -NoColdDrain -Timeline -ExecutableName VoxelForgeGpuFarStep21.exe -LogStem step21_auto` — owner's cached-path verification. When the app marker and external driver cache are warm, use the whole-view and export timeline plus `[GpuFarSlowDispatchTile]` keys; sandbox forced-cached runs do not establish that driver state.
- `[GpuFarSlowDispatchTile]` prints the tile key and input characteristics for timestamped dispatches above 5 ms when `-Timeline` is enabled. It covers startup-overlap, takeover, and audit dispatch paths. `[GpuFarMaxDispatchTile]` continues to record each run's maximum tile key.
- `Research\OwnerShapeWorld\gpu_field_port\RunGpuFarAgreement.ps1 -ExecutableName VoxelForgeGpuFarAgreementStep21.exe -WholeNear` (repeat with `-WholeFar`) — compares CPU, hardware GPU, and WARP, including protected and thin-feature topology.
- `Research\OwnerShapeWorld\gpu_field_port\RunStep15CpuReference.ps1 -ExecutableName VoxelForgeGpuFarStep21.exe -LogPrefix step21_cpu_reference` — checks the unchanged CPU W1/W8 hashes, 2,226 generated tiles, and 5,642/5,642 walkability.
- `Research\OwnerShapeWorld\gpu_field_port\RunStep15GpuSurvival.ps1 -ExecutableName VoxelForgeGpuFarStep21.exe -LogName step21_gpu_survival` — checks full-residency GPU survival and required field readback.
- `Research\OwnerShapeWorld\gpu_field_port\RunStep15CaptureMatrix.ps1 -ExecutableName VoxelForgeGpuFarStep21.exe -Prefix step21 -GpuWarmupBeforeWorkers` then `python Research\OwnerShapeWorld\gpu_field_port\CompareStep15Captures.py --prefix step21` — captures ten CPU/GPU pairs and compares the full-size files numerically. See [step21_capture_comparison.md](step21_capture_comparison.md); no capture was opened in an image viewer.
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.
@@ -0,0 +1,62 @@
# GPU field port, Step 21: scheduling across the CPU/GPU boundary
## Change
GPU field-input export now shares `FStandaloneStreamingGenerator`'s existing eight workers with CPU tile meshing. When both queues have work, each worker handles up to three GPU input tasks before taking one CPU tile job. If either queue is empty, workers drain the available queue. This starts GPU preparation without creating a second worker pool and keeps CPU mesh jobs progressing.
The CPU still owns layout, shape data, routes, hashes, sparse protected/thin exceptions, and collision. Export functions, field math, and meshing arithmetic were not changed. GPU results remain consumed by the main-thread path; the completed CPU W1/W8 and topology audits confirm the scheduling change did not alter generated content. A first-use marker miss still defers GPU takeover until warm-up finishes, leaving the first view on the CPU route.
Changed source files are `Source/StandaloneViewer.cpp` and `Source/StreamingWorld.h`. No `Core\` files changed.
## Whole-view timings
Times are from separate forced-route processes in the sandbox. The GPU-mode column is the whole-view wall time; rows with CPU fallback and zero GPU submissions are labelled explicitly.
| Run | CPU whole view | GPU-mode whole view | Route | Slowest GPU tile |
|---|---:|---:|---|---|
| CPU route | 1.318 s | — | CPU-only | — |
| Forced cold | 1.313 s | — | CPU view first; warm-up deferred; zero GPU submissions | — |
| Forced cached, run 1 | — | 2.068 s | Marker match; driver queue cold; zero GPU submissions; CPU fallback | — |
| Forced cached, run 2 | — | 2.066 s | Marker match; driver queue cold; zero GPU submissions; CPU fallback | — |
| Warm-up-before-workers diagnostic | — | 2.120 s | Cold warm-up, shared-pool overlap, 1,418 GPU submissions | `(-2,-13,-4,lod0)`, 58.072 ms |
The warm-up-before-workers route is useful for measuring overlap and GPU work, but its wall time includes 658.337 ms of shader warm-up and is not a cached-launch comparison. The two forced-cached runs matched the application's cache marker but took 1,256.689 ms and 1,154.209 ms to drain the driver's shader warm-up queue. In both, all 1,434 candidates were claimed by CPU workers before GPU takeover, so there was no GPU input-export interval to time. The requested ≤1.0 s cached whole-view target therefore remains unverified in this environment; the owner should verify it with the external warm driver cache.
## Shared-worker timeline
The takeover diagnostic's times below are milliseconds after cold process start. CPU tile meshing was still in progress when the GPU input tasks were queued.
| Event | Time | Measurement |
|---|---:|---|
| GPU output pool begin | 1,441.885 | 1,434 output slots |
| GPU output pool end | 1,447.248 | 5.363 ms reserve |
| Priority input queue | 1,447.372 | 0.120 ms after pool end; 8 shared workers |
| Input workers started | 1,448.948 | 1.576 ms after queueing |
| First input ready | 1,476.657 | 27.709 ms after workers started |
| Input export end | 1,652.234 | 203.286 ms wall; 723.258 ms worker sum; 0 failures |
| Whole view ready | 2,120.237 | 1,418 GPU submissions; CPU mesh worker sum 1.295 s |
This addresses the Step 20 cached-route ordering in which CPU-owned meshing held the shared workers until after the output pool was ready. It demonstrates GPU input preparation overlapped with CPU work, but it is not a warm-driver cached-start measurement.
## Slow GPU tiles
The new `[GpuFarSlowDispatchTile]` record includes the key and input features for timestamped dispatches above 5 ms when `-Timeline` is on. It is called on the automatic, forced, and audit dispatch paths. The takeover run reported:
- `(-2,-13,-4,lod0)`: 58.072 ms, `cells=32`, `step=1`, near detail enabled, 42,875 near-proxy samples, no protected/thin/solid samples, skirts, or connectors.
- `(-4,-14,-1,lod0)`: 9.692 ms, with the same reported input characteristics.
- An earlier Step 21 takeover sample recorded `(-1,-14,-5,lod0)` at 31.547 ms.
These keys all identify ordinary near-detail LOD0 inputs. The outlier changes tile between runs and the recorded characteristics do not isolate an expensive field feature. There is no per-shader-stage GPU profile in this run, so no field or meshing optimization was made on speculation. Use the new metadata plus stage profiling to determine whether the cause is dispatch workload, GPU contention, or timing variance.
## Correctness and survival
- CPU W1 and W8 both generated 2,226/2,226 tiles and matched all four hashes: density `0xdb2991c73215af4c`, mesh `0x8236c5edd7fc8e4f`, payload `0x4c48fa75a543d58e`, 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 WholeFar vertex-distance diagnostic still reports the known 247 bound failures across LOD6/7/8 (46/48/153; maximum 64.560 cm), matching Step 16, 18, 19, and 20 reports. Sign and topology checks pass; this is not a new Step 21 difference.
- Full-residency GPU survival passed with required field readback. Peak private plus DXGI-local memory was 6.13 GiB in the survival runner and no more than 6.30 GB across the capture matrix, below the 8 GB limit.
- All ten CPU/GPU full-residency captures were compared numerically, without opening full-size images in a viewer. CPU-rock-to-GPU-background counts were zero in eight views and two in each remote yaw 180 and 270, matching the prior baseline. See [step21_capture_comparison.md](step21_capture_comparison.md).
## Sandbox limits
The cached marker was forced to match, but the driver's warm-up queue was cold and no GPU submissions occurred on that route. The marker was not cleared to force a miss, so the first-use CPU route is preserved by the unchanged route condition rather than directly exercised as a marker-miss run here. The owner should use the Step 21 `-WarmupPolicy auto -Timeline` runner outside the sandbox with the real warm marker and cache to verify cached critical-path time and collect slow dispatch keys. The Step 21 runner uses the same BelowNormal priority, `0xFF00` affinity, `/MP8`, memory ceiling, retry policy, and viewer shutdown behavior as prior steps. No cache or driver was manually reset or changed.
@@ -0,0 +1,18 @@
# Step21 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.0309 | 0.7039 | 2.7813% | 0.0127% | 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) |
+165 -114
View File
@@ -1371,7 +1371,11 @@ namespace
VoxelForgeGpuFarViewer::GpuTimingQueries TimingQueries{};
uint64 VertexCapacityBytes=0;
uint32 OutputSlot=0,IndirectArgsByteOffset=0,BatchIndex=UINT32_MAX;
int32 InputCells=0,InputStep=0;
uint64 ProtectedSamples=0,ProtectedThinSamples=0,ProtectedSolidSamples=0;
uint32 NearProxySamples=0,ConnectorRings=0,ConnectorVertices=0;
uint8 VisibleSkirtFaceMask=0;
bool bNearDetailInput=false;
bool bSkirtDrawArgsAudited=false;
double GpuDispatchMs=0.0;
std::chrono::steady_clock::time_point PreparedAt{};
@@ -1457,6 +1461,19 @@ namespace
std::chrono::steady_clock::time_point FinishedAt{};
uint64 FinishedFrame=0;
};
struct FGpuFarPreparationBatch
{
std::once_flag StartOnce;
std::chrono::steady_clock::time_point DispatchAt{};
std::chrono::steady_clock::time_point PrepareStart{};
std::atomic<std::size_t> Remaining{0};
std::atomic<std::size_t> Failed{0};
std::atomic<uint64> WorkerNanoseconds{0};
std::atomic<bool> bCancelled{false};
uint64 Generation=0;
std::size_t TileCount=0;
std::size_t SharedWorkerCount=0;
};
HWND Window=nullptr;
ID3D11Device* Device=nullptr;
@@ -1536,9 +1553,10 @@ namespace
bool bGpuFarWarmupOnly=false;
std::string GpuWarmupEntryFilter;
std::thread GpuShaderWarmupThread;
std::thread GpuFarPreparationThread;
std::shared_ptr<FGpuFarPreparationBatch> GpuFarPreparationState;
std::mutex GpuFarPreparedResultsMutex;
std::deque<FPreparedGpuFarInputResult> GpuFarPreparedResults;
std::atomic<uint64> GpuFarPreparationGeneration{0};
std::atomic<bool> bGpuFarPreparationFinished{false};
bool bGpuFarPreparationStarted=false,bGpuFarInputsFinalized=false;
bool bGpuFarFirstPreparedInputReported=false;
@@ -1661,7 +1679,6 @@ namespace
double BatchUploadBudgetMilliseconds=2.0;
~FApp(){
if(GpuFarPreparationThread.joinable())GpuFarPreparationThread.join();
if(GpuShaderWarmupThread.joinable())GpuShaderWarmupThread.join();
StopGeneration(true);ReleaseMeshes();
if(SolidRasterizer)SolidRasterizer->Release();if(WireRasterizer)WireRasterizer->Release();
@@ -1734,6 +1751,7 @@ namespace
GpuFarBatches.clear();GpuFarQueue.clear();GpuFarQueued.clear();GpuFarEligibleTiles.clear();
GpuFarFreeOutputSlots.clear();GpuFarNextOutputSlot=0;
GpuFarPreparedInputs.clear();GpuFarPreparationErrors.clear();GpuFarPreparedAt.clear();
GpuFarPreparationState.reset();
{std::lock_guard Lock(GpuFarPreparedResultsMutex);GpuFarPreparedResults.clear();}
GpuFarCandidateTiles.clear();GpuFarBaseCpuFallbackTiles.clear();
GpuFarCpuFallbackTiles.clear();GpuFarNeedPreparation.clear();
@@ -1774,7 +1792,7 @@ namespace
(void)bCancel;
if(Streamer)Streamer->Stop();
Streamer.reset();
if(GpuFarPreparationThread.joinable())GpuFarPreparationThread.join();
GpuFarPreparationState.reset();
bGpuFarPreparationStarted=false;
if(FullShapeBuildThread.joinable())FullShapeBuildThread.join();
PendingFullShapeWorld.reset();
@@ -2598,7 +2616,6 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
void StartFarShapeGeneration()
{
if(!bFarShapeStagePending||!ShapeWorld||ShapeWorld->hasFarWorld()||!Streamer)return;
if(GpuFarPreparationThread.joinable())GpuFarPreparationThread.join();
ShapeWorld->buildFarWorld();
bFarShapeStagePending=false;
Streamer->Stop();
@@ -2745,6 +2762,7 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
Batch.TimestampFrequency,TileDispatchMs))
{
Tile.GpuDispatchMs=TileDispatchMs;Tile.bTimingResolved=true;
ReportGpuFarSlowDispatchTile(Key,Tile,TileDispatchMs);
GpuFarGpuDispatchMs+=TileDispatchMs;
if(TileDispatchMs>GpuFarGpuDispatchMaxMs)
{
@@ -2775,6 +2793,7 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
if(Tile.GpuDispatchMs==0.0)
{
Tile.GpuDispatchMs=AsyncDispatchMs;
ReportGpuFarSlowDispatchTile(Key,Tile,AsyncDispatchMs);
GpuFarGpuDispatchMs+=AsyncDispatchMs;
if(AsyncDispatchMs>GpuFarGpuDispatchMaxMs)
{
@@ -2795,91 +2814,122 @@ 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,
std::chrono::steady_clock::time_point DispatchAt)
void PrepareGpuFarInputTile(const FVoxelTileKey& Key,uint8 VisibleSkirtFaceMask,
const std::shared_ptr<FGpuFarPreparationBatch>& Batch)
{
if(NeedPreparation.empty())return;
const auto PrepareStart=std::chrono::steady_clock::now();
if(bGpuFarTimelineEnabled)
std::printf("[ColdTimeline] event=gpu_input_export_thread_started at_ms=%.3f dispatch_to_start_ms=%.3f frame=%llu\n",
ColdTimelineMilliseconds(PrepareStart),
std::chrono::duration<double,std::milli>(PrepareStart-DispatchAt).count(),
static_cast<unsigned long long>(ColdTimelineFrame()));
std::printf("[ColdTimeline] stage=gpu_input_export begin_ms=%.3f tiles=%zu\n",
ColdTimelineMilliseconds(PrepareStart),NeedPreparation.size());
const std::size_t ThreadCount=std::min<std::size_t>(8,NeedPreparation.size());
std::atomic<std::size_t> Next{0};
std::atomic<std::size_t> Failed{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)
if(Batch->bCancelled.load(std::memory_order_acquire))return;
std::call_once(Batch->StartOnce,[&]()
{
Workers.emplace_back([&,Worker]
Batch->PrepareStart=std::chrono::steady_clock::now();
if(bGpuFarTimelineEnabled)
std::printf("[ColdTimeline] event=gpu_input_export_workers_started at_ms=%.3f dispatch_to_start_ms=%.3f frame=%llu shared_workers=%zu\n",
ColdTimelineMilliseconds(Batch->PrepareStart),
std::chrono::duration<double,std::milli>(
Batch->PrepareStart-Batch->DispatchAt).count(),
static_cast<unsigned long long>(ColdTimelineFrame()),
Batch->SharedWorkerCount);
std::printf("[ColdTimeline] stage=gpu_input_export begin_ms=%.3f tiles=%zu shared_workers=%zu\n",
ColdTimelineMilliseconds(Batch->PrepareStart),Batch->TileCount,
Batch->SharedWorkerCount);
});
const auto WorkerStart=std::chrono::steady_clock::now();
const VoxelForgeCore::TileKey CoreKey{{Key.Coord.X,Key.Coord.Y,Key.Coord.Z},Key.Level};
FPreparedGpuFarInputResult Result;Result.Key=Key;
try
{
Result.Input=Key.Level<World->FullResClipLevels
?ShapeWorld->exportGpuFieldTile(CoreKey,Result.Error)
:ShapeWorld->exportFarFieldTile(CoreKey,Result.Error);
if(Result.Input&&Key.Level>=World->FullResClipLevels)
{
const auto WorkerStart=std::chrono::steady_clock::now();
for(;;)
Result.Input->visibleSkirtFaceMask=VisibleSkirtFaceMask;
// The CPU far mesher uses extent / CoarseTileCells, which can
// differ from Key.StepVoxels() when coarse tiles use a reduced
// cell count. Reuse the exported mesher step so skirt depth
// stays identical to the CPU reference.
Result.Input->skirtDepthVoxels=std::max(1.0f,
static_cast<float>(World->SkirtCells))*Result.Input->step;
if(!bGpuConnectorRepairEnabled)
{
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};
FPreparedGpuFarInputResult Result;Result.Key=Key;
try
{
Result.Input=Key.Level<World->FullResClipLevels
?ShapeWorld->exportGpuFieldTile(CoreKey,Result.Error)
:ShapeWorld->exportFarFieldTile(CoreKey,Result.Error);
if(Result.Input&&Key.Level>=World->FullResClipLevels)
{
Result.Input->visibleSkirtFaceMask=World->bGenerateSkirts
&&bGpuSkirtEmitEnabled
?ComputeVisibleSkirtFaceMask(Key,DesiredTileSet):0;
// The CPU far mesher uses extent / CoarseTileCells, which can
// differ from Key.StepVoxels() when coarse tiles use a reduced
// cell count. Reuse the exported mesher step so skirt depth
// stays identical to the CPU reference.
Result.Input->skirtDepthVoxels=std::max(1.0f,
static_cast<float>(World->SkirtCells))*Result.Input->step;
if(!bGpuConnectorRepairEnabled)
{
Result.Input->connectorPanels.clear();
Result.Input->connectorRingCount=0;
Result.Input->connectorVertexCount=0;
}
}
}
catch(const std::exception& Error) { Result.Error=Error.what(); }
Result.FinishedAt=std::chrono::steady_clock::now();
Result.FinishedFrame=ColdTimelineFrame();
if(!Result.Input)Failed.fetch_add(1,std::memory_order_relaxed);
{
std::lock_guard Lock(GpuFarPreparedResultsMutex);
GpuFarPreparedResults.push_back(std::move(Result));
}
Result.Input->connectorPanels.clear();
Result.Input->connectorRingCount=0;
Result.Input->connectorVertexCount=0;
}
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::printf("[GpuFarInputPrepare] tiles=%zu workers=%zu wall_ms=%.3f worker_sum_ms=%.3f failed=%zu\n",
NeedPreparation.size(),ThreadCount,PreparationMs,WorkerSumMs,
Failed.load(std::memory_order_relaxed));
std::printf("[ColdTimeline] stage=gpu_input_export end_ms=%.3f duration_ms=%.3f worker_sum_ms=%.3f\n",
ColdTimelineMilliseconds(std::chrono::steady_clock::now()),
PreparationMs,WorkerSumMs);
std::fflush(stdout);
catch(const std::exception& Error) { Result.Error=Error.what(); }
catch(...) { Result.Error="unknown input preparation failure"; }
if(Batch->bCancelled.load(std::memory_order_acquire))
{
Batch->Remaining.fetch_sub(1,std::memory_order_relaxed);
return;
}
Result.FinishedAt=std::chrono::steady_clock::now();
Result.FinishedFrame=ColdTimelineFrame();
if(!Result.Input)Batch->Failed.fetch_add(1,std::memory_order_relaxed);
const uint64 WorkerNanoseconds=static_cast<uint64>(std::chrono::duration_cast<
std::chrono::nanoseconds>(Result.FinishedAt-WorkerStart).count());
Batch->WorkerNanoseconds.fetch_add(WorkerNanoseconds,std::memory_order_relaxed);
{
std::lock_guard Lock(GpuFarPreparedResultsMutex);
if(Batch->bCancelled.load(std::memory_order_acquire)
||Batch->Generation!=GpuFarPreparationGeneration.load(std::memory_order_acquire))
{
Batch->Remaining.fetch_sub(1,std::memory_order_relaxed);
return;
}
GpuFarPreparedResults.push_back(std::move(Result));
}
if(Batch->Remaining.fetch_sub(1,std::memory_order_acq_rel)==1)
{
const auto PrepareEnd=std::chrono::steady_clock::now();
const double PreparationMs=std::chrono::duration<double,std::milli>(
PrepareEnd-Batch->PrepareStart).count();
const double WorkerSumMs=static_cast<double>(
Batch->WorkerNanoseconds.load(std::memory_order_relaxed))/1.0e6;
GpuFarPreparationWallMs+=PreparationMs;
GpuFarPreparationWorkerMs+=WorkerSumMs;
std::printf("[GpuFarInputPrepare] tiles=%zu workers=%zu mode=shared_priority_queue wall_ms=%.3f worker_sum_ms=%.3f failed=%zu\n",
Batch->TileCount,Batch->SharedWorkerCount,PreparationMs,WorkerSumMs,
Batch->Failed.load(std::memory_order_relaxed));
std::printf("[ColdTimeline] stage=gpu_input_export end_ms=%.3f duration_ms=%.3f worker_sum_ms=%.3f\n",
ColdTimelineMilliseconds(PrepareEnd),PreparationMs,WorkerSumMs);
std::fflush(stdout);
std::lock_guard Lock(GpuFarPreparedResultsMutex);
if(!Batch->bCancelled.load(std::memory_order_acquire)
&&Batch->Generation==GpuFarPreparationGeneration.load(std::memory_order_acquire))
bGpuFarPreparationFinished.store(true,std::memory_order_release);
}
}
void ReportGpuFarSlowDispatchTile(const FVoxelTileKey& Key,const FGpuFarTile& Tile,
double DispatchMs)const
{
if(!bGpuFarTimelineEnabled||DispatchMs<5.0)return;
std::printf("[GpuFarSlowDispatchTile] key=(%d,%d,%d,lod%d) dispatch_ms=%.6f cells=%d step=%d near_detail=%d protected=%llu thin=%llu solid=%llu near_proxy_samples=%u skirt_faces=0x%02x connector_rings=%u connector_vertices=%u\n",
Key.Coord.X,Key.Coord.Y,Key.Coord.Z,Key.Level,DispatchMs,Tile.InputCells,
Tile.InputStep,Tile.bNearDetailInput?1:0,
static_cast<unsigned long long>(Tile.ProtectedSamples),
static_cast<unsigned long long>(Tile.ProtectedThinSamples),
static_cast<unsigned long long>(Tile.ProtectedSolidSamples),Tile.NearProxySamples,
static_cast<unsigned>(Tile.VisibleSkirtFaceMask),Tile.ConnectorRings,
Tile.ConnectorVertices);
}
void ScheduleGpuFarTiles(const std::vector<FVoxelTileKey>& Keys)
{
if(GpuFarPreparationState)
GpuFarPreparationState->bCancelled.store(true,std::memory_order_release);
if(Streamer)Streamer->ClearPriorityTasks();
GpuFarPreparationState.reset();
{
std::lock_guard Lock(GpuFarPreparedResultsMutex);
GpuFarPreparationGeneration.fetch_add(1,std::memory_order_acq_rel);
GpuFarPreparedResults.clear();
}
GpuFarQueue.clear();GpuFarQueued.clear();GpuFarEligibleTiles.clear();
GpuFarPreparedInputs.clear();GpuFarPreparationErrors.clear();GpuFarPreparedAt.clear();
{std::lock_guard Lock(GpuFarPreparedResultsMutex);GpuFarPreparedResults.clear();}
GpuFarCandidateTiles.clear();GpuFarBaseCpuFallbackTiles.clear();
GpuFarCpuFallbackTiles.clear();GpuFarNeedPreparation.clear();
bGpuFarPreparationStarted=false;bGpuFarInputsFinalized=false;
@@ -3044,7 +3094,6 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
bGpuFarPreparationFinished.store(true,std::memory_order_relaxed);
return;
}
bGpuFarPreparationFinished.store(false,std::memory_order_relaxed);
const auto PreparationListStart=std::chrono::steady_clock::now();
std::vector<FVoxelTileKey> NeedPreparation=GpuFarNeedPreparation;
std::stable_sort(NeedPreparation.begin(),NeedPreparation.end(),
@@ -3052,45 +3101,38 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
const double PreparationListMs=std::chrono::duration<double,std::milli>(
std::chrono::steady_clock::now()-PreparationListStart).count();
const auto DispatchAt=std::chrono::steady_clock::now();
auto Batch=std::make_shared<FGpuFarPreparationBatch>();
Batch->DispatchAt=DispatchAt;Batch->TileCount=NeedPreparation.size();
Batch->SharedWorkerCount=Streamer?static_cast<std::size_t>(Streamer->GetWorkerCount()):0;
Batch->Generation=GpuFarPreparationGeneration.load(std::memory_order_acquire);
Batch->Remaining.store(NeedPreparation.size(),std::memory_order_relaxed);
GpuFarPreparationState=Batch;
bGpuFarPreparationFinished.store(false,std::memory_order_relaxed);
if(bGpuFarTimelineEnabled)
std::printf("[ColdTimeline] event=gpu_input_export_thread_dispatch at_ms=%.3f after_pool_ms=%.3f memory_sample_ms=%.3f list_copy_sort_ms=%.3f tiles=%zu\n",
std::printf("[ColdTimeline] event=gpu_input_export_priority_queue at_ms=%.3f after_pool_ms=%.3f memory_sample_ms=%.3f list_copy_sort_ms=%.3f tiles=%zu shared_workers=%zu\n",
ColdTimelineMilliseconds(DispatchAt),
std::chrono::duration<double,std::milli>(DispatchAt-PoolEnd).count(),
MemorySampleMs,PreparationListMs,NeedPreparation.size());
GpuFarPreparationThread=std::thread([this,NeedPreparation,DispatchAt]()
MemorySampleMs,PreparationListMs,NeedPreparation.size(),Batch->SharedWorkerCount);
constexpr std::size_t GpuInputPriorityBurst=3;
std::printf("[GpuFarSchedulingPolicy] workers=%zu gpu_input_priority_burst=%zu cpu_mesh_jobs_per_burst=1 mode=shared_worker_pool\n",
Batch->SharedWorkerCount,GpuInputPriorityBurst);
std::vector<std::function<void()>> Tasks;Tasks.reserve(NeedPreparation.size());
for(const FVoxelTileKey& Key:NeedPreparation)
{
const bool bPrioritySet=SetThreadPriority(GetCurrentThread(),THREAD_PRIORITY_NORMAL)!=0;
if(bGpuFarTimelineEnabled)
std::printf("[GpuFarPreparationThread] priority=%s process=BelowNormal thread_relative=Normal\n",
bPrioritySet?"Normal":"set_failed");
try { PrepareGpuFarInputs(NeedPreparation,DispatchAt); }
catch(const std::exception& Error)
{
std::lock_guard Lock(GpuFarPreparedResultsMutex);
for(const FVoxelTileKey& Key:NeedPreparation)
{
FPreparedGpuFarInputResult Result;Result.Key=Key;Result.Error=Error.what();
Result.FinishedAt=std::chrono::steady_clock::now();
Result.FinishedFrame=ColdTimelineFrame();
GpuFarPreparedResults.push_back(std::move(Result));
}
std::fprintf(stderr,"[GpuFarInputPrepareError] %s\n",Error.what());
}
catch(...)
{
std::lock_guard Lock(GpuFarPreparedResultsMutex);
for(const FVoxelTileKey& Key:NeedPreparation)
{
FPreparedGpuFarInputResult Result;Result.Key=Key;
Result.Error="unknown input preparation failure";
Result.FinishedAt=std::chrono::steady_clock::now();
Result.FinishedFrame=ColdTimelineFrame();
GpuFarPreparedResults.push_back(std::move(Result));
}
std::fprintf(stderr,"[GpuFarInputPrepareError] unknown failure\n");
}
const uint8 VisibleSkirtFaceMask=Key.Level>=World->FullResClipLevels
&&World->bGenerateSkirts&&bGpuSkirtEmitEnabled
?ComputeVisibleSkirtFaceMask(Key,DesiredTileSet):0;
Tasks.emplace_back([this,Key,VisibleSkirtFaceMask,Batch]()
{PrepareGpuFarInputTile(Key,VisibleSkirtFaceMask,Batch);});
}
if(!Streamer||!Streamer->QueuePriorityTasks(std::move(Tasks)))
{
std::fprintf(stderr,"[GpuFarInputPrepareError] shared worker queue is unavailable\n");
GpuFarPreparationState.reset();bGpuFarEnabled=false;
bGpuFarInputsFinalized=true;
bGpuFarPreparationFinished.store(true,std::memory_order_release);
});
AddGpuFarCpuFallbackTiles(DesiredTiles);
}
}
bool GpuFarKeyLess(const FVoxelTileKey& A,const FVoxelTileKey& B)const
@@ -3237,7 +3279,6 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
}
if(!bGpuFarPreparationFinished.load(std::memory_order_acquire))
return !GpuFarQueue.empty();
if(GpuFarPreparationThread.joinable())GpuFarPreparationThread.join();
if(!bGpuFarEnabled)
{
GpuFarCandidateTiles.clear();GpuFarQueue.clear();GpuFarQueued.clear();
@@ -3378,6 +3419,16 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
break;
}
const uint8 PreparedSkirtFaceMask=PreparedInput->visibleSkirtFaceMask;
Tile.InputCells=PreparedInput->cells;Tile.InputStep=PreparedInput->step;
Tile.bNearDetailInput=PreparedInput->nearDetailEnabled;
Tile.ProtectedSamples=PreparedInput->protectedSampleCount;
Tile.ProtectedThinSamples=PreparedInput->protectedThinSampleCount;
Tile.ProtectedSolidSamples=PreparedInput->protectedSolidSampleCount;
Tile.NearProxySamples=static_cast<uint32>(std::min<std::size_t>(
PreparedInput->nearPackedProxy.size(),std::numeric_limits<uint32>::max()));
Tile.ConnectorRings=PreparedInput->connectorRingCount;
Tile.ConnectorVertices=PreparedInput->connectorVertexCount;
Tile.VisibleSkirtFaceMask=PreparedSkirtFaceMask;
if(!VoxelForgeGpuFarViewer::BuildFarTile(Device,Context,*ShapeWorld,CoreKey,
PreparedInput,
GpuFarShaderPath,&Tile.VertexBuffer,&Tile.IndexBuffer,&Tile.IndirectArgs,
@@ -3809,7 +3860,6 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
}
Tile.VertexCapacityBytes=static_cast<uint64>(
VoxelForgeGpuFarViewer::PooledOutputVertexCapacityPerTile)*32u;
Tile.VisibleSkirtFaceMask=PreparedSkirtFaceMask;
const auto PreparedAtIt=GpuFarPreparedAt.find(Key);
if(PreparedAtIt!=GpuFarPreparedAt.end())Tile.PreparedAt=PreparedAtIt->second;
Tile.SubmittedAt=std::chrono::steady_clock::now();
@@ -3817,6 +3867,7 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
GpuFarEnqueueWallSeconds+=EnqueueMs/1000.0;
if(GpuDispatchMs>0.0)
{
ReportGpuFarSlowDispatchTile(Key,Tile,GpuDispatchMs);
GpuFarGpuDispatchMs+=GpuDispatchMs;
if(GpuDispatchMs>GpuFarGpuDispatchMaxMs)
{
+45 -5
View File
@@ -11,8 +11,11 @@
#include <chrono>
#include <cstdint>
#include <cstring>
#include <deque>
#include <functional>
#include <fstream>
#include <unordered_set>
#include <utility>
struct FStandaloneTileKeyHash
{
@@ -467,12 +470,30 @@ public:
bUseTileFilter=true;
}
bool QueuePriorityTasks(std::vector<std::function<void()>> Tasks)
{
if(Tasks.empty())return true;
{
std::lock_guard<std::mutex> Lock(Mutex);
if(bStop||Workers.empty())return false;
for(auto& Task:Tasks)PriorityTasks.push_back(std::move(Task));
}
Wake.notify_all();
return true;
}
void ClearPriorityTasks()
{
std::lock_guard<std::mutex> Lock(Mutex);
PriorityTasks.clear();
}
void Stop()
{
{
std::lock_guard<std::mutex> Lock(Mutex);
bStop=true;
Queue.clear();
Queue.clear();PriorityTasks.clear();
}
Wake.notify_all();
for(std::thread& Worker:Workers)if(Worker.joinable())Worker.join();
@@ -799,16 +820,34 @@ private:
void WorkerLoop()
{
constexpr uint32 PriorityTaskBurst=3;
uint32 ConsecutivePriorityTasks=0;
for(;;)
{
FStandaloneTileJob Job;
std::function<void()> PriorityTask;
{
std::unique_lock<std::mutex> Lock(Mutex);
Wake.wait(Lock,[&](){return bStop||!Queue.empty();});
Wake.wait(Lock,[&](){return bStop||!PriorityTasks.empty()||!Queue.empty();});
if(bStop)return;
std::pop_heap(Queue.begin(),Queue.end(),FStandaloneTileJobHeapOrder{});
Job=Queue.back();Queue.pop_back();Job.StartedAt=FClock::now();
Running.insert(Job.Key);
const bool bPreferPriority=!PriorityTasks.empty()
&&(Queue.empty()||ConsecutivePriorityTasks<PriorityTaskBurst);
if(bPreferPriority)
{
PriorityTask=std::move(PriorityTasks.front());PriorityTasks.pop_front();
++ConsecutivePriorityTasks;
}
else
{
std::pop_heap(Queue.begin(),Queue.end(),FStandaloneTileJobHeapOrder{});
Job=Queue.back();Queue.pop_back();Job.StartedAt=FClock::now();
Running.insert(Job.Key);ConsecutivePriorityTasks=0;
}
}
if(PriorityTask)
{
PriorityTask();
continue;
}
if(bColdTimelineTrace)TraceTileEvent("started",Job.Key,Job.StartedAt,Job.QueuedAt,Job.StartedAt);
FStandaloneCpuTile Tile=Generate(Job);
@@ -857,6 +896,7 @@ private:
std::condition_variable Wake;
bool bStop=true;
std::vector<std::thread> Workers;
std::deque<std::function<void()>> PriorityTasks;
std::vector<FStandaloneTileJob> Queue;
std::vector<FStandaloneCpuTile> Ready;
std::unordered_set<FVoxelTileKey,FStandaloneTileKeyHash> Running;