Skip to content

perf(agc): present and keep pending only the drawn slice of an array color target - #4935

Merged
oneandonlydean merged 6 commits into
boykopovar:mainfrom
nicolasabosch:perf/agc-present-array-slice
Oct 14, 2026
Merged

oneandonlydean merged 6 commits into
boykopovar:mainfrom
nicolasabosch:perf/agc-present-array-slice

Conversation

@nicolasabosch

@nicolasabosch nicolasabosch commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

What

A title whose color target is one slice of an array image (CB_COLOR_VIEW slice of a surface with several slices) now presents from that image and keeps only the slice it drew pending. Before, the whole array image was treated as one unit in two places:

  • ResidentPresentable refused any pending image with more than one layer, so every flip of such a display buffer went through guest memory: the frame's results were stored, 33 MB were read back and uploaded again, with the GPU mutex held. ResidentPresentSlice now finds the slice whose bytes are the display buffer, and the blit, the converted path (GpuColorTransfer::DetileImage) and the frame dump read that layer. The pending image is looked up with StorageTexture::FindPendingSlice, which also finds an array image that contains the display buffer past its first slice; FindPending only matches an image that starts at the display buffer's address, so alone it served slice 0 only. Only an image the range is exactly one slice of counts, and a range that is a slice of more than one pending array image throws (is a slice of the pending array images ...: which one holds its results is not modeled).
  • After a draw, StorageTexture::MarkDirty marked every tracked layer or 64 KiB unit of the target pending. Memory under slices no draw rendered then counted as holding GPU results: any buffer or shader capture over it stored those units back and the next draw waited for the GPU. MarkSliceDirty marks the units of the rendered slice only. It falls back to MarkDirty for volumes, single-layer images and DCC-compressed targets.

No change for single-layer targets.

Tested

Windows 11, NVIDIA RTX 4070 Laptop GPU, WinLibs GCC 15.2.0 r7, Release, ANYPS5_ENABLE_SPIRV_TOOLS=ON. Tests at e7d9ff8ff on main 7e181fcc3; the title runs and traces are from 9d53a588f, before the lookup by containment was added, and were not repeated.

  • New agc_slice_dirty: clears the two slices of a linear and of a tiled 128x128 array image to different colors, marks slice 0 and stores; slice 0 holds its color and the memory of slice 1 is untouched; after marking slice 1 it is stored too. With the whole image marked instead, the test fails. With slice 0 pending it also checks the lookup presentation uses: a display buffer that is slice 0 or slice 1 finds the image through FindPendingSlice and ResidentPresentSlice names that layer, FindPending does not find slice 1, and a range past the last slice finds nothing. With two images over the same memory pending, the lookup of slice 1 throws.
  • New agc_resident_present (the existing resident presentation tests, which no CTest entry ran): the slice decision (ResidentPresentSlice) and the converted path reading layers 1 and 2 of an array image; with the layer ignored in DetileImage it fails.
  • Full ctest -j 8: 1037 passed, 8 skipped, 0 failed, out of 1045.
  • Not covered: a slice 1 display buffer presented through VulkanDevice::PresentDisplayBuffer. No test presents to a swapchain (nothing under tests calls it, and the video_out_flip_* tests are skipped here), and this title's display buffers start at their image's address, so its runs exercise slice 0 only.
  • PPSA99006 01.000.010 (ProsperoPuzzles; its display buffers are slices of two-slice 3840x2160 targets). It needs feat(agc): sample a multisampled color target through its 2D MSAA texture #4820 to draw, so both were merged locally with main 7e181fcc3. Performance traces (APS5_TRACE_FRAMES=300:600), menu screen, 45 draws per frame:
Runs Frame time p50 / p99 / max Draw work per frame GPU busy per present
#4820 alone 3 all three stop before present 300: buffer descriptor uses an unsupported type
#4820 and this 3 16.69 / 17.32 / 17.56 ms, 16.69 / 17.31 / 17.87 ms, 16.67 / 17.31 / 17.56 ms 3.9 to 4.0 ms 4.1 to 4.2 ms

On the older main e51428bf2, where #4820 alone did run, two traces of 200 presents had a p50 of 39.2 and 48.1 ms, with 9.6 to 10 ms per frame in Vulkan.Present.pixel_upload and 7.4 to 8 ms of draws waiting for the GPU mutex; with only the presentation change the p50 was 18.9 ms and the p90 42 ms.
Summary lines (aps5-summary.jsonl). There is none for main itself: the title does not draw without #4820. Before, #4820 at e7cf856f0 on main e51428bf2, APS5_TRACE_FRAMES=150:200:

{"commit":"e7cf856f08e5","dirty":false,"env":{"APS5_TRACE_DIR":"<path>","APS5_TRACE_FRAMES":"150:200"},"gpu":"NVIDIA GeForce RTX 4070 Laptop GPU","driver":"NVIDIA 616.92","os":"Windows 10.0.26300","resolution":"3840x2160","window":{"spec":"frames 150:200","first_present":150,"ms":6046.054},"presents":200,"frame_ms":{"p50":39.218,"p90":42.175,"p99":47.073,"max":56.079},"gpu_busy_ms_per_present":11.363,"worker_busy_pct":{"queue worker 0x0":73.204},"top_phases_ms_per_frame":{"Driver.Packet":22.067,"Driver.Packet.draw":21.845,"Driver.Draw":21.823,"Driver.Present":13.720,"Driver.Draw.shader_compile_and_link":12.005,"Driver.Present.present_display_buffer":9.680,"Vulkan.Present":9.677,"Vulkan.Present.pixel_upload":9.591},"trace_dropped":false,"trace":"aps5-trace-e7cf856f08e5-48708.json.gz"}
{"commit":"e7cf856f08e5","dirty":false,"env":{"APS5_TRACE_DIR":"<path>","APS5_TRACE_FRAMES":"150:200"},"gpu":"NVIDIA GeForce RTX 4070 Laptop GPU","driver":"NVIDIA 616.92","os":"Windows 10.0.26300","resolution":"3840x2160","window":{"spec":"frames 150:200","first_present":150,"ms":7355.568},"presents":200,"frame_ms":{"p50":48.132,"p90":53.128,"p99":65.473,"max":67.487},"gpu_busy_ms_per_present":12.647,"worker_busy_pct":{"queue worker 0x0":78.119},"top_phases_ms_per_frame":{"Driver.Packet":28.664,"Driver.Packet.draw":28.354,"Driver.Draw":28.324,"Driver.Draw.shader_compile_and_link":16.499,"Driver.Present":14.099,"Driver.Present.present_display_buffer":10.102,"Vulkan.Present":10.100,"Vulkan.Present.pixel_upload":10.008},"trace_dropped":false,"trace":"aps5-trace-e7cf856f08e5-52820.json.gz"}

After, #4820 and 9d53a588f merged locally with main 7e181fcc3 (commit ab8f91d0e6cb), APS5_TRACE_FRAMES=300:600:

{"commit":"ab8f91d0e6cb","dirty":false,"env":{"APS5_TRACE_DIR":"<path>","APS5_TRACE_FRAMES":"300:600"},"gpu":"NVIDIA GeForce RTX 4070 Laptop GPU","driver":"NVIDIA 616.92","os":"Windows 10.0.26300","resolution":"3840x2160","window":{"spec":"frames 300:600","first_present":300,"ms":10009.962},"presents":600,"frame_ms":{"p50":16.686,"p90":17.021,"p99":17.320,"max":17.556},"gpu_busy_ms_per_present":4.141,"worker_busy_pct":{"queue worker 0x0":25.698},"top_phases_ms_per_frame":{"Driver.Packet":4.207,"Driver.Packet.draw":4.001,"Driver.Draw":3.987,"Driver.Draw.draw_and_resource_release":2.529,"Graphics.Draw":2.496,"Graphics.Draw.draw_and_resource_release":2.465,"Driver.Present":2.181,"Vulkan.Retire":1.844},"trace_dropped":false,"trace":"aps5-trace-ab8f91d0e6cb-31404.json.gz"}
{"commit":"ab8f91d0e6cb","dirty":false,"env":{"APS5_TRACE_DIR":"<path>","APS5_TRACE_FRAMES":"300:600"},"gpu":"NVIDIA GeForce RTX 4070 Laptop GPU","driver":"NVIDIA 616.92","os":"Windows 10.0.26300","resolution":"3840x2160","window":{"spec":"frames 300:600","first_present":300,"ms":10009.763},"presents":600,"frame_ms":{"p50":16.685,"p90":17.008,"p99":17.311,"max":17.868},"gpu_busy_ms_per_present":4.183,"worker_busy_pct":{"queue worker 0x0":25.338},"top_phases_ms_per_frame":{"Driver.Packet":4.147,"Driver.Packet.draw":3.942,"Driver.Draw":3.927,"Driver.Draw.draw_and_resource_release":2.501,"Graphics.Draw":2.465,"Graphics.Draw.draw_and_resource_release":2.436,"Driver.Present":2.204,"Vulkan.Retire":1.871},"trace_dropped":false,"trace":"aps5-trace-ab8f91d0e6cb-13080.json.gz"}
{"commit":"ab8f91d0e6cb","dirty":false,"env":{"APS5_TRACE_DIR":"<path>","APS5_TRACE_FRAMES":"300:600"},"gpu":"NVIDIA GeForce RTX 4070 Laptop GPU","driver":"NVIDIA 616.92","os":"Windows 10.0.26300","resolution":"3840x2160","window":{"spec":"frames 300:600","first_present":300,"ms":10009.763},"presents":600,"frame_ms":{"p50":16.674,"p90":17.003,"p99":17.307,"max":17.559},"gpu_busy_ms_per_present":4.140,"worker_busy_pct":{"queue worker 0x0":24.970},"top_phases_ms_per_frame":{"Driver.Packet":4.085,"Driver.Packet.draw":3.877,"Driver.Draw":3.863,"Driver.Draw.draw_and_resource_release":2.502,"Graphics.Draw":2.455,"Graphics.Draw.draw_and_resource_release":2.425,"Driver.Present":2.335,"Vulkan.Retire":1.990},"trace_dropped":false,"trace":"aps5-trace-ab8f91d0e6cb-44432.json.gz"}

The two sets are not the same window or the same main, so they compare the order of magnitude, not exact numbers.

Not tested here: Linux, AMD and Intel GPUs, lavapipe, other titles, and why #4820 alone stops here on main 7e181fcc3 (not investigated; it does not stop with this change, and it did not stop on the Radeon 860M on a later main). The AMD result above is the reporter's own.

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: PPSA99006 ProsperoPuzzles (PPSA99006) on Windows / NVIDIA: MSAA blockers #4427
  • GPUs tested: NVIDIA GeForce RTX 4070 Laptop GPU, driver 616.92, Windows 11 (26300). The AMD Radeon 860M numbers above are a title run by the reporter, not the GPU tests.
  • AI-assisted: yes

🤖 Generated with Claude Code

@nicolasabosch

Copy link
Copy Markdown
Contributor Author

Me volvi loco con esta mierda, horas de laburo espero sirva de algo

@xXPBSXx

xXPBSXx commented Oct 13, 2026

Copy link
Copy Markdown
Contributor

Windows test (MinGW GCC 15.2, Release, Windows 10, RTX 4080 SUPER). Built on main 2a2a3dc65 together with nine other open GPU PRs (their own commits applied, no merge commits; one further PR, #5024, conflicted with them and was left out), so only the tests that belong to this PR say something about it:

  • tests/Recorder.cpp (target agc_driver_recorder_tests; agc_pipeline_cache, agc_pipeline_cache_file, agc_pipeline_library, _off, _fast_only, agc_unchanged_cpu_stamps, agc_storage_*, agc_recorder_read_list, agc_host_import_unmap and the other tests of that target): 3 of 3 runs passed for each.

Not tested: no game run. The batch was built once, not per PR; nothing failed, so no bisect was needed.

AI-assisted (Claude Code).

@oneandonlydean

Copy link
Copy Markdown
Collaborator

StorageTexture::FindPending only returns an image whose base address is the display buffer's (Texture.cpp:3061), so a display buffer in slice 1 or later never reaches ResidentPresentSlice and still goes through guest memory. The new test calls ResidentPresentSlice directly, so it doesn't catch this. Please find the image by containment and present slice 1 through PresentDisplayBuffer in the test, or say in the description that only slice 0 is served.

Please also add the GPUs tested line and the summary lines of the traces you compared. And what makes buffer descriptor uses an unsupported type go away with this change on 7e181fcc3?

@nicolasabosch

Copy link
Copy Markdown
Contributor Author

@oneandonlydean you are right, thank you. FindPending only matches an image that starts at the display buffer's address, so at 9d53a588f only slice 0 was served and a display buffer in a later slice still went through guest memory.

  • 71c74b10d adds StorageTexture::FindPendingSlice: the lookup by base address first, then an array image that contains the range past its first slice. PresentableResident uses it, and ResidentPresentSlice still decides whether the range is exactly one slice.
  • Test: agc_slice_dirty now checks, on a pending two-slice image (linear and tiled), that a display buffer that is slice 0 or slice 1 finds the image and gets that layer, that FindPending does not find slice 1, and that a range past the last slice finds nothing. It does not go through PresentDisplayBuffer: no test in the tree presents to a swapchain, so I could not add that without a new harness. The description says so, and that this title only exercises slice 0.
  • GPUs tested and the summary lines are in the description. There is no summary line for main itself, because the title does not draw without feat(agc): sample a multisampled color target through its 2D MSAA texture #4820; the "before" lines are feat(agc): sample a multisampled color target through its 2D MSAA texture #4820 alone on an older main. I can attach the trace files if you want them.
  • Why buffer descriptor uses an unsupported type goes away: I do not know. I did not trace it. My guess is that marking the whole array pending made the store-back write stale units of the slice no draw rendered over the memory where the title uploads its per-draw data, but that is unverified, and LeandroLP23's runs on a later main did not hit the stop without this change. The description lists it as not investigated.

AI-assisted: yes (Claude Code).

@oneandonlydean

Copy link
Copy Markdown
Collaborator

Thanks, slice 1 finds its image now. If more than one pending array image contains the range, FindPendingSlice returns whichever it meets first; please throw there instead of picking one.

@nicolasabosch

Copy link
Copy Markdown
Contributor Author

@oneandonlydean done in e7d9ff8ff: FindPendingSlice throws when the range is a slice of more than one pending array image. It also only counts an image the range is exactly one slice of, so an image that merely contains the range no longer hides the one it is a slice of. agc_slice_dirty marks two images over the same memory pending and checks the throw.

AI-assisted: yes (Claude Code).

nicolasabosch and others added 6 commits October 14, 2026 17:37
… image

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

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

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

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@oneandonlydean
oneandonlydean force-pushed the perf/agc-present-array-slice branch from e7d9ff8 to 3ddfc23 Compare October 14, 2026 04:42
@oneandonlydean

Copy link
Copy Markdown
Collaborator

Rebased onto main after #5311: its one-pass present now takes the drawn slice, so a display buffer in slice 1 or later presents through it too. agc_driver_presentation_pass now presents each slice of a two-slice image through the pass and checks it, and its frame dump, against the convert path.

@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 14, 2026
@github-actions

github-actions Bot commented Oct 14, 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
#1189 core/libs/prx/libSceAgcDriver/CMakeLists.txt L1003, L1005
#4978 core/libs/prx/libSceAgcDriver/CMakeLists.txt L1005
#5232 core/libs/prx/libSceAgcDriver/CMakeLists.txt L1003, L1005
#5249 core/libs/prx/libSceAgcDriver/CMakeLists.txt L1005
#5465 core/libs/prx/libSceAgcDriver/CMakeLists.txt L1003, L1005
#5560 core/libs/prx/libSceAgcDriver/CMakeLists.txt L1003, L1005

@oneandonlydean
oneandonlydean merged commit fcd0ff8 into boykopovar:main Oct 14, 2026
8 checks passed
@nicolasabosch
nicolasabosch deleted the perf/agc-present-array-slice branch October 14, 2026 09:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agc libSceAgc* build

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants