Skip to content

perf(agc): keep the 10-bit scanout resident and present it with the guest path's pixels - #278

Merged
boykopovar merged 1 commit into
boykopovar:mainfrom
oneandonlydean:perf/agc-resident-10bit-scanout
Oct 3, 2026
Merged

boykopovar merged 1 commit into
boykopovar:mainfrom
oneandonlydean:perf/agc-resident-10bit-scanout

Conversation

@oneandonlydean

Copy link
Copy Markdown
Collaborator

What

A title that composites into a 10-bit scanout (COLOR_2_10_10_10 with the alternate swap, A2R10G10B10) never presents from a resident image. No guest texture format matches that color format, so the target stays non-resident. Every frame's composite draw then runs synchronously, and the flip reads the frame back through guest memory. On main, ResidentPresentable also only accepts an image in exactly the display's format, and it would blit a 10-bit image. A blit rounds where the guest-memory path drops each channel's low two bits.

  • Resident target. The blue-low 10-bit color target now keeps its bits in the storage image of its surface's red-low 2-10-10-10 format, the same way B8G8R8A8 targets map to R8G8B8A8 (FindGuestColorTargetFormat).
  • Display format. DisplayFormat.cpp names a display buffer's texel order as the guest-memory path reads it: red low for the R8G8B8A8 base, otherwise blue low, with A2B10G10R10 or A2R10G10B10 for the 10-bit variant. The bit-56 and R8G8B8A8 checks that VulkanDevice.cpp repeated in two places now use it.
  • Present path. ResidentPresentPath blits a resident image only when it holds the display's 8-bit texel format and can be a blit source, which makes it an exact copy. It converts instead when the image is 10-bit, holds the other 8-bit order or is an sRGB type that a blit would decode. GpuColorTransfer::DetileImage copies the image's texels raw into the color transfer's buffer and runs the guest path's own conversion shader on them, so both paths present the same pixels. A converted present uses the shared transfer and scaler source, so it waits for the slots like the guest-memory path (PresentWaitsForSlots).
  • Write-back. The image's write-back is unchanged. Its results stay pending like any resident target's and are stored when a reader of the memory flushes them.

This builds on the recorder's resident presentation path (ResidentPresentable / PresentableResident, from #5) and @boykopovar's GPU color transfer (7ed9ef1). DetileImage reuses GpuColorTransfer's buffers and pipeline. convert was split into the copy and a dispatch so the image path skips the guest upload.

Measured in PPSA21564 on my integration branch: the final composite renders into 4K A2R10G10B10 scanout buffers. Before this change that composite ran synchronously, about 11 ms each, 143 times per 10 s at the title, plus a guest-memory readback for every flip. With this change the scanout stays resident, so the composite is recorded like any other draw and the flip presents from the image through the conversion, with no readback.

Tested

  • Linux (CachyOS, RTX 4090). New tests/ResidentPresent.cpp, run from agc_driver_recorder_tests (a GPU test, built but not part of ctest):
    • It checks the format decision for both 8-bit and both 10-bit orders.
    • It checks that the blue-low 10-bit color target has a storage format.
    • It converts a random image in each order through DetileImage. The result must equal both the CPU decode (DecodeDisplayBuffer) and the GPU detile of the same words in guest memory.
  • The decision and conversion functions are new, so the test has no main counterpart. Instead I confirmed that each part fails without its change:
    • Without the TextureFormat.cpp line it fails with "the blue-low 10-bit color target has no storage format".
    • With main's rule (blit a 10-bit A2B10G10R10 image) it fails with "a 10-bit image is blitted (the blit rounds where the guest path truncates)".
    • Converting the raw words as tiled fails with "the resident image presents other pixels than guest memory".
  • ctest --test-dir build --timeout 120: 96/96 pass on current main with this branch. agc_driver_recorder_tests, agc_driver_graphics_tests and agc_structurizer_tests pass when run directly, and the libs target builds.
  • Exercised in PPSA21564 on my integration branch (numbers above), which passes my visual regression check of the title, menu and level frames.

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 in TechnicalDebt
  • No notes, investigation .md files or images added to the repository
  • New third-party code is a submodule built from source (none added)
  • Generic behaviour, not specific to one title
  • Depends on: none
  • AI-assisted: yes

@github-actions github-actions Bot added build agc libSceAgc* labels Oct 2, 2026
@github-actions github-actions Bot added the needs rebase Conflicts with main, rebase needed label Oct 2, 2026
…uest path's pixels

PPSA21564's final composite renders into 4K 10-bit scanout buffers
(COLOR_2_10_10_10 with the alternate swap, A2R10G10B10). No guest texture
format matched that color format, so the target stayed non-resident:
every frame's composite draw ran synchronously (about 11 ms, 143 per 10 s
at the title) and the flip read the frame back through guest memory.

The blue-low 10-bit color target now keeps its bits in the storage image
of its surface's red-low 2-10-10-10 format, as B8G8R8A8 targets map to
R8G8B8A8.

Presenting it: DisplayTexelFormat names a display buffer's texel order as
the guest-memory path reads it (R8G8B8A8 base: red low, else blue low;
the 10-bit variant: A2B10G10R10 or A2R10G10B10). ResidentPresentPath
blits a resident image only when it holds the display's 8-bit texel
format (an exact copy). A 10-bit image (whose blit would round where the
guest path drops the low two bits), the other 8-bit order or an sRGB type
is converted instead: GpuColorTransfer::DetileImage copies the image's
texels raw and runs the guest path's own conversion shader on them, so
both paths present the same pixels.

The image's write-back is unchanged: its results stay pending like any
resident target's and are stored when a reader of the memory flushes
them.

Tests: agc_driver_recorder_tests checks the format decision for both
8-bit and both 10-bit orders, and converts an image in each order,
comparing it with the CPU decode and the GPU detile of the same words in
guest memory.
@oneandonlydean
oneandonlydean force-pushed the perf/agc-resident-10bit-scanout branch from a99ddc1 to 6142e82 Compare October 2, 2026 20:24
@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 2, 2026
@boykopovar
boykopovar merged commit 5dd3c6c into boykopovar:main Oct 3, 2026
3 checks passed
@oneandonlydean
oneandonlydean deleted the perf/agc-resident-10bit-scanout branch October 3, 2026 07:02
oneandonlydean added a commit to oneandonlydean/AnyPS5 that referenced this pull request Oct 3, 2026
…oykopovar#304 boykopovar#305 boykopovar#306 boykopovar#322 boykopovar#409 boykopovar#416, boykopovar#276 boykopovar#277 boykopovar#275 boykopovar#307, shader ops boykopovar#226 boykopovar#255 boykopovar#260 boykopovar#280 boykopovar#308 boykopovar#309 boykopovar#311 boykopovar#318 boykopovar#320 boykopovar#324 boykopovar#326 boykopovar#379 boykopovar#384 boykopovar#399, boykopovar#261, kernel boykopovar#335 boykopovar#341 boykopovar#349 boykopovar#355, libs boykopovar#227 boykopovar#244 boykopovar#245 boykopovar#273 boykopovar#274 boykopovar#279 boykopovar#286 boykopovar#287 boykopovar#289 boykopovar#296 boykopovar#300 boykopovar#328 boykopovar#358 boykopovar#402) into astrobot

Where our PR merged in a revised form, main's version is taken; where
astrobot carries the same work plus later local commits, astrobot's code
stays and main's additions appear once.

Conflicts:
- AvPlayer (Export.cpp both libs, include/AvPlayer.hpp, src/Player.cpp,
  src/Source.cpp, AvPlayerLibrary.cmake, CMakeLists.txt, SceTypes.hpp,
  tests/GuestAvPlayer.cpp): main's boykopovar#307 (our reviewed squash, which carries
  9c8e7fd e9ea5c8 adcede3 fb4eeb9 953057c ac456ee). It links the
  shared ffmpeg-core target, so astrobot's second FFmpeg build goes:
  3rdparty/FFmpeg submodule, core/libs/FFmpeg.cmake and src/Exports.cpp.
- libScePngEnc: main's boykopovar#227 (supersedes OverkillLabs' encoder).
- libScePad Export.cpp: main's boykopovar#287 tilt correction (supersedes 49d45f5).
- libSceAudioPropagation Export.cpp: astrobot's object model, which
  defines every function boykopovar#289 declares as a stub.
- libSceSaveData.native Export.cpp: both (SaveDataHoldWrites and boykopovar#289's
  unknown exports).
- VideoOutDriver.cpp: astrobot's (2546bd1 counters around boykopovar#416's
  lastFlipVblank store, same value). tests/Flip.cpp: boykopovar#416's pacing test and
  boykopovar#417's after-GPU test both kept.
- boykopovar#278 files (DisplayFormat, VulkanDevice, GpuColorTransfer,
  ResidentPresent, agc CMakeLists): main's comment-free boykopovar#278 plus
  astrobot's P5 present-direct (b29728d), which removed DetileImage,
  ResidentPresent.hpp and the recorder-test hook; the resident present test
  stays its own executable.
- VulkanDevice.cpp/.hpp: astrobot's GDS, local memory probe, sample dumps
  and P5 code; boykopovar#303/boykopovar#409 comments as main has them; main's shader clock
  (boykopovar#324) and DepthSurface-free includes.
- RequestSerializer.cpp: version stays 8 (6 mesh passthrough, 7
  nonConstantImageOffsets, 8 astrobot's later field). boykopovar#303's auto-merged
  second write and read of nonConstantImageOffsets removed.
- SpirvEmitter.hpp, SpirvEmitterState.hpp, Recompiler.hpp/.cpp, StageIO.hpp,
  SpirvOptimizer.cpp, InstructionTranslator.cpp, ShaderReplay.cpp: local
  fields kept, boykopovar#303/boykopovar#305 comments as main has them.
- SpirvAluEmitterMath.cpp: v_readlane keeps the split-wave path and takes
  boykopovar#322's wrap at the wave size for the others.
- SpirvEmitter.cpp: astrobot's single-lane layout check.
- SpirvFlowEmitter.cpp: boykopovar#304 as merged (the second lane emits a shared
  value itself when the first lane defined nothing).
- SpirvImageEmitter.cpp: main's Offset operand placed before MinLod.
- boykopovar#304: main's WaveUniformValues (SpirvAnalysis.cpp) replaces astrobot's
  copy in SpirvWaveExchange.cpp.
- Recorder.hpp/.cpp, Draw.hpp, Graphics Draw.cpp, ShaderResources.cpp:
  astrobot's boykopovar#276/boykopovar#277 with its later snapshot work (per-use budgets,
  stale-snapshot refresh, profile counters, boykopovar#410, boykopovar#424, fan GS); boykopovar#409/boykopovar#306
  comments as main has them.
- PacketMemory.cpp: boykopovar#409 as merged plus astrobot's two opt-outs.
- tests: ShaderMemory.cpp takes boykopovar#303's and boykopovar#304's new tests; Recorder.cpp
  takes boykopovar#409's afterRecordedWorkTests and drops main's copies of the
  boykopovar#276/boykopovar#277 tests astrobot already has; Structurizer.cpp keeps boykopovar#413's case.

boykopovar#143 (NGG passthrough) stays as on astrobot: upstream did not touch its
files since the boykopovar#258 revert.
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.

2 participants