This document covers how to build OpenXR (VR/AR) applications using SDL's GPU API with OpenXR integration.
SDL3 provides OpenXR integration through the GPU API, allowing you to render to VR/AR headsets using a unified interface across multiple graphics backends (Vulkan, D3D12, Metal).
Key features:
OpenXR Loader (openxr_loader.dll / libopenxr_loader.so)
libopenxr-loader1 on Ubuntu)SDL_HINT_OPENXR_LIBRARY to specify a custom loader pathOpenXR Runtime
VR Headset
#include <openxr/openxr.h> #include <SDL3/SDL.h> #include <SDL3/SDL_openxr.h> // These will be populated by SDL XrInstance xr_instance = XR_NULL_HANDLE; XrSystemId xr_system_id = 0; // Create GPU device with XR enabled SDL_PropertiesID props = SDL_CreateProperties(); SDL_SetBooleanProperty(props, SDL_PROP_GPU_DEVICE_CREATE_XR_ENABLE_BOOLEAN, true); SDL_SetPointerProperty(props, SDL_PROP_GPU_DEVICE_CREATE_XR_INSTANCE_POINTER, &xr_instance); SDL_SetPointerProperty(props, SDL_PROP_GPU_DEVICE_CREATE_XR_SYSTEM_ID_POINTER, &xr_system_id); // Optional: Override app name/version (defaults to SDL_SetAppMetadata values if not set) SDL_SetStringProperty(props, SDL_PROP_GPU_DEVICE_CREATE_XR_APPLICATION_NAME_STRING, "My VR App"); SDL_SetNumberProperty(props, SDL_PROP_GPU_DEVICE_CREATE_XR_APPLICATION_VERSION_NUMBER, 1); SDL_GPUDevice *device = SDL_CreateGPUDeviceWithProperties(props); SDL_DestroyProperties(props); // xr_instance and xr_system_id are now populated by SDL
See test/testgpu_spinning_cube_xr.c for a complete example.
Building OpenXR applications for Android standalone headsets (Meta Quest, Pico, etc.) requires additional manifest configuration beyond standard Android apps.
The manifest requirements fall into three categories:
These are required by the Khronos OpenXR specification for Android:
<!-- OpenXR runtime broker communication --> <uses-permission android:name="org.khronos.openxr.permission.OPENXR" /> <uses-permission android:name="org.khronos.openxr.permission.OPENXR_SYSTEM" />
Required for the app to discover OpenXR runtimes:
<queries> <provider android:authorities="org.khronos.openxr.runtime_broker;org.khronos.openxr.system_runtime_broker" /> <intent> <action android:name="org.khronos.openxr.OpenXRRuntimeService" /> </intent> <intent> <action android:name="org.khronos.openxr.OpenXRApiLayerService" /> </intent> </queries>
<!-- VR head tracking (standard OpenXR requirement) --> <uses-feature android:name="android.hardware.vr.headtracking" android:required="true" android:version="1" /> <!-- Touchscreen not required for VR --> <uses-feature android:name="android.hardware.touchscreen" android:required="false" /> <!-- Graphics requirements --> <uses-feature android:glEsVersion="0x00030002" android:required="true" /> <uses-feature android:name="android.hardware.vulkan.level" android:required="true" android:version="1" /> <uses-feature android:name="android.hardware.vulkan.version" android:required="true" android:version="0x00401000" />
<activity ...> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> <!-- Khronos OpenXR immersive app category --> <category android:name="org.khronos.openxr.intent.category.IMMERSIVE_HMD" /> </intent-filter> </activity>
These are required for apps to run properly on Meta Quest devices. Without these, your app may launch in “pancake” 2D mode instead of VR.
<activity ...> <intent-filter> ... <!-- CRITICAL: Without this, app launches in 2D mode on Quest! --> <category android:name="com.oculus.intent.category.VR" /> </intent-filter> </activity>
<application ...> <!-- Required: Specifies which Quest devices are supported --> <meta-data android:name="com.oculus.supportedDevices" android:value="quest|quest2|questpro|quest3|quest3s" /> </application>
<application ...> <!-- Properly handles when user opens the Quest system menu --> <meta-data android:name="com.oculus.vr.focusaware" android:value="true" /> </application>
<!-- Feature declaration --> <uses-feature android:name="oculus.software.handtracking" android:required="false" /> <application ...> <!-- V2.0 allows app to launch without controllers --> <meta-data android:name="com.oculus.handtracking.version" android:value="V2.0" /> <meta-data android:name="com.oculus.handtracking.frequency" android:value="HIGH" /> </application>
<application ...> <meta-data android:name="com.oculus.ossplash" android:value="true" /> <meta-data android:name="com.oculus.ossplash.colorspace" android:value="QUEST_SRGB_NONGAMMA" /> <meta-data android:name="com.oculus.ossplash.background" android:resource="@drawable/vr_splash" /> </application>
For Pico Neo, Pico 4, and other Pico headsets:
<activity ...> <intent-filter> ... <!-- Pico VR category --> <category android:name="com.picovr.intent.category.VR" /> </intent-filter> </activity>
<application ...> <!-- Pico device support --> <meta-data android:name="pvr.app.type" android:value="vr" /> </application>
<activity ...> <intent-filter> ... <!-- HTC Vive category --> <category android:name="com.htc.intent.category.VRAPP" /> </intent-filter> </activity>
| Declaration | Purpose | Scope |
|---|---|---|
org.khronos.openxr.permission.OPENXR | Runtime communication | All OpenXR |
android.hardware.vr.headtracking | Marks app as VR | All OpenXR |
org.khronos.openxr.intent.category.IMMERSIVE_HMD | Khronos standard VR category | All OpenXR |
com.oculus.intent.category.VR | Launch in VR mode | Meta Quest |
com.oculus.supportedDevices | Device compatibility | Meta Quest |
com.oculus.vr.focusaware | System menu handling | Meta Quest |
com.picovr.intent.category.VR | Launch in VR mode | Pico |
com.htc.intent.category.VRAPP | Launch in VR mode | HTC Vive |
SDL provides an example XR manifest template at: test/android/cmake/AndroidManifest.xr.xml.cmake
This template includes:
SDL_ANDROID_XR_META_SUPPORT CMake option)Cause: Missing platform-specific VR intent category.
Solution: Add the appropriate category for your target platform:
com.oculus.intent.category.VRcom.picovr.intent.category.VRcom.htc.intent.category.VRAPPCause: The OpenXR loader can't find a runtime.
Solutions:
<queries> block for runtime discoverylibopenxr-loader1 and configure the active runtimeCause: openxr_loader.dll / libopenxr_loader.so is not in the library path.
Solutions:
SDL_HINT_OPENXR_LIBRARY to specify the loader path explicitlyCause: GPU resources destroyed while still in use.
Solution: Call SDL_WaitForGPUIdle(device) before releasing any GPU resources or destroying the device.
test/testgpu_spinning_cube_xr.c