From 849c3e66775c32fb9e43b0e10d98949770beb082 Mon Sep 17 00:00:00 2001 From: awtterpip Date: Tue, 22 Aug 2023 21:19:41 -0500 Subject: [PATCH] reworked code --- Cargo.toml | 8 ++ examples/xr.rs | 46 +++++++++++ src/input.rs | 55 +++++++++++++ src/lib.rs | 130 ++++++++++++++++++++++-------- src/resource_macros.rs | 58 ++++++++++++++ src/resources.rs | 23 ++++++ src/state.rs | 67 ---------------- srcold/input.rs | 141 ++++++++++++++++++++++++++++++++ srcold/lib.rs | 52 ++++++++++++ srcold/xr/mod.rs | 178 +++++++++++++++++++++++++++++++++++++++++ 10 files changed, 659 insertions(+), 99 deletions(-) create mode 100644 examples/xr.rs create mode 100644 src/input.rs create mode 100644 src/resource_macros.rs create mode 100644 src/resources.rs delete mode 100644 src/state.rs create mode 100644 srcold/input.rs create mode 100644 srcold/lib.rs create mode 100644 srcold/xr/mod.rs diff --git a/Cargo.toml b/Cargo.toml index 6618d76..4c935e7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,8 +6,16 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +anyhow = "1.0.75" bevy = { git = "https://github.com/awtterpip/bevy", default-features = false, features = ["bevy_render"] } openxr = "0.17.1" wgpu = "0.16.0" wgpu-core = "0.16.0" wgpu-hal = "0.16.0" + +[dev-dependencies] +bevy = { git = "https://github.com/awtterpip/bevy" } + +[[example]] +name = "xr" +path = "examples/xr.rs" \ No newline at end of file diff --git a/examples/xr.rs b/examples/xr.rs new file mode 100644 index 0000000..6fd6993 --- /dev/null +++ b/examples/xr.rs @@ -0,0 +1,46 @@ +//! A simple 3D scene with light shining over a cube sitting on a plane. +use bevy_openxr::DefaultXrPlugins; +use bevy::prelude::*; + +fn main() { + App::new() + .add_plugins(DefaultXrPlugins) + .add_systems(Startup, setup) + .run(); +} + +/// set up a simple 3D scene +fn setup( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, +) { + // plane + commands.spawn(PbrBundle { + mesh: meshes.add(shape::Plane::from_size(5.0).into()), + material: materials.add(Color::rgb(0.3, 0.5, 0.3).into()), + ..default() + }); + // cube + commands.spawn(PbrBundle { + mesh: meshes.add(Mesh::from(shape::Cube { size: 1.0 })), + material: materials.add(Color::rgb(0.8, 0.7, 0.6).into()), + transform: Transform::from_xyz(0.0, 0.5, 0.0), + ..default() + }); + // light + commands.spawn(PointLightBundle { + point_light: PointLight { + intensity: 1500.0, + shadows_enabled: true, + ..default() + }, + transform: Transform::from_xyz(4.0, 8.0, 4.0), + ..default() + }); + // camera + commands.spawn(Camera3dBundle { + transform: Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y), + ..default() + }); +} diff --git a/src/input.rs b/src/input.rs new file mode 100644 index 0000000..b35cb38 --- /dev/null +++ b/src/input.rs @@ -0,0 +1,55 @@ +use bevy::prelude::*; +use openxr as xr; + +type XrPose = (Vec3, Quat); + +#[derive(Resource)] +pub struct XrInput { + action_set: xr::ActionSet, + right_action: xr::Action, + left_action: xr::Action, + right_space: xr::Space, + left_space: xr::Space, + stage: xr::Space, +} + +impl XrInput { + pub fn new( + instance: xr::Instance, + session: xr::Session, + ) -> xr::Result { + let action_set = instance.create_action_set("input", "input pose information", 0)?; + let right_action = + action_set.create_action::("right_hand", "Right Hand Controller", &[])?; + let left_action = + action_set.create_action::("left_hand", "Left Hand Controller", &[])?; + instance.suggest_interaction_profile_bindings( + instance.string_to_path("/interaction_profiles/khr/simple_controller")?, + &[ + xr::Binding::new( + &right_action, + instance.string_to_path("/user/hand/right/input/grip/pose")?, + ), + xr::Binding::new( + &left_action, + instance.string_to_path("/user/hand/left/input/grip/pose")?, + ), + ], + )?; + session.attach_action_sets(&[&action_set])?; + let right_space = + right_action.create_space(session.clone(), xr::Path::NULL, xr::Posef::IDENTITY)?; + let left_space = + left_action.create_space(session.clone(), xr::Path::NULL, xr::Posef::IDENTITY)?; + let stage = + session.create_reference_space(xr::ReferenceSpaceType::STAGE, xr::Posef::IDENTITY)?; + Ok(Self { + action_set, + right_action, + left_action, + right_space, + left_space, + stage, + }) + } +} diff --git a/src/lib.rs b/src/lib.rs index f223692..41aa0c2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,52 +1,118 @@ +pub mod input; +pub mod resource_macros; +pub mod resources; + use std::sync::{Arc, Mutex}; use bevy::ecs::system::SystemState; use bevy::prelude::*; use bevy::render::settings::WgpuSettings; -use bevy::render::FutureRendererResources; +use bevy::render::{FutureRendererResources, RenderPlugin, renderer}; use bevy::window::{PrimaryWindow, RawHandleWrapper}; -use state::XrState; +use input::XrInput; +use resources::*; -mod state; - -pub struct OpenXrPlugin { - pub wgpu_settings: WgpuSettings, -} +/// Adds OpenXR support to an App +#[derive(Default)] +pub struct OpenXrPlugin; #[derive(Resource)] -struct FutureXrResources ( - Arc< +pub struct FutureXrResources( + pub Arc< Mutex< - Option< - XrState - > - > - > + Option<( + XrInstance, + XrSession, + XrEnvironmentBlendMode, + XrSessionRunning, + XrFrameWaiter, + XrSwapchain, + XrInput, + )>, + >, + >, ); impl Plugin for OpenXrPlugin { fn build(&self, app: &mut App) { - if let Some(backends) = self.wgpu_settings.backends { - let future_renderer_resources_wrapper = Arc::new(Mutex::new(None)); - let future_xr_resources_wrapper = Arc::new(Mutex::new(None)); - app.insert_resource(FutureRendererResources( - future_renderer_resources_wrapper.clone(), - )); + let future_renderer_resources_wrapper = Arc::new(Mutex::new(None)); + app.insert_resource(FutureRendererResources( + future_renderer_resources_wrapper.clone(), + )); - app.insert_resource(FutureXrResources( - future_xr_resources_wrapper.clone(), - )); + let future_xr_resources_wrapper = Arc::new(Mutex::new(None)); + app.insert_resource(FutureXrResources( + future_xr_resources_wrapper.clone() + )); - let mut system_state: SystemState>> = - SystemState::new(&mut app.world); - let primary_window = system_state.get(&app.world).get_single().ok().cloned(); + let mut system_state: SystemState>> = + SystemState::new(&mut app.world); + let primary_window = system_state.get(&app.world).get_single().ok().cloned(); - let settings = self.wgpu_settings.clone(); - bevy::tasks::IoTaskPool::get() - .spawn_local(async move { - - }) - .detach(); + let settings = WgpuSettings::default(); + bevy::tasks::IoTaskPool::get() + .spawn_local(async move { + let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { + backends: settings.backends.unwrap(), + dx12_shader_compiler: settings.dx12_shader_compiler.clone(), + }); + let surface = primary_window.map(|wrapper| unsafe { + // SAFETY: Plugins should be set up on the main thread. + let handle = wrapper.get_handle(); + instance + .create_surface(&handle) + .expect("Failed to create wgpu surface") + }); + + let request_adapter_options = wgpu::RequestAdapterOptions { + power_preference: settings.power_preference, + compatible_surface: surface.as_ref(), + ..Default::default() + }; + + let (device, queue, adapter_info, render_adapter) = + renderer::initialize_renderer(&instance, &settings, &request_adapter_options) + .await; + debug!("Configured wgpu adapter Limits: {:#?}", device.limits()); + debug!("Configured wgpu adapter Features: {:#?}", device.features()); + let mut future_renderer_resources_inner = + future_renderer_resources_wrapper.lock().unwrap(); + *future_renderer_resources_inner = + Some((device, queue, adapter_info, render_adapter, instance)); + }) + .detach(); + } + + fn ready(&self, app: &App) -> bool { + app.world + .get_resource::() + .and_then(|frr| frr.0.try_lock().map(|locked| locked.is_some()).ok()) + .unwrap_or(true) + } + + fn finish(&self, app: &mut App) { + if let Some(future_renderer_resources) = + app.world.remove_resource::() + { + let (instance, session, blend_mode, session_running, frame_waiter, swapchain, input) = + future_renderer_resources.0.lock().unwrap().take().unwrap(); + + app.insert_resource(instance.clone()) + .insert_resource(session.clone()) + .insert_resource(blend_mode.clone()) + .insert_resource(session_running.clone()) + .insert_resource(frame_waiter.clone()) + .insert_resource(swapchain.clone()); } } } + +pub struct DefaultXrPlugins; + +impl PluginGroup for DefaultXrPlugins { + fn build(self) -> bevy::app::PluginGroupBuilder { + DefaultPlugins + .build() + .add_before::(OpenXrPlugin) + } +} diff --git a/src/resource_macros.rs b/src/resource_macros.rs new file mode 100644 index 0000000..358b6ee --- /dev/null +++ b/src/resource_macros.rs @@ -0,0 +1,58 @@ +#[macro_export] +macro_rules! xr_resource_wrapper { + ($wrapper_type:ident, $xr_type:ty) => { + #[derive(Clone, bevy::prelude::Resource)] + pub struct $wrapper_type($xr_type); + + impl $wrapper_type { + pub fn new(value: $xr_type) -> Self { + Self(value) + } + } + + impl std::ops::Deref for $wrapper_type { + type Target = $xr_type; + + fn deref(&self) -> &Self::Target { + &self.0 + } + } + + impl From<$xr_type> for $wrapper_type { + fn from(value: $xr_type) -> Self { + Self::new(value) + } + } + } +} + +#[macro_export] +macro_rules! xr_arc_resource_wrapper { + ($wrapper_type:ident, $xr_type:ty) => { + #[derive(Clone, bevy::prelude::Resource)] + pub struct $wrapper_type(std::sync::Arc<$xr_type>); + + impl $wrapper_type { + pub fn new(value: $xr_type) -> Self { + Self(std::sync::Arc::new(value)) + } + } + + impl std::ops::Deref for $wrapper_type { + type Target = $xr_type; + + fn deref(&self) -> &Self::Target { + self.0.as_ref() + } + } + + impl From<$xr_type> for $wrapper_type { + fn from(value: $xr_type) -> Self { + Self::new(value) + } + } + } +} + +pub use xr_resource_wrapper; +pub use xr_arc_resource_wrapper; \ No newline at end of file diff --git a/src/resources.rs b/src/resources.rs new file mode 100644 index 0000000..80cb575 --- /dev/null +++ b/src/resources.rs @@ -0,0 +1,23 @@ +use std::sync::Mutex; +use std::sync::atomic::AtomicBool; + +use crate::resource_macros::*; +use openxr as xr; + +xr_resource_wrapper!(XrInstance, xr::Instance); +xr_resource_wrapper!(XrSession, xr::Session); +xr_resource_wrapper!(XrEnvironmentBlendMode, xr::EnvironmentBlendMode); +xr_arc_resource_wrapper!(XrSessionRunning, AtomicBool); +xr_arc_resource_wrapper!(XrFrameWaiter, Mutex); +xr_arc_resource_wrapper!(XrSwapchain, Mutex); + +pub enum Swapchain { + Vulkan(SwapchainInner) +} + +pub struct SwapchainInner { + stream: xr::FrameStream, + handle: xr::Swapchain, + resolution: (u32, u32), + buffers: Vec, +} \ No newline at end of file diff --git a/src/state.rs b/src/state.rs deleted file mode 100644 index b2d287e..0000000 --- a/src/state.rs +++ /dev/null @@ -1,67 +0,0 @@ -use std::sync::atomic::AtomicBool; -use std::sync::Mutex; - -use bevy::prelude::*; -use openxr as xr; -use xr::Result as XrResult; - -#[derive(Resource)] -pub struct XrState { - instance: xr::Instance, - session: xr::Session, - session_running: AtomicBool, - event_buffer: xr::EventDataBuffer, - views: Vec, - graphics: XrGraphics, -} - -enum XrGraphics { - Vulkan(Mutex>) -} - -impl XrGraphics { - fn begin(&self) -> XrResult { - match self { - XrGraphics::Vulkan(inner) => inner.lock().unwrap().begin(), - } - } -} - -struct XrGraphicsInner { - wait: xr::FrameWaiter, - stream: xr::FrameStream, - swapchain: xr::Swapchain, - blend_mode: xr::EnvironmentBlendMode, - resolution: Extent2D, - buffers: Vec, -} - -impl XrGraphicsInner { - fn begin(&mut self) -> XrResult { - let frame_state = self.wait.wait()?; - self.stream.begin()?; - Ok(frame_state) - } - - fn get_render_view(&mut self, layer: u32) -> wgpu::TextureView { - let image_index = self.swapchain.acquire_image().unwrap(); - self.swapchain.wait_image(xr::Duration::INFINITE).unwrap(); - - let texture = &self.buffers[image_index as usize]; - - texture.create_view(&wgpu::TextureViewDescriptor { - dimension: Some(wgpu::TextureViewDimension::D2), - array_layer_count: Some(1), - base_array_layer: layer, - ..Default::default() - }) - } -} - -struct Extent2D { - width: u32, - height: u32, -} - -unsafe impl Sync for XrState {} -unsafe impl Send for XrState {} \ No newline at end of file diff --git a/srcold/input.rs b/srcold/input.rs new file mode 100644 index 0000000..d7022aa --- /dev/null +++ b/srcold/input.rs @@ -0,0 +1,141 @@ +use std::sync::Mutex; + +use glam::{Quat, Vec3}; +use openxr as xr; + +use crate::xr::{VIEW_TYPE, XrPose}; + +#[derive(Clone)] +pub struct PostFrameData { + pub views: Vec, + pub left_hand: Option, + pub right_hand: Option, +} + +pub(crate) struct XrInput { + session: xr::Session, + action_set: xr::ActionSet, + right_action: xr::Action, + left_action: xr::Action, + right_space: xr::Space, + left_space: xr::Space, + stage: xr::Space, + left_hand: Mutex, + right_hand: Mutex, + views: Mutex>, +} + +impl XrInput { + pub(crate) fn new( + instance: xr::Instance, + session: xr::Session, + ) -> anyhow::Result { + let action_set = instance.create_action_set("input", "input pose information", 0)?; + let right_action = + action_set.create_action::("right_hand", "Right Hand Controller", &[])?; + let left_action = + action_set.create_action::("left_hand", "Left Hand Controller", &[])?; + instance.suggest_interaction_profile_bindings( + instance.string_to_path("/interaction_profiles/khr/simple_controller")?, + &[ + xr::Binding::new( + &right_action, + instance.string_to_path("/user/hand/right/input/grip/pose")?, + ), + xr::Binding::new( + &left_action, + instance.string_to_path("/user/hand/left/input/grip/pose")?, + ), + ], + )?; + session.attach_action_sets(&[&action_set])?; + let right_space = + right_action.create_space(session.clone(), xr::Path::NULL, xr::Posef::IDENTITY)?; + let left_space = + left_action.create_space(session.clone(), xr::Path::NULL, xr::Posef::IDENTITY)?; + let stage = + session.create_reference_space(xr::ReferenceSpaceType::STAGE, xr::Posef::IDENTITY)?; + Ok(Self { + left_action, + left_space, + right_action, + right_space, + action_set, + stage, + session, + left_hand: Default::default(), + right_hand: Default::default(), + views: Default::default(), + }) + } + + pub(crate) fn post_frame( + &self, + xr_frame_state: xr::FrameState, + ) -> xr::Result { + self.session.sync_actions(&[(&self.action_set).into()])?; + let locate_hand_pose = |action: &xr::Action, + space: &xr::Space| + -> xr::Result> { + if action.is_active(&self.session, xr::Path::NULL)? { + Ok(Some(openxr_pose_to_glam( + &space + .locate(&self.stage, xr_frame_state.predicted_display_time)? + .pose, + ))) + } else { + Ok(None) + } + }; + + let left_hand = locate_hand_pose(&self.left_action, &self.left_space)?; + let right_hand = locate_hand_pose(&self.right_action, &self.right_space)?; + let (_, views) = self.session.locate_views( + VIEW_TYPE, + xr_frame_state.predicted_display_time, + &self.stage, + )?; + + if let Some(left_hand) = &left_hand { + *self.left_hand.lock().unwrap() = left_hand.clone(); + } + + if let Some(right_hand) = &left_hand { + *self.right_hand.lock().unwrap() = right_hand.clone(); + } + + *self.views.lock().unwrap() = views.iter().map(|f| openxr_pose_to_glam(&f.pose)).collect(); + + Ok(PostFrameData { + views, + left_hand, + right_hand, + }) + } + + pub fn stage(&self) -> &xr::Space { + &self.stage + } + + pub fn left_hand(&self) -> XrPose { + *self.left_hand.lock().unwrap() + } + + pub fn right_hand(&self) -> XrPose { + *self.right_hand.lock().unwrap() + } + + pub fn views(&self) -> Vec { + self.views.lock().unwrap().clone() + } +} + +pub fn openxr_pose_to_glam(pose: &openxr::Posef) -> (Vec3, Quat) { + // with enough sign errors anything is possible + let rotation = { + let o = pose.orientation; + Quat::from_rotation_x(180.0f32.to_radians()) * glam::quat(o.w, o.z, o.y, o.x) + }; + let translation = glam::vec3(-pose.position.x, pose.position.y, -pose.position.z); + (translation, rotation) +} diff --git a/srcold/lib.rs b/srcold/lib.rs new file mode 100644 index 0000000..b4b9b9f --- /dev/null +++ b/srcold/lib.rs @@ -0,0 +1,52 @@ +use std::sync::{Arc, Mutex}; + +use bevy::ecs::system::SystemState; +use bevy::prelude::*; +use bevy::render::settings::WgpuSettings; +use bevy::render::FutureRendererResources; +use bevy::window::{PrimaryWindow, RawHandleWrapper}; + +mod xr; +mod input; + +pub struct OpenXrPlugin { + pub wgpu_settings: WgpuSettings, +} + +#[derive(Resource)] +struct FutureXrResources ( + Arc< + Mutex< + Option< + () + > + > + > +); + +impl Plugin for OpenXrPlugin { + fn build(&self, app: &mut App) { + if let Some(backends) = self.wgpu_settings.backends { + let future_renderer_resources_wrapper = Arc::new(Mutex::new(None)); + let future_xr_resources_wrapper = Arc::new(Mutex::new(None)); + app.insert_resource(FutureRendererResources( + future_renderer_resources_wrapper.clone(), + )); + + app.insert_resource(FutureXrResources( + future_xr_resources_wrapper.clone(), + )); + + let mut system_state: SystemState>> = + SystemState::new(&mut app.world); + let primary_window = system_state.get(&app.world).get_single().ok().cloned(); + + let settings = self.wgpu_settings.clone(); + bevy::tasks::IoTaskPool::get() + .spawn_local(async move { + + }) + .detach(); + } + } +} diff --git a/srcold/xr/mod.rs b/srcold/xr/mod.rs new file mode 100644 index 0000000..94f05c7 --- /dev/null +++ b/srcold/xr/mod.rs @@ -0,0 +1,178 @@ +use std::cell::UnsafeCell; +use std::sync::atomic::AtomicBool; +use std::sync::Mutex; + +use glam::{Quat, Vec3}; +use openxr as xr; + +use crate::input::{PostFrameData, XrInput}; + +pub type XrPose = (Vec3, Quat); +pub const VIEW_TYPE: xr::ViewConfigurationType = xr::ViewConfigurationType::PRIMARY_STEREO; + +pub enum XrState { + Vulkan(XrStateInner), +} + +pub struct XrStateInner { + instance: xr::Instance, + session: xr::Session, + session_running: AtomicBool, + frame: Mutex>, + frame_state: Mutex>, + post_frame_data: Mutex>, + event_buffer: UnsafeCell, + input: XrInput, +} + +unsafe impl Sync for XrStateInner {} +unsafe impl Send for XrStateInner {} + +impl XrStateInner { + pub fn preframe(&self) -> xr::Result<()> { + let event_buffer = unsafe { &mut *self.event_buffer.get() }; + while let Some(event) = self.instance.poll_event(event_buffer)? { + use xr::Event::*; + match event { + SessionStateChanged(e) => { + // Session state change is where we can begin and end sessions, as well as + // find quit messages! + match e.state() { + xr::SessionState::READY => { + self.session + .begin(VIEW_TYPE)?; // TODO! support other view types + self.session_running + .store(true, std::sync::atomic::Ordering::Relaxed); + } + xr::SessionState::STOPPING => { + self.session.end()?; + self.session_running + .store(false, std::sync::atomic::Ordering::Relaxed); + } + xr::SessionState::EXITING | xr::SessionState::LOSS_PENDING => { + *self.frame_state.lock().unwrap() = None; + return Ok(()); + } + _ => {} + } + } + InstanceLossPending(_) => { + *self.frame_state.lock().unwrap() = None; + return Ok(()); + } + EventsLost(e) => {} + _ => {} + } + } + if !self + .session_running + .load(std::sync::atomic::Ordering::Relaxed) + { + // Don't grind up the CPU + std::thread::sleep(std::time::Duration::from_millis(10)); + *self.frame_state.lock().unwrap() = None; + return Ok(()); + } + + *self.frame_state.lock().unwrap() = Some(self.frame.lock().unwrap().begin()?); + + Ok(()) + } + + pub fn post_frame(&self) -> xr::Result<(wgpu::TextureView, wgpu::TextureView)> { + *self.post_frame_data.lock().unwrap() = Some(self.input.post_frame(self.frame_state.lock().unwrap().unwrap().clone())?); + Ok(self.frame.lock().unwrap().get_render_views()) + } + + pub fn post_queue_submit(&self) -> xr::Result<()> { + let pfd = self.post_frame_data.lock().unwrap(); + self.frame.lock().unwrap().post_queue_submit(self.frame_state.lock().unwrap().unwrap().clone(), &(*pfd).clone().unwrap().views, self.input.stage()) + } +} + +pub struct FrameInner { + waiter: xr::FrameWaiter, + stream: xr::FrameStream, + blend_mode: xr::EnvironmentBlendMode, + views: Vec, + swapchain: xr::Swapchain, + resolution: Extent2D, + buffers: Vec, +} + +impl FrameInner { + fn begin(&mut self) -> xr::Result { + let frame_state = self.waiter.wait()?; + self.stream.begin()?; + Ok(frame_state) + } + + fn get_render_views(&mut self) -> (wgpu::TextureView, wgpu::TextureView) { + let image_index = self.swapchain.acquire_image().unwrap(); + self.swapchain.wait_image(xr::Duration::INFINITE).unwrap(); + + let texture = &self.buffers[image_index as usize]; + + ( + texture.create_view(&wgpu::TextureViewDescriptor { + dimension: Some(wgpu::TextureViewDimension::D2), + array_layer_count: Some(1), + base_array_layer: 0, + ..Default::default() + }), + texture.create_view(&wgpu::TextureViewDescriptor { + dimension: Some(wgpu::TextureViewDimension::D2), + array_layer_count: Some(1), + base_array_layer: 1, + ..Default::default() + }), + ) + } + + fn post_queue_submit( + &mut self, + xr_frame_state: xr::FrameState, + views: &[openxr::View], + stage: &xr::Space, + ) -> xr::Result<()> { + self.swapchain.release_image()?; + let rect = xr::Rect2Di { + offset: xr::Offset2Di { x: 0, y: 0 }, + extent: xr::Extent2Di { + width: self.resolution.width as _, + height: self.resolution.height as _, + }, + }; + self.stream.end( + xr_frame_state.predicted_display_time, + self.blend_mode, + &[&xr::CompositionLayerProjection::new().space(stage).views(&[ + xr::CompositionLayerProjectionView::new() + .pose(views[0].pose) + .fov(views[0].fov) + .sub_image( + xr::SwapchainSubImage::new() + .swapchain(&self.swapchain) + .image_array_index(0) + .image_rect(rect), + ), + xr::CompositionLayerProjectionView::new() + .pose(views[1].pose) + .fov(views[1].fov) + .sub_image( + xr::SwapchainSubImage::new() + .swapchain(&self.swapchain) + .image_array_index(1) + .image_rect(rect), + ), + ])], + )?; + + Ok(()) + } +} + +pub struct Extent2D { + pub width: u32, + pub height: u32, +}