From d8d5b7c6c535195f14576bf730397dc72f437bd8 Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
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 8129432a4ba89..e1128cfdb3744 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 88d95499edd6c..a1d791522d91a 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 55ed128a9fcaa..c64f178d5fd0e 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 a59414b4d32ec..76fc80ec18e0c 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 a7f30aa30cf55..275a2c156c020 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)
{