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
beanstalk: approved @ 1697603
beanstalk: approved @ 2c5a62e
beanstalk: approved @ 4fd82b7
beanstalk: approved @ 64d6d09
beanstalk: approved @ 6f91fae
beanstalk: approved @ 70a78f9
beanstalk: approved @ 8d361dc
beanstalk: approved @ 9d8356e
beanstalk: approved @ abf2227
beanstalk: approved @ d82f202
beanstalk: approved @ e5f1e40
beanstalk: approved @ ee8ab5f
ci: approved
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/macos
platform/steam-deck
platform/windows
preview
priority
critical
priority
high
priority
low
regression
status
blocked
status
confirmed
status
duplicate
status
invalid
status
needs-info
status
wontfix
testing wanted
ci: approved
No milestone
No project
No assignees
6 participants
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). Since rebased linearly onto main
76699aad(head5c7342c9, the merge dropped, no conflicts: the code commits areunchanged); the four runs' rows moved to
docs/tested/.run_allwas not run again on this head here.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/for each new measured run (format indocs/tested-configurations.md; no appended row or assembled view).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)Tested on NVIDIA, Windows (test request T-NSLN): both switches work as described. OWN removes every wait at
vkResetDescriptorPool. IDLE=wait spins less, but the replay waits longer. Neither switch shows a new validation error.Build: this PR's head
d958b46a(longshot v3.0.0-rc.3-34-gd958b46a), built withmake_game.py --update(llvm-mingw).Hardware: RTX 5070 Ti 16 GB, driver 621.01 (Vulkan 0x9b404000), Ryzen 7 7800X3D, 32 GB, Windows 11 Pro 26200.
Scene: Hyrule Field from a save, the right stick held fully right from the moment play starts. Frame rate unlocked (
HOS_FPS_MAX=240),HOS_FPS_LOG=1, a 1280x720 window (1920x1080 at 150 % scaling), 2048x1152 output, Quality preset Custom with Render resolution 75 %, no frame generation, audio off.Method: one warm-up leg was dropped. Then 3 rounds of the four arms, in alternating order (off, thread, own, idle, then reversed). Each leg waits 5 s and measures 30 s. fps and frame times come from PresentMon 2.6. CPU ms a frame is the process CPU time over the window divided by its frames. The recorder rows come from its 10 s stats lines in the window, and the replay rows from its "a frame's replay time" lines. The game clock read 1.00 in every leg.
RX_VK_RECORDER=threadRX_VK_RECORDER_OWN=1RX_VK_RECORDER_IDLE=waitThe fps differences are under 1 % and the runs overlap, so they are diagnostic only. The mechanism reproduces the AMD numbers:
Vulkan validation (
RX_VK_VALIDATION=1, Khronos layer from SDK 1.4.341.1, confirmed loaded in the process): one leg each of thread, thread+OWN and thread+IDLE=wait, same scene.VkShaderModuleCreateInfo-pCode-08740,RuntimeSpirv-MeshEXT-10883,RuntimeSpirv-shaderDenormFlushToZeroFloat32-06300and-06297. These are unrelated to this PR.vkCmdDrawIndexed-format-07753once: the samplerfs_tex2_a8expects UINT but is bound to a B10G11R11_UFLOAT view. That is main's path and not this PR's.Not tested: Linux, Intel, other scenes, and both switches together.
Test request T-GTM9: mixed
Hardware: RTX 3070 Ti 8 GB (it also drives the desktop), NVIDIA 595.104.02 (Vulkan driver 0x94da0080), i7-12700K, 16 GB, Pop!_OS 24.04 (kernel 7.2.15), Linux. Built
d958b46a(the PR head,longshot v3.0.0-rc.3-34-gd958b46a, source c1dc3c0b) from source with clang (make_game.py, 18 min 52 s).In short: on Linux both switches do what the PR says. OWN takes the replay's waits at
vkResetDescriptorPoolfrom about 1,250 a second to none. IDLE=wait takes the futex path (no "no WaitOnAddress" line), spins less and makes the replay wait longer. Neither switch adds a validation error. Two things stop me calling it a pass: the fps comparison came out inconclusive here, and under the Khronos layer this PC has, every recorder-thread run crashes inside the layer during the save's load screen, the plainRX_VK_RECORDER=threadarm included. So play was never validated with either switch.What I checked: Hyrule Field from a save (child Link by the stone sign), the right stick held fully right from before the load ends,
HOS_FPS_MAX=240, the game's own 2048x1152 output (16:9), headless (no window), audio off. One warm-up per arm first. Then 3 exclusive GPU holds of 4 runs each, alternated: thread/OWN/thread/OWN, OWN/thread/OWN/thread, thread/IDLE/thread/IDLE. That gives 6 thread, 4 OWN and 2 IDLE runs. Each run measures 30 s, starting 5 s after the save's load gap. fps comes from the renderer's frame log (headless, so CPU intervals after submission, not display timing), and CPU ms a frame from the process's CPU time over the window. Other processes used 0.04–0.24 cores in every window. The recorder rows are the 10 svk: recorder:lines inside the window. The options file was the one Auto (this PC) wrote on the first start, tuned once and then unchanged through all the runs (checksum checked after each hold).Recorder (each run, in the window):
vkResetDescriptorPoolgo to none in all 4 runs. The replay's total wait time for the recorder falls by about 1.5 ms a second (7.0 against 8.5). The guard line ("a recording slot reused before the recorder ran its last end") was never logged.fps: inconclusive. Runs fall into two GPU states regardless of arm. In the "fast" state GPU time is about 11 ms a frame and the longest submission 3.5 ms. In the "slow" state the draw counts and VRAM use are the same (5.3–5.5 of 6.8 GB), but GPU time is 15.5 ms and the longest submission 11–12 ms. The slow state hit 2 of 6 thread runs, 2 of 4 OWN and 0 of 2 IDLE, so the repeats of an arm differ by far more than 5 % and no pair decides. Comparing only fast-state runs: thread 49.65–49.96 fps (4 runs), OWN 49.76 and 53.66, IDLE 49.28 and 49.61. Nothing lower outside noise, but too few runs to claim anything. I didn't look further into what flips the GPU state; it shows in every arm, so it isn't this PR.
Vulkan validation (
RX_VK_VALIDATION=1, Ubuntu'svulkan-validationlayers1.3.275, its default reporting; the shader precompile skipped withRX_VK_PRECOMPILE=0because it doesn't finish under the layer), 150 s runs:VkShaderModuleCreateInfo-pCode-08740,RuntimeSpirv-shaderDenormFlushToZeroFloat32-06300vkCmdDrawIndexed-None-07627/07628/07629(5,032 each) andvkCmdDrawMeshTasksEXT-None-07627/07628/07629(133 each)CRASH: exception 0xc0000005 (read 0x1a8) at libVkLayer_khronos_validation.so+0x141e7e9, called frommkwvk::rec::Recorder::loop()). An earlier round withoutRX_VK_PRECOMPILE=0crashed the same way.vkCmdSetColorBlendEnableEXT,...EquationEXTand...WriteMaskEXTwere "not called in the current command buffer" before a draw. They appear only with the recorder thread on, and the same in every recorder arm.RX_VK_RECORDER=threadpath, not this PR's.Game-free:
tests/test_recorder_own_idle.pybuilds and passes on Linux with clang 23 (all cases, and all 7 broken variants fail as they should). That covers the fixture, not host/posix's futexWaitOnAddressitself, which the IDLE runs above use.Not tested: both switches together. Another scene. A newer validation layer on Linux. A windowed (swapchain) run. The cause of the GPU's slow state.
🤖 Generated with Claude Code
Crash test (play, not a measurement): a person played the game with the recorder thread and this PR's own slot resets,
RX_VK_RECORDER=threadandRX_VK_RECORDER_OWN=1, to see whether the GPU crashes.RX_VK_RECORDER_IDLE=waitwas not set, so this covers OWN only. (Edited: it said "both of this PR's switches" before; thanks, mooki.)Build: this PR's commits on main
839e85ff(9ac9b714, the same patch as the measured pack), renderer DLL onlyHardware: RX 9070 XT, AMD 26.10.2, Ryzen 7 5700X3D, 64 GB, Windows 11
Play: about 25 minutes in two runs (5 and 20 minutes), from a save in normal play, keyboard and mouse, the runtime's options file as it comes (the game ran at 60 fps)
Result: no crash. No device lost, no Vulkan error, no exception or abort in either log, and the game exited normally when its window was closed. It held 60 fps for most of the play; the frames over 100 ms (25 of 230 ten-second stretches) came while pipelines were still being prepared in play.
Not tested: the default (both switches off) in the same play; a controller (it gave no input in this build, also with Steam closed; this PR does not touch input, so it is not from it); the PR's current head
d958b46a(main514591a3merged in) in play.Test request T-NSLN: mixed — both mechanisms reproduce, no new validation errors; no demonstrated FPS benefit on this PC.
Hardware: RTX 4070 12 GB, NVIDIA 621.01, Core i7-13700F, 32 GB RAM, Windows 11 Home 26H2 (26300.9974).
Build: renderer-only dev pack from PR head
d958b46a28(renderer source fingerprint c1dc3c0b), on the existing main514591a30dbase. Host remains main's v2.1.0-rc.3-413-g514591a3. The pack contains only longshot_vk.dll. Renderer compile/link succeeded, 0 compiler failures. All arms use that same host and renderer; only the recorder switches differ.Scene/settings: Hyrule Field, child Link outside Lon Lon Ranch, separate copy of the same save each launch. Right stick fully right, left stick neutral. 1280x720 window (bridge-confirmed), 2048x1152 output, Custom quality, render resolution 100%, SDR, DLSS/FG/VSync off, HOS_FPS_MAX=240, real-time game speed, audio off. Camera rotation begins once the field is reached, followed by 12 s settling and a 30 s measurement.
Method: excluded first shader-preparation/warm-up run, then three rounds: thread/OWN/IDLE, IDLE/OWN/thread, thread/OWN/IDLE. Full present trace from HOS_PRESENT_TRACE, process CPU time divided by measured frames, and recorder 10 s stats inside each window. All nine measured windows passed background-load checks (mean <3 other cores, 5 s peak <=6). Validation runs are separate and excluded from these numbers. These are game-present-return intervals, not PresentMon/display scanout timings.
Per-run data (all retained, no timed run rejected):
OWN removes the logged waits at vkResetDescriptorPool in all three runs (about 2,248/second to zero). Total recorder wait time falls from 12.40 to 9.64 ms/second. The slot-reuse guard never logged. IDLE=wait reduces spin from 0.463 to 0.338 of a core, while increasing recorder wait time to 15.40 ms/second; the Windows wait functions were available (no fallback log).
FPS medians are OWN -1.43% and IDLE -0.60% relative to thread. Three short rounds, under-5% differences and overlapping ranges are diagnostic, not proof of a speedup or regression. The requested “FPS not lower” outcome is not established here, hence mixed rather than pass. CPU/frame did not improve with OWN. No frames >100 ms in any non-validation measured window. No crash/device-loss line in any run, and the inspected screenshots show the expected field and HUD.
Vulkan validation: official Khronos portable Windows nightly, manifest API 1.4.368, archive SHA-256 cfcb5fe29f248c295256366dfbf399511af7d8b5f03b14dd7c9563840d9cd39a. RX_VK_VALIDATION=1; RX_VK_PRECOMPILE=0 (the documented switch) in all three arms. The layer DLL was confirmed loaded in each game process. Each arm reached Hyrule Field, rotated the camera and completed a 30 s validation window, then exited normally.
All three arms report the same VUID set: VkShaderModuleCreateInfo-pCode-08740, RuntimeSpirv-shaderDenormFlushToZeroFloat32-06300 and RuntimeSpirv-shaderDenormPreserveFloat32-06297. No additional VUID with either switch, and no threading, command-pool-in-use or descriptor-pool-in-use error. This is a comparison against recorder-only's existing errors, not a clean validation pass.
game-free-bundled: 497 passed, 4 failed, 33 skipped in 121 s (windows, 24 at once)
main-failures: 4 passed, 4 failed, 0 skipped in 10 s (windows, 24 at once)
The four failing checks are nx2/test_nup_paths.py, tests/test_check_third_party.py, tests/test_menu_i18n.py and tests/test_release.py. All four also fail when rerun on clean main
514591a3with the same interpreter/environment. The new recorder own/idle test and all changed-source fixture tests pass. Final suite uses the project's bundled Python 3.14.7, llvm-mingw and the existing header cache, without the optional NGX build variable leaking into game-free fixtures. An earlier system-Python run had additional missing-package/environment failures and is superseded by this run.Recorder-off follow-up (same verified binaries and settings; fresh interleaved comparison, kept separate from the earlier nine runs):
The recorder is fully disabled in OFF: RX_VK_RECORDER, RX_VK_RECORDER_OWN and RX_VK_RECORDER_IDLE are all unset. THREAD sets only RX_VK_RECORDER=thread. One OFF warm-up was excluded, then three pairs in OFF/THREAD, THREAD/OFF, OFF/THREAD order. Same copied save, camera rotation, 12 s settling and 30 s measurement per run. All six windows passed the same background-load checks.
Relative to OFF, THREAD median FPS is +2.50% and CPU cost is +6.86 ms/frame. These short three-pair results are diagnostic; the prior OWN/IDLE data are not pooled with this later comparison.
Recorder-OFF Vulkan validation also reached Hyrule Field, rotated the camera for a 30 s window and exited normally. Same portable Khronos layer and RX_VK_PRECOMPILE=0 as before; the layer DLL was confirmed loaded. VUIDs: RuntimeSpirv-shaderDenormFlushToZeroFloat32-06300, RuntimeSpirv-shaderDenormPreserveFloat32-06297, VkShaderModuleCreateInfo-pCode-08740. The VUID set matches the earlier THREAD/OWN/IDLE validation runs.
Not tested: both switches together, Linux/AMD/Intel, other scenes, long play sessions or a full host rebuild of the PR. The test request was unapproved when read, so this was a direct PR test without a request claim.
Tested with Codex following the repository's testing workflow. Game files, saves, runtimes and raw traces remain local.
Read-only review of head
d958b46aagainst main514591a3. I read the patch, the recording and descriptor code around it, the tests and the whole discussion. Nothing was built or run by me. I found no correctness problem; the points below are about coverage and about fitting in with other open work.What is good
OwnSlotremoves the wait for the recorder to drain, not the Vulkan calls or who makes them. Descriptor allocation and updates stay on the replay thread (gpu/vk/vk_replay.cpp,gpu/vk/vk_draw.cpp).recording_waitcovers the slot's previous use by the GPU;slot_begincompares the recorder's consumed position with the position saved after both command buffers ended. Neither stands in for the other.gpu/vk/vk_recorder.h).RX_VK_RECORDER_IDLE=waitdid not raise the frame rate.Suggestions
tests/recorder_own_idle_test.cppthe stand-in forvkResetDescriptorPoolrecords when it ran but ignores which pool it was given, so the case cannot tell resetting this slot's pool from resetting another's. The command-pool reset, command-buffer begin and extra-pool destruction are not exercised with identities either. A case with distinct slots, buffers and pools, a delayed recorder and a delayed GPU completion, that fails when the wrong pool is reset or a slot is reused early, would pin the ownership rule the patch relies on. The source-placement checks are useful but do not establish it._Exit, so an orderly shutdown in the new idle mode is never run. Cases through the real adapter for a publish while parked, a publish just before the wait, and a stop while parked would cover it, as would both switches together (the combined-environment case exits before the recorder starts).tests/test_recorder_own_idle.pycopiesvk_work.h,vk_recorder.hand the test loader into its own include folder. #60 makesvk_work.hinclude a new header,gpu_wait_stats.h, and updates the older recorder fixture's copy list, but it cannot update this new one. As I read the include paths, whichever of the two lands second has to add that header to this fixture or it will not compile. #288 carries the same change. This is not a fault of this head against main.docs/tested-configurations.mdgains four rows at the top of the shared table, where every pull request with results collides. #45 and #96 propose freezing that table and keeping new runs in their own files. Main still asks for rows today, so this is only a heads-up to keep the results in the description too until that settles.Small
RX_VK_RECORDER=threadandRX_VK_RECORDER_OWN=1, notRX_VK_RECORDER_IDLE=wait. Worth saying which were set before it is counted as covering both.Not checked
Nothing was built or run here, the fixtures included. I traced the ownership argument by reading and found no path that binds an old descriptor set from a slot being recycled, but that argument rests on where the calls sit today:
OwnSlotitself does not check which handles it is used for, which is why suggestion 1 matters.d958b46a285c7342c997Rebased onto current main 76699aad so the PR merges again. The new head is
5c7342c9.514591a3is dropped. The five code and docs commits (038c70a9..cb5719d2) were replayed in order onto76699aad. They are still klimovich008's, and none of them conflicted. Their own +/- lines against the new base are identical to the original ones (714 lines, compared line for line).occ_collect()from main stays at the top ofbegin_recording, outside theOwnSlot.rec::slot_begin/OwnSlotstill sit at the slot's ownvkResetCommandPool/vkBeginCommandBuffer, and indraw_begin_recordingatvkResetDescriptorPool.rec_end_posis still set after both ends insubmit_recording. Main added no slot reset or begin elsewhere. The source-cutting fixtures still find their anchors. Main's own new stubs (occ_collect,Writes& wr) are there next to the PR'srecstubs. No newer check compiles the cut regions without them:test_vk_occlusion_queriesonly readsbegin_recordingas text.docs/tested/. Main merged #96, which freezesdocs/tested-configurations.md. So the three commits that added and edited rows there (0806b1af,0d934b04,d958b46a) are dropped. The four per-arm rows of the839e85ffA/B are now one file per run, with the author's data unchanged and "(iterations 0 and 2 of the A/B)" added to each Result:docs/tested/2026-11-13-recorder-off-klimovich008-run01.mddocs/tested/2026-11-13-recorder-thread-klimovich008-run02.mddocs/tested/2026-11-13-recorder-own-klimovich008-run03.mddocs/tested/2026-11-13-recorder-idle-klimovich008-run04.mdNothing was built or run here: no game, no
run_all, not even the tested-configurations check. The file format was checked by readingscripts/tested_configurations.py.dukenheimer referenced this pull request2026-11-14 02:39:20 +00:00
Sorry: at about 02:33Z I pushed review commits to this branch, replacing @HumanBean's rebase (
5c7342c9), which had already been merged here at 02:11Z. Main has exactly 5c7342c9's tree in the squash98e2d95f, so nothing on main changed. The branch is restored to5c7342c9, and the test changes below are in #342 on current main instead.Thanks, @mooki, for the careful read. Point by point (#342, tests only; the production code is unchanged):
Identities. New
own_slotscases intests/recorder_own_idle_test.cpp: two slots, each with its own command pool, commandbuffer, descriptor pool, extra pool and a set from each pool. Slot 0 is used and ended. Slot 1 is begun and its two binds are
held in the recorder. Then slot 0 comes round, its GPU work completing 20 ms late (standing in for
recording_wait), and makesits own calls: command pool reset, begin, descriptor pool reset and extra pool destruction. The fakes know which object each
held packet names, and record a fault for a reset, begin or destruction of an object a held packet still uses, or of a slot
whose GPU work has not completed. The case passes only with no fault, slot 1's packets still held when slot 0's calls return
(none waited), and no watchdog. Two self-checks show the fakes catch each fault:
own_slots_wrongresets slot 1's pool, andown_slots_earlyreuses slot 0 before its GPU work completes. SinceOwnSlotitself takes no handles, the source check nowalso pins what the fixture can't see: in
begin_recording,recording_wait(r)comes beforerec::slot_begin(), and theOwnSlotblocks call only the slot's own handles (r.pool,r.upload_pool,r.cmd,r.upload;res[slot]'s pool andextra pools, with
draw_begin_recording(r.slot, ...)andslot_begin(rec_end_pos[r.slot])). The source check reads thecode with comments left out, and has controls for
recording_waitcommented out and for another slot's descriptor pools.The real wait and shutdown. New
real_cases go throughvk_replay.cpp's own adapter: the test cuts therec_wait_hooksbody from the source, so it is kernelbase'sWaitOnAddress. The cases:allows, so only the wake can end it in time. A run counts only when the wait that returned began before the publish and
blocked 15 ms or more, so a compare at its start can't stand in for the wake; otherwise it is retried (up to 20 attempts);
ERROR_TIMEOUT, sinceWaitOnAddressmay also return early), and brings the recorder back to a later publish;vkDestroyDevice's teardown;Each
real_case ends with the recorder stopped and joined throughvkDestroyDevice, its watchdog threads joined, then anormal return through
exit(), no_Exit. Three new broken variants must fail: the wait's timeout made infinite fails the timeout case, and the adapter witha wake that does nothing, or with a wait comparing against W as it is now, fails the parked or the race case. Since the OS
wait may return early, a broken real-wait build counts as caught when any of three runs fails it; the correct code passes
every run. The
real_cases run onWindows. The Linux futex path is
host/posix's and isn't built by this test; tonesi's Linux run exercised it in the game (no"no WaitOnAddress" line, less spin). The older fake cases still leave through
_Exit, because their recorder waits on thefixture's own condition variable, which
exit()would destroy.#60 and #288. In #342 the fixture copies every header
vk_work.handvk_recorder.hinclude fromgpu/vk, sogpu_wait_stats.hcomes along whichever lands first. Said in the reviewer notes too.
Result rows. The results are in the description as well as the rows, which @HumanBean's rebase moved to
docs/tested/.The testing since, in one place now that the PR is merged. On
d958b46a, the same code as merged:core against 0.42, and the replay waited longer. fps was within 1 %. Under validation, no new error in any arm.
was inconclusive. Under the old 1.3.275 layer every recorder-thread run crashed inside the layer during the load, the plain
RX_VK_RECORDER=threadarm included, so play under validation is not established on Linux.was within 1.5 %. Same VUID set in all arms.
So OWN removes the waits it targets everywhere tested, but gained fps only on the RX 9070 XT; IDLE=wait cost fps everywhere,
and both stay off. Intel and the Steam Deck are untested.
Small: the play report covered
RX_VK_RECORDER_OWN=1only. It was corrected (thanks).Checks, in #342 on main
cf605c85: the new test passes its 22 cases and 9 broken variants three times in a row on each commit, andtests/run_all.pypasses all but five tests that fail the same on a clean main. Our own second-opinion review wentthrough eleven rounds on these tests; each found timing or lifetime gaps in them, fixed in #342, and the last
round is clean.