ailloli_ui_openxr/openxr_runtime/
swapchain.rs

1//! OpenXR Vulkan swapchain allocation, acquisition, views, and frame targets.
2
3use ailloli_ui_render_vulkan::VulkanFrameTarget;
4use ash::vk::{self, Handle};
5use openxr as xr;
6
7use super::error::OpenXrRuntimeError;
8use super::vulkan::OpenXrVulkanContext;
9
10#[derive(Clone, Copy, PartialEq, Eq)]
11/// One swapchain format represented in Vulkan and raw OpenXR form.
12///
13/// # Examples
14///
15/// ```
16/// use ailloli_ui_openxr::OpenXrSwapchainFormat;
17/// let format = OpenXrSwapchainFormat { vk: ash::vk::Format::R8G8B8A8_UNORM, xr: ash::vk::Format::R8G8B8A8_UNORM.as_raw() as u32 };
18/// assert_eq!(format.xr, 37);
19/// ```
20pub struct OpenXrSwapchainFormat {
21    /// Vulkan format passed to renderer and image-view creation.
22    pub vk: vk::Format,
23    /// Raw format value passed to OpenXR.
24    pub xr: u32,
25}
26
27#[derive(Clone, Copy, PartialEq, Eq)]
28/// One acquired and waited swapchain image with renderer metadata.
29///
30/// A null `view` means the swapchain was created without a Vulkan device and is
31/// suitable only for direct operations that do not require an image view.
32///
33/// # Examples
34///
35/// ```no_run
36/// use ailloli_ui_openxr::OpenXrAcquiredImage;
37/// fn index(image: &OpenXrAcquiredImage) -> u32 { image.image_index }
38/// ```
39pub struct OpenXrAcquiredImage {
40    /// Runtime image index used for bounds diagnostics.
41    pub image_index: u32,
42    /// Borrowed swapchain-owned Vulkan image handle.
43    pub image: vk::Image,
44    /// Owned image-view handle, or null when no device was supplied.
45    pub view: vk::ImageView,
46    /// Selected color format.
47    pub format: OpenXrSwapchainFormat,
48    /// Physical image extent in pixels.
49    pub extent: vk::Extent2D,
50}
51
52/// Single-image-array OpenXR swapchain for a UI composition layer.
53///
54/// The wrapper owns image views only when constructed with a Vulkan device; the
55/// OpenXR runtime owns its images. Acquired images must be released exactly once.
56///
57/// # Examples
58///
59/// ```no_run
60/// use ailloli_ui_openxr::OpenXrQuadSwapchain;
61/// fn extent(swapchain: &OpenXrQuadSwapchain) -> ash::vk::Extent2D { swapchain.extent() }
62/// ```
63pub struct OpenXrQuadSwapchain {
64    /// Vulkan device owning image views, absent after explicit teardown.
65    device: Option<ash::Device>,
66    /// OpenXR swapchain governing image acquire/wait/release order.
67    handle: xr::Swapchain<xr::Vulkan>,
68    /// Runtime-selected OpenXR/Vulkan image format.
69    format: OpenXrSwapchainFormat,
70    /// Runtime-owned Vulkan images in stable swapchain index order.
71    images: Vec<vk::Image>,
72    /// Vulkan image views created for each entry in `images`.
73    views: Vec<vk::ImageView>,
74    /// Physical swapchain width and height in pixels.
75    extent: vk::Extent2D,
76}
77
78impl OpenXrQuadSwapchain {
79    /// Creates a swapchain without Vulkan image views.
80    ///
81    /// Use this path for clear-only composition; [`Self::frame_target`] will
82    /// contain a null view. Width and height must both be nonzero.
83    ///
84    /// # Errors
85    ///
86    /// Returns invalid extent, format enumeration/selection, swapchain creation,
87    /// or image-enumeration failures.
88    ///
89    /// # Examples
90    ///
91    /// ```no_run
92    /// use ailloli_ui_openxr::{OpenXrQuadSwapchain, OpenXrRuntimeError};
93    /// fn create(session: &openxr::Session<openxr::Vulkan>) -> Result<OpenXrQuadSwapchain, OpenXrRuntimeError> { OpenXrQuadSwapchain::new(session, 1024, 576) }
94    /// ```
95    pub fn new(
96        session: &xr::Session<xr::Vulkan>,
97        width: u32,
98        height: u32,
99    ) -> Result<Self, OpenXrRuntimeError> {
100        Self::new_internal(session, None, width, height)
101    }
102
103    /// Creates a swapchain and image views from an owned runtime Vulkan context.
104    ///
105    /// # Errors
106    ///
107    /// Returns the same failures as [`Self::new`], plus image-view failures.
108    ///
109    /// # Examples
110    ///
111    /// ```no_run
112    /// use ailloli_ui_openxr::{OpenXrQuadSwapchain, OpenXrRuntimeError, OpenXrVulkanContext};
113    /// fn create(session: &openxr::Session<openxr::Vulkan>, vk: &OpenXrVulkanContext) -> Result<OpenXrQuadSwapchain, OpenXrRuntimeError> { OpenXrQuadSwapchain::new_with_vulkan_context(session, vk, 1024, 576) }
114    /// ```
115    pub fn new_with_vulkan_context(
116        session: &xr::Session<xr::Vulkan>,
117        vk: &OpenXrVulkanContext,
118        width: u32,
119        height: u32,
120    ) -> Result<Self, OpenXrRuntimeError> {
121        Self::new_internal(session, Some(&vk.vk_device), width, height)
122    }
123
124    /// Creates a swapchain and image views from an externally owned Vulkan device.
125    ///
126    /// The device must be the one backing `session` and must outlive the returned
127    /// wrapper so its image views can be destroyed safely.
128    ///
129    /// # Errors
130    ///
131    /// Returns invalid extent, format, swapchain, enumeration, or view failures.
132    ///
133    /// # Examples
134    ///
135    /// ```no_run
136    /// use ailloli_ui_openxr::{OpenXrQuadSwapchain, OpenXrRuntimeError};
137    /// fn create(session: &openxr::Session<openxr::Vulkan>, device: &ash::Device) -> Result<OpenXrQuadSwapchain, OpenXrRuntimeError> { OpenXrQuadSwapchain::new_with_device(session, device, 800, 600) }
138    /// ```
139    pub fn new_with_device(
140        session: &xr::Session<xr::Vulkan>,
141        device: &ash::Device,
142        width: u32,
143        height: u32,
144    ) -> Result<Self, OpenXrRuntimeError> {
145        Self::new_internal(session, Some(device), width, height)
146    }
147
148    /// Shared constructor implementing extent, format, handle, image, and view setup.
149    ///
150    /// # Errors
151    ///
152    /// Returns [`OpenXrRuntimeError::InvalidSwapchainExtent`] for a zero
153    /// dimension, or propagates format enumeration/selection, swapchain
154    /// creation/image enumeration, and optional Vulkan image-view failures.
155    fn new_internal(
156        session: &xr::Session<xr::Vulkan>,
157        device: Option<&ash::Device>,
158        width: u32,
159        height: u32,
160    ) -> Result<Self, OpenXrRuntimeError> {
161        if width == 0 || height == 0 {
162            return Err(OpenXrRuntimeError::InvalidSwapchainExtent { width, height });
163        }
164
165        let format = select_swapchain_format(session)?;
166        let handle = create_swapchain(session, format, width, height)?;
167        let images = handle
168            .enumerate_images()
169            .map_err(|result| OpenXrRuntimeError::EnumerateSwapchainImages { result })?
170            .into_iter()
171            .map(|image| vk::Image::from_raw(image as _))
172            .collect::<Vec<_>>();
173        let views = if let Some(device) = device {
174            create_swapchain_views(device, &images, format.vk)?
175        } else {
176            Vec::new()
177        };
178
179        Ok(Self {
180            device: device.cloned(),
181            handle,
182            format,
183            images,
184            views,
185            extent: vk::Extent2D { width, height },
186        })
187    }
188
189    /// Returns the native swapchain handle used by composition layers.
190    ///
191    /// # Examples
192    ///
193    /// ```no_run
194    /// use ailloli_ui_openxr::OpenXrQuadSwapchain;
195    /// fn handle(swapchain: &OpenXrQuadSwapchain) -> &openxr::Swapchain<openxr::Vulkan> { swapchain.handle() }
196    /// ```
197    pub fn handle(&self) -> &xr::Swapchain<xr::Vulkan> {
198        &self.handle
199    }
200
201    /// Returns the selected Vulkan/raw format pair.
202    ///
203    /// # Examples
204    ///
205    /// ```no_run
206    /// use ailloli_ui_openxr::{OpenXrQuadSwapchain, OpenXrSwapchainFormat};
207    /// fn format(swapchain: &OpenXrQuadSwapchain) -> OpenXrSwapchainFormat { swapchain.format() }
208    /// ```
209    pub fn format(&self) -> OpenXrSwapchainFormat {
210        self.format
211    }
212
213    /// Returns the physical pixel extent supplied at construction.
214    ///
215    /// # Examples
216    ///
217    /// ```no_run
218    /// use ailloli_ui_openxr::OpenXrQuadSwapchain;
219    /// fn width(swapchain: &OpenXrQuadSwapchain) -> u32 { swapchain.extent().width }
220    /// ```
221    pub fn extent(&self) -> vk::Extent2D {
222        self.extent
223    }
224
225    /// Returns a full-image OpenXR rectangle with zero offset.
226    ///
227    /// Width and height are cast to signed OpenXR fields; practical swapchain
228    /// extents must therefore fit in `i32`.
229    ///
230    /// # Examples
231    ///
232    /// ```no_run
233    /// use ailloli_ui_openxr::OpenXrQuadSwapchain;
234    /// fn rect(swapchain: &OpenXrQuadSwapchain) -> openxr::Rect2Di { swapchain.image_rect() }
235    /// ```
236    pub fn image_rect(&self) -> xr::Rect2Di {
237        xr::Rect2Di {
238            offset: xr::Offset2Di { x: 0, y: 0 },
239            extent: xr::Extent2Di {
240                width: self.extent.width as i32,
241                height: self.extent.height as i32,
242            },
243        }
244    }
245
246    /// Acquires the next image and waits indefinitely until it is writable.
247    ///
248    /// A successful call must be paired with [`Self::release`]. The returned
249    /// view is null if the swapchain was constructed with [`Self::new`].
250    ///
251    /// # Errors
252    ///
253    /// Returns acquisition, wait, or runtime-index bounds failures.
254    ///
255    /// # Examples
256    ///
257    /// ```no_run
258    /// use ailloli_ui_openxr::{OpenXrAcquiredImage, OpenXrQuadSwapchain, OpenXrRuntimeError};
259    /// fn acquire(swapchain: &mut OpenXrQuadSwapchain) -> Result<OpenXrAcquiredImage, OpenXrRuntimeError> { swapchain.acquire_wait() }
260    /// ```
261    pub fn acquire_wait(&mut self) -> Result<OpenXrAcquiredImage, OpenXrRuntimeError> {
262        let image_index = self
263            .handle
264            .acquire_image()
265            .map_err(|result| OpenXrRuntimeError::AcquireSwapchainImage { result })?;
266        self.handle
267            .wait_image(xr::Duration::INFINITE)
268            .map_err(|result| OpenXrRuntimeError::WaitSwapchainImage { result })?;
269        let image = *self.images.get(image_index as usize).ok_or(
270            OpenXrRuntimeError::SwapchainImageIndexOutOfBounds {
271                index: image_index,
272                len: self.images.len(),
273            },
274        )?;
275        let view = if self.views.is_empty() {
276            vk::ImageView::null()
277        } else {
278            *self.views.get(image_index as usize).ok_or(
279                OpenXrRuntimeError::SwapchainImageIndexOutOfBounds {
280                    index: image_index,
281                    len: self.views.len(),
282                },
283            )?
284        };
285
286        Ok(OpenXrAcquiredImage {
287            image_index,
288            image,
289            view,
290            format: self.format,
291            extent: self.extent,
292        })
293    }
294
295    /// Releases the currently acquired image back to the runtime.
296    ///
297    /// # Errors
298    ///
299    /// Returns the native OpenXR release failure.
300    ///
301    /// # Examples
302    ///
303    /// ```no_run
304    /// use ailloli_ui_openxr::{OpenXrQuadSwapchain, OpenXrRuntimeError};
305    /// fn release(swapchain: &mut OpenXrQuadSwapchain) -> Result<(), OpenXrRuntimeError> { swapchain.release() }
306    /// ```
307    pub fn release(&mut self) -> Result<(), OpenXrRuntimeError> {
308        self.handle
309            .release_image()
310            .map_err(|result| OpenXrRuntimeError::ReleaseSwapchainImage { result })
311    }
312
313    /// Clears an acquired image and transitions it to shader-read layout.
314    ///
315    /// The RGBA color is forwarded without clamping. Work is submitted through
316    /// the runtime Vulkan context and waits for the queue to become idle.
317    ///
318    /// # Errors
319    ///
320    /// Returns command-buffer allocation, recording, submission, or wait errors.
321    ///
322    /// # Examples
323    ///
324    /// ```no_run
325    /// use ailloli_ui_openxr::{OpenXrAcquiredImage, OpenXrQuadSwapchain, OpenXrRuntimeError, OpenXrVulkanContext};
326    /// fn clear(swapchain: &OpenXrQuadSwapchain, vk: &OpenXrVulkanContext, image: &OpenXrAcquiredImage) -> Result<(), OpenXrRuntimeError> { swapchain.clear_acquired_image(vk, image, [0.0, 0.0, 0.0, 1.0]) }
327    /// ```
328    pub fn clear_acquired_image(
329        &self,
330        vk: &OpenXrVulkanContext,
331        acquired: &OpenXrAcquiredImage,
332        color: [f32; 4],
333    ) -> Result<(), OpenXrRuntimeError> {
334        vk.submit_one_time_commands(|command_buffer| unsafe {
335            record_clear_image(
336                &vk.vk_device,
337                command_buffer,
338                acquired.image,
339                acquired.extent,
340                color,
341            );
342        })
343    }
344
345    /// Wraps an acquired image for `ailloli_ui_render_vulkan`.
346    ///
347    /// Initial layout is declared undefined and final layout shader-read-only.
348    /// The caller must have constructed image views for renderer use.
349    ///
350    /// # Examples
351    ///
352    /// ```no_run
353    /// use ailloli_ui_openxr::{OpenXrAcquiredImage, OpenXrQuadSwapchain};
354    /// use ailloli_ui_render_vulkan::VulkanFrameTarget;
355    /// fn target(swapchain: &OpenXrQuadSwapchain, image: &OpenXrAcquiredImage) -> VulkanFrameTarget { swapchain.frame_target(image) }
356    /// ```
357    pub fn frame_target(&self, acquired: &OpenXrAcquiredImage) -> VulkanFrameTarget {
358        VulkanFrameTarget::new(
359            acquired.image,
360            acquired.view,
361            acquired.format.vk,
362            acquired.extent,
363        )
364        .with_layouts(
365            vk::ImageLayout::UNDEFINED,
366            vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL,
367        )
368    }
369}
370
371impl Drop for OpenXrQuadSwapchain {
372    fn drop(&mut self) {
373        if let Some(device) = &self.device {
374            unsafe {
375                for view in self.views.drain(..) {
376                    if view != vk::ImageView::null() {
377                        device.destroy_image_view(view, None);
378                    }
379                }
380            }
381        }
382    }
383}
384
385/// Selects the first supported RGBA/BGRA UNORM/SRGB format in preference order.
386///
387/// # Errors
388///
389/// Returns [`OpenXrRuntimeError::EnumerateSwapchainFormats`] if the runtime
390/// cannot enumerate formats, or [`OpenXrRuntimeError::UnsupportedSwapchainFormat`]
391/// with the complete reported list when none of the four preferred formats is
392/// available.
393///
394/// # Examples
395///
396/// ```no_run
397/// use ailloli_ui_openxr::{OpenXrQuadSwapchain, OpenXrRuntimeError};
398/// fn create_with_selected_format(session: &openxr::Session<openxr::Vulkan>) -> Result<OpenXrQuadSwapchain, OpenXrRuntimeError> { OpenXrQuadSwapchain::new(session, 1, 1) }
399/// ```
400pub(crate) fn select_swapchain_format(
401    session: &xr::Session<xr::Vulkan>,
402) -> Result<OpenXrSwapchainFormat, OpenXrRuntimeError> {
403    let supported = session
404        .enumerate_swapchain_formats()
405        .map_err(|result| OpenXrRuntimeError::EnumerateSwapchainFormats { result })?;
406    let preferred = [
407        vk::Format::R8G8B8A8_UNORM,
408        vk::Format::B8G8R8A8_UNORM,
409        vk::Format::R8G8B8A8_SRGB,
410        vk::Format::B8G8R8A8_SRGB,
411    ];
412
413    for vk_format in preferred {
414        let xr_format = vk_format.as_raw() as u32;
415        if supported.contains(&xr_format) {
416            return Ok(OpenXrSwapchainFormat {
417                vk: vk_format,
418                xr: xr_format,
419            });
420        }
421    }
422
423    Err(OpenXrRuntimeError::UnsupportedSwapchainFormat { supported })
424}
425
426/// Creates a color-attachment swapchain, falling back to transfer+sampled usage.
427///
428/// # Errors
429///
430/// Returns [`OpenXrRuntimeError::CreateSwapchain`] when both the preferred
431/// color-attachment usage and the transfer-plus-sampled fallback are rejected;
432/// the error retains the fallback runtime result.
433fn create_swapchain(
434    session: &xr::Session<xr::Vulkan>,
435    format: OpenXrSwapchainFormat,
436    width: u32,
437    height: u32,
438) -> Result<xr::Swapchain<xr::Vulkan>, OpenXrRuntimeError> {
439    let sampled_transfer = xr::SwapchainUsageFlags::TRANSFER_DST | xr::SwapchainUsageFlags::SAMPLED;
440    let color_usage = sampled_transfer | xr::SwapchainUsageFlags::COLOR_ATTACHMENT;
441
442    match session.create_swapchain(&swapchain_create_info(format, width, height, color_usage)) {
443        Ok(handle) => Ok(handle),
444        Err(_) => session
445            .create_swapchain(&swapchain_create_info(
446                format,
447                width,
448                height,
449                sampled_transfer,
450            ))
451            .map_err(|result| OpenXrRuntimeError::CreateSwapchain {
452                usage: "TRANSFER_DST | SAMPLED",
453                result,
454            }),
455    }
456}
457
458/// Creates one 2D color view per runtime-owned swapchain image.
459///
460/// # Errors
461///
462/// Returns [`OpenXrRuntimeError::CreateSwapchainImageView`] with the first Vulkan
463/// driver failure. Views created for earlier images are not returned.
464fn create_swapchain_views(
465    device: &ash::Device,
466    images: &[vk::Image],
467    format: vk::Format,
468) -> Result<Vec<vk::ImageView>, OpenXrRuntimeError> {
469    let mut views = Vec::with_capacity(images.len());
470    for &image in images {
471        let create_info = vk::ImageViewCreateInfo::default()
472            .image(image)
473            .view_type(vk::ImageViewType::TYPE_2D)
474            .format(format)
475            .components(vk::ComponentMapping::default())
476            .subresource_range(vk::ImageSubresourceRange {
477                aspect_mask: vk::ImageAspectFlags::COLOR,
478                base_mip_level: 0,
479                level_count: 1,
480                base_array_layer: 0,
481                layer_count: 1,
482            });
483        let view = unsafe { device.create_image_view(&create_info, None) }
484            .map_err(|result| OpenXrRuntimeError::CreateSwapchainImageView { result })?;
485        views.push(view);
486    }
487    Ok(views)
488}
489
490/// Builds the single-sample, single-face, single-array swapchain descriptor.
491fn swapchain_create_info(
492    format: OpenXrSwapchainFormat,
493    width: u32,
494    height: u32,
495    usage_flags: xr::SwapchainUsageFlags,
496) -> xr::SwapchainCreateInfo<xr::Vulkan> {
497    xr::SwapchainCreateInfo {
498        create_flags: xr::SwapchainCreateFlags::EMPTY,
499        usage_flags,
500        format: format.xr,
501        sample_count: 1,
502        width,
503        height,
504        face_count: 1,
505        array_size: 1,
506        mip_count: 1,
507    }
508}
509
510/// Records undefined-to-transfer, clear, and shader-read transitions.
511///
512/// # Safety
513///
514/// The image and command buffer must be valid, compatible handles from `device`,
515/// and recording must occur outside a render pass.
516unsafe fn record_clear_image(
517    device: &ash::Device,
518    command_buffer: vk::CommandBuffer,
519    image: vk::Image,
520    extent: vk::Extent2D,
521    color: [f32; 4],
522) {
523    let color_range = vk::ImageSubresourceRange {
524        aspect_mask: vk::ImageAspectFlags::COLOR,
525        base_mip_level: 0,
526        level_count: 1,
527        base_array_layer: 0,
528        layer_count: 1,
529    };
530    let barrier_to_transfer = vk::ImageMemoryBarrier::default()
531        .image(image)
532        .old_layout(vk::ImageLayout::UNDEFINED)
533        .new_layout(vk::ImageLayout::TRANSFER_DST_OPTIMAL)
534        .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
535        .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
536        .subresource_range(color_range);
537
538    device.cmd_pipeline_barrier(
539        command_buffer,
540        vk::PipelineStageFlags::TOP_OF_PIPE,
541        vk::PipelineStageFlags::TRANSFER,
542        vk::DependencyFlags::empty(),
543        &[],
544        &[],
545        &[barrier_to_transfer],
546    );
547
548    let clear_color = vk::ClearColorValue { float32: color };
549    device.cmd_clear_color_image(
550        command_buffer,
551        image,
552        vk::ImageLayout::TRANSFER_DST_OPTIMAL,
553        &clear_color,
554        &[color_range],
555    );
556
557    let barrier_to_shader = vk::ImageMemoryBarrier::default()
558        .image(image)
559        .old_layout(vk::ImageLayout::TRANSFER_DST_OPTIMAL)
560        .new_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL)
561        .src_access_mask(vk::AccessFlags::TRANSFER_WRITE)
562        .dst_access_mask(vk::AccessFlags::SHADER_READ)
563        .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
564        .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
565        .subresource_range(color_range);
566
567    device.cmd_pipeline_barrier(
568        command_buffer,
569        vk::PipelineStageFlags::TRANSFER,
570        vk::PipelineStageFlags::FRAGMENT_SHADER,
571        vk::DependencyFlags::empty(),
572        &[],
573        &[],
574        &[barrier_to_shader],
575    );
576
577    let _ = extent;
578}