Skip to content

feat(videoout): present PQ display buffers through an HDR10 swapchain - #5697

Merged
adriadam10 merged 3 commits into
boykopovar:mainfrom
gjonn:hdr-output
Oct 14, 2026
Merged

adriadam10 merged 3 commits into
boykopovar:mainfrom
gjonn:hdr-output

Conversation

@gjonn

@gjonn gjonn commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

What

Depends on #5696. This branch is built on that PR's branch, and its first commit is that PR. Review only the second commit, feat(videoout): present PQ display buffers through an HDR10 swapchain.

This PR reports HDR output to titles when the host can show HDR10, and presents their BT.2100 PQ display buffers unchanged through an HDR10 swapchain. When HDR output is unavailable, nothing changes: the status stays SDR, the swapchain stays B8G8R8A8 sRGB, and PQ buffers take #5696's SDR conversion.

Detection. HDR output is available when all of these hold:

  • The window's Vulkan surface offers a 10-bit format (A2B10G10R10 or A2R10G10B10) in VK_COLOR_SPACE_HDR10_ST2084_EXT, with the blit-destination and color-attachment features the presenter writes it with.
  • The instance has VK_EXT_swapchain_colorspace and the device has VK_EXT_hdr_metadata. Both are enabled when present.
  • On Windows, the monitor showing the window must also run in HDR: DXGI's IDXGIOutput6::GetDesc1 reports DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020. Its peak, full-frame and minimum luminance come from the same call. The monitor is read again when the window moves to another monitor or the DXGI factory goes stale, which happens when HDR is toggled.
  • The device must be able to blit and linearly filter both 10-bit PQ texel formats, because the presenter scales them unchanged.
  • HDR output is Windows-only for now. On Linux and other hosts the probe reports an SDR monitor, so HDR output stays off whatever the surface offers. A surface can list HDR10 formats while the display is not in HDR: KDE Wayland with RADV does this with the output's HDR disabled. SDL2 and Vulkan expose no output HDR state; a later PR can add a compositor or output-state signal in the probe's non-Windows branch.
  • A Windows monitor that reports no usable peak gets a default of 1000 nits peak and full frame, 0 nits black. The default is documented in docs/user/USAGE.md and in the technical debt.

The host state lives in libSceAgcDriver (HostHdr.cpp). libSceVideoOut and libSceSystemService reach it only through extern "C" _nid_postfix exports and header-inline code. objdump of the built .prx files shows every libSceVideoOut and libSceSystemService import from libSceAgcDriver in its export table.

What the title sees.

  • sceVideoOutGetOutputStatus (and ...WithVrr, which embeds it) reports dynamic range 2 and flag bit 0 while HDR output is available, and 1 and 0 otherwise. The values are named in SharpProspero (VideoOutOutputDynamicRange.Hdr = 2, VideoOutOutputStatusFlags.Hdr = 1 << 0).
  • sceSystemServiceGetHdrToneMapLuminance reports the monitor's peak, full-frame peak and black level while HDR output is available, and 100/100/0 otherwise, as before. The peak it returns is recorded.
  • Every open port's output-mode event fires when HDR output becomes available or unavailable, and when its luminance changes while it is available.
  • Detection starts when VideoOut is first used. Before that, sceSystemServiceGetHdrToneMapLuminance reports SDR. The availability found at start-up fires no event beyond the one every output-mode subscription already gets when it is made. This is documented in the technical debt.

Presentation. VulkanDevice::SelectDynamicRange runs at each flip, after Resize, under the GPU mutex. A PQ display buffer while HDR output is available recreates the swapchain in the 10-bit HDR10 format, preferring the buffer's own channel order, and sets VkHdrMetadataEXT:

  • BT.2020 primaries and D65;
  • the monitor's luminance range;
  • MaxCLL = the peak the title was told;
  • MaxFALL = the full-frame peak.

The PQ texels then reach the swapchain unchanged on every path:

  • Linear buffers: raw texels go through a scaler whose source image has the buffer's 10-bit format.
  • Tiled buffers: a raw ColorTransfer copy goes through that same scaler.
  • DCC clears: the raw 10-bit clear value.
  • Resident images: PresentationPass draws to the HDR10 target with the 10-bit codes kept (RecordPresent takes the destination color space).

Any non-PQ display buffer switches back to the SDR swapchain. The surface formats and the host state are read again before every swapchain recreation. If HDR output went away or the HDR10 format is no longer offered (HDR turned off, or the window moved to an SDR monitor), the recreated swapchain is SDR, so an out-of-date HDR10 swapchain is never recreated in a color space the surface dropped. A blank flip keeps the current one. Frame dumps (APS5_DUMP_FRAMES) of an HDR10 presentation hold its SDR conversion. Change to #5696's conversion (called out so it is reviewed here): without HDR output, the white level of the PQ to SDR conversion is now the peak sceSystemServiceGetHdrToneMapLuminance last returned to the title, or the current one when it never asked. A title grades to the value it read and does not follow a later HDR change. With #5696 alone that peak is always 100 nits, so the change has no effect until this PR reports other values. That is why it is in this PR and not in #5696.

No environment switches are added.

Overlap:

Tested

Windows 11 (10.0.26220), MinGW-w64 GCC 15.2.0, Release, BUILD_TESTING=ON, NVIDIA GeForce RTX 5090 Laptop GPU (driver 616.92):

  • agc_host_hdr (CPU):
    • the DXGI luminance clean-up and the default;
    • the choice of HDR10 surface format;
    • the white levels;
    • the availability rule and when the generation (the output-mode event trigger) changes;
    • the CPU conversion and DCC clear value of a PQ buffer at the peak the title was told (1000 nits);
    • raw texel reads;
    • the swapchain format choice (DynamicRangeSurfaceFormat): SDR without HDR output or for SDR buffers, the HDR10 format in the buffer's own order otherwise, and a throw when HDR output is reported but no HDR10 format is offered;
    • the raw clear color of a PQ DCC clear (PqClearColor).
      Each test function resets the host state first, so the order does not matter.
  • guest_video_out_hdr (guest ABI, opens VideoOut): drives the host state through the exports and checks:
    • sceVideoOutGetOutputStatus and ...WithVrr;
    • the sceSystemServiceGetHdrToneMapLuminance values and the recorded peak;
    • that the output-mode event fires when availability or luminance changes, and does not fire for an HDR10 monitor without an HDR10 surface.
  • agc_driver_presentation_pass (Vulkan):
    • a resident PQ image drawn to A2B10G10R10 and A2R10G10B10 HDR10 targets keeps every 10-bit code, and its letterbox is PQ black;
    • an SDR buffer is refused for an HDR10 target.
  • video_out_hdr_present (real window, sceVideoOutSubmitFlip):
    • With HDR output, tiled, linear and DCC-cleared PQ buffers must reach an HDR10 swapchain, a blank flip must keep it, an SDR buffer must bring the SDR swapchain back, and a PQ buffer must bring HDR10 back.
    • The host is then set to SDR and the title is "told" 437 nits. The same buffers, each flipped twice, must reach the SDR swapchain converted at a 437 nit white level. Among the frame dumps there must be one within one level of the CPU conversion for each buffer, which tolerates frames dropped by an out-of-date swapchain. Measured: a largest difference of 0 for the tiled (GPU detile), linear and DCC-cleared buffers.
    • The HDR10 half ran on real hardware after Windows HDR was turned on: DXGI reports the monitor at 1064.5 nits peak, 483.9 full frame and 4.8387 black, and NVIDIA's surface offers A2B10G10R10 in HDR10. The run is in the PR comment. It shows each of these steps:
      • PQ buffers presenting HDR10 (swapchain format 64 in VK_COLOR_SPACE_HDR10_ST2084_EXT) with HDR metadata;
      • the SDR buffer switching back to SDR, and PQ switching to HDR10 again;
      • a monitor luminance change re-setting the metadata (600 nits);
      • the monitor reported as SDR switching back to SDR, with the 437-nit conversion matching the CPU exactly.
        The real monitor-change paths (toggling Windows HDR or moving the window during a run) have not been exercised; they go through the same DXGI re-read and the swapchain recreation shown here.
  • video_out_hdr_probe (offscreen SDL window): with the surface reporting HDR10, a window without a Windows monitor report reads as an SDR monitor and HDR output stays unavailable. On Windows the offscreen window has no Win32 handle, which takes the same SDR answer, so the non-Windows branch itself is exercised only on Linux CI.
  • video_out_flip_tests present, dcc, backtoback, lifetime and aftergpu, run by hand in a real window: passed. ctest runs these modes only with SDL's offscreen driver, where they skip.
  • Full ctest: 1136 tests, 0 failed. 8 skipped: video_out_flip_* with the offscreen driver, guest_video_out_wqhd_detection_no_vulkan and agc_driver_stencil_ref_export, as on main, and video_out_hdr_present in a run before the change above. This was before the review fixes and the rebase; the HDR tests and feat(agc): present BT.2100 PQ display buffers on an SDR output #5696's tests were rerun after both.
  • The DXGI probe treats every DXGI failure (for example DXGI_ERROR_NOT_CURRENTLY_AVAILABLE under Remote Desktop) and a window without a Win32 handle as an SDR monitor, and never throws from the presenter loop. This path is untested.
  • tools/check_conventions.py passes.
  • Linux: HDR output is off by design (see above). The branch was not built or run on Linux.
  • PPSA01317: this branch has not been run in the title. main alone does not get the title far enough without unrelated local fixes. A different, equivalent HDR patch on the user's local tree did present the title's 0x8100070422000000 buffers through an HDR10 swapchain, and a Windows HDR toggle switched SDR and HDR10. That is not evidence for this branch.

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: feat(agc): present BT.2100 PQ display buffers on an SDR output #5696
  • Unblocks: PPSA01317 Gran Turismo 7 (PPSA01317) #4697
  • GPUs tested: NVIDIA GeForce RTX 5090 Laptop GPU (driver 616.92), Windows 11 10.0.26220, HDR10 swapchain exercised with Windows HDR on; not run on Linux or lavapipe.
  • AI-assisted: yes

gjonn added 2 commits October 14, 2026 02:55
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
Detect HDR output on the host: on Windows the monitor showing the window
must run in HDR10 (DXGI IDXGIOutput6), and on every host the window's
Vulkan surface must offer a 10-bit VK_COLOR_SPACE_HDR10_ST2084_EXT
swapchain with VK_EXT_swapchain_colorspace and VK_EXT_hdr_metadata. The
monitor's luminance comes from DXGI; hosts that report none get 1000
nits peak and full frame, 0 nits black.

While HDR output is available, sceVideoOutGetOutputStatus reports
dynamic range 2 and flag bit 0, sceSystemServiceGetHdrToneMapLuminance
reports the monitor's luminance, and every open port's output mode
event fires when that changes. PQ display buffers are presented
unchanged through an HDR10 swapchain with HDR metadata; any other
display buffer switches back to the SDR swapchain. Without HDR output
the PQ to SDR conversion uses the peak the title was last told as its
white level.

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
#5131 docs/user/USAGE.md L163
#5525 docs/user/USAGE.md L163
#5800 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp L2664-2665, L2717-2718

This was referenced Oct 14, 2026

@adriadam10 adriadam10 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the second commit only (stacked on #5696, still open). Built and ran the scoped ctest on the PR merged onto main: agc_host_hdr, guest_video_out_hdr, agc_driver_presentation_pass, agc_resident_present, agc_display_buffer_dcc, linear_display_buffer and video_out_hdr_present pass (agc_driver_tessellation segfaults inside libvulkan_radeon.so here, same as on main).

  • HostHdrProbe::Poll (non-Windows branch) and the body ("a surface that offers HDR10 is taken to mean an HDR display"): this is wrong on a real Linux setup. On KDE Wayland with RADV, vulkaninfo lists A2B10G10R10_UNORM_PACK32 and A2R10G10B10_UNORM_PACK32 in COLOR_SPACE_HDR10_ST2084_EXT while kscreen-doctor -o reports HDR: disabled on the HDMI output and HDR: incapable on DP and eDP. With this PR sceVideoOutGetOutputStatus would report HDR with a made-up 1000 nit peak, and PQ buffers would go to an HDR10 swapchain on an SDR monitor. Linux needs a signal that the output is actually in HDR (compositor/output state), or HDR output stays off there until one exists.
  • The HDR10 half of the PR (SelectDynamicRange, recreateSwapchain, setHdrMetadata, the HDR10 scaler and the passthrough paths in VulkanDevice.cpp) has never run against a real HDR10 swapchain: the Tested section says NVIDIA's surface offered no HDR10 formats and video_out_hdr_present passed on its SDR half only. Please attach a run where it printed [videoout] presenting HDR10 and the frames reached the display (Windows with a driver that offers HDR10, or Linux with an output really in HDR), including the switch back to SDR and the monitor-change path.
  • The PPSA01317 evidence is for a different patch ("This branch was not run in the title"). Please re-run the title on this branch once the HDR10 path has been exercised.

Rebase onto main after #5696 lands is ours to do; nothing needed from you there.

A Vulkan surface can offer HDR10 formats while the display is not in
HDR (KDE Wayland with RADV lists them with the output's HDR disabled),
so on hosts other than Windows the probe now reports an SDR monitor and
HDR output stays off until an output-state signal exists. A probe test
checks that a window without a Windows monitor report reads as SDR with
an HDR10 surface.

The HDR10 present test now also changes the monitor's luminance while
presenting HDR10, and the presenter logs the HDR metadata it sets.

AI-assisted: yes
@gjonn

gjonn commented Oct 14, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the review. All three points are addressed in 4e27d8d (a new commit; no force-push).

1. Non-Windows detection. You're right: the surface's formats don't say whether the output is in HDR. HDR output is now Windows-only.

  • On Linux and other hosts the probe reports an SDR monitor, so sceVideoOutGetOutputStatus stays SDR and PQ buffers take feat(agc): present BT.2100 PQ display buffers on an SDR output #5696's SDR conversion, whatever the surface offers.
  • The probe's non-Windows branch is where an output-state signal (compositor or kscreen/DRM) can be added later. No compositor code is added now.
  • New test video_out_hdr_probe: with the surface reporting HDR10, a window without a Windows monitor report must read as an SDR monitor, with HDR output unavailable. On Linux CI this runs the non-Windows branch. On Windows the offscreen window has no Win32 handle, which gives the same SDR answer.
  • USAGE.md, the libSceVideoOut debt entry and the PR body now say HDR output is Windows-only. They cite the KDE Wayland/RADV case.

2. The HDR10 half on real hardware. Windows HDR is now on here. DXGI reports the monitor in DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020 at 1064.5 nits peak, 483.9 nits full frame and 4.8387 nits black, and NVIDIA's surface (RTX 5090 Laptop, driver 616.92) offers A2B10G10R10 in VK_COLOR_SPACE_HDR10_ST2084_EXT.

video_out_hdr_present on this branch at 4e27d8d does the following:

  • flips tiled, linear and DCC-cleared PQ buffers, then a blank flip, through sceVideoOutSubmitFlip;
  • flips an SDR buffer, then PQ again;
  • changes the monitor's luminance while presenting HDR10;
  • sets the monitor to SDR and flips the PQ buffers again.

Every flip completed through vkQueuePresentKHR. The output, trimmed to the presenter's lines (format 64 is A2B10G10R10, 44 is B8G8R8A8, and 1000104008 is VK_COLOR_SPACE_HDR10_ST2084_EXT):

[videoout] HDR output available: peak 1064.5 nits, full frame 483.9 nits, black 4.8387 nits
[videoout] presenting HDR10: swapchain format 64 in color space 1000104008
[videoout] HDR metadata: mastering 1064.5 to 4.8387 nits, MaxCLL 1064.5 nits, MaxFALL 483.9 nits
[videoout] presenting SDR: swapchain format 44 in color space 0
[videoout] presenting HDR10: swapchain format 64 in color space 1000104008
[videoout] HDR metadata: mastering 1064.5 to 4.8387 nits, MaxCLL 1064.5 nits, MaxFALL 483.9 nits
[videoout] HDR output available: peak 600.0 nits, full frame 350.0 nits, black 0.0500 nits
[videoout] HDR metadata: mastering 600.0 to 0.0500 nits, MaxCLL 600.0 nits, MaxFALL 350.0 nits
[videoout] HDR output unavailable (display in HDR: no, surface offers HDR10: yes)
[videoout] presenting SDR: swapchain format 44 in color space 0
HDR10 presentation passed (peak 1064.5 nits)
SDR presentation of the tiled PQ buffer at 437 nits (GPU detile conversion): largest difference from the CPU conversion 0
SDR presentation of the linear PQ buffer at 437 nits (CPU conversion): largest difference from the CPU conversion 0
SDR presentation of the DCC-cleared PQ buffer at 437 nits (clear value conversion): largest difference from the CPU conversion 0
1/1 Test #874: video_out_hdr_present ............   Passed    2.16 sec

The test drives the monitor change through the same host-state entry point the DXGI probe uses. A physical change during a run (toggling Windows HDR, or moving the window to another monitor) has not been captured yet; I'll add it here when it has been done. The other HDR suites also pass with HDR on: agc_host_hdr, guest_video_out_hdr, agc_driver_presentation_pass, agc_resident_present, agc_display_buffer_dcc and linear_display_buffer. So do the real-window flip modes present, dcc and lifetime.

3. The title evidence. That evidence was for a different patch, and the body no longer suggests otherwise. This branch has not been run in PPSA01317. Stock main plus this branch doesn't get the title far enough without unrelated local fixes, so I can't claim a title test on this exact branch. For the record only: an equivalent HDR patch on a local tree did present the title's 0x8100070422000000 buffers through an HDR10 swapchain, and a Windows HDR toggle switched SDR and HDR10.

Overlap noted in the body: #5800 conflicts textually on the two frame-dump conditions (whichever lands second ANDs frameDumpOutput.Enabled_nid_no_patch() in). #5131 and #5525 touch neighbouring USAGE.md lines.

AI-assisted: yes

@adriadam10 adriadam10 self-assigned this Oct 14, 2026
@gjonn
gjonn requested a review from adriadam10 October 14, 2026 13:08

@adriadam10 adriadam10 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-checked 4e27d8d against the three points of the previous review. The non-Windows probe now stays SDR (covered by video_out_hdr_probe, which fails with the old behaviour), the HDR10 path has a real-hardware run (format 64 in VK_COLOR_SPACE_HDR10_ST2084_EXT, metadata, SDR/HDR10 switches, luminance change), and the PPSA01317 claim is scoped correctly. Built and ran the scoped ctest on the PR merged onto main: agc_host_hdr, guest_video_out_hdr, agc_driver_presentation_pass, agc_resident_present, agc_display_buffer_dcc, linear_display_buffer, video_out_hdr_present and video_out_hdr_probe pass. Merging once the Windows build is green.

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