From d8d5b7c6c535195f14576bf730397dc72f437bd8 Mon Sep 17 00:00:00 2001 From: Sam Lantinga Date: Mon, 6 Jul 2026 13:24:20 -0700 Subject: [PATCH] Added a slider to manually adjust HDR headroom on visionOS Ideally the OS would provide a way to query the current headroom, but for now users can manually adjust it. --- include/SDL3/SDL_hints.h | 14 ++++++ .../uikit/SDL_CurvedContentHosting.swift | 49 +++++++++++++++++++ src/video/uikit/SDL_CurvedContentView.swift | 5 ++ src/video/uikit/SDL_UIKitBridge-swift.h | 6 +++ src/video/uikit/SDL_UIKitBridge.m | 15 ++++++ 5 files changed, 89 insertions(+) diff --git a/include/SDL3/SDL_hints.h b/include/SDL3/SDL_hints.h index 8129432a4b..e1128cfdb3 100644 --- a/include/SDL3/SDL_hints.h +++ b/include/SDL3/SDL_hints.h @@ -4324,6 +4324,20 @@ extern "C" { */ #define SDL_HINT_VIDEO_X11_XRANDR "SDL_VIDEO_X11_XRANDR" +/** + * A variable controlling whether the HDR headroom slider should be shown in the visionOS window settings. + * + * The variable can be set to the following values: + * + * - "0": The HDR headroom slider is not shown. (default) + * - "1": The HDR headroom slider is shown. + * + * This hint should be set before SDL is initialized. + * + * \since This hint is available since SDL 3.6.0. + */ +#define SDL_HINT_VISIONOS_HDR_HEADROOM_UI "SDL_VISIONOS_HDR_HEADROOM_UI" + /** * A variable controlling whether touch should be enabled on the back panel of * the PlayStation Vita. diff --git a/src/video/uikit/SDL_CurvedContentHosting.swift b/src/video/uikit/SDL_CurvedContentHosting.swift index 88d95499ed..a1d791522d 100644 --- a/src/video/uikit/SDL_CurvedContentHosting.swift +++ b/src/video/uikit/SDL_CurvedContentHosting.swift @@ -224,6 +224,7 @@ internal class SDL_CurvedContentPersistentSettings: Codable { var showHover: Bool? var isDimmed: Bool? var curvatureRadius: Float? + var headroom: Float? } @Observable @@ -247,6 +248,9 @@ internal class SDL_CurvedContentSettings { if let curvatureRadius = values.curvatureRadius { settings.curvatureRadius = curvatureRadius } + if let headroom = values.headroom { + settings.headroom = headroom + } } catch { NSLog("Couldn't parse window settings: %@", error.localizedDescription) } @@ -261,6 +265,7 @@ internal class SDL_CurvedContentSettings { values.showHover = showHover values.isDimmed = isDimmed values.curvatureRadius = curvatureRadius + values.headroom = headroom do { let data = try JSONEncoder().encode(values) @@ -276,6 +281,8 @@ internal class SDL_CurvedContentSettings { var isDimmed: Bool = false var dimmingReady: Bool = false var curvatureRadius: Float = 0.0 + var headroom_enabled: Bool = SDL_VisionOS_ShouldShowHeadroomUI() + var headroom: Float = 2.0 var sceneState: SceneState = .interactive var isSnapped: Bool = false var settingsExpanded: Bool = false @@ -314,6 +321,16 @@ struct SDL_SettingsPanelView: View { } } + private var headroomLabel: String { + let formatter = NumberFormatter() + formatter.maximumFractionDigits = 2 + if let label = formatter.string(for: settings.headroom) { + return label + } else { + return "" + } + } + var body: some View { if settings.settingsExpanded { expandedPanel @@ -333,6 +350,11 @@ struct SDL_SettingsPanelView: View { Image(systemName: settings.isDimmed ? "moon.fill" : "sun.max") .foregroundStyle(settings.isDimmed ? .primary : .secondary) + if settings.headroom_enabled { + Text("HDR") + .font(.caption) + } + Divider().frame(height: 8) if settings.curvatureRadius == 0 { @@ -355,6 +377,11 @@ struct SDL_SettingsPanelView: View { Image(systemName: settings.isDimmed ? "moon.fill" : "sun.max") .foregroundStyle(settings.isDimmed ? .primary : .secondary) + if settings.headroom_enabled { + Text("HDR") + .font(.caption) + } + Divider().frame(height: 8) if settings.curvatureRadius == 0 { @@ -415,6 +442,28 @@ struct SDL_SettingsPanelView: View { Spacer() } + if settings.headroom_enabled { + // HDR slider + VStack(spacing: 4) { + Text("HDR \(headroomLabel)") + .font(.caption) + + HStack() { + Image(systemName: "moon.fill") + + Slider(value: $settings.headroom, in: 1.1...4) { + } currentValueLabel: { + Text("\(headroomLabel)") + } + .onChange(of: settings.headroom) { + settings.save() + } + + Image(systemName: "sun.max") + } + } + } + // Curvature slider VStack(spacing: 4) { Text("\(curvatureLabel)") diff --git a/src/video/uikit/SDL_CurvedContentView.swift b/src/video/uikit/SDL_CurvedContentView.swift index 55ed128a9f..c64f178d5f 100644 --- a/src/video/uikit/SDL_CurvedContentView.swift +++ b/src/video/uikit/SDL_CurvedContentView.swift @@ -264,6 +264,11 @@ internal struct SDL_CurvedContentView: View { } } } + .onChange(of: settings.headroom, initial: true) { oldHeadroom, headroom in + if settings.headroom_enabled { + SDL_VisionOS_SendHeadroom(headroom) + } + } .modifier(AnimatedCurveRadiusModifier(helper: helper, curveRadius: animatedScreenRadius)) .onChange(of: sceneActivationOrObject(shouldPopulateCollisionShape ? helper.collisionShape : nil)) { guard let curvedUIEntity else { return } diff --git a/src/video/uikit/SDL_UIKitBridge-swift.h b/src/video/uikit/SDL_UIKitBridge-swift.h index a59414b4d3..76fc80ec18 100644 --- a/src/video/uikit/SDL_UIKitBridge-swift.h +++ b/src/video/uikit/SDL_UIKitBridge-swift.h @@ -32,6 +32,12 @@ void SDL_VisionOS_SendWindowSettings(NSString *settings); // Called from Swift scene delegates when pointer mode changes void SDL_VisionOS_SendPointerMode(bool enabled); +// Called from Swift scene delegates when window settings are loaded +bool SDL_VisionOS_ShouldShowHeadroomUI(); + +// Called from Swift scene delegates when headroom changes +void SDL_VisionOS_SendHeadroom(float headroom); + // Called from Swift scene delegates when visionOS delivers a touch event void SDL_VisionOS_SendTouch(NSTimeInterval timestamp, SDL_FingerID fingerID, Uint32 eventType, float x, float y); diff --git a/src/video/uikit/SDL_UIKitBridge.m b/src/video/uikit/SDL_UIKitBridge.m index a7f30aa30c..275a2c156c 100644 --- a/src/video/uikit/SDL_UIKitBridge.m +++ b/src/video/uikit/SDL_UIKitBridge.m @@ -94,6 +94,21 @@ bool SDL_VisionOS_PointerModeEnabled() return SDL_pointer_mode; } +bool SDL_VisionOS_ShouldShowHeadroomUI() +{ + return SDL_GetHintBoolean(SDL_HINT_VISIONOS_HDR_HEADROOM_UI, false); +} + +void SDL_VisionOS_SendHeadroom(float headroom) +{ + SDL_VideoDisplay *display = SDL_GetVideoDisplay(SDL_GetPrimaryDisplay()); + if (display) { + SDL_HDROutputProperties HDR = { 1.0f, headroom }; + + SDL_SetDisplayHDRProperties(display, &HDR); + } +} + // Called from Swift scene delegates when visionOS delivers a touch event void SDL_VisionOS_SendTouch(NSTimeInterval timestamp, SDL_FingerID fingerID, Uint32 eventType, float x, float y) {