perf(vk): the next frame's CPU work overlaps the GPU's end of the current frame (presenter thread, fixed and on by default on Linux) #2622

Open
hckrman101 wants to merge 11 commits from hckrman101/perf/present-overlap into main
hckrman101 commented 2026-11-12 02:51:45 +00:00 (Migrated from github.com)

Parked. Not for testing or merging. The gain turned out small and step 3 is not safe: see "Why it is parked". The branch stays at e59c8c80; the work continues as a different design (the game running a frame ahead with readback results delivered in order), not as fixes to this PR.

The game's frame is strictly serial today: the next frame's CPU work does not start until the GPU has finished the current one. This PR lets the two overlap.

Cause

In Hyrule Field with #2602 (RTX 3090, Ryzen 9 7900X, Linux, 2560x1440, uncapped) a frame is 12.6 ms:

stretch ms
the game's Presentation thread replays the frame's submission 7.0
the same thread's present call, until it returns 4.2-4.5
the game's main thread, then the hand-off back 1.0

The 4 ms is vkQueuePresentKHR. NVIDIA's driver on Wayland waits there on the CPU until the frame's copy into the window is done on the GPU, which is after all of the frame's GPU work. The game's main thread waits for that present to return before it builds the next frame, so the GPU's tail of one frame and the CPU's start of the next never overlap. It is not V-Sync: the swapchain is mailbox, and the wait does not follow the refresh.

What is in it

  1. The presenter thread (from #2325, turned off by #2544 after a review found races; those findings are fixed here). The game's present is handed to a thread of its own, which waits for the copy's fence and presents on a queue no other thread uses. On by default on Linux; on Windows only with RX_VK_PRESENT_THREAD=1.
  2. Late pages of a capture wait only for the part that writes them back (uses #2602's page-to-part table), and with frame generation the copy's fence is waited without the present lock held (NVIDIA on Linux). These two are also in #2633; whichever PR merges second gets a merge from main.
  3. Step 3: the replay's submits are held instead of the capture waiting. One exposure-meter page a frame is written by the previous frame's last part, and the game's main thread waited about 1.5 ms for it each frame (sometimes 11-55 ms). Now the next frame is recorded at once and its submits are held; when the previous frame's part is done the page is copied and the held submits go out in order. RX_VK_CAPTURE_HOLD=0 turns it off. It has no platform check, so it is on everywhere, and it has only run on Linux with NVIDIA.

Measured

Hyrule Field, RTX 3090, Linux/Wayland, 2560x1440, uncapped, Reflex and frame generation off, 15 s runs against #2602's build:

Build Median frame Mean fps Frames over 20 ms Frames over 33 ms
#2602 11.64 ms 79.1 8 7
This PR at e59c8c80 (paired run) 11.17 ms 84.5 7 7
This PR at e59c8c80 (a second run) 10.43 ms 92.6 2 1
This PR at e59c8c80 with RX_VK_PRESENT_THREAD=0 11.93 ms 80.6 3 2
  • About 4 % on the median in the paired run; the two runs of this PR differ by more than that, so the gain is small and not pinned down.
  • The PC was short of memory during all of these runs, and frames over 20 and 33 ms swing with that on both builds; they show no difference either way.
  • A tester on an RTX 4090 saw no fps change from the presenter thread alone (93-94 fps on and off).

Why it is parked

  1. The gain is small, and only where the present blocks. The presenter thread takes the present's wait off the game's thread, but the wait for the GPU moves to the capture of the next frame: one exposure-meter page a frame is written back by the previous frame's last part, and the main thread waits for it. What is left is about 4 % on the median. With RX_VK_PRESENT_THREAD=0 on the same build the median is 11.93 ms against 11.64 ms for #2602, so there is no gain without a present that blocks on the game's thread (NVIDIA's driver on Wayland here). A tester on an RTX 4090 whose present does not block saw 93-94 fps on and off.
  2. The presenter thread alone adds stalls. The capture's wait then happens every frame and sometimes runs 16-250 ms: 6-8 such stalls in a 30 s run against 0-3 with #2602.
  3. Step 3 does not remove the wait. 470-530 of every 600 holds are ended about 0.15 ms after they start by a flush that then waits 0.9-1.7 ms for the GPU. The waiter is not identified; the likely one is a readback of the previous frame's results, whose wait flushed held work it did not need (a local, unpushed commit changes that; it has not been run).
  4. Step 3's first fix flashed. The review found that a held page rewritten by the game before its delayed copy was not detected. The fix added detection and, as the review suggested, copied only the bytes the page's write-back covers. That copy loses the lighting of the ground for one frame, about one frame in 100:
Variant (flash-only runs, Hyrule Field) Runs Flash events / frames
step 3 + byte-range copy (local 54d1ab82) 5 24 / 2632
step 3 + whole-page copy (as e59c8c80) 2 0 / 728
step 3 off (RX_VK_CAPTURE_HOLD=0: presenter + part wait) 1 0 / 382
#2602 2 0 / 673
  • Why the byte-range copy flashes is not known. It has been taken out again locally.
  • 728 frames do not show that the whole-page hold is clean: one of the five byte-range runs had no event either.
  • A held page rewritten before its copy is now detected (and the hold turned off for the run), not prevented. Preventing it needs the capture not to depend on a later copy at all.
  1. CPU reads of a held page and the 250 ms wait under the present lock (the review's other findings) are fixed locally (54d1ab82), unpushed.

Never tested

  • DLSS, frame generation and Reflex on; Kokiri Forest or any other scene with step 3; Windows; AMD and Intel GPUs; X11; V-Sync On; a window resize or monitor change while playing.
  • The whole suite on the last commits (the PR's own tests pass).

Off switches

  • RX_VK_PRESENT_THREAD=0: the present back on the game's thread. The log says which: vk: presenter thread: ....
  • RX_VK_CAPTURE_HOLD=0: the capture waits for its late page as before.

Tests

  • tests/test_present_thread.py: the held submits are sent and the presenter idle before the acquire and before the fence's reset; the drain has no limit; no present is skipped; only the game's own present is handed over, never while Reflex numbers the presents; one vkDeviceWaitIdle, under every queue's lock; the lost-surface reset; the default per platform.
  • tests/presenter_queue_test.cpp: the queue choice on NVIDIA's and RADV's layouts.
  • tests/test_fps_stats.py and tests/test_present_semaphores.py updated for the changed functions.
  • A test of the held submits against a fake queue (23 checks): the order, the page copy before any held work, the timeout and the cap.
**Parked. Not for testing or merging.** The gain turned out small and step 3 is not safe: see "Why it is parked". The branch stays at `e59c8c80`; the work continues as a different design (the game running a frame ahead with readback results delivered in order), not as fixes to this PR. The game's frame is strictly serial today: the next frame's CPU work does not start until the GPU has finished the current one. This PR lets the two overlap. ## Cause In Hyrule Field with #2602 (RTX 3090, Ryzen 9 7900X, Linux, 2560x1440, uncapped) a frame is 12.6 ms: | stretch | ms | |---|---| | the game's Presentation thread replays the frame's submission | 7.0 | | the same thread's present call, until it returns | 4.2-4.5 | | the game's main thread, then the hand-off back | 1.0 | The 4 ms is `vkQueuePresentKHR`. NVIDIA's driver on Wayland waits there on the CPU until the frame's copy into the window is done on the GPU, which is after all of the frame's GPU work. The game's main thread waits for that present to return before it builds the next frame, so the GPU's tail of one frame and the CPU's start of the next never overlap. It is not V-Sync: the swapchain is mailbox, and the wait does not follow the refresh. ## What is in it 1. **The presenter thread** (from #2325, turned off by #2544 after a review found races; those findings are fixed here). The game's present is handed to a thread of its own, which waits for the copy's fence and presents on a queue no other thread uses. On by default on Linux; on Windows only with `RX_VK_PRESENT_THREAD=1`. 2. **Late pages of a capture wait only for the part that writes them back** (uses #2602's page-to-part table), and with frame generation **the copy's fence is waited without the present lock held** (NVIDIA on Linux). These two are also in #2633; whichever PR merges second gets a merge from main. 3. **Step 3: the replay's submits are held instead of the capture waiting.** One exposure-meter page a frame is written by the previous frame's last part, and the game's main thread waited about 1.5 ms for it each frame (sometimes 11-55 ms). Now the next frame is recorded at once and its submits are held; when the previous frame's part is done the page is copied and the held submits go out in order. `RX_VK_CAPTURE_HOLD=0` turns it off. It has no platform check, so it is on everywhere, and it has only run on Linux with NVIDIA. ## Measured Hyrule Field, RTX 3090, Linux/Wayland, 2560x1440, uncapped, Reflex and frame generation off, 15 s runs against #2602's build: | Build | Median frame | Mean fps | Frames over 20 ms | Frames over 33 ms | |---|---|---|---|---| | #2602 | 11.64 ms | 79.1 | 8 | 7 | | This PR at `e59c8c80` (paired run) | 11.17 ms | 84.5 | 7 | 7 | | This PR at `e59c8c80` (a second run) | 10.43 ms | 92.6 | 2 | 1 | | This PR at `e59c8c80` with `RX_VK_PRESENT_THREAD=0` | 11.93 ms | 80.6 | 3 | 2 | - About 4 % on the median in the paired run; the two runs of this PR differ by more than that, so the gain is small and not pinned down. - The PC was short of memory during all of these runs, and frames over 20 and 33 ms swing with that on both builds; they show no difference either way. - A tester on an RTX 4090 saw no fps change from the presenter thread alone (93-94 fps on and off). ## Why it is parked 1. **The gain is small, and only where the present blocks.** The presenter thread takes the present's wait off the game's thread, but the wait for the GPU moves to the capture of the next frame: one exposure-meter page a frame is written back by the previous frame's last part, and the main thread waits for it. What is left is about 4 % on the median. With `RX_VK_PRESENT_THREAD=0` on the same build the median is 11.93 ms against 11.64 ms for #2602, so there is no gain without a present that blocks on the game's thread (NVIDIA's driver on Wayland here). A tester on an RTX 4090 whose present does not block saw 93-94 fps on and off. 2. **The presenter thread alone adds stalls.** The capture's wait then happens every frame and sometimes runs 16-250 ms: 6-8 such stalls in a 30 s run against 0-3 with #2602. 3. **Step 3 does not remove the wait.** 470-530 of every 600 holds are ended about 0.15 ms after they start by a flush that then waits 0.9-1.7 ms for the GPU. The waiter is not identified; the likely one is a readback of the previous frame's results, whose wait flushed held work it did not need (a local, unpushed commit changes that; it has not been run). 4. **Step 3's first fix flashed.** The review found that a held page rewritten by the game before its delayed copy was not detected. The fix added detection and, as the review suggested, copied only the bytes the page's write-back covers. That copy loses the lighting of the ground for one frame, about one frame in 100: | Variant (flash-only runs, Hyrule Field) | Runs | Flash events / frames | |---|---|---| | step 3 + byte-range copy (local `54d1ab82`) | 5 | 24 / 2632 | | step 3 + whole-page copy (as `e59c8c80`) | 2 | 0 / 728 | | step 3 off (`RX_VK_CAPTURE_HOLD=0`: presenter + part wait) | 1 | 0 / 382 | | #2602 | 2 | 0 / 673 | - Why the byte-range copy flashes is not known. It has been taken out again locally. - 728 frames do not show that the whole-page hold is clean: one of the five byte-range runs had no event either. - A held page rewritten before its copy is now detected (and the hold turned off for the run), not prevented. Preventing it needs the capture not to depend on a later copy at all. 5. **CPU reads of a held page and the 250 ms wait under the present lock** (the review's other findings) are fixed locally (`54d1ab82`), unpushed. ## Never tested - DLSS, frame generation and Reflex on; Kokiri Forest or any other scene with step 3; Windows; AMD and Intel GPUs; X11; V-Sync On; a window resize or monitor change while playing. - The whole suite on the last commits (the PR's own tests pass). ## Off switches - `RX_VK_PRESENT_THREAD=0`: the present back on the game's thread. The log says which: `vk: presenter thread: ...`. - `RX_VK_CAPTURE_HOLD=0`: the capture waits for its late page as before. ## Tests - `tests/test_present_thread.py`: the held submits are sent and the presenter idle before the acquire and before the fence's reset; the drain has no limit; no present is skipped; only the game's own present is handed over, never while Reflex numbers the presents; one `vkDeviceWaitIdle`, under every queue's lock; the lost-surface reset; the default per platform. - `tests/presenter_queue_test.cpp`: the queue choice on NVIDIA's and RADV's layouts. - `tests/test_fps_stats.py` and `tests/test_present_semaphores.py` updated for the changed functions. - A test of the held submits against a fake queue (23 checks): the order, the page copy before any held work, the timeout and the cap.
hckrman101 commented 2026-11-12 03:12:05 +00:00 (Migrated from github.com)

First measurements of 8dceed53 (the presenter thread on its own, before a9a2e746/5ae50a53). RTX 3090, Ryzen 9 7900X, Linux/Wayland, 2560x1440, uncapped, default options (Reflex and frame generation off), Hyrule Field, 25 s each, RX_VK_PRESENT_THREAD off vs on in the same build.

run fps (mean) median frame p99 CPU a frame
round 1, off invalid (never reached the field)
round 1, on 85.0 10.83 ms 16.5 ms 45.6 ms
round 2, off 81.9 11.51 ms 16.2 ms 47.2 ms
round 2, on 60.3 11.05 ms 194 ms 51.7 ms
  • Gain: about 4-6 % on the median frame (11.5 → 10.8-11.1 ms), well short of the "could approach 8 ms" in the description, which I'll correct. The presenter log confirms the present's 4 ms left the game's thread (the copy fence waits 3.5-4.3 ms there, the present 0.2-0.3 ms).
  • Why it's small: the wait moved. The capture's wait for late pages went from 0 of 600 takes to 88-96 of 600 (0.8 ms each). The replay's memory sync went from 0.3 to 1.1-1.25 ms a frame, and the game's main thread now waits about 1 ms a frame there, for the previous frame's whole submission.
  • Hitches: open. Round 2's "on" run has a p99 of 194 ms. Every run in that session hitched, though, the off run too (6 frames over 33 ms, up to 977 ms). The stalls came in clusters in both builds, while this PC was short of memory (24 of 30 GB, 14 GB in swap). So it's consistent with swapping but not proven. Four more pairs (field and Kokiri, counting frames over 16.7/20/33 ms) are running.

Next on this PR:

  1. The capture's late pages wait only for the part that writes them back (#2602's page-to-part table), not the submission's last part.
  2. With frame generation, the copy's fence is waited with dev.present_mu released (behind a switch, measured). With Reflex/frame generation on, this step does nothing yet; that case should gain the most.

Testers: Linux with Reflex and frame generation off for now. Compare against main in the same spot, or toggle RX_VK_PRESENT_THREAD=0. The most useful report is about stutter (frames over 20-33 ms), and also resizing, alt-tab, the fullscreen toggle and quitting.

First measurements of `8dceed53` (the presenter thread on its own, before `a9a2e746`/`5ae50a53`). RTX 3090, Ryzen 9 7900X, Linux/Wayland, 2560x1440, uncapped, default options (Reflex and frame generation off), Hyrule Field, 25 s each, `RX_VK_PRESENT_THREAD` off vs on in the same build. | run | fps (mean) | median frame | p99 | CPU a frame | |---|---|---|---|---| | round 1, off | invalid (never reached the field) | | | | | round 1, on | 85.0 | 10.83 ms | 16.5 ms | 45.6 ms | | round 2, off | 81.9 | 11.51 ms | 16.2 ms | 47.2 ms | | round 2, on | 60.3 | 11.05 ms | 194 ms | 51.7 ms | - **Gain:** about 4-6 % on the median frame (11.5 → 10.8-11.1 ms), well short of the "could approach 8 ms" in the description, which I'll correct. The presenter log confirms the present's 4 ms left the game's thread (the copy fence waits 3.5-4.3 ms there, the present 0.2-0.3 ms). - **Why it's small: the wait moved.** The capture's wait for late pages went from 0 of 600 takes to 88-96 of 600 (0.8 ms each). The replay's memory sync went from 0.3 to 1.1-1.25 ms a frame, and the game's main thread now waits about 1 ms a frame there, for the previous frame's whole submission. - **Hitches: open.** Round 2's "on" run has a p99 of 194 ms. Every run in that session hitched, though, the off run too (6 frames over 33 ms, up to 977 ms). The stalls came in clusters in both builds, while this PC was short of memory (24 of 30 GB, 14 GB in swap). So it's consistent with swapping but not proven. Four more pairs (field and Kokiri, counting frames over 16.7/20/33 ms) are running. **Next on this PR:** 1. The capture's late pages wait only for the part that writes them back (#2602's page-to-part table), not the submission's last part. 2. With frame generation, the copy's fence is waited with `dev.present_mu` released (behind a switch, measured). With Reflex/frame generation on, this step does nothing yet; that case should gain the most. **Testers:** Linux with Reflex and frame generation off for now. Compare against main in the same spot, or toggle `RX_VK_PRESENT_THREAD=0`. The most useful report is about stutter (frames over 20-33 ms), and also resizing, alt-tab, the fullscreen toggle and quitting.
hckrman101 commented 2026-11-12 03:17:28 +00:00 (Migrated from github.com)

Testers: please hold off on this build for now. As pushed (5ae50a53), the presenter thread adds hitches with default options. It's not only swap noise.

Hyrule Field, 5 runs with it on against 3 off, same session:

  • "slow submit" lines on the game's main thread (capture waits of 16-208 ms): 6-8 per run with it on, against 0, 1 and 3 with it off.
  • Frames over 33 ms in the measured span: 10, 19, 5 and 12 with it on, against 6, 3 and 3 with it off.
  • Median frame: 10.8-11.2 ms on, 11.5-11.7 ms off, so about 4 % faster but with more hitches.

Cause: once the game runs a frame ahead, a capture finds pages still to be written back (the exposure meter's) and waits for that submission's value. That value is reached only with the submission's last part. When the submission is stopped at an event, the presentation thread has to finish it, while the main thread is the one waiting, so the wait runs tens of ms, up to the 250 ms limit. Before this PR the present had already waited for the whole frame, so this never happened (0 of 600 takes). The same wait already happens on main with frame generation on, where the present is already off the game's thread, so it is probably part of the stutter frame-generation users see today.

Fix, committed locally, not yet run in the game: a capture's late pages wait for the part that writes them back (#2602's page-to-part table), not the submission's last part. Only pages written in place keep the old wait. RX_VK_CAPTURE_PART=0 restores the old wait. Timing pairs are running; I'll push it once they're clean.

**Testers: please hold off on this build for now.** As pushed (`5ae50a53`), the presenter thread adds hitches with default options. It's not only swap noise. Hyrule Field, 5 runs with it on against 3 off, same session: - **"slow submit" lines on the game's main thread** (capture waits of 16-208 ms): 6-8 per run with it on, against 0, 1 and 3 with it off. - **Frames over 33 ms in the measured span:** 10, 19, 5 and 12 with it on, against 6, 3 and 3 with it off. - **Median frame:** 10.8-11.2 ms on, 11.5-11.7 ms off, so about 4 % faster but with more hitches. **Cause:** once the game runs a frame ahead, a capture finds pages still to be written back (the exposure meter's) and waits for that submission's value. That value is reached only with the submission's last part. When the submission is stopped at an event, the presentation thread has to finish it, while the main thread is the one waiting, so the wait runs tens of ms, up to the 250 ms limit. Before this PR the present had already waited for the whole frame, so this never happened (0 of 600 takes). The same wait already happens on main with frame generation on, where the present is already off the game's thread, so it is probably part of the stutter frame-generation users see today. **Fix, committed locally, not yet run in the game:** a capture's late pages wait for the part that writes them back (#2602's page-to-part table), not the submission's last part. Only pages written in place keep the old wait. `RX_VK_CAPTURE_PART=0` restores the old wait. Timing pairs are running; I'll push it once they're clean.
mooki000 commented 2026-11-12 03:39:49 +00:00 (Migrated from github.com)

A second machine for the hitch question on 5ae50a53, before your fix, with memory to spare. Linux 7.2.15-1-omarchy, Hyprland 0.56.2 on Wayland, RTX 4090 (NVIDIA 615.78.08), i9-13900K, one 1920x1080 monitor at 60 Hz (scale 1.25). A real window at the monitor's size, not fullscreen; the log says swapchain 1920x1080, 4 images, mailbox (V-Sync Auto), so mailbox as you assume. Hyrule Field from a save, with scripted camera turns, HOS_FPS_MAX=240, the game's own upscaler, Reflex and frame generation off.

One hold with nothing else on the GPU and no builds running, the same build four times in turn. Memory available 66.9 GB before and 75.7 GB after; pages swapped in 5632 and out 0 during the hold.

Runs of 105 s; the numbers are from the window that starts 15 s after the save has loaded (55 s, judged in two halves: a half counts when everything else on the PC used under 3 cores on average over it and no 5 s sample of that was over 6). Frame times are the swapchain's present intervals (RX_VK_FRAME_LOG, written when each present call returned, on whichever thread made it); the 1 % low is 1000 over the mean of the slowest 1 % of them.

run presenter thread frames fps median p99 1 % low over 16.7 / 20 / 25 / 33 ms longest frame vk: slow lines in the window (longest) of them the main thread's, memory sync (longest) late pages: takes of 600, ms a take presenter's line: copy's fence / present CPU a frame counts
X1/1 on (default) 5138 93.4 10.48 ms 14.92 ms 60.9 12 / 1 / 1 / 0 32.7 ms 1 (21.5 ms) 0 (- ms) 68, 0.04 ms 1.72 / 0.10 ms 49.2 ms yes
X1/2 off (RX_VK_PRESENT_THREAD=0) 5169 94.0 10.37 ms 15.57 ms 58.7 27 / 2 / 0 / 0 20.7 ms 0 (- ms) 0 (- ms) 69, 0.03 ms none (off) 48.9 ms yes
X1/3 on (default) 5184 94.3 10.37 ms 14.71 ms 62.5 11 / 1 / 0 / 0 20.5 ms 0 (- ms) 0 (- ms) 70, 0.04 ms 1.68 / 0.10 ms 49.2 ms yes
X1/4 off (RX_VK_PRESENT_THREAD=0) 5142 93.5 10.45 ms 15.52 ms 59.1 20 / 2 / 0 / 0 22.6 ms 0 (- ms) 0 (- ms) 61, 0.03 ms none (off) 49.2 ms yes

Thread on against off, from the parts that count: frames over 33 ms 0 and 0 against 0 and 0; over 20 ms 1 and 1 against 2 and 2; longest frame 32.7 and 20.5 ms against 20.7 and 22.6 ms; median 10.48 and 10.37 ms against 10.37 and 10.45 ms; fps 93.4 and 94.3 against 94.0 and 93.5; main-thread memory-sync slow submit lines 0 and 0 against 0 and 0.

So the hitches do not show on this PC: no frame over 33 ms in any of the four windows (110 s and about 10,300 frames a side). The main thread's memory-sync slow submit lines are in every run, on and off alike, but only at the save's load (two or three a run, the longest 122 to 148 ms, within about 240 presents of the load) and none in play. The capture's late-page waits do not grow with the thread on either. The gain does not show here either: the median is the same, and only the tail is a little tighter with the thread on (p99 14.7 and 14.9 ms against 15.5 and 15.6 ms, about half as many frames over 16.7 ms). My guess at why both are missing: the GPU is not the limit in a 1920x1080 window on this card (the presenter's line has the copy's fence at 1.7 ms where yours waits 3.5 to 4.3 ms), so the game is rarely a frame ahead of it. The windows are 55 s each, so this says the hitches are not general; it does not say they cannot happen here.

Quitting, as far as a script reaches: each run ends with the window being closed by SIGTERM at its time limit. 4 of 4 runs (2 with the thread on) ended 2.9 to 3.2 s after the signal with display: window closed, exiting as the log's last line; no driver line in the kernel log (0).

No line in any of the logs that names a swapchain error, a lost surface, an out-of-date or suboptimal swapchain, or a wait that gave up.

The build is the exact commit plus one local counting patch (it logs each buffer readback). The vk: slow lines are the renderer's own (over 15 ms, at most 80 a run), placed in the window by the 120-presents lines around them. I did not use the log's per-frame breakdown.

Not tested: resizing, alt-tab and the fullscreen toggle (the test window takes no input and its fullscreen requests are refused), V-Sync On, Reflex or frame generation on, Kokiri Forest and the 60 cap on this head (waiting for the new head), other GPUs, X11.

A second machine for the hitch question on `5ae50a53`, before your fix, with memory to spare. Linux 7.2.15-1-omarchy, Hyprland 0.56.2 on Wayland, RTX 4090 (NVIDIA 615.78.08), i9-13900K, one 1920x1080 monitor at 60 Hz (scale 1.25). A real window at the monitor's size, not fullscreen; the log says `swapchain 1920x1080, 4 images, mailbox (V-Sync Auto)`, so mailbox as you assume. Hyrule Field from a save, with scripted camera turns, `HOS_FPS_MAX=240`, the game's own upscaler, Reflex and frame generation off. One hold with nothing else on the GPU and no builds running, the same build four times in turn. Memory available 66.9 GB before and 75.7 GB after; pages swapped in 5632 and out 0 during the hold. Runs of 105 s; the numbers are from the window that starts 15 s after the save has loaded (55 s, judged in two halves: a half counts when everything else on the PC used under 3 cores on average over it and no 5 s sample of that was over 6). Frame times are the swapchain's present intervals (`RX_VK_FRAME_LOG`, written when each present call returned, on whichever thread made it); the 1 % low is 1000 over the mean of the slowest 1 % of them. | run | presenter thread | frames | fps | median | p99 | 1 % low | over 16.7 / 20 / 25 / 33 ms | longest frame | `vk: slow` lines in the window (longest) | of them the main thread's, memory sync (longest) | late pages: takes of 600, ms a take | presenter's line: copy's fence / present | CPU a frame | counts | |---|---|---|---|---|---|---|---|---|---|---|---|---|---|---| | X1/1 | on (default) | 5138 | 93.4 | 10.48 ms | 14.92 ms | 60.9 | 12 / 1 / 1 / 0 | 32.7 ms | 1 (21.5 ms) | 0 (- ms) | 68, 0.04 ms | 1.72 / 0.10 ms | 49.2 ms | yes | | X1/2 | off (`RX_VK_PRESENT_THREAD=0`) | 5169 | 94.0 | 10.37 ms | 15.57 ms | 58.7 | 27 / 2 / 0 / 0 | 20.7 ms | 0 (- ms) | 0 (- ms) | 69, 0.03 ms | none (off) | 48.9 ms | yes | | X1/3 | on (default) | 5184 | 94.3 | 10.37 ms | 14.71 ms | 62.5 | 11 / 1 / 0 / 0 | 20.5 ms | 0 (- ms) | 0 (- ms) | 70, 0.04 ms | 1.68 / 0.10 ms | 49.2 ms | yes | | X1/4 | off (`RX_VK_PRESENT_THREAD=0`) | 5142 | 93.5 | 10.45 ms | 15.52 ms | 59.1 | 20 / 2 / 0 / 0 | 22.6 ms | 0 (- ms) | 0 (- ms) | 61, 0.03 ms | none (off) | 49.2 ms | yes | Thread on against off, from the parts that count: frames over 33 ms 0 and 0 against 0 and 0; over 20 ms 1 and 1 against 2 and 2; longest frame 32.7 and 20.5 ms against 20.7 and 22.6 ms; median 10.48 and 10.37 ms against 10.37 and 10.45 ms; fps 93.4 and 94.3 against 94.0 and 93.5; main-thread memory-sync `slow submit` lines 0 and 0 against 0 and 0. So the hitches do not show on this PC: no frame over 33 ms in any of the four windows (110 s and about 10,300 frames a side). The main thread's memory-sync `slow submit` lines are in every run, on and off alike, but only at the save's load (two or three a run, the longest 122 to 148 ms, within about 240 presents of the load) and none in play. The capture's late-page waits do not grow with the thread on either. The gain does not show here either: the median is the same, and only the tail is a little tighter with the thread on (p99 14.7 and 14.9 ms against 15.5 and 15.6 ms, about half as many frames over 16.7 ms). My guess at why both are missing: the GPU is not the limit in a 1920x1080 window on this card (the presenter's line has the copy's fence at 1.7 ms where yours waits 3.5 to 4.3 ms), so the game is rarely a frame ahead of it. The windows are 55 s each, so this says the hitches are not general; it does not say they cannot happen here. Quitting, as far as a script reaches: each run ends with the window being closed by SIGTERM at its time limit. 4 of 4 runs (2 with the thread on) ended 2.9 to 3.2 s after the signal with `display: window closed, exiting` as the log's last line; no driver line in the kernel log (0). No line in any of the logs that names a swapchain error, a lost surface, an out-of-date or suboptimal swapchain, or a wait that gave up. The build is the exact commit plus one local counting patch (it logs each buffer readback). The `vk: slow` lines are the renderer's own (over 15 ms, at most 80 a run), placed in the window by the 120-presents lines around them. I did not use the log's per-frame breakdown. Not tested: resizing, alt-tab and the fullscreen toggle (the test window takes no input and its fullscreen requests are refused), V-Sync On, Reflex or frame generation on, Kokiri Forest and the 60 cap on this head (waiting for the new head), other GPUs, X11.
github-sync changed title from WIP: perf(vk): the next frame's CPU work overlaps the GPU's end of the current frame (presenter thread, fixed and on by default on Linux) to perf(vk): the next frame's CPU work overlaps the GPU's end of the current frame (presenter thread, fixed and on by default on Linux) 2026-11-12 06:23:32 +00:00
Owner

@PedroFlaminio commented on GitHub · 2026-11-12 15:49 UTC · original

Tested in the game on Linux + NVIDIA: the hold works as described and the capture's wait is gone, but the frame rate did not change here, because this machine is GPU-limited in this scene, so the wait moves to the hold's flush.

Hardware: RTX 3060 12 GB, NVIDIA 615.71.09, Manjaro (kernel 6.12), Core Ultra 7 265KF, Wayland. Built on main 51da0464 with this PR merged onto it locally (the merge conflicted in vk_memory.cpp and vk_replay.cpp, mostly additions on both sides; resolved by keeping both, a test build only, not pushed), only hookshot_vk.so built (RX_MARCH=x86-64-v3 RX_RELEASE=1) and swapped into the same game folder; main's built the same way as the baseline.
Settings: picture 2576x1080 (Aspect ratio: monitor's), Render resolution 100%, frame interpolation 2x, Reflex on, V-Sync off, no cap. Scene: Hyrule Field from the same save, standing, no input; only the 5 s windows at 3,370-3,730 draws counted.

renderer fps median (min-max) replay submit memory sync capture: GPU wait for late pages
main (two runs) 42.7 (41.4-43.4) 21.4 ms 8.9-9.4 ms 1.43-1.49 ms a take, 100 of 600 takes (~9 ms a frame)
this PR 43.0 (42.0-43.7) 12.7 ms 0.36 ms 0 of 600 takes
  • The log line for the holds: held submits for a capture's late page: 600 holds, 4.75 ms each from the capture to the page's copy; 596 ended in a flush that waited for the GPU, 4.26 ms each.
  • A later run with RX_VK_PASS_TIMING=1 on main in the same spot measured about 20 ms of GPU time a frame (GPU 84-90 % busy in nvidia-smi), so on this card the GPU, not the replay, sets the pace; the replay's 9 ms saved shows up as the flush waiting instead.
  • GPU-bound wait in the vk: N presents line read 0.0 ms in every run, also on main where the capture waited ~9 ms a frame for the GPU, so that counter does not see this wait.
  • No device lost, no visual problem seen while playing.
> [@PedroFlaminio](https://github.com/PedroFlaminio) commented on GitHub · 2026-11-12 15:49 UTC · [original](https://github.com/NotRexed/Hookshot/pull/2622#issuecomment-6828403620) Tested in the game on Linux + NVIDIA: the hold works as described and the capture's wait is gone, but the frame rate did not change here, because this machine is GPU-limited in this scene, so the wait moves to the hold's flush. Hardware: RTX 3060 12 GB, NVIDIA 615.71.09, Manjaro (kernel 6.12), Core Ultra 7 265KF, Wayland. Built on main `51da0464` with this PR merged onto it locally (the merge conflicted in `vk_memory.cpp` and `vk_replay.cpp`, mostly additions on both sides; resolved by keeping both, a test build only, not pushed), only `hookshot_vk.so` built (`RX_MARCH=x86-64-v3 RX_RELEASE=1`) and swapped into the same game folder; main's built the same way as the baseline. Settings: picture 2576x1080 (Aspect ratio: monitor's), Render resolution 100%, frame interpolation 2x, Reflex on, V-Sync off, no cap. Scene: Hyrule Field from the same save, standing, no input; only the 5 s windows at 3,370-3,730 draws counted. | renderer | fps median (min-max) | replay submit | memory sync | capture: GPU wait for late pages | |---|---|---|---|---| | main (two runs) | 42.7 (41.4-43.4) | 21.4 ms | 8.9-9.4 ms | 1.43-1.49 ms a take, 100 of 600 takes (~9 ms a frame) | | this PR | 43.0 (42.0-43.7) | 12.7 ms | 0.36 ms | 0 of 600 takes | - The log line for the holds: `held submits for a capture's late page: 600 holds, 4.75 ms each from the capture to the page's copy; 596 ended in a flush that waited for the GPU, 4.26 ms each`. - A later run with `RX_VK_PASS_TIMING=1` on main in the same spot measured about 20 ms of GPU time a frame (GPU 84-90 % busy in `nvidia-smi`), so on this card the GPU, not the replay, sets the pace; the replay's 9 ms saved shows up as the flush waiting instead. - `GPU-bound wait` in the `vk: N presents` line read 0.0 ms in every run, also on main where the capture waited ~9 ms a frame for the GPU, so that counter does not see this wait. - No device lost, no visual problem seen while playing. <!-- github-sync:c:6828403620 -->
Commenting is not possible because the repository is archived.
No description provided.