feat(vk): NVIDIA Reflex: each queue submit tagged with the game frame its GPU work belongs to #2499
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!2499
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/reflex-submit-ids"
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?
Follows @nullifyshot's lead on #2118 (comment). Under DLSS frame generation, Reflex's own "to the GPU's end" read 243–442 ms there (RTX 5080, 4x), next to ~21 ms from simulation start to present end. Hookshot chained no
VkLatencySubmissionPresentIdNVto its queue submits, so the driver had to guess which frame each submit's GPU work belongs to. Without frame generation it guesses right. With it, the display thread's copies and DLSS-G's work sit between the game's frames and get counted as the next game frame's rendering.What changes
reflex_submit_id(next, frame)(vk_reflex.cpp) chains aVkLatencySubmissionPresentIdNVto aVkSubmitInfo2(vk.xml:structextends="VkSubmitInfo,VkSubmitInfo2"). It is used at the four submit sites in vk_replay.cpp:submit_recording's direct submit and the parallel segment jobs' submit: the frame being made.queue_flush_locked): the frame of the batch's first command buffer, kept inpend.reflexwhen the batch starts.present_image's copy: the frame it shows, by the same rule as the present id: the set's game frame (reflex_present_frame), no tag for a made frame, and the frame being made without frame generation.There is no tag when Reflex is off, when no frame is marked, or when that present id was presented already (present ids only go up). Each tag is taken right before its
vkQueueSubmit2, after any flush, and that flush tags its own batch.RX_VK_REFLEX_SUBMIT_IDS=0leaves the tags off, for an A/B comparison.In game: RTX 5090, 5120x2160 swapchain, DLAA and DLSS-G 3x at 4416x1864 (600 of 600 presents made by DLSS-G), V-Sync Auto, Reflex On + Boost,
RX_VK_REFLEX_LOG=1, Kokiri Forest. Same build both runs; run 2 only addsRX_VK_REFLEX_SUBMIT_IDS=0.RX_VK_REFLEX_SUBMIT_IDS=0)With the tags, "to the GPU's end" lands a few ms after present end, the same pattern as without frame generation (12.1 vs 10.1 ms in nullifyshot's run), so the driver now paces on real GPU timings. The tags-off run also felt worse to play. Its simulation-to-present numbers are a bit higher too, but it was a short run (5 reports), so I wouldn't read much into that gap yet.
What this doesn't measure: input to screen. @nullifyshot, if you have time, your
HOS_INPUT_LIVEmethod on this head would show what it's actually worth. The ~⅔ of a game frame that interpolation holds the real frame back stays as it was; Reflex can't remove that.Tests:
test_reflex_swapchain_lifetime.pycopiesreflex_submit_idand gains 8 checks (the frame being made, a kept frame, frame 0, no frame marked, one already presented, Reflex off, retired before the lock).draw_stream_visibility_test.cpp,fsr_image_retirement_test.cppandparallel_init_order_test.cppget stand-ins.tests/run_all.py(Windows, llvm-mingw clang++): 391 passed, 31 skipped, 1 failed:test_vram_budget_row, which reads onlyvk_menu.cpp,vk_menu.handvk_memory.cpp(not touched here) and fails the same way on main.🤖 Generated with Claude Code
The tagging looks right. Each tag is taken under
dev.present_muright before itsvkQueueSubmit2, after any flush (which tags its own batch), so nothing can overwrite the thread_local struct in between. Takingdev.present_mubeforer.muis the orderreflex_submit_ends()already uses. Made frames and ids already presented get no tag, and the A/B numbers are convincing. On other GPUs the cost is one uncontended lock per submit, which is fine.One rule issue:
gpu/vk/vk_reflex.cpp:245readsRX_VK_REFLEX_SUBMIT_IDSwithatoi(...) == 0. Since #2042 the convention isenv_value_off/env_onfromcompat/hookshot_env.h. Withatoi,=trueor=onturns the tags off.const char* e = getenv(...); off = e && env_value_off(e);would fix it. Please also add the switch to the Reflex row indocs/development.md:170.After that it's ready from my side. A batch that spans two game frames is credited to the first frame's id. That seems an acceptable approximation, but the comment should say so.
Ran the input-to-screen test on
ca483d88with the same PC, method and spot as on #2118: RTX 5080, 360 Hz G-SYNC, DLAA, DLSS-G 4x, Reflex On + Boost,RX_VK_REFLEX_LOG=1. The game presses its own buttons throughHOS_INPUT_LIVE: 20 camera flicks, 20 sword swings and 20 steps per run. Each is timed from the press being due to the first desktop frame (Desktop Duplication) that shows the reaction. All three runs are the same build. Run 2 only addsRX_VK_REFLEX_SUBMIT_IDS=0, and run 3 setsRX_VK_DLSS_FG=0andRX_VK_FRAMEGEN=0. All 180 presses were measured, and DLSS-G made 600 of 600 presents in every window of runs 1 and 2.Reflex's own timing over the runs:
One thing that isn't this PR: compared with my #2118 run on
3a39e3f, this build's game frame rate is lower in roughly the same spot (about 68 vs 77 fps with DLSS-G, and 87 vs 97 with it off). The sword and walk are also slower in every mode. Main moved about 100 commits between the two, so that's somewhere in main, not the tags.Nice, and the numbers back it up: your 5090 run and @nullifyshot's input-to-screen run both show the GPU-end timing going from hundreds of ms to a few ms past present end, with a likely 6–7 ms gain on sword/walk.
I checked the four sites. Each tag is taken under
dev.present_muright before itsvkQueueSubmit2, so the thread_local struct can't be overwritten in between.present_imageflushes the batch (tagged with its own frame) before tagging its copy. The lock order (present_muthenr.mu) matches the existing callers. Made frames, unmarked frames and ids already presented get no tag.VkLatencySubmissionPresentIdNVis allowed onVkSubmitInfo2, andon()covers the extension being enabled. The test stand-ins match the new signature. CI is green.I agree with @Joey-Magnus's review and won't repeat it. It's still open at this head:
RX_VK_REFLEX_SUBMIT_IDSshould useenv_value_offfromgpu/vk/compat/hookshot_env.hrather thanatoi(...) == 0, the switch needs adding to the Reflex row indocs/development.md, and the comment should say a batch is credited to its first frame. Once those are in, this is good to go from my side.One small addition to that last point, inline.
@ -419,3 +423,4 @@si.pNext = reflex_submit_id(nullptr); // (NVIDIA Reflex: the frame being made)const VkResult res = vkQueueSubmit2(dev.queue, 1, &si, r.fence);r.submitted = res == VK_SUCCESS;static int failures;Nit, on top of Joey's note: the batch's frame is read only when it starts. So a batch started before the frame's sleep returned (
reflex_frame()= 0) stays untagged, even when the rest of its command buffers belong to the new frame. That's harmless, since batching is off by default outside RADV, so on NVIDIA this only matters withRX_VK_BATCH=1. It's worth a word in the same comment, though.This now conflicts with main in
gpu/vk/vk_reflex.cpp. Could you merge main into the branch (or rebase) when you get a chance? I'll take another look once it's up to date.Thanks both, and thanks @nullifyshot for the input-to-screen run.
Updated: main merged first (
8678b655). #2325'sreflex_present_id_featuresnow sits next toreflex_submit_id. With Reflex on, its presenter thread isn't used, so the present's tag stays on the game's thread, wherereflex_present_frameis set. Then the review notes (fa75d051):RX_VK_REFLEX_SUBMIT_IDSis read withenv_value_offfromhookshot_env.h.test_reflex_swapchain_lifetime.pynow runs its fixture with=on(tags on; the oldatoireading fails it at check 21) and with=0(untagged).docs/development.mdlists the switch.vk_reflex.cppand the batch start invk_replay.cppsay how a batch is credited: to the frame of its first command buffer, read when the batch starts. So one that spans two game frames counts as the first's, and one begun before the frame's sleep returned goes untagged. Batching is on by default only with RADV, which has no Reflex.tests/run_all.py(Windows, llvm-mingw): 432 passed, 35 skipped, 3 failed, none of them in this PR:test_env_on(main's #2470 readsHOS_FPS_LOGas set-means-on inhost/oot_high_fps.c:15),test_vram_budget_row(fails the same on main here) andtest_portable_builder_startup(itsdotnet publishneeds packages this PC doesn't have).On the frame-rate drop between the two builds (about 68 vs 77 fps with DLSS-G, 87 vs 97 without): agreed, that's somewhere in main, not here. It's next on my list for the frame-rate work.
Thanks, all three of @Joey-Magnus's asks are in, plus my nit:
RX_VK_REFLEX_SUBMIT_IDSis read withenv_value_offfromhookshot_env.h. The test now runs with=on(which the oldatoireading would have turned off) and=0.docs/development.md.vk_reflex.cppand the batch start invk_replay.cppboth say a batch is credited to its first command buffer's frame, and that one started before the frame is marked stays untagged.The merge is clean. The PR is 0 behind main. Against main,
vk_reflex.cpphas only additions: the header comment, the include andreflex_submit_id, sitting next to #2325'sreflex_present_id_features. None of main's lines were dropped.vk_replay.cppdiffers from main only at the four tag sites andpend.reflex. With #2325's presenter thread, the copy's submit and its tag stay inpresent_imageon the game's thread. A deferred present carriesreflex_present_framewith it, so the tag still matches the frame that's shown.CI was still pending at this head when I looked. The
test_env_onfailure you mention comes from main's #2470, not from this PR. Good to merge once CI is green.@ -419,3 +423,4 @@si.pNext = reflex_submit_id(nullptr); // (NVIDIA Reflex: the frame being made)const VkResult res = vkQueueSubmit2(dev.queue, 1, &si, r.fence);r.submitted = res == VK_SUCCESS;static int failures;Done in
fa75d051: the comment at the batch start says it now. Thanks.github-sync referenced this pull request2026-11-12 06:22:54 +00:00