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

Closed
hckrman101 wants to merge 2 commits from flicker-queue-order into main
hckrman101 commented 2026-11-08 00:20:39 +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 aren't explained yet.
  • 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 (after #416). 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 aren't explained yet. - 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 (after #416). The test now checks `take_capture()`, which both capture paths share, for `cap_view_bump()`. The numbers above were measured on ba594dac.
Kirkidoo commented 2026-11-08 02:28:58 +00:00 (Migrated from github.com)

Tested on Windows 11, AMD Radeon RX 9070 XT (Vulkan driver 0x8001a0, 32.0.32015.7029), Ryzen 7 5800X3D, 3440x1440 fullscreen. Built from no-visual-studio with this PR plus #423, #435, #436, #329 and #205.

With this PR the fps goes up, but the screen gets very fast, subtle white flashes in Kokiri Forest (not there on the same build without it). Results:

  • Both halves on: white flashing.
  • RX_VK_RESUME_CAPTURE=0 only: still flashing.
  • RX_VK_PER_QUEUE_FENCES=0 and RX_VK_RESUME_CAPTURE=0: no flashing, but the fps gain is gone.

So the per-queue fences seem to cause the flashing on this AMD card. I'm happy to test a new version or send a log.

Tested on Windows 11, AMD Radeon RX 9070 XT (Vulkan driver 0x8001a0, 32.0.32015.7029), Ryzen 7 5800X3D, 3440x1440 fullscreen. Built from `no-visual-studio` with this PR plus #423, #435, #436, #329 and #205. With this PR the fps goes up, but the screen gets very fast, subtle white flashes in Kokiri Forest (not there on the same build without it). Results: - Both halves on: white flashing. - `RX_VK_RESUME_CAPTURE=0` only: still flashing. - `RX_VK_PER_QUEUE_FENCES=0` and `RX_VK_RESUME_CAPTURE=0`: no flashing, but the fps gain is gone. So the per-queue fences seem to cause the flashing on this AMD card. I'm happy to test a new version or send a log.
hckrman101 commented 2026-11-08 02:48:29 +00:00 (Migrated from github.com)

Going to rework this and ensure it's better

Going to rework this and ensure it's better
azlekayn commented 2026-11-08 04:05:10 +00:00 (Migrated from github.com)

Hang on RADV with batched submits

This PR hangs at startup on RADV, where batched submits (#413) are on by default. The game's main thread blocks forever in nvnSyncWait, before the first movie:

#5  vk_common_WaitSemaphores () from libvulkan_radeon.so
#6  mkwvk::(anonymous namespace)::wait_recorded   gpu/vk/vk_replay.cpp
#7  mkwvk::(anonymous namespace)::wait            gpu/vk/vk_replay.cpp
#8  n_nvnSyncWait ()

Cause

  • submit_recording puts the rec_timeline signal in signals[], and only the unbatched vkQueueSubmit2 uses that array.
  • With batch_on(), the command buffers go into pend, and queue_flush_locked() signals only timeline and batch_tl.
  • So rec_timeline never advances, and the first wait_recorded() that reaches it never returns.

It doesn't show with batching off, which is why it wouldn't show on NVIDIA.

Reproduction

RX 7800 XT, RADV (Mesa, Vulkan 1.4), Fedora 44, Linux 7.2. This PR merged onto current main.

Run Result
defaults hang, 3 of 3 runs
RX_VK_PER_QUEUE_FENCES=0 fine
RX_VK_RESUME_CAPTURE=0 fine

The guest memory was a memfd window, from #448. With anonymous memory (HOS_GUEST_MEMFD=0) it didn't hang in one run, so timing seems to decide whether a wait reaches the new path. The missing signal is there either way.

Fix

Carry the highest recording number in the batch, and signal it at the flush. With this, the game starts every time and plays (90 s and 5.5 min runs).

@@ -70,7 +70,7 @@ std::mutex replay_mu;   // one replay at a time (the game submits from several t
 // (RX_VK_BATCH: see queue_flush_locked)
 VkSemaphore batch_tl = VK_NULL_HANDLE;
 uint64_t batch_next = 0, batch_flushed = 0;
-struct { std::vector<VkCommandBufferSubmitInfo> cbs; uint64_t signal = 0; bool any_signal = false; } pend;
+struct { std::vector<VkCommandBufferSubmitInfo> cbs; uint64_t signal = 0; bool any_signal = false; uint64_t rec = 0; } pend;   // (rec: the recordings' timeline value the held ones reach)
 int64_t st_flushes, st_batched_cbs;
 enum { FR_OTHER, FR_POLL, FR_WAIT, FR_SLOT, FR_PRESENT, FR_CAP, FR_VALUE, FR_COUNT };
 int64_t st_reason[FR_COUNT];
@@ -146,12 +146,17 @@ bool batch_on() {
 } // namespace
 void queue_flush_locked() {   // (dev.present_mu held)
     if (pend.cbs.empty()) return;
-    VkSemaphoreSubmitInfo sig[2]{{VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO}, {VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO}};
+    VkSemaphoreSubmitInfo sig[3]{{VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO}, {VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO},
+                                 {VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO}};
     uint32_t n = 0;
     if (pend.any_signal) {
         sig[n].semaphore = timeline, sig[n].value = pend.signal, sig[n].stageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT;
         n++;
     }
+    if (pend.rec) {   // (the recordings' timeline too: wait_recorded waits on it, and held submits carried no signal of it)
+        sig[n].semaphore = rec_timeline, sig[n].value = pend.rec, sig[n].stageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT;
+        n++;
+    }
     sig[n].semaphore = batch_tl, sig[n].value = batch_next, sig[n].stageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT;
     n++;
     VkSubmitInfo2 si{VK_STRUCTURE_TYPE_SUBMIT_INFO_2};
@@ -168,7 +173,7 @@ void queue_flush_locked() {   // (dev.present_mu held)
     st_flushes++, st_batched_cbs += (int64_t)pend.cbs.size();
     st_reason[flush_reason]++;
     flush_reason = FR_OTHER;
-    pend.cbs.clear(), pend.any_signal = false, pend.signal = 0;
+    pend.cbs.clear(), pend.any_signal = false, pend.signal = 0, pend.rec = 0;
 }
 void queue_flush() {
     std::lock_guard lk(dev.present_mu);
@@ -280,8 +285,9 @@ void submit_recording(Recording& r, bool signal_value = true, uint64_t signal_at
     si.pCommandBufferInfos = cb;
     VkSemaphoreSubmitInfo signals[2] = {signal, {VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO}};
     uint32_t nsig = signal_value ? 1 : 0;
+    uint64_t seq = 0;
     if (rec_timeline) {   // (and its number on the recordings' timeline; every caller holds replay_mu: in order)
-        const uint64_t seq = ++rec_seq;
+        seq = ++rec_seq;
         signals[nsig].semaphore = rec_timeline;
         signals[nsig].value = seq;
         signals[nsig].stageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT;
@@ -299,6 +305,7 @@ void submit_recording(Recording& r, bool signal_value = true, uint64_t signal_at
         if (pend.cbs.empty()) batch_next++, pend_since = std::chrono::steady_clock::now();
         for (uint32_t i = 0; i < n; i++) pend.cbs.push_back(cb[i]);
         if (signal_value) pend.signal = std::max(pend.signal, signal.value), pend.any_signal = true;
+        if (seq) pend.rec = std::max(pend.rec, seq);
         r.batch = batch_next;
         r.submitted = true;
         // (RX_VK_BATCH_MAX=<command buffers>: held at most this many, default 32)

I'm measuring this PR's effect on the flashes on RADV now and will post the numbers here.

Found and patched with Claude (Anthropic); I ran the tests on my machine.

## Hang on RADV with batched submits This PR hangs at startup on RADV, where batched submits (#413) are on by default. The game's main thread blocks forever in `nvnSyncWait`, before the first movie: ``` #5 vk_common_WaitSemaphores () from libvulkan_radeon.so #6 mkwvk::(anonymous namespace)::wait_recorded gpu/vk/vk_replay.cpp #7 mkwvk::(anonymous namespace)::wait gpu/vk/vk_replay.cpp #8 n_nvnSyncWait () ``` ### Cause - `submit_recording` puts the `rec_timeline` signal in `signals[]`, and only the unbatched `vkQueueSubmit2` uses that array. - With `batch_on()`, the command buffers go into `pend`, and `queue_flush_locked()` signals only `timeline` and `batch_tl`. - So `rec_timeline` never advances, and the first `wait_recorded()` that reaches it never returns. It doesn't show with batching off, which is why it wouldn't show on NVIDIA. ### Reproduction RX 7800 XT, RADV (Mesa, Vulkan 1.4), Fedora 44, Linux 7.2. This PR merged onto current main. | Run | Result | |---|---| | defaults | hang, 3 of 3 runs | | `RX_VK_PER_QUEUE_FENCES=0` | fine | | `RX_VK_RESUME_CAPTURE=0` | fine | The guest memory was a memfd window, from #448. With anonymous memory (`HOS_GUEST_MEMFD=0`) it didn't hang in one run, so timing seems to decide whether a wait reaches the new path. The missing signal is there either way. ### Fix Carry the highest recording number in the batch, and signal it at the flush. With this, the game starts every time and plays (90 s and 5.5 min runs). ```diff @@ -70,7 +70,7 @@ std::mutex replay_mu; // one replay at a time (the game submits from several t // (RX_VK_BATCH: see queue_flush_locked) VkSemaphore batch_tl = VK_NULL_HANDLE; uint64_t batch_next = 0, batch_flushed = 0; -struct { std::vector<VkCommandBufferSubmitInfo> cbs; uint64_t signal = 0; bool any_signal = false; } pend; +struct { std::vector<VkCommandBufferSubmitInfo> cbs; uint64_t signal = 0; bool any_signal = false; uint64_t rec = 0; } pend; // (rec: the recordings' timeline value the held ones reach) int64_t st_flushes, st_batched_cbs; enum { FR_OTHER, FR_POLL, FR_WAIT, FR_SLOT, FR_PRESENT, FR_CAP, FR_VALUE, FR_COUNT }; int64_t st_reason[FR_COUNT]; @@ -146,12 +146,17 @@ bool batch_on() { } // namespace void queue_flush_locked() { // (dev.present_mu held) if (pend.cbs.empty()) return; - VkSemaphoreSubmitInfo sig[2]{{VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO}, {VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO}}; + VkSemaphoreSubmitInfo sig[3]{{VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO}, {VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO}, + {VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO}}; uint32_t n = 0; if (pend.any_signal) { sig[n].semaphore = timeline, sig[n].value = pend.signal, sig[n].stageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT; n++; } + if (pend.rec) { // (the recordings' timeline too: wait_recorded waits on it, and held submits carried no signal of it) + sig[n].semaphore = rec_timeline, sig[n].value = pend.rec, sig[n].stageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT; + n++; + } sig[n].semaphore = batch_tl, sig[n].value = batch_next, sig[n].stageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT; n++; VkSubmitInfo2 si{VK_STRUCTURE_TYPE_SUBMIT_INFO_2}; @@ -168,7 +173,7 @@ void queue_flush_locked() { // (dev.present_mu held) st_flushes++, st_batched_cbs += (int64_t)pend.cbs.size(); st_reason[flush_reason]++; flush_reason = FR_OTHER; - pend.cbs.clear(), pend.any_signal = false, pend.signal = 0; + pend.cbs.clear(), pend.any_signal = false, pend.signal = 0, pend.rec = 0; } void queue_flush() { std::lock_guard lk(dev.present_mu); @@ -280,8 +285,9 @@ void submit_recording(Recording& r, bool signal_value = true, uint64_t signal_at si.pCommandBufferInfos = cb; VkSemaphoreSubmitInfo signals[2] = {signal, {VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO}}; uint32_t nsig = signal_value ? 1 : 0; + uint64_t seq = 0; if (rec_timeline) { // (and its number on the recordings' timeline; every caller holds replay_mu: in order) - const uint64_t seq = ++rec_seq; + seq = ++rec_seq; signals[nsig].semaphore = rec_timeline; signals[nsig].value = seq; signals[nsig].stageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT; @@ -299,6 +305,7 @@ void submit_recording(Recording& r, bool signal_value = true, uint64_t signal_at if (pend.cbs.empty()) batch_next++, pend_since = std::chrono::steady_clock::now(); for (uint32_t i = 0; i < n; i++) pend.cbs.push_back(cb[i]); if (signal_value) pend.signal = std::max(pend.signal, signal.value), pend.any_signal = true; + if (seq) pend.rec = std::max(pend.rec, seq); r.batch = batch_next; r.submitted = true; // (RX_VK_BATCH_MAX=<command buffers>: held at most this many, default 32) ``` I'm measuring this PR's effect on the flashes on RADV now and will post the numbers here. Found and patched with Claude (Anthropic); I ran the tests on my machine.
hckrman101 commented 2026-11-08 04:18:11 +00:00 (Migrated from github.com)

Thanks @azlekayn, that's right: held submissions never signalled rec_timeline. I've pushed your fix as its own commit, credited to you. I could reproduce the hang on NVIDIA by forcing RX_VK_BATCH=1: it hung before the first present, and now it starts and plays.

@Kirkidoo, thanks for testing on the RX 9070 XT. Your result matches what I measure: the per-queue fences are what bring the white flashes back. They change when the CPU reads OoT's exposure meter, and the meter then sometimes reads 0 or twice its value for a frame. I'm working on a fix for the meter itself, and on how to measure it reliably, because one-frame flash counts vary too much between runs to compare small changes. I'll post numbers here when I have them. Until then, RX_VK_PER_QUEUE_FENCES=0 keeps the other half of this PR, which roughly halved the flashes in my runs, without the fences.

Thanks @azlekayn, that's right: held submissions never signalled `rec_timeline`. I've pushed your fix as its own commit, credited to you. I could reproduce the hang on NVIDIA by forcing `RX_VK_BATCH=1`: it hung before the first present, and now it starts and plays. @Kirkidoo, thanks for testing on the RX 9070 XT. Your result matches what I measure: the per-queue fences are what bring the white flashes back. They change when the CPU reads OoT's exposure meter, and the meter then sometimes reads 0 or twice its value for a frame. I'm working on a fix for the meter itself, and on how to measure it reliably, because one-frame flash counts vary too much between runs to compare small changes. I'll post numbers here when I have them. Until then, `RX_VK_PER_QUEUE_FENCES=0` keeps the other half of this PR, which roughly halved the flashes in my runs, without the fences.
hckrman101 commented 2026-11-08 05:30:35 +00:00 (Migrated from github.com)

I'm closing this for now. I'll open a new PR once I have a fix I'm confident in, which should be soon.

What I've found since opening it, on an RTX 3090 (Linux):

  • The per-queue fences make the white flashes worse. That matches what @Kirkidoo saw on the RX 9070 XT. The fresh capture on resume does help, but it doesn't fix the cause.
  • The flashes come in pairs. On some frames the scene is rendered brighter (it looks like a haze or sun-shaft layer appearing for one frame). The auto-exposure measures that frame, the exposure jumps, and that gives the second flash.
  • The exposure meter buffers themselves are handed between the GPU and the CPU correctly: reset, mirror sync, the GPU's sum, write-back and the CPU's read all happen in order every frame.
  • The meter pass (program 435a0138b9013040) samples the HDR scene image after it has been drawn that frame, on bad frames too. On a bad frame it measures the same brighter version of the scene every time, so the problem is earlier, in what draws the scene that frame.
  • My current lead is the parameter blocks the CPU rewrites each frame and many passes read through the mirror. Their timing is clearly sensitive: binding them in guest memory made the flashes more frequent.
  • One-frame flash counts vary a lot between runs of the same build (3 to 164 a minute). So I'm measuring with an in-game count of bad meter readings, which tracks the flashes and is far more stable.

Thanks to @azlekayn for the batched-submit fix (it's in this branch) and to @Kirkidoo for testing. I'll link the new PR here.

I'm closing this for now. I'll open a new PR once I have a fix I'm confident in, which should be soon. What I've found since opening it, on an RTX 3090 (Linux): - The per-queue fences make the white flashes worse. That matches what @Kirkidoo saw on the RX 9070 XT. The fresh capture on resume does help, but it doesn't fix the cause. - The flashes come in pairs. On some frames the scene is rendered brighter (it looks like a haze or sun-shaft layer appearing for one frame). The auto-exposure measures that frame, the exposure jumps, and that gives the second flash. - The exposure meter buffers themselves are handed between the GPU and the CPU correctly: reset, mirror sync, the GPU's sum, write-back and the CPU's read all happen in order every frame. - The meter pass (program `435a0138b9013040`) samples the HDR scene image after it has been drawn that frame, on bad frames too. On a bad frame it measures the same brighter version of the scene every time, so the problem is earlier, in what draws the scene that frame. - My current lead is the parameter blocks the CPU rewrites each frame and many passes read through the mirror. Their timing is clearly sensitive: binding them in guest memory made the flashes more frequent. - One-frame flash counts vary a lot between runs of the same build (3 to 164 a minute). So I'm measuring with an in-game count of bad meter readings, which tracks the flashes and is far more stable. Thanks to @azlekayn for the batched-submit fix (it's in this branch) and to @Kirkidoo for testing. I'll link the new PR here.
pr0vId3nce commented 2026-11-08 05:41:52 +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 + #415 42.0 0 8.0 (all darker, one 20x20 block each) 0

No regression in fps, no white frames or device lost. Plain main barely flickers on this card, so I can't confirm the fix here. One note: #415 showed 8 small darker blips/min against main's 3-4. That's a single run each and may be noise, but it lines up with Kirkidoo's report of new flashes with the per-queue fences.

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` + #415 | 42.0 | 0 | 8.0 (all darker, one 20x20 block each) | 0 | No regression in fps, no white frames or device lost. Plain `main` barely flickers on this card, so I can't confirm the fix here. One note: #415 showed 8 small darker blips/min against main's 3-4. That's a single run each and may be noise, but it lines up with Kirkidoo's report of new flashes with the per-queue fences.
hckrman101 commented 2026-11-08 07:34:54 +00:00 (Migrated from github.com)

The follow-up is #513: it includes both commits from here, rebased, plus the fix for the remaining flashes (the CPU running a frame ahead and overwriting data the GPU still had to read). A/B: 5 flashes to 0, brightness unchanged.

The follow-up is #513: it includes both commits from here, rebased, plus the fix for the remaining flashes (the CPU running a frame ahead and overwriting data the GPU still had to read). A/B: 5 flashes to 0, brightness unchanged.
hckrman101 commented 2026-11-08 08:09:46 +00:00 (Migrated from github.com)

Update: this PR's commits are now in #523 (rebased, with the RADV batched-submit fix); #513 only has the fix for the remaining race.

Update: this PR's commits are now in #523 (rebased, with the RADV batched-submit fix); #513 only has the fix for the remaining race.
azlekayn commented 2026-11-08 12:40:43 +00:00 (Migrated from github.com)

A follow-up to the RADV hang fix above, in case it's useful for this PR: https://github.com/azlekayn/Hookshot/tree/fix/readback-wait-when-behind

The branch is this PR plus two commits:

  • e5dc399: the hang fix from my earlier comment (batched submits also signal the recordings' timeline).
  • bdf58d3: on this PR's unforced readback path, a readback waits for its write-back only while the replay is behind. #296's wait applies only then, not on every readback. RX_VK_READBACK_WAIT_ALWAYS=1 restores always waiting.

Results on an RX 7800 XT (RADV), Kokiri gameplay, 75 s frame logs. A flash is a frame where at least 20 of 144 screen blocks jump the same way:

Build fps Flashes/min
main + this PR, readback wait off 60 49
main + this PR (always waits) 46.5 0
main + this PR + behind-only 60 0–3.2, median 0.8 (6 runs)
main without this PR, behind-only 45 ~1200

The last row shows the change needs this PR's per-queue fences. Without them, "behind" can't be detected reliably. Feel free to cherry-pick either commit.

A follow-up to the RADV hang fix above, in case it's useful for this PR: https://github.com/azlekayn/Hookshot/tree/fix/readback-wait-when-behind The branch is this PR plus two commits: - `e5dc399`: the hang fix from my earlier comment (batched submits also signal the recordings' timeline). - `bdf58d3`: on this PR's unforced readback path, a readback waits for its write-back only while the replay is behind. #296's wait applies only then, not on every readback. `RX_VK_READBACK_WAIT_ALWAYS=1` restores always waiting. Results on an RX 7800 XT (RADV), Kokiri gameplay, 75 s frame logs. A flash is a frame where at least 20 of 144 screen blocks jump the same way: | Build | fps | Flashes/min | |---|---|---| | main + this PR, readback wait off | 60 | 49 | | main + this PR (always waits) | 46.5 | 0 | | main + this PR + behind-only | 60 | 0–3.2, median 0.8 (6 runs) | | main without this PR, behind-only | 45 | ~1200 | The last row shows the change needs this PR's per-queue fences. Without them, "behind" can't be detected reliably. Feel free to cherry-pick either commit.
azlekayn commented 2026-11-08 13:09:32 +00:00 (Migrated from github.com)

Correction to my comment above (the fix/readback-wait-when-behind branch): please disregard it. I posted it without seeing that this PR had been replaced by #523.

I re-tested on current main (97e7c55, with #513 and #523, plus my open #448 for guest memory on this setup). RX 7800 XT, RADV, Kokiri, one run of about 45-55 s per setting. A flash is a frame where at least 20 of 144 screen blocks jump the same way:

Setting fps Flashes/min
main as merged 45.6 0
readbacks wait only while behind 35.1 0

So the behind-only wait no longer helps, and I am not proposing it. The RADV hang fix from my earlier comment is already in main through #523.

Correction to my comment above (the `fix/readback-wait-when-behind` branch): please disregard it. I posted it without seeing that this PR had been replaced by #523. I re-tested on current main (97e7c55, with #513 and #523, plus my open #448 for guest memory on this setup). RX 7800 XT, RADV, Kokiri, one run of about 45-55 s per setting. A flash is a frame where at least 20 of 144 screen blocks jump the same way: | Setting | fps | Flashes/min | |---|---|---| | main as merged | 45.6 | 0 | | readbacks wait only while behind | 35.1 | 0 | So the behind-only wait no longer helps, and I am not proposing it. The RADV hang fix from my earlier comment is already in main through #523.
Commenting is not possible because the repository is archived.
No description provided.