1use ash::vk::{self, Handle};
4use openxr as xr;
5
6use crate::math::Vec3;
7
8use super::error::OpenXrRuntimeError;
9use super::swapchain::{select_swapchain_format, OpenXrSwapchainFormat};
10use super::ui_layer::OpenXrExternalVulkanContext;
11
12pub const OPENXR_RAY_TEXTURE_WIDTH: u32 = 16;
20pub const OPENXR_RAY_TEXTURE_HEIGHT: u32 = 256;
28pub const OPENXR_RAY_WIDTH_METERS: f32 = 0.003;
36pub const OPENXR_RAY_MIN_LENGTH_METERS: f32 = 0.05;
44pub const OPENXR_RAY_MAX_LENGTH_METERS: f32 = 3.0;
52
53const RAY_STAGING_USAGE: &str = "OpenXR ray overlay";
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub enum OpenXrRayHitKind {
66 Miss,
68 Ui,
70}
71
72#[derive(Debug, Clone, Copy)]
73pub struct OpenXrRaySample {
87 pub origin: Vec3,
89 pub direction: Vec3,
91 pub hit_kind: OpenXrRayHitKind,
93 pub hit_distance: f32,
95}
96
97impl OpenXrRaySample {
98 pub fn new(
109 origin: Vec3,
110 direction: Vec3,
111 hit_kind: OpenXrRayHitKind,
112 hit_distance: f32,
113 ) -> Self {
114 Self {
115 origin,
116 direction,
117 hit_kind,
118 hit_distance,
119 }
120 }
121}
122
123#[derive(Debug, Clone, Copy)]
124pub struct OpenXrRayOverlayOptions {
138 pub texture_width: u32,
140 pub texture_height: u32,
142 pub ray_width_m: f32,
144 pub min_length_m: f32,
146 pub max_length_m: f32,
148 pub eye_visibility: xr::EyeVisibility,
150 pub layer_flags: xr::CompositionLayerFlags,
152}
153
154impl Default for OpenXrRayOverlayOptions {
155 fn default() -> Self {
156 Self {
157 texture_width: OPENXR_RAY_TEXTURE_WIDTH,
158 texture_height: OPENXR_RAY_TEXTURE_HEIGHT,
159 ray_width_m: OPENXR_RAY_WIDTH_METERS,
160 min_length_m: OPENXR_RAY_MIN_LENGTH_METERS,
161 max_length_m: OPENXR_RAY_MAX_LENGTH_METERS,
162 eye_visibility: xr::EyeVisibility::BOTH,
163 layer_flags: xr::CompositionLayerFlags::BLEND_TEXTURE_SOURCE_ALPHA
164 | xr::CompositionLayerFlags::UNPREMULTIPLIED_ALPHA,
165 }
166 }
167}
168
169struct RayStagingBuffer {
171 buffer: vk::Buffer,
173 memory: vk::DeviceMemory,
175 mapped_ptr: *mut u8,
177 size: vk::DeviceSize,
179}
180
181pub struct OpenXrRayOverlay {
194 device: ash::Device,
196 handle: xr::Swapchain<xr::Vulkan>,
198 format: OpenXrSwapchainFormat,
200 images: Vec<vk::Image>,
202 staging: RayStagingBuffer,
204 ready_image_index: Option<u32>,
206 uploaded_hit: Option<OpenXrRayHitKind>,
208 has_texture: bool,
210 options: OpenXrRayOverlayOptions,
212}
213
214impl OpenXrRayOverlay {
215 pub fn new(
234 session: &xr::Session<xr::Vulkan>,
235 context: OpenXrExternalVulkanContext<'_>,
236 options: OpenXrRayOverlayOptions,
237 ) -> Result<Self, OpenXrRuntimeError> {
238 let texture_width = options.texture_width.max(1);
239 let texture_height = options.texture_height.max(1);
240 let format = select_swapchain_format(session)?;
241 let handle = session
242 .create_swapchain(&xr::SwapchainCreateInfo {
243 create_flags: xr::SwapchainCreateFlags::EMPTY,
244 usage_flags: xr::SwapchainUsageFlags::TRANSFER_DST
245 | xr::SwapchainUsageFlags::SAMPLED,
246 format: format.xr,
247 sample_count: 1,
248 width: texture_width,
249 height: texture_height,
250 face_count: 1,
251 array_size: 1,
252 mip_count: 1,
253 })
254 .map_err(|result| OpenXrRuntimeError::CreateSwapchain {
255 usage: "ray TRANSFER_DST | SAMPLED",
256 result,
257 })?;
258 let images = handle
259 .enumerate_images()
260 .map_err(|result| OpenXrRuntimeError::EnumerateSwapchainImages { result })?
261 .into_iter()
262 .map(|image| vk::Image::from_raw(image as _))
263 .collect::<Vec<_>>();
264 let memory_properties =
265 context
266 .memory_properties
267 .ok_or(OpenXrRuntimeError::MissingVulkanMemoryProperties {
268 usage: RAY_STAGING_USAGE,
269 })?;
270 let staging_size = (texture_width as vk::DeviceSize)
271 .saturating_mul(texture_height as vk::DeviceSize)
272 .saturating_mul(4);
273 let staging = RayStagingBuffer::new(context.device, memory_properties, staging_size)?;
274
275 Ok(Self {
276 device: context.device.clone(),
277 handle,
278 format,
279 images,
280 staging,
281 ready_image_index: None,
282 uploaded_hit: None,
283 has_texture: false,
284 options: OpenXrRayOverlayOptions {
285 texture_width,
286 texture_height,
287 ..options
288 },
289 })
290 }
291
292 pub fn ensure_texture(
310 &mut self,
311 context: OpenXrExternalVulkanContext<'_>,
312 hit_kind: OpenXrRayHitKind,
313 ) -> Result<(), OpenXrRuntimeError> {
314 if self.uploaded_hit == Some(hit_kind) && self.has_texture {
315 return Ok(());
316 }
317
318 let rgba = ray_rgba(hit_kind, self.options);
319 self.upload_rgba(context, &rgba, hit_kind)
320 }
321
322 pub fn build_layer<'a>(
337 &'a self,
338 reference_space: &'a xr::Space,
339 sample: &OpenXrRaySample,
340 ) -> Option<xr::CompositionLayerQuad<'a, xr::Vulkan>> {
341 if !self.has_texture {
342 return None;
343 }
344 let _ = self.ready_image_index?;
345
346 let length = clamp_ray_length(sample.hit_distance, self.options);
347 let direction = sample.direction.normalize_or(Vec3::new(0.0, 0.0, -1.0));
348 let mid = sample.origin + direction * (length * 0.5);
349 let rect = xr::Rect2Di {
350 offset: xr::Offset2Di { x: 0, y: 0 },
351 extent: xr::Extent2Di {
352 width: self.options.texture_width as i32,
353 height: self.options.texture_height as i32,
354 },
355 };
356
357 Some(
358 xr::CompositionLayerQuad::new()
359 .space(reference_space)
360 .eye_visibility(self.options.eye_visibility)
361 .sub_image(
362 xr::SwapchainSubImage::new()
363 .swapchain(&self.handle)
364 .image_array_index(0)
365 .image_rect(rect),
366 )
367 .pose(ray_pose(mid, direction))
368 .size(xr::Extent2Df {
369 width: self.options.ray_width_m,
370 height: length,
371 })
372 .layer_flags(self.options.layer_flags),
373 )
374 }
375
376 fn upload_rgba(
384 &mut self,
385 context: OpenXrExternalVulkanContext<'_>,
386 rgba: &[u8],
387 hit_kind: OpenXrRayHitKind,
388 ) -> Result<(), OpenXrRuntimeError> {
389 copy_rgba_to_staging(
390 self.staging.mapped_ptr,
391 rgba,
392 upload_swizzle(self.format.vk),
393 );
394 let image_index = self
395 .handle
396 .acquire_image()
397 .map_err(|result| OpenXrRuntimeError::AcquireSwapchainImage { result })?;
398 self.handle
399 .wait_image(xr::Duration::INFINITE)
400 .map_err(|result| OpenXrRuntimeError::WaitSwapchainImage { result })?;
401 let image = *self.images.get(image_index as usize).ok_or(
402 OpenXrRuntimeError::SwapchainImageIndexOutOfBounds {
403 index: image_index,
404 len: self.images.len(),
405 },
406 )?;
407
408 let upload_result = submit_one_time_commands(context, |command_buffer| unsafe {
409 record_rgba_upload(
410 context.device,
411 command_buffer,
412 image,
413 self.options.texture_width,
414 self.options.texture_height,
415 self.staging.buffer,
416 self.staging.size,
417 );
418 });
419 let release_result = self
420 .handle
421 .release_image()
422 .map_err(|result| OpenXrRuntimeError::ReleaseSwapchainImage { result });
423
424 upload_result?;
425 release_result?;
426
427 self.ready_image_index = Some(image_index);
428 self.uploaded_hit = Some(hit_kind);
429 self.has_texture = true;
430 Ok(())
431 }
432}
433
434impl Drop for OpenXrRayOverlay {
435 fn drop(&mut self) {
436 unsafe {
437 self.device.device_wait_idle().ok();
438 self.device.unmap_memory(self.staging.memory);
439 self.device.destroy_buffer(self.staging.buffer, None);
440 self.device.free_memory(self.staging.memory, None);
441 }
442 }
443}
444
445impl RayStagingBuffer {
446 fn new(
453 device: &ash::Device,
454 memory_properties: &vk::PhysicalDeviceMemoryProperties,
455 size: vk::DeviceSize,
456 ) -> Result<Self, OpenXrRuntimeError> {
457 let buffer = unsafe {
458 device.create_buffer(
459 &vk::BufferCreateInfo::default()
460 .size(size)
461 .usage(vk::BufferUsageFlags::TRANSFER_SRC)
462 .sharing_mode(vk::SharingMode::EXCLUSIVE),
463 None,
464 )
465 }
466 .map_err(|result| OpenXrRuntimeError::CreateStagingBuffer {
467 usage: RAY_STAGING_USAGE,
468 result,
469 })?;
470
471 let memory_requirements = unsafe { device.get_buffer_memory_requirements(buffer) };
472 let memory_type_index =
473 find_host_visible_memory_type(memory_properties, memory_requirements).ok_or(
474 OpenXrRuntimeError::NoHostVisibleMemoryType {
475 usage: RAY_STAGING_USAGE,
476 },
477 )?;
478 let memory = unsafe {
479 device.allocate_memory(
480 &vk::MemoryAllocateInfo::default()
481 .allocation_size(size)
482 .memory_type_index(memory_type_index),
483 None,
484 )
485 }
486 .map_err(|result| OpenXrRuntimeError::AllocateStagingMemory {
487 usage: RAY_STAGING_USAGE,
488 result,
489 })?;
490
491 unsafe { device.bind_buffer_memory(buffer, memory, 0) }.map_err(|result| {
492 OpenXrRuntimeError::BindStagingMemory {
493 usage: RAY_STAGING_USAGE,
494 result,
495 }
496 })?;
497 let mapped_ptr = unsafe { device.map_memory(memory, 0, size, vk::MemoryMapFlags::empty()) }
498 .map_err(|result| OpenXrRuntimeError::MapStagingMemory {
499 usage: RAY_STAGING_USAGE,
500 result,
501 })?
502 .cast();
503
504 Ok(Self {
505 buffer,
506 memory,
507 mapped_ptr,
508 size,
509 })
510 }
511}
512
513fn find_host_visible_memory_type(
515 memory_properties: &vk::PhysicalDeviceMemoryProperties,
516 memory_requirements: vk::MemoryRequirements,
517) -> Option<u32> {
518 (0..memory_properties.memory_type_count).find(|i| {
519 let suitable = memory_requirements.memory_type_bits & (1 << i) != 0;
520 let flags = memory_properties.memory_types[*i as usize].property_flags;
521 suitable
522 && flags.contains(vk::MemoryPropertyFlags::HOST_VISIBLE)
523 && flags.contains(vk::MemoryPropertyFlags::HOST_COHERENT)
524 })
525}
526
527fn submit_one_time_commands(
539 context: OpenXrExternalVulkanContext<'_>,
540 record: impl FnOnce(vk::CommandBuffer),
541) -> Result<(), OpenXrRuntimeError> {
542 let command_buffer = unsafe {
543 context.device.allocate_command_buffers(
544 &vk::CommandBufferAllocateInfo::default()
545 .command_pool(context.command_pool)
546 .level(vk::CommandBufferLevel::PRIMARY)
547 .command_buffer_count(1),
548 )
549 }
550 .map_err(|result| OpenXrRuntimeError::AllocateCommandBuffer { result })?[0];
551
552 let result = (|| {
553 unsafe {
554 context.device.begin_command_buffer(
555 command_buffer,
556 &vk::CommandBufferBeginInfo::default()
557 .flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT),
558 )
559 }
560 .map_err(|result| OpenXrRuntimeError::BeginCommandBuffer { result })?;
561 record(command_buffer);
562 unsafe { context.device.end_command_buffer(command_buffer) }
563 .map_err(|result| OpenXrRuntimeError::EndCommandBuffer { result })?;
564 unsafe {
565 context.device.queue_submit(
566 context.queue,
567 &[vk::SubmitInfo::default().command_buffers(&[command_buffer])],
568 vk::Fence::null(),
569 )
570 }
571 .map_err(|result| OpenXrRuntimeError::QueueSubmit { result })?;
572 unsafe { context.device.queue_wait_idle(context.queue) }
573 .map_err(|result| OpenXrRuntimeError::QueueWaitIdle { result })?;
574 Ok(())
575 })();
576
577 unsafe {
578 context
579 .device
580 .free_command_buffers(context.command_pool, &[command_buffer]);
581 }
582 result
583}
584
585unsafe fn record_rgba_upload(
592 device: &ash::Device,
593 command_buffer: vk::CommandBuffer,
594 image: vk::Image,
595 width: u32,
596 height: u32,
597 staging: vk::Buffer,
598 buffer_size: vk::DeviceSize,
599) {
600 let color_range = vk::ImageSubresourceRange {
601 aspect_mask: vk::ImageAspectFlags::COLOR,
602 base_mip_level: 0,
603 level_count: 1,
604 base_array_layer: 0,
605 layer_count: 1,
606 };
607 let barrier_to_transfer = vk::ImageMemoryBarrier::default()
608 .image(image)
609 .old_layout(vk::ImageLayout::UNDEFINED)
610 .new_layout(vk::ImageLayout::TRANSFER_DST_OPTIMAL)
611 .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
612 .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
613 .subresource_range(color_range);
614
615 device.cmd_pipeline_barrier(
616 command_buffer,
617 vk::PipelineStageFlags::TOP_OF_PIPE,
618 vk::PipelineStageFlags::TRANSFER,
619 vk::DependencyFlags::empty(),
620 &[],
621 &[],
622 &[barrier_to_transfer],
623 );
624
625 let region = vk::BufferImageCopy::default()
626 .buffer_offset(0)
627 .buffer_row_length(0)
628 .buffer_image_height(0)
629 .image_subresource(vk::ImageSubresourceLayers {
630 aspect_mask: vk::ImageAspectFlags::COLOR,
631 mip_level: 0,
632 base_array_layer: 0,
633 layer_count: 1,
634 })
635 .image_offset(vk::Offset3D::default())
636 .image_extent(vk::Extent3D {
637 width,
638 height,
639 depth: 1,
640 });
641
642 device.cmd_copy_buffer_to_image(
643 command_buffer,
644 staging,
645 image,
646 vk::ImageLayout::TRANSFER_DST_OPTIMAL,
647 &[region],
648 );
649
650 let barrier_to_shader = vk::ImageMemoryBarrier::default()
651 .image(image)
652 .old_layout(vk::ImageLayout::TRANSFER_DST_OPTIMAL)
653 .new_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL)
654 .src_access_mask(vk::AccessFlags::TRANSFER_WRITE)
655 .dst_access_mask(vk::AccessFlags::SHADER_READ)
656 .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
657 .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
658 .subresource_range(color_range);
659
660 device.cmd_pipeline_barrier(
661 command_buffer,
662 vk::PipelineStageFlags::TRANSFER,
663 vk::PipelineStageFlags::FRAGMENT_SHADER,
664 vk::DependencyFlags::empty(),
665 &[],
666 &[],
667 &[barrier_to_shader],
668 );
669
670 let _ = buffer_size;
671}
672
673#[derive(Clone, Copy)]
674enum RayUploadSwizzle {
676 Rgba,
678 Bgra,
680}
681
682fn upload_swizzle(format: vk::Format) -> RayUploadSwizzle {
684 match format {
685 vk::Format::B8G8R8A8_UNORM | vk::Format::B8G8R8A8_SRGB => RayUploadSwizzle::Bgra,
686 _ => RayUploadSwizzle::Rgba,
687 }
688}
689
690fn copy_rgba_to_staging(dst: *mut u8, rgba: &[u8], swizzle: RayUploadSwizzle) {
694 match swizzle {
695 RayUploadSwizzle::Rgba => unsafe {
696 std::ptr::copy_nonoverlapping(rgba.as_ptr(), dst, rgba.len());
697 },
698 RayUploadSwizzle::Bgra => {
699 let dst_slice = unsafe { std::slice::from_raw_parts_mut(dst, rgba.len()) };
700 for (src, out) in rgba.chunks_exact(4).zip(dst_slice.chunks_exact_mut(4)) {
701 out[0] = src[2];
702 out[1] = src[1];
703 out[2] = src[0];
704 out[3] = src[3];
705 }
706 }
707 }
708}
709
710fn clamp_ray_length(hit_distance: f32, options: OpenXrRayOverlayOptions) -> f32 {
712 let min = options.min_length_m.max(0.001);
713 let max = options.max_length_m.max(min);
714 hit_distance.clamp(min, max)
715}
716
717fn ray_pose(mid: Vec3, direction: Vec3) -> xr::Posef {
719 xr::Posef {
720 orientation: orientation_for_ray(direction),
721 position: xr::Vector3f {
722 x: mid.x,
723 y: mid.y,
724 z: mid.z,
725 },
726 }
727}
728
729fn orientation_for_ray(direction: Vec3) -> xr::Quaternionf {
731 let y_axis = direction.normalize_or(Vec3::new(0.0, 0.0, -1.0));
732 let mut z_hint = Vec3::new(0.0, 1.0, 0.0);
733 if y_axis.cross(z_hint).len() < 1e-4 {
734 z_hint = Vec3::new(1.0, 0.0, 0.0);
735 }
736 let x_axis = y_axis.cross(z_hint).normalize_or(Vec3::new(1.0, 0.0, 0.0));
737 let z_axis = x_axis.cross(y_axis).normalize_or(Vec3::new(0.0, 0.0, 1.0));
738
739 rotation_matrix_to_quaternion([
740 [x_axis.x, y_axis.x, z_axis.x],
741 [x_axis.y, y_axis.y, z_axis.y],
742 [x_axis.z, y_axis.z, z_axis.z],
743 ])
744}
745
746fn rotation_matrix_to_quaternion(m: [[f32; 3]; 3]) -> xr::Quaternionf {
748 let trace = m[0][0] + m[1][1] + m[2][2];
749 if trace > 0.0 {
750 let s = (trace + 1.0).sqrt() * 2.0;
751 xr::Quaternionf {
752 w: 0.25 * s,
753 x: (m[2][1] - m[1][2]) / s,
754 y: (m[0][2] - m[2][0]) / s,
755 z: (m[1][0] - m[0][1]) / s,
756 }
757 } else if m[0][0] > m[1][1] && m[0][0] > m[2][2] {
758 let s = (1.0 + m[0][0] - m[1][1] - m[2][2]).sqrt() * 2.0;
759 xr::Quaternionf {
760 w: (m[2][1] - m[1][2]) / s,
761 x: 0.25 * s,
762 y: (m[0][1] + m[1][0]) / s,
763 z: (m[0][2] + m[2][0]) / s,
764 }
765 } else if m[1][1] > m[2][2] {
766 let s = (1.0 + m[1][1] - m[0][0] - m[2][2]).sqrt() * 2.0;
767 xr::Quaternionf {
768 w: (m[0][2] - m[2][0]) / s,
769 x: (m[0][1] + m[1][0]) / s,
770 y: 0.25 * s,
771 z: (m[1][2] + m[2][1]) / s,
772 }
773 } else {
774 let s = (1.0 + m[2][2] - m[0][0] - m[1][1]).sqrt() * 2.0;
775 xr::Quaternionf {
776 w: (m[1][0] - m[0][1]) / s,
777 x: (m[0][2] + m[2][0]) / s,
778 y: (m[1][2] + m[2][1]) / s,
779 z: 0.25 * s,
780 }
781 }
782}
783
784fn ray_rgba(hit_kind: OpenXrRayHitKind, options: OpenXrRayOverlayOptions) -> Vec<u8> {
786 let (r, g, b, base_alpha) = match hit_kind {
787 OpenXrRayHitKind::Miss => (160u8, 160u8, 160u8, 102u8),
788 OpenXrRayHitKind::Ui => (60u8, 220u8, 90u8, 230u8),
789 };
790 let w = options.texture_width.max(1) as i32;
791 let h = options.texture_height.max(1) as i32;
792 let mut rgba = vec![0u8; (w * h * 4) as usize];
793 for y in 0..h {
794 let t = y as f32 / (h - 1).max(1) as f32;
795 let alpha = (base_alpha as f32 * (1.0 - t * 0.65)) as u8;
796 for x in 0..w {
797 let cx = (w - 1) as f32 * 0.5;
798 let dx = (x as f32 - cx).abs() / cx.max(1.0);
799 let edge = (1.0 - dx).clamp(0.0, 1.0);
800 let a = (alpha as f32 * edge) as u8;
801 let i = ((y * w + x) * 4) as usize;
802 rgba[i] = r;
803 rgba[i + 1] = g;
804 rgba[i + 2] = b;
805 rgba[i + 3] = a;
806 }
807 }
808 rgba
809}
810
811#[cfg(test)]
812mod tests {
814 use super::*;
815
816 fn first_visible_pixel(rgba: &[u8]) -> &[u8] {
818 rgba.chunks_exact(4)
819 .find(|pixel| pixel[3] > 0)
820 .expect("visible pixel")
821 }
822
823 #[test]
824 fn ray_rgba_has_expected_len_and_alpha() {
825 let options = OpenXrRayOverlayOptions::default();
826 let rgba = ray_rgba(OpenXrRayHitKind::Ui, options);
827 assert_eq!(
828 rgba.len(),
829 (options.texture_width * options.texture_height * 4) as usize
830 );
831 assert!(rgba.chunks_exact(4).any(|pixel| pixel[3] > 0));
832 }
833
834 #[test]
835 fn miss_ray_rgba_is_gray() {
836 let rgba = ray_rgba(OpenXrRayHitKind::Miss, OpenXrRayOverlayOptions::default());
837 let pixel = first_visible_pixel(&rgba);
838 assert_eq!(&pixel[0..3], &[160, 160, 160]);
839 }
840
841 #[test]
842 fn ui_ray_rgba_is_green() {
843 let rgba = ray_rgba(OpenXrRayHitKind::Ui, OpenXrRayOverlayOptions::default());
844 let pixel = first_visible_pixel(&rgba);
845 assert_eq!(&pixel[0..3], &[60, 220, 90]);
846 }
847
848 #[test]
849 fn ray_length_is_clamped() {
850 let options = OpenXrRayOverlayOptions::default();
851 assert_eq!(clamp_ray_length(0.001, options), options.min_length_m);
852 assert_eq!(clamp_ray_length(99.0, options), options.max_length_m);
853 }
854
855 #[test]
856 fn ray_pose_uses_midpoint_position() {
857 let pose = ray_pose(Vec3::new(0.0, 0.0, -1.5), Vec3::new(0.0, 0.0, -1.0));
858 assert!((pose.position.z + 1.5).abs() < 1e-4);
859 assert!(pose.orientation.x.is_finite());
860 assert!(pose.orientation.y.is_finite());
861 assert!(pose.orientation.z.is_finite());
862 assert!(pose.orientation.w.is_finite());
863 }
864}