rendering code and api crate
This commit is contained in:
25
crates/bevy_openxr/Cargo.toml
Normal file
25
crates/bevy_openxr/Cargo.toml
Normal 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"] }
|
||||
55
crates/bevy_openxr/examples/3d_scene.rs
Normal file
55
crates/bevy_openxr/examples/3d_scene.rs
Normal 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()
|
||||
// });
|
||||
}
|
||||
0
crates/bevy_openxr/src/camera.rs
Normal file
0
crates/bevy_openxr/src/camera.rs
Normal file
66
crates/bevy_openxr/src/error.rs
Normal file
66
crates/bevy_openxr/src/error.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
322
crates/bevy_openxr/src/extensions.rs
Normal file
322
crates/bevy_openxr/src/extensions.rs
Normal file
@@ -0,0 +1,322 @@
|
||||
use bevy::prelude::{Deref, DerefMut};
|
||||
use openxr::ExtensionSet;
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Deref, DerefMut)]
|
||||
pub struct XrExtensions(ExtensionSet);
|
||||
impl XrExtensions {
|
||||
pub fn raw_mut(&mut self) -> &mut ExtensionSet {
|
||||
&mut self.0
|
||||
}
|
||||
pub fn raw(&self) -> &ExtensionSet {
|
||||
&self.0
|
||||
}
|
||||
pub fn enable_fb_passthrough(&mut self) -> &mut Self {
|
||||
self.0.fb_passthrough = true;
|
||||
self
|
||||
}
|
||||
pub fn disable_fb_passthrough(&mut self) -> &mut Self {
|
||||
self.0.fb_passthrough = false;
|
||||
self
|
||||
}
|
||||
pub fn enable_hand_tracking(&mut self) -> &mut Self {
|
||||
self.0.ext_hand_tracking = true;
|
||||
self
|
||||
}
|
||||
pub fn disable_hand_tracking(&mut self) -> &mut Self {
|
||||
self.0.ext_hand_tracking = false;
|
||||
self
|
||||
}
|
||||
/// returns true if all of the extensions enabled are also available in `available_exts`
|
||||
pub fn is_available(&self, available_exts: &XrExtensions) -> bool {
|
||||
self.clone() & available_exts.clone() == *self
|
||||
}
|
||||
}
|
||||
impl From<ExtensionSet> for XrExtensions {
|
||||
fn from(value: ExtensionSet) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
impl From<XrExtensions> for ExtensionSet {
|
||||
fn from(val: XrExtensions) -> Self {
|
||||
val.0
|
||||
}
|
||||
}
|
||||
impl Default for XrExtensions {
|
||||
fn default() -> Self {
|
||||
let exts = ExtensionSet::default();
|
||||
//exts.ext_hand_tracking = true;
|
||||
Self(exts)
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! unavailable_exts {
|
||||
(
|
||||
$exts:ty;
|
||||
$(
|
||||
$(
|
||||
#[$meta:meta]
|
||||
)*
|
||||
$ident:ident
|
||||
),*
|
||||
$(,)?
|
||||
) => {
|
||||
impl $exts {
|
||||
/// Returns any extensions needed by `required_exts` that aren't available in `self`
|
||||
pub fn unavailable_exts(&self, required_exts: &Self) -> Vec<std::borrow::Cow<'static, str>> {
|
||||
let mut exts = vec![];
|
||||
$(
|
||||
$(
|
||||
#[$meta]
|
||||
)*
|
||||
if required_exts.0.$ident && !self.0.$ident {
|
||||
exts.push(std::borrow::Cow::Borrowed(stringify!($ident)))
|
||||
}
|
||||
)*
|
||||
for ext in required_exts.0.other.iter() {
|
||||
if !self.0.other.contains(ext) {
|
||||
exts.push(std::borrow::Cow::Owned(ext.clone()))
|
||||
}
|
||||
}
|
||||
exts
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! bitor {
|
||||
(
|
||||
$exts:ty;
|
||||
$(
|
||||
$(
|
||||
#[$meta:meta]
|
||||
)*
|
||||
$ident:ident
|
||||
),*
|
||||
$(,)?
|
||||
) => {
|
||||
impl std::ops::BitOr for $exts {
|
||||
type Output = Self;
|
||||
|
||||
fn bitor(self, rhs: Self) -> Self::Output {
|
||||
let mut out = ExtensionSet::default();
|
||||
$(
|
||||
$(
|
||||
#[$meta]
|
||||
)*
|
||||
{
|
||||
out.$ident = self.0.$ident || rhs.0.$ident;
|
||||
}
|
||||
|
||||
)*
|
||||
out.other = self.0.other;
|
||||
for ext in rhs.0.other {
|
||||
if !out.other.contains(&ext) {
|
||||
out.other.push(ext);
|
||||
}
|
||||
}
|
||||
Self(out)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! bitand {
|
||||
(
|
||||
$exts:ty;
|
||||
$(
|
||||
$(
|
||||
#[$meta:meta]
|
||||
)*
|
||||
$ident:ident
|
||||
),*
|
||||
$(,)?
|
||||
) => {
|
||||
impl std::ops::BitAnd for $exts {
|
||||
type Output = Self;
|
||||
|
||||
fn bitand(self, rhs: Self) -> Self::Output {
|
||||
let mut out = ExtensionSet::default();
|
||||
$(
|
||||
$(
|
||||
#[$meta]
|
||||
)*
|
||||
{
|
||||
out.$ident = self.0.$ident && rhs.0.$ident;
|
||||
}
|
||||
|
||||
)*
|
||||
for ext in self.0.other {
|
||||
if rhs.0.other.contains(&ext) {
|
||||
out.other.push(ext);
|
||||
}
|
||||
}
|
||||
Self(out)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! impl_ext {
|
||||
(
|
||||
$(
|
||||
$macro:ident
|
||||
),*
|
||||
|
||||
) => {
|
||||
$(
|
||||
$macro! {
|
||||
XrExtensions;
|
||||
almalence_digital_lens_control,
|
||||
bd_controller_interaction,
|
||||
epic_view_configuration_fov,
|
||||
ext_performance_settings,
|
||||
ext_thermal_query,
|
||||
ext_debug_utils,
|
||||
ext_eye_gaze_interaction,
|
||||
ext_view_configuration_depth_range,
|
||||
ext_conformance_automation,
|
||||
ext_hand_tracking,
|
||||
#[cfg(windows)]
|
||||
ext_win32_appcontainer_compatible,
|
||||
ext_dpad_binding,
|
||||
ext_hand_joints_motion_range,
|
||||
ext_samsung_odyssey_controller,
|
||||
ext_hp_mixed_reality_controller,
|
||||
ext_palm_pose,
|
||||
ext_uuid,
|
||||
ext_hand_interaction,
|
||||
ext_active_action_set_priority,
|
||||
ext_local_floor,
|
||||
ext_hand_tracking_data_source,
|
||||
ext_plane_detection,
|
||||
fb_composition_layer_image_layout,
|
||||
fb_composition_layer_alpha_blend,
|
||||
#[cfg(target_os = "android")]
|
||||
fb_android_surface_swapchain_create,
|
||||
fb_swapchain_update_state,
|
||||
fb_composition_layer_secure_content,
|
||||
fb_body_tracking,
|
||||
fb_display_refresh_rate,
|
||||
fb_color_space,
|
||||
fb_hand_tracking_mesh,
|
||||
fb_hand_tracking_aim,
|
||||
fb_hand_tracking_capsules,
|
||||
fb_spatial_entity,
|
||||
fb_foveation,
|
||||
fb_foveation_configuration,
|
||||
fb_keyboard_tracking,
|
||||
fb_triangle_mesh,
|
||||
fb_passthrough,
|
||||
fb_render_model,
|
||||
fb_spatial_entity_query,
|
||||
fb_spatial_entity_storage,
|
||||
fb_foveation_vulkan,
|
||||
#[cfg(target_os = "android")]
|
||||
fb_swapchain_update_state_android_surface,
|
||||
fb_swapchain_update_state_opengl_es,
|
||||
fb_swapchain_update_state_vulkan,
|
||||
fb_touch_controller_pro,
|
||||
fb_spatial_entity_sharing,
|
||||
fb_space_warp,
|
||||
fb_haptic_amplitude_envelope,
|
||||
fb_scene,
|
||||
fb_scene_capture,
|
||||
fb_spatial_entity_container,
|
||||
fb_face_tracking,
|
||||
fb_eye_tracking_social,
|
||||
fb_passthrough_keyboard_hands,
|
||||
fb_composition_layer_settings,
|
||||
fb_touch_controller_proximity,
|
||||
fb_haptic_pcm,
|
||||
fb_composition_layer_depth_test,
|
||||
fb_spatial_entity_storage_batch,
|
||||
fb_spatial_entity_user,
|
||||
htc_vive_cosmos_controller_interaction,
|
||||
htc_facial_tracking,
|
||||
htc_vive_focus3_controller_interaction,
|
||||
htc_hand_interaction,
|
||||
htc_vive_wrist_tracker_interaction,
|
||||
htc_passthrough,
|
||||
htc_foveation,
|
||||
huawei_controller_interaction,
|
||||
#[cfg(target_os = "android")]
|
||||
khr_android_thread_settings,
|
||||
#[cfg(target_os = "android")]
|
||||
khr_android_surface_swapchain,
|
||||
khr_composition_layer_cube,
|
||||
#[cfg(target_os = "android")]
|
||||
khr_android_create_instance,
|
||||
khr_composition_layer_depth,
|
||||
khr_vulkan_swapchain_format_list,
|
||||
khr_composition_layer_cylinder,
|
||||
khr_composition_layer_equirect,
|
||||
khr_opengl_enable,
|
||||
khr_opengl_es_enable,
|
||||
khr_vulkan_enable,
|
||||
#[cfg(windows)]
|
||||
khr_d3d11_enable,
|
||||
#[cfg(windows)]
|
||||
khr_d3d12_enable,
|
||||
khr_visibility_mask,
|
||||
khr_composition_layer_color_scale_bias,
|
||||
#[cfg(windows)]
|
||||
khr_win32_convert_performance_counter_time,
|
||||
khr_convert_timespec_time,
|
||||
khr_loader_init,
|
||||
#[cfg(target_os = "android")]
|
||||
khr_loader_init_android,
|
||||
khr_vulkan_enable2,
|
||||
khr_composition_layer_equirect2,
|
||||
khr_binding_modification,
|
||||
khr_swapchain_usage_input_attachment_bit,
|
||||
meta_foveation_eye_tracked,
|
||||
meta_local_dimming,
|
||||
meta_passthrough_preferences,
|
||||
meta_virtual_keyboard,
|
||||
meta_vulkan_swapchain_create_info,
|
||||
meta_performance_metrics,
|
||||
meta_headset_id,
|
||||
meta_passthrough_color_lut,
|
||||
ml_ml2_controller_interaction,
|
||||
ml_frame_end_info,
|
||||
ml_global_dimmer,
|
||||
ml_compat,
|
||||
ml_user_calibration,
|
||||
mnd_headless,
|
||||
mnd_swapchain_usage_input_attachment_bit,
|
||||
msft_unbounded_reference_space,
|
||||
msft_spatial_anchor,
|
||||
msft_spatial_graph_bridge,
|
||||
msft_hand_interaction,
|
||||
msft_hand_tracking_mesh,
|
||||
msft_secondary_view_configuration,
|
||||
msft_first_person_observer,
|
||||
msft_controller_model,
|
||||
#[cfg(windows)]
|
||||
msft_perception_anchor_interop,
|
||||
#[cfg(windows)]
|
||||
msft_holographic_window_attachment,
|
||||
msft_composition_layer_reprojection,
|
||||
msft_spatial_anchor_persistence,
|
||||
#[cfg(target_os = "android")]
|
||||
oculus_android_session_state_enable,
|
||||
oculus_audio_device_guid,
|
||||
oculus_external_camera,
|
||||
oppo_controller_interaction,
|
||||
qcom_tracking_optimization_settings,
|
||||
ultraleap_hand_tracking_forearm,
|
||||
valve_analog_threshold,
|
||||
varjo_quad_views,
|
||||
varjo_foveated_rendering,
|
||||
varjo_composition_layer_depth_test,
|
||||
varjo_environment_depth_estimation,
|
||||
varjo_marker_tracking,
|
||||
varjo_view_offset,
|
||||
yvr_controller_interaction,
|
||||
}
|
||||
)*
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
impl_ext!(bitor, bitand, unavailable_exts);
|
||||
131
crates/bevy_openxr/src/graphics.rs
Normal file
131
crates/bevy_openxr/src/graphics.rs
Normal file
@@ -0,0 +1,131 @@
|
||||
#[cfg(feature = "vulkan")]
|
||||
pub mod vulkan;
|
||||
|
||||
use std::any::TypeId;
|
||||
|
||||
use crate::extensions::XrExtensions;
|
||||
use crate::types::*;
|
||||
|
||||
pub unsafe trait GraphicsExt: openxr::Graphics {
|
||||
/// Wrap the graphics specific type into the [GraphicsWrap] enum
|
||||
fn wrap<T: GraphicsType>(item: T::Inner<Self>) -> GraphicsWrap<T>;
|
||||
/// Convert from wgpu format to the graphics format
|
||||
fn from_wgpu_format(format: wgpu::TextureFormat) -> Option<Self::Format>;
|
||||
/// Convert from the graphics format to wgpu format
|
||||
fn to_wgpu_format(format: Self::Format) -> Option<wgpu::TextureFormat>;
|
||||
/// Initialize graphics for this backend
|
||||
fn init_graphics(
|
||||
app_info: &AppInfo,
|
||||
instance: &openxr::Instance,
|
||||
system_id: openxr::SystemId,
|
||||
) -> Result<(WgpuGraphics, Self::SessionCreateInfo)>;
|
||||
/// Convert a swapchain function
|
||||
unsafe fn to_wgpu_img(
|
||||
image: Self::SwapchainImage,
|
||||
device: &wgpu::Device,
|
||||
format: wgpu::TextureFormat,
|
||||
resolution: UVec2,
|
||||
) -> Result<wgpu::Texture>;
|
||||
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)]
|
||||
pub enum GraphicsWrap<T: GraphicsType> {
|
||||
#[cfg(feature = "vulkan")]
|
||||
Vulkan(T::Inner<openxr::Vulkan>),
|
||||
}
|
||||
|
||||
impl<T: GraphicsType> GraphicsWrap<T> {
|
||||
/// Returns the name of the graphics api this struct is using.
|
||||
pub fn graphics_name(&self) -> &'static str {
|
||||
graphics_match!(
|
||||
self;
|
||||
_ => std::any::type_name::<Api>()
|
||||
)
|
||||
}
|
||||
|
||||
fn graphics_type(&self) -> TypeId {
|
||||
graphics_match!(
|
||||
self;
|
||||
_ => TypeId::of::<Api>()
|
||||
)
|
||||
}
|
||||
|
||||
/// Checks if this struct is using the wanted graphics api.
|
||||
pub fn using_graphics<G: GraphicsExt + 'static>(&self) -> bool {
|
||||
self.graphics_type() == TypeId::of::<G>()
|
||||
}
|
||||
|
||||
/// Checks if the two values are both using the same graphics backend
|
||||
pub fn using_graphics_of_val<V: GraphicsType>(&self, other: &GraphicsWrap<V>) -> bool {
|
||||
self.graphics_type() == other.graphics_type()
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! graphics_match {
|
||||
(
|
||||
$field:expr;
|
||||
$var:pat => $expr:expr $(=> $($return:tt)*)?
|
||||
) => {
|
||||
match $field {
|
||||
#[cfg(feature = "vulkan")]
|
||||
$crate::graphics::GraphicsWrap::Vulkan($var) => {
|
||||
#[allow(unused)]
|
||||
type Api = openxr::Vulkan;
|
||||
graphics_match!(@arm_impl Vulkan; $expr $(=> $($return)*)?)
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
(
|
||||
@arm_impl
|
||||
$variant:ident;
|
||||
$expr:expr => $wrap_ty:ty
|
||||
) => {
|
||||
GraphicsWrap::<$wrap_ty>::$variant($expr)
|
||||
};
|
||||
|
||||
(
|
||||
@arm_impl
|
||||
$variant:ident;
|
||||
$expr:expr
|
||||
) => {
|
||||
$expr
|
||||
};
|
||||
}
|
||||
|
||||
use bevy::math::UVec2;
|
||||
pub(crate) use graphics_match;
|
||||
678
crates/bevy_openxr/src/graphics/vulkan.rs
Normal file
678
crates/bevy_openxr/src/graphics/vulkan.rs
Normal file
@@ -0,0 +1,678 @@
|
||||
use std::ffi::{c_void, CString};
|
||||
|
||||
use ash::vk::Handle;
|
||||
use bevy::log::error;
|
||||
use bevy::math::UVec2;
|
||||
use openxr::Version;
|
||||
use wgpu_hal::api::Vulkan;
|
||||
use wgpu_hal::Api;
|
||||
|
||||
use crate::error::XrError;
|
||||
use crate::extensions::XrExtensions;
|
||||
use crate::types::*;
|
||||
|
||||
use super::GraphicsExt;
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
const VK_TARGET_VERSION: Version = Version::new(1, 2, 0);
|
||||
#[cfg(target_os = "android")]
|
||||
const VK_TARGET_VERSION: Version = Version::new(1, 1, 0);
|
||||
|
||||
const VK_TARGET_VERSION_ASH: u32 = ash::vk::make_api_version(
|
||||
0,
|
||||
VK_TARGET_VERSION.major() as u32,
|
||||
VK_TARGET_VERSION.minor() as u32,
|
||||
VK_TARGET_VERSION.patch() as u32,
|
||||
);
|
||||
|
||||
unsafe impl GraphicsExt for openxr::Vulkan {
|
||||
fn from_wgpu_format(format: wgpu::TextureFormat) -> Option<Self::Format> {
|
||||
wgpu_to_vulkan(format).map(|f| f.as_raw() as _)
|
||||
}
|
||||
|
||||
fn to_wgpu_format(format: Self::Format) -> Option<wgpu::TextureFormat> {
|
||||
vulkan_to_wgpu(ash::vk::Format::from_raw(format as _))
|
||||
}
|
||||
|
||||
fn init_graphics(
|
||||
app_info: &AppInfo,
|
||||
instance: &openxr::Instance,
|
||||
system_id: openxr::SystemId,
|
||||
) -> Result<(WgpuGraphics, Self::SessionCreateInfo)> {
|
||||
let reqs = instance.graphics_requirements::<openxr::Vulkan>(system_id)?;
|
||||
if VK_TARGET_VERSION < reqs.min_api_version_supported
|
||||
|| VK_TARGET_VERSION.major() > reqs.max_api_version_supported.major()
|
||||
{
|
||||
error!(
|
||||
"OpenXR runtime requires Vulkan version > {}, < {}.0.0",
|
||||
reqs.min_api_version_supported,
|
||||
reqs.max_api_version_supported.major() + 1
|
||||
);
|
||||
return Err(XrError::FailedGraphicsRequirements);
|
||||
};
|
||||
let vk_entry = unsafe { ash::Entry::load() }?;
|
||||
let flags = wgpu::InstanceFlags::empty();
|
||||
let extensions =
|
||||
<Vulkan as Api>::Instance::desired_extensions(&vk_entry, VK_TARGET_VERSION_ASH, flags)?;
|
||||
let device_extensions = vec![
|
||||
ash::extensions::khr::Swapchain::name(),
|
||||
ash::extensions::khr::DrawIndirectCount::name(),
|
||||
#[cfg(target_os = "android")]
|
||||
ash::extensions::khr::TimelineSemaphore::name(),
|
||||
];
|
||||
|
||||
let vk_instance = unsafe {
|
||||
let extensions_cchar: Vec<_> = extensions.iter().map(|s| s.as_ptr()).collect();
|
||||
|
||||
let app_name = CString::new(app_info.name.clone().into_owned())?;
|
||||
let vk_app_info = ash::vk::ApplicationInfo::builder()
|
||||
.application_name(&app_name)
|
||||
.application_version(1)
|
||||
.engine_name(&app_name)
|
||||
.engine_version(1)
|
||||
.api_version(VK_TARGET_VERSION_ASH);
|
||||
|
||||
let vk_instance = instance
|
||||
.create_vulkan_instance(
|
||||
system_id,
|
||||
std::mem::transmute(vk_entry.static_fn().get_instance_proc_addr),
|
||||
&ash::vk::InstanceCreateInfo::builder()
|
||||
.application_info(&vk_app_info)
|
||||
.enabled_extension_names(&extensions_cchar) as *const _
|
||||
as *const _,
|
||||
)?
|
||||
.map_err(ash::vk::Result::from_raw)?;
|
||||
|
||||
ash::Instance::load(
|
||||
vk_entry.static_fn(),
|
||||
ash::vk::Instance::from_raw(vk_instance as _),
|
||||
)
|
||||
};
|
||||
|
||||
let vk_instance_ptr = vk_instance.handle().as_raw() as *const c_void;
|
||||
|
||||
let vk_physical_device = ash::vk::PhysicalDevice::from_raw(unsafe {
|
||||
instance.vulkan_graphics_device(system_id, vk_instance.handle().as_raw() as _)? as _
|
||||
});
|
||||
let vk_physical_device_ptr = vk_physical_device.as_raw() as *const c_void;
|
||||
|
||||
let vk_device_properties =
|
||||
unsafe { vk_instance.get_physical_device_properties(vk_physical_device) };
|
||||
|
||||
if vk_device_properties.api_version < VK_TARGET_VERSION_ASH {
|
||||
unsafe { vk_instance.destroy_instance(None) }
|
||||
error!(
|
||||
"Vulkan physical device doesn't support version {}.{}.{}",
|
||||
VK_TARGET_VERSION.major(),
|
||||
VK_TARGET_VERSION.minor(),
|
||||
VK_TARGET_VERSION.patch()
|
||||
);
|
||||
return Err(XrError::FailedGraphicsRequirements);
|
||||
}
|
||||
|
||||
let wgpu_vk_instance = unsafe {
|
||||
<Vulkan as Api>::Instance::from_raw(
|
||||
vk_entry.clone(),
|
||||
vk_instance.clone(),
|
||||
VK_TARGET_VERSION_ASH,
|
||||
0,
|
||||
None,
|
||||
extensions,
|
||||
flags,
|
||||
false,
|
||||
Some(Box::new(())),
|
||||
)?
|
||||
};
|
||||
|
||||
let wgpu_features = wgpu::Features::TEXTURE_ADAPTER_SPECIFIC_FORMAT_FEATURES
|
||||
| wgpu::Features::MULTIVIEW
|
||||
| wgpu::Features::MULTI_DRAW_INDIRECT_COUNT
|
||||
| wgpu::Features::MULTI_DRAW_INDIRECT;
|
||||
|
||||
let Some(wgpu_exposed_adapter) = wgpu_vk_instance.expose_adapter(vk_physical_device) else {
|
||||
error!("WGPU failed to provide an adapter");
|
||||
return Err(XrError::FailedGraphicsRequirements);
|
||||
};
|
||||
|
||||
let enabled_extensions = wgpu_exposed_adapter
|
||||
.adapter
|
||||
.required_device_extensions(wgpu_features);
|
||||
|
||||
let (wgpu_open_device, vk_device_ptr, queue_family_index) = {
|
||||
let extensions_cchar: Vec<_> = device_extensions.iter().map(|s| s.as_ptr()).collect();
|
||||
let mut enabled_phd_features = wgpu_exposed_adapter
|
||||
.adapter
|
||||
.physical_device_features(&enabled_extensions, wgpu_features);
|
||||
let family_index = 0;
|
||||
let family_info = ash::vk::DeviceQueueCreateInfo::builder()
|
||||
.queue_family_index(family_index)
|
||||
.queue_priorities(&[1.0])
|
||||
.build();
|
||||
let family_infos = [family_info];
|
||||
let info = enabled_phd_features
|
||||
.add_to_device_create_builder(
|
||||
ash::vk::DeviceCreateInfo::builder()
|
||||
.queue_create_infos(&family_infos)
|
||||
.push_next(&mut ash::vk::PhysicalDeviceMultiviewFeatures {
|
||||
multiview: ash::vk::TRUE,
|
||||
..Default::default()
|
||||
}),
|
||||
)
|
||||
.enabled_extension_names(&extensions_cchar)
|
||||
.build();
|
||||
let vk_device = unsafe {
|
||||
let vk_device = instance
|
||||
.create_vulkan_device(
|
||||
system_id,
|
||||
std::mem::transmute(vk_entry.static_fn().get_instance_proc_addr),
|
||||
vk_physical_device.as_raw() as _,
|
||||
&info as *const _ as *const _,
|
||||
)?
|
||||
.map_err(ash::vk::Result::from_raw)?;
|
||||
|
||||
ash::Device::load(
|
||||
vk_instance.fp_v1_0(),
|
||||
ash::vk::Device::from_raw(vk_device as _),
|
||||
)
|
||||
};
|
||||
let vk_device_ptr = vk_device.handle().as_raw() as *const c_void;
|
||||
|
||||
let wgpu_open_device = unsafe {
|
||||
wgpu_exposed_adapter.adapter.device_from_raw(
|
||||
vk_device,
|
||||
true,
|
||||
&enabled_extensions,
|
||||
wgpu_features,
|
||||
family_info.queue_family_index,
|
||||
0,
|
||||
)
|
||||
}?;
|
||||
|
||||
(
|
||||
wgpu_open_device,
|
||||
vk_device_ptr,
|
||||
family_info.queue_family_index,
|
||||
)
|
||||
};
|
||||
|
||||
let wgpu_instance =
|
||||
unsafe { wgpu::Instance::from_hal::<wgpu_hal::api::Vulkan>(wgpu_vk_instance) };
|
||||
let wgpu_adapter = unsafe { wgpu_instance.create_adapter_from_hal(wgpu_exposed_adapter) };
|
||||
let (wgpu_device, wgpu_queue) = unsafe {
|
||||
wgpu_adapter.create_device_from_hal(
|
||||
wgpu_open_device,
|
||||
&wgpu::DeviceDescriptor {
|
||||
label: None,
|
||||
required_features: wgpu_features,
|
||||
required_limits: wgpu::Limits {
|
||||
max_bind_groups: 8,
|
||||
max_storage_buffer_binding_size: wgpu_adapter
|
||||
.limits()
|
||||
.max_storage_buffer_binding_size,
|
||||
max_push_constant_size: 4,
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
None,
|
||||
)
|
||||
}?;
|
||||
|
||||
Ok((
|
||||
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,
|
||||
device: vk_device_ptr,
|
||||
queue_family_index,
|
||||
queue_index: 0,
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
unsafe fn to_wgpu_img(
|
||||
color_image: Self::SwapchainImage,
|
||||
device: &wgpu::Device,
|
||||
format: wgpu::TextureFormat,
|
||||
resolution: UVec2,
|
||||
) -> Result<wgpu::Texture> {
|
||||
let color_image = ash::vk::Image::from_raw(color_image);
|
||||
let wgpu_hal_texture = unsafe {
|
||||
<wgpu_hal::vulkan::Api as wgpu_hal::Api>::Device::texture_from_raw(
|
||||
color_image,
|
||||
&wgpu_hal::TextureDescriptor {
|
||||
label: Some("VR Swapchain"),
|
||||
size: wgpu::Extent3d {
|
||||
width: resolution.x,
|
||||
height: resolution.y,
|
||||
depth_or_array_layers: 2,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: format,
|
||||
usage: wgpu_hal::TextureUses::COLOR_TARGET | wgpu_hal::TextureUses::COPY_DST,
|
||||
memory_flags: wgpu_hal::MemoryFlags::empty(),
|
||||
view_formats: vec![],
|
||||
},
|
||||
None,
|
||||
)
|
||||
};
|
||||
let texture = unsafe {
|
||||
device.create_texture_from_hal::<wgpu_hal::vulkan::Api>(
|
||||
wgpu_hal_texture,
|
||||
&wgpu::TextureDescriptor {
|
||||
label: Some("VR Swapchain"),
|
||||
size: wgpu::Extent3d {
|
||||
width: resolution.x,
|
||||
height: resolution.y,
|
||||
depth_or_array_layers: 2,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: format,
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_DST,
|
||||
view_formats: &[],
|
||||
},
|
||||
)
|
||||
};
|
||||
Ok(texture)
|
||||
}
|
||||
|
||||
fn required_exts() -> XrExtensions {
|
||||
let mut extensions = openxr::ExtensionSet::default();
|
||||
extensions.khr_vulkan_enable2 = true;
|
||||
extensions.into()
|
||||
}
|
||||
|
||||
fn wrap<T: super::GraphicsType>(item: T::Inner<Self>) -> super::GraphicsWrap<T> {
|
||||
super::GraphicsWrap::Vulkan(item)
|
||||
}
|
||||
}
|
||||
|
||||
fn vulkan_to_wgpu(format: ash::vk::Format) -> Option<wgpu::TextureFormat> {
|
||||
use ash::vk::Format as F;
|
||||
use wgpu::TextureFormat as Tf;
|
||||
use wgpu::{AstcBlock, AstcChannel};
|
||||
Some(match format {
|
||||
F::R8_UNORM => Tf::R8Unorm,
|
||||
F::R8_SNORM => Tf::R8Snorm,
|
||||
F::R8_UINT => Tf::R8Uint,
|
||||
F::R8_SINT => Tf::R8Sint,
|
||||
F::R16_UINT => Tf::R16Uint,
|
||||
F::R16_SINT => Tf::R16Sint,
|
||||
F::R16_UNORM => Tf::R16Unorm,
|
||||
F::R16_SNORM => Tf::R16Snorm,
|
||||
F::R16_SFLOAT => Tf::R16Float,
|
||||
F::R8G8_UNORM => Tf::Rg8Unorm,
|
||||
F::R8G8_SNORM => Tf::Rg8Snorm,
|
||||
F::R8G8_UINT => Tf::Rg8Uint,
|
||||
F::R8G8_SINT => Tf::Rg8Sint,
|
||||
F::R16G16_UNORM => Tf::Rg16Unorm,
|
||||
F::R16G16_SNORM => Tf::Rg16Snorm,
|
||||
F::R32_UINT => Tf::R32Uint,
|
||||
F::R32_SINT => Tf::R32Sint,
|
||||
F::R32_SFLOAT => Tf::R32Float,
|
||||
F::R16G16_UINT => Tf::Rg16Uint,
|
||||
F::R16G16_SINT => Tf::Rg16Sint,
|
||||
F::R16G16_SFLOAT => Tf::Rg16Float,
|
||||
F::R8G8B8A8_UNORM => Tf::Rgba8Unorm,
|
||||
F::R8G8B8A8_SRGB => Tf::Rgba8UnormSrgb,
|
||||
F::B8G8R8A8_SRGB => Tf::Bgra8UnormSrgb,
|
||||
F::R8G8B8A8_SNORM => Tf::Rgba8Snorm,
|
||||
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,
|
||||
F::R32G32_SINT => Tf::Rg32Sint,
|
||||
F::R32G32_SFLOAT => Tf::Rg32Float,
|
||||
F::R16G16B16A16_UINT => Tf::Rgba16Uint,
|
||||
F::R16G16B16A16_SINT => Tf::Rgba16Sint,
|
||||
F::R16G16B16A16_UNORM => Tf::Rgba16Unorm,
|
||||
F::R16G16B16A16_SNORM => Tf::Rgba16Snorm,
|
||||
F::R16G16B16A16_SFLOAT => Tf::Rgba16Float,
|
||||
F::R32G32B32A32_UINT => Tf::Rgba32Uint,
|
||||
F::R32G32B32A32_SINT => Tf::Rgba32Sint,
|
||||
F::R32G32B32A32_SFLOAT => Tf::Rgba32Float,
|
||||
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,
|
||||
F::BC2_UNORM_BLOCK => Tf::Bc2RgbaUnorm,
|
||||
F::BC2_SRGB_BLOCK => Tf::Bc2RgbaUnormSrgb,
|
||||
F::BC3_UNORM_BLOCK => Tf::Bc3RgbaUnorm,
|
||||
F::BC3_SRGB_BLOCK => Tf::Bc3RgbaUnormSrgb,
|
||||
F::BC4_UNORM_BLOCK => Tf::Bc4RUnorm,
|
||||
F::BC4_SNORM_BLOCK => Tf::Bc4RSnorm,
|
||||
F::BC5_UNORM_BLOCK => Tf::Bc5RgUnorm,
|
||||
F::BC5_SNORM_BLOCK => Tf::Bc5RgSnorm,
|
||||
F::BC6H_UFLOAT_BLOCK => Tf::Bc6hRgbUfloat,
|
||||
F::BC6H_SFLOAT_BLOCK => Tf::Bc6hRgbFloat,
|
||||
F::BC7_UNORM_BLOCK => Tf::Bc7RgbaUnorm,
|
||||
F::BC7_SRGB_BLOCK => Tf::Bc7RgbaUnormSrgb,
|
||||
F::ETC2_R8G8B8_UNORM_BLOCK => Tf::Etc2Rgb8Unorm,
|
||||
F::ETC2_R8G8B8_SRGB_BLOCK => Tf::Etc2Rgb8UnormSrgb,
|
||||
F::ETC2_R8G8B8A1_UNORM_BLOCK => Tf::Etc2Rgb8A1Unorm,
|
||||
F::ETC2_R8G8B8A1_SRGB_BLOCK => Tf::Etc2Rgb8A1UnormSrgb,
|
||||
F::ETC2_R8G8B8A8_UNORM_BLOCK => Tf::Etc2Rgba8Unorm,
|
||||
F::ETC2_R8G8B8A8_SRGB_BLOCK => Tf::Etc2Rgba8UnormSrgb,
|
||||
F::EAC_R11_UNORM_BLOCK => Tf::EacR11Unorm,
|
||||
F::EAC_R11_SNORM_BLOCK => Tf::EacR11Snorm,
|
||||
F::EAC_R11G11_UNORM_BLOCK => Tf::EacRg11Unorm,
|
||||
F::EAC_R11G11_SNORM_BLOCK => Tf::EacRg11Snorm,
|
||||
F::ASTC_4X4_UNORM_BLOCK => Tf::Astc {
|
||||
block: AstcBlock::B4x4,
|
||||
channel: AstcChannel::Unorm,
|
||||
},
|
||||
F::ASTC_5X4_UNORM_BLOCK => Tf::Astc {
|
||||
block: AstcBlock::B5x4,
|
||||
channel: AstcChannel::Unorm,
|
||||
},
|
||||
F::ASTC_5X5_UNORM_BLOCK => Tf::Astc {
|
||||
block: AstcBlock::B5x5,
|
||||
channel: AstcChannel::Unorm,
|
||||
},
|
||||
F::ASTC_6X5_UNORM_BLOCK => Tf::Astc {
|
||||
block: AstcBlock::B6x5,
|
||||
channel: AstcChannel::Unorm,
|
||||
},
|
||||
F::ASTC_6X6_UNORM_BLOCK => Tf::Astc {
|
||||
block: AstcBlock::B6x6,
|
||||
channel: AstcChannel::Unorm,
|
||||
},
|
||||
F::ASTC_8X5_UNORM_BLOCK => Tf::Astc {
|
||||
block: AstcBlock::B8x5,
|
||||
channel: AstcChannel::Unorm,
|
||||
},
|
||||
F::ASTC_8X6_UNORM_BLOCK => Tf::Astc {
|
||||
block: AstcBlock::B8x6,
|
||||
channel: AstcChannel::Unorm,
|
||||
},
|
||||
F::ASTC_8X8_UNORM_BLOCK => Tf::Astc {
|
||||
block: AstcBlock::B8x8,
|
||||
channel: AstcChannel::Unorm,
|
||||
},
|
||||
F::ASTC_10X5_UNORM_BLOCK => Tf::Astc {
|
||||
block: AstcBlock::B10x5,
|
||||
channel: AstcChannel::Unorm,
|
||||
},
|
||||
F::ASTC_10X6_UNORM_BLOCK => Tf::Astc {
|
||||
block: AstcBlock::B10x6,
|
||||
channel: AstcChannel::Unorm,
|
||||
},
|
||||
F::ASTC_10X8_UNORM_BLOCK => Tf::Astc {
|
||||
block: AstcBlock::B10x8,
|
||||
channel: AstcChannel::Unorm,
|
||||
},
|
||||
F::ASTC_10X10_UNORM_BLOCK => Tf::Astc {
|
||||
block: AstcBlock::B10x10,
|
||||
channel: AstcChannel::Unorm,
|
||||
},
|
||||
F::ASTC_12X10_UNORM_BLOCK => Tf::Astc {
|
||||
block: AstcBlock::B12x10,
|
||||
channel: AstcChannel::Unorm,
|
||||
},
|
||||
F::ASTC_12X12_UNORM_BLOCK => Tf::Astc {
|
||||
block: AstcBlock::B12x12,
|
||||
channel: AstcChannel::Unorm,
|
||||
},
|
||||
F::ASTC_4X4_SRGB_BLOCK => Tf::Astc {
|
||||
block: AstcBlock::B4x4,
|
||||
channel: AstcChannel::UnormSrgb,
|
||||
},
|
||||
F::ASTC_5X4_SRGB_BLOCK => Tf::Astc {
|
||||
block: AstcBlock::B5x4,
|
||||
channel: AstcChannel::UnormSrgb,
|
||||
},
|
||||
F::ASTC_5X5_SRGB_BLOCK => Tf::Astc {
|
||||
block: AstcBlock::B5x5,
|
||||
channel: AstcChannel::UnormSrgb,
|
||||
},
|
||||
F::ASTC_6X5_SRGB_BLOCK => Tf::Astc {
|
||||
block: AstcBlock::B6x5,
|
||||
channel: AstcChannel::UnormSrgb,
|
||||
},
|
||||
F::ASTC_6X6_SRGB_BLOCK => Tf::Astc {
|
||||
block: AstcBlock::B6x6,
|
||||
channel: AstcChannel::UnormSrgb,
|
||||
},
|
||||
F::ASTC_8X5_SRGB_BLOCK => Tf::Astc {
|
||||
block: AstcBlock::B8x5,
|
||||
channel: AstcChannel::UnormSrgb,
|
||||
},
|
||||
F::ASTC_8X6_SRGB_BLOCK => Tf::Astc {
|
||||
block: AstcBlock::B8x6,
|
||||
channel: AstcChannel::UnormSrgb,
|
||||
},
|
||||
F::ASTC_8X8_SRGB_BLOCK => Tf::Astc {
|
||||
block: AstcBlock::B8x8,
|
||||
channel: AstcChannel::UnormSrgb,
|
||||
},
|
||||
F::ASTC_10X5_SRGB_BLOCK => Tf::Astc {
|
||||
block: AstcBlock::B10x5,
|
||||
channel: AstcChannel::UnormSrgb,
|
||||
},
|
||||
F::ASTC_10X6_SRGB_BLOCK => Tf::Astc {
|
||||
block: AstcBlock::B10x6,
|
||||
channel: AstcChannel::UnormSrgb,
|
||||
},
|
||||
F::ASTC_10X8_SRGB_BLOCK => Tf::Astc {
|
||||
block: AstcBlock::B10x8,
|
||||
channel: AstcChannel::UnormSrgb,
|
||||
},
|
||||
F::ASTC_10X10_SRGB_BLOCK => Tf::Astc {
|
||||
block: AstcBlock::B10x10,
|
||||
channel: AstcChannel::UnormSrgb,
|
||||
},
|
||||
F::ASTC_12X10_SRGB_BLOCK => Tf::Astc {
|
||||
block: AstcBlock::B12x10,
|
||||
channel: AstcChannel::UnormSrgb,
|
||||
},
|
||||
F::ASTC_12X12_SRGB_BLOCK => Tf::Astc {
|
||||
block: AstcBlock::B12x12,
|
||||
channel: AstcChannel::UnormSrgb,
|
||||
},
|
||||
F::ASTC_4X4_SFLOAT_BLOCK_EXT => Tf::Astc {
|
||||
block: AstcBlock::B4x4,
|
||||
channel: AstcChannel::Hdr,
|
||||
},
|
||||
F::ASTC_5X4_SFLOAT_BLOCK_EXT => Tf::Astc {
|
||||
block: AstcBlock::B5x4,
|
||||
channel: AstcChannel::Hdr,
|
||||
},
|
||||
F::ASTC_5X5_SFLOAT_BLOCK_EXT => Tf::Astc {
|
||||
block: AstcBlock::B5x5,
|
||||
channel: AstcChannel::Hdr,
|
||||
},
|
||||
F::ASTC_6X5_SFLOAT_BLOCK_EXT => Tf::Astc {
|
||||
block: AstcBlock::B6x5,
|
||||
channel: AstcChannel::Hdr,
|
||||
},
|
||||
F::ASTC_6X6_SFLOAT_BLOCK_EXT => Tf::Astc {
|
||||
block: AstcBlock::B6x6,
|
||||
channel: AstcChannel::Hdr,
|
||||
},
|
||||
F::ASTC_8X5_SFLOAT_BLOCK_EXT => Tf::Astc {
|
||||
block: AstcBlock::B8x5,
|
||||
channel: AstcChannel::Hdr,
|
||||
},
|
||||
F::ASTC_8X6_SFLOAT_BLOCK_EXT => Tf::Astc {
|
||||
block: AstcBlock::B8x6,
|
||||
channel: AstcChannel::Hdr,
|
||||
},
|
||||
F::ASTC_8X8_SFLOAT_BLOCK_EXT => Tf::Astc {
|
||||
block: AstcBlock::B8x8,
|
||||
channel: AstcChannel::Hdr,
|
||||
},
|
||||
F::ASTC_10X5_SFLOAT_BLOCK_EXT => Tf::Astc {
|
||||
block: AstcBlock::B10x5,
|
||||
channel: AstcChannel::Hdr,
|
||||
},
|
||||
F::ASTC_10X6_SFLOAT_BLOCK_EXT => Tf::Astc {
|
||||
block: AstcBlock::B10x6,
|
||||
channel: AstcChannel::Hdr,
|
||||
},
|
||||
F::ASTC_10X8_SFLOAT_BLOCK_EXT => Tf::Astc {
|
||||
block: AstcBlock::B10x8,
|
||||
channel: AstcChannel::Hdr,
|
||||
},
|
||||
F::ASTC_10X10_SFLOAT_BLOCK_EXT => Tf::Astc {
|
||||
block: AstcBlock::B10x10,
|
||||
channel: AstcChannel::Hdr,
|
||||
},
|
||||
F::ASTC_12X10_SFLOAT_BLOCK_EXT => Tf::Astc {
|
||||
block: AstcBlock::B12x10,
|
||||
channel: AstcChannel::Hdr,
|
||||
},
|
||||
F::ASTC_12X12_SFLOAT_BLOCK_EXT => Tf::Astc {
|
||||
block: AstcBlock::B12x12,
|
||||
channel: AstcChannel::Hdr,
|
||||
},
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
fn wgpu_to_vulkan(format: wgpu::TextureFormat) -> Option<ash::vk::Format> {
|
||||
// Copied with minor modification from:
|
||||
// https://github.com/gfx-rs/wgpu/blob/a7defb723f856d946d6d220e9897d20dbb7b8f61/wgpu-hal/src/vulkan/conv.rs#L5-L151
|
||||
// license: MIT OR Apache-2.0
|
||||
use ash::vk::Format as F;
|
||||
use wgpu::TextureFormat as Tf;
|
||||
use wgpu::{AstcBlock, AstcChannel};
|
||||
Some(match format {
|
||||
Tf::R8Unorm => F::R8_UNORM,
|
||||
Tf::R8Snorm => F::R8_SNORM,
|
||||
Tf::R8Uint => F::R8_UINT,
|
||||
Tf::R8Sint => F::R8_SINT,
|
||||
Tf::R16Uint => F::R16_UINT,
|
||||
Tf::R16Sint => F::R16_SINT,
|
||||
Tf::R16Unorm => F::R16_UNORM,
|
||||
Tf::R16Snorm => F::R16_SNORM,
|
||||
Tf::R16Float => F::R16_SFLOAT,
|
||||
Tf::Rg8Unorm => F::R8G8_UNORM,
|
||||
Tf::Rg8Snorm => F::R8G8_SNORM,
|
||||
Tf::Rg8Uint => F::R8G8_UINT,
|
||||
Tf::Rg8Sint => F::R8G8_SINT,
|
||||
Tf::Rg16Unorm => F::R16G16_UNORM,
|
||||
Tf::Rg16Snorm => F::R16G16_SNORM,
|
||||
Tf::R32Uint => F::R32_UINT,
|
||||
Tf::R32Sint => F::R32_SINT,
|
||||
Tf::R32Float => F::R32_SFLOAT,
|
||||
Tf::Rg16Uint => F::R16G16_UINT,
|
||||
Tf::Rg16Sint => F::R16G16_SINT,
|
||||
Tf::Rg16Float => F::R16G16_SFLOAT,
|
||||
Tf::Rgba8Unorm => F::R8G8B8A8_UNORM,
|
||||
Tf::Rgba8UnormSrgb => F::R8G8B8A8_SRGB,
|
||||
Tf::Bgra8UnormSrgb => F::B8G8R8A8_SRGB,
|
||||
Tf::Rgba8Snorm => F::R8G8B8A8_SNORM,
|
||||
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,
|
||||
Tf::Rg32Sint => F::R32G32_SINT,
|
||||
Tf::Rg32Float => F::R32G32_SFLOAT,
|
||||
Tf::Rgba16Uint => F::R16G16B16A16_UINT,
|
||||
Tf::Rgba16Sint => F::R16G16B16A16_SINT,
|
||||
Tf::Rgba16Unorm => F::R16G16B16A16_UNORM,
|
||||
Tf::Rgba16Snorm => F::R16G16B16A16_SNORM,
|
||||
Tf::Rgba16Float => F::R16G16B16A16_SFLOAT,
|
||||
Tf::Rgba32Uint => F::R32G32B32A32_UINT,
|
||||
Tf::Rgba32Sint => F::R32G32B32A32_SINT,
|
||||
Tf::Rgba32Float => F::R32G32B32A32_SFLOAT,
|
||||
Tf::Depth32Float => F::D32_SFLOAT,
|
||||
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,
|
||||
Tf::Bc2RgbaUnorm => F::BC2_UNORM_BLOCK,
|
||||
Tf::Bc2RgbaUnormSrgb => F::BC2_SRGB_BLOCK,
|
||||
Tf::Bc3RgbaUnorm => F::BC3_UNORM_BLOCK,
|
||||
Tf::Bc3RgbaUnormSrgb => F::BC3_SRGB_BLOCK,
|
||||
Tf::Bc4RUnorm => F::BC4_UNORM_BLOCK,
|
||||
Tf::Bc4RSnorm => F::BC4_SNORM_BLOCK,
|
||||
Tf::Bc5RgUnorm => F::BC5_UNORM_BLOCK,
|
||||
Tf::Bc5RgSnorm => F::BC5_SNORM_BLOCK,
|
||||
Tf::Bc6hRgbUfloat => F::BC6H_UFLOAT_BLOCK,
|
||||
Tf::Bc6hRgbFloat => F::BC6H_SFLOAT_BLOCK,
|
||||
Tf::Bc7RgbaUnorm => F::BC7_UNORM_BLOCK,
|
||||
Tf::Bc7RgbaUnormSrgb => F::BC7_SRGB_BLOCK,
|
||||
Tf::Etc2Rgb8Unorm => F::ETC2_R8G8B8_UNORM_BLOCK,
|
||||
Tf::Etc2Rgb8UnormSrgb => F::ETC2_R8G8B8_SRGB_BLOCK,
|
||||
Tf::Etc2Rgb8A1Unorm => F::ETC2_R8G8B8A1_UNORM_BLOCK,
|
||||
Tf::Etc2Rgb8A1UnormSrgb => F::ETC2_R8G8B8A1_SRGB_BLOCK,
|
||||
Tf::Etc2Rgba8Unorm => F::ETC2_R8G8B8A8_UNORM_BLOCK,
|
||||
Tf::Etc2Rgba8UnormSrgb => F::ETC2_R8G8B8A8_SRGB_BLOCK,
|
||||
Tf::EacR11Unorm => F::EAC_R11_UNORM_BLOCK,
|
||||
Tf::EacR11Snorm => F::EAC_R11_SNORM_BLOCK,
|
||||
Tf::EacRg11Unorm => F::EAC_R11G11_UNORM_BLOCK,
|
||||
Tf::EacRg11Snorm => F::EAC_R11G11_SNORM_BLOCK,
|
||||
Tf::Astc { block, channel } => match channel {
|
||||
AstcChannel::Unorm => match block {
|
||||
AstcBlock::B4x4 => F::ASTC_4X4_UNORM_BLOCK,
|
||||
AstcBlock::B5x4 => F::ASTC_5X4_UNORM_BLOCK,
|
||||
AstcBlock::B5x5 => F::ASTC_5X5_UNORM_BLOCK,
|
||||
AstcBlock::B6x5 => F::ASTC_6X5_UNORM_BLOCK,
|
||||
AstcBlock::B6x6 => F::ASTC_6X6_UNORM_BLOCK,
|
||||
AstcBlock::B8x5 => F::ASTC_8X5_UNORM_BLOCK,
|
||||
AstcBlock::B8x6 => F::ASTC_8X6_UNORM_BLOCK,
|
||||
AstcBlock::B8x8 => F::ASTC_8X8_UNORM_BLOCK,
|
||||
AstcBlock::B10x5 => F::ASTC_10X5_UNORM_BLOCK,
|
||||
AstcBlock::B10x6 => F::ASTC_10X6_UNORM_BLOCK,
|
||||
AstcBlock::B10x8 => F::ASTC_10X8_UNORM_BLOCK,
|
||||
AstcBlock::B10x10 => F::ASTC_10X10_UNORM_BLOCK,
|
||||
AstcBlock::B12x10 => F::ASTC_12X10_UNORM_BLOCK,
|
||||
AstcBlock::B12x12 => F::ASTC_12X12_UNORM_BLOCK,
|
||||
},
|
||||
AstcChannel::UnormSrgb => match block {
|
||||
AstcBlock::B4x4 => F::ASTC_4X4_SRGB_BLOCK,
|
||||
AstcBlock::B5x4 => F::ASTC_5X4_SRGB_BLOCK,
|
||||
AstcBlock::B5x5 => F::ASTC_5X5_SRGB_BLOCK,
|
||||
AstcBlock::B6x5 => F::ASTC_6X5_SRGB_BLOCK,
|
||||
AstcBlock::B6x6 => F::ASTC_6X6_SRGB_BLOCK,
|
||||
AstcBlock::B8x5 => F::ASTC_8X5_SRGB_BLOCK,
|
||||
AstcBlock::B8x6 => F::ASTC_8X6_SRGB_BLOCK,
|
||||
AstcBlock::B8x8 => F::ASTC_8X8_SRGB_BLOCK,
|
||||
AstcBlock::B10x5 => F::ASTC_10X5_SRGB_BLOCK,
|
||||
AstcBlock::B10x6 => F::ASTC_10X6_SRGB_BLOCK,
|
||||
AstcBlock::B10x8 => F::ASTC_10X8_SRGB_BLOCK,
|
||||
AstcBlock::B10x10 => F::ASTC_10X10_SRGB_BLOCK,
|
||||
AstcBlock::B12x10 => F::ASTC_12X10_SRGB_BLOCK,
|
||||
AstcBlock::B12x12 => F::ASTC_12X12_SRGB_BLOCK,
|
||||
},
|
||||
AstcChannel::Hdr => match block {
|
||||
AstcBlock::B4x4 => F::ASTC_4X4_SFLOAT_BLOCK_EXT,
|
||||
AstcBlock::B5x4 => F::ASTC_5X4_SFLOAT_BLOCK_EXT,
|
||||
AstcBlock::B5x5 => F::ASTC_5X5_SFLOAT_BLOCK_EXT,
|
||||
AstcBlock::B6x5 => F::ASTC_6X5_SFLOAT_BLOCK_EXT,
|
||||
AstcBlock::B6x6 => F::ASTC_6X6_SFLOAT_BLOCK_EXT,
|
||||
AstcBlock::B8x5 => F::ASTC_8X5_SFLOAT_BLOCK_EXT,
|
||||
AstcBlock::B8x6 => F::ASTC_8X6_SFLOAT_BLOCK_EXT,
|
||||
AstcBlock::B8x8 => F::ASTC_8X8_SFLOAT_BLOCK_EXT,
|
||||
AstcBlock::B10x5 => F::ASTC_10X5_SFLOAT_BLOCK_EXT,
|
||||
AstcBlock::B10x6 => F::ASTC_10X6_SFLOAT_BLOCK_EXT,
|
||||
AstcBlock::B10x8 => F::ASTC_10X8_SFLOAT_BLOCK_EXT,
|
||||
AstcBlock::B10x10 => F::ASTC_10X10_SFLOAT_BLOCK_EXT,
|
||||
AstcBlock::B12x10 => F::ASTC_12X10_SFLOAT_BLOCK_EXT,
|
||||
AstcBlock::B12x12 => F::ASTC_12X12_SFLOAT_BLOCK_EXT,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
455
crates/bevy_openxr/src/init.rs
Normal file
455
crates/bevy_openxr/src/init.rs
Normal 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();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
169
crates/bevy_openxr/src/layer_builder.rs
Normal file
169
crates/bevy_openxr/src/layer_builder.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
36
crates/bevy_openxr/src/lib.rs
Normal file
36
crates/bevy_openxr/src/lib.rs
Normal 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)
|
||||
}
|
||||
332
crates/bevy_openxr/src/render.rs
Normal file
332
crates/bevy_openxr/src/render.rs
Normal 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");
|
||||
}
|
||||
281
crates/bevy_openxr/src/resources.rs
Normal file
281
crates/bevy_openxr/src/resources.rs
Normal file
@@ -0,0 +1,281 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::error::XrError;
|
||||
use crate::graphics::*;
|
||||
use crate::layer_builder::CompositionLayer;
|
||||
use crate::types::*;
|
||||
use bevy::prelude::*;
|
||||
use bevy::render::extract_resource::ExtractResource;
|
||||
use openxr::AnyGraphics;
|
||||
|
||||
#[derive(Deref, Clone)]
|
||||
pub struct XrEntry(pub openxr::Entry);
|
||||
|
||||
impl XrEntry {
|
||||
pub fn enumerate_extensions(&self) -> Result<XrExtensions> {
|
||||
Ok(self.0.enumerate_extensions().map(Into::into)?)
|
||||
}
|
||||
|
||||
pub fn create_instance(
|
||||
&self,
|
||||
app_info: AppInfo,
|
||||
exts: XrExtensions,
|
||||
backend: GraphicsBackend,
|
||||
) -> Result<XrInstance> {
|
||||
let available_exts = self.enumerate_extensions()?;
|
||||
|
||||
if !backend.is_available(&available_exts) {
|
||||
return Err(XrError::UnavailableBackend(backend));
|
||||
}
|
||||
|
||||
let required_exts = exts | backend.required_exts();
|
||||
|
||||
let instance = self.0.create_instance(
|
||||
&openxr::ApplicationInfo {
|
||||
application_name: &app_info.name,
|
||||
application_version: app_info.version.to_u32(),
|
||||
engine_name: "Bevy",
|
||||
engine_version: Version::BEVY.to_u32(),
|
||||
},
|
||||
&required_exts.into(),
|
||||
&[],
|
||||
)?;
|
||||
|
||||
Ok(XrInstance(instance, backend, app_info))
|
||||
}
|
||||
|
||||
pub fn available_backends(&self) -> Result<Vec<GraphicsBackend>> {
|
||||
Ok(GraphicsBackend::available_backends(
|
||||
&self.enumerate_extensions()?,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Resource, Deref, Clone)]
|
||||
pub struct XrInstance(
|
||||
#[deref] pub openxr::Instance,
|
||||
pub(crate) GraphicsBackend,
|
||||
pub(crate) AppInfo,
|
||||
);
|
||||
|
||||
impl XrInstance {
|
||||
pub fn init_graphics(
|
||||
&self,
|
||||
system_id: openxr::SystemId,
|
||||
) -> Result<(WgpuGraphics, SessionCreateInfo)> {
|
||||
graphics_match!(
|
||||
self.1;
|
||||
_ => {
|
||||
let (graphics, session_info) = Api::init_graphics(&self.2, &self, system_id)?;
|
||||
|
||||
Ok((graphics, SessionCreateInfo(Api::wrap(session_info))))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// `info` must contain valid handles for the graphics api
|
||||
pub unsafe fn create_session(
|
||||
&self,
|
||||
system_id: openxr::SystemId,
|
||||
info: SessionCreateInfo,
|
||||
) -> Result<(XrSession, XrFrameWaiter, XrFrameStream)> {
|
||||
if !info.0.using_graphics_of_val(&self.1) {
|
||||
return Err(XrError::GraphicsBackendMismatch {
|
||||
item: std::any::type_name::<SessionCreateInfo>(),
|
||||
backend: info.0.graphics_name(),
|
||||
expected_backend: self.1.graphics_name(),
|
||||
});
|
||||
}
|
||||
graphics_match!(
|
||||
info.0;
|
||||
info => {
|
||||
let (session, frame_waiter, frame_stream) = self.0.create_session::<Api>(system_id, &info)?;
|
||||
Ok((session.into(), XrFrameWaiter(frame_waiter), XrFrameStream(Api::wrap(frame_stream))))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SessionCreateInfo(pub(crate) GraphicsWrap<Self>);
|
||||
|
||||
impl GraphicsType for SessionCreateInfo {
|
||||
type Inner<G: GraphicsExt> = G::SessionCreateInfo;
|
||||
}
|
||||
|
||||
impl GraphicsType for XrSession {
|
||||
type Inner<G: GraphicsExt> = openxr::Session<G>;
|
||||
}
|
||||
|
||||
#[derive(Resource, Deref, Clone)]
|
||||
pub struct XrSession(
|
||||
#[deref] pub(crate) openxr::Session<AnyGraphics>,
|
||||
pub(crate) GraphicsWrap<Self>,
|
||||
);
|
||||
|
||||
impl<G: GraphicsExt> From<openxr::Session<G>> for XrSession {
|
||||
fn from(value: openxr::Session<G>) -> Self {
|
||||
Self(value.clone().into_any_graphics(), G::wrap(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl XrSession {
|
||||
pub fn enumerate_swapchain_formats(&self) -> Result<Vec<wgpu::TextureFormat>> {
|
||||
graphics_match!(
|
||||
&self.1;
|
||||
session => Ok(session.enumerate_swapchain_formats()?.into_iter().filter_map(Api::to_wgpu_format).collect())
|
||||
)
|
||||
}
|
||||
|
||||
pub fn create_swapchain(&self, info: SwapchainCreateInfo) -> Result<XrSwapchain> {
|
||||
Ok(XrSwapchain(graphics_match!(
|
||||
&self.1;
|
||||
session => session.create_swapchain(&info.try_into()?)? => XrSwapchain
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Resource)]
|
||||
pub struct XrFrameStream(pub(crate) GraphicsWrap<Self>);
|
||||
|
||||
impl GraphicsType for XrFrameStream {
|
||||
type Inner<G: GraphicsExt> = openxr::FrameStream<G>;
|
||||
}
|
||||
|
||||
impl XrFrameStream {
|
||||
pub fn begin(&mut self) -> openxr::Result<()> {
|
||||
graphics_match!(
|
||||
&mut self.0;
|
||||
stream => stream.begin()
|
||||
)
|
||||
}
|
||||
|
||||
pub fn end(
|
||||
&mut self,
|
||||
display_time: openxr::Time,
|
||||
environment_blend_mode: openxr::EnvironmentBlendMode,
|
||||
layers: &[&dyn CompositionLayer],
|
||||
) -> Result<()> {
|
||||
graphics_match!(
|
||||
&mut self.0;
|
||||
stream => {
|
||||
let mut new_layers = vec![];
|
||||
|
||||
for (i, layer) in layers.into_iter().enumerate() {
|
||||
if let Some(swapchain) = layer.swapchain() {
|
||||
if !swapchain.0.using_graphics::<Api>() {
|
||||
error!(
|
||||
"Composition layer {i} is using graphics api '{}', expected graphics api '{}'. Excluding layer from frame submission.",
|
||||
swapchain.0.graphics_name(),
|
||||
std::any::type_name::<Api>(),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
new_layers.push(unsafe { std::mem::transmute(layer.header()) });
|
||||
}
|
||||
|
||||
Ok(stream.end(display_time, environment_blend_mode, new_layers.as_slice())?)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Resource, Deref, DerefMut)]
|
||||
pub struct XrFrameWaiter(pub openxr::FrameWaiter);
|
||||
|
||||
#[derive(Resource)]
|
||||
pub struct XrSwapchain(pub(crate) GraphicsWrap<Self>);
|
||||
|
||||
impl GraphicsType for XrSwapchain {
|
||||
type Inner<G: GraphicsExt> = openxr::Swapchain<G>;
|
||||
}
|
||||
|
||||
impl XrSwapchain {
|
||||
pub fn acquire_image(&mut self) -> Result<u32> {
|
||||
graphics_match!(
|
||||
&mut self.0;
|
||||
swap => Ok(swap.acquire_image()?)
|
||||
)
|
||||
}
|
||||
|
||||
pub fn wait_image(&mut self, timeout: openxr::Duration) -> Result<()> {
|
||||
graphics_match!(
|
||||
&mut self.0;
|
||||
swap => Ok(swap.wait_image(timeout)?)
|
||||
)
|
||||
}
|
||||
|
||||
pub fn release_image(&mut self) -> Result<()> {
|
||||
graphics_match!(
|
||||
&mut self.0;
|
||||
swap => Ok(swap.release_image()?)
|
||||
)
|
||||
}
|
||||
|
||||
pub fn enumerate_images(
|
||||
&mut self,
|
||||
device: &wgpu::Device,
|
||||
format: wgpu::TextureFormat,
|
||||
resolution: UVec2,
|
||||
) -> Result<XrSwapchainImages> {
|
||||
graphics_match!(
|
||||
&mut self.0;
|
||||
swap => {
|
||||
let mut images = vec![];
|
||||
for image in swap.enumerate_images()? {
|
||||
unsafe {
|
||||
images.push(Api::to_wgpu_img(image, device, format, resolution)?);
|
||||
}
|
||||
}
|
||||
Ok(XrSwapchainImages(images.into()))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deref, Clone, Resource)]
|
||||
pub struct XrStage(pub Arc<openxr::Space>);
|
||||
|
||||
#[derive(Debug, Deref, Resource, Clone)]
|
||||
pub struct XrSwapchainImages(pub Arc<Vec<wgpu::Texture>>);
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Deref, DerefMut, Resource, ExtractResource)]
|
||||
pub struct XrTime(pub openxr::Time);
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Resource)]
|
||||
pub struct XrSwapchainInfo {
|
||||
pub format: wgpu::TextureFormat,
|
||||
pub resolution: UVec2,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Deref, Default, Eq, PartialEq, Ord, PartialOrd, Hash, Resource)]
|
||||
pub struct SystemId(pub openxr::SystemId);
|
||||
|
||||
#[derive(Clone, Copy, Eq, PartialEq, Default, Resource, ExtractResource)]
|
||||
pub struct XrStatus {
|
||||
pub instance_created: bool,
|
||||
pub session_created: bool,
|
||||
pub session_ready: bool,
|
||||
pub session_running: bool,
|
||||
}
|
||||
|
||||
impl XrStatus {
|
||||
pub const UNINITIALIZED: Self = Self {
|
||||
instance_created: false,
|
||||
session_created: false,
|
||||
session_ready: false,
|
||||
session_running: false,
|
||||
};
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Resource)]
|
||||
pub struct XrGraphicsInfo {
|
||||
pub blend_mode: EnvironmentBlendMode,
|
||||
pub resolution: UVec2,
|
||||
pub format: wgpu::TextureFormat,
|
||||
}
|
||||
|
||||
#[derive(Clone, Resource)]
|
||||
pub struct XrViews(pub Vec<Entity>);
|
||||
77
crates/bevy_openxr/src/types.rs
Normal file
77
crates/bevy_openxr/src/types.rs
Normal 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,
|
||||
})
|
||||
}
|
||||
}
|
||||
9
crates/bevy_xr/Cargo.toml
Normal file
9
crates/bevy_xr/Cargo.toml
Normal 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
|
||||
102
crates/bevy_xr/src/camera.rs
Normal file
102
crates/bevy_xr/src/camera.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
2
crates/bevy_xr/src/lib.rs
Normal file
2
crates/bevy_xr/src/lib.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod camera;
|
||||
pub mod session;
|
||||
89
crates/bevy_xr/src/session.rs
Normal file
89
crates/bevy_xr/src/session.rs
Normal 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;
|
||||
Reference in New Issue
Block a user