Skip to content

perf(agc): join the imports of a buffer bound from the start of adjacent ranges - #9652

Open
JJenkx wants to merge 3 commits into
boykopovar:mainfrom
JJenkx:perf/agc-join-from-run-start
Open

JJenkx wants to merge 3 commits into
boykopovar:mainfrom
JJenkx:perf/agc-join-from-run-start

Conversation

@JJenkx

@JJenkx JJenkx commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

What

joinedRun joins the imports of a run of adjacent registered ranges under one of two conditions: every range in the run is larger than the 8 MiB import window, or the binding covers the run exactly (ca95f74). In Echoes of the End (PPSA25636), a 24 MiB buffer meets neither. It is bound from the start of two or three adjacent ranges, at least one of them no larger than the window, and ends inside the last one. For example, 0x32a5700000+0x1800000 over the run 0x32a5700000+0x1970000.

No import covers that buffer, so copyRegion copies it on the CPU at every use and diffs it at write-back: 16,680 copies in one 480 s run (APS5_TRACE_BIGBUF).

With this change, a binding that starts at the first crossed range also joins the ranges it crosses. The test begin == run.base covers the exact case too, because crossedRun stops at the range that holds the binding's last byte. ca95f74's other two properties are kept:

  • a binding that starts inside the first range stays apart (the existing negative case);
  • a range registered after the run lies outside the ranges a binding into the run crosses, so growth beside a joined import does not re-import it.

The last crossed range is imported whole, as the all-large branch already does for runs of large ranges.

Tested

  • importJoinTests gains a case: a buffer bound from the first of three pieces into the last is imported once, over the three pieces. Linux, RX 6650 XT, RADV, one build directory:

    • base and test without the fix: agc_driver_graphics fails with "a buffer bound from the first of three pieces into the last was not imported over the pieces it crosses";
    • with the fix: it passes.
    • Focused tests on this PR's base (17f5335562) and on the change, under ANYPS5_REQUIRE_VULKAN=1 ANYPS5_NO_SHADER_CACHE=1 VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation VK_LOADER_DEBUG=layer with ctest -V: agc_driver_graphics, agc_sparse_*, agc_heap_mirror_*, agc_host_import_unmap, agc_host_import_retirement and agc_host_import_sparse pass on both (14 tests; agc_host_import_retire was not built in this job). The loader adds the layer for the tests that create a real instance. The VUIDs are identical on both: one pre-existing VUID-vkCmdCopyBuffer-srcBuffer-00118 in agc_sparse_run_import_copied.
  • Echoes of the End (PPSA25636), Linux 7.2.9, RX 6650 XT, RADV (Mesa 26.2.4). The title needs open PRs to reach its scene (fix(shader): preserve read lane dominance across branches #8089, feat(libSceAgcDriver): import scattered direct memory as a dma-buf run list and probe its coherence #4449, fix(agc): lower import budgets only on memory exhaustion #8091, fix(agc): keep the write-back ready barrier while hole copies are pending #8908). Both arms are the same integration tree, which also carries perf(agc): mirror a sparse heap whose committed span crosses adjacent ranges #9551, with and without this change.

    • With a 2048 MiB storage image cache budget (private override), the setting where the buffer is hottest, 480 s, APS5_PROFILE_DRAW=1 APS5_TRACE_BIGBUF=1:
      • without: 16,680 CPU copies of the buffer;
      • with: the buffer is joined (crossed run 0x32a8d10000+0x1970000 of 2 ranges, joined 2 ranges), and 4 MiB is read by CPU copies in the whole run;
      • presents per 10 s at the end: 2 without, 9-11 with;
      • presents by 400 s: 358 without, 388 with;
      • 0 amdgpu kernel events.
    • Present-matched warm pair at default settings: APS5_TRACE_FRAMES=560:100, same seed of shader caches and save state, one build directory, 480 s per arm, 0 amdgpu kernel events in either.
      • without (4ff898faef): frames 560:100 from present 560: 100 presents, frame p50 958.0 / p90 1456.9 / p99 3292.4 / max 3292.4 ms, GPU busy 434.1 ms per present
      • with (a526b9bb47): frames 560:100 from present 560: 100 presents, frame p50 1034.0 / p90 1190.2 / p99 1658.5 / max 1658.5 ms, GPU busy 429.1 ms per present
      • tools/performance_compare.py: mean frame -1.4 %, p50 +7.9 %, p90 -18.3 %, p99 -49.6 %, GPU busy -1.1 %.
      • At default settings the buffer is used much less than with the larger storage cache, and the mean frame time is within run-to-run variance. This PR claims no frame-rate gain at defaults. It removes the CPU copy path for such bindings, and with it the collapse when the buffer is hot.
      • Traces, summary lines, digests and the comparison: https://github.com/JJenkx/AnyPS5/releases/tag/pr9652-evidence
  • Independent results from other reviewers:

    • LeoFeitosa (comment): RoboCop: Rogue City - Unfinished Business (PPSA28329), Windows 10.0.26200, RTX 5070 Ti (NVIDIA 617.42).
      • Setup: main 559f35d685 with this PR's three commits cherry-picked, against the same tree without them. Same install, same input script, warm shader cache.
      • Before the change, stack samples put about 15 % of the queue 0 thread in copyRegion → GuestMemory::Read, mostly copies of about 1.8 MiB of 0x30a0000000.
      • CPU copies outside imports, cumulative over one run each: 109,192 on the base, 8 (11 MiB) with this PR.
      • Frame time (median of the per-10 s p50 over gameplay), three alternated base/PR pairs: 205.4 vs 168.1, 203.5 vs 163.1 and 211.4 vs 167.4 ms, 18-21 % less. GPU busy per present is about the same (103-107 ms). A long run with this PR held 172.6 ms over 21 gameplay intervals.
      • Limits they reported: the 3D title screen looks the same with and without the PR at the same script point, and the title's menu artifacts also appear on the base. In 1 of 5 runs with the PR, the title stayed on the intro logos (presenting normally, no fault); they have seen the same stall without this PR and do not attribute it to it.
    • Retained RoboCop pair 2 artifacts: LeoFeitosa's summaries and logs. Independently reading lines 3–8 reproduces the median of interval frame-p50 values: 203.54 ms on 559f35d685a9, 163.14 ms on 2efa0e483cc1 (19.85% lower). This is one unprofiled tester pair, not a local reproduction. The baseline reports dirty: true; the tester attributes that to the test-only test(agc): align the array window test's texels at run time #8310 file, which we have not independently verified. No trace archives or digests were captured for this pair, so it does not replace the missing required performance-trace evidence.
    • jotitadev, Apple M2 Max / MoltenVK: the new importJoinTests case fails without the change and passes with it.

Follow-up validation and retained evidence (2026-10-22)

The rebased focused gate tested clean main 1a745ae38e against submitted head 65dec56705f797f44fdc0e01446a3a7e75c3e6b5: 15/15 tests passed in each arm, with no skips and no VUIDs. Independently reading the retained logs confirms four VK_LAYER_KHRONOS_validation instance-layer insertions per arm. The test-only negative control failed only agc_driver_graphics, with the expected three-piece import assertion (14/15 passed). This identifies the tested main revision; main has continued advancing since that snapshot.

The same command was used for all three arms on Linux 7.2.9, RX 6650 XT, RADV Mesa 26.2.4:

ANYPS5_REQUIRE_VULKAN=1 ANYPS5_NO_SHADER_CACHE=1 VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation VK_LOADER_DEBUG=layer ctest -V -j1 -R '^(agc_driver_graphics|agc_sparse_.*|agc_heap_mirror_.*|agc_host_import.*)$'

This supersedes the earlier focused gate on main 3ca07d086d5f44fbe3c00370e7f03953ab46ca1b / head da7eeb2201062c864cf0762d071d229ee43ae993; those logs remain in the release as historical evidence.

The retained release assets include both Echoes trace archives, both summary files, both trace_digest.py reports and performance_compare.py output. Both Echoes summaries report trace_dropped: true; detailed phase attribution is incomplete. These remain integration-relative observations with the prerequisites listed above, and do not establish a clean-current-main title speedup. The rebase adds focused correctness evidence; qualifying clean-main title-performance evidence remains outstanding.

Summary lines

The aps5-summary.jsonl lines of the present-matched pair (APS5_TRACE_FRAMES=560:100). The traces and the tools/trace_digest.py output are on https://github.com/JJenkx/AnyPS5/releases/tag/pr9652-evidence.

Baseline (without this change):

{"commit":"4ff898faefa3","dirty":false,"env":{"APS5_PAD_INPUT_FILE":"<path>","APS5_TRACE_DIR":"<path>","APS5_TRACE_FRAMES":"560:100"},"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":"frames 560:100","first_present":560,"ms":105019.366},"presents":100,"frame_ms":{"p50":958.000,"p90":1456.922,"p99":3292.388,"max":3292.388},"gpu_busy_ms_per_present":434.116,"worker_busy_pct":{"queue worker 0x0":99.794,"queue worker 0x20":97.524},"top_phases_ms_per_frame":{"Driver.Packet":2048.790,"Driver.Packet.dispatch":1056.775,"Driver.Dispatch":992.721,"Driver.Packet.memory_wait":677.391,"Vulkan.Dispatch":628.122,"Vulkan.Dispatch.shader_resources":616.824,"Resources.Complete":615.461,"GpuMutex.wait.indirect":147.167},"trace_dropped":true,"trace_dropped_threads":{"queue worker 0x0":{"events_full_after_s":41.830,"events_lost":484625,"totals_full":false}},"trace":"aps5-trace-4ff898faefa3-3124014.json.gz"}

With this change:

{"commit":"a526b9bb47ca","dirty":false,"env":{"APS5_PAD_INPUT_FILE":"<path>","APS5_TRACE_DIR":"<path>","APS5_TRACE_FRAMES":"560:100"},"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":"frames 560:100","first_present":560,"ms":103536.928},"presents":100,"frame_ms":{"p50":1033.962,"p90":1190.235,"p99":1658.498,"max":1658.498},"gpu_busy_ms_per_present":429.131,"worker_busy_pct":{"queue worker 0x0":99.764,"queue worker 0x20":97.312},"top_phases_ms_per_frame":{"Driver.Packet":2002.157,"Driver.Packet.dispatch":1030.803,"Driver.Dispatch":972.049,"Driver.Packet.memory_wait":663.697,"Vulkan.Dispatch":612.851,"Vulkan.Dispatch.shader_resources":602.088,"Resources.Complete":591.123,"Recorder.Submit":154.274},"trace_dropped":true,"trace_dropped_threads":{"queue worker 0x0":{"events_full_after_s":36.981,"events_lost":517833,"totals_full":false}},"trace":"aps5-trace-a526b9bb47ca-3289446.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 unimplemented path added)
  • 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 (not applicable)
  • Generic behaviour, not specific to one title
  • Depends on: none. perf(agc): import storage surfaces that cross the import window through a joined import #6197 covers the storage-surface side of imports that do not span a run.
  • Fixes: none
  • Unblocks: none. This is a performance change measured on Echoes of the End (PPSA25636), which has no Game bring-up discussion.
  • GPUs tested: AMD Radeon RX 6650 XT, RADV Mesa 26.2.4, Linux 7.2.9
  • AI-assisted: yes

🤖 Generated with Claude Code

@github-actions github-actions Bot added perf Performance change perf numbers missing perf pull request without a before and after summary line unblocks title Names a title ID on the Unblocks line labels Oct 22, 2026
@github-actions

Copy link
Copy Markdown

This pull request is labeled perf, so it needs the summary line of a capture on main and one with the change, from the same title, scene and machine, as the Performance report form asks. Please paste both aps5-summary.jsonl lines, attach the two traces and add the output of tools/trace_digest.py.

@JJenkx

JJenkx commented Oct 22, 2026

Copy link
Copy Markdown
Contributor Author

The aps5-summary.jsonl lines of the present-matched pair (APS5_TRACE_FRAMES=560:100). The traces and the tools/trace_digest.py output are on https://github.com/JJenkx/AnyPS5/tree/evidence/join-run-start.

Baseline (without this change):

{"commit":"4ff898faefa3","dirty":false,"env":{"APS5_PAD_INPUT_FILE":"<path>","APS5_TRACE_DIR":"<path>","APS5_TRACE_FRAMES":"560:100"},"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":"frames 560:100","first_present":560,"ms":105019.366},"presents":100,"frame_ms":{"p50":958.000,"p90":1456.922,"p99":3292.388,"max":3292.388},"gpu_busy_ms_per_present":434.116,"worker_busy_pct":{"queue worker 0x0":99.794,"queue worker 0x20":97.524},"top_phases_ms_per_frame":{"Driver.Packet":2048.790,"Driver.Packet.dispatch":1056.775,"Driver.Dispatch":992.721,"Driver.Packet.memory_wait":677.391,"Vulkan.Dispatch":628.122,"Vulkan.Dispatch.shader_resources":616.824,"Resources.Complete":615.461,"GpuMutex.wait.indirect":147.167},"trace_dropped":true,"trace_dropped_threads":{"queue worker 0x0":{"events_full_after_s":41.830,"events_lost":484625,"totals_full":false}},"trace":"aps5-trace-4ff898faefa3-3124014.json.gz"}

With this change:

{"commit":"a526b9bb47ca","dirty":false,"env":{"APS5_PAD_INPUT_FILE":"<path>","APS5_TRACE_DIR":"<path>","APS5_TRACE_FRAMES":"560:100"},"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":"frames 560:100","first_present":560,"ms":103536.928},"presents":100,"frame_ms":{"p50":1033.962,"p90":1190.235,"p99":1658.498,"max":1658.498},"gpu_busy_ms_per_present":429.131,"worker_busy_pct":{"queue worker 0x0":99.764,"queue worker 0x20":97.312},"top_phases_ms_per_frame":{"Driver.Packet":2002.157,"Driver.Packet.dispatch":1030.803,"Driver.Dispatch":972.049,"Driver.Packet.memory_wait":663.697,"Vulkan.Dispatch":612.851,"Vulkan.Dispatch.shader_resources":602.088,"Resources.Complete":591.123,"Recorder.Submit":154.274},"trace_dropped":true,"trace_dropped_threads":{"queue worker 0x0":{"events_full_after_s":36.981,"events_lost":517833,"totals_full":false}},"trace":"aps5-trace-a526b9bb47ca-3289446.json.gz"}

@github-actions github-actions Bot removed the perf numbers missing perf pull request without a before and after summary line label Oct 22, 2026
@github-actions

github-actions Bot commented Oct 22, 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
PR 5372 core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp L1368
core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp L604
docs/dev/debt/libSceAgcDriver.md L164

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 1191 core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
PR 2434 core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
docs/dev/debt/libSceAgcDriver.md
PR 4448 core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp
docs/dev/debt/libSceAgcDriver.md
PR 5021 core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp
PR 5058 docs/dev/debt/libSceAgcDriver.md
PR 5925 docs/dev/debt/libSceAgcDriver.md
PR 6186 core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
docs/dev/debt/libSceAgcDriver.md
PR 6194 docs/dev/debt/libSceAgcDriver.md
PR 6197 core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp
PR 6223 docs/dev/debt/libSceAgcDriver.md
PR 6273 docs/dev/debt/libSceAgcDriver.md
PR 6280 docs/dev/debt/libSceAgcDriver.md
PR 6300 docs/dev/debt/libSceAgcDriver.md
PR 6412 core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp
docs/dev/debt/libSceAgcDriver.md
PR 6481 core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
docs/dev/debt/libSceAgcDriver.md
PR 6548 core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
docs/dev/debt/libSceAgcDriver.md
PR 6549 docs/dev/debt/libSceAgcDriver.md
PR 6628 core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp
PR 6808 docs/dev/debt/libSceAgcDriver.md
PR 6867 docs/dev/debt/libSceAgcDriver.md
PR 6918 core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp
PR 6977 docs/dev/debt/libSceAgcDriver.md
PR 7085 docs/dev/debt/libSceAgcDriver.md
PR 7201 core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp
PR 7256 docs/dev/debt/libSceAgcDriver.md
PR 7307 docs/dev/debt/libSceAgcDriver.md
PR 7449 docs/dev/debt/libSceAgcDriver.md
PR 7457 core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
PR 7465 core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp
PR 7604 docs/dev/debt/libSceAgcDriver.md
PR 7618 core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
docs/dev/debt/libSceAgcDriver.md
PR 7642 core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp
docs/dev/debt/libSceAgcDriver.md
PR 7844 core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
docs/dev/debt/libSceAgcDriver.md
PR 7893 docs/dev/debt/libSceAgcDriver.md
PR 7910 docs/dev/debt/libSceAgcDriver.md
PR 8014 docs/dev/debt/libSceAgcDriver.md
PR 8223 docs/dev/debt/libSceAgcDriver.md
PR 8319 docs/dev/debt/libSceAgcDriver.md
PR 8365 docs/dev/debt/libSceAgcDriver.md
PR 8419 docs/dev/debt/libSceAgcDriver.md
PR 8426 docs/dev/debt/libSceAgcDriver.md
PR 8542 docs/dev/debt/libSceAgcDriver.md
PR 8592 docs/dev/debt/libSceAgcDriver.md
PR 9020 docs/dev/debt/libSceAgcDriver.md
PR 9335 docs/dev/debt/libSceAgcDriver.md
PR 9348 docs/dev/debt/libSceAgcDriver.md
PR 9356 docs/dev/debt/libSceAgcDriver.md
PR 9387 docs/dev/debt/libSceAgcDriver.md
PR 9393 docs/dev/debt/libSceAgcDriver.md
PR 9431 docs/dev/debt/libSceAgcDriver.md
PR 9432 docs/dev/debt/libSceAgcDriver.md
PR 9508 core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp
docs/dev/debt/libSceAgcDriver.md
PR 9592 docs/dev/debt/libSceAgcDriver.md
PR 9672 docs/dev/debt/libSceAgcDriver.md
PR 9673 docs/dev/debt/libSceAgcDriver.md
PR 9674 docs/dev/debt/libSceAgcDriver.md
PR 9675 docs/dev/debt/libSceAgcDriver.md
PR 9676 docs/dev/debt/libSceAgcDriver.md
PR 9700 docs/dev/debt/libSceAgcDriver.md
PR 9748 docs/dev/debt/libSceAgcDriver.md
PR 9842 docs/dev/debt/libSceAgcDriver.md
PR 9852 core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
PR 9891 docs/dev/debt/libSceAgcDriver.md
PR 9892 docs/dev/debt/libSceAgcDriver.md
PR 9893 docs/dev/debt/libSceAgcDriver.md
PR 9895 docs/dev/debt/libSceAgcDriver.md
PR 9898 core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
docs/dev/debt/libSceAgcDriver.md
PR 9904 docs/dev/debt/libSceAgcDriver.md
PR 9905 docs/dev/debt/libSceAgcDriver.md
PR 9906 docs/dev/debt/libSceAgcDriver.md
PR 9941 core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
docs/dev/debt/libSceAgcDriver.md
PR 10049 docs/dev/debt/libSceAgcDriver.md
PR 10064 docs/dev/debt/libSceAgcDriver.md
PR 10193 core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp
docs/dev/debt/libSceAgcDriver.md
PR 10194 core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
PR 10359 docs/dev/debt/libSceAgcDriver.md

@jotitadev jotitadev left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Apple GPU / MoltenVK run, as a second vendor next to the RX 6650 XT. On MoltenVK the new importJoinTests case fails without the change and passes with it.

Machine: Apple M2 Max, macOS 27.2, x86_64 under Rosetta 2. Release, BUILD_TESTING=ON.
Drivers: MoltenVK 1.4.2 (Vulkan SDK 1.4.363.0), and MoltenVK main + SPIRV-Cross #2704.
Base/method: PR #8658 feat/macos-port @ ee0a438415 (current main plus the macOS port), not main, which does not build on macOS. This head merged into it (git merge-tree, no conflicts), diff without docs/. Full rebuild before each run.

  • Test only (the new case in tests/BdaResources.cpp, GuestBufferMemory.cpp at the base): agc_driver_graphics fails on both drivers with a buffer bound from the first of three pieces into the last was not imported over the pieces it crosses.
  • With the PR: agc_driver_graphics passes 2 of 2 on both drivers.
  • Full ^agc suite with the PR: -j1 --timeout 30, 856 tests. The failures are the same as the base on both drivers (17 with MoltenVK 1.4.2, 16 with main + #2704). No timeouts.

GPUs tested: Apple M2 Max (MoltenVK 1.4.2; MoltenVK main + SPIRV-Cross #2704)

Written with AI assistance (Claude Code). I ran the builds and tests above on my machine.

@Scramblrrrr

Copy link
Copy Markdown
Contributor

Preparing a Windows 007 cross-test of 09fb61a, preserving your commit on the same current-main integration as our baseline:
https://github.com/Scramblrrrr/AnyPS5/tree/codex/first-light-oct22-import-join

Head 62cf2d1 = b36dea0 (main d6eafa5 + #9591 + #6918), current-main #7961 merge 7e2ba73, then this PR. Clean source merge and conventions pass; UNBUILT and UNTESTED so far. The game is still running an unchanged b36 cache-capacity comparison, so no compilation or native tests concurrently.

The baseline logs show substantial repeated CPU buffer copies outside imports, concentrated in one sparse heap. That is a reason to test your change, not proof it is this particular joined-run case. Next I will run the focused Windows/NVIDIA controls with a real Khronos layer, including a production-only negative control for your added assertion, before a title comparison. No 007 benefit claimed yet; title code/captures stay local. AI-assisted: Codex.

@Scramblrrrr

Copy link
Copy Markdown
Contributor

Windows native cross-test completed for 09fb61a on integration 62cf2d1 (base58dd0d5e5 = b36dea0 + main7e2ba73e4; #6918 stays on both arms):

  • Same Windows11 10.0.26200 / Ryzen7800X3D / RTX5070Ti, NVIDIA617.42, WinLibs GCC15.2r7 Release, SPIRV Tools OFF.
  • Focused regex: ^(agc_driver_graphics|agc_sparse_.*|agc_heap_mirror_.*|agc_host_import_unmap|agc_host_import_retirement|agc_host_import_sparse)$.
  • Base:12PASS +1expected Linux-only skip,7.61s. Change:12PASS +same skip,7.90s. Actual Khronos1.4.363 DLL loaded in the real-device tests, ANYPS5_REQUIRE_VULKAN=1 / ANYPS5_NO_SHADER_CACHE=1 / ctest -V. Neither log has VUIDs or missing-layer errors. The copied-buffer00118 is gone because both arms include now-merged fix(agc): give copied guest buffer regions transfer-source usage #7961.
  • Production-only negative control: kept the candidate fixtures and replaced only GuestBufferMemory.cpp with the paired base. Only agc_driver_graphics failed, at a buffer bound from the first of three pieces into the last was not imported over the pieces it crosses. Restored the exact candidate source:12PASS +same skip,7.47s. Fixture EXE SHA256 values unchanged throughout; no VUIDs/missing-layer errors in either control.

The broader179-case gate is running now. This is a native functional cross-test, not a 007 FPS/copy-count result. The prior b36 game session hit its unchanged28GiB private-memory guard during a large pipeline compile, about192s after Resume; no verified cinematic/gameplay. AI-assisted: Codex.

@fogydes

fogydes commented Oct 22, 2026

Copy link
Copy Markdown
Contributor

Reviewed; the one-line predicate relaxation is sound. joinedRun previously required the binding to span the run exactly (begin == run.base && end == run.base + run.bytes); requiring only begin == run.base is the coherent next step because crossedRun stops on the range holding the binding's last byte (so the run covers it whole and is imported whole either way), a bind starting inside the first range still falls through to the copy path, and the retirement/refusal bookkeeping keys on recomputed runs (entry extents, run.base) rather than snapshot values, so a mid-frame rebind under the relaxed rule stays consistent. The continuesRun adjacency/same-permission gate is unchanged, so incompatible neighbor ranges still won't merge. Test covers the positive case and both negative ones (mid-range bind stays copied, post-join growth doesn't re-import).

Two things to settle before merge:

  1. The debt entry now documents the old rule. The "Host imports" entry in docs/dev/debt/libSceAgcDriver.md states the join condition in prose: "A buffer bound across adjacent ranges that are each larger than the window, or bound exactly over adjacent ranges of any size (from the first one's start to the last one's end), joins them into one import when it is bound". That phrase — "from the first one's start to the last one's end" — is exactly the condition this PR removes; after it lands, a binding that starts at the run's first byte and ends inside the last range also joins, and the entry is wrong. This is the same parallel-documentation debt rule as the code itself (the entry is where future contributors look for the behavior), so the PR should update that clause in the same stroke.

  2. Nit: the perf evidence (the aps5-summary.jsonl lines plus the traces behind them) lives on a fork branch (JJenkx/AnyPS5, evidence/join-run-start) rather than in the PR or an issue. The two summary lines are present and correct, but a reviewer re-running tools/trace_digest.py has to fetch the fork to get anything beyond the summary numbers — worth linking the fork at least.

@Scramblrrrr

Copy link
Copy Markdown
Contributor

007 title cross-test update: the combined9652+9565 integration c915980 aborted during startup after about29s, before reaching the menu. It throws std::invalid_argument, VOP2 opcode is not implemented, exit0xC0000409; last private memory was about8GiB, so this was not the RAM guard.

Not attributing it to this PR yet. Isolating the two changes now: c4162b2 has the same production sources as c915 except joinedRun retains the old exact-end predicate. The only other diff is your three-line regression addition in BdaResources.cpp. Both heads are public; the isolated branch is codex/first-light-oct22-command-memory. Rebuilding/validating that control before the next title run.

The previously posted native results remain valid; they did not catch this title failure. No unsupported opcode bypass and no title shader/code uploaded. I will report which isolated arm reproduces it. AI-assisted: Codex.

@MarcZX10 MarcZX10 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The single predicate change is internally coherent: crossedRun limits the joined range to the registered pieces containing the binding, the positive test covers that case, and existing negatives protect mid-range binds and later growth. Exact-head diff-check and conventions pass. The PR supplies strong paired Vulkan evidence on RX 6650 XT, Windows/NVIDIA and MoltenVK, including a production-only negative control; I could not run GPU tests in this environment, and hosted GPU jobs are skipped.

Please resolve these repository requirements before merge:

  1. Update the technical debt entry. docs/dev/debt/libSceAgcDriver.md still says the binding must cover exactly from the run start through its end, and later says growth is re-imported only when the buffer is bound exactly over all pieces. Both statements are now false. Please update the documentation alongside the implementation.
  2. Refresh the base and rerun the paired gate. This head is based on 17f5335562, while current main is 61 commits ahead. GitHub can merge the branches cleanly, but GuestBufferMemory.cpp changed on main. Rebase and rerun the required focused validation-layer tests on current main and the rebased change, then compare VUIDs. The current performance A/B is from an integration tree containing other open PRs (which you disclose), not the repository's current main. If the title cannot reach the measured scene on main, document that limitation and get maintainer agreement on the integration-base comparison.
  3. Attach the performance traces. The summary lines are in the PR, but the two trace files are on a separate fork branch. CONTRIBUTING asks perf PRs to attach the traces; please attach them to this PR or a linked report so reviewers can retain and inspect the exact artifacts.
  4. Complete the title reference. The checklist says Unblocks: PPSA25636 (performance) but gives no Game bring-up discussion. I could not find one for PPSA25636 in the repository's discussions. Add the discussion link if this title is blocked on the change, or clarify the checklist if this is only a performance measurement.

if (run.ranges.empty()) return run;
if (std::all_of(run.ranges.begin(), run.ranges.end(), [window](const auto& range) { return range->bytes > window; })) return run;
if (begin == run.base && end == run.base + run.bytes) return run;
if (begin == run.base) return run;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Please update the technical-debt description in the same PR. It still describes exact start-to-end bindings and exact-all-piece growth as the only join cases; this relaxed start-at-run-base case changes both statements.

@JJenkx
JJenkx force-pushed the perf/agc-join-from-run-start branch from 09fb61a to aac27c3 Compare October 22, 2026 09:37
@github-actions github-actions Bot removed the unblocks title Names a title ID on the Unblocks line label Oct 22, 2026
@JJenkx

JJenkx commented Oct 22, 2026

Copy link
Copy Markdown
Contributor Author

@MarcZX10 @fogydes thanks for the reviews. Status per point:

  1. Debt entry: updated in aac27c32ff. The host import entry now says that a buffer bound from the first range's start, ending anywhere in the last range it crosses, joins the ranges it crosses. It also says a grown heap re-imports its joined run only when such a binding crosses into a piece the joined import does not cover.
  2. Base: rebased onto current main (7e2ba73e4c) and force-pushed.
  3. Traces: I work from the CLI, which cannot upload files to a PR comment. The two traces, both summary lines, both trace_digest.py outputs and the comparison are on https://github.com/JJenkx/AnyPS5/tree/evidence/join-run-start, a branch I'll keep. If a maintainer prefers them attached here, or in a Performance report issue, I can open one, though it can only link to the same files.
  4. Title reference: the checklist now reads Unblocks: none. There is no Game bring-up discussion for PPSA25636; it is a performance measurement.

@Scramblrrrr thanks for isolating the 007 abort. VOP2 opcode is not implemented is thrown by the shader recompiler, and this PR only changes which host import a buffer binding uses. I would be surprised if it were the cause, but I'll wait for your isolated arm.

@JJenkx

JJenkx commented Oct 22, 2026

Copy link
Copy Markdown
Contributor Author

Paired focused tests on current main after the rebase, as asked.

Setup:

  • RX 6650 XT, RADV Mesa 26.2.4, Linux 7.2.9, one build directory.
  • ANYPS5_REQUIRE_VULKAN=1 ANYPS5_NO_SHADER_CACHE=1 VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation VK_LOADER_DEBUG=layer ctest -V -R '^(agc_driver_graphics|agc_sparse_.*|agc_heap_mirror_.*|agc_host_import.*)$'
  • That is 15 tests, including agc_host_import_retire, which the earlier run did not build.
Arm Head Result VUIDs
main 7e2ba73e4c 15/15 pass none
main + test commit only 7e2ba73e4c + dfb60fb1db agc_driver_graphics fails: "a buffer bound from the first of three pieces into the last was not imported over the pieces it crosses" none
change aac27c32ff 15/15 pass none
  • The loader shows VK_LAYER_KHRONOS_validation added in every arm (8 lines each) for the tests that create a real instance.
  • The vkCmdCopyBuffer-srcBuffer-00118 from the earlier run is no longer reported on current main.

Logs (paths redacted): https://github.com/JJenkx/AnyPS5/tree/evidence/join-run-start/vvl

@Scramblrrrr

Copy link
Copy Markdown
Contributor

Follow-up from a short local copied-region diagnostic on 3f516d8 (current main4777 integration plus aac27c3, native87pass+1expectedskip with identical baseline VUIDs; production-only negative/restoration also passes the intended control). The prior VOP2 exception did not repeat through ~98s of this startup; I stopped intentionally for diagnostics, so this is not a complete-boot result or proof that the old failure is resolved.

The dominant heap's region-buffer upload sample is finite and non-sparse: 12.5MiB, starting606720bytes into a2MiB allocation, writable=1/hostBacked=0/sparse=0. 7450 of7953 sampled uploads in that heap had this shape (diagnostic region-buffer uploads, not independently proven CPU reads for every row). It does not start at run.base, so the prefix-only predicate still declines it; #9551's sparse-only path also does not apply.

I am investigating a dependent extension for interior starts whose descriptor span is at least the configured import window (8MiB in the runtime), preserving the existing tiny-crossing refusal. That would join only the contiguous registered owners intersected by the view and retain the existing ownership/budget checks. Test first: fresh small adjacent owners, interior large view, exact descriptor offset, reuse, removal invalidation and tiny-crossing control; then actual Vulkan controls and a title comparison. Let me know if you are already covering that case or know a reason it must stay copied. No speed claim; raw requests/captures stay local. AI-assisted: Codex.

@JJenkx

JJenkx commented Oct 22, 2026

Copy link
Copy Markdown
Contributor Author

@Scramblrrrr thanks for isolating it, and good to hear the VOP2 exception did not repeat with this head.

I'm not covering interior starts. Please take that extension; I'm happy to test it on Echoes afterwards.

The one thing I'd watch is the reason ca95f74 kept interior starts apart: two joined imports that share a range retire each other. importJoinTests already pins the case where a later bind grows a join ("two joins sharing a range retired each other"). With interior starts allowed, two large views over overlapping but different sets of owners, both bound in the same frame, could alternate between the two joins. A test with two such views bound alternately would show whether the retire/re-import cycle settles. Your window threshold and the tiny-crossing control keep the common case safe. For this PR's run-start rule the risk is smaller, because every join from a given base covers the ranges up to the binding's end, and a longer binding from the same base grows the same import.

@Scramblrrrr

Copy link
Copy Markdown
Contributor

Thanks, taking that extension. Source integration is public now at 8942cb7, on current main4777 + our bring-up stack + your aac27c3:
https://github.com/Scramblrrrr/AnyPS5/tree/codex/interior-host-import-views

The production change adds end-begin >= window alongside begin==run.base; tiny crossings remain refused. Your overlapping-owner concern is now covered: large views over owner sets0..2 and2..4 alternate repeatedly, keeping the same grown0..4 import, and two views bind that established import in one build. Native Vulkan repeats actual byte readbacks for alternating views and checks the stable buffer handle after growth. The separately acquired-registry three-owner case also checks offset/reuse/removal; unused acquired ranges are not counted as the requested view's copies.

Windows focused base/change gate:87pass+1expectedLinuxskip (36.74/35.97s), actual Khronos1.4.363 layer, identical two existing02275 errors and no new VUID. The two final new fixtures fail their intended assertions with only baseline production restored, then pass after exact candidate restoration, with fixture executable hashes unchanged and zero VUID in those controls. Source conventions:0errors, existing long host-import debt row warns.

Full matched runtime packaging and guarded007 test next. No speed/title correctness claim yet. The branch is an integration WIP, not a clean upstream PR base; a standalone submission would explicitly depend on yours. AI-assisted: Codex.

@MarcZX10 MarcZX10 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for adding the paired current-main-at-the-time validation run (15/15 on the base and change, with the negative control failing only the intended regression), plus the Windows/NVIDIA and MoltenVK cross-checks. The debt description now matches the relaxed start-at-run-base rule, and Unblocks: none is clear.

One repository requirement has become stale again: the PR base is 7e2ba73e, while upstream main is now 8b3a6ba8 (13 commits ahead). I checked the intervening history; it does not touch GuestBufferMemory.cpp, BdaResources.cpp or the AGC debt entry. Please still rebase on current main and rerun the paired focused validation-layer gate there, so the checked merge result is current.

The performance summaries are in this PR, but the two trace archives and digests remain on the separate JJenkx/AnyPS5:evidence/join-run-start branch. CONTRIBUTING.md asks perf PRs to attach the traces and digest; please attach them here or provide a stable linked report artifact. I could not run the AGC/Vulkan suite locally, and the hosted product jobs are skipped, so the GPU evidence above is author/independent-tester reported.

@JJenkx
JJenkx force-pushed the perf/agc-join-from-run-start branch from aac27c3 to da7eeb2 Compare October 22, 2026 13:00
@JJenkx

JJenkx commented Oct 22, 2026

Copy link
Copy Markdown
Contributor Author

@MarcZX10 thanks.

  • Rebased onto current main 3ca07d086d. The new head is da7eeb2201, with the same three commits, a clean rebase, and both changed files compiling. The paired focused validation-layer gate on this base is queued on the shared GPU, and I'll post its results and logs here.
  • Stable artifacts: a release on my fork, https://github.com/JJenkx/AnyPS5/releases/tag/pr9652-evidence, has both aps5-summary.jsonl lines, the two trace archives, both tools/trace_digest.py outputs, the tools/performance_compare.py table and the redacted ctest logs as fixed release assets. I work from the CLI, which cannot attach files to a PR comment, so a release is the closest stable equivalent.
  • Conventions: tools/check_conventions.py --base origin/main reports one warning and no errors. The host import debt entry is 2,979 characters, over the 1,000-character guideline. It was already that long; this PR rewrites only its join rule, so it is flagged.

@LeoFeitosa

Copy link
Copy Markdown
Contributor

A second title with the same pattern: RoboCop: Rogue City - Unfinished Business (PPSA28329), Windows 10.0.26200, RTX 5070 Ti (NVIDIA 617.42).

Setup: main 559f35d with this PR's three commits cherry-picked (ec0ed6b, dfb60fb, aac27c3; no conflicts), against the same tree without them. Same install, same input script, warm shader cache.

Why I looked: stack samples of the queue 0 thread during gameplay showed about 15 % of it in GuestBufferMemory::copyRegion → GuestMemory::Read (memcpy). APS5_PROFILE_DRAW showed the CPU copies were "outside imports", mostly in one range (0x30a0000000, about 1.8 MiB per copy). APS5_TRACE_HOST_IMPORT logged no refusal for them.

CPU copies outside imports (APS5_PROFILE_DRAW=1, cumulative at the end of the run, one run each):

copies outside imports 0x30a0000000
base 109,192 1,497 / 2,615 MiB
this PR 8 (11 MiB) none

Frame time (median of the per-10 s frame p50 over the gameplay intervals, base and PR alternated):

pair base this PR
1 205.4 ms 168.1 ms
2 203.5 ms 163.1 ms
3 (profiled) 211.4 ms 167.4 ms

That is 18–21 % less frame time in all three pairs. GPU busy per present is about the same in both (103–107 ms). A separate long run with this PR held 172.6 ms over 21 gameplay intervals.

Image: at the same point of the input script, the 3D title screen looks the same with and without this PR. The menu artifacts this title shows also appear on the base, so they don't come from this PR.

In 1 of 5 runs with this PR, the title stayed on the intro logos and did not take the input script. It was presenting normally at 60 presents per 10 s, with no fault. I have seen the same stall on builds without this PR, so I don't attribute it here.

Logs are available if useful.

@JJenkx

JJenkx commented Oct 22, 2026

Copy link
Copy Markdown
Contributor Author

@LeoFeitosa thank you, that's a valuable second title on a different vendor and OS. It also matters that it is a case where the change helps at default settings: on Echoes the buffer is only hot under a larger storage cache, while RoboCop shows 18–21 % less frame time across three alternated pairs and CPU copies outside imports falling from 109,192 to 8. I'll reference your results in the description.

@JJenkx

JJenkx commented Oct 22, 2026

Copy link
Copy Markdown
Contributor Author

Paired focused validation-layer gate on the rebased head, on current main 3ca07d086d.

Setup: same machine, ANYPS5_REQUIRE_VULKAN=1 ANYPS5_NO_SHADER_CACHE=1 VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation VK_LOADER_DEBUG=layer ctest -V -R '^(agc_driver_graphics|agc_sparse_.*|agc_heap_mirror_.*|agc_host_import.*)$' (15 tests).

Arm Head Result VUIDs
main 3ca07d086d 15/15 pass none
main + test commit only 3ca07d086d + 85557919db agc_driver_graphics fails: "a buffer bound from the first of three pieces into the last was not imported over the pieces it crosses" none
change da7eeb2201 15/15 pass none

The loader adds VK_LAYER_KHRONOS_validation in every arm (8 lines each). The logs (paths redacted) are the vvl-3ca07d086d-* assets of https://github.com/JJenkx/AnyPS5/releases/tag/pr9652-evidence.

@JJenkx

JJenkx commented Oct 22, 2026

Copy link
Copy Markdown
Contributor Author

@LeoFeitosa could you share the logs you offered for the RoboCop comparison, preferably the aps5-summary.jsonl lines, matching trace archives/digests, exact candidate commit and the gameplay intervals used for one of the unprofiled alternating pairs? A stable linked archive is fine.

Your clean-main-based test is particularly useful because our Echoes performance capture uses a disclosed integration tree. Retaining the exact RoboCop artifacts would let reviewers inspect the separate main-versus-change measurement against CONTRIBUTING's performance-report requirements. We will keep your results attributed as independent tester measurements; we have not reproduced the RoboCop run locally. No need to repeat the runs if those existing files are available.

@LeoFeitosa

Copy link
Copy Markdown
Contributor

@JJenkx here are the files of pair 2, an unprofiled pair (base 203.5 ms, this PR 163.1 ms): https://gist.github.com/LeoFeitosa/bb17132e2f9442986c4d0fd4a87926c7

Gameplay intervals used: lines 3–8 of each summary. Those are the intervals with ≥ 5 presents and more than 20 ms GPU busy per present. The figure is the median of their frame p50:

  • base: 233.5, 203.4, 203.6, 203.2, 203.5, 203.8 → 203.5 ms
  • this PR: 191.2, 162.5, 163.6, 162.7, 162.3, 165.5 → 163.1 ms

Line 3 is the first gameplay interval after the load in both runs.

What I don't have: no trace archives or digests. These runs had APS5_TRACE_SUMMARY_EVERY=10 only, with no APS5_TRACE_FRAMES, so the summary lines are all there is. The other two pairs and the profiled pair have the same kind of files, if you want them too.

@github-actions github-actions Bot added docs agc libSceAgc* labels Oct 22, 2026
JJenkx and others added 3 commits October 22, 2026 17:14
…ent ranges

joinedRun joined a run of adjacent registered ranges that has one no
larger than the import window only for a binding exactly over the run.
Echoes of the End (PPSA25636) binds a 24 MiB buffer from the start of
three such ranges that ends inside the last one, so every use of it was
copied on the CPU: 16,680 copies in one 480 s run.

A binding that starts at the first crossed range now joins the ranges it
crosses. One that starts inside the first range still stays apart, and
a range registered after the run is outside the ranges a binding into
the run crosses, so growth beside a joined import does not re-import it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… the last

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@JJenkx
JJenkx force-pushed the perf/agc-join-from-run-start branch from da7eeb2 to 65dec56 Compare October 22, 2026 23:04
@JJenkx

JJenkx commented Oct 22, 2026

Copy link
Copy Markdown
Contributor Author

Rebased on main 1a745ae38e (head 65dec56705). The rebase was clean, and the diff is unchanged: 3 files, +5/-2.

I ran the focused gate again on the new base, in one build directory with the same command on every arm:

  • Command: ANYPS5_REQUIRE_VULKAN=1 ANYPS5_NO_SHADER_CACHE=1 VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation VK_LOADER_DEBUG=layer ctest -V -j1 -R '^(agc_driver_graphics|agc_sparse_.*|agc_heap_mirror_.*|agc_host_import.*)$'
  • Machine: Linux 7.2.9, RX 6650 XT, RADV (Mesa 26.2.4).
arm head result validation layer inserted VUIDs
main 1a745ae38e 15/15 pass 4 tests 0
main + this PR's test only 1a745ae38e + 1e206939d9 14/15: agc_driver_graphics fails with "a buffer bound from the first of three pieces into the last was not imported over the pieces it crosses" 4 tests 0
this PR 65dec56705 15/15 pass 4 tests 0

No test was skipped. Logs, with local paths redacted:

Title evidence is unchanged by the rebase. Echoes of the End remains integration-only, with the limits stated in the description. The independent RoboCop: Unfinished Business report above is from another tester's machine.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agc libSceAgc* docs perf Performance change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants