fix(vk): let a catch-up go on only past met waits, not every stopped queue #449

Merged
azlekayn merged 1 commit from fix/catchup-stopped-queues into main 2026-11-08 08:19:57 +00:00
azlekayn commented 2026-11-08 02:46:29 +00:00 (Migrated from github.com)

What this fixes

One-frame flashes in Kokiri Forest once the game runs at speed. The scene goes black, grey or blown out for a frame, with the HUD and Navi still drawn on top. It often alternates with normal frames for several seconds. This may be what #145, #305 and #307 describe.

Why

In sync mode, wait_queue_empty() calls sync_drain(2), which forces every queue stopped at an event wait past its wait. The callers are a queue finish, an invalidate that reads back GPU-written data, and a pool's mapping changing.

The work those waits are for is mostly not submitted yet, because the thread that's waiting is the one that would submit it. As a test, holding the waiter for up to 20 ms let only 32 of about 3,600 stopped queues go on by themselves. Forced past its wait, the stopped submission's lit passes read results before they're made.

A frame probe (RX_VK_PROBE=2) of the dark frames showed:

  • the same ~120 draws into the 1536x864 scene target produced 0;
  • that frame's 512x512 compute dispatches were recorded before the passes instead of after them.

At low frame rates the other queue's work had always arrived first, so this only shows once the game runs fast. For us that was after the udmabuf change.

Change

gpu/vk/vk_replay.cpp: in sync mode, a catch-up now only schedule()s what can go on, meaning waits that are met. Stopped work still goes on, as before, with:

  • its own queue's later work;
  • a present;
  • sync_watch's hold_ms.

RX_VK_CATCHUP_FORCE=1 forces, as before.

This complements #430, which does the same for releases (drain 1).

Tested

  • Method: a frame log (RX_VK_FRAMELOG) of 70 s of Kokiri gameplay, counting frames that differ from both neighbours but match the frame two back.
  • Hardware: RX 7800 XT, RADV, Linux.
  • Results:
waits gone past forced by "waits for the replay" flashing frames
before 10,000-40,000 600-2,900 250-1,100 (4 runs)
after 264-378 0 65-113 (3 runs)
  • Frame rate: 60 fps either way.
  • Stability: no hangs, stalls or device loss. A finish() that needs stopped work waits on the timeline until sync_watch moves it, but that never showed up as lost frames.
  • Remaining flashes: they line up with the presents' drain(0), 114-362 per run. #430's readback refresh may cover what remains.
  • Builds measured: the foliage-linux-updater release with this patch.
  • Against main: rebased onto current main, where hookshot_vk builds. I have not run a full main build.
  • Not tested: Windows, NVIDIA.

Analysis and patch made with Claude (Anthropic); I ran every test on my machine.

🤖 Generated with Claude Code

## What this fixes One-frame flashes in Kokiri Forest once the game runs at speed. The scene goes black, grey or blown out for a frame, with the HUD and Navi still drawn on top. It often alternates with normal frames for several seconds. This may be what #145, #305 and #307 describe. ## Why In sync mode, `wait_queue_empty()` calls `sync_drain(2)`, which forces every queue stopped at an event wait past its wait. The callers are a queue finish, an invalidate that reads back GPU-written data, and a pool's mapping changing. The work those waits are for is mostly not submitted yet, because the thread that's waiting is the one that would submit it. As a test, holding the waiter for up to 20 ms let only 32 of about 3,600 stopped queues go on by themselves. Forced past its wait, the stopped submission's lit passes read results before they're made. A frame probe (`RX_VK_PROBE=2`) of the dark frames showed: - the same ~120 draws into the 1536x864 scene target produced 0; - that frame's 512x512 compute dispatches were recorded before the passes instead of after them. At low frame rates the other queue's work had always arrived first, so this only shows once the game runs fast. For us that was after the udmabuf change. ## Change `gpu/vk/vk_replay.cpp`: in sync mode, a catch-up now only `schedule()`s what can go on, meaning waits that are met. Stopped work still goes on, as before, with: - its own queue's later work; - a present; - `sync_watch`'s `hold_ms`. `RX_VK_CATCHUP_FORCE=1` forces, as before. This complements #430, which does the same for releases (drain 1). ## Tested - **Method:** a frame log (`RX_VK_FRAMELOG`) of 70 s of Kokiri gameplay, counting frames that differ from both neighbours but match the frame two back. - **Hardware:** RX 7800 XT, RADV, Linux. - **Results:** | | waits gone past | forced by "waits for the replay" | flashing frames | |---|---|---|---| | before | 10,000-40,000 | 600-2,900 | 250-1,100 (4 runs) | | after | 264-378 | 0 | 65-113 (3 runs) | - **Frame rate:** 60 fps either way. - **Stability:** no hangs, stalls or device loss. A `finish()` that needs stopped work waits on the timeline until `sync_watch` moves it, but that never showed up as lost frames. - **Remaining flashes:** they line up with the presents' `drain(0)`, 114-362 per run. #430's readback refresh may cover what remains. - **Builds measured:** the `foliage-linux-updater` release with this patch. - **Against `main`:** rebased onto current `main`, where `hookshot_vk` builds. I have not run a full `main` build. - **Not tested:** Windows, NVIDIA. Analysis and patch made with Claude (Anthropic); I ran every test on my machine. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
felipecsl commented 2026-11-08 03:25:02 +00:00 (Migrated from github.com)

Tested on Intel Arc B390 (Core Ultra X7 358H iGPU), Linux, Mesa 26.2.5 ANV, kernel 7.2.15, Hyprland (Wayland). This fixes the flashing here.

Setup: release no-visual-studio (e40306e) built with the Linux AppImage. I applied this PR's diff to e40306e and rebuilt only hookshot_vk.so (RX_VK_ONLY=1 RX_RELEASE=1 nx2/build.py --standalone, the AppImage's clang, headers at the versions deps.sh pins), then swapped it into the game folder. Test: Kokiri Forest save, standing still outside Link's house, 30 s RX_VK_FRAMELOG, counted with scripts/framelog.py. Default settings otherwise.

e40306e release e40306e + this PR
framelog.py events 1481 in 1020 frames 5 in 823 frames (all small, first 3 s)
Waits gone past (per 6000 stopped) ~4100 533
Made to go by "waits for the replay" hundreds 0
fps ~34-35 ~27
  • Before, the flashing alternated every other frame: steady frames at mean luma ~123, the frames between them 118-141. They were the whole scene brighter or hazier (about 128 of 144 blocks), with the HUD unchanged. This is the pattern described here.
  • With the patch, the "normal" frames also look different: the white/milky haze over Kokiri is mostly gone and the scene has proper contrast (mean luma 126 -> 75 at the same spot). Some of the white haze reported for this same chip in #17 may be this bug.
  • fps dropped from ~35 to ~27 on this iGPU, unlike the 60 fps either way on the RX 7800 XT. I did not run a control with my own build of hookshot_vk.so without the patch, so part of that difference could come from my build, not the PR.

Thanks for the fix!

Tested on **Intel Arc B390** (Core Ultra X7 358H iGPU), **Linux, Mesa 26.2.5 ANV**, kernel 7.2.15, Hyprland (Wayland). This fixes the flashing here. **Setup:** release `no-visual-studio` (`e40306e`) built with the Linux AppImage. I applied this PR's diff to `e40306e` and rebuilt only `hookshot_vk.so` (`RX_VK_ONLY=1 RX_RELEASE=1 nx2/build.py --standalone`, the AppImage's clang, headers at the versions `deps.sh` pins), then swapped it into the game folder. Test: Kokiri Forest save, standing still outside Link's house, 30 s `RX_VK_FRAMELOG`, counted with `scripts/framelog.py`. Default settings otherwise. | | `e40306e` release | `e40306e` + this PR | |---|---|---| | framelog.py events | **1481** in 1020 frames | **5** in 823 frames (all small, first 3 s) | | Waits gone past (per 6000 stopped) | ~4100 | 533 | | Made to go by "waits for the replay" | hundreds | **0** | | fps | ~34-35 | ~27 | - Before, the flashing alternated every other frame: steady frames at mean luma ~123, the frames between them 118-141. They were the whole scene brighter or hazier (about 128 of 144 blocks), with the HUD unchanged. This is the pattern described here. - With the patch, the "normal" frames also look different: the white/milky haze over Kokiri is mostly gone and the scene has proper contrast (mean luma 126 -> 75 at the same spot). Some of the white haze reported for this same chip in #17 may be this bug. - fps dropped from ~35 to ~27 on this iGPU, unlike the 60 fps either way on the RX 7800 XT. I did not run a control with my own build of `hookshot_vk.so` without the patch, so part of that difference could come from my build, not the PR. Thanks for the fix!
pr0vId3nce commented 2026-11-08 05:41:53 +00:00 (Migrated from github.com)

Tested on an RTX 4080 / Windows 11 / Ryzen 9 7900X: main 4cfa08f with this PR merged, warm caches, Kokiri Forest standing still, 60 s RX_VK_FRAMELOG per run, windowed 1280x720, frame generation off. One run per build, plus two runs of plain main.

Build fps White frames Small flashes/min Device lost
main (2 runs) 37.2 / 40.5 0 3.0 / 4.0 0
main + #449 37.7 0 0 0

No regression in fps, no white frames or device lost; 0 flashes in this run. Plain main barely flickers on this card, so this can't confirm the fix, only that it does no harm here. waits gone past dropped from 403-553 on main to 147.

Tested on an RTX 4080 / Windows 11 / Ryzen 9 7900X: `main` 4cfa08f with this PR merged, warm caches, Kokiri Forest standing still, 60 s `RX_VK_FRAMELOG` per run, windowed 1280x720, frame generation off. One run per build, plus two runs of plain `main`. | Build | fps | White frames | Small flashes/min | Device lost | |---|---|---|---|---| | `main` (2 runs) | 37.2 / 40.5 | 0 | 3.0 / 4.0 | 0 | | `main` + #449 | 37.7 | 0 | 0 | 0 | No regression in fps, no white frames or device lost; 0 flashes in this run. Plain `main` barely flickers on this card, so this can't confirm the fix, only that it does no harm here. `waits gone past` dropped from 403-553 on main to 147.
Commenting is not possible because the repository is archived.
No description provided.