Skip to content

perf(agc): replace the indexed 16-byte copy kernel with a buffer copy - #7968

Merged
adriadam10 merged 3 commits into
boykopovar:mainfrom
saarthakpant:perf/agc-indexed-copy-kernel
Oct 23, 2026
Merged

adriadam10 merged 3 commits into
boykopovar:mainfrom
saarthakpant:perf/agc-indexed-copy-kernel

Conversation

@saarthakpant

@saarthakpant saarthakpant commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

What

Unreal Engine 4 titles copy GPU buffers with a compute kernel that moves 16-byte elements from a source V# to a destination V#, with the element count and the source and destination offsets in a one-record constant buffer. The copy HLE knew two other copy kernels (the hashed dword copy and the stream copy) but not this one, so it ran as a real dispatch: 64-thread groups, one 16-byte load and store per thread, over host-imported guest memory. On Ghostrunner 2 (PPSA09911) that was about 28 ms of GPU time per dispatch, four dispatches per frame.

MatchesIndexedCopyKernel recognises the kernel by its 19 instruction words and the descriptors (stride 16, not swizzled, no add-tid, raw out-of-bounds mode, type 0, equal word 3 on source and destination, a record descriptor of stride 16 with at least one record). IndexedCopyRange reads {count, sourceOffset, destinationOffset} from the record, clamps the count to the launched threads, refuses windows past either descriptor's records, copies above 16 MiB and unaligned bases, and Driver::copyBuffer then takes the same path as the other kernels (the record is read only when nothing is pending over it, and the copy goes through VulkanDevice::CopyBuffer with the existing CPU and GPU limits, overlap and label checks). The two dispatch call sites treat the kernel like the other copy kernels.

Tested

Windows 11 (10.0.26200), MinGW-w64 GCC 15.2.0 UCRT, Release.

  • New agc_driver_indexed_copy: the kernel matches; every one-word alteration, a truncated kernel, other strides, a swizzled descriptor, differing formats, add-tid, raw out-of-bounds, another descriptor type, an empty record descriptor, another user-SGPR count, 32-thread groups and a dispatch without the group id do not; range arithmetic with offsets, count below and above the launch, windows past the records, 0 elements, 0 groups, 16 MiB accepted and above refused, misaligned bases refused; eight dispatches over real memory, five taking the HLE path and three (windows past the records, above 16 MiB) running the kernel, all with the expected destination contents.
  • agc_driver_indexed_copy_gpu_parameters (from eliasbruno124-dev's review): an address-based dispatch writes the copy's record in the same submission; the HLE now waits for leased writers (FinishLeaseWork, as readDispatchArguments does) before reading it. Failed at destination word 16 before the wait, passes with it.
  • agc_driver_stream_copy, agc_driver_indexed_copy and agc_driver_indexed_copy_gpu_parameters with VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation ANYPS5_NO_SHADER_CACHE=1 ANYPS5_REQUIRE_VULKAN=1 (ctest -V) on main (stream copy only, the other two are new) and on this branch: all pass, the layer was found, no VUID- lines in either log.
  • Ghostrunner 2 (PPSA09911): the kernel at program 0x200e100000 now goes through the copy HLE; its GPU time per dispatch fell from about 28 ms to about 1.3 ms.

Performance, same machine, same session, warm caches, idle in-game scene (death screen), APS5_TRACE_SECONDS=120:60, no other switches. The base is main 4b462ef plus #7687, because main alone stops this title before gameplay (#7017); this change does not depend on #7687.

main + #7687 (db0cb30aa):

{"commit":"db0cb30aaaad","dirty":false,"env":{"APS5_PLAYGO_UNLISTED_CHUNKS":"installed","APS5_TRACE_DIR":"<path>","APS5_TRACE_SECONDS":"120:60"},"gpu":"NVIDIA GeForce RTX 2060 SUPER","driver":"NVIDIA 617.42","os":"Windows 10.0.26200","resolution":"3840x2160","window":{"spec":"seconds 120:60","first_present":1166,"ms":60247.923},"presents":188,"frame_ms":{"p50":309.886,"p90":336.916,"p99":594.174,"max":595.994},"gpu_busy_ms_per_present":208.564,"worker_busy_pct":{"queue worker 0x0":98.983,"queue worker 0x20":99.495},"top_phases_ms_per_frame":{"Driver.Packet":632.832,"Driver.Packet.memory_wait":315.737,"Resources.Complete":148.403,"Graphics.Draw":143.937,"Graphics.Draw.draw_and_resource_release":125.999,"Driver.Packet.draw":124.129,"Driver.Draw":123.192,"Driver.Packet.dispatch":105.326},"trace_dropped":true,"trace_dropped_threads":{"queue worker 0x0":{"events_full_after_s":13.457,"events_lost":931329,"totals_full":false},"draw committer":{"events_full_after_s":28.593,"events_lost":290723,"totals_full":false}},"trace":"aps5-trace-db0cb30aaaad-10428.json.gz"}

this change (7d798795d):

{"commit":"7d798795d78c","dirty":false,"env":{"APS5_PLAYGO_UNLISTED_CHUNKS":"installed","APS5_TRACE_DIR":"<path>","APS5_TRACE_SECONDS":"120:60"},"gpu":"NVIDIA GeForce RTX 2060 SUPER","driver":"NVIDIA 617.42","os":"Windows 10.0.26200","resolution":"3840x2160","window":{"spec":"seconds 120:60","first_present":1246,"ms":60258.336},"presents":175,"frame_ms":{"p50":306.110,"p90":394.146,"p99":745.884,"max":4203.981},"gpu_busy_ms_per_present":96.262,"worker_busy_pct":{"queue worker 0x0":98.372,"queue worker 0x20":99.384},"top_phases_ms_per_frame":{"Driver.Packet":679.618,"Driver.Packet.memory_wait":336.187,"Graphics.Draw":160.820,"Driver.Packet.draw":149.034,"Driver.Draw":147.289,"Graphics.Draw.draw_and_resource_release":119.477,"Driver.Packet.device_lock":109.898,"Driver.Draw.shader_compile_and_link":78.745},"trace_dropped":true,"trace_dropped_threads":{"queue worker 0x0":{"events_full_after_s":9.388,"events_lost":1909697,"totals_full":false},"draw committer":{"events_full_after_s":24.118,"events_lost":418959,"totals_full":false}},"trace":"aps5-trace-7d798795d78c-13420.json.gz"}

GPU busy per present 208.6 ms → 96.3 ms. The frame time does not move (p50 309.9 → 306.1 ms): both queue workers stay at 99 %, and the compute queue spends most of each frame in Driver.Packet.memory_wait on labels the graphics queue writes, so this title is bound by that path, not by the GPU. The max of 4204 ms in the second capture is one shader compile at the start of the window (Driver.Draw.shader_compile_and_link). Both traces are attached; their event buffers filled after 13 s and 9 s of the window (trace_dropped), the summary counters are complete.

Digest of each trace: python3 tools/trace_digest.py <trace> output attached as digest-base.txt and digest-change.txt.

tools/pr_overlap.py reports no open pull request changing the same lines. It could not compare eight pull requests that conflict with main and touch the same files: #6292 and #6615 change the type of countsMutex in BufferCopy.cpp; #5925, #7201, #7233, #7332, #7446 and #7642 change other parts of Dispatch.cpp. None of them touches the copy-kernel recognition.

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: PPSA09911 Ghostrunner 2 (PPSA09911) #9679
  • GPUs tested: NVIDIA GeForce RTX 2060 SUPER, driver 617.42, Windows 11 10.0.26200 (Khronos validation layer 1.4.363)
  • AI-assisted: yes

🤖 Generated with Claude Code

@github-actions github-actions Bot added perf Performance change unblocks title Names a title ID on the Unblocks line labels Oct 19, 2026
@github-actions

github-actions Bot commented Oct 19, 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 1896 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp
PR 5925 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp
PR 6804 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp
PR 7201 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp
PR 7410 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp
PR 7642 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp
PR 8552 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferCopy.cpp
PR 8735 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp
PR 9508 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp
PR 9599 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp

@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.

I reviewed the matcher and range checks. The HLE is restricted to the exact instruction sequence, descriptor layout, launch shape and record format; record visibility, pending labels, descriptor bounds, overlap and the 16 MiB cap are checked before the shared copy path runs. The test fixture also compares actual destination contents for both HLE and rejected-kernel cases. The performance report distinguishes the GPU-time reduction from the unchanged frame-time bottleneck. I did not independently run the Windows tests or title capture.

@eliasbruno124-dev

Copy link
Copy Markdown
Contributor

I reproduced a parameter-visibility failure in 1a6b03bc1, applied unchanged to main dbac9fa15.

An address-based GPU dispatch writes {count=23, sourceOffset=17, destinationOffset=29}, followed by this indexed copy in the same submission. The record initially contains {1, 0, 0}. The producer is deferred, and the HLE reads the old parameters: the regression reports GPU parameter copy differs at word 16. It also verifies that the producer eventually wrote all three expected values, so the failure is in the dependent copy.

Calling Graphics::FinishLeaseWork() before reading the indexed record, under the existing GPU lock, fixes this case. The existing snapshot/texture/label checks remain in place. The wait finishes the newest leased batch, so its performance cost still needs measurement.

Minimal correction and standalone regression are available for reuse; that single commit adds the wait, its include and IndexedCopyGpuParameters.cpp. I have not opened a competing PR for the kernel.

Check Result
Your original IndexedCopy and StreamCopy tests 2 passed
Added GPU-parameter regression before the correction Failed at destination word 16
Same regression with APS5_NO_COPY_HLE=1 Passed
IndexedCopy, StreamCopy and the regression after the correction 3 passed

Windows 11 10.0.26300, GCC 15.2.0 UCRT, Release, NVIDIA RTX 3060 Laptop GPU, driver 617.42. All runs used ANYPS5_REQUIRE_VULKAN=1, ANYPS5_NO_SHADER_CACHE=1 and the actually loaded Khronos validation layer, with zero skips and zero VUID- lines. The corrected tests took 15.31 s, 15.06 s and 0.64 s respectively. Conventions and whitespace checks pass. Linux and other GPU vendors were not tested, and this is a correctness regression, not a title-performance claim.

Prepared with AI assistance.

@saarthakpant

Copy link
Copy Markdown
Contributor Author

Thanks for the reproduction and the fix. You are right: the record checks covered snapshot writes, pending storage results and labels, but not a deferred address-based batch that writes the record, and the indirect-dispatch path already waits for exactly that (readDispatchArguments calls FinishLeaseWork before reading the arguments).

Your commit is on the branch as it is (c60f655, your authorship kept). On Windows 11 (10.0.26200), RTX 2060 SUPER, NVIDIA 617.42, GCC 15.2.0, with ANYPS5_REQUIRE_VULKAN=1 ANYPS5_NO_SHADER_CACHE=1 VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation (ctest -V): agc_driver_indexed_copy, agc_driver_indexed_copy_gpu_parameters and agc_driver_stream_copy pass (7.9 s, 1.6 s, 8.5 s), the layer was found, no VUID- lines.

On the cost: FinishLeaseWork returns at once unless an address-based batch is newer than what the recorder has completed, so it only waits while such a batch is in flight. I have not got a clean A/B on Ghostrunner 2 yet (the one run I made was not the same scene as the baseline); I will add one when I have it.

@saarthakpant

Copy link
Copy Markdown
Contributor Author

Cost of the wait on Ghostrunner 2 (PPSA09911), RTX 2060 SUPER, NVIDIA 617.42, Windows 11 (10.0.26200): no measurable cost.

Two runs of the same build (local base: main 4b462ef + #7687 + #5372 + this branch, with and without c60f655), identical scripted input (Space through the loading screen, then a fixed key cycle with the respawn key), APS5_PROFILE_GPU=1 APS5_PROFILE_DRAW=1, 26 ten-second windows each. Scripted runs still die and respawn at different moments, so the windows are matched by GPU load rather than compared end to end:

GPU busy per frame without the wait: windows, presents / 10 s with the wait: windows, presents / 10 s
60–85 ms 10, 32.0 6, 30.5
85–100 ms 7, 25.0 4, 25.0
100–120 ms 3, 19.0 7, 22.0
120–220 ms 6, 21.5 8, 16.0

The recorder's totals over each run: 8,758 syncs waiting 17.1 s for the GPU without the wait, 7,822 syncs waiting 16.5 s with it (the other and address-based sources, where this wait would count, went from 31 / 0.5 s and 11 / 0.1 s to 27 / 0.4 s and 2 / 0.0 s). 1,110 and 902 indexed copies went through the HLE in the two runs, all on the GPU path. So on this title the wait almost never has a newer address-based batch to finish, and the frame rate at equal GPU load is the same within run-to-run noise.

@github-actions github-actions Bot removed the unblocks title Names a title ID on the Unblocks line label Oct 20, 2026
@saarthakpant
saarthakpant force-pushed the perf/agc-indexed-copy-kernel branch from c60f655 to af7808a Compare October 20, 2026 07:50
@adriadam10 adriadam10 self-assigned this Oct 20, 2026
@saarthakpant
saarthakpant force-pushed the perf/agc-indexed-copy-kernel branch 2 times, most recently from a9ba4e4 to 7fa819b Compare October 21, 2026 13:38
@github-actions github-actions Bot added the unblocks title Names a title ID on the Unblocks line label Oct 22, 2026
saarthakpant and others added 2 commits October 22, 2026 13:46
Recognise the compute kernel that copies count 16-byte elements from a
source V# to a destination V# at offsets read from a one-record constant
buffer, and run it through the copy HLE like the other two copy kernels.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@saarthakpant
saarthakpant force-pushed the perf/agc-indexed-copy-kernel branch from 7fa819b to 17bc1bc Compare October 22, 2026 08:17
@github-actions github-actions Bot added the needs rebase Conflicts with main, rebase needed label Oct 23, 2026
…/agc-indexed-copy-kernel

# Conflicts:
#	core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferCopy.cpp
@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 23, 2026
@adriadam10
adriadam10 merged commit b7b97e9 into boykopovar:main Oct 23, 2026
10 checks passed
@adriadam10

Copy link
Copy Markdown
Collaborator

Re-checked on current main e3d03fbae: the merge with main is clean, the copy path and the leased-writer wait read as reviewed, and both new tests still cover the HLE path and the destination contents; merged.

@eliasbruno124-dev

Copy link
Copy Markdown
Contributor

GTA III PPSA03527/01.003.000 follow-up: the paired title capture finished after this PR merged. Both arms reach the first mission, with Claude stationary beside the Kuruma and blue marker at Callahan Bridge. The same position, camera and HUD were checked in renderer exports immediately before capture (04:18 vs 04:19).

Pinned pre-merge main ba1b117cec6c plus common #8527 (a0ecec3d3b5c) and #8672 (b996e31d8c11) forms baseline ef6b0b59833a. Candidate 29fe923626ce adds only this PR, exactly matching its five changed files at dd2b1d02faa9, including the leased-writer wait. This is a comparison over those declared dependencies, not pure main or a new fix.

Same Windows 10.0.26300 machine, RTX 3060 Laptop GPU/NVIDIA 617.42, i5-11400H, 3840x2160. Each side ran the mission once, then restarted with its warm cache and the same private save/profile seed and scripted game-local input. No other TRACE/PROFILE switch and no redirected native log. Each capture covers presents 4560:20, with no dropped events or dirty source. All 61 loaded PRXs and the complete runtimes were verified; original game files/names stayed unchanged.

Measured in this short window Baseline With #7968
Frame p50, ms 999.354 172.670
Frame p90, ms 1200.110 194.293
Frame p99/max, ms 1242.261 217.448
GPU busy, ms/present 720.962 98.501
Window duration, ms 20135.541 3471.663

Limits: baseline varies substantially. An earlier 120-present warm run had p50 461.943 ms and GPU 403.451 ms/present, but overflowed its detailed trace; this prompted the explicit shorter window on both arms. Candidate's preceding mission warmup was p50 210.055 ms/GPU 96.156 ms. The direction is encouraging, but this pair does not establish a repeatable speed factor. The initial cold-current baseline also exited 0xC0000005 before the mission; warmed runs did not reproduce it and its cause remains unknown. Controls and save/resume were not tested here; 173 ms median remains far from smooth gameplay.

The complete runtime pair passed the same 30 focused cases and 11 copy-byte comparisons with actual Khronos synchronization validation, no skips or validation errors and unchanged 30-second individual limits. The candidate's indexed-copy and GPU-parameter tests also passed. Linux and other GPU vendors were not run locally.

Traces and full tools/trace_digest.py output are attached below. The remaining candidate trace has queue-worker 0x0 busy at 154.9 ms/present and draw-committer busy at 95.0 ms/present; nested scope times must not be added together.

Raw summary lines
{"commit":"ef6b0b59833a","dirty":false,"env":{"APS5_DUMP_FRAMES":"151","APS5_DUMP_FRAMES_EVERY":"30","APS5_DUMP_SCALE":"4","APS5_LAZY_SHADER_PREPARE":"1","APS5_NO_PAD_INPUT":"1","APS5_PAD_INPUT_FILE":"<path>","APS5_PIPELINE_JOURNAL_THREADS":"2","APS5_TRACE_FRAMES":"4560:20"},"gpu":"NVIDIA GeForce RTX 3060 Laptop GPU","driver":"NVIDIA 617.42","os":"Windows 10.0.26300","cpu":"11th Gen Intel(R) Core(TM) i5-11400H @ 2.70GHz","cpu_threads":12,"memory_mib":32509,"title_id":"PPSA03527","title":"Grand Theft Auto III \u00e2\u0080\u0093 The Definitive Edition","content_version":"01.003.000","resolution":"3840x2160","window":{"spec":"frames 4560:20","first_present":4560,"ms":20135.541},"presents":20,"frame_ms":{"p50":999.354,"p90":1200.11,"p99":1242.261,"max":1242.261},"gpu_busy_ms_per_present":720.962,"worker_busy_pct":{"queue worker 0x0":99.424,"queue worker 0x20":97.097},"top_phases_ms_per_frame":{"Driver.Packet":1975.185,"Driver.Packet.memory_wait":954.715,"Driver.Packet.dispatch":551.048,"Driver.Dispatch":542.058,"Resources.Complete":534.656,"Vulkan.Dispatch":460.549,"Vulkan.Dispatch.shader_resources":458.31,"Graphics.Draw":391.282},"trace_dropped":false,"trace":"aps5-trace-ef6b0b59833a-22716.json.gz"}
{"commit":"29fe923626ce","dirty":false,"env":{"APS5_DUMP_FRAMES":"151","APS5_DUMP_FRAMES_EVERY":"30","APS5_DUMP_SCALE":"4","APS5_LAZY_SHADER_PREPARE":"1","APS5_NO_PAD_INPUT":"1","APS5_PAD_INPUT_FILE":"<path>","APS5_PIPELINE_JOURNAL_THREADS":"2","APS5_TRACE_FRAMES":"4560:20"},"gpu":"NVIDIA GeForce RTX 3060 Laptop GPU","driver":"NVIDIA 617.42","os":"Windows 10.0.26300","cpu":"11th Gen Intel(R) Core(TM) i5-11400H @ 2.70GHz","cpu_threads":12,"memory_mib":32509,"title_id":"PPSA03527","title":"Grand Theft Auto III \u00e2\u0080\u0093 The Definitive Edition","content_version":"01.003.000","resolution":"3840x2160","window":{"spec":"frames 4560:20","first_present":4560,"ms":3471.663},"presents":20,"frame_ms":{"p50":172.67,"p90":194.293,"p99":217.448,"max":217.448},"gpu_busy_ms_per_present":98.501,"worker_busy_pct":{"queue worker 0x0":97.882,"queue worker 0x20":95.561},"top_phases_ms_per_frame":{"Driver.Packet":334.331,"Driver.Packet.memory_wait":155.688,"Graphics.Draw":83.613,"Graphics.Draw.draw_and_resource_release":66.759,"Driver.Packet.draw":64.743,"Driver.Draw":64.139,"Driver.Packet.device_lock":43.638,"Driver.Draw.shader_compile_and_link":38.733},"trace_dropped":false,"trace":"aps5-trace-29fe923626ce-2208.json.gz"}

Prepared with AI assistance.
gta3-indexed-copy-20261023.zip

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

Labels

agc libSceAgc* perf Performance change unblocks title Names a title ID on the Unblocks line

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants