Skip to content

perf(shader): compile resource plan walks instead of interpreting them - #6095

Merged
oneandonlydean merged 1 commit into
boykopovar:mainfrom
meh7an:perf/shader-srt-tape
Oct 18, 2026
Merged

oneandonlydean merged 1 commit into
boykopovar:mainfrom
meh7an:perf/shader-srt-tape

Conversation

@meh7an

@meh7an meh7an commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

What

Every capture walks its resource plan's runtime sources (SrtWalker::EvaluateRuntimeSources): the descriptor dwords, the flat SRT slots and the control-flow conditions. Each of these is an IR expression that the Evaluator interprets value by value. On the S2 page of Sonic Origins Plus's main menu, the walk cost the queue worker 3.58 samples per frame (APS5 sampler, 15 s), about 8% of its time. That was before #1193, which made the interpretation itself cheaper: values are memoized in a per-thread array instead of a hash table, and the walk reads a compact copy of the plan.

A plan's walk is now compiled once into a flat node array, one node per IR value and context, and run on every capture. The node array (SrtTape) is kept with the plan. The tape keeps the Evaluator's results and reads: the clean and read-first-lane contexts, lazy evaluation, the read trace, image activity, and a constant read past its V# reading 0. It also records the user-data use that the Evaluator records for draw relocation (#4943):

  • asValue: a user-data word read as a value;
  • asBase: a word that a raw read takes directly as its base address, which does not count as a value;
  • reads: each raw read's address, together with that word.

A walk the tape hands back to the Evaluator leaves no record behind.

The tape hands a walk to the Evaluator whenever it cannot finish it exactly as the Evaluator would. That covers a read through a null user-data root, an inaccessible word, or a failed operation. APS5_NO_SRT_TAPE=1 evaluates with the Evaluator only. APS5_VERIFY_SRT_TAPE=1 runs both and reports any difference. The Evaluator gets its own user-data use there, which is compared with the tape's.

  • The plan codec does not store the tape, so the IrResourcePlan guards move to 24 fields and, on Linux with libstdc++, 728 bytes.
  • agc_shader_memory (verifySrtTape) walks three cases through both the tape and the Evaluator: the guarded-image program in four (T#, branch) states, a null SRT pointer, and a constant read past its V#. It requires every output to match, the user-data use included, and requires the guarded program's walk to report a user-data use. A tape that leaves out image activity, or does not mark a word it reads as a value, fails it.
  • In game, APS5_VERIFY_SRT_TAPE=1 compared 1,000,000 walks of the S2 page with the Evaluator and found 0 differences. That run predates the user-data use, which main added with perf(agc): find cached draws across moved user-data pointers #4943 after this PR's previous rebase.

Before #1193, EvaluateRuntimeSources fell from 3.58 to 1.68 samples per frame, and the Evaluator's own time from 2.95 to 0. Against today's compact walk, the tape cuts the walk's time per capture from 4.70 to 2.84 us (see Tested). The title FPS does not move with it (10.52 against 10.39), so the gain is the walk's own time, about 1.3 ms per frame.

Overlapping PRs:

Tested

Windows 11 (10.0.26300), WinLibs GCC 15.2.0 (UCRT), Release, NVIDIA GeForce RTX 2080 Ti, driver 617.14.

  • Rebased onto main db7176f, which records the user-data use (perf(agc): find cached draws across moved user-data pointers #4943). The tape runs first, and a walk it hands back goes to main's walk: the compact one, or the IR one when the plan has no compact copy. perf(agc): cheaper shader-memory captures per draw #1193 made the Evaluator a template over the node view without changing what it computes, so the tape still mirrors it.
  • On that main, with this change alone, the full ctest suite passes 1347 of 1349, including agc_shader_memory (with verifySrtTape), agc_shader_disk_cache, agc_shader_bindless and agc_shader_srt_read_patching.
    • agc_driver_gpu_indirect_arguments_misaligned_writer_ahead is not run: main itself does not compile it with MinGW.
    • select_tests fails in its setup on Windows without the symlink privilege (WinError 1314 from symlink_to), as on main.
  • verifySrtTape fails when the tape's asValue update is removed.
  • With APS5_VERIFY_SRT_TAPE=1, agc_shader_memory's walks report 0 differences, the user-data use included.
  • Sonic Origins Plus, main menu, S2 page (the 3D background), 1233x694 window.
    • Captured with APS5_TRACE_SECONDS=200:15 on the same machine in one session, caches warm. Traces attached.
    • Baseline f7a41ad, this change b593e36:
Metric Baseline This PR Change
frame_ms.p50 139.360 136.324 -2.2%
frame_ms.p90 164.732 148.219 -10.0%
mean_frame_ms 143.522 138.568 -3.5%
gpu_busy_ms_per_present 72.522 75.602 +4.2%
Driver.Packet (ms per frame) 140.976 135.898 -3.6%

The FPS shown in the title went from 7.53 to 7.96 (median of six readings).

These numbers predate #1193: their baseline interprets with the hash-table Evaluator. Against today's walk I measured the walk itself:

  • Build: main 8856f18 with this change. Baseline: APS5_NO_SRT_TAPE=1 (main's compact walk). Change: the tape.
  • Profile: APS5_PROFILE_DRAW=1, its [capture] line (printed every 10 s).
  • Scene: Sonic Origins Plus's main menu, on the Sonic 1 page, which the menu opened on. One run each, back to back.
Per capture Compact walk Tape
SRT evaluate, whole run (us) 4.70 2.84
SRT evaluate, each 10 s window on the menu (us) 4.49 to 5.15 2.59 to 3.30
Materialize, whole run (us) 5.9 4.2
Captures, whole run 687,454 581,554

The menu makes about 7,300 captures per second, so that is about 1.3 ms per frame at its 10.5 FPS. The title FPS does not show a change of that size: 10.52 with the compact walk, 10.39 with the tape (median of 12 readings, one run each).

Note: measuring base. These numbers come from main 0a56aaa, the last main before #5596. On Windows, later mains compare direct imports on every use, which keeps this menu at 3-4 FPS and hides a change of this size. The base also carries these commits from my fork, in both captures:

All of them are on measure/tape-2026-10-14: the baseline is f7a41ad and this change is b593e36.

The comparison tool marks the window's first present (3037 vs 2664) as a configuration difference: the window opens 200 s into each run. Both summaries set trace_dropped, because the worker filled its event buffer at about 2,700 draws per frame. The phase totals above stay complete.

The window is 15 s here (200:15). A 5 s window of the same pair gave a mean of -3.0%, with GPU busy +10.1% from the scene. The menu's background animates, so a longer window averages that out.

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 2080 Ti, driver 617.14, Windows 11
  • AI-assisted: yes

Traces and summary lines (baseline f7a41ad, this change b593e36):
tape-traces.zip

@mrfelek

mrfelek commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Castlevania and Diablo II on this PR: about +11% and +13% presents per second on Linux

System: Linux, Ryzen 9 5950X, RTX 3080 Ti (NVIDIA 615.71.09)
Builds: main eea428c, and main eea428c plus this PR (cf6e640), merged locally (see the conflict below)

Results

Steady-state windows of 10 s, about 2 minutes in game.

Title Build Presents per 10 s FPS
Castlevania: Belmont's Curse (PPSA35399, 01.000.000), after the opening cinematic main, one run 35–36 3.5
main + this PR, one run 38–40 3.9
Diablo II: Resurrected (PPSA03912, 01.039.000), Rogue Encampment main, two runs 41.4 and 42.3 4.2
main + this PR, one run 46.4 4.6
  • Same picture on both builds in both titles.
  • Castlevania: the queue worker gets through about 12% more draws per second (about 100k to 112k per 10 s); the time to record a draw stays at 42 µs.
  • Why I tried it: in both titles one worker thread limits the frame rate (83% of the samples in Castlevania), and the shader recompiler's evaluator and bindings are about 19% of that thread. That is the walk this PR compiles.
  • I did not run APS5_VERIFY_SRT_TAPE=1, so this is a speed check only.

Merge conflict with main (eea428c)

One file conflicts: IrMetadata/ResourcePlan.hpp. main added IrValueArena where this PR adds SrtTapeSlot. They are independent, and keeping both resolves it. After the merge, ShaderDiskCache.cpp fails static_assert(sizeof(IrResourcePlan) == 584). The size is 600 with both changes, so that assertion needs updating in the rebase. The build and the runs were fine after that.

AI-assisted; the runs are from my machine.

@meh7an
meh7an force-pushed the perf/shader-srt-tape branch from cf6e640 to 7174e15 Compare October 15, 2026 05:33
@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 15, 2026
@github-actions github-actions Bot added the needs rebase Conflicts with main, rebase needed label Oct 15, 2026
@meh7an meh7an mentioned this pull request Oct 15, 2026
11 tasks done
@meh7an
meh7an force-pushed the perf/shader-srt-tape branch from 7174e15 to 5aeb788 Compare October 15, 2026 11:16
@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 15, 2026
@github-actions

github-actions Bot commented Oct 15, 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 1341 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/CMakeLists.txt
core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ResourcePlan.hpp
core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp
PR 3247 core/shader/recompiler/ShaderDiskCache.cpp
PR 5371 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
PR 5500 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/ShaderDiskCache.cpp
PR 5609 core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp
core/shader/recompiler/ShaderDiskCache.cpp
PR 5925 core/shader/recompiler/CMakeLists.txt
core/shader/recompiler/ShaderDiskCache.cpp
PR 5958 core/shader/recompiler/ShaderDiskCache.cpp
PR 6032 core/shader/recompiler/ShaderDiskCache.cpp
PR 6094 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
PR 6177 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
PR 6222 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
PR 6232 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/ShaderDiskCache.cpp
PR 6235 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
PR 6296 core/shader/recompiler/ShaderDiskCache.cpp
PR 6412 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ResourcePlan.hpp
core/shader/recompiler/ShaderDiskCache.cpp
PR 6454 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/CMakeLists.txt
core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ResourcePlan.hpp
core/shader/recompiler/ShaderDiskCache.cpp
PR 6488 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ResourcePlan.hpp
core/shader/recompiler/ShaderDiskCache.cpp
PR 6503 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
PR 6539 core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp
core/shader/recompiler/ShaderDiskCache.cpp
PR 6645 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
PR 6674 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/CMakeLists.txt
PR 6879 core/shader/recompiler/CMakeLists.txt
core/shader/recompiler/ShaderDiskCache.cpp
PR 6924 core/shader/recompiler/ShaderDiskCache.cpp
PR 6941 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/CMakeLists.txt
core/shader/recompiler/ShaderDiskCache.cpp
PR 6946 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/CMakeLists.txt
core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ResourcePlan.hpp
core/shader/recompiler/ShaderDiskCache.cpp
PR 6947 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/CMakeLists.txt
core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ResourcePlan.hpp
core/shader/recompiler/ShaderDiskCache.cpp
PR 6977 core/shader/recompiler/CMakeLists.txt
core/shader/recompiler/ShaderDiskCache.cpp
PR 6987 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
PR 6989 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
PR 6995 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
PR 7002 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
PR 7201 core/shader/recompiler/CMakeLists.txt
PR 7223 core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ResourcePlan.hpp
core/shader/recompiler/ShaderDiskCache.cpp
PR 7332 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
PR 7395 core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ResourcePlan.hpp
core/shader/recompiler/ShaderDiskCache.cpp

@adriadam10 adriadam10 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the tape, the ops match SrtEvaluator.cpp and agc_shader_memory passes on the merge with main (Linux). One thing blocks it:

  • SrtTape::Run never fills runtime.userDataUse. materializeCapture sets it (Recompiler.cpp:1347) and ResourceMaterializer.cpp:803 passes the plan's own materializationSources/cleanFlatSlots, so every capture takes the tape path. The Evaluator records asValue (SrtEvaluator.cpp:422), asBase (:205) and reads (:243); the tape records none, so capture.userDataUse stays empty and reaches tagVariantRuns (DrawCapture.cpp:164).
  • verifyTape hides it: it copies the runtime with the same userDataUse pointer, so the Evaluator fills it during verification and APS5_VERIFY_SRT_TAPE=1 reports 0 differences. verifySrtTape does not set userDataUse either.

To fix: record the same userDataUse in the tape (UserData nodes, and the base/read origin for raw reads), give verifyTape its own SrtUserDataUse and compare it (asValue, asBase, reads) with the one the tape produced, and make verifySrtTape run with userDataUse set and require equality. Mutating the new check (dropping the asValue update) should make it fail.

Smaller notes: the standard "revert the non-test files" negative can't build because CMakeLists.txt lists SrtTape.cpp; I mutated the tape's branch selection by hand and agc_shader_memory_tests fails, so that part is covered. The title FPS did not move in your own measurement (10.52 vs 10.39), so the gain is the ~1.3 ms/frame walk time; please say so in the description. #6946 also edits ResourcePlan.hpp and the IrResourcePlan size assert in ShaderDiskCache.cpp, so whichever lands second needs a rebase.

@meh7an
meh7an force-pushed the perf/shader-srt-tape branch from 5aeb788 to acee01a Compare October 17, 2026 00:05
@meh7an

meh7an commented Oct 17, 2026

Copy link
Copy Markdown
Contributor Author

Thanks, that was a real gap. The user-data use came with #4943 after this PR's last rebase, and the tape never recorded it. Fixed in acee01a:

  • The tape records the Evaluator's user-data use:
    • UserData nodes set asValue.
    • A raw read whose base low word is a user-data register reads it directly and sets asBase, not asValue.
    • Every raw read records (address, origin) at the point where the Evaluator does.
    • A walk the tape refuses rolls its records back.
  • verifyTape gives the Evaluator its own SrtUserDataUse, seeded with the state before the walk.
    • It compares asValue, asBase and the reads, sorted and deduplicated as tagVariantRuns uses them.
    • On a difference it keeps the Evaluator's.
  • verifySrtTape runs both walks with userDataUse set. It requires them to be equal and requires the guarded program to report a use. Removing the tape's asValue update makes it fail.
  • The description now says the title FPS did not move and that the gain is the ~1.3 ms per frame of walk time. It also notes perf(agc): draw finisher pipeline (APS5_DRAW_PIPELINE) #6946 and perf(shader): compile reusable SRT expression plans #1341.

Rebased onto db7176f. The full suite passes 1347 of 1349: the misaligned test is not built with MinGW, and select_tests needs the symlink privilege.

@meh7an
meh7an requested a review from adriadam10 October 17, 2026 00:06
@oneandonlydean
oneandonlydean dismissed adriadam10’s stale review October 18, 2026 04:41

Stale: the requested userDataUse change is in place on acee01a (checked while @adriadam10 is away).

@oneandonlydean
oneandonlydean merged commit 0e03263 into boykopovar:main Oct 18, 2026
8 of 9 checks passed
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.

4 participants