stereokit_rust/tools/
xr_comp_layers.rs

1//! XR_KHR_android_surface_swapchain extension implementation
2//!
3//! **This is a rust adaptation of <https://github.com/StereoKit/StereoKit/blob/develop/Examples/StereoKitTest/Tools/XrCompLayers.cs>**
4//!
5//! <https://registry.khronos.org/OpenXR/specs/1.1/html/xrspec.html#XR_KHR_android_surface_swapchain>
6
7use crate::{
8    maths::{Pose, Quat, Rect, Vec2},
9    prelude::*,
10    system::{Backend, BackendGraphics, BackendOpenXR, BackendXRType},
11    tex::{Tex, TexFormat, TexType},
12};
13#[cfg(target_os = "android")]
14use openxr_sys::{pfn::CreateSwapchainAndroidSurfaceKHR, platform::jobject};
15
16use openxr_sys::{
17    Duration, Session, Space, Swapchain, SwapchainImageWaitInfo,
18    pfn::{
19        AcquireSwapchainImage, CreateSwapchain, DestroySwapchain, EnumerateSwapchainImages, ReleaseSwapchainImage,
20        WaitSwapchainImage,
21    },
22};
23
24use openxr_sys::{
25    CompositionLayerFlags, CompositionLayerQuad, Extent2Df, Extent2Di, EyeVisibility, Offset2Di, Posef, Quaternionf,
26    Rect2Di, Result as XrResult, StructureType, SwapchainCreateFlags, SwapchainCreateInfo, SwapchainSubImage,
27    SwapchainUsageFlags, Vector3f,
28};
29
30use std::ptr::null_mut;
31
32#[derive(Debug)]
33
34/// `XrCompLayers` provides low-level OpenXR composition layer functionality, while `SwapchainSk`
35/// offers a high-level wrapper for creating and managing OpenXR swapchains with StereoKit integration.
36///
37/// ### Examples
38/// ## Basic Usage with SwapchainSk
39/// ```
40/// # stereokit_rust::test_init_sk!(); // !!!! Get a proper way to initialize sk !!!!
41/// use stereokit_rust::{ maths::{Vec3, Matrix, Pose, Vec2, Rect},  render_list::RenderList,
42///     util::{named_colors, Color128, Time}, tex::TexFormat, material::Material, mesh::Mesh,
43///     system::{Backend, BackendXRType, RenderClear}, tools::xr_comp_layers::* };
44///
45/// use openxr_sys::SwapchainUsageFlags;
46///
47/// // Check if OpenXR is available
48/// if Backend::xr_type() == BackendXRType::OpenXR {
49///     // Create XrCompLayers instance
50///     if let Some(mut swapchain) = SwapchainSk::new(
51///             TexFormat::RGBA32, 512, 512, None ){
52///         
53///         // Set up rendering components
54///         let mut render_list = RenderList::new();
55///         let mut material = Material::default().copy();
56///         let projection = Matrix::orthographic(0.2, 0.2, 0.01, 10.0);
57///         
58///         // Add a sphere to the scene
59///         render_list.add_mesh( Mesh::sphere(), material, Matrix::s(0.05 * Vec3::ONE),
60///                               named_colors::WHITE, None);
61///         
62///         // Render loop example
63///         test_steps!( // !!!! Get a proper main loop !!!!
64///             // Acquire the next swapchain image
65///             if let Ok(_image_index) = swapchain.acquire_image(None) {
66///                 // Get the render target texture
67///                 if let Some(render_tex) = swapchain.get_render_target() {
68///                     // Render to the swapchain texture
69///                     render_list.draw_now(
70///                         render_tex,
71///                         Matrix::look_at(Vec3::angle_xy(Time::get_totalf() * 90.0, 0.0), Vec3::ZERO, None),
72///                         projection,
73///                         Some(Color128::new(0.4, 0.3, 0.2, 1.0)),
74///                         Some(RenderClear::Color),
75///                         Rect::new(0.0, 0.0, 1.0, 1.0),
76///                         None,
77///                     );
78///                 }
79///                 
80///                 // Release the image back to the swapchain
81///                 swapchain.release_image().expect("Failed to release image");
82///                 
83///                 // Submit the quad layer to OpenXR
84///                 XrCompLayers::submit_quad_layer(
85///                     Pose::new(Vec3::new(0.0, 1.5, -1.0), None), // World position
86///                     Vec2::new(0.3, 0.3),                        // Quad size
87///                     swapchain.handle,                           // Swapchain handle
88///                     Rect::new(0.0, 0.0, 512.0, 512.0),          // Texture rectangle
89///                     0,                                          // Array index
90///                     1,                                          // Sort order
91///                     None,                                       // Eye visibility
92///                     None,                                       // XR space
93///                 );
94///             }
95///         );
96///         
97///         // Clean up
98///         swapchain.destroy();
99///     }
100/// }
101/// ```
102///
103pub struct XrCompLayers {
104    // OpenXR function pointers
105    #[cfg(target_os = "android")]
106    xr_create_swapchain_android: Option<CreateSwapchainAndroidSurfaceKHR>,
107    xr_create_swapchain: Option<CreateSwapchain>,
108    xr_destroy_swapchain: Option<DestroySwapchain>,
109    xr_enumerate_swaptchain_images: Option<EnumerateSwapchainImages>,
110    xr_acquire_swapchain_image: Option<AcquireSwapchainImage>,
111    xr_wait_swaptchain_image: Option<WaitSwapchainImage>,
112    xr_release_swaptchain_image: Option<ReleaseSwapchainImage>,
113}
114
115impl Default for XrCompLayers {
116    fn default() -> Self {
117        Self {
118            #[cfg(target_os = "android")]
119            xr_create_swapchain_android: BackendOpenXR::get_function::<CreateSwapchainAndroidSurfaceKHR>(
120                "xrCreateSwapchainAndroidSurfaceKHR",
121            ),
122            //#[cfg(not(target_os = "android"))]
123            xr_create_swapchain: BackendOpenXR::get_function::<CreateSwapchain>("xrCreateSwapchain"),
124
125            xr_destroy_swapchain: BackendOpenXR::get_function::<DestroySwapchain>("xrDestroySwapchain"),
126            xr_enumerate_swaptchain_images: BackendOpenXR::get_function::<EnumerateSwapchainImages>(
127                "xrEnumerateSwapchainImages",
128            ),
129            xr_acquire_swapchain_image: BackendOpenXR::get_function::<AcquireSwapchainImage>("xrAcquireSwapchainImage"),
130            xr_wait_swaptchain_image: BackendOpenXR::get_function::<WaitSwapchainImage>("xrWaitSwapchainImage"),
131            xr_release_swaptchain_image: BackendOpenXR::get_function::<ReleaseSwapchainImage>(
132                "xrReleaseSwapchainImage",
133            ),
134        }
135    }
136}
137
138impl XrCompLayers {
139    /// Initializes the composition layers helper by loading required OpenXR bindings.
140    /// Returns `Some(Self)` if the XR runtime type is OpenXR and all bindings are present.
141    pub fn new() -> Option<Self> {
142        let this = Self::default();
143
144        #[cfg(target_os = "android")]
145        {
146            if !BackendOpenXR::ext_enabled("XR_KHR_android_surface_swapchain") {
147                Log::warn(
148                    "XrCompLayers: XR_KHR_android_surface_swapchain extension is not enabled. You have to enable it before sk::init.",
149                );
150                return None;
151            }
152        }
153
154        if !(Backend::xr_type() == BackendXRType::OpenXR && this.load_bindings()) {
155            Log::warn(format!("❌ XrCompLayers: some function bindings are missing : {this:?}"));
156            return None;
157        }
158
159        Some(this)
160    }
161
162    /// Checks presence of all required swapchain function bindings.
163    fn load_bindings(&self) -> bool {
164        let mut swapchain_func_present = true;
165        swapchain_func_present &= self.xr_create_swapchain.is_some();
166
167        #[cfg(target_os = "android")]
168        {
169            swapchain_func_present &= self.xr_create_swapchain_android.is_some();
170        }
171
172        swapchain_func_present
173            && self.xr_destroy_swapchain.is_some()
174            && self.xr_enumerate_swaptchain_images.is_some()
175            && self.xr_acquire_swapchain_image.is_some()
176            && self.xr_wait_swaptchain_image.is_some()
177            && self.xr_release_swaptchain_image.is_some()
178    }
179
180    /// Convert a StereoKit `TexFormat` into the corresponding native OpenXR format value.
181    pub fn to_native_format(format: TexFormat) -> i64 {
182        match Backend::graphics() {
183            BackendGraphics::D3D11 => match format {
184                TexFormat::RGBA32 => 29,
185                TexFormat::RGBA32Linear => 28,
186                TexFormat::BGRA32 => 91,
187                TexFormat::BGRA32Linear => 87,
188                TexFormat::RGB10A2 => 24,
189                TexFormat::RG11B10 => 26,
190                _ => panic!("Unsupported texture format"),
191            },
192            BackendGraphics::OpenGLESEGL => match format {
193                TexFormat::RGBA32 => 0x8C43,
194                TexFormat::RGBA32Linear => 0x8058,
195                TexFormat::RGB10A2 => 0x8059,
196                TexFormat::RG11B10 => 0x8C3A,
197                _ => panic!("Unsupported texture format"),
198            },
199            _ => panic!("Unsupported graphics backend"),
200        }
201    }
202
203    /// Submit a quad layer to the OpenXR composition.
204    ///
205    /// This method submits a rendered quad layer to the OpenXR compositor, which will
206    /// be displayed in the XR environment. The quad appears as a 2D surface in 3D space.
207    ///
208    /// # Parameters
209    /// - `world_pose`: Pose of the quad in world space (position and orientation).
210    /// - `size`: Dimensions of the quad in meters.
211    /// - `swapchain`: Swapchain handle to sample the texture from.
212    /// - `swapchain_rect`: Texture rectangle within the swapchain image (in pixel coordinates).
213    /// - `swapchain_array_index`: Array slice index for texture arrays (usually 0).
214    /// - `composition_sort_order`: Ordering for layer submission (higher values render on top).
215    /// - `visibility`: Optional eye visibility mask (None means both eyes).
216    /// - `xr_space`: Optional XR space handle (None uses default space).
217    ///
218    #[allow(clippy::too_many_arguments)]
219    pub fn submit_quad_layer(
220        world_pose: Pose,
221        size: Vec2,
222        swapchain: Swapchain,
223        swapchain_rect: Rect,
224        swapchain_array_index: u32,
225        composition_sort_order: i32,
226        visibility: Option<EyeVisibility>,
227        xr_space: Option<u64>,
228    ) {
229        let orientation = (world_pose.orientation * Quat::from_angles(180.0, 0.0, 0.0)).conjugate();
230        let xr_space = xr_space.unwrap_or_else(BackendOpenXR::space);
231        let mut quad_layer = CompositionLayerQuad {
232            ty: StructureType::COMPOSITION_LAYER_QUAD,
233            next: null_mut(),
234            layer_flags: CompositionLayerFlags::BLEND_TEXTURE_SOURCE_ALPHA,
235            space: Space::from_raw(xr_space),
236            eye_visibility: visibility.unwrap_or(EyeVisibility::BOTH),
237            sub_image: SwapchainSubImage {
238                swapchain,
239                image_rect: Rect2Di {
240                    offset: Offset2Di { x: swapchain_rect.x as i32, y: swapchain_rect.y as i32 },
241                    extent: Extent2Di { width: swapchain_rect.width as i32, height: swapchain_rect.height as i32 },
242                },
243                image_array_index: swapchain_array_index,
244            },
245            pose: Posef {
246                orientation: Quaternionf { x: orientation.x, y: orientation.y, z: orientation.z, w: orientation.w },
247                position: Vector3f { x: world_pose.position.x, y: world_pose.position.y, z: world_pose.position.z },
248            },
249            size: Extent2Df { width: size.x, height: size.y },
250        };
251
252        BackendOpenXR::add_composition_layer(&mut quad_layer, composition_sort_order);
253    }
254
255    /// Create an Android surface swapchain via `XR_KHR_android_surface_swapchain`.
256    /// Returns the swapchain handle and raw `jobject` pointer on success.
257    /// /// ## Android Surface Swapchain (Android only)
258    ///
259    /// ```no_run
260    /// # #[cfg(target_os = "android")]
261    /// # {
262    /// use stereokit_rust::tools::xr_comp_layers::XrCompLayers;
263    /// use openxr_sys::SwapchainUsageFlags;
264    ///
265    /// if let Some(comp_layers) = XrCompLayers::new() {
266    ///     if let Some((swapchain_handle, android_surface)) = comp_layers.try_make_android_swapchain(
267    ///         512, 512, SwapchainUsageFlags::COLOR_ATTACHMENT, false) {
268    ///
269    ///         println!("Created Android surface swapchain: {:?}", android_surface);
270    ///         
271    ///         // Use the surface
272    ///         
273    ///         // Clean up
274    ///         comp_layers.destroy_android_swapchain(swapchain_handle);
275    ///     }
276    /// }
277    /// # }
278    #[cfg(target_os = "android")]
279    pub fn try_make_android_swapchain(
280        &self,
281        width: u32,
282        height: u32,
283        usage: SwapchainUsageFlags,
284        single_image: bool,
285    ) -> Option<(Swapchain, *mut jobject)> {
286        use openxr_sys::platform::jobject;
287
288        let mut swapchain = Swapchain::default();
289        let mut surface: *mut jobject = null_mut();
290
291        let create_flags = if single_image {
292            SwapchainCreateFlags::STATIC_IMAGE
293        } else {
294            SwapchainCreateFlags::PROTECTED_CONTENT
295        };
296
297        let info = SwapchainCreateInfo {
298            ty: StructureType::SWAPCHAIN_CREATE_INFO,
299            next: null_mut(),
300            create_flags: create_flags,
301            usage_flags: usage,
302            format: 0,       // Required by spec to be zero for Android surface swapchains
303            sample_count: 0, // Required by spec to be zero for Android surface swapchains
304            width,
305            height,
306            face_count: 0, // Required by spec to be zero for Android surface swapchains
307            array_size: 0, // Required by spec to be zero for Android surface swapchains
308            mip_count: 0,  // Required by spec to be zero for Android surface swapchains
309        };
310
311        if let Some(func) = self.xr_create_swapchain_android {
312            let res = unsafe {
313                func(
314                    Session::from_raw(BackendOpenXR::session()),
315                    &info,
316                    &mut swapchain,
317                    &mut surface as *mut _ as *mut *mut std::ffi::c_void,
318                )
319            };
320            match res {
321                XrResult::SUCCESS => Some((swapchain, surface)),
322                otherwise => {
323                    Log::err(format!("❌ xrDestroySwapchain failed: {otherwise}"));
324                    None
325                }
326            }
327        } else {
328            None
329        }
330    }
331
332    /// Destroy the given Android swapchain handle.
333    #[cfg(target_os = "android")]
334    pub fn destroy_android_swapchain(&self, handle: Swapchain) {
335        match unsafe { self.xr_destroy_swapchain.unwrap()(handle) } {
336            XrResult::SUCCESS => {}
337            otherwise => {
338                Log::err(format!("❌ xrDestroySwapchain failed: {otherwise}"));
339            }
340        }
341    }
342
343    /// Create a standard XR swapchain with the given parameters.
344    pub fn try_make_swapchain(
345        &self,
346        width: u32,
347        height: u32,
348        format: TexFormat,
349        usage: SwapchainUsageFlags,
350        single_image: bool,
351    ) -> Option<Swapchain> {
352        let mut swapchain = Swapchain::default();
353        let create_flags = if single_image {
354            SwapchainCreateFlags::STATIC_IMAGE
355        } else {
356            SwapchainCreateFlags::PROTECTED_CONTENT
357        };
358
359        let info = SwapchainCreateInfo {
360            ty: StructureType::SWAPCHAIN_CREATE_INFO,
361            next: null_mut(),
362            format: Self::to_native_format(format),
363            create_flags,
364            usage_flags: usage,
365            sample_count: 1,
366            width,
367            height,
368            face_count: 1,
369            array_size: 1,
370            mip_count: 1,
371        };
372
373        match unsafe {
374            self.xr_create_swapchain.unwrap()(Session::from_raw(BackendOpenXR::session()), &info, &mut swapchain)
375        } {
376            XrResult::SUCCESS => {}
377            otherwise => {
378                Log::err(format!("❌ xrCreateSwapchain failed: {otherwise}"));
379                return None;
380            }
381        }
382        Some(swapchain)
383    }
384
385    /// Destroy the given XR swapchain handle.
386    pub fn destroy_swapchain(swapchain: Swapchain) {
387        // We need to get the function pointer directly
388        let xr_destroy_swapchain = BackendOpenXR::get_function::<DestroySwapchain>("xrDestroySwapchain");
389
390        if let Some(func) = xr_destroy_swapchain {
391            unsafe {
392                func(swapchain);
393            }
394        }
395    }
396}
397
398/// High-level wrapper around an OpenXR swapchain.
399/// Manages creation of render-target textures, image acquisition and release.
400///
401/// `SwapchainSk` provides a convenient interface for working with OpenXR swapchains
402/// in StereoKit applications. It handles the complexity of swapchain image management
403/// and provides StereoKit `Tex` objects for rendering.
404///
405/// ### Examples
406/// ```
407/// # stereokit_rust::test_init_sk!(); // !!!! Get a proper way to initialize sk !!!!
408/// use stereokit_rust::{ maths::{Vec3, Matrix, Pose, Vec2, Rect},  render_list::RenderList,
409///     util::{named_colors, Color128, Time}, tex::TexFormat, material::Material, mesh::Mesh,
410///     system::{Backend, BackendXRType, RenderClear}, tools::xr_comp_layers::* };
411///
412/// // Create a swapchain
413/// if let Some(mut swapchain) = SwapchainSk::new(TexFormat::RGBA32, 512, 512, None) {
414///    
415///     // Set up rendering
416///     let mut render_list = RenderList::new();
417///     let material = Material::default().copy();
418///     render_list.add_mesh(
419///         Mesh::cube(),
420///         material,
421///         Matrix::IDENTITY,
422///         named_colors::RED,
423///         None
424///     );
425///    
426///     // Render to swapchain
427///     if let Ok(_) = swapchain.acquire_image(None) {
428///         if let Some(render_target) = swapchain.get_render_target() {
429///             render_list.draw_now(
430///                 render_target,
431///                 Matrix::look_at(Vec3::angle_xy(Time::get_totalf() * 90.0, 0.0), Vec3::ZERO, None),
432///                 Matrix::orthographic(1.0, 1.0, 0.1, 10.0),
433///                 None,
434///                 Some(RenderClear::All),
435///                 Rect::new(0.0, 0.0, 1.0, 1.0),
436///                 None,
437///             );
438///         }
439///         swapchain.release_image().expect("Failed to release image");
440///     }
441///    
442///     // Clean up
443///     swapchain.destroy();
444/// }
445/// ```
446pub struct SwapchainSk {
447    pub xr_comp_layers: XrCompLayers,
448    pub handle: Swapchain,
449    pub width: u32,
450    pub height: u32,
451    pub acquired: u32,
452    images: Vec<Tex>,
453    #[cfg(unix)]
454    gles_images: Vec<openxr_sys::SwapchainImageOpenGLESKHR>,
455    #[cfg(windows)]
456    d3d_images: Vec<openxr_sys::SwapchainImageD3D11KHR>,
457}
458
459impl SwapchainSk {
460    /// Create a new `SwapchainSk` for rendering into an OpenXR quad layer.
461    /// Returns `Some<Self>` if the XR runtime and swapchain creation succeed.
462    pub fn new(format: TexFormat, width: u32, height: u32, xr_comp_layers: Option<XrCompLayers>) -> Option<Self> {
463        let xr_comp_layers = get_xr_comp_layers(xr_comp_layers)?;
464        if Backend::xr_type() == BackendXRType::OpenXR {
465            if let Some(handle) =
466                xr_comp_layers.try_make_swapchain(width, height, format, SwapchainUsageFlags::COLOR_ATTACHMENT, false)
467            {
468                SwapchainSk::wrap(handle, format, width, height, Some(xr_comp_layers))
469            } else {
470                Log::warn("❌ Failed to create XR swapchain: Try_make_swapchain failed");
471                None
472            }
473        } else {
474            Log::warn("Swapchain: OpenXR backend is not available");
475            None
476        }
477    }
478
479    /// Return a reference to the currently acquired render-target texture, if any.
480    ///
481    /// This method provides access to the StereoKit `Tex` object that represents
482    /// the currently acquired swapchain image. The texture can be used as a render
483    /// target for drawing operations.
484    ///
485    /// # Returns
486    /// - `Some(&Tex)`: Reference to the current render target texture.
487    /// - `None`: No image is currently acquired or swapchain is empty.
488    ///
489    /// # Example
490    /// ```no_run
491    /// # use stereokit_rust::tools::xr_comp_layers::SwapchainSk;
492    /// # let mut swapchain: SwapchainSk = todo!();
493    /// if let Ok(_) = swapchain.acquire_image(None) {
494    ///     if let Some(render_target) = swapchain.get_render_target() {
495    ///         // Use render_target for drawing operations
496    ///         println!("Render target size: {}x{}",
497    ///                  render_target.get_width().unwrap_or(0),
498    ///                  render_target.get_height().unwrap_or(0));
499    ///         
500    ///         // ... perform rendering to render_target ...
501    ///     }
502    ///     swapchain.release_image().expect("Failed to release");
503    /// }
504    /// ```
505    pub fn get_render_target(&self) -> Option<&Tex> {
506        if self.images.is_empty() {
507            return None;
508        }
509        Some(&self.images[self.acquired as usize])
510    }
511
512    /// Wrap OpenGL ES swapchain images into `Tex` objects for `unix` platforms.
513    #[cfg(unix)]
514    pub fn wrap(
515        handle: Swapchain,
516        format: TexFormat,
517        width: u32,
518        height: u32,
519        xr_comp_layers: Option<XrCompLayers>,
520    ) -> Option<Self> {
521        use openxr_sys::SwapchainImageOpenGLESKHR;
522
523        let xr_comp_layers = get_xr_comp_layers(xr_comp_layers)?;
524
525        let mut image_count = 0;
526        match unsafe { xr_comp_layers.xr_enumerate_swaptchain_images.unwrap()(handle, 0, &mut image_count, null_mut()) }
527        {
528            XrResult::SUCCESS => {}
529            otherwise => {
530                Log::err(format!("❌ xrEnumerateSwapchainImages failed: {otherwise}"));
531                return None;
532            }
533        }
534
535        if Backend::graphics() == BackendGraphics::OpenGLESEGL {
536            let mut gles_images: Vec<SwapchainImageOpenGLESKHR> = {
537                let images: Vec<SwapchainImageOpenGLESKHR> = vec![
538                    SwapchainImageOpenGLESKHR {
539                        image: 0,
540                        ty: StructureType::SWAPCHAIN_IMAGE_OPENGL_ES_KHR,
541                        next: null_mut()
542                    };
543                    image_count as usize
544                ];
545                images
546            };
547
548            let mut final_count = 0;
549            match unsafe {
550                xr_comp_layers.xr_enumerate_swaptchain_images.unwrap()(
551                    handle,
552                    image_count,
553                    &mut final_count,
554                    gles_images.as_mut_ptr() as *mut _,
555                )
556            } {
557                XrResult::SUCCESS => {}
558                otherwise => {
559                    Log::err(format!("❌ xrEnumerateSwapchainImages failed: {otherwise}"));
560                    return None;
561                }
562            }
563
564            assert_eq!(gles_images.len(), image_count as usize);
565            //assert_eq!(gles_images.len(), 3);
566
567            let mut this =
568                Self { xr_comp_layers, handle, width, height, acquired: 0, gles_images, images: Vec::with_capacity(0) };
569
570            for image in &this.gles_images {
571                Log::diag(format!("SwapchainSk: image: {image:#?}"));
572                // let mut image_sk =
573                //     Tex::gen_color(named_colors::BLUE_VIOLET, width, height, TexType::Rendertarget, format);
574                //let mut image_sk = Tex::new(TexType::Rendertarget, format, None);
575                let mut image_sk =
576                    Tex::render_target(width as usize, height as usize, Some(2), Some(format), None).unwrap();
577                unsafe {
578                    image_sk.set_native_surface(
579                        image.image as *mut std::ffi::c_void,
580                        TexType::Rendertarget,
581                        XrCompLayers::to_native_format(format),
582                        width as i32,
583                        height as i32,
584                        1,
585                        true,
586                    )
587                };
588                this.images.push(image_sk);
589            }
590            Some(this)
591        } else {
592            Log::warn("❌ SwapchainSk: OpenGL ES backend is not available");
593            None
594        }
595    }
596
597    /// Wrap D3D11 swapchain images into `Tex` objects for `windows` platforms.
598    #[cfg(windows)]
599    pub fn wrap(
600        handle: Swapchain,
601        format: TexFormat,
602        width: u32,
603        height: u32,
604        xr_comp_layers: Option<XrCompLayers>,
605    ) -> Option<Self> {
606        use openxr_sys::SwapchainImageD3D11KHR;
607        use std::ptr::null_mut;
608
609        let xr_comp_layers = get_xr_comp_layers(xr_comp_layers)?;
610
611        // First, get the image count
612        let mut image_count = 0;
613        match unsafe { xr_comp_layers.xr_enumerate_swaptchain_images.unwrap()(handle, 0, &mut image_count, null_mut()) }
614        {
615            XrResult::SUCCESS => {}
616            err => {
617                Log::err(format!("❌ xrEnumerateSwapchainImages failed: {err}"));
618                return None;
619            }
620        }
621
622        // Only proceed for D3D11 backend
623        if Backend::graphics() == BackendGraphics::D3D11 {
624            // Prepare D3D11 image array
625            let mut d3d_images: Vec<SwapchainImageD3D11KHR> = vec![
626                SwapchainImageD3D11KHR {
627                    texture: null_mut(),
628                    ty: StructureType::SWAPCHAIN_IMAGE_D3D11_KHR,
629                    next: null_mut(),
630                };
631                image_count as usize
632            ];
633            let mut final_count = 0;
634            match unsafe {
635                xr_comp_layers.xr_enumerate_swaptchain_images.unwrap()(
636                    handle,
637                    image_count,
638                    &mut final_count,
639                    d3d_images.as_mut_ptr() as *mut _,
640                )
641            } {
642                XrResult::SUCCESS => {}
643                err => {
644                    Log::err(format!("❌ xrEnumerateSwapchainImages failed: {err}"));
645                    return None;
646                }
647            }
648            let mut this =
649                Self { xr_comp_layers, handle, width, height, acquired: 0, d3d_images, images: Vec::with_capacity(0) };
650
651            // Wrap each D3D11 texture into a Tex object
652            for img in &this.d3d_images {
653                Log::diag(format!("SwapchainSk: image: {:#?}", img));
654                // let mut image_sk =
655                //     Tex::gen_color(named_colors::BLUE_VIOLET, width, height, TexType::Rendertarget, format);
656                // let mut image_sk = Tex::new(TexType::Rendertarget, format, None);
657                let mut image_sk =
658                    Tex::render_target(width as usize, height as usize, Some(1), Some(format), None).unwrap();
659
660                unsafe {
661                    image_sk.set_native_surface(
662                        img.texture,
663                        TexType::Rendertarget,
664                        XrCompLayers::to_native_format(format),
665                        width as i32,
666                        height as i32,
667                        1,
668                        true,
669                    );
670                }
671                this.images.push(image_sk);
672            }
673            Some(this)
674        } else {
675            Log::warn("❌ SwapchainSk: D3D11 backend is not available");
676            None
677        }
678    }
679
680    /// Acquire the next image from the swapchain, waiting up to `timeout_ns` nanoseconds.
681    ///
682    /// This method must be called before rendering to the swapchain. It acquires an available
683    /// image from the swapchain and waits for it to be ready for rendering.
684    ///
685    /// # Parameters
686    /// - `timeout_ns`: Optional timeout in nanoseconds. If `None`, waits indefinitely.
687    ///
688    /// # Returns
689    /// - `Ok(image_index)`: The index of the acquired image on success.
690    /// - `Err(XrResult)`: OpenXR error code if acquisition fails.
691    ///
692    /// # Example
693    /// ```no_run
694    /// # use stereokit_rust::tools::xr_comp_layers::SwapchainSk;
695    /// # let mut swapchain: SwapchainSk = todo!();
696    /// // Acquire with default timeout
697    /// match swapchain.acquire_image(None) {
698    ///     Ok(image_index) => {
699    ///         println!("Acquired image {}", image_index);
700    ///         // Render to swapchain.get_render_target()
701    ///         // ... rendering code ...
702    ///         swapchain.release_image().expect("Failed to release");
703    ///     }
704    ///     Err(e) => eprintln!("Failed to acquire image: {:?}", e),
705    /// }
706    /// ```
707    pub fn acquire_image(&mut self, timeout_ns: Option<i64>) -> std::result::Result<u32, XrResult> {
708        let timeout_ns = timeout_ns.unwrap_or(0x7fffffffffffffff);
709        let timeout = Duration::from_nanos(timeout_ns);
710        match unsafe {
711            self.xr_comp_layers.xr_acquire_swapchain_image.unwrap()(self.handle, null_mut(), &mut self.acquired)
712        } {
713            XrResult::SUCCESS => {}
714            otherwise => return Err(otherwise),
715        }
716
717        let swapchain_image_wait_info =
718            SwapchainImageWaitInfo { ty: StructureType::SWAPCHAIN_IMAGE_WAIT_INFO, next: null_mut(), timeout };
719
720        match unsafe { self.xr_comp_layers.xr_wait_swaptchain_image.unwrap()(self.handle, &swapchain_image_wait_info) }
721        {
722            XrResult::SUCCESS => Ok(self.acquired),
723            otherwise => Err(otherwise),
724        }
725    }
726
727    /// Release the currently held image back to the swapchain.
728    ///
729    /// This method must be called after finishing rendering to the acquired image.
730    /// It signals to the OpenXR runtime that rendering is complete and the image
731    /// can be used for composition.
732    ///
733    /// # Returns
734    /// - `Ok(())`: Successfully released the image.
735    /// - `Err(XrResult)`: OpenXR error code if release fails.
736    ///
737    /// # Example
738    /// ```no_run
739    /// # use stereokit_rust::tools::xr_comp_layers::SwapchainSk;
740    /// # let mut swapchain: SwapchainSk = todo!();
741    /// // After acquiring and rendering to the image
742    /// if let Ok(_) = swapchain.acquire_image(None) {
743    ///     // ... render to swapchain.get_render_target() ...
744    ///     
745    ///     // Must release the image when done
746    ///     swapchain.release_image().expect("Failed to release image");
747    /// }
748    /// ```
749    pub fn release_image(&mut self) -> std::result::Result<(), XrResult> {
750        match unsafe { self.xr_comp_layers.xr_release_swaptchain_image.unwrap()(self.handle, null_mut()) } {
751            XrResult::SUCCESS => Ok(()),
752            otherwise => Err(otherwise),
753        }
754    }
755
756    /// Destroy the swapchain and all associated resources.
757    pub fn destroy(&mut self) {
758        XrCompLayers::destroy_swapchain(self.handle);
759        self.handle = Swapchain::default();
760    }
761}
762
763/// Ensure an `XrCompLayers` instance is available, either using the provided one or creating a new one.
764pub fn get_xr_comp_layers(xr_comp_layers: Option<XrCompLayers>) -> Option<XrCompLayers> {
765    if let Some(comp_layers) = xr_comp_layers { Some(comp_layers) } else { XrCompLayers::new() }
766}