fix(vk): snapshot uniform buffers on Intel when the buffer pools have no VRAM mirror #2464
No reviewers
Labels
No labels
accessibility
autorelease: pending
autorelease: tagged
bug
dependencies
documentation
duplicate
enhancement
github_actions
good first issue
help wanted
invalid
question
testing wanted
wontfix
No milestone
No assignees
1 participant
Due date
No due date set.
Dependencies
No dependencies set
Reference
MagicBeanDev/Hookshot!2464
Loading…
Reference in a new issue
No description provided.
Delete branch "fix/igpu-ub-snapshot"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
This is my first contribution to Hookshot, so I am happy to change anything about the approach, the style or the scope. I did the investigation and the change with the help of AI, as the commit trailers say. I checked the results on my own machine, described below.
On Intel Core Ultra iGPUs, Kokiri Forest is covered by a white veil: the ground is missing and the sky/fog is painted over it. The mechanism is not Intel-specific (any GPU whose buffer pools have no VRAM mirror can read uniform buffers stale), but the new default is limited to Intel for now.
Why
With no VRAM mirror (
mirror_on()off: integrated GPUs, GCN since #2343,RX_VK_MIRROR=0), a draw's uniform buffers are bound straight into the imported guest pools. The GPU reads them when it runs the frame. By then the game has already written the next frame's per-object matrices over the ones that were submitted.In Kokiri, the depth prepass (generic program
ffb53ed102dd9908) drew the terrain with the wrongc4matrices, so the terrain landed off screen. The depth there stayed at far (1.0). The fog/atmosphere composite (1a7482c0ec9991ea) then treats every pixel above 0.9999 as sky and painted fog over the ground. The forward pass draws the ground colour correctly, which is why only the fog layer looked wrong. On a discrete GPU the mirror is copied at submission time, so it never sees the rewrite.What changed
RX_VK_UB_SNAPSHOTalready copies a uniform buffer into the stream as submitted (memory_read_as_submitted). It is now on by default on Intel when the pools are not mirrored (vendorID == 0x8086 && !pools_mirrored()). Other GPUs without a mirror (AMD APUs, Steam Deck, GCN,RX_VK_MIRROR=0) keep it off until someone measures the cost there.RX_VK_UB_SNAPSHOT=0turns it off;=1forces it on anywhere.How it was found
RX_VK_DUMP_EACH=ffb53ed102dd9908+RX_VK_DUMP_ARGSread the depth back after each prepass draw. 551 of 558 draws added no depth at all, including 12k-24k-index terrain meshes drawn while the depth was still empty.c3/c4, put 454 of them on screen with depth ~0.99.Testing
HOS_OUTPUT_RES=3),RX_VK_FIFO=1,RX_VK_PREPARE_BACKGROUND=1.main(3e32949, with #2419) built from source with this branch rebased on it.Related: #999 (Intel tracker); report and workaround: #999
🤖 Generated with Claude Code
I tested this on a second iGPU, on Linux. My machine doesn't reproduce the veil, so I can't confirm the fix, but I found no regressions, and I have some history on the same root cause that may help.
Setup
main(4b5266a0): no conflicts, and the build is cleanvk: VRAM mirror of buffer pools off, so the new default applies on this GPUResult
RX_VK_UB_SNAPSHOT=0: the same. I don't see the white veil on this machine with the snapshot off either, so I can't confirm the fix from here. I saw no regression with it on.Earlier report of the same cause
On 2026-11-10 I tracked down a different symptom on this Iris Xe to what looks like the same root cause. Link's hair, hat and sleeves and the leaves around his door were white in most frames, and the hearts and button prompts were drawn huge or out of place. The iGPU, replaying behind the game, read a later frame's constants from the unmirrored pools. My local fix turned mirrors on for Intel iGPUs (
mirror_on()), which fixed it at a cost of roughly 2–3 fps (22 vs 24 in Link's house). By the time I meant to open a PR, those artifacts no longer showed on upstreammainwithout mirrors, so I dropped it.That suggests the stale reads are timing-dependent: whether a frame's uniforms get overwritten before the GPU reads them depends on how far the replay falls behind the game. That fits the bug showing on your Core Ultra in Kokiri and not on my Iris Xe today. This PR fixes it more narrowly and more cheaply than mirroring every pool: only the uniform buffers are copied, and you measured no fps cost. That's what I'd want on iGPUs.
Good investigation, but:
6c8c1bf, before #2419 landed. #2419 fixes the same class of bug: the iGPU game overwrites uniforms before later parts of a split submission read them (#997). Please retest the veil on current main without this change first.tests/test_env_on.pyfails ongetenv(...) ? env_on(...) : ...atvk_draw.cpp:2780.mirror_on()is off on every non-discrete GPU, so this also switches on per-draw uniform snapshots on the Steam Deck and AMD APUs like the 780M. It was only tested on one Intel laptop. #2343 also turns mirrors off on GCN dGPUs, which would quietly enable this there too.Thanks, this is a careful write-up, and the depth readback numbers make the mechanism easy to follow. I agree with @Joey-Magnus's three points and won't repeat them in full. Here's where I land on each:
RX_VK_UB_SNAPSHOT=1stays as the opt-in it already is. If it's still there, that's a strong result and worth posting.test_env_on.py: see the inline comment for a form that passes the check.!mirror_on()covers more than Intel iGPUs. It's true on every non-discrete GPU (Deck, 780M/8060S APUs), on GCN dGPUs since #2343, and withRX_VK_MIRROR=0. On all of those, every UBO bind becomes a stream allocation plus a copy throughmemory_read_as_submitted, which takespools_muwhen the range spans pages. You measured no fps change on the 155H, but nobody has measured the Deck or an RX 580, where the frame budget is tighter. If the retest still shows the veil, I'd either want a number from one AMD APU and one GCN card or limit the new default to Intel for now.No CI ran on this branch. It's 52 commits behind main, but the hunk's context is unchanged there, so it should rebase cleanly.
@ -2787,7 +2771,17 @@ VkDescriptorBufferInfo uniform_range(const NvnState& s, const Stage& st, uint32_return {cur->stream, at, n};test_env_on.pyflagsgetenv(name) ?when that is the name's onlygetenvread. Reading the variable once into a local keeps the tri-state and passes the check (same shape asmirror_on()):Nit: the comment above says the pools are unmirrored on "an integrated GPU", but that's also the case on GCN dGPUs (#2343) and with
RX_VK_MIRROR=0. Worth saying "no VRAM mirror (mirror_on()off)" rather than naming the GPU class.Thanks for the reviews. I retested on current main (
3e32949, with #2419), built from source with this branch rebased on it, and ran the same Kokiri scene withRX_VK_UB_SNAPSHOT=0(main's behaviour) and without it (this change).The veil is still there on main. Same passes, same order, "far" depth samples out of 57,600:
RX_VK_UB_SNAPSHOT=0f8d01f66)879fe82e1a7482c0, 4 dumps over 4 minThat is the same profile as on


6c8c1bf(~45,000 vs ~1,000), so the split-submission fence fix doesn't cover this case. Screenshots: main without / with the snapshot.main,
RX_VK_UB_SNAPSHOT=0:main + this change:
Caveats:
RX_VK_PCACHE_LOAD=0): on this drivervkGetPipelineCacheDatafaults (read 0x190 in igvk64.dll, in thepipeline_cache_savethread) on a cache created from file data. That happens on v2.1.0 too and is unrelated to this PR.In two of three further runs without the snapshot the device was lost (Edit: later runs with the snapshot (and with #2419 reverted) lost the device the same way, so that's unrelated to this change. It looks like something on current main on this machine: the same scene and settings on v2.1.0 didn't lose the device. I'm still looking at it.VK_ERROR_DEVICE_LOST) while Kokiri loaded; none with it. Only an observation, I haven't dug into it.Changes (force-pushed, one commit on current main):
tests/test_env_on.pypasses.vendorID == 0x8086 && !pools_mirrored(). AMD APUs, GCN (#2343) andRX_VK_MIRROR=0keep it off until someone measures it there;RX_VK_UB_SNAPSHOT=1still turns it on anywhere.mirror_on()off)" instead of naming a GPU class, and mentions #2419.3e32949.Thanks also to the Iris Xe tester for checking it on Mesa ANV.
@ -2787,7 +2771,17 @@ VkDescriptorBufferInfo uniform_range(const NvnState& s, const Stage& st, uint32_return {cur->stream, at, n};Done in
83c18ac: the switch is read with this form, and the comment now says "no VRAM mirror (mirror_on()off)".github-sync referenced this pull request2026-11-12 06:22:54 +00:00