perf(vk): the all-zero check of a big texture reads only what the game wrote of it (Linux) #567
No reviewers
Labels
No labels
area/accessibility
area/audio
area/input
area/install
area/menu
area/renderer
area/saves
area/tooling
area/upscaling
beanstalk: approved @ 1697603
beanstalk: approved @ 2c5a62e
beanstalk: approved @ 4fd82b7
beanstalk: approved @ 64d6d09
beanstalk: approved @ 6f91fae
beanstalk: approved @ 70a78f9
beanstalk: approved @ 8015c9a
beanstalk: approved @ 8d361dc
beanstalk: approved @ 9d8356e
beanstalk: approved @ abf2227
beanstalk: approved @ d82f202
beanstalk: approved @ e5f1e40
beanstalk: approved @ ee8ab5f
ci: approved
good first issue
gpu/amd
gpu/igpu
gpu/intel
gpu/nvidia
help wanted
kind
bug
kind
chore
kind
docs
kind
feature
kind
performance
kind
question
needs measurements
platform/linux
platform/macos
platform/steam-deck
platform/windows
preview
priority
critical
priority
high
priority
low
regression
status
blocked
status
confirmed
status
duplicate
status
invalid
status
needs-info
status
wontfix
testing wanted
ci: approved
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
MagicBeanDev/Longshot!567
Loading…
Reference in a new issue
No description provided.
Delete branch "p0lux/Longshot:perf/zero-upload-holes"
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?
Type: perf · Risk: low · Tested on: Linux (SteamOS), Steam Deck OLED (RADV VANGOGH) · Needs: none
Why
When an area loads, the renderer reads the whole storage of a big new texture to see whether it is all zero, so it can clear the image instead of unswizzling it. On Linux that read is wasted on storage the game never wrote: the shadow map (12288 x 12288 D16, 288 MB) took 108 ms of the replay thread at each arrival in Kokiri Forest on a Steam Deck, and the read itself made about 190 MB of pages that then stayed in the game's memory. See #388.
What changed
On Linux the all-zero check of a big texture now asks the kernel which parts of the storage the game ever wrote, and reads only those; the rest is known to be zero without a read. On Windows, and where the question has no answer, all of the storage is read as before.
upload_all_zero,gpu/vk/vk_texture.cpp). On Linux the guest window is a memfd's (host/hos_main.c), and a part of it never written, or given back since (MADV_REMOVE), is a hole of that file: it reads as zero, and reading it through the mapping makes its pages. The check now asks the file which parts hold data (lseekSEEK_DATA/SEEK_HOLE) and reads only those. The log'sis all zero: cleared on the GPUline says how many MB were read.gpu/vk/guest_holes.h:data_parts(the written parts of a range of a file) andall_zero(the threaded read, moved out ofupload_all_zeroso it can be tested).memory_guest_data_parts(gpu/vk/vk_memory.cpp, declared invk_internal.h): finds the window's memfd the wayimport_dma_bufdoes (hos_posix_guest_memfd) and turns a host pointer into the file's offset. Renderer only: no host change,vk_api.huntouched.threads - 1bytes at the end were never read. The share is now the ceiling.tests/test_guest_holes.cpphas the case (4 MB and 3 bytes on 4 threads).RX_VK_ZERO_HOLESindocs/development.md; one run file indocs/tested/.Measurements
Verdict: it helps one frame and the memory: the all-zero check of the 288 MB shadow map goes from 108 ms to 2 ms at each arrival in Kokiri Forest on a Steam Deck OLED, the load's transition frame from 1,069 to 910 ms, and about 190 MiB of pages are no longer made. The arrival as a whole is not measurably shorter, and nothing changes in Hyrule Field. No frame rate gain is claimed. Passed.
Hardware: SteamOS 3.9.5 (kernel 7.2.7-valve2), Steam Deck OLED: AMD Custom APU 0932, RADV VANGOGH, Mesa 26.2.0-devel, 16 GB shared with the GPU; the GPU's clock fixed at 1600 MHz
Scene: the arrival in an area: a save in Kokiri Forest and one in Hyrule Field loaded by script; no screen (headless), handheld profile, Minimum preset, the game's own upscaler, no frame generation, no cap
Method: the game and host built from main
9c9d69ed; the back end built alone from each commit (clang, same flags) and swapped in that game folder. Each run starts the game and loads the save, withRX_VK_FRAME_TRACE=1. The check's time is the log's own line. Frame times are the trace's, over the load's transition frame (the first frame over 600 ms that uploads over 60 MB) and the 600 presents after it. Memory is/proc/<pid>/statusat the end of the run, about half a minute after the arrival. Main, this PR, and a third renderer (another pull request's), alternated, 3 runs a scene; the median of 3How to reproduce: load a save in Kokiri Forest, then
grep "is all zero" logs/longshot_log.txt;RX_VK_ZERO_HOLES=0for the old readthis PR: perf/zero-upload-holes @ 8fa1f2c1 (this PR's code commit on main
9c9d69ed; on the fork asmeasured/zero-upload-holes-8fa1f2c1). The branch is now on main9e6f231d(ae150395and a docs commit):git range-diffshows the same patch, with only neighbouring rows ofdocs/development.mddiffering, and main has changed nothing inupload_all_zeroor in how the guest window is mapped in between, so the numbers hold. Not measured again on this main.Medians; each run in brackets, in the order run:
9c9d69edRssShmem(the guest window's pages)9c9d69edRssShmemRead with care. This is one check in one frame: it takes 106 ms out of a frame of about a second and gives back about 190 MiB, and that is all it does. The arrival as a whole (6 to 9 s spent in slow frames in Kokiri Forest) does not get measurably shorter, and nothing changes in Hyrule Field, where no texture of 32 MB or more is checked. The 288 MB storage had 16 MB written, so about 272 MB of pages are no longer made by the check; the 188 MiB is what is left of that half a minute later, between runs that differ by up to 158 MiB on main.
Flags and settings
One new A/B switch.
RX_VK_ZERO_HOLES=0=0: the old readTesting
Tested:
python -I -B tests/run_all.pyon macOS with the Vulkan headers given, on the rebased head: 507 passed, 48 failed, 39 skipped. The 48 fail the same way on main9e6f231don that machine (no/proc, noposix_fallocate, Python 3.9, the Linux launch scripts and the like). The suite ran beside four others and 10 more checks timed out in that run (on main too, 10); each passes when run alone.tests/test_guest_holes.py(new): on macOS the read and the "not known" answer; on Linux (Debian, g++ 14, kernel 7.2.9, and on the Deck's kernel 7.2.7) the question against a real memfd: never written, written with zeros, a range not page aligned, given back withMADV_REMOVE, the last page, past the end. It fails on main (no such check) and with the old rounding. The existing checks that readvk_texture.cpp,vk_memory.cppandvk_internal.hneeded no change and pass.a1fdd59c; clang, x86-64-v3, on the Steam Deck): 0 failed. Windows:longshot_vk.dllcross-builds from the same head with llvm-mingw 20261104 on Linux: 0 failed. Not run on Windows.Not tested:
guest_holes::all_zero(the test runs it on plain memory)./dev/udmabuf(anonymous window): the "not known" path is the test's first case only.Must be tested: None: what a hole reads as is the kernel's, checked by the test on any Linux, and Windows keeps the read it had.
Risks
What could break: a texture cleared when it should have been uploaded, if a part of the window held bytes that are not the memfd's at that offset. The check's answer does not change otherwise: a hole reads as zero, and what was written is still read byte by byte. Today the only mapping made in the window is the whole-window one at offset 0 (
hos_main.c;VirtualAllocin it only changes protection, discards areMADV_REMOVE), andmemory_guest_data_partsrefuses a pointer outside the window.lseekmoves the memfd's file position, which nothing reads (the mapping and udmabuf take offsets).Turning it off:
RX_VK_ZERO_HOLES=0is the old read; reverting the commit removes the change.Compatibility: Nothing changes: saves and
longshot_options.iniare untouched, no shader or pipeline cache is rebuilt (no stamped file is touched),MKWVK_API_VERSIONis as it was.Reviewer notes
all_zerois the same loop over the written parts, with at most one thread a MB of data.Issues
See #388
Checklist
Everyone
type(scope): description) of the status line's type.main.python -I -B tests/run_all.pypasses (any failure that also fails onmainis named).third_party/untouched (a new folder with aPROVENANCE.mdinstead).When it applies
Assisted-by:/Co-Authored-By:trailers.docs/tested/for each new measured run (format indocs/tested-configurations.md).mainand passing here.WIP: perf(vk): the all-zero check of a big texture reads only what the game wrote of it (Linux)to perf(vk): the all-zero check of a big texture reads only what the game wrote of it (Linux)Rebased onto main
9e6f231d(heada1fdd59c): no conflict;git range-diffshows the same code patch (neighbouring rows ofdocs/development.mddiffer). The numbers are the ones measured before; the description says why they still hold.The description now follows the new template: the status line, a summary under What changed, the verdict, Flags and settings under Measurements, Must be tested, and Risks with Compatibility.
On the rebased head:
python -I -B tests/run_all.pyon macOS gives 507 passed, 48 failed, 39 skipped, the 48 failing the same way on main9e6f231don that machine; the back end builds for Linux (clang, on a Steam Deck) and for Windows (llvm-mingw 20261104, cross-built), 0 failed.e6430ec2f7a1fdd59cd3a1fdd59cd3e832dd0760Rebased onto
main@bbe6fdf2. No tests were run here; the test boxes stay yours. One conflict in gpu/vk/vk_memory.cpp: the include list (main'slinux_pressure.hand this PR'sguest_holes.h), both kept.Code review only (nothing built or run). This PR duplicates 790 and the two cannot both land.
Both add
gpu/vk/guest_holes.h,tests/test_guest_holes.cppandtests/test_guest_holes.py, both rewrite the zero check inupload_all_zero(gpu/vk/vk_texture.cpp) and both add theRX_VK_ZERO_HOLESswitch (SEEK_DATA/SEEK_HOLE on the guest memfd). The second to merge gets an add/add conflict. This is the older PR and the better base, in my reading: it fixes the share rounding in the old read (a tail of up tothreads - 1bytes was never read whentotal / threadswas already a multiple of 4096), it spreads the threads over the data rather than over the address range, it is rebased, and its lookup sits invk_memory.cppwith novk_api.hchange. The authors should agree on one PR; 790's author should close theirs or rebase onto this one.Code review of this PR, no blocking bug found:
data_parts: offsets are relative tolo;ENXIOmeans no more data; a failed or non-advancing seek clears the parts and returns false, so the caller reads everything. A page in swap counts as data in shmem, so a swapped-out page is read, not skipped.all_zero:peris the ceiling rounded to 4096, sothreads * per >= data; the skip and left bookkeeping across parts is right; the earlyreturnis in a void lambda, fine.memory_guest_data_parts: bounds check cannot overflow, the memfd is looked up once, and non-Linux returns false.Small things, none blocking, and good to take from 790 while merging the two:
lseekcalls. Probably harmless, but a cap is cheap.RX_VK_ZERO_HOLESis read withatoi(...) == 0, sooff,falseornodo not turn it off; only0does. The docs row says=0, so it matches, but the rest of the switches useenv_value_off. Also the static is namedread_holesand is true when the switch is off, which reads backwards.Leaving this as a comment, not an approval, because it cannot merge alongside 790 and the description should name 790 and say which PR supersedes which.
View command line instructions
Manual merge helper
Use this merge commit message when completing the merge manually.
Checkout
From your project repository, check out a new branch and test the changes.