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

Open
klimovich008 wants to merge 9 commits from klimovich008/Longshot:perf/recorder-own-idle into main
Contributor

Why

With RX_VK_RECORDER=thread (Hookshot #1473) the replay thread still waits for the recorder thread at every recording slot's
reuse, 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

  • recorder: RX_VK_RECORDER_OWN=1 (gpu/vk/vk_recorder.h: OwnSlot, slot_begin, slot_done, produced, and one line in
    boundary()). A recording slot's own calls at its reuse skip the replay thread's wait for the recorder: begin_recording's two
    vkResetCommandPool and two vkBeginCommandBuffer (gpu/vk/vk_replay.cpp), and draw_begin_recording's
    vkResetDescriptorPool and the destruction of the extra pools it grew (gpu/vk/vk_draw.cpp). Nothing still in the ring can name
    them: 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 write
    descriptor sets from that pool; only the wait goes. slot_begin checks the recorder has run past the slot's last end
    (rec_end_pos, set after the two ends in submit_recording); if not, or with the switch off, the calls wait as before (logged
    once). Every other reset, begin and destruction keeps its wait: the frame's command buffer, ImGui, NGX, FSR.
  • recorder: RX_VK_RECORDER_IDLE=wait (Recorder::idle, wake, start). With nothing to run the recorder spins 20 us, then
    waits on its ring counter W with WaitOnAddress (compare against the last W it saw, 2 ms timeout), and the replay thread's
    publish wakes it with WakeByAddressSingle; unset or spin, 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: tests/test_recorder_own_idle.py and tests/recorder_own_idle_test.cpp (new); the source-cutting fixtures of
    test_draw_stream_visibility, test_fsr_uniform_ownership, test_fsr_image_retirement and test_recording_admission get stubs
    for the new marks.
  • docs: both switches in docs/development.md (The GPU).

Flags and settings

Flag What it does Default now Recommendation Helps on Doesn't help on Untested on
RX_VK_RECORDER_OWN (set) With RX_VK_RECORDER=thread: a slot's own pool resets and begins at its reuse without the wait for the recorder off off by default for now; on with the recorder after more testing (other GPUs, Linux) RX 9070 XT, Windows: +2.0 % fps, 3.7 ms less CPU a frame than the recorder alone (n=2, diagnostic) not known NVIDIA, Intel, Linux, Steam Deck
RX_VK_RECORDER_IDLE=wait With RX_VK_RECORDER=thread: the recorder's idle waits on its ring after a 20 us spin, instead of spinning up to 200 us off off: it costs fps here - RX 9070 XT, Windows: -1.2 % fps (n=2, diagnostic); the replay thread waits longer for the woken recorder NVIDIA, Intel, Linux, Steam Deck

Measurements

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 generation
Method: 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, then RX_VK_RECORDER_OWN=1 or RX_VK_RECORDER_IDLE=wait;
our standard-test script with one arm each, alternated.

Baseline: Longshot 839e85ff + this PR @ 9ac9b714, RX_VK_RECORDER=thread

Main 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 at 514591a3 since. 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_recording and submit_recording in vk_replay.cpp, draw_begin_recording in vk_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.

Longshot 839e85ff + this PR @ 9ac9b714, recorder off Longshot 839e85ff + this PR @ 9ac9b714, RX_VK_RECORDER=thread Longshot 839e85ff + this PR @ 9ac9b714, thread + RX_VK_RECORDER_OWN=1 Longshot 839e85ff + this PR @ 9ac9b714, thread + RX_VK_RECORDER_IDLE=wait
fps 66.52 75.04 76.57 (+2.0 %) 74.12 (-1.2 %)
CPU ms a frame 59.07 68.65 64.91 (-3.7) 66.82 (-1.8)
p99 frame ms 22.10 19.93 19.01 20.17
worst frame ms 57.4 67.8 47.3 69.5
replay thread's waits for the recorder, ms a second 14.97 9.34 22.10
of them at vkResetDescriptorPool, a second 1,693 (7.65 ms) 0 1,688 (15.01 ms)
recorder spinning idle, of a core 0.44 0.45 0.32
replay's submit time, ms a frame 11.46 9.23 9.07 9.61
replay's present time, ms a frame 0.10 0.11 0.10 0.12

Each 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.

std_test, per run (fps, CPU ms a frame; iterations 0 and 2)
off       66.59 57.95 | 66.44 60.20
thread    75.54 67.33 | 74.54 69.96
own       76.79 64.78 | 76.34 65.04
idle      74.79 66.12 | 73.44 67.52
vk: recorder: ... the replay waited for it N times, T ms in all; ... vkResetDescriptorPool N/T ms   (10 s lines in the window)
vk: a frame's replay time (ms): submit S, present P, ...                                             (in the window)

Testing

Tested: game-free, on Windows with llvm-mingw clang. tests/test_recorder_own_idle.py builds a fixture with the real
vk_work.h and vk_recorder.h and a fake Vulkan, the recorder thread held in a gated packet (no timing: a test hook lets the
packet run when the replay thread starts to wait, and a 3 s watchdog turns a wrong wait into a failure):

  • the switches read as the renderer reads them (RX_VK_RECORDER_OWN by env_on; 0 and off are off), and named in the
    recorder's log line;
  • OWN: the slot's descriptor pool reset within OwnSlot reaches the driver while a packet is in flight; a call that is not the
    slot's own (vkEndCommandBuffer) within it, the reset outside it, the switch off, and a slot whose last end the recorder has
    not run (the guard, logged once) all wait for the packet;
  • IDLE: unset, WaitOnAddress is never called; =wait without the functions is turned off and logged; the recorder waiting on W
    is woken by a publish; a publish between sleeping set and the wait, with the wake made to do nothing, makes the wait return
    at once on the changed W;
  • the sources: OwnSlot only at the slot's own resets and begins, after slot_begin, and the end position kept after both ends.
    Controls: vk_recorder.h with vkEndCommandBuffer among the slot's own calls, the guard removed, the switch ignored, OwnSlot
    ignoring slot_begin, the wait's compare taken from W instead of the last W seen, or the wake removed, each fails its case; the
    source check finds an OwnSlot elsewhere and slot_begin after it. On the head d958b46a (main 514591a3 merged in): the renderer
    DLL builds and links, and tests/run_all.py gives 496 passed, 4 failed, 34 skipped. The four fail on main 514591a3 too
    (nx2/test_nup_paths.py, tests/test_check_third_party.py, tests/test_release.py, and tests/test_menu_i18n.py, checked
    on 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=thread on is not changed by this PR).

Needs other testers:

  • NVIDIA on Windows: RX_VK_RECORDER=thread with each switch, in the standard test (Hyrule Field, the camera turning fast), no new errors with Vulkan validation and the
    recorder's stats line, expecting fewer waits at "vkResetDescriptorPool" with OWN and less spin with IDLE (T-NSLN)
  • Linux, RADV: as above, and the RX_VK_RECORDER_IDLE=wait futex 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 a
later 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

  • One topic.
  • The title is a Conventional Commit (type(scope): description).
  • Rebased on current main.
  • python -I -B tests/run_all.py run and passing (any failure that also fails on main is named above).
  • Tested in the game, or what wasn't is named under Testing.
  • Testing filled in: what was tested, what wasn't, and what needs other testers (with test requests).
  • Measured: main and this PR at named commits, on the same PC, scene and settings, at least 2 interleaved runs each, the median reported.
  • No game files, dumps, build output or generated files.
  • third_party/ untouched (a new folder with a PROVENANCE.md instead).
  • The license is OK: GPL-3.0-or-later, nothing incompatible and nothing decompiled.

If applicable

  • AI assistance recorded with Assisted-by: / Co-Authored-By: trailers.
  • Flags noted, and the switch reference updated (docs/development.md, the README).
  • A row in docs/tested-configurations.md for each measured run.
  • A game-free test added for the fix, failing on main and passing here.
  • A check under tests/ that reads changed source updated in this PR, not weakened.
  • Docs updated.

🤖 Generated with Claude Code

## Why With `RX_VK_RECORDER=thread` ([Hookshot #1473](https://github.com/NotRexed/Hookshot/pull/1473)) the replay thread still waits for the recorder thread at every recording slot's reuse, 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 - recorder: `RX_VK_RECORDER_OWN=1` (`gpu/vk/vk_recorder.h`: `OwnSlot`, `slot_begin`, `slot_done`, `produced`, and one line in `boundary()`). A recording slot's own calls at its reuse skip the replay thread's wait for the recorder: `begin_recording`'s two `vkResetCommandPool` and two `vkBeginCommandBuffer` (`gpu/vk/vk_replay.cpp`), and `draw_begin_recording`'s `vkResetDescriptorPool` and the destruction of the extra pools it grew (`gpu/vk/vk_draw.cpp`). Nothing still in the ring can name them: 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 write descriptor sets from that pool; only the wait goes. `slot_begin` checks the recorder has run past the slot's last end (`rec_end_pos`, set after the two ends in `submit_recording`); if not, or with the switch off, the calls wait as before (logged once). Every other reset, begin and destruction keeps its wait: the frame's command buffer, ImGui, NGX, FSR. - recorder: `RX_VK_RECORDER_IDLE=wait` (`Recorder::idle`, `wake`, `start`). With nothing to run the recorder spins 20 us, then waits on its ring counter W with `WaitOnAddress` (compare against the last W it saw, 2 ms timeout), and the replay thread's publish wakes it with `WakeByAddressSingle`; unset or `spin`, 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: `tests/test_recorder_own_idle.py` and `tests/recorder_own_idle_test.cpp` (new); the source-cutting fixtures of `test_draw_stream_visibility`, `test_fsr_uniform_ownership`, `test_fsr_image_retirement` and `test_recording_admission` get stubs for the new marks. - docs: both switches in `docs/development.md` (The GPU). ## Flags and settings | Flag | What it does | Default now | Recommendation | Helps on | Doesn't help on | Untested on | |---|---|---|---|---|---|---| | `RX_VK_RECORDER_OWN` (set) | With `RX_VK_RECORDER=thread`: a slot's own pool resets and begins at its reuse without the wait for the recorder | off | off by default for now; on with the recorder after more testing (other GPUs, Linux) | RX 9070 XT, Windows: +2.0 % fps, 3.7 ms less CPU a frame than the recorder alone (n=2, diagnostic) | not known | NVIDIA, Intel, Linux, Steam Deck | | `RX_VK_RECORDER_IDLE=wait` | With `RX_VK_RECORDER=thread`: the recorder's idle waits on its ring after a 20 us spin, instead of spinning up to 200 us | off | off: it costs fps here | - | RX 9070 XT, Windows: -1.2 % fps (n=2, diagnostic); the replay thread waits longer for the woken recorder | NVIDIA, Intel, Linux, Steam Deck | ## Measurements 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 generation Method: 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`, then `RX_VK_RECORDER_OWN=1` or `RX_VK_RECORDER_IDLE=wait`; our standard-test script with one arm each, alternated. Baseline: Longshot 839e85ff + this PR @ 9ac9b714, RX_VK_RECORDER=thread Main 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 at 514591a3 since. 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_recording` and `submit_recording` in `vk_replay.cpp`, `draw_begin_recording` in `vk_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. | | Longshot 839e85ff + this PR @ 9ac9b714, recorder off | Longshot 839e85ff + this PR @ 9ac9b714, RX_VK_RECORDER=thread | Longshot 839e85ff + this PR @ 9ac9b714, thread + RX_VK_RECORDER_OWN=1 | Longshot 839e85ff + this PR @ 9ac9b714, thread + RX_VK_RECORDER_IDLE=wait | |---|---|---|---|---| | fps | 66.52 | 75.04 | 76.57 (+2.0 %) | 74.12 (-1.2 %) | | CPU ms a frame | 59.07 | 68.65 | 64.91 (-3.7) | 66.82 (-1.8) | | p99 frame ms | 22.10 | 19.93 | 19.01 | 20.17 | | worst frame ms | 57.4 | 67.8 | 47.3 | 69.5 | | replay thread's waits for the recorder, ms a second | | 14.97 | 9.34 | 22.10 | | of them at vkResetDescriptorPool, a second | | 1,693 (7.65 ms) | 0 | 1,688 (15.01 ms) | | recorder spinning idle, of a core | | 0.44 | 0.45 | 0.32 | | replay's submit time, ms a frame | 11.46 | 9.23 | 9.07 | 9.61 | | replay's present time, ms a frame | 0.10 | 0.11 | 0.10 | 0.12 | Each 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. ``` std_test, per run (fps, CPU ms a frame; iterations 0 and 2) off 66.59 57.95 | 66.44 60.20 thread 75.54 67.33 | 74.54 69.96 own 76.79 64.78 | 76.34 65.04 idle 74.79 66.12 | 73.44 67.52 vk: recorder: ... the replay waited for it N times, T ms in all; ... vkResetDescriptorPool N/T ms (10 s lines in the window) vk: a frame's replay time (ms): submit S, present P, ... (in the window) ``` ## Testing **Tested:** game-free, on Windows with llvm-mingw clang. `tests/test_recorder_own_idle.py` builds a fixture with the real `vk_work.h` and `vk_recorder.h` and a fake Vulkan, the recorder thread held in a gated packet (no timing: a test hook lets the packet run when the replay thread starts to wait, and a 3 s watchdog turns a wrong wait into a failure): - the switches read as the renderer reads them (`RX_VK_RECORDER_OWN` by `env_on`; 0 and off are off), and named in the recorder's log line; - OWN: the slot's descriptor pool reset within `OwnSlot` reaches the driver while a packet is in flight; a call that is not the slot's own (`vkEndCommandBuffer`) within it, the reset outside it, the switch off, and a slot whose last end the recorder has not run (the guard, logged once) all wait for the packet; - IDLE: unset, `WaitOnAddress` is never called; `=wait` without the functions is turned off and logged; the recorder waiting on W is woken by a publish; a publish between `sleeping` set and the wait, with the wake made to do nothing, makes the wait return at once on the changed W; - the sources: `OwnSlot` only at the slot's own resets and begins, after `slot_begin`, and the end position kept after both ends. Controls: `vk_recorder.h` with `vkEndCommandBuffer` among the slot's own calls, the guard removed, the switch ignored, `OwnSlot` ignoring `slot_begin`, the wait's compare taken from W instead of the last W seen, or the wake removed, each fails its case; the source check finds an `OwnSlot` elsewhere and `slot_begin` after it. On the head d958b46a (main 514591a3 merged in): the renderer DLL builds and links, and `tests/run_all.py` gives 496 passed, 4 failed, 34 skipped. The four fail on main 514591a3 too (`nx2/test_nup_paths.py`, `tests/test_check_third_party.py`, `tests/test_release.py`, and `tests/test_menu_i18n.py`, checked on 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=thread` on is not changed by this PR). **Needs other testers:** - NVIDIA on Windows: `RX_VK_RECORDER=thread` with each switch, in the standard test (Hyrule Field, the camera turning fast), no new errors with Vulkan validation and the recorder's stats line, expecting fewer waits at "vkResetDescriptorPool" with OWN and less spin with IDLE (T-NSLN) - Linux, RADV: as above, and the `RX_VK_RECORDER_IDLE=wait` futex 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 a later 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](https://github.com/NotRexed/Hookshot/pull/2912) measured the recorder thread itself (rows only). ## Issues None. ## Checklist ### For everyone - [x] One topic. - [x] The title is a Conventional Commit (`type(scope): description`). - [x] Rebased on current `main`. - [x] `python -I -B tests/run_all.py` run and passing (any failure that also fails on `main` is named above). - [x] Tested in the game, or what wasn't is named under Testing. - [x] Testing filled in: what was tested, what wasn't, and what needs other testers (with test requests). - [x] Measured: main and this PR at named commits, on the same PC, scene and settings, at least 2 interleaved runs each, the median reported. - [x] No game files, dumps, build output or generated files. - [x] `third_party/` untouched (a new folder with a `PROVENANCE.md` instead). - [x] The license is OK: GPL-3.0-or-later, nothing incompatible and nothing decompiled. ### If applicable - [x] AI assistance recorded with `Assisted-by:` / `Co-Authored-By:` trailers. - [x] Flags noted, and the switch reference updated (docs/development.md, the README). - [x] A row in `docs/tested-configurations.md` for each measured run. - [x] A game-free test added for the fix, failing on `main` and passing here. - [x] A check under `tests/` that reads changed source updated in this PR, not weakened. - [x] Docs updated. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Both opt-in, for RX_VK_RECORDER=thread. OWN: a recording slot's own command
pool resets and begins and its descriptor pool reset, at the slot's reuse, no
longer wait for the recorder (OwnSlot at those call sites; slot_begin() checks
the recorder ran the slot's last end). IDLE=wait: after a 20 us spin the
recorder waits on its ring counter (WaitOnAddress, a futex on Linux), and a
publish wakes it. Unset, both behave as before.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The OWN cases let the gated packet run when the producer starts waiting
(a test hook in wait_idle), not after 20 ms; the race case wakes the parked
recorder directly instead of through a packet the producer would wait for;
the switches printed after mode() reads them; the source check takes the
OwnSlot's whole block.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
draw_stream_visibility, fsr_uniform_ownership, fsr_image_retirement and
recording_admission cut begin_recording, submit_recording or
draw_begin_recording from the source: they get rec::slot_begin, slot_done,
produced, OwnSlot and rec_end_pos as the recorder-off renderer has them. The
OWN skip in boundary() is its own line, so test_recorder_sync's unordered()
variant finds its anchor.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
docs(development): RX_VK_RECORDER_OWN and RX_VK_RECORDER_IDLE in The GPU
Some checks failed
PR description / Description follows the template (pull_request_target) Successful in 4s
CI / Conventional Commit title (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been cancelled
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been cancelled
CI / Workflow lint (actionlint) (pull_request) Has been cancelled
Third-party parts / As their notes say (pull_request) Has been cancelled
Third-party parts / The same as upstream (pull_request) Has been cancelled
cb5719d270
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Author
Contributor

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.

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.
Author
Contributor

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 (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.
Author
Contributor

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).

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).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
docs(tested): RX 9070 XT, the standard test with the recorder thread and each of its new switches
Some checks failed
Website / Website on Cloudflare (pull_request) Blocked by required conditions
PR description / Description follows the template (pull_request_target) Successful in 5s
CI / Conventional Commit title (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been cancelled
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been cancelled
CI / Workflow lint (actionlint) (pull_request) Has been cancelled
Third-party parts / As their notes say (pull_request) Has been cancelled
Third-party parts / The same as upstream (pull_request) Has been cancelled
0d934b045f
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Author
Contributor

Says why the 839e85ff numbers hold on today's main: main's later commits do not touch the recorder or the slot functions.

Says why the 839e85ff numbers hold on today's main: main's later commits do not touch the recorder or the slot functions.
docs(tested): one set of rows for the 839e85ff A/B (the per-arm ones)
Some checks are pending
CI / Conventional Commit title (pull_request) Blocked by required conditions
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Blocked by required conditions
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Blocked by required conditions
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Blocked by required conditions
CI / Workflow lint (actionlint) (pull_request) Blocked by required conditions
Third-party parts / As their notes say (pull_request) Blocked by required conditions
Third-party parts / The same as upstream (pull_request) Blocked by required conditions
PR description / Description follows the template (pull_request_target) Successful in 3s
d958b46a28
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Author
Contributor

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.

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.
Author
Contributor

run_all on d958b46a (main 514591a3 merged in): 496 passed, 4 failed, all four failing on main 514591a3 too (test_menu_i18n checked on a clean main checkout); the renderer DLL builds.

run_all on d958b46a (main 514591a3 merged in): 496 passed, 4 failed, all four failing on main 514591a3 too (test_menu_i18n checked on a clean main checkout); the renderer DLL builds.
klimovich008 changed title from 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) 2026-11-13 20:21:00 +00:00
Some checks are pending
CI / Conventional Commit title (pull_request) Blocked by required conditions
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Blocked by required conditions
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Blocked by required conditions
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Blocked by required conditions
CI / Workflow lint (actionlint) (pull_request) Blocked by required conditions
Third-party parts / As their notes say (pull_request) Blocked by required conditions
Third-party parts / The same as upstream (pull_request) Blocked by required conditions
PR description / Description follows the template (pull_request_target) Successful in 3s
This pull request doesn't have enough approvals yet. 0 of 1 approvals granted.
Some workflows are waiting to be reviewed.
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u 'perf/recorder-own-idle:klimovich008-perf/recorder-own-idle'
git switch 'klimovich008-perf/recorder-own-idle'
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
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/Longshot!162
No description provided.