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}