Skip to content

feat(agc): present BT.2100 PQ display buffers on an SDR output - #5696

Merged
adriadam10 merged 1 commit into
boykopovar:mainfrom
gjonn:hdr-pq-display-buffers
Oct 14, 2026
Merged

adriadam10 merged 1 commit into
boykopovar:mainfrom
gjonn:hdr-pq-display-buffers

Conversation

@gjonn

@gjonn gjonn commented Oct 14, 2026

Copy link
Copy Markdown
Contributor

What

Presents display buffers in the 10-bit BT.2100 PQ pixel formats 0x8100070422000000 (red in the low bits) and 0x8100070400000000 (blue in the low bits) instead of throwing VideoOut: unsupported display pixel format at sceVideoOutRegisterBuffers2.

The format values come from open references: SharpProspero names both (Rgb10A2Bt2100Pq, Bgr10A2Bt2100Pq) and KytyPS5 maps the first (VIDEO_OUT_PIXEL_FORMAT_R10_G10_B10_A2_BT2100_PQ). PPSA01317 registers the first itself. Main's output is SDR, so the texels are converted to 8-bit sRGB when presented:

  1. The SMPTE ST 2084 EOTF, from a 1024-entry table computed once in double precision. The CPU and both shaders read the same table.
  2. BT.2020 to BT.709 primaries (linear 3x3 matrix), with negative components clipped.
  3. A luminance tone map that keeps hue. min(203 nits, white level) is SDR white; 203 nits is the BT.2408 HDR reference white. Luminance passes unchanged up to 0.75 of SDR white. Above that, an extended Reinhard shoulder reaches 1.0 exactly at the white level, with a continuous slope at the knee. The white level is the tone-map luminance sceSystemServiceGetHdrToneMapLuminance reports. On main that is always 100 nits, which is now one shared constant (ToneMapLuminance.hpp), so the shoulder reduces to a clip at 100 nits. The follow-up HDR output PR makes the white level follow the luminance reported to the title.
  4. The sRGB encoding, rounded to 8 bits. Alpha is opaque.

Every display path main has converts them:

  • Linear buffers: DecodeDisplayBuffer on the CPU.
  • Tiled buffers from guest memory: GpuColorTransfer::Detile, as a new mode of ColorTransfer.comp.
  • Resident images: PresentationPass.frag (perf(agc): present resident displays without the 4K convert chain #5311's resident present without the 4K convert chain). ResidentPresentPath returns Convert for these formats, so a PQ image is never blitted.
  • DCC fast-clear values: DisplayBufferClearPixel.
  • Frame dumps follow the same paths.

The two shaders share Graphics/shaders/PqToSdr.glsl through GL_GOOGLE_include_directive. Other format values with PQ or BT.2020 bits (8-bit, float, linear transfer) still throw. The technical debt entry records the open references, and that the tone map is ours: how a console shows such a buffer on an SDR display is unknown.

Overlap:

Tested

Windows 11 (10.0.26220), MinGW-w64 GCC 15.2.0, Release, BUILD_TESTING=ON, this branch on main d8888df.

  • linear_display_buffer (CPU):
    • Both formats are accepted, with their sizes, texel formats and resident decisions. Six nearby format values are refused.
    • Hand-computed anchors at a 100 nit white level, exact, in both channel orders.
    • Six anchors at a 1000 nit white level that check the shoulder independently: a grey above white gives 255, and 537, 341 and 147 nit greys and a below-knee grey give their computed codes.
    • 34x34x34 PQ code triples at 100, 150 and 1000 nits against an independent double-precision reference, within one level.
  • agc_display_buffer_dcc (CPU): fast-clear codes 0000 and 1111, and register clears in both orders.
  • agc_resident_present (Vulkan): GPU detile of random tiled PQ texels from guest memory against the CPU conversion, within one level, at 100, 1000 and 150 nits.
  • agc_driver_presentation_pass (Vulkan): resident PQ images in A2B10G10R10 and A2R10G10B10, both orders, at 100 and 1000 nits, against the CPU conversion:
    • exact size and letterboxed: within one level;
    • scaled: within two levels, the limit the existing formats use;
    • frame dumps: within one level;
    • a white level below 1 nit is refused.
  • agc_driver_bda_device and guest_system_service pass unchanged.
  • On main these tests fail: DisplayBufferSize throws for both formats, and DisplayTexelFormat(0x8100070422000000) returns the blue-low format.
  • Mutations: a 200 nit reference white in the shader fails agc_resident_present. A wrong knee or a hard-coded white level in the fragment shader fails agc_driver_presentation_pass at 1000 nits.
  • tools/check_conventions.py --base origin/main passes.
  • PPSA01317: the patch this PR re-implements on main ran the title on Windows from a local tree, with the HDR output of the follow-up PR. This branch was not run in the title, and the SDR conversion has not been compared with the title's 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 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: PPSA01317 Gran Turismo 7 (PPSA01317) #4697
  • GPUs tested: NVIDIA GeForce RTX 5090 Laptop GPU (driver 616.92) and Intel Graphics (driver 101.8860), Windows 11 10.0.26220. Not run on lavapipe or Linux.
  • AI-assisted: yes

Accept the 10-bit BT.2100 PQ display formats 0x8100070422000000 (red
in the low bits) and 0x8100070400000000 (blue in the low bits) and
convert them to 8-bit sRGB when presented: PQ EOTF, BT.2020 to BT.709
primaries, a luminance tone map whose white level is the tone-map
luminance sceSystemServiceGetHdrToneMapLuminance reports (reference
white min(203 nits, white level)), and the sRGB encoding.

The conversion runs on the CPU for linear buffers and DCC clear values,
in the color transfer shader for tiled guest buffers, and in the
presentation pass for resident images. The three share the PQ EOTF
table computed once in double precision.

AI-assisted: yes
@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
#5581 core/libs/prx/libSceAgcDriver/Graphics/include/GpuColorTransfer.hpp L38
core/libs/prx/libSceAgcDriver/Graphics/shaders/ColorTransfer.comp L6
core/libs/prx/libSceAgcDriver/Graphics/src/GpuColorTransfer.cpp L5-6, L55-65

@adriadam10

Copy link
Copy Markdown
Collaborator

Checked on the PR merged onto main: agc_display_buffer_dcc, agc_resident_present, agc_driver_presentation_pass and linear_display_buffer pass, the new tests don't build without the change, the CPU, compute and fragment paths agree within the stated tolerances, and the debt entry records that the tone map is ours. Resident PQ images always go through the presentation pass, so the DetileImage path never sees them. Merging once the full Windows build is green.

@adriadam10
adriadam10 merged commit 29f4891 into boykopovar:main Oct 14, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants