Atlas - testgpu_spinning_cube_xr.c
Home / ext / SDL / test Lines: 1 | Size: 37779 bytes [Download] [Show on GitHub] [Search similar files] [Raw] [Raw (proxy)][FILE BEGIN]1/* 2 Copyright (C) 1997-2026 Sam Lantinga <[email protected]> 3 4 This software is provided 'as-is', without any express or implied 5 warranty. In no event will the authors be held liable for any damages 6 arising from the use of this software. 7 8 Permission is granted to anyone to use this software for any purpose, 9 including commercial applications, and to alter it and redistribute it 10 freely. 11*/ 12 13/* 14 * testgpu_spinning_cube_xr.c - SDL3 GPU API OpenXR Spinning Cubes Test 15 * 16 * This is an XR-enabled version of testgpu_spinning_cube that renders 17 * spinning colored cubes in VR using OpenXR and SDL's GPU API. 18 * 19 * Rendering approach: Multi-pass stereo (one render pass per eye) 20 * This is the simplest and most compatible approach, working on all 21 * OpenXR-capable platforms (Desktop VR runtimes, Quest, etc.) 22 * 23 * For more information on stereo rendering techniques, see: 24 * - Multi-pass: Traditional, 2 render passes (used here) 25 * - Multiview (GL_OVR_multiview): Single pass with texture arrays 26 * - Single-pass instanced: GPU instancing to select eye 27 */ 28 29#include <SDL3/SDL.h> 30#include <SDL3/SDL_main.h> 31 32/* Include OpenXR headers BEFORE SDL_openxr.h to get full type definitions */ 33#ifdef HAVE_OPENXR_H 34#include <openxr/openxr.h> 35#else 36/* SDL includes a copy for building on systems without the OpenXR SDK */ 37#include "../src/video/khronos/openxr/openxr.h" 38#endif 39 40#include <SDL3/SDL_openxr.h> 41 42/* Standard library for exit() */ 43#include <stdlib.h> 44 45/* Include compiled shader bytecode for all backends */ 46#include "testgpu/cube.frag.dxil.h" 47#include "testgpu/cube.frag.spv.h" 48#include "testgpu/cube.vert.dxil.h" 49#include "testgpu/cube.vert.spv.h" 50 51#define CHECK_CREATE(var, thing) { if (!(var)) { SDL_Log("Failed to create %s: %s", thing, SDL_GetError()); return false; } } 52#define XR_CHECK(result, msg) do { if (XR_FAILED(result)) { SDL_Log("OpenXR Error: %s (result=%d)", msg, (int)(result)); return false; } } while(0) 53#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) 54 55/* ======================================================================== 56 * Math Types and Functions 57 * ======================================================================== */ 58 59typedef struct { float x, y, z; } Vec3; 60typedef struct { float m[16]; } Mat4; 61 62static Mat4 Mat4_Multiply(Mat4 a, Mat4 b) 63{ 64 Mat4 result = {{0}}; 65 for (int i = 0; i < 4; i++) { 66 for (int j = 0; j < 4; j++) { 67 for (int k = 0; k < 4; k++) { 68 result.m[i * 4 + j] += a.m[i * 4 + k] * b.m[k * 4 + j]; 69 } 70 } 71 } 72 return result; 73} 74 75static Mat4 Mat4_Translation(float x, float y, float z) 76{ 77 return (Mat4){{ 1,0,0,0, 0,1,0,0, 0,0,1,0, x,y,z,1 }}; 78} 79 80static Mat4 Mat4_Scale(float s) 81{ 82 return (Mat4){{ s,0,0,0, 0,s,0,0, 0,0,s,0, 0,0,0,1 }}; 83} 84 85static Mat4 Mat4_RotationY(float rad) 86{ 87 float c = SDL_cosf(rad), s = SDL_sinf(rad); 88 return (Mat4){{ c,0,-s,0, 0,1,0,0, s,0,c,0, 0,0,0,1 }}; 89} 90 91static Mat4 Mat4_RotationX(float rad) 92{ 93 float c = SDL_cosf(rad), s = SDL_sinf(rad); 94 return (Mat4){{ 1,0,0,0, 0,c,s,0, 0,-s,c,0, 0,0,0,1 }}; 95} 96 97/* Convert XrPosef to view matrix (inverted transform) */ 98static Mat4 Mat4_FromXrPose(XrPosef pose) 99{ 100 float x = pose.orientation.x, y = pose.orientation.y; 101 float z = pose.orientation.z, w = pose.orientation.w; 102 103 /* Quaternion to rotation matrix columns */ 104 Vec3 right = { 1-2*(y*y+z*z), 2*(x*y+w*z), 2*(x*z-w*y) }; 105 Vec3 up = { 2*(x*y-w*z), 1-2*(x*x+z*z), 2*(y*z+w*x) }; 106 Vec3 fwd = { 2*(x*z+w*y), 2*(y*z-w*x), 1-2*(x*x+y*y) }; 107 Vec3 pos = { pose.position.x, pose.position.y, pose.position.z }; 108 109 /* Inverted transform for view matrix */ 110 float dr = -(right.x*pos.x + right.y*pos.y + right.z*pos.z); 111 float du = -(up.x*pos.x + up.y*pos.y + up.z*pos.z); 112 float df = -(fwd.x*pos.x + fwd.y*pos.y + fwd.z*pos.z); 113 114 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 }}; 115} 116 117/* Create asymmetric projection matrix from XR FOV */ 118static Mat4 Mat4_Projection(XrFovf fov, float nearZ, float farZ) 119{ 120 float tL = SDL_tanf(fov.angleLeft), tR = SDL_tanf(fov.angleRight); 121 float tU = SDL_tanf(fov.angleUp), tD = SDL_tanf(fov.angleDown); 122 float w = tR - tL, h = tU - tD; 123 124 return (Mat4){{ 125 2/w, 0, 0, 0, 126 0, 2/h, 0, 0, 127 (tR+tL)/w, (tU+tD)/h, -farZ/(farZ-nearZ), -1, 128 0, 0, -(farZ*nearZ)/(farZ-nearZ), 0 129 }}; 130} 131 132/* ======================================================================== 133 * Vertex Data 134 * ======================================================================== */ 135 136typedef struct { 137 float x, y, z; 138 Uint8 r, g, b, a; 139} PositionColorVertex; 140 141/* Cube vertices - 0.25m half-size, each face a different color */ 142static const float CUBE_HALF_SIZE = 0.25f; 143 144/* ======================================================================== 145 * OpenXR Function Pointers (loaded dynamically) 146 * ======================================================================== */ 147 148static PFN_xrGetInstanceProcAddr pfn_xrGetInstanceProcAddr = NULL; 149static PFN_xrEnumerateViewConfigurationViews pfn_xrEnumerateViewConfigurationViews = NULL; 150static PFN_xrEnumerateSwapchainImages pfn_xrEnumerateSwapchainImages = NULL; 151static PFN_xrCreateReferenceSpace pfn_xrCreateReferenceSpace = NULL; 152static PFN_xrDestroySpace pfn_xrDestroySpace = NULL; 153static PFN_xrDestroySession pfn_xrDestroySession = NULL; 154static PFN_xrDestroyInstance pfn_xrDestroyInstance = NULL; 155static PFN_xrPollEvent pfn_xrPollEvent = NULL; 156static PFN_xrBeginSession pfn_xrBeginSession = NULL; 157static PFN_xrEndSession pfn_xrEndSession = NULL; 158static PFN_xrWaitFrame pfn_xrWaitFrame = NULL; 159static PFN_xrBeginFrame pfn_xrBeginFrame = NULL; 160static PFN_xrEndFrame pfn_xrEndFrame = NULL; 161static PFN_xrLocateViews pfn_xrLocateViews = NULL; 162static PFN_xrAcquireSwapchainImage pfn_xrAcquireSwapchainImage = NULL; 163static PFN_xrWaitSwapchainImage pfn_xrWaitSwapchainImage = NULL; 164static PFN_xrReleaseSwapchainImage pfn_xrReleaseSwapchainImage = NULL; 165 166/* ======================================================================== 167 * Global State 168 * ======================================================================== */ 169 170/* OpenXR state */ 171static XrInstance xr_instance = XR_NULL_HANDLE; 172static XrSystemId xr_system_id = XR_NULL_SYSTEM_ID; 173static XrSession xr_session = XR_NULL_HANDLE; 174static XrSpace xr_local_space = XR_NULL_HANDLE; 175static bool xr_session_running = false; 176static bool xr_should_quit = false; 177 178/* Swapchain state */ 179typedef struct { 180 XrSwapchain swapchain; 181 SDL_GPUTexture **images; 182 SDL_GPUTexture *depth_texture; /* Local depth buffer for z-ordering */ 183 XrExtent2Di size; 184 SDL_GPUTextureFormat format; 185 Uint32 image_count; 186} VRSwapchain; 187 188/* Depth buffer format - use D24 for wide compatibility */ 189static const SDL_GPUTextureFormat DEPTH_FORMAT = SDL_GPU_TEXTUREFORMAT_D24_UNORM; 190 191static VRSwapchain *vr_swapchains = NULL; 192static XrView *xr_views = NULL; 193static Uint32 view_count = 0; 194 195/* SDL GPU state */ 196static SDL_GPUDevice *gpu_device = NULL; 197static SDL_GPUGraphicsPipeline *pipeline = NULL; 198static SDL_GPUBuffer *vertex_buffer = NULL; 199static SDL_GPUBuffer *index_buffer = NULL; 200 201/* Animation time */ 202static float anim_time = 0.0f; 203static Uint64 last_ticks = 0; 204 205/* Cube scene configuration */ 206#define NUM_CUBES 5 207static Vec3 cube_positions[NUM_CUBES] = { 208 { 0.0f, 0.0f, -2.0f }, /* Center, in front */ 209 { -1.2f, 0.4f, -2.5f }, /* Upper left */ 210 { 1.2f, 0.3f, -2.5f }, /* Upper right */ 211 { -0.6f, -0.4f, -1.8f }, /* Lower left close */ 212 { 0.6f, -0.3f, -1.8f }, /* Lower right close */ 213}; 214static float cube_scales[NUM_CUBES] = { 1.0f, 0.6f, 0.6f, 0.5f, 0.5f }; 215static float cube_speeds[NUM_CUBES] = { 1.0f, 1.5f, -1.2f, 2.0f, -0.8f }; 216 217/* ======================================================================== 218 * Cleanup and Quit 219 * ======================================================================== */ 220 221static void quit(int rc) 222{ 223 SDL_Log("Cleaning up..."); 224 225 /* CRITICAL: Wait for GPU to finish before destroying resources 226 * Per PR #14837 discussion - prevents Vulkan validation errors */ 227 if (gpu_device) { 228 SDL_WaitForGPUIdle(gpu_device); 229 } 230 231 /* Release GPU resources first */ 232 if (pipeline) { 233 SDL_ReleaseGPUGraphicsPipeline(gpu_device, pipeline); 234 pipeline = NULL; 235 } 236 if (vertex_buffer) { 237 SDL_ReleaseGPUBuffer(gpu_device, vertex_buffer); 238 vertex_buffer = NULL; 239 } 240 if (index_buffer) { 241 SDL_ReleaseGPUBuffer(gpu_device, index_buffer); 242 index_buffer = NULL; 243 } 244 245 /* Release swapchains and depth textures */ 246 if (vr_swapchains) { 247 for (Uint32 i = 0; i < view_count; i++) { 248 if (vr_swapchains[i].depth_texture) { 249 SDL_ReleaseGPUTexture(gpu_device, vr_swapchains[i].depth_texture); 250 } 251 if (vr_swapchains[i].swapchain) { 252 SDL_DestroyGPUXRSwapchain(gpu_device, vr_swapchains[i].swapchain, vr_swapchains[i].images); 253 } 254 } 255 SDL_free(vr_swapchains); 256 vr_swapchains = NULL; 257 } 258 259 if (xr_views) { 260 SDL_free(xr_views); 261 xr_views = NULL; 262 } 263 264 /* Destroy OpenXR resources */ 265 if (xr_local_space && pfn_xrDestroySpace) { 266 pfn_xrDestroySpace(xr_local_space); 267 xr_local_space = XR_NULL_HANDLE; 268 } 269 if (xr_session && pfn_xrDestroySession) { 270 pfn_xrDestroySession(xr_session); 271 xr_session = XR_NULL_HANDLE; 272 } 273 274 /* Destroy GPU device (this also handles XR instance cleanup) */ 275 if (gpu_device) { 276 SDL_DestroyGPUDevice(gpu_device); 277 gpu_device = NULL; 278 } 279 280 SDL_Quit(); 281 exit(rc); 282} 283 284/* ======================================================================== 285 * Shader Loading 286 * ======================================================================== */ 287 288static SDL_GPUShader *load_shader(bool is_vertex, Uint32 sampler_count, Uint32 uniform_buffer_count) 289{ 290 SDL_GPUShaderCreateInfo createinfo; 291 createinfo.num_samplers = sampler_count; 292 createinfo.num_storage_buffers = 0; 293 createinfo.num_storage_textures = 0; 294 createinfo.num_uniform_buffers = uniform_buffer_count; 295 296 SDL_GPUShaderFormat format = SDL_GetGPUShaderFormats(gpu_device); 297 if (format & SDL_GPU_SHADERFORMAT_DXIL) { 298 createinfo.format = SDL_GPU_SHADERFORMAT_DXIL; 299 if (is_vertex) { 300 createinfo.code = cube_vert_dxil; 301 createinfo.code_size = cube_vert_dxil_len; 302 createinfo.entrypoint = "main"; 303 } else { 304 createinfo.code = cube_frag_dxil; 305 createinfo.code_size = cube_frag_dxil_len; 306 createinfo.entrypoint = "main"; 307 } 308 } else if (format & SDL_GPU_SHADERFORMAT_SPIRV) { 309 createinfo.format = SDL_GPU_SHADERFORMAT_SPIRV; 310 if (is_vertex) { 311 createinfo.code = cube_vert_spv; 312 createinfo.code_size = cube_vert_spv_len; 313 createinfo.entrypoint = "main"; 314 } else { 315 createinfo.code = cube_frag_spv; 316 createinfo.code_size = cube_frag_spv_len; 317 createinfo.entrypoint = "main"; 318 } 319 } else { 320 SDL_Log("No supported shader format found!"); 321 return NULL; 322 } 323 324 createinfo.stage = is_vertex ? SDL_GPU_SHADERSTAGE_VERTEX : SDL_GPU_SHADERSTAGE_FRAGMENT; 325 createinfo.props = 0; 326 327 return SDL_CreateGPUShader(gpu_device, &createinfo); 328} 329 330/* ======================================================================== 331 * OpenXR Function Loading 332 * ======================================================================== */ 333 334static bool load_xr_functions(void) 335{ 336 pfn_xrGetInstanceProcAddr = (PFN_xrGetInstanceProcAddr)SDL_OpenXR_GetXrGetInstanceProcAddr(); 337 if (!pfn_xrGetInstanceProcAddr) { 338 SDL_Log("Failed to get xrGetInstanceProcAddr"); 339 return false; 340 } 341 342#define XR_LOAD(fn) \ 343 if (XR_FAILED(pfn_xrGetInstanceProcAddr(xr_instance, #fn, (PFN_xrVoidFunction*)&pfn_##fn))) { \ 344 SDL_Log("Failed to load " #fn); \ 345 return false; \ 346 } 347 348 XR_LOAD(xrEnumerateViewConfigurationViews); 349 XR_LOAD(xrEnumerateSwapchainImages); 350 XR_LOAD(xrCreateReferenceSpace); 351 XR_LOAD(xrDestroySpace); 352 XR_LOAD(xrDestroySession); 353 XR_LOAD(xrDestroyInstance); 354 XR_LOAD(xrPollEvent); 355 XR_LOAD(xrBeginSession); 356 XR_LOAD(xrEndSession); 357 XR_LOAD(xrWaitFrame); 358 XR_LOAD(xrBeginFrame); 359 XR_LOAD(xrEndFrame); 360 XR_LOAD(xrLocateViews); 361 XR_LOAD(xrAcquireSwapchainImage); 362 XR_LOAD(xrWaitSwapchainImage); 363 XR_LOAD(xrReleaseSwapchainImage); 364 365#undef XR_LOAD 366 367 SDL_Log("Loaded all XR functions successfully"); 368 return true; 369} 370 371/* ======================================================================== 372 * Pipeline and Buffer Creation 373 * ======================================================================== */ 374 375static bool create_pipeline(SDL_GPUTextureFormat color_format) 376{ 377 SDL_GPUShader *vert_shader = load_shader(true, 0, 1); 378 SDL_GPUShader *frag_shader = load_shader(false, 0, 0); 379 380 if (!vert_shader || !frag_shader) { 381 if (vert_shader) SDL_ReleaseGPUShader(gpu_device, vert_shader); 382 if (frag_shader) SDL_ReleaseGPUShader(gpu_device, frag_shader); 383 return false; 384 } 385 386 SDL_GPUGraphicsPipelineCreateInfo pipeline_info = { 387 .vertex_shader = vert_shader, 388 .fragment_shader = frag_shader, 389 .target_info = { 390 .num_color_targets = 1, 391 .color_target_descriptions = (SDL_GPUColorTargetDescription[]){{ 392 .format = color_format 393 }}, 394 .has_depth_stencil_target = true, 395 .depth_stencil_format = DEPTH_FORMAT 396 }, 397 .depth_stencil_state = { 398 .enable_depth_test = true, 399 .enable_depth_write = true, 400 .compare_op = SDL_GPU_COMPAREOP_LESS_OR_EQUAL 401 }, 402 .rasterizer_state = { 403 .cull_mode = SDL_GPU_CULLMODE_BACK, 404 .front_face = SDL_GPU_FRONTFACE_CLOCKWISE, /* Cube indices wind clockwise when viewed from outside */ 405 .fill_mode = SDL_GPU_FILLMODE_FILL 406 }, 407 .vertex_input_state = { 408 .num_vertex_buffers = 1, 409 .vertex_buffer_descriptions = (SDL_GPUVertexBufferDescription[]){{ 410 .slot = 0, 411 .pitch = sizeof(PositionColorVertex), 412 .input_rate = SDL_GPU_VERTEXINPUTRATE_VERTEX 413 }}, 414 .num_vertex_attributes = 2, 415 .vertex_attributes = (SDL_GPUVertexAttribute[]){{ 416 .location = 0, 417 .buffer_slot = 0, 418 .format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT3, 419 .offset = 0 420 }, { 421 .location = 1, 422 .buffer_slot = 0, 423 .format = SDL_GPU_VERTEXELEMENTFORMAT_UBYTE4_NORM, 424 .offset = sizeof(float) * 3 425 }} 426 }, 427 .primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST 428 }; 429 430 pipeline = SDL_CreateGPUGraphicsPipeline(gpu_device, &pipeline_info); 431 432 SDL_ReleaseGPUShader(gpu_device, vert_shader); 433 SDL_ReleaseGPUShader(gpu_device, frag_shader); 434 435 if (!pipeline) { 436 SDL_Log("Failed to create pipeline: %s", SDL_GetError()); 437 return false; 438 } 439 440 SDL_Log("Created graphics pipeline for format %d", color_format); 441 return true; 442} 443 444static bool create_cube_buffers(void) 445{ 446 float s = CUBE_HALF_SIZE; 447 448 PositionColorVertex vertices[24] = { 449 /* Front face (red) */ 450 {-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}, 451 /* Back face (green) */ 452 {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}, 453 /* Left face (blue) */ 454 {-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}, 455 /* Right face (yellow) */ 456 {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}, 457 /* Top face (magenta) */ 458 {-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}, 459 /* Bottom face (cyan) */ 460 {-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} 461 }; 462 463 Uint16 indices[36] = { 464 0,1,2, 0,2,3, /* Front */ 465 4,5,6, 4,6,7, /* Back */ 466 8,9,10, 8,10,11, /* Left */ 467 12,13,14, 12,14,15, /* Right */ 468 16,17,18, 16,18,19, /* Top */ 469 20,21,22, 20,22,23 /* Bottom */ 470 }; 471 472 SDL_GPUBufferCreateInfo vertex_buf_info = { 473 .usage = SDL_GPU_BUFFERUSAGE_VERTEX, 474 .size = sizeof(vertices) 475 }; 476 vertex_buffer = SDL_CreateGPUBuffer(gpu_device, &vertex_buf_info); 477 CHECK_CREATE(vertex_buffer, "Vertex Buffer"); 478 479 SDL_GPUBufferCreateInfo index_buf_info = { 480 .usage = SDL_GPU_BUFFERUSAGE_INDEX, 481 .size = sizeof(indices) 482 }; 483 index_buffer = SDL_CreateGPUBuffer(gpu_device, &index_buf_info); 484 CHECK_CREATE(index_buffer, "Index Buffer"); 485 486 /* Create transfer buffer and upload data */ 487 SDL_GPUTransferBufferCreateInfo transfer_info = { 488 .usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD, 489 .size = sizeof(vertices) + sizeof(indices) 490 }; 491 SDL_GPUTransferBuffer *transfer = SDL_CreateGPUTransferBuffer(gpu_device, &transfer_info); 492 CHECK_CREATE(transfer, "Transfer Buffer"); 493 494 void *data = SDL_MapGPUTransferBuffer(gpu_device, transfer, false); 495 SDL_memcpy(data, vertices, sizeof(vertices)); 496 SDL_memcpy((Uint8*)data + sizeof(vertices), indices, sizeof(indices)); 497 SDL_UnmapGPUTransferBuffer(gpu_device, transfer); 498 499 SDL_GPUCommandBuffer *cmd = SDL_AcquireGPUCommandBuffer(gpu_device); 500 SDL_GPUCopyPass *copy_pass = SDL_BeginGPUCopyPass(cmd); 501 502 SDL_GPUTransferBufferLocation src_vertex = { .transfer_buffer = transfer, .offset = 0 }; 503 SDL_GPUBufferRegion dst_vertex = { .buffer = vertex_buffer, .offset = 0, .size = sizeof(vertices) }; 504 SDL_UploadToGPUBuffer(copy_pass, &src_vertex, &dst_vertex, false); 505 506 SDL_GPUTransferBufferLocation src_index = { .transfer_buffer = transfer, .offset = sizeof(vertices) }; 507 SDL_GPUBufferRegion dst_index = { .buffer = index_buffer, .offset = 0, .size = sizeof(indices) }; 508 SDL_UploadToGPUBuffer(copy_pass, &src_index, &dst_index, false); 509 510 SDL_EndGPUCopyPass(copy_pass); 511 SDL_SubmitGPUCommandBuffer(cmd); 512 SDL_ReleaseGPUTransferBuffer(gpu_device, transfer); 513 514 SDL_Log("Created cube vertex (%u bytes) and index (%u bytes) buffers", (unsigned int)sizeof(vertices), (unsigned int)sizeof(indices)); 515 return true; 516} 517 518/* ======================================================================== 519 * XR Session Initialization 520 * ======================================================================== */ 521 522static bool init_xr_session(void) 523{ 524 XrResult result; 525 526 /* Create session */ 527 XrSessionCreateInfo session_info = { XR_TYPE_SESSION_CREATE_INFO }; 528 result = SDL_CreateGPUXRSession(gpu_device, &session_info, &xr_session); 529 XR_CHECK(result, "Failed to create XR session"); 530 531 /* Create reference space */ 532 XrReferenceSpaceCreateInfo space_info = { XR_TYPE_REFERENCE_SPACE_CREATE_INFO }; 533 space_info.referenceSpaceType = XR_REFERENCE_SPACE_TYPE_LOCAL; 534 space_info.poseInReferenceSpace.orientation.w = 1.0f; /* Identity quaternion */ 535 536 result = pfn_xrCreateReferenceSpace(xr_session, &space_info, &xr_local_space); 537 XR_CHECK(result, "Failed to create reference space"); 538 539 return true; 540} 541 542static bool create_swapchains(void) 543{ 544 XrResult result; 545 546 /* Get view configuration */ 547 result = pfn_xrEnumerateViewConfigurationViews( 548 xr_instance, xr_system_id, 549 XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO, 550 0, &view_count, NULL); 551 XR_CHECK(result, "Failed to enumerate view config views (count)"); 552 553 SDL_Log("View count: %" SDL_PRIu32, view_count); 554 555 XrViewConfigurationView *view_configs = SDL_calloc(view_count, sizeof(XrViewConfigurationView)); 556 for (Uint32 i = 0; i < view_count; i++) { 557 view_configs[i].type = XR_TYPE_VIEW_CONFIGURATION_VIEW; 558 } 559 560 result = pfn_xrEnumerateViewConfigurationViews( 561 xr_instance, xr_system_id, 562 XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO, 563 view_count, &view_count, view_configs); 564 if (XR_FAILED(result)) { 565 SDL_free(view_configs); 566 SDL_Log("Failed to enumerate view config views"); 567 return false; 568 } 569 570 /* Allocate swapchains and views */ 571 vr_swapchains = SDL_calloc(view_count, sizeof(VRSwapchain)); 572 xr_views = SDL_calloc(view_count, sizeof(XrView)); 573 574 /* Query available swapchain formats 575 * Per PR #14837: format arrays are terminated with SDL_GPU_TEXTUREFORMAT_INVALID */ 576 int num_formats = 0; 577 SDL_GPUTextureFormat *formats = SDL_GetGPUXRSwapchainFormats(gpu_device, xr_session, &num_formats); 578 if (!formats || num_formats == 0) { 579 SDL_Log("Failed to get XR swapchain formats"); 580 SDL_free(view_configs); 581 return false; 582 } 583 584 /* Use first available format (typically sRGB) 585 * Note: Could iterate with: while (formats[i] != SDL_GPU_TEXTUREFORMAT_INVALID) */ 586 SDL_GPUTextureFormat swapchain_format = formats[0]; 587 SDL_Log("Using swapchain format: %d (of %d available)", swapchain_format, num_formats); 588 589 /* Log all available formats for debugging */ 590 for (int f = 0; f < num_formats && formats[f] != SDL_GPU_TEXTUREFORMAT_INVALID; f++) { 591 SDL_Log(" Available format [%d]: %d", f, formats[f]); 592 } 593 SDL_free(formats); 594 595 for (Uint32 i = 0; i < view_count; i++) { 596 xr_views[i].type = XR_TYPE_VIEW; 597 xr_views[i].pose.orientation.w = 1.0f; 598 599 SDL_Log("Eye %" SDL_PRIu32 ": recommended %ux%u", i, 600 (unsigned int)view_configs[i].recommendedImageRectWidth, 601 (unsigned int)view_configs[i].recommendedImageRectHeight); 602 603 /* Create swapchain using OpenXR's XrSwapchainCreateInfo */ 604 XrSwapchainCreateInfo swapchain_info = { XR_TYPE_SWAPCHAIN_CREATE_INFO }; 605 swapchain_info.usageFlags = XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT | XR_SWAPCHAIN_USAGE_SAMPLED_BIT; 606 swapchain_info.format = 0; /* Ignored - SDL uses the format parameter */ 607 swapchain_info.sampleCount = 1; 608 swapchain_info.width = view_configs[i].recommendedImageRectWidth; 609 swapchain_info.height = view_configs[i].recommendedImageRectHeight; 610 swapchain_info.faceCount = 1; 611 swapchain_info.arraySize = 1; 612 swapchain_info.mipCount = 1; 613 614 result = SDL_CreateGPUXRSwapchain( 615 gpu_device, 616 xr_session, 617 &swapchain_info, 618 swapchain_format, 619 &vr_swapchains[i].swapchain, 620 &vr_swapchains[i].images); 621 622 vr_swapchains[i].format = swapchain_format; 623 624 if (XR_FAILED(result)) { 625 SDL_Log("Failed to create swapchain %" SDL_PRIu32, i); 626 SDL_free(view_configs); 627 return false; 628 } 629 630 /* Get image count by enumerating swapchain images */ 631 result = pfn_xrEnumerateSwapchainImages(vr_swapchains[i].swapchain, 0, &vr_swapchains[i].image_count, NULL); 632 if (XR_FAILED(result)) { 633 vr_swapchains[i].image_count = 3; /* Assume 3 if we can't query */ 634 } 635 636 vr_swapchains[i].size.width = (int32_t)swapchain_info.width; 637 vr_swapchains[i].size.height = (int32_t)swapchain_info.height; 638 639 /* Create local depth texture for this eye 640 * Per PR #14837: Depth buffers are "really recommended" for XR apps. 641 * Using a local depth texture (not XR-managed) is the simplest approach 642 * for proper z-ordering without requiring XR_KHR_composition_layer_depth. */ 643 SDL_GPUTextureCreateInfo depth_info = { 644 .type = SDL_GPU_TEXTURETYPE_2D, 645 .format = DEPTH_FORMAT, 646 .width = swapchain_info.width, 647 .height = swapchain_info.height, 648 .layer_count_or_depth = 1, 649 .num_levels = 1, 650 .sample_count = SDL_GPU_SAMPLECOUNT_1, 651 .usage = SDL_GPU_TEXTUREUSAGE_DEPTH_STENCIL_TARGET, 652 .props = 0 653 }; 654 vr_swapchains[i].depth_texture = SDL_CreateGPUTexture(gpu_device, &depth_info); 655 if (!vr_swapchains[i].depth_texture) { 656 SDL_Log("Failed to create depth texture for eye %" SDL_PRIu32 ": %s", i, SDL_GetError()); 657 SDL_free(view_configs); 658 return false; 659 } 660 661 SDL_Log("Created swapchain %" SDL_PRIu32 ": %" SDL_PRIs32 "x%" SDL_PRIs32 ", %" SDL_PRIu32 " images, with depth buffer", 662 i, vr_swapchains[i].size.width, vr_swapchains[i].size.height, 663 vr_swapchains[i].image_count); 664 } 665 666 SDL_free(view_configs); 667 668 /* Create the pipeline using the swapchain format */ 669 if (view_count > 0 && pipeline == NULL) { 670 if (!create_pipeline(vr_swapchains[0].format)) { 671 return false; 672 } 673 if (!create_cube_buffers()) { 674 return false; 675 } 676 } 677 678 return true; 679} 680 681/* ======================================================================== 682 * XR Event Handling 683 * ======================================================================== */ 684 685static void handle_xr_events(void) 686{ 687 XrEventDataBuffer event_buffer = { XR_TYPE_EVENT_DATA_BUFFER }; 688 689 while (pfn_xrPollEvent(xr_instance, &event_buffer) == XR_SUCCESS) { 690 switch (event_buffer.type) { 691 case XR_TYPE_EVENT_DATA_SESSION_STATE_CHANGED: { 692 XrEventDataSessionStateChanged *state_event = 693 (XrEventDataSessionStateChanged*)&event_buffer; 694 695 SDL_Log("Session state changed: %d", state_event->state); 696 697 switch (state_event->state) { 698 case XR_SESSION_STATE_READY: { 699 XrSessionBeginInfo begin_info = { XR_TYPE_SESSION_BEGIN_INFO }; 700 begin_info.primaryViewConfigurationType = XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO; 701 702 XrResult result = pfn_xrBeginSession(xr_session, &begin_info); 703 if (XR_SUCCEEDED(result)) { 704 SDL_Log("XR Session begun!"); 705 xr_session_running = true; 706 707 /* Create swapchains now that session is ready */ 708 if (!create_swapchains()) { 709 SDL_Log("Failed to create swapchains"); 710 xr_should_quit = true; 711 } 712 } 713 break; 714 } 715 case XR_SESSION_STATE_STOPPING: 716 pfn_xrEndSession(xr_session); 717 xr_session_running = false; 718 break; 719 case XR_SESSION_STATE_EXITING: 720 case XR_SESSION_STATE_LOSS_PENDING: 721 xr_should_quit = true; 722 break; 723 default: 724 break; 725 } 726 break; 727 } 728 case XR_TYPE_EVENT_DATA_INSTANCE_LOSS_PENDING: 729 xr_should_quit = true; 730 break; 731 default: 732 break; 733 } 734 735 event_buffer.type = XR_TYPE_EVENT_DATA_BUFFER; 736 } 737} 738 739/* ======================================================================== 740 * Rendering 741 * ======================================================================== */ 742 743static void render_frame(void) 744{ 745 if (!xr_session_running) return; 746 747 XrFrameState frame_state = { XR_TYPE_FRAME_STATE }; 748 XrFrameWaitInfo wait_info = { XR_TYPE_FRAME_WAIT_INFO }; 749 750 XrResult result = pfn_xrWaitFrame(xr_session, &wait_info, &frame_state); 751 if (XR_FAILED(result)) return; 752 753 XrFrameBeginInfo begin_info = { XR_TYPE_FRAME_BEGIN_INFO }; 754 result = pfn_xrBeginFrame(xr_session, &begin_info); 755 if (XR_FAILED(result)) return; 756 757 XrCompositionLayerProjectionView *proj_views = NULL; 758 XrCompositionLayerProjection layer = { XR_TYPE_COMPOSITION_LAYER_PROJECTION }; 759 Uint32 layer_count = 0; 760 const XrCompositionLayerBaseHeader *layers[1] = {0}; 761 762 if (frame_state.shouldRender && view_count > 0 && vr_swapchains != NULL) { 763 /* Update animation time */ 764 Uint64 now = SDL_GetTicks(); 765 if (last_ticks == 0) last_ticks = now; 766 float delta = (float)(now - last_ticks) / 1000.0f; 767 last_ticks = now; 768 anim_time += delta; 769 770 /* Locate views */ 771 XrViewState view_state = { XR_TYPE_VIEW_STATE }; 772 XrViewLocateInfo locate_info = { XR_TYPE_VIEW_LOCATE_INFO }; 773 locate_info.viewConfigurationType = XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO; 774 locate_info.displayTime = frame_state.predictedDisplayTime; 775 locate_info.space = xr_local_space; 776 777 Uint32 view_count_output; 778 result = pfn_xrLocateViews(xr_session, &locate_info, &view_state, view_count, &view_count_output, xr_views); 779 if (XR_FAILED(result)) { 780 SDL_Log("xrLocateViews failed"); 781 goto endFrame; 782 } 783 784 proj_views = SDL_calloc(view_count, sizeof(XrCompositionLayerProjectionView)); 785 786 SDL_GPUCommandBuffer *cmd_buf = SDL_AcquireGPUCommandBuffer(gpu_device); 787 788 /* Multi-pass stereo: render each eye separately */ 789 for (Uint32 i = 0; i < view_count; i++) { 790 VRSwapchain *swapchain = &vr_swapchains[i]; 791 792 /* Acquire swapchain image */ 793 Uint32 image_index; 794 XrSwapchainImageAcquireInfo acquire_info = { XR_TYPE_SWAPCHAIN_IMAGE_ACQUIRE_INFO }; 795 result = pfn_xrAcquireSwapchainImage(swapchain->swapchain, &acquire_info, &image_index); 796 if (XR_FAILED(result)) continue; 797 798 XrSwapchainImageWaitInfo wait_image_info = { XR_TYPE_SWAPCHAIN_IMAGE_WAIT_INFO }; 799 wait_image_info.timeout = XR_INFINITE_DURATION; 800 result = pfn_xrWaitSwapchainImage(swapchain->swapchain, &wait_image_info); 801 if (XR_FAILED(result)) { 802 XrSwapchainImageReleaseInfo release_info = { XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO }; 803 pfn_xrReleaseSwapchainImage(swapchain->swapchain, &release_info); 804 continue; 805 } 806 807 /* Render the scene to this eye */ 808 SDL_GPUTexture *target_texture = swapchain->images[image_index]; 809 810 /* Build view and projection matrices from XR pose/fov */ 811 Mat4 view_matrix = Mat4_FromXrPose(xr_views[i].pose); 812 Mat4 proj_matrix = Mat4_Projection(xr_views[i].fov, 0.05f, 100.0f); 813 814 SDL_GPUColorTargetInfo color_target = {0}; 815 color_target.texture = target_texture; 816 color_target.load_op = SDL_GPU_LOADOP_CLEAR; 817 color_target.store_op = SDL_GPU_STOREOP_STORE; 818 /* Dark blue background */ 819 color_target.clear_color.r = 0.05f; 820 color_target.clear_color.g = 0.05f; 821 color_target.clear_color.b = 0.15f; 822 color_target.clear_color.a = 1.0f; 823 824 /* Set up depth target for proper z-ordering */ 825 SDL_GPUDepthStencilTargetInfo depth_target = {0}; 826 depth_target.texture = swapchain->depth_texture; 827 depth_target.clear_depth = 1.0f; /* Far plane */ 828 depth_target.load_op = SDL_GPU_LOADOP_CLEAR; 829 depth_target.store_op = SDL_GPU_STOREOP_DONT_CARE; /* We don't need to preserve depth */ 830 depth_target.stencil_load_op = SDL_GPU_LOADOP_DONT_CARE; 831 depth_target.stencil_store_op = SDL_GPU_STOREOP_DONT_CARE; 832 depth_target.cycle = true; /* Allow GPU to cycle the texture for efficiency */ 833 834 SDL_GPURenderPass *render_pass = SDL_BeginGPURenderPass(cmd_buf, &color_target, 1, &depth_target); 835 836 if (pipeline && vertex_buffer && index_buffer) { 837 SDL_BindGPUGraphicsPipeline(render_pass, pipeline); 838 839 SDL_GPUViewport viewport = {0, 0, (float)swapchain->size.width, (float)swapchain->size.height, 0, 1}; 840 SDL_SetGPUViewport(render_pass, &viewport); 841 842 SDL_Rect scissor = {0, 0, swapchain->size.width, swapchain->size.height}; 843 SDL_SetGPUScissor(render_pass, &scissor); 844 845 SDL_GPUBufferBinding vertex_binding = {vertex_buffer, 0}; 846 SDL_BindGPUVertexBuffers(render_pass, 0, &vertex_binding, 1); 847 848 SDL_GPUBufferBinding index_binding = {index_buffer, 0}; 849 SDL_BindGPUIndexBuffer(render_pass, &index_binding, SDL_GPU_INDEXELEMENTSIZE_16BIT); 850 851 /* Draw each cube */ 852 for (int cube_idx = 0; cube_idx < NUM_CUBES; cube_idx++) { 853 float rot = anim_time * cube_speeds[cube_idx]; 854 Vec3 pos = cube_positions[cube_idx]; 855 856 /* Build model matrix: scale -> rotateY -> rotateX -> translate */ 857 Mat4 scale = Mat4_Scale(cube_scales[cube_idx]); 858 Mat4 rotY = Mat4_RotationY(rot); 859 Mat4 rotX = Mat4_RotationX(rot * 0.7f); 860 Mat4 trans = Mat4_Translation(pos.x, pos.y, pos.z); 861 862 Mat4 model = Mat4_Multiply(Mat4_Multiply(Mat4_Multiply(scale, rotY), rotX), trans); 863 Mat4 mv = Mat4_Multiply(model, view_matrix); 864 Mat4 mvp = Mat4_Multiply(mv, proj_matrix); 865 866 SDL_PushGPUVertexUniformData(cmd_buf, 0, &mvp, sizeof(mvp)); 867 SDL_DrawGPUIndexedPrimitives(render_pass, 36, 1, 0, 0, 0); 868 } 869 } 870 871 SDL_EndGPURenderPass(render_pass); 872 873 /* Release swapchain image */ 874 XrSwapchainImageReleaseInfo release_info = { XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO }; 875 pfn_xrReleaseSwapchainImage(swapchain->swapchain, &release_info); 876 877 /* Set up projection view */ 878 proj_views[i].type = XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW; 879 proj_views[i].pose = xr_views[i].pose; 880 proj_views[i].fov = xr_views[i].fov; 881 proj_views[i].subImage.swapchain = swapchain->swapchain; 882 proj_views[i].subImage.imageRect.offset.x = 0; 883 proj_views[i].subImage.imageRect.offset.y = 0; 884 proj_views[i].subImage.imageRect.extent = swapchain->size; 885 proj_views[i].subImage.imageArrayIndex = 0; 886 } 887 888 SDL_SubmitGPUCommandBuffer(cmd_buf); 889 890 layer.space = xr_local_space; 891 layer.viewCount = view_count; 892 layer.views = proj_views; 893 layers[0] = (XrCompositionLayerBaseHeader*)&layer; 894 layer_count = 1; 895 } 896 897endFrame:; 898 XrFrameEndInfo end_info = { XR_TYPE_FRAME_END_INFO }; 899 end_info.displayTime = frame_state.predictedDisplayTime; 900 end_info.environmentBlendMode = XR_ENVIRONMENT_BLEND_MODE_OPAQUE; 901 end_info.layerCount = layer_count; 902 end_info.layers = layers; 903 904 pfn_xrEndFrame(xr_session, &end_info); 905 906 if (proj_views) SDL_free(proj_views); 907} 908 909/* ======================================================================== 910 * Main 911 * ======================================================================== */ 912 913int main(int argc, char *argv[]) 914{ 915 (void)argc; 916 (void)argv; 917 918 SDL_Log("SDL GPU OpenXR Spinning Cubes Test starting..."); 919 SDL_Log("Stereo rendering mode: Multi-pass (one render pass per eye)"); 920 921 if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS)) { 922 SDL_Log("SDL_Init failed: %s", SDL_GetError()); 923 return 1; 924 } 925 926 SDL_Log("SDL initialized"); 927 928 /* Create GPU device with OpenXR enabled */ 929 SDL_Log("Creating GPU device with OpenXR enabled..."); 930 931 SDL_PropertiesID props = SDL_CreateProperties(); 932 SDL_SetBooleanProperty(props, SDL_PROP_GPU_DEVICE_CREATE_SHADERS_SPIRV_BOOLEAN, true); 933 SDL_SetBooleanProperty(props, SDL_PROP_GPU_DEVICE_CREATE_SHADERS_DXIL_BOOLEAN, true); 934 SDL_SetBooleanProperty(props, SDL_PROP_GPU_DEVICE_CREATE_DEBUGMODE_BOOLEAN, true); 935 /* Enable XR - SDL will create the OpenXR instance for us */ 936 SDL_SetBooleanProperty(props, SDL_PROP_GPU_DEVICE_CREATE_XR_ENABLE_BOOLEAN, true); 937 SDL_SetPointerProperty(props, SDL_PROP_GPU_DEVICE_CREATE_XR_INSTANCE_POINTER, &xr_instance); 938 SDL_SetPointerProperty(props, SDL_PROP_GPU_DEVICE_CREATE_XR_SYSTEM_ID_POINTER, &xr_system_id); 939 SDL_SetStringProperty(props, SDL_PROP_GPU_DEVICE_CREATE_XR_APPLICATION_NAME_STRING, "SDL XR Spinning Cubes Test"); 940 SDL_SetNumberProperty(props, SDL_PROP_GPU_DEVICE_CREATE_XR_APPLICATION_VERSION_NUMBER, 1); 941 942 gpu_device = SDL_CreateGPUDeviceWithProperties(props); 943 SDL_DestroyProperties(props); 944 945 if (!gpu_device) { 946 SDL_Log("Failed to create GPU device: %s", SDL_GetError()); 947 SDL_Quit(); 948 return 1; 949 } 950 951 /* Load OpenXR function pointers */ 952 if (!load_xr_functions()) { 953 SDL_Log("Failed to load XR functions"); 954 quit(1); 955 } 956 957 /* Initialize XR session */ 958 if (!init_xr_session()) { 959 SDL_Log("Failed to init XR session"); 960 quit(1); 961 } 962 963 SDL_Log("Entering main loop... Put on your VR headset!"); 964 965 /* Main loop */ 966 while (!xr_should_quit) { 967 SDL_Event event; 968 while (SDL_PollEvent(&event)) { 969 if (event.type == SDL_EVENT_QUIT) { 970 xr_should_quit = true; 971 } 972 if (event.type == SDL_EVENT_KEY_DOWN && event.key.key == SDLK_ESCAPE) { 973 xr_should_quit = true; 974 } 975 } 976 977 handle_xr_events(); 978 render_frame(); 979 } 980 981 quit(0); 982 return 0; 983} 984[FILE END](C) 2025 0x4248 (C) 2025 4248 Media and 4248 Systems, All part of 0x4248 See LICENCE files for more information. Not all files are by 0x4248 always check Licencing.