perf(vk): the recorder thread's slot resets without the wait and an idle that waits on its ring (RX_VK_RECORDER_OWN, RX_VK_RECORDER_IDLE=wait; opt-in) #162
No reviewers
Labels
No labels
area/accessibility
area/audio
area/input
area/install
area/menu
area/renderer
area/saves
area/upscaling
good first issue
gpu/amd
gpu/igpu
gpu/intel
gpu/nvidia
help wanted
kind
bug
kind
chore
kind
docs
kind
feature
kind
performance
kind
question
needs measurements
platform/linux
platform/steam-deck
platform/windows
preview
priority
critical
priority
high
priority
low
status
blocked
status
confirmed
status
duplicate
status
invalid
status
needs-info
status
wontfix
testing wanted
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
MagicBeanDev/Longshot!162
Loading…
Reference in a new issue
No description provided.
Delete branch "klimovich008/Longshot:perf/recorder-own-idle"
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?
Why
With
RX_VK_RECORDER=thread(Hookshot #1473) the replay thread still waits for the recorder thread at every recording slot'sreuse, for a descriptor pool reset nothing in the recorder's ring can touch, and the recorder spins up to 200 us each time it runs
dry: in a census of an older Hookshot build (Hyrule Field, about 60 fps) about 25-32 such waits a frame (~0.13 ms of the replay thread) and about half a core of spin. This adds two
opt-in switches that take each away. Both are off by default; with both unset nothing changes.
What changed
RX_VK_RECORDER_OWN=1(gpu/vk/vk_recorder.h:OwnSlot,slot_begin,slot_done,produced, and one line inboundary()). A recording slot's own calls at its reuse skip the replay thread's wait for the recorder:begin_recording's twovkResetCommandPooland twovkBeginCommandBuffer(gpu/vk/vk_replay.cpp), anddraw_begin_recording'svkResetDescriptorPooland the destruction of the extra pools it grew (gpu/vk/vk_draw.cpp). Nothing still in the ring can namethem: the slot's last recording ended with a wait for the recorder (
vkEndCommandBuffer), was submitted (each submit waits too)and its submit is done (
recording_wait). The calls stay direct on the replay thread, which goes on to allocate and writedescriptor sets from that pool; only the wait goes.
slot_beginchecks the recorder has run past the slot's last end(
rec_end_pos, set after the two ends insubmit_recording); if not, or with the switch off, the calls wait as before (loggedonce). Every other reset, begin and destruction keeps its wait: the frame's command buffer, ImGui, NGX, FSR.
RX_VK_RECORDER_IDLE=wait(Recorder::idle,wake,start). With nothing to run the recorder spins 20 us, thenwaits on its ring counter W with
WaitOnAddress(compare against the last W it saw, 2 ms timeout), and the replay thread'spublish wakes it with
WakeByAddressSingle; unset orspin, the adaptive 20-200 us spin and the condition variable as before.On Windows the two functions come from kernelbase.dll at run time (no Synchronization.lib in the link); on Linux, host/posix's
futex. Without them the switch turns itself off at the recorder's start, logged.
tests/test_recorder_own_idle.pyandtests/recorder_own_idle_test.cpp(new); the source-cutting fixtures oftest_draw_stream_visibility,test_fsr_uniform_ownership,test_fsr_image_retirementandtest_recording_admissionget stubsfor the new marks.
docs/development.md(The GPU).Flags and settings
RX_VK_RECORDER_OWN(set)RX_VK_RECORDER=thread: a slot's own pool resets and begins at its reuse without the wait for the recorderRX_VK_RECORDER_IDLE=waitRX_VK_RECORDER=thread: the recorder's idle waits on its ring after a 20 us spin, instead of spinning up to 200 usMeasurements
main:
839e85ff(2026-11-13)this PR: perf/recorder-own-idle, its commits on
839e85ff@ 9ac9b714 (the same diff as this branch's)Hardware: RX 9070 XT, AMD 26.10.2 (Vulkan driver 0x8001a0), Ryzen 7 5700X3D, 64 GB, Windows 11
Scene: Hyrule Field, the standard test: the save loaded, the camera turning at full speed, frame rate unlocked
(
HOS_FPS_MAX=240), 1280x720 window, 2048x1152 output (16:9), no frame generationMethod: a 20 s window after the load, warm caches, 3 alternating pairs of the four arms; iteration 1 was rejected whole (the
thread arm's clock read 0.908 over frames of 407 and 486 ms), so 2 runs an arm. fps and CPU ms a frame from the present log and
the process times; the replay's times from the log's "a frame's replay time (ms)" lines and the recorder's from its 10 s stats
line, both inside the window.
How to reproduce: the build above with
RX_VK_RECORDER=thread, thenRX_VK_RECORDER_OWN=1orRX_VK_RECORDER_IDLE=wait;our standard-test script with one arm each, alternated.
Baseline: Longshot
839e85ff+ this PR @ 9ac9b714, RX_VK_RECORDER=threadMain was not the comparison: both switches are options of the recorder thread, which is itself opt-in. With them unset the code
is main's, so the recorder-off and recorder-on columns are main's two paths on this build.
Measured on main
839e85ff; main is at514591a3since. Its 36 commits in between add the occlusion query reports (#197, #260) and the Damage taken row, among others, and change none of the code this PR changes (vk_recorder.h,begin_recordingandsubmit_recordinginvk_replay.cpp,draw_begin_recordinginvk_draw.cpp): the fps on today's main will differ, the difference between the arms is this PR's own. A measurement on the next base will replace these.839e85ff+ this PR @ 9ac9b714, recorder off839e85ff+ this PR @ 9ac9b714, RX_VK_RECORDER=thread839e85ff+ this PR @ 9ac9b714, thread + RX_VK_RECORDER_OWN=1839e85ff+ this PR @ 9ac9b714, thread + RX_VK_RECORDER_IDLE=waitEach difference is under 5 % with 2 runs an arm: diagnostic. OWN removes the replay thread's waits at the descriptor pool reset
(1,693 a second to none) and its waits for the recorder fall by about 5.6 ms a second. The present did not take up what the
replay saved: the replay's present time is ~0.1 ms a frame in every arm. IDLE=wait spins less (0.32 of a core against 0.44) but
the replay thread waits longer for the recorder it woke (22.1 against 15.0 ms a second), and fps drops: as measured here it does
not pay.
Testing
Tested: game-free, on Windows with llvm-mingw clang.
tests/test_recorder_own_idle.pybuilds a fixture with the realvk_work.handvk_recorder.hand a fake Vulkan, the recorder thread held in a gated packet (no timing: a test hook lets thepacket run when the replay thread starts to wait, and a 3 s watchdog turns a wrong wait into a failure):
RX_VK_RECORDER_OWNbyenv_on; 0 and off are off), and named in therecorder's log line;
OwnSlotreaches the driver while a packet is in flight; a call that is not theslot's own (
vkEndCommandBuffer) within it, the reset outside it, the switch off, and a slot whose last end the recorder hasnot run (the guard, logged once) all wait for the packet;
WaitOnAddressis never called;=waitwithout the functions is turned off and logged; the recorder waiting on Wis woken by a publish; a publish between
sleepingset and the wait, with the wake made to do nothing, makes the wait returnat once on the changed W;
OwnSlotonly at the slot's own resets and begins, afterslot_begin, and the end position kept after both ends.Controls:
vk_recorder.hwithvkEndCommandBufferamong the slot's own calls, the guard removed, the switch ignored,OwnSlotignoring
slot_begin, the wait's compare taken from W instead of the last W seen, or the wake removed, each fails its case; thesource check finds an
OwnSlotelsewhere andslot_beginafter it. On the headd958b46a(main514591a3merged in): the rendererDLL builds and links, and
tests/run_all.pygives 496 passed, 4 failed, 34 skipped. The four fail on main514591a3too(
nx2/test_nup_paths.py,tests/test_check_third_party.py,tests/test_release.py, andtests/test_menu_i18n.py, checkedon a clean main checkout).
Tested in the game: the four arms above on the RX 9070 XT (Windows 11), Hyrule Field; each switch named in the recorder's
start line, the guard's line never logged.
Not tested: Vulkan validation with either switch. Other scenes (the standard test only, by our rule). Not built on Linux.
With both switches unset the code paths are main's, the recorder's log line included (a Steam Deck profile that turns
RX_VK_RECORDER=threadon is not changed by this PR).Needs other testers:
RX_VK_RECORDER=threadwith each switch, in the standard test (Hyrule Field, the camera turning fast), no new errors with Vulkan validation and therecorder's stats line, expecting fewer waits at "vkResetDescriptorPool" with OWN and less spin with IDLE (T-NSLN)
RX_VK_RECORDER_IDLE=waitfutex path (T-GTM9)Screenshots
Nothing on screen.
Risk and rollback
Both off unless set, and only with
RX_VK_RECORDER=thread, itself opt-in. OWN could let a reset race a recorded command if alater change queued slot ends or let a slot be reused before its end ran: the guard then makes the calls wait, logged once.
IDLE=wait could leave the recorder asleep through a publish it missed: the 2 ms timeout and the replay thread's kick while it
waits remain as before. Unsetting a switch turns it off; reverting the commits is enough.
Reviewer notes
These are steps 1 and 2 of a design that went through four rounds of review; its step 3, moving the submits to the recorder, is
not part of this PR and is a separate decision. In practice step 1 is the descriptor pool reset: the
command pool resets and begins already find the ring empty.
Duplicate check (2026-11-13): Longshot's open and closed PRs and issues, #agent-updates, and NotRexed/Hookshot's open PRs
searched for the recorder, replay thread, parallel replay, dirty pages, descriptors, drains, futex/WaitOnAddress and spin. No one
implements either switch. #101 and #60 here touch
tests/test_recorder_sync.py, which this PR does not change.Hookshot #2912 measured the recorder thread itself (rows only).
Issues
None.
Checklist
For everyone
type(scope): description).main.python -I -B tests/run_all.pyrun and passing (any failure that also fails onmainis named above).third_party/untouched (a new folder with aPROVENANCE.mdinstead).If applicable
Assisted-by:/Co-Authored-By:trailers.docs/tested-configurations.mdfor each measured run.mainand passing here.tests/that reads changed source updated in this PR, not weakened.🤖 Generated with Claude Code
Measurement plan: the standard Hyrule test only (no Kokiri or Temple route), 3 alternating pairs per comparison on the Longshot base at 16:9; under 5 % from 3 pairs reported as diagnostic. The tester request names the same scene.
Measured on Longshot (base
839e85ff+ this PR as a pack of the same patch, Hyrule standard test at 16:9, 2 valid pairs of 3 on an RX 9070 XT): recorder off 66.52 fps, RX_VK_RECORDER=thread 75.04, + RX_VK_RECORDER_OWN=1 76.57 (+2.0 % over the thread alone, about 40 % fewer replay waits for the recorder), + RX_VK_RECORDER_IDLE=wait 74.12 (-1.2 %). The third pair was rejected for stalls in a thread-only run. Diagnostic; both switches stay off. Description updated; merging current main next.Measured on Longshot
839e85ff+ this PR (9ac9b714, same diff), Hyrule standard test at 16:9, 2 valid runs an arm: the recorder thread 75.04 fps; with RX_VK_RECORDER_OWN=1 76.57 (+2.0 %, 3.7 ms less CPU a frame, no waits left at vkResetDescriptorPool); with RX_VK_RECORDER_IDLE=wait 74.12 (-1.2 %: the replay waits longer for the woken recorder). Diagnostic. The present took up none of it (~0.1 ms a frame in every arm). Rows added to docs/tested-configurations.md (8d rows commit).514591a3into perf/recorder-own-idle 9eee81642aSays why the
839e85ffnumbers hold on today's main: main's later commits do not touch the recorder or the slot functions.839e85ffA/B (the per-arm ones)Test requests open: T-NSLN (NVIDIA RTX, Windows) and T-GTM9 (AMD or NVIDIA, Linux). They can be claimed once a maintainer adds the 'testing wanted' label.
run_all on
d958b46a(main514591a3merged in): 496 passed, 4 failed, all four failing on main514591a3too (test_menu_i18n checked on a clean main checkout); the renderer DLL builds.WIP: perf(vk): the recorder thread's slot resets without the wait and an idle that waits on its ring (RX_VK_RECORDER_OWN, RX_VK_RECORDER_IDLE=wait; opt-in)to perf(vk): the recorder thread's slot resets without the wait and an idle that waits on its ring (RX_VK_RECORDER_OWN, RX_VK_RECORDER_IDLE=wait; opt-in)View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.