Files
bevy_oxr/crates/bevy_openxr/src/openxr/init.rs
2025-10-19 17:14:35 +02:00

611 lines
22 KiB
Rust

use std::sync::Arc;
use std::sync::atomic::Ordering;
use bevy_app::App;
use bevy_app::Plugin;
use bevy_ecs::message::MessageWriter;
use bevy_ecs::message::Messages;
use bevy_ecs::resource::Resource;
use bevy_ecs::schedule::IntoScheduleConfigs as _;
use bevy_ecs::schedule::SystemCondition as _;
use bevy_ecs::schedule::common_conditions::on_message;
use bevy_ecs::schedule::common_conditions::resource_exists;
use bevy_ecs::system::Commands;
use bevy_ecs::system::Local;
use bevy_ecs::system::Res;
use bevy_ecs::system::ResMut;
use bevy_ecs::world::World;
use bevy_log::debug;
use bevy_log::debug_span;
use bevy_log::error;
use bevy_log::info;
use bevy_log::warn;
use bevy_math::UVec2;
use bevy_mod_xr::session::*;
use bevy_render::ExtractSchedule;
use bevy_render::MainWorld;
use bevy_render::Render;
use bevy_render::RenderApp;
use bevy_render::RenderDebugFlags;
use bevy_render::RenderPlugin;
use bevy_render::extract_resource::ExtractResourcePlugin;
use bevy_render::renderer::RenderAdapter;
use bevy_render::renderer::RenderAdapterInfo;
use bevy_render::renderer::RenderDevice;
use bevy_render::renderer::RenderInstance;
use bevy_render::renderer::RenderQueue;
use bevy_render::renderer::WgpuWrapper;
use bevy_render::settings::RenderCreation;
#[cfg(feature = "window_support")]
use bevy_winit::UpdateMode;
#[cfg(feature = "window_support")]
use bevy_winit::WinitSettings;
use crate::error::OxrError;
use crate::graphics::*;
use crate::resources::*;
use crate::session::OxrSession;
use crate::session::OxrSessionCreateNextChain;
use crate::types::Result as OxrResult;
use crate::types::*;
use super::environment_blend_mode::OxrEnvironmentBlendModes;
use super::exts::OxrEnabledExtensions;
use super::poll_events::OxrEventHandlerExt;
use super::poll_events::OxrEventIn;
pub fn session_started(started: Option<Res<OxrSessionStarted>>) -> bool {
started.is_some_and(|started| started.0)
}
pub fn should_run_frame_loop(
started: Option<Res<OxrSessionStarted>>,
state: Option<Res<XrState>>,
) -> bool {
started.is_some_and(|started| started.0)
&& state.is_some_and(|state| *state != XrState::Stopping)
}
pub fn should_render(frame_state: Option<Res<OxrFrameState>>) -> bool {
frame_state.is_some_and(|frame_state| frame_state.should_render)
}
pub struct OxrInitPlugin {
/// 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: OxrExtensions,
/// List of backends the openxr session can use. If [None], pick the first available backend.
pub backends: Option<Vec<GraphicsBackend>>,
/// Passed into the render plugin when added to the app.
pub synchronous_pipeline_compilation: bool,
pub render_debug_flags: RenderDebugFlags,
}
impl Default for OxrInitPlugin {
fn default() -> Self {
Self {
app_info: Default::default(),
exts: {
let mut exts = OxrExtensions::default();
exts.enable_hand_tracking();
exts
},
backends: Default::default(),
synchronous_pipeline_compilation: false,
render_debug_flags: Default::default(),
}
}
}
impl Plugin for OxrInitPlugin {
fn build(&self, app: &mut App) {
app.init_resource::<OxrSessionConfig>();
let cfg = app.world_mut().remove_resource::<OxrManualGraphicsConfig>();
match self.init_xr(cfg.as_ref()) {
Ok((
instance,
system_id,
WgpuGraphics(device, queue, adapter_info, adapter, wgpu_instance),
enabled_exts,
graphics_info,
)) => {
app.insert_resource(enabled_exts)
.add_plugins((
RenderPlugin {
render_creation: RenderCreation::manual(
device.into(),
RenderQueue(Arc::new(WgpuWrapper::new(queue))),
RenderAdapterInfo(WgpuWrapper::new(adapter_info)),
RenderAdapter(Arc::new(WgpuWrapper::new(adapter))),
RenderInstance(Arc::new(WgpuWrapper::new(wgpu_instance))),
),
synchronous_pipeline_compilation: self.synchronous_pipeline_compilation,
debug_flags: self.render_debug_flags,
},
ExtractResourcePlugin::<OxrSessionStarted>::default(),
ExtractResourcePlugin::<OxrEnvironmentBlendModes>::default(),
))
.add_oxr_event_handler(handle_events)
.add_systems(
XrFirst,
(
create_xr_session
.run_if(state_equals(XrState::Available))
.run_if(on_message::<XrCreateSessionMessage>),
(
destroy_xr_session,
(|v: Res<XrDestroySessionRender>| {
v.0.store(true, Ordering::Relaxed);
debug!("setting destroy render session");
}),
)
.chain()
.run_if(state_matches!(XrState::Exiting { .. }))
.run_if(on_message::<XrDestroySessionMessage>),
begin_xr_session
.run_if(state_equals(XrState::Ready))
.run_if(on_message::<XrBeginSessionMessage>),
end_xr_session
.run_if(state_equals(XrState::Stopping))
.run_if(on_message::<XrEndSessionMessage>),
request_exit_xr_session
.run_if(session_created)
.run_if(on_message::<XrRequestExitMessage>),
detect_session_destroyed,
)
.in_set(XrHandleEvents::SessionStateUpdateEvents),
)
.insert_resource(instance.clone())
.insert_resource(system_id)
.insert_resource(XrState::Available)
.insert_resource(OxrSessionStarted(false))
.insert_non_send_resource(graphics_info)
.init_non_send_resource::<OxrSessionCreateNextChain>();
#[cfg(feature = "window_support")]
{
app.insert_resource(WinitSettings {
focused_mode: UpdateMode::Continuous,
unfocused_mode: UpdateMode::Continuous,
});
};
app.world_mut()
.resource_mut::<Messages<XrStateChanged>>()
.write(XrStateChanged(XrState::Available));
let render_app = app.sub_app_mut(RenderApp);
render_app
.add_systems(ExtractSchedule, transfer_xr_resources)
.insert_resource(instance)
.insert_resource(system_id)
.insert_resource(XrState::Available)
.insert_resource(OxrSessionStarted(false));
}
Err(e) => {
error!("Failed to initialize openxr: {e}");
if let Some(cfg) = cfg
&& let Ok(WgpuGraphics(device, queue, adapter_info, adapter, wgpu_instance)) =
graphics_match!(
cfg.fallback_backend;
_ => Api::init_fallback_graphics(&self.app_info ,&cfg)
)
.inspect_err(|err| {
error!("Failed to initialize custom fallback graphics: {err}")
})
{
app.add_plugins(RenderPlugin {
render_creation: RenderCreation::manual(
device.into(),
RenderQueue(Arc::new(WgpuWrapper::new(queue))),
RenderAdapterInfo(WgpuWrapper::new(adapter_info)),
RenderAdapter(Arc::new(WgpuWrapper::new(adapter))),
RenderInstance(Arc::new(WgpuWrapper::new(wgpu_instance))),
),
synchronous_pipeline_compilation: self.synchronous_pipeline_compilation,
debug_flags: self.render_debug_flags,
})
.insert_resource(XrState::Unavailable);
return;
}
app.add_plugins(RenderPlugin::default())
.insert_resource(XrState::Unavailable);
}
};
}
fn finish(&self, app: &mut App) {
app.sub_app_mut(RenderApp).add_systems(
Render,
(
destroy_xr_session,
|v: Res<XrDestroySessionRender>, mut cmds: Commands| {
v.0.store(false, Ordering::Relaxed);
cmds.insert_resource(XrState::Available);
},
)
.chain()
.run_if(
resource_exists::<XrDestroySessionRender>
.and(|v: Res<XrDestroySessionRender>| v.0.load(Ordering::Relaxed)),
),
);
}
}
fn detect_session_destroyed(
mut last_state: Local<bool>,
state: Res<XrDestroySessionRender>,
mut sender: MessageWriter<XrSessionDestroyedMessage>,
mut cmds: Commands,
) {
let state = state.0.load(Ordering::Relaxed);
if *last_state && !state {
debug!("XrSession was fully destroyed!");
sender.write_default();
cmds.insert_resource(XrState::Available);
}
*last_state = state;
}
impl OxrInitPlugin {
fn init_xr(
&self,
cfg: Option<&OxrManualGraphicsConfig>,
) -> OxrResult<(
OxrInstance,
OxrSystemId,
WgpuGraphics,
OxrEnabledExtensions,
SessionGraphicsCreateInfo,
)> {
#[cfg(windows)]
let entry = OxrEntry(openxr::Entry::linked());
#[cfg(not(windows))]
let entry = OxrEntry(unsafe { openxr::Entry::load()? });
#[cfg(target_os = "android")]
entry.initialize_android_loader()?;
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(&self.exts) {
warn!(
"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) = &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 {
available_backends.first().copied()
}
.ok_or(OxrError::NoAvailableBackend)?;
let exts = self.exts.clone() & available_exts;
let instance = entry.create_instance(self.app_info.clone(), exts.clone(), &[], 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 (graphics, graphics_info) = instance.init_graphics(system_id, cfg)?;
Ok((
instance,
OxrSystemId(system_id),
graphics,
OxrEnabledExtensions(exts),
graphics_info,
))
}
}
pub fn handle_events(
event: OxrEventIn,
mut status: ResMut<XrState>,
mut changed_event: MessageWriter<XrStateChanged>,
) {
use openxr::Event::*;
match *event {
SessionStateChanged(state) => {
use openxr::SessionState;
let state = state.state();
info!("entered XR state {:?}", state);
let new_status = match state {
SessionState::IDLE => XrState::Idle,
SessionState::READY => XrState::Ready,
SessionState::SYNCHRONIZED | SessionState::VISIBLE | SessionState::FOCUSED => {
XrState::Running
}
SessionState::STOPPING => XrState::Stopping,
SessionState::EXITING => XrState::Exiting {
should_restart: false,
},
SessionState::LOSS_PENDING => XrState::Exiting {
should_restart: true,
},
_ => unreachable!(),
};
changed_event.write(XrStateChanged(new_status));
*status = new_status;
}
InstanceLossPending(_) => {}
EventsLost(e) => warn!("lost {} XR events", e.lost_event_count()),
_ => {}
}
}
fn init_xr_session(
device: &wgpu::Device,
instance: &OxrInstance,
system_id: openxr::SystemId,
chain: &mut OxrSessionCreateNextChain,
OxrSessionConfig {
blend_mode_preference,
formats,
resolutions,
}: OxrSessionConfig,
graphics_info: SessionGraphicsCreateInfo,
) -> OxrResult<(
OxrSession,
OxrFrameWaiter,
OxrFrameStream,
OxrSwapchain,
OxrSwapchainImages,
OxrCurrentSessionConfig,
OxrEnvironmentBlendModes,
)> {
let (session, frame_waiter, frame_stream) =
unsafe { instance.create_session(system_id, graphics_info, chain)? };
// 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(OxrError::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) = &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 {
view_configuration_views.first().map(|config| {
(
UVec2::new(
config.recommended_image_rect_width,
config.recommended_image_rect_height,
),
*config,
)
})
}
.ok_or(OxrError::NoAvailableViewConfiguration)?;
let available_formats = session.enumerate_swapchain_formats()?;
let format = if let Some(formats) = &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(OxrError::NoAvailableFormat)?;
let 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(device, format, resolution)?;
let available_blend_modes =
instance.enumerate_environment_blend_modes(system_id, view_configuration_type)?;
// blend mode selection
let blend_modes = OxrEnvironmentBlendModes::new(available_blend_modes, &blend_mode_preference)
.ok_or(OxrError::NoAvailableBlendMode)?;
let graphics_info = OxrCurrentSessionConfig { resolution, format };
Ok((
session,
frame_waiter,
frame_stream,
swapchain,
images,
graphics_info,
blend_modes,
))
}
pub fn create_xr_session(world: &mut World) {
let mut chain = world
.remove_non_send_resource::<OxrSessionCreateNextChain>()
.unwrap();
let device = world.resource::<RenderDevice>();
let instance = world.resource::<OxrInstance>();
let session_config = world.resource::<OxrSessionConfig>();
let session_create_info = world.non_send_resource::<SessionGraphicsCreateInfo>();
let system_id = world.resource::<OxrSystemId>();
match init_xr_session(
device.wgpu_device(),
instance,
**system_id,
&mut chain,
session_config.clone(),
session_create_info.clone(),
) {
Ok((
session,
frame_waiter,
frame_stream,
swapchain,
images,
graphics_info,
blend_modes,
)) => {
world.insert_resource(session.clone());
world.insert_resource(frame_waiter);
world.insert_resource(images);
world.insert_resource(graphics_info);
world.insert_resource(OxrRenderResources {
session,
frame_stream,
swapchain,
images,
graphics_info,
session_destroy_flag: world
.get_resource::<XrDestroySessionRender>()
.expect("added by xr session plugin")
.clone(),
});
world.insert_resource(blend_modes);
}
Err(e) => error!("Failed to initialize XrSession: {e}"),
}
world.insert_non_send_resource(chain);
world.run_schedule(XrSessionCreated);
world.write_message(XrSessionCreatedMessage);
}
pub fn destroy_xr_session(world: &mut World) {
world.run_schedule(XrPreDestroySession);
world.remove_resource::<OxrSession>();
world.remove_resource::<OxrFrameWaiter>();
world.remove_resource::<OxrFrameStream>();
world.remove_resource::<OxrSwapchain>();
world.remove_resource::<OxrSwapchainImages>();
world.remove_resource::<OxrCurrentSessionConfig>();
world.insert_resource(XrState::Available);
}
pub fn begin_xr_session(world: &mut World) {
let _span = debug_span!("xr_begin_session").entered();
world
.get_resource::<OxrSession>()
.unwrap()
.begin(openxr::ViewConfigurationType::PRIMARY_STEREO)
.expect("Failed to begin session");
drop(_span);
world.get_resource_mut::<OxrSessionStarted>().unwrap().0 = true;
world.run_schedule(XrPostSessionBegin);
}
pub fn end_xr_session(world: &mut World) {
world.run_schedule(XrPreSessionEnd);
let _span = debug_span!("xr_end_session").entered();
world
.get_resource::<OxrSession>()
.unwrap()
.end()
.expect("Failed to end session");
world.get_resource_mut::<OxrSessionStarted>().unwrap().0 = false;
}
pub fn request_exit_xr_session(session: Res<OxrSession>) {
session.request_exit().expect("Failed to request exit");
}
/// This is used solely to transport resources from the main world to the render world.
#[derive(Resource)]
struct OxrRenderResources {
session: OxrSession,
frame_stream: OxrFrameStream,
swapchain: OxrSwapchain,
images: OxrSwapchainImages,
graphics_info: OxrCurrentSessionConfig,
session_destroy_flag: XrDestroySessionRender,
}
/// 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(OxrRenderResources {
session,
frame_stream,
swapchain,
images,
graphics_info,
session_destroy_flag,
}) = 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(session_destroy_flag);
}