mod graphics; mod utils; use std::{marker::PhantomData, rc::Rc, sync::Mutex}; use glam::{Mat4, UVec2, Vec2}; use hashbrown::{hash_map, HashMap}; use openxr::{EnvironmentBlendMode, Vector2f}; use tracing::{info, info_span, warn}; use crate::{backend::oxr::graphics::VIEW_TYPE, path, prelude::*}; pub struct OXrEntry(openxr::Entry); impl OXrEntry { pub fn new() -> Self { #[cfg(feature = "linked")] return OXrEntry(openxr::Entry::linked()); #[cfg(not(feature = "linked"))] return OXrEntry(unsafe { openxr::Entry::load().expect("Failed to load OpenXR runtime") }); } } impl Into for ExtensionSet { fn into(self) -> openxr::ExtensionSet { let mut set = openxr::ExtensionSet::default(); set.khr_vulkan_enable2 = self.vulkan; set } } impl EntryTrait for OXrEntry { fn available_extensions(&self) -> Result { // self.0.enumerate_extensions(); Ok(ExtensionSet::default()) } fn create_instance(&self, exts: ExtensionSet) -> Result { #[allow(unused_mut)] let mut enabled_extensions: openxr::ExtensionSet = exts.into(); #[cfg(target_os = "android")] { enabled_extensions.khr_android_create_instance = true; } let xr_instance = self.0.create_instance( &openxr::ApplicationInfo { application_name: "bevy", ..Default::default() }, &enabled_extensions, &[], )?; Ok(OXrInstance(xr_instance, exts).into()) } } #[derive(Clone)] pub struct OXrInstance(openxr::Instance, ExtensionSet); impl InstanceTrait for OXrInstance { fn entry(&self) -> Entry { OXrEntry(self.0.entry().clone()).into() } fn enabled_extensions(&self) -> ExtensionSet { self.1 } fn create_session(&self, info: SessionCreateInfo) -> Result { graphics::init_oxr_graphics(self.clone(), self.1, info.texture_format).map(Into::into) } } pub(crate) enum UntypedOXrAction { Haptics(openxr::Action), Pose(openxr::Action), Float(openxr::Action), Bool(openxr::Action), Vec2(openxr::Action), } #[derive(Default)] pub(crate) struct BindingState { sessions_attached: bool, bindings: Option, map: HashMap, } pub struct OXrSession { pub(crate) instance: Instance, // this could definitely be done better pub(crate) inner_instance: openxr::Instance, pub(crate) session: openxr::Session, pub(crate) action_sets: Mutex>, pub(crate) render_resources: Mutex< Option<( wgpu::Device, wgpu::Queue, wgpu::AdapterInfo, wgpu::Adapter, wgpu::Instance, )>, >, pub(crate) bindings: Rc>, pub(crate) frame_state: Rc>, pub(crate) views: Mutex<[openxr::View; 2]>, pub(crate) frame_waiter: Mutex, pub(crate) swapchain: graphics::Swapchain, pub(crate) stage: Rc, pub(crate) head: openxr::Space, pub(crate) resolution: UVec2, pub(crate) blend_mode: EnvironmentBlendMode, pub(crate) format: wgpu::TextureFormat, } impl SessionTrait for OXrSession { fn instance(&self) -> &Instance { &self.instance } fn get_render_resources( &self, ) -> Option<( wgpu::Device, wgpu::Queue, wgpu::AdapterInfo, wgpu::Adapter, wgpu::Instance, )> { std::mem::take(&mut self.render_resources.lock().unwrap()) } fn create_input(&self, bindings: Bindings) -> Result { let mut binding_state = self.bindings.lock().unwrap(); if binding_state.bindings.is_none() { binding_state.bindings = Some(bindings); } else { Err(XrError::Placeholder)? } let action_set = self .inner_instance .create_action_set("xr_input", "XR Input", 0)?; self.action_sets.lock().unwrap().push(action_set.clone()); Ok(OXrInput { inner_session: self.session.clone().into_any_graphics(), action_set, bindings: self.bindings.clone(), stage: self.stage.clone(), frame_state: self.frame_state.clone(), } .into()) } fn begin_frame(&self) -> Result<(View, View)> { { let mut bindings = self.bindings.lock().unwrap(); if !bindings.sessions_attached { if let Some(interaction_profile) = bindings.bindings { let _span = info_span!("xr_attach_actions"); let controller_bindings: Vec = bindings .map .iter() .filter_map(|(path, action)| { let path = match self.inner_instance.string_to_path(path) { Ok(path) => path, Err(e) => { warn!("{e}"); return None; } }; Some(match action { UntypedOXrAction::Haptics(action) => { openxr::Binding::new(action, path) } UntypedOXrAction::Pose(action) => { openxr::Binding::new(action, path) } UntypedOXrAction::Float(action) => { openxr::Binding::new(action, path) } UntypedOXrAction::Bool(action) => { openxr::Binding::new(action, path) } UntypedOXrAction::Vec2(action) => { openxr::Binding::new(action, path) } }) }) .collect(); self.inner_instance.suggest_interaction_profile_bindings( self.inner_instance .string_to_path(interaction_profile.get_interaction_profile())?, &controller_bindings, )?; let action_sets = self.action_sets.lock().unwrap(); self.session .attach_action_sets(&action_sets.iter().collect::>())?; } bindings.sessions_attached = true; } } { let _span = info_span!("xr_poll_events"); while let Some(event) = self .inner_instance .poll_event(&mut Default::default()) .unwrap() { use openxr::Event::*; match event { SessionStateChanged(e) => { // Session state change is where we can begin and end sessions, as well as // find quit messages! info!("entered XR state {:?}", e.state()); match e.state() { openxr::SessionState::READY => { self.session.begin(VIEW_TYPE).unwrap(); } openxr::SessionState::STOPPING => { self.session.end().unwrap(); } openxr::SessionState::EXITING | openxr::SessionState::LOSS_PENDING => { return Err(XrError::Placeholder); } _ => {} } } InstanceLossPending(_) => return Err(XrError::Placeholder), EventsLost(e) => { warn!("lost {} XR events", e.lost_event_count()); } _ => {} } } } { let _span = info_span!("xr_wait_frame").entered(); *self.frame_state.lock().unwrap() = match self.frame_waiter.lock().unwrap().wait() { Ok(a) => a, Err(e) => { warn!("error: {}", e); return Err(XrError::Placeholder); } }; } { let _span = info_span!("xr_begin_frame").entered(); self.swapchain.begin().unwrap() } { let _span = info_span!("xr_acquire_image").entered(); self.swapchain.acquire_image().unwrap() } { let _span = info_span!("xr_wait_image").entered(); self.swapchain.wait_image().unwrap(); } { let action_sets = self.action_sets.lock().unwrap(); self.session .sync_actions(&action_sets.iter().map(Into::into).collect::>())?; } let views = { let _span = info_span!("xr_locate_views").entered(); self.session .locate_views( VIEW_TYPE, self.frame_state.lock().unwrap().predicted_display_time, &self.stage, ) .unwrap() .1 }; *self.views.lock().unwrap() = [views[0].clone(), views[1].clone()]; { let _span = info_span!("xr_update_manual_texture_views").entered(); let (left, right) = self.swapchain.get_render_views(); let left = OXrView { texture: Mutex::new(Some(left)), view: views[0], resolution: self.resolution, format: self.format, }; let right = OXrView { texture: Mutex::new(Some(right)), view: views[1], resolution: self.resolution, format: self.format, }; Ok((left.into(), right.into())) } } fn end_frame(&self) -> Result<()> { { let _span = info_span!("xr_release_image").entered(); self.swapchain.release_image().unwrap(); } { let _span = info_span!("xr_end_frame").entered(); let frame_state = self.frame_state.lock().unwrap().clone(); let result = self.swapchain.end( frame_state.predicted_display_time, &*self.views.lock().unwrap(), &self.stage, self.resolution, self.blend_mode, frame_state.should_render, // passthrough_layer.map(|p| p.into_inner()), ); match result { Ok(_) => {} Err(e) => warn!("error: {}", e), } } Ok(()) } fn resolution(&self) -> UVec2 { self.resolution } fn format(&self) -> wgpu::TextureFormat { self.format } } pub struct OXrInput { inner_session: openxr::Session, action_set: openxr::ActionSet, bindings: Rc>, stage: Rc, frame_state: Rc>, } impl InputTrait for OXrInput { fn create_action_haptics(&self, path: path::UntypedActionPath) -> Result> { let mut bindings = self.bindings.lock().unwrap(); let action = match bindings.map.entry(path.into_xr_path()) { hash_map::Entry::Occupied(entry) => match entry.get() { UntypedOXrAction::Haptics(action) => action.clone(), _ => Err(XrError::Placeholder)?, }, hash_map::Entry::Vacant(entry) => { let name = &path.into_name(); let action = self .action_set .create_action::(name, name, &[])?; entry.insert(UntypedOXrAction::Haptics(action.clone())); action } }; Ok(OXrHaptics(action, self.inner_session.clone()).into()) } fn create_action_pose(&self, path: path::UntypedActionPath) -> Result> { let mut bindings = self.bindings.lock().unwrap(); let action = match bindings.map.entry(path.into_xr_path()) { hash_map::Entry::Occupied(entry) => match entry.get() { UntypedOXrAction::Pose(action) => action.clone(), _ => Err(XrError::Placeholder)?, }, hash_map::Entry::Vacant(entry) => { let name = &path.into_name(); let action = self .action_set .create_action::(name, name, &[])?; entry.insert(UntypedOXrAction::Pose(action.clone())); action } }; Ok(OXrPoseAction { pose_space: action.create_space( self.inner_session.clone(), openxr::Path::NULL, openxr::Posef::IDENTITY, )?, stage: self.stage.clone(), frame_state: self.frame_state.clone(), } .into()) } fn create_action_float(&self, path: path::UntypedActionPath) -> Result> { let mut bindings = self.bindings.lock().unwrap(); let action = match bindings.map.entry(path.into_xr_path()) { hash_map::Entry::Occupied(entry) => match entry.get() { UntypedOXrAction::Float(action) => action.clone(), _ => Err(XrError::Placeholder)?, }, hash_map::Entry::Vacant(entry) => { let name = &path.into_name(); let action = self.action_set.create_action::(name, name, &[])?; entry.insert(UntypedOXrAction::Float(action.clone())); action } }; Ok(OXrAction(action, self.inner_session.clone(), PhantomData).into()) } fn create_action_bool(&self, path: path::UntypedActionPath) -> Result> { let mut bindings = self.bindings.lock().unwrap(); let action = match bindings.map.entry(path.into_xr_path()) { hash_map::Entry::Occupied(entry) => match entry.get() { UntypedOXrAction::Bool(action) => action.clone(), _ => Err(XrError::Placeholder)?, }, hash_map::Entry::Vacant(entry) => { let name = &path.into_name(); let action = self.action_set.create_action::(name, name, &[])?; entry.insert(UntypedOXrAction::Bool(action.clone())); action } }; Ok(OXrAction(action, self.inner_session.clone(), PhantomData).into()) } fn create_action_vec2(&self, path: path::UntypedActionPath) -> Result> { let mut bindings = self.bindings.lock().unwrap(); let action = match bindings.map.entry(path.into_xr_path()) { hash_map::Entry::Occupied(entry) => match entry.get() { UntypedOXrAction::Vec2(action) => action.clone(), _ => Err(XrError::Placeholder)?, }, hash_map::Entry::Vacant(entry) => { let name = &path.into_name(); let action = self.action_set.create_action::(name, name, &[])?; entry.insert(UntypedOXrAction::Vec2(action.clone())); action } }; Ok(OXrAction::(action, self.inner_session.clone(), PhantomData).into()) } } pub struct OXrHaptics( openxr::Action, openxr::Session, ); impl HapticTrait for OXrHaptics {} pub struct OXrPoseAction { pose_space: openxr::Space, stage: Rc, frame_state: Rc>, } impl ActionInputTrait for OXrPoseAction { fn get(&self) -> Result { let pose = self .pose_space .locate( &self.stage, self.frame_state.lock().unwrap().predicted_display_time, )? .pose; Ok(pose.into()) } } pub struct OXrAction( openxr::Action, openxr::Session, PhantomData, ); impl ActionInputTrait for OXrAction { fn get(&self) -> Result { Ok(self.0.state(&self.1, openxr::Path::NULL)?.current_state) } } impl ActionInputTrait for OXrAction { fn get(&self) -> Result { let Vector2f { x, y } = self.0.state(&self.1, openxr::Path::NULL)?.current_state; Ok(Vec2 { x, y }) } } pub struct OXrView { texture: Mutex>, view: openxr::View, resolution: UVec2, format: wgpu::TextureFormat, } impl ViewTrait for OXrView { fn texture_view(&self) -> Option { std::mem::take(&mut *self.texture.lock().unwrap()) } fn pose(&self) -> Pose { self.view.pose.clone().into() } fn projection_matrix(&self) -> glam::Mat4 { // symmetric perspective for debugging // let x_fov = (self.fov.angle_left.abs() + self.fov.angle_right.abs()); // let y_fov = (self.fov.angle_up.abs() + self.fov.angle_down.abs()); // return Mat4::perspective_infinite_reverse_rh(y_fov, x_fov / y_fov, self.near); let fov = self.view.fov; let is_vulkan_api = false; // FIXME wgpu probably abstracts this let near_z = 0.1; let far_z = -1.; // use infinite proj // let far_z = self.far; let tan_angle_left = fov.angle_left.tan(); let tan_angle_right = fov.angle_right.tan(); let tan_angle_down = fov.angle_down.tan(); let tan_angle_up = fov.angle_up.tan(); let tan_angle_width = tan_angle_right - tan_angle_left; // Set to tanAngleDown - tanAngleUp for a clip space with positive Y // down (Vulkan). Set to tanAngleUp - tanAngleDown for a clip space with // positive Y up (OpenGL / D3D / Metal). // const float tanAngleHeight = // graphicsApi == GRAPHICS_VULKAN ? (tanAngleDown - tanAngleUp) : (tanAngleUp - tanAngleDown); let tan_angle_height = if is_vulkan_api { tan_angle_down - tan_angle_up } else { tan_angle_up - tan_angle_down }; // Set to nearZ for a [-1,1] Z clip space (OpenGL / OpenGL ES). // Set to zero for a [0,1] Z clip space (Vulkan / D3D / Metal). // const float offsetZ = // (graphicsApi == GRAPHICS_OPENGL || graphicsApi == GRAPHICS_OPENGL_ES) ? nearZ : 0; // FIXME handle enum of graphics apis let offset_z = 0.; let mut cols: [f32; 16] = [0.0; 16]; if far_z <= near_z { // place the far plane at infinity cols[0] = 2. / tan_angle_width; cols[4] = 0.; cols[8] = (tan_angle_right + tan_angle_left) / tan_angle_width; cols[12] = 0.; cols[1] = 0.; cols[5] = 2. / tan_angle_height; cols[9] = (tan_angle_up + tan_angle_down) / tan_angle_height; cols[13] = 0.; cols[2] = 0.; cols[6] = 0.; cols[10] = -1.; cols[14] = -(near_z + offset_z); cols[3] = 0.; cols[7] = 0.; cols[11] = -1.; cols[15] = 0.; // bevy uses the _reverse_ infinite projection // https://dev.theomader.com/depth-precision/ let z_reversal = Mat4::from_cols_array_2d(&[ [1f32, 0., 0., 0.], [0., 1., 0., 0.], [0., 0., -1., 0.], [0., 0., 1., 1.], ]); return z_reversal * Mat4::from_cols_array(&cols); } else { // normal projection cols[0] = 2. / tan_angle_width; cols[4] = 0.; cols[8] = (tan_angle_right + tan_angle_left) / tan_angle_width; cols[12] = 0.; cols[1] = 0.; cols[5] = 2. / tan_angle_height; cols[9] = (tan_angle_up + tan_angle_down) / tan_angle_height; cols[13] = 0.; cols[2] = 0.; cols[6] = 0.; cols[10] = -(far_z + offset_z) / (far_z - near_z); cols[14] = -(far_z * (near_z + offset_z)) / (far_z - near_z); cols[3] = 0.; cols[7] = 0.; cols[11] = -1.; cols[15] = 0.; } Mat4::from_cols_array(&cols) } fn resolution(&self) -> UVec2 { self.resolution } fn format(&self) -> wgpu::TextureFormat { self.format } }