diff --git a/crates/bevy_openxr/src/graphics.rs b/crates/bevy_openxr/src/graphics.rs index be0fa52..4d1e86b 100644 --- a/crates/bevy_openxr/src/graphics.rs +++ b/crates/bevy_openxr/src/graphics.rs @@ -3,6 +3,8 @@ pub mod vulkan; use std::any::TypeId; +use bevy::math::UVec2; + use crate::extensions::XrExtensions; use crate::types::*; @@ -127,5 +129,4 @@ macro_rules! graphics_match { }; } -use bevy::math::UVec2; pub(crate) use graphics_match; diff --git a/crates/bevy_openxr/src/init.rs b/crates/bevy_openxr/src/init.rs index 18cb0f5..40fe4f2 100644 --- a/crates/bevy_openxr/src/init.rs +++ b/crates/bevy_openxr/src/init.rs @@ -1,35 +1,30 @@ -use bevy::app::{App, First, Plugin, PostUpdate, PreUpdate}; -use bevy::ecs::change_detection::DetectChangesMut; -use bevy::ecs::component::Component; -use bevy::ecs::entity::Entity; -use bevy::ecs::query::{With, Without}; -use bevy::ecs::schedule::common_conditions::{not, on_event}; -use bevy::ecs::schedule::IntoSystemConfigs; -use bevy::ecs::system::{Commands, Query, Res, ResMut, Resource}; -use bevy::ecs::world::World; -use bevy::hierarchy::{BuildChildren, Parent}; -use bevy::log::{error, info, warn}; -use bevy::math::{uvec2, UVec2}; -use bevy::prelude::{Deref, DerefMut}; -use bevy::render::extract_component::{ExtractComponent, ExtractComponentPlugin}; +use bevy::math::uvec2; +use bevy::prelude::*; use bevy::render::extract_resource::ExtractResourcePlugin; use bevy::render::renderer::{ RenderAdapter, RenderAdapterInfo, RenderDevice, RenderInstance, RenderQueue, }; use bevy::render::settings::RenderCreation; -use bevy::render::{ExtractSchedule, MainWorld, RenderApp, RenderPlugin}; -use bevy::transform::components::GlobalTransform; -use bevy::transform::{TransformBundle, TransformSystem}; +use bevy::render::{MainWorld, RenderApp, RenderPlugin}; use bevy_xr::session::{ handle_session, session_available, session_running, status_equals, BeginXrSession, - CreateXrSession, XrStatus, + CreateXrSession, EndXrSession, XrSharedStatus, XrStatus, }; -use crate::error::XrError; -use crate::graphics::GraphicsBackend; +use crate::graphics::*; use crate::resources::*; use crate::types::*; +pub fn session_started(started: Option>) -> bool { + started.is_some_and(|started| started.get()) +} + +#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy, SystemSet)] +pub enum XrPreUpdateSet { + PollEvents, + HandleEvents, +} + pub struct XrInitPlugin { /// Information about the app this is being used to build. pub app_info: AppInfo, @@ -50,11 +45,92 @@ pub struct XrInitPlugin { impl Plugin for XrInitPlugin { fn build(&self, app: &mut App) { - if let Err(e) = init_xr(&self, app) { - error!("Failed to initialize openxr instance: {e}."); - app.add_plugins(RenderPlugin::default()) - .insert_resource(XrStatus::Unavailable); - } + match self.init_xr() { + Ok(( + instance, + system_id, + WgpuGraphics(device, queue, adapter_info, adapter, wgpu_instance), + session_create_info, + )) => { + let status = XrSharedStatus::new(XrStatus::Available); + + app.add_plugins(( + RenderPlugin { + render_creation: RenderCreation::manual( + device.into(), + RenderQueue(queue.into()), + RenderAdapterInfo(adapter_info), + RenderAdapter(adapter.into()), + RenderInstance(wgpu_instance.into()), + ), + synchronous_pipeline_compilation: self.synchronous_pipeline_compilation, + }, + ExtractResourcePlugin::::default(), + )) + .add_systems( + PreUpdate, + ( + poll_events + .run_if(session_available) + .in_set(XrPreUpdateSet::PollEvents), + ( + (create_xr_session, apply_deferred) + .chain() + .run_if(on_event::()) + .run_if(status_equals(XrStatus::Available)), + begin_xr_session + .run_if(on_event::()) + .run_if(status_equals(XrStatus::Ready)), + end_xr_session + .run_if(on_event::()) + .run_if(session_started), + ) + .in_set(XrPreUpdateSet::HandleEvents), + ), + ) + .insert_resource(instance.clone()) + .insert_resource(system_id) + .insert_resource(status.clone()) + .insert_non_send_resource(session_create_info); + + let render_app = app.sub_app_mut(RenderApp); + render_app + .insert_resource(instance) + .insert_resource(system_id) + .insert_resource(status) + .add_systems( + ExtractSchedule, + transfer_xr_resources.run_if(not(session_running)), + ); + } + Err(e) => { + error!("Failed to initialize openxr: {e}"); + let status = XrSharedStatus::new(XrStatus::Unavailable); + + app.add_plugins(RenderPlugin::default()) + .insert_resource(status.clone()); + + let render_app = app.sub_app_mut(RenderApp); + + render_app.insert_resource(status); + } + }; + + app.configure_sets( + PreUpdate, + ( + XrPreUpdateSet::PollEvents.before(handle_session), + XrPreUpdateSet::HandleEvents.after(handle_session), + ), + ); + + let session_started = XrSessionStarted::default(); + + app.insert_resource(session_started.clone()); + + let render_app = app.sub_app_mut(RenderApp); + + render_app.insert_resource(session_started); } } @@ -66,214 +142,103 @@ fn xr_entry() -> Result { Ok(XrEntry(entry)) } -/// This is called from [`XrInitPlugin::build()`]. Its a separate function so that we can return a [Result] and control flow is cleaner. -fn init_xr(config: &XrInitPlugin, app: &mut App) -> Result<()> { - let entry = xr_entry()?; +impl XrInitPlugin { + fn init_xr(&self) -> Result<(XrInstance, XrSystemId, WgpuGraphics, XrSessionCreateInfo)> { + let entry = xr_entry()?; - let available_exts = entry.enumerate_extensions()?; + let available_exts = entry.enumerate_extensions()?; - // check available extensions and send a warning for any wanted extensions that aren't available. - for ext in available_exts.unavailable_exts(&config.exts) { - error!( - "Extension \"{ext}\" not available in the current OpenXR runtime. Disabling extension." - ); - } - - let available_backends = GraphicsBackend::available_backends(&available_exts); - - // Backend selection - let backend = if let Some(wanted_backends) = &config.backends { - let mut backend = None; - for wanted_backend in wanted_backends { - if available_backends.contains(wanted_backend) { - backend = Some(*wanted_backend); - break; - } + // check available extensions and send a warning for any wanted extensions that aren't available. + for ext in available_exts.unavailable_exts(&self.exts) { + error!( + "Extension \"{ext}\" not available in the current OpenXR runtime. Disabling extension." + ); } - backend - } else { - available_backends.first().copied() - } - .ok_or(XrError::NoAvailableBackend)?; - let exts = config.exts.clone() & available_exts; + let available_backends = GraphicsBackend::available_backends(&available_exts); - let instance = entry.create_instance( - config.app_info.clone(), - exts, - &["XR_APILAYER_LUNARG_api_dump"], - backend, - )?; - let instance_props = instance.properties()?; - - info!( - "Loaded OpenXR runtime: {} {}", - instance_props.runtime_name, instance_props.runtime_version - ); - - let system_id = instance.system(openxr::FormFactor::HEAD_MOUNTED_DISPLAY)?; - let system_props = instance.system_properties(system_id)?; - - info!( - "Using system: {}", - if system_props.system_name.is_empty() { - "" + // Backend selection + let backend = if let Some(wanted_backends) = &self.backends { + let mut backend = None; + for wanted_backend in wanted_backends { + if available_backends.contains(wanted_backend) { + backend = Some(*wanted_backend); + break; + } + } + backend } else { - &system_props.system_name + available_backends.first().copied() } - ); + .ok_or(XrError::NoAvailableBackend)?; - let (WgpuGraphics(device, queue, adapter_info, adapter, wgpu_instance), create_info) = - instance.init_graphics(system_id)?; + let exts = self.exts.clone() & available_exts; - app.add_plugins(( - RenderPlugin { - render_creation: RenderCreation::manual( - device.into(), - RenderQueue(queue.into()), - RenderAdapterInfo(adapter_info), - RenderAdapter(adapter.into()), - RenderInstance(wgpu_instance.into()), - ), - synchronous_pipeline_compilation: config.synchronous_pipeline_compilation, - }, - ExtractComponentPlugin::::default(), - ExtractResourcePlugin::::default(), - ExtractResourcePlugin::::default(), - )) - .insert_resource(instance.clone()) - .insert_resource(SystemId(system_id)) - .insert_resource(XrStatus::Available) - .insert_non_send_resource(XrSessionInitConfig { - blend_modes: config.blend_modes.clone(), - formats: config.formats.clone(), - resolutions: config.resolutions.clone(), - create_info, - }) - .add_systems( - First, - poll_events.run_if(session_available).before(handle_session), - ) - .add_systems( - PreUpdate, - ( - create_xr_session - .run_if(on_event::()) - .run_if(status_equals(XrStatus::Available)), - begin_xr_session - .run_if(status_equals(XrStatus::Ready)) - .run_if(on_event::()), - adopt_open_xr_trackers, - ) - .chain(), - ) - .add_systems( - PostUpdate, - update_root_transform_components.after(TransformSystem::TransformPropagate), - ) - .sub_app_mut(RenderApp) - .insert_resource(instance) - .insert_resource(SystemId(system_id)) - .add_systems( - ExtractSchedule, - transfer_xr_resources.run_if(not(session_running)), - ); + let instance = entry.create_instance( + self.app_info.clone(), + exts, + // &["XR_APILAYER_LUNARG_api_dump"], + &[], + backend, + )?; + let instance_props = instance.properties()?; - app.world - .spawn((TransformBundle::default(), OpenXrTrackingRoot)); - - Ok(()) -} - -#[derive(Component, ExtractComponent, Clone, Deref, DerefMut, Default)] -pub struct XrRoot(pub GlobalTransform); - -#[derive(Component)] -/// This is the root location of the playspace. Moving this entity around moves the rest of the playspace around. -pub struct OpenXrTrackingRoot; - -#[derive(Component)] -/// Marker component for any entities that should be children of [`OpenXrTrackingRoot`] -pub struct OpenXrTracker; - -pub fn adopt_open_xr_trackers( - query: Query, Without)>, - mut commands: Commands, - tracking_root_query: Query>, -) { - let root = tracking_root_query.get_single(); - match root { - Ok(root) => { - // info!("root is"); - for tracker in query.iter() { - info!("we got a new tracker"); - commands.entity(root).add_child(tracker); - } - } - Err(_) => info!("root isnt spawned yet?"), - } -} - -fn update_root_transform_components( - mut component_query: Query<&mut XrRoot>, - root_query: Query<&GlobalTransform, With>, -) { - let root = match root_query.get_single() { - Ok(v) => v, - Err(err) => { - warn!("No or too many XrTracking Roots: {}", err); - return; - } - }; - component_query - .par_iter_mut() - .for_each(|mut root_transform| **root_transform = *root); -} - -/// This is used to store information from startup that is needed to create the session after the instance has been created. -struct XrSessionInitConfig { - /// List of blend modes the openxr session can use. If [None], pick the first available blend mode. - blend_modes: Option>, - /// List of formats the openxr session can use. If [None], pick the first available format - formats: Option>, - /// List of resolutions that the openxr swapchain can use. If [None] pick the first available resolution. - resolutions: Option>, - /// Graphics info used to create a session. - create_info: SessionCreateInfo, -} - -pub fn create_xr_session(world: &mut World) { - let Some(create_info) = world.remove_non_send_resource() else { - error!( - "Failed to retrive SessionCreateInfo. This is likely due to improper initialization." + info!( + "Loaded OpenXR runtime: {} {}", + instance_props.runtime_name, instance_props.runtime_version ); - return; - }; - let Some(instance) = world.get_resource().cloned() else { - error!("Failed to retrieve XrInstance. This is likely due to improper initialization."); - return; - }; + let system_id = instance.system(openxr::FormFactor::HEAD_MOUNTED_DISPLAY)?; + let system_props = instance.system_properties(system_id)?; - let Some(system_id) = world.get_resource::().cloned() else { - error!("Failed to retrieve SystemId. THis is likely due to improper initialization"); - return; - }; + info!( + "Using system: {}", + if system_props.system_name.is_empty() { + "" + } else { + &system_props.system_name + } + ); - if let Err(e) = create_xr_session_inner(world, instance, *system_id, create_info) { - error!("Failed to initialize XrSession: {e}"); + let (graphics, graphics_info) = instance.init_graphics(system_id)?; + + let session_create_info = XrSessionCreateInfo { + blend_modes: self.blend_modes.clone(), + formats: self.formats.clone(), + resolutions: self.resolutions.clone(), + graphics_info, + }; + + Ok(( + instance, + XrSystemId(system_id), + graphics, + session_create_info, + )) } } -/// This is called from [create_xr_session]. It is a separate function to allow us to return a [Result] and make control flow cleaner. -fn create_xr_session_inner( - world: &mut World, - instance: XrInstance, +fn init_xr_session( + device: &wgpu::Device, + instance: &XrInstance, system_id: openxr::SystemId, - config: XrSessionInitConfig, -) -> Result<()> { + XrSessionCreateInfo { + blend_modes, + formats, + resolutions, + graphics_info, + }: XrSessionCreateInfo, +) -> Result<( + XrSession, + XrFrameWaiter, + XrFrameStream, + XrSwapchain, + XrSwapchainImages, + XrGraphicsInfo, + XrStage, +)> { let (session, frame_waiter, frame_stream) = - unsafe { instance.create_session(system_id, config.create_info)? }; + unsafe { instance.create_session(system_id, graphics_info)? }; // TODO!() support other view configurations let available_view_configurations = instance.enumerate_view_configurations(system_id)?; @@ -286,7 +251,7 @@ fn create_xr_session_inner( let view_configuration_views = instance.enumerate_view_configuration_views(system_id, view_configuration_type)?; - let (resolution, _view) = if let Some(resolutions) = &config.resolutions { + let (resolution, _view) = if let Some(resolutions) = &resolutions { let mut preferred = None; for resolution in resolutions { for view_config in view_configuration_views.iter() { @@ -328,7 +293,7 @@ fn create_xr_session_inner( let available_formats = session.enumerate_swapchain_formats()?; - let format = if let Some(formats) = &config.formats { + let format = if let Some(formats) = &formats { let mut format = None; for wanted_format in formats { if available_formats.contains(wanted_format) { @@ -354,17 +319,13 @@ fn create_xr_session_inner( mip_count: 1, })?; - let images = swapchain.enumerate_images( - world.resource::().wgpu_device(), - format, - resolution, - )?; + let images = swapchain.enumerate_images(device, format, resolution)?; let available_blend_modes = instance.enumerate_environment_blend_modes(system_id, view_configuration_type)?; // blend mode selection - let blend_mode = if let Some(wanted_blend_modes) = &config.blend_modes { + let blend_mode = if let Some(wanted_blend_modes) = &blend_modes { let mut blend_mode = None; for wanted_blend_mode in wanted_blend_modes { if available_blend_modes.contains(wanted_blend_mode) { @@ -390,29 +351,15 @@ fn create_xr_session_inner( format, }; - world.insert_resource(session.clone()); - world.insert_resource(frame_waiter); - world.insert_resource(images.clone()); - world.insert_resource(graphics_info.clone()); - world.insert_resource(stage.clone()); - world.insert_resource(frame_stream.clone()); - world.insert_resource(XrRenderResources { + Ok(( session, + frame_waiter, frame_stream, swapchain, images, graphics_info, stage, - }); - - Ok(()) -} - -pub fn begin_xr_session(session: Res, mut status: ResMut) { - session - .begin(openxr::ViewConfigurationType::PRIMARY_STEREO) - .expect("Failed to begin session"); - *status = XrStatus::Running; + )) } /// This is used solely to transport resources from the main world to the render world. @@ -426,6 +373,51 @@ struct XrRenderResources { stage: XrStage, } +pub fn create_xr_session( + device: Res, + instance: Res, + create_info: NonSend, + system_id: Res, + mut commands: Commands, +) { + match init_xr_session( + device.wgpu_device(), + &instance, + **system_id, + create_info.clone(), + ) { + Ok((session, frame_waiter, frame_stream, swapchain, images, graphics_info, stage)) => { + commands.insert_resource(session.clone()); + commands.insert_resource(frame_waiter); + commands.insert_resource(images.clone()); + commands.insert_resource(graphics_info.clone()); + commands.insert_resource(stage.clone()); + commands.insert_resource(frame_stream.clone()); + commands.insert_resource(XrRenderResources { + session, + frame_stream, + swapchain, + images, + graphics_info, + stage, + }); + } + Err(e) => error!("Failed to initialize XrSession: {e}"), + } +} + +pub fn begin_xr_session(session: Res, session_started: Res) { + session + .begin(openxr::ViewConfigurationType::PRIMARY_STEREO) + .expect("Failed to begin session"); + session_started.set(true); +} + +pub fn end_xr_session(session: Res, session_started: Res) { + session.end().expect("Failed to end session"); + session_started.set(false); +} + /// This system transfers important render resources from the main world to the render world when a session is created. pub fn transfer_xr_resources(mut commands: Commands, mut world: ResMut) { let Some(XrRenderResources { @@ -448,8 +440,8 @@ pub fn transfer_xr_resources(mut commands: Commands, mut world: ResMut, mut status: ResMut) { +/// Polls any OpenXR events and handles them accordingly +pub fn poll_events(instance: Res, status: Res) { let mut buffer = Default::default(); while let Some(event) = instance .poll_event(&mut buffer) @@ -457,29 +449,28 @@ pub fn poll_events(instance: Res, mut status: ResMut) { { use openxr::Event::*; match event { - SessionStateChanged(e) => { - info!("entered XR state {:?}", e.state()); + SessionStateChanged(state) => { use openxr::SessionState; - match e.state() { - SessionState::IDLE => { - *status = XrStatus::Idle; - } - SessionState::READY => { - *status = XrStatus::Ready; - } + let state = state.state(); + + info!("entered XR state {:?}", state); + + let new_status = match state { + SessionState::IDLE => XrStatus::Idle, + SessionState::READY => XrStatus::Ready, SessionState::SYNCHRONIZED | SessionState::VISIBLE | SessionState::FOCUSED => { - status.set_if_neq(XrStatus::Running); + XrStatus::Running } - SessionState::STOPPING => *status = XrStatus::Stopping, - // TODO: figure out how to destroy the session - SessionState::EXITING | SessionState::LOSS_PENDING => { - *status = XrStatus::Exiting; - } - _ => {} - } + SessionState::STOPPING => XrStatus::Stopping, + SessionState::EXITING | SessionState::LOSS_PENDING => XrStatus::Exiting, + _ => unreachable!(), + }; + + status.set(new_status); } InstanceLossPending(_) => {} + EventsLost(e) => warn!("lost {} XR events", e.lost_event_count()), _ => {} } } diff --git a/crates/bevy_openxr/src/initt.rs b/crates/bevy_openxr/src/initt.rs new file mode 100644 index 0000000..18cb0f5 --- /dev/null +++ b/crates/bevy_openxr/src/initt.rs @@ -0,0 +1,486 @@ +use bevy::app::{App, First, Plugin, PostUpdate, PreUpdate}; +use bevy::ecs::change_detection::DetectChangesMut; +use bevy::ecs::component::Component; +use bevy::ecs::entity::Entity; +use bevy::ecs::query::{With, Without}; +use bevy::ecs::schedule::common_conditions::{not, on_event}; +use bevy::ecs::schedule::IntoSystemConfigs; +use bevy::ecs::system::{Commands, Query, Res, ResMut, Resource}; +use bevy::ecs::world::World; +use bevy::hierarchy::{BuildChildren, Parent}; +use bevy::log::{error, info, warn}; +use bevy::math::{uvec2, UVec2}; +use bevy::prelude::{Deref, DerefMut}; +use bevy::render::extract_component::{ExtractComponent, ExtractComponentPlugin}; +use bevy::render::extract_resource::ExtractResourcePlugin; +use bevy::render::renderer::{ + RenderAdapter, RenderAdapterInfo, RenderDevice, RenderInstance, RenderQueue, +}; +use bevy::render::settings::RenderCreation; +use bevy::render::{ExtractSchedule, MainWorld, RenderApp, RenderPlugin}; +use bevy::transform::components::GlobalTransform; +use bevy::transform::{TransformBundle, TransformSystem}; +use bevy_xr::session::{ + handle_session, session_available, session_running, status_equals, BeginXrSession, + CreateXrSession, XrStatus, +}; + +use crate::error::XrError; +use crate::graphics::GraphicsBackend; +use crate::resources::*; +use crate::types::*; + +pub struct XrInitPlugin { + /// Information about the app this is being used to build. + pub app_info: AppInfo, + /// Extensions wanted for this session. + // TODO!() This should be changed to take a simpler list of features wanted that this crate supports. i.e. hand tracking + pub exts: XrExtensions, + /// List of blend modes the openxr session can use. If [None], pick the first available blend mode. + pub blend_modes: Option>, + /// List of backends the openxr session can use. If [None], pick the first available backend. + pub backends: Option>, + /// List of formats the openxr session can use. If [None], pick the first available format + pub formats: Option>, + /// List of resolutions that the openxr swapchain can use. If [None] pick the first available resolution. + pub resolutions: Option>, + /// Passed into the render plugin when added to the app. + pub synchronous_pipeline_compilation: bool, +} + +impl Plugin for XrInitPlugin { + fn build(&self, app: &mut App) { + if let Err(e) = init_xr(&self, app) { + error!("Failed to initialize openxr instance: {e}."); + app.add_plugins(RenderPlugin::default()) + .insert_resource(XrStatus::Unavailable); + } + } +} + +fn xr_entry() -> Result { + #[cfg(windows)] + let entry = openxr::Entry::linked(); + #[cfg(not(windows))] + let entry = unsafe { openxr::Entry::load()? }; + Ok(XrEntry(entry)) +} + +/// This is called from [`XrInitPlugin::build()`]. Its a separate function so that we can return a [Result] and control flow is cleaner. +fn init_xr(config: &XrInitPlugin, app: &mut App) -> Result<()> { + let entry = xr_entry()?; + + let available_exts = entry.enumerate_extensions()?; + + // check available extensions and send a warning for any wanted extensions that aren't available. + for ext in available_exts.unavailable_exts(&config.exts) { + error!( + "Extension \"{ext}\" not available in the current OpenXR runtime. Disabling extension." + ); + } + + let available_backends = GraphicsBackend::available_backends(&available_exts); + + // Backend selection + let backend = if let Some(wanted_backends) = &config.backends { + let mut backend = None; + for wanted_backend in wanted_backends { + if available_backends.contains(wanted_backend) { + backend = Some(*wanted_backend); + break; + } + } + backend + } else { + available_backends.first().copied() + } + .ok_or(XrError::NoAvailableBackend)?; + + let exts = config.exts.clone() & available_exts; + + let instance = entry.create_instance( + config.app_info.clone(), + exts, + &["XR_APILAYER_LUNARG_api_dump"], + backend, + )?; + let instance_props = instance.properties()?; + + info!( + "Loaded OpenXR runtime: {} {}", + instance_props.runtime_name, instance_props.runtime_version + ); + + let system_id = instance.system(openxr::FormFactor::HEAD_MOUNTED_DISPLAY)?; + let system_props = instance.system_properties(system_id)?; + + info!( + "Using system: {}", + if system_props.system_name.is_empty() { + "" + } else { + &system_props.system_name + } + ); + + let (WgpuGraphics(device, queue, adapter_info, adapter, wgpu_instance), create_info) = + instance.init_graphics(system_id)?; + + app.add_plugins(( + RenderPlugin { + render_creation: RenderCreation::manual( + device.into(), + RenderQueue(queue.into()), + RenderAdapterInfo(adapter_info), + RenderAdapter(adapter.into()), + RenderInstance(wgpu_instance.into()), + ), + synchronous_pipeline_compilation: config.synchronous_pipeline_compilation, + }, + ExtractComponentPlugin::::default(), + ExtractResourcePlugin::::default(), + ExtractResourcePlugin::::default(), + )) + .insert_resource(instance.clone()) + .insert_resource(SystemId(system_id)) + .insert_resource(XrStatus::Available) + .insert_non_send_resource(XrSessionInitConfig { + blend_modes: config.blend_modes.clone(), + formats: config.formats.clone(), + resolutions: config.resolutions.clone(), + create_info, + }) + .add_systems( + First, + poll_events.run_if(session_available).before(handle_session), + ) + .add_systems( + PreUpdate, + ( + create_xr_session + .run_if(on_event::()) + .run_if(status_equals(XrStatus::Available)), + begin_xr_session + .run_if(status_equals(XrStatus::Ready)) + .run_if(on_event::()), + adopt_open_xr_trackers, + ) + .chain(), + ) + .add_systems( + PostUpdate, + update_root_transform_components.after(TransformSystem::TransformPropagate), + ) + .sub_app_mut(RenderApp) + .insert_resource(instance) + .insert_resource(SystemId(system_id)) + .add_systems( + ExtractSchedule, + transfer_xr_resources.run_if(not(session_running)), + ); + + app.world + .spawn((TransformBundle::default(), OpenXrTrackingRoot)); + + Ok(()) +} + +#[derive(Component, ExtractComponent, Clone, Deref, DerefMut, Default)] +pub struct XrRoot(pub GlobalTransform); + +#[derive(Component)] +/// This is the root location of the playspace. Moving this entity around moves the rest of the playspace around. +pub struct OpenXrTrackingRoot; + +#[derive(Component)] +/// Marker component for any entities that should be children of [`OpenXrTrackingRoot`] +pub struct OpenXrTracker; + +pub fn adopt_open_xr_trackers( + query: Query, Without)>, + mut commands: Commands, + tracking_root_query: Query>, +) { + let root = tracking_root_query.get_single(); + match root { + Ok(root) => { + // info!("root is"); + for tracker in query.iter() { + info!("we got a new tracker"); + commands.entity(root).add_child(tracker); + } + } + Err(_) => info!("root isnt spawned yet?"), + } +} + +fn update_root_transform_components( + mut component_query: Query<&mut XrRoot>, + root_query: Query<&GlobalTransform, With>, +) { + let root = match root_query.get_single() { + Ok(v) => v, + Err(err) => { + warn!("No or too many XrTracking Roots: {}", err); + return; + } + }; + component_query + .par_iter_mut() + .for_each(|mut root_transform| **root_transform = *root); +} + +/// This is used to store information from startup that is needed to create the session after the instance has been created. +struct XrSessionInitConfig { + /// List of blend modes the openxr session can use. If [None], pick the first available blend mode. + blend_modes: Option>, + /// List of formats the openxr session can use. If [None], pick the first available format + formats: Option>, + /// List of resolutions that the openxr swapchain can use. If [None] pick the first available resolution. + resolutions: Option>, + /// Graphics info used to create a session. + create_info: SessionCreateInfo, +} + +pub fn create_xr_session(world: &mut World) { + let Some(create_info) = world.remove_non_send_resource() else { + error!( + "Failed to retrive SessionCreateInfo. This is likely due to improper initialization." + ); + return; + }; + + let Some(instance) = world.get_resource().cloned() else { + error!("Failed to retrieve XrInstance. This is likely due to improper initialization."); + return; + }; + + let Some(system_id) = world.get_resource::().cloned() else { + error!("Failed to retrieve SystemId. THis is likely due to improper initialization"); + return; + }; + + if let Err(e) = create_xr_session_inner(world, instance, *system_id, create_info) { + error!("Failed to initialize XrSession: {e}"); + } +} + +/// This is called from [create_xr_session]. It is a separate function to allow us to return a [Result] and make control flow cleaner. +fn create_xr_session_inner( + world: &mut World, + instance: XrInstance, + system_id: openxr::SystemId, + config: XrSessionInitConfig, +) -> Result<()> { + let (session, frame_waiter, frame_stream) = + unsafe { instance.create_session(system_id, config.create_info)? }; + + // TODO!() support other view configurations + let available_view_configurations = instance.enumerate_view_configurations(system_id)?; + if !available_view_configurations.contains(&openxr::ViewConfigurationType::PRIMARY_STEREO) { + return Err(XrError::NoAvailableViewConfiguration); + } + + let view_configuration_type = openxr::ViewConfigurationType::PRIMARY_STEREO; + + let view_configuration_views = + instance.enumerate_view_configuration_views(system_id, view_configuration_type)?; + + let (resolution, _view) = if let Some(resolutions) = &config.resolutions { + let mut preferred = None; + for resolution in resolutions { + for view_config in view_configuration_views.iter() { + if view_config.recommended_image_rect_height == resolution.y + && view_config.recommended_image_rect_width == resolution.x + { + preferred = Some((*resolution, *view_config)); + } + } + } + + if preferred.is_none() { + for resolution in resolutions { + for view_config in view_configuration_views.iter() { + if view_config.max_image_rect_height >= resolution.y + && view_config.max_image_rect_width >= resolution.x + { + preferred = Some((*resolution, *view_config)); + } + } + } + } + + preferred + } else { + if let Some(config) = view_configuration_views.first() { + Some(( + uvec2( + config.recommended_image_rect_width, + config.recommended_image_rect_height, + ), + *config, + )) + } else { + None + } + } + .ok_or(XrError::NoAvailableViewConfiguration)?; + + let available_formats = session.enumerate_swapchain_formats()?; + + let format = if let Some(formats) = &config.formats { + let mut format = None; + for wanted_format in formats { + if available_formats.contains(wanted_format) { + format = Some(*wanted_format); + } + } + format + } else { + available_formats.first().copied() + } + .ok_or(XrError::NoAvailableFormat)?; + + let mut swapchain = session.create_swapchain(SwapchainCreateInfo { + create_flags: SwapchainCreateFlags::EMPTY, + usage_flags: SwapchainUsageFlags::COLOR_ATTACHMENT | SwapchainUsageFlags::SAMPLED, + format, + // TODO() add support for multisampling + sample_count: 1, + width: resolution.x, + height: resolution.y, + face_count: 1, + array_size: 2, + mip_count: 1, + })?; + + let images = swapchain.enumerate_images( + world.resource::().wgpu_device(), + format, + resolution, + )?; + + let available_blend_modes = + instance.enumerate_environment_blend_modes(system_id, view_configuration_type)?; + + // blend mode selection + let blend_mode = if let Some(wanted_blend_modes) = &config.blend_modes { + let mut blend_mode = None; + for wanted_blend_mode in wanted_blend_modes { + if available_blend_modes.contains(wanted_blend_mode) { + blend_mode = Some(*wanted_blend_mode); + break; + } + } + blend_mode + } else { + available_blend_modes.first().copied() + } + .ok_or(XrError::NoAvailableBackend)?; + + let stage = XrStage( + session + .create_reference_space(openxr::ReferenceSpaceType::STAGE, openxr::Posef::IDENTITY)? + .into(), + ); + + let graphics_info = XrGraphicsInfo { + blend_mode, + resolution, + format, + }; + + world.insert_resource(session.clone()); + world.insert_resource(frame_waiter); + world.insert_resource(images.clone()); + world.insert_resource(graphics_info.clone()); + world.insert_resource(stage.clone()); + world.insert_resource(frame_stream.clone()); + world.insert_resource(XrRenderResources { + session, + frame_stream, + swapchain, + images, + graphics_info, + stage, + }); + + Ok(()) +} + +pub fn begin_xr_session(session: Res, mut status: ResMut) { + session + .begin(openxr::ViewConfigurationType::PRIMARY_STEREO) + .expect("Failed to begin session"); + *status = XrStatus::Running; +} + +/// This is used solely to transport resources from the main world to the render world. +#[derive(Resource)] +struct XrRenderResources { + session: XrSession, + frame_stream: XrFrameStream, + swapchain: XrSwapchain, + images: XrSwapchainImages, + graphics_info: XrGraphicsInfo, + stage: XrStage, +} + +/// This system transfers important render resources from the main world to the render world when a session is created. +pub fn transfer_xr_resources(mut commands: Commands, mut world: ResMut) { + let Some(XrRenderResources { + session, + frame_stream, + swapchain, + images, + graphics_info, + stage, + }) = world.remove_resource() + else { + return; + }; + + commands.insert_resource(session); + commands.insert_resource(frame_stream); + commands.insert_resource(swapchain); + commands.insert_resource(images); + commands.insert_resource(graphics_info); + commands.insert_resource(stage); +} + +/// Poll any OpenXR events and handle them accordingly +pub fn poll_events(instance: Res, mut status: ResMut) { + let mut buffer = Default::default(); + while let Some(event) = instance + .poll_event(&mut buffer) + .expect("Failed to poll event") + { + use openxr::Event::*; + match event { + SessionStateChanged(e) => { + info!("entered XR state {:?}", e.state()); + use openxr::SessionState; + + match e.state() { + SessionState::IDLE => { + *status = XrStatus::Idle; + } + SessionState::READY => { + *status = XrStatus::Ready; + } + SessionState::SYNCHRONIZED | SessionState::VISIBLE | SessionState::FOCUSED => { + status.set_if_neq(XrStatus::Running); + } + SessionState::STOPPING => *status = XrStatus::Stopping, + // TODO: figure out how to destroy the session + SessionState::EXITING | SessionState::LOSS_PENDING => { + *status = XrStatus::Exiting; + } + _ => {} + } + } + InstanceLossPending(_) => {} + _ => {} + } + } +} diff --git a/crates/bevy_openxr/src/lib.rs b/crates/bevy_openxr/src/lib.rs index 834330b..71e24c2 100644 --- a/crates/bevy_openxr/src/lib.rs +++ b/crates/bevy_openxr/src/lib.rs @@ -2,6 +2,7 @@ use bevy::{ app::{PluginGroup, PluginGroupBuilder}, render::{pipelined_rendering::PipelinedRenderingPlugin, RenderPlugin}, utils::default, + window::{PresentMode, Window, WindowPlugin}, }; use bevy_xr::camera::XrCameraPlugin; use init::XrInitPlugin; @@ -34,4 +35,20 @@ pub fn add_xr_plugins(plugins: G) -> PluginGroupBuilder { }) .add(XrRenderPlugin) .add(XrCameraPlugin) + .set(WindowPlugin { + #[cfg(not(target_os = "android"))] + primary_window: Some(Window { + transparent: true, + present_mode: PresentMode::AutoNoVsync, + // title: self.app_info.name.clone(), + ..default() + }), + #[cfg(target_os = "android")] + primary_window: None, // ? + #[cfg(target_os = "android")] + exit_condition: bevy::window::ExitCondition::DontExit, + #[cfg(target_os = "android")] + close_when_requested: true, + ..default() + }) } diff --git a/crates/bevy_openxr/src/render.rs b/crates/bevy_openxr/src/render.rs index 0bd8698..fc39edf 100644 --- a/crates/bevy_openxr/src/render.rs +++ b/crates/bevy_openxr/src/render.rs @@ -3,19 +3,16 @@ use bevy::{ render::{ camera::{ManualTextureView, ManualTextureViewHandle, ManualTextureViews, RenderTarget}, extract_resource::ExtractResourcePlugin, - renderer::render_system, - view::ExtractedView, Render, RenderApp, RenderSet, }, + transform::TransformSystem, }; -use bevy_xr::{ - camera::{XrCamera, XrCameraBundle, XrProjection}, - session::session_running, -}; +use bevy_xr::camera::{XrCamera, XrCameraBundle, XrProjection}; use openxr::{CompositionLayerFlags, ViewStateFlags}; -use crate::{init::OpenXrTracker, resources::*}; -use crate::{init::XrRoot, layer_builder::*}; +use crate::init::{session_started, XrPreUpdateSet}; +use crate::layer_builder::*; +use crate::resources::*; pub struct XrRenderPlugin; @@ -26,27 +23,30 @@ impl Plugin for XrRenderPlugin { PreUpdate, ( init_views.run_if(resource_added::), - wait_frame.run_if(session_running), - locate_views.run_if(session_running), - update_views.run_if(session_running), + wait_frame.run_if(session_started), ) - .chain(), + .chain() + .after(XrPreUpdateSet::HandleEvents), + ) + .add_systems( + PostUpdate, + (locate_views, update_views) + .chain() + .run_if(session_started) + .before(TransformSystem::TransformPropagate), ); - // .add_systems(Startup, init_views); app.sub_app_mut(RenderApp).add_systems( Render, ( ( - //locate_views, - update_views_render_world, - insert_texture_views, + // begin_frame, + insert_texture_views ) .chain() - .in_set(RenderSet::PrepareAssets) - .before(render_system), - end_frame.in_set(RenderSet::Cleanup), + .in_set(RenderSet::PrepareAssets), + (locate_views, end_frame).chain().in_set(RenderSet::Cleanup), ) - .run_if(session_running), + .run_if(session_started), ); } } @@ -79,8 +79,8 @@ pub fn init_views( view: XrCamera(index), ..Default::default() }, - OpenXrTracker, - XrRoot::default(), + // OpenXrTracker, + // XrRoot::default(), )); views.push(default()); } @@ -90,7 +90,7 @@ pub fn init_views( pub fn wait_frame( mut frame_waiter: ResMut, mut commands: Commands, - frame_stream: ResMut, + frame_stream: Res, ) { let _span = info_span!("xr_wait_frame"); let state = frame_waiter.wait().expect("Failed to wait frame"); @@ -157,25 +157,6 @@ pub fn update_views( } } -pub fn update_views_render_world( - views: Res, - mut query: Query<(&mut ExtractedView, &XrRoot, &XrCamera)>, -) { - for (mut extracted_view, root, camera) in query.iter_mut() { - let Some(view) = views.get(camera.0 as usize) else { - continue; - }; - let mut transform = Transform::IDENTITY; - let openxr::Quaternionf { x, y, z, w } = view.pose.orientation; - let rotation = Quat::from_xyzw(x, y, z, w); - transform.rotation = rotation; - let openxr::Vector3f { x, y, z } = view.pose.position; - let translation = Vec3::new(x, y, z); - transform.translation = translation; - extracted_view.transform = root.mul_transform(transform); - } -} - fn calculate_projection(near_z: f32, fov: openxr::Fovf) -> Mat4 { // symmetric perspective for debugging // let x_fov = (self.fov.angle_left.abs() + self.fov.angle_right.abs()); @@ -312,6 +293,10 @@ pub fn add_texture_view( handle } +pub fn begin_frame(frame_stream: ResMut) { + frame_stream.begin().expect("Failed to begin frame") +} + pub fn end_frame( mut frame_stream: ResMut, mut swapchain: ResMut, @@ -357,5 +342,5 @@ pub fn end_frame( ), ])], ) - .expect("Failed to end stream"); + .expect("Failed to end frame"); } diff --git a/crates/bevy_openxr/src/resources.rs b/crates/bevy_openxr/src/resources.rs index 14ca7f7..3c96fba 100644 --- a/crates/bevy_openxr/src/resources.rs +++ b/crates/bevy_openxr/src/resources.rs @@ -1,3 +1,4 @@ +use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex}; use crate::error::XrError; @@ -63,13 +64,13 @@ impl XrInstance { pub fn init_graphics( &self, system_id: openxr::SystemId, - ) -> Result<(WgpuGraphics, SessionCreateInfo)> { + ) -> Result<(WgpuGraphics, XrSessionGraphicsInfo)> { graphics_match!( self.1; _ => { let (graphics, session_info) = Api::init_graphics(&self.2, &self, system_id)?; - Ok((graphics, SessionCreateInfo(Api::wrap(session_info)))) + Ok((graphics, XrSessionGraphicsInfo(Api::wrap(session_info)))) } ) } @@ -80,11 +81,11 @@ impl XrInstance { pub unsafe fn create_session( &self, system_id: openxr::SystemId, - info: SessionCreateInfo, + info: XrSessionGraphicsInfo, ) -> Result<(XrSession, XrFrameWaiter, XrFrameStream)> { if !info.0.using_graphics_of_val(&self.1) { return Err(XrError::GraphicsBackendMismatch { - item: std::any::type_name::(), + item: std::any::type_name::(), backend: info.0.graphics_name(), expected_backend: self.1.graphics_name(), }); @@ -99,22 +100,23 @@ impl XrInstance { } } -pub struct SessionCreateInfo(pub(crate) GraphicsWrap); +#[derive(Clone)] +pub struct XrSessionGraphicsInfo(pub(crate) GraphicsWrap); -impl GraphicsType for SessionCreateInfo { +impl GraphicsType for XrSessionGraphicsInfo { type Inner = G::SessionCreateInfo; } -impl GraphicsType for XrSession { - type Inner = openxr::Session; -} - #[derive(Resource, Deref, Clone)] pub struct XrSession( #[deref] pub(crate) openxr::Session, pub(crate) GraphicsWrap, ); +impl GraphicsType for XrSession { + type Inner = openxr::Session; +} + impl From> for XrSession { fn from(value: openxr::Session) -> Self { Self(value.clone().into_any_graphics(), G::wrap(value)) @@ -159,7 +161,7 @@ impl XrFrameStream { layers: &[&dyn CompositionLayer], ) -> Result<()> { graphics_match!( - &self.0; + &mut self.0; stream => { let mut stream = stream.lock().unwrap(); let mut new_layers = vec![]; @@ -253,7 +255,7 @@ pub struct XrSwapchainInfo { } #[derive(Debug, Copy, Clone, Deref, Default, Eq, PartialEq, Ord, PartialOrd, Hash, Resource)] -pub struct SystemId(pub openxr::SystemId); +pub struct XrSystemId(pub openxr::SystemId); #[derive(Clone, Copy, Resource)] pub struct XrGraphicsInfo { @@ -264,3 +266,29 @@ pub struct XrGraphicsInfo { #[derive(Clone, Resource, ExtractResource, Deref, DerefMut)] pub struct XrViews(pub Vec); + +#[derive(Clone)] +/// This is used to store information from startup that is needed to create the session after the instance has been created. +pub struct XrSessionCreateInfo { + /// List of blend modes the openxr session can use. If [None], pick the first available blend mode. + pub blend_modes: Option>, + /// List of formats the openxr session can use. If [None], pick the first available format + pub formats: Option>, + /// List of resolutions that the openxr swapchain can use. If [None] pick the first available resolution. + pub resolutions: Option>, + /// Graphics info used to create a session. + pub graphics_info: XrSessionGraphicsInfo, +} + +#[derive(Resource, Clone, Default)] +pub struct XrSessionStarted(Arc); + +impl XrSessionStarted { + pub fn set(&self, val: bool) { + self.0.store(val, Ordering::SeqCst); + } + + pub fn get(&self) -> bool { + self.0.load(Ordering::SeqCst) + } +} diff --git a/crates/bevy_openxr/src/types.rs b/crates/bevy_openxr/src/types.rs index d1a1e96..305e0eb 100644 --- a/crates/bevy_openxr/src/types.rs +++ b/crates/bevy_openxr/src/types.rs @@ -1,6 +1,6 @@ use std::borrow::Cow; -use crate::error::XrError; +pub use crate::error::XrError; pub use crate::extensions::XrExtensions; use crate::graphics::GraphicsExt; diff --git a/crates/bevy_xr/src/session.rs b/crates/bevy_xr/src/session.rs index 1c885e6..31a3382 100644 --- a/crates/bevy_xr/src/session.rs +++ b/crates/bevy_xr/src/session.rs @@ -1,9 +1,6 @@ -use bevy::app::{App, First, Plugin}; -use bevy::ecs::event::{Event, EventWriter}; -use bevy::ecs::schedule::common_conditions::resource_exists_and_changed; -use bevy::ecs::schedule::IntoSystemConfigs; -use bevy::ecs::system::{Res, Resource}; -use bevy::render::extract_resource::ExtractResource; +use std::sync::{Arc, RwLock}; + +use bevy::prelude::*; pub struct XrSessionPlugin; @@ -14,13 +11,31 @@ impl Plugin for XrSessionPlugin { .add_event::() .add_event::() .add_systems( - First, - handle_session.run_if(resource_exists_and_changed::), + PreUpdate, + handle_session.run_if(resource_exists::), ); } } -#[derive(Resource, ExtractResource, Clone, Copy, Debug, PartialEq, Eq)] +#[derive(Resource, Clone)] +pub struct XrSharedStatus(Arc>); + +impl XrSharedStatus { + pub fn new(status: XrStatus) -> Self { + Self(Arc::new(RwLock::new(status))) + } + + pub fn get(&self) -> XrStatus { + *self.0.read().unwrap() + } + + pub fn set(&self, status: XrStatus) { + *self.0.write().unwrap() = status; + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[repr(u8)] pub enum XrStatus { /// An XR session is not available here Unavailable, @@ -39,44 +54,55 @@ pub enum XrStatus { } pub fn handle_session( - status: Res, + status: Res, + mut previous_status: Local>, mut create_session: EventWriter, mut begin_session: EventWriter, - mut end_session: EventWriter, + mut _end_session: EventWriter, ) { - match *status { - XrStatus::Unavailable => {} - XrStatus::Available => { - create_session.send_default(); + let current_status = status.get(); + if *previous_status != Some(current_status) { + match current_status { + XrStatus::Unavailable => {} + XrStatus::Available => { + create_session.send_default(); + } + XrStatus::Idle => {} + XrStatus::Ready => { + begin_session.send_default(); + } + XrStatus::Running => {} + XrStatus::Stopping => {} + XrStatus::Exiting => {} } - XrStatus::Idle => {} - XrStatus::Ready => { - begin_session.send_default(); - } - XrStatus::Running => {} - XrStatus::Stopping => {} - XrStatus::Exiting => {} } + *previous_status = Some(current_status); } /// A [`Condition`](bevy::ecs::schedule::Condition) system that says if the XR session is available. Returns true as long as [`XrStatus`] exists and isn't [`Unavailable`](XrStatus::Unavailable). -pub fn session_available(status: Option>) -> bool { - status.is_some_and(|s| *s != XrStatus::Unavailable) +pub fn session_available(status: Option>) -> bool { + status.is_some_and(|s| s.get() != XrStatus::Unavailable) } /// A [`Condition`](bevy::ecs::schedule::Condition) system that says if the XR session is ready or running -pub fn session_created(status: Option>) -> bool { - matches!(status.as_deref(), Some(XrStatus::Ready | XrStatus::Running)) +pub fn session_created(status: Option>) -> bool { + matches!( + status.as_deref().map(XrSharedStatus::get), + Some(XrStatus::Ready | XrStatus::Running) + ) } /// A [`Condition`](bevy::ecs::schedule::Condition) system that says if the XR session is running -pub fn session_running(status: Option>) -> bool { - matches!(status.as_deref(), Some(XrStatus::Running)) +pub fn session_running(status: Option>) -> bool { + matches!( + status.as_deref().map(XrSharedStatus::get), + Some(XrStatus::Running) + ) } /// A function that returns a [`Condition`](bevy::ecs::schedule::Condition) system that says if an the [`XrStatus`] is in a specific state -pub fn status_equals(status: XrStatus) -> impl FnMut(Option>) -> bool { - move |state: Option>| state.is_some_and(|s| *s == status) +pub fn status_equals(status: XrStatus) -> impl FnMut(Option>) -> bool { + move |state: Option>| state.is_some_and(|s| s.get() == status) } /// Event sent to backends to create an XR session. Should only be called in the [`XrStatus::Available`] state.