Skip to content

perf(vk): with batched submits (RADV), send a batch at once while the GPU is idle (#1993) - #1999

Merged
NotRexed merged 5 commits into
mainfrom
perf/batch-idle-flush
Nov 11, 2026
Merged

NotRexed merged 5 commits into
mainfrom
perf/batch-idle-flush

Conversation

@NotRexed

Copy link
Copy Markdown
Owner

Implements #1993's proposal by @mrdev023, as a default where batching is on.

With batched submits (on by default with RADV, so Linux AMD including the Steam Deck; RX_VK_BATCH=1 elsewhere), a held batch waited for the next wait, present or the 32-buffer cap. That happened even when the GPU had finished everything it was sent. So the GPU sat idle until the game waited for it, and with serial replay that's mid-frame, at take_capture's late-page wait.

Now, when a submit joins the batch and the GPU has completed every batch sent so far (batch_tl >= batch_flushed), the batch goes out at once. While the GPU is busy, submits are still grouped.

The reporter's numbers (RX 9070 XT, RADV, Kokiri Forest, FSR 4, 5120x1440):

build fps in play flashes (framelog, 5+ blocks)
main 33-41 0
idle flush 46-61 0 in 5230 frames

Flushes went from about 5.7 a frame to about 10 of 3.7 command buffers each, and the late-page wait from about 1.4 to 0.64 ms.

RX_VK_BATCH_IDLE=0 keeps the old behaviour.

Not yet checked here: I have no RADV machine. I'll A/B it on Windows with RX_VK_BATCH=1 (NVIDIA) for fps and flashes before merging. Drivers other than RADV are unchanged, since batching stays off there by default.

🤖 Generated with Claude Code

… GPU is idle (#1993)

The held batch waited for the next wait or present while the GPU had finished everything sent, so it sat idle until the
game waited for it (serial replay's mid-frame late-page wait above all). Proposed and measured by mrdev023 in #1993:
RX 9070 XT, RADV, Kokiri Forest, 33-41 fps -> 46-61, no flashes; batching still groups submits while the GPU is busy.
RX_VK_BATCH_IDLE=0 keeps the old hold.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@mrdev023

mrdev023 commented Nov 10, 2026 •

Copy link
Copy Markdown

Tested this branch as is (35a8aaa on main abd5e5f) on RX 9070 XT, RADV, Ryzen 9 9950X3D, NixOS. Kokiri Forest, FSR 4 at 5120x1440, GPU power level auto, same build both ways:

fps in play framelog events of 5+ blocks flushes a frame late-page wait a frame
RX_VK_BATCH_IDLE=0 29-38 0 / 2068 ~4.1 (14.1 cbs each) ~7.9 ms
this PR 41-57 0 / 3044 ~14.5 (4.0 cbs each) ~4.6 ms

Same gain as in #1993, no flashes.

…e tests' fixtures stub both)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@NotRexed

Copy link
Copy Markdown
Owner Author

A/B on Windows (RTX 4060 Ti, batching forced on with RX_VK_BATCH=1), smooth.ps1's route (Kokiri Forest, Hyrule Field, Kakariko), 2 rounds alternating:

run median p99 crashes
batching off (Windows' default) 59 / 59 fps 29.1 / 29.5 ms 0
batching on + idle flush (this PR) 59 / 58 fps 28.9 / 31.5 ms 0
batching on, held (RX_VK_BATCH_IDLE=0, before this PR) 47 / 46 fps 34.1 / 37.3 ms 0

With the batch held, the GPU sits idle and loses ~12 fps here, matching what #1993 measured on RADV. The idle flush brings batching back to the unbatched frame rate, and Windows' default (batching off) is unchanged.

🤖 Generated with Claude Code

NotRexed added a commit that referenced this pull request Nov 11, 2026
…ggle stub lacks switching; actionlint SC2012) (#2092)

Main has been red since #2046 and the builder track switch landed:

- **tests/test_pipeline_state_portable.py** (Windows): the fixture
compiles with \`-Werror\` and clang on Windows marks \`fopen\`
deprecated. It now passes \`-D_CRT_SECURE_NO_WARNINGS\`, as the other
fixture tests do.
- **tests/test_make_game_ngx.py** (all OSes): \`Builder.apply_mode\` now
calls \`self.switching()\`, and the test's stub had none. It gets
\`switching=lambda: False\`.
- **.github/workflows/linux-appimage.yml** (actionlint, from #2079): the
object count used `ls | wc -l` (shellcheck SC2012). It counts with a
glob array, as the loop above it already does.

Checked locally: both pass (fixture built with clang++ and Vulkan
headers 1.4.345; test_make_game_ngx 17 tests OK). Unblocks #1999 and the
other open PRs.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
NotRexed added a commit that referenced this pull request Nov 11, 2026
…c fixture getenv deprecation) (#2109)

Main is red again.
[#1866](#1866) added this to
the finish block of `make_game.main()`:

```python
if fsr_wanted(a, out) and getattr(a, "fetch_fsr_runtime", False):
```

`tests/test_finish_output.py` and `tests/test_linux_update.py`
(`test_a_packaged_build_puts_the_launcher_in_the_folder_and_the_game_in_bin`)
run that block on its own, without `a`, so both fail with `NameError:
name 'a' is not defined`.

Both tests now pass an argument namespace with FSR off (`fsr=False,
no_fsr=False, update=None, fetch_fsr_runtime=False`). The production
code is unchanged.

Checked locally: test_finish_output passes. The test_linux_update case
only runs on Linux; CI covers it. Locally on Windows that file has
unrelated symlink failures that are the same before and after this
change.

This unblocks #1999 and the other open PRs.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@NotRexed
NotRexed merged commit fab8cb3 into main Nov 11, 2026
12 checks passed
@mrdev023

Copy link
Copy Markdown

the description's late-page wait (~1.4 -> 0.64 ms) doesn't match #1993's or mrdev023's per-frame numbers (~7.9 -> ~4.6 ms)

Those came from my first write-up in #1993, and they were my misreading. The log line a capture taken (ms): ... GPU wait for late pages X (100 of 600 takes) averages over all 600 takes. There are 6 takes a frame and the wait falls in one of them, so the per-frame wait is 6 × X: ~8.4 -> ~3.8 ms in #1993, ~7.9 -> ~4.6 ms in my rerun of this branch. I've corrected both. The relative gain is unchanged.

The catch is that both runs are one machine (9070 XT + 9950X3D at 5120x1440 with FSR 4), and that machine is GPU-bound.

I don't think it is, after profiling it today (main abd5e5f + this PR, Kokiri Forest):

  • Render scale barely matters: FSR 4 Native AA -> Performance (50%) -> Ultra Performance (33%) gave 46 -> 50 fps at best. GPU time stays at ~11-14 ms a frame for a ~19-21 ms frame.
  • HOS_SAMPLE_ALL + scripts/crit.py: the main thread runs ~18% of the time. It waits ~25% in take_capture's GPU wait and ~35% in event_wait for the Presentation thread, which does the replay. The replay costs ~14-20 ms of CPU a frame (a frame's replay time: draw 6-10, descriptors 2-4, commands 2.5-4).
  • RX_VK_PASS_TIMING=1: the GPU's work is ~175 passes (~39 us each) and ~143 dispatches (~21 us each), with ~409 full barriers a frame. That looks latency-bound (many small serialized pieces), not throughput-bound, which is why the resolution doesn't change it.

So on this box the frame is the CPU replay plus the mid-frame wait, and the GPU mostly waits for work. That also explains why RX_VK_BATCH=0 was faster than the held batch: the GPU sat idle while the batch was held.

@hckrman101

addisonbair added a commit to addisonbair/Hookshot that referenced this pull request Nov 12, 2026
… by default

The idle send (NotRexed#1999) costs a Steam Deck LCD (Van Gogh, RADV) ~5 ms of queue submit a frame in Kokiri Forest on current
main (11-12 ms with it, 6.6-7.1 without; 18.6-18.8 fps -> 20.0-20.7), where each vkQueueSubmit costs ~1 ms. The rule is
limited to Van Gogh by device ID (0x163f LCD, 0x1435 OLED); every other GPU, other APUs included (780M, 890M, 8060S,
unmeasured), keeps it. RX_VK_BATCH_IDLE=1 or 0 still chooses, and the log says which.

Assisted-by: Claude Opus 5.5 (Anthropic)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants