fix(vk): a shader preparation the GPU driver stops answering in goes on or ends, and says why (#2107) #2123
Closed
nullifyshot wants to merge 3 commits from
fix/predict-stall-guard into main
pull from: fix/predict-stall-guard
merge into: MagicBeanDev:main
MagicBeanDev:main
MagicBeanDev:github/pr/2893
MagicBeanDev:github/pr/2890
MagicBeanDev:github/pr/2887
MagicBeanDev:github/pr/2878
MagicBeanDev:github/pr/2875
MagicBeanDev:github/pr/2874
MagicBeanDev:github/pr/2873
MagicBeanDev:github/pr/2871
MagicBeanDev:github/pr/2756
MagicBeanDev:github/pr/2897
MagicBeanDev:github/pr/2896
MagicBeanDev:github/pr/2718
MagicBeanDev:github/pr/2842
MagicBeanDev:github/pr/2691
MagicBeanDev:github/pr/2895
MagicBeanDev:github/pr/2743
MagicBeanDev:github/pr/2765
MagicBeanDev:github/pr/2708
MagicBeanDev:github/pr/2732
MagicBeanDev:github/pr/2755
MagicBeanDev:github/pr/2817
MagicBeanDev:github/pr/2876
MagicBeanDev:release-please--branches--main
MagicBeanDev:release-please--branches--main--release-notes
MagicBeanDev:github/pr/2778
MagicBeanDev:github/pr/2888
MagicBeanDev:github/pr/2866
MagicBeanDev:github/pr/2877
MagicBeanDev:github/pr/2800
MagicBeanDev:github/pr/2829
MagicBeanDev:github/pr/2850
MagicBeanDev:github/pr/2889
MagicBeanDev:github/pr/2882
MagicBeanDev:github/pr/2860
MagicBeanDev:github/pr/2849
MagicBeanDev:github/pr/2886
MagicBeanDev:github/pr/2794
MagicBeanDev:github/pr/2840
MagicBeanDev:github/pr/2830
MagicBeanDev:github/pr/2834
MagicBeanDev:github/pr/2719
MagicBeanDev:github/pr/2742
MagicBeanDev:github/pr/2741
MagicBeanDev:github/pr/2820
MagicBeanDev:github/pr/2771
MagicBeanDev:github/pr/2859
MagicBeanDev:github/pr/2858
MagicBeanDev:github/pr/2823
MagicBeanDev:github/pr/2757
MagicBeanDev:github/pr/2769
MagicBeanDev:github/pr/2844
MagicBeanDev:github/pr/2843
MagicBeanDev:github/pr/2673
MagicBeanDev:github/pr/2855
MagicBeanDev:github/pr/2739
MagicBeanDev:github/pr/2807
MagicBeanDev:github/pr/2851
MagicBeanDev:github/pr/2789
MagicBeanDev:github/pr/2814
MagicBeanDev:github/pr/2818
MagicBeanDev:github/pr/2774
MagicBeanDev:github/pr/2772
MagicBeanDev:github/pr/2750
MagicBeanDev:github/pr/2786
MagicBeanDev:github/pr/2785
MagicBeanDev:github/pr/2782
MagicBeanDev:github/pr/2779
MagicBeanDev:github/pr/2775
MagicBeanDev:github/pr/2706
MagicBeanDev:github/pr/2766
MagicBeanDev:github/pr/2747
MagicBeanDev:github/pr/2759
MagicBeanDev:github/pr/2745
MagicBeanDev:github/pr/2740
MagicBeanDev:github/pr/2733
MagicBeanDev:github/pr/2725
MagicBeanDev:github/pr/2727
MagicBeanDev:github/pr/2728
MagicBeanDev:github/pr/2729
MagicBeanDev:github/pr/2721
MagicBeanDev:github/pr/2722
MagicBeanDev:github/pr/2715
MagicBeanDev:github/pr/2714
MagicBeanDev:github/pr/2707
MagicBeanDev:github/pr/2703
MagicBeanDev:github/pr/2705
MagicBeanDev:github/pr/2697
MagicBeanDev:github/pr/2679
MagicBeanDev:github/pr/2676
MagicBeanDev:github/pr/2669
MagicBeanDev:docs/what-hookshot-connects-to
MagicBeanDev:fix/hdr-encode-test
MagicBeanDev:fix/main-tests
MagicBeanDev:fix/play-cache-files
MagicBeanDev:perf/game-barrier-order
MagicBeanDev:feat/heap-lazy
MagicBeanDev:feat/texture-detail
MagicBeanDev:perf/astc-opaque-bc1
MagicBeanDev:fix/fs-dir-lock
MagicBeanDev:fix/framegen-native-kept
MagicBeanDev:fix/update-no-downgrade
MagicBeanDev:port/k10-gcn-f16-mirrors
MagicBeanDev:fix/framegen-mult-cap
MagicBeanDev:chore/release-as-2.1.1
MagicBeanDev:perf/pcache-load-switch
MagicBeanDev:fix/prepare-progress-log
MagicBeanDev:fix/update-closes-stale-game
MagicBeanDev:fix/hdr-scaled-sampled
MagicBeanDev:perf/first-start-preset
MagicBeanDev:perf/zero-upload
MagicBeanDev:fix/vcore-escape-2s
MagicBeanDev:test/fake-gh-atomic
MagicBeanDev:perf/batch-idle-flush
MagicBeanDev:test/finish-stub-a
MagicBeanDev:chore/record-2.1.0
MagicBeanDev:fix/main-red-tests
MagicBeanDev:fix/test-display-startup-driver-cache
MagicBeanDev:feat/mesh-shader-mode
MagicBeanDev:fix/menu-probes-at-startup
MagicBeanDev:fix/fps-counter-classic
MagicBeanDev:fix/test-log-batch-flake
MagicBeanDev:fix/narrow-screen-hud
MagicBeanDev:ci/prereleases
MagicBeanDev:release/v2.0.15
MagicBeanDev:hotfix/v2.0.15
MagicBeanDev:fix/present-ahead-replay
MagicBeanDev:release/v2.0.13
MagicBeanDev:fix/bracket-paths-2013
MagicBeanDev:fix/hdr-overlays-2013
MagicBeanDev:fix/fps-counter-scale-2013
MagicBeanDev:fix/stick-seen-at-rest
MagicBeanDev:hotfix/menu-fits-screen
MagicBeanDev:fix/f1-always-opens-menu
MagicBeanDev:fix/ini-keep-lines
MagicBeanDev:claude/cranky-pike-7cf247
MagicBeanDev:perf/texture-answer-flat
MagicBeanDev:fix/stamp-test-modules
MagicBeanDev:fix/appimage-fonts
MagicBeanDev:fix/display-awake
MagicBeanDev:release/v2.0.12
MagicBeanDev:fix/discord-windows
MagicBeanDev:rc/v2.0.12-tests
MagicBeanDev:fix/dlss-keep-feature
MagicBeanDev:rc/v2.0.12
MagicBeanDev:button-icons
No reviewers
Labels
Clear labels
accessibility
Barrier affecting people with disabilities
autorelease: pending
autorelease: tagged
bug
Something isn't working
dependencies
Pull requests that update a dependency file
documentation
Improvements or additions to documentation
duplicate
This issue or pull request already exists
enhancement
New feature or request
github_actions
Pull requests that update GitHub Actions code
good first issue
Good for newcomers
help wanted
Extra attention is needed
invalid
This doesn't seem right
question
Further information is requested
testing wanted
wontfix
This will not be worked on
No labels
accessibility
autorelease: pending
autorelease: tagged
bug
dependencies
documentation
duplicate
enhancement
github_actions
good first issue
help wanted
invalid
question
testing wanted
wontfix
Milestone
Clear milestone
No items
No milestone
Assignees
Clear assignees
No assignees
1 participant
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".
No due date set.
Dependencies
No dependencies set
Reference
MagicBeanDev/Hookshot!2123
Loading…
Reference in a new issue
No description provided.
Delete branch "fix/predict-stall-guard"
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?
Since v2.1.0, builds can stop for good while preparing shaders (#2107). The reports come from players on a range of graphics cards, AMD and NVIDIA alike, so this isn't one vendor's or one series' bug. The Builder's details freeze on
vertex format NVN 0xe -> vk 17and nothing says why. This makes the preparation go on, or end with a reason, and keeps the log saying what it's doing.What happens
One player's log (an RX 580) shows a likely cause:
guarded_graphicscatches the driver's crash by jumping out of the compile, and that leaves the compile unfinished inside the driver. #1823's process dump of issue 996 shows what follows on AMD: later builds of that program wait inside the driver for ever.predict_run, which runs at build time since #2032 (before that, new builds predicted 0 pipelines). Unlikeprecompile_run, it had no stall guard.precompile_runalready avoids that, because of a known NVIDIA fault (621.14, RTX 4070 Ti Super).Changes
predict_run(gpu/vk/vk_draw.cpp)precompile_rundoes.RX_VK_PREDICT_STALL_S, 0 waits for all), it goes on and leaves the stuck workers in the driver, with their state on the heap as inprecompile_run. This happens:driver_faults());precompile_run's guard does, so only the last stragglers are given up, never the bulk. A slow driver that still works keeps its pipelines, at the game's own start too. A real hang before then is the watchdog's to end.predict_runandprecompile_runrecord when they leave workers in the driver (prepare_left_workers()).A watchdog for the whole
--prepare-shadersrun (gpu/vk/vk_main.cpp), covering every part on every GPU:Live status in the log. After 30 s with no other line, it writes what it's working on with its count, or, if the count hasn't moved, what it's waiting on and for how long. That's "the GPU driver or the disk", "the GPU driver, which faulted (above)", or "the GPU driver, which still has pipelines it did not finish". A driver fault it caught gets a line of its own as soon as it happens.
Stops when nothing moves for 5 minutes (
RX_VK_PREPARE_STALL_S, 0 never stops). It then:STOPPED: ... the GPU driver stopped respondinghookshot_prepare_skipped, so the game's own starts don't hang in the same place (what wasn't prepared is built when first drawn)TerminateProcess, notexit(), which would unload the stuck driver and could wait on itGoing on to the next part wouldn't help once the driver is stuck, because every part needs it. The 5-minute limit is long on purpose: reading the caches and deciding the predictions report no count for a while on a slow disk or CPU.
The pipeline cache's save, the last step:
vkGetPipelineCacheDatacan wait on them. A run that gets through ends at once withTerminateProcess(after@@done shaders), notexit().PREPARE_SILENCE(15 minutes with no line) still ends a save that never finishes.The part, its count and the time are read under one lock.
Elsewhere
make_game.pymaps exit status 4 to "Left to the game's first start: the GPU driver stopped responding". Before, players waited forPREPARE_SILENCE, 15 minutes, and the game's first start could then stop in the same place.driver_faults(): the fault guards count what they catch. These are the same four guard blocks #1823 changes, so whichever lands second needs a mechanical rebase. After #1823, the count would follow itskDriverFaultpath.Tested
tests/test_prepare_stall.py(new) compilespredict_runand the watchdog from the source with a stand-in driver. All 12 cases pass:predict_run:prepare_endmake_game.py: its message for exit status 4vk_main.cppandvk_draw.cppcompile with the Windows toolchain's flags (llvm-mingw,-fms-extensions), and the new functions link between them.test_prepare_cache_save,test_precompile_compute_guard,test_linux_driver_fault_guard.python -I -B tests/run_all.pyon Windows: 341 passed, 3 failed.test_recorder_sync,test_fpx_stickyandtest_precompile_compute_guardfail the same way on an untouched copy of main on this PC (the last needs MSVC's libraries). The Linux-only checks,test_linux_driver_fault_guardamong them, are left to CI.🤖 Generated with Claude Code
Thanks for picking this up. The status lines alone are a real improvement for #2107: players can't tell "slow" from "stuck" right now. Moving the count per pipeline and building the first pipeline alone in
predict_runboth look right to me. Keeping the stuck workers' state on the heap and detaching them followsprecompile_run's pattern and is safe as written: they only touchPre, never the stack. Exit status 4 and its make_game message line up, andRUN_ALL_ARGSis declared.Some things I'd like sorted before this goes in:
CI is red on Windows because of the new test.
test_prepare_stall.pyfails in part 2/2. clang++ targeting MSVC turnsgetenv(andfopenin the watchdog piece too) into-Wdeprecated-declarationserrors under-Werror. Add-D_CRT_SECURE_NO_WARNINGSon Windows, as the other compiled tests do. This is the PR's own test, not one of main's known failures.The save step can now hang forever (inline). Part 3 is never stopped, because "every pipeline is made". That's no longer true once
predict_run(orprecompile_run) has moved on and left a worker insidevkCreateGraphicsPipelines. If that worker holds the driver's lock or the cache's lock,vkGetPipelineCacheDatainpipeline_cache_save(true)waits on it. Worse, the watchdog then logs "still writing ... this is the disk" every 30 s. That output keeps make_game'sPREPARE_SILENCEfrom ever firing, so the 15-minute backstop this PR replaces is gone exactly in the #2107 case. The same goes for the plainreturn 0→exit()after@@done, with a detached thread still in the driver (the unload hazard the description itself cites forexit()). Some ideas: let part 3 stop too when a part moved on (or a fault was counted), or after a longer cap. Stop the status lines after N minutes so the silence timeout still applies. And end the run with_Exit/TerminateProcessafter the save when workers were left behind.30 s for
predict_runmay catch slow drivers that still work (inline).precompile_run's guard only fires once everything has been handed out (next >= total), so it only ever drops the last stragglers. This one fires whenever nothing finishes for 30 s, wherever the run is, and gives up on everything left (tens of thousands of pipelines, which are then built in play). It also runs at the game's own start (the start screen and console paths), where there's no watchdog. In #2107, oniryuuko describes a run that was "working, just very very slow", with the progress sometimes stopping "for up to 30 seconds as it hangs on a particular pipeline". That's right at this limit, and right after the first pipeline, which the others now wait for. I'd only fire early oncedriver_faults()has counted something (the case the log shows). Otherwise use the tail-only rule, or a much longer limit, and leave the real hang to the watchdog in--prepare-shaders.Overlap with #1823 (approved, open). Both change the
__except/sigsetjmpblocks ofguarded_graphics/guarded_compute,test_linux_driver_fault_guard.pyandtest_precompile_compute_guard.py, so whichever lands second needs a rebase. Two semantic points as well. #1823 makes the graphics guard return-77like compute and adds*pl = VK_NULL_HANDLE, and its gate serializes a program's builds on AMD so that "a build behind a faulted one" doesn't hang (issue 996). That sounds a lot like the fault-then-stuck theory here, and the #2107 reporter is on an RX 580. Please say how the two fit together, e.g. whether the fault counter should also count #1823'skDriverFaultpath and whether its quarantine would already have kept this run from hanging.Smaller:
abandonedis only checked at the top of the loop, so those pipelines can run alongsideprecompile_runand the save. A check inside the per-pipeline loop would stop it.prepare_movedupdates part/done/total as three separate atomics, so the watchdog can read a mixed snapshot. That's harmless for a log line, but worth knowing if it ever decides anything.The watchdog/diagnostic part is close. Points 1–3 are what I'd want fixed first.
Unlike
precompile_run's guard (next.load() >= total &&), this fires anywhere in the run and gives up on all that's left, here and at the game's own start (no watchdog there). With the first pipeline built alone, a single 30 s compile (which #2107 reports describe on slow-but-working runs) is enough. I'd fire early only oncedriver_faults() > 0, and otherwise keep the tail-only rule or a much longer limit.@ -727,6 +733,117 @@ std::string skipped_path() {std::string path(exe, n);These lines reset make_game's silence timer, so if the save (or the
exit()after@@done) hangs, the build waits forever. Maybe stop emitting after a few minutes in part 3, or end the run here too whendriver_faults()or a moved-on part says the driver may be stuck."Every pipeline is made" isn't true when predict/precompile moved on and left a worker inside
vkCreate*Pipelines.vkGetPipelineCacheDataon the same cache can then wait on it forever. Because part 3 keeps logging "still writing", make_game's 15 minPREPARE_SILENCEnever fires either. I think the save needs a cap, or a stop, when workers were left behind.@ -0,0 +1,359 @@# SPDX-License-Identifier: GPL-3.0-or-laterThis is what fails on windows-2022 (part 2/2):
getenvis deprecated under MSVC's CRT, and with-Werrorthat's an error (fopenin the watchdog piece would hit the same). Add-D_CRT_SECURE_NO_WARNINGSwhenos.name == "nt".Thanks for the careful review, hckrman101. All of points 1–3 and both nits are in
c69b5a6.CI: the test compiles with
-D_CRT_SECURE_NO_WARNINGS, as the other compiled checks do.The save:
predict_runandprecompile_runnow record when they go on and leave workers in the driver (prepare_left_workers()).TerminateProcess(prepare_end), not withreturn→exit(). It prints@@done shadersfirst, as the host would.PREPARE_SILENCEstill ends a save that never finishes.The 30 s in
predict_run: it now goes on from anywhere in the run only ifdriver_faults()rose during that run. Otherwise it usesprecompile_run's tail-only rule (next >= keys.size()), so a slow driver that still works keeps its pipelines, at the game's own start too. A real hang before the tail is left to the watchdog. The test has a case for each: a fault and then a hang early in the run goes on, the same hang with no fault is waited for, and a slow program (1.5 s, past a 1 s limit) isn't given up.Nits:
abandonedbefore each pipeline of its list.PrepareSeen).recovered with optimization disabled. As I read #1823'sbuild_graphics, that AMD retry runs beforefault_quarantine().end(p.key, r == kDriverFault), sorisVK_SUCCESSby then and the program isn't quarantined. Later builds of it would go back to the driver. It may be worth quarantining on the first fault even when the retry recovers, but that's #1823's call.kDriverFaultpath. Mynote_driver_fault()calls sit in the same four guard blocks, next to #1823's*pl = VK_NULL_HANDLEandreturn (VkResult)-77, and so do the two test stubs. The conflict is mechanical. Since #1823 is approved, I'd let it land first and rebase this onto it.On the evidence: you're right that the root cause comes from one log, and that other reports read as slowness. With point 3 changed, the only behaviour change without a counted fault is the watchdog's 5-minute stop in
--prepare-shaders. Slow-but-working runs now get status lines and otherwise run as before.Locally:
test_prepare_stallpasses all 12 cases. Full suite on Windows: 341 passed, 3 failed. The 3 (test_recorder_sync,test_fpx_sticky,test_precompile_compute_guard) fail the same way on an untouched copy of main on this PC.Done in
c69b5a6. It goes on from anywhere in the run only ifdriver_faults()rose during this run. Otherwise it uses the tail-only rule, asprecompile_rundoes (next >= keys.size()).test_prepare_stall.pychecks that a hang early in the run with no fault is waited for.Agreed.
predict_runandprecompile_runnow callprepare_left_workers_note()when they go on. When that's set, or a fault was counted, the watchdog stops the save the same way it stops the other parts.@ -727,6 +733,117 @@ std::string skipped_path() {std::string path(exe, n);Both are handled in
c69b5a6. In a clean run, the save's status lines stop after 300 s, soPREPARE_SILENCEapplies again. In a risky run (workers left, or a fault), the save is stopped at 300 s, and a run that gets through ends withTerminateProcess(prepare_end), notexit().@ -0,0 +1,359 @@# SPDX-License-Identifier: GPL-3.0-or-laterFixed:
-D_CRT_SECURE_NO_WARNINGSis added to the compile command, as in the other compiled checks.Closing in favour of #2195.
#2186 landed on main in the meantime. It has the preparation's pipeline steps log their progress every 10 s, which covers the "is it slow or stuck?" half of this PR and conflicted with it. #2195 is this work rebased onto #2186, with two changes on top:
Everything else carries over unchanged, including the changes from hckrman101's review here:
predict_run's first pipeline built aloneTerminateProcesswhen workers were leftA new PR keeps this one's review and history readable as they were, instead of force-pushing over them.