Skip to content

perf(agc): pipelined draws on by default - #1202

Merged
adriadam10 merged 2 commits into
boykopovar:mainfrom
MichonGoddijn231849:perf/draw-path-trims
Oct 15, 2026
Merged

adriadam10 merged 2 commits into
boykopovar:mainfrom
MichonGoddijn231849:perf/draw-path-trims

Conversation

@MichonGoddijn231849

@MichonGoddijn231849 MichonGoddijn231849 commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

What

Turns the queue-0 draw pipeline from #1201 on by default:

  • APS5_PIPELINED_DRAWS is on (depth 8) unless set to 0.
  • APS5_EXACT_DRAW_WRITES is on unless set to 0: queued draws carry the exact color, DCC, CMASK, depth and stencil sizes instead of the estimates.
  • A pipelined DMA_DATA fill notes HTILE and CMASK fills in order, as the packet path does (Michon's commit; agc_driver_depth_fast_clear_submit failed without it with APS5_PIPELINE_DMA=1). APS5_PIPELINE_DMA itself stays opt-in here; making it the default is perf(agc): pipeline DMA_DATA when APS5_PIPELINE_DMA=1 #6208.
  • ARCHITECTURE.md says the pipeline is on and how to turn it off.
  • The base tests (agc_driver, agc_driver_pm4_failure, agc_driver_draw_cache_reuse, ...) now run with the pipeline on, as asked in review; agc_driver_pm4_failure_inline and agc_driver_draw_cache_reuse_inline (APS5_PIPELINED_DRAWS=0) keep the inline path covered.

Reshaped after #1201 merged. Left out of the old head (backup: adriadam10/AnyPS5 branch backup/MichonGoddijn231849-perf-draw-path-trims, c653e9e):

Open review points from the thread: the loading hang (fill under the GPU lock) is fixed by c653e9e in #1201; failed commits fail like the inline path (#1201); the CB metadata pass drains first (#1201, c50e231). The CMASK-of-DCC abort fma-rc saw on fda7dbf has no rerun since.

Tested

Linux, Ryzen 5 7535HS / Radeon 660M, RADV, GCC, ANYPS5_ENABLE_SPIRV_TOOLS=ON, ctest -R 'agc|shader' (595 tests):

  • defaults (pipeline on): 594/595
  • APS5_PIPELINED_DRAWS=0: 594/595
  • APS5_PIPELINE_DMA=1: 593/595; the extra failure is agc_driver's copiesToHostMemory count check (the copy is enqueued, the bytes are right), which is perf(agc): pipeline DMA_DATA when APS5_PIPELINE_DMA=1 #6208's to settle
  • stress, --repeat until-fail:20 over the pipelined, fill, predication, label, draw-cache, failure, fast-clear and DMA tests with APS5_PIPELINE_DMA=1: 33/33

The only failure in every mode, agc_driver_tessellation, segfaults on main too on this GPU. Windows full build dispatched next.
Game reports on the pipeline (on #1201's c653e9e): mrfelek Diablo II +51 %, Castlevania +67 % presents/s; JAB-dev Besiege 20→33, HSG2 19.5→36.6, Vampire Survivors 25-28→48-58. @fma-rc, an Astro's Playroom run on this head would tell whether the CMASK-of-DCC abort is still there.

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: nothing (perf(agc): pipeline queue-0 draw commits behind the packet worker #1201 merged)
  • AI-assisted: yes

@github-actions github-actions Bot added needs rebase Conflicts with main, rebase needed build shader core/shader agc libSceAgc* labels Oct 7, 2026
@fisherjoey

Copy link
Copy Markdown
Contributor

Test result: Demon's Souls on Linux/NVIDIA with #1202 (and #1201 under it) on by default

Setup: Nobara 44 x86-64, kernel 7.2, RTX 5070 Ti, NVIDIA 595.91.07. Demon's Souls (PPSA01341 v1.07), tutorial level (Outpost Passage), steady state for about 150 s after the HUD appears. Base is upstream main df16c4c plus #861, #864 and #157, our local fixes (including the NVIDIA dma-buf import guard) and a job-worker idle sleep for this title. The PR side is the same tree with this PR merged, plus #1071, which only touches shutdown. The merge conflicted in 5 files against our tree (Dispatch.cpp, WrittenBuffers.cpp, PacketExecution.cpp, Submission.cpp, ShaderResources.cpp), all because of our local changes. The only code fix it needed was our own noteLabelStore signature in commitLabel/commitDmaStore. Env: APS5_PROFILE_DRAW=1, APS5_TEXTURE_CACHE_MIB=8192, APS5_PIPELINE_CACHE_ENTRIES=4096, APS5_BINDLESS_SLOTS=48. Shader cache warm. The level is still CPU-bound on queue 0.

Two runs per side. Numbers are medians of the 10 s windows in the steady state:

base (runs 240, 242) with #1202 (runs 247, 248)
presents per 10 s 13, 15.5 21, 17
fps 1.30, 1.55 2.10, 1.70
queue 0 draw packets per 10 s 151k, 154k 214k, 213k
Graphics::Draw on queue 0 per 10 s 2239, 2232 ms 1554, 1554 ms
queue 0 direct dispatch 60.0, 57.6 us 53.6, 49.7 us
app.elf CPU 378 % (run 242) 380 %, 379 %

Median fps goes from 1.43 to 1.90 (+33 %), and both PR runs beat both base runs. Queue 0 spends about 0.7 s less per 10 s in Graphics::Draw while getting through about 40 % more draw packets. The run logs show no new error signatures, and the picture is unchanged. Driver ctest -j1: 412/413. The one failure (guest_force_tls_destructor) also fails on main.

Unrelated to this PR: both PR runs still crash at teardown when the window closes (one double free, one SIGSEGV), and so does the base.

AI-assisted: yes (an agent ran the builds, the game runs and the measurements).

@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 8, 2026
@fisherjoey

Copy link
Copy Markdown
Contributor

@MichonGoddijn231849 a follow-up to the test result above, about your next step of keeping compute dispatches from draining the pipeline. On Demon's Souls that looks like the main remaining gap, so I wanted to check where you are with it before we write anything.

Per frame in the tutorial level (queue 0, same stack as above, ~2.2 fps, 448 ms on the worker):

GTA V (your thread) Demon's Souls
drawn draws ~1,800, nearly all pipelined 3,908, of which ~2,640 are DRAW_INDEX_INDIRECT and run inline on the worker
compute dispatches on queue 0 few 3,812 direct + 62 indirect, ~40-52 us each on the CPU
committer per draw ~23 us 21.6 us

The committer is ~4.6% busy here while the worker is pegged, so the time goes to the dispatch path and to indirect draws that #1201 doesn't pipeline. About 2.6k of the dispatches are one chain of small dispatches (program 0x248994d00, ~21 us of GPU each), and each one drains the pipeline before the next draw.

Two questions:

  • Are you working on dispatches that don't drain (or pipelining indirect draws)? If so I'll test your branch on this title rather than duplicate it.
  • If not, would you take that as a follow-up PR on top of perf(agc): pipelined draws on by default #1202? I can bring Demon's Souls numbers on Linux/NVIDIA for either.

@mrfelek

mrfelek commented Oct 14, 2026

Copy link
Copy Markdown
Contributor

Diablo II with this PR: the GPU is now the limit (Nsight Systems trace)

Follow-up to the numbers above, same setup: PPSA03912 in the Rogue Encampment, Linux, RTX 3080 Ti (NVIDIA 615.71.09), this PR's head c653e9e on main a56c905 plus the four PRs the title needs. Nsight Systems 2025.5, Vulkan trace, about 6 s in game with 44 presents (about 5.5 FPS).

What a frame does

Per frame (average) Calls Total in the capture
vkQueueSubmit ~60 2,651
vkCmdBindPipeline ~2,660 117,042
vkUpdateDescriptorSets ~3,150 138,805
vkAllocateDescriptorSets ~1,120 49,370
vkCreateBuffer ~400 17,703
vkCmdPipelineBarrier ~214 9,421

So there is about one pipeline bind per draw, a few thousand descriptor updates per frame, but few barriers.

Where the time goes

  • vkWaitForFences takes 2.3 s of the capture's 2.6 s of Vulkan API time (133 calls, about 17 ms each): the CPU spends most of its API time waiting for the GPU.
  • Merging every GPU workload range in the capture gives 100% busy over the whole window: the GPU timeline has no gaps.
  • nvidia-smi, 45 s in game: GPU utilization is 62% on plain main (mean, range 24 to 79%) and 86% with this PR (mean, range 50 to 100%).

So on main the title is limited by the CPU thread and the GPU waits for it; with the pipeline the CPU limit is mostly gone and the GPU is close to saturated. That puts the ceiling after this PR at roughly 155 ms of GPU time per frame (about 60 us for each of about 2,600 draws).

What I could not see

I have no GPU performance counters here (Nsight's GPU metrics need a driver permission I have not changed), so I cannot say which draws or passes make up that 155 ms. Candidates are the 3840x2160 targets, blending over large areas, and what each draw's sampled images need. If someone has a GPU capture tool, a frame of this title is a good case.

AI-assisted; the runs are from my machine.

@mrfelek

mrfelek commented Oct 14, 2026

Copy link
Copy Markdown
Contributor

Castlevania: Belmont's Curse on this PR: about +67% presents per second

Title: PPSA35399, 01.000.000 (Unity IL2CPP), in game after the opening cinematic
System: Linux, Ryzen 9 5950X, RTX 3080 Ti (NVIDIA 615.71.09)
This PR's head: c653e9e, on main a56c905 plus #5003, #5205, #1186 and #5779 (the same base as my Diablo II numbers above)
Also on both builds: #5456 (2D compare on 1D T#s) and c8662b6bc (alpha-to-coverage without a color target), because this title does not get to its first screen on a56c905 without them

Results

Each row is one run, about 2 minutes in game, steady-state windows of 10 s.

Build Presents per 10 s Draws per 10 s Time to record a draw FPS
Same base, without this PR 28–30 about 82k 49 µs 3.0
Same base, with this PR 47–56 (mostly 49–50) about 143k 30 µs 5.0
  • The PR records about 75% more draws per second, which is the whole gain: the title draws about 2,800 times per frame, so its frame rate is the draw rate divided by that.
  • The GPU still has room with the PR: about 58 ms busy and 140 ms idle per frame.
  • The same procedure on today's main (eea428c, without this PR) gives 35–36 presents per 10 s (3.5 FPS), so main has recovered part of the difference on its own. I have not rebased this PR onto it (it conflicts in 13 files).
  • One run per build, same scene right after skipping the cinematic.

Linux build

The PR does not build on Linux as pushed: GuestBufferMemory.cpp includes HostThreadLocal.hpp only under #ifdef _WIN32 (line 14) but uses HostThreadLocal at line 2061. This one-line change fixes it, and is the only change I made to the PR:

-#ifdef _WIN32
 #include "prx/libc/include/HostThreadLocal.hpp"
+#ifdef _WIN32
 #include <windows.h>

Stability matches what I reported above: this run was clean for about 2 minutes on each build.

AI-assisted; the runs are from my machine.

@github-actions

github-actions Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

Other open pull requests touch the same code. Merging one will break the other, or both implement the same thing. If this one builds on them, list them in Depends on.

Pull request Conflicting files (lines on main) Same exports Same new files
#4855 core/libs/prx/libSceAgcDriver/CMakeLists.txt L1642

MichonGoddijn231849 and others added 2 commits October 15, 2026 12:13
With APS5_PIPELINE_DMA=1 a DMA_DATA was enqueued before the
packet path's metadata notes ran, so a fill of 0 over a depth surface's HTILE (boykopovar#374) or a color
target's CMASK (boykopovar#419) never reached the surface: agc_driver_depth_fast_clear_submit failed. The
queued commit now notes the fill in order before it stores it, and a DMA copy is checked against
multisampled targets at enqueue, as on the packet path.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
(cherry picked from commit fda7dbf)
Queue 0 now pipelines draw commits unless APS5_PIPELINED_DRAWS=0, and draw write ranges use the exact
color, DCC, CMASK, depth and stencil sizes unless APS5_EXACT_DRAW_WRITES=0. The base tests now run the
pipeline; agc_driver_pm4_failure_inline and agc_driver_draw_cache_reuse_inline keep the inline path
covered.

Co-Authored-By: Claude <noreply@anthropic.com>
@adriadam10
adriadam10 force-pushed the perf/draw-path-trims branch from c653e9e to 0340145 Compare October 15, 2026 10:21
@adriadam10 adriadam10 changed the title perf(agc): cheaper draw commits; pipelined draws on by default perf(agc): pipelined draws on by default Oct 15, 2026
@adriadam10

Copy link
Copy Markdown
Collaborator

Rebased after #1201 merged and reshaped (details in the description): this PR is now just the default switch (APS5_PIPELINED_DRAWS and APS5_EXACT_DRAW_WRITES on) plus the DMA_DATA metadata-fill fix. The snapshot ring and the other trims are out (they conflict with main's snapshot caches; the arena goes in its own PR). The base tests now run with the pipeline on, and *_inline variants cover the old path. Local agc|shader: 594/595 both on and off (only agc_driver_tessellation, red on main here), stress 33/33 ×20. Old head backed up at adriadam10/AnyPS5 backup/MichonGoddijn231849-perf-draw-path-trims.

@adriadam10
adriadam10 merged commit 8856f18 into boykopovar:main Oct 15, 2026
8 of 10 checks passed
EagleBearerYS added a commit to EagleBearerYS/AnyPS5 that referenced this pull request Oct 15, 2026
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.
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.
Scramblrrrr pushed a commit to Scramblrrrr/AnyPS5 that referenced this pull request Oct 19, 2026
With the queue-0 draw pipeline on, the default since boykopovar#1202, the packet
loop hands every label-writing packet to the pipeline and skips the rest
of the loop. A RELEASE_MEM that completes a reserved EOP flip
(sceVideoOutSubmitEopFlip, boykopovar#5539) is a label write, so its label landed
but the flip branch never ran: the reservation was consumed and the flip
never presented. video_out_flip_eop and video_out_flip_eop_main failed
and video_out_flip_eop_aftergpu timed out.

Keep packets that carry a reserved flip out of the pipeline. They take
the normal path, which drains the pipeline before the flip as it does
for flip packets.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
adriadam10 pushed a commit that referenced this pull request Oct 19, 2026
With the queue-0 draw pipeline on, the default since #1202, the packet
loop hands every label-writing packet to the pipeline and skips the rest
of the loop. A RELEASE_MEM that completes a reserved EOP flip
(sceVideoOutSubmitEopFlip, #5539) is a label write, so its label landed
but the flip branch never ran: the reservation was consumed and the flip
never presented. video_out_flip_eop and video_out_flip_eop_main failed
and video_out_flip_eop_aftergpu timed out.

Keep packets that carry a reserved flip out of the pipeline. They take
the normal path, which drains the pipeline before the flip as it does
for flip packets.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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 shader core/shader

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants