use super::frame::XrFrameContext;
use super::input::XrInput;
use ash::vk::{self, Handle};
use openxr as xr;
use std::ffi::{CString, c_void};
const VK_TARGET_VERSION: xr::Version = xr::Version::new(1, 1, 0);
const VK_TARGET_VERSION_ASH: u32 = vk::make_api_version(
0,
VK_TARGET_VERSION.major() as u32,
VK_TARGET_VERSION.minor() as u32,
VK_TARGET_VERSION.patch(),
);
pub struct XrContext {
_vk_entry: ash::Entry,
_vk_instance: ash::Instance,
instance: xr::Instance,
_system: xr::SystemId,
session: xr::Session<xr::Vulkan>,
frame_wait: xr::FrameWaiter,
frame_stream: xr::FrameStream<xr::Vulkan>,
stage: xr::Space,
swapchain: xr::Swapchain<xr::Vulkan>,
swapchain_buffers: Vec<wgpu::Texture>,
resolution: (u32, u32),
_views: Vec<xr::ViewConfigurationView>,
action_set: xr::ActionSet,
move_action: xr::Action<xr::Vector2f>,
turn_action: xr::Action<xr::Vector2f>,
_left_hand_action: xr::Action<xr::Posef>,
_right_hand_action: xr::Action<xr::Posef>,
left_trigger_action: xr::Action<f32>,
right_trigger_action: xr::Action<f32>,
left_grip_action: xr::Action<f32>,
right_grip_action: xr::Action<f32>,
a_button_action: xr::Action<bool>,
b_button_action: xr::Action<bool>,
left_thumbstick_click_action: xr::Action<bool>,
left_hand_space: xr::Space,
right_hand_space: xr::Space,
player_position: nalgebra_glm::Vec3,
player_yaw: f32,
session_running: bool,
device: wgpu::Device,
queue: wgpu::Queue,
}
impl XrContext {
pub fn new() -> Result<Self, Box<dyn std::error::Error>> {
let xr_entry = xr::Entry::linked();
let mut required_extensions = xr::ExtensionSet::default();
required_extensions.khr_vulkan_enable2 = true;
let xr_instance = xr_entry.create_instance(
&xr::ApplicationInfo {
application_name: "nightshade-xr",
application_version: 1,
engine_name: "nightshade",
engine_version: 1,
api_version: xr::Version::new(1, 0, 0),
},
&required_extensions,
&[],
)?;
let system = xr_instance.system(xr::FormFactor::HEAD_MOUNTED_DISPLAY)?;
let views = xr_instance.enumerate_view_configuration_views(
system,
xr::ViewConfigurationType::PRIMARY_STEREO,
)?;
let resolution = (
views[0].recommended_image_rect_width,
views[0].recommended_image_rect_height,
);
let reqs = xr_instance.graphics_requirements::<xr::Vulkan>(system)?;
if VK_TARGET_VERSION < reqs.min_api_version_supported
|| VK_TARGET_VERSION.major() > reqs.max_api_version_supported.major()
{
return Err(format!(
"OpenXR runtime requires Vulkan version > {}, < {}.0.0",
reqs.min_api_version_supported,
reqs.max_api_version_supported.major() + 1
)
.into());
}
let vk_entry = unsafe { ash::Entry::load()? };
let flags = wgpu::InstanceFlags::empty();
let instance_exts = <wgpu_hal::vulkan::Api as wgpu_hal::Api>::Instance::desired_extensions(
&vk_entry,
VK_TARGET_VERSION_ASH,
flags,
)?;
let vk_instance = unsafe {
let extensions_cchar: Vec<_> = instance_exts.iter().map(|s| s.as_ptr()).collect();
let app_name = CString::new("nightshade-xr")?;
let vk_app_info = vk::ApplicationInfo::default()
.application_name(&app_name)
.application_version(1)
.engine_name(&app_name)
.engine_version(1)
.api_version(VK_TARGET_VERSION_ASH);
let get_instance_proc_addr: unsafe extern "system" fn(
*const c_void,
*const i8,
) -> Option<
unsafe extern "system" fn(),
> = std::mem::transmute(vk_entry.static_fn().get_instance_proc_addr);
let vk_instance = xr_instance
.create_vulkan_instance(
system,
get_instance_proc_addr,
&vk::InstanceCreateInfo::default()
.application_info(&vk_app_info)
.enabled_extension_names(&extensions_cchar) as *const _
as *const _,
)?
.map_err(vk::Result::from_raw)?;
ash::Instance::load(
vk_entry.static_fn(),
vk::Instance::from_raw(vk_instance as _),
)
};
let vk_physical_device = vk::PhysicalDevice::from_raw(unsafe {
xr_instance.vulkan_graphics_device(system, vk_instance.handle().as_raw() as _)? as _
});
let vk_instance_ptr = vk_instance.handle().as_raw() as *const c_void;
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 {
return Err("Vulkan physical device doesn't support version 1.1".into());
}
let wgpu_vk_instance = unsafe {
<wgpu_hal::vulkan::Api as wgpu_hal::Api>::Instance::from_raw(
vk_entry.clone(),
vk_instance.clone(),
vk_device_properties.api_version,
0,
None,
instance_exts,
flags,
wgpu::MemoryBudgetThresholds::default(),
false,
Some(Box::new(|| {})),
)?
};
let wgpu_exposed_adapter = wgpu_vk_instance
.expose_adapter(vk_physical_device)
.ok_or("Failed to expose adapter")?;
let adapter_features = wgpu_exposed_adapter.features;
let desired_features =
wgpu::Features::INDIRECT_FIRST_INSTANCE | wgpu::Features::MULTI_DRAW_INDIRECT_COUNT;
let wgpu_features = adapter_features & desired_features;
let mut enabled_extensions = wgpu_exposed_adapter
.adapter
.required_device_extensions(wgpu_features);
if !enabled_extensions.contains(&ash::khr::swapchain::NAME) {
enabled_extensions.push(ash::khr::swapchain::NAME);
}
let (wgpu_open_device, vk_device_ptr, queue_family_index) = {
let extensions_cchar: Vec<_> = enabled_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 queue_priorities = [1.0_f32];
let family_info = vk::DeviceQueueCreateInfo::default()
.queue_family_index(family_index)
.queue_priorities(&queue_priorities);
let family_infos = [family_info];
let info = enabled_phd_features
.add_to_device_create(
vk::DeviceCreateInfo::default().queue_create_infos(&family_infos),
)
.enabled_extension_names(&extensions_cchar);
let vk_device = unsafe {
let get_instance_proc_addr: unsafe extern "system" fn(
*const c_void,
*const i8,
) -> Option<
unsafe extern "system" fn(),
> = std::mem::transmute(vk_entry.static_fn().get_instance_proc_addr);
let vk_device = xr_instance
.create_vulkan_device(
system,
get_instance_proc_addr,
vk_physical_device.as_raw() as _,
&info as *const _ as *const _,
)?
.map_err(vk::Result::from_raw)?;
ash::Device::load(vk_instance.fp_v1_0(), 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,
None,
&enabled_extensions,
wgpu_features,
&wgpu::Limits::default(),
&wgpu::MemoryHints::Performance,
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 adapter_limits = wgpu_adapter.limits();
let limits = adapter_limits;
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: limits,
memory_hints: wgpu::MemoryHints::Performance,
experimental_features: wgpu::ExperimentalFeatures::disabled(),
trace: wgpu::Trace::Off,
},
)
}?;
let (session, frame_wait, frame_stream) = unsafe {
xr_instance.create_session::<xr::Vulkan>(
system,
&xr::vulkan::SessionCreateInfo {
instance: vk_instance_ptr,
physical_device: vk_physical_device_ptr,
device: vk_device_ptr,
queue_family_index,
queue_index: 0,
},
)
}?;
let action_set = xr_instance.create_action_set("gameplay", "Gameplay Actions", 0)?;
let move_action = action_set.create_action::<xr::Vector2f>("move", "Move", &[])?;
let turn_action = action_set.create_action::<xr::Vector2f>("turn", "Turn", &[])?;
let left_hand_action =
action_set.create_action::<xr::Posef>("left_hand_pose", "Left Hand Pose", &[])?;
let right_hand_action =
action_set.create_action::<xr::Posef>("right_hand_pose", "Right Hand Pose", &[])?;
let left_trigger_action =
action_set.create_action::<f32>("left_trigger", "Left Trigger", &[])?;
let right_trigger_action =
action_set.create_action::<f32>("right_trigger", "Right Trigger", &[])?;
let left_grip_action = action_set.create_action::<f32>("left_grip", "Left Grip", &[])?;
let right_grip_action = action_set.create_action::<f32>("right_grip", "Right Grip", &[])?;
let a_button_action = action_set.create_action::<bool>("a_button", "A Button", &[])?;
let b_button_action = action_set.create_action::<bool>("b_button", "B Button", &[])?;
let left_thumbstick_click_action = action_set.create_action::<bool>(
"left_thumbstick_click",
"Left Thumbstick Click",
&[],
)?;
xr_instance.suggest_interaction_profile_bindings(
xr_instance.string_to_path("/interaction_profiles/oculus/touch_controller")?,
&[
xr::Binding::new(
&move_action,
xr_instance.string_to_path("/user/hand/left/input/thumbstick")?,
),
xr::Binding::new(
&turn_action,
xr_instance.string_to_path("/user/hand/right/input/thumbstick")?,
),
xr::Binding::new(
&left_hand_action,
xr_instance.string_to_path("/user/hand/left/input/grip/pose")?,
),
xr::Binding::new(
&right_hand_action,
xr_instance.string_to_path("/user/hand/right/input/grip/pose")?,
),
xr::Binding::new(
&left_trigger_action,
xr_instance.string_to_path("/user/hand/left/input/trigger/value")?,
),
xr::Binding::new(
&right_trigger_action,
xr_instance.string_to_path("/user/hand/right/input/trigger/value")?,
),
xr::Binding::new(
&left_grip_action,
xr_instance.string_to_path("/user/hand/left/input/squeeze/value")?,
),
xr::Binding::new(
&right_grip_action,
xr_instance.string_to_path("/user/hand/right/input/squeeze/value")?,
),
xr::Binding::new(
&a_button_action,
xr_instance.string_to_path("/user/hand/right/input/a/click")?,
),
xr::Binding::new(
&b_button_action,
xr_instance.string_to_path("/user/hand/right/input/b/click")?,
),
xr::Binding::new(
&left_thumbstick_click_action,
xr_instance.string_to_path("/user/hand/left/input/thumbstick/click")?,
),
],
)?;
session.attach_action_sets(&[&action_set])?;
let left_hand_space =
left_hand_action.create_space(&session, xr::Path::NULL, xr::Posef::IDENTITY)?;
let right_hand_space =
right_hand_action.create_space(&session, xr::Path::NULL, xr::Posef::IDENTITY)?;
let stage =
session.create_reference_space(xr::ReferenceSpaceType::STAGE, xr::Posef::IDENTITY)?;
let swapchain = session.create_swapchain(&xr::SwapchainCreateInfo {
create_flags: xr::SwapchainCreateFlags::EMPTY,
usage_flags: xr::SwapchainUsageFlags::COLOR_ATTACHMENT
| xr::SwapchainUsageFlags::SAMPLED,
format: vk::Format::R8G8B8A8_SRGB.as_raw() as _,
sample_count: 1,
width: resolution.0,
height: resolution.1,
face_count: 1,
array_size: 2,
mip_count: 1,
})?;
let swapchain_images = swapchain.enumerate_images()?;
let swapchain_buffers: Vec<wgpu::Texture> = swapchain_images
.into_iter()
.map(|color_image| {
let color_image = vk::Image::from_raw(color_image);
let wgpu_hal_texture = unsafe {
let hal_dev = wgpu_device
.as_hal::<wgpu_hal::vulkan::Api>()
.ok_or("Failed to get HAL device")?;
hal_dev.texture_from_raw(
color_image,
&wgpu_hal::TextureDescriptor {
label: Some("VR Swapchain"),
size: wgpu::Extent3d {
width: resolution.0,
height: resolution.1,
depth_or_array_layers: 2,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8UnormSrgb,
usage: wgpu::TextureUses::COLOR_TARGET | wgpu::TextureUses::COPY_DST,
memory_flags: wgpu_hal::MemoryFlags::empty(),
view_formats: vec![],
},
None,
wgpu_hal::vulkan::TextureMemory::External,
)
};
let texture = unsafe {
wgpu_device.create_texture_from_hal::<wgpu_hal::vulkan::Api>(
wgpu_hal_texture,
&wgpu::TextureDescriptor {
label: Some("VR Swapchain"),
size: wgpu::Extent3d {
width: resolution.0,
height: resolution.1,
depth_or_array_layers: 2,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8UnormSrgb,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT
| wgpu::TextureUsages::COPY_DST,
view_formats: &[],
},
)
};
Ok(texture)
})
.collect::<Result<Vec<_>, Box<dyn std::error::Error>>>()?;
tracing::info!(
"OpenXR session created successfully with resolution {}x{}",
resolution.0,
resolution.1
);
Ok(Self {
_vk_entry: vk_entry,
_vk_instance: vk_instance,
instance: xr_instance,
_system: system,
session,
frame_wait,
frame_stream,
stage,
swapchain,
swapchain_buffers,
resolution,
_views: views,
action_set,
move_action,
turn_action,
_left_hand_action: left_hand_action,
_right_hand_action: right_hand_action,
left_trigger_action,
right_trigger_action,
left_grip_action,
right_grip_action,
a_button_action,
b_button_action,
left_thumbstick_click_action,
left_hand_space,
right_hand_space,
player_position: nalgebra_glm::vec3(0.0, 0.0, 0.0),
player_yaw: 0.0,
session_running: false,
device: wgpu_device,
queue: wgpu_queue,
})
}
pub fn device(&self) -> &wgpu::Device {
&self.device
}
pub fn queue(&self) -> &wgpu::Queue {
&self.queue
}
pub fn resolution(&self) -> (u32, u32) {
self.resolution
}
pub fn surface_format(&self) -> wgpu::TextureFormat {
wgpu::TextureFormat::Rgba8UnormSrgb
}
pub fn set_player_yaw(&mut self, yaw: f32) {
self.player_yaw = yaw;
}
pub fn set_player_position(&mut self, position: nalgebra_glm::Vec3) {
self.player_position = position;
}
pub fn poll_events(&mut self) -> Result<bool, Box<dyn std::error::Error>> {
let mut event_buffer = xr::EventDataBuffer::new();
while let Some(event) = self.instance.poll_event(&mut event_buffer)? {
match event {
xr::Event::SessionStateChanged(state_change) => {
tracing::info!("XR Session state changed to: {:?}", state_change.state());
match state_change.state() {
xr::SessionState::READY => {
self.session
.begin(xr::ViewConfigurationType::PRIMARY_STEREO)?;
self.session_running = true;
tracing::info!("XR Session started");
}
xr::SessionState::STOPPING => {
self.session.end()?;
self.session_running = false;
tracing::info!("XR Session ended");
}
xr::SessionState::EXITING | xr::SessionState::LOSS_PENDING => {
tracing::info!("XR Session exiting");
return Ok(false);
}
_ => {}
}
}
xr::Event::InstanceLossPending(_) => {
tracing::info!("XR Instance loss pending");
return Ok(false);
}
_ => {}
}
}
Ok(true)
}
pub fn wait_frame(&mut self) -> Result<Option<xr::FrameState>, Box<dyn std::error::Error>> {
if !self.session_running {
return Ok(None);
}
Ok(Some(self.frame_wait.wait()?))
}
pub fn update_input(
&mut self,
delta_time: f32,
predicted_display_time: xr::Time,
locomotion_enabled: bool,
locomotion_speed: f32,
) -> Result<XrInput, Box<dyn std::error::Error>> {
self.session.sync_actions(&[(&self.action_set).into()])?;
let move_state = self.move_action.state(&self.session, xr::Path::NULL)?;
let turn_state = self.turn_action.state(&self.session, xr::Path::NULL)?;
let (_, views) = self.session.locate_views(
xr::ViewConfigurationType::PRIMARY_STEREO,
predicted_display_time,
&self.stage,
)?;
let (head_yaw, openxr_quat, head_pose_position) = if !views.is_empty() {
let head_pose = &views[0].pose;
let quat = nalgebra_glm::quat(
head_pose.orientation.w,
head_pose.orientation.z,
head_pose.orientation.y,
head_pose.orientation.x,
);
let head_forward =
nalgebra_glm::quat_rotate_vec3(&quat, &nalgebra_glm::vec3(0.0, 0.0, -1.0));
let yaw = (-head_forward.x).atan2(-head_forward.z);
(yaw, Some(quat), Some(head_pose.position))
} else {
(0.0, None, None)
};
if turn_state.current_state.x.abs() > 0.1 {
let turn_speed = 2.0;
self.player_yaw -= turn_state.current_state.x * turn_speed * delta_time;
}
if locomotion_enabled {
let move_speed = if locomotion_speed > 0.0 {
locomotion_speed
} else {
2.0
};
if move_state.current_state.x.abs() > 0.1 || move_state.current_state.y.abs() > 0.1 {
let combined_yaw = head_yaw + self.player_yaw;
let move_x = move_state.current_state.x;
let move_z = -move_state.current_state.y;
let rotated_x = move_x * combined_yaw.cos() - move_z * combined_yaw.sin();
let rotated_z = move_x * combined_yaw.sin() + move_z * combined_yaw.cos();
self.player_position.x += rotated_x * move_speed * delta_time;
self.player_position.z += rotated_z * move_speed * delta_time;
}
let left_trigger_value = self
.left_trigger_action
.state(&self.session, xr::Path::NULL)
.map(|s| s.current_state)
.unwrap_or(0.0);
let right_trigger_value = self
.right_trigger_action
.state(&self.session, xr::Path::NULL)
.map(|s| s.current_state)
.unwrap_or(0.0);
let vertical_input = right_trigger_value - left_trigger_value;
if vertical_input.abs() > 0.1 {
self.player_position.y += vertical_input * move_speed * delta_time;
}
}
let (head_position, head_orientation) = if let (Some(quat), Some(pos)) =
(openxr_quat, head_pose_position)
{
let local_x = -pos.x;
let local_z = -pos.z;
let rotated_x = local_x * self.player_yaw.cos() + local_z * self.player_yaw.sin();
let rotated_z = -local_x * self.player_yaw.sin() + local_z * self.player_yaw.cos();
let world_head_pos =
self.player_position + nalgebra_glm::vec3(rotated_x, pos.y, rotated_z);
let player_rotation =
nalgebra_glm::quat_angle_axis(self.player_yaw, &nalgebra_glm::vec3(0.0, 1.0, 0.0));
let flip_x = nalgebra_glm::quat_angle_axis(
std::f32::consts::PI,
&nalgebra_glm::vec3(1.0, 0.0, 0.0),
);
let world_head_orientation = player_rotation * flip_x * quat;
(world_head_pos, world_head_orientation)
} else {
(self.player_position, nalgebra_glm::quat_identity())
};
let left_trigger = self
.left_trigger_action
.state(&self.session, xr::Path::NULL)
.map(|s| s.current_state)
.unwrap_or(0.0);
let right_trigger = self
.right_trigger_action
.state(&self.session, xr::Path::NULL)
.map(|s| s.current_state)
.unwrap_or(0.0);
let left_grip = self
.left_grip_action
.state(&self.session, xr::Path::NULL)
.map(|s| s.current_state)
.unwrap_or(0.0);
let right_grip = self
.right_grip_action
.state(&self.session, xr::Path::NULL)
.map(|s| s.current_state)
.unwrap_or(0.0);
let a_button = self
.a_button_action
.state(&self.session, xr::Path::NULL)
.map(|s| s.current_state)
.unwrap_or(false);
let b_button = self
.b_button_action
.state(&self.session, xr::Path::NULL)
.map(|s| s.current_state)
.unwrap_or(false);
let left_thumbstick_click = self
.left_thumbstick_click_action
.state(&self.session, xr::Path::NULL)
.map(|s| s.current_state)
.unwrap_or(false);
let left_hand_pose = self
.left_hand_space
.locate(&self.stage, predicted_display_time)
.ok()
.filter(|loc| {
loc.location_flags.contains(
xr::SpaceLocationFlags::POSITION_VALID
| xr::SpaceLocationFlags::ORIENTATION_VALID,
)
})
.map(|loc| loc.pose);
let right_hand_pose = self
.right_hand_space
.locate(&self.stage, predicted_display_time)
.ok()
.filter(|loc| {
loc.location_flags.contains(
xr::SpaceLocationFlags::POSITION_VALID
| xr::SpaceLocationFlags::ORIENTATION_VALID,
)
})
.map(|loc| loc.pose);
Ok(XrInput {
thumbstick: nalgebra_glm::vec2(move_state.current_state.x, move_state.current_state.y),
right_thumbstick: nalgebra_glm::vec2(
turn_state.current_state.x,
turn_state.current_state.y,
),
left_trigger,
right_trigger,
left_grip,
right_grip,
a_button,
b_button,
left_thumbstick_click,
left_hand_pose,
right_hand_pose,
player_position: self.player_position,
player_yaw: self.player_yaw,
head_yaw,
head_position,
head_orientation,
})
}
pub fn begin_frame(&mut self) -> Result<Option<XrFrameContext>, Box<dyn std::error::Error>> {
let Some(frame_state) = self.wait_frame()? else {
return Ok(None);
};
self.frame_stream.begin()?;
if !frame_state.should_render {
self.frame_stream.end(
frame_state.predicted_display_time,
xr::EnvironmentBlendMode::OPAQUE,
&[],
)?;
return Ok(None);
}
let (view_state_flags, views) = self.session.locate_views(
xr::ViewConfigurationType::PRIMARY_STEREO,
frame_state.predicted_display_time,
&self.stage,
)?;
if !view_state_flags
.contains(xr::ViewStateFlags::POSITION_VALID | xr::ViewStateFlags::ORIENTATION_VALID)
{
self.frame_stream.end(
frame_state.predicted_display_time,
xr::EnvironmentBlendMode::OPAQUE,
&[],
)?;
return Ok(None);
}
let image_index = self.swapchain.acquire_image()?;
self.swapchain.wait_image(xr::Duration::INFINITE)?;
Ok(Some(XrFrameContext {
frame_state,
views,
image_index,
player_position: self.player_position,
player_yaw: self.player_yaw,
}))
}
pub fn get_eye_texture_view(
&self,
frame_ctx: &XrFrameContext,
eye_index: usize,
) -> wgpu::TextureView {
let swapchain_texture = &self.swapchain_buffers[frame_ctx.image_index as usize];
swapchain_texture.create_view(&wgpu::TextureViewDescriptor {
label: Some(&format!("XR Eye {}", eye_index)),
format: Some(wgpu::TextureFormat::Rgba8UnormSrgb),
dimension: Some(wgpu::TextureViewDimension::D2),
aspect: wgpu::TextureAspect::All,
base_mip_level: 0,
mip_level_count: None,
base_array_layer: eye_index as u32,
array_layer_count: Some(1),
usage: None,
})
}
pub fn end_frame(
&mut self,
frame_ctx: XrFrameContext,
) -> Result<(), Box<dyn std::error::Error>> {
self.swapchain.release_image()?;
let rect = xr::Rect2Di {
offset: xr::Offset2Di { x: 0, y: 0 },
extent: xr::Extent2Di {
width: self.resolution.0 as i32,
height: self.resolution.1 as i32,
},
};
let sub_images: Vec<_> = frame_ctx
.views
.iter()
.enumerate()
.map(|(view_index, view)| {
xr::CompositionLayerProjectionView::new()
.pose(view.pose)
.fov(view.fov)
.sub_image(
xr::SwapchainSubImage::new()
.swapchain(&self.swapchain)
.image_array_index(view_index as u32)
.image_rect(rect),
)
})
.collect();
let projection_layer = xr::CompositionLayerProjection::new()
.space(&self.stage)
.views(&sub_images);
self.frame_stream.end(
frame_ctx.frame_state.predicted_display_time,
xr::EnvironmentBlendMode::OPAQUE,
&[&projection_layer],
)?;
Ok(())
}
}