Skip to content

feat(macos): EDR and HDR PQ/HLG presentation modes for the Metal view - #69

Merged
mcoliver merged 4 commits into
mainfrom
feat/macos-edr
Sep 30, 2026
Merged

mcoliver merged 4 commits into
mainfrom
feat/macos-edr

Conversation

@mcoliver

Copy link
Copy Markdown
Collaborator

Summary

Adds EDR and HDR (PQ/HLG) presentation to the macOS Metal view. SDR stays the default.

What was wrong

  • Everything above SDR white was clipped. The Metal path renders into an RGBA16F FBO, then copies each frame into a 10-bit unorm IOSurface. So the output was 10-bit SDR, even though the layer sets wantsExtendedDynamicRangeContent and the README claimed EDR.
  • The IOSurface colorspace tag was ignored. IOSurfaceSetValue(surf, kCVImageBufferCGColorSpaceKey, cs) stores a private CGColorSpace key, not the IOSurfaceColorSpace attachment Core Animation reads.

Changes

  • MetalPresentationFormat (new, Qt-free): defines each mode (IOSurface pixel format, matching GL formats, colorspace) and creates IOSurfaces tagged through CoreVideo (CVBufferSetAttachment), which writes IOSurfaceColorSpace/IOSurfaceColorSpaceID.

    Mode IOSurface Tag Use
    sdr (default) ARGB2101010 sRGB unchanged behaviour
    edr 64RGBAHalf extended sRGB IPCore's unclamped sRGB-encoded output passes through; values above 1.0 use the display's EDR headroom
    pq ARGB2101010 BT.2100 PQ display colorspace SMPTE 2084 or an OCIO PQ view
    hlg ARGB2101010 BT.2100 HLG display colorspace HLG or an OCIO HLG view
  • QTMetalVideoDevice / MetalView: the zero-copy ring, the CPU fallback and the fallback upload now use the selected format.

  • Choosing a mode: it's read once at startup from UTV_MACOS_PRESENTATION or Preferences → Rendering → macOS Presentation (Display/macosPresentation). The control is added in code (macOS only), so the shared .ui files don't change.

  • Logging: when an HDR mode is active, the display's current/potential EDR headroom is logged.

  • README: now describes the macOS modes accurately. docs/testing/macos-edr.md has test instructions.

Verification (Apple Silicon, macOS 27)

  • New MetalPresentationTest (headless, doctest, 41 assertions). For each mode it checks the IOSurface pixel format and colorspace tag. It then runs the same GL path as the zero-copy ring (RGBA16F FBO → blit → IOSurface-backed texture) and reads the IOSurface back:
    • EDR keeps 4.0.
    • SDR clips to 1.0.
    • PQ/HLG keep 10-bit precision.
  • IPCore doesn't clip values above 1. utvio renders linear 4.0 with -outsrgb as 1.825, exactly the extended-sRGB value.
  • This MacBook's built-in display reports potential EDR headroom 2.0×, so EDR can be checked visually without an HDR monitor.
  • ctest: 119/119 pass.

Not yet verified

  • Visual check on a display with EDR headroom, using the three-tile 1×/2×/4× test in the doc. I couldn't do this: my session had no attached display.
  • PQ/HLG with real HDR content and an OCIO HDR view.
  • The new test runs in CI once test(ci): add CLI smoke and image format round-trip tests; run ctest in CI #64's ctest step lands. On GPU-less runners it falls back to the software renderer and skips the GL round trip if IOSurface binding is unavailable.

Follow-ups

  • Tone-map or roll off highlights to the reported headroom, instead of letting macOS clip, and show headroom in the HUD.
  • React to headroom changes (display switches, brightness) via NSScreen notifications.
  • A cross-platform presenter on Qt's QRhi swapchain (HDRExtendedDisplayP3Linear / scRGB / HDR10) behind the same format abstraction.

🤖 Generated with Claude Code

The Metal presentation path rendered into an RGBA16F FBO but copied every
frame into a 10-bit unorm IOSurface, clipping everything above SDR white,
so it was 10-bit SDR only despite the layer requesting EDR. The IOSurface
colorspace tag was also ineffective: it was set with IOSurfaceSetValue()
under kCVImageBufferCGColorSpaceKey, which stores a private "CGColorSpace"
key rather than the IOSurfaceColorSpace attachment Core Animation reads.

Add MetalPresentationFormat, a small Qt-free module describing each mode
(IOSurface pixel format, matching GL formats, colorspace) and creating
correctly tagged IOSurfaces through CoreVideo:

  sdr  10-bit ARGB2101010, sRGB (default; unchanged behaviour)
  edr  64RGBAHalf, extended sRGB: IPCore's unclamped sRGB-encoded output
       passes through, values above 1.0 use the display's EDR headroom
  pq   10-bit, ITU-R BT.2100 PQ (for SMPTE 2084 / OCIO PQ display output)
  hlg  10-bit, ITU-R BT.2100 HLG

QTMetalVideoDevice's zero-copy ring and CPU fallback, and MetalView's
fallback upload, now use the selected format. The mode is chosen once per
process from UTV_MACOS_PRESENTATION or Preferences -> Rendering -> macOS
Presentation (Display/macosPresentation). MetalView logs the display's
current/potential EDR headroom when an HDR mode is active.

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

Signed-off-by: Michael Oliver <mcoliver@gmail.com>
- MetalPresentationTest (macOS, headless): checks each mode's IOSurface
  pixel format and colorspace tag, then blits an RGBA16F render FBO into
  the IOSurface as QTMetalVideoDevice does and reads it back: EDR keeps
  4.0, SDR clips to 1.0, PQ/HLG keep 10-bit precision.
- docs/testing/macos-edr.md: how to select a mode and a three-tile
  1x/2x/4x check that works on built-in Apple displays.
- README: describe the macOS SDR/EDR/HDR modes accurately.

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

Signed-off-by: Michael Oliver <mcoliver@gmail.com>
# Conflicts:
#	src/test/CMakeLists.txt

Signed-off-by: Michael Oliver <mcoliver@gmail.com>
CI builds the main_executable target, not ALL, so the test executable was
never built there and ctest reported it as "Not Run". Stage it with
RV_STAGE like FastMemcpyTest and QFontTest, which adds it to
main_executable.

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

Signed-off-by: Michael Oliver <mcoliver@gmail.com>
@mcoliver
mcoliver marked this pull request as ready for review September 30, 2026 17:37
@mcoliver
mcoliver merged commit 57b079a into main Sep 30, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant