Skip to content

perf(agc): present resident displays without the 4K convert chain - #5311

Merged
oneandonlydean merged 1 commit into
boykopovar:mainfrom
oneandonlydean:up4/present-resident-direct-2
Oct 14, 2026
Merged

oneandonlydean merged 1 commit into
boykopovar:mainfrom
oneandonlydean:up4/present-resident-direct-2

Conversation

@oneandonlydean

@oneandonlydean oneandonlydean commented Oct 13, 2026 •

Copy link
Copy Markdown
Collaborator

What

A resident display buffer that can't be blitted as-is (10-bit, 8-bit in the other channel order, or an sRGB type) went through four full-resolution steps plus a full clear of the swapchain image:

  • vkCmdCopyImageToBuffer;
  • the color transfer's convert dispatch;
  • vkCmdCopyBufferToImage into the scaler source;
  • a blit.

The scaler source image and the color transfer's buffers are single objects, so before recording that chain each present also waited for every earlier present still in flight (RetirePresents(0) behind SharedSlotInFlight).

PresentationPass now draws such an image straight into the swapchain image in one fragment pass. It reads the raw texels through an R32_UINT view and decodes them as the guest-memory path does (the display's channel order, the 10-bit variant truncated to 8 bits). It scales with a bilinear filter of the decoded values and writes black outside the aspect-fit rectangle, so there is no intermediate buffer and no clear. Its descriptor set is per present slot, so it doesn't wait for other presents. The swapchain stays B8G8R8A8_UNORM and also allows color-attachment use.

  • A 16-bit float display buffer (feat(agc): present 16-bit float display buffers #3134) keeps the convert chain: its encode goes through the scanout threshold tables. The path is chosen by the format before recording, and PresentationPass::Bind throws for such a buffer.
  • The blit paths clear only the letterbox bands instead of the whole swapchain image.
  • GPU frame dumps of a drawn image are drawn by the same pass at the dump size, sampling every n-th texel, so they hold the same bytes as before.
  • Scaled output differs from the hardware blit's filter by at most two levels per channel (filtering in the shader instead of the blit). Unscaled and letterboxed output is identical.

Tested

Linux (CachyOS, kernel 7.2, GCC 16.2, RTX 4090, NVIDIA 610.57.04), Release, BUILD_TESTING=ON, on main 0214069.

  • New agc_driver_presentation_pass (exit 77 without Vulkan). For both 8-bit and both 10-bit channel orders it compares the pass with the convert path: identical at 1:1 and letterboxed, within two levels when scaled; a 10-bit destination keeps every source bit. It also checks that dumps equal the subsampled guest-path frame, that the band clears equal a full clear, and that a 16-bit float display buffer is refused. The existing resident-present conversion tests (including 16-bit float) are unchanged and pass.
  • ctest -R "agc|shader|video": 505 of 505. The presentation tests pass again merged onto main 30f9612.
  • python3 tools/check_conventions.py --base origin/main passes.
  • A 2D title: boot, menu and gameplay to 220 s, all reference frames match, no skipped draws.

PPSA21564 scans out a 3840x2160 10-bit buffer. main 30f9612 against the same commit with this change merged, same build options, warm caches (one warm-up run each, not counted), runs alternated main, change, main, change. Title and menu are frames per second over fixed windows; the level is milliseconds per present over level frames 400 to 1200 (below 30 fps it advances a fixed step per frame, so frame indices compare like for like). A browser was open during all runs, on both sides. One main run of the third pair is left out because its presses landed during the intro and it never reached the level; a fourth pair replaced it.

scene main (n=3) this (n=3) mean change
title, fps 20.85, 22.35, 19.35 (mean 20.85) 22.5, 22.2, 23.05 (mean 22.58) +8%
menu, fps 17.1, 18.9, 19.25 (mean 18.4) 16.75, 19.5, 19.9 (mean 18.7) +2%, within the spread
first level, frames 400 to 1200, ms 34.6, 31.0, 29.9 (mean 31.8) 32.7, 29.1, 29.1 (mean 30.3) −5% (−2.7% to −6.1% per pair)

With APS5_PROFILE_DRAW=1 APS5_PROFILE_GPU=1 (one run each), present-blit GPU time per present: 2.49 ms on main, 0.11 ms with this, the same before the level and in it. The same runs: title 21.6 and 23.05 fps, menu 18.9 and 19.5 fps, first level 32.6 and 28.9 ms.

Summary lines, captured as the Performance report form describes (no APS5_PROFILE_*, caches warm), two runs each, main first in each pair, APS5_TRACE_SECONDS=155:5 (5 s in the first level). APS5_LOOP_GUARD, APS5_PAD_PRESS and APS5_DUMP_FRAMES are set by my test harness on both sides; the other three variables it sets are not read by main. The window starts at a fixed time, and the level starts 122 to 143 s into these runs, so the four windows cover different parts of the level; the frame-index numbers above are the like-for-like comparison. Over level frames 400 to 1200 these runs measured 35.9 and 30.6 ms on main, 29.6 and 27.8 ms with this. The traces are not attached (the PR was edited from the command line); I can add them.

{"commit":"30f961266ade","dirty":null,"env":{"APS5_DUMP_FRAMES":"6000","APS5_INEXACT_SINGLE_LANE":"500597a00","APS5_LOOP_GUARD":"2000","APS5_PAD_PRESS":"120:Return,130:Return","APS5_TOLERATE_DISPATCH_FAILURES":"1","APS5_TRACE_DIR":"<path>","APS5_TRACE_SECONDS":"155:5","APS5_TWO_LANE":"500597a00"},"gpu":"NVIDIA GeForce RTX 4090","driver":"NVIDIA 610.57.04","os":"Linux 7.2.3-1-cachyos","resolution":"3840x2160","window":{"spec":"seconds 155:5","first_present":3863,"ms":5029.966},"presents":146,"frame_ms":{"p50":31.314,"p90":37.484,"p99":91.894,"max":99.980},"gpu_busy_ms_per_present":17.465,"worker_busy_pct":{"queue worker 0x0":99.051,"queue worker 0x40":99.272,"queue worker 0x48":94.526,"queue worker 0x3a":0.000,"queue worker 0x46":4.046},"top_phases_ms_per_frame":{"Driver.Packet":101.903,"Driver.Packet.memory_wait":61.990,"Driver.Packet.draw":23.766,"Driver.Draw":23.708,"Driver.Draw.draw_and_resource_release":14.205,"Graphics.Draw":14.060,"Graphics.Draw.draw_and_resource_release":11.445,"Driver.Packet.prepare_memory":7.909},"trace_dropped":false,"trace":"aps5-trace-30f961266ade-1271581.json.gz"}
{"commit":"f3d1e6bcd120","dirty":null,"env":{"APS5_DUMP_FRAMES":"6000","APS5_INEXACT_SINGLE_LANE":"500597a00","APS5_LOOP_GUARD":"2000","APS5_PAD_PRESS":"120:Return,130:Return","APS5_TOLERATE_DISPATCH_FAILURES":"1","APS5_TRACE_DIR":"<path>","APS5_TRACE_SECONDS":"155:5","APS5_TWO_LANE":"500597a00"},"gpu":"NVIDIA GeForce RTX 4090","driver":"NVIDIA 610.57.04","os":"Linux 7.2.3-1-cachyos","resolution":"3840x2160","window":{"spec":"seconds 155:5","first_present":3864,"ms":5010.792},"presents":171,"frame_ms":{"p50":29.288,"p90":31.893,"p99":33.766,"max":34.137},"gpu_busy_ms_per_present":20.436,"worker_busy_pct":{"queue worker 0x0":99.070,"queue worker 0x40":99.446,"queue worker 0x48":95.266,"queue worker 0x3a":0.000,"queue worker 0x46":3.074},"top_phases_ms_per_frame":{"Driver.Packet":86.742,"Driver.Packet.memory_wait":53.453,"Driver.Packet.draw":18.090,"Driver.Draw":18.044,"Driver.Draw.draw_and_resource_release":10.021,"Graphics.Draw":9.904,"Driver.Packet.prepare_memory":8.460,"Graphics.Draw.draw_and_resource_release":7.876},"trace_dropped":false,"trace":"aps5-trace-f3d1e6bcd120-1566760.json.gz"}
{"commit":"30f961266ade","dirty":null,"env":{"APS5_DUMP_FRAMES":"6000","APS5_INEXACT_SINGLE_LANE":"500597a00","APS5_LOOP_GUARD":"2000","APS5_PAD_PRESS":"120:Return,130:Return","APS5_TOLERATE_DISPATCH_FAILURES":"1","APS5_TRACE_DIR":"<path>","APS5_TRACE_SECONDS":"155:5","APS5_TWO_LANE":"500597a00"},"gpu":"NVIDIA GeForce RTX 4090","driver":"NVIDIA 610.57.04","os":"Linux 7.2.3-1-cachyos","resolution":"3840x2160","window":{"spec":"seconds 155:5","first_present":3566,"ms":5028.586},"presents":146,"frame_ms":{"p50":33.608,"p90":37.784,"p99":39.506,"max":45.511},"gpu_busy_ms_per_present":16.351,"worker_busy_pct":{"queue worker 0x0":99.127,"queue worker 0x40":99.434,"queue worker 0x48":95.830,"queue worker 0x3a":0.000,"queue worker 0x46":2.772},"top_phases_ms_per_frame":{"Driver.Packet":102.058,"Driver.Packet.memory_wait":63.552,"Driver.Packet.draw":23.518,"Driver.Draw":23.466,"Driver.Draw.draw_and_resource_release":14.447,"Graphics.Draw":14.323,"Graphics.Draw.draw_and_resource_release":11.988,"Driver.Packet.prepare_memory":7.490},"trace_dropped":false,"trace":"aps5-trace-30f961266ade-1633952.json.gz"}
{"commit":"f3d1e6bcd120","dirty":null,"env":{"APS5_DUMP_FRAMES":"6000","APS5_INEXACT_SINGLE_LANE":"500597a00","APS5_LOOP_GUARD":"2000","APS5_PAD_PRESS":"120:Return,130:Return","APS5_TOLERATE_DISPATCH_FAILURES":"1","APS5_TRACE_DIR":"<path>","APS5_TRACE_SECONDS":"155:5","APS5_TWO_LANE":"500597a00"},"gpu":"NVIDIA GeForce RTX 4090","driver":"NVIDIA 610.57.04","os":"Linux 7.2.3-1-cachyos","resolution":"3840x2160","window":{"spec":"seconds 155:5","first_present":3725,"ms":5006.581},"presents":144,"frame_ms":{"p50":33.515,"p90":39.173,"p99":45.262,"max":148.414},"gpu_busy_ms_per_present":17.311,"worker_busy_pct":{"queue worker 0x0":99.069,"queue worker 0x40":99.452,"queue worker 0x48":94.956,"queue worker 0x3a":0.000,"queue worker 0x46":3.843},"top_phases_ms_per_frame":{"Driver.Packet":103.133,"Driver.Packet.memory_wait":62.849,"Driver.Packet.draw":23.668,"Driver.Draw":23.607,"Driver.Draw.draw_and_resource_release":12.346,"Graphics.Draw":12.210,"Driver.Packet.prepare_memory":10.135,"Graphics.Draw.draw_and_resource_release":9.609},"trace_dropped":false,"trace":"aps5-trace-f3d1e6bcd120-1993370.json.gz"}

Not tested: Windows, AMD, a title that presents an 8-bit display in the other channel order on a device.

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: none
  • GPUs tested: NVIDIA GeForce RTX 4090, driver 610.57.04, Linux (CachyOS, kernel 7.2)
  • AI-assisted: yes

A 10-bit resident display buffer (and an 8-bit one in the other channel
order or an sRGB type) went through vkCmdCopyImageToBuffer, the color
transfer's convert dispatch, vkCmdCopyBufferToImage into the scaler source
and a blit, all at the display's resolution, plus a full clear of the
swapchain image.

PresentationPass now draws such an image straight into the swapchain image
with one fragment pass: it reads the raw texels through an R32_UINT view,
decodes them as the guest-memory path does (the display's channel order, the
10-bit variant truncated to 8 bits), scales with a bilinear filter of the
decoded values and writes black outside the aspect-fit rectangle, so it
needs neither intermediate buffer nor a clear. The swapchain stays
B8G8R8A8_UNORM and now also allows color attachment use. A 16-bit float
display buffer keeps the convert chain: its encode goes through the
scanout threshold tables, and the pass refuses it.

The blit paths clear only the letterbox bands (blits from a 1x1 black
image) instead of the whole swapchain image, and the scaler source image is
only used by the guest-memory, uniform and 16-bit float paths. GPU frame
dumps of a drawn image are drawn by the same pass at the dump size,
sampling every n-th texel, so they hold the same bytes as before.

Test: agc_driver_presentation_pass compares the pass with the convert path
for both 8-bit and both 10-bit channel orders (identical unscaled and
letterboxed, within two levels of the blit filter scaled), checks that a
10-bit destination keeps every source bit, that dumps
equal the subsampled guest-path frame, that the band clears equal a full
clear, and that a 16-bit float display buffer is refused.
@github-actions

github-actions Bot commented Oct 13, 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
#4040 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp L16-17, L360, L696-697, L1705, L2382, L2850-2857
#4935 core/libs/prx/libSceAgcDriver/Execution/include/PresentationScaler.hpp L32
core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp L2797-2798, L2808-2839, L2932-2933

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.

1 participant