SDL: GPU: OpenXR integration (#14837)

From 9a91d7236a13e8770af4cdb7a880a24de397fdc2 Mon Sep 17 00:00:00 2001
From: Aaron Benjamin <[EMAIL REDACTED]>
Date: Fri, 30 Jan 2026 17:18:51 -0500
Subject: [PATCH] GPU: OpenXR integration (#14837)

Based on Beyley's initial draft in #11601.

Co-authored-by: Beyley Cardellio <ep1cm1n10n123@gmail.com>
Co-authored-by: Ethan Lee <flibitijibibo@gmail.com>
---
 Android.mk                                    |    1 +
 CMakeLists.txt                                |    6 +
 VisualC-GDK/SDL/SDL.vcxproj                   |    6 +
 VisualC-GDK/SDL/SDL.vcxproj.filters           |    3 +
 VisualC/SDL/SDL.vcxproj                       |    4 +
 VisualC/SDL/SDL.vcxproj.filters               |   29 +
 Xcode/SDL/SDL.xcodeproj/project.pbxproj       |   22 +
 include/SDL3/SDL_gpu.h                        |   14 +
 include/SDL3/SDL_hints.h                      |   12 +
 include/SDL3/SDL_openxr.h                     |  207 +
 include/build_config/SDL_build_config.h.cmake |    1 +
 .../build_config/SDL_build_config_android.h   |    1 +
 .../build_config/SDL_build_config_windows.h   |    1 +
 src/SDL_internal.h                            |    1 +
 src/dynapi/SDL_dynapi.c                       |    2 +
 src/dynapi/SDL_dynapi.sym                     |    7 +
 src/dynapi/SDL_dynapi_overrides.h             |    7 +
 src/dynapi/SDL_dynapi_procs.h                 |    7 +
 src/gpu/SDL_gpu.c                             |   47 +
 src/gpu/SDL_sysgpu.h                          |   29 +
 src/gpu/d3d12/SDL_gpu_d3d12.c                 |  577 +-
 src/gpu/metal/SDL_gpu_metal.m                 |   49 +
 src/gpu/vulkan/SDL_gpu_vulkan.c               |  815 +-
 src/gpu/xr/SDL_gpu_openxr.c                   |  199 +
 src/gpu/xr/SDL_gpu_openxr.h                   |   30 +
 src/gpu/xr/SDL_openxr_internal.h              |   52 +
 src/gpu/xr/SDL_openxrdyn.c                    |  414 +
 src/gpu/xr/SDL_openxrdyn.h                    |   55 +
 src/gpu/xr/SDL_openxrsym.h                    |   49 +
 src/video/khronos/openxr/openxr.h             | 8583 +++++++++++++++++
 .../openxr/openxr_loader_negotiation.h        |  141 +
 src/video/khronos/openxr/openxr_platform.h    |  776 ++
 .../khronos/openxr/openxr_platform_defines.h  |  114 +
 src/video/khronos/openxr/openxr_reflection.h  | 7226 ++++++++++++++
 .../openxr/openxr_reflection_parent_structs.h |  330 +
 test/testsymbols.c                            |    2 +
 36 files changed, 19720 insertions(+), 99 deletions(-)
 create mode 100644 include/SDL3/SDL_openxr.h
 create mode 100644 src/gpu/xr/SDL_gpu_openxr.c
 create mode 100644 src/gpu/xr/SDL_gpu_openxr.h
 create mode 100644 src/gpu/xr/SDL_openxr_internal.h
 create mode 100644 src/gpu/xr/SDL_openxrdyn.c
 create mode 100644 src/gpu/xr/SDL_openxrdyn.h
 create mode 100644 src/gpu/xr/SDL_openxrsym.h
 create mode 100644 src/video/khronos/openxr/openxr.h
 create mode 100644 src/video/khronos/openxr/openxr_loader_negotiation.h
 create mode 100644 src/video/khronos/openxr/openxr_platform.h
 create mode 100644 src/video/khronos/openxr/openxr_platform_defines.h
 create mode 100644 src/video/khronos/openxr/openxr_reflection.h
 create mode 100644 src/video/khronos/openxr/openxr_reflection_parent_structs.h

diff --git a/Android.mk b/Android.mk
index f4600bfa2757e..2e3b11483c75e 100644
--- a/Android.mk
+++ b/Android.mk
@@ -39,6 +39,7 @@ LOCAL_SRC_FILES := \
 	$(wildcard $(LOCAL_PATH)/src/io/generic/*.c) \
 	$(wildcard $(LOCAL_PATH)/src/gpu/*.c) \
 	$(wildcard $(LOCAL_PATH)/src/gpu/vulkan/*.c) \
+	$(wildcard $(LOCAL_PATH)/src/gpu/xr/*.c) \
 	$(wildcard $(LOCAL_PATH)/src/haptic/*.c) \
 	$(wildcard $(LOCAL_PATH)/src/haptic/android/*.c) \
 	$(wildcard $(LOCAL_PATH)/src/haptic/dummy/*.c) \
diff --git a/CMakeLists.txt b/CMakeLists.txt
index e03bae7ee5a3d..1211d81acc63c 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -391,6 +391,7 @@ set_option(SDL_LIBUDEV             "Enable libudev support" ON)
 set_option(SDL_ASAN                "Use AddressSanitizer to detect memory errors" OFF)
 set_option(SDL_CCACHE              "Use Ccache to speed up build" OFF)
 set_option(SDL_CLANG_TIDY          "Run clang-tidy static analysis" OFF)
+dep_option(SDL_GPU_OPENXR          "Build SDL_GPU with OpenXR support" ON "SDL_GPU;NOT RISCOS" OFF)
 
 set(SDL_VENDOR_INFO "" CACHE STRING "Vendor name and/or version to add to SDL_REVISION")
 
@@ -1272,6 +1273,8 @@ sdl_glob_sources(
   "${SDL3_SOURCE_DIR}/src/filesystem/*.h"
   "${SDL3_SOURCE_DIR}/src/gpu/*.c"
   "${SDL3_SOURCE_DIR}/src/gpu/*.h"
+  "${SDL3_SOURCE_DIR}/src/gpu/xr/*.c"
+  "${SDL3_SOURCE_DIR}/src/gpu/xr/*.h"
   "${SDL3_SOURCE_DIR}/src/joystick/*.c"
   "${SDL3_SOURCE_DIR}/src/joystick/*.h"
   "${SDL3_SOURCE_DIR}/src/haptic/*.c"
@@ -3550,6 +3553,9 @@ if(SDL_GPU)
     set(SDL_VIDEO_RENDER_GPU 1)
     set(HAVE_RENDER_GPU TRUE)
   endif()
+  if(SDL_GPU_OPENXR)
+    set(HAVE_GPU_OPENXR 1)
+  endif()
 endif()
 
 # Dummies
diff --git a/VisualC-GDK/SDL/SDL.vcxproj b/VisualC-GDK/SDL/SDL.vcxproj
index a1356ab17f8f5..c92e3b4f8328c 100644
--- a/VisualC-GDK/SDL/SDL.vcxproj
+++ b/VisualC-GDK/SDL/SDL.vcxproj
@@ -927,6 +927,12 @@
       <CompileAs Condition="'$(Configuration)|$(Platform)'=='Release|Gaming.Xbox.XboxOne.x64'">CompileAsCpp</CompileAs>
     </ClCompile>
     <ClCompile Include="..\..\src\gpu\vulkan\SDL_gpu_vulkan.c" />
+    <ClCompile Include="..\..\src\gpu\xr\SDL_gpu_openxr.c" />
+    <ClCompile Include="..\..\src\gpu\xr\SDL_openxrdyn.c" />
+  </ItemGroup>
+  <ItemGroup>
+    <ClInclude Include="..\..\src\gpu\xr\SDL_gpu_openxr_c.h" />
+    <ClInclude Include="..\..\src\gpu\xr\SDL_openxr_internal.h" />
   </ItemGroup>
   <ItemGroup>
     <ResourceCompile Include="..\..\src\core\windows\version.rc" />
diff --git a/VisualC-GDK/SDL/SDL.vcxproj.filters b/VisualC-GDK/SDL/SDL.vcxproj.filters
index a2f431abf1942..06bcb2460c467 100644
--- a/VisualC-GDK/SDL/SDL.vcxproj.filters
+++ b/VisualC-GDK/SDL/SDL.vcxproj.filters
@@ -52,6 +52,9 @@
     <ClCompile Include="..\..\src\gpu\SDL_gpu.c" />
     <ClCompile Include="..\..\src\gpu\d3d12\SDL_gpu_d3d12.c" />
     <ClCompile Include="..\..\src\gpu\vulkan\SDL_gpu_vulkan.c" />
+    <ClCompile Include="..\..\src\gpu\xr\SDL_gpu_openxr.c" />
+    <ClCompile Include="..\..\src\gpu\xr\SDL_openxrdyn.c" />
+    <ClInclude Include="..\..\src\gpu\xr\SDL_openxr_internal.h" />
     <ClCompile Include="..\..\src\haptic\dummy\SDL_syshaptic.c" />
     <ClCompile Include="..\..\src\haptic\SDL_haptic.c" />
     <ClCompile Include="..\..\src\haptic\windows\SDL_dinputhaptic.c" />
diff --git a/VisualC/SDL/SDL.vcxproj b/VisualC/SDL/SDL.vcxproj
index 588f17693ba42..4ce70c0261b72 100644
--- a/VisualC/SDL/SDL.vcxproj
+++ b/VisualC/SDL/SDL.vcxproj
@@ -475,6 +475,8 @@
     <ClInclude Include="..\..\src\gpu\d3d12\D3D12_Blit.h" />
     <ClInclude Include="..\..\src\gpu\SDL_sysgpu.h" />
     <ClInclude Include="..\..\src\gpu\vulkan\SDL_gpu_vulkan_vkfuncs.h" />
+    <ClInclude Include="..\..\src\gpu\xr\SDL_gpu_openxr_c.h" />
+    <ClInclude Include="..\..\src\gpu\xr\SDL_openxr_internal.h" />
     <ClInclude Include="..\..\src\hidapi\SDL_hidapi_windows.h" />
     <ClInclude Include="..\..\src\hidapi\windows\hidapi_cfgmgr32.h" />
     <ClInclude Include="..\..\src\hidapi\windows\hidapi_descriptor_reconstruct.h" />
@@ -629,6 +631,8 @@
     <ClCompile Include="..\..\src\gpu\SDL_gpu.c" />
     <ClCompile Include="..\..\src\gpu\d3d12\SDL_gpu_d3d12.c" />
     <ClCompile Include="..\..\src\gpu\vulkan\SDL_gpu_vulkan.c" />
+    <ClCompile Include="..\..\src\gpu\xr\SDL_gpu_openxr.c" />
+    <ClCompile Include="..\..\src\gpu\xr\SDL_openxrdyn.c" />
     <ClCompile Include="..\..\src\io\generic\SDL_asyncio_generic.c" />
     <ClCompile Include="..\..\src\io\SDL_asyncio.c" />
     <ClCompile Include="..\..\src\main\generic\SDL_sysmain_callbacks.c" />
diff --git a/VisualC/SDL/SDL.vcxproj.filters b/VisualC/SDL/SDL.vcxproj.filters
index c97143a205015..3c2dab84a9410 100644
--- a/VisualC/SDL/SDL.vcxproj.filters
+++ b/VisualC/SDL/SDL.vcxproj.filters
@@ -981,6 +981,15 @@
     <ClInclude Include="..\..\src\gpu\SDL_sysgpu.h">
       <Filter>gpu</Filter>
     </ClInclude>
+    <ClInclude Include="..\..\src\gpu\vulkan\SDL_gpu_vulkan_vkfuncs.h">
+      <Filter>gpu</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\src\gpu\xr\SDL_gpu_openxr_c.h">
+      <Filter>gpu</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\src\gpu\xr\SDL_openxr_internal.h">
+      <Filter>gpu</Filter>
+    </ClInclude>
     <ClInclude Include="..\..\include\SDL3\SDL_storage.h" />
     <ClInclude Include="..\..\include\SDL3\SDL_time.h" />
     <ClInclude Include="..\..\src\core\SDL_core_unsupported.h" />
@@ -1953,6 +1962,26 @@
     <ClCompile Include="..\..\src\gpu\SDL_gpu.c">
       <Filter>gpu</Filter>
     </ClCompile>
+    <ClCompile Include="..\..\src\gpu\d3d12\SDL_gpu_d3d12.c">
+      <Filter>gpu</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\src\gpu\vulkan\SDL_gpu_vulkan.c">
+      <Filter>gpu</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\src\gpu\xr\SDL_gpu_openxr.c">
+      <Filter>gpu</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\src\gpu\xr\SDL_openxrdyn.c">
+      <Filter>gpu</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\src\process\SDL_process.c" />
+    <ClCompile Include="..\..\src\process\windows\SDL_windowsprocess.c" />
+    <ClCompile Include="..\..\src\render\gpu\SDL_pipeline_gpu.c" />
+    <ClCompile Include="..\..\src\render\gpu\SDL_render_gpu.c" />
+    <ClCompile Include="..\..\src\render\gpu\SDL_shaders_gpu.c" />
+    <ClCompile Include="..\..\src\storage\generic\SDL_genericstorage.c" />
+    <ClCompile Include="..\..\src\storage\steam\SDL_steamstorage.c" />
+    <ClCompile Include="..\..\src\storage\SDL_storage.c" />
     <ClCompile Include="..\..\src\joystick\hidapi\SDL_hidapi_steam_triton.c">
       <Filter>joystick\hidapi</Filter>
     </ClCompile>
diff --git a/Xcode/SDL/SDL.xcodeproj/project.pbxproj b/Xcode/SDL/SDL.xcodeproj/project.pbxproj
index 46a6edc2993e6..a7c4f3d01ab86 100644
--- a/Xcode/SDL/SDL.xcodeproj/project.pbxproj
+++ b/Xcode/SDL/SDL.xcodeproj/project.pbxproj
@@ -366,6 +366,10 @@
 		E4F257952C81903800FCEAFC /* SDL_gpu_vulkan.c in Sources */ = {isa = PBXBuildFile; fileRef = E4F257832C81903800FCEAFC /* SDL_gpu_vulkan.c */; };
 		E4F257962C81903800FCEAFC /* SDL_gpu.c in Sources */ = {isa = PBXBuildFile; fileRef = E4F257852C81903800FCEAFC /* SDL_gpu.c */; };
 		E4F257972C81903800FCEAFC /* SDL_sysgpu.h in Headers */ = {isa = PBXBuildFile; fileRef = E4F257862C81903800FCEAFC /* SDL_sysgpu.h */; };
+		E4F257982C81903800FCEAFC /* SDL_gpu_openxr.c in Sources */ = {isa = PBXBuildFile; fileRef = E4F257882C81903800FCEAFC /* SDL_gpu_openxr.c */; };
+		E4F257992C81903800FCEAFC /* SDL_openxrdyn.c in Sources */ = {isa = PBXBuildFile; fileRef = E4F257892C81903800FCEAFC /* SDL_openxrdyn.c */; };
+		E4F2579A2C81903800FCEAFC /* SDL_gpu_openxr_c.h in Headers */ = {isa = PBXBuildFile; fileRef = E4F2578A2C81903800FCEAFC /* SDL_gpu_openxr_c.h */; };
+		E4F2579B2C81903800FCEAFC /* SDL_openxr_internal.h in Headers */ = {isa = PBXBuildFile; fileRef = E4F2578C2C81903800FCEAFC /* SDL_openxr_internal.h */; };
 		E4F7981A2AD8D84800669F54 /* SDL_core_unsupported.c in Sources */ = {isa = PBXBuildFile; fileRef = E4F798192AD8D84800669F54 /* SDL_core_unsupported.c */; };
 		E4F7981C2AD8D85500669F54 /* SDL_dynapi_unsupported.h in Headers */ = {isa = PBXBuildFile; fileRef = E4F7981B2AD8D85500669F54 /* SDL_dynapi_unsupported.h */; };
 		E4F7981E2AD8D86A00669F54 /* SDL_render_unsupported.c in Sources */ = {isa = PBXBuildFile; fileRef = E4F7981D2AD8D86A00669F54 /* SDL_render_unsupported.c */; };
@@ -937,6 +941,10 @@
 		E4F257832C81903800FCEAFC /* SDL_gpu_vulkan.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = SDL_gpu_vulkan.c; sourceTree = "<group>"; };
 		E4F257852C81903800FCEAFC /* SDL_gpu.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = SDL_gpu.c; sourceTree = "<group>"; };
 		E4F257862C81903800FCEAFC /* SDL_sysgpu.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SDL_sysgpu.h; sourceTree = "<group>"; };
+		E4F257882C81903800FCEAFC /* SDL_gpu_openxr.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = SDL_gpu_openxr.c; sourceTree = "<group>"; };
+		E4F257892C81903800FCEAFC /* SDL_openxrdyn.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = SDL_openxrdyn.c; sourceTree = "<group>"; };
+		E4F2578A2C81903800FCEAFC /* SDL_gpu_openxr_c.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SDL_gpu_openxr_c.h; sourceTree = "<group>"; };
+		E4F2578C2C81903800FCEAFC /* SDL_openxr_internal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SDL_openxr_internal.h; sourceTree = "<group>"; };
 		E4F798192AD8D84800669F54 /* SDL_core_unsupported.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = SDL_core_unsupported.c; sourceTree = "<group>"; };
 		E4F7981B2AD8D85500669F54 /* SDL_dynapi_unsupported.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SDL_dynapi_unsupported.h; sourceTree = "<group>"; };
 		E4F7981D2AD8D86A00669F54 /* SDL_render_unsupported.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = SDL_render_unsupported.c; sourceTree = "<group>"; };
@@ -2354,11 +2362,23 @@
 			path = vulkan;
 			sourceTree = "<group>";
 		};
+		E4F2578B2C81903800FCEAFC /* xr */ = {
+			isa = PBXGroup;
+			children = (
+				E4F257882C81903800FCEAFC /* SDL_gpu_openxr.c */,
+				E4F257892C81903800FCEAFC /* SDL_openxrdyn.c */,
+				E4F2578A2C81903800FCEAFC /* SDL_gpu_openxr_c.h */,
+				E4F2578C2C81903800FCEAFC /* SDL_openxr_internal.h */,
+			);
+			path = xr;
+			sourceTree = "<group>";
+		};
 		E4F257872C81903800FCEAFC /* gpu */ = {
 			isa = PBXGroup;
 			children = (
 				E4F257812C81903800FCEAFC /* metal */,
 				E4F257842C81903800FCEAFC /* vulkan */,
+				E4F2578B2C81903800FCEAFC /* xr */,
 				E4F257852C81903800FCEAFC /* SDL_gpu.c */,
 				E4F257862C81903800FCEAFC /* SDL_sysgpu.h */,
 			);
@@ -2946,6 +2966,8 @@
 				A7D8A95723E2514000DCD162 /* SDL_atomic.c in Sources */,
 				A75FDBCE23EA380300529352 /* SDL_hidapi_rumble.c in Sources */,
 				E4F257952C81903800FCEAFC /* SDL_gpu_vulkan.c in Sources */,
+				E4F257982C81903800FCEAFC /* SDL_gpu_openxr.c in Sources */,
+				E4F257992C81903800FCEAFC /* SDL_openxrdyn.c in Sources */,
 				A7D8BB2723E2514500DCD162 /* SDL_displayevents.c in Sources */,
 				A7D8AB2523E2514100DCD162 /* SDL_log.c in Sources */,
 				A7D8AE8823E2514100DCD162 /* SDL_cocoaopengl.m in Sources */,
diff --git a/include/SDL3/SDL_gpu.h b/include/SDL3/SDL_gpu.h
index 2fe19429df8ac..cfcad7464bc46 100644
--- a/include/SDL3/SDL_gpu.h
+++ b/include/SDL3/SDL_gpu.h
@@ -2364,6 +2364,20 @@ extern SDL_DECLSPEC SDL_GPUDevice * SDLCALL SDL_CreateGPUDeviceWithProperties(
 #define SDL_PROP_GPU_DEVICE_CREATE_VULKAN_OPTIONS_POINTER                       "SDL.gpu.device.create.vulkan.options"
 #define SDL_PROP_GPU_DEVICE_CREATE_METAL_ALLOW_MACFAMILY1_BOOLEAN               "SDL.gpu.device.create.metal.allowmacfamily1"
 
+#define SDL_PROP_GPU_DEVICE_CREATE_XR_ENABLE_BOOLEAN                            "SDL.gpu.device.create.xr.enable"
+#define SDL_PROP_GPU_DEVICE_CREATE_XR_INSTANCE_POINTER                          "SDL.gpu.device.create.xr.instance_out"
+#define SDL_PROP_GPU_DEVICE_CREATE_XR_SYSTEM_ID_POINTER                         "SDL.gpu.device.create.xr.system_id_out"
+#define SDL_PROP_GPU_DEVICE_CREATE_XR_VERSION_NUMBER                            "SDL.gpu.device.create.xr.version"
+#define SDL_PROP_GPU_DEVICE_CREATE_XR_FORM_FACTOR_NUMBER                        "SDL.gpu.device.create.xr.form_factor"
+#define SDL_PROP_GPU_DEVICE_CREATE_XR_EXTENSION_COUNT_NUMBER                    "SDL.gpu.device.create.xr.extensions.count"
+#define SDL_PROP_GPU_DEVICE_CREATE_XR_EXTENSION_NAMES_POINTER                   "SDL.gpu.device.create.xr.extensions.names"
+#define SDL_PROP_GPU_DEVICE_CREATE_XR_LAYER_COUNT_NUMBER                        "SDL.gpu.device.create.xr.layers.count"
+#define SDL_PROP_GPU_DEVICE_CREATE_XR_LAYER_NAMES_POINTER                       "SDL.gpu.device.create.xr.layers.names"
+#define SDL_PROP_GPU_DEVICE_CREATE_XR_APPLICATION_NAME_STRING                   "SDL.gpu.device.create.xr.application.name"
+#define SDL_PROP_GPU_DEVICE_CREATE_XR_APPLICATION_VERSION_NUMBER                "SDL.gpu.device.create.xr.application.version"
+#define SDL_PROP_GPU_DEVICE_CREATE_XR_ENGINE_NAME_STRING                        "SDL.gpu.device.create.xr.engine.name"
+#define SDL_PROP_GPU_DEVICE_CREATE_XR_ENGINE_VERSION_NUMBER                     "SDL.gpu.device.create.xr.engine.version"
+
 
 /**
  * A structure specifying additional options when using Vulkan.
diff --git a/include/SDL3/SDL_hints.h b/include/SDL3/SDL_hints.h
index ce9b711f7f3e4..d7841e398b89a 100644
--- a/include/SDL3/SDL_hints.h
+++ b/include/SDL3/SDL_hints.h
@@ -1145,6 +1145,18 @@ extern "C" {
  */
 #define SDL_HINT_GPU_DRIVER "SDL_GPU_DRIVER"
 
+/**
+ * A variable that specifies the library name to use when loading the OpenXR loader.
+ *
+ * By default, SDL will try the system default name, but on some platforms like Windows,
+ * debug builds of the OpenXR loader have a different name, and are not always directly compatible with release applications.
+ * Setting this hint allows you to compensate for this difference in your app when applicable.
+ *
+ * This hint should be set before the OpenXR loader is loaded.
+ * For example, creating an OpenXR GPU device will load the OpenXR loader.
+ */
+#define SDL_HINT_OPENXR_LIBRARY "SDL_OPENXR_LIBRARY"
+
 /**
  * A variable to control whether SDL_hid_enumerate() enumerates all HID
  * devices or only controllers.
diff --git a/include/SDL3/SDL_openxr.h b/include/SDL3/SDL_openxr.h
new file mode 100644
index 0000000000000..e8d9c06f83b20
--- /dev/null
+++ b/include/SDL3/SDL_openxr.h
@@ -0,0 +1,207 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2026 Sam Lantinga <slouken@libsdl.org>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+*/
+
+/**
+ * # CategoryOpenXR
+ *
+ * Functions for creating OpenXR handles for SDL_gpu contexts.
+ *
+ * For the most part, OpenXR operates independent of SDL, but 
+ * the graphics initialization depends on direct support from SDL_gpu.
+ *
+ */
+
+#ifndef SDL_openxr_h_
+#define SDL_openxr_h_
+
+#include <SDL3/SDL_stdinc.h>
+#include <SDL3/SDL_gpu.h>
+
+#include <SDL3/SDL_begin_code.h>
+/* Set up for C function definitions, even when using C++ */
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#if defined(OPENXR_H_)
+#define NO_SDL_OPENXR_TYPEDEFS 1
+#endif /* OPENXR_H_ */
+
+#if !defined(NO_SDL_OPENXR_TYPEDEFS)
+#define XR_NULL_HANDLE 0
+
+#if !defined(XR_DEFINE_HANDLE)
+    #define XR_DEFINE_HANDLE(object) typedef Uint64 object;
+#endif /* XR_DEFINE_HANDLE */
+
+typedef enum XrStructureType {
+    XR_TYPE_SESSION_CREATE_INFO = 8,
+    XR_TYPE_SWAPCHAIN_CREATE_INFO = 9,
+} XrStructureType;
+
+XR_DEFINE_HANDLE(XrInstance)
+XR_DEFINE_HANDLE(XrSystemId)
+XR_DEFINE_HANDLE(XrSession)
+XR_DEFINE_HANDLE(XrSwapchain)
+
+typedef struct {
+    XrStructureType type;
+    const void* next;
+} XrSessionCreateInfo;
+typedef struct {
+    XrStructureType type;
+    const void* next;
+} XrSwapchainCreateInfo;
+
+typedef enum XrResult {
+    XR_ERROR_FUNCTION_UNSUPPORTED = -7,
+    XR_ERROR_HANDLE_INVALID = -12,
+} XrResult;
+
+#define PFN_xrGetInstanceProcAddr SDL_FunctionPointer
+#endif /* NO_SDL_OPENXR_TYPEDEFS */
+
+/**
+ * Creates an OpenXR session. The OpenXR system ID is pulled from the passed GPU context.
+ * 
+ * \param device a GPU context.
+ * \param createinfo the create info for the OpenXR session, sans the system ID.
+ * \param session a pointer filled in with an OpenXR session created for the given device.
+ * \returns the result of the call.
+ * 
+ * \sa SDL_CreateGPUDeviceWithProperties
+ */
+extern SDL_DECLSPEC XrResult SDLCALL SDL_CreateGPUXRSession(SDL_GPUDevice *device, const XrSessionCreateInfo *createinfo, XrSession *session);
+
+/**
+ * Queries the GPU device for supported XR swapchain image formats.
+ *
+ * The returned pointer should be allocated with SDL_malloc() and will be
+ * passed to SDL_free().
+ *
+ * \param device a GPU context.
+ * \param session an OpenXR session created for the given device.
+ * \param num_formats a pointer filled with the number of supported XR swapchain formats.
+ * \returns a 0 terminated array of supported formats or NULL on failure;
+ *          call SDL_GetError() for more information. This should be freed
+ *          with SDL_free() when it is no longer needed.
+ *
+ * \sa SDL_CreateGPUXRSwapchain
+ */
+extern SDL_DECLSPEC SDL_GPUTextureFormat * SDLCALL SDL_GetGPUXRSwapchainFormats(SDL_GPUDevice *device, XrSession session, int *num_formats);
+
+/**
+ * Creates an OpenXR swapchain.
+ *
+ * The array returned via `textures` is sized according to
+ *`xrEnumerateSwapchainImages`, and thus should only be accessed via index
+ * values returned from `xrAcquireSwapchainImage`.
+ *
+ * Applications are still allowed to call `xrEnumerateSwapchainImages` on the
+ * returned XrSwapchain if they need to get the exact size of the array.
+ *
+ * \param device a GPU context.
+ * \param session an OpenXR session created for the given device.
+ * \param createinfo the create info for the OpenXR swapchain, sans the format.
+ * \param format a supported format for the OpenXR swapchain.
+ * \param swapchain a pointer filled in with the created OpenXR swapchain.
+ * \param textures a pointer filled in with the array of created swapchain images.
+ * \returns the result of the call.
+ * 
+ * \sa SDL_CreateGPUDeviceWithProperties
+ * \sa SDL_CreateGPUXRSession
+ * \sa SDL_GetGPUXRSwapchainFormats
+ * \sa SDL_DestroyGPUXRSwapchain
+ */
+extern SDL_DECLSPEC XrResult SDLCALL SDL_CreateGPUXRSwapchain(
+    SDL_GPUDevice *device,
+    XrSession session,
+    const XrSwapchainCreateInfo *createinfo, 
+    SDL_GPUTextureFormat format,
+    XrSwapchain *swapchain,
+    SDL_GPUTexture ***textures);
+
+/**
+ * Destroys and OpenXR swapchain previously returned by SDL_CreateGPUXRSwapchain.
+ * 
+ * \param device a GPU context.
+ * \param swapchain a swapchain previously returned by SDL_CreateGPUXRSwapchain.
+ * \param swapchainImages an array of swapchain images returned by the same call to SDL_CreateGPUXRSwapchain.
+ * \returns the result of the call.
+ * 
+ * \sa SDL_CreateGPUDeviceWithProperties
+ * \sa SDL_CreateGPUXRSession
+ * \sa SDL_CreateGPUXRSwapchain
+ */
+extern SDL_DECLSPEC XrResult SDLCALL SDL_DestroyGPUXRSwapchain(SDL_GPUDevice *device, XrSwapchain swapchain, SDL_GPUTexture **swapchainImages);
+
+/**
+ * Dynamically load the OpenXR loader. This can be called at any time.
+ * 
+ * SDL keeps a reference count of the OpenXR loader, calling this function multiple 
+ * times will increment that count, rather than loading the library multiple times.
+ * 
+ * If not called, this will be implicitly called when creating a GPU device with OpenXR.
+ * 
+ * This function will use the platform default OpenXR loader name, 
+ * unless the `SDL_HINT_OPENXR_LIBRARY` hint is set.
+ * 
+ * \returns true on success or false on failure; call SDL_GetError() for more
+ *          information.
+ * 
+ * \threadsafety This function is not thread safe.
+ * 
+ * \sa SDL_HINT_OPENXR_LIBRARY
+ */
+extern SDL_DECLSPEC bool SDLCALL SDL_OpenXR_LoadLibrary(void);
+
+/**
+ * Unload the OpenXR loader previously loaded by SDL_OpenXR_LoadLibrary.
+ * 
+ * SDL keeps a reference count of the OpenXR loader, calling this function will decrement that count. 
+ * Once the reference count reaches zero, the library is unloaded.
+ * 
+ * \threadsafety This function is not thread safe.
+ */
+extern SDL_DECLSPEC void SDLCALL SDL_OpenXR_UnloadLibrary(void);
+
+/**
+ * Get the address of the `xrGetInstanceProcAddr` function.
+ * 
+ * This should be called after either calling SDL_OpenXR_LoadLibrary() or
+ * creating an OpenXR SDL_GPUDevice.
+ * 
+ * The actual type of the returned function pointer is PFN_xrGetInstanceProcAddr, 
+ * but that isn't always available. You should include the OpenXR headers before this header, 
+ * or cast the return value of this function to the correct type.
+ * 
+ * \returns the function pointer for `xrGetInstanceProcAddr` or NULL on
+ *          failure; call SDL_GetError() for more information.
+ */
+extern SDL_DECLSPEC PFN_xrGetInstanceProcAddr SDLCALL SDL_OpenXR_GetXrGetInstanceProcAddr(void);
+
+/* Ends C function definitions when using C++ */
+#ifdef __cplusplus
+}
+#endif
+#include <SDL3/SDL_close_code.h>
+
+#endif /* SDL_openxr_h_ */
diff --git a/include/build_config/SDL_build_config.h.cmake b/include/build_config/SDL_build_config.h.cmake
index 5d4e0717efa3e..3350723aa2eb7 100644
--- a/include/build_config/SDL_build_config.h.cmake
+++ b/include/build_config/SDL_build_config.h.cmake
@@ -488,6 +488,7 @@
 #cmakedefine SDL_GPU_D3D12 1
 #cmakedefine SDL_GPU_VULKAN 1
 #cmakedefine SDL_GPU_METAL 1
+#cmakedefine HAVE_GPU_OPENXR 1
 
 #cmakedefine SDL_GPU_PRIVATE 1
 
diff --git a/include/build_config/SDL_build_config_android.h b/include/build_config/SDL_build_config_android.h
index ce6f3e20ae4a2..e709ec9ee04e4 100644
--- a/include/build_config/SDL_build_config_android.h
+++ b/include/build_config/SDL_build_config_android.h
@@ -193,6 +193,7 @@
 #define SDL_VIDEO_VULKAN 1
 #define SDL_VIDEO_RENDER_VULKAN 1
 #define SDL_GPU_VULKAN 1
+#define HAVE_GPU_OPENXR 1
 #define SDL_VIDEO_RENDER_GPU 1
 #endif
 
diff --git a/include/build_config/SDL_build_config_windows.h b/include/build_config/SDL_build_config_windows.h
index 07eecb4d3fb6f..eb7ff81312e4e 100644
--- a/include/build_config/SDL_build_config_windows.h
+++ b/include/build_config/SDL_build_config_windows.h
@@ -289,6 +289,7 @@ typedef unsigned int uintptr_t;
 #endif
 #define SDL_GPU_D3D12 1
 #define SDL_GPU_VULKAN 1
+#define HAVE_GPU_OPENXR 1
 #define SDL_VIDEO_RENDER_GPU 1
 
 /* Enable system power support */
diff --git a/src/SDL_internal.h b/src/SDL_internal.h
index 3fa50c30f06e1..3e903be1c34f0 100644
--- a/src/SDL_internal.h
+++ b/src/SDL_internal.h
@@ -233,6 +233,7 @@
 #undef SDL_GPU_D3D12
 #undef SDL_GPU_METAL
 #undef SDL_GPU_VULKAN
+#undef HAVE_GPU_OPENXR
 #undef SDL_VIDEO_RENDER_GPU
 #endif // SDL_GPU_DISABLED
 
diff --git a/src/dynapi/SDL_dynapi.c b/src/dynapi/SDL_dynapi.c
index 495a8e52c4412..e88b24f19137b 100644
--- a/src/dynapi/SDL_dynapi.c
+++ b/src/dynapi/SDL_dynapi.c
@@ -38,6 +38,7 @@
 #endif
 
 #include <SDL3/SDL.h>
+#include <SDL3/SDL_openxr.h>
 #define SDL_MAIN_NOIMPL // don't drag in header-only implementation of SDL_main
 #include <SDL3/SDL_main.h>
 #include "../core/SDL_core_unsupported.h"
@@ -597,6 +598,7 @@ static void SDL_InitDynamicAPI(void)
 #else // SDL_DYNAMIC_API
 
 #include <SDL3/SDL.h>
+#include <SDL3/SDL_openxr.h>
 
 Sint32 SDL_DYNAPI_entry(Uint32 apiver, void *table, Uint32 tablesize);
 Sint32 SDL_DYNAPI_entry(Uint32 apiver, void *table, Uint32 tablesize)
diff --git a/src/dynapi/SDL_dynapi.sym b/src/dynapi/SDL_dynapi.sym
index 2252d31d13c89..2f2ff3998bbc8 100644
--- a/src/dynapi/SDL_dynapi.sym
+++ b/src/dynapi/SDL_dynapi.sym
@@ -1272,6 +1272,13 @@ SDL3_0.0.0 {
     SDL_LoadSurface;
     SDL_SetWindowFillDocument;
     SDL_TryLockJoysticks;
+    SDL_CreateGPUXRSession;
+    SDL_GetGPUXRSwapchainFormats;
+    SDL_CreateGPUXRSwapchain;
+    SDL_DestroyGPUXRSwapchain;
+    SDL_OpenXR_LoadLibrary;
+    SDL_OpenXR_UnloadLibrary;
+    SDL_OpenXR_GetXrGetInstanceProcAddr;
     # extra symbols go here (don't modify this line)
   local: *;
 };
diff --git a/src/dynapi/SDL_dynapi_overrides.h b/src/dynapi/SDL_dynapi_overrides.h
index d95d3754e7992..fd7627cf325da 100644
--- a/src/dynapi/SDL_dynapi_overrides.h
+++ b/src/dynapi/SDL_dynapi_overrides.h
@@ -1298,3 +1298,10 @@
 #define SDL_LoadSurface SDL_LoadSurface_REAL
 #define SDL_SetWindowFillDocument SDL_SetWindowFillDocument_REAL
 #define SDL_TryLockJoysticks SDL_TryLockJoysticks_REAL
+#define SDL_CreateGPUXRSession SDL_CreateGPUXRSession_REAL
+#define SDL_GetGPUXRSwapchainFormats SDL_GetGPUXRSwapchainFormats_REAL
+#define SDL_CreateGPUXRSwapchain SDL_CreateGPUXRSwapchain_REAL
+#define SDL_DestroyGPUXRSwapchain SDL_DestroyGPUXRSwapchain_REAL
+#define SDL_OpenXR_LoadLibrary SDL_OpenXR_LoadLibrary_REAL
+#define SDL_OpenXR_UnloadLibrary SDL_OpenXR_UnloadLibrary_REAL
+#define SDL_OpenXR_GetXrGetInstanceProcAddr SDL_OpenXR_GetXrGetInstanceProcAddr_REAL
diff --git a/src/dynapi/SDL_dynapi_procs.h b/src/dynapi/SDL_dynapi_procs.h
index ee60a4227ddb4..bd44244021075 100644
--- a/src/dynapi/SDL_dynapi_procs.h
+++ b/src/dynapi/SDL_dynapi_procs.h
@@ -1306,3 +1306,10 @@ SDL_DYNAPI_PROC(SDL_Surface*,SDL_LoadSurface_IO,(SDL_IOStream *a,bool b),(a,b),r
 SDL_DYNAPI_PROC(SDL_Surface*,SDL_LoadSurface,(const char *a),(a),return)
 SDL_DYNAPI_PROC(bool,SDL_SetWindowFillDocument,(SDL_Window *a,bool b),(a,b),return)
 SDL_DYNAPI_PROC(bool,SDL_TryLockJoysticks,(void),(),return)
+SDL_DYNAPI_PROC(XrResult,SDL_CreateGPUXRSession,(SDL_GPUDevice *a,const XrSessionCreateInfo *b,XrSession *c),(a,b,c),return)
+SDL_DYNAPI_PROC(SDL_GPUTextureFormat*,SDL_GetGPUXRSwapchainFormats,(SDL_GPUDevice *a,XrSession b,int *c),(a,b,c),return)
+SDL_DYNAPI_PROC(XrResult,SDL_CreateGPUXRSwapchain,(SDL_GPUDevice *a,XrSession b,const XrSwapchainCreateInfo *c,SDL_GPUTextureFormat d,XrSwapchain *e,SDL_GPUTexture ***f),(a,b,c,d,e,f),return)
+SDL_DYNAPI_PROC(XrResult,SDL_DestroyGPUXRSwapchain,(SDL_GPUDevice *a,XrSwapchain b,SDL_GPUTexture **c),(a,b,c),return)
+SDL_DYNAPI_PROC(bool,SDL_OpenXR_LoadLibrary,(void),(),return)
+SDL_DYNAPI_PROC(void,SDL_OpenXR_UnloadLibrary,(void),(),)
+SDL_DYNAPI_PROC(PFN_xrGetInstanceProcAddr,SDL_OpenXR_GetXrGetInstanceProcAddr,(void),(),return)
diff --git a/src/gpu/SDL_gpu.c b/src/gpu/SDL_gpu.c
index bec9bf4df5fa1..11b0b1210093b 100644
--- a/src/gpu/SDL_gpu.c
+++ b/src/gpu/SDL_gpu.c
@@ -607,6 +607,13 @@ static const SDL_GPUBootstrap * SDL_GPUSelectBackend(SDL_PropertiesID props)
         return NULL;
     }
 
+#ifndef HAVE_GPU_OPENXR
+    if (SDL_GetBooleanProperty(props, SDL_PROP_GPU_DEVICE_CREATE_XR_ENABLE_BOOLEAN, false)) {
+        SDL_SetError("OpenXR is not enabled in this build of SDL");
+        return NULL;
+    }
+#endif
+
     gpudriver = SDL_GetHint(SDL_HINT_GPU_DRIVER);
     if (gpudriver == NULL) {
         gpudriver = SDL_GetStringProperty(props, SDL_PROP_GPU_DEVICE_CREATE_NAME_STRING, NULL);
@@ -752,6 +759,13 @@ void SDL_DestroyGPUDevice(SDL_GPUDevice *device)
     device->DestroyDevice(device);
 }
 
+XrResult SDL_DestroyGPUXRSwapchain(SDL_GPUDevice *device, XrSwapchain swapchain, SDL_GPUTexture **swapchainImages)
+{
+    CHECK_DEVICE_MAGIC(device, XR_ERROR_HANDLE_INVALID);
+
+    return device->DestroyXRSwapchain(device->dri

(Patch may be truncated, please check the link at the top of this post.)