fix(vk): end the game with the GPU report when submitted work stops finishing #2407
No reviewers
Labels
No labels
accessibility
autorelease: pending
autorelease: tagged
bug
dependencies
documentation
duplicate
enhancement
github_actions
good first issue
help wanted
invalid
question
testing wanted
wontfix
No milestone
No assignees
1 participant
Due date
No due date set.
Dependencies
No dependencies set
Reference
MagicBeanDev/Hookshot!2407
Loading…
Reference in a new issue
No description provided.
Delete branch "fix/vk-gpu-hang-watch"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
A GPU hang that the driver never reports as
VK_ERROR_DEVICE_LOSTfreezes the game for good and leaves no clue in the log about which GPU work died.device_lost()(#1275) only runs when a Vulkan call returns that error, and on NVIDIA's Xid 109 none does.What happened
RTX 4070 Ti, NVIDIA 595.99.02, Linux, sneaking into Hyrule Castle: the kernel logged
NVRM: Xid 109 ... name=hookshot ... CTX SWITCH TIMEOUT, and the game froze. The log's last lines are three offrom the main and presentation threads, and nothing after: the timeline stopped 4 short of what had been sent, the 250 ms write-back waits kept coming back
VK_TIMEOUT, and the other waits (recording_wait,wait_value) have no limit. No call returnedVK_ERROR_DEVICE_LOST, so the checkpoint report never ran.The change
hang_watch_start(), reads the timeline once a second (vkGetSemaphoreCounterValueonly: it needs no external synchronization, unlike the recordings' fences).device_lost().note_timeline_sent, at the two submissions that signal the timeline) that finishes nothing forRX_VK_GPU_HANG_SECseconds (default 30) is logged as a hang and goes todevice_lost(): the checkpoint report, the log flushed, exit code 3.RX_VK_GPU_HANG_SEC=0only logs the stall (at 10 s, then every 30 s) and lets the game wait.Trade-off
The present's submission waits for its swapchain image, and a window the system stops presenting could in principle hold the queue as long. That is why the limit is 30 s, not the few seconds a real GPU timeout takes, and why
=0exists. I have not seen it happen.Tests
tests/test_gpu_hang_watch.pycutsgpu_hang_watch_start()into a fixture with the timeline anddevice_lost()stubbed. A stuck timeline ends the process after the limit, a lost device ends it at once, and a moving timeline or=0never does. It also checks that both timeline-signalling submissions are counted and that the watch starts at the first present.tests/draw_stream_visibility_test.cppandtests/fsr_image_retirement_test.cppget anote_timeline_sentstub, since they compile the submit functions.python -I -B tests/run_all.pyon Linux: 381 passed, 15 skipped, 1 failed (test_make_game_cache.py, which fails the same way onmainwithout this change).vk_replay.cppbuilt withnx2/build.py's Vulkan flags.Complements #2117, which makes the report itself richer when there are no checkpoints. Refs #1000.
Looks good to me. A hang the driver never reports as lost is exactly the case
device_lost()can't see, and reading the timeline from a side thread is the cheapest thing that can.I went through the false-positive cases:
timeline_sentonly moves aftervkQueueSubmit2succeeds (and only on a flush when batching), sodone >= sentand the counter never runs. The same goes for a value held back as in #2222.f.acquired. On Linux the acquire is already bounded at 100 ms on the CPU (the "window is not shown" path), so nothing gets queued behind an image that never comes back. On Windows the acquire returns before the semaphore matters. The 30 s margin plus=0covers whatever is left.The report path on a hung device:
device_lost()→call_once(checkpoints_report)→replay_fatal→_Exit(3). It does no fence or semaphore waits, takes onlywatch_mubriefly, and if the main thread hits a real DEVICE_LOST at the same moment,call_oncecovers it. Nothing tears down the device at exit either, so the detached thread can't race a destroy. Parity is fine: plainstd::thread/getenv, and the llvm-mingw back end builds.Notes (none blocking):
vkGetQueueCheckpointDataNVis technically outside the spec (it requires the device to be in the lost state, and validation will flag it). On NVIDIA it should still return the last checkpoints, which is the point, but it hasn't been run in the game yet, so it's worth confirming the report actually has markers in it when this recurs. Related: when #2117 lands, its automaticvkGetDeviceFaultInfoEXTwill run on this path on a device that isn't lost. Whichever PR goes in second should check that the call returns there. The existing comment incheckpoints_reportalready says it may not return on NVIDIA, and the queued log is only flushed inreplay_fatal, after it.mainwould make it cheaper.CI: the only failure is
test_native_astc_texture_count.pyon Windows (ascanfdeprecation in its fixture). It fails the same way on main and isn't touched here.test_gpu_hang_watch.pypasses on both Linux and Windows.@ -2630,3 +2638,2 @@std::call_once(once, checkpoints_report);replay_fatal("the GPU device was lost (VK_ERROR_DEVICE_LOST): the driver stopped or reset the GPU, and nothing sent ""to it runs any more (on Linux, dmesg or journalctl -k around this time names the reset)");replay_fatal(why ? whyNit: this ends in
replay_fatal("the GPU device was lost (VK_ERROR_DEVICE_LOST)..."), so the stderr/FATAL line claims the driver returned DEVICE_LOST when here it didn't. The log line above it says so, but stderr (and anything that shows only the FATAL line) won't. Adevice_lost(const char* why = ...)overload or a flag would keep the two apart.Nit:
atoiturns a typo (RX_VK_GPU_HANG_SEC=30sworks,=abcdoesn't) into 0, which silently switches the exit off. Logging the value it settled on once at start would make that visible.Nit:
hang_watch_start()names its thread (SetThreadDescription(..., L"mkw_vk hang watch")). Doing the same here would make this one easy to tell apart in a hang dump.Done in
0361f50a: the limit it settled on is logged at start, and with 0 it says the stall is only logged and shows the raw value it read, so=abcis visible.Done in
0361f50a: namedmkw_vk gpu hang watch.@ -2630,3 +2638,2 @@std::call_once(once, checkpoints_report);replay_fatal("the GPU device was lost (VK_ERROR_DEVICE_LOST): the driver stopped or reset the GPU, and nothing sent ""to it runs any more (on Linux, dmesg or journalctl -k around this time names the reset)");replay_fatal(why ? whyDone in
0361f50a:device_lost(const char* why = nullptr). The watch passes "the GPU hung: no submission finished in N s, and the driver never reported the device lost ...", and every other site keeps the VK_ERROR_DEVICE_LOST line. Both still share the onecall_oncereport.test_device_lost_exit.pycuts the new signature, andtest_gpu_hang_watch.pychecks which line each path gives.Thanks for going through the false-positive cases. Besides the three nits, the fixture now sleeps 3.5 s (the test takes about 11 s, was 15).
On the notes: I'll confirm the report has markers in it when the hang recurs. If #2117 lands first, I'll check here that
vkGetDeviceFaultInfoEXTreturns on a hung but not lost device before its automatic call can run on this path.tests/run_all.pyon Linux: 382 passed, 0 failed.Re-checked at
0361f50. All three nits are done:device_lost(why)gets "the GPU hung ... the driver never reported the device lost" from the watch, every other caller keeps the VK_ERROR_DEVICE_LOST text, and both still go through the onecall_oncereport. The test checks which line each path prints.=abcno longer switches the exit off unnoticed.mkw_vk gpu hang watch.The test is down to about 11 s, which is fine.
The two notes stay notes. You plan to confirm the checkpoint markers when the hang happens again, and to check
vkGetDeviceFaultInfoEXTon this path if #2117 lands first. That covers it.CI: everything passes except Windows part 2/2, where the only failure is
test_native_astc_texture_count.py(scanfdeprecated under -Werror). That fails on main too and is fixed by #2397, so it isn't from this PR. No conflicts with main.Good to merge from my side.
github-sync referenced this pull request2026-11-12 06:22:54 +00:00