From 47eba9215fe508af6ed16ac9188eb575183b1674 Mon Sep 17 00:00:00 2001 From: awtterpip Date: Fri, 8 Mar 2024 18:27:33 -0600 Subject: [PATCH] rendering code --- Cargo.toml | 11 +- examples/3d_scene.rs | 101 +++---- src/action_paths.rs | 130 ++++++++ src/{actions => action_paths}/oculus_touch.rs | 0 src/actions.rs | 147 +-------- src/lib.rs | 1 + src/openxr.rs | 278 +++++++++++++++++- src/openxr/extensions.rs | 4 +- src/openxr/graphics.rs | 10 +- src/openxr/graphics/vulkan.rs | 38 ++- src/openxr/render.rs | 176 +++++++++++ src/openxr/resources.rs | 43 ++- src/openxr/types.rs | 30 +- src/types.rs | 8 + 14 files changed, 735 insertions(+), 242 deletions(-) create mode 100644 src/action_paths.rs rename src/{actions => action_paths}/oculus_touch.rs (100%) create mode 100644 src/openxr/render.rs diff --git a/Cargo.toml b/Cargo.toml index ce042aa..af2baa7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,11 +14,11 @@ vulkan = ["dep:ash"] [dependencies] anyhow = "1.0.79" async-std = "1.12.0" -bevy = "0.12.1" +bevy = "0.13.0" paste = "1.0.14" thiserror = "1.0.57" -wgpu = "0.17.1" -wgpu-hal = "0.17.1" +wgpu = "0.19.3" +wgpu-hal = "0.19.3" winit = "0.28.7" [target.'cfg(target_family = "unix")'.dependencies] @@ -34,7 +34,7 @@ ash = { version = "0.37.3", optional = true } js-sys = "0.3" wasm-bindgen = "0.2.91" glow = "0.12.1" -web-sys = { version = "0.3.68", features = [ +web-sys = { version = "0.3.67", features = [ # STANDARD 'console', 'Document', @@ -88,6 +88,3 @@ web-sys = { version = "0.3.68", features = [ 'XrSystem', ] } wasm-bindgen-futures = "0.4" - -[workspace] -members = ["xr_api"] diff --git a/examples/3d_scene.rs b/examples/3d_scene.rs index 8afe0d9..57190ac 100644 --- a/examples/3d_scene.rs +++ b/examples/3d_scene.rs @@ -1,56 +1,51 @@ -// //! 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::*; -// use bevy::render::camera::RenderTarget; -// use bevy_oxr::webxr::render::{XrRenderingPlugin, XR_TEXTURE_VIEW_HANDLE}; -// use bevy_oxr::webxr::XrInitPlugin; +use bevy::{prelude::*, render::camera::RenderTarget}; +use bevy_oxr::openxr::{render::LEFT_XR_TEXTURE_HANDLE, DefaultXrPlugins}; -// fn main() { -// App::new() -// .add_plugins((DefaultPlugins, XrInitPlugin, XrRenderingPlugin)) -// .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(shape::Circle::new(4.0).into()), -// material: materials.add(Color::WHITE.into()), -// transform: Transform::from_rotation(Quat::from_rotation_x(-std::f32::consts::FRAC_PI_2)), -// ..default() -// }); -// // cube -// commands.spawn(PbrBundle { -// mesh: meshes.add(Mesh::from(shape::Cube { size: 1.0 })), -// material: materials.add(Color::rgb_u8(124, 144, 255).into()), -// transform: Transform::from_xyz(0.0, 0.5, 0.0), -// ..default() -// }); -// // light -// commands.spawn(PointLightBundle { -// point_light: PointLight { -// intensity: 1500.0, -// 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(XR_TEXTURE_VIEW_HANDLE), -// ..default() -// }, -// ..default() -// }); -// } - -fn main() {} +/// 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() + }); +} diff --git a/src/action_paths.rs b/src/action_paths.rs new file mode 100644 index 0000000..16921db --- /dev/null +++ b/src/action_paths.rs @@ -0,0 +1,130 @@ +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/actions/oculus_touch.rs b/src/action_paths/oculus_touch.rs similarity index 100% rename from src/actions/oculus_touch.rs rename to src/action_paths/oculus_touch.rs diff --git a/src/actions.rs b/src/actions.rs index 27bacde..6d05025 100644 --- a/src/actions.rs +++ b/src/actions.rs @@ -1,142 +1,23 @@ -pub mod oculus_touch; +use std::{borrow::Cow, marker::PhantomData}; -mod private { - use bevy::math::Vec2; +use bevy::prelude::*; - use crate::types::{Haptic, Pose}; +pub use crate::action_paths::*; - pub trait Sealed {} - - impl Sealed for bool {} - impl Sealed for f32 {} - impl Sealed for Vec2 {} - impl Sealed for Pose {} - impl Sealed for Haptic {} +#[derive(Event)] +pub struct XrCreateActionSet { + pub handle: Handle, + pub name: String, } -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 { - path: &'static str, - name: &'static str, +pub struct XrAction<'a, T: ActionType> { + pub name: Cow<'a, str>, + pub pretty_name: Cow<'a, str>, + pub action_set: Handle, _marker: PhantomData, } -impl ActionPathTrait for ActionPath { - type PathType = T; - - fn path(&self) -> Cow<'_, str> { - self.path.into() - } - - fn name(&self) -> Cow<'_, str> { - self.name.into() - } +#[derive(TypePath, Asset)] +pub struct XrActionSet { + pub name: String, } - -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::actions::ActionPath<$path_type> = crate::actions::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::path::actions! { - $($subpath),* - $id { - path: $path; - name: $name; - path_type: $path_type; - } - } - - crate::path::actions! { - $($subpath),* - $id { - path: $path; - $($children)* - } - } - }; - - // handle children - ( - $($subpath:literal),* - $id:ident { - path: $path:literal; - $($children:tt)* - } - ) => { - pub mod $id { - crate::actions::actions! { - $($subpath,)* $path - $($children)* - } - } - }; - - // handle siblings - ( - $($subpath:literal),* - $id:ident { - path: $path:literal; - $($attrs:tt)* - } - $($siblings:tt)* - ) => { - crate::actions::actions! { - $($subpath),* - $id { - path: $path; - $($attrs)* - } - } - crate::actions::actions! { - $($subpath),* - $($siblings)* - } - }; -} - -pub(crate) use actions; diff --git a/src/lib.rs b/src/lib.rs index b4e1f2a..47d79e9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,4 @@ +mod action_paths; pub mod actions; #[cfg(not(target_family = "wasm"))] pub mod openxr; diff --git a/src/openxr.rs b/src/openxr.rs index 1d1ffcb..524e83e 100644 --- a/src/openxr.rs +++ b/src/openxr.rs @@ -1,13 +1,23 @@ 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, Plugin}; -use bevy::log::error; +use bevy::app::{App, First, Plugin, PluginGroup}; +use bevy::log::{error, info, warn}; pub fn xr_entry() -> Result { #[cfg(windows)] @@ -21,24 +31,35 @@ pub struct XrInitPlugin { /// Information about the app this is being used to build. pub app_info: AppInfo, /// Extensions wanted for this session. - // This should preferably be changed into a simpler list of features wanted that this crate supports. i.e. hand tracking + // 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) { - init_xr(self, app).unwrap(); - todo!() + 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<()> { +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." @@ -65,8 +86,249 @@ fn init_xr(config: &XrInitPlugin, _app: &mut App) -> Result<()> { let exts = config.exts.clone() & available_exts; let instance = entry.create_instance(config.app_info.clone(), exts, backend)?; - let _system_id = instance.system(openxr::FormFactor::HEAD_MOUNTED_DISPLAY)?; + 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::(); - //instance.create_session(system_id)?; 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/extensions.rs b/src/openxr/extensions.rs index b5d8f09..0423ed7 100644 --- a/src/openxr/extensions.rs +++ b/src/openxr/extensions.rs @@ -43,8 +43,8 @@ impl From for ExtensionSet { } impl Default for XrExtensions { fn default() -> Self { - let mut exts = ExtensionSet::default(); - exts.ext_hand_tracking = true; + let exts = ExtensionSet::default(); + //exts.ext_hand_tracking = true; Self(exts) } } diff --git a/src/openxr/graphics.rs b/src/openxr/graphics.rs index 2680035..a1900f1 100644 --- a/src/openxr/graphics.rs +++ b/src/openxr/graphics.rs @@ -19,13 +19,7 @@ pub unsafe trait GraphicsExt: openxr::Graphics { app_info: &AppInfo, instance: &openxr::Instance, system_id: openxr::SystemId, - ) -> Result<( - wgpu::Device, - wgpu::Queue, - wgpu::Adapter, - wgpu::Instance, - Self::SessionCreateInfo, - )>; + ) -> Result<(WgpuGraphics, Self::SessionCreateInfo)>; /// Convert a swapchain function unsafe fn to_wgpu_img( image: Self::SwapchainImage, @@ -109,7 +103,7 @@ macro_rules! graphics_match { pub(crate) use graphics_match; -use super::XrExtensions; +use super::{WgpuGraphics, XrExtensions}; impl From for BlendMode { fn from(value: openxr::EnvironmentBlendMode) -> Self { diff --git a/src/openxr/graphics/vulkan.rs b/src/openxr/graphics/vulkan.rs index 4bc1889..60a8610 100644 --- a/src/openxr/graphics/vulkan.rs +++ b/src/openxr/graphics/vulkan.rs @@ -7,8 +7,8 @@ use openxr::Version; use wgpu_hal::api::Vulkan; use wgpu_hal::Api; -use crate::openxr::extensions::XrExtensions; use crate::openxr::types::Result; +use crate::openxr::{extensions::XrExtensions, WgpuGraphics}; use super::{AppInfo, GraphicsExt, XrError}; @@ -37,13 +37,7 @@ unsafe impl GraphicsExt for openxr::Vulkan { app_info: &AppInfo, instance: &openxr::Instance, system_id: openxr::SystemId, - ) -> Result<( - wgpu::Device, - wgpu::Queue, - wgpu::Adapter, - wgpu::Instance, - Self::SessionCreateInfo, - )> { + ) -> Result<(WgpuGraphics, Self::SessionCreateInfo)> { let reqs = instance.graphics_requirements::(system_id)?; if VK_TARGET_VERSION < reqs.min_api_version_supported || VK_TARGET_VERSION.major() > reqs.max_api_version_supported.major() @@ -56,12 +50,9 @@ unsafe impl GraphicsExt for openxr::Vulkan { return Err(XrError::FailedGraphicsRequirements); }; let vk_entry = unsafe { ash::Entry::load() }?; - let flags = wgpu_hal::InstanceFlags::empty(); - let extensions = ::Instance::required_extensions( - &vk_entry, - VK_TARGET_VERSION_ASH, - flags, - )?; + let flags = wgpu::InstanceFlags::empty(); + let extensions = + ::Instance::desired_extensions(&vk_entry, VK_TARGET_VERSION_ASH, flags)?; let device_extensions = vec![ ash::extensions::khr::Swapchain::name(), ash::extensions::khr::DrawIndirectCount::name(), @@ -211,8 +202,8 @@ unsafe impl GraphicsExt for openxr::Vulkan { wgpu_open_device, &wgpu::DeviceDescriptor { label: None, - features: wgpu_features, - limits: wgpu::Limits { + required_features: wgpu_features, + required_limits: wgpu::Limits { max_bind_groups: 8, max_storage_buffer_binding_size: wgpu_adapter .limits() @@ -226,10 +217,13 @@ unsafe impl GraphicsExt for openxr::Vulkan { }?; Ok(( - wgpu_device, - wgpu_queue, - wgpu_adapter, - wgpu_instance, + WgpuGraphics( + wgpu_device, + wgpu_queue, + wgpu_adapter.get_info(), + wgpu_adapter, + wgpu_instance, + ), openxr::vulkan::SessionCreateInfo { instance: vk_instance_ptr, physical_device: vk_physical_device_ptr, @@ -334,6 +328,7 @@ fn vulkan_to_wgpu(format: ash::vk::Format) -> Option { F::B8G8R8A8_UNORM => Tf::Bgra8Unorm, F::R8G8B8A8_UINT => Tf::Rgba8Uint, F::R8G8B8A8_SINT => Tf::Rgba8Sint, + F::A2B10G10R10_UINT_PACK32 => Tf::Rgb10a2Uint, F::A2B10G10R10_UNORM_PACK32 => Tf::Rgb10a2Unorm, F::B10G11R11_UFLOAT_PACK32 => Tf::Rg11b10Float, F::R32G32_UINT => Tf::Rg32Uint, @@ -350,6 +345,7 @@ fn vulkan_to_wgpu(format: ash::vk::Format) -> Option { F::D32_SFLOAT => Tf::Depth32Float, F::D32_SFLOAT_S8_UINT => Tf::Depth32FloatStencil8, F::D16_UNORM => Tf::Depth16Unorm, + F::G8_B8R8_2PLANE_420_UNORM => Tf::NV12, F::E5B9G9R9_UFLOAT_PACK32 => Tf::Rgb9e5Ufloat, F::BC1_RGBA_UNORM_BLOCK => Tf::Bc1RgbaUnorm, F::BC1_RGBA_SRGB_BLOCK => Tf::Bc1RgbaUnormSrgb, @@ -583,6 +579,7 @@ fn wgpu_to_vulkan(format: wgpu::TextureFormat) -> Option { Tf::Bgra8Unorm => F::B8G8R8A8_UNORM, Tf::Rgba8Uint => F::R8G8B8A8_UINT, Tf::Rgba8Sint => F::R8G8B8A8_SINT, + Tf::Rgb10a2Uint => F::A2B10G10R10_UINT_PACK32, Tf::Rgb10a2Unorm => F::A2B10G10R10_UNORM_PACK32, Tf::Rg11b10Float => F::B10G11R11_UFLOAT_PACK32, Tf::Rg32Uint => F::R32G32_UINT, @@ -600,6 +597,7 @@ fn wgpu_to_vulkan(format: wgpu::TextureFormat) -> Option { Tf::Depth32FloatStencil8 => F::D32_SFLOAT_S8_UINT, Tf::Depth24Plus | Tf::Depth24PlusStencil8 | Tf::Stencil8 => return None, // Dependent on device properties Tf::Depth16Unorm => F::D16_UNORM, + Tf::NV12 => F::G8_B8R8_2PLANE_420_UNORM, Tf::Rgb9e5Ufloat => F::E5B9G9R9_UFLOAT_PACK32, Tf::Bc1RgbaUnorm => F::BC1_RGBA_UNORM_BLOCK, Tf::Bc1RgbaUnormSrgb => F::BC1_RGBA_SRGB_BLOCK, diff --git a/src/openxr/render.rs b/src/openxr/render.rs new file mode 100644 index 0000000..67e75db --- /dev/null +++ b/src/openxr/render.rs @@ -0,0 +1,176 @@ +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/resources.rs b/src/openxr/resources.rs index 07e0bc4..b6857bc 100644 --- a/src/openxr/resources.rs +++ b/src/openxr/resources.rs @@ -1,7 +1,8 @@ +use std::sync::Arc; + use bevy::prelude::*; -use bevy::render::renderer::{RenderAdapter, RenderAdapterInfo, RenderInstance, RenderQueue}; -use bevy::render::settings::RenderCreation; +use bevy::render::extract_resource::ExtractResource; use openxr::AnyGraphics; use super::graphics::{graphics_match, GraphicsExt, GraphicsType, GraphicsWrap}; @@ -50,6 +51,16 @@ 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, @@ -61,13 +72,13 @@ impl XrInstance { pub fn init_graphics( &self, system_id: openxr::SystemId, - ) -> Result<(RenderCreation, SessionCreateInfo)> { + ) -> Result<(WgpuGraphics, SessionCreateInfo)> { graphics_match!( self.1; _ => { - let (device, queue, adapter, instance, session_info) = Api::init_graphics(&self.2, &self, system_id)?; + let (graphics, session_info) = Api::init_graphics(&self.2, &self, system_id)?; - Ok((RenderCreation::manual(device.into(), RenderQueue(queue.into()), RenderAdapterInfo(adapter.get_info()), RenderAdapter(adapter.into()), RenderInstance(instance.into())), SessionCreateInfo(Api::wrap(session_info)))) + Ok((graphics, SessionCreateInfo(Api::wrap(session_info)))) } ) } @@ -90,7 +101,7 @@ impl XrInstance { graphics_match!( info.0; info => { - let (session, frame_waiter, frame_stream) = unsafe { self.0.create_session::(system_id, &info)? }; + let (session, frame_waiter, frame_stream) = self.0.create_session::(system_id, &info)?; Ok((session.into(), XrFrameWaiter(frame_waiter), XrFrameStream(Api::wrap(frame_stream)))) } ) @@ -218,7 +229,7 @@ impl XrSwapchain { device: &wgpu::Device, format: wgpu::TextureFormat, resolution: UVec2, - ) -> Result> { + ) -> Result { graphics_match!( &mut self.0; swap => { @@ -228,8 +239,24 @@ impl XrSwapchain { images.push(Api::to_wgpu_img(image, device, format, resolution)?); } } - Ok(images) + Ok(SwapchainImages(images.into())) } ) } } + +#[derive(Deref, Clone, Resource)] +pub struct XrStage(pub Arc); + +#[derive(Debug, Deref, Resource, Clone)] +pub struct SwapchainImages(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, +} diff --git a/src/openxr/types.rs b/src/openxr/types.rs index 8a6ec45..0fc6410 100644 --- a/src/openxr/types.rs +++ b/src/openxr/types.rs @@ -4,10 +4,19 @@ use super::graphics::{graphics_match, GraphicsExt, GraphicsWrap}; pub use super::extensions::XrExtensions; pub use openxr::{ - Extent2Di, Graphics, Offset2Di, Rect2Di, SwapchainCreateFlags, SwapchainUsageFlags, SystemId, + 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); @@ -21,12 +30,21 @@ impl Version { } } -#[derive(Clone, Debug, Default, PartialEq)] +#[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 { @@ -78,8 +96,14 @@ mod error { VulkanLoadingError(#[from] ash::LoadingError), #[error("Graphics backend '{0:?}' is not available")] UnavailableBackend(GraphicsBackend), - #[error("No available backend")] + #[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")] diff --git a/src/types.rs b/src/types.rs index f637f5f..7877626 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,4 +1,6 @@ +use bevy::ecs::component::Component; use bevy::math::{Quat, Vec3}; +use bevy::render::camera::ManualTextureViewHandle; #[derive(Debug, Clone)] pub struct Pose { @@ -6,6 +8,12 @@ pub struct Pose { 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)]