Repository navigation
perf: PPSA28997 gameplay 26.4 ms on RADV REMBRANDT (16.9 ms on the 10-02 snapshot) #7138
Description
Title and scene
PPSA28997 (God of War: Sons of Sparta), gameplay: Continue from an existing save, Kratos idle at the Sparta save point. 1920x1080 swapchain, gamescope.
Lenovo Legion Go S: Ryzen Z2 Go (RADV REMBRANDT), Mesa 26.2.0-devel, SteamOS 3.8, Linux 6.18.
What is slow
main e09dbc2 + #6842 (main segfaults at start-up without it): 26.4 ms p50 (38 FPS), GPU busy 18.9 ms per present, queue worker 98 % busy (APS5_TRACE_SUMMARY_EVERY=10, no trace).
Same scene on the AnyPS5 snapshot of about 10-02 (bundled in OverkillLabs3/SoS-PS5): 16.9 ms (59 FPS, vsync), GPU busy 10.5 ms.
Findings (APS5_PROFILE_DRAW=1, per 10 s, separate runs):
- Drains at waits that already pass.
[draw] pipelined: drains: packet 13235 (4280.1 ms), 0x3c x12304, while[packets] waits satisfied: at entry 15120, polled 0.keepsDrawPipelinedrains on every WAIT_REG_MEM thatPendingLabeldoes not settle. Keeping the pipeline when no queued item overlaps the awaited bytes and the wait already passes (diff below) takes the 0x3c drains to 0. The committer then saturates:108958 commits at 82.4 us each. - Pixel shaders of registration modules (GPL).
RADV_DEBUG=shaderstats, all pixel shaders compiled in the run:
| build | shaders | avg instr | max instr | avg VMEM |
|---|---|---|---|---|
| main | 170 | 1992 | 6564 | 20.3 |
main, APS5_NO_GPL=1 |
75 | 1154 | 5452 | 3.2 |
| 10-02 snapshot | 77 | 389 | 2178 | 3.1 |
Each ReadConstBuffer dword decodes the runtime V#, checks the guest bounds, then readBuffer checks OpArrayLength inside that branch, so the descriptor load repeats per dword (233 load_global_amd + 94 load_ssbo in the largest FS).
3. GPU time stays near 18 ms per present with the smaller shaders (old: 10.5 ms). APS5_PROFILE_GPU: 43 draw ranges per present, 18.6 ms. 6 storage write-backs per present at flip (143 MiB / 10 s); the old snapshot has none. Not narrowed down.
4. CPU per draw: [draws] avg 106 us recorded (old: 45 us); prepare 1786 ms, lookup 1248 ms, record 1724 ms. [rescache] revalidate 556338 calls 5820 ms, [guestmem] collect 686573 calls 4.1 s.
Traced captures (APS5_TRACE_SECONDS=70:10, warm caches, frame times include the trace cost):
| capture | p50 ms | p90 ms | GPU busy ms |
|---|---|---|---|
| c2: main + #6842 | 38.9 | 43.2 | 20.5 |
| c3: c2 + wait diff | 37.3 | 43.6 | 20.3 |
c4: c3 + APS5_NO_GPL=1 |
35.4 | 40.5 | 19.6 |
Untraced, c4's setup runs at 24.5 ms vs 26.4 ms.
Wait diff (PacketExecution.cpp):
case 0x63: case 0x64: case 0x9f:
return packet.size() >= 5 && !DrawPipeline::Queue0().Overlaps(packet[1] | (static_cast<std::uint64_t>(packet[2]) << 32u), static_cast<std::size_t>(packet[4]) * 8);
+ case 0x3c: case 0x93: {
+ const std::uint64_t awaited = packet[2] | (static_cast<std::uint64_t>(packet[3]) << 32u);
+ const auto bytes = Pm4::WaitAwaitedBytes(packet);
+ if (DrawPipeline::Queue0().Overlaps(awaited, bytes)) return false;
+ GuestMemory::CheckRange(reinterpret_cast<const void*>(awaited), bytes, bytes);
+ return Pm4::WaitSatisfiedUnchecked(packet);
+ }Summary lines
{"commit":"e09dbc2b4231","dirty":true,"env":{"APS5_PAD_INPUT_FILE":"<path>","APS5_TRACE_SECONDS":"70:10"},"gpu":"AMD Ryzen Z2 Go (RADV REMBRANDT)","driver":"radv Mesa 26.2.0-devel (git-035ae2f854)","os":"Linux 6.18.50-valve2-1-neptune-618-gc7289a96b14d","resolution":"1920x1080","window":{"spec":"seconds 70:10","first_present":2517,"ms":10020.462},"presents":255,"frame_ms":{"p50":38.855,"p90":43.233,"p99":45.820,"max":46.431},"gpu_busy_ms_per_present":20.482,"worker_busy_pct":{"queue worker 0x0":97.529},"top_phases_ms_per_frame":{"Driver.Packet":38.016,"Driver.Packet.device_lock":19.032,"Driver.Packet.draw":16.291,"Driver.Draw":16.142,"Driver.Draw.shader_compile_and_link":10.971,"Shader.DrawStage":7.282,"Shader.DrawStage.materialize":4.353,"VideoOut.Flip":3.635},"trace_dropped":false,"trace":"aps5-trace-e09dbc2b4231-31734.json.gz"}
{"commit":"e09dbc2b4231","dirty":true,"env":{"APS5_PAD_INPUT_FILE":"<path>","APS5_TRACE_SECONDS":"70:10"},"gpu":"AMD Ryzen Z2 Go (RADV REMBRANDT)","driver":"radv Mesa 26.2.0-devel (git-035ae2f854)","os":"Linux 6.18.50-valve2-1-neptune-618-gc7289a96b14d","resolution":"1920x1080","window":{"spec":"seconds 70:10","first_present":2530,"ms":10019.389},"presents":260,"frame_ms":{"p50":37.269,"p90":43.565,"p99":44.881,"max":45.380},"gpu_busy_ms_per_present":20.316,"worker_busy_pct":{"queue worker 0x0":97.474},"top_phases_ms_per_frame":{"Driver.Packet":36.881,"Driver.Packet.draw":20.977,"Driver.Draw":20.824,"Driver.Draw.shader_compile_and_link":11.940,"Driver.Packet.device_lock":10.285,"Shader.DrawStage":7.987,"Driver.Draw.draw_enqueued":6.100,"Shader.DrawStage.materialize":4.680},"trace_dropped":false,"trace":"aps5-trace-e09dbc2b4231-32303.json.gz"}
{"commit":"e09dbc2b4231","dirty":true,"env":{"APS5_NO_GPL":"1","APS5_PAD_INPUT_FILE":"<path>","APS5_TRACE_SECONDS":"70:10"},"gpu":"AMD Ryzen Z2 Go (RADV REMBRANDT)","driver":"radv Mesa 26.2.0-devel (git-035ae2f854)","os":"Linux 6.18.50-valve2-1-neptune-618-gc7289a96b14d","resolution":"1920x1080","window":{"spec":"seconds 70:10","first_present":2590,"ms":10034.604},"presents":279,"frame_ms":{"p50":35.416,"p90":40.499,"p99":43.202,"max":47.230},"gpu_busy_ms_per_present":19.614,"worker_busy_pct":{"queue worker 0x0":97.115},"top_phases_ms_per_frame":{"Driver.Packet":34.431,"Driver.Packet.draw":17.774,"Driver.Draw":17.623,"Driver.Packet.device_lock":11.139,"Driver.Draw.shader_compile_and_link":9.830,"Shader.DrawStage":6.126,"Driver.Draw.draw_enqueued":5.277,"VideoOut.Flip":3.017},"trace_dropped":false,"trace":"aps5-trace-e09dbc2b4231-32918.json.gz"}Traces
https://github.com/utajum/AnyPS5/releases/tag/perf-ppsa28997 (c2, c3, c4 .json.gz and the summary lines)
Digest
c2, python3 tools/trace_digest.py:
| thread | busy | wait | untraced | in slices < 20 us | largest |
|---|---|---|---|---|---|
| queue worker 0x0 | 36.8 (94 %) | 0.0 (0 %) | 2.5 | 12.3 | Driver.Packet 36.5, Driver.EndSubmission 0.3 |
| presenter | 1.5 (4 %) | 2.2 (6 %) | 35.6 | 0.1 | VideoOut.Flip 3.6, Driver.Present 0.0 |
| gpu | 20.6 (52 %) | 0.0 (0 %) | 18.7 | 0.0 | batch 20.6 |
Largest slices (ms per frame, inclusive, all threads): Driver.Packet 36.5, batch 20.6, Driver.Packet.device_lock 18.8, Driver.Packet.draw 16.2, Driver.Draw 16.1, Driver.Draw.shader_compile_and_link 10.7, Shader.DrawStage 5.4, VideoOut.Flip 3.6, VideoOut.Flip.present 3.4, Driver.Present 3.4
Capture
- The caches were warm (the scene ran once and the title was restarted)
- No other APS5_TRACE_* or APS5_PROFILE_* switch was set and the log was not redirected: none set;
APS5_PAD_INPUT_FILEdrives the menu; stdout/stderr go to files (Steam shortcut). - Captures compared here come from the same machine, scene and settings
Activity
MarcZX10 commentedon Oct 17, 2026
I opened #7153 for the satisfied WAIT_REG_MEM drain case. It keeps queue 0 pipelined only when the wait range does not overlap outstanding pipeline writes and the wait already passes, and adds an execution regression for unrelated queued work. The PR cites your same-machine c2/c3 captures as reference evidence, clearly marked as your reported run; I could not build the target locally because this checkout lacks the SDL2 submodule and XRandR development support. It addresses only item 1, not the shader, GPU or remaining CPU findings.
plokmklo commentedon Oct 18, 2026
Bisection report (code-level, CPU-only — nothing was run on hardware), with the ranked findings; branch
https://github.com/plokmklo/AnyPS5/tree/contrib/debt-pick-2carries no code change because the proper fix needs the recompiler test suite to validate.Window: the "10-02 snapshot" corresponds to
3cb7ccc80; the reporter's build ise09dbc2b4231. 8,260 commits in between, so findings are ranked by mechanism rather than full bisection.graphicsPipelineLibrary/registrationModulesat all; on main,target.registrationModules = state->graphicsPipelineLibraryenables the form by default wherever RADV reports GPL fast linking (APS5_NO_GPL/APS5_NO_LIBRARY_PREBUILDare the only opt-outs) — matching the shaderstats A/B (170/avg 1992 vs 77/389;NO_GPL75/1154).ddb668066addedEmitRuntimeScalarBufferLoad: each per-dword const-buffer read re-decodes the runtime V#, and the per-component path repeats bounds +OpArrayLengthwork — the 233load_global_amd+ 94load_ssbo. It should also account for most of the warmshader_compile_and_link≈ 10.7 ms/frame. Fix direction: CSE/hoist the V# decode per descriptor across a shader's scalar reads and emit one access per instruction instead of per component.5c42e97f4writes multisampled color targets through storage images; such writes stay pending and the queue worker flushes them when queue 0 goes idle (StoreIdleResults→FlushAllPending("idle")), i.e. right after each present — matching "6 write-backs per present at flip". Confirm via the[storage] … write-backs by reasonline.ShaderResources.cpp+ ~60GuestMemory.cppwindow commits. Needs hardware per-commit bisection.Suggested measurements: main vs
APS5_NO_GPL=1+APS5_NO_LIBRARY_PREBUILD=1, comparing shaderstats,shader_compile_and_link, and the[rescache]/[storage]reason lines, to confirm items 1–2 before a descriptor-decode fix lands.m335671 commentedon Oct 19, 2026
NVIDIA data point for the GPL suggestion: Sword of the Sea (PPSA25344, UE5), title screen, no input,
maina373e85e5, Windows 11 Pro 26200, Ryzen 9 7900, RTX 4070 12 GB (NVIDIA 617.42), WinLibs GCC 15.2.0. The device reportspipelineLibrary=1, so the registration-module path is on by default here too.D = default, N =
APS5_NO_GPL=1 APS5_NO_LIBRARY_PREBUILD=1. Each arm has its own shader cache, warmed by one uncounted boot, then the runs alternated D N D N D N, 150 s each withAPS5_PROFILE_DRAW=1. Numbers are the last 60 s of each run (6[present]windows) and the last[draw] driver phasesline.recompileis theshader_compile_and_linkmark.recompileus per draw packetOn this scene and GPU the effect points the same way but is small. Turning GPL off gives about +1.8 % presents and about −1.1 ms GPU per frame, and the compile-and-link phase drops from 11.2 to about 8.7 us per draw packet (−23 %). The
[storage]write-backs by reason and the[rescache]draw counters are the same in both arms. For example in run 3, storage refresh 11,475 vs 11,445 and flip 504 vs 500 write-backs, and rescache 7,168 vs 7,179 hits with 4 misses each, so item 2 doesn't show up here. That's a light, static scene: it doesn't say how big the effect is in PPSA28997 gameplay on RADV.AI-assisted: yes (Claude Code). The builds, the runs and this comment were done and written by Claude Code on my machine.
akkadbakkad89 commentedon Oct 21, 2026
Item 2 scoping (code-level, no hardware): the per-dword repeat is real and sits across IR instructions.
EmitReadConstBuffer→EmitRuntimeScalarBufferLoad(SpirvRuntimeBuffer.cpp:810) callsdescriptor()(line 56), which re-decodes the V# every call — stride/swizzle/valid/invalid/addTidfields, present check, index fix-up — roughly 20 emitted SPIR-V ops, then ~16 for the size/bounds select, thenreadBuffer→directAccess(line 133) emits a freshOpAccessChain+OpArrayLengthplus the bounds/select/load sequence (~18 more).EmitRuntimeBufferLoadand the stores/atomics sharedescriptor()across their own components, but nothing carries it across instructions, so a shader that reads N const-buffer dwords pays the full decode +OpAccessChain+OpArrayLengthN times. On thegpuDescriptorpath the repetition is worse: each dword also runsfaultIf+StopBdaInvocationIfand abdaPointerFunction/bdaProbeFunctioncall, i.e. the page-table walk that shows up asload_global_amdin shaderstats.The cacheable part is exactly the descriptor-args function: keyed on the resolved
GetBufferResourcearguments +state.currentLabel, the decodedRuntimeDescriptorfields and thedirectAccesschain/length are safe to reuse within a label — the same invalidation #7747 already landed forbufferLengths(state.bufferLengths.clear()per block inEmitControlFlow). So the natural shape is one label-scopedRuntimeDescriptor/access cache next tobufferLengths, generalizing it rather than adding a parallel mechanism; per-instruction parts (index, offset, bounds, loads) stay per call.That leaves a coordination question: this builds directly on #7747's infra, which is mid-review with a direction question still open. If #7747 lands I'd stack the descriptor-decode cache on top; if its scope shrinks to correctness-only, the perf half could ride here instead. I can prototype either way and measure instruction counts on the recompiler test suite plus
agc_shader_replay --preparetask-clock — the GoW replay set isn't available to me, so end-to-end compile-time on PPSA28997 would still need a volunteer's capture.AI-assisted: yes (Devin).