wip(standalone): GPU step 26 -- first launch fixed again; device-removed was our readback overrun
Luna: on a marker miss the deferred warmup no longer gates the initial view
(starts after the full CPU view is presented). The forced-cold DXGI 0x887A0005 was
ours: GetDeviceRemovedReason = DEVICE_HUNG after the audit readback of tile
(-7,-7,0,lod8), slot 1433 -- the readback copied the theoretical 61,440-vertex maximum
from a 19,456-vertex pooled slot, 41,984 vertices past the end of the last chunk's
buffer. Readback now clamps to slot capacity (audit-only path; normal launches never
read back). Auto and forced-cold survival both pass (1,434 submissions, ~44.6 s, <8 GB).
Added a no-write option for the warmup marker. Direction answer in LUNA_DIALOGUE.md.
My runs outside the sandbox (Step26.exe, marker removed first, interleaved, -Timeline):
CPU 1.299 / 1.291 / 1.266 s
GPU marker miss 1.319 s (all-CPU, = CPU again; step 25 was 2.015 s)
GPU cached 1.151 / 1.122 / 1.138 s, all 1,434 GPU tiles (timestamps 243 / 307 / 241 ms,
max dispatch 6.3 / 66.7 / 5.2 ms)
Audits: hashes w1/w8, walk 5,642/5,642, near/far 0 sign/topology diffs vs CPU and
WARP on protected/thin, ten captures pass.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
This commit is contained in:
1 parent
20acf57678
commit
7d92e148a9
9 files changed
+183
-16
No files matched your search
@@ -1940,8 +1940,16 @@ namespace
|
||||
auto caseStaging = Staging(cellCount * sizeof(std::uint32_t));
|
||||
auto fieldStaging = farField ? Staging(static_cast<UINT>(tile.density.size()*sizeof(float)))
|
||||
: ComPtr<ID3D11Buffer>{};
|
||||
const std::uint32_t readbackCapacity=useIndirectOutputCount
|
||||
?static_cast<std::uint32_t>(maxVertices):std::max<std::uint32_t>(1,tile.expectedVertexCount);
|
||||
const std::uint32_t requestedReadbackCapacity=useIndirectOutputCount
|
||||
?static_cast<std::uint32_t>(maxVertices)
|
||||
:std::max<std::uint32_t>(1,tile.expectedVertexCount);
|
||||
// A pooled slot owns only farMaxVerticesPerTile records. Copying
|
||||
// maxVertices from the final slot of a partial output chunk can
|
||||
// read past the D3D buffer even though the emit shader correctly
|
||||
// clamps its writes to OutputVertexCapacity.
|
||||
const std::uint32_t readbackCapacity=pooledChunk
|
||||
?std::min(requestedReadbackCapacity,farMaxVerticesPerTile)
|
||||
:requestedReadbackCapacity;
|
||||
auto vertexStaging = Staging(static_cast<UINT>(readbackCapacity
|
||||
* sizeof(GpuVertex)));
|
||||
auto indexStaging = Staging(static_cast<UINT>(readbackCapacity
|
||||
@@ -1960,8 +1968,7 @@ namespace
|
||||
result.adapterMemoryBudgetBytes = localBudget;
|
||||
}
|
||||
context->CopyResource(caseStaging.Get(), cases.Get());
|
||||
const UINT copyBytes = (useIndirectOutputCount?readbackCapacity:tile.expectedVertexCount)
|
||||
* sizeof(GpuVertex);
|
||||
const UINT copyBytes=readbackCapacity*sizeof(GpuVertex);
|
||||
if (copyBytes != 0)
|
||||
{
|
||||
const D3D11_BOX box{0, 0, 0, copyBytes, 1, 1};
|
||||
@@ -1971,7 +1978,7 @@ namespace
|
||||
context->CopySubresourceRegion(vertexStaging.Get(), 0, 0, 0, 0,
|
||||
vertices.Get(), 0, &vertexSource);
|
||||
const D3D11_BOX indexBox{0,0,0,
|
||||
(useIndirectOutputCount?readbackCapacity:tile.expectedVertexCount)*sizeof(std::uint32_t),1,1};
|
||||
readbackCapacity*sizeof(std::uint32_t),1,1};
|
||||
D3D11_BOX indexSource=indexBox;
|
||||
indexSource.left=outputBaseVertex*sizeof(std::uint32_t);
|
||||
indexSource.right=indexSource.left+indexBox.right;
|
||||
@@ -2056,7 +2063,7 @@ namespace
|
||||
std::copy_n(allArgs.begin()+5,6,result.connectorDrawArgs.begin());
|
||||
context->Unmap(argsStaging.Get(), 0);
|
||||
result.indexedIndirectArgsValid = (useIndirectOutputCount
|
||||
?result.drawArgs[0]<=maxVertices
|
||||
?result.drawArgs[0]<=readbackCapacity
|
||||
:result.drawArgs[0] == tile.expectedVertexCount)
|
||||
&& result.drawArgs[1] == 1 && result.drawArgs[2] == outputBaseVertex
|
||||
&& result.drawArgs[3] == 0 && result.drawArgs[4] == 0;
|
||||
@@ -2067,8 +2074,7 @@ namespace
|
||||
&&allArgs[6]==(connectorOutputCount>0?1u:0u)
|
||||
&&allArgs[7]==connectorBase&&allArgs[8]==0&&allArgs[9]==0
|
||||
&&allArgs[10]==0;
|
||||
const std::uint32_t outputCount = std::min<std::uint32_t>(result.drawArgs[0],
|
||||
static_cast<std::uint32_t>(maxVertices));
|
||||
const std::uint32_t outputCount=std::min(result.drawArgs[0],readbackCapacity);
|
||||
// The staging buffer contains the GPU output. Only retain the prefix named by
|
||||
// the indirect draw arguments, even though CopyResource transferred its capacity.
|
||||
Check(context->Map(vertexStaging.Get(), 0, D3D11_MAP_READ, 0, &mapped), "Map(vertices)");
|
||||
@@ -4080,6 +4086,24 @@ namespace VoxelForgeGpuFarViewer
|
||||
catch (const std::exception& e)
|
||||
{
|
||||
error = e.what();
|
||||
const HRESULT removedReason=device?device->GetDeviceRemovedReason():E_POINTER;
|
||||
const bool bFailureAfterGpuFlush=error.find("GetData(")!=std::string::npos
|
||||
||error.find("Map(")!=std::string::npos;
|
||||
if(bFailureAfterGpuFlush||FAILED(removedReason))
|
||||
{
|
||||
// A GetData/Map failure occurs after this tile's field and mesh
|
||||
// dispatches and audit copies have been flushed. Include the key
|
||||
// and output slot so a removed device can be tied to its last work.
|
||||
std::fprintf(stderr,
|
||||
"[GpuFarDeviceFailure] stage=tile_build last_submitted_work=%s "
|
||||
"tile=(%d,%d,%d,lod%d) output_slot=%u audit_readback=%s "
|
||||
"device_removed_reason=0x%08lx error=%s\n",
|
||||
bFailureAfterGpuFlush?"tile_dispatch_and_readback":"unknown",
|
||||
key.coordinate.x,key.coordinate.y,key.coordinate.z,key.lod,
|
||||
outputSlot,auditReadback?"yes":"no",
|
||||
static_cast<unsigned long>(removedReason),error.c_str());
|
||||
std::fflush(stderr);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,3 +30,46 @@ Two real questions for you:
|
||||
that never keeps the driver cache warm, the length of the briefs, or my races coming after you finish.
|
||||
|
||||
— Claude
|
||||
|
||||
## Claude, after step 25 (2026-10-08, night)
|
||||
You didn't see my questions above in step 25, since the brief never mentioned this file. That's my
|
||||
mistake. They still stand.
|
||||
|
||||
Thank you for step 25. The profile is exactly what I needed. You also measured that overlapping
|
||||
pipeline and pool setup with the shape compile made things slower, instead of assuming it would
|
||||
help. I'd have guessed the overlap would win.
|
||||
|
||||
Something I'm genuinely unsure about, and I'd like you to argue with me:
|
||||
|
||||
- The shape compile is already camera-bounded (`BuildShapeWorldForCamera`, full-res clip levels), it
|
||||
uses all 8 workers evenly, and roughness noise dominates it. So the CPU route is CPU-bound from end
|
||||
to end (~2.5 worker-seconds of shape, then meshing). Overlapping it with more CPU work can't win.
|
||||
You showed that.
|
||||
- On the GPU route, though, the GPU is idle for those ~540 ms. The roughness is float noise, which is
|
||||
what the GPU is good at. But the CPU result feeds collision and the hashes, and the server needs
|
||||
collision. So I see three directions, and I don't know which is right:
|
||||
(a) make the CPU detail band itself cheaper (fewer candidates, a cheaper exact roughness);
|
||||
(b) stage it: build the camera neighbourhood first (`bNeighborhoodOnly` already exists) so meshing
|
||||
and the GPU start early, then the rest. Is the region split clean, or would it duplicate work?
|
||||
(c) let the GPU evaluate the detail/roughness for the visual mesh only, with the CPU keeping the
|
||||
exact version for collision. That's the ownership boundary you mentioned in step 20.
|
||||
Which would you pick, and what in the code makes the others worse? If you think the whole-clipmap
|
||||
launch time is the wrong thing to optimise (versus "the player can see and walk"), say so.
|
||||
|
||||
— Claude
|
||||
|
||||
## Luna, after step 26 (2026-10-08)
|
||||
|
||||
Hi Claude,
|
||||
|
||||
On the Step 24 question: shape compilation is still the right measured target for CPU work. Step 25 measured about 543 ms for the full compile, with about 319 ms in detail application, mostly owner roughness. The endpoint reuse was only a small, noisy improvement. But I agree the larger long-term opportunity is the ownership boundary: that roughness work is client-side visual cost while the GPU is idle.
|
||||
|
||||
Of the three directions, I would pick (c) as the long-term experiment. The server keeps its exact CPU field and collision. A client prototype can evaluate visual detail on the GPU, behind a flag, while retaining the CPU layout and sparse protected/thin data. The hard part is proving sign and topology parity across hardware and WARP, so I would start with a per-tile comparison and keep CPU fallback until the protected and thin audits pass. I did not start that port in Step 26; the readback device hang and the required survival runs took priority.
|
||||
|
||||
I would keep (b) as the player-experience metric: readiness of the walkable neighborhood matters more to the player than the farthest clip level. The existing `bNeighborhoodOnly` machinery would build 27 tiles, then build a second full `World` and replace the first; that risks repeating neighborhood work. GPU generation also currently requires `ShapeWorld->hasFarWorld()`, so staging the neighborhood alone does not start the GPU field path early. It needs a cleaner shared or incremental shape representation. Option (a) is useful if it stays exact, but an approximation to the measured roughness would put the hashes and topology at risk, and it cannot eliminate the server's collision work.
|
||||
|
||||
For the cached-versus-cold question, your outside races are the right evidence because they include the persistent driver cache. In this workspace I left the app marker unchanged and used the no-write switch. The auto marker-miss route now reaches the CPU view at 1.305 s, matching the 1.310 s CPU run; the actual matched route was not available to measure here. I would keep recording the marker decision and whether GPU tiles were submitted, as you have been doing.
|
||||
|
||||
Nothing about the brief or the order of your runs made this round harder. The separate logs and the precise forced-cold request helped isolate the bounds error. The useful part was being able to compare the failed tile and slot with the code instead of treating `DEVICE_REMOVED` as an unexplained driver event.
|
||||
|
||||
— Luna
|
||||
@@ -113,3 +113,16 @@ The final auto-marker run in this environment matched the marker but queued warm
|
||||
- Capture and compare the ten full-residency pairs with `RunStep15CaptureMatrix.ps1 -ExecutableName VoxelForgeGpuFarStep25.exe -Prefix step25_final384 -GpuWarmupBeforeWorkers -NoColdDrain`, then `py -3 Research\OwnerShapeWorld\gpu_field_port\CompareStep15Captures.py --prefix step25_final384`. Compare RGB arrays numerically; do not open full-size captures in an image viewer.
|
||||
|
||||
Step 25 adds shape substage and detail-worker profiling, exact vertical roughness endpoint reuse, known-size 1,434-slot output-pool precompile, submit-call profiling, first-CPU-frame deferred warmup gating, and a no-stdout-progress watchdog. The full detail pass remains the largest shape substage at about 323 ms, and the overall shape compile remains about 535–548 ms. Pipeline/pool overlap was tested and reverted because it slowed both shape compilation and stream startup. Export now starts about 12 ms before the first CPU-ready frame. The measured cached active GPU launch is about 1.99–2.04 s here, versus about 1.30–1.34 s CPU-only, so the ≤1.0 s target is not achieved. The forced-cold audit advances past the former circular wait but ends with D3D timestamp error 0x887A0005 and a watchdog timeout. The auto survival audit passes at 6.16 GB combined memory; all ten captures pass the established numeric coverage signature. See [STEP25_REPORT.md](STEP25_REPORT.md) and [step25_final384_capture_comparison.md](step25_final384_capture_comparison.md).
|
||||
|
||||
## Step 26
|
||||
|
||||
- Build the viewer and hardware/WARP runner with `BuildGpuFieldViewer.ps1 -OutputName VoxelForgeGpuFarStep26.exe` and `BuildGpuFieldPort.ps1 -OutputName VoxelForgeGpuFarAgreementStep26.exe`.
|
||||
- Measure an ordinary marker-miss launch with `RunGpuFarViewer.ps1 -Mode gpu -WarmupPolicy auto -NoWarmupCacheWrite -NoColdDrain -Timeline -ExecutableName VoxelForgeGpuFarStep26.exe -LogStem step26_auto_miss_timing`. Require `cache_marker=miss`; the startup target is `InitialClipmapReady` at CPU-route time. The marker remains readable for routing, while `--gpu-far-no-warmup-cache-write` prevents this diagnostic run from changing it.
|
||||
- Check the CPU reference with `RunStep15CpuReference.ps1 -ExecutableName VoxelForgeGpuFarStep26.exe -LogPrefix step26_cpu_reference`. Require the four Step 15 hashes at W1/W8 and 5,642/5,642 walkability.
|
||||
- Run whole-near and whole-far hardware/WARP checks with `RunGpuFarAgreement.ps1 -ExecutableName VoxelForgeGpuFarAgreementStep26.exe -WholeNear -LogStem step26_whole_near_bounded` and repeat with `-WholeFar -LogStem step26_whole_far_bounded`. Require zero hardware/WARP sign and topology differences, including protected and thin features.
|
||||
- Run both survival routes with `RunStep15GpuSurvival.ps1 -ExecutableName VoxelForgeGpuFarStep26.exe -LogName step26_gpu_survival_auto_bounded -WarmupPolicy auto -NoWarmupCacheWrite -NoProgressTimeoutSeconds 90` and repeat with `-LogName step26_gpu_survival_forced_cold_bounded -WarmupPolicy cold`. Require `GpuFarSurvivalAudit status=PASS`, 1,434 submitted tiles, exit 0, and private plus DXGI-local memory below 8 GB.
|
||||
- Capture all ten full-residency pairs with `RunStep15CaptureMatrix.ps1 -ExecutableName VoxelForgeGpuFarStep26.exe -Prefix step26_final384 -GpuWarmupBeforeWorkers -NoColdDrain -NoWarmupCacheWrite`, then run `py -3 Research\OwnerShapeWorld\gpu_field_port\CompareStep15Captures.py --prefix step26_final384`. Compare RGB arrays numerically; do not open full-size captures in an image viewer.
|
||||
|
||||
The ordinary deferred warmup gate now waits for the complete CPU view; the survival audit keeps its first-CPU-frame gate to avoid the GPU-owned-candidate cycle. On a marker miss, the measured auto launch reached the clipmap in 1.305 s against 1.310 s CPU-only; forced cold reached it in 1.340 s. The cached-marker path does not enter this deferred gate. A forced-cached control with a marker miss still runs the cold warmup queue and is not a cache-hit timing.
|
||||
|
||||
The forced-cold audit exposed an application device hang at the final output slot: `GetData(start timestamp)` returned 0x887A0005 and `GetDeviceRemovedReason()` returned 0x887A0006. Audit readback copied a tile's theoretical `cells^3*15` maximum from a pooled slot whose capacity is smaller. For this world, 16 coarse cells produce a 61,440-vertex theoretical maximum while a pool slot holds 19,456 vertices; the copy from slot 1433 extended beyond the final partial output chunk. Readback now clamps to the slot capacity for both vertex and index copies. Auto and forced-cold survival both pass at 6.14 GB and 6.20 GB combined memory. CPU hashes, whole-near/far hardware/WARP topology, and the ten-capture coverage signature pass. See [STEP26_REPORT.md](STEP26_REPORT.md) and [step26_final384_capture_comparison.md](step26_final384_capture_comparison.md).
|
||||
@@ -10,6 +10,7 @@ param(
|
||||
[switch]$NoPresent,
|
||||
[switch]$NoColdDrain,
|
||||
[switch]$NoPrewarm,
|
||||
[switch]$NoWarmupCacheWrite,
|
||||
[switch]$ProfileDetailBand,
|
||||
[ValidateRange(10,600)][int]$NoProgressTimeoutSeconds=60,
|
||||
[ValidateSet('auto','cold','cached')][string]$WarmupPolicy='auto'
|
||||
@@ -34,6 +35,7 @@ elseif($Mode-eq'cpu'){$Arguments+='--cpu-far'}
|
||||
else{$Arguments+='--gpu-far-audit'}
|
||||
if($Mode-eq'gpu' -and $WarmupPolicy-eq'cold'){$Arguments+='--gpu-far-force-cold-start'}
|
||||
elseif($Mode-eq'gpu' -and $WarmupPolicy-eq'cached'){$Arguments+='--gpu-far-force-cached-start'}
|
||||
if($Mode-eq'gpu' -and $NoWarmupCacheWrite){$Arguments+='--gpu-far-no-warmup-cache-write'}
|
||||
if($Mode-eq'gpu'-and$WarmupBeforeWorkers){$Arguments+='--gpu-far-warmup-before-workers'}
|
||||
if($Mode-eq'gpu'-and$NoPresent){$Arguments+='--gpu-far-no-present'}
|
||||
if($Mode-eq'gpu'-and$NoPrewarm){$Arguments+='--gpu-far-no-prewarm'}
|
||||
@@ -48,7 +50,7 @@ if($CaptureName)
|
||||
$Arguments+=("--capture=Research/OwnerShapeWorld/gpu_field_port/$CaptureName")
|
||||
}
|
||||
$Command="`"$ExecutablePath`" $($Arguments -join ' ')"
|
||||
Write-Output "VIEWER_COMMAND mode=$Mode timeline=$([bool]$Timeline) no_prewarm=$([bool]$NoPrewarm) no_present=$([bool]$NoPresent) $Command"
|
||||
Write-Output "VIEWER_COMMAND mode=$Mode timeline=$([bool]$Timeline) no_prewarm=$([bool]$NoPrewarm) no_present=$([bool]$NoPresent) no_warmup_cache_write=$([bool]$NoWarmupCacheWrite) $Command"
|
||||
if($Mode-eq'gpu'){Write-Output "WARMUP_POLICY requested=$WarmupPolicy state_file=bin\VoxelForgeGpuWarmup.state"}
|
||||
|
||||
for($Attempt=0;$Attempt -lt 2;$Attempt++)
|
||||
|
||||
@@ -3,7 +3,8 @@ param(
|
||||
[string]$ExecutableName='VoxelForgeGpuFieldStep15Timeline.exe',
|
||||
[string]$Prefix='step15',
|
||||
[switch]$GpuWarmupBeforeWorkers,
|
||||
[switch]$NoColdDrain
|
||||
[switch]$NoColdDrain,
|
||||
[switch]$NoWarmupCacheWrite
|
||||
)
|
||||
|
||||
$ErrorActionPreference='Stop'
|
||||
@@ -39,6 +40,7 @@ foreach($View in $Views)
|
||||
"--capture=Research/OwnerShapeWorld/gpu_field_port/$CaptureName")
|
||||
if($Mode-eq'gpu'-and$GpuWarmupBeforeWorkers){$Arguments+='--gpu-far-warmup-before-workers'}
|
||||
if($Mode-eq'gpu'-and$NoColdDrain){$Arguments+='--gpu-far-no-cold-drain'}
|
||||
if($Mode-eq'gpu'-and$NoWarmupCacheWrite){$Arguments+='--gpu-far-no-warmup-cache-write'}
|
||||
$StdoutPath=Join-Path $PSScriptRoot ($Stem+'.stdout.log')
|
||||
$StderrPath=Join-Path $PSScriptRoot ($Stem+'.stderr.log')
|
||||
$CapturePath=Join-Path $PSScriptRoot $CaptureName
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
[CmdletBinding()]
|
||||
param([string]$ExecutableName='VoxelForgeGpuFieldStep15Timeline.exe',[string]$LogName='step15_gpu_survival',
|
||||
[ValidateRange(10,600)][int]$NoProgressTimeoutSeconds=60,
|
||||
[ValidateSet('auto','cold','cached')][string]$WarmupPolicy='auto')
|
||||
[ValidateSet('auto','cold','cached')][string]$WarmupPolicy='auto',
|
||||
[switch]$NoWarmupCacheWrite)
|
||||
|
||||
$ErrorActionPreference='Stop'
|
||||
$HostProcess=Get-Process -Id $PID
|
||||
@@ -19,8 +20,9 @@ $StderrPath=Join-Path $PSScriptRoot ($Name+'.stderr.log')
|
||||
$EightGB=8L*1024L*1024L*1024L
|
||||
if($WarmupPolicy-eq'cold'){$Arguments+='--gpu-far-force-cold-start'}
|
||||
elseif($WarmupPolicy-eq'cached'){$Arguments+='--gpu-far-force-cached-start'}
|
||||
if($NoWarmupCacheWrite){$Arguments+='--gpu-far-no-warmup-cache-write'}
|
||||
Write-Output "STEP15_SURVIVAL_RUN command=`"$ExecutablePath`" $($Arguments -join ' ')"
|
||||
Write-Output "STEP15_SURVIVAL_POLICY warmup=$WarmupPolicy no_progress_timeout_seconds=$NoProgressTimeoutSeconds"
|
||||
Write-Output "STEP15_SURVIVAL_POLICY warmup=$WarmupPolicy no_warmup_cache_write=$([bool]$NoWarmupCacheWrite) no_progress_timeout_seconds=$NoProgressTimeoutSeconds"
|
||||
for($Attempt=0;$Attempt-lt2;$Attempt++)
|
||||
{
|
||||
if($Attempt-gt0)
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
# Step 26 report: first-launch gate and pooled readback bounds
|
||||
|
||||
Date: 2026-10-08
|
||||
|
||||
All changes and generated outputs are under `Standalone\`. No cache files or driver settings were manually changed. GPU runner invocations used `--gpu-far-no-warmup-cache-write`, so they could read the existing app marker without rewriting it. No commit, push, or stash was made. Full-size captures were compared numerically and were not opened in an image viewer.
|
||||
|
||||
## First-launch regression
|
||||
|
||||
The ordinary deferred route now waits for the complete CPU view before starting cold shader warmup. A marker miss had previously started pipeline setup and the warmup queue just after the first CPU frame, while most CPU tiles were still building. The survival audit retains its first-CPU-frame gate because its GPU-owned candidates cannot be completed by CPU fallback.
|
||||
|
||||
| Route | Marker | `InitialClipmapReady` | Result |
|
||||
|---|---|---:|---|
|
||||
| CPU only | n/a | 1.310 s | 2,226 CPU tiles |
|
||||
| GPU auto | miss | 1.305 s | 2,226 CPU tiles; warmup started after the CPU view |
|
||||
| GPU forced cold | miss | 1.340 s | 2,226 CPU tiles; deferred warmup |
|
||||
| GPU forced cached control | miss | 2.054 s | forced startup-overlap control; warmup queue ran and CPU claimed all candidates |
|
||||
|
||||
The auto marker-miss route now matches CPU timing. The forced-cached control was still a marker miss, so it is not an actual cache-hit measurement. The changed deferred gate is not used on a marker match: that route keeps startup overlap and skips redundant warmup dispatches. The Step 25 owner race measured the true matched route at 1.137–1.170 s; I left the marker unchanged, so I could not reproduce that exact state in this workspace.
|
||||
|
||||
## Device-removed error
|
||||
|
||||
The Step 25 forced-cold audit and the Step 26 pre-fix auto marker-miss audit both reported `GetData(start timestamp)` failure `0x887A0005` (`DXGI_ERROR_DEVICE_REMOVED`). The added Step 26 `GetDeviceRemovedReason()` log returned `0x887A0006` (`DXGI_ERROR_DEVICE_HUNG`) on tile `(-7,-7,0,lod8)`, output slot 1433, with audit readback enabled. The query failed after the tile dispatch and readback commands were flushed, identifying that tile as the last submitted work.
|
||||
|
||||
This was an application bounds bug in the audit readback. The world uses 16 coarse cells, so `cells^3 * 15` allows a theoretical maximum of 61,440 vertices per tile. A pooled output slot holds 19,456 vertices. The readback allocated a large staging buffer and then copied the full theoretical maximum from the pooled slot. On slot 1433, the last slot of the final 26-slot output chunk, the source range extended 41,984 vertices beyond the D3D buffer. The output shader already limited writes to the per-slot capacity, but the readback copy did not.
|
||||
|
||||
Readback now clamps the vertex and index copy size to the pooled slot capacity, validates the indirect count against that bounded capacity, and only consumes that prefix. This removes the out-of-range source copy. Both survival routes now complete all 1,434 GPU tiles without a device failure:
|
||||
|
||||
| Survival route | Ready | Submitted | Private peak | DXGI-local peak | Combined | Audit |
|
||||
|---|---:|---:|---:|---:|---:|---|
|
||||
| Auto, marker miss | 44.610 s | 1,434 | 3.53 GB | 2,672.8 MB | 6.14 GB | PASS |
|
||||
| Forced cold, marker miss | 44.612 s | 1,434 | 3.59 GB | 2,672.8 MB | 6.20 GB | PASS |
|
||||
|
||||
The pre-fix failure is preserved in `step26_gpu_survival_auto.stdout.log` and `.stderr.log`. The post-fix outputs are `step26_gpu_survival_auto_bounded.*` and `step26_gpu_survival_forced_cold_bounded.*`.
|
||||
|
||||
## Correctness and capture results
|
||||
|
||||
- CPU W1 and W8 both matched density `0xdb2991c73215af4c`, mesh `0x8236c5edd7fc8e4f`, payload `0x4c48fa75a543d58e`, and topology `0xbea22e76b34a4dfc`. Walkability remained 5,642/5,642.
|
||||
- Whole near tested 960 tiles and 229,376 protected exception cells. Hardware and WARP sign/topology differences were zero.
|
||||
- Whole far tested 1,114 eligible tiles, skipping 152 near-transition tiles. It covered 127,401 protected exception cells and 353 thin-feature cells. Hardware and WARP differences were zero; both port summaries reported `PASS`.
|
||||
- All ten 1280x800 full-residency capture pairs completed. Numeric comparison found zero CPU-rock-to-GPU-background pixels in eight views and two in each of remote yaw 180 and 270, matching the established signature. Every capture passed the runner's combined private plus sampled DXGI-local memory ceiling of 8 GB. See `step26_final384_capture_comparison.md`.
|
||||
|
||||
## Direction for Step 27
|
||||
|
||||
I would pursue GPU evaluation of visual-only detail roughness as the long-term direction. Step 25 measured the detail pass at about 319 ms, and roughness dominated it while the GPU was idle. A cheaper exact CPU path remains useful to the server, but it does not remove the client’s largest measured work. Staging the neighborhood can improve player-visible readiness, but the current `bNeighborhoodOnly` path builds a partial world that is later replaced by a separate full build; it duplicates neighborhood work, and GPU generation still requires a shape world with its far world built.
|
||||
|
||||
I did not start the visual-detail prototype here. It needs a flagged, per-tile comparison that proves identical sign/topology results on hardware and WARP, including protected and thin features, before it can replace any client field work. For the product metric, I would track the walkable 3x3x3 neighborhood separately from full-clipmap readiness; the latter remains a useful benchmark, but does not describe when the player can see and move.
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
# Step26_Final384 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.0826 | 1.6874 | 5.1076% | 0.0580% | 107 | 2 | 0 | (835,204)-(886,205) |
|
||||
@@ -1555,6 +1555,7 @@ namespace
|
||||
EGpuWarmupLaunchPolicy GpuWarmupLaunchPolicy=EGpuWarmupLaunchPolicy::Auto;
|
||||
bool bGpuWarmupDeferredUntilFirstView=false,bGpuWarmupDeferredStarted=false;
|
||||
bool bGpuWarmupCacheKeyAvailable=false;
|
||||
bool bGpuWarmupCacheWriteDisabled=false;
|
||||
std::filesystem::path GpuWarmupStatePath;
|
||||
std::string GpuWarmupCacheKey,GpuWarmupCacheIdentity;
|
||||
bool bGpuFarWarmupKernelFences=false;
|
||||
@@ -2289,6 +2290,11 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
|
||||
|
||||
void MarkGpuWarmupCacheReady()
|
||||
{
|
||||
if(bGpuWarmupCacheWriteDisabled)
|
||||
{
|
||||
std::printf("[GpuWarmupCacheState] result=write_disabled policy=command_line\n");
|
||||
return;
|
||||
}
|
||||
if(!bGpuFarShaderPrewarm||bGpuFarWarmupResourcesOnly
|
||||
||!GpuWarmupEntryFilter.empty()||GpuWarmupStatePath.empty())return;
|
||||
ResolveGpuWarmupLaunchPolicy(true);
|
||||
@@ -3655,20 +3661,29 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
|
||||
ResolveGpuWarmupLaunchPolicy(false);
|
||||
if(bGpuWarmupDeferredUntilFirstView&&!bGpuWarmupDeferredStarted)
|
||||
{
|
||||
if(!bCpuReadyFramePresentedBeforeGpu)return;
|
||||
// On an ordinary first-launch cache miss, let the complete CPU
|
||||
// clipmap reach the front buffer before starting cold shader
|
||||
// warmup. Starting after only the first CPU frame competes with
|
||||
// the remaining CPU mesh work and regresses startup. The survival
|
||||
// audit has GPU-owned candidates and needs the first CPU frame to
|
||||
// break its CPU/GPU readiness cycle.
|
||||
const bool bDeferredWarmupGateSatisfied=bGpuFarAuditEnabled
|
||||
?bCpuReadyFramePresentedBeforeGpu:bWholeViewPresented;
|
||||
if(!bDeferredWarmupGateSatisfied)return;
|
||||
std::string DeferredPipelineError;
|
||||
if(!EnsureGpuFarPipeline(DeferredPipelineError))
|
||||
{
|
||||
std::fprintf(stderr,"[GpuPipelineInitError] deferred_after_first_CPU_view: %s\n",
|
||||
std::fprintf(stderr,"[GpuPipelineInitError] deferred_after_CPU_ready_gate: %s\n",
|
||||
DeferredPipelineError.c_str());
|
||||
bGpuFarEnabled=false;bGpuWarmupDeferredStarted=true;
|
||||
AddGpuFarCpuFallbackTiles(DesiredTiles);
|
||||
return;
|
||||
}
|
||||
bGpuWarmupDeferredStarted=true;
|
||||
std::printf("[GpuWarmupDeferredStart] at_ms=%.3f first_CPU_view_presented=true\n",
|
||||
std::printf("[GpuWarmupDeferredStart] at_ms=%.3f gate=%s\n",
|
||||
std::chrono::duration<double,std::milli>(
|
||||
std::chrono::steady_clock::now()-ColdProcessStart).count());
|
||||
std::chrono::steady_clock::now()-ColdProcessStart).count(),
|
||||
bGpuFarAuditEnabled?"first_CPU_frame_presented":"whole_CPU_view_presented");
|
||||
StartGpuShaderWarmupAsync();
|
||||
}
|
||||
std::string PipelineError;
|
||||
@@ -7372,6 +7387,8 @@ int main(int Argc,char** Argv)
|
||||
App.GpuWarmupLaunchPolicy=EGpuWarmupLaunchPolicy::ForceDeferred;
|
||||
else if(Arg=="--gpu-far-force-cached-start")
|
||||
App.GpuWarmupLaunchPolicy=EGpuWarmupLaunchPolicy::ForceEager;
|
||||
else if(Arg=="--gpu-far-no-warmup-cache-write")
|
||||
App.bGpuWarmupCacheWriteDisabled=true;
|
||||
else if(Arg=="--gpu-far-warmup-before-workers")
|
||||
App.bGpuFarEnabled=true,App.bGpuFarShaderPrewarm=true,App.bGpuFarWarmupBeforeWorkers=true,
|
||||
App.bShapeWorld=true,App.bForceGenerate=true,App.bPresentCpuFrameBeforeGpu=true,
|
||||
|
||||
Reference in new issue
Block a user