| /* |
| 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; |
| } |