testgpu_spinning_cube_xr.c 37 KB

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