fix(vk): submit a 2x set's copies together so its presents keep their times #1720
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!1720
Loading…
Reference in a new issue
No description provided.
Delete branch "fix/present-ahead"
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?
Summary
With Frame generation 2x and Frame pacing on, frames reached the screen alternately ~5.7 and ~11 ms apart instead of every 8.3 ms, so a VRR display's refresh changed on every frame (flicker on OLED). Addresses #1702.
The pacer's times were right. Each frame's copy into its swapchain image was submitted at its due time, and by the game frame's turn the next game frame's rendering (~11 ms here) was already on the same queue, so its copy and present waited for that, while the made frame's went out at once.
Present ahead: for a paced set of two,
display_mainnow submits both copies together (withpresent_defer_intoset,present_imagereturns aftervkQueueSubmit2with whatpresent_commitneeds), waits for the first copy's fence, and keeps onlyvkQueuePresentKHRon the pacer's times.RX_VK_PRESENT_AHEAD=0turns it off.dev.swap_spare, the image count less the surface'sminImageCount, set when the swapchain is made. Past that an acquire has to time out (the Vulkan spec's limit), and on Linux the 100 ms acquire would take the window as not shown. The swapchain asks for 4 images, so with NVIDIA's minimum of 2 it runs. 3x and up, and unpaced output, are unchanged.present_commit, and that image is not presented.RX_VK_PRESENT_QUEUE=1the uneven delivery is gone (below).Validation
Intel PresentMon 2.3.1 (
MsBetweenDisplayChange), 60 s of running and turning in Hyrule Field. RTX 5080, NVIDIA 617.42, Ryzen 7 7800X3D, Windows 11, 3840x2160 at 165 Hz with G-Sync, DLSS Quality, Frame generation 2x (the default, AMD FSR 3.1's made frames), the game at its 60. These runs used v2.0.12 with this change, the same code in these files as main; the swapchain guard and the test were added for this PR.RX_VK_PRESENT_QUEUE=1(4 runs)RX_VK_FSR_FG=0, 60 s of normal playRX_VK_PRESENT_QUEUE=1without present ahead (v2.0.10)tests/test_present_ahead.py(new) compilespresent_ahead_wantedinto a fixture (on only for a paced 2x set with two spare images; off with the switch, unpaced, 1x, 3x, one or no spare image) and checks thatpresent_imagestops after the submit when deferred, thatpresent_commitskips a swapchain made again, and that the spare count comes fromminImageCount. It fails on main and on four mutations (one spare image allowed, 3x allowed, no swapchain check, no deferral).RX_VK_PRESENT_QUEUE=1on this PC, with no device loss or black screen.Limits: measured on one NVIDIA PC. The swapchain guard and the test are new since those runs and have not been run in game yet. Not tested on AMD, Intel or Linux, where the guard decides whether it runs. Keeping the presents fully on time on the single queue would need another way to keep them out of the next frame's queue order (present timing or metering, for example); not attempted here.
Found, patched and measured with the help of Claude (an AI assistant). No game files, generated code, binaries or saves are included.
Read through the diff and the surrounding present_image / display_main code. The approach holds up, and the numbers in the PR back it.
Things I checked:
present_commit(present_image_mu, thendev.present_mu) matchespresent_image, andpresent_outkeeps theqk.owns_lock()handling for the separate present queue.frames[]ring is used for two copies per set, and the next set's firstpresent_imagewaits on a different slot's fence.present_image(acquire timeout while the window is unseen, out-of-date, failed swapchain create) leavespending[k].usedfalse, sopresent_commitskips it. A swapchain made again between copy and present is caught by the swapchain handle compare.present_defer_intois thread_local and only set on the display thread, so the game thread's directpresent_imagecall is unaffected.swap_spareis computed afternis final, and the gate (paced, shown == 2, spare >= 2) keeps 3x and up and unpaced output on the old path.tests/test_present_ahead.pyis picked up byrun_all.py'stest_*.pydiscovery, so no registration is needed.Nits, none blocking:
test_present_ahead.pyis mostly source-text ordering checks plus the small gate fixture. That is fine as a guard, but it won't catch a regression in how the pieces behave together.RX_VK_PRESENT_AHEAD=0escape hatch covers AMD, Intel and Linux.menu_note_frameand the shown-at stats still count it as shown. That only matters around a resize, so it's cosmetic.CI hadn't reported on the head sha when I looked (status pending, no check runs).
Thanks for the review. Two commits on top of the merge:
tests/test_present_semaphores.py, the "Python source checks" failure on both OSes): the test readpi.pWaitSemaphoresfrompresent_image, and the present itself is nowpresent_out, so it reads it from there. The test also compiles the swapchain's making, wheredev.swap_spareis now set, so its stand-inDevicehas the field andrecreate()aminImageCountof 2, with new checks: 4 images leave 2 spare, 2 leave 0, 5 leave 3, and 0 after a failed creation.swap_spareis now reset with the swapchain, so a failed re-creation leaves no stale count.present_commitreturns whether it presented. A skipped present keeps the pacer's line (next_show) and stays out ofmenu_note_frameand the shown/late counts.Checked here:
test_present_semaphores.pyandtest_present_ahead.pypass, and fail on mutations (no reset, spare = image count, plus the earlier four).tests/run_all.pyon the branch head: 236 passed, 5 failed, 37 skipped. The same 5 fail on main0d8e755on this PC: four need tools it lacks (zstandard, a .NET SDK, C++ and Windows SDK headers for clang), andnx2/test_nup_stand_in.pyfails withTypeError: <lambda>() takes 1 positional argument but 2 were giveninmake_game.stand_in_damaged_files, which looks like it came with #1764, not this PR.hookshot_vk.dll(RX_VK_ONLY, on v2.0.13's tree) on the RTX 5080 / 617.42, DLSS Quality, 2x at 60 fps,RX_VK_PRESENT_QUEUE=1: the log has the present-ahead line, and 60 s of PresentMon gave 0.1% of frames shifting over 4 ms, a 1% low of 99 fps and a worst frame of 20 ms, the same as the first version.Fix conflicts, but also check with #1730
Heads-up: this conflicts with main now (a lot landed today). Could you merge main into the branch (or rebase) and resolve the conflicts? Thanks!
Re-checked at
7006ea0. Both follow-ups look right:present_commitnow returns false for a skipped present, and the loop movesnext_showon andcontinues pastmenu_note_frameand the counts. That closes my third nit.swap_spare = 0is now in the retire block ofcreate_swapchain, so a failed re-create leaves no stale count.test_present_semaphores.pyreadspWaitSemaphoresfrompresent_outnow and checks the 2/0/3/0 spare counts. That fixes the earlier "Python source checks" failure.One thing blocks the merge. Main moved again after your merge: #1515 (presentation device loss, merged 18:11) landed and the PR shows conflicts again. The conflict is in
gpu/vk/vk_replay.cpp, where #1515 changed the tail ofpresent_image(res = vkQueueSubmit2(...)+device_lost(), andif (res == VK_ERROR_DEVICE_LOST) device_lost();after the twovkQueuePresentKHRcalls). When you resolve it, keep the submit check inpresent_imageand move the present check intopresent_out.tests/test_present_device_loss.pyscans frompresent_imageto the nextnamespace {, so it will pick uppresent_outas long as it stays in that range.Nit: per #1822,
tests/test_present_ahead.pyshould declareRUN_ALL_ARGS = ['--cxx', '{cxx}'](see inline).CI hasn't reported any checks on this head. Approving again, assuming a clean merge of main.
@ -0,0 +33,4 @@def main():p = argparse.ArgumentParser(description=__doc__)p.add_argument("--cxx", default="g++")Nit: since #1822, run_all.py takes a test's arguments from its own
RUN_ALL_ARGS. WithoutRUN_ALL_ARGS = ['--cxx', '{cxx}']at module level, this runs with a hard-codedg++, not the compiler run_all found, and won't skip cleanly on a machine that has none (the same waytest_present_device_loss.pydeclares it).Merged main into the branch (
7006ea0). The one conflict wastests/test_present_semaphores.py: it now readspresent_imageandpresent_outeach as a complete function. The related checks pass.Checked with #1730 (2x now uses the renderer's own blend by default). Same spot as before (a dungeon entrance, 60 s of running and turning), RTX 5080 / 617.42, DLSS Quality, 2x with the game at 60, v2.0.13 with this PR's renderer files, the own blend, and the default single queue (
RX_VK_PRESENT_QUEUEunset):RX_VK_PRESENT_AHEAD=0RX_VK_PRESENT_QUEUE=1, for referenceSo with the own blend, the default single queue gets most of the gain; the separate queue removes the rest. With FSR 3.1's interpolation (
RX_VK_FSR_FG=1) on the single queue, present ahead helped little in three runs here (82-97% of frames shifting), so that combination still needs the present queue.Heads-up: this was merged at
a259978, a one-step commit wheregpu/vk/vk_replay.cppwas briefly back at main's version while I worked around the conflict (the re-apply was the next commit). So main is missing that file's half:present_commitandpresent_defer_intohave no definition (unresolved link), andtest_present_ahead.py/test_present_semaphores.pyfail. #1853 puts the same 24 lines back on top of main. Sorry about that.@mayathaim-hue @hckrman101: heads-up. The last commit before this merge had reset
gpu/vk/vk_replay.cppto main's copy, and the re-apply never landed. Sopresent_commitandpresent_defer_intoare declared and called, but not defined onmain, and the Vulkan back end does not link (Windows and the AppImage). #1858 puts the 24 missing lines back on currentmain, and will be merged once its CI is green. Until then, a red AppImage or Windows Vulkan check on a PR updated frommainis this, not the PR. Thanks!