fix(vk): a fence wait covers every part of a split submission, not only the first #2419
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!2419
Loading…
Reference in a new issue
No description provided.
Delete branch "fix/value-rec-last-part"
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?
On AMD integrated GPUs (ROG Ally, Legion Go, Steam Deck) the picture breaks for one frame at a time: the HUD drawn stretched across the screen, grass flat white, the whole screen flashing. The game rewrites memory the GPU is still reading, because its fence waits return after the first part of a submission.
Cause
A submission longer than
RX_VK_CHUNKdraws (default 120), or one stopped at an event wait and finished later, is recorded in several recordings that all carry its value.submit_recordingmaps the value to its recording withvalue_rec.emplace, which keeps the first one: when the last part finishes the submission, the key is already there and nothing changes. Sowait_recorded(the game's fence waits inwait(), and the exposure readback wait) returns once the first ~120 draws are done.With no VRAM mirror of the buffer pools (integrated GPUs), the later parts read guest memory in place, and the game has by then written the next frame's uniforms, vertices and exposure meter into it. The HUD is drawn late in the frame, so it gets the wrong transforms most often; the stale exposure readback gives the whole-screen flashes.
myylkn found the same root cause on a Radeon 8060S (#997, with a patch in a comment). This PR is a variant of that patch:
findand an update in place rather thanvalue_rec[v], so it builds againsttests/draw_stream_visibility_test.cpp, which modelsvalue_recas astd::multimap.r.valuewhen the recording does not signal (the parts split off before the last), raised to the latest seq. A wait on a stopped submission still finds its recording, instead of falling back to the timeline wait, which forces stopped work past its event waits.Change
gpu/vk/vk_replay.cpp,submit_recording: each value maps to the latest recording that carries it.tests/test_value_rec_last_part.py: runs the statements cut fromsubmit_recording, for a submission split in three parts and for a stopped submission finished in a later recording. It fails onmain(value 10 -> recording 1 (expected 3),value 11 -> recording 4 (expected 5)) and passes with the change.docs/tested-configurations.md: the three runs below.Tested
ROG Ally X (Ryzen Z1 Extreme, RADV PHOENIX, Mesa 26.2.3), Linux (Anatase, Fedora-based), counting one-frame luminance spikes in screen recordings (a frame brighter or darker than both its neighbours):
GPU waits in 5 sThen about a day and a half of play with the change, rebuilt for v2.0.14 and v2.1.0 (the renderer alone,
RX_VK_ONLY=1): no flashes or stray HUD seen in Hyrule Field, Death Mountain Trail and Goron City, in Desktop Mode and in Steam's Gaming Mode. A 57 s Gaming Mode recording on v2.0.14 had no one-frame spikes at all.It costs frame rate on this device. The FPS overlay in the field read roughly 45–70 before and 38–49 after (two recordings, not a controlled benchmark): part of the old frame rate was the game running ahead of the GPU. The waits now last as long as the GPU work they stand for.
python -I -B tests/run_all.pywith the Vulkan headers: the same results on a clean checkout ofmainwith and without the change, apart from the new test, which passes.test_draw_stream_visibility,test_capture_slot_ownershipandtest_parallel_rb_refresh, which compilesubmit_recording, pass.Not tested: Windows, NVIDIA and discrete AMD cards (the early wait is there too, but their VRAM mirror is synced at the submit, which should hide it; not measured),
RX_VK_PARALLELsegments (off by default; the same mapping applies to them), and the Steam Deck.Related
RX_VK_UB_SNAPSHOT=0would show how much of its effect was this wait.value_rec, which this corrects.Screenshots
Before (v2.0.13, Hyrule Field and Castle Town; each strip is the frame before, the broken frame, the frame after):
After (v2.0.13 with the change, Hyrule Field, fights included; the last strip is the largest one-frame spike left, a sword hit):
The analysis and the change were made with AI assistance (Claude); the testing and recordings are mine.
🤖 Generated with Claude Code
This looks right to me, and it's a well-made PR: a clear cause, a fixture cut from the real code, and before/after recordings on hardware. I confirmed that
emplacekeeps the first part'sseq, and thatmaybe_split(gpu/vk/vk_replay.cpp:1321), the parallel segments (:1480) andfinish_recording(:1892) all submit recordings that carry the samer.value. Before this change,wait_recordedtherefore waited on the first part. I rantest_value_rec_last_part,test_draw_stream_visibility,test_capture_slot_ownershipandtest_parallel_rb_refreshon the PR's head, and they pass. As a negative control I pointed the new test at main'svk_replay.cppwith--source: it fails the two cases you list. The frame-rate cost is honest and expected, since the waits now cover the work they stand for.question
gpu/vk/vk_replay.cpp:382-388and:2143-2150: a window remains while a submission is still being recorded. Once the first part has gone throughmaybe_split,value_rec[v]is that part'ssequntil the last part is submitted.wait()doesn't takereplay_mu, andq_last/value_queueare set beforesubmit_job(:4261). So another game thread that waits on that queue's fence in that window still hasminetrue (the queue isn't blocked) and returns after the first part only. The same holds after a stopped submission is let go and while its remainder is being recorded. This is much narrower than before, when the mapping stayed wrong for good, so I don't think it should hold the PR up. If you want to close it, one option is to mark the value as "still recording" (for example, a set underrec_map_muthatrun_submitfills while it records andfinish_recordingclears).wait_recordedwould then return false for a marked value and fall back to the timeline wait. If it isn't worth it, a one-line note in the comment that the map is final only once the last part is submitted would help the next reader.minor: the PR conflicts with main in
docs/tested-configurations.mdonly (rows added at the top since). The code merges cleanly.github-sync referenced this pull request2026-11-12 06:22:54 +00:00