Skip to content

perf(agc): share unchanged shader registry chunks between published versions - #7288

Merged
oneandonlydean merged 1 commit into
boykopovar:mainfrom
milescoviello:perf/shader-registry-chunks
Oct 18, 2026
Merged

oneandonlydean merged 1 commit into
boykopovar:mainfrom
milescoviello:perf/shader-registry-chunks

Conversation

@milescoviello

Copy link
Copy Markdown
Contributor

What

PublishRegisteredShader publishes each registration as a new immutable registry. In-flight submissions keep the version they were enqueued with. To do that it copies the whole std::map of code addresses, every node, every header vector and every snapshot reference, under the driver mutex. The old version is freed node by node once its last reader drops it. So registration n costs O(n), and a title that registers its shaders while it loads pays O(n²) overall.

ShaderRegistry becomes a small class with the same interface the driver and tests use (find, upper_bound, contains, at, bidirectional const_iterator, operator[], emplace, erase, clear). It keeps its entries in address-ordered chunks of at most 64 entries, held by shared_ptr. Copying a registry copies only the vector of chunk pointers. A write clones only the chunk it changes, and only when another version still shares that chunk. A chunk that grows past 64 entries splits in half. PublishRegisteredShader itself is unchanged: it still copies the registry and edits the copy, so earlier versions stay as they were.

Lookup order and results are those of the map. RegisteredProgram and the draw keys still find the greatest code address at or below an address through upper_bound and --.

Tested

Linux (Ubuntu 24.04, GCC 13.3, Release), RTX 4070 Ti, NVIDIA 595.99.02; main 6447ed1.

  • agc_driver_prepared_publication, new RegistryVersions:

    • 4096 shaders are published at pseudo-random code addresses. Order, backward iteration, upper_bound, find and contains must match a std::map at and around every address.
    • Publishing one more shader while an earlier version is held must take a new reference to at most 128 of the 4096 earlier snapshots, and must leave the earlier version unchanged.
    • Erasing every third address from a copy must leave the original unchanged. Emptying the copy and adding to it again must work.

    With main's ShaderRegistry.hpp/.cpp it fails 3 of 3 with publishing a shader copied the registry's entries for other code addresses. With this change it passes 3 of 3.

  • ctest ^agc_ (671 tests): all pass.

  • A local benchmark (not in the PR) publishes N shaders at random addresses through PublishRegisteredShader, holding a few earlier versions as submissions do:

N main this PR
5,000 423 ms (161 µs per publish at 3.7k–5k entries) 13.9 ms (3.6 µs)
20,000 13,894 ms (1,377 µs per publish at 15k–20k entries) 81.8 ms (5.1 µs)
100,000 not run 827 ms (12.7 µs per publish at 75k–100k entries)
  • Forspoken (PPSA03984), warm shader and pipeline caches, 90 s with scripted pad input, two interleaved runs each of main 82cad6b and this PR on it. A local print (not in the PR) logged the registry's size at every 1,000 code addresses, timed from the first registration. The title registers about 2,000 shaders at start-up and the rest in a burst starting about 60 s after the first registration.
main this PR
3,000 → 12,000 code addresses 61.9 → 69.6 s, 60.5 → 68.2 s (about 1,200 per second) 64.4 → 65.9 s, 65.8 → 67.7 s (about 5,000 per second)
Registry size at 90 s 12,000 to 12,999 in both runs 21,000 to 21,999 in both runs (21,000 reached at 68.1 s and 69.9 s)
Type 8 headers registered (unsupported registered shader type 8 lines, distinct addresses) 1,505, 1,505 2,726, 2,726
Presents in 90 s 180, 190 145, 137
of which after the first 10 s 127, 136 126, 119

The difference in presents is in the first 10 s, the loading screen (0.2 ms of GPU work per present): 53 and 54 on main, 19 and 18 with this PR. I haven't looked at why.

Shader.Register is a PerformanceTimer scope, but the trace records only the threads that bind a trace buffer (AnyPS5, the queue workers, shader prepare, presenter). Forspoken registers from its own threads, so no trace window has that scope: APS5_TRACE_SECONDS=0:60 and 45:40 held none on either build. That's why the registration timing above comes from a local print. The summary lines of a 45:40 window (45 to 85 s after the first present) from two interleaved rounds of 100 s (main 922dc89, this PR's code on it at 0b1f9c6d9e, no APS5_PROFILE_* switch):

main 1: seconds 45:40: 57 presents, frame p50 487.0 / p90 1124.6 ms
PR 1:   seconds 45:40: 58 presents, frame p50 471.7 / p90 977.6 ms
main 2: seconds 45:40: 47 presents, frame p50 547.3 / p90 1471.5 ms
PR 2:   seconds 45:40: 63 presents, frame p50 434.6 / p90 1006.1 ms

The type 8 headers registered within those 100 s runs were 1,826 and 2,294 on main and 2,726 in both runs with this PR.

Not checked on Windows, macOS, AMD or Intel.

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 (performance)
  • GPUs tested: NVIDIA GeForce RTX 4070 Ti (595.99.02, Linux)
  • AI-assisted: yes

🤖 Generated with Claude Code

…ersions

PublishRegisteredShader copied the whole code-address map, with every
header vector and snapshot reference, for each registration under the
driver mutex, so a title registering its shaders while it loads paid
O(n^2). The registry now keeps its entries in address-ordered chunks of
at most 64 shared by every version that has not changed them; a write
clones only the chunk it touches.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added the agc libSceAgc* label Oct 17, 2026
@github-actions

github-actions Bot commented Oct 17, 2026 •

Copy link
Copy Markdown

Some comparisons could not be completed because a pull request conflicts with the base branch. Review these shared files manually before concluding that there is no overlap.

Pull request Shared files not compared
PR 5115 core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp
core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp
PR 5958 core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp
core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp
PR 6134 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp
PR 6188 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp
PR 6867 core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp
core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp
PR 6946 core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp
core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp
PR 6947 core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp
core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp
PR 6977 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp

@oneandonlydean
oneandonlydean merged commit e5c2b3c into boykopovar:main Oct 18, 2026
10 checks passed
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.

2 participants