diff --git a/Cargo.toml b/Cargo.toml index a9c6f9b..ce042aa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,9 +12,11 @@ linked = ["openxr/linked"] vulkan = ["dep:ash"] [dependencies] +anyhow = "1.0.79" async-std = "1.12.0" bevy = "0.12.1" paste = "1.0.14" +thiserror = "1.0.57" wgpu = "0.17.1" wgpu-hal = "0.17.1" winit = "0.28.7" @@ -31,6 +33,7 @@ ash = { version = "0.37.3", optional = true } [target.'cfg(target_family = "wasm")'.dependencies] js-sys = "0.3" wasm-bindgen = "0.2.91" +glow = "0.12.1" web-sys = { version = "0.3.68", features = [ # STANDARD 'console', diff --git a/examples/3d_scene.rs b/examples/3d_scene.rs index dbf1602..8afe0d9 100644 --- a/examples/3d_scene.rs +++ b/examples/3d_scene.rs @@ -1,48 +1,56 @@ -//! A simple 3D scene with light shining over a cube sitting on a plane. +// //! A simple 3D scene with light shining over a cube sitting on a plane. -use bevy::prelude::*; -use bevy_oxr::webxr::XrInitPlugin; +// use bevy::prelude::*; +// use bevy::render::camera::RenderTarget; +// use bevy_oxr::webxr::render::{XrRenderingPlugin, XR_TEXTURE_VIEW_HANDLE}; +// use bevy_oxr::webxr::XrInitPlugin; -fn main() { - App::new() - .add_plugins((DefaultPlugins, XrInitPlugin)) - .add_systems(Startup, setup) - .run(); -} +// fn main() { +// App::new() +// .add_plugins((DefaultPlugins, XrInitPlugin, XrRenderingPlugin)) +// .add_systems(Startup, setup) +// .run(); +// } -/// set up a simple 3D scene -fn setup( - mut commands: Commands, - mut meshes: ResMut>, - mut materials: ResMut>, -) { - // circular base - commands.spawn(PbrBundle { - mesh: meshes.add(shape::Circle::new(4.0).into()), - material: materials.add(Color::WHITE.into()), - transform: Transform::from_rotation(Quat::from_rotation_x(-std::f32::consts::FRAC_PI_2)), - ..default() - }); - // cube - commands.spawn(PbrBundle { - mesh: meshes.add(Mesh::from(shape::Cube { size: 1.0 })), - material: materials.add(Color::rgb_u8(124, 144, 255).into()), - transform: Transform::from_xyz(0.0, 0.5, 0.0), - ..default() - }); - // light - commands.spawn(PointLightBundle { - point_light: PointLight { - intensity: 1500.0, - shadows_enabled: true, - ..default() - }, - transform: Transform::from_xyz(4.0, 8.0, 4.0), - ..default() - }); - // camera - commands.spawn(Camera3dBundle { - transform: Transform::from_xyz(-2.5, 4.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y), - ..default() - }); -} +// /// set up a simple 3D scene +// fn setup( +// mut commands: Commands, +// mut meshes: ResMut>, +// mut materials: ResMut>, +// ) { +// // circular base +// commands.spawn(PbrBundle { +// mesh: meshes.add(shape::Circle::new(4.0).into()), +// material: materials.add(Color::WHITE.into()), +// transform: Transform::from_rotation(Quat::from_rotation_x(-std::f32::consts::FRAC_PI_2)), +// ..default() +// }); +// // cube +// commands.spawn(PbrBundle { +// mesh: meshes.add(Mesh::from(shape::Cube { size: 1.0 })), +// material: materials.add(Color::rgb_u8(124, 144, 255).into()), +// transform: Transform::from_xyz(0.0, 0.5, 0.0), +// ..default() +// }); +// // light +// commands.spawn(PointLightBundle { +// point_light: PointLight { +// intensity: 1500.0, +// shadows_enabled: true, +// ..default() +// }, +// transform: Transform::from_xyz(4.0, 8.0, 4.0), +// ..default() +// }); +// // camera +// commands.spawn(Camera3dBundle { +// transform: Transform::from_xyz(-2.5, 4.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y), +// camera: Camera { +// target: RenderTarget::TextureView(XR_TEXTURE_VIEW_HANDLE), +// ..default() +// }, +// ..default() +// }); +// } + +fn main() {} diff --git a/src/lib.rs b/src/lib.rs index b00ee2a..b4e1f2a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,7 @@ pub mod actions; +#[cfg(not(target_family = "wasm"))] +pub mod openxr; +pub mod render; pub mod types; #[cfg(target_family = "wasm")] pub mod webxr; diff --git a/src/openxr.rs b/src/openxr.rs new file mode 100644 index 0000000..e511552 --- /dev/null +++ b/src/openxr.rs @@ -0,0 +1,16 @@ +pub mod extensions; +pub mod init; +mod resources; +pub mod types; + +pub use resources::*; + +use bevy::app::{App, Plugin}; + +pub struct XrInitPlugin; + +impl Plugin for XrInitPlugin { + fn build(&self, app: &mut App) { + todo!() + } +} diff --git a/src/openxr/extensions.rs b/src/openxr/extensions.rs new file mode 100644 index 0000000..b5d8f09 --- /dev/null +++ b/src/openxr/extensions.rs @@ -0,0 +1,291 @@ +use bevy::prelude::{Deref, DerefMut}; +use openxr::ExtensionSet; + +#[derive(Clone, Debug, Eq, PartialEq, Deref, DerefMut)] +pub struct XrExtensions(ExtensionSet); +impl XrExtensions { + pub fn raw_mut(&mut self) -> &mut ExtensionSet { + &mut self.0 + } + pub fn raw(&self) -> &ExtensionSet { + &self.0 + } + pub fn enable_fb_passthrough(&mut self) -> &mut Self { + self.0.fb_passthrough = true; + self + } + pub fn disable_fb_passthrough(&mut self) -> &mut Self { + self.0.fb_passthrough = false; + self + } + pub fn enable_hand_tracking(&mut self) -> &mut Self { + self.0.ext_hand_tracking = true; + self + } + pub fn disable_hand_tracking(&mut self) -> &mut Self { + self.0.ext_hand_tracking = false; + self + } + /// returns true if all of the extensions enabled are also available in `available_exts` + pub fn is_available(&self, available_exts: &XrExtensions) -> bool { + self.clone() & available_exts.clone() == *self + } +} +impl From for XrExtensions { + fn from(value: ExtensionSet) -> Self { + Self(value) + } +} +impl From for ExtensionSet { + fn from(val: XrExtensions) -> Self { + val.0 + } +} +impl Default for XrExtensions { + fn default() -> Self { + let mut exts = ExtensionSet::default(); + exts.ext_hand_tracking = true; + Self(exts) + } +} + +macro_rules! unavailable_exts { + ( + $exts:ty; + $( + $( + #[$meta:meta] + )* + $ident:ident + ),* + $(,)? + ) => { + impl $exts { + /// Returns any extensions needed by `required_exts` that aren't available in `self` + pub(crate) fn unavailable_exts(&self, required_exts: &Self) -> Vec> { + let mut exts = vec![]; + $( + $( + #[$meta] + )* + if required_exts.0.$ident && !self.0.$ident { + exts.push(std::borrow::Cow::Borrowed(stringify!($ident))) + } + )* + for ext in required_exts.0.other.iter() { + if !self.0.other.contains(ext) { + exts.push(std::borrow::Cow::Owned(ext.clone())) + } + } + exts + } + } + }; +} + +macro_rules! bitor { + ( + $exts:ty; + $( + $( + #[$meta:meta] + )* + $ident:ident + ),* + $(,)? + ) => { + impl std::ops::BitOr for $exts { + type Output = Self; + + fn bitor(self, rhs: Self) -> Self::Output { + let mut out = ExtensionSet::default(); + $( + $( + #[$meta] + )* + { + out.$ident = self.0.$ident || rhs.0.$ident; + } + + )* + out.other = self.0.other; + for ext in rhs.0.other { + if !out.other.contains(&ext) { + out.other.push(ext); + } + } + Self(out) + } + } + }; +} + +macro_rules! bitand { + ( + $exts:ty; + $( + $( + #[$meta:meta] + )* + $ident:ident + ),* + $(,)? + ) => { + impl std::ops::BitAnd for $exts { + type Output = Self; + + fn bitand(self, rhs: Self) -> Self::Output { + let mut out = ExtensionSet::default(); + $( + $( + #[$meta] + )* + { + out.$ident = self.0.$ident && rhs.0.$ident; + } + + )* + for ext in self.0.other { + if rhs.0.other.contains(&ext) { + out.other.push(ext); + } + } + Self(out) + } + } + }; +} + +macro_rules! impl_ext { + ( + $( + $macro:ident + ),* + + ) => { + $( + $macro! { + XrExtensions; + almalence_digital_lens_control, + epic_view_configuration_fov, + ext_performance_settings, + ext_thermal_query, + ext_debug_utils, + ext_eye_gaze_interaction, + ext_view_configuration_depth_range, + ext_conformance_automation, + ext_hand_tracking, + #[cfg(windows)] + ext_win32_appcontainer_compatible, + ext_dpad_binding, + ext_hand_joints_motion_range, + ext_samsung_odyssey_controller, + ext_hp_mixed_reality_controller, + ext_palm_pose, + ext_uuid, + extx_overlay, + fb_composition_layer_image_layout, + fb_composition_layer_alpha_blend, + #[cfg(target_os = "android")] + fb_android_surface_swapchain_create, + fb_swapchain_update_state, + fb_composition_layer_secure_content, + fb_display_refresh_rate, + fb_color_space, + fb_hand_tracking_mesh, + fb_hand_tracking_aim, + fb_hand_tracking_capsules, + fb_spatial_entity, + fb_foveation, + fb_foveation_configuration, + fb_keyboard_tracking, + fb_triangle_mesh, + fb_passthrough, + fb_render_model, + fb_spatial_entity_query, + fb_spatial_entity_storage, + fb_foveation_vulkan, + #[cfg(target_os = "android")] + fb_swapchain_update_state_android_surface, + fb_swapchain_update_state_opengl_es, + fb_swapchain_update_state_vulkan, + fb_space_warp, + fb_scene, + fb_spatial_entity_container, + fb_passthrough_keyboard_hands, + fb_composition_layer_settings, + htc_vive_cosmos_controller_interaction, + htc_facial_tracking, + htc_vive_focus3_controller_interaction, + htc_hand_interaction, + htc_vive_wrist_tracker_interaction, + htcx_vive_tracker_interaction, + huawei_controller_interaction, + #[cfg(target_os = "android")] + khr_android_thread_settings, + #[cfg(target_os = "android")] + khr_android_surface_swapchain, + khr_composition_layer_cube, + #[cfg(target_os = "android")] + khr_android_create_instance, + khr_composition_layer_depth, + khr_vulkan_swapchain_format_list, + khr_composition_layer_cylinder, + khr_composition_layer_equirect, + khr_opengl_enable, + khr_opengl_es_enable, + khr_vulkan_enable, + #[cfg(windows)] + khr_d3d11_enable, + #[cfg(windows)] + khr_d3d12_enable, + khr_visibility_mask, + khr_composition_layer_color_scale_bias, + #[cfg(windows)] + khr_win32_convert_performance_counter_time, + khr_convert_timespec_time, + khr_loader_init, + #[cfg(target_os = "android")] + khr_loader_init_android, + khr_vulkan_enable2, + khr_composition_layer_equirect2, + khr_binding_modification, + khr_swapchain_usage_input_attachment_bit, + meta_vulkan_swapchain_create_info, + meta_performance_metrics, + ml_ml2_controller_interaction, + mnd_headless, + mnd_swapchain_usage_input_attachment_bit, + mndx_egl_enable, + msft_unbounded_reference_space, + msft_spatial_anchor, + msft_spatial_graph_bridge, + msft_hand_interaction, + msft_hand_tracking_mesh, + msft_secondary_view_configuration, + msft_first_person_observer, + msft_controller_model, + #[cfg(windows)] + msft_perception_anchor_interop, + #[cfg(windows)] + msft_holographic_window_attachment, + msft_composition_layer_reprojection, + msft_spatial_anchor_persistence, + #[cfg(target_os = "android")] + oculus_android_session_state_enable, + oculus_audio_device_guid, + ultraleap_hand_tracking_forearm, + valve_analog_threshold, + varjo_quad_views, + varjo_foveated_rendering, + varjo_composition_layer_depth_test, + varjo_environment_depth_estimation, + varjo_marker_tracking, + varjo_view_offset, + } + )* + + }; +} + +impl_ext!(bitor, bitand, unavailable_exts); diff --git a/src/openxr/init.rs b/src/openxr/init.rs new file mode 100644 index 0000000..38de9bc --- /dev/null +++ b/src/openxr/init.rs @@ -0,0 +1,224 @@ +pub mod vulkan; + +use bevy::log::{info, warn}; +use bevy::math::{uvec2, UVec2}; +use thiserror::Error; + +use crate::openxr::resources::*; +use crate::types::BlendMode; + +use super::extensions::XrExtensions; + +#[derive(Clone, Copy, Debug, Default, PartialEq)] +pub struct Version(pub u8, pub u8, pub u16); + +impl Version { + pub const BEVY: Self = Self(0, 12, 1); + + pub const fn to_u32(self) -> u32 { + let major = (self.0 as u32) << 24; + let minor = (self.1 as u32) << 16; + self.2 as u32 | major | minor + } +} + +#[derive(Clone, Copy, Debug, Default, PartialEq)] +pub struct AppInfo<'a> { + pub name: &'a str, + pub version: Version, +} + +#[derive(Clone, Copy, Debug)] +pub enum GraphicsBackend { + Vulkan, +} + +impl GraphicsBackend { + const ALL: &'static [Self] = &[Self::Vulkan]; + + pub fn available_backends(exts: &XrExtensions) -> Vec { + Self::ALL + .iter() + .copied() + .filter(|backend| backend.is_available(exts)) + .collect() + } + + pub fn is_available(&self, exts: &XrExtensions) -> bool { + self.required_exts().is_available(exts) + } + + pub fn required_exts(&self) -> XrExtensions { + match self { + GraphicsBackend::Vulkan => vulkan::required_exts(), + } + } +} + +#[derive(Error, Debug)] +pub enum XrInitError { + #[error("OpenXR error: {0}")] + OpenXrError(#[from] openxr::sys::Result), + #[error("OpenXR loading error: {0}")] + OpenXrLoadingError(#[from] openxr::LoadError), + #[error("WGPU instance error: {0}")] + WgpuInstanceError(#[from] wgpu_hal::InstanceError), + #[error("WGPU device error: {0}")] + WgpuDeviceError(#[from] wgpu_hal::DeviceError), + #[error("WGPU request device error: {0}")] + WgpuRequestDeviceError(#[from] wgpu::RequestDeviceError), + #[error("Unsupported texture format: {0:?}")] + UnsupportedTextureFormat(wgpu::TextureFormat), + #[error("Vulkan error: {0}")] + VulkanError(#[from] ash::vk::Result), + #[error("Vulkan loading error: {0}")] + VulkanLoadingError(#[from] ash::LoadingError), + #[error("Graphics backend '{0:?}' is not available")] + UnavailableBackend(GraphicsBackend), + #[error("Could not meet graphics requirements for platform. See console for details")] + FailedGraphicsRequirements, + #[error("Failed to create CString: {0}")] + NulError(#[from] std::ffi::NulError), +} + +pub trait GraphicsExt: openxr::Graphics { + fn from_wgpu_format(format: wgpu::TextureFormat) -> Option; + fn to_wgpu_format(format: Self::Format) -> Option; + fn create_session( + app_info: AppInfo, + instance: &openxr::Instance, + system_id: openxr::SystemId, + format: wgpu::TextureFormat, + resolution: UVec2, + ) -> Result< + ( + wgpu::Device, + wgpu::Queue, + wgpu::Adapter, + wgpu::Instance, + openxr::Session, + openxr::FrameWaiter, + FrameStreamInner, + ), + XrInitError, + >; +} + +fn xr_entry() -> Result { + #[cfg(windows)] + let entry = Some(openxr::Entry::linked()); + #[cfg(not(windows))] + let entry = unsafe { openxr::Entry::load()? }; + Ok(entry) +} + +// pub fn init_xr( +// app_info: AppInfo, +// requested_exts: XrExtensions, +// format: wgpu::TextureFormat, +// preferred_blend_mode: BlendMode, +// ) -> Result< +// ( +// wgpu::Device, +// wgpu::Queue, +// wgpu::Adapter, +// wgpu::Instance, +// openxr::Instance, +// openxr::Session, +// openxr::FrameWaiter, +// FrameStreamInner, +// XrSwapchain, +// ), +// XrInitError, +// > { +// let entry = xr_entry().unwrap(); + +// let required_exts = vulkan::required_exts() | requested_exts; +// let available_exts: XrExtensions = entry.enumerate_extensions()?.into(); +// for ext in available_exts.unavailable_exts(&required_exts) { +// warn!("OpenXR extension '{ext}' is not supported by the current OpenXR runtime") +// } +// let enabled_exts = required_exts & available_exts; + +// let instance = entry.create_instance( +// &openxr::ApplicationInfo { +// application_name: app_info.name, +// application_version: app_info.version.to_u32(), +// engine_name: "Bevy", +// engine_version: Version::BEVY.to_u32(), +// }, +// &enabled_exts.into(), +// &[], +// )?; +// info!("Created OpenXR Instance: {:#?}", instance.properties()?); + +// let system_id = instance.system(openxr::FormFactor::HEAD_MOUNTED_DISPLAY)?; +// info!( +// "Using system: {:#?}", +// instance.system_properties(system_id)? +// ); + +// let view = instance +// .enumerate_view_configurations(system_id)? +// .first() +// .copied() +// .unwrap_or(openxr::ViewConfigurationType::PRIMARY_STEREO); + +// let resolution = instance +// .enumerate_view_configuration_views(system_id, view)? +// .first() +// .map(|c| { +// uvec2( +// c.recommended_image_rect_width, +// c.recommended_image_rect_height, +// ) +// }) +// .unwrap(); + +// let (wgpu_device, wgpu_queue, wgpu_adapter, wgpu_instance, session, frame_waiter, frame_stream) = +// vulkan::create_session(app_info, &instance, system_id, format, resolution)?; + +// let blend_modes = instance.enumerate_environment_blend_modes(system_id, view)?; + +// let blend_mode = if blend_modes.contains(&preferred_blend_mode.into()) { +// preferred_blend_mode.into() +// } else { +// warn!( +// "Runtime does not support blend mode '{:?}'", +// preferred_blend_mode +// ); +// blend_modes +// .first() +// .copied() +// .unwrap_or(openxr::EnvironmentBlendMode::OPAQUE) +// }; +// info!("Using blend mode '{:?}'", blend_mode); + +// todo!() +// } + +impl From for BlendMode { + fn from(value: openxr::EnvironmentBlendMode) -> Self { + use openxr::EnvironmentBlendMode; + if value == EnvironmentBlendMode::OPAQUE { + BlendMode::Opaque + } else if value == EnvironmentBlendMode::ADDITIVE { + BlendMode::Additive + } else if value == EnvironmentBlendMode::ALPHA_BLEND { + BlendMode::AlphaBlend + } else { + unreachable!() + } + } +} + +impl From for openxr::EnvironmentBlendMode { + fn from(value: BlendMode) -> Self { + use openxr::EnvironmentBlendMode; + match value { + BlendMode::Opaque => EnvironmentBlendMode::OPAQUE, + BlendMode::Additive => EnvironmentBlendMode::ADDITIVE, + BlendMode::AlphaBlend => EnvironmentBlendMode::ALPHA_BLEND, + } + } +} diff --git a/src/openxr/init/vulkan.rs b/src/openxr/init/vulkan.rs new file mode 100644 index 0000000..3a4445f --- /dev/null +++ b/src/openxr/init/vulkan.rs @@ -0,0 +1,670 @@ +use std::ffi::{c_void, CString}; + +use ash::vk::Handle; +use bevy::log::error; +use bevy::math::UVec2; +use openxr::Version; +use wgpu_hal::api::Vulkan; +use wgpu_hal::Api; + +use crate::openxr::extensions::XrExtensions; +use crate::openxr::resources::*; + +use super::{AppInfo, GraphicsExt, XrInitError}; + +pub fn required_exts() -> XrExtensions { + let mut extensions = openxr::ExtensionSet::default(); + extensions.khr_vulkan_enable2 = true; + extensions.into() +} + +#[cfg(not(target_os = "android"))] +const VK_TARGET_VERSION: Version = Version::new(1, 2, 0); +#[cfg(target_os = "android")] +const VK_TARGET_VERSION: Version = Version::new(1, 1, 0); + +const VK_TARGET_VERSION_ASH: u32 = ash::vk::make_api_version( + 0, + VK_TARGET_VERSION.major() as u32, + VK_TARGET_VERSION.minor() as u32, + VK_TARGET_VERSION.patch() as u32, +); + +pub fn enumerate_swapchain_formats( + session: &openxr::Session, +) -> openxr::Result> { + let formats = session + .enumerate_swapchain_formats()? + .into_iter() + .filter_map(|f| vulkan_to_wgpu(ash::vk::Format::from_raw(f as _))) + .collect(); + Ok(formats) +} + +pub fn create_session( + app_info: AppInfo, + instance: &openxr::Instance, + system_id: openxr::SystemId, + format: wgpu::TextureFormat, + resolution: UVec2, +) -> Result< + ( + wgpu::Device, + wgpu::Queue, + wgpu::Adapter, + wgpu::Instance, + openxr::Session, + openxr::FrameWaiter, + FrameStreamInner, + ), + XrInitError, +> { + let reqs = instance.graphics_requirements::(system_id)?; + if VK_TARGET_VERSION < reqs.min_api_version_supported + || VK_TARGET_VERSION.major() > reqs.max_api_version_supported.major() + { + error!( + "OpenXR runtime requires Vulkan version > {}, < {}.0.0", + reqs.min_api_version_supported, + reqs.max_api_version_supported.major() + 1 + ); + return Err(XrInitError::FailedGraphicsRequirements); + }; + let vk_entry = unsafe { ash::Entry::load() }?; + let flags = wgpu_hal::InstanceFlags::empty(); + let extensions = + ::Instance::required_extensions(&vk_entry, VK_TARGET_VERSION_ASH, flags)?; + let device_extensions = vec![ + ash::extensions::khr::Swapchain::name(), + ash::extensions::khr::DrawIndirectCount::name(), + #[cfg(target_os = "android")] + ash::extensions::khr::TimelineSemaphore::name(), + ]; + + let vk_instance = unsafe { + let extensions_cchar: Vec<_> = extensions.iter().map(|s| s.as_ptr()).collect(); + + let app_name = CString::new(app_info.name)?; + let vk_app_info = ash::vk::ApplicationInfo::builder() + .application_name(&app_name) + .application_version(1) + .engine_name(&app_name) + .engine_version(1) + .api_version(VK_TARGET_VERSION_ASH); + + let vk_instance = instance + .create_vulkan_instance( + system_id, + std::mem::transmute(vk_entry.static_fn().get_instance_proc_addr), + &ash::vk::InstanceCreateInfo::builder() + .application_info(&vk_app_info) + .enabled_extension_names(&extensions_cchar) as *const _ + as *const _, + )? + .map_err(ash::vk::Result::from_raw)?; + + ash::Instance::load( + vk_entry.static_fn(), + ash::vk::Instance::from_raw(vk_instance as _), + ) + }; + + let vk_instance_ptr = vk_instance.handle().as_raw() as *const c_void; + + let vk_physical_device = ash::vk::PhysicalDevice::from_raw(unsafe { + instance.vulkan_graphics_device(system_id, vk_instance.handle().as_raw() as _)? as _ + }); + let vk_physical_device_ptr = vk_physical_device.as_raw() as *const c_void; + + let vk_device_properties = + unsafe { vk_instance.get_physical_device_properties(vk_physical_device) }; + + if vk_device_properties.api_version < VK_TARGET_VERSION_ASH { + unsafe { vk_instance.destroy_instance(None) } + error!( + "Vulkan physical device doesn't support version {}.{}.{}", + VK_TARGET_VERSION.major(), + VK_TARGET_VERSION.minor(), + VK_TARGET_VERSION.patch() + ); + return Err(XrInitError::FailedGraphicsRequirements); + } + + let wgpu_vk_instance = unsafe { + ::Instance::from_raw( + vk_entry.clone(), + vk_instance.clone(), + VK_TARGET_VERSION_ASH, + 0, + None, + extensions, + flags, + false, + Some(Box::new(())), + )? + }; + + let wgpu_features = wgpu::Features::TEXTURE_ADAPTER_SPECIFIC_FORMAT_FEATURES + | wgpu::Features::MULTIVIEW + | wgpu::Features::MULTI_DRAW_INDIRECT_COUNT + | wgpu::Features::MULTI_DRAW_INDIRECT; + + let Some(wgpu_exposed_adapter) = wgpu_vk_instance.expose_adapter(vk_physical_device) else { + error!("WGPU failed to provide an adapter"); + return Err(XrInitError::FailedGraphicsRequirements); + }; + + let enabled_extensions = wgpu_exposed_adapter + .adapter + .required_device_extensions(wgpu_features); + + let (wgpu_open_device, vk_device_ptr, queue_family_index) = { + let extensions_cchar: Vec<_> = device_extensions.iter().map(|s| s.as_ptr()).collect(); + let mut enabled_phd_features = wgpu_exposed_adapter + .adapter + .physical_device_features(&enabled_extensions, wgpu_features); + let family_index = 0; + let family_info = ash::vk::DeviceQueueCreateInfo::builder() + .queue_family_index(family_index) + .queue_priorities(&[1.0]) + .build(); + let family_infos = [family_info]; + let info = enabled_phd_features + .add_to_device_create_builder( + ash::vk::DeviceCreateInfo::builder() + .queue_create_infos(&family_infos) + .push_next(&mut ash::vk::PhysicalDeviceMultiviewFeatures { + multiview: ash::vk::TRUE, + ..Default::default() + }), + ) + .enabled_extension_names(&extensions_cchar) + .build(); + let vk_device = unsafe { + let vk_device = instance + .create_vulkan_device( + system_id, + std::mem::transmute(vk_entry.static_fn().get_instance_proc_addr), + vk_physical_device.as_raw() as _, + &info as *const _ as *const _, + )? + .map_err(ash::vk::Result::from_raw)?; + + ash::Device::load( + vk_instance.fp_v1_0(), + ash::vk::Device::from_raw(vk_device as _), + ) + }; + let vk_device_ptr = vk_device.handle().as_raw() as *const c_void; + + let wgpu_open_device = unsafe { + wgpu_exposed_adapter.adapter.device_from_raw( + vk_device, + true, + &enabled_extensions, + wgpu_features, + family_info.queue_family_index, + 0, + ) + }?; + + ( + wgpu_open_device, + vk_device_ptr, + family_info.queue_family_index, + ) + }; + + let wgpu_instance = + unsafe { wgpu::Instance::from_hal::(wgpu_vk_instance) }; + let wgpu_adapter = unsafe { wgpu_instance.create_adapter_from_hal(wgpu_exposed_adapter) }; + let (wgpu_device, wgpu_queue) = unsafe { + wgpu_adapter.create_device_from_hal( + wgpu_open_device, + &wgpu::DeviceDescriptor { + label: None, + features: wgpu_features, + limits: wgpu::Limits { + max_bind_groups: 8, + max_storage_buffer_binding_size: wgpu_adapter + .limits() + .max_storage_buffer_binding_size, + max_push_constant_size: 4, + ..Default::default() + }, + }, + None, + ) + }?; + + let (session, frame_wait, frame_stream) = unsafe { + instance.create_session::( + system_id, + &openxr::vulkan::SessionCreateInfo { + instance: vk_instance_ptr, + physical_device: vk_physical_device_ptr, + device: vk_device_ptr, + queue_family_index, + queue_index: 0, + }, + ) + }?; + + Ok(( + wgpu_device, + wgpu_queue, + wgpu_adapter, + wgpu_instance, + session.into_any_graphics(), + frame_wait, + FrameStreamInner::Vulkan(frame_stream), + )) +} + +impl GraphicsExt for openxr::Vulkan { + fn from_wgpu_format(format: wgpu::TextureFormat) -> Option { + wgpu_to_vulkan(format).map(|f| f.as_raw() as _) + } + + fn to_wgpu_format(format: Self::Format) -> Option { + vulkan_to_wgpu(ash::vk::Format::from_raw(format as _)) + } + + fn create_session( + app_info: AppInfo, + instance: &openxr::Instance, + system_id: openxr::SystemId, + format: wgpu::TextureFormat, + resolution: UVec2, + ) -> Result< + ( + wgpu::Device, + wgpu::Queue, + wgpu::Adapter, + wgpu::Instance, + openxr::Session, + openxr::FrameWaiter, + FrameStreamInner, + ), + XrInitError, + > { + create_session(app_info, instance, system_id, format, resolution) + } +} + +fn vulkan_to_wgpu(format: ash::vk::Format) -> Option { + use ash::vk::Format as F; + use wgpu::TextureFormat as Tf; + use wgpu::{AstcBlock, AstcChannel}; + Some(match format { + F::R8_UNORM => Tf::R8Unorm, + F::R8_SNORM => Tf::R8Snorm, + F::R8_UINT => Tf::R8Uint, + F::R8_SINT => Tf::R8Sint, + F::R16_UINT => Tf::R16Uint, + F::R16_SINT => Tf::R16Sint, + F::R16_UNORM => Tf::R16Unorm, + F::R16_SNORM => Tf::R16Snorm, + F::R16_SFLOAT => Tf::R16Float, + F::R8G8_UNORM => Tf::Rg8Unorm, + F::R8G8_SNORM => Tf::Rg8Snorm, + F::R8G8_UINT => Tf::Rg8Uint, + F::R8G8_SINT => Tf::Rg8Sint, + F::R16G16_UNORM => Tf::Rg16Unorm, + F::R16G16_SNORM => Tf::Rg16Snorm, + F::R32_UINT => Tf::R32Uint, + F::R32_SINT => Tf::R32Sint, + F::R32_SFLOAT => Tf::R32Float, + F::R16G16_UINT => Tf::Rg16Uint, + F::R16G16_SINT => Tf::Rg16Sint, + F::R16G16_SFLOAT => Tf::Rg16Float, + F::R8G8B8A8_UNORM => Tf::Rgba8Unorm, + F::R8G8B8A8_SRGB => Tf::Rgba8UnormSrgb, + F::B8G8R8A8_SRGB => Tf::Bgra8UnormSrgb, + F::R8G8B8A8_SNORM => Tf::Rgba8Snorm, + F::B8G8R8A8_UNORM => Tf::Bgra8Unorm, + F::R8G8B8A8_UINT => Tf::Rgba8Uint, + F::R8G8B8A8_SINT => Tf::Rgba8Sint, + F::A2B10G10R10_UNORM_PACK32 => Tf::Rgb10a2Unorm, + F::B10G11R11_UFLOAT_PACK32 => Tf::Rg11b10Float, + F::R32G32_UINT => Tf::Rg32Uint, + F::R32G32_SINT => Tf::Rg32Sint, + F::R32G32_SFLOAT => Tf::Rg32Float, + F::R16G16B16A16_UINT => Tf::Rgba16Uint, + F::R16G16B16A16_SINT => Tf::Rgba16Sint, + F::R16G16B16A16_UNORM => Tf::Rgba16Unorm, + F::R16G16B16A16_SNORM => Tf::Rgba16Snorm, + F::R16G16B16A16_SFLOAT => Tf::Rgba16Float, + F::R32G32B32A32_UINT => Tf::Rgba32Uint, + F::R32G32B32A32_SINT => Tf::Rgba32Sint, + F::R32G32B32A32_SFLOAT => Tf::Rgba32Float, + F::D32_SFLOAT => Tf::Depth32Float, + F::D32_SFLOAT_S8_UINT => Tf::Depth32FloatStencil8, + F::D16_UNORM => Tf::Depth16Unorm, + F::E5B9G9R9_UFLOAT_PACK32 => Tf::Rgb9e5Ufloat, + F::BC1_RGBA_UNORM_BLOCK => Tf::Bc1RgbaUnorm, + F::BC1_RGBA_SRGB_BLOCK => Tf::Bc1RgbaUnormSrgb, + F::BC2_UNORM_BLOCK => Tf::Bc2RgbaUnorm, + F::BC2_SRGB_BLOCK => Tf::Bc2RgbaUnormSrgb, + F::BC3_UNORM_BLOCK => Tf::Bc3RgbaUnorm, + F::BC3_SRGB_BLOCK => Tf::Bc3RgbaUnormSrgb, + F::BC4_UNORM_BLOCK => Tf::Bc4RUnorm, + F::BC4_SNORM_BLOCK => Tf::Bc4RSnorm, + F::BC5_UNORM_BLOCK => Tf::Bc5RgUnorm, + F::BC5_SNORM_BLOCK => Tf::Bc5RgSnorm, + F::BC6H_UFLOAT_BLOCK => Tf::Bc6hRgbUfloat, + F::BC6H_SFLOAT_BLOCK => Tf::Bc6hRgbFloat, + F::BC7_UNORM_BLOCK => Tf::Bc7RgbaUnorm, + F::BC7_SRGB_BLOCK => Tf::Bc7RgbaUnormSrgb, + F::ETC2_R8G8B8_UNORM_BLOCK => Tf::Etc2Rgb8Unorm, + F::ETC2_R8G8B8_SRGB_BLOCK => Tf::Etc2Rgb8UnormSrgb, + F::ETC2_R8G8B8A1_UNORM_BLOCK => Tf::Etc2Rgb8A1Unorm, + F::ETC2_R8G8B8A1_SRGB_BLOCK => Tf::Etc2Rgb8A1UnormSrgb, + F::ETC2_R8G8B8A8_UNORM_BLOCK => Tf::Etc2Rgba8Unorm, + F::ETC2_R8G8B8A8_SRGB_BLOCK => Tf::Etc2Rgba8UnormSrgb, + F::EAC_R11_UNORM_BLOCK => Tf::EacR11Unorm, + F::EAC_R11_SNORM_BLOCK => Tf::EacR11Snorm, + F::EAC_R11G11_UNORM_BLOCK => Tf::EacRg11Unorm, + F::EAC_R11G11_SNORM_BLOCK => Tf::EacRg11Snorm, + F::ASTC_4X4_UNORM_BLOCK => Tf::Astc { + block: AstcBlock::B4x4, + channel: AstcChannel::Unorm, + }, + F::ASTC_5X4_UNORM_BLOCK => Tf::Astc { + block: AstcBlock::B5x4, + channel: AstcChannel::Unorm, + }, + F::ASTC_5X5_UNORM_BLOCK => Tf::Astc { + block: AstcBlock::B5x5, + channel: AstcChannel::Unorm, + }, + F::ASTC_6X5_UNORM_BLOCK => Tf::Astc { + block: AstcBlock::B6x5, + channel: AstcChannel::Unorm, + }, + F::ASTC_6X6_UNORM_BLOCK => Tf::Astc { + block: AstcBlock::B6x6, + channel: AstcChannel::Unorm, + }, + F::ASTC_8X5_UNORM_BLOCK => Tf::Astc { + block: AstcBlock::B8x5, + channel: AstcChannel::Unorm, + }, + F::ASTC_8X6_UNORM_BLOCK => Tf::Astc { + block: AstcBlock::B8x6, + channel: AstcChannel::Unorm, + }, + F::ASTC_8X8_UNORM_BLOCK => Tf::Astc { + block: AstcBlock::B8x8, + channel: AstcChannel::Unorm, + }, + F::ASTC_10X5_UNORM_BLOCK => Tf::Astc { + block: AstcBlock::B10x5, + channel: AstcChannel::Unorm, + }, + F::ASTC_10X6_UNORM_BLOCK => Tf::Astc { + block: AstcBlock::B10x6, + channel: AstcChannel::Unorm, + }, + F::ASTC_10X8_UNORM_BLOCK => Tf::Astc { + block: AstcBlock::B10x8, + channel: AstcChannel::Unorm, + }, + F::ASTC_10X10_UNORM_BLOCK => Tf::Astc { + block: AstcBlock::B10x10, + channel: AstcChannel::Unorm, + }, + F::ASTC_12X10_UNORM_BLOCK => Tf::Astc { + block: AstcBlock::B12x10, + channel: AstcChannel::Unorm, + }, + F::ASTC_12X12_UNORM_BLOCK => Tf::Astc { + block: AstcBlock::B12x12, + channel: AstcChannel::Unorm, + }, + F::ASTC_4X4_SRGB_BLOCK => Tf::Astc { + block: AstcBlock::B4x4, + channel: AstcChannel::UnormSrgb, + }, + F::ASTC_5X4_SRGB_BLOCK => Tf::Astc { + block: AstcBlock::B5x4, + channel: AstcChannel::UnormSrgb, + }, + F::ASTC_5X5_SRGB_BLOCK => Tf::Astc { + block: AstcBlock::B5x5, + channel: AstcChannel::UnormSrgb, + }, + F::ASTC_6X5_SRGB_BLOCK => Tf::Astc { + block: AstcBlock::B6x5, + channel: AstcChannel::UnormSrgb, + }, + F::ASTC_6X6_SRGB_BLOCK => Tf::Astc { + block: AstcBlock::B6x6, + channel: AstcChannel::UnormSrgb, + }, + F::ASTC_8X5_SRGB_BLOCK => Tf::Astc { + block: AstcBlock::B8x5, + channel: AstcChannel::UnormSrgb, + }, + F::ASTC_8X6_SRGB_BLOCK => Tf::Astc { + block: AstcBlock::B8x6, + channel: AstcChannel::UnormSrgb, + }, + F::ASTC_8X8_SRGB_BLOCK => Tf::Astc { + block: AstcBlock::B8x8, + channel: AstcChannel::UnormSrgb, + }, + F::ASTC_10X5_SRGB_BLOCK => Tf::Astc { + block: AstcBlock::B10x5, + channel: AstcChannel::UnormSrgb, + }, + F::ASTC_10X6_SRGB_BLOCK => Tf::Astc { + block: AstcBlock::B10x6, + channel: AstcChannel::UnormSrgb, + }, + F::ASTC_10X8_SRGB_BLOCK => Tf::Astc { + block: AstcBlock::B10x8, + channel: AstcChannel::UnormSrgb, + }, + F::ASTC_10X10_SRGB_BLOCK => Tf::Astc { + block: AstcBlock::B10x10, + channel: AstcChannel::UnormSrgb, + }, + F::ASTC_12X10_SRGB_BLOCK => Tf::Astc { + block: AstcBlock::B12x10, + channel: AstcChannel::UnormSrgb, + }, + F::ASTC_12X12_SRGB_BLOCK => Tf::Astc { + block: AstcBlock::B12x12, + channel: AstcChannel::UnormSrgb, + }, + F::ASTC_4X4_SFLOAT_BLOCK_EXT => Tf::Astc { + block: AstcBlock::B4x4, + channel: AstcChannel::Hdr, + }, + F::ASTC_5X4_SFLOAT_BLOCK_EXT => Tf::Astc { + block: AstcBlock::B5x4, + channel: AstcChannel::Hdr, + }, + F::ASTC_5X5_SFLOAT_BLOCK_EXT => Tf::Astc { + block: AstcBlock::B5x5, + channel: AstcChannel::Hdr, + }, + F::ASTC_6X5_SFLOAT_BLOCK_EXT => Tf::Astc { + block: AstcBlock::B6x5, + channel: AstcChannel::Hdr, + }, + F::ASTC_6X6_SFLOAT_BLOCK_EXT => Tf::Astc { + block: AstcBlock::B6x6, + channel: AstcChannel::Hdr, + }, + F::ASTC_8X5_SFLOAT_BLOCK_EXT => Tf::Astc { + block: AstcBlock::B8x5, + channel: AstcChannel::Hdr, + }, + F::ASTC_8X6_SFLOAT_BLOCK_EXT => Tf::Astc { + block: AstcBlock::B8x6, + channel: AstcChannel::Hdr, + }, + F::ASTC_8X8_SFLOAT_BLOCK_EXT => Tf::Astc { + block: AstcBlock::B8x8, + channel: AstcChannel::Hdr, + }, + F::ASTC_10X5_SFLOAT_BLOCK_EXT => Tf::Astc { + block: AstcBlock::B10x5, + channel: AstcChannel::Hdr, + }, + F::ASTC_10X6_SFLOAT_BLOCK_EXT => Tf::Astc { + block: AstcBlock::B10x6, + channel: AstcChannel::Hdr, + }, + F::ASTC_10X8_SFLOAT_BLOCK_EXT => Tf::Astc { + block: AstcBlock::B10x8, + channel: AstcChannel::Hdr, + }, + F::ASTC_10X10_SFLOAT_BLOCK_EXT => Tf::Astc { + block: AstcBlock::B10x10, + channel: AstcChannel::Hdr, + }, + F::ASTC_12X10_SFLOAT_BLOCK_EXT => Tf::Astc { + block: AstcBlock::B12x10, + channel: AstcChannel::Hdr, + }, + F::ASTC_12X12_SFLOAT_BLOCK_EXT => Tf::Astc { + block: AstcBlock::B12x12, + channel: AstcChannel::Hdr, + }, + _ => return None, + }) +} + +fn wgpu_to_vulkan(format: wgpu::TextureFormat) -> Option { + // Copied with minor modification from: + // https://github.com/gfx-rs/wgpu/blob/a7defb723f856d946d6d220e9897d20dbb7b8f61/wgpu-hal/src/vulkan/conv.rs#L5-L151 + // license: MIT OR Apache-2.0 + use ash::vk::Format as F; + use wgpu::TextureFormat as Tf; + use wgpu::{AstcBlock, AstcChannel}; + Some(match format { + Tf::R8Unorm => F::R8_UNORM, + Tf::R8Snorm => F::R8_SNORM, + Tf::R8Uint => F::R8_UINT, + Tf::R8Sint => F::R8_SINT, + Tf::R16Uint => F::R16_UINT, + Tf::R16Sint => F::R16_SINT, + Tf::R16Unorm => F::R16_UNORM, + Tf::R16Snorm => F::R16_SNORM, + Tf::R16Float => F::R16_SFLOAT, + Tf::Rg8Unorm => F::R8G8_UNORM, + Tf::Rg8Snorm => F::R8G8_SNORM, + Tf::Rg8Uint => F::R8G8_UINT, + Tf::Rg8Sint => F::R8G8_SINT, + Tf::Rg16Unorm => F::R16G16_UNORM, + Tf::Rg16Snorm => F::R16G16_SNORM, + Tf::R32Uint => F::R32_UINT, + Tf::R32Sint => F::R32_SINT, + Tf::R32Float => F::R32_SFLOAT, + Tf::Rg16Uint => F::R16G16_UINT, + Tf::Rg16Sint => F::R16G16_SINT, + Tf::Rg16Float => F::R16G16_SFLOAT, + Tf::Rgba8Unorm => F::R8G8B8A8_UNORM, + Tf::Rgba8UnormSrgb => F::R8G8B8A8_SRGB, + Tf::Bgra8UnormSrgb => F::B8G8R8A8_SRGB, + Tf::Rgba8Snorm => F::R8G8B8A8_SNORM, + Tf::Bgra8Unorm => F::B8G8R8A8_UNORM, + Tf::Rgba8Uint => F::R8G8B8A8_UINT, + Tf::Rgba8Sint => F::R8G8B8A8_SINT, + Tf::Rgb10a2Unorm => F::A2B10G10R10_UNORM_PACK32, + Tf::Rg11b10Float => F::B10G11R11_UFLOAT_PACK32, + Tf::Rg32Uint => F::R32G32_UINT, + Tf::Rg32Sint => F::R32G32_SINT, + Tf::Rg32Float => F::R32G32_SFLOAT, + Tf::Rgba16Uint => F::R16G16B16A16_UINT, + Tf::Rgba16Sint => F::R16G16B16A16_SINT, + Tf::Rgba16Unorm => F::R16G16B16A16_UNORM, + Tf::Rgba16Snorm => F::R16G16B16A16_SNORM, + Tf::Rgba16Float => F::R16G16B16A16_SFLOAT, + Tf::Rgba32Uint => F::R32G32B32A32_UINT, + Tf::Rgba32Sint => F::R32G32B32A32_SINT, + Tf::Rgba32Float => F::R32G32B32A32_SFLOAT, + Tf::Depth32Float => F::D32_SFLOAT, + Tf::Depth32FloatStencil8 => F::D32_SFLOAT_S8_UINT, + Tf::Depth24Plus | Tf::Depth24PlusStencil8 | Tf::Stencil8 => return None, // Dependent on device properties + Tf::Depth16Unorm => F::D16_UNORM, + Tf::Rgb9e5Ufloat => F::E5B9G9R9_UFLOAT_PACK32, + Tf::Bc1RgbaUnorm => F::BC1_RGBA_UNORM_BLOCK, + Tf::Bc1RgbaUnormSrgb => F::BC1_RGBA_SRGB_BLOCK, + Tf::Bc2RgbaUnorm => F::BC2_UNORM_BLOCK, + Tf::Bc2RgbaUnormSrgb => F::BC2_SRGB_BLOCK, + Tf::Bc3RgbaUnorm => F::BC3_UNORM_BLOCK, + Tf::Bc3RgbaUnormSrgb => F::BC3_SRGB_BLOCK, + Tf::Bc4RUnorm => F::BC4_UNORM_BLOCK, + Tf::Bc4RSnorm => F::BC4_SNORM_BLOCK, + Tf::Bc5RgUnorm => F::BC5_UNORM_BLOCK, + Tf::Bc5RgSnorm => F::BC5_SNORM_BLOCK, + Tf::Bc6hRgbUfloat => F::BC6H_UFLOAT_BLOCK, + Tf::Bc6hRgbFloat => F::BC6H_SFLOAT_BLOCK, + Tf::Bc7RgbaUnorm => F::BC7_UNORM_BLOCK, + Tf::Bc7RgbaUnormSrgb => F::BC7_SRGB_BLOCK, + Tf::Etc2Rgb8Unorm => F::ETC2_R8G8B8_UNORM_BLOCK, + Tf::Etc2Rgb8UnormSrgb => F::ETC2_R8G8B8_SRGB_BLOCK, + Tf::Etc2Rgb8A1Unorm => F::ETC2_R8G8B8A1_UNORM_BLOCK, + Tf::Etc2Rgb8A1UnormSrgb => F::ETC2_R8G8B8A1_SRGB_BLOCK, + Tf::Etc2Rgba8Unorm => F::ETC2_R8G8B8A8_UNORM_BLOCK, + Tf::Etc2Rgba8UnormSrgb => F::ETC2_R8G8B8A8_SRGB_BLOCK, + Tf::EacR11Unorm => F::EAC_R11_UNORM_BLOCK, + Tf::EacR11Snorm => F::EAC_R11_SNORM_BLOCK, + Tf::EacRg11Unorm => F::EAC_R11G11_UNORM_BLOCK, + Tf::EacRg11Snorm => F::EAC_R11G11_SNORM_BLOCK, + Tf::Astc { block, channel } => match channel { + AstcChannel::Unorm => match block { + AstcBlock::B4x4 => F::ASTC_4X4_UNORM_BLOCK, + AstcBlock::B5x4 => F::ASTC_5X4_UNORM_BLOCK, + AstcBlock::B5x5 => F::ASTC_5X5_UNORM_BLOCK, + AstcBlock::B6x5 => F::ASTC_6X5_UNORM_BLOCK, + AstcBlock::B6x6 => F::ASTC_6X6_UNORM_BLOCK, + AstcBlock::B8x5 => F::ASTC_8X5_UNORM_BLOCK, + AstcBlock::B8x6 => F::ASTC_8X6_UNORM_BLOCK, + AstcBlock::B8x8 => F::ASTC_8X8_UNORM_BLOCK, + AstcBlock::B10x5 => F::ASTC_10X5_UNORM_BLOCK, + AstcBlock::B10x6 => F::ASTC_10X6_UNORM_BLOCK, + AstcBlock::B10x8 => F::ASTC_10X8_UNORM_BLOCK, + AstcBlock::B10x10 => F::ASTC_10X10_UNORM_BLOCK, + AstcBlock::B12x10 => F::ASTC_12X10_UNORM_BLOCK, + AstcBlock::B12x12 => F::ASTC_12X12_UNORM_BLOCK, + }, + AstcChannel::UnormSrgb => match block { + AstcBlock::B4x4 => F::ASTC_4X4_SRGB_BLOCK, + AstcBlock::B5x4 => F::ASTC_5X4_SRGB_BLOCK, + AstcBlock::B5x5 => F::ASTC_5X5_SRGB_BLOCK, + AstcBlock::B6x5 => F::ASTC_6X5_SRGB_BLOCK, + AstcBlock::B6x6 => F::ASTC_6X6_SRGB_BLOCK, + AstcBlock::B8x5 => F::ASTC_8X5_SRGB_BLOCK, + AstcBlock::B8x6 => F::ASTC_8X6_SRGB_BLOCK, + AstcBlock::B8x8 => F::ASTC_8X8_SRGB_BLOCK, + AstcBlock::B10x5 => F::ASTC_10X5_SRGB_BLOCK, + AstcBlock::B10x6 => F::ASTC_10X6_SRGB_BLOCK, + AstcBlock::B10x8 => F::ASTC_10X8_SRGB_BLOCK, + AstcBlock::B10x10 => F::ASTC_10X10_SRGB_BLOCK, + AstcBlock::B12x10 => F::ASTC_12X10_SRGB_BLOCK, + AstcBlock::B12x12 => F::ASTC_12X12_SRGB_BLOCK, + }, + AstcChannel::Hdr => match block { + AstcBlock::B4x4 => F::ASTC_4X4_SFLOAT_BLOCK_EXT, + AstcBlock::B5x4 => F::ASTC_5X4_SFLOAT_BLOCK_EXT, + AstcBlock::B5x5 => F::ASTC_5X5_SFLOAT_BLOCK_EXT, + AstcBlock::B6x5 => F::ASTC_6X5_SFLOAT_BLOCK_EXT, + AstcBlock::B6x6 => F::ASTC_6X6_SFLOAT_BLOCK_EXT, + AstcBlock::B8x5 => F::ASTC_8X5_SFLOAT_BLOCK_EXT, + AstcBlock::B8x6 => F::ASTC_8X6_SFLOAT_BLOCK_EXT, + AstcBlock::B8x8 => F::ASTC_8X8_SFLOAT_BLOCK_EXT, + AstcBlock::B10x5 => F::ASTC_10X5_SFLOAT_BLOCK_EXT, + AstcBlock::B10x6 => F::ASTC_10X6_SFLOAT_BLOCK_EXT, + AstcBlock::B10x8 => F::ASTC_10X8_SFLOAT_BLOCK_EXT, + AstcBlock::B10x10 => F::ASTC_10X10_SFLOAT_BLOCK_EXT, + AstcBlock::B12x10 => F::ASTC_12X10_SFLOAT_BLOCK_EXT, + AstcBlock::B12x12 => F::ASTC_12X12_SFLOAT_BLOCK_EXT, + }, + }, + }) +} diff --git a/src/openxr/resources.rs b/src/openxr/resources.rs new file mode 100644 index 0000000..6ee6304 --- /dev/null +++ b/src/openxr/resources.rs @@ -0,0 +1,251 @@ +use std::sync::{Arc, Mutex}; + +use ash::vk::Handle; +use bevy::prelude::*; + +use openxr::{AnyGraphics, Vulkan}; + +use crate::openxr::init::Version; + +use super::extensions::XrExtensions; +use super::init::{self, AppInfo, GraphicsBackend, GraphicsExt, XrInitError}; +type Result = std::result::Result; +use super::types::*; + +#[derive(Deref, Clone)] +pub struct XrEntry(openxr::Entry); + +impl XrEntry { + pub fn enumerate_extensions(&self) -> Result { + Ok(self.0.enumerate_extensions().map(Into::into)?) + } + + pub fn create_instance( + entry: XrEntry, + app_info: AppInfo, + exts: XrExtensions, + backend: GraphicsBackend, + ) -> Result { + let available_exts = entry.enumerate_extensions()?; + + if !backend.is_available(&available_exts) { + return Err(XrInitError::UnavailableBackend(backend)); + } + + let required_exts = exts | backend.required_exts(); + + let instance = entry.create_instance( + &openxr::ApplicationInfo { + application_name: app_info.name, + application_version: app_info.version.to_u32(), + engine_name: "Bevy", + engine_version: Version::BEVY.to_u32(), + }, + &required_exts.into(), + &[], + )?; + + Ok(XrInstance(instance, backend)) + } + + pub fn available_backends(&self) -> Result> { + Ok(GraphicsBackend::available_backends( + &self.enumerate_extensions()?, + )) + } +} + +impl From for XrEntry { + fn from(value: openxr::Entry) -> Self { + Self(value) + } +} + +#[derive(Resource, Deref, Clone)] +pub struct XrInstance( + #[deref] pub(crate) openxr::Instance, + pub(crate) GraphicsBackend, +); + +impl XrInstance { + pub fn create_session( + &self, + app_info: AppInfo, + system_id: openxr::SystemId, + format: wgpu::TextureFormat, + resolution: UVec2, + ) -> Result<( + wgpu::Device, + wgpu::Queue, + wgpu::Adapter, + wgpu::Instance, + openxr::Session, + openxr::FrameWaiter, + FrameStreamInner, + )> { + match self.1 { + GraphicsBackend::Vulkan => { + openxr::Vulkan::create_session(app_info, &self.0, system_id, format, resolution) + } + } + } +} + +#[derive(Resource, Deref, Clone)] +pub struct XrSession( + #[deref] pub(crate) openxr::Session, + pub(crate) TypedSession, +); + +impl XrSession { + pub fn enumerate_swapchain_formats(&self) -> Result> { + self.1.enumerate_swapchain_formats() + } + + pub fn create_swapchain(&self, info: SwapchainCreateInfo) -> Result { + self.1.create_swapchain(info) + } +} + +#[derive(Clone)] +pub enum TypedSession { + Vulkan(openxr::Session), +} + +impl TypedSession { + pub fn into_any_graphics(&self) -> openxr::Session { + match self { + TypedSession::Vulkan(session) => session.clone().into_any_graphics(), + } + } + + pub fn enumerate_swapchain_formats(&self) -> Result> { + Ok(match self { + TypedSession::Vulkan(session) => init::vulkan::enumerate_swapchain_formats(session), + }?) + } + + pub fn create_swapchain(&self, info: SwapchainCreateInfo) -> Result { + Ok(match self { + TypedSession::Vulkan(session) => { + XrSwapchain::Vulkan(session.create_swapchain(&info.try_into()?)?) + } + }) + } +} + +#[derive(Resource, Default, Deref)] +pub struct Framebuffers(pub Vec); + +#[derive(Clone)] +pub struct Swapchain { + pub inner: Arc>, + pub format: wgpu::TextureFormat, + pub resolution: UVec2, +} + +pub enum FrameStreamInner { + Vulkan(openxr::FrameStream), +} + +pub enum XrSwapchain { + Vulkan(openxr::Swapchain), +} + +impl XrSwapchain { + pub fn acquire_image(&mut self) -> Result { + Ok(match self { + XrSwapchain::Vulkan(swap) => swap.acquire_image()?, + }) + } + + pub fn wait_image(&mut self, timeout: openxr::Duration) -> Result<()> { + Ok(match self { + XrSwapchain::Vulkan(swap) => swap.wait_image(timeout)?, + }) + } + + pub fn release_image(&mut self) -> Result<()> { + Ok(match self { + XrSwapchain::Vulkan(swap) => swap.release_image()?, + }) + } + + pub fn enumerate_images( + &mut self, + device: wgpu::Device, + format: wgpu::TextureFormat, + resolution: UVec2, + ) -> Result> { + match self { + XrSwapchain::Vulkan(swap) => swap.enumerate_imgs(device, format, resolution), + } + } +} + +trait EnumerateImages { + fn enumerate_imgs( + &mut self, + device: wgpu::Device, + format: wgpu::TextureFormat, + resolution: UVec2, + ) -> Result>; +} + +impl EnumerateImages for openxr::Swapchain { + fn enumerate_imgs( + &mut self, + device: wgpu::Device, + format: wgpu::TextureFormat, + resolution: UVec2, + ) -> Result> { + let images = self.enumerate_images()?; + let images = images.into_iter().map(|color_image| { + let color_image = ash::vk::Image::from_raw(color_image); + let wgpu_hal_texture = unsafe { + ::Device::texture_from_raw( + color_image, + &wgpu_hal::TextureDescriptor { + label: Some("VR Swapchain"), + size: wgpu::Extent3d { + width: resolution.x, + height: resolution.y, + depth_or_array_layers: 2, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: format, + usage: wgpu_hal::TextureUses::COLOR_TARGET + | wgpu_hal::TextureUses::COPY_DST, + memory_flags: wgpu_hal::MemoryFlags::empty(), + view_formats: vec![], + }, + None, + ) + }; + let texture = unsafe { + device.create_texture_from_hal::( + wgpu_hal_texture, + &wgpu::TextureDescriptor { + label: Some("VR Swapchain"), + size: wgpu::Extent3d { + width: resolution.x, + height: resolution.y, + depth_or_array_layers: 2, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: format, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT + | wgpu::TextureUsages::COPY_DST, + view_formats: &[], + }, + ) + }; + texture + }); + Ok(images.collect()) + } +} diff --git a/src/openxr/types.rs b/src/openxr/types.rs new file mode 100644 index 0000000..334ef3d --- /dev/null +++ b/src/openxr/types.rs @@ -0,0 +1,35 @@ +pub use openxr::{SwapchainCreateFlags, SwapchainUsageFlags, SystemId}; + +use super::init::{GraphicsExt, XrInitError}; + +#[derive(Debug, Copy, Clone)] +pub struct SwapchainCreateInfo { + pub create_flags: SwapchainCreateFlags, + pub usage_flags: SwapchainUsageFlags, + pub format: wgpu::TextureFormat, + pub sample_count: u32, + pub width: u32, + pub height: u32, + pub face_count: u32, + pub array_size: u32, + pub mip_count: u32, +} + +impl TryFrom for openxr::SwapchainCreateInfo { + type Error = XrInitError; + + fn try_from(value: SwapchainCreateInfo) -> Result { + Ok(openxr::SwapchainCreateInfo { + create_flags: value.create_flags, + usage_flags: value.usage_flags, + format: G::from_wgpu_format(value.format) + .ok_or(XrInitError::UnsupportedTextureFormat(value.format))?, + sample_count: value.sample_count, + width: value.width, + height: value.height, + face_count: value.face_count, + array_size: value.array_size, + mip_count: value.mip_count, + }) + } +} diff --git a/src/render.rs b/src/render.rs new file mode 100644 index 0000000..d79c6f8 --- /dev/null +++ b/src/render.rs @@ -0,0 +1,19 @@ +use bevy::ecs::system::Resource; +use bevy::math::Mat4; +use bevy::prelude::{Deref, DerefMut}; +use bevy::render::camera::{RenderTarget, Viewport}; + +use crate::types::Pose; + +pub(crate) const XR_TEXTURE_VIEW_INDEX: u32 = 1208214591; + +#[derive(Debug, Clone)] +pub struct XrView { + pub projection_matrix: Mat4, + pub pose: Pose, + pub render_target: RenderTarget, + pub view_port: Option, +} + +#[derive(Deref, DerefMut, Default, Debug, Clone, Resource)] +pub struct XrViews(#[deref] pub Vec); diff --git a/src/types.rs b/src/types.rs index 18cd965..f637f5f 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,8 +1,16 @@ use bevy::math::{Quat, Vec3}; +#[derive(Debug, Clone)] pub struct Pose { pub translation: Vec3, pub rotation: Quat, } pub struct Haptic; + +#[derive(Clone, Copy, Debug)] +pub enum BlendMode { + Opaque, + Additive, + AlphaBlend, +} diff --git a/src/webxr.rs b/src/webxr.rs index 72f4136..3378c77 100644 --- a/src/webxr.rs +++ b/src/webxr.rs @@ -1,3 +1,8 @@ +pub mod render; +mod resources; + +pub use resources::*; + use std::cell::RefCell; use std::rc::Rc; use std::sync::Mutex; @@ -16,8 +21,8 @@ use wasm_bindgen::closure::Closure; use wasm_bindgen::{JsCast, JsValue}; use wasm_bindgen_futures::JsFuture; use web_sys::{ - HtmlCanvasElement, WebGl2RenderingContext, XrFrame, XrReferenceSpace, XrReferenceSpaceType, - XrRenderStateInit, XrSession, XrSessionMode, XrWebGlLayer, + HtmlCanvasElement, WebGl2RenderingContext, XrReferenceSpaceType, XrRenderStateInit, + XrSessionMode, }; use winit::platform::web::WindowExtWebSys; @@ -33,12 +38,8 @@ impl Plugin for XrInitPlugin { app.set_runner(webxr_runner); app.insert_non_send_resource(future_session.clone()); bevy::tasks::IoTaskPool::get().spawn_local(async move { - let result = init_webxr( - canvas, - XrSessionMode::ImmersiveVr, - XrReferenceSpaceType::Local, - ) - .await; + let result = + init_webxr(canvas, XrSessionMode::Inline, XrReferenceSpaceType::Viewer).await; *future_session.0.lock().unwrap() = Some(result); }); } @@ -53,11 +54,12 @@ impl Plugin for XrInitPlugin { fn finish(&self, app: &mut App) { info!("finishing"); - if let Some(Ok((session, reference_space))) = app + if let Some(result) = app .world .remove_non_send_resource::() .and_then(|fxr| fxr.0.lock().unwrap().take()) { + let (session, reference_space) = result.unwrap(); app.insert_non_send_resource(session.clone()) .insert_non_send_resource(reference_space.clone()); app.sub_app_mut(RenderApp) @@ -97,13 +99,14 @@ fn webxr_runner(mut app: App) { fn run_xr_app(mut app: App) { let session = app.world.non_send_resource::().clone(); - let inner_closure: Rc>>> = + let inner_closure: Rc>>> = Rc::new(RefCell::new(None)); let closure = inner_closure.clone(); - *closure.borrow_mut() = Some(Closure::new(move |_time, frame: XrFrame| { + *closure.borrow_mut() = Some(Closure::new(move |_time, frame: web_sys::XrFrame| { let session = frame.session(); - app.insert_non_send_resource(frame); - info!("update"); + app.insert_non_send_resource(XrFrame(frame.clone())); + app.sub_app_mut(RenderApp) + .insert_non_send_resource(XrFrame(frame)); app.update(); session.request_animation_frame( inner_closure @@ -143,7 +146,7 @@ async fn init_webxr( } info!("creating session"); - let session: XrSession = JsFuture::from(xr.request_session(mode)).await?.into(); + let session: web_sys::XrSession = JsFuture::from(xr.request_session(mode)).await?.into(); info!("creating gl"); let gl: WebGl2RenderingContext = { @@ -161,7 +164,7 @@ async fn init_webxr( .dyn_into()? }; - let xr_gl_layer = XrWebGlLayer::new_with_web_gl2_rendering_context(&session, &gl)?; + let xr_gl_layer = web_sys::XrWebGlLayer::new_with_web_gl2_rendering_context(&session, &gl)?; let mut render_state_init = XrRenderStateInit::new(); render_state_init.base_layer(Some(&xr_gl_layer)); session.update_render_state_with_state(&render_state_init); @@ -171,5 +174,5 @@ async fn init_webxr( .into(); info!("finished"); - Ok((session, reference_space)) + Ok((XrSession(session), XrReferenceSpace(reference_space))) } diff --git a/src/webxr/render.rs b/src/webxr/render.rs new file mode 100644 index 0000000..7b46e2a --- /dev/null +++ b/src/webxr/render.rs @@ -0,0 +1,154 @@ +use crate::render::{XrView, XrViews}; +use crate::types::Pose; + +use super::resources::*; + +use bevy::app::{App, Plugin, PreUpdate}; +use bevy::ecs::schedule::IntoSystemConfigs; +use bevy::ecs::system::{NonSend, Res, ResMut}; +use bevy::ecs::world::World; +use bevy::math::{quat, uvec2, vec3, Mat4}; +use bevy::render::camera::{ + ManualTextureView, ManualTextureViewHandle, ManualTextureViews, RenderTarget, Viewport, +}; +use bevy::render::renderer::RenderDevice; +use bevy::utils::default; + +pub const XR_TEXTURE_VIEW_HANDLE: ManualTextureViewHandle = + ManualTextureViewHandle(crate::render::XR_TEXTURE_VIEW_INDEX); + +pub struct XrRenderingPlugin; + +impl Plugin for XrRenderingPlugin { + fn build(&self, app: &mut App) { + app.init_resource::(); + app.add_systems( + PreUpdate, + (insert_gl_layer, update_manual_texture_views, insert_views).chain(), + ); + } +} + +pub fn insert_gl_layer(world: &mut World) { + let gl_layer = world + .non_send_resource::() + .session() + .render_state() + .base_layer() + .unwrap(); + world.insert_non_send_resource(XrWebGlLayer(gl_layer)); +} + +pub fn update_manual_texture_views( + gl_layer: NonSend, + render_device: Res, + mut manual_tex_view: ResMut, +) { + let dest_texture = create_framebuffer_texture(render_device.wgpu_device(), &gl_layer); + let view = dest_texture.create_view(&default()); + + manual_tex_view.insert( + XR_TEXTURE_VIEW_HANDLE, + ManualTextureView::with_default_format( + view.into(), + uvec2(gl_layer.framebuffer_width(), gl_layer.framebuffer_height()), + ), + ); +} + +pub fn insert_views( + gl_layer: NonSend, + reference_space: NonSend, + frame: NonSend, + mut xr_views: ResMut, +) { + let Some(viewer_pose) = frame.get_viewer_pose(&reference_space) else { + return; + }; + + let views = viewer_pose + .views() + .into_iter() + .map(Into::::into) + .map(|view| { + let transform = view.transform(); + let position = transform.position(); + let orientation = transform.orientation(); + let viewport = gl_layer + .get_viewport(&view) + .map(|viewport| Viewport { + physical_position: uvec2(viewport.x() as u32, viewport.y() as u32), + physical_size: uvec2(viewport.width() as u32, viewport.height() as u32), + ..Default::default() + }) + .unwrap(); + XrView { + projection_matrix: Mat4::from_cols_array( + &view.projection_matrix().try_into().unwrap(), + ), + pose: Pose { + translation: vec3( + position.x() as f32, + position.y() as f32, + position.z() as f32, + ), + rotation: quat( + orientation.x() as f32, + orientation.y() as f32, + orientation.z() as f32, + orientation.w() as f32, + ), + }, + render_target: RenderTarget::TextureView(XR_TEXTURE_VIEW_HANDLE), + view_port: Some(viewport), + } + }) + .collect(); + xr_views.0 = views; +} + +pub fn create_framebuffer_texture(device: &wgpu::Device, gl_layer: &XrWebGlLayer) -> wgpu::Texture { + unsafe { + device.create_texture_from_hal::( + wgpu_hal::gles::Texture { + inner: wgpu_hal::gles::TextureInner::ExternalFramebuffer { + // inner: framebuffer, + inner: gl_layer.framebuffer_unwrapped(), + // inner: framebuffer.as_ref().unwrap().clone(), + }, + mip_level_count: 1, + array_layer_count: 1, + format: wgpu::TextureFormat::Rgba8Unorm, //TODO check this is ok, different from bevy default + format_desc: wgpu_hal::gles::TextureFormatDesc { + internal: glow::RGBA, + external: glow::RGBA, + data_type: glow::UNSIGNED_BYTE, + }, + copy_size: wgpu_hal::CopyExtent { + width: gl_layer.framebuffer_width(), + height: gl_layer.framebuffer_height(), + depth: 1, + }, + drop_guard: None, + is_cubemap: false, + }, + &wgpu::TextureDescriptor { + label: Some("framebuffer (color)"), + size: wgpu::Extent3d { + width: gl_layer.framebuffer_width(), + height: gl_layer.framebuffer_height(), + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Rgba8UnormSrgb, + view_formats: &[wgpu::TextureFormat::Rgba8UnormSrgb], + usage: wgpu::TextureUsages::RENDER_ATTACHMENT + | wgpu::TextureUsages::TEXTURE_BINDING, + // | wgpu::TextureUsages::COPY_SRC, + // | wgpu::TextureUsages::COPY_DST, + }, + ) + } +} diff --git a/src/webxr/resources.rs b/src/webxr/resources.rs new file mode 100644 index 0000000..3a894a4 --- /dev/null +++ b/src/webxr/resources.rs @@ -0,0 +1,25 @@ +use bevy::prelude::{Deref, DerefMut}; +use web_sys::WebGlFramebuffer; + +#[derive(Deref, DerefMut, Clone)] +pub struct XrSession(#[deref] pub(crate) web_sys::XrSession); + +#[derive(Deref, DerefMut, Clone)] +pub struct XrReferenceSpace(#[deref] pub(crate) web_sys::XrReferenceSpace); + +#[derive(Deref, DerefMut, Clone)] +pub struct XrFrame(#[deref] pub(crate) web_sys::XrFrame); + +#[derive(Deref, DerefMut, Clone)] +pub struct XrWebGlLayer(#[deref] pub(crate) web_sys::XrWebGlLayer); + +impl XrWebGlLayer { + pub(crate) fn framebuffer_unwrapped(&self) -> WebGlFramebuffer { + js_sys::Reflect::get(&self, &"framebuffer".into()) + .unwrap() + .into() + } +} + +#[derive(Clone)] +pub struct XrView(pub(crate) web_sys::XrView);