Repository navigation
perf(agc): record occlusion counter dumps on the GPU - #463
Merged
oneandonlydean merged 2 commits intoOct 4, 2026
Merged
oneandonlydean merged 2 commits into
oneandonlydean merged 2 commits into
Conversation
oneandonlydean
force-pushed
the
perf/agc-occlusion-dumps-on-gpu
branch
from
October 4, 2026 03:07
f84e04e to
911ce1f
Compare
Every ZPASS_DONE counter dump drained the device so the CPU could add up the occlusion queries of the finished batches and store the running total. PPSA21564 dumps around each occlusion-tested pass: about 1.5k drains per 10 s at its title screen, and in a level's early passes four drains a frame that took 5.6 s of every 10 s of the graphics worker. The running total now lives in a small buffer the GPU updates in batch order. A batch still ends its occlusion query at submit; a query that saw draws is remembered, and its pool is kept alive until it is folded. A dump ends the open batch's query, copies the remembered results next to the total, adds them with a one-thread compute shader that also stores the 16 DB counters into the dump's host import, and begins a fresh query for the rest of the batch. The dump is ordered with the work around it like any other GPU store, noted as a pending write and stamped. More than 64 remembered results are folded at a submit. Query pools are recycled. A dump into memory the GPU cannot see, under a pending copied write-back or over a label the CPU stores at a completion falls back to the drain, which settles the remembered results on the CPU. Without buffer device addresses the counting stays on the CPU.
…r DCC clear The first draw into a target whose DCC keys hold the register clear code materializes the clear. The GPU clear of the resident image refuses while a recorded write to the keys is still pending, so whenever the title's own key fill had not run yet the clear went the CPU way: the clear texels stored into guest memory (16 MiB for PPSA21564's normal G-buffer) and the image uploaded again. With the occlusion dumps no longer draining the device, the fill is usually still pending at that draw, and that was every frame of PPSA21564's level. The draw now waits for the recorded writes of the key range alone, checks the keys again, and clears on the GPU when they still hold the register code, as it did when the fill had already landed.
oneandonlydean
force-pushed
the
perf/agc-occlusion-dumps-on-gpu
branch
from
October 4, 2026 03:32
911ce1f to
584c5bc
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Every occlusion counter dump (
EVENT_WRITEZPASS_DONE, event 0x39) drained the device. The CPU then added up the occlusion queries of the finished batches and stored the running total into the dump's 16 DB counters. PPSA21564 dumps around each occlusion-tested pass:This PR records the dump on the GPU instead.
How
vkCmdCopyQueryPoolResultswithWAIT_BIT);Graphics/shaders/SampleCounter.comp), which also stores the 16 counters into the dump address through its host import, in the same layout the CPU path writes (DB 0 holds the total, the others 0, bit 63 set on each);When a dump still drains
The drain then settles the remembered results on the CPU, exactly as before.
A second commit removes a cost that becomes visible once the dumps stop draining. The first draw into a target whose DCC keys hold the register clear code materializes the clear. The GPU clear of the resident image refuses while a recorded write to the keys is still pending. Without the drains, the title's own key fill is usually still pending at that draw, so every frame took the CPU way: 16 MiB of clear texels stored into guest memory, then the image uploaded again. The draw now waits for the recorded writes of the key range alone, checks the keys again, and clears on the GPU when they still hold the register code. That is the result it got before, when the fill had already landed.
Why the counters are exact
On the hardware, ZPASS_DONE does not stall the command processor:
WAIT_REG_MEM, or polling the valid bit (bit 63) of each counter.The GPU dump gives the same guarantees:
WRITE_DATAright before the begin dump);The drain stored them earlier than the hardware could, which was correct but slow. The GPU store is no later than any point the title can synchronize on. In PPSA21564, a hardware watchpoint on a counter slot showed the title reading each slot once per use, 45 to 110 ms after the batch holding the dump completed, and the per-slot values were identical with drains and with GPU dumps.
Why this depends on the write-watch fix
When I first moved these dumps to the GPU on my integration branch, PPSA21564's moon and rocks flickered. The cause was not the counters. It was a driver CPU store whose write-watch faults stayed behind, so a later walk re-uploaded a G-buffer over GPU results. The drains had hidden it by letting a pending key fill land first. #462 fixes that; this PR should go in after it.
Tested
Linux (CachyOS, x86-64, RTX 4090): full build with
BUILD_TESTING=ONandctestpass (145/145).Windows (MinGW, CI on my fork): build and
ctestpass.New
sampleDumpTestsinagc_driver_recorder_tests(tests/Recorder.cpp). That binary needs a Vulkan device and is not registered inctest, like the rest of the file; I ran it by hand and it passes. It checks five things:No unit test for the key-fill wait: it needs a recorded key fill still pending at a draw. PPSA21564 exercises it every frame.
PPSA21564 on my integration branch, with these two commits, the write-watch fix and fix(agc): complete a flip only once the frame's GPU work has finished #417, over several runs of my visual regression check (checkpoint frames, flicker, shimmer, video tearing, FPS):
Frame rate on my integration branch, before / after:
Without the key-fill wait, the title is at 38.5 and the menu at 37.3, every frame paying the CPU clear.
No open PR touches the occlusion counters or
materializeRegisterClear. GTA V: the prologue plays (testing branch of the PRs it needs) #426 (a testing branch) and feat(agc): host import windows, least-recent eviction under the budget #290/perf(agc): draw cache keys on the user words a program reads, memoizes failures, serves data-only hits #291 edit the same files and merge cleanly.Checklist
main; no other open PR implements the same functionsNotImplemented_nid_no_patch); silent stubs are listed in TechnicalDebt.mdfiles or images added to the repository (attach them to this PR)