rendering code and api crate

This commit is contained in:
awtterpip
2024-03-10 19:59:19 -05:00
parent 47eba9215f
commit 91eb263b4f
31 changed files with 1597 additions and 1775 deletions

View File

@@ -12,6 +12,8 @@ linked = ["openxr/linked"]
vulkan = ["dep:ash"] vulkan = ["dep:ash"]
[dependencies] [dependencies]
bevy_openxr.path = "./crates/bevy_openxr"
bevy_xr.path = "./crates/bevy_xr"
anyhow = "1.0.79" anyhow = "1.0.79"
async-std = "1.12.0" async-std = "1.12.0"
bevy = "0.13.0" bevy = "0.13.0"
@@ -88,3 +90,9 @@ web-sys = { version = "0.3.67", features = [
'XrSystem', 'XrSystem',
] } ] }
wasm-bindgen-futures = "0.4" wasm-bindgen-futures = "0.4"
[workspace]
members = ["crates/*"]
[workspace.dependencies]
bevy = "0.13.0"

View File

@@ -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"] }

View File

@@ -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<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
// 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()
// });
}

View File

View File

@@ -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<Vec<Cow<'static, str>>> for XrError {
fn from(value: Vec<Cow<'static, str>>) -> Self {
Self::UnavailableExtensions(UnavailableExts(value))
}
}
#[derive(Debug)]
pub struct UnavailableExts(Vec<Cow<'static, str>>);
impl fmt::Display for UnavailableExts {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for s in &self.0 {
write!(f, "\t{s}")?;
}
Ok(())
}
}

View File

@@ -62,7 +62,7 @@ macro_rules! unavailable_exts {
) => { ) => {
impl $exts { impl $exts {
/// Returns any extensions needed by `required_exts` that aren't available in `self` /// Returns any extensions needed by `required_exts` that aren't available in `self`
pub(crate) fn unavailable_exts(&self, required_exts: &Self) -> Vec<std::borrow::Cow<'static, str>> { pub fn unavailable_exts(&self, required_exts: &Self) -> Vec<std::borrow::Cow<'static, str>> {
let mut exts = vec![]; let mut exts = vec![];
$( $(
$( $(
@@ -167,6 +167,7 @@ macro_rules! impl_ext {
$macro! { $macro! {
XrExtensions; XrExtensions;
almalence_digital_lens_control, almalence_digital_lens_control,
bd_controller_interaction,
epic_view_configuration_fov, epic_view_configuration_fov,
ext_performance_settings, ext_performance_settings,
ext_thermal_query, ext_thermal_query,
@@ -183,13 +184,18 @@ macro_rules! impl_ext {
ext_hp_mixed_reality_controller, ext_hp_mixed_reality_controller,
ext_palm_pose, ext_palm_pose,
ext_uuid, 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_image_layout,
fb_composition_layer_alpha_blend, fb_composition_layer_alpha_blend,
#[cfg(target_os = "android")] #[cfg(target_os = "android")]
fb_android_surface_swapchain_create, fb_android_surface_swapchain_create,
fb_swapchain_update_state, fb_swapchain_update_state,
fb_composition_layer_secure_content, fb_composition_layer_secure_content,
fb_body_tracking,
fb_display_refresh_rate, fb_display_refresh_rate,
fb_color_space, fb_color_space,
fb_hand_tracking_mesh, fb_hand_tracking_mesh,
@@ -209,17 +215,29 @@ macro_rules! impl_ext {
fb_swapchain_update_state_android_surface, fb_swapchain_update_state_android_surface,
fb_swapchain_update_state_opengl_es, fb_swapchain_update_state_opengl_es,
fb_swapchain_update_state_vulkan, fb_swapchain_update_state_vulkan,
fb_touch_controller_pro,
fb_spatial_entity_sharing,
fb_space_warp, fb_space_warp,
fb_haptic_amplitude_envelope,
fb_scene, fb_scene,
fb_scene_capture,
fb_spatial_entity_container, fb_spatial_entity_container,
fb_face_tracking,
fb_eye_tracking_social,
fb_passthrough_keyboard_hands, fb_passthrough_keyboard_hands,
fb_composition_layer_settings, 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_vive_cosmos_controller_interaction,
htc_facial_tracking, htc_facial_tracking,
htc_vive_focus3_controller_interaction, htc_vive_focus3_controller_interaction,
htc_hand_interaction, htc_hand_interaction,
htc_vive_wrist_tracker_interaction, htc_vive_wrist_tracker_interaction,
htcx_vive_tracker_interaction, htc_passthrough,
htc_foveation,
huawei_controller_interaction, huawei_controller_interaction,
#[cfg(target_os = "android")] #[cfg(target_os = "android")]
khr_android_thread_settings, khr_android_thread_settings,
@@ -251,12 +269,21 @@ macro_rules! impl_ext {
khr_composition_layer_equirect2, khr_composition_layer_equirect2,
khr_binding_modification, khr_binding_modification,
khr_swapchain_usage_input_attachment_bit, 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_vulkan_swapchain_create_info,
meta_performance_metrics, meta_performance_metrics,
meta_headset_id,
meta_passthrough_color_lut,
ml_ml2_controller_interaction, ml_ml2_controller_interaction,
ml_frame_end_info,
ml_global_dimmer,
ml_compat,
ml_user_calibration,
mnd_headless, mnd_headless,
mnd_swapchain_usage_input_attachment_bit, mnd_swapchain_usage_input_attachment_bit,
mndx_egl_enable,
msft_unbounded_reference_space, msft_unbounded_reference_space,
msft_spatial_anchor, msft_spatial_anchor,
msft_spatial_graph_bridge, msft_spatial_graph_bridge,
@@ -274,6 +301,9 @@ macro_rules! impl_ext {
#[cfg(target_os = "android")] #[cfg(target_os = "android")]
oculus_android_session_state_enable, oculus_android_session_state_enable,
oculus_audio_device_guid, oculus_audio_device_guid,
oculus_external_camera,
oppo_controller_interaction,
qcom_tracking_optimization_settings,
ultraleap_hand_tracking_forearm, ultraleap_hand_tracking_forearm,
valve_analog_threshold, valve_analog_threshold,
varjo_quad_views, varjo_quad_views,
@@ -282,6 +312,7 @@ macro_rules! impl_ext {
varjo_environment_depth_estimation, varjo_environment_depth_estimation,
varjo_marker_tracking, varjo_marker_tracking,
varjo_view_offset, varjo_view_offset,
yvr_controller_interaction,
} }
)* )*

View File

@@ -1,11 +1,10 @@
#[cfg(feature = "vulkan")]
pub mod vulkan; pub mod vulkan;
use std::any::TypeId; use std::any::TypeId;
use bevy::math::UVec2; use crate::extensions::XrExtensions;
use crate::types::*;
use crate::openxr::types::{AppInfo, Result, XrError};
use crate::types::BlendMode;
pub unsafe trait GraphicsExt: openxr::Graphics { pub unsafe trait GraphicsExt: openxr::Graphics {
/// Wrap the graphics specific type into the [GraphicsWrap] enum /// Wrap the graphics specific type into the [GraphicsWrap] enum
@@ -30,8 +29,42 @@ pub unsafe trait GraphicsExt: openxr::Graphics {
fn required_exts() -> XrExtensions; fn required_exts() -> XrExtensions;
} }
pub trait GraphicsType {
type Inner<G: GraphicsExt>;
}
impl GraphicsType for () {
type Inner<G: GraphicsExt> = ();
}
pub type GraphicsBackend = GraphicsWrap<()>;
impl GraphicsBackend {
const ALL: &'static [Self] = &[Self::Vulkan(())];
pub fn available_backends(exts: &XrExtensions) -> Vec<Self> {
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)] #[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum GraphicsWrap<T: GraphicsType> { pub enum GraphicsWrap<T: GraphicsType> {
#[cfg(feature = "vulkan")]
Vulkan(T::Inner<openxr::Vulkan>), Vulkan(T::Inner<openxr::Vulkan>),
} }
@@ -62,21 +95,14 @@ impl<T: GraphicsType> GraphicsWrap<T> {
} }
} }
pub trait GraphicsType {
type Inner<G: GraphicsExt>;
}
impl GraphicsType for () {
type Inner<G: GraphicsExt> = ();
}
macro_rules! graphics_match { macro_rules! graphics_match {
( (
$field:expr; $field:expr;
$var:pat => $expr:expr $(=> $($return:tt)*)? $var:pat => $expr:expr $(=> $($return:tt)*)?
) => { ) => {
match $field { match $field {
$crate::openxr::graphics::GraphicsWrap::Vulkan($var) => { #[cfg(feature = "vulkan")]
$crate::graphics::GraphicsWrap::Vulkan($var) => {
#[allow(unused)] #[allow(unused)]
type Api = openxr::Vulkan; type Api = openxr::Vulkan;
graphics_match!(@arm_impl Vulkan; $expr $(=> $($return)*)?) graphics_match!(@arm_impl Vulkan; $expr $(=> $($return)*)?)
@@ -101,32 +127,5 @@ macro_rules! graphics_match {
}; };
} }
use bevy::math::UVec2;
pub(crate) use graphics_match; pub(crate) use graphics_match;
use super::{WgpuGraphics, XrExtensions};
impl From<openxr::EnvironmentBlendMode> 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<BlendMode> 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,
}
}
}

View File

@@ -7,10 +7,11 @@ use openxr::Version;
use wgpu_hal::api::Vulkan; use wgpu_hal::api::Vulkan;
use wgpu_hal::Api; use wgpu_hal::Api;
use crate::openxr::types::Result; use crate::error::XrError;
use crate::openxr::{extensions::XrExtensions, WgpuGraphics}; use crate::extensions::XrExtensions;
use crate::types::*;
use super::{AppInfo, GraphicsExt, XrError}; use super::GraphicsExt;
#[cfg(not(target_os = "android"))] #[cfg(not(target_os = "android"))]
const VK_TARGET_VERSION: Version = Version::new(1, 2, 0); const VK_TARGET_VERSION: Version = Version::new(1, 2, 0);

View File

@@ -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<Vec<EnvironmentBlendMode>>,
/// List of backends the openxr session can use. If [None], pick the first available backend.
pub backends: Option<Vec<GraphicsBackend>>,
/// List of formats the openxr session can use. If [None], pick the first available format
pub formats: Option<Vec<wgpu::TextureFormat>>,
/// List of resolutions that the openxr swapchain can use. If [None] pick the first available resolution.
pub resolutions: Option<Vec<UVec2>>,
/// Passed into the render plugin when added to the app.
pub synchronous_pipeline_compilation: bool,
}
pub fn instance_created(status: Option<Res<XrStatus>>) -> bool {
status.is_some_and(|status| status.instance_created)
}
pub fn session_created(status: Option<Res<XrStatus>>) -> bool {
status.is_some_and(|status| status.session_created)
}
pub fn session_running(status: Option<Res<XrStatus>>) -> bool {
status.is_some_and(|status| status.session_running)
}
pub fn session_ready(status: Option<Res<XrStatus>>) -> 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<XrEntry> {
#[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() {
"<unnamed>"
} 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::<XrTime>::default(),
ExtractResourcePlugin::<XrStatus>::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::<CreateXrSession>()),
begin_xr_session
.run_if(session_ready)
.run_if(on_event::<BeginXrSession>()),
),
)
.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<Vec<EnvironmentBlendMode>>,
/// List of formats the openxr session can use. If [None], pick the first available format
formats: Option<Vec<wgpu::TextureFormat>>,
/// List of resolutions that the openxr swapchain can use. If [None] pick the first available resolution.
resolutions: Option<Vec<UVec2>>,
/// Graphics info used to create a session.
create_info: SessionCreateInfo,
}
pub fn create_xr_session(world: &mut World) {
let Some(create_info) = world.remove_non_send_resource() else {
error!(
"Failed to retrive SessionCreateInfo. This is likely due to improper initialization."
);
return;
};
let Some(instance) = world.get_resource().cloned() else {
error!("Failed to retrieve XrInstance. This is likely due to improper initialization.");
return;
};
let Some(system_id) = world.get_resource::<SystemId>().cloned() else {
error!("Failed to retrieve SystemId. THis is likely due to improper initialization");
return;
};
if let Err(e) = 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::<RenderDevice>().wgpu_device(),
format,
resolution,
)?;
let available_blend_modes =
instance.enumerate_environment_blend_modes(system_id, view_configuration_type)?;
// blend mode selection
let blend_mode = if let Some(wanted_blend_modes) = &config.blend_modes {
let mut blend_mode = None;
for wanted_blend_mode in wanted_blend_modes {
if available_blend_modes.contains(wanted_blend_mode) {
blend_mode = Some(*wanted_blend_mode);
break;
}
}
blend_mode
} else {
available_blend_modes.first().copied()
}
.ok_or(XrError::NoAvailableBackend)?;
let stage = XrStage(
session
.create_reference_space(openxr::ReferenceSpaceType::STAGE, openxr::Posef::IDENTITY)?
.into(),
);
let graphics_info = XrGraphicsInfo {
blend_mode,
resolution,
format,
};
world.resource_mut::<XrStatus>().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<XrSession>,
mut session_state: EventWriter<XrSessionState>,
mut status: ResMut<XrStatus>,
) {
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<MainWorld>) {
let Some(XrRenderResources {
session,
frame_stream,
swapchain,
images,
graphics_info,
stage,
}) = world.remove_resource()
else {
return;
};
commands.insert_resource(session);
commands.insert_resource(frame_stream);
commands.insert_resource(swapchain);
commands.insert_resource(images);
commands.insert_resource(graphics_info);
commands.insert_resource(stage);
}
pub fn poll_events(
instance: Res<XrInstance>,
session: Option<Res<XrSession>>,
mut session_state: EventWriter<XrSessionState>,
mut instance_destroyed: EventWriter<XrInstanceDestroyed>,
mut status: ResMut<XrStatus>,
) {
let mut buffer = Default::default();
while let Some(event) = instance
.poll_event(&mut buffer)
.expect("Failed to poll event")
{
use openxr::Event::*;
match event {
SessionStateChanged(e) => {
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();
}
_ => {}
}
}
}

View File

@@ -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<sys::CompositionLayerProjectionView>,
}
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()
}
}

View File

@@ -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<G: PluginGroup>(plugins: G) -> PluginGroupBuilder {
plugins
.build()
.disable::<RenderPlugin>()
.add_before::<RenderPlugin, _>(bevy_xr::session::XrSessionPlugin)
.add_before::<RenderPlugin, _>(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)
}

View File

@@ -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::<XrGraphicsInfo>),
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<XrGraphicsInfo>,
mut manual_texture_views: ResMut<ManualTextureViews>,
swapchain_images: Res<XrSwapchainImages>,
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<XrFrameWaiter>, 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<XrView>>,
view_entities: Res<XrViews>,
session: Res<XrSession>,
stage: Res<XrStage>,
time: Res<XrTime>,
) {
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<XrFrameStream>) {
frame_stream.begin().expect("Failed to begin frame");
}
pub fn insert_texture_views(
swapchain_images: Res<XrSwapchainImages>,
mut swapchain: ResMut<XrSwapchain>,
mut manual_texture_views: ResMut<ManualTextureViews>,
graphics_info: Res<XrGraphicsInfo>,
) {
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<XrFrameStream>,
session: Res<XrSession>,
mut swapchain: ResMut<XrSwapchain>,
stage: Res<XrStage>,
display_time: Res<XrTime>,
graphics_info: Res<XrGraphicsInfo>,
) {
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");
}

View File

@@ -1,13 +1,13 @@
use std::sync::Arc; use std::sync::Arc;
use crate::error::XrError;
use crate::graphics::*;
use crate::layer_builder::CompositionLayer;
use crate::types::*;
use bevy::prelude::*; use bevy::prelude::*;
use bevy::render::extract_resource::ExtractResource; use bevy::render::extract_resource::ExtractResource;
use openxr::AnyGraphics; use openxr::AnyGraphics;
use super::graphics::{graphics_match, GraphicsExt, GraphicsType, GraphicsWrap};
use super::types::*;
#[derive(Deref, Clone)] #[derive(Deref, Clone)]
pub struct XrEntry(pub openxr::Entry); 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)] #[derive(Resource, Deref, Clone)]
pub struct XrInstance( pub struct XrInstance(
#[deref] pub openxr::Instance, #[deref] pub openxr::Instance,
@@ -229,7 +219,7 @@ impl XrSwapchain {
device: &wgpu::Device, device: &wgpu::Device,
format: wgpu::TextureFormat, format: wgpu::TextureFormat,
resolution: UVec2, resolution: UVec2,
) -> Result<SwapchainImages> { ) -> Result<XrSwapchainImages> {
graphics_match!( graphics_match!(
&mut self.0; &mut self.0;
swap => { swap => {
@@ -239,7 +229,7 @@ impl XrSwapchain {
images.push(Api::to_wgpu_img(image, device, format, resolution)?); 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<openxr::Space>); pub struct XrStage(pub Arc<openxr::Space>);
#[derive(Debug, Deref, Resource, Clone)] #[derive(Debug, Deref, Resource, Clone)]
pub struct SwapchainImages(pub Arc<Vec<wgpu::Texture>>); pub struct XrSwapchainImages(pub Arc<Vec<wgpu::Texture>>);
#[derive(Copy, Clone, Eq, PartialEq, Deref, DerefMut, Resource, ExtractResource)] #[derive(Copy, Clone, Eq, PartialEq, Deref, DerefMut, Resource, ExtractResource)]
pub struct XrTime(pub openxr::Time); pub struct XrTime(pub openxr::Time);
#[derive(Clone, Copy, Eq, PartialEq, Default, Resource, ExtractResource)] #[derive(Copy, Clone, Eq, PartialEq, Resource)]
pub enum XrStatus { pub struct XrSwapchainInfo {
Enabled, pub format: wgpu::TextureFormat,
#[default] pub resolution: UVec2,
Disabled,
} }
#[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<Entity>);

View File

@@ -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<T> = std::result::Result<T, XrError>;
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<G: GraphicsExt> TryFrom<SwapchainCreateInfo> for openxr::SwapchainCreateInfo<G> {
type Error = XrError;
fn try_from(value: SwapchainCreateInfo) -> Result<Self> {
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,
})
}
}

View File

@@ -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

View File

@@ -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::<XrProjection>::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,
}
}
}

View File

@@ -0,0 +1,2 @@
pub mod camera;
pub mod session;

View File

@@ -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::<CreateXrSession>()
.add_event::<BeginXrSession>()
.add_event::<EndXrSession>()
.add_event::<XrSessionState>()
.add_event::<XrInstanceCreated>()
.add_event::<XrInstanceDestroyed>()
.add_systems(PreUpdate, handle_xr_events);
}
}
pub fn handle_xr_events(
mut instance_created: EventReader<XrInstanceCreated>,
mut session_state: EventReader<XrSessionState>,
mut instance_destroyed: EventReader<XrInstanceDestroyed>,
mut create_session: EventWriter<CreateXrSession>,
mut begin_session: EventWriter<BeginXrSession>,
mut has_instance: Local<bool>,
mut local_session_state: Local<Option<XrSessionState>>,
) {
// 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;

View File

@@ -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::{prelude::*, render::camera::RenderTarget};
use bevy_oxr::openxr::{render::LEFT_XR_TEXTURE_HANDLE, DefaultXrPlugins}; // use bevy_oxr::openxr::{render::LEFT_XR_TEXTURE_HANDLE, DefaultXrPlugins};
fn main() { // fn main() {
App::new() // App::new()
.add_plugins(DefaultXrPlugins) // .add_plugins(DefaultXrPlugins)
.add_systems(Startup, setup) // .add_systems(Startup, setup)
.run(); // .run();
} // }
/// set up a simple 3D scene // /// set up a simple 3D scene
fn setup( // fn setup(
mut commands: Commands, // mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>, // mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>, // mut materials: ResMut<Assets<StandardMaterial>>,
) { // ) {
// circular base // // circular base
commands.spawn(PbrBundle { // commands.spawn(PbrBundle {
mesh: meshes.add(Circle::new(4.0)), // mesh: meshes.add(Circle::new(4.0)),
material: materials.add(Color::WHITE), // material: materials.add(Color::WHITE),
transform: Transform::from_rotation(Quat::from_rotation_x(-std::f32::consts::FRAC_PI_2)), // transform: Transform::from_rotation(Quat::from_rotation_x(-std::f32::consts::FRAC_PI_2)),
..default() // ..default()
}); // });
// cube // // cube
commands.spawn(PbrBundle { // commands.spawn(PbrBundle {
mesh: meshes.add(Cuboid::new(1.0, 1.0, 1.0)), // mesh: meshes.add(Cuboid::new(1.0, 1.0, 1.0)),
material: materials.add(Color::rgb_u8(124, 144, 255)), // material: materials.add(Color::rgb_u8(124, 144, 255)),
transform: Transform::from_xyz(0.0, 0.5, 0.0), // transform: Transform::from_xyz(0.0, 0.5, 0.0),
..default() // ..default()
}); // });
// light // // light
commands.spawn(PointLightBundle { // commands.spawn(PointLightBundle {
point_light: PointLight { // point_light: PointLight {
shadows_enabled: true, // shadows_enabled: true,
..default() // ..default()
}, // },
transform: Transform::from_xyz(4.0, 8.0, 4.0), // transform: Transform::from_xyz(4.0, 8.0, 4.0),
..default() // ..default()
}); // });
// camera // // camera
commands.spawn(Camera3dBundle { // commands.spawn(Camera3dBundle {
transform: Transform::from_xyz(-2.5, 4.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y), // transform: Transform::from_xyz(-2.5, 4.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y),
camera: Camera { // camera: Camera {
target: RenderTarget::TextureView(LEFT_XR_TEXTURE_HANDLE), // target: RenderTarget::TextureView(LEFT_XR_TEXTURE_HANDLE),
..default() // ..default()
}, // },
..default() // ..default()
}); // });
} // }
fn main() {}

View File

@@ -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<T: private::Sealed> ActionType for T {}
pub trait ActionPathTrait {
type PathType: ActionType;
fn path(&self) -> Cow<'_, str>;
fn name(&self) -> Cow<'_, str>;
}
pub struct ActionPath<T: ActionType> {
pub path: &'static str,
pub name: &'static str,
_marker: PhantomData<T>,
}
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;

View File

@@ -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;
}
}
}
}
}

View File

@@ -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<XrActionSet>,
pub name: String,
}
pub struct XrAction<'a, T: ActionType> {
pub name: Cow<'a, str>,
pub pretty_name: Cow<'a, str>,
pub action_set: Handle<XrActionSet>,
_marker: PhantomData<T>,
}
#[derive(TypePath, Asset)]
pub struct XrActionSet {
pub name: String,
}

View File

@@ -1,8 +1,2 @@
mod action_paths; pub use bevy_openxr;
pub mod actions; pub use bevy_xr;
#[cfg(not(target_family = "wasm"))]
pub mod openxr;
pub mod render;
pub mod types;
#[cfg(target_family = "wasm")]
pub mod webxr;

View File

@@ -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<XrEntry> {
#[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<Vec<EnvironmentBlendMode>>,
/// List of backends the openxr session can use. If [None], pick the first available backend.
pub backends: Option<Vec<GraphicsBackend>>,
/// List of formats the openxr session can use. If [None], pick the first available format
pub formats: Option<Vec<wgpu::TextureFormat>>,
/// List of resolutions that the openxr swapchain can use. If [None] pick the first available resolution.
pub resolutions: Option<Vec<UVec2>>,
/// Passed into the render plugin when added to the app.
pub synchronous_pipeline_compilation: bool,
}
impl Plugin for XrInitPlugin {
fn build(&self, app: &mut App) {
if let Err(e) = init_xr(self, app) {
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() {
"<unnamed>"
} 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::<XrTime>::default(),
ExtractResourcePlugin::<XrStatus>::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::<XrStatus>();
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::<XrStatus>();
Ok(())
}
pub fn session_running() -> impl FnMut(Res<XrStatus>) -> bool {
resource_equals(XrStatus::Enabled)
}
pub fn poll_events(
instance: Res<XrInstance>,
session: Res<XrSession>,
mut xr_status: ResMut<XrStatus>,
) {
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::<RenderPlugin>()
.add_before::<RenderPlugin, _>(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)
}
}

View File

@@ -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<XrGraphicsInfo>,
mut manual_texture_views: ResMut<ManualTextureViews>,
swapchain_images: Res<SwapchainImages>,
) {
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<XrFrameWaiter>, 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<XrFrameStream>) {
frame_stream.begin().expect("Failed to begin frame");
}
fn insert_texture_views(
swapchain_images: Res<SwapchainImages>,
mut swapchain: ResMut<XrSwapchain>,
mut manual_texture_views: ResMut<ManualTextureViews>,
graphics_info: Res<XrGraphicsInfo>,
) {
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<XrFrameStream>,
session: Res<XrSession>,
mut swapchain: ResMut<XrSwapchain>,
stage: Res<XrStage>,
display_time: Res<XrTime>,
graphics_info: Res<XrGraphicsInfo>,
) {
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");
}

View File

@@ -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<T> = std::result::Result<T, XrError>;
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> {
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<Vec<Cow<'static, str>>> for XrError {
fn from(value: Vec<Cow<'static, str>>) -> Self {
Self::UnavailableExtensions(UnavailableExts(value))
}
}
#[derive(Debug)]
pub struct UnavailableExts(Vec<Cow<'static, str>>);
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<G: GraphicsExt> TryFrom<SwapchainCreateInfo> for openxr::SwapchainCreateInfo<G> {
type Error = XrError;
fn try_from(value: SwapchainCreateInfo) -> Result<Self> {
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<sys::CompositionLayerProjectionView>,
}
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()
}
}
}

View File

@@ -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<Viewport>,
}
#[derive(Deref, DerefMut, Default, Debug, Clone, Resource)]
pub struct XrViews(#[deref] pub Vec<XrView>);

View File

@@ -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,
}

View File

@@ -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<Mutex<Option<Result<(XrSession, XrReferenceSpace), JsValue>>>>);
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::<FutureXrSession>()
.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::<FutureXrSession>()
.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<dyn FnMut()>, 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<RefCell<Option<Closure<dyn FnMut()>>>> = 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::<XrSession>().clone();
let inner_closure: Rc<RefCell<Option<Closure<dyn FnMut(f64, web_sys::XrFrame)>>>> =
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<HtmlCanvasElement> {
let window_entity = world
.query_filtered::<Entity, With<PrimaryWindow>>()
.get_single(world)
.ok()?;
let windows = world.get_non_send_resource::<WinitWindows>()?;
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)))
}

View File

@@ -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::<XrViews>();
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::<XrFrame>()
.session()
.render_state()
.base_layer()
.unwrap();
world.insert_non_send_resource(XrWebGlLayer(gl_layer));
}
pub fn update_manual_texture_views(
gl_layer: NonSend<XrWebGlLayer>,
render_device: Res<RenderDevice>,
mut manual_tex_view: ResMut<ManualTextureViews>,
) {
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<XrWebGlLayer>,
reference_space: NonSend<XrReferenceSpace>,
frame: NonSend<XrFrame>,
mut xr_views: ResMut<XrViews>,
) {
let Some(viewer_pose) = frame.get_viewer_pose(&reference_space) else {
return;
};
let views = viewer_pose
.views()
.into_iter()
.map(Into::<web_sys::XrView>::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::Api>(
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,
},
)
}
}

View File

@@ -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);