Skip to content

perf(agc): record occlusion counter dumps on the GPU - #463

Merged
oneandonlydean merged 2 commits into
boykopovar:mainfrom
oneandonlydean:perf/agc-occlusion-dumps-on-gpu
Oct 4, 2026
Merged

oneandonlydean merged 2 commits into
boykopovar:mainfrom
oneandonlydean:perf/agc-occlusion-dumps-on-gpu

Conversation

@oneandonlydean

Copy link
Copy Markdown
Collaborator

What

Every occlusion counter dump (EVENT_WRITE ZPASS_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:

  • at the title screen that is about 1.5k drains per 10 s, about 1.5 s of the graphics worker's time;
  • in a level's early passes it is four drains a frame. In Snowy Canyon they took 5.6 s of every 10 s of the worker.

This PR records the dump on the GPU instead.

How

  • The running total lives in a small device buffer that 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 does four things:
    • ends the open batch's query;
    • copies the remembered results next to the total (vkCmdCopyQueryPoolResults with WAIT_BIT);
    • adds them with a one-thread compute shader (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);
    • begins a fresh query for the rest of the batch.
  • The fold sits between barriers:
    • one from all earlier work of the batch to the compute stage;
    • one from the compute stage to all later work and the host.
  • The dump range is noted as a pending write of the batch and stamped like any other GPU store.
  • More than 64 remembered results are folded at a submit. Query pools are recycled.

When a dump still drains

  • the dump memory has no host import the GPU can store through;
  • a pending copied write-back or a label the CPU stores at a completion overlaps it;
  • the device has no buffer device addresses.

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:

  • each DB writes its count when the event reaches it in the pipeline, that is, after the depth tests of the draws before it;
  • the write lands some time after the packet was processed;
  • nothing guarantees the value is in memory until the title orders itself behind the dump: an end-of-pipe label or interrupt after it, a WAIT_REG_MEM, or polling the valid bit (bit 63) of each counter.

The GPU dump gives the same guarantees:

  • the fold runs after every earlier command of its batch (barrier, and the result copies wait for availability), so it counts every draw before the dump;
  • it lands before every later command of the batch, and batches run in submission order, so any end-of-pipe label the title writes after the dump lands after the counters;
  • a title that polls bit 63 sees it only once the counters are stored, provided it cleared the slots first, as it must on the hardware too (PPSA21564 clears each slot with a WRITE_DATA right before the begin dump);
  • the flip completes only after the frame's GPU work with fix(agc): complete a flip only once the frame's GPU work has finished #417, so a title that reads the counters once the flip completes also sees them stored.

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=ON and ctest pass (145/145).

  • Windows (MinGW, CI on my fork): build and ctest pass.

  • New sampleDumpTests in agc_driver_recorder_tests (tests/Recorder.cpp). That binary needs a Vulkan device and is not registered in ctest, like the rest of the file; I ran it by hand and it passes. It checks five things:

    • a dump returns without waiting for the GPU and has not landed before its batch runs;
    • after a sync the 16 counters hold the sample total, with the ready bit, and nothing past them is touched;
    • a second dump with nothing drawn in between stores the same values and does not overwrite the first;
    • the total does not move with nothing drawn;
    • 40 dumps in one batch, past the query slots one fold handles, keep the total.
  • 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):

    • all checks pass;
    • the intro level's moon is lit in every frame;
    • shimmer over the same level content is unchanged (0.155 before, 0.156 after, every frame kept).
  • Frame rate on my integration branch, before / after:

    scene before (one run) after (three runs)
    intro 44.7 46.3 to 47.2
    title 46.6 49.6 to 50.1
    menu 45.0 47.6 to 48.5
    level 28.9 29.2 to 29.8
    world map 24 26
    Snowy Canyon 1.23 2.31

    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

@oneandonlydean oneandonlydean changed the title perf(agc): wait for a pending key fill before materializing a register DCC clear perf(agc): record occlusion counter dumps on the GPU Oct 3, 2026
@github-actions github-actions Bot added build agc libSceAgc* needs rebase Conflicts with main, rebase needed labels Oct 3, 2026
@oneandonlydean
oneandonlydean force-pushed the perf/agc-occlusion-dumps-on-gpu branch from f84e04e to 911ce1f Compare October 4, 2026 03:07
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
oneandonlydean force-pushed the perf/agc-occlusion-dumps-on-gpu branch from 911ce1f to 584c5bc Compare October 4, 2026 03:32
@oneandonlydean
oneandonlydean merged commit 8f19576 into boykopovar:main Oct 4, 2026
3 checks passed
@oneandonlydean
oneandonlydean deleted the perf/agc-occlusion-dumps-on-gpu branch October 4, 2026 03:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agc libSceAgc* build needs rebase Conflicts with main, rebase needed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant