vk: small ranges the CPU reads back are bound in guest memory #1150
Open
azlekayn wants to merge 1 commit from
azlekayn/perf/readback-direct into main
pull from: azlekayn/perf/readback-direct
merge into: MagicBeanDev:main
MagicBeanDev:main
MagicBeanDev:github/pr/2893
MagicBeanDev:github/pr/2890
MagicBeanDev:github/pr/2887
MagicBeanDev:github/pr/2878
MagicBeanDev:github/pr/2875
MagicBeanDev:github/pr/2874
MagicBeanDev:github/pr/2873
MagicBeanDev:github/pr/2871
MagicBeanDev:github/pr/2756
MagicBeanDev:github/pr/2897
MagicBeanDev:github/pr/2896
MagicBeanDev:github/pr/2718
MagicBeanDev:github/pr/2842
MagicBeanDev:github/pr/2691
MagicBeanDev:github/pr/2895
MagicBeanDev:github/pr/2743
MagicBeanDev:github/pr/2765
MagicBeanDev:github/pr/2708
MagicBeanDev:github/pr/2732
MagicBeanDev:github/pr/2755
MagicBeanDev:github/pr/2817
MagicBeanDev:github/pr/2876
MagicBeanDev:release-please--branches--main
MagicBeanDev:release-please--branches--main--release-notes
MagicBeanDev:github/pr/2778
MagicBeanDev:github/pr/2888
MagicBeanDev:github/pr/2866
MagicBeanDev:github/pr/2877
MagicBeanDev:github/pr/2800
MagicBeanDev:github/pr/2829
MagicBeanDev:github/pr/2850
MagicBeanDev:github/pr/2889
MagicBeanDev:github/pr/2882
MagicBeanDev:github/pr/2860
MagicBeanDev:github/pr/2849
MagicBeanDev:github/pr/2886
MagicBeanDev:github/pr/2794
MagicBeanDev:github/pr/2840
MagicBeanDev:github/pr/2830
MagicBeanDev:github/pr/2834
MagicBeanDev:github/pr/2719
MagicBeanDev:github/pr/2742
MagicBeanDev:github/pr/2741
MagicBeanDev:github/pr/2820
MagicBeanDev:github/pr/2771
MagicBeanDev:github/pr/2859
MagicBeanDev:github/pr/2858
MagicBeanDev:github/pr/2823
MagicBeanDev:github/pr/2757
MagicBeanDev:github/pr/2769
MagicBeanDev:github/pr/2844
MagicBeanDev:github/pr/2843
MagicBeanDev:github/pr/2673
MagicBeanDev:github/pr/2855
MagicBeanDev:github/pr/2739
MagicBeanDev:github/pr/2807
MagicBeanDev:github/pr/2851
MagicBeanDev:github/pr/2789
MagicBeanDev:github/pr/2814
MagicBeanDev:github/pr/2818
MagicBeanDev:github/pr/2774
MagicBeanDev:github/pr/2772
MagicBeanDev:github/pr/2750
MagicBeanDev:github/pr/2786
MagicBeanDev:github/pr/2785
MagicBeanDev:github/pr/2782
MagicBeanDev:github/pr/2779
MagicBeanDev:github/pr/2775
MagicBeanDev:github/pr/2706
MagicBeanDev:github/pr/2766
MagicBeanDev:github/pr/2747
MagicBeanDev:github/pr/2759
MagicBeanDev:github/pr/2745
MagicBeanDev:github/pr/2740
MagicBeanDev:github/pr/2733
MagicBeanDev:github/pr/2725
MagicBeanDev:github/pr/2727
MagicBeanDev:github/pr/2728
MagicBeanDev:github/pr/2729
MagicBeanDev:github/pr/2721
MagicBeanDev:github/pr/2722
MagicBeanDev:github/pr/2715
MagicBeanDev:github/pr/2714
MagicBeanDev:github/pr/2707
MagicBeanDev:github/pr/2703
MagicBeanDev:github/pr/2705
MagicBeanDev:github/pr/2697
MagicBeanDev:github/pr/2679
MagicBeanDev:github/pr/2676
MagicBeanDev:github/pr/2669
MagicBeanDev:docs/what-hookshot-connects-to
MagicBeanDev:fix/hdr-encode-test
MagicBeanDev:fix/main-tests
MagicBeanDev:fix/play-cache-files
MagicBeanDev:perf/game-barrier-order
MagicBeanDev:feat/heap-lazy
MagicBeanDev:feat/texture-detail
MagicBeanDev:perf/astc-opaque-bc1
MagicBeanDev:fix/fs-dir-lock
MagicBeanDev:fix/framegen-native-kept
MagicBeanDev:fix/update-no-downgrade
MagicBeanDev:port/k10-gcn-f16-mirrors
MagicBeanDev:fix/framegen-mult-cap
MagicBeanDev:chore/release-as-2.1.1
MagicBeanDev:perf/pcache-load-switch
MagicBeanDev:fix/prepare-progress-log
MagicBeanDev:fix/update-closes-stale-game
MagicBeanDev:fix/hdr-scaled-sampled
MagicBeanDev:perf/first-start-preset
MagicBeanDev:perf/zero-upload
MagicBeanDev:fix/vcore-escape-2s
MagicBeanDev:test/fake-gh-atomic
MagicBeanDev:perf/batch-idle-flush
MagicBeanDev:test/finish-stub-a
MagicBeanDev:chore/record-2.1.0
MagicBeanDev:fix/main-red-tests
MagicBeanDev:fix/test-display-startup-driver-cache
MagicBeanDev:feat/mesh-shader-mode
MagicBeanDev:fix/menu-probes-at-startup
MagicBeanDev:fix/fps-counter-classic
MagicBeanDev:fix/test-log-batch-flake
MagicBeanDev:fix/narrow-screen-hud
MagicBeanDev:ci/prereleases
MagicBeanDev:release/v2.0.15
MagicBeanDev:hotfix/v2.0.15
MagicBeanDev:fix/present-ahead-replay
MagicBeanDev:release/v2.0.13
MagicBeanDev:fix/bracket-paths-2013
MagicBeanDev:fix/hdr-overlays-2013
MagicBeanDev:fix/fps-counter-scale-2013
MagicBeanDev:fix/stick-seen-at-rest
MagicBeanDev:hotfix/menu-fits-screen
MagicBeanDev:fix/f1-always-opens-menu
MagicBeanDev:fix/ini-keep-lines
MagicBeanDev:claude/cranky-pike-7cf247
MagicBeanDev:perf/texture-answer-flat
MagicBeanDev:fix/stamp-test-modules
MagicBeanDev:fix/appimage-fonts
MagicBeanDev:fix/display-awake
MagicBeanDev:release/v2.0.12
MagicBeanDev:fix/discord-windows
MagicBeanDev:rc/v2.0.12-tests
MagicBeanDev:fix/dlss-keep-feature
MagicBeanDev:rc/v2.0.12
MagicBeanDev:button-icons
No reviewers
Labels
Clear labels
accessibility
Barrier affecting people with disabilities
autorelease: pending
autorelease: tagged
bug
Something isn't working
dependencies
Pull requests that update a dependency file
documentation
Improvements or additions to documentation
duplicate
This issue or pull request already exists
enhancement
New feature or request
github_actions
Pull requests that update GitHub Actions code
good first issue
Good for newcomers
help wanted
Extra attention is needed
invalid
This doesn't seem right
question
Further information is requested
testing wanted
wontfix
This will not be worked on
No labels
accessibility
autorelease: pending
autorelease: tagged
bug
dependencies
documentation
duplicate
enhancement
github_actions
good first issue
help wanted
invalid
question
testing wanted
wontfix
Milestone
Clear milestone
No items
No milestone
Assignees
Clear assignees
No assignees
2 participants
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".
No due date set.
Dependencies
No dependencies set
Reference
MagicBeanDev/Hookshot!1150
Loading…
Reference in a new issue
No description provided.
Delete branch "azlekayn/perf/readback-direct"
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?
Draft: reworked after review. It still needs an NVIDIA retest before it can be considered.
What
A range the game invalidates to read (
nvnMemoryPoolInvalidateMappedRange,nvnBufferInvalidateMappedRange) holds GPU results. From a range's first invalidate on, a binding wholly inside it is bound in guest memory instead of the VRAM mirror, and the guest page table points there too. Ranges are rounded to pages.wb_pages_note, so the CPU's read still waits for that submission.RX_VK_RB_DIRECT=0restores the old behaviour. It only applies when mirrors are on.Measurements
RX 7800 XT, RADV (Mesa 26.2.6), Ryzen 7 7800X3D, Linux, ~2150-draw Kokiri Forest balcony view, default settings, base
7dbdcdc. Only the renderer.sodiffers, runs were alternated, and frames were presented through a headless weston on the same GPU.RX_VK_RB_DIRECT=0)A 4-minute run averaged 54.7 fps over 13,146 frames with 0 flashes and no device loss. Direct bindings average 3.4 KB, against about 400 KB in the first version. The spike count takes every frame's mean brightness. With the read-back wait removed (
RX_VK_NO_READBACK_WAIT=1) it reports 270-360 spikes a minute, so it can see the bug.Limits
direct_write_epochupdate.Tests
tests/test_rb_direct.pychecks which bindings go direct, the range list's limits and reuse, and (from the source) the read-back wait, the size cap and the pool retirement.tests/run_all.pyhas the same two failures as main (nx2/test_nup_read_name.pyand the byte-compile step), plus this test passing.🤖 Generated with Claude Code
Thanks for this, the fps numbers are interesting and the code is tidy. What checks out:
ParLock->pools_mu->rb_direct_muinrb_direct_apply, the direct bind path,mirror_makeand the write-back, andmemory_rb_direct_noteonly takesrb_direct_mu. No inversion that I can see.rb_direct_hit) is fine for lock-free readers.cut_rb_direct's page math handles runs that start or end mid-page, and the test covers that.mirror_makepointing the direct pages back at guest memory is the right spot, andRX_VK_RB_DIRECT=0is a clean way out.My main concern is that I think the speedup comes from removing the readback wait that fixed the exposure flashes, not from skipping the write-back copy:
invalidate()waits onmemory_writeback_value(), which only knows pages fromwb_pages_note(). That's called from the write-back runs (aftercut_rb_direct, so never for direct pages) and from the small in-place writes path. The new direct path doesn't call it. So the CPU can now read OoT's exposure meter, and set it to 0, before the replay has even recorded that frame's adds. This is the 0 / 2x pattern described in the EvilBoyce comment ininvalidate(). Also, the PR body says the GPU still sees what the CPU wrote as of the submit, but that's not true for direct pages. The GPU reads live guest memory there, so the CPU's zero for frame N+1 can land before frame N's adds. The "big flashes" count may not catch the small 0.2 EV steps, so it would be worth checking exposure values directly (RX_VK_LOG_READBACK).rb_direct_hitis a plain overlap test against page-rounded ranges. So a bigger SSBO or UBO binding that only touches a readback page gets bound entirely in guest memory. Its GPU writes outside the readback pages never reach the mirror, and later mirror-bound or page-table reads see stale data. Its reads see live CPU writes instead of the submit-time capture, which is the tearing the mirror exists to prevent. The 4 KB rounding also pulls in neighbouring data in the same pages.cut_rb_directonly ever drops real data. Direct bindings never add write-backs, so the only write-back runs that can cover a direct page come from mirror bindings recorded before that range's first invalidate.invalidate()runs on the game thread, ahead of the replay. Cutting those runs throws away the GPU's result for the first readback.Smaller points: ranges and pages never expire, so memory reused after a scene change stays direct for good, and a game with ring-buffer readbacks fills the 512 entries quickly. The new test isn't in
ARGSintests/run_all.py(inline comment). This overlaps #1133, which tracks writes from direct bindings ingpu_buffer_bind(the new early return skips that), and #1184, which touchesmirror_makeand the parallel segments. #1109 and #1069 are merged now, so a re-test on current main would help.Suggestion: keep the direct binding but call
wb_pages_note(g0, g0 + size, mem_rec_value)for written direct bindings so the CPU still waits, only go direct when the binding sits fully inside direct pages, and drop the cut (or only cut pages that were direct when the submission started). Then measure again to see how much of the gain is left.@ -2428,0 +2487,4 @@if (!rb_direct_on() || !size || size > kRbDirectMax) return;std::lock_guard lk(rb_direct_mu);const uint64_t lo = guest & ~0xFFFull, hi = (guest + size + 0xFFF) & ~0xFFFull;if (!rb_direct_range_add(lo, hi)) {This sends the whole binding direct if it overlaps a readback range at all. GPU writes to the rest of the binding then only land in guest memory: they aren't in
writebacksand the mirror keeps stale data. Reads of the rest see live CPU writes instead of the capture. I'd only go direct when[g0, g0 + size)sits entirely inside direct pages, and fall through otherwise.The in-place path below calls
wb_pages_note(...)for written bindings soinvalidate()waits for the GPU. This path doesn't, and the write-back that used to note these pages is cut, somemory_writeback_value()never returns a pending value for them. Something likeif (written) wb_pages_note(g0, g0 + size, mem_rec_value);before returning would keep the wait.Since every overlapping binding now goes direct, a write-back run can only cover a direct page if the GPU wrote it into the mirror, from bindings recorded before
memory_rb_direct_noteran on the game thread. Cutting it drops that result, so the first readback after the switch reads stale data. I think this cut should go, or only apply to pages that were already direct when the submission started recording.Pages added here are never removed, so a range stays direct after the game frees or reuses that memory (a scene change), and ring-style readbacks at moving addresses will fill the 512 slots. Maybe drop the ranges of a pool when it's retired?
@ -0,0 +67,4 @@with tempfile.TemporaryDirectory() as d:src, exe = Path(d) / 't.cpp', Path(d) / 't'src.write_text(fixture, encoding='utf-8')subprocess.run([args.cxx, '-std=c++20', '-O1', '-o', str(exe), str(src)], check=True)Could you add
"tests/test_rb_direct.py": ["--cxx", "{cxx}"],toARGSintests/run_all.py, like the other C++ fixture tests? Then a runner withoutg++skips it instead of failing. Also, this only covers the run cutting. The changes in behaviour (binding choice, readback wait) aren't tested.NVIDIA / Windows: a large slowdown here. RTX 3070 8 GB, Windows 11, driver 0x98960000, main
09e4eb5merged with this PR (clean merge), defaultRX_VK_MIRROR_MB(2048 on this card). The setup:RX_VK_FRAME_LOG;The path is active:
read-back direct: 20001 bindings, 375-480 KB each.Where the time goes. I ran both builds with #1243 on top, which times a capture's GPU wait for its late pages apart from the rest:
So on this card about every sixth capture waits ~135 ms for the GPU. That is ~22 ms a capture on average, about six captures a frame. The replay thread mostly sits in
vk_timeline_wait(c->late_value).My reading: the directly bound read-back ranges are written by the GPU straight into imported guest memory. The pages they share with mirrored data then become "late", and the next captures wait for the submission that writes them. The same wait shows, much smaller, when mirrors are short (#995). On RADV the wait may be short enough not to show. On the NVIDIA Windows driver, GPU writes to imported host memory seem to make it long.
No device lost, the picture matches main (exposure looked the same), just the frame rate.
On Windows with NVIDIA this makes the GPU about 18 times slower in Kokiri Forest.
RX_VK_RB_DIRECT=0brings it back.Setup. RTX 4080 SUPER, driver 616.92, Windows 11, Ryzen 7 5800X3D. Built from GitHub's
pull/1150/merge(ce164ce on016d582) with llvm-mingw. The save is near Saria's spot in Kokiri Forest, loaded by anHOS_INPUT_SCRIPT, then the camera stands still. Output 3840x2160, 3D at 67%, game's own upscale. The same build was run twice, back to back:vk: GPU timeafter the save loadsRX_VK_RB_DIRECT=0The title screen and the load look normal (GPU 0.2-6 ms a frame). The slowdown starts once the scene is drawn and stays. Main without this PR runs the same scene at 7-12 ms of GPU a frame.
My guess, not verified: on this setup guest memory is imported host memory, so every shader read of those ranges crosses PCIe. If something reads the exposure sums per pixel or per draw, that adds up fast, while it may be cheap on RADV. I have not run
RX_VK_PASS_TIMINGto name the pass yet.Not checked: the AMD/Linux case the PR measured, and whether only some of the bound ranges cause it.
@itaskaev2's numbers match my logs, and they correct my reading above. In the same runs, the log's
vk: GPU timeline gives:vk: GPU timea frame (longest submission)So the cause is GPU-side: the work itself gets ~13x slower once those ranges are bound in imported host memory, plausibly the per-pixel or per-draw reads over PCIe that @itaskaev2 suggests. The capture's late-page wait I measured (22 ms) is just the replay thread waiting for that slow submission, not a separate cost. Sorry for the misleading framing.
One data point from AMD / Windows, because the reports above are NVIDIA / Windows and show a large GPU slowdown: I do not see that slowdown on an RX 7800 XT. RX 7800 XT 16 GB, Ryzen 7 5800X3D, Windows 11, 2560x1440, frame generation x2, v2.0.11 with this PR merged on top (clean merge; the same build also has #1267 and some AMD changes of mine), normal play with a save, about 12000 presents.
From the log's
vk: GPU timeline: 7.5 ms a frame on average over 88 samples, 16.2 ms at most, longest submission 30.6 ms at most, and 10-16 ms in the heaviest scenes I walked through. That is the same range as my sessions without the PR (9-13 ms). The log showsread-back direct: 2875 pages, then5688 pages, so the path is active.I have no controlled A/B (no scripted route,
RX_VK_RB_DIRECT=0not run on the same scene), so I cannot say it makes anything faster here, only that the 18x slowdown does not show up on this card. No device lost in the log; I have not looked at the exposure the description warns about.Thanks all, this was very useful. hckrman101's main concern was right, and I've reworked the PR and put it in draft.
Review points
wb_pages_note. It cost about a third of the gain.cut_rb_direct: removed.ARGS, and they now cover the binding choice and the wait.7dbdcdc, so all three are included. #1133 is still open, and itsdirect_write_epochupdate would be needed in this path if it lands first.Exposure:
RX_VK_LOG_READBACKlogs before the wait, so I compared every frame's mean brightness instead. A spike is a frame that differs from both neighbours in the same direction. Removing the wait gives 270-360 spikes over 2% per minute. Main, the first version and this version all give 0 at this view. That is one static view, so the wait stays regardless.Result on RX 7800 XT / RADV: main 51.3, 51.6, 49.9 fps, this version 56.9, 54.2, 54.2. A 4-minute soak averaged 54.7. I also tried exact, unrounded ranges, and the gain went away (51.2-51.8 fps), so ranges stay page-rounded.
NVIDIA: thanks Nitnek and itaskaev2 for the GPU time numbers. They point to the GPU work itself being slow in imported memory. Only ranges up to 64 KB now go direct, which brings the average direct binding from about 400 KB to 3.4 KB. I can't test NVIDIA here, and this may not be enough if the cost is per-pixel access to small ranges such as the exposure sums. If either of you has time, could you rerun with this version and post the
vk: GPU timeline? If it's still slow, the options are limiting this to AMD or dropping it.Thanks also lucasmazzoni45-del for the AMD/Windows data point.
🤖 Generated with Claude Code
Thanks for the rework, this is much closer. Everything from my last review is addressed:
wb_pages_note, soinvalidate()waits again. UsingRX_VK_NO_READBACK_WAIT=1as a control for the spike count is a nice way to show the measurement can see the bug.cut_rb_direct: gone, so mirror write-backs are kept.ARGS, and they cover the binding choice, with source checks for the wait, the cap and the retirement.What's still open:
RX_VK_RB_DIRECToff on NVIDIA (vendorID 0x10DE) would keep the RADV gain without the risk.tests/run_all.pyonly. YourARGSline is next to main's newtest_present_geometry_trace.pyentry, so keep both.vk_memory.cpp,vk_replay.cppandvk_internal.hmerge cleanly, including #1184'smirror_inheritchanges inmirror_make.CI is green. I'm leaving this as a comment while it's a draft waiting on the NVIDIA retest. On the code side I'd approve once that's in, or once it's gated off on NVIDIA.
One coherence gap is left: the mirror's copy of a direct page goes stale. GPU writes here land in guest memory and don't bump the page generation, so
mirror_syncnever copies those pages again. The mirror keeps what it had atmirror_makeor at the CPU's last write to the page. Two things follow:RX_VK_WB_DIFF=0copies the whole run and can wipe that submission's direct results.The same thing happens once per range on the switch-over: bindings recorded before the first invalidate are mirrored and written back over the direct ones later in that submission. It happens again after a pool retirement drops the range. That costs one wrong read-back each time.
I doubt OoT hits the steady-state case. A cheap check would tell you: on the mirror path, a written binding that overlaps
rb_direct_pages, logged once. If it does show up, sending that binding direct too, or keeping the range mirrored, would close it.Tested on AMD / RADV.
Setup: RX 9070 XT (RADV, Mesa, Vulkan 1.4), Ryzen 9 9950X3D, NixOS. Linux build of
mainat51de681+ #1858's fix (be8b512) + this PR cherry-picked (therun_all.pyconflict resolved,RUN_ALL_ARGSmoved intotest_rb_direct.py, and thedirect_write_epochline you mention for #1133 added in the early return). AMD FSR 4, 3840x1080 -> 5120x1440. Same Kokiri Forest area each run, default settings otherwise; flashes checked withRX_VK_FRAMELOG+scripts/framelog.py.mainmain+ this PRNo regression, no visible flashes, and a small fps gain here.
Retest of the reworked 64 KiB-capped version: the large NVIDIA slowdown is still present on this RTX 3070.
Tested exact head
a5bee1bec1484c9e10f07a799ee5ae216414d0b8, native Windows 11, Ryzen 7 5800X / RTX 3070 8 GB, NVIDIA 610.88. These runs predate this morning's Windows update. Same EXE/DLL, copied save/settings, custom 1280×720 with frame generation off, matched stationary view; onlyRX_VK_RB_DIRECT=0/1changed between the measured arms.RX_VK_RB_DIRECTvk: GPU timeper frameThe B logs confirm the direct path was exercised. A2 returned to the fast regime after 60.685 s of scene warm-up, with 62.294 s measured. Both OFF runs being fast, while both ON runs have much higher GPU time, supports the slowdown association in this scene even after the binding-size cap.
Limits: A2 began 63.26 minutes after B2, with intervening activity/cache history: this was not a continuous ABBA. Sparse matched captures do not establish flash-free rendering or direct/mirror coherency. Initial preparation reported 83 failures, retained as a qualification limitation rather than erased by the later cached run. A2 was deliberately stopped at its bound (game exit 1); its native controller exit code was not recorded. These are performance observations, not a clean shader/correctness/stability pass or a claim about all NVIDIA cards/scenes.
For this RTX 3070, the 64 KiB cap is not sufficient. I would keep this path disabled on NVIDIA pending further work; this does not invalidate the separate AMD results. No FPS gain is claimed from A2 versus A1: both are OFF controls.
AI-assisted testing and analysis with Hermes Agent / Claude Code; measurements come from the recorded native runs.
Two things before this comes out of draft.
First, @Nitnek retested this exact head (
a5bee1b) on an RTX 3070. GPU time still goes from about 13 ms to about 151 ms a frame with the direct path on. The path is on by default wherever mirrors are on: rb_direct_on() in gpu/vk/vk_memory.cpp:2010 returns true when RX_VK_RB_DIRECT is unset. Unless the cause is found, it should default to off on NVIDIA (vendorID 0x10de).Second, that switch is read with atoi. Since #2042, switches go through env_value_off in gpu/vk/compat/hookshot_env.h.
The branch also conflicts with main in vk_memory.cpp, vk_replay.cpp and run_all.py. Since this branch's base, main has merged:
So the rebase needs care around the mirror and write-back paths. test_rb_direct.py passes on the head as it stands.
WIP: vk: small ranges the CPU reads back are bound in guest memoryto vk: small ranges the CPU reads back are bound in guest memoryI rebased this onto current main and applied the review points on a branch in my fork: fix/pr1150-readback-direct.
rb_direct_on()now defaults off on NVIDIA (vendor 0x10de); an explicitRX_VK_RB_DIRECTvalue overrides either way. It still requiresmirror_on(), so #2343 turns it off on GCN too.atoireplaced byenv_value_off.wb_pages_notecall no longer matched main, so the direct path now callsnote_direct_write(*p, g0 - p->lo, size)(setsdirect_write_epoch, notes the write-back). That goes slightly beyond the original change, so please look at it.tests/test_direct_buffer_writes.pyfixture needed stubs for therb_direct_*functions;test_rb_direct.pyalso checks the NVIDIA default and explicit overrides.Tests pass on Windows (llvm-mingw) apart from 4 failures that also fail on main here. Not measured: no in-game or NVIDIA run, so the NVIDIA default is untested on hardware.
🤖 Generated with Claude Code
Update to my branch fix/pr1150-readback-direct:
rb_direct_on()now defaults to AMD only (vendor 0x1002) instead of 'everything but NVIDIA', so Intel Arc and other untested vendors keep the old path. An explicitRX_VK_RB_DIRECTstill overrides either way andmirror_on()is still required.tests/test_rb_direct.pychecks AMD on, NVIDIA and Intel off by default, and the overrides. No hardware run of the direct path was made.🤖 Generated with Claude Code