Vulkan: no one-frame fog/exposure flashes from OoT's queue ordering #523

Merged
hckrman101 merged 2 commits from flicker-queue-order into main 2026-11-08 09:12:23 +00:00
hckrman101 commented 2026-11-08 08:09:34 +00:00 (Migrated from github.com)

Fixes most of the remaining one-frame flashes on Linux (#371): haze and sun shafts appearing or vanishing for one frame, with the environment brighter or darker while the HUD and characters stay the same.

OoT's presentation queues submit a frame's work before two things it waits for are ready: the scene work (through event waits) and the CPU's writes for that frame (the CPU signals an event when it has finished). Two paths replayed that work against the previous frame's data.

Forced waits. Every NVN queue shared one fence timeline, and it only advanced as far as the oldest submission stopped at a wait. Three things waited on that timeline and pushed the presentation queues' stopped work past its waits:

  • the exposure readback's catch-up (the write-back wait from #296);
  • the texture readback's catch-up;
  • waits on main-queue fences.

Fences are now per queue: nvnQueueFenceSync takes the queue's own last submission. They are waited on through a second timeline that numbers the recordings. The readbacks no longer force anything through. RX_VK_PER_QUEUE_FENCES=0 restores the old behaviour.

Stale captures. The memory capture was taken at submit time, so it held the previous frame's data. Now a submission stopped at a wait for a CPU-signalled event takes a fresh capture of the CPU's writes when it resumes, because the console's GPU reads memory after its wait. RX_VK_RESUME_CAPTURE=0 turns this off.

Measured on Linux with an RTX 3090: Kokiri Forest save, standing still, 60 s frame log on a 75 Hz monitor, one-frame events counted with scripts/framelog.py.

Build Events Frames fps
Before (ba594dac) 26 2900 ~48
After 10 3586 ~60

The frame rate goes up because the main thread no longer blocks on the other queues' stopped work.

Diagnosis:

  • 144 of 150 events followed a forced drain.
  • Once forcing was removed, 87 of 92 events followed a stop at the CPU event's wait.

Known gaps:

  • The 10 remaining events are a separate race (the CPU's next-frame writes over words the GPU wrote on a shared page), fixed in #513.
  • A fence taken on a queue that is itself stopped still falls back to the old forced path.

The branch is rebased onto current main and includes the RADV hang fix for batched submits (Vulkan: batched submits signal the recordings' timeline, reported in #415 by azlekayn: with RX_VK_BATCH, RADV's default since #413, the recordings' timeline was never signalled and the game hung before the first movie). The test now checks take_capture(), which both capture paths share, for cap_view_bump(). The numbers above were measured on ba594dac.

Fixes most of the remaining one-frame flashes on Linux (#371): haze and sun shafts appearing or vanishing for one frame, with the environment brighter or darker while the HUD and characters stay the same. OoT's presentation queues submit a frame's work before two things it waits for are ready: the scene work (through event waits) and the CPU's writes for that frame (the CPU signals an event when it has finished). Two paths replayed that work against the previous frame's data. **Forced waits.** Every NVN queue shared one fence timeline, and it only advanced as far as the oldest submission stopped at a wait. Three things waited on that timeline and pushed the presentation queues' stopped work past its waits: - the exposure readback's catch-up (the write-back wait from #296); - the texture readback's catch-up; - waits on main-queue fences. Fences are now per queue: `nvnQueueFenceSync` takes the queue's own last submission. They are waited on through a second timeline that numbers the recordings. The readbacks no longer force anything through. `RX_VK_PER_QUEUE_FENCES=0` restores the old behaviour. **Stale captures.** The memory capture was taken at submit time, so it held the previous frame's data. Now a submission stopped at a wait for a CPU-signalled event takes a fresh capture of the CPU's writes when it resumes, because the console's GPU reads memory after its wait. `RX_VK_RESUME_CAPTURE=0` turns this off. **Measured** on Linux with an RTX 3090: Kokiri Forest save, standing still, 60 s frame log on a 75 Hz monitor, one-frame events counted with `scripts/framelog.py`. | Build | Events | Frames | fps | |---|---|---|---| | Before (ba594dac) | 26 | 2900 | ~48 | | After | 10 | 3586 | ~60 | The frame rate goes up because the main thread no longer blocks on the other queues' stopped work. **Diagnosis:** - 144 of 150 events followed a forced drain. - Once forcing was removed, 87 of 92 events followed a stop at the CPU event's wait. **Known gaps:** - The 10 remaining events are a separate race (the CPU's next-frame writes over words the GPU wrote on a shared page), fixed in #513. - A fence taken on a queue that is itself stopped still falls back to the old forced path. The branch is rebased onto current main and includes the RADV hang fix for batched submits (`Vulkan: batched submits signal the recordings' timeline`, reported in #415 by azlekayn: with `RX_VK_BATCH`, RADV's default since #413, the recordings' timeline was never signalled and the game hung before the first movie). The test now checks `take_capture()`, which both capture paths share, for `cap_view_bump()`. The numbers above were measured on ba594dac.
hckrman101 commented 2026-11-08 08:15:14 +00:00 (Migrated from github.com)

More numbers, with #513's frame wait applied (same build, 90 s each, Kokiri Forest): with this PR 0 and 1 flash events at ~50 fps; without it (RX_VK_PER_QUEUE_FENCES=0 RX_VK_RESUME_CAPTURE=0) 45 and 19 events at ~38 fps. #513 depends on this one.

More numbers, with #513's frame wait applied (same build, 90 s each, Kokiri Forest): with this PR 0 and 1 flash events at ~50 fps; without it (`RX_VK_PER_QUEUE_FENCES=0 RX_VK_RESUME_CAPTURE=0`) 45 and 19 events at ~38 fps. #513 depends on this one.
Mabec-games commented 2026-11-08 09:39:17 +00:00 (Migrated from github.com)

Tested on Linux: CachyOS (KDE Wayland), RTX 3080 Laptop 8 GB, driver 615.78, SDL3, 3D resolution 50%.

The failing "Python source checks" job isn't caused by this PR: the branch predates 2d3faa3 ("test: cover chunk prefetch in the import-window fixture"), so its copy of nx2/test_import_windows.py doesn't know the prefetch code in vk_memory.cpp. Main's version of the test passes against this branch's vk_memory.cpp, and the branch merges into current main without conflicts, so a rebase should turn it green.

Results: the same build each time, with this PR on or off (RX_VK_PER_QUEUE_FENCES=0 RX_VK_RESUME_CAPTURE=0). Same save in Kakariko Village, camera untouched for 120 s, runs alternated:

run average fps median frame interval
on 1 39.2 27.3 ms
off 1 33.3 35.2 ms
on 2 43.2 23.1 ms
off 2 33.2 35.1 ms

So about 20-30% faster with it on, the same direction as your Kokiri numbers. In this still scene there were no fog/exposure flashes to remove either way (the few the frame recorder flagged were Navi's glow moving, identical before and after), so I can't confirm the flash fix itself from this test, only that nothing new flickers with it on.

Tested on Linux: CachyOS (KDE Wayland), RTX 3080 Laptop 8 GB, driver 615.78, SDL3, 3D resolution 50%. **The failing "Python source checks" job** isn't caused by this PR: the branch predates 2d3faa3 ("test: cover chunk prefetch in the import-window fixture"), so its copy of `nx2/test_import_windows.py` doesn't know the prefetch code in `vk_memory.cpp`. Main's version of the test passes against this branch's `vk_memory.cpp`, and the branch merges into current main without conflicts, so a rebase should turn it green. **Results:** the same build each time, with this PR on or off (`RX_VK_PER_QUEUE_FENCES=0 RX_VK_RESUME_CAPTURE=0`). Same save in Kakariko Village, camera untouched for 120 s, runs alternated: | run | average fps | median frame interval | |---|---|---| | on 1 | 39.2 | 27.3 ms | | off 1 | 33.3 | 35.2 ms | | on 2 | 43.2 | 23.1 ms | | off 2 | 33.2 | 35.1 ms | So about 20-30% faster with it on, the same direction as your Kokiri numbers. In this still scene there were no fog/exposure flashes to remove either way (the few the frame recorder flagged were Navi's glow moving, identical before and after), so I can't confirm the flash fix itself from this test, only that nothing new flickers with it on.
Commenting is not possible because the repository is archived.
No description provided.