Skip to content

perf(agc): draws that differ only in user data reuse their shape's decode and stages - #494

Merged
adriadam10 merged 2 commits into
boykopovar:mainfrom
MichonGoddijn231849:perf/draw-data-hits
Oct 14, 2026
Merged

adriadam10 merged 2 commits into
boykopovar:mainfrom
MichonGoddijn231849:perf/draw-data-hits

Conversation

@MichonGoddijn231849

@MichonGoddijn231849 MichonGoddijn231849 commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor

What

Split out of #291, as discussed there. It builds on #493 (merged), which keys draws on the user words each stage reads.

A draw that misses the cache often differs from an earlier draw only in user data, for example a per-object constant buffer pointer. Such a draw decoded and compiled from scratch. Now:

  • The draw key also yields a shape: the key without the user words the stages read (and without the merged-stage pointer registers).
  • On a key miss whose shape has an entry, the draw copies that entry's decode and re-reads only the user words and merged-stage pointers.
  • Each compiled stage is reused if its captured memory reads still hold; the rest are compiled.
  • A new key of a shape replaces the shape's previous entry instead of keeping both, and starts without that entry's variants, which were compiled under other user words.

APS5_NO_DRAW_DATA_HITS=1 disables it. APS5_VERIFY_DRAW_DATA_HITS=1 also decodes and compiles everything in full and throws when the results differ. APS5_TRACE_DRAW_CACHE counts why keys missed (new registers, user words only, evicted, never inserted), and the profile line counts data-only hits and reused or compiled stages.

#291's time-based failure memo is not part of this: #422 memoizes source failures already, and a skip that ends on a timer isn't exact.

Tested

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

  • ctest: everything that passes on main here passes.
  • 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).
  • In-game, this change was tested as part of perf(agc): draw cache keys on the user words a program reads, memoizes failures, serves data-only hits #291: GTA V (PPSA04264) Story Mode, and Dreaming Sarah (PPSA02929) whose title menu text shows with data-only hits on. This split version hasn't been re-run in a game.

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
  • New third-party code is a submodule built from source
  • Generic behaviour, not specific to one title
  • Depends on: none (perf(agc): draw cache keys on the user words each stage reads #493 is merged)
  • AI-assisted: yes. Written with Claude (Anthropic) under my direction; I reviewed and tested it.

🤖 Generated with Claude Code

@oneandonlydean

Copy link
Copy Markdown
Collaborator

Thanks, the data-only path reads right to me. The same shape means the same registers apart from the user words and merged pointers, the copied decode re-reads exactly those, and a stage is only reused when its own user words are unchanged and its captured reads validate. So the result should match a full decode and compile.

Two things before I'd take it:

  1. The supersede in insertDrawEntry runs whether or not data hits are on: cacheDrawStages passes the shape whenever the register key is used. So with APS5_NO_DRAW_DATA_HITS=1, a new key of a shape still erases the previous key's entry, and the draw then decodes and compiles in full. That's worse than main rather than the old behaviour. Could the shape (and so the supersede) only be passed when drawDataHits() is on?
  2. With data hits on, two draws of one shape that alternate with stable user words (two objects with their own constant pointers, every frame) used to be two key hits. Now each one erases the other's entry, so every draw misses its key and recompiles the stage whose words differ. Did you see that pattern in GTA V? The APS5_TRACE_DRAW_CACHE counters (superseded, data-only hits, stages compiled) from a run of this split version, plus one run with APS5_VERIFY_DRAW_DATA_HITS=1, would show whether keeping the previous entry and only pointing the shape at the newest key works better.

A small one: this commit drops <algorithm> and <array> from DrawRegisterKey.cpp while it still uses std::min and std::array. It builds through other headers, but I'd keep them.

@github-actions github-actions Bot added agc libSceAgc* needs rebase Conflicts with main, rebase needed labels Oct 4, 2026
@MichonGoddijn231849

Copy link
Copy Markdown
Contributor Author

Thanks. Both points and the includes are done in 795b8bc, on top of a rebase onto current main:

  1. The shape (and so drawShapes) is only passed to insertDrawEntry when drawDataHits() is on. With APS5_NO_DRAW_DATA_HITS=1 nothing is keyed by shape, so it behaves like perf(agc): draw cache keys on the user words each stage reads #493 alone.
  2. The supersede is gone. A new key of a shape keeps the previous key's entry, and the shape only points at its newest key. Two objects of one shape with their own stable user words keep their own key hits again. Entries are still bounded by the normal LRU (APS5_DRAW_CACHE_ENTRIES), and an evicted key's shape link is dropped as before.
  3. DrawRegisterKey.cpp keeps <algorithm> and <array>.

The rebase had one conflict: main's null pixel program (#469) reads RSRC2 as 0 when it's unset, and this PR had moved that read into readUserWords. DrawProgram now carries nullPixel, so the data-only path re-reads the user words the same way.

I haven't measured the APS5_TRACE_DRAW_CACHE counters or a APS5_VERIFY_DRAW_DATA_HITS=1 run in GTA V on this version yet. I'll post both here once I have them. Build and ctest are clean apart from the failures main has locally (agc_driver_f32_output_modifier, agc_driver_scalar_sop1 and the relinker Python tests). #493 is rebased too, with its commit unchanged.

@oneandonlydean

Copy link
Copy Markdown
Collaborator

Thanks, both points are done in 795b8bc. The shape is only passed when drawDataHits() is on, so APS5_NO_DRAW_DATA_HITS=1 doesn't touch shapes at all. And with the supersede gone, a shape's earlier keys keep their entries, under the normal LRU.

It conflicts with main now in DrawCacheStatistics.cpp: #798 moved the periodic reports off the GPU workers and replaced the std::fprintf(stderr, ...) calls with AgcDriver::ProfilePrint_nid_no_patch(...). Keeping your text assembly (with profileDrawCacheLines) and printing it through ProfilePrint_nid_no_patch("%s", text.c_str()) should resolve it.

@MichonGoddijn231849

Copy link
Copy Markdown
Contributor Author

Rebased onto current main (now 9f52b1e). The DrawCacheStatistics.cpp conflict is resolved as you suggested: the text assembly with profileDrawCacheLines stays and goes out through ProfilePrint_nid_no_patch("%s", text.c_str()). Its two profile lines are the same format strings as main's, and the PR adds no other stderr reports. Builds; the driver tests pass apart from agc_driver_scalar_sop1, which fails without this PR too.

@MichonGoddijn231849

Copy link
Copy Markdown
Contributor Author

Rebased onto current main (now 35c8550). One conflict, in DrawDecode.cpp: #1037 made unwritten user-data registers read as zero (readUserData) in the loop this PR moves into readUserWords. readUserWords now uses readUserData, so the full decode and a data-only hit's refresh read the same words. #493's commit is still included first. Builds; the driver tests pass apart from agc_driver_scalar_sop1 (fixed by #928).

@adriadam10

Copy link
Copy Markdown
Collaborator

Taking the rebase onto #5216 (with the shape admission rule above and a case in agc_driver_draw_cache_reuse) from here, so it isn't done twice.

MichonGoddijn231849 and others added 2 commits October 14, 2026 22:52
…code and stages

The draw key now has a shape: the key without the user words the stages read.
A draw whose key misses but whose shape has an entry copies that entry's decode,
re-reads only the user words (and merged-stage pointers), and reuses each
compiled stage whose captured reads still hold, compiling the rest.

APS5_NO_DRAW_DATA_HITS=1 disables it; APS5_VERIFY_DRAW_DATA_HITS=1 also decodes
and compiles in full and throws when the results differ.

A shape keeps its previous entries and only points at its newest key, so
draws of one shape that alternate between stable user words keep their own
key hits. The shape is passed only when data hits are on, so
APS5_NO_DRAW_DATA_HITS=1 keeps the previous behaviour exactly.

Rebased onto main a8b032b by CouchRiv (the conflicts with boykopovar#639 resolved; built and tested on Linux with an RTX 4070 Ti and RADV, and on Windows with an RTX 5080), and onto main 6ac63bf by gjonn (main's free-function decoder with its static-ABI mode, its incremental register key as the shape key).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: CouchRiv <270190108+CouchRiv@users.noreply.github.com>
Co-Authored-By: Gjon Gojcevic <7807870+gjonn@users.noreply.github.com>
Since boykopovar#5216 a key gets its entry at its second sight, so a shape whose
user words change on every draw never got an entry and its data-only hits
never started. A key whose shape was seen before but has no entry is now
admitted at its first sight. A shape that already has an entry serves
data-only hits from it, and its new keys are still admitted at their
second sight, so per-draw keys don't churn the cache. Shapes are
remembered in a second sightings table of the same size.

agc_driver_draw_cache_reuse draws three times with new pixel user words
over a cached shape (three data-only hits, the vertex stages reused, no
insert), then three keys of a new shape (the second one is admitted), and
runs again with APS5_VERIFY_DRAW_DATA_HITS=1.

Co-Authored-By: Claude <noreply@anthropic.com>
@adriadam10

Copy link
Copy Markdown
Collaborator

Took over the rebase onto #5216, as announced. Pushed on top of your commit:

  • Rebased onto main and resolved the conflicts with perf(agc): cache a draw key the second time it is seen #5216 and the tessellation-instanced exclusion in Draw.cpp (stageEntries/compiledStages from main, your data-candidate reuse kept).
  • ReadUserWords now recomputes tailContinuation too, so a data-only hit refreshes it. sameDecode compares it.
  • Shape admission: a key whose shape was seen before but has no entry yet is admitted at its first sight. A shape that already has an entry serves data-only hits from that entry, and its new keys are still admitted at their second sight. This differs a little from your proposal: admitting every key of a known shape at first sight would bring back the per-draw insert churn that perf(agc): cache a draw key the second time it is seen #5216 removed. Shapes are tracked in a second DrawKeySightings table.
  • agc_driver_draw_cache_reuse gets a new case, UserDataDrawsReuseTheirShape:
    • three draws with new pixel user words over a cached shape give 3 data-only hits, 0 inserts and the 3 vertex stages reused;
    • three keys of a new shape give 1 insert and 1 data-only hit.
  • A new test, agc_driver_draw_cache_reuse_verify, runs it with APS5_VERIFY_DRAW_DATA_HITS=1.
  • The case fails with the shape rule disabled ("inserted 0 draw entries, expected 1"). On main it doesn't compile, because main has no data-hit counters.

agc_driver_draw_cache_reuse, _off and _verify pass on RADV. The in-title numbers above were all measured before #5216. A before/after on one scene with this head would still be welcome. Merging once the Windows build is green.

@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
#5238 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp L8
#5364 core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawRegisterKey.hpp L8
core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp L130-131
core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp L72-83
core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp L36, L63, L66-67
#5631 core/libs/prx/libSceAgcDriver/CMakeLists.txt L1616
core/libs/prx/libSceAgcDriver/tests/execution/DrawCacheReuse.cpp L106, L129
#5751 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp L35-39

@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 14, 2026
@adriadam10
adriadam10 merged commit f59d91f into boykopovar:main Oct 14, 2026
8 checks passed
Raghav314 added a commit to Raghav314/AnyPS5 that referenced this pull request Oct 16, 2026
A data-only hit (boykopovar#494) reused a cached stage only when every user data word
of the stage was unchanged. A stage records the user words it depends on when
it is captured: the words its SRT walk reads (GetUserData), the words its
binding layout forwards into shader data, and the vertex fetch table words of
its vertex stage info. A cached stage is now a candidate when only words
outside that set changed; its captured memory is validated as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Raghav314 added a commit to Raghav314/AnyPS5 that referenced this pull request Oct 16, 2026
A data-only hit (boykopovar#494) reused a cached stage only when every user data word
of the stage was unchanged. A stage records the user words it depends on when
it is captured: the words its SRT walk reads as values or bases
(SrtUserDataUse), the words its binding layout forwards into shader data
(userDataRegisterReads), and the vertex fetch table words of its vertex stage
info. A cached stage is now a candidate when only words outside that set
changed; its captured memory is validated as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Raghav314 added a commit to Raghav314/AnyPS5 that referenced this pull request Oct 17, 2026
A data-only hit (boykopovar#494) reused a cached stage only when every user data word
of the stage was unchanged. A stage records the user words it depends on when
it is captured: the words its SRT walk reads as values or bases
(SrtUserDataUse), the words its binding layout forwards into shader data
(userDataRegisterReads), and the vertex fetch table words of its vertex stage
info. A cached stage is now a candidate when only words outside that set
changed; its captured memory is validated as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Flamefusion pushed a commit to Flamefusion/AnyPS5 that referenced this pull request Oct 17, 2026
A data-only hit (boykopovar#494) reused a cached stage only when every user data word
of the stage was unchanged. A stage records the user words it depends on when
it is captured: the words its SRT walk reads as values or bases
(SrtUserDataUse), the words its binding layout forwards into shader data
(userDataRegisterReads), and the vertex fetch table words of its vertex stage
info. A cached stage is now a candidate when only words outside that set
changed; its captured memory is validated as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Raghav314 added a commit to Raghav314/AnyPS5 that referenced this pull request Oct 17, 2026
A data-only hit (boykopovar#494) reused a cached stage only when every user data word
of the stage was unchanged. A stage records the user words it depends on when
it is captured: the words its SRT walk reads as values or bases
(SrtUserDataUse), the words its binding layout forwards into shader data
(userDataRegisterReads), and the vertex fetch table words of its vertex stage
info. A cached stage is now a candidate when only words outside that set
changed; its captured memory is validated as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Raghav314 added a commit to Raghav314/AnyPS5 that referenced this pull request Oct 17, 2026
A data-only hit (boykopovar#494) reused a cached stage only when every user data word
of the stage was unchanged. A stage records the user words it depends on when
it is captured: the words its SRT walk reads as values or bases
(SrtUserDataUse), the words its binding layout forwards into shader data
(userDataRegisterReads), and the vertex fetch table words of its vertex stage
info. A cached stage is now a candidate when only words outside that set
changed; its captured memory is validated as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Raghav314 added a commit to Raghav314/AnyPS5 that referenced this pull request Oct 18, 2026
A data-only hit (boykopovar#494) reused a cached stage only when every user data word
of the stage was unchanged. A stage records the user words it depends on when
it is captured: the words its SRT walk reads as values or bases
(SrtUserDataUse), the words its binding layout forwards into shader data
(userDataRegisterReads), and the vertex fetch table words of its vertex stage
info. A cached stage is now a candidate when only words outside that set
changed; its captured memory is validated as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Raghav314 added a commit to Raghav314/AnyPS5 that referenced this pull request Oct 18, 2026
A data-only hit (boykopovar#494) reused a cached stage only when every user data word
of the stage was unchanged. A stage records the user words it depends on when
it is captured: the words its SRT walk reads as values or bases
(SrtUserDataUse), the words its binding layout forwards into shader data
(userDataRegisterReads), and the vertex fetch table words of its vertex stage
info. A cached stage is now a candidate when only words outside that set
changed; its captured memory is validated as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Raghav314 added a commit to Raghav314/AnyPS5 that referenced this pull request Oct 18, 2026
A data-only hit (boykopovar#494) reused a cached stage only when every user data word
of the stage was unchanged. A stage records the user words it depends on when
it is captured: the words its SRT walk reads as values or bases
(SrtUserDataUse), the words its binding layout forwards into shader data
(userDataRegisterReads), and the vertex fetch table words of its vertex stage
info. A cached stage is now a candidate when only words outside that set
changed; its captured memory is validated as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
poonasor pushed a commit to poonasor/AnyPS5 that referenced this pull request Oct 20, 2026
A data-only hit (boykopovar#494) reused a cached stage only when every user data word
of the stage was unchanged. A stage records the user words it depends on when
it is captured: the words its SRT walk reads as values or bases
(SrtUserDataUse), the words its binding layout forwards into shader data
(userDataRegisterReads), and the vertex fetch table words of its vertex stage
info. A cached stage is now a candidate when only words outside that set
changed; its captured memory is validated as before.

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.

6 participants