mirror of
https://github.com/libsdl-org/SDL.git
synced 2026-03-10 02:55:36 +00:00
test: Add testgpu_spinning_cube_xr (#14943)
Co-authored-by: Ethan Lee <flibitijibibo@gmail.com>
This commit is contained in:
320
docs/README-xr.md
Normal file
320
docs/README-xr.md
Normal file
@@ -0,0 +1,320 @@
|
||||
# OpenXR / VR Development with SDL
|
||||
|
||||
This document covers how to build OpenXR (VR/AR) applications using SDL's GPU API with OpenXR integration.
|
||||
|
||||
## Overview
|
||||
|
||||
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:**
|
||||
- Automatic OpenXR instance and session management
|
||||
- Swapchain creation and image acquisition
|
||||
- Support for multi-pass stereo rendering
|
||||
- Works with desktop VR runtimes (SteamVR, Oculus, Windows Mixed Reality) and standalone headsets (Meta Quest, Pico)
|
||||
|
||||
## Desktop Development
|
||||
|
||||
### Requirements
|
||||
|
||||
1. **OpenXR Loader** (`openxr_loader.dll` / `libopenxr_loader.so`)
|
||||
- On Windows: Usually installed with VR runtime software (Oculus, SteamVR)
|
||||
- On Linux: Install via package manager (e.g., `libopenxr-loader1` on Ubuntu)
|
||||
- Can also use `SDL_HINT_OPENXR_LIBRARY` to specify a custom loader path
|
||||
|
||||
2. **OpenXR Runtime**
|
||||
- At least one OpenXR runtime must be installed and active
|
||||
- Examples: SteamVR, Oculus Desktop, Monado (Linux)
|
||||
|
||||
3. **VR Headset**
|
||||
- Connected and recognized by the runtime
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```c
|
||||
#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.
|
||||
|
||||
---
|
||||
|
||||
## Android Development
|
||||
|
||||
Building OpenXR applications for Android standalone headsets (Meta Quest, Pico, etc.) requires additional manifest configuration beyond standard Android apps.
|
||||
|
||||
### Android Manifest Requirements
|
||||
|
||||
The manifest requirements fall into three categories:
|
||||
|
||||
1. **OpenXR Standard (Khronos)** - Required for all OpenXR apps
|
||||
2. **Platform-Specific** - Required for specific headset platforms
|
||||
3. **Optional Features** - Enable additional capabilities
|
||||
|
||||
---
|
||||
|
||||
### OpenXR Standard Requirements (All Platforms)
|
||||
|
||||
These are required by the Khronos OpenXR specification for Android:
|
||||
|
||||
#### Permissions
|
||||
|
||||
```xml
|
||||
<!-- OpenXR runtime broker communication -->
|
||||
<uses-permission android:name="org.khronos.openxr.permission.OPENXR" />
|
||||
<uses-permission android:name="org.khronos.openxr.permission.OPENXR_SYSTEM" />
|
||||
```
|
||||
|
||||
#### Queries (Android 11+)
|
||||
|
||||
Required for the app to discover OpenXR runtimes:
|
||||
|
||||
```xml
|
||||
<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>
|
||||
```
|
||||
|
||||
#### Hardware Features
|
||||
|
||||
```xml
|
||||
<!-- 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" />
|
||||
```
|
||||
|
||||
#### Intent Category
|
||||
|
||||
```xml
|
||||
<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>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Meta Quest Requirements
|
||||
|
||||
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.
|
||||
|
||||
#### VR Intent Category (Critical!)
|
||||
|
||||
```xml
|
||||
<activity ...>
|
||||
<intent-filter>
|
||||
...
|
||||
<!-- CRITICAL: Without this, app launches in 2D mode on Quest! -->
|
||||
<category android:name="com.oculus.intent.category.VR" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
```
|
||||
|
||||
#### Supported Devices
|
||||
|
||||
```xml
|
||||
<application ...>
|
||||
<!-- Required: Specifies which Quest devices are supported -->
|
||||
<meta-data android:name="com.oculus.supportedDevices"
|
||||
android:value="quest|quest2|questpro|quest3|quest3s" />
|
||||
</application>
|
||||
```
|
||||
|
||||
#### Focus Handling (Recommended)
|
||||
|
||||
```xml
|
||||
<application ...>
|
||||
<!-- Properly handles when user opens the Quest system menu -->
|
||||
<meta-data android:name="com.oculus.vr.focusaware"
|
||||
android:value="true" />
|
||||
</application>
|
||||
```
|
||||
|
||||
#### Hand Tracking (Optional)
|
||||
|
||||
```xml
|
||||
<!-- 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>
|
||||
```
|
||||
|
||||
#### VR Splash Screen (Optional)
|
||||
|
||||
```xml
|
||||
<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>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Pico Requirements
|
||||
|
||||
For Pico Neo, Pico 4, and other Pico headsets:
|
||||
|
||||
#### VR Intent Category
|
||||
|
||||
```xml
|
||||
<activity ...>
|
||||
<intent-filter>
|
||||
...
|
||||
<!-- Pico VR category -->
|
||||
<category android:name="com.picovr.intent.category.VR" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
```
|
||||
|
||||
#### Supported Devices (Optional)
|
||||
|
||||
```xml
|
||||
<application ...>
|
||||
<!-- Pico device support -->
|
||||
<meta-data android:name="pvr.app.type"
|
||||
android:value="vr" />
|
||||
</application>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### HTC Vive Focus / VIVE XR Elite
|
||||
|
||||
```xml
|
||||
<activity ...>
|
||||
<intent-filter>
|
||||
...
|
||||
<!-- HTC Vive category -->
|
||||
<category android:name="com.htc.intent.category.VRAPP" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference Table
|
||||
|
||||
| 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 |
|
||||
|
||||
---
|
||||
|
||||
## Example Manifest
|
||||
|
||||
SDL provides an example XR manifest template at:
|
||||
`test/android/cmake/AndroidManifest.xr.xml.cmake`
|
||||
|
||||
This template includes:
|
||||
- All Khronos OpenXR requirements
|
||||
- Meta Quest support (configurable via `SDL_ANDROID_XR_META_SUPPORT` CMake option)
|
||||
- Proper intent filters for VR launching
|
||||
|
||||
---
|
||||
|
||||
## Common Issues
|
||||
|
||||
### App launches in 2D "pancake" mode
|
||||
|
||||
**Cause:** Missing platform-specific VR intent category.
|
||||
|
||||
**Solution:** Add the appropriate category for your target platform:
|
||||
- Meta Quest: `com.oculus.intent.category.VR`
|
||||
- Pico: `com.picovr.intent.category.VR`
|
||||
- HTC: `com.htc.intent.category.VRAPP`
|
||||
|
||||
### "No OpenXR runtime found" error
|
||||
|
||||
**Cause:** The OpenXR loader can't find a runtime.
|
||||
|
||||
**Solutions:**
|
||||
- **Desktop:** Ensure VR software (SteamVR, Oculus) is installed and running
|
||||
- **Android:** Ensure your manifest has the correct `<queries>` block for runtime discovery
|
||||
- **Linux:** Install `libopenxr-loader1` and configure the active runtime
|
||||
|
||||
### OpenXR loader not found
|
||||
|
||||
**Cause:** `openxr_loader.dll` / `libopenxr_loader.so` is not in the library path.
|
||||
|
||||
**Solutions:**
|
||||
- Install the Khronos OpenXR SDK
|
||||
- On Windows, VR runtimes typically install this, but may not add it to PATH
|
||||
- Use `SDL_HINT_OPENXR_LIBRARY` to specify the loader path explicitly
|
||||
|
||||
### Vulkan validation errors on shutdown
|
||||
|
||||
**Cause:** GPU resources destroyed while still in use.
|
||||
|
||||
**Solution:** Call `SDL_WaitForGPUIdle(device)` before releasing any GPU resources or destroying the device.
|
||||
|
||||
---
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [Khronos OpenXR Specification](https://www.khronos.org/openxr/)
|
||||
- [Meta Quest Developer Documentation](https://developer.oculus.com/documentation/)
|
||||
- [Pico Developer Documentation](https://developer.pico-interactive.com/)
|
||||
- [SDL GPU API Documentation](https://wiki.libsdl.org/)
|
||||
- Example code: `test/testgpu_spinning_cube_xr.c`
|
||||
@@ -382,6 +382,8 @@ add_sdl_test_executable(testgpu_simple_clear SOURCES testgpu_simple_clear.c)
|
||||
add_sdl_test_executable(testgpu_spinning_cube SOURCES testgpu_spinning_cube.c ${icon_png_header} DEPENDS generate-icon_png_header)
|
||||
add_sdl_test_executable(testgpurender_effects MAIN_CALLBACKS NEEDS_RESOURCES TESTUTILS SOURCES testgpurender_effects.c)
|
||||
add_sdl_test_executable(testgpurender_msdf MAIN_CALLBACKS NEEDS_RESOURCES TESTUTILS SOURCES testgpurender_msdf.c)
|
||||
add_sdl_test_executable(testgpu_spinning_cube_xr SOURCES testgpu_spinning_cube_xr.c)
|
||||
|
||||
if(ANDROID)
|
||||
target_link_libraries(testgles PRIVATE GLESv1_CM)
|
||||
elseif(IOS OR TVOS)
|
||||
@@ -750,7 +752,35 @@ if(ANDROID AND TARGET SDL3::Jar)
|
||||
configure_file(android/cmake/SDLTestActivity.java.cmake "${JAVA_PACKAGE_DIR}/SDLTestActivity.java" @ONLY)
|
||||
configure_file(android/cmake/res/values/strings.xml.cmake android/res/values/strings-${TEST}.xml @ONLY)
|
||||
configure_file(android/cmake/res/xml/shortcuts.xml.cmake "${GENERATED_RES_FOLDER}/xml/shortcuts.xml" @ONLY)
|
||||
configure_file(android/cmake/AndroidManifest.xml.cmake "${generated_manifest_path}" @ONLY)
|
||||
# Use XR-specific manifest for XR tests, standard manifest for others
|
||||
if("${TEST}" MATCHES "_xr$")
|
||||
# Meta Quest-specific manifest sections (enabled by default, set to empty to disable)
|
||||
# These are ignored by non-Meta runtimes but required for proper Quest integration
|
||||
if(NOT DEFINED SDL_ANDROID_XR_META_SUPPORT OR SDL_ANDROID_XR_META_SUPPORT)
|
||||
set(ANDROID_XR_META_FEATURES
|
||||
" <!-- Meta Quest hand tracking support -->
|
||||
<uses-feature android:name=\"oculus.software.handtracking\" android:required=\"false\" />
|
||||
")
|
||||
set(ANDROID_XR_META_METADATA
|
||||
" <!-- Meta Quest supported devices -->
|
||||
<meta-data android:name=\"com.oculus.supportedDevices\" android:value=\"quest|quest2|questpro|quest3|quest3s\" />
|
||||
<meta-data android:name=\"com.oculus.vr.focusaware\" android:value=\"true\" />
|
||||
<!-- Hand tracking support level (V2 allows launching 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\" />
|
||||
")
|
||||
set(ANDROID_XR_META_INTENT_CATEGORY
|
||||
" <!-- VR intent category for Meta Quest -->
|
||||
<category android:name=\"com.oculus.intent.category.VR\" />")
|
||||
else()
|
||||
set(ANDROID_XR_META_FEATURES "")
|
||||
set(ANDROID_XR_META_METADATA "")
|
||||
set(ANDROID_XR_META_INTENT_CATEGORY "")
|
||||
endif()
|
||||
configure_file(android/cmake/AndroidManifest.xr.xml.cmake "${generated_manifest_path}" @ONLY)
|
||||
else()
|
||||
configure_file(android/cmake/AndroidManifest.xml.cmake "${generated_manifest_path}" @ONLY)
|
||||
endif()
|
||||
file(GENERATE
|
||||
OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/android/${TEST}-$<CONFIG>/res/values/strings.xml"
|
||||
INPUT "${CMAKE_CURRENT_BINARY_DIR}/android/res/values/strings-${TEST}.xml"
|
||||
|
||||
94
test/android/cmake/AndroidManifest.xr.xml.cmake
Normal file
94
test/android/cmake/AndroidManifest.xr.xml.cmake
Normal file
@@ -0,0 +1,94 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="@ANDROID_MANIFEST_PACKAGE@"
|
||||
android:versionCode="1"
|
||||
android:versionName="1.0"
|
||||
android:installLocation="auto">
|
||||
|
||||
<!-- OpenGL ES 3.2 for Vulkan fallback on XR devices -->
|
||||
<uses-feature android:glEsVersion="0x00030002" android:required="true" />
|
||||
|
||||
<!-- Vulkan requirements -->
|
||||
<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" />
|
||||
|
||||
<!-- 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" />
|
||||
|
||||
@ANDROID_XR_META_FEATURES@
|
||||
<!-- Game controller support -->
|
||||
<uses-feature
|
||||
android:name="android.hardware.bluetooth"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.gamepad"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.usb.host"
|
||||
android:required="false" />
|
||||
|
||||
<!-- Allow access to the vibrator (for controller haptics) -->
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
|
||||
<!-- OpenXR permissions (for runtime broker communication) -->
|
||||
<uses-permission android:name="org.khronos.openxr.permission.OPENXR" />
|
||||
<uses-permission android:name="org.khronos.openxr.permission.OPENXR_SYSTEM" />
|
||||
|
||||
<!-- OpenXR runtime/layer queries -->
|
||||
<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>
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/sdl-test"
|
||||
android:roundIcon="@mipmap/sdl-test_round"
|
||||
android:label="@string/label"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen"
|
||||
android:enableOnBackInvokedCallback="false"
|
||||
android:hardwareAccelerated="true">
|
||||
|
||||
@ANDROID_XR_META_METADATA@
|
||||
<activity
|
||||
android:name="@ANDROID_MANIFEST_PACKAGE@.SDLTestActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/label"
|
||||
android:alwaysRetainTaskState="true"
|
||||
android:launchMode="singleTask"
|
||||
android:configChanges="density|keyboard|keyboardHidden|navigation|orientation|screenLayout|screenSize|uiMode"
|
||||
android:screenOrientation="landscape"
|
||||
android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen"
|
||||
android:excludeFromRecents="false"
|
||||
android:resizeableActivity="false"
|
||||
tools:ignore="NonResizeableActivity">
|
||||
|
||||
<!-- Standard launcher intent -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
@ANDROID_XR_META_INTENT_CATEGORY@
|
||||
<!-- Khronos OpenXR category (for broader compatibility) -->
|
||||
<category android:name="org.khronos.openxr.intent.category.IMMERSIVE_HMD" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name="@ANDROID_MANIFEST_PACKAGE@.SDLEntryTestActivity"
|
||||
android:exported="false"
|
||||
android:label="@string/label">
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
||||
983
test/testgpu_spinning_cube_xr.c
Normal file
983
test/testgpu_spinning_cube_xr.c
Normal file
@@ -0,0 +1,983 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
/*
|
||||
* testgpu_spinning_cube_xr.c - SDL3 GPU API OpenXR Spinning Cubes Test
|
||||
*
|
||||
* This is an XR-enabled version of testgpu_spinning_cube that renders
|
||||
* spinning colored cubes in VR using OpenXR and SDL's GPU API.
|
||||
*
|
||||
* Rendering approach: Multi-pass stereo (one render pass per eye)
|
||||
* This is the simplest and most compatible approach, working on all
|
||||
* OpenXR-capable platforms (Desktop VR runtimes, Quest, etc.)
|
||||
*
|
||||
* For more information on stereo rendering techniques, see:
|
||||
* - Multi-pass: Traditional, 2 render passes (used here)
|
||||
* - Multiview (GL_OVR_multiview): Single pass with texture arrays
|
||||
* - Single-pass instanced: GPU instancing to select eye
|
||||
*/
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <SDL3/SDL_main.h>
|
||||
|
||||
/* Include OpenXR headers BEFORE SDL_openxr.h to get full type definitions */
|
||||
#ifdef HAVE_OPENXR_H
|
||||
#include <openxr/openxr.h>
|
||||
#else
|
||||
/* SDL includes a copy for building on systems without the OpenXR SDK */
|
||||
#include "../src/video/khronos/openxr/openxr.h"
|
||||
#endif
|
||||
|
||||
#include <SDL3/SDL_openxr.h>
|
||||
|
||||
/* Standard library for exit() */
|
||||
#include <stdlib.h>
|
||||
|
||||
/* Include compiled shader bytecode for all backends */
|
||||
#include "testgpu/cube.frag.dxil.h"
|
||||
#include "testgpu/cube.frag.spv.h"
|
||||
#include "testgpu/cube.vert.dxil.h"
|
||||
#include "testgpu/cube.vert.spv.h"
|
||||
|
||||
#define CHECK_CREATE(var, thing) { if (!(var)) { SDL_Log("Failed to create %s: %s", thing, SDL_GetError()); return false; } }
|
||||
#define XR_CHECK(result, msg) do { if (XR_FAILED(result)) { SDL_Log("OpenXR Error: %s (result=%d)", msg, (int)(result)); return false; } } while(0)
|
||||
#define XR_CHECK_QUIT(result, msg) do { if (XR_FAILED(result)) { SDL_Log("OpenXR Error: %s (result=%d)", msg, (int)(result)); quit(2); return; } } while(0)
|
||||
|
||||
/* ========================================================================
|
||||
* Math Types and Functions
|
||||
* ======================================================================== */
|
||||
|
||||
typedef struct { float x, y, z; } Vec3;
|
||||
typedef struct { float m[16]; } Mat4;
|
||||
|
||||
static Mat4 Mat4_Multiply(Mat4 a, Mat4 b)
|
||||
{
|
||||
Mat4 result = {{0}};
|
||||
for (int i = 0; i < 4; i++) {
|
||||
for (int j = 0; j < 4; j++) {
|
||||
for (int k = 0; k < 4; k++) {
|
||||
result.m[i * 4 + j] += a.m[i * 4 + k] * b.m[k * 4 + j];
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static Mat4 Mat4_Translation(float x, float y, float z)
|
||||
{
|
||||
return (Mat4){{ 1,0,0,0, 0,1,0,0, 0,0,1,0, x,y,z,1 }};
|
||||
}
|
||||
|
||||
static Mat4 Mat4_Scale(float s)
|
||||
{
|
||||
return (Mat4){{ s,0,0,0, 0,s,0,0, 0,0,s,0, 0,0,0,1 }};
|
||||
}
|
||||
|
||||
static Mat4 Mat4_RotationY(float rad)
|
||||
{
|
||||
float c = SDL_cosf(rad), s = SDL_sinf(rad);
|
||||
return (Mat4){{ c,0,-s,0, 0,1,0,0, s,0,c,0, 0,0,0,1 }};
|
||||
}
|
||||
|
||||
static Mat4 Mat4_RotationX(float rad)
|
||||
{
|
||||
float c = SDL_cosf(rad), s = SDL_sinf(rad);
|
||||
return (Mat4){{ 1,0,0,0, 0,c,s,0, 0,-s,c,0, 0,0,0,1 }};
|
||||
}
|
||||
|
||||
/* Convert XrPosef to view matrix (inverted transform) */
|
||||
static Mat4 Mat4_FromXrPose(XrPosef pose)
|
||||
{
|
||||
float x = pose.orientation.x, y = pose.orientation.y;
|
||||
float z = pose.orientation.z, w = pose.orientation.w;
|
||||
|
||||
/* Quaternion to rotation matrix columns */
|
||||
Vec3 right = { 1-2*(y*y+z*z), 2*(x*y+w*z), 2*(x*z-w*y) };
|
||||
Vec3 up = { 2*(x*y-w*z), 1-2*(x*x+z*z), 2*(y*z+w*x) };
|
||||
Vec3 fwd = { 2*(x*z+w*y), 2*(y*z-w*x), 1-2*(x*x+y*y) };
|
||||
Vec3 pos = { pose.position.x, pose.position.y, pose.position.z };
|
||||
|
||||
/* Inverted transform for view matrix */
|
||||
float dr = -(right.x*pos.x + right.y*pos.y + right.z*pos.z);
|
||||
float du = -(up.x*pos.x + up.y*pos.y + up.z*pos.z);
|
||||
float df = -(fwd.x*pos.x + fwd.y*pos.y + fwd.z*pos.z);
|
||||
|
||||
return (Mat4){{ right.x,up.x,fwd.x,0, right.y,up.y,fwd.y,0, right.z,up.z,fwd.z,0, dr,du,df,1 }};
|
||||
}
|
||||
|
||||
/* Create asymmetric projection matrix from XR FOV */
|
||||
static Mat4 Mat4_Projection(XrFovf fov, float nearZ, float farZ)
|
||||
{
|
||||
float tL = SDL_tanf(fov.angleLeft), tR = SDL_tanf(fov.angleRight);
|
||||
float tU = SDL_tanf(fov.angleUp), tD = SDL_tanf(fov.angleDown);
|
||||
float w = tR - tL, h = tU - tD;
|
||||
|
||||
return (Mat4){{
|
||||
2/w, 0, 0, 0,
|
||||
0, 2/h, 0, 0,
|
||||
(tR+tL)/w, (tU+tD)/h, -farZ/(farZ-nearZ), -1,
|
||||
0, 0, -(farZ*nearZ)/(farZ-nearZ), 0
|
||||
}};
|
||||
}
|
||||
|
||||
/* ========================================================================
|
||||
* Vertex Data
|
||||
* ======================================================================== */
|
||||
|
||||
typedef struct {
|
||||
float x, y, z;
|
||||
Uint8 r, g, b, a;
|
||||
} PositionColorVertex;
|
||||
|
||||
/* Cube vertices - 0.25m half-size, each face a different color */
|
||||
static const float CUBE_HALF_SIZE = 0.25f;
|
||||
|
||||
/* ========================================================================
|
||||
* OpenXR Function Pointers (loaded dynamically)
|
||||
* ======================================================================== */
|
||||
|
||||
static PFN_xrGetInstanceProcAddr pfn_xrGetInstanceProcAddr = NULL;
|
||||
static PFN_xrEnumerateViewConfigurationViews pfn_xrEnumerateViewConfigurationViews = NULL;
|
||||
static PFN_xrEnumerateSwapchainImages pfn_xrEnumerateSwapchainImages = NULL;
|
||||
static PFN_xrCreateReferenceSpace pfn_xrCreateReferenceSpace = NULL;
|
||||
static PFN_xrDestroySpace pfn_xrDestroySpace = NULL;
|
||||
static PFN_xrDestroySession pfn_xrDestroySession = NULL;
|
||||
static PFN_xrDestroyInstance pfn_xrDestroyInstance = NULL;
|
||||
static PFN_xrPollEvent pfn_xrPollEvent = NULL;
|
||||
static PFN_xrBeginSession pfn_xrBeginSession = NULL;
|
||||
static PFN_xrEndSession pfn_xrEndSession = NULL;
|
||||
static PFN_xrWaitFrame pfn_xrWaitFrame = NULL;
|
||||
static PFN_xrBeginFrame pfn_xrBeginFrame = NULL;
|
||||
static PFN_xrEndFrame pfn_xrEndFrame = NULL;
|
||||
static PFN_xrLocateViews pfn_xrLocateViews = NULL;
|
||||
static PFN_xrAcquireSwapchainImage pfn_xrAcquireSwapchainImage = NULL;
|
||||
static PFN_xrWaitSwapchainImage pfn_xrWaitSwapchainImage = NULL;
|
||||
static PFN_xrReleaseSwapchainImage pfn_xrReleaseSwapchainImage = NULL;
|
||||
|
||||
/* ========================================================================
|
||||
* Global State
|
||||
* ======================================================================== */
|
||||
|
||||
/* OpenXR state */
|
||||
static XrInstance xr_instance = XR_NULL_HANDLE;
|
||||
static XrSystemId xr_system_id = XR_NULL_SYSTEM_ID;
|
||||
static XrSession xr_session = XR_NULL_HANDLE;
|
||||
static XrSpace xr_local_space = XR_NULL_HANDLE;
|
||||
static bool xr_session_running = false;
|
||||
static bool xr_should_quit = false;
|
||||
|
||||
/* Swapchain state */
|
||||
typedef struct {
|
||||
XrSwapchain swapchain;
|
||||
SDL_GPUTexture **images;
|
||||
SDL_GPUTexture *depth_texture; /* Local depth buffer for z-ordering */
|
||||
XrExtent2Di size;
|
||||
SDL_GPUTextureFormat format;
|
||||
Uint32 image_count;
|
||||
} VRSwapchain;
|
||||
|
||||
/* Depth buffer format - use D24 for wide compatibility */
|
||||
static const SDL_GPUTextureFormat DEPTH_FORMAT = SDL_GPU_TEXTUREFORMAT_D24_UNORM;
|
||||
|
||||
static VRSwapchain *vr_swapchains = NULL;
|
||||
static XrView *xr_views = NULL;
|
||||
static Uint32 view_count = 0;
|
||||
|
||||
/* SDL GPU state */
|
||||
static SDL_GPUDevice *gpu_device = NULL;
|
||||
static SDL_GPUGraphicsPipeline *pipeline = NULL;
|
||||
static SDL_GPUBuffer *vertex_buffer = NULL;
|
||||
static SDL_GPUBuffer *index_buffer = NULL;
|
||||
|
||||
/* Animation time */
|
||||
static float anim_time = 0.0f;
|
||||
static Uint64 last_ticks = 0;
|
||||
|
||||
/* Cube scene configuration */
|
||||
#define NUM_CUBES 5
|
||||
static Vec3 cube_positions[NUM_CUBES] = {
|
||||
{ 0.0f, 0.0f, -2.0f }, /* Center, in front */
|
||||
{ -1.2f, 0.4f, -2.5f }, /* Upper left */
|
||||
{ 1.2f, 0.3f, -2.5f }, /* Upper right */
|
||||
{ -0.6f, -0.4f, -1.8f }, /* Lower left close */
|
||||
{ 0.6f, -0.3f, -1.8f }, /* Lower right close */
|
||||
};
|
||||
static float cube_scales[NUM_CUBES] = { 1.0f, 0.6f, 0.6f, 0.5f, 0.5f };
|
||||
static float cube_speeds[NUM_CUBES] = { 1.0f, 1.5f, -1.2f, 2.0f, -0.8f };
|
||||
|
||||
/* ========================================================================
|
||||
* Cleanup and Quit
|
||||
* ======================================================================== */
|
||||
|
||||
static void quit(int rc)
|
||||
{
|
||||
SDL_Log("Cleaning up...");
|
||||
|
||||
/* CRITICAL: Wait for GPU to finish before destroying resources
|
||||
* Per PR #14837 discussion - prevents Vulkan validation errors */
|
||||
if (gpu_device) {
|
||||
SDL_WaitForGPUIdle(gpu_device);
|
||||
}
|
||||
|
||||
/* Release GPU resources first */
|
||||
if (pipeline) {
|
||||
SDL_ReleaseGPUGraphicsPipeline(gpu_device, pipeline);
|
||||
pipeline = NULL;
|
||||
}
|
||||
if (vertex_buffer) {
|
||||
SDL_ReleaseGPUBuffer(gpu_device, vertex_buffer);
|
||||
vertex_buffer = NULL;
|
||||
}
|
||||
if (index_buffer) {
|
||||
SDL_ReleaseGPUBuffer(gpu_device, index_buffer);
|
||||
index_buffer = NULL;
|
||||
}
|
||||
|
||||
/* Release swapchains and depth textures */
|
||||
if (vr_swapchains) {
|
||||
for (Uint32 i = 0; i < view_count; i++) {
|
||||
if (vr_swapchains[i].depth_texture) {
|
||||
SDL_ReleaseGPUTexture(gpu_device, vr_swapchains[i].depth_texture);
|
||||
}
|
||||
if (vr_swapchains[i].swapchain) {
|
||||
SDL_DestroyGPUXRSwapchain(gpu_device, vr_swapchains[i].swapchain, vr_swapchains[i].images);
|
||||
}
|
||||
}
|
||||
SDL_free(vr_swapchains);
|
||||
vr_swapchains = NULL;
|
||||
}
|
||||
|
||||
if (xr_views) {
|
||||
SDL_free(xr_views);
|
||||
xr_views = NULL;
|
||||
}
|
||||
|
||||
/* Destroy OpenXR resources */
|
||||
if (xr_local_space && pfn_xrDestroySpace) {
|
||||
pfn_xrDestroySpace(xr_local_space);
|
||||
xr_local_space = XR_NULL_HANDLE;
|
||||
}
|
||||
if (xr_session && pfn_xrDestroySession) {
|
||||
pfn_xrDestroySession(xr_session);
|
||||
xr_session = XR_NULL_HANDLE;
|
||||
}
|
||||
|
||||
/* Destroy GPU device (this also handles XR instance cleanup) */
|
||||
if (gpu_device) {
|
||||
SDL_DestroyGPUDevice(gpu_device);
|
||||
gpu_device = NULL;
|
||||
}
|
||||
|
||||
SDL_Quit();
|
||||
exit(rc);
|
||||
}
|
||||
|
||||
/* ========================================================================
|
||||
* Shader Loading
|
||||
* ======================================================================== */
|
||||
|
||||
static SDL_GPUShader *load_shader(bool is_vertex, Uint32 sampler_count, Uint32 uniform_buffer_count)
|
||||
{
|
||||
SDL_GPUShaderCreateInfo createinfo;
|
||||
createinfo.num_samplers = sampler_count;
|
||||
createinfo.num_storage_buffers = 0;
|
||||
createinfo.num_storage_textures = 0;
|
||||
createinfo.num_uniform_buffers = uniform_buffer_count;
|
||||
|
||||
SDL_GPUShaderFormat format = SDL_GetGPUShaderFormats(gpu_device);
|
||||
if (format & SDL_GPU_SHADERFORMAT_DXIL) {
|
||||
createinfo.format = SDL_GPU_SHADERFORMAT_DXIL;
|
||||
if (is_vertex) {
|
||||
createinfo.code = cube_vert_dxil;
|
||||
createinfo.code_size = cube_vert_dxil_len;
|
||||
createinfo.entrypoint = "main";
|
||||
} else {
|
||||
createinfo.code = cube_frag_dxil;
|
||||
createinfo.code_size = cube_frag_dxil_len;
|
||||
createinfo.entrypoint = "main";
|
||||
}
|
||||
} else if (format & SDL_GPU_SHADERFORMAT_SPIRV) {
|
||||
createinfo.format = SDL_GPU_SHADERFORMAT_SPIRV;
|
||||
if (is_vertex) {
|
||||
createinfo.code = cube_vert_spv;
|
||||
createinfo.code_size = cube_vert_spv_len;
|
||||
createinfo.entrypoint = "main";
|
||||
} else {
|
||||
createinfo.code = cube_frag_spv;
|
||||
createinfo.code_size = cube_frag_spv_len;
|
||||
createinfo.entrypoint = "main";
|
||||
}
|
||||
} else {
|
||||
SDL_Log("No supported shader format found!");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
createinfo.stage = is_vertex ? SDL_GPU_SHADERSTAGE_VERTEX : SDL_GPU_SHADERSTAGE_FRAGMENT;
|
||||
createinfo.props = 0;
|
||||
|
||||
return SDL_CreateGPUShader(gpu_device, &createinfo);
|
||||
}
|
||||
|
||||
/* ========================================================================
|
||||
* OpenXR Function Loading
|
||||
* ======================================================================== */
|
||||
|
||||
static bool load_xr_functions(void)
|
||||
{
|
||||
pfn_xrGetInstanceProcAddr = (PFN_xrGetInstanceProcAddr)SDL_OpenXR_GetXrGetInstanceProcAddr();
|
||||
if (!pfn_xrGetInstanceProcAddr) {
|
||||
SDL_Log("Failed to get xrGetInstanceProcAddr");
|
||||
return false;
|
||||
}
|
||||
|
||||
#define XR_LOAD(fn) \
|
||||
if (XR_FAILED(pfn_xrGetInstanceProcAddr(xr_instance, #fn, (PFN_xrVoidFunction*)&pfn_##fn))) { \
|
||||
SDL_Log("Failed to load " #fn); \
|
||||
return false; \
|
||||
}
|
||||
|
||||
XR_LOAD(xrEnumerateViewConfigurationViews);
|
||||
XR_LOAD(xrEnumerateSwapchainImages);
|
||||
XR_LOAD(xrCreateReferenceSpace);
|
||||
XR_LOAD(xrDestroySpace);
|
||||
XR_LOAD(xrDestroySession);
|
||||
XR_LOAD(xrDestroyInstance);
|
||||
XR_LOAD(xrPollEvent);
|
||||
XR_LOAD(xrBeginSession);
|
||||
XR_LOAD(xrEndSession);
|
||||
XR_LOAD(xrWaitFrame);
|
||||
XR_LOAD(xrBeginFrame);
|
||||
XR_LOAD(xrEndFrame);
|
||||
XR_LOAD(xrLocateViews);
|
||||
XR_LOAD(xrAcquireSwapchainImage);
|
||||
XR_LOAD(xrWaitSwapchainImage);
|
||||
XR_LOAD(xrReleaseSwapchainImage);
|
||||
|
||||
#undef XR_LOAD
|
||||
|
||||
SDL_Log("Loaded all XR functions successfully");
|
||||
return true;
|
||||
}
|
||||
|
||||
/* ========================================================================
|
||||
* Pipeline and Buffer Creation
|
||||
* ======================================================================== */
|
||||
|
||||
static bool create_pipeline(SDL_GPUTextureFormat color_format)
|
||||
{
|
||||
SDL_GPUShader *vert_shader = load_shader(true, 0, 1);
|
||||
SDL_GPUShader *frag_shader = load_shader(false, 0, 0);
|
||||
|
||||
if (!vert_shader || !frag_shader) {
|
||||
if (vert_shader) SDL_ReleaseGPUShader(gpu_device, vert_shader);
|
||||
if (frag_shader) SDL_ReleaseGPUShader(gpu_device, frag_shader);
|
||||
return false;
|
||||
}
|
||||
|
||||
SDL_GPUGraphicsPipelineCreateInfo pipeline_info = {
|
||||
.vertex_shader = vert_shader,
|
||||
.fragment_shader = frag_shader,
|
||||
.target_info = {
|
||||
.num_color_targets = 1,
|
||||
.color_target_descriptions = (SDL_GPUColorTargetDescription[]){{
|
||||
.format = color_format
|
||||
}},
|
||||
.has_depth_stencil_target = true,
|
||||
.depth_stencil_format = DEPTH_FORMAT
|
||||
},
|
||||
.depth_stencil_state = {
|
||||
.enable_depth_test = true,
|
||||
.enable_depth_write = true,
|
||||
.compare_op = SDL_GPU_COMPAREOP_LESS_OR_EQUAL
|
||||
},
|
||||
.rasterizer_state = {
|
||||
.cull_mode = SDL_GPU_CULLMODE_BACK,
|
||||
.front_face = SDL_GPU_FRONTFACE_CLOCKWISE, /* Cube indices wind clockwise when viewed from outside */
|
||||
.fill_mode = SDL_GPU_FILLMODE_FILL
|
||||
},
|
||||
.vertex_input_state = {
|
||||
.num_vertex_buffers = 1,
|
||||
.vertex_buffer_descriptions = (SDL_GPUVertexBufferDescription[]){{
|
||||
.slot = 0,
|
||||
.pitch = sizeof(PositionColorVertex),
|
||||
.input_rate = SDL_GPU_VERTEXINPUTRATE_VERTEX
|
||||
}},
|
||||
.num_vertex_attributes = 2,
|
||||
.vertex_attributes = (SDL_GPUVertexAttribute[]){{
|
||||
.location = 0,
|
||||
.buffer_slot = 0,
|
||||
.format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT3,
|
||||
.offset = 0
|
||||
}, {
|
||||
.location = 1,
|
||||
.buffer_slot = 0,
|
||||
.format = SDL_GPU_VERTEXELEMENTFORMAT_UBYTE4_NORM,
|
||||
.offset = sizeof(float) * 3
|
||||
}}
|
||||
},
|
||||
.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST
|
||||
};
|
||||
|
||||
pipeline = SDL_CreateGPUGraphicsPipeline(gpu_device, &pipeline_info);
|
||||
|
||||
SDL_ReleaseGPUShader(gpu_device, vert_shader);
|
||||
SDL_ReleaseGPUShader(gpu_device, frag_shader);
|
||||
|
||||
if (!pipeline) {
|
||||
SDL_Log("Failed to create pipeline: %s", SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
|
||||
SDL_Log("Created graphics pipeline for format %d", color_format);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool create_cube_buffers(void)
|
||||
{
|
||||
float s = CUBE_HALF_SIZE;
|
||||
|
||||
PositionColorVertex vertices[24] = {
|
||||
/* Front face (red) */
|
||||
{-s,-s,-s, 255,0,0,255}, {s,-s,-s, 255,0,0,255}, {s,s,-s, 255,0,0,255}, {-s,s,-s, 255,0,0,255},
|
||||
/* Back face (green) */
|
||||
{s,-s,s, 0,255,0,255}, {-s,-s,s, 0,255,0,255}, {-s,s,s, 0,255,0,255}, {s,s,s, 0,255,0,255},
|
||||
/* Left face (blue) */
|
||||
{-s,-s,s, 0,0,255,255}, {-s,-s,-s, 0,0,255,255}, {-s,s,-s, 0,0,255,255}, {-s,s,s, 0,0,255,255},
|
||||
/* Right face (yellow) */
|
||||
{s,-s,-s, 255,255,0,255}, {s,-s,s, 255,255,0,255}, {s,s,s, 255,255,0,255}, {s,s,-s, 255,255,0,255},
|
||||
/* Top face (magenta) */
|
||||
{-s,s,-s, 255,0,255,255}, {s,s,-s, 255,0,255,255}, {s,s,s, 255,0,255,255}, {-s,s,s, 255,0,255,255},
|
||||
/* Bottom face (cyan) */
|
||||
{-s,-s,s, 0,255,255,255}, {s,-s,s, 0,255,255,255}, {s,-s,-s, 0,255,255,255}, {-s,-s,-s, 0,255,255,255}
|
||||
};
|
||||
|
||||
Uint16 indices[36] = {
|
||||
0,1,2, 0,2,3, /* Front */
|
||||
4,5,6, 4,6,7, /* Back */
|
||||
8,9,10, 8,10,11, /* Left */
|
||||
12,13,14, 12,14,15, /* Right */
|
||||
16,17,18, 16,18,19, /* Top */
|
||||
20,21,22, 20,22,23 /* Bottom */
|
||||
};
|
||||
|
||||
SDL_GPUBufferCreateInfo vertex_buf_info = {
|
||||
.usage = SDL_GPU_BUFFERUSAGE_VERTEX,
|
||||
.size = sizeof(vertices)
|
||||
};
|
||||
vertex_buffer = SDL_CreateGPUBuffer(gpu_device, &vertex_buf_info);
|
||||
CHECK_CREATE(vertex_buffer, "Vertex Buffer");
|
||||
|
||||
SDL_GPUBufferCreateInfo index_buf_info = {
|
||||
.usage = SDL_GPU_BUFFERUSAGE_INDEX,
|
||||
.size = sizeof(indices)
|
||||
};
|
||||
index_buffer = SDL_CreateGPUBuffer(gpu_device, &index_buf_info);
|
||||
CHECK_CREATE(index_buffer, "Index Buffer");
|
||||
|
||||
/* Create transfer buffer and upload data */
|
||||
SDL_GPUTransferBufferCreateInfo transfer_info = {
|
||||
.usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD,
|
||||
.size = sizeof(vertices) + sizeof(indices)
|
||||
};
|
||||
SDL_GPUTransferBuffer *transfer = SDL_CreateGPUTransferBuffer(gpu_device, &transfer_info);
|
||||
CHECK_CREATE(transfer, "Transfer Buffer");
|
||||
|
||||
void *data = SDL_MapGPUTransferBuffer(gpu_device, transfer, false);
|
||||
SDL_memcpy(data, vertices, sizeof(vertices));
|
||||
SDL_memcpy((Uint8*)data + sizeof(vertices), indices, sizeof(indices));
|
||||
SDL_UnmapGPUTransferBuffer(gpu_device, transfer);
|
||||
|
||||
SDL_GPUCommandBuffer *cmd = SDL_AcquireGPUCommandBuffer(gpu_device);
|
||||
SDL_GPUCopyPass *copy_pass = SDL_BeginGPUCopyPass(cmd);
|
||||
|
||||
SDL_GPUTransferBufferLocation src_vertex = { .transfer_buffer = transfer, .offset = 0 };
|
||||
SDL_GPUBufferRegion dst_vertex = { .buffer = vertex_buffer, .offset = 0, .size = sizeof(vertices) };
|
||||
SDL_UploadToGPUBuffer(copy_pass, &src_vertex, &dst_vertex, false);
|
||||
|
||||
SDL_GPUTransferBufferLocation src_index = { .transfer_buffer = transfer, .offset = sizeof(vertices) };
|
||||
SDL_GPUBufferRegion dst_index = { .buffer = index_buffer, .offset = 0, .size = sizeof(indices) };
|
||||
SDL_UploadToGPUBuffer(copy_pass, &src_index, &dst_index, false);
|
||||
|
||||
SDL_EndGPUCopyPass(copy_pass);
|
||||
SDL_SubmitGPUCommandBuffer(cmd);
|
||||
SDL_ReleaseGPUTransferBuffer(gpu_device, transfer);
|
||||
|
||||
SDL_Log("Created cube vertex (%u bytes) and index (%u bytes) buffers", (unsigned int)sizeof(vertices), (unsigned int)sizeof(indices));
|
||||
return true;
|
||||
}
|
||||
|
||||
/* ========================================================================
|
||||
* XR Session Initialization
|
||||
* ======================================================================== */
|
||||
|
||||
static bool init_xr_session(void)
|
||||
{
|
||||
XrResult result;
|
||||
|
||||
/* Create session */
|
||||
XrSessionCreateInfo session_info = { XR_TYPE_SESSION_CREATE_INFO };
|
||||
result = SDL_CreateGPUXRSession(gpu_device, &session_info, &xr_session);
|
||||
XR_CHECK(result, "Failed to create XR session");
|
||||
|
||||
/* Create reference space */
|
||||
XrReferenceSpaceCreateInfo space_info = { XR_TYPE_REFERENCE_SPACE_CREATE_INFO };
|
||||
space_info.referenceSpaceType = XR_REFERENCE_SPACE_TYPE_LOCAL;
|
||||
space_info.poseInReferenceSpace.orientation.w = 1.0f; /* Identity quaternion */
|
||||
|
||||
result = pfn_xrCreateReferenceSpace(xr_session, &space_info, &xr_local_space);
|
||||
XR_CHECK(result, "Failed to create reference space");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool create_swapchains(void)
|
||||
{
|
||||
XrResult result;
|
||||
|
||||
/* Get view configuration */
|
||||
result = pfn_xrEnumerateViewConfigurationViews(
|
||||
xr_instance, xr_system_id,
|
||||
XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO,
|
||||
0, &view_count, NULL);
|
||||
XR_CHECK(result, "Failed to enumerate view config views (count)");
|
||||
|
||||
SDL_Log("View count: %" SDL_PRIu32, view_count);
|
||||
|
||||
XrViewConfigurationView *view_configs = SDL_calloc(view_count, sizeof(XrViewConfigurationView));
|
||||
for (Uint32 i = 0; i < view_count; i++) {
|
||||
view_configs[i].type = XR_TYPE_VIEW_CONFIGURATION_VIEW;
|
||||
}
|
||||
|
||||
result = pfn_xrEnumerateViewConfigurationViews(
|
||||
xr_instance, xr_system_id,
|
||||
XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO,
|
||||
view_count, &view_count, view_configs);
|
||||
if (XR_FAILED(result)) {
|
||||
SDL_free(view_configs);
|
||||
SDL_Log("Failed to enumerate view config views");
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Allocate swapchains and views */
|
||||
vr_swapchains = SDL_calloc(view_count, sizeof(VRSwapchain));
|
||||
xr_views = SDL_calloc(view_count, sizeof(XrView));
|
||||
|
||||
/* Query available swapchain formats
|
||||
* Per PR #14837: format arrays are terminated with SDL_GPU_TEXTUREFORMAT_INVALID */
|
||||
int num_formats = 0;
|
||||
SDL_GPUTextureFormat *formats = SDL_GetGPUXRSwapchainFormats(gpu_device, xr_session, &num_formats);
|
||||
if (!formats || num_formats == 0) {
|
||||
SDL_Log("Failed to get XR swapchain formats");
|
||||
SDL_free(view_configs);
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Use first available format (typically sRGB)
|
||||
* Note: Could iterate with: while (formats[i] != SDL_GPU_TEXTUREFORMAT_INVALID) */
|
||||
SDL_GPUTextureFormat swapchain_format = formats[0];
|
||||
SDL_Log("Using swapchain format: %d (of %d available)", swapchain_format, num_formats);
|
||||
|
||||
/* Log all available formats for debugging */
|
||||
for (int f = 0; f < num_formats && formats[f] != SDL_GPU_TEXTUREFORMAT_INVALID; f++) {
|
||||
SDL_Log(" Available format [%d]: %d", f, formats[f]);
|
||||
}
|
||||
SDL_free(formats);
|
||||
|
||||
for (Uint32 i = 0; i < view_count; i++) {
|
||||
xr_views[i].type = XR_TYPE_VIEW;
|
||||
xr_views[i].pose.orientation.w = 1.0f;
|
||||
|
||||
SDL_Log("Eye %" SDL_PRIu32 ": recommended %ux%u", i,
|
||||
(unsigned int)view_configs[i].recommendedImageRectWidth,
|
||||
(unsigned int)view_configs[i].recommendedImageRectHeight);
|
||||
|
||||
/* Create swapchain using OpenXR's XrSwapchainCreateInfo */
|
||||
XrSwapchainCreateInfo swapchain_info = { XR_TYPE_SWAPCHAIN_CREATE_INFO };
|
||||
swapchain_info.usageFlags = XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT | XR_SWAPCHAIN_USAGE_SAMPLED_BIT;
|
||||
swapchain_info.format = 0; /* Ignored - SDL uses the format parameter */
|
||||
swapchain_info.sampleCount = 1;
|
||||
swapchain_info.width = view_configs[i].recommendedImageRectWidth;
|
||||
swapchain_info.height = view_configs[i].recommendedImageRectHeight;
|
||||
swapchain_info.faceCount = 1;
|
||||
swapchain_info.arraySize = 1;
|
||||
swapchain_info.mipCount = 1;
|
||||
|
||||
result = SDL_CreateGPUXRSwapchain(
|
||||
gpu_device,
|
||||
xr_session,
|
||||
&swapchain_info,
|
||||
swapchain_format,
|
||||
&vr_swapchains[i].swapchain,
|
||||
&vr_swapchains[i].images);
|
||||
|
||||
vr_swapchains[i].format = swapchain_format;
|
||||
|
||||
if (XR_FAILED(result)) {
|
||||
SDL_Log("Failed to create swapchain %" SDL_PRIu32, i);
|
||||
SDL_free(view_configs);
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Get image count by enumerating swapchain images */
|
||||
result = pfn_xrEnumerateSwapchainImages(vr_swapchains[i].swapchain, 0, &vr_swapchains[i].image_count, NULL);
|
||||
if (XR_FAILED(result)) {
|
||||
vr_swapchains[i].image_count = 3; /* Assume 3 if we can't query */
|
||||
}
|
||||
|
||||
vr_swapchains[i].size.width = (int32_t)swapchain_info.width;
|
||||
vr_swapchains[i].size.height = (int32_t)swapchain_info.height;
|
||||
|
||||
/* Create local depth texture for this eye
|
||||
* Per PR #14837: Depth buffers are "really recommended" for XR apps.
|
||||
* Using a local depth texture (not XR-managed) is the simplest approach
|
||||
* for proper z-ordering without requiring XR_KHR_composition_layer_depth. */
|
||||
SDL_GPUTextureCreateInfo depth_info = {
|
||||
.type = SDL_GPU_TEXTURETYPE_2D,
|
||||
.format = DEPTH_FORMAT,
|
||||
.width = swapchain_info.width,
|
||||
.height = swapchain_info.height,
|
||||
.layer_count_or_depth = 1,
|
||||
.num_levels = 1,
|
||||
.sample_count = SDL_GPU_SAMPLECOUNT_1,
|
||||
.usage = SDL_GPU_TEXTUREUSAGE_DEPTH_STENCIL_TARGET,
|
||||
.props = 0
|
||||
};
|
||||
vr_swapchains[i].depth_texture = SDL_CreateGPUTexture(gpu_device, &depth_info);
|
||||
if (!vr_swapchains[i].depth_texture) {
|
||||
SDL_Log("Failed to create depth texture for eye %" SDL_PRIu32 ": %s", i, SDL_GetError());
|
||||
SDL_free(view_configs);
|
||||
return false;
|
||||
}
|
||||
|
||||
SDL_Log("Created swapchain %" SDL_PRIu32 ": %" SDL_PRIs32 "x%" SDL_PRIs32 ", %" SDL_PRIu32 " images, with depth buffer",
|
||||
i, vr_swapchains[i].size.width, vr_swapchains[i].size.height,
|
||||
vr_swapchains[i].image_count);
|
||||
}
|
||||
|
||||
SDL_free(view_configs);
|
||||
|
||||
/* Create the pipeline using the swapchain format */
|
||||
if (view_count > 0 && pipeline == NULL) {
|
||||
if (!create_pipeline(vr_swapchains[0].format)) {
|
||||
return false;
|
||||
}
|
||||
if (!create_cube_buffers()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/* ========================================================================
|
||||
* XR Event Handling
|
||||
* ======================================================================== */
|
||||
|
||||
static void handle_xr_events(void)
|
||||
{
|
||||
XrEventDataBuffer event_buffer = { XR_TYPE_EVENT_DATA_BUFFER };
|
||||
|
||||
while (pfn_xrPollEvent(xr_instance, &event_buffer) == XR_SUCCESS) {
|
||||
switch (event_buffer.type) {
|
||||
case XR_TYPE_EVENT_DATA_SESSION_STATE_CHANGED: {
|
||||
XrEventDataSessionStateChanged *state_event =
|
||||
(XrEventDataSessionStateChanged*)&event_buffer;
|
||||
|
||||
SDL_Log("Session state changed: %d", state_event->state);
|
||||
|
||||
switch (state_event->state) {
|
||||
case XR_SESSION_STATE_READY: {
|
||||
XrSessionBeginInfo begin_info = { XR_TYPE_SESSION_BEGIN_INFO };
|
||||
begin_info.primaryViewConfigurationType = XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO;
|
||||
|
||||
XrResult result = pfn_xrBeginSession(xr_session, &begin_info);
|
||||
if (XR_SUCCEEDED(result)) {
|
||||
SDL_Log("XR Session begun!");
|
||||
xr_session_running = true;
|
||||
|
||||
/* Create swapchains now that session is ready */
|
||||
if (!create_swapchains()) {
|
||||
SDL_Log("Failed to create swapchains");
|
||||
xr_should_quit = true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case XR_SESSION_STATE_STOPPING:
|
||||
pfn_xrEndSession(xr_session);
|
||||
xr_session_running = false;
|
||||
break;
|
||||
case XR_SESSION_STATE_EXITING:
|
||||
case XR_SESSION_STATE_LOSS_PENDING:
|
||||
xr_should_quit = true;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case XR_TYPE_EVENT_DATA_INSTANCE_LOSS_PENDING:
|
||||
xr_should_quit = true;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
event_buffer.type = XR_TYPE_EVENT_DATA_BUFFER;
|
||||
}
|
||||
}
|
||||
|
||||
/* ========================================================================
|
||||
* Rendering
|
||||
* ======================================================================== */
|
||||
|
||||
static void render_frame(void)
|
||||
{
|
||||
if (!xr_session_running) return;
|
||||
|
||||
XrFrameState frame_state = { XR_TYPE_FRAME_STATE };
|
||||
XrFrameWaitInfo wait_info = { XR_TYPE_FRAME_WAIT_INFO };
|
||||
|
||||
XrResult result = pfn_xrWaitFrame(xr_session, &wait_info, &frame_state);
|
||||
if (XR_FAILED(result)) return;
|
||||
|
||||
XrFrameBeginInfo begin_info = { XR_TYPE_FRAME_BEGIN_INFO };
|
||||
result = pfn_xrBeginFrame(xr_session, &begin_info);
|
||||
if (XR_FAILED(result)) return;
|
||||
|
||||
XrCompositionLayerProjectionView *proj_views = NULL;
|
||||
XrCompositionLayerProjection layer = { XR_TYPE_COMPOSITION_LAYER_PROJECTION };
|
||||
Uint32 layer_count = 0;
|
||||
const XrCompositionLayerBaseHeader *layers[1] = {0};
|
||||
|
||||
if (frame_state.shouldRender && view_count > 0 && vr_swapchains != NULL) {
|
||||
/* Update animation time */
|
||||
Uint64 now = SDL_GetTicks();
|
||||
if (last_ticks == 0) last_ticks = now;
|
||||
float delta = (float)(now - last_ticks) / 1000.0f;
|
||||
last_ticks = now;
|
||||
anim_time += delta;
|
||||
|
||||
/* Locate views */
|
||||
XrViewState view_state = { XR_TYPE_VIEW_STATE };
|
||||
XrViewLocateInfo locate_info = { XR_TYPE_VIEW_LOCATE_INFO };
|
||||
locate_info.viewConfigurationType = XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO;
|
||||
locate_info.displayTime = frame_state.predictedDisplayTime;
|
||||
locate_info.space = xr_local_space;
|
||||
|
||||
Uint32 view_count_output;
|
||||
result = pfn_xrLocateViews(xr_session, &locate_info, &view_state, view_count, &view_count_output, xr_views);
|
||||
if (XR_FAILED(result)) {
|
||||
SDL_Log("xrLocateViews failed");
|
||||
goto endFrame;
|
||||
}
|
||||
|
||||
proj_views = SDL_calloc(view_count, sizeof(XrCompositionLayerProjectionView));
|
||||
|
||||
SDL_GPUCommandBuffer *cmd_buf = SDL_AcquireGPUCommandBuffer(gpu_device);
|
||||
|
||||
/* Multi-pass stereo: render each eye separately */
|
||||
for (Uint32 i = 0; i < view_count; i++) {
|
||||
VRSwapchain *swapchain = &vr_swapchains[i];
|
||||
|
||||
/* Acquire swapchain image */
|
||||
Uint32 image_index;
|
||||
XrSwapchainImageAcquireInfo acquire_info = { XR_TYPE_SWAPCHAIN_IMAGE_ACQUIRE_INFO };
|
||||
result = pfn_xrAcquireSwapchainImage(swapchain->swapchain, &acquire_info, &image_index);
|
||||
if (XR_FAILED(result)) continue;
|
||||
|
||||
XrSwapchainImageWaitInfo wait_image_info = { XR_TYPE_SWAPCHAIN_IMAGE_WAIT_INFO };
|
||||
wait_image_info.timeout = XR_INFINITE_DURATION;
|
||||
result = pfn_xrWaitSwapchainImage(swapchain->swapchain, &wait_image_info);
|
||||
if (XR_FAILED(result)) {
|
||||
XrSwapchainImageReleaseInfo release_info = { XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO };
|
||||
pfn_xrReleaseSwapchainImage(swapchain->swapchain, &release_info);
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Render the scene to this eye */
|
||||
SDL_GPUTexture *target_texture = swapchain->images[image_index];
|
||||
|
||||
/* Build view and projection matrices from XR pose/fov */
|
||||
Mat4 view_matrix = Mat4_FromXrPose(xr_views[i].pose);
|
||||
Mat4 proj_matrix = Mat4_Projection(xr_views[i].fov, 0.05f, 100.0f);
|
||||
|
||||
SDL_GPUColorTargetInfo color_target = {0};
|
||||
color_target.texture = target_texture;
|
||||
color_target.load_op = SDL_GPU_LOADOP_CLEAR;
|
||||
color_target.store_op = SDL_GPU_STOREOP_STORE;
|
||||
/* Dark blue background */
|
||||
color_target.clear_color.r = 0.05f;
|
||||
color_target.clear_color.g = 0.05f;
|
||||
color_target.clear_color.b = 0.15f;
|
||||
color_target.clear_color.a = 1.0f;
|
||||
|
||||
/* Set up depth target for proper z-ordering */
|
||||
SDL_GPUDepthStencilTargetInfo depth_target = {0};
|
||||
depth_target.texture = swapchain->depth_texture;
|
||||
depth_target.clear_depth = 1.0f; /* Far plane */
|
||||
depth_target.load_op = SDL_GPU_LOADOP_CLEAR;
|
||||
depth_target.store_op = SDL_GPU_STOREOP_DONT_CARE; /* We don't need to preserve depth */
|
||||
depth_target.stencil_load_op = SDL_GPU_LOADOP_DONT_CARE;
|
||||
depth_target.stencil_store_op = SDL_GPU_STOREOP_DONT_CARE;
|
||||
depth_target.cycle = true; /* Allow GPU to cycle the texture for efficiency */
|
||||
|
||||
SDL_GPURenderPass *render_pass = SDL_BeginGPURenderPass(cmd_buf, &color_target, 1, &depth_target);
|
||||
|
||||
if (pipeline && vertex_buffer && index_buffer) {
|
||||
SDL_BindGPUGraphicsPipeline(render_pass, pipeline);
|
||||
|
||||
SDL_GPUViewport viewport = {0, 0, (float)swapchain->size.width, (float)swapchain->size.height, 0, 1};
|
||||
SDL_SetGPUViewport(render_pass, &viewport);
|
||||
|
||||
SDL_Rect scissor = {0, 0, swapchain->size.width, swapchain->size.height};
|
||||
SDL_SetGPUScissor(render_pass, &scissor);
|
||||
|
||||
SDL_GPUBufferBinding vertex_binding = {vertex_buffer, 0};
|
||||
SDL_BindGPUVertexBuffers(render_pass, 0, &vertex_binding, 1);
|
||||
|
||||
SDL_GPUBufferBinding index_binding = {index_buffer, 0};
|
||||
SDL_BindGPUIndexBuffer(render_pass, &index_binding, SDL_GPU_INDEXELEMENTSIZE_16BIT);
|
||||
|
||||
/* Draw each cube */
|
||||
for (int cube_idx = 0; cube_idx < NUM_CUBES; cube_idx++) {
|
||||
float rot = anim_time * cube_speeds[cube_idx];
|
||||
Vec3 pos = cube_positions[cube_idx];
|
||||
|
||||
/* Build model matrix: scale -> rotateY -> rotateX -> translate */
|
||||
Mat4 scale = Mat4_Scale(cube_scales[cube_idx]);
|
||||
Mat4 rotY = Mat4_RotationY(rot);
|
||||
Mat4 rotX = Mat4_RotationX(rot * 0.7f);
|
||||
Mat4 trans = Mat4_Translation(pos.x, pos.y, pos.z);
|
||||
|
||||
Mat4 model = Mat4_Multiply(Mat4_Multiply(Mat4_Multiply(scale, rotY), rotX), trans);
|
||||
Mat4 mv = Mat4_Multiply(model, view_matrix);
|
||||
Mat4 mvp = Mat4_Multiply(mv, proj_matrix);
|
||||
|
||||
SDL_PushGPUVertexUniformData(cmd_buf, 0, &mvp, sizeof(mvp));
|
||||
SDL_DrawGPUIndexedPrimitives(render_pass, 36, 1, 0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
SDL_EndGPURenderPass(render_pass);
|
||||
|
||||
/* Release swapchain image */
|
||||
XrSwapchainImageReleaseInfo release_info = { XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO };
|
||||
pfn_xrReleaseSwapchainImage(swapchain->swapchain, &release_info);
|
||||
|
||||
/* Set up projection view */
|
||||
proj_views[i].type = XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW;
|
||||
proj_views[i].pose = xr_views[i].pose;
|
||||
proj_views[i].fov = xr_views[i].fov;
|
||||
proj_views[i].subImage.swapchain = swapchain->swapchain;
|
||||
proj_views[i].subImage.imageRect.offset.x = 0;
|
||||
proj_views[i].subImage.imageRect.offset.y = 0;
|
||||
proj_views[i].subImage.imageRect.extent = swapchain->size;
|
||||
proj_views[i].subImage.imageArrayIndex = 0;
|
||||
}
|
||||
|
||||
SDL_SubmitGPUCommandBuffer(cmd_buf);
|
||||
|
||||
layer.space = xr_local_space;
|
||||
layer.viewCount = view_count;
|
||||
layer.views = proj_views;
|
||||
layers[0] = (XrCompositionLayerBaseHeader*)&layer;
|
||||
layer_count = 1;
|
||||
}
|
||||
|
||||
endFrame:;
|
||||
XrFrameEndInfo end_info = { XR_TYPE_FRAME_END_INFO };
|
||||
end_info.displayTime = frame_state.predictedDisplayTime;
|
||||
end_info.environmentBlendMode = XR_ENVIRONMENT_BLEND_MODE_OPAQUE;
|
||||
end_info.layerCount = layer_count;
|
||||
end_info.layers = layers;
|
||||
|
||||
pfn_xrEndFrame(xr_session, &end_info);
|
||||
|
||||
if (proj_views) SDL_free(proj_views);
|
||||
}
|
||||
|
||||
/* ========================================================================
|
||||
* Main
|
||||
* ======================================================================== */
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
|
||||
SDL_Log("SDL GPU OpenXR Spinning Cubes Test starting...");
|
||||
SDL_Log("Stereo rendering mode: Multi-pass (one render pass per eye)");
|
||||
|
||||
if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS)) {
|
||||
SDL_Log("SDL_Init failed: %s", SDL_GetError());
|
||||
return 1;
|
||||
}
|
||||
|
||||
SDL_Log("SDL initialized");
|
||||
|
||||
/* Create GPU device with OpenXR enabled */
|
||||
SDL_Log("Creating GPU device with OpenXR enabled...");
|
||||
|
||||
SDL_PropertiesID props = SDL_CreateProperties();
|
||||
SDL_SetBooleanProperty(props, SDL_PROP_GPU_DEVICE_CREATE_SHADERS_SPIRV_BOOLEAN, true);
|
||||
SDL_SetBooleanProperty(props, SDL_PROP_GPU_DEVICE_CREATE_SHADERS_DXIL_BOOLEAN, true);
|
||||
SDL_SetBooleanProperty(props, SDL_PROP_GPU_DEVICE_CREATE_DEBUGMODE_BOOLEAN, true);
|
||||
/* Enable XR - SDL will create the OpenXR instance for us */
|
||||
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);
|
||||
SDL_SetStringProperty(props, SDL_PROP_GPU_DEVICE_CREATE_XR_APPLICATION_NAME_STRING, "SDL XR Spinning Cubes Test");
|
||||
SDL_SetNumberProperty(props, SDL_PROP_GPU_DEVICE_CREATE_XR_APPLICATION_VERSION_NUMBER, 1);
|
||||
|
||||
gpu_device = SDL_CreateGPUDeviceWithProperties(props);
|
||||
SDL_DestroyProperties(props);
|
||||
|
||||
if (!gpu_device) {
|
||||
SDL_Log("Failed to create GPU device: %s", SDL_GetError());
|
||||
SDL_Quit();
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Load OpenXR function pointers */
|
||||
if (!load_xr_functions()) {
|
||||
SDL_Log("Failed to load XR functions");
|
||||
quit(1);
|
||||
}
|
||||
|
||||
/* Initialize XR session */
|
||||
if (!init_xr_session()) {
|
||||
SDL_Log("Failed to init XR session");
|
||||
quit(1);
|
||||
}
|
||||
|
||||
SDL_Log("Entering main loop... Put on your VR headset!");
|
||||
|
||||
/* Main loop */
|
||||
while (!xr_should_quit) {
|
||||
SDL_Event event;
|
||||
while (SDL_PollEvent(&event)) {
|
||||
if (event.type == SDL_EVENT_QUIT) {
|
||||
xr_should_quit = true;
|
||||
}
|
||||
if (event.type == SDL_EVENT_KEY_DOWN && event.key.key == SDLK_ESCAPE) {
|
||||
xr_should_quit = true;
|
||||
}
|
||||
}
|
||||
|
||||
handle_xr_events();
|
||||
render_frame();
|
||||
}
|
||||
|
||||
quit(0);
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user