Skip to content

perf(agc): scan page-state runs a chunk at a time - #12371

Closed
JJenkx wants to merge 1 commit into
boykopovar:mainfrom
JJenkx:perf/guest-memory-run-end-chunk-scan
Closed

JJenkx wants to merge 1 commit into
boykopovar:mainfrom
JJenkx:perf/guest-memory-run-end-chunk-scan

Conversation

@JJenkx

@JJenkx JJenkx commented Oct 28, 2026

Copy link
Copy Markdown
Contributor

What

PageSpan::runEnd finds where a run of equal page-state bytes ends. It called entry() for every 8 pages and compared one 64-bit word per iteration. It now takes the entry pointer once per span (the whole table on Linux, one chunk of 65536 entries on Windows), compares 32 pages per iteration while the run holds, then 8, then single pages. The run it returns is unchanged: the word compares still start at a multiple of 8 pages, the byte scan still finds the first page that differs, and an absent chunk is still skipped (state 0) or ends the run.

Why

GuestMemory::DescribeCommitted walks page runs with it, and it is called for every descriptor region a build binds (GuestBufferMemory::addDescriptorRegion) and by ShaderResources::revalidateFull. In Echoes of the End (PPSA25636), a private counter on those two callers measured 9.3 million and 115,400 calls costing 49.6 s and 10.5 s over an 1800 s run, with ranges of hundreds of MiB to 4 GiB per call; a CPU profile of the same scene put 7.1% of all samples in runEnd. With this change the same counter read 13.2 s and 2.5 s over 10.9 million and 127,459 calls.

Tested

  • Linux 7.2.9, AMD Ryzen 9 7950X, AMD Radeon RX 6650 XT, RADV Mesa 26.2.4.
  • Equivalence and timing outside the tree: a harness with the old and new runEnd copied verbatim compares them on 200 random page-state layouts (runs broken by random pages of other states) and 400,000 random queries (unaligned starts, limits inside and past the span): 0 differences. A 4 GiB uniform run: 97.2 us per scan before, 16.2 us after (output).
  • On Linux, runEnd only runs over the guest arena's page table, which none of the Linux test binaries set up, so no focused test reaches it there; on Windows agc_page_states covers DescribeCommitted across chunk boundaries (not run here).
  • Performance report, current main against this change. Echoes needs feat(libSceAgcDriver): import scattered direct memory as a dma-buf run list and probe its coherence #4449, fix(shader): preserve read lane dominance across branches #8089, fix(agc): wait for a shader's registered preparation before taking the transaction lock #11873 and perf(agc): wait at submit only for the preparations of the programs a submission binds #6867 to boot on main, so both arms are main 439d03db387 with those merged: base evidence/12066-base 9bcf009b515, change evidence/runend-change 81680502cfa (base + this commit). Same machine and session, the same save, scripted input and starting caches; each arm ran the scene once for 400 s and its caches after that run seeded its capture (8.6 GiB title / 69-70 MiB Mesa for both). Capture 1830 s at the display's 3840x2160, APS5_TRACE_SECONDS=1500:300, no other APS5_* switch.
    • base: 190 presents, frame p50 1500.1 / p90 1970.0 / p99 4203.4 ms, GPU busy 747.9 ms per present
    • change: 232 presents, frame p50 1266.4 / p90 1618.7 / p99 1763.7 ms, GPU busy 584.0 ms per present
    • The change left the in-world loading screen earlier (base still on it at 1440 s, change past it), so its window has more post-loading frames. One pair; neither run crashed.
    • Both traces dropped events once a thread's buffer filled (base: queue worker 0x0 1,050,953, queue worker 0x20 152,275; change: 1,347,520 and 246,352); present counts and frame times come from the presenter, which dropped none.
    • Summary lines:
{"commit":"9bcf009b5157","dirty":false,"env":{"APS5_PAD_INPUT_FILE":"<path>","APS5_TRACE_DIR":"<path>","APS5_TRACE_SECONDS":"1500:300"},"gpu":"AMD Radeon RX 6650 XT (RADV NAVI23)","driver":"radv Mesa 26.2.4-arch1.1","os":"Linux 7.2.9-arch1-1","cpu":"AMD Ryzen 9 7950X 16-Core Processor","cpu_threads":32,"memory_mib":127901,"title_id":"PPSA25636","title":"Echoes of the End","content_version":"01.012.000","resolution":"3840x2160","window":{"spec":"seconds 1500:300","first_present":851,"ms":301194.705},"presents":190,"frame_ms":{"p50":1500.104,"p90":1970.022,"p99":4203.369,"max":4732.120},"gpu_busy_ms_per_present":747.896,"worker_busy_pct":{"queue worker 0x0":99.867,"queue worker 0x20":98.495},"top_phases_ms_per_frame":{"Driver.Packet":3113.965,"Driver.Packet.dispatch":1678.636,"Driver.Dispatch":1504.432,"Driver.Packet.memory_wait":1028.663,"Resources.Complete":941.241,"Vulkan.Dispatch":903.235,"Vulkan.Dispatch.shader_resources":890.374,"Graphics.Draw":235.731},"trace_dropped":true,"trace_dropped_threads":{"queue worker 0x0":{"events_full_after_s":56.598,"events_lost":1050953,"totals_full":false},"queue worker 0x20":{"events_full_after_s":185.137,"events_lost":152275,"totals_full":false}},"trace":"aps5-trace-9bcf009b5157-3983020.json.gz"}
{"commit":"81680502cfa0","dirty":false,"env":{"APS5_PAD_INPUT_FILE":"<path>","APS5_TRACE_DIR":"<path>","APS5_TRACE_SECONDS":"1500:300"},"gpu":"AMD Radeon RX 6650 XT (RADV NAVI23)","driver":"radv Mesa 26.2.4-arch1.1","os":"Linux 7.2.9-arch1-1","cpu":"AMD Ryzen 9 7950X 16-Core Processor","cpu_threads":32,"memory_mib":127901,"title_id":"PPSA25636","title":"Echoes of the End","content_version":"01.012.000","resolution":"3840x2160","window":{"spec":"seconds 1500:300","first_present":1204,"ms":300217.358},"presents":232,"frame_ms":{"p50":1266.387,"p90":1618.744,"p99":1763.661,"max":1778.084},"gpu_busy_ms_per_present":584.033,"worker_busy_pct":{"queue worker 0x0":99.836,"queue worker 0x20":98.241},"top_phases_ms_per_frame":{"Driver.Packet":2539.315,"Driver.Packet.dispatch":1318.402,"Driver.Dispatch":1172.602,"Driver.Packet.memory_wait":906.397,"Resources.Complete":723.116,"Vulkan.Dispatch":716.290,"Vulkan.Dispatch.shader_resources":704.502,"Graphics.Draw":218.574},"trace_dropped":true,"trace_dropped_threads":{"queue worker 0x0":{"events_full_after_s":50.912,"events_lost":1347520,"totals_full":false},"queue worker 0x20":{"events_full_after_s":154.181,"events_lost":246352,"totals_full":false}},"trace":"aps5-trace-81680502cfa0-681174.json.gz"}

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
  • Fixes: none
  • Unblocks: none claimed
  • GPUs tested: AMD Radeon RX 6650 XT, RADV Mesa 26.2.4, Linux 7.2.9
  • AI-assisted: yes

🤖 Generated with Claude Code

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

JJenkx commented Oct 28, 2026

Copy link
Copy Markdown
Contributor Author

Closing as a duplicate of #6921, which already makes PageSpan::runEnd cross uniform runs 64 pages per load and was open first; I missed it in my overlap check. For #6921's reviewers, the Echoes of the End (PPSA25636) data from this branch: a counter on DescribeCommitted's two hot callers went from 49.6 s + 10.5 s to 13.2 s + 2.5 s over an 1800 s run, and an offline harness found the chunk-wise scan 6x faster on a 4 GiB uniform run with identical results on 400,000 random queries (evidence: https://github.com/JJenkx/AnyPS5/releases/tag/runend-chunk-scan-evidence).

@JJenkx JJenkx closed this Oct 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant