Skip to content

perf(agc): pipeline queue-0 draw commits behind the packet worker - #1201

Merged
adriadam10 merged 18 commits into
boykopovar:mainfrom
MichonGoddijn231849:perf/pipelined-draws
Oct 15, 2026
Merged

adriadam10 merged 18 commits into
boykopovar:mainfrom
MichonGoddijn231849:perf/pipelined-draws

Conversation

@MichonGoddijn231849

@MichonGoddijn231849 MichonGoddijn231849 commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

What

With APS5_PIPELINED_DRAWS=1, queue 0's worker decodes, captures and recompiles a draw, and hands it to a committer thread that records it (Graphics::Draw under the GPU lock). The two now run in parallel instead of one after the other. Work the committer has not reached yet is tracked by the guest ranges it will write. A later packet that reads or writes those ranges, or one the pipeline cannot order, drains it.

These are ordered as commit items instead of drains:

  • labels (WRITE_DATA, RELEASE_MEM), and same-queue waits on them (APS5_PIPELINE_DRAIN_LABELS=1 drains instead);
  • the end of a queue-0 submission, and a released REWIND tail (APS5_PIPELINE_DRAIN_SUBMISSIONS=1 drains);
  • the title's buffer fill kernels (APS5_PIPELINE_DRAIN_FILLS=1 drains);
  • DMA_DATA immediates and copies (APS5_PIPELINE_DMA=1).

A draw's write ranges include the storage images it writes and its exact color and depth target ranges (APS5_EXACT_DRAW_WRITES=1), not estimates.

Draw-path costs the pipeline exposed:

  • ShaderMemory moves, so a commit owns its capture's pages.
  • A submission made before the worker's current collect epoch began keeps the epoch, so a worker behind a queue of submissions walks a surface once per catch-up (APS5_SUBMISSION_EPOCH_EACH=1 bumps at every submission). A thread remembers the ranges it walked for the whole epoch.
  • The pipeline signals only threads that wait.
  • The pending-image and unit-shadow registries take a spinning slim lock instead of std::mutex (winpthreads contention showed up as kernel events).
  • ScanPending skips the registry walk when no pending image can overlap.
  • Fewer heap allocations per recorded draw: reused descriptor sets and scratch vectors, and buffer pool size classes are kept.

The pipeline is off by default in this PR; the next one in the series turns it on.

A predicated packet is left to the normal path while predication is active, so a false predicate still skips it.

Two fixes since the rebase, each its own commit: a null storage image in a draw's written set is skipped before it is decoded (gjonn and SleglJan's report, PPSA01317), and a pipelined RELEASE_MEM with INT_SEL 3 raises no interrupt, as on main (TheAggie's report).

Note for #374 and #419: they note HTILE and CMASK fills in the DMA_DATA path. Whichever lands second also has to call NoteDepthMetadataFill and NoteColorMetadataFill in the pipelined DMA commit (commitDmaStore).

Tested

Windows 11 Pro, AMD Ryzen 5 7600, NVIDIA GeForce RTX 5080 (driver 32.0.16.1074), 32 GB, MinGW-w64 GCC 15.2.0.

  • GTA V PPSA04264 (01.005) runs with this change on the combined gta-v/testing branch (Performance mode, the prologue). GTA V Performance mode, the prologue's bank interior: about 11.7 to 20 fps together with the cheaper captures below it.
  • On this branch: builds, and ctest passes. Not run here: the relinker's Python tests (my local Python does not find their helper modules) and guest_gameupdate.
  • Rebased on main a56c905 (2026-10-13), starting from gjonn's rebase of this series onto 6ac63bf (thanks). On Windows (MinGW GCC 15.2, RTX 5080) the whole agc_* suite passes on the top of the series: 483 tests, 1 skipped (agc_driver_stencil_ref_export).
  • With APS5_PIPELINED_DRAWS=1 the same suite passes except agc_driver_pm4_failure, which expects a synchronous rejection (perf(agc): pipelined draws on by default #1202 runs it with the pipeline off).

Checklist

  • Based on current main; no other open PR implements the same functions
  • One topic per PR; follow-ups go in a new PR
  • No comments in code except technical debt
  • Unimplemented paths throw (NotImplemented_nid_no_patch); silent stubs are listed in TechnicalDebt
  • No notes, investigation .md files or images added to the repository (attach them to this PR)
  • New third-party code is a submodule built from source
  • Generic behaviour, not specific to one title
  • Depends on: perf(agc): cheaper shader-memory captures per draw #1193 (stacked; only the last commit is this PR)
  • AI-assisted: yes. Written with Claude (Anthropic) under my direction; I reviewed and tested it.

🤖 Generated with Claude Code

@github-actions github-actions Bot added build shader core/shader agc libSceAgc* labels Oct 7, 2026
adriadam10 and others added 8 commits October 15, 2026 11:34
…rror

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Keeping it made evictOldest read the front of an empty deque (agc_buffer_pool aborted with a
double free) and stopped Take at an empty class instead of trying the next size.

Co-Authored-By: Claude <noreply@anthropic.com>
… stages alive

The commit lambda held raw pointers into the draw's stage-data hits and compiled stages, which
were freed when the worker returned (agc_driver_draw_cache_reuse with APS5_PIPELINED_DRAWS=1).

Co-Authored-By: Claude <noreply@anthropic.com>
…draws, pin CMASK write ranges

Co-Authored-By: Claude <noreply@anthropic.com>
… now that it cannot be moved

Co-Authored-By: Claude <noreply@anthropic.com>
@MichonGoddijn231849
MichonGoddijn231849 marked this pull request as ready for review October 15, 2026 09:36
@adriadam10
adriadam10 force-pushed the perf/pipelined-draws branch from 31c9a0f to 43d7c11 Compare October 15, 2026 09:40
@adriadam10

Copy link
Copy Markdown
Collaborator

Rebased onto main (no merge commits; backup of 31c9a0f at adriadam10/AnyPS5:backup/MichonGoddijn231849-perf-pipelined-draws) and folded in the fixes from this thread, plus what the tests turned up with the pipeline on. The branch no longer sits on #1191 (#494 and #1193 are on main, and the pipeline doesn't need #1191's resident images). Default stays off (APS5_PIPELINED_DRAWS unset = today's inline path). #6203 was a byte-identical copy and is closed in favour of this one.

Picked from the threads (cherry-picked with -x, authors kept):

  • @Raghav314: CB metadata pass drains the pipeline first (c50e231), entry validation drains queued draws that write its runs (a9c15be), HTILE depth clear noted after the drain (41f385a, conflict with main's 2-argument NoteHtileDepthClear resolved keeping main's version, moved after the drain).
  • @eliyarson: predicated packets drain writes to their predicate (56b9448); a label behind a deferred label is not enqueued ahead of it (1f894c9).
  • @oneandonlydean: fill inputs read after queued writes, no wait for room under the GPU lock (c653e9e).

Found here and fixed:

  • BufferPool.cpp back to main's: the PR dropped the erase of an emptied size class, so evictOldest hit a dangling class (double free in agc_buffer_pool, segfault in agc_driver_graphics).
  • A pipelined commit captured raw pointers to the draw's stage-data hits and freshly materialized stages, freed when the worker returned (after main's stage-data hits); the commit now owns them (agc_driver_draw_cache_reuse aborted with the pipeline on).
  • A failed pipelined commit now stops queue 0 and goes through Driver::ReportFailure like a failed worker (except ProcessShutdown), instead of a log line; later commits are skipped. Without it agc_driver_pm4_failure hangs with the pipeline on.
  • Committer rethrows std::exception like the inline draw path.
  • After perf(agc): cheaper shader-memory captures per draw #1193 ShaderMemory is neither copyable nor movable; the draw holds it in a unique_ptr so the pipelined commit can own it.
  • Queued draw write ranges include the color target's CMASK (exact and estimated).
  • Memory-value fill (4 bytes at valueAddress) and buffer-pattern source drain queued writes before reading; the inputLock release moved into fillBuffer since main split out storeBufferFill.
  • ARCHITECTURE.md: a paragraph on the queue-0 draw pipeline.

New tests (each check proven by mutating the fix and seeing it fail): agc_driver_pm4_failure_pipelined (times out without ReportFailure), agc_driver_draw_cache_reuse_pipelined (aborts without the stage capture), CMASK rows in agc_driver_draw_write_ranges.

Local results (Linux, Radeon 660M, RADV, ANYPS5_ENABLE_SPIRV_TOOLS=ON), ctest -R 'agc|shader':

  • pipeline off: 594/595
  • pipeline on (APS5_PIPELINED_DRAWS=1 APS5_EXACT_DRAW_WRITES=1): 594/595
  • stress, pipeline on, --repeat until-fail:20 over the pipelined/fill/predication/label/draw-cache/failure tests: 16 tests x 20, all passed

The only failure, agc_driver_tessellation, segfaults on main too on this GPU. Windows full build dispatched next; merging with the pipeline off once it is green. #1202 (on by default) stays held on its open blockers.

@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 15, 2026
@adriadam10
adriadam10 merged commit e49ec59 into boykopovar:main Oct 15, 2026
8 checks passed
@eliyarson

Copy link
Copy Markdown
Contributor

Thanks @adriadam10 for rebasing, folding in the predicate and deferred-label fixes, and chasing down the pipelined-commit lifetime and failure-path bugs, and to everyone whose fixes went in. I'll keep testing the pipeline on Hades II and report back on #1202.

EagleBearerYS added a commit to EagleBearerYS/AnyPS5 that referenced this pull request Oct 15, 2026
Brings the series onto main 4e9a2a8 (boykopovar#1193, boykopovar#1201, boykopovar#1202, boykopovar#6060,
boykopovar#6333 and the descriptor word tracking of 682b07e).

Resolutions:
- ShaderMemory: the column memo's supplied and spilled blocks now sit
  on boykopovar#1193's page vector, page pool and word masks. Pages are found by
  binary search, since a spilled block can add many pages, and a block
  placed into a lazily read page fills the rest of the 16-word blocks it
  touches, so a later lazy read never overwrites placed words.
- ResourceMaterializer and SharedValues: the selected-record path and
  the words index carry availableDwordMask, as the snapshot hash does.
  A selected record's dwords past the end of its buffer count as read
  zeros, which is what a scalar buffer load returns there, so a key past
  the heap still selects a null image without reading the heap.
- DescriptorCache: kept table sets live beside boykopovar#1201's recycled sets.
- Tests and CMake: both sides' tests are kept.
mrzacarias pushed a commit to mrzacarias/AnyPS5 that referenced this pull request Oct 15, 2026
Upstream main now carries the final versions of PRs boykopovar#1193, boykopovar#1201, boykopovar#1202, boykopovar#5216 and boykopovar#4465, so their
draft copies on this branch give way to main's: the snapshot ring, the pipeline identity memo, the
code-hash memo and APS5_RESIDENT_LINEAR_TARGETS are gone with them. The port of boykopovar#4943 (draw-cache
relocation across moved user-data pointers, not merged upstream) is dropped: the draw prefetch
replaces it.

Kept on top of main: the arm64 FEXCore runner and macOS libraries, the MoltenVK settings, rect-list
clip and cull distances, GPU-ordered in-place vertex inputs, the draw pipeline's spin-first locks and
lock-free overlap check, SharedVector binding flags, the result-memo admission, the draw prefetch
(APS5_DRAW_PREFETCH) and the 1D T# sample modes. New for arm64: fegetenv from the runner's guest
control words, and the direct-memory reuse map on macOS. On macOS the guest buffers keep one
descriptor array (no buffers_coherent or buffers_u8 aliases): MoltenVK aliases a descriptor array only
through Metal argument buffers, which the driver turns off, and Vampire Crawlers' first compute
pipeline failed to compile.
@SP4C3B4R-8

Copy link
Copy Markdown
Contributor

@MichonGoddijn231849 @oneandonlydean a question about where this goes next, so nobody does the same work twice.

On Windows (RTX 5070 Ti / 7800X3D), with #6763 (draw inputs copied instead of bound in place: the GPU time of the draws went 164 -> 45 ms per frame) and #6188, Astro Bot's Crash Site (~2200 draws per frame) is now bound by this PR's committer: it holds the GPU mutex 68% of the time at ~34 us per draw, and the packet worker spends 22.6 of its 33 us per packet waiting for it. Per draw: snapshots and descriptor sets ~7.6 us, pipeline/resource lookup ~8, vertex copies ~5, target refresh ~3, the Vulkan commands themselves ~3.

We are trimming those pieces one by one, but past that the single committer is the ceiling. Is anyone working on, or planning, recording draws on more than one thread (for example secondary command buffers per render pass, committed in order)? If it is on your list we will stay out of it and measure your branch on Windows; if not, we would like to know whether you would take it in that form before we start.

adriadam10 pushed a commit that referenced this pull request Oct 19, 2026
Brings the series onto main 4e9a2a8 (#1193, #1201, #1202, #6060,
#6333 and the descriptor word tracking of 682b07e).

Resolutions:
- ShaderMemory: the column memo's supplied and spilled blocks now sit
  on #1193's page vector, page pool and word masks. Pages are found by
  binary search, since a spilled block can add many pages, and a block
  placed into a lazily read page fills the rest of the 16-word blocks it
  touches, so a later lazy read never overwrites placed words.
- ResourceMaterializer and SharedValues: the selected-record path and
  the words index carry availableDwordMask, as the snapshot hash does.
  A selected record's dwords past the end of its buffer count as read
  zeros, which is what a scalar buffer load returns there, so a key past
  the heap still selects a null image without reading the heap.
- DescriptorCache: kept table sets live beside #1201's recycled sets.
- Tests and CMake: both sides' tests are kept.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agc libSceAgc* build shader core/shader

Projects

None yet

Development

Successfully merging this pull request may close these issues.