Repository navigation
perf(vk): sparse VRAM mirrors, memory only where a game pool is (RX_VK_MIRROR_SPARSE=1) - #2794
SadTransGirl wants to merge 1 commit into
Conversation
…K_MIRROR_SPARSE=1) A pool's VRAM mirror covers its import's whole range, made by chunk around what was bound: 243-533 MB of the 1.1-1.6 GB of mirrors (DLAA 2560x1440) held no game pool, chunk padding and pools the game had let go. With RX_VK_MIRROR_SPARSE=1 (off by default) a mirror is a sparse-residency buffer whose 64 KB blocks get memory only where a game pool is (gpu/vk/sparse_mirror.h). A block with none is reached in guest memory: its page-table entries point there, and a binding that reaches one is served from guest memory for that submission. At each submission's start, blocks a binding wanted, and every block a new game pool is in, get memory in one batched sparse bind and are filled as a new mirror is (guest copy, captured pages over). A block that no game pool has been in and no binding has reached for 600 submissions gives its memory back once the GPU is past it; wanted again before that, it takes it back without a bind. Write-backs and inheritance copies are clipped to blocks with memory; a written range served from guest memory is copied in again, and inheritance leaves such pending ranges to guest memory. The binds are not waited for on the CPU (each waited for the whole queue, ~3 ms): they signal a timeline semaphore that every submission of recordings waits for on the GPU, and memory an unbind let go is freed once the semaphore has passed it. RX_VK_SPARSE_WAIT=1 waits on the CPU as before. vmaFreeMemory joins the recorder's drain boundaries. Needs strict sparse residency for buffers. Measured on an RTX 4070 SUPER (Windows 11, 617.42), the same build with and without the switch, unattended: - 6 GB allowance (RX_VK_TEST_ALLOWED_MB=6144, RX_VK_MIRROR_MB=1536), five-minute field walk, three pairs: 57.3 / 55.3 / 58.2 fps against 47.7 / 51.5 / 49.9; frames over 25 ms 625 / 1160 / 408 against 3042 / 1840 / 2114. - 8 GB allowance (6900 MB): 59.1-59.3 fps against 59.0-59.1; VRAM peak 6676-7018 MB against 7028-7138. - Castle Town to the field: 55.4 / 52.5 / 54.8 fps against 54.0 / 52.7; 790-860 MB of mirrors spared; the load's longest frame 866-961 ms against 816-902 (1197-1232 with RX_VK_SPARSE_WAIT=1). - Kokiri Forest: VRAM 7462-7507 MB against 8215. Two independent code reviews; their findings are fixed. Screenshots of Castle Town, Kokiri and the field look alike either way; no device lost. tests/test_sparse_mirror.py runs the header and checks the wiring; test_direct_buffer_writes' and parallel_init_order's fixtures get the helpers they now reach; test_readback_part counts the new reads of guest memory. docs/tested-configurations.md has the runs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Joey-Magnus
left a comment
There was a problem hiding this comment.
I merged this into current main and ran tests/run_all.py. test_draw_stream_visibility and test_fsr_image_retirement fail here exactly as they do in CI, and they fail because of this change. Both cut submit_recording (and queue_flush_locked, in the fsr one) out of vk_replay.cpp and compile them alone. Those functions now call memory_sparse_wait, which their fixtures don't declare ("use of undeclared identifier 'memory_sparse_wait'"). They need the same bool memory_sparse_wait(VkSemaphoreSubmitInfo*) { return false; } stub you gave parallel_init_order_test.cpp. test_sparse_mirror passes.
It's off by default and gated on sparseResidencyBuffer, residencyNonResidentStrict and a sparse-binding queue, so other tiers aren't exposed. Only the three recording submits wait on the binds' timeline. A sentence on why no other submission (presents, upscalers, texture uploads) can read a newly bound block would help. The 6 and 8 GB rows come from a 12 GB card with a lowered allowance, not real 6/8 GB cards. Your Not tested section says so, but the summary reads as if they were real. Conflicts: #2618 in mirror_make, and #2795 only in docs.
perf(vk): sparse VRAM mirrors, memory only where a game pool is (RX_VK_MIRROR_SPARSE=1)
On cards short of video memory the buffer pools' VRAM mirrors hold hundreds of MB that no game pool is in. This makes them sparse, opt-in. A 6 GB card's field walk went from 47.7-51.5 fps to 55.3-58.2 fps over three pairs. At 8 GB and more there's no fps change, but the VRAM peak is lower. Loads are no slower.
Cause. A pool's mirror covers its import's whole range, and imports are made by chunk around what was bound (
import_chunk). A temporary measuring patch counted, per 64 KB block of the live mirrors, whether anyguest_poolsentry was in it. Of 1.1-1.6 GB of mirrors, 243-533 MB had none (DLAA 2560x1440: Hyrule Field, Kokiri, and up to 533 MB after Castle Town to the field): chunk padding, and pools the game had let go. On an 8 GB card's allowance (6,900 MB) the field walk peaked at 6,952 MB, with 321 frames over 33 ms in five minutes.Change (
RX_VK_MIRROR_SPARSE=1; off by default, and only with strict sparse residency for buffers):gpu/vk/sparse_mirror.h: which blocks a game pool is in, block spans, runs, and copies clipped to blocks with memory or cut by ranges.gpu/vk/vk_vma.cpp: a sparse buffer, and its blocks' memory from VMA (counted in its budget).vmaFreeMemoryjoins the recorder's drain boundaries (vk_recorder.h,vk_work.h).mirror_make: a sparse buffer, with memory for the blocks a game pool is in. The page table points blocks with none at guest memory, so the shaders' pointers reach the game's own memory there, as for a pool with no mirror.gpu_buffer_bind: a binding that reaches a block with none is served from guest memory for that submission. It's noted as an in-place read, or, if written, as a direct write with its shadow forgotten, and the block is wanted.guest_pools_gen), get memory in one batchedvkQueueBindSparse.RX_VK_SPARSE_WAIT=1waits on the CPU, as before.Measured. RTX 4070 SUPER 12 GB (617.42), Windows 11, DLAA 2560x1440, unattended, the same build with and without the switch:
RX_VK_MIRROR_MB=1536), field walk, three pairs: fpsRX_VK_SPARSE_WAIT=1)Screenshots of Castle Town, Kokiri and the field look the same on and off. No device was lost and no write-back warning appeared in any run. The rows are in
docs/tested-configurations.md.Two independent code reviews of earlier versions found ten problems in all, each fixed as described above:
Tests.
tests/test_sparse_mirror.pyruns the header: live blocks, an earlier long pool, partial blocks, spans, runs, clipping across both sides' block ends, and cut ranges. It also checks the wiring in vk_memory.cpp, vk_vma.cpp and vk_replay.cpp. The fixtures oftest_direct_buffer_writesandtest_parallel_init_orderget the helpers they now reach.test_readback_partcounts the three new reads of guest memory, each noted. The checks that read these files pass, except 18 that fail the same way on plain main on this PC (their compiler wrappers).Overlap with open PRs. It merges cleanly with #2711 and #2760. With #2618 (a mirror that found no VRAM asked for again) it conflicts in the same lines of
mirror_make(where a whole mirror's buffer is made): no conflict in what they do, since a sparse mirror that cannot be filled falls back to that same whole buffer, where #2618's retry would apply. Whichever lands second takes a small merge, which I'll do if it's this one. #1377, #2338 and #2609 conflict with main already.Not tested. AMD (RADV and Windows), Intel, Linux; a real 6 or 8 GB card; a long session.
🤖 Generated with Claude Code