fixed jitter
This commit is contained in:
@@ -3,6 +3,8 @@ pub mod vulkan;
|
|||||||
|
|
||||||
use std::any::TypeId;
|
use std::any::TypeId;
|
||||||
|
|
||||||
|
use bevy::math::UVec2;
|
||||||
|
|
||||||
use crate::extensions::XrExtensions;
|
use crate::extensions::XrExtensions;
|
||||||
use crate::types::*;
|
use crate::types::*;
|
||||||
|
|
||||||
@@ -127,5 +129,4 @@ macro_rules! graphics_match {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
use bevy::math::UVec2;
|
|
||||||
pub(crate) use graphics_match;
|
pub(crate) use graphics_match;
|
||||||
|
|||||||
@@ -1,35 +1,30 @@
|
|||||||
use bevy::app::{App, First, Plugin, PostUpdate, PreUpdate};
|
use bevy::math::uvec2;
|
||||||
use bevy::ecs::change_detection::DetectChangesMut;
|
use bevy::prelude::*;
|
||||||
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::extract_resource::ExtractResourcePlugin;
|
||||||
use bevy::render::renderer::{
|
use bevy::render::renderer::{
|
||||||
RenderAdapter, RenderAdapterInfo, RenderDevice, RenderInstance, RenderQueue,
|
RenderAdapter, RenderAdapterInfo, RenderDevice, RenderInstance, RenderQueue,
|
||||||
};
|
};
|
||||||
use bevy::render::settings::RenderCreation;
|
use bevy::render::settings::RenderCreation;
|
||||||
use bevy::render::{ExtractSchedule, MainWorld, RenderApp, RenderPlugin};
|
use bevy::render::{MainWorld, RenderApp, RenderPlugin};
|
||||||
use bevy::transform::components::GlobalTransform;
|
|
||||||
use bevy::transform::{TransformBundle, TransformSystem};
|
|
||||||
use bevy_xr::session::{
|
use bevy_xr::session::{
|
||||||
handle_session, session_available, session_running, status_equals, BeginXrSession,
|
handle_session, session_available, session_running, status_equals, BeginXrSession,
|
||||||
CreateXrSession, XrStatus,
|
CreateXrSession, EndXrSession, XrSharedStatus, XrStatus,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::error::XrError;
|
use crate::graphics::*;
|
||||||
use crate::graphics::GraphicsBackend;
|
|
||||||
use crate::resources::*;
|
use crate::resources::*;
|
||||||
use crate::types::*;
|
use crate::types::*;
|
||||||
|
|
||||||
|
pub fn session_started(started: Option<Res<XrSessionStarted>>) -> bool {
|
||||||
|
started.is_some_and(|started| started.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy, SystemSet)]
|
||||||
|
pub enum XrPreUpdateSet {
|
||||||
|
PollEvents,
|
||||||
|
HandleEvents,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct XrInitPlugin {
|
pub struct XrInitPlugin {
|
||||||
/// Information about the app this is being used to build.
|
/// Information about the app this is being used to build.
|
||||||
pub app_info: AppInfo,
|
pub app_info: AppInfo,
|
||||||
@@ -50,11 +45,92 @@ pub struct XrInitPlugin {
|
|||||||
|
|
||||||
impl Plugin for XrInitPlugin {
|
impl Plugin for XrInitPlugin {
|
||||||
fn build(&self, app: &mut App) {
|
fn build(&self, app: &mut App) {
|
||||||
if let Err(e) = init_xr(&self, app) {
|
match self.init_xr() {
|
||||||
error!("Failed to initialize openxr instance: {e}.");
|
Ok((
|
||||||
app.add_plugins(RenderPlugin::default())
|
instance,
|
||||||
.insert_resource(XrStatus::Unavailable);
|
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::<XrTime>::default(),
|
||||||
|
))
|
||||||
|
.add_systems(
|
||||||
|
PreUpdate,
|
||||||
|
(
|
||||||
|
poll_events
|
||||||
|
.run_if(session_available)
|
||||||
|
.in_set(XrPreUpdateSet::PollEvents),
|
||||||
|
(
|
||||||
|
(create_xr_session, apply_deferred)
|
||||||
|
.chain()
|
||||||
|
.run_if(on_event::<CreateXrSession>())
|
||||||
|
.run_if(status_equals(XrStatus::Available)),
|
||||||
|
begin_xr_session
|
||||||
|
.run_if(on_event::<BeginXrSession>())
|
||||||
|
.run_if(status_equals(XrStatus::Ready)),
|
||||||
|
end_xr_session
|
||||||
|
.run_if(on_event::<EndXrSession>())
|
||||||
|
.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,14 +142,14 @@ fn xr_entry() -> Result<XrEntry> {
|
|||||||
Ok(XrEntry(entry))
|
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.
|
impl XrInitPlugin {
|
||||||
fn init_xr(config: &XrInitPlugin, app: &mut App) -> Result<()> {
|
fn init_xr(&self) -> Result<(XrInstance, XrSystemId, WgpuGraphics, XrSessionCreateInfo)> {
|
||||||
let entry = xr_entry()?;
|
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.
|
// check available extensions and send a warning for any wanted extensions that aren't available.
|
||||||
for ext in available_exts.unavailable_exts(&config.exts) {
|
for ext in available_exts.unavailable_exts(&self.exts) {
|
||||||
error!(
|
error!(
|
||||||
"Extension \"{ext}\" not available in the current OpenXR runtime. Disabling extension."
|
"Extension \"{ext}\" not available in the current OpenXR runtime. Disabling extension."
|
||||||
);
|
);
|
||||||
@@ -82,7 +158,7 @@ fn init_xr(config: &XrInitPlugin, app: &mut App) -> Result<()> {
|
|||||||
let available_backends = GraphicsBackend::available_backends(&available_exts);
|
let available_backends = GraphicsBackend::available_backends(&available_exts);
|
||||||
|
|
||||||
// Backend selection
|
// Backend selection
|
||||||
let backend = if let Some(wanted_backends) = &config.backends {
|
let backend = if let Some(wanted_backends) = &self.backends {
|
||||||
let mut backend = None;
|
let mut backend = None;
|
||||||
for wanted_backend in wanted_backends {
|
for wanted_backend in wanted_backends {
|
||||||
if available_backends.contains(wanted_backend) {
|
if available_backends.contains(wanted_backend) {
|
||||||
@@ -96,12 +172,13 @@ fn init_xr(config: &XrInitPlugin, app: &mut App) -> Result<()> {
|
|||||||
}
|
}
|
||||||
.ok_or(XrError::NoAvailableBackend)?;
|
.ok_or(XrError::NoAvailableBackend)?;
|
||||||
|
|
||||||
let exts = config.exts.clone() & available_exts;
|
let exts = self.exts.clone() & available_exts;
|
||||||
|
|
||||||
let instance = entry.create_instance(
|
let instance = entry.create_instance(
|
||||||
config.app_info.clone(),
|
self.app_info.clone(),
|
||||||
exts,
|
exts,
|
||||||
&["XR_APILAYER_LUNARG_api_dump"],
|
// &["XR_APILAYER_LUNARG_api_dump"],
|
||||||
|
&[],
|
||||||
backend,
|
backend,
|
||||||
)?;
|
)?;
|
||||||
let instance_props = instance.properties()?;
|
let instance_props = instance.properties()?;
|
||||||
@@ -123,157 +200,45 @@ fn init_xr(config: &XrInitPlugin, app: &mut App) -> Result<()> {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
let (WgpuGraphics(device, queue, adapter_info, adapter, wgpu_instance), create_info) =
|
let (graphics, graphics_info) = instance.init_graphics(system_id)?;
|
||||||
instance.init_graphics(system_id)?;
|
|
||||||
|
|
||||||
app.add_plugins((
|
let session_create_info = XrSessionCreateInfo {
|
||||||
RenderPlugin {
|
blend_modes: self.blend_modes.clone(),
|
||||||
render_creation: RenderCreation::manual(
|
formats: self.formats.clone(),
|
||||||
device.into(),
|
resolutions: self.resolutions.clone(),
|
||||||
RenderQueue(queue.into()),
|
graphics_info,
|
||||||
RenderAdapterInfo(adapter_info),
|
};
|
||||||
RenderAdapter(adapter.into()),
|
|
||||||
RenderInstance(wgpu_instance.into()),
|
Ok((
|
||||||
),
|
instance,
|
||||||
synchronous_pipeline_compilation: config.synchronous_pipeline_compilation,
|
XrSystemId(system_id),
|
||||||
},
|
graphics,
|
||||||
ExtractComponentPlugin::<XrRoot>::default(),
|
session_create_info,
|
||||||
ExtractResourcePlugin::<XrTime>::default(),
|
|
||||||
ExtractResourcePlugin::<XrStatus>::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::<CreateXrSession>())
|
|
||||||
.run_if(status_equals(XrStatus::Available)),
|
|
||||||
begin_xr_session
|
|
||||||
.run_if(status_equals(XrStatus::Ready))
|
|
||||||
.run_if(on_event::<BeginXrSession>()),
|
|
||||||
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<Entity, (With<OpenXrTracker>, Without<Parent>)>,
|
|
||||||
mut commands: Commands,
|
|
||||||
tracking_root_query: Query<Entity, With<OpenXrTrackingRoot>>,
|
|
||||||
) {
|
|
||||||
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(
|
fn init_xr_session(
|
||||||
mut component_query: Query<&mut XrRoot>,
|
device: &wgpu::Device,
|
||||||
root_query: Query<&GlobalTransform, With<OpenXrTrackingRoot>>,
|
instance: &XrInstance,
|
||||||
) {
|
|
||||||
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<Vec<EnvironmentBlendMode>>,
|
|
||||||
/// List of formats the openxr session can use. If [None], pick the first available format
|
|
||||||
formats: Option<Vec<wgpu::TextureFormat>>,
|
|
||||||
/// List of resolutions that the openxr swapchain can use. If [None] pick the first available resolution.
|
|
||||||
resolutions: Option<Vec<UVec2>>,
|
|
||||||
/// 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::<SystemId>().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,
|
system_id: openxr::SystemId,
|
||||||
config: XrSessionInitConfig,
|
XrSessionCreateInfo {
|
||||||
) -> Result<()> {
|
blend_modes,
|
||||||
|
formats,
|
||||||
|
resolutions,
|
||||||
|
graphics_info,
|
||||||
|
}: XrSessionCreateInfo,
|
||||||
|
) -> Result<(
|
||||||
|
XrSession,
|
||||||
|
XrFrameWaiter,
|
||||||
|
XrFrameStream,
|
||||||
|
XrSwapchain,
|
||||||
|
XrSwapchainImages,
|
||||||
|
XrGraphicsInfo,
|
||||||
|
XrStage,
|
||||||
|
)> {
|
||||||
let (session, frame_waiter, frame_stream) =
|
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
|
// TODO!() support other view configurations
|
||||||
let available_view_configurations = instance.enumerate_view_configurations(system_id)?;
|
let available_view_configurations = instance.enumerate_view_configurations(system_id)?;
|
||||||
@@ -286,7 +251,7 @@ fn create_xr_session_inner(
|
|||||||
let view_configuration_views =
|
let view_configuration_views =
|
||||||
instance.enumerate_view_configuration_views(system_id, view_configuration_type)?;
|
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;
|
let mut preferred = None;
|
||||||
for resolution in resolutions {
|
for resolution in resolutions {
|
||||||
for view_config in view_configuration_views.iter() {
|
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 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;
|
let mut format = None;
|
||||||
for wanted_format in formats {
|
for wanted_format in formats {
|
||||||
if available_formats.contains(wanted_format) {
|
if available_formats.contains(wanted_format) {
|
||||||
@@ -354,17 +319,13 @@ fn create_xr_session_inner(
|
|||||||
mip_count: 1,
|
mip_count: 1,
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let images = swapchain.enumerate_images(
|
let images = swapchain.enumerate_images(device, format, resolution)?;
|
||||||
world.resource::<RenderDevice>().wgpu_device(),
|
|
||||||
format,
|
|
||||||
resolution,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let available_blend_modes =
|
let available_blend_modes =
|
||||||
instance.enumerate_environment_blend_modes(system_id, view_configuration_type)?;
|
instance.enumerate_environment_blend_modes(system_id, view_configuration_type)?;
|
||||||
|
|
||||||
// blend mode selection
|
// 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;
|
let mut blend_mode = None;
|
||||||
for wanted_blend_mode in wanted_blend_modes {
|
for wanted_blend_mode in wanted_blend_modes {
|
||||||
if available_blend_modes.contains(wanted_blend_mode) {
|
if available_blend_modes.contains(wanted_blend_mode) {
|
||||||
@@ -390,29 +351,15 @@ fn create_xr_session_inner(
|
|||||||
format,
|
format,
|
||||||
};
|
};
|
||||||
|
|
||||||
world.insert_resource(session.clone());
|
Ok((
|
||||||
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,
|
session,
|
||||||
|
frame_waiter,
|
||||||
frame_stream,
|
frame_stream,
|
||||||
swapchain,
|
swapchain,
|
||||||
images,
|
images,
|
||||||
graphics_info,
|
graphics_info,
|
||||||
stage,
|
stage,
|
||||||
});
|
))
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn begin_xr_session(session: Res<XrSession>, mut status: ResMut<XrStatus>) {
|
|
||||||
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.
|
/// This is used solely to transport resources from the main world to the render world.
|
||||||
@@ -426,6 +373,51 @@ struct XrRenderResources {
|
|||||||
stage: XrStage,
|
stage: XrStage,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn create_xr_session(
|
||||||
|
device: Res<RenderDevice>,
|
||||||
|
instance: Res<XrInstance>,
|
||||||
|
create_info: NonSend<XrSessionCreateInfo>,
|
||||||
|
system_id: Res<XrSystemId>,
|
||||||
|
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<XrSession>, session_started: Res<XrSessionStarted>) {
|
||||||
|
session
|
||||||
|
.begin(openxr::ViewConfigurationType::PRIMARY_STEREO)
|
||||||
|
.expect("Failed to begin session");
|
||||||
|
session_started.set(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn end_xr_session(session: Res<XrSession>, session_started: Res<XrSessionStarted>) {
|
||||||
|
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.
|
/// 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<MainWorld>) {
|
pub fn transfer_xr_resources(mut commands: Commands, mut world: ResMut<MainWorld>) {
|
||||||
let Some(XrRenderResources {
|
let Some(XrRenderResources {
|
||||||
@@ -448,8 +440,8 @@ pub fn transfer_xr_resources(mut commands: Commands, mut world: ResMut<MainWorld
|
|||||||
commands.insert_resource(stage);
|
commands.insert_resource(stage);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Poll any OpenXR events and handle them accordingly
|
/// Polls any OpenXR events and handles them accordingly
|
||||||
pub fn poll_events(instance: Res<XrInstance>, mut status: ResMut<XrStatus>) {
|
pub fn poll_events(instance: Res<XrInstance>, status: Res<XrSharedStatus>) {
|
||||||
let mut buffer = Default::default();
|
let mut buffer = Default::default();
|
||||||
while let Some(event) = instance
|
while let Some(event) = instance
|
||||||
.poll_event(&mut buffer)
|
.poll_event(&mut buffer)
|
||||||
@@ -457,29 +449,28 @@ pub fn poll_events(instance: Res<XrInstance>, mut status: ResMut<XrStatus>) {
|
|||||||
{
|
{
|
||||||
use openxr::Event::*;
|
use openxr::Event::*;
|
||||||
match event {
|
match event {
|
||||||
SessionStateChanged(e) => {
|
SessionStateChanged(state) => {
|
||||||
info!("entered XR state {:?}", e.state());
|
|
||||||
use openxr::SessionState;
|
use openxr::SessionState;
|
||||||
|
|
||||||
match e.state() {
|
let state = state.state();
|
||||||
SessionState::IDLE => {
|
|
||||||
*status = XrStatus::Idle;
|
info!("entered XR state {:?}", state);
|
||||||
}
|
|
||||||
SessionState::READY => {
|
let new_status = match state {
|
||||||
*status = XrStatus::Ready;
|
SessionState::IDLE => XrStatus::Idle,
|
||||||
}
|
SessionState::READY => XrStatus::Ready,
|
||||||
SessionState::SYNCHRONIZED | SessionState::VISIBLE | SessionState::FOCUSED => {
|
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(_) => {}
|
InstanceLossPending(_) => {}
|
||||||
|
EventsLost(e) => warn!("lost {} XR events", e.lost_event_count()),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
486
crates/bevy_openxr/src/initt.rs
Normal file
486
crates/bevy_openxr/src/initt.rs
Normal file
@@ -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<Vec<EnvironmentBlendMode>>,
|
||||||
|
/// List of backends the openxr session can use. If [None], pick the first available backend.
|
||||||
|
pub backends: Option<Vec<GraphicsBackend>>,
|
||||||
|
/// List of formats the openxr session can use. If [None], pick the first available format
|
||||||
|
pub formats: Option<Vec<wgpu::TextureFormat>>,
|
||||||
|
/// List of resolutions that the openxr swapchain can use. If [None] pick the first available resolution.
|
||||||
|
pub resolutions: Option<Vec<UVec2>>,
|
||||||
|
/// 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<XrEntry> {
|
||||||
|
#[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() {
|
||||||
|
"<unnamed>"
|
||||||
|
} 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::<XrRoot>::default(),
|
||||||
|
ExtractResourcePlugin::<XrTime>::default(),
|
||||||
|
ExtractResourcePlugin::<XrStatus>::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::<CreateXrSession>())
|
||||||
|
.run_if(status_equals(XrStatus::Available)),
|
||||||
|
begin_xr_session
|
||||||
|
.run_if(status_equals(XrStatus::Ready))
|
||||||
|
.run_if(on_event::<BeginXrSession>()),
|
||||||
|
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<Entity, (With<OpenXrTracker>, Without<Parent>)>,
|
||||||
|
mut commands: Commands,
|
||||||
|
tracking_root_query: Query<Entity, With<OpenXrTrackingRoot>>,
|
||||||
|
) {
|
||||||
|
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<OpenXrTrackingRoot>>,
|
||||||
|
) {
|
||||||
|
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<Vec<EnvironmentBlendMode>>,
|
||||||
|
/// List of formats the openxr session can use. If [None], pick the first available format
|
||||||
|
formats: Option<Vec<wgpu::TextureFormat>>,
|
||||||
|
/// List of resolutions that the openxr swapchain can use. If [None] pick the first available resolution.
|
||||||
|
resolutions: Option<Vec<UVec2>>,
|
||||||
|
/// 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::<SystemId>().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::<RenderDevice>().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<XrSession>, mut status: ResMut<XrStatus>) {
|
||||||
|
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<MainWorld>) {
|
||||||
|
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<XrInstance>, mut status: ResMut<XrStatus>) {
|
||||||
|
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(_) => {}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ use bevy::{
|
|||||||
app::{PluginGroup, PluginGroupBuilder},
|
app::{PluginGroup, PluginGroupBuilder},
|
||||||
render::{pipelined_rendering::PipelinedRenderingPlugin, RenderPlugin},
|
render::{pipelined_rendering::PipelinedRenderingPlugin, RenderPlugin},
|
||||||
utils::default,
|
utils::default,
|
||||||
|
window::{PresentMode, Window, WindowPlugin},
|
||||||
};
|
};
|
||||||
use bevy_xr::camera::XrCameraPlugin;
|
use bevy_xr::camera::XrCameraPlugin;
|
||||||
use init::XrInitPlugin;
|
use init::XrInitPlugin;
|
||||||
@@ -34,4 +35,20 @@ pub fn add_xr_plugins<G: PluginGroup>(plugins: G) -> PluginGroupBuilder {
|
|||||||
})
|
})
|
||||||
.add(XrRenderPlugin)
|
.add(XrRenderPlugin)
|
||||||
.add(XrCameraPlugin)
|
.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()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,19 +3,16 @@ use bevy::{
|
|||||||
render::{
|
render::{
|
||||||
camera::{ManualTextureView, ManualTextureViewHandle, ManualTextureViews, RenderTarget},
|
camera::{ManualTextureView, ManualTextureViewHandle, ManualTextureViews, RenderTarget},
|
||||||
extract_resource::ExtractResourcePlugin,
|
extract_resource::ExtractResourcePlugin,
|
||||||
renderer::render_system,
|
|
||||||
view::ExtractedView,
|
|
||||||
Render, RenderApp, RenderSet,
|
Render, RenderApp, RenderSet,
|
||||||
},
|
},
|
||||||
|
transform::TransformSystem,
|
||||||
};
|
};
|
||||||
use bevy_xr::{
|
use bevy_xr::camera::{XrCamera, XrCameraBundle, XrProjection};
|
||||||
camera::{XrCamera, XrCameraBundle, XrProjection},
|
|
||||||
session::session_running,
|
|
||||||
};
|
|
||||||
use openxr::{CompositionLayerFlags, ViewStateFlags};
|
use openxr::{CompositionLayerFlags, ViewStateFlags};
|
||||||
|
|
||||||
use crate::{init::OpenXrTracker, resources::*};
|
use crate::init::{session_started, XrPreUpdateSet};
|
||||||
use crate::{init::XrRoot, layer_builder::*};
|
use crate::layer_builder::*;
|
||||||
|
use crate::resources::*;
|
||||||
|
|
||||||
pub struct XrRenderPlugin;
|
pub struct XrRenderPlugin;
|
||||||
|
|
||||||
@@ -26,27 +23,30 @@ impl Plugin for XrRenderPlugin {
|
|||||||
PreUpdate,
|
PreUpdate,
|
||||||
(
|
(
|
||||||
init_views.run_if(resource_added::<XrGraphicsInfo>),
|
init_views.run_if(resource_added::<XrGraphicsInfo>),
|
||||||
wait_frame.run_if(session_running),
|
wait_frame.run_if(session_started),
|
||||||
locate_views.run_if(session_running),
|
|
||||||
update_views.run_if(session_running),
|
|
||||||
)
|
)
|
||||||
.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(
|
app.sub_app_mut(RenderApp).add_systems(
|
||||||
Render,
|
Render,
|
||||||
(
|
(
|
||||||
(
|
(
|
||||||
//locate_views,
|
// begin_frame,
|
||||||
update_views_render_world,
|
insert_texture_views
|
||||||
insert_texture_views,
|
|
||||||
)
|
)
|
||||||
.chain()
|
.chain()
|
||||||
.in_set(RenderSet::PrepareAssets)
|
.in_set(RenderSet::PrepareAssets),
|
||||||
.before(render_system),
|
(locate_views, end_frame).chain().in_set(RenderSet::Cleanup),
|
||||||
end_frame.in_set(RenderSet::Cleanup),
|
|
||||||
)
|
)
|
||||||
.run_if(session_running),
|
.run_if(session_started),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -79,8 +79,8 @@ pub fn init_views(
|
|||||||
view: XrCamera(index),
|
view: XrCamera(index),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
OpenXrTracker,
|
// OpenXrTracker,
|
||||||
XrRoot::default(),
|
// XrRoot::default(),
|
||||||
));
|
));
|
||||||
views.push(default());
|
views.push(default());
|
||||||
}
|
}
|
||||||
@@ -90,7 +90,7 @@ pub fn init_views(
|
|||||||
pub fn wait_frame(
|
pub fn wait_frame(
|
||||||
mut frame_waiter: ResMut<XrFrameWaiter>,
|
mut frame_waiter: ResMut<XrFrameWaiter>,
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
frame_stream: ResMut<XrFrameStream>,
|
frame_stream: Res<XrFrameStream>,
|
||||||
) {
|
) {
|
||||||
let _span = info_span!("xr_wait_frame");
|
let _span = info_span!("xr_wait_frame");
|
||||||
let state = frame_waiter.wait().expect("Failed to 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<crate::resources::XrViews>,
|
|
||||||
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 {
|
fn calculate_projection(near_z: f32, fov: openxr::Fovf) -> Mat4 {
|
||||||
// symmetric perspective for debugging
|
// symmetric perspective for debugging
|
||||||
// let x_fov = (self.fov.angle_left.abs() + self.fov.angle_right.abs());
|
// let x_fov = (self.fov.angle_left.abs() + self.fov.angle_right.abs());
|
||||||
@@ -312,6 +293,10 @@ pub fn add_texture_view(
|
|||||||
handle
|
handle
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn begin_frame(frame_stream: ResMut<XrFrameStream>) {
|
||||||
|
frame_stream.begin().expect("Failed to begin frame")
|
||||||
|
}
|
||||||
|
|
||||||
pub fn end_frame(
|
pub fn end_frame(
|
||||||
mut frame_stream: ResMut<XrFrameStream>,
|
mut frame_stream: ResMut<XrFrameStream>,
|
||||||
mut swapchain: ResMut<XrSwapchain>,
|
mut swapchain: ResMut<XrSwapchain>,
|
||||||
@@ -357,5 +342,5 @@ pub fn end_frame(
|
|||||||
),
|
),
|
||||||
])],
|
])],
|
||||||
)
|
)
|
||||||
.expect("Failed to end stream");
|
.expect("Failed to end frame");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
use crate::error::XrError;
|
use crate::error::XrError;
|
||||||
@@ -63,13 +64,13 @@ impl XrInstance {
|
|||||||
pub fn init_graphics(
|
pub fn init_graphics(
|
||||||
&self,
|
&self,
|
||||||
system_id: openxr::SystemId,
|
system_id: openxr::SystemId,
|
||||||
) -> Result<(WgpuGraphics, SessionCreateInfo)> {
|
) -> Result<(WgpuGraphics, XrSessionGraphicsInfo)> {
|
||||||
graphics_match!(
|
graphics_match!(
|
||||||
self.1;
|
self.1;
|
||||||
_ => {
|
_ => {
|
||||||
let (graphics, session_info) = Api::init_graphics(&self.2, &self, system_id)?;
|
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(
|
pub unsafe fn create_session(
|
||||||
&self,
|
&self,
|
||||||
system_id: openxr::SystemId,
|
system_id: openxr::SystemId,
|
||||||
info: SessionCreateInfo,
|
info: XrSessionGraphicsInfo,
|
||||||
) -> Result<(XrSession, XrFrameWaiter, XrFrameStream)> {
|
) -> Result<(XrSession, XrFrameWaiter, XrFrameStream)> {
|
||||||
if !info.0.using_graphics_of_val(&self.1) {
|
if !info.0.using_graphics_of_val(&self.1) {
|
||||||
return Err(XrError::GraphicsBackendMismatch {
|
return Err(XrError::GraphicsBackendMismatch {
|
||||||
item: std::any::type_name::<SessionCreateInfo>(),
|
item: std::any::type_name::<XrSessionGraphicsInfo>(),
|
||||||
backend: info.0.graphics_name(),
|
backend: info.0.graphics_name(),
|
||||||
expected_backend: self.1.graphics_name(),
|
expected_backend: self.1.graphics_name(),
|
||||||
});
|
});
|
||||||
@@ -99,22 +100,23 @@ impl XrInstance {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct SessionCreateInfo(pub(crate) GraphicsWrap<Self>);
|
#[derive(Clone)]
|
||||||
|
pub struct XrSessionGraphicsInfo(pub(crate) GraphicsWrap<Self>);
|
||||||
|
|
||||||
impl GraphicsType for SessionCreateInfo {
|
impl GraphicsType for XrSessionGraphicsInfo {
|
||||||
type Inner<G: GraphicsExt> = G::SessionCreateInfo;
|
type Inner<G: GraphicsExt> = G::SessionCreateInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GraphicsType for XrSession {
|
|
||||||
type Inner<G: GraphicsExt> = openxr::Session<G>;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Resource, Deref, Clone)]
|
#[derive(Resource, Deref, Clone)]
|
||||||
pub struct XrSession(
|
pub struct XrSession(
|
||||||
#[deref] pub(crate) openxr::Session<AnyGraphics>,
|
#[deref] pub(crate) openxr::Session<AnyGraphics>,
|
||||||
pub(crate) GraphicsWrap<Self>,
|
pub(crate) GraphicsWrap<Self>,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
impl GraphicsType for XrSession {
|
||||||
|
type Inner<G: GraphicsExt> = openxr::Session<G>;
|
||||||
|
}
|
||||||
|
|
||||||
impl<G: GraphicsExt> From<openxr::Session<G>> for XrSession {
|
impl<G: GraphicsExt> From<openxr::Session<G>> for XrSession {
|
||||||
fn from(value: openxr::Session<G>) -> Self {
|
fn from(value: openxr::Session<G>) -> Self {
|
||||||
Self(value.clone().into_any_graphics(), G::wrap(value))
|
Self(value.clone().into_any_graphics(), G::wrap(value))
|
||||||
@@ -159,7 +161,7 @@ impl XrFrameStream {
|
|||||||
layers: &[&dyn CompositionLayer],
|
layers: &[&dyn CompositionLayer],
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
graphics_match!(
|
graphics_match!(
|
||||||
&self.0;
|
&mut self.0;
|
||||||
stream => {
|
stream => {
|
||||||
let mut stream = stream.lock().unwrap();
|
let mut stream = stream.lock().unwrap();
|
||||||
let mut new_layers = vec![];
|
let mut new_layers = vec![];
|
||||||
@@ -253,7 +255,7 @@ pub struct XrSwapchainInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone, Deref, Default, Eq, PartialEq, Ord, PartialOrd, Hash, Resource)]
|
#[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)]
|
#[derive(Clone, Copy, Resource)]
|
||||||
pub struct XrGraphicsInfo {
|
pub struct XrGraphicsInfo {
|
||||||
@@ -264,3 +266,29 @@ pub struct XrGraphicsInfo {
|
|||||||
|
|
||||||
#[derive(Clone, Resource, ExtractResource, Deref, DerefMut)]
|
#[derive(Clone, Resource, ExtractResource, Deref, DerefMut)]
|
||||||
pub struct XrViews(pub Vec<openxr::View>);
|
pub struct XrViews(pub Vec<openxr::View>);
|
||||||
|
|
||||||
|
#[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<Vec<EnvironmentBlendMode>>,
|
||||||
|
/// List of formats the openxr session can use. If [None], pick the first available format
|
||||||
|
pub formats: Option<Vec<wgpu::TextureFormat>>,
|
||||||
|
/// List of resolutions that the openxr swapchain can use. If [None] pick the first available resolution.
|
||||||
|
pub resolutions: Option<Vec<UVec2>>,
|
||||||
|
/// Graphics info used to create a session.
|
||||||
|
pub graphics_info: XrSessionGraphicsInfo,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Resource, Clone, Default)]
|
||||||
|
pub struct XrSessionStarted(Arc<AtomicBool>);
|
||||||
|
|
||||||
|
impl XrSessionStarted {
|
||||||
|
pub fn set(&self, val: bool) {
|
||||||
|
self.0.store(val, Ordering::SeqCst);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(&self) -> bool {
|
||||||
|
self.0.load(Ordering::SeqCst)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
|
||||||
use crate::error::XrError;
|
pub use crate::error::XrError;
|
||||||
pub use crate::extensions::XrExtensions;
|
pub use crate::extensions::XrExtensions;
|
||||||
use crate::graphics::GraphicsExt;
|
use crate::graphics::GraphicsExt;
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
use bevy::app::{App, First, Plugin};
|
use std::sync::{Arc, RwLock};
|
||||||
use bevy::ecs::event::{Event, EventWriter};
|
|
||||||
use bevy::ecs::schedule::common_conditions::resource_exists_and_changed;
|
use bevy::prelude::*;
|
||||||
use bevy::ecs::schedule::IntoSystemConfigs;
|
|
||||||
use bevy::ecs::system::{Res, Resource};
|
|
||||||
use bevy::render::extract_resource::ExtractResource;
|
|
||||||
|
|
||||||
pub struct XrSessionPlugin;
|
pub struct XrSessionPlugin;
|
||||||
|
|
||||||
@@ -14,13 +11,31 @@ impl Plugin for XrSessionPlugin {
|
|||||||
.add_event::<BeginXrSession>()
|
.add_event::<BeginXrSession>()
|
||||||
.add_event::<EndXrSession>()
|
.add_event::<EndXrSession>()
|
||||||
.add_systems(
|
.add_systems(
|
||||||
First,
|
PreUpdate,
|
||||||
handle_session.run_if(resource_exists_and_changed::<XrStatus>),
|
handle_session.run_if(resource_exists::<XrSharedStatus>),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Resource, ExtractResource, Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Resource, Clone)]
|
||||||
|
pub struct XrSharedStatus(Arc<RwLock<XrStatus>>);
|
||||||
|
|
||||||
|
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 {
|
pub enum XrStatus {
|
||||||
/// An XR session is not available here
|
/// An XR session is not available here
|
||||||
Unavailable,
|
Unavailable,
|
||||||
@@ -39,12 +54,15 @@ pub enum XrStatus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_session(
|
pub fn handle_session(
|
||||||
status: Res<XrStatus>,
|
status: Res<XrSharedStatus>,
|
||||||
|
mut previous_status: Local<Option<XrStatus>>,
|
||||||
mut create_session: EventWriter<CreateXrSession>,
|
mut create_session: EventWriter<CreateXrSession>,
|
||||||
mut begin_session: EventWriter<BeginXrSession>,
|
mut begin_session: EventWriter<BeginXrSession>,
|
||||||
mut end_session: EventWriter<EndXrSession>,
|
mut _end_session: EventWriter<EndXrSession>,
|
||||||
) {
|
) {
|
||||||
match *status {
|
let current_status = status.get();
|
||||||
|
if *previous_status != Some(current_status) {
|
||||||
|
match current_status {
|
||||||
XrStatus::Unavailable => {}
|
XrStatus::Unavailable => {}
|
||||||
XrStatus::Available => {
|
XrStatus::Available => {
|
||||||
create_session.send_default();
|
create_session.send_default();
|
||||||
@@ -57,26 +75,34 @@ pub fn handle_session(
|
|||||||
XrStatus::Stopping => {}
|
XrStatus::Stopping => {}
|
||||||
XrStatus::Exiting => {}
|
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).
|
/// 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<Res<XrStatus>>) -> bool {
|
pub fn session_available(status: Option<Res<XrSharedStatus>>) -> bool {
|
||||||
status.is_some_and(|s| *s != XrStatus::Unavailable)
|
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
|
/// A [`Condition`](bevy::ecs::schedule::Condition) system that says if the XR session is ready or running
|
||||||
pub fn session_created(status: Option<Res<XrStatus>>) -> bool {
|
pub fn session_created(status: Option<Res<XrSharedStatus>>) -> bool {
|
||||||
matches!(status.as_deref(), Some(XrStatus::Ready | XrStatus::Running))
|
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
|
/// A [`Condition`](bevy::ecs::schedule::Condition) system that says if the XR session is running
|
||||||
pub fn session_running(status: Option<Res<XrStatus>>) -> bool {
|
pub fn session_running(status: Option<Res<XrSharedStatus>>) -> bool {
|
||||||
matches!(status.as_deref(), Some(XrStatus::Running))
|
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
|
/// 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<Res<XrStatus>>) -> bool {
|
pub fn status_equals(status: XrStatus) -> impl FnMut(Option<Res<XrSharedStatus>>) -> bool {
|
||||||
move |state: Option<Res<XrStatus>>| state.is_some_and(|s| *s == status)
|
move |state: Option<Res<XrSharedStatus>>| 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.
|
/// Event sent to backends to create an XR session. Should only be called in the [`XrStatus::Available`] state.
|
||||||
|
|||||||
Reference in New Issue
Block a user