Skip to content

perf(agc): draw stages use their shared compiled result instead of a copy - #5687

Merged
adriadam10 merged 1 commit into
boykopovar:mainfrom
Raghav314:perf/agc-draw-shared-stage-results
Oct 14, 2026
Merged

adriadam10 merged 1 commit into
boykopovar:mainfrom
Raghav314:perf/agc-draw-shared-stage-results

Conversation

@Raghav314

@Raghav314 Raghav314 commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

What

materializeDrawStage returned a copy of the compiled result it had just materialized (RecompileResult result = *stageCapture.compiled), so every stage of every draw that is not a draw-cache hit copied a whole RecompileResult (its bindings, push constants and vertex attributes) into Driver::draw's results, and freed it again at the end of the draw. The shared result stays alive in stageCaptures for the whole draw anyway.

  • materializeDrawStage returns the shared result, and Driver::draw keeps it in compiledStages; the stages point at it.
  • Only the CPU path of an indirect draw rewrites a stage: each record that patches a user data word materializes the program again into its slot. That path now copies the stages it may rewrite first on a draw-cache miss too (it did so on a hit already), so a rewrite never reaches the shared result.
  • Rect-list and barycentric stages are still built into results, as before. results keeps its reserve of programs.size() + 2, which still covers the most it holds (two rect-list stages and a copy of each program).

The shared draw-stage result was in an earlier version of #1202 and left out in its rebase because main rewrites stage results; this keeps the copies for the one path that rewrites them.

PPSA36082

Standing in the hub, the queue worker is 99% busy, and a profile of it showed about 3.4% of its time copying and freeing these results (inside malloc/free). Two hub runs in one sitting, one after the other (traces attached):

{"commit":"f74ec639c457","dirty":false,"env":{"APS5_HOST_IMPORT_MIB":"6784","APS5_TRACE_DIR":"<path>","APS5_TRACE_SECONDS":"120:15"},"gpu":"NVIDIA GeForce RTX 4090","driver":"NVIDIA 615.78.08","os":"Linux 7.2.9-2-cachyos","resolution":"1920x1080","window":{"spec":"seconds 120:15","first_present":5426,"ms":15016.964},"presents":522,"frame_ms":{"p50":28.293,"p90":31.130,"p99":33.144,"max":38.386},"gpu_busy_ms_per_present":9.002,"worker_busy_pct":{"queue worker 0x0":98.916},"top_phases_ms_per_frame":{"Driver.Packet":28.450,"Driver.Packet.draw":26.055,"Driver.Draw":25.890,"Driver.Draw.draw_and_resource_release":11.232,"Driver.Draw.shader_compile_and_link":11.191,"Graphics.Draw":11.023,"Shader.DrawStage":9.129,"Graphics.Draw.draw_and_resource_release":8.696},"trace_dropped":true,"trace":"aps5-trace-f74ec639c457-743649.json.gz"}
  • this change on f74ec63 (built from the working tree before the commit, hence dirty):
{"commit":"f74ec639c457","dirty":true,"env":{"APS5_HOST_IMPORT_MIB":"6784","APS5_TRACE_DIR":"<path>","APS5_TRACE_SECONDS":"120:15"},"gpu":"NVIDIA GeForce RTX 4090","driver":"NVIDIA 615.78.08","os":"Linux 7.2.9-2-cachyos","resolution":"1920x1080","window":{"spec":"seconds 120:15","first_present":7542,"ms":15003.395},"presents":539,"frame_ms":{"p50":27.557,"p90":29.798,"p99":32.578,"max":39.142},"gpu_busy_ms_per_present":9.613,"worker_busy_pct":{"queue worker 0x0":98.825},"top_phases_ms_per_frame":{"Driver.Packet":27.501,"Driver.Packet.draw":25.036,"Driver.Draw":24.870,"Driver.Draw.draw_and_resource_release":11.140,"Graphics.Draw":10.939,"Driver.Draw.shader_compile_and_link":10.606,"Graphics.Draw.draw_and_resource_release":8.775,"Shader.DrawStage":8.569},"trace_dropped":true,"trace":"aps5-trace-f74ec639c457-742938.json.gz"}

The stage path this changes takes less of the frame: Shader.DrawStage 9.13 to 8.57 ms per frame and Driver.Draw.shader_compile_and_link 11.19 to 10.61 ms. The frame time p50 went from 28.3 to 27.6 ms, but an earlier main run on the same machine gave 500 presents and p50 29.677, so whole-frame numbers vary by about as much as this difference. The picture in the hub is unchanged.

aps5-trace-f74ec639c457-742938.json.gz - This PR

aps5-trace-f74ec639c457-743649.json.gz - Main

Tested

Linux (CachyOS), x86-64, GCC, 7800X3D / RTX 4090, NVIDIA 615.78.08.

  • New agc_driver_indirect_patched_stage: a non-indexed DRAW_INDIRECT whose base vertex location is the vertex stage's user data word 0 takes the CPU path (the stage does not fold that word). The vertex stage writes that word as its depth. A record with 0 over a register value of 2.0 must draw the same pixels as a direct draw with 0, and a record with 2.0 over a register value of 0 must draw nothing (clipped). It passes on main and with this change. It fails when the copy before the rewrite is skipped on a miss (segmentation fault) and when the patched stage is not materialized again (the draw keeps the register's depth).
  • The linux job of build.yml run locally on this branch (rebased on main d8888df): configure with SPIRV tools on, test registration check, build, libs, ctest 1115/1115, check_skips.py clean (7 expected skips).
  • Performance: APS5_HOST_IMPORT_MIB=6784 APS5_TRACE_SECONDS=120:15, hub standing still, shader cache warmed for each build.

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 under Silent stubs in their library's technical debt file
  • 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: none
  • Unblocks: none
  • GPUs tested: NVIDIA GeForce RTX 4090, 615.78.08, Linux 7.2.9 (CachyOS)
  • AI-assisted: yes

…copy

Driver::draw copied every stage's compiled result (RecompileResult, with
its binding, push-constant and attribute vectors) out of the shared result
the capture materialized, once per stage per draw. The stages now point at
the shared result. Only the CPU path of an indirect draw rewrites a stage
(each record's patched user data is materialized again), so that path
copies the stages it may rewrite first, on a cache hit or a miss.

agc_driver_indirect_patched_stage draws through that path: a record that
patches the user data word a vertex stage writes as its depth must draw
with the record's value, not the register's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 14, 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
#5431 core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp L129
core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp L217, L348
core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp L13

@tibzejoker

Copy link
Copy Markdown
Contributor

Runs for this PR at head 73a808e9a. Windows 11, MinGW-w64 GCC 15.2.0 Release, two GPUs in one machine: Intel UHD Graphics 770 (driver 32.0.101.6129, picked with ANYPS5_GPU=Intel) and RTX 4090 (NVIDIA 616.92).

  • The tests built from this PR's changed test files (agc_driver_indirect_patched_stage) pass on both GPUs.
  • ctest -R "agc_driver|agc_masked|agc_shader" -j4: no failures on either GPU. Intel: 399 tests, 28 skipped. NVIDIA: 399 tests, 1 skipped (agc_driver_stencil_ref_export). The skips are the same as on main with each GPU. On the UHD 770 they are features it doesn't support: f64, 64-bit and float atomics, mesh shaders, fp16 interpolation and dot, shader clock.

No difference between the two GPUs.

AI-assisted (Claude Code).

@adriadam10 adriadam10 self-assigned this Oct 14, 2026
@adriadam10

Copy link
Copy Markdown
Collaborator

Checked the diff (shared result lifetime via compiledStages/stageCaptures, the indirect CPU path now always copies the stages it rewrites, results reserve still covers the maximum), built and ran the libSceAgcDriver tests (agc_driver_indirect_patched_stage passes; agc_driver_tessellation crashes inside local RADV and does so identically with the PR's source changes reverted) and merge-tree against #5431 (clean). Windows runs from @tibzejoker on Intel and NVIDIA are green. Merging once the full Build is green.

@adriadam10
adriadam10 merged commit a0b44ca into boykopovar:main Oct 14, 2026
11 checks passed
bubblegg added a commit to bubblegg/AnyPS5 that referenced this pull request Oct 14, 2026
Conflicts kept our shared stage results in the draw path (upstream's boykopovar#5687 is
the same change), our clean-range skip beside upstream's collectable-range memo
and ordered view lookup in WindowsMappings, and our single-sample MSAA mode,
under which upstream's dithered alpha-to-coverage emulation stays off.

Nonzero constant dword fills (boykopovar#5380) do not mark the images over their keys as
filled, as for the source fill kernel: they were dispatches before. Our SSL
connect, read and write keep returning the network error.

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*

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants