From 91eb263b4ff709dd7e2ff454f28c53c63a680a15 Mon Sep 17 00:00:00 2001 From: awtterpip Date: Sun, 10 Mar 2024 19:59:19 -0500 Subject: [PATCH] rendering code and api crate --- Cargo.toml | 8 + crates/bevy_openxr/Cargo.toml | 25 + crates/bevy_openxr/examples/3d_scene.rs | 55 +++ crates/bevy_openxr/src/camera.rs | 0 crates/bevy_openxr/src/error.rs | 66 +++ .../bevy_openxr/src}/extensions.rs | 39 +- .../bevy_openxr/src}/graphics.rs | 81 ++-- .../bevy_openxr/src}/graphics/vulkan.rs | 7 +- crates/bevy_openxr/src/init.rs | 455 ++++++++++++++++++ crates/bevy_openxr/src/layer_builder.rs | 169 +++++++ crates/bevy_openxr/src/lib.rs | 36 ++ crates/bevy_openxr/src/render.rs | 332 +++++++++++++ .../bevy_openxr/src}/resources.rs | 63 ++- crates/bevy_openxr/src/types.rs | 77 +++ crates/bevy_xr/Cargo.toml | 9 + crates/bevy_xr/src/camera.rs | 102 ++++ crates/bevy_xr/src/lib.rs | 2 + crates/bevy_xr/src/session.rs | 89 ++++ examples/3d_scene.rs | 98 ++-- src/action_paths.rs | 130 ----- src/action_paths/oculus_touch.rs | 238 --------- src/actions.rs | 23 - src/lib.rs | 10 +- src/openxr.rs | 334 ------------- src/openxr/render.rs | 176 ------- src/openxr/types.rs | 348 -------------- src/render.rs | 19 - src/types.rs | 24 - src/webxr.rs | 178 ------- src/webxr/render.rs | 154 ------ src/webxr/resources.rs | 25 - 31 files changed, 1597 insertions(+), 1775 deletions(-) create mode 100644 crates/bevy_openxr/Cargo.toml create mode 100644 crates/bevy_openxr/examples/3d_scene.rs create mode 100644 crates/bevy_openxr/src/camera.rs create mode 100644 crates/bevy_openxr/src/error.rs rename {src/openxr => crates/bevy_openxr/src}/extensions.rs (86%) rename {src/openxr => crates/bevy_openxr/src}/graphics.rs (72%) rename {src/openxr => crates/bevy_openxr/src}/graphics/vulkan.rs (99%) create mode 100644 crates/bevy_openxr/src/init.rs create mode 100644 crates/bevy_openxr/src/layer_builder.rs create mode 100644 crates/bevy_openxr/src/lib.rs create mode 100644 crates/bevy_openxr/src/render.rs rename {src/openxr => crates/bevy_openxr/src}/resources.rs (88%) create mode 100644 crates/bevy_openxr/src/types.rs create mode 100644 crates/bevy_xr/Cargo.toml create mode 100644 crates/bevy_xr/src/camera.rs create mode 100644 crates/bevy_xr/src/lib.rs create mode 100644 crates/bevy_xr/src/session.rs delete mode 100644 src/action_paths.rs delete mode 100644 src/action_paths/oculus_touch.rs delete mode 100644 src/actions.rs delete mode 100644 src/openxr.rs delete mode 100644 src/openxr/render.rs delete mode 100644 src/openxr/types.rs delete mode 100644 src/render.rs delete mode 100644 src/types.rs delete mode 100644 src/webxr.rs delete mode 100644 src/webxr/render.rs delete mode 100644 src/webxr/resources.rs diff --git a/Cargo.toml b/Cargo.toml index af2baa7..157b840 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,8 @@ linked = ["openxr/linked"] vulkan = ["dep:ash"] [dependencies] +bevy_openxr.path = "./crates/bevy_openxr" +bevy_xr.path = "./crates/bevy_xr" anyhow = "1.0.79" async-std = "1.12.0" bevy = "0.13.0" @@ -88,3 +90,9 @@ web-sys = { version = "0.3.67", features = [ 'XrSystem', ] } wasm-bindgen-futures = "0.4" + +[workspace] +members = ["crates/*"] + +[workspace.dependencies] +bevy = "0.13.0" diff --git a/crates/bevy_openxr/Cargo.toml b/crates/bevy_openxr/Cargo.toml new file mode 100644 index 0000000..c9e8514 --- /dev/null +++ b/crates/bevy_openxr/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "bevy_openxr" +version = "0.1.0" +edition = "2021" + +[features] +default = ["vulkan"] +vulkan = ["dep:ash"] + +[dependencies] +thiserror = "1.0.57" +wgpu = "0.19.3" +wgpu-hal = "0.19.3" + +bevy_xr.path = "../bevy_xr" +bevy.workspace = true + + +ash = { version = "0.37.3", optional = true } + +[target.'cfg(target_family = "unix")'.dependencies] +openxr = { version = "0.18.0", features = ["mint"] } + +[target.'cfg(target_family = "windows")'.dependencies] +openxr = { version = "0.18.0", features = ["mint", "static"] } diff --git a/crates/bevy_openxr/examples/3d_scene.rs b/crates/bevy_openxr/examples/3d_scene.rs new file mode 100644 index 0000000..391a9ab --- /dev/null +++ b/crates/bevy_openxr/examples/3d_scene.rs @@ -0,0 +1,55 @@ +//! A simple 3D scene with light shining over a cube sitting on a plane. + +use bevy::{ + prelude::*, + render::camera::{ManualTextureViewHandle, RenderTarget}, +}; +use bevy_openxr::{add_xr_plugins, render::XR_TEXTURE_INDEX}; +use bevy_xr::camera::XrCameraBundle; + +fn main() { + App::new() + .add_plugins(add_xr_plugins(DefaultPlugins)) + .add_systems(Startup, setup) + .run(); +} + +/// set up a simple 3D scene +fn setup( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, +) { + // circular base + commands.spawn(PbrBundle { + mesh: meshes.add(Circle::new(4.0)), + material: materials.add(Color::WHITE), + transform: Transform::from_rotation(Quat::from_rotation_x(-std::f32::consts::FRAC_PI_2)), + ..default() + }); + // cube + commands.spawn(PbrBundle { + mesh: meshes.add(Cuboid::new(1.0, 1.0, 1.0)), + material: materials.add(Color::rgb_u8(124, 144, 255)), + transform: Transform::from_xyz(0.0, 0.5, 0.0), + ..default() + }); + // light + commands.spawn(PointLightBundle { + point_light: PointLight { + shadows_enabled: true, + ..default() + }, + transform: Transform::from_xyz(4.0, 8.0, 4.0), + ..default() + }); + // // camera + // commands.spawn(XrCameraBundle { + // transform: Transform::from_xyz(-2.5, 4.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y), + // camera: Camera { + // target: RenderTarget::TextureView(ManualTextureViewHandle(XR_TEXTURE_INDEX + 1)), + // ..default() + // }, + // ..default() + // }); +} diff --git a/crates/bevy_openxr/src/camera.rs b/crates/bevy_openxr/src/camera.rs new file mode 100644 index 0000000..e69de29 diff --git a/crates/bevy_openxr/src/error.rs b/crates/bevy_openxr/src/error.rs new file mode 100644 index 0000000..cf18d49 --- /dev/null +++ b/crates/bevy_openxr/src/error.rs @@ -0,0 +1,66 @@ +use crate::graphics::GraphicsBackend; +use std::borrow::Cow; +use std::fmt; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum XrError { + #[error("OpenXR error: {0}")] + OpenXrError(#[from] openxr::sys::Result), + #[error("OpenXR loading error: {0}")] + OpenXrLoadingError(#[from] openxr::LoadError), + #[error("WGPU instance error: {0}")] + WgpuInstanceError(#[from] wgpu_hal::InstanceError), + #[error("WGPU device error: {0}")] + WgpuDeviceError(#[from] wgpu_hal::DeviceError), + #[error("WGPU request device error: {0}")] + WgpuRequestDeviceError(#[from] wgpu::RequestDeviceError), + #[error("Unsupported texture format: {0:?}")] + UnsupportedTextureFormat(wgpu::TextureFormat), + #[error("Vulkan error: {0}")] + VulkanError(#[from] ash::vk::Result), + #[error("Vulkan loading error: {0}")] + VulkanLoadingError(#[from] ash::LoadingError), + #[error("Graphics backend '{0:?}' is not available")] + UnavailableBackend(GraphicsBackend), + #[error("No compatible backend available")] + NoAvailableBackend, + #[error("No compatible view configuration available")] + NoAvailableViewConfiguration, + #[error("No compatible blend mode available")] + NoAvailableBlendMode, + #[error("No compatible format available")] + NoAvailableFormat, + #[error("OpenXR runtime does not support these extensions: {0}")] + UnavailableExtensions(UnavailableExts), + #[error("Could not meet graphics requirements for platform. See console for details")] + FailedGraphicsRequirements, + #[error( + "Tried to use item {item} with backend {backend}. Expected backend {expected_backend}" + )] + GraphicsBackendMismatch { + item: &'static str, + backend: &'static str, + expected_backend: &'static str, + }, + #[error("Failed to create CString: {0}")] + NulError(#[from] std::ffi::NulError), +} + +impl From>> for XrError { + fn from(value: Vec>) -> Self { + Self::UnavailableExtensions(UnavailableExts(value)) + } +} + +#[derive(Debug)] +pub struct UnavailableExts(Vec>); + +impl fmt::Display for UnavailableExts { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for s in &self.0 { + write!(f, "\t{s}")?; + } + Ok(()) + } +} diff --git a/src/openxr/extensions.rs b/crates/bevy_openxr/src/extensions.rs similarity index 86% rename from src/openxr/extensions.rs rename to crates/bevy_openxr/src/extensions.rs index 0423ed7..7c0ab27 100644 --- a/src/openxr/extensions.rs +++ b/crates/bevy_openxr/src/extensions.rs @@ -62,7 +62,7 @@ macro_rules! unavailable_exts { ) => { impl $exts { /// Returns any extensions needed by `required_exts` that aren't available in `self` - pub(crate) fn unavailable_exts(&self, required_exts: &Self) -> Vec> { + pub fn unavailable_exts(&self, required_exts: &Self) -> Vec> { let mut exts = vec![]; $( $( @@ -167,6 +167,7 @@ macro_rules! impl_ext { $macro! { XrExtensions; almalence_digital_lens_control, + bd_controller_interaction, epic_view_configuration_fov, ext_performance_settings, ext_thermal_query, @@ -183,13 +184,18 @@ macro_rules! impl_ext { ext_hp_mixed_reality_controller, ext_palm_pose, ext_uuid, - extx_overlay, + ext_hand_interaction, + ext_active_action_set_priority, + ext_local_floor, + ext_hand_tracking_data_source, + ext_plane_detection, fb_composition_layer_image_layout, fb_composition_layer_alpha_blend, #[cfg(target_os = "android")] fb_android_surface_swapchain_create, fb_swapchain_update_state, fb_composition_layer_secure_content, + fb_body_tracking, fb_display_refresh_rate, fb_color_space, fb_hand_tracking_mesh, @@ -209,17 +215,29 @@ macro_rules! impl_ext { fb_swapchain_update_state_android_surface, fb_swapchain_update_state_opengl_es, fb_swapchain_update_state_vulkan, + fb_touch_controller_pro, + fb_spatial_entity_sharing, fb_space_warp, + fb_haptic_amplitude_envelope, fb_scene, + fb_scene_capture, fb_spatial_entity_container, + fb_face_tracking, + fb_eye_tracking_social, fb_passthrough_keyboard_hands, fb_composition_layer_settings, + fb_touch_controller_proximity, + fb_haptic_pcm, + fb_composition_layer_depth_test, + fb_spatial_entity_storage_batch, + fb_spatial_entity_user, htc_vive_cosmos_controller_interaction, htc_facial_tracking, htc_vive_focus3_controller_interaction, htc_hand_interaction, htc_vive_wrist_tracker_interaction, - htcx_vive_tracker_interaction, + htc_passthrough, + htc_foveation, huawei_controller_interaction, #[cfg(target_os = "android")] khr_android_thread_settings, @@ -251,12 +269,21 @@ macro_rules! impl_ext { khr_composition_layer_equirect2, khr_binding_modification, khr_swapchain_usage_input_attachment_bit, + meta_foveation_eye_tracked, + meta_local_dimming, + meta_passthrough_preferences, + meta_virtual_keyboard, meta_vulkan_swapchain_create_info, meta_performance_metrics, + meta_headset_id, + meta_passthrough_color_lut, ml_ml2_controller_interaction, + ml_frame_end_info, + ml_global_dimmer, + ml_compat, + ml_user_calibration, mnd_headless, mnd_swapchain_usage_input_attachment_bit, - mndx_egl_enable, msft_unbounded_reference_space, msft_spatial_anchor, msft_spatial_graph_bridge, @@ -274,6 +301,9 @@ macro_rules! impl_ext { #[cfg(target_os = "android")] oculus_android_session_state_enable, oculus_audio_device_guid, + oculus_external_camera, + oppo_controller_interaction, + qcom_tracking_optimization_settings, ultraleap_hand_tracking_forearm, valve_analog_threshold, varjo_quad_views, @@ -282,6 +312,7 @@ macro_rules! impl_ext { varjo_environment_depth_estimation, varjo_marker_tracking, varjo_view_offset, + yvr_controller_interaction, } )* diff --git a/src/openxr/graphics.rs b/crates/bevy_openxr/src/graphics.rs similarity index 72% rename from src/openxr/graphics.rs rename to crates/bevy_openxr/src/graphics.rs index a1900f1..be0fa52 100644 --- a/src/openxr/graphics.rs +++ b/crates/bevy_openxr/src/graphics.rs @@ -1,11 +1,10 @@ +#[cfg(feature = "vulkan")] pub mod vulkan; use std::any::TypeId; -use bevy::math::UVec2; - -use crate::openxr::types::{AppInfo, Result, XrError}; -use crate::types::BlendMode; +use crate::extensions::XrExtensions; +use crate::types::*; pub unsafe trait GraphicsExt: openxr::Graphics { /// Wrap the graphics specific type into the [GraphicsWrap] enum @@ -30,8 +29,42 @@ pub unsafe trait GraphicsExt: openxr::Graphics { fn required_exts() -> XrExtensions; } +pub trait GraphicsType { + type Inner; +} + +impl GraphicsType for () { + type Inner = (); +} + +pub type GraphicsBackend = GraphicsWrap<()>; + +impl GraphicsBackend { + const ALL: &'static [Self] = &[Self::Vulkan(())]; + + pub fn available_backends(exts: &XrExtensions) -> Vec { + Self::ALL + .iter() + .copied() + .filter(|backend| backend.is_available(exts)) + .collect() + } + + pub fn is_available(&self, exts: &XrExtensions) -> bool { + self.required_exts().is_available(exts) + } + + pub fn required_exts(&self) -> XrExtensions { + graphics_match!( + self; + _ => Api::required_exts() + ) + } +} + #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum GraphicsWrap { + #[cfg(feature = "vulkan")] Vulkan(T::Inner), } @@ -62,21 +95,14 @@ impl GraphicsWrap { } } -pub trait GraphicsType { - type Inner; -} - -impl GraphicsType for () { - type Inner = (); -} - macro_rules! graphics_match { ( $field:expr; $var:pat => $expr:expr $(=> $($return:tt)*)? ) => { match $field { - $crate::openxr::graphics::GraphicsWrap::Vulkan($var) => { + #[cfg(feature = "vulkan")] + $crate::graphics::GraphicsWrap::Vulkan($var) => { #[allow(unused)] type Api = openxr::Vulkan; graphics_match!(@arm_impl Vulkan; $expr $(=> $($return)*)?) @@ -101,32 +127,5 @@ macro_rules! graphics_match { }; } +use bevy::math::UVec2; pub(crate) use graphics_match; - -use super::{WgpuGraphics, XrExtensions}; - -impl From for BlendMode { - fn from(value: openxr::EnvironmentBlendMode) -> Self { - use openxr::EnvironmentBlendMode; - if value == EnvironmentBlendMode::OPAQUE { - BlendMode::Opaque - } else if value == EnvironmentBlendMode::ADDITIVE { - BlendMode::Additive - } else if value == EnvironmentBlendMode::ALPHA_BLEND { - BlendMode::AlphaBlend - } else { - unreachable!() - } - } -} - -impl From for openxr::EnvironmentBlendMode { - fn from(value: BlendMode) -> Self { - use openxr::EnvironmentBlendMode; - match value { - BlendMode::Opaque => EnvironmentBlendMode::OPAQUE, - BlendMode::Additive => EnvironmentBlendMode::ADDITIVE, - BlendMode::AlphaBlend => EnvironmentBlendMode::ALPHA_BLEND, - } - } -} diff --git a/src/openxr/graphics/vulkan.rs b/crates/bevy_openxr/src/graphics/vulkan.rs similarity index 99% rename from src/openxr/graphics/vulkan.rs rename to crates/bevy_openxr/src/graphics/vulkan.rs index 60a8610..469815e 100644 --- a/src/openxr/graphics/vulkan.rs +++ b/crates/bevy_openxr/src/graphics/vulkan.rs @@ -7,10 +7,11 @@ use openxr::Version; use wgpu_hal::api::Vulkan; use wgpu_hal::Api; -use crate::openxr::types::Result; -use crate::openxr::{extensions::XrExtensions, WgpuGraphics}; +use crate::error::XrError; +use crate::extensions::XrExtensions; +use crate::types::*; -use super::{AppInfo, GraphicsExt, XrError}; +use super::GraphicsExt; #[cfg(not(target_os = "android"))] const VK_TARGET_VERSION: Version = Version::new(1, 2, 0); diff --git a/crates/bevy_openxr/src/init.rs b/crates/bevy_openxr/src/init.rs new file mode 100644 index 0000000..3076281 --- /dev/null +++ b/crates/bevy_openxr/src/init.rs @@ -0,0 +1,455 @@ +use bevy::app::{App, First, Plugin, PreUpdate}; +use bevy::ecs::event::EventWriter; +use bevy::ecs::schedule::common_conditions::{not, on_event}; +use bevy::ecs::schedule::IntoSystemConfigs; +use bevy::ecs::system::{Commands, Res, ResMut, Resource}; +use bevy::ecs::world::World; +use bevy::log::{error, info}; +use bevy::math::{uvec2, UVec2}; +use bevy::render::camera::ManualTextureViews; +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_xr::session::{ + BeginXrSession, CreateXrSession, XrInstanceCreated, XrInstanceDestroyed, XrSessionState, +}; + +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, +} + +pub fn instance_created(status: Option>) -> bool { + status.is_some_and(|status| status.instance_created) +} + +pub fn session_created(status: Option>) -> bool { + status.is_some_and(|status| status.session_created) +} + +pub fn session_running(status: Option>) -> bool { + status.is_some_and(|status| status.session_running) +} + +pub fn session_ready(status: Option>) -> bool { + status.is_some_and(|status| status.session_ready) +} + +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::UNINITIALIZED); + } + } +} + +fn xr_entry() -> Result { + #[cfg(windows)] + let entry = openxr::Entry::linked(); + #[cfg(not(windows))] + let entry = unsafe { openxr::Entry::load()? }; + Ok(XrEntry(entry)) +} + +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, 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.world.send_event(XrInstanceCreated); + 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, + }, + ExtractResourcePlugin::::default(), + ExtractResourcePlugin::::default(), + )) + .insert_resource(instance.clone()) + .insert_resource(SystemId(system_id)) + .insert_resource(XrStatus { + instance_created: true, + ..Default::default() + }) + .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(instance_created), + create_xr_session + .run_if(not(session_created)) + .run_if(on_event::()), + begin_xr_session + .run_if(session_ready) + .run_if(on_event::()), + ), + ) + .sub_app_mut(RenderApp) + .insert_resource(instance) + .insert_resource(SystemId(system_id)) + .add_systems( + ExtractSchedule, + transfer_xr_resources.run_if(not(session_running)), + ); + + Ok(()) +} + +/// 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) = init_xr_session(world, instance, *system_id, create_info) { + error!("Failed to initialize XrSession: {e}"); + } +} + +fn init_xr_session( + 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.resource_mut::().session_created = true; + 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(XrRenderResources { + session, + frame_stream, + swapchain, + images, + graphics_info, + stage, + }); + + Ok(()) +} + +pub fn begin_xr_session( + session: Res, + mut session_state: EventWriter, + mut status: ResMut, +) { + session + .begin(openxr::ViewConfigurationType::PRIMARY_STEREO) + .expect("Failed to begin session"); + status.session_running = true; + session_state.send(XrSessionState::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, +} + +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); +} + +pub fn poll_events( + instance: Res, + session: Option>, + mut session_state: EventWriter, + mut instance_destroyed: EventWriter, + 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) => { + if let Some(ref session) = session { + info!("entered XR state {:?}", e.state()); + use openxr::SessionState; + + match e.state() { + SessionState::IDLE => { + status.session_ready = false; + session_state.send(XrSessionState::Idle); + } + SessionState::READY => { + status.session_ready = true; + session_state.send(XrSessionState::Ready); + } + SessionState::STOPPING => { + status.session_running = false; + status.session_ready = false; + session.end().expect("Failed to end session"); + session_state.send(XrSessionState::Stopping); + } + // TODO: figure out how to destroy the session + SessionState::EXITING | SessionState::LOSS_PENDING => { + status.session_running = false; + status.session_created = false; + status.session_ready = false; + session.end().expect("Failed to end session"); + session_state.send(XrSessionState::Destroyed); + } + _ => {} + } + } + } + InstanceLossPending(_) => { + *status = XrStatus::UNINITIALIZED; + instance_destroyed.send_default(); + } + _ => {} + } + } +} diff --git a/crates/bevy_openxr/src/layer_builder.rs b/crates/bevy_openxr/src/layer_builder.rs new file mode 100644 index 0000000..08257bf --- /dev/null +++ b/crates/bevy_openxr/src/layer_builder.rs @@ -0,0 +1,169 @@ +use std::mem; + +use openxr::{sys, CompositionLayerFlags, Fovf, Posef, Rect2Di, Space}; + +use crate::graphics::graphics_match; +use crate::resources::XrSwapchain; + +#[derive(Copy, Clone)] +pub struct SwapchainSubImage<'a> { + inner: sys::SwapchainSubImage, + swapchain: Option<&'a XrSwapchain>, +} + +impl<'a> SwapchainSubImage<'a> { + #[inline] + pub fn new() -> Self { + Self { + inner: sys::SwapchainSubImage { + ..unsafe { mem::zeroed() } + }, + swapchain: None, + } + } + #[inline] + pub fn into_raw(self) -> sys::SwapchainSubImage { + self.inner + } + #[inline] + pub fn as_raw(&self) -> &sys::SwapchainSubImage { + &self.inner + } + #[inline] + pub fn swapchain(mut self, value: &'a XrSwapchain) -> Self { + graphics_match!( + &value.0; + swap => self.inner.swapchain = swap.as_raw() + ); + self.swapchain = Some(value); + self + } + #[inline] + pub fn image_rect(mut self, value: Rect2Di) -> Self { + self.inner.image_rect = value; + self + } + #[inline] + pub fn image_array_index(mut self, value: u32) -> Self { + self.inner.image_array_index = value; + self + } +} + +impl<'a> Default for SwapchainSubImage<'a> { + fn default() -> Self { + Self::new() + } +} + +#[derive(Copy, Clone)] +pub struct CompositionLayerProjectionView<'a> { + inner: sys::CompositionLayerProjectionView, + swapchain: Option<&'a XrSwapchain>, +} + +impl<'a> CompositionLayerProjectionView<'a> { + #[inline] + pub fn new() -> Self { + Self { + inner: sys::CompositionLayerProjectionView { + ty: sys::StructureType::COMPOSITION_LAYER_PROJECTION_VIEW, + ..unsafe { mem::zeroed() } + }, + swapchain: None, + } + } + #[inline] + pub fn into_raw(self) -> sys::CompositionLayerProjectionView { + self.inner + } + #[inline] + pub fn as_raw(&self) -> &sys::CompositionLayerProjectionView { + &self.inner + } + #[inline] + pub fn pose(mut self, value: Posef) -> Self { + self.inner.pose = value; + self + } + #[inline] + pub fn fov(mut self, value: Fovf) -> Self { + self.inner.fov = value; + self + } + #[inline] + pub fn sub_image(mut self, value: SwapchainSubImage<'a>) -> Self { + self.inner.sub_image = value.inner; + self.swapchain = value.swapchain; + self + } +} +impl<'a> Default for CompositionLayerProjectionView<'a> { + fn default() -> Self { + Self::new() + } +} +pub unsafe trait CompositionLayer<'a> { + fn swapchain(&self) -> Option<&'a XrSwapchain>; + fn header(&self) -> &'a sys::CompositionLayerBaseHeader; +} +#[derive(Clone)] +pub struct CompositionLayerProjection<'a> { + inner: sys::CompositionLayerProjection, + swapchain: Option<&'a XrSwapchain>, + views: Vec, +} +impl<'a> CompositionLayerProjection<'a> { + #[inline] + pub fn new() -> Self { + Self { + inner: sys::CompositionLayerProjection { + ty: sys::StructureType::COMPOSITION_LAYER_PROJECTION, + ..unsafe { mem::zeroed() } + }, + swapchain: None, + views: Vec::new(), + } + } + #[inline] + pub fn into_raw(self) -> sys::CompositionLayerProjection { + self.inner + } + #[inline] + pub fn as_raw(&self) -> &sys::CompositionLayerProjection { + &self.inner + } + #[inline] + pub fn layer_flags(mut self, value: CompositionLayerFlags) -> Self { + self.inner.layer_flags = value; + self + } + #[inline] + pub fn space(mut self, value: &'a Space) -> Self { + self.inner.space = value.as_raw(); + self + } + #[inline] + pub fn views(mut self, value: &'a [CompositionLayerProjectionView<'a>]) -> Self { + for view in value { + self.views.push(view.inner.clone()); + } + self.inner.views = self.views.as_slice().as_ptr() as *const _ as _; + self.inner.view_count = value.len() as u32; + self + } +} +unsafe impl<'a> CompositionLayer<'a> for CompositionLayerProjection<'a> { + fn swapchain(&self) -> Option<&'a XrSwapchain> { + self.swapchain + } + + fn header(&self) -> &'a sys::CompositionLayerBaseHeader { + unsafe { std::mem::transmute(&self.inner) } + } +} +impl<'a> Default for CompositionLayerProjection<'a> { + fn default() -> Self { + Self::new() + } +} diff --git a/crates/bevy_openxr/src/lib.rs b/crates/bevy_openxr/src/lib.rs new file mode 100644 index 0000000..66e9d40 --- /dev/null +++ b/crates/bevy_openxr/src/lib.rs @@ -0,0 +1,36 @@ +use bevy::{ + app::{PluginGroup, PluginGroupBuilder}, + render::RenderPlugin, + utils::default, +}; +use bevy_xr::camera::XrCameraPlugin; +use init::XrInitPlugin; +use render::XrRenderPlugin; + +pub mod camera; +pub mod error; +pub mod extensions; +pub mod graphics; +pub mod init; +pub mod layer_builder; +pub mod render; +pub mod resources; +pub mod types; + +pub fn add_xr_plugins(plugins: G) -> PluginGroupBuilder { + plugins + .build() + .disable::() + .add_before::(bevy_xr::session::XrSessionPlugin) + .add_before::(XrInitPlugin { + app_info: default(), + exts: default(), + blend_modes: default(), + backends: default(), + formats: Some(vec![wgpu::TextureFormat::Rgba8UnormSrgb]), + resolutions: default(), + synchronous_pipeline_compilation: default(), + }) + .add(XrRenderPlugin) + .add(XrCameraPlugin) +} diff --git a/crates/bevy_openxr/src/render.rs b/crates/bevy_openxr/src/render.rs new file mode 100644 index 0000000..044fd46 --- /dev/null +++ b/crates/bevy_openxr/src/render.rs @@ -0,0 +1,332 @@ +use bevy::{ + prelude::*, + render::{ + camera::{ManualTextureView, ManualTextureViewHandle, ManualTextureViews, RenderTarget}, + renderer::render_system, + Render, RenderApp, RenderSet, + }, +}; +use bevy_xr::camera::{XrCameraBundle, XrProjection, XrView}; +use openxr::CompositionLayerFlags; + +use crate::layer_builder::*; +use crate::resources::*; + +use crate::init::session_running; + +pub struct XrRenderPlugin; + +impl Plugin for XrRenderPlugin { + fn build(&self, app: &mut App) { + app.add_systems( + PreUpdate, + ( + init_views.run_if(resource_added::), + wait_frame.run_if(session_running), + ), + ); + // .add_systems(Startup, init_views); + app.sub_app_mut(RenderApp).add_systems( + Render, + ( + (begin_frame, insert_texture_views) + .chain() + .in_set(RenderSet::PrepareAssets) + .before(render_system), + end_frame.in_set(RenderSet::Cleanup), + ) + .run_if(session_running), + ); + } +} + +pub const XR_TEXTURE_INDEX: u32 = 3383858418; + +/// This is needed to properly initialize the texture views so that bevy will set them to the correct resolution despite them being updated in the render world. +pub fn init_views( + graphics_info: Res, + mut manual_texture_views: ResMut, + swapchain_images: Res, + mut commands: Commands, +) { + let temp_tex = swapchain_images.first().unwrap(); + // this for loop is to easily add support for quad or mono views in the future. + let mut views = vec![]; + for index in 0..2 { + info!("{}", graphics_info.resolution); + let view_handle = + add_texture_view(&mut manual_texture_views, temp_tex, &graphics_info, index); + + let entity = commands + .spawn(XrCameraBundle { + camera: Camera { + target: RenderTarget::TextureView(view_handle), + ..Default::default() + }, + ..Default::default() + }) + .id(); + views.push(entity); + } + commands.insert_resource(XrViews(views)); +} + +pub fn wait_frame(mut frame_waiter: ResMut, mut commands: Commands) { + let state = frame_waiter.wait().expect("Failed to wait frame"); + commands.insert_resource(XrTime(openxr::Time::from_nanos( + state.predicted_display_time.as_nanos() + state.predicted_display_period.as_nanos(), + ))); +} + +pub fn update_views( + mut views: Query<(&mut Transform, &mut XrProjection), With>, + view_entities: Res, + session: Res, + stage: Res, + time: Res, +) { + let (_flags, xr_views) = session + .locate_views( + openxr::ViewConfigurationType::PRIMARY_STEREO, + **time, + &stage, + ) + .expect("Failed to locate views"); + + for (i, view) in xr_views.iter().enumerate() { + if let Some((mut transform, mut projection)) = view_entities + .0 + .get(i) + .and_then(|entity| views.get_mut(*entity).ok()) + { + let projection_matrix = calculate_projection(projection.near, view.fov); + projection.projection_matrix = projection_matrix; + + 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; + } + } +} + +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()); + // let y_fov = (self.fov.angle_up.abs() + self.fov.angle_down.abs()); + // return Mat4::perspective_infinite_reverse_rh(y_fov, x_fov / y_fov, self.near); + + let is_vulkan_api = false; // FIXME wgpu probably abstracts this + let far_z = -1.; // use infinite proj + // let far_z = self.far; + + let tan_angle_left = fov.angle_left.tan(); + let tan_angle_right = fov.angle_right.tan(); + + let tan_angle_down = fov.angle_down.tan(); + let tan_angle_up = fov.angle_up.tan(); + + let tan_angle_width = tan_angle_right - tan_angle_left; + + // Set to tanAngleDown - tanAngleUp for a clip space with positive Y + // down (Vulkan). Set to tanAngleUp - tanAngleDown for a clip space with + // positive Y up (OpenGL / D3D / Metal). + // const float tanAngleHeight = + // graphicsApi == GRAPHICS_VULKAN ? (tanAngleDown - tanAngleUp) : (tanAngleUp - tanAngleDown); + let tan_angle_height = if is_vulkan_api { + tan_angle_down - tan_angle_up + } else { + tan_angle_up - tan_angle_down + }; + + // Set to nearZ for a [-1,1] Z clip space (OpenGL / OpenGL ES). + // Set to zero for a [0,1] Z clip space (Vulkan / D3D / Metal). + // const float offsetZ = + // (graphicsApi == GRAPHICS_OPENGL || graphicsApi == GRAPHICS_OPENGL_ES) ? nearZ : 0; + // FIXME handle enum of graphics apis + let offset_z = 0.; + + let mut cols: [f32; 16] = [0.0; 16]; + + if far_z <= near_z { + // place the far plane at infinity + cols[0] = 2. / tan_angle_width; + cols[4] = 0.; + cols[8] = (tan_angle_right + tan_angle_left) / tan_angle_width; + cols[12] = 0.; + + cols[1] = 0.; + cols[5] = 2. / tan_angle_height; + cols[9] = (tan_angle_up + tan_angle_down) / tan_angle_height; + cols[13] = 0.; + + cols[2] = 0.; + cols[6] = 0.; + cols[10] = -1.; + cols[14] = -(near_z + offset_z); + + cols[3] = 0.; + cols[7] = 0.; + cols[11] = -1.; + cols[15] = 0.; + + // bevy uses the _reverse_ infinite projection + // https://dev.theomader.com/depth-precision/ + let z_reversal = Mat4::from_cols_array_2d(&[ + [1f32, 0., 0., 0.], + [0., 1., 0., 0.], + [0., 0., -1., 0.], + [0., 0., 1., 1.], + ]); + + return z_reversal * Mat4::from_cols_array(&cols); + } else { + // normal projection + cols[0] = 2. / tan_angle_width; + cols[4] = 0.; + cols[8] = (tan_angle_right + tan_angle_left) / tan_angle_width; + cols[12] = 0.; + + cols[1] = 0.; + cols[5] = 2. / tan_angle_height; + cols[9] = (tan_angle_up + tan_angle_down) / tan_angle_height; + cols[13] = 0.; + + cols[2] = 0.; + cols[6] = 0.; + cols[10] = -(far_z + offset_z) / (far_z - near_z); + cols[14] = -(far_z * (near_z + offset_z)) / (far_z - near_z); + + cols[3] = 0.; + cols[7] = 0.; + cols[11] = -1.; + cols[15] = 0.; + } + + Mat4::from_cols_array(&cols) +} + +pub fn begin_frame(mut frame_stream: ResMut) { + frame_stream.begin().expect("Failed to begin frame"); +} + +pub fn insert_texture_views( + swapchain_images: Res, + mut swapchain: ResMut, + mut manual_texture_views: ResMut, + graphics_info: Res, +) { + let index = swapchain.acquire_image().expect("Failed to acquire image"); + swapchain + .wait_image(openxr::Duration::INFINITE) + .expect("Failed to wait image"); + let image = &swapchain_images[index as usize]; + + for i in 0..2 { + add_texture_view(&mut manual_texture_views, image, &graphics_info, i); + } + // let left = image.create_view(&wgpu::TextureViewDescriptor { + // dimension: Some(wgpu::TextureViewDimension::D2), + // array_layer_count: Some(1), + // ..Default::default() + // }); + // let right = image.create_view(&wgpu::TextureViewDescriptor { + // dimension: Some(wgpu::TextureViewDimension::D2), + // array_layer_count: Some(1), + // base_array_layer: 1, + // ..Default::default() + // }); + // let resolution = graphics_info.resolution; + // let format = graphics_info.format; + // let left = ManualTextureView { + // texture_view: left.into(), + // size: resolution, + // format: format, + // }; + // let right = ManualTextureView { + // texture_view: right.into(), + // size: resolution, + // format: format, + // }; + // manual_texture_views.insert(LEFT_XR_TEXTURE_HANDLE, left); + // manual_texture_views.insert(RIGHT_XR_TEXTURE_HANDLE, right); +} + +pub fn add_texture_view( + manual_texture_views: &mut ManualTextureViews, + texture: &wgpu::Texture, + info: &XrGraphicsInfo, + index: u32, +) -> ManualTextureViewHandle { + let view = texture.create_view(&wgpu::TextureViewDescriptor { + dimension: Some(wgpu::TextureViewDimension::D2), + array_layer_count: Some(1), + base_array_layer: index, + ..default() + }); + let view = ManualTextureView { + texture_view: view.into(), + size: info.resolution, + format: info.format, + }; + let handle = ManualTextureViewHandle(XR_TEXTURE_INDEX + index); + manual_texture_views.insert(handle, view); + handle +} + +pub fn end_frame( + mut frame_stream: ResMut, + session: Res, + mut swapchain: ResMut, + stage: Res, + display_time: Res, + graphics_info: Res, +) { + swapchain.release_image().unwrap(); + let (_flags, views) = session + .locate_views( + openxr::ViewConfigurationType::PRIMARY_STEREO, + **display_time, + &stage, + ) + .expect("Failed to locate views"); + + let rect = openxr::Rect2Di { + offset: openxr::Offset2Di { x: 0, y: 0 }, + extent: openxr::Extent2Di { + width: graphics_info.resolution.x as _, + height: graphics_info.resolution.y as _, + }, + }; + frame_stream + .end( + **display_time, + graphics_info.blend_mode, + &[&CompositionLayerProjection::new() + .layer_flags(CompositionLayerFlags::BLEND_TEXTURE_SOURCE_ALPHA) + .space(&stage) + .views(&[ + CompositionLayerProjectionView::new() + .pose(views[0].pose) + .fov(views[0].fov) + .sub_image( + SwapchainSubImage::new() + .swapchain(&swapchain) + .image_array_index(0) + .image_rect(rect), + ), + CompositionLayerProjectionView::new() + .pose(views[1].pose) + .fov(views[1].fov) + .sub_image( + SwapchainSubImage::new() + .swapchain(&swapchain) + .image_array_index(1) + .image_rect(rect), + ), + ])], + ) + .expect("Failed to end stream"); +} diff --git a/src/openxr/resources.rs b/crates/bevy_openxr/src/resources.rs similarity index 88% rename from src/openxr/resources.rs rename to crates/bevy_openxr/src/resources.rs index b6857bc..6fec233 100644 --- a/src/openxr/resources.rs +++ b/crates/bevy_openxr/src/resources.rs @@ -1,13 +1,13 @@ use std::sync::Arc; +use crate::error::XrError; +use crate::graphics::*; +use crate::layer_builder::CompositionLayer; +use crate::types::*; use bevy::prelude::*; - use bevy::render::extract_resource::ExtractResource; use openxr::AnyGraphics; -use super::graphics::{graphics_match, GraphicsExt, GraphicsType, GraphicsWrap}; -use super::types::*; - #[derive(Deref, Clone)] pub struct XrEntry(pub openxr::Entry); @@ -51,16 +51,6 @@ impl XrEntry { } } -#[derive(Debug, Copy, Clone, Deref, Default, Eq, PartialEq, Ord, PartialOrd, Hash, Resource)] -pub struct SystemId(pub openxr::SystemId); - -#[derive(Debug, Copy, Clone, Eq, PartialEq, Resource)] -pub struct XrGraphicsInfo { - pub blend_mode: EnvironmentBlendMode, - pub swapchain_resolution: UVec2, - pub swapchain_format: wgpu::TextureFormat, -} - #[derive(Resource, Deref, Clone)] pub struct XrInstance( #[deref] pub openxr::Instance, @@ -229,7 +219,7 @@ impl XrSwapchain { device: &wgpu::Device, format: wgpu::TextureFormat, resolution: UVec2, - ) -> Result { + ) -> Result { graphics_match!( &mut self.0; swap => { @@ -239,7 +229,7 @@ impl XrSwapchain { images.push(Api::to_wgpu_img(image, device, format, resolution)?); } } - Ok(SwapchainImages(images.into())) + Ok(XrSwapchainImages(images.into())) } ) } @@ -249,14 +239,43 @@ impl XrSwapchain { pub struct XrStage(pub Arc); #[derive(Debug, Deref, Resource, Clone)] -pub struct SwapchainImages(pub Arc>); +pub struct XrSwapchainImages(pub Arc>); #[derive(Copy, Clone, Eq, PartialEq, Deref, DerefMut, Resource, ExtractResource)] pub struct XrTime(pub openxr::Time); -#[derive(Clone, Copy, Eq, PartialEq, Default, Resource, ExtractResource)] -pub enum XrStatus { - Enabled, - #[default] - Disabled, +#[derive(Copy, Clone, Eq, PartialEq, Resource)] +pub struct XrSwapchainInfo { + pub format: wgpu::TextureFormat, + pub resolution: UVec2, } + +#[derive(Debug, Copy, Clone, Deref, Default, Eq, PartialEq, Ord, PartialOrd, Hash, Resource)] +pub struct SystemId(pub openxr::SystemId); + +#[derive(Clone, Copy, Eq, PartialEq, Default, Resource, ExtractResource)] +pub struct XrStatus { + pub instance_created: bool, + pub session_created: bool, + pub session_ready: bool, + pub session_running: bool, +} + +impl XrStatus { + pub const UNINITIALIZED: Self = Self { + instance_created: false, + session_created: false, + session_ready: false, + session_running: false, + }; +} + +#[derive(Clone, Copy, Resource)] +pub struct XrGraphicsInfo { + pub blend_mode: EnvironmentBlendMode, + pub resolution: UVec2, + pub format: wgpu::TextureFormat, +} + +#[derive(Clone, Resource)] +pub struct XrViews(pub Vec); diff --git a/crates/bevy_openxr/src/types.rs b/crates/bevy_openxr/src/types.rs new file mode 100644 index 0000000..d10ef02 --- /dev/null +++ b/crates/bevy_openxr/src/types.rs @@ -0,0 +1,77 @@ +use std::borrow::Cow; + +use crate::error::XrError; +pub use crate::extensions::XrExtensions; +use crate::graphics::GraphicsExt; + +pub use openxr::{EnvironmentBlendMode, SwapchainCreateFlags, SwapchainUsageFlags}; + +pub type Result = std::result::Result; + +pub struct WgpuGraphics( + pub wgpu::Device, + pub wgpu::Queue, + pub wgpu::AdapterInfo, + pub wgpu::Adapter, + pub wgpu::Instance, +); + +#[derive(Clone, Copy, Debug, Default, PartialEq)] +pub struct Version(pub u8, pub u8, pub u16); + +impl Version { + pub const BEVY: Self = Self(0, 12, 1); + + pub const fn to_u32(self) -> u32 { + let major = (self.0 as u32) << 24; + let minor = (self.1 as u32) << 16; + self.2 as u32 | major | minor + } +} + +#[derive(Clone, Debug, PartialEq)] +pub struct AppInfo { + pub name: Cow<'static, str>, + pub version: Version, +} + +impl Default for AppInfo { + fn default() -> Self { + Self { + name: "Bevy".into(), + version: Version::BEVY, + } + } +} + +#[derive(Debug, Copy, Clone)] +pub struct SwapchainCreateInfo { + pub create_flags: SwapchainCreateFlags, + pub usage_flags: SwapchainUsageFlags, + pub format: wgpu::TextureFormat, + pub sample_count: u32, + pub width: u32, + pub height: u32, + pub face_count: u32, + pub array_size: u32, + pub mip_count: u32, +} + +impl TryFrom for openxr::SwapchainCreateInfo { + type Error = XrError; + + fn try_from(value: SwapchainCreateInfo) -> Result { + Ok(openxr::SwapchainCreateInfo { + create_flags: value.create_flags, + usage_flags: value.usage_flags, + format: G::from_wgpu_format(value.format) + .ok_or(XrError::UnsupportedTextureFormat(value.format))?, + sample_count: value.sample_count, + width: value.width, + height: value.height, + face_count: value.face_count, + array_size: value.array_size, + mip_count: value.mip_count, + }) + } +} diff --git a/crates/bevy_xr/Cargo.toml b/crates/bevy_xr/Cargo.toml new file mode 100644 index 0000000..9b586c6 --- /dev/null +++ b/crates/bevy_xr/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "bevy_xr" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +bevy.workspace = true diff --git a/crates/bevy_xr/src/camera.rs b/crates/bevy_xr/src/camera.rs new file mode 100644 index 0000000..5ffde09 --- /dev/null +++ b/crates/bevy_xr/src/camera.rs @@ -0,0 +1,102 @@ +use bevy::app::{App, Plugin}; +use bevy::core_pipeline::core_3d::graph::Core3d; +use bevy::core_pipeline::core_3d::Camera3d; +use bevy::core_pipeline::tonemapping::{DebandDither, Tonemapping}; +use bevy::ecs::bundle::Bundle; +use bevy::ecs::component::Component; +use bevy::ecs::reflect::ReflectComponent; +use bevy::math::{Mat4, Vec3A}; +use bevy::reflect::Reflect; +use bevy::render::camera::{ + Camera, CameraMainTextureUsages, CameraProjection, CameraProjectionPlugin, CameraRenderGraph, + Exposure, +}; +use bevy::render::primitives::Frustum; +use bevy::render::view::{ColorGrading, VisibleEntities}; +use bevy::transform::components::{GlobalTransform, Transform}; + +pub struct XrCameraPlugin; + +impl Plugin for XrCameraPlugin { + fn build(&self, app: &mut App) { + app.add_plugins(CameraProjectionPlugin::::default()); + } +} + +#[derive(Clone, Copy, Component, Reflect, Debug)] +#[reflect(Component)] +pub struct XrProjection { + pub projection_matrix: Mat4, + pub near: f32, + pub far: f32, +} + +impl Default for XrProjection { + fn default() -> Self { + Self { + near: 0.1, + far: 1000., + projection_matrix: Mat4::IDENTITY, + } + } +} + +/// Marker component for an XR view. It is the backends responsibility to update this. +#[derive(Clone, Copy, Component, Debug, Default)] +pub struct XrView; + +impl CameraProjection for XrProjection { + fn get_projection_matrix(&self) -> Mat4 { + self.projection_matrix + } + + fn update(&mut self, _width: f32, _height: f32) {} + + fn far(&self) -> f32 { + self.far + } + + // TODO calculate this properly + fn get_frustum_corners(&self, _z_near: f32, _z_far: f32) -> [Vec3A; 8] { + Default::default() + } +} + +#[derive(Bundle)] +pub struct XrCameraBundle { + pub camera: Camera, + pub camera_render_graph: CameraRenderGraph, + pub projection: XrProjection, + pub visible_entities: VisibleEntities, + pub frustum: Frustum, + pub transform: Transform, + pub global_transform: GlobalTransform, + pub camera_3d: Camera3d, + pub tonemapping: Tonemapping, + pub dither: DebandDither, + pub color_grading: ColorGrading, + pub exposure: Exposure, + pub main_texture_usages: CameraMainTextureUsages, + pub view: XrView, +} + +impl Default for XrCameraBundle { + fn default() -> Self { + Self { + camera_render_graph: CameraRenderGraph::new(Core3d), + camera: Default::default(), + projection: Default::default(), + visible_entities: Default::default(), + frustum: Default::default(), + transform: Default::default(), + global_transform: Default::default(), + camera_3d: Default::default(), + tonemapping: Default::default(), + color_grading: Default::default(), + exposure: Default::default(), + main_texture_usages: Default::default(), + dither: DebandDither::Enabled, + view: XrView, + } + } +} diff --git a/crates/bevy_xr/src/lib.rs b/crates/bevy_xr/src/lib.rs new file mode 100644 index 0000000..4ad0204 --- /dev/null +++ b/crates/bevy_xr/src/lib.rs @@ -0,0 +1,2 @@ +pub mod camera; +pub mod session; diff --git a/crates/bevy_xr/src/session.rs b/crates/bevy_xr/src/session.rs new file mode 100644 index 0000000..eeff78b --- /dev/null +++ b/crates/bevy_xr/src/session.rs @@ -0,0 +1,89 @@ +use bevy::app::{App, Plugin, PreUpdate}; +use bevy::ecs::event::{Event, EventReader, EventWriter}; +use bevy::ecs::system::Local; + +pub struct XrSessionPlugin; + +impl Plugin for XrSessionPlugin { + fn build(&self, app: &mut App) { + app.add_event::() + .add_event::() + .add_event::() + .add_event::() + .add_event::() + .add_event::() + .add_systems(PreUpdate, handle_xr_events); + } +} + +pub fn handle_xr_events( + mut instance_created: EventReader, + mut session_state: EventReader, + mut instance_destroyed: EventReader, + mut create_session: EventWriter, + mut begin_session: EventWriter, + mut has_instance: Local, + mut local_session_state: Local>, +) { + // Don't do anything if no events recieved + if instance_created.is_empty() && instance_destroyed.is_empty() && session_state.is_empty() { + return; + } + if !instance_created.is_empty() { + *has_instance = true; + instance_created.clear(); + } + if !instance_destroyed.is_empty() { + *has_instance = false; + instance_destroyed.clear(); + } + for state in session_state.read() { + *local_session_state = Some(*state); + } + if *has_instance { + if local_session_state.is_none() { + create_session.send_default(); + } else if matches!(*local_session_state, Some(XrSessionState::Ready)) { + begin_session.send_default(); + } + } +} + +/// Event sent to backends to create an XR session +#[derive(Event, Clone, Copy, Default)] +pub struct CreateXrSession; + +/// Event sent to backends to begin an XR session +#[derive(Event, Clone, Copy, Default)] +pub struct BeginXrSession; + +/// Event sent to backends to end an XR session. +#[derive(Event, Clone, Copy, Default)] +pub struct EndXrSession; + +// /// Event sent to backends to destroy an XR session. +// #[derive(Event, Clone, Copy, Default)] +// pub struct DestroyXrSession; + +/// Event sent from backends to inform the frontend of the session state. +#[derive(Event, Clone, Copy)] +pub enum XrSessionState { + /// The session is in an idle state. Either just created or stopped + Idle, + /// The session is ready. You may send a [`BeginXrSession`] event. + Ready, + /// The session is running. + Running, + /// The session is being stopped + Stopping, + /// The session is destroyed + Destroyed, +} + +/// Event sent from backends to inform the frontend that the instance was created. +#[derive(Event, Clone, Copy, Default)] +pub struct XrInstanceCreated; + +/// Event sent from backends to inform the frontend that the instance was destroyed. +#[derive(Event, Clone, Copy, Default)] +pub struct XrInstanceDestroyed; diff --git a/examples/3d_scene.rs b/examples/3d_scene.rs index 57190ac..1ce5628 100644 --- a/examples/3d_scene.rs +++ b/examples/3d_scene.rs @@ -1,51 +1,53 @@ -//! A simple 3D scene with light shining over a cube sitting on a plane. +// //! A simple 3D scene with light shining over a cube sitting on a plane. -use bevy::{prelude::*, render::camera::RenderTarget}; -use bevy_oxr::openxr::{render::LEFT_XR_TEXTURE_HANDLE, DefaultXrPlugins}; +// use bevy::{prelude::*, render::camera::RenderTarget}; +// use bevy_oxr::openxr::{render::LEFT_XR_TEXTURE_HANDLE, DefaultXrPlugins}; -fn main() { - App::new() - .add_plugins(DefaultXrPlugins) - .add_systems(Startup, setup) - .run(); -} +// fn main() { +// App::new() +// .add_plugins(DefaultXrPlugins) +// .add_systems(Startup, setup) +// .run(); +// } -/// set up a simple 3D scene -fn setup( - mut commands: Commands, - mut meshes: ResMut>, - mut materials: ResMut>, -) { - // circular base - commands.spawn(PbrBundle { - mesh: meshes.add(Circle::new(4.0)), - material: materials.add(Color::WHITE), - transform: Transform::from_rotation(Quat::from_rotation_x(-std::f32::consts::FRAC_PI_2)), - ..default() - }); - // cube - commands.spawn(PbrBundle { - mesh: meshes.add(Cuboid::new(1.0, 1.0, 1.0)), - material: materials.add(Color::rgb_u8(124, 144, 255)), - transform: Transform::from_xyz(0.0, 0.5, 0.0), - ..default() - }); - // light - commands.spawn(PointLightBundle { - point_light: PointLight { - shadows_enabled: true, - ..default() - }, - transform: Transform::from_xyz(4.0, 8.0, 4.0), - ..default() - }); - // camera - commands.spawn(Camera3dBundle { - transform: Transform::from_xyz(-2.5, 4.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y), - camera: Camera { - target: RenderTarget::TextureView(LEFT_XR_TEXTURE_HANDLE), - ..default() - }, - ..default() - }); -} +// /// set up a simple 3D scene +// fn setup( +// mut commands: Commands, +// mut meshes: ResMut>, +// mut materials: ResMut>, +// ) { +// // circular base +// commands.spawn(PbrBundle { +// mesh: meshes.add(Circle::new(4.0)), +// material: materials.add(Color::WHITE), +// transform: Transform::from_rotation(Quat::from_rotation_x(-std::f32::consts::FRAC_PI_2)), +// ..default() +// }); +// // cube +// commands.spawn(PbrBundle { +// mesh: meshes.add(Cuboid::new(1.0, 1.0, 1.0)), +// material: materials.add(Color::rgb_u8(124, 144, 255)), +// transform: Transform::from_xyz(0.0, 0.5, 0.0), +// ..default() +// }); +// // light +// commands.spawn(PointLightBundle { +// point_light: PointLight { +// shadows_enabled: true, +// ..default() +// }, +// transform: Transform::from_xyz(4.0, 8.0, 4.0), +// ..default() +// }); +// // camera +// commands.spawn(Camera3dBundle { +// transform: Transform::from_xyz(-2.5, 4.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y), +// camera: Camera { +// target: RenderTarget::TextureView(LEFT_XR_TEXTURE_HANDLE), +// ..default() +// }, +// ..default() +// }); +// } + +fn main() {} diff --git a/src/action_paths.rs b/src/action_paths.rs deleted file mode 100644 index 16921db..0000000 --- a/src/action_paths.rs +++ /dev/null @@ -1,130 +0,0 @@ -pub mod oculus_touch; - -mod private { - use bevy::math::Vec2; - - use crate::types::{Haptic, Pose}; - - pub trait Sealed {} - - impl Sealed for bool {} - impl Sealed for f32 {} - impl Sealed for Vec2 {} - impl Sealed for Pose {} - impl Sealed for Haptic {} -} - -use std::borrow::Cow; -use std::marker::PhantomData; - -pub trait ActionType: private::Sealed {} - -impl ActionType for T {} - -pub trait ActionPathTrait { - type PathType: ActionType; - fn path(&self) -> Cow<'_, str>; - fn name(&self) -> Cow<'_, str>; -} - -pub struct ActionPath { - pub path: &'static str, - pub name: &'static str, - _marker: PhantomData, -} - -macro_rules! actions { - // create path struct - ( - $($subpath:literal),* - $id:ident { - path: $path:literal; - } - ) => {}; - - // handle action path attrs - ( - $($subpath:literal),* - $id:ident { - path: $path:literal; - name: $name:literal; - path_type: $path_type:ty; - } - ) => { - paste::paste! { - pub const [<$id:snake:upper>]: crate::action_paths::ActionPath<$path_type> = crate::action_paths::ActionPath { - path: concat!($($subpath,)* $path), - name: $name, - _marker: std::marker::PhantomData, - }; - } - }; - - // handle action path attrs - ( - $($subpath:literal),* - $id:ident { - path: $path:literal; - name: $name:literal; - path_type: $path_type:ty; - $($children:tt)* - } - ) => { - crate::action_paths::actions! { - $($subpath),* - $id { - path: $path; - name: $name; - path_type: $path_type; - } - } - - crate::action_paths::actions! { - $($subpath),* - $id { - path: $path; - $($children)* - } - } - }; - - // handle children - ( - $($subpath:literal),* - $id:ident { - path: $path:literal; - $($children:tt)* - } - ) => { - pub mod $id { - crate::action_paths::actions! { - $($subpath,)* $path - $($children)* - } - } - }; - - // handle siblings - ( - $($subpath:literal),* - $id:ident { - path: $path:literal; - $($attrs:tt)* - } - $($siblings:tt)* - ) => { - crate::action_paths::actions! { - $($subpath),* - $id { - path: $path; - $($attrs)* - } - } - crate::action_paths::actions! { - $($subpath),* - $($siblings)* - } - }; -} - -pub(crate) use actions; diff --git a/src/action_paths/oculus_touch.rs b/src/action_paths/oculus_touch.rs deleted file mode 100644 index 895c2ef..0000000 --- a/src/action_paths/oculus_touch.rs +++ /dev/null @@ -1,238 +0,0 @@ -super::actions! { - "/user" - hand { - path: "/hand"; - left { - path: "/left"; - input { - path: "/input"; - x { - path: "/x"; - click { - path: "/click"; - name: "x_click"; - path_type: bool; - } - touch { - path: "/touch"; - name: "x_touch"; - path_type: bool; - } - } - y { - path: "/y"; - click { - path: "/click"; - name: "y_click"; - path_type: bool; - } - touch { - path: "/touch"; - name: "y_touch"; - path_type: bool; - } - } - menu { - path: "/menu"; - click { - path: "/click"; - name: "menu_click"; - path_type: bool; - } - } - squeeze { - path: "/squeeze"; - value { - path: "/value"; - name: "left_grip_val"; - path_type: f32; - } - } - trigger { - path: "/trigger"; - value { - path: "/value"; - name: "left_trigger_val"; - path_type: f32; - } - touch { - path: "/touch"; - name: "left_trigger_touch"; - path_type: bool; - } - } - thumbstick { - path: "/thumbstick"; - x { - path: "/x"; - name: "left_thumbstick_x"; - path_type: f32; - } - y { - path: "/y"; - name: "left_thumbstick_y"; - path_type: f32; - } - click { - path: "/click"; - name: "left_thumbstick_click"; - path_type: bool; - } - touch { - path: "/touch"; - name: "left_thumbstick_touch"; - path_type: bool; - } - } - thumbrest { - path: "/thumbrest"; - touch { - path: "/touch"; - name: "left_thumbrest_touch"; - path_type: bool; - } - } - grip { - path: "/grip"; - pose { - path: "/pose"; - name: "left_grip_pose"; - path_type: crate::types::Pose; - } - } - aim { - path: "/aim"; - pose { - path: "/pose"; - name: "left_aim_pose"; - path_type: crate::types::Pose; - } - } - } - output { - path: "/output"; - haptic { - path: "/haptic"; - name: "left_controller_haptic"; - path_type: crate::types::Haptic; - } - } - } - right { - path: "/right"; - input { - path: "/input"; - a { - path: "/a"; - click { - path: "/click"; - name: "a_click"; - path_type: bool; - } - touch { - path: "/touch"; - name: "a_touch"; - path_type: bool; - } - } - b { - path: "/b"; - click { - path: "/click"; - name: "b_click"; - path_type: bool; - } - touch { - path: "/touch"; - name: "b_touch"; - path_type: bool; - } - } - system { - path: "/system"; - click { - path: "/click"; - name: "system_click"; - path_type: bool; - } - } - squeeze { - path: "/squeeze"; - value { - path: "/value"; - name: "right_grip_val"; - path_type: f32; - } - } - trigger { - path: "/trigger"; - value { - path: "/value"; - name: "right_trigger_val"; - path_type: f32; - } - touch { - path: "/touch"; - name: "right_trigger_touch"; - path_type: bool; - } - } - thumbstick { - path: "/thumbstick"; - x { - path: "/x"; - name: "right_thumbstick_x"; - path_type: f32; - } - y { - path: "/y"; - name: "right_thumbstick_y"; - path_type: f32; - } - click { - path: "/click"; - name: "right_thumbstick_click"; - path_type: bool; - } - touch { - path: "/touch"; - name: "right_thumbstick_touch"; - path_type: bool; - } - } - thumbrest { - path: "/thumbrest"; - touch { - path: "/touch"; - name: "right_thumbrest_touch"; - path_type: bool; - } - } - grip { - path: "/grip"; - pose { - path: "/pose"; - name: "right_grip_pose"; - path_type: crate::types::Pose; - } - } - aim { - path: "/aim"; - pose { - path: "/pose"; - name: "right_aim_pose"; - path_type: crate::types::Pose; - } - } - } - output { - path: "/output"; - haptic { - path: "/haptic"; - name: "right_controller_haptic"; - path_type: crate::types::Haptic; - } - } - } - } -} diff --git a/src/actions.rs b/src/actions.rs deleted file mode 100644 index 6d05025..0000000 --- a/src/actions.rs +++ /dev/null @@ -1,23 +0,0 @@ -use std::{borrow::Cow, marker::PhantomData}; - -use bevy::prelude::*; - -pub use crate::action_paths::*; - -#[derive(Event)] -pub struct XrCreateActionSet { - pub handle: Handle, - pub name: String, -} - -pub struct XrAction<'a, T: ActionType> { - pub name: Cow<'a, str>, - pub pretty_name: Cow<'a, str>, - pub action_set: Handle, - _marker: PhantomData, -} - -#[derive(TypePath, Asset)] -pub struct XrActionSet { - pub name: String, -} diff --git a/src/lib.rs b/src/lib.rs index 47d79e9..646e102 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,8 +1,2 @@ -mod action_paths; -pub mod actions; -#[cfg(not(target_family = "wasm"))] -pub mod openxr; -pub mod render; -pub mod types; -#[cfg(target_family = "wasm")] -pub mod webxr; +pub use bevy_openxr; +pub use bevy_xr; diff --git a/src/openxr.rs b/src/openxr.rs deleted file mode 100644 index 524e83e..0000000 --- a/src/openxr.rs +++ /dev/null @@ -1,334 +0,0 @@ -mod extensions; -pub mod graphics; -pub mod render; -mod resources; -pub mod types; - -use bevy::ecs::schedule::common_conditions::resource_equals; -use bevy::ecs::system::{Res, ResMut}; -use bevy::math::{uvec2, UVec2}; -use bevy::render::extract_resource::ExtractResourcePlugin; -use bevy::render::renderer::{RenderAdapter, RenderAdapterInfo, RenderInstance, RenderQueue}; -use bevy::render::settings::RenderCreation; -use bevy::render::{RenderApp, RenderPlugin}; -use bevy::utils::default; -use bevy::DefaultPlugins; -pub use resources::*; -pub use types::*; - -use bevy::app::{App, First, Plugin, PluginGroup}; -use bevy::log::{error, info, warn}; - -pub fn xr_entry() -> Result { - #[cfg(windows)] - let entry = openxr::Entry::linked(); - #[cfg(not(windows))] - let entry = unsafe { openxr::Entry::load()? }; - Ok(XrEntry(entry)) -} - -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) { - panic!("Encountered an error while trying to initialize XR: {e}"); - } - app.add_systems(First, poll_events); - } -} - -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, 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 - } - ); - - // 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 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 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 (WgpuGraphics(device, queue, adapter_info, adapter, wgpu_instance), create_info) = - instance.init_graphics(system_id)?; - - let (session, frame_waiter, frame_stream) = - unsafe { instance.create_session(system_id, create_info)? }; - - 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(&device, format, resolution)?; - - let stage = XrStage( - session - .create_reference_space(openxr::ReferenceSpaceType::STAGE, openxr::Posef::IDENTITY)? - .into(), - ); - - 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, - }, - ExtractResourcePlugin::::default(), - ExtractResourcePlugin::::default(), - )); - let graphics_info = XrGraphicsInfo { - blend_mode, - swapchain_resolution: resolution, - swapchain_format: format, - }; - - app.insert_resource(instance.clone()) - .insert_resource(session.clone()) - .insert_resource(frame_waiter) - .insert_resource(images.clone()) - .insert_resource(graphics_info) - .insert_resource(stage.clone()) - .init_resource::(); - app.sub_app_mut(RenderApp) - .insert_resource(instance) - .insert_resource(session) - .insert_resource(frame_stream) - .insert_resource(swapchain) - .insert_resource(images) - .insert_resource(graphics_info) - .insert_resource(stage) - .init_resource::(); - - Ok(()) -} - -pub fn session_running() -> impl FnMut(Res) -> bool { - resource_equals(XrStatus::Enabled) -} - -pub fn poll_events( - instance: Res, - session: Res, - mut xr_status: ResMut, -) { - while let Some(event) = instance.poll_event(&mut Default::default()).unwrap() { - use openxr::Event::*; - match event { - SessionStateChanged(e) => { - // Session state change is where we can begin and end sessions, as well as - // find quit messages! - info!("entered XR state {:?}", e.state()); - match e.state() { - openxr::SessionState::READY => { - info!("Calling Session begin :3"); - session - .begin(openxr::ViewConfigurationType::PRIMARY_STEREO) - .unwrap(); - *xr_status = XrStatus::Enabled; - } - openxr::SessionState::STOPPING => { - session.end().unwrap(); - *xr_status = XrStatus::Disabled; - } - // openxr::SessionState::EXITING => { - // if *exit_type == ExitAppOnSessionExit::Always - // || *exit_type == ExitAppOnSessionExit::OnlyOnExit - // { - // app_exit.send_default(); - // } - // } - // openxr::SessionState::LOSS_PENDING => { - // if *exit_type == ExitAppOnSessionExit::Always { - // app_exit.send_default(); - // } - // if *exit_type == ExitAppOnSessionExit::OnlyOnExit { - // start_session.send_default(); - // } - // } - _ => {} - } - } - // InstanceLossPending(_) => { - // app_exit.send_default(); - // } - EventsLost(e) => { - warn!("lost {} XR events", e.lost_event_count()); - } - _ => {} - } - } -} - -pub struct DefaultXrPlugins; - -impl PluginGroup for DefaultXrPlugins { - fn build(self) -> bevy::app::PluginGroupBuilder { - DefaultPlugins - .build() - .disable::() - .add_before::(XrInitPlugin { - app_info: default(), - exts: default(), - blend_modes: default(), - backends: default(), - formats: Some(vec![wgpu::TextureFormat::Rgba8UnormSrgb]), - resolutions: default(), - synchronous_pipeline_compilation: default(), - }) - .add(render::XrRenderPlugin) - } -} diff --git a/src/openxr/render.rs b/src/openxr/render.rs deleted file mode 100644 index 67e75db..0000000 --- a/src/openxr/render.rs +++ /dev/null @@ -1,176 +0,0 @@ -use bevy::{ - prelude::*, - render::{ - camera::{ManualTextureView, ManualTextureViewHandle, ManualTextureViews}, - Render, RenderApp, RenderSet, - }, -}; -use openxr::CompositionLayerFlags; - -use crate::openxr::resources::*; -use crate::openxr::types::*; -use crate::openxr::XrTime; - -use super::{poll_events, session_running}; - -pub struct XrRenderPlugin; - -impl Plugin for XrRenderPlugin { - fn build(&self, app: &mut App) { - app.add_systems( - First, - wait_frame.after(poll_events).run_if(session_running()), - ) - .add_systems(Startup, init_texture_views); - app.sub_app_mut(RenderApp).add_systems( - Render, - ( - (begin_frame, insert_texture_views) - .chain() - .in_set(RenderSet::PrepareAssets), - end_frame.in_set(RenderSet::Cleanup), - ) - .run_if(session_running()), - ); - } -} - -pub const LEFT_XR_TEXTURE_HANDLE: ManualTextureViewHandle = ManualTextureViewHandle(1208214591); -pub const RIGHT_XR_TEXTURE_HANDLE: ManualTextureViewHandle = ManualTextureViewHandle(3383858418); - -fn init_texture_views( - graphics_info: Res, - mut manual_texture_views: ResMut, - swapchain_images: Res, -) { - let temp_tex = swapchain_images.first().unwrap(); - let left = temp_tex.create_view(&wgpu::TextureViewDescriptor { - dimension: Some(wgpu::TextureViewDimension::D2), - array_layer_count: Some(1), - ..Default::default() - }); - let right = temp_tex.create_view(&wgpu::TextureViewDescriptor { - dimension: Some(wgpu::TextureViewDimension::D2), - array_layer_count: Some(1), - base_array_layer: 1, - ..Default::default() - }); - let resolution = graphics_info.swapchain_resolution; - let format = graphics_info.swapchain_format; - let left = ManualTextureView { - texture_view: left.into(), - size: resolution, - format: format, - }; - let right = ManualTextureView { - texture_view: right.into(), - size: resolution, - format: format, - }; - manual_texture_views.insert(LEFT_XR_TEXTURE_HANDLE, left); - manual_texture_views.insert(RIGHT_XR_TEXTURE_HANDLE, right); -} - -fn wait_frame(mut frame_waiter: ResMut, mut commands: Commands) { - let state = frame_waiter.wait().expect("Failed to wait frame"); - commands.insert_resource(XrTime(openxr::Time::from_nanos( - state.predicted_display_time.as_nanos() + state.predicted_display_period.as_nanos(), - ))); -} - -pub fn begin_frame(mut frame_stream: ResMut) { - frame_stream.begin().expect("Failed to begin frame"); -} - -fn insert_texture_views( - swapchain_images: Res, - mut swapchain: ResMut, - mut manual_texture_views: ResMut, - graphics_info: Res, -) { - let index = swapchain.acquire_image().expect("Failed to acquire image"); - swapchain - .wait_image(openxr::Duration::INFINITE) - .expect("Failed to wait image"); - let image = &swapchain_images[index as usize]; - let left = image.create_view(&wgpu::TextureViewDescriptor { - dimension: Some(wgpu::TextureViewDimension::D2), - array_layer_count: Some(1), - ..Default::default() - }); - let right = image.create_view(&wgpu::TextureViewDescriptor { - dimension: Some(wgpu::TextureViewDimension::D2), - array_layer_count: Some(1), - base_array_layer: 1, - ..Default::default() - }); - let resolution = graphics_info.swapchain_resolution; - let format = graphics_info.swapchain_format; - let left = ManualTextureView { - texture_view: left.into(), - size: resolution, - format: format, - }; - let right = ManualTextureView { - texture_view: right.into(), - size: resolution, - format: format, - }; - manual_texture_views.insert(LEFT_XR_TEXTURE_HANDLE, left); - manual_texture_views.insert(RIGHT_XR_TEXTURE_HANDLE, right); -} - -fn end_frame( - mut frame_stream: ResMut, - session: Res, - mut swapchain: ResMut, - stage: Res, - display_time: Res, - graphics_info: Res, -) { - swapchain.release_image().unwrap(); - let (_flags, views) = session - .locate_views( - openxr::ViewConfigurationType::PRIMARY_STEREO, - **display_time, - &stage, - ) - .expect("Failed to locate views"); - - let rect = openxr::Rect2Di { - offset: openxr::Offset2Di { x: 0, y: 0 }, - extent: openxr::Extent2Di { - width: graphics_info.swapchain_resolution.x as _, - height: graphics_info.swapchain_resolution.y as _, - }, - }; - frame_stream - .end( - **display_time, - graphics_info.blend_mode, - &[&CompositionLayerProjection::new() - .layer_flags(CompositionLayerFlags::BLEND_TEXTURE_SOURCE_ALPHA) - .space(&stage) - .views(&[ - CompositionLayerProjectionView::new() - .pose(views[0].pose) - .fov(views[0].fov) - .sub_image( - SwapchainSubImage::new() - .swapchain(&swapchain) - .image_array_index(0) - .image_rect(rect), - ), - CompositionLayerProjectionView::new() - .pose(views[0].pose) - .fov(views[0].fov) - .sub_image( - SwapchainSubImage::new() - .swapchain(&swapchain) - .image_array_index(0) - .image_rect(rect), - ), - ])], - ) - .expect("Failed to end stream"); -} diff --git a/src/openxr/types.rs b/src/openxr/types.rs deleted file mode 100644 index 0fc6410..0000000 --- a/src/openxr/types.rs +++ /dev/null @@ -1,348 +0,0 @@ -use std::borrow::Cow; - -use super::graphics::{graphics_match, GraphicsExt, GraphicsWrap}; - -pub use super::extensions::XrExtensions; -pub use openxr::{ - EnvironmentBlendMode, Extent2Di, FormFactor, Graphics, Offset2Di, Rect2Di, - SwapchainCreateFlags, SwapchainUsageFlags, -}; -pub type Result = std::result::Result; - -pub struct WgpuGraphics( - pub wgpu::Device, - pub wgpu::Queue, - pub wgpu::AdapterInfo, - pub wgpu::Adapter, - pub wgpu::Instance, -); - -#[derive(Clone, Copy, Debug, Default, PartialEq)] -pub struct Version(pub u8, pub u8, pub u16); - -impl Version { - pub const BEVY: Self = Self(0, 12, 1); - - pub const fn to_u32(self) -> u32 { - let major = (self.0 as u32) << 24; - let minor = (self.1 as u32) << 16; - self.2 as u32 | major | minor - } -} - -#[derive(Clone, Debug, PartialEq)] -pub struct AppInfo { - pub name: Cow<'static, str>, - pub version: Version, -} - -impl Default for AppInfo { - fn default() -> Self { - Self { - name: "Bevy".into(), - version: Version::BEVY, - } - } -} - -pub type GraphicsBackend = GraphicsWrap<()>; - -impl GraphicsBackend { - const ALL: &'static [Self] = &[Self::Vulkan(())]; - - pub fn available_backends(exts: &XrExtensions) -> Vec { - Self::ALL - .iter() - .copied() - .filter(|backend| backend.is_available(exts)) - .collect() - } - - pub fn is_available(&self, exts: &XrExtensions) -> bool { - self.required_exts().is_available(exts) - } - - pub fn required_exts(&self) -> XrExtensions { - graphics_match!( - self; - _ => Api::required_exts() - ) - } -} - -mod error { - use super::GraphicsBackend; - use std::borrow::Cow; - use std::fmt; - use thiserror::Error; - - #[derive(Error, Debug)] - pub enum XrError { - #[error("OpenXR error: {0}")] - OpenXrError(#[from] openxr::sys::Result), - #[error("OpenXR loading error: {0}")] - OpenXrLoadingError(#[from] openxr::LoadError), - #[error("WGPU instance error: {0}")] - WgpuInstanceError(#[from] wgpu_hal::InstanceError), - #[error("WGPU device error: {0}")] - WgpuDeviceError(#[from] wgpu_hal::DeviceError), - #[error("WGPU request device error: {0}")] - WgpuRequestDeviceError(#[from] wgpu::RequestDeviceError), - #[error("Unsupported texture format: {0:?}")] - UnsupportedTextureFormat(wgpu::TextureFormat), - #[error("Vulkan error: {0}")] - VulkanError(#[from] ash::vk::Result), - #[error("Vulkan loading error: {0}")] - VulkanLoadingError(#[from] ash::LoadingError), - #[error("Graphics backend '{0:?}' is not available")] - UnavailableBackend(GraphicsBackend), - #[error("No compatible backend available")] - NoAvailableBackend, - #[error("No compatible view configuration available")] - NoAvailableViewConfiguration, - #[error("No compatible blend mode available")] - NoAvailableBlendMode, - #[error("No compatible format available")] - NoAvailableFormat, - #[error("OpenXR runtime does not support these extensions: {0}")] - UnavailableExtensions(UnavailableExts), - #[error("Could not meet graphics requirements for platform. See console for details")] - FailedGraphicsRequirements, - #[error( - "Tried to use item {item} with backend {backend}. Expected backend {expected_backend}" - )] - GraphicsBackendMismatch { - item: &'static str, - backend: &'static str, - expected_backend: &'static str, - }, - #[error("Failed to create CString: {0}")] - NulError(#[from] std::ffi::NulError), - } - - impl From>> for XrError { - fn from(value: Vec>) -> Self { - Self::UnavailableExtensions(UnavailableExts(value)) - } - } - - #[derive(Debug)] - pub struct UnavailableExts(Vec>); - - impl fmt::Display for UnavailableExts { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - for s in &self.0 { - write!(f, "\t{s}")?; - } - Ok(()) - } - } -} - -pub use error::XrError; - -#[derive(Debug, Copy, Clone)] -pub struct SwapchainCreateInfo { - pub create_flags: SwapchainCreateFlags, - pub usage_flags: SwapchainUsageFlags, - pub format: wgpu::TextureFormat, - pub sample_count: u32, - pub width: u32, - pub height: u32, - pub face_count: u32, - pub array_size: u32, - pub mip_count: u32, -} - -impl TryFrom for openxr::SwapchainCreateInfo { - type Error = XrError; - - fn try_from(value: SwapchainCreateInfo) -> Result { - Ok(openxr::SwapchainCreateInfo { - create_flags: value.create_flags, - usage_flags: value.usage_flags, - format: G::from_wgpu_format(value.format) - .ok_or(XrError::UnsupportedTextureFormat(value.format))?, - sample_count: value.sample_count, - width: value.width, - height: value.height, - face_count: value.face_count, - array_size: value.array_size, - mip_count: value.mip_count, - }) - } -} - -pub use builder::*; - -/// Copied with modification from the openxr crate to allow for a safe, graphics agnostic api to work with Bevy. -mod builder { - use std::mem; - - use openxr::{sys, CompositionLayerFlags, Fovf, Posef, Rect2Di, Space}; - - use crate::openxr::{graphics::graphics_match, XrSwapchain}; - - #[derive(Copy, Clone)] - pub struct SwapchainSubImage<'a> { - inner: sys::SwapchainSubImage, - swapchain: Option<&'a XrSwapchain>, - } - - impl<'a> SwapchainSubImage<'a> { - #[inline] - pub fn new() -> Self { - Self { - inner: sys::SwapchainSubImage { - ..unsafe { mem::zeroed() } - }, - swapchain: None, - } - } - #[inline] - pub fn into_raw(self) -> sys::SwapchainSubImage { - self.inner - } - #[inline] - pub fn as_raw(&self) -> &sys::SwapchainSubImage { - &self.inner - } - #[inline] - pub fn swapchain(mut self, value: &'a XrSwapchain) -> Self { - graphics_match!( - &value.0; - swap => self.inner.swapchain = swap.as_raw() - ); - self.swapchain = Some(value); - self - } - #[inline] - pub fn image_rect(mut self, value: Rect2Di) -> Self { - self.inner.image_rect = value; - self - } - #[inline] - pub fn image_array_index(mut self, value: u32) -> Self { - self.inner.image_array_index = value; - self - } - } - - impl<'a> Default for SwapchainSubImage<'a> { - fn default() -> Self { - Self::new() - } - } - - #[derive(Copy, Clone)] - pub struct CompositionLayerProjectionView<'a> { - inner: sys::CompositionLayerProjectionView, - swapchain: Option<&'a XrSwapchain>, - } - - impl<'a> CompositionLayerProjectionView<'a> { - #[inline] - pub fn new() -> Self { - Self { - inner: sys::CompositionLayerProjectionView { - ty: sys::StructureType::COMPOSITION_LAYER_PROJECTION_VIEW, - ..unsafe { mem::zeroed() } - }, - swapchain: None, - } - } - #[inline] - pub fn into_raw(self) -> sys::CompositionLayerProjectionView { - self.inner - } - #[inline] - pub fn as_raw(&self) -> &sys::CompositionLayerProjectionView { - &self.inner - } - #[inline] - pub fn pose(mut self, value: Posef) -> Self { - self.inner.pose = value; - self - } - #[inline] - pub fn fov(mut self, value: Fovf) -> Self { - self.inner.fov = value; - self - } - #[inline] - pub fn sub_image(mut self, value: SwapchainSubImage<'a>) -> Self { - self.inner.sub_image = value.inner; - self.swapchain = value.swapchain; - self - } - } - impl<'a> Default for CompositionLayerProjectionView<'a> { - fn default() -> Self { - Self::new() - } - } - pub unsafe trait CompositionLayer<'a> { - fn swapchain(&self) -> Option<&'a XrSwapchain>; - fn header(&self) -> &'a sys::CompositionLayerBaseHeader; - } - #[derive(Clone)] - pub struct CompositionLayerProjection<'a> { - inner: sys::CompositionLayerProjection, - swapchain: Option<&'a XrSwapchain>, - views: Vec, - } - impl<'a> CompositionLayerProjection<'a> { - #[inline] - pub fn new() -> Self { - Self { - inner: sys::CompositionLayerProjection { - ty: sys::StructureType::COMPOSITION_LAYER_PROJECTION, - ..unsafe { mem::zeroed() } - }, - swapchain: None, - views: Vec::new(), - } - } - #[inline] - pub fn into_raw(self) -> sys::CompositionLayerProjection { - self.inner - } - #[inline] - pub fn as_raw(&self) -> &sys::CompositionLayerProjection { - &self.inner - } - #[inline] - pub fn layer_flags(mut self, value: CompositionLayerFlags) -> Self { - self.inner.layer_flags = value; - self - } - #[inline] - pub fn space(mut self, value: &'a Space) -> Self { - self.inner.space = value.as_raw(); - self - } - #[inline] - pub fn views(mut self, value: &'a [CompositionLayerProjectionView<'a>]) -> Self { - for view in value { - self.views.push(view.inner.clone()); - } - self.inner.views = self.views.as_slice().as_ptr() as *const _ as _; - self.inner.view_count = value.len() as u32; - self - } - } - unsafe impl<'a> CompositionLayer<'a> for CompositionLayerProjection<'a> { - fn swapchain(&self) -> Option<&'a XrSwapchain> { - self.swapchain - } - - fn header(&self) -> &'a sys::CompositionLayerBaseHeader { - unsafe { std::mem::transmute(&self.inner) } - } - } - impl<'a> Default for CompositionLayerProjection<'a> { - fn default() -> Self { - Self::new() - } - } -} diff --git a/src/render.rs b/src/render.rs deleted file mode 100644 index 5e63cfb..0000000 --- a/src/render.rs +++ /dev/null @@ -1,19 +0,0 @@ -use bevy::ecs::system::Resource; -use bevy::math::Mat4; -use bevy::prelude::{Deref, DerefMut}; -use bevy::render::camera::{RenderTarget, Viewport}; - -use crate::types::Pose; - -pub const XR_TEXTURE_VIEW_INDEX: u32 = 1208214591; - -#[derive(Debug, Clone)] -pub struct XrView { - pub projection_matrix: Mat4, - pub pose: Pose, - pub render_target: RenderTarget, - pub view_port: Option, -} - -#[derive(Deref, DerefMut, Default, Debug, Clone, Resource)] -pub struct XrViews(#[deref] pub Vec); diff --git a/src/types.rs b/src/types.rs deleted file mode 100644 index 7877626..0000000 --- a/src/types.rs +++ /dev/null @@ -1,24 +0,0 @@ -use bevy::ecs::component::Component; -use bevy::math::{Quat, Vec3}; -use bevy::render::camera::ManualTextureViewHandle; - -#[derive(Debug, Clone)] -pub struct Pose { - pub translation: Vec3, - pub rotation: Quat, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, Component)] -pub struct XrView { - pub view_handle: ManualTextureViewHandle, - pub view_index: usize, -} - -pub struct Haptic; - -#[derive(Clone, Copy, Debug)] -pub enum BlendMode { - Opaque, - Additive, - AlphaBlend, -} diff --git a/src/webxr.rs b/src/webxr.rs deleted file mode 100644 index 3378c77..0000000 --- a/src/webxr.rs +++ /dev/null @@ -1,178 +0,0 @@ -pub mod render; -mod resources; - -pub use resources::*; - -use std::cell::RefCell; -use std::rc::Rc; -use std::sync::Mutex; -use std::time::Duration; - -use bevy::app::{App, Plugin, PluginsState}; -use bevy::ecs::entity::Entity; -use bevy::ecs::query::With; -use bevy::ecs::world::World; -use bevy::log::{error, info}; -use bevy::render::RenderApp; -use bevy::window::PrimaryWindow; -use bevy::winit::WinitWindows; -use js_sys::Object; -use wasm_bindgen::closure::Closure; -use wasm_bindgen::{JsCast, JsValue}; -use wasm_bindgen_futures::JsFuture; -use web_sys::{ - HtmlCanvasElement, WebGl2RenderingContext, XrReferenceSpaceType, XrRenderStateInit, - XrSessionMode, -}; -use winit::platform::web::WindowExtWebSys; - -#[derive(Clone)] -struct FutureXrSession(Rc>>>); - -pub struct XrInitPlugin; - -impl Plugin for XrInitPlugin { - fn build(&self, app: &mut App) { - let canvas = get_canvas(&mut app.world).unwrap(); - let future_session = FutureXrSession(Default::default()); - app.set_runner(webxr_runner); - app.insert_non_send_resource(future_session.clone()); - bevy::tasks::IoTaskPool::get().spawn_local(async move { - let result = - init_webxr(canvas, XrSessionMode::Inline, XrReferenceSpaceType::Viewer).await; - *future_session.0.lock().unwrap() = Some(result); - }); - } - - fn ready(&self, app: &App) -> bool { - app.world - .get_non_send_resource::() - .and_then(|fxr| fxr.0.try_lock().map(|locked| locked.is_some()).ok()) - .unwrap_or(true) - } - - fn finish(&self, app: &mut App) { - info!("finishing"); - - if let Some(result) = app - .world - .remove_non_send_resource::() - .and_then(|fxr| fxr.0.lock().unwrap().take()) - { - let (session, reference_space) = result.unwrap(); - app.insert_non_send_resource(session.clone()) - .insert_non_send_resource(reference_space.clone()); - app.sub_app_mut(RenderApp) - .insert_non_send_resource(session) - .insert_non_send_resource(reference_space); - } - } -} - -fn webxr_runner(mut app: App) { - fn set_timeout(f: &Closure, dur: Duration) { - web_sys::window() - .unwrap() - .set_timeout_with_callback_and_timeout_and_arguments_0( - f.as_ref().unchecked_ref(), - dur.as_millis() as i32, - ) - .expect("Should register `setTimeout`."); - } - let run_xr_inner = Rc::new(RefCell::new(None)); - let run_xr: Rc>>> = run_xr_inner.clone(); - *run_xr.borrow_mut() = Some(Closure::new(move || { - let app = &mut app; - if app.plugins_state() == PluginsState::Ready { - app.finish(); - app.cleanup(); - run_xr_app(std::mem::take(app)); - } else { - set_timeout( - run_xr_inner.borrow().as_ref().unwrap(), - Duration::from_millis(1), - ); - } - })); - set_timeout(run_xr.borrow().as_ref().unwrap(), Duration::from_millis(1)); -} - -fn run_xr_app(mut app: App) { - let session = app.world.non_send_resource::().clone(); - let inner_closure: Rc>>> = - Rc::new(RefCell::new(None)); - let closure = inner_closure.clone(); - *closure.borrow_mut() = Some(Closure::new(move |_time, frame: web_sys::XrFrame| { - let session = frame.session(); - app.insert_non_send_resource(XrFrame(frame.clone())); - app.sub_app_mut(RenderApp) - .insert_non_send_resource(XrFrame(frame)); - app.update(); - session.request_animation_frame( - inner_closure - .borrow() - .as_ref() - .unwrap() - .as_ref() - .unchecked_ref(), - ); - })); - session.request_animation_frame(closure.borrow().as_ref().unwrap().as_ref().unchecked_ref()); -} - -fn get_canvas(world: &mut World) -> Option { - let window_entity = world - .query_filtered::>() - .get_single(world) - .ok()?; - let windows = world.get_non_send_resource::()?; - Some(windows.get_window(window_entity)?.canvas()) -} - -async fn init_webxr( - canvas: HtmlCanvasElement, - mode: XrSessionMode, - reference_type: XrReferenceSpaceType, -) -> Result<(XrSession, XrReferenceSpace), JsValue> { - let xr = web_sys::window().unwrap().navigator().xr(); - - let supports_session = JsFuture::from(xr.is_session_supported(mode)).await?; - if supports_session == false { - error!("XR session {:?} not supported", mode); - return Err(JsValue::from_str(&format!( - "XR session {:?} not supported", - mode - ))); - } - - info!("creating session"); - let session: web_sys::XrSession = JsFuture::from(xr.request_session(mode)).await?.into(); - - info!("creating gl"); - let gl: WebGl2RenderingContext = { - let gl_attribs = Object::new(); - js_sys::Reflect::set( - &gl_attribs, - &JsValue::from_str("xrCompatible"), - &JsValue::TRUE, - )?; - canvas - .get_context_with_context_options("webgl2", &gl_attribs)? - .ok_or(JsValue::from_str( - "Unable to create WebGL rendering context", - ))? - .dyn_into()? - }; - - let xr_gl_layer = web_sys::XrWebGlLayer::new_with_web_gl2_rendering_context(&session, &gl)?; - let mut render_state_init = XrRenderStateInit::new(); - render_state_init.base_layer(Some(&xr_gl_layer)); - session.update_render_state_with_state(&render_state_init); - info!("creating ref space"); - let reference_space = JsFuture::from(session.request_reference_space(reference_type)) - .await? - .into(); - - info!("finished"); - Ok((XrSession(session), XrReferenceSpace(reference_space))) -} diff --git a/src/webxr/render.rs b/src/webxr/render.rs deleted file mode 100644 index 7b46e2a..0000000 --- a/src/webxr/render.rs +++ /dev/null @@ -1,154 +0,0 @@ -use crate::render::{XrView, XrViews}; -use crate::types::Pose; - -use super::resources::*; - -use bevy::app::{App, Plugin, PreUpdate}; -use bevy::ecs::schedule::IntoSystemConfigs; -use bevy::ecs::system::{NonSend, Res, ResMut}; -use bevy::ecs::world::World; -use bevy::math::{quat, uvec2, vec3, Mat4}; -use bevy::render::camera::{ - ManualTextureView, ManualTextureViewHandle, ManualTextureViews, RenderTarget, Viewport, -}; -use bevy::render::renderer::RenderDevice; -use bevy::utils::default; - -pub const XR_TEXTURE_VIEW_HANDLE: ManualTextureViewHandle = - ManualTextureViewHandle(crate::render::XR_TEXTURE_VIEW_INDEX); - -pub struct XrRenderingPlugin; - -impl Plugin for XrRenderingPlugin { - fn build(&self, app: &mut App) { - app.init_resource::(); - app.add_systems( - PreUpdate, - (insert_gl_layer, update_manual_texture_views, insert_views).chain(), - ); - } -} - -pub fn insert_gl_layer(world: &mut World) { - let gl_layer = world - .non_send_resource::() - .session() - .render_state() - .base_layer() - .unwrap(); - world.insert_non_send_resource(XrWebGlLayer(gl_layer)); -} - -pub fn update_manual_texture_views( - gl_layer: NonSend, - render_device: Res, - mut manual_tex_view: ResMut, -) { - let dest_texture = create_framebuffer_texture(render_device.wgpu_device(), &gl_layer); - let view = dest_texture.create_view(&default()); - - manual_tex_view.insert( - XR_TEXTURE_VIEW_HANDLE, - ManualTextureView::with_default_format( - view.into(), - uvec2(gl_layer.framebuffer_width(), gl_layer.framebuffer_height()), - ), - ); -} - -pub fn insert_views( - gl_layer: NonSend, - reference_space: NonSend, - frame: NonSend, - mut xr_views: ResMut, -) { - let Some(viewer_pose) = frame.get_viewer_pose(&reference_space) else { - return; - }; - - let views = viewer_pose - .views() - .into_iter() - .map(Into::::into) - .map(|view| { - let transform = view.transform(); - let position = transform.position(); - let orientation = transform.orientation(); - let viewport = gl_layer - .get_viewport(&view) - .map(|viewport| Viewport { - physical_position: uvec2(viewport.x() as u32, viewport.y() as u32), - physical_size: uvec2(viewport.width() as u32, viewport.height() as u32), - ..Default::default() - }) - .unwrap(); - XrView { - projection_matrix: Mat4::from_cols_array( - &view.projection_matrix().try_into().unwrap(), - ), - pose: Pose { - translation: vec3( - position.x() as f32, - position.y() as f32, - position.z() as f32, - ), - rotation: quat( - orientation.x() as f32, - orientation.y() as f32, - orientation.z() as f32, - orientation.w() as f32, - ), - }, - render_target: RenderTarget::TextureView(XR_TEXTURE_VIEW_HANDLE), - view_port: Some(viewport), - } - }) - .collect(); - xr_views.0 = views; -} - -pub fn create_framebuffer_texture(device: &wgpu::Device, gl_layer: &XrWebGlLayer) -> wgpu::Texture { - unsafe { - device.create_texture_from_hal::( - wgpu_hal::gles::Texture { - inner: wgpu_hal::gles::TextureInner::ExternalFramebuffer { - // inner: framebuffer, - inner: gl_layer.framebuffer_unwrapped(), - // inner: framebuffer.as_ref().unwrap().clone(), - }, - mip_level_count: 1, - array_layer_count: 1, - format: wgpu::TextureFormat::Rgba8Unorm, //TODO check this is ok, different from bevy default - format_desc: wgpu_hal::gles::TextureFormatDesc { - internal: glow::RGBA, - external: glow::RGBA, - data_type: glow::UNSIGNED_BYTE, - }, - copy_size: wgpu_hal::CopyExtent { - width: gl_layer.framebuffer_width(), - height: gl_layer.framebuffer_height(), - depth: 1, - }, - drop_guard: None, - is_cubemap: false, - }, - &wgpu::TextureDescriptor { - label: Some("framebuffer (color)"), - size: wgpu::Extent3d { - width: gl_layer.framebuffer_width(), - height: gl_layer.framebuffer_height(), - depth_or_array_layers: 1, - }, - mip_level_count: 1, - sample_count: 1, - dimension: wgpu::TextureDimension::D2, - format: wgpu::TextureFormat::Rgba8UnormSrgb, - view_formats: &[wgpu::TextureFormat::Rgba8UnormSrgb], - usage: wgpu::TextureUsages::RENDER_ATTACHMENT - | wgpu::TextureUsages::TEXTURE_BINDING, - // | wgpu::TextureUsages::COPY_SRC, - // | wgpu::TextureUsages::COPY_DST, - }, - ) - } -} diff --git a/src/webxr/resources.rs b/src/webxr/resources.rs deleted file mode 100644 index 3a894a4..0000000 --- a/src/webxr/resources.rs +++ /dev/null @@ -1,25 +0,0 @@ -use bevy::prelude::{Deref, DerefMut}; -use web_sys::WebGlFramebuffer; - -#[derive(Deref, DerefMut, Clone)] -pub struct XrSession(#[deref] pub(crate) web_sys::XrSession); - -#[derive(Deref, DerefMut, Clone)] -pub struct XrReferenceSpace(#[deref] pub(crate) web_sys::XrReferenceSpace); - -#[derive(Deref, DerefMut, Clone)] -pub struct XrFrame(#[deref] pub(crate) web_sys::XrFrame); - -#[derive(Deref, DerefMut, Clone)] -pub struct XrWebGlLayer(#[deref] pub(crate) web_sys::XrWebGlLayer); - -impl XrWebGlLayer { - pub(crate) fn framebuffer_unwrapped(&self) -> WebGlFramebuffer { - js_sys::Reflect::get(&self, &"framebuffer".into()) - .unwrap() - .into() - } -} - -#[derive(Clone)] -pub struct XrView(pub(crate) web_sys::XrView);