Skip to content

Commit 6040eac

Browse files
committed
Metal: add HDR10 / scRGB output support with tonemapped read_viewport
Ports the Vulkan driver's HDR implementation to Metal, covering both HDR10 (ST.2084 PQ, BT.2020, 10-bit swapchain) and scRGB (BT.709 linear, FP16 swapchain) output modes. The shader math in gfx/common/metal/Shaders.metal is a direct MSL port of gfx/drivers/vulkan_shaders/hdr_common.glsl, hdr.frag and hdr_tonemap.frag so shader presets authored against the Vulkan HDR pipeline look the same on Metal. Pipeline overview shader chain final pass -> HDR offscreen (RGBA16Float) -> hdr_composite_fragment -> CAMetalLayer drawable (RGB10A2Unorm PQ / RGBA16Float scRGB) -> menu / overlay / OSD layered on top The composite fragment has five branches matching the Vulkan reference: SDR -> HDR10 inverse-tonemap + PQ encode, SDR -> scRGB, shader-emitted PQ passthrough on HDR10 swapchain, shader-emitted FP16 passthrough on scRGB swapchain, and shader-emitted PQ -> scRGB conversion (HDRMode 3). When the output resolution is large enough (y > 240 * 4) and the scanlines flag is set, a CRT-mask branch in hdr_crt:: generates per-subpixel scanline luminance in linear Rec.709 and applies the mask in the target colour space — same geometry as hdr.frag. Shader-emitted HDR detection keys on SLANG_FORMAT_A2B10G10R10_* (HDR10) or SLANG_FORMAT_R16G16B16A16_SFLOAT (HDR16) in the final pass's slang output format, parallel to vulkan_filter_chain::set_hdr10() / set_hdr16(). When detected, the composite bypasses inverse-tonemap + PQ encode and either passes through (format match) or converts (PQ -> scRGB when output is scRGB). Composite is sourced from FrameView.shaderOutputTexture when a preset is active (the last shader pass's RT), or from FrameView.frameTexture otherwise (the raw core frame). Both sources are sampled into the video-viewport rect of the drawable, so aspect-ratio / integer-scale settings are honoured the same way the SDR path honours them — outside the viewport remains the clear colour, matching the SDR letterbox / pillarbox. read_viewport (off / HDR10 / scRGB) Context readBackBuffer routes through _runHDRTonemapForReadbackInto: when HDR is on. That method runs hdr_tonemap_fragment over the current drawable into a BGRA8 landing-pad texture, then the existing row-copy unpacks BGRA -> BGR and flips vertically into the caller's buffer. Screenshots, recordings, and every other consumer of video_driver_read_viewport get correct SDR pixels in all three modes. Tonemap math lives in hdr_tonemap_fragment and matches the Vulkan hdr_tonemap.frag two-branch logic. OS availability HDR is compile-gated on SDK version and runtime-gated on OS version: macOS : SDK >= 11.0, runtime >= 11.0 (kCGColorSpaceITUR_2100_PQ) iOS : SDK >= 16.0, runtime >= 16.0 (EDR on CAMetalLayer) tvOS : SDK >= 16.0, runtime >= 16.0 (EDR on CAMetalLayer) We key the compile gate on Availability.h's __MAC_11_0 / __IPHONE_16_0 / __TVOS_16_0 tokens rather than the AvailabilityMacros.h MAC_OS_X_VERSION_* constants — the former are defined consistently in all modern SDKs, while the latter have been phased out for newer point releases. RetroArch's current Apple deployment targets (macOS 10.13, iOS 11, tvOS 12.1) are below the first HDR-capable OS release, so the runtime @available(...) guards matter in addition to the compile gate. On older OSes the driver quietly falls back to SDR. Init-time mode selection video_hdr_mode is read from settings once in initWithVideo and becomes fixed for that driver instance; changing it requires a driver restart (same as Vulkan). Every MTLRenderPipelineState with a colour attachment against _layer.pixelFormat (stock blit, clear, menu, font, slang shader passes) bakes that format into its state — switching the drawable's format at runtime would invalidate all of them. metal_apply_hdr_layer_config() reconfigures the CAMetalLayer's pixelFormat / colorspace / wantsExtendedDynamicRangeContent BEFORE _initMetal compiles any pipelines, so downstream pipelines pick up the correct format at construction. Per-frame HDR knobs (paper_white_nits, expand_gamut, scanlines, subpixel_layout) are plain uniforms in the shared HDRUniforms buffer and can change at runtime via the poke interface. EDR capability announcement Driver init queries display EDR headroom (NSScreen.maximumPotentialExtendedDynamicRangeColorComponentValue on macOS, UIScreen.potentialEDRHeadroom on iOS 16+, assumed-true on tvOS 16+ since Apple TV 4K is the only HDR-capable tvOS hardware) and announces VIDEO_FLAG_HDR_SUPPORT / HDR10_SUPPORT / SCRGB_SUPPORT via video_driver_set_disp_flags() when supported. Flags are cleared on dealloc so a subsequent driver switch doesn't see stale bits. A single diagnostic log line is printed at init describing the result, to make it easy to debug cases where the HDR menu entry doesn't appear ("[Metal] HDR capability: display EDR detected, advertising HDR support YES (HDR10 + scRGB)."). Also fixed in passing The existing metal_poke_interface had four NULL slots with comments that didn't match the actual video_poke_interface_t layout: NULL, /* set_hdr_menu_nits */ NULL, /* set_hdr_paper_white_nits */ NULL, /* set_hdr_contrast */ -- no longer exists in the struct NULL /* set_hdr_expand_gamut */ -- missing 5th entry entirely Replaced with five real entries: metal_set_hdr_menu_nits, metal_set_hdr_paper_white_nits, metal_set_hdr_expand_gamut, metal_set_hdr_scanlines, metal_set_hdr_subpixel_layout. The MetalRaster font pipeline was also hardcoded to MTLPixelFormatBGRA8Unorm, which would fail validation against an HDR drawable (RGB10A2Unorm / RGBA16Float). Now uses a new Context.drawableFormat property to match whatever the layer is configured as. SDR-only builds are unaffected. Known limitations Menu / overlay / OSD render directly onto the HDR backbuffer through stock SDR pipelines (compiled against the HDR drawable format so they pass Metal validation, but emitting sRGB-encoded values that the HDR swapchain interprets in its native colour space). Visible effect differs by mode: - HDR10 : sRGB bytes land in the dark region of the PQ curve — coincidentally close enough to correct that the UI looks approximately right. - scRGB : sRGB values are interpreted as extended-linear — blacks are lifted, menu background appears medium-gray instead of dark. Fixing this properly requires a separate SDR-offscreen menu composite path (Vulkan has one as of Jan 2026: commit 12d188e "Fixed menu system displaying incorrectly in HDR on D3D11, D3D12 and Vulkan") and is deferred as follow-up work. Core video composites correctly via the HDR composite fragment in both modes. The CRT scanline/mask HDR branch has fixed geometry constants (k_crt_h_size = 1.0, k_crt_pin_phase = 0.0, etc.) identical to hdr.frag; these aren't user-configurable from the frontend yet (neither are Vulkan's). Files gfx/common/metal/metal_shader_types.h : HDRUniforms (CPU <-> GPU). gfx/common/metal/Shaders.metal : hdr:: and hdr_crt:: namespaces; hdr_composite_vertex, hdr_composite_fragment, hdr_tonemap_fragment. gfx/common/metal_common.h : HDR API on Context + FrameView; METAL_HDR_OUTPUT_* constants. gfx/drivers/metal.m : OS gate; Context HDR state + pipelines + composite/tonemap; FrameView HDR offscreen routing; setShaderFromPath HDR-format and emits-HDR detection; renderFrame composite hookup; readBackBuffer HDR-aware path; init-time layer config; EDR-headroom query and disp-flag announce; five poke setter functions.
1 parent 41d4600 commit 6040eac

4 files changed

Lines changed: 1760 additions & 18 deletions

File tree

0 commit comments

Comments
 (0)