Repository navigation
fix(vk): cyan after a pause in the background: keep the mirrors of pools the GPU writes - #2154
Merged
Merged
Conversation
An idle pool's VRAM mirror is given back after kIdleEpochs submissions (at once past kLongIdle, or when a wanted mirror does not fit), and made again from guest memory when the pool is next bound. What a shader wrote through a bare entry, the pointer table or a wide binding is never copied back to guest memory (memory_note_gpu_write): it was in the old mirror alone, so the new one started from stale data and the GPU went on from that. On an RX 5500M, pausing for 1-2 minutes with the game in the background gave back the idle pools' mirrors (51 mirrors, 1136 MB in one pause), and every resume brought cyan for a few seconds. With RX_VK_MIRROR_KEEP=1 (no give-back) two such pauses resumed clean, at Auto and at 33% Render resolution. memory_note_gpu_write now marks the pools it reaches (Pool::gpu_only), the idle give-back leaves their mirrors, and a pool that takes others in keeps their mark. A pool refused a mirror for the budget meanwhile is read in system memory: slower, not wrong. The give-back's log line says how many MB of idle mirrors are kept this way. tests/test_mirror_gpu_only.py reads the source for the three (fails on main).
…carries over a merge nx2/test_import_windows.py compiles pool_lookup from gpu/vk/vk_memory.cpp against a Pool of its own; the merge now copies the taken-in pools' gpu_only mark, so that Pool needs the field (CI: 'struct Pool' has no member named 'gpu_only').
…oo, and log the ones still given back After a save reload, cyan showed in play and over the Start menu, with no pool written past the write-back given back: the mirrors still going were of pools the GPU writes through a normal binding (RX 5500M: three reloads clean with RX_VK_MIRROR_KEEP=1, cyan after each reload without it). Pool::gpu_written is set by gpu_buffer_bind for a written binding of the mirror, carried over a merge, and the idle give-back keeps those mirrors as it keeps gpu_only ones. The mirrors still given back are logged (the first 200): retired, retired and not inherited in time, idle, or idle for room, with how long since their sync and the pool's marks.
Joey-Magnus
reviewed
Nov 11, 2026
Joey-Magnus
left a comment
Contributor
There was a problem hiding this comment.
The first commit is right: data memory_note_gpu_write never copies back lives only in the VRAM mirror, so keeping gpu_only pools' mirrors fixes the cause.
The second commit is untested in game by your own note. It sets gpu_written on every written binding (vk_memory.cpp:2760) and never clears it, and mirrors_give_back then never frees those mirrors (:2901), not even in the "for room" pass. With write-back on by default, that data is already in guest memory. On a 4 GB card (512 MB mirror budget, 256 MB on Minimum) other pools get pushed into system memory for good. Either ship the first commit alone, or let the "for room" pass free gpu_written-only mirrors, and attach a 5500M log with save reloads.
dazemc
added a commit
to dazemc/Hookshot-rs
that referenced
this pull request
Nov 12, 2026
Port of upstream NotRexed#2154. An idle pool's mirror could be given back though the GPU had written data there that is never copied back (guessed ranges, the pointer table, wide bindings) or through a normal mirror binding. Made again from guest memory, that data was gone and the picture showed cyan for a few seconds after a pause or a save reload. Such pools keep their mirrors; a pool that takes another in inherits both marks, and the give-back log says which mirrors still go and why.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Pause the game, leave it in the background for 1–2 minutes (another window or the Start menu in front), then come back and resume: for a few seconds the picture has cyan patches. On an RX 5500M (RDNA1, 4 GB, Windows 11, AMD driver) this happened on every resume.
While the game is paused, most of its memory pools are not bound. After
kIdleEpochs(600) submissions an unbound pool's VRAM mirror is idle, andmirrors_give_backgives it back once it has been idle pastkLongIdle(6000 submissions) or when a wanted mirror does not fit. In one such pause the running count in the log went from 35 to 51 (51 mirrors given back (1136 MB in all)). At the pool's next binding,mirror_makemakes a new mirror from guest memory (plus the CPU's newer writes from the captures).Most of what the GPU writes into a mirror is copied back to guest memory: a written binding queues a write-back. But some of it is never copied back, by design, and is noted only as a hazard by
memory_note_gpu_write: what a shader writes through a bare entry's guessed range, the pointer table's range or a wide binding. (Copying the whole guessed range back overwrote the CPU's own data next to it.) That data was in the old mirror alone. The new mirror starts without it, and the GPU goes on from stale data until it has rebuilt it, which is the few seconds of cyan.Change
Pool::gpu_only: set bymemory_note_gpu_writeon every imported pool its range reaches.mirrors_give_backleaves an idlegpu_onlypool's mirror alone. Its other idle mirrors go as before.pool_lookup's merge) keeps their mark: their mirrors' contents come with them.Pool::gpu_written, set bygpu_buffer_bindfor a written binding of the mirror, carried over a merge likegpu_only, and the idle give-back keeps those mirrors too.A pool that is refused a mirror for the budget meanwhile is read in system memory, which is slower but not wrong. Retired pools are unchanged: their GPU contents already go to the pool that took them in (
inherit_pending).Tested
RX 5500M, Windows 11, AMD driver, main
92907f7with #1581 and #2125, same settings throughout. Each pause was 1–2 minutes with the game in the background:92907f7+ #1581 + #2125)RX_VK_MIRROR_KEEP=1(no give-back at all)A very short black flash right on resume is still there at both resolutions. At Auto it has been there since the first builds on this card, including the
RX_VK_MIRROR_KEEP=1run, so it looks separate.Save reloads
With the first commit, cyan came back after reloading a save within a run (Hyrule Castle, Hyrule Field), in play and over the Start menu, with no pause and no
not importedpools in the log. (That run also hadRX_VK_FIFO=1, which turned out not to matter.) AgainstRX_VK_MIRROR_KEEP=1, which never gives a mirror back:RX_VK_MIRROR_KEEP=1So mirrors given back explain most of the cyan after a reload, but not all of it. The first commit keeps only pools the GPU wrote past the write-back. The pools whose mirrors still went are the ones the GPU writes through a normal binding, which the second commit keeps too.
Not tested in game yet: the second commit. I haven't played a build with it, so I don't know yet whether it matches
RX_VK_MIRROR_KEEP=1or what it costs in VRAM. Its log line is there to answer both, and to show which mirrors are still given back before any flash that's left.Tests
tests/test_mirror_gpu_only.py(new) reads the source and checks four things:memory_note_gpu_writemarks the pools it reaches,gpu_buffer_bindmarks a pool written through its mirror, the idle give-back leaves marked pools, and a merge keeps both marks. It fails on current main and passes with this change.nx2/test_import_windows.pybuildspool_lookupagainst aPoolof its own, which now has both fields.tests/run_all.pyon main4d68bc83with this branch merged: 311 passed. The 6 that fail are the same 6 that fail on plain main on this PC.Related: #1005 (cyan and UI flashes after focus loss), #2125.