perf(vk): add zero-drain host-cached AutoReadback ring for 1x1 luminance textures #2462
Open
Erfboom wants to merge 4 commits from
Erfboom/perf/vk-zero-drain-readback into main
pull from: Erfboom/perf/vk-zero-drain-readback
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!2462
Loading…
Reference in a new issue
No description provided.
Delete branch "Erfboom/perf/vk-zero-drain-readback"
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?
Problem
The Legend of Zelda: Ocarina of Time frequently renders 1x1 RGBA32F luminance / exposure metering and occlusion textures on the GPU and polls them on the CPU through
nvnMemoryPoolInvalidateMappedRange/texture_flush_to_guest().Previously, every poll triggered
flush_texture(), forcing a synchronous copy and waiting on the entire submission queue to idle or drain, stalling both CPU and GPU execution each frame (~0.5ms–1.2ms queue stall per readback in outdoor scenes).Solution
Texture::AutoReadback) backed by host-cached VMA memory (create_buffer_cached) for small GPU-written textures (guest_size <= 64 KB, 1 level, 1 layer, 2D).texture_end_recording(), if anAutoReadback-enabled texture was dirtied in the recording, append an asynchronousvkCmdCopyImageToBufferinto the next ring slot and issue aVK_PIPELINE_STAGE_2_HOST_BITmemory barrier.texture_flush_to_guest(), if the ring slot is ready and not dirty in the currently open recording, wait on the slot's timeline semaphore value. If the timeline wait completes, finish the flush directly out of the host-cached mapping (f.alloc = rb.allocfor mapped cache invalidation); otherwise, fall back cleanly toflush_texture()to prevent stale data.flush_texture()if readback is disabled viaRX_VK_NO_AUTO_READBACK=1(read viaenv_on) or if the texture is written outside normal recording flow.test_fsr_image_retirement.pyandtest_env_on.py.Measurements in OoT
Tested on Linux (x86-64-v3, Vulkan / RADV):
flush_texture()wait: ~0.5ms–1.1ms per readback event (4–6 events per frame during lighting transitions).I ran the suite on this merged into main, since CI hasn't run here. Blocking issues:
Flushwithout settingf.alloc(vk_texture.cpp:3274).flush_finishthen callsinvalidate_mapped(f.alloc, total)(:3056), and VMA dereferences a null allocation on the first readback served from the ring, with default settings.test_fsr_image_retirement.py(no stub fortexture_end_recording) andtest_env_on.py(RX_VK_NO_AUTO_READBACKat:3261usesgetenv, notenv_on).vk_timeline_wait(:3269) returns and the slot is read anyway, so the game can silently get a stale value.texture_markedchange (:3439) also changes when parallel segments take the lock.Thanks for the thorough review and catch! I've addressed all points in
676dfde2:Crash (
f.allocdereference):f.alloc = rb.alloc;before passing toflush_finish(f, true).flush_finish(),invalidate_mappedis guarded withif (ok && !flush_uncached() && f.alloc)to safeguard against null allocations.Test Failures:
test_env_on.py: SwitchedRX_VK_NO_AUTO_READBACKcheck to useenv_on("RX_VK_NO_AUTO_READBACK").test_fsr_image_retirement.py: Added the missingvoid texture_end_recording(VkCommandBuffer, uint64_t) {}test stub intests/fsr_image_retirement_test.cpp. All 28 test cases pass cleanly.Timeline Timeout / Stale Value:
vk_timeline_wait(rb.slot_val[slot]), we verifyif (vk_timeline_completed() >= rb.slot_val[slot]).served_from_ringstaysfalse, invalidating the ready state and cleanly falling back to synchronousflush_texture(*t, lo, hi)instead of serving stale data.Parallel Segment Lock (
texture_marked):texture_markedto its original condition:!tex || (tex->gpu_written && !tex->released && tex->uploaded && tex->owned_frame == frame_no && tex->first_render_frame).Description / OoT Measurements:
texture_flush_to_guest().flush_texture()stalls averaged ~0.5ms–1.1ms per readback event (4–6 events per frame during lighting transitions), draining the submission queue.Thanks for the quick turnaround. I went through
676dfdeagainst @Joey-Magnus's review: the nullf.alloccrash is fixed (f.alloc = rb.allocplus the guard inflush_finish), thefsr_image_retirementstub and theenv_onswitch are in, a timed-out wait now falls back toflush_texture, and thetexture_markedchange is gone. Those points look resolved to me. His request for an OoT measurement got numbers in the description, but see below.What's still open:
invalidate_mapped(alloc, size)always invalidates from offset 0 (vmaInvalidateAllocation(vma, allocation, 0, size)in vk_vma.cpp), but the fast path reads atslot * slot_sz. On a driver that hands out cached memory that isn't host-coherent, three reads out of four come from stale cache lines. Coherent memory hides it, so it won't show on most desktop GPUs. Inline note below.texture_flush_to_guestalready catch up before they call it: the invalidate path runssync_settle()/wait_queue_empty(), andvmap_changingrunswait_queue_empty(). So the ring saves the separaterb_cmdsubmit and wait, not a queue drain. That may still help, but the measurements need something reproducible. The existing log linevk: virtual pages ... remapped: ... (%.1f ms; ... ms in all)already reports the time spent in this function. A before/after of that line, with and withoutRX_VK_NO_AUTO_READBACK=1in the same scene, would back up the numbers. Right now the 0.5-1.1 ms and 0.02 ms figures have nothing showing where they came from.texture_end_recordingcomes beforememory_end_submissioninfinish_recording, the fast path setsf.allocand falls back whenvk_timeline_completed()is short, anddestroy_latercallsauto_rb_forget. That would keep these fixes from regressing.Smaller points:
bench_replay,valueispushed, which is already reached by definition ("no slot waits on it"). With RX_VK_BENCH, the fast path would treat the slot as complete before the copy runs. This only affects the dev tool.Overlap with your other open PRs: #2467 also edits
texture_touchin vk_texture.cpp, about 30 lines away from this hunk, so there's no textual conflict and they don't interact functionally. #2469, #2482, #2487 and #2491 touch other code.nit:
valuehere ispushed, which is already reached, sovk_timeline_completed() >= slot_valholds before this submission has run. Under RX_VK_BENCH the fast path could read the slot before the copy lands. Passing 0 (which leaves the slot not ready) or skipping the ring in bench mode avoids that.invalidate_mapped(f.alloc, total)inflush_finishinvalidates[0, total)of the allocation, butf.mappoints atslot * slot_sz. For slots 1-3 on non-coherent cached memory, the range being read never gets invalidated. Either invalidate[slot * slot_sz, +total)here (VMA rounds it tononCoherentAtomSizeitself) and leavef.allocnull forflush_finish, or add an offset-takinginvalidate_mapped_rangenext toflush_mapped_rangein vk_vma.cpp.Fixed in
bc3fd7f1: addedinvalidate_mapped_range(VmaAllocation, uint64_t offset, uint64_t size)invk_vma.cpp, addedoffsettoFlush, and updatedflush_finishto callinvalidate_mapped_range(f.alloc, f.offset, total)withf.offset = (uint64_t)slot * rb.slot_sz.Fixed in
bc3fd7f1:bench_replaynow passes0(texture_end_recording(r.cmd, 0)), which clearsdirty_in_recand setsrb.ready = falsewithout recording a copy.Thanks @hckrman101 for the thorough follow-up review!
1. Code Fixes in
bc3fd7f1vk_vma.cpp,vk_internal.h,vk_texture.cpp):invalidate_mapped_range(VmaAllocation allocation, uint64_t offset, uint64_t size)next toflush_mapped_rangeingpu/vk/vk_vma.cpp.uint64_t offset = 0;tostruct Flush, setf.offset = (uint64_t)slot * rb.slot_sz;intexture_flush_to_guest's ring fast path, and updatedflush_finishto callinvalidate_mapped_range(f.alloc, f.offset, total)so slots 1–3 invalidate[slot * slot_sz, +total)on non-coherent host-cached memory.bench_replaypassing0(vk_replay.cpp,vk_texture.cpp):bench_replayto pass0(texture_end_recording(r.cmd, 0)) and updatedtexture_end_recordingto check!value, clearingrb.ready = falsewithout stamping an already-reachedpushedtimeline value underRX_VK_BENCH.kMaxUnpolledWrites = 64) (vk_texture.h,vk_texture.cpp):unpolled_writestracking toTexture::AutoReadback, reset to0when polled intexture_flush_to_guest. If a texture is written across more than 64 recordings without a CPU poll,texture_end_recordingcallsauto_rb_forget(*t)to retire the ring and stop insertingALL_COMMANDS -> COPYbarriers and copies on one-off flushes.tests/test_auto_readback_ring.py):tests/test_auto_readback_ring.pycheckingfinish_recordingordering (end_pass->texture_end_recording->memory_end_submission),bench_replaypassing0,destroy_latercallingauto_rb_forget, slot-range invalidation viainvalidate_mapped_range(f.alloc, f.offset, total), timeline timeout fallback, and unpolled ring expiry.2. In-Game OoT Log Verification (
vk: virtual pages ... remapped:)When we ran a live 2,760-frame test pass across Hyrule Field, Market Town, Guardhouse (Pots), and Hyrule Castle to capture a clean before/after of
vk: virtual pages ... remapped:, we found thattexture_flush_to_guestfired 0 times (done == 0) across the entire run.Tracing why:
mkw-recomp(Mario Kart World), the game computes scene luminance in a 1x1 RGBA32F texture and invalidates its virtual pool range (vk_texture.cpp:2715), hittingtexture_flush_to_guest.vk_replay.cpp:2277: "OoT reads its exposure meter (a sum the GPU adds to each frame and the CPU reads and sets to 0) this way"), which goes throughmemory_writeback_valueinvk_memory.cppand logs: (That buffer readback path is what #2458 targets.)So in full transparency: while all the code review fixes and the source test in
bc3fd7f1are in place and verified clean in-game, OoT does not poll 1x1 luminance textures throughtexture_flush_to_guest. If you'd prefer not to carryAutoReadbackinvk_texture.cppwithout an active OoT call site, I am completely happy to close this PR in favor of #2458!Thanks for tracing it through and saying so plainly. Your 2,760-frame run shows
texture_flush_to_guestis never called in OoT: the exposure meter is a buffer read throughmemory_writeback_value, which #2458 (merged) covers. So this ring has no call site in the game we ship, and I'd take your offer and close it in favor of #2458.If you'd rather keep it open, three things first:
texture_flush_to_guest, and its "Measurements in OoT" section (0.5-1.1 ms before, 0.02 ms after) can't come from a path that runs 0 times. Please remove those numbers. Only post measurements from a run you made, with the log line they came from.vk: virtual pages ... remapped:line with and withoutRX_VK_NO_AUTO_READBACK=1, as hckrman101 suggested.vk_replay.cpp,vk_texture.cppandtests/fsr_image_retirement_test.cpp. (At its headtest_env_on.pyalso fails, onHOS_FPS_LOG, but that file isn't yours and main has fixed it.)The fixes for the review points (the
f.alloccrash, the timeout fallback, slot-range invalidation, ring expiry and the source test) look right to me. It's the case for carrying the code that's missing.