You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
agc_driver_recorder_tests fails on Windows since #2096: the refresh store check, plus a more frequent device loss #2155
Since #2096 merged, agc_driver_recorder_tests fails on Windows in every run that gets as far as storageRefreshTests. A device loss at the same spot also happens before #2096, less often. The test is EXCLUDE_FROM_ALL and not in ctest, so CI doesn't run it.
Results
Windows 10 (19045), RTX 3060 Laptop (NVIDIA 616.92), MinGW-w64 GCC 15.2.0 (WinLibs r7), Release. The test was built with cmake --build . --target agc_driver_recorder_tests.
Refresh store:AGC graphics: the refresh's store of the results did not reach guest memory, at Recorder.cpp:1672 in storageRefreshTests. After a draw and image->Refresh(), the image holds the draw's texels but guest memory doesn't. It appears only with 0e1d25a (avoid redundant transfers for untracked imported textures).
Device loss:AGC graphics: vkQueueSubmit recorder: Vulkan result -4, right after host imports are compared, not watched: unit shadows not tested, at the start of storageRefreshTests. When it happens, the refresh-store check never runs. It's intermittent before fix(win): windows host import texture reuploads #2096 too, but came up in 3 or 4 of 5 runs with it.
The full ctest passes both before and after (494/494).
Not established
Whether the refresh store failure is a real regression or the test's expectation no longer fits compared imports. With 0e1d25a the refresh of an untracked, compared import takes the new comparison path.
What causes the device loss. Nothing in ctest shows it here.
changed the title [-]agc_driver_recorder_tests fails on Windows since #2096, mostly with VK_ERROR_DEVICE_LOST[/-][+]agc_driver_recorder_tests fails on Windows since #2096: the refresh store check, plus a more frequent device loss[/+]on Oct 9, 2026
Reproduced on maind70b899: Windows 11, RTX 5060 Ti (NVIDIA 617.42), MinGW-w64 GCC 15.2.0 (WinLibs r7), Release. The refresh store failure happens in 5 of 5 runs. The device loss did not happen in any run on this GPU, so this comment covers only the refresh store.
What happens
I added temporary logging to StorageTexture::Refresh and writeBackLayers. The failing refresh is the unwatched pass (VirtualAlloc block):
The import is untracked, so ChangedBlocks fails and 0e1d25a's compareUntracked runs. The guest bytes still match the snapshot taken at upload, so every unit is BlockUnchanged and Refresh returns early through if (unchanged).
On the direct path, unchanged pending units keep their results on the GPU (else if (direct) continue;, the same rule as for tracked memory). Nothing is stored at the refresh.
Before 0e1d25a, untracked memory could never be proved unchanged. Every refresh treated all units as changed, and since droppable is false without tracking, it stored the pending units. The assertion at Recorder.cpp:1672 was written against that behaviour.
The results are not lost
Directly after the refresh, guest memory is still 55 55 55 55 in every unit. After StorageTexture::FlushPending(address, surfaceBytes, nullptr, ...), then recorder.Submit(), device.WaitQueue() and recorder.Sync(), it holds ff 00 00 ff in every unit. The write-back's own comparison saw all blocks unchanged (0 0 0 0), so nothing was dropped as a CPU store. With that flush in place the rest of agc_driver_recorder_tests passes (Recorder read tracking and label tests passed), 8 of 8 runs.
Question
Is keeping the results pending in compared (untracked) memory intended? The CPU's reads of that memory aren't trapped, so the results reach it only at the driver's next flush point for that range. Before 0e1d25a that flush point was the refresh. If this is intended, the test could check it like this, keeping the strict assertion for watched memory:
if (!memoryHolds({255, 0, 0, 255})) {
Require(!watched && !AgcDriver::GuestMemory::Watched(address, bytes), "the refresh's store of the results did not reach guest memory");
Require(StorageTexture::FlushPending(address, surfaceBytes, nullptr, "storage refresh test"), "results the refresh kept on the GPU were not pending");
recorder.Submit();
device.WaitQueue();
recorder.Sync();
Require(memoryHolds({255, 0, 0, 255}), "results the refresh kept on the GPU did not reach guest memory when flushed");
}
If it isn't intended, the fix belongs in Refresh: a compared import with pending results would store them, as before. I can open a PR for whichever direction you prefer.
I checked the existing Refresh contract and agc_driver_dcc_keys_first_write: an unchanged image with pending results must remain resident for the next GPU use, and Refresh() is not the CPU visibility barrier for untracked imports. My attempted production write-back caused agc_driver_dcc_keys_first_write to fail with “an unchanged refresh wrote the pending image back,” so I discarded that approach. PR #2248 updates the recorder regression to assert that the image remains pending, then explicitly FlushPending, wait, and check guest memory. With that test expectation, agc_driver_recorder_tests passes, and agc_driver_dcc_keys_first_write passes (1/1).
Found the cause of the device loss. It comes from unitShadowTests, not from #2096.
On Windows, host imports are compared, so unitShadowTests returns right after it imports its 17 MiB block. Its cleanup removes the registration and releases the block. The import stays until the next lookup, which is the HostImportFor at the start of storageRefreshTests. That retire's vkFreeMemory runs after the pages are gone.
I put vkQueueWaitIdle probes around each step. In all 7 device losses out of 20 runs, the queue was healthy just before that vkFreeMemory and lost right after it.
It only happens when the block is released shortly after the import. In a local probe that imports a 17 MiB block, releases it, then lets the next lookup retire the import, the device was lost in:
12 of 60 runs when the block was released about 10 µs after the import
4 of 20 at 0.5 ms
1 of 20 at 1 ms
0 of 20 at each of 1.5, 2, 5, 20 and 100 ms
0 of 60 when the import was retired before the release
That would explain why it's intermittent, and why it may not show up on other GPUs or drivers.
#2299 makes the cleanup retire the import before it releases the block. With #2248 and #2299, agc_driver_recorder_tests passes 30 of 30 here. With #2299 alone, there was no device loss in 30 runs; all 30 still fail at the refresh store, which #2248 fixes.
The runtime unmap path uses the same order. GuestAllocationsUnmap_nid_postfix releases the pages before the registry changes, and the import is retired at the next lookup. I haven't checked whether any title releases memory within a millisecond of importing it.
AMD data point: on the AMD Windows driver the test stops much earlier than in the NVIDIA runs above, in readTrackingTests, before it reaches storageRefreshTests.
Radeon 860M (RDNA 3.5, 0x1114), AMD driver 24.30.58.08 (32.0.13058.8001), Windows 11 (26300), WinLibs GCC 15.2.0 r7, Release, main00159cf. Five of five runs:
AGC graphics: a signaled batch's read still refuses (tests/Recorder.cpp:247)
The check right before it calls device.WaitQueue() (vkQueueWaitIdle) and expects the batch's fence to read as signaled, but on this driver the fence usually lags the idle wait. A standalone probe (source below) submits to one queue, calls vkQueueWaitIdle, then vkGetFenceStatus, 2000 times per variant:
Submit
Fence not ready right after vkQueueWaitIdle
Worst extra vkGetFenceStatus polls until ready
no command buffer, fence only
1920 / 2000
1415
command buffer with a barrier, fence
1928 / 2000
6318
barrier, fence and a timeline semaphore signal (as Recorder::Submit does)
1918 / 2000
369
So vkQueueWaitIdle returns before the fence reads as signaled on this driver, almost every time. When I polled the overlap query for a short time, that check passed and the test stopped at the next check of the same kind, a signaled batch's write is not settled (line 271), after the same WaitQueue() pattern. Production code only treats such a batch as still in flight a little longer, which is conservative. The test, though, would need to wait on the batch's fence (vkWaitForFences) or poll briefly to get past these checks on AMD, before it can show whether #2248 helps with the refresh store here.
Activity
[-]agc_driver_recorder_tests fails on Windows since #2096, mostly with VK_ERROR_DEVICE_LOST[/-][+]agc_driver_recorder_tests fails on Windows since #2096: the refresh store check, plus a more frequent device loss[/+]rmz-24 commentedon Oct 9, 2026
Reproduced on
maind70b899: Windows 11, RTX 5060 Ti (NVIDIA 617.42), MinGW-w64 GCC 15.2.0 (WinLibs r7), Release. The refresh store failure happens in 5 of 5 runs. The device loss did not happen in any run on this GPU, so this comment covers only the refresh store.What happens
I added temporary logging to
StorageTexture::RefreshandwriteBackLayers. The failing refresh is the unwatched pass (VirtualAllocblock):ChangedBlocksfails and 0e1d25a'scompareUntrackedruns. The guest bytes still match the snapshot taken at upload, so every unit isBlockUnchangedandRefreshreturns early throughif (unchanged).else if (direct) continue;, the same rule as for tracked memory). Nothing is stored at the refresh.droppableis false without tracking, it stored the pending units. The assertion atRecorder.cpp:1672was written against that behaviour.The results are not lost
Directly after the refresh, guest memory is still
55 55 55 55in every unit. AfterStorageTexture::FlushPending(address, surfaceBytes, nullptr, ...), thenrecorder.Submit(),device.WaitQueue()andrecorder.Sync(), it holdsff 00 00 ffin every unit. The write-back's own comparison saw all blocks unchanged (0 0 0 0), so nothing was dropped as a CPU store. With that flush in place the rest ofagc_driver_recorder_testspasses (Recorder read tracking and label tests passed), 8 of 8 runs.Question
Is keeping the results pending in compared (untracked) memory intended? The CPU's reads of that memory aren't trapped, so the results reach it only at the driver's next flush point for that range. Before 0e1d25a that flush point was the refresh. If this is intended, the test could check it like this, keeping the strict assertion for watched memory:
If it isn't intended, the fix belongs in
Refresh: a compared import with pending results would store them, as before. I can open a PR for whichever direction you prefer.AI-assisted: yes
MarcZX10 commentedon Oct 9, 2026
I checked the existing Refresh contract and
agc_driver_dcc_keys_first_write: an unchanged image with pending results must remain resident for the next GPU use, andRefresh()is not the CPU visibility barrier for untracked imports. My attempted production write-back causedagc_driver_dcc_keys_first_writeto fail with “an unchanged refresh wrote the pending image back,” so I discarded that approach. PR #2248 updates the recorder regression to assert that the image remains pending, then explicitlyFlushPending, wait, and check guest memory. With that test expectation,agc_driver_recorder_testspasses, andagc_driver_dcc_keys_first_writepasses (1/1).popcoder565 commentedon Oct 9, 2026
Found the cause of the device loss. It comes from
unitShadowTests, not from #2096.On Windows, host imports are compared, so
unitShadowTestsreturns right after it imports its 17 MiB block. Its cleanup removes the registration and releases the block. The import stays until the next lookup, which is theHostImportForat the start ofstorageRefreshTests. That retire'svkFreeMemoryruns after the pages are gone.I put
vkQueueWaitIdleprobes around each step. In all 7 device losses out of 20 runs, the queue was healthy just before thatvkFreeMemoryand lost right after it.It only happens when the block is released shortly after the import. In a local probe that imports a 17 MiB block, releases it, then lets the next lookup retire the import, the device was lost in:
That would explain why it's intermittent, and why it may not show up on other GPUs or drivers.
#2299 makes the cleanup retire the import before it releases the block. With #2248 and #2299,
agc_driver_recorder_testspasses 30 of 30 here. With #2299 alone, there was no device loss in 30 runs; all 30 still fail at the refresh store, which #2248 fixes.The runtime unmap path uses the same order.
GuestAllocationsUnmap_nid_postfixreleases the pages before the registry changes, and the import is retired at the next lookup. I haven't checked whether any title releases memory within a millisecond of importing it.🤖 Addressed by Claude Code
test(agc-driver): check explicit flush for untracked refresh
test(agc-driver): check explicit flush for untracked refresh
test(agc-driver): check explicit flush for untracked refresh
test(agc-driver): check explicit flush for untracked refresh
LeandroLP23 commentedon Oct 9, 2026
AMD data point: on the AMD Windows driver the test stops much earlier than in the NVIDIA runs above, in
readTrackingTests, before it reachesstorageRefreshTests.Radeon 860M (RDNA 3.5,
0x1114), AMD driver 24.30.58.08 (32.0.13058.8001), Windows 11 (26300), WinLibs GCC 15.2.0 r7, Release,main00159cf. Five of five runs:The check right before it calls
device.WaitQueue()(vkQueueWaitIdle) and expects the batch's fence to read as signaled, but on this driver the fence usually lags the idle wait. A standalone probe (source below) submits to one queue, callsvkQueueWaitIdle, thenvkGetFenceStatus, 2000 times per variant:vkQueueWaitIdlevkGetFenceStatuspolls until readyRecorder::Submitdoes)So
vkQueueWaitIdlereturns before the fence reads as signaled on this driver, almost every time. When I polled the overlap query for a short time, that check passed and the test stopped at the next check of the same kind,a signaled batch's write is not settled(line 271), after the sameWaitQueue()pattern. Production code only treats such a batch as still in flight a little longer, which is conservative. The test, though, would need to wait on the batch's fence (vkWaitForFences) or poll briefly to get past these checks on AMD, before it can show whether #2248 helps with the refresh store here.Probe source (fenceprobe.cpp, g++ -std=c++20 -I3rdparty/Vulkan-Headers/include)
AI-assisted: yes (Claude Code).
test(agc-driver): check explicit flush for untracked refresh