/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ use std::collections::HashMap; use std::ops::Deref; use std::sync::{Arc, Mutex}; use std::time::Duration; use std::{mem, thread}; use euclid::{Point2D, Rect, RigidTransform3D, Rotation3D, Size2D, Transform3D, Vector3D}; use glow::{self as gl, HasContext, PixelUnpackData}; use interaction_profiles::{get_profiles_from_path, get_supported_interaction_profiles}; use log::{error, warn}; use openxr::sys::CompositionLayerPassthroughFB; use openxr::{ self, ActionSet, ActiveActionSet, ApplicationInfo, CompositionLayerBase, CompositionLayerFlags, CompositionLayerProjection, Entry, EnvironmentBlendMode, ExtensionSet, Extent2Di, FormFactor, Fovf, FrameState, FrameStream, FrameWaiter, Graphics, Instance, Passthrough, PassthroughFlagsFB, PassthroughLayer, PassthroughLayerPurposeFB, Posef, Quaternionf, ReferenceSpaceType, SecondaryEndInfo, Session, Space, Swapchain, SwapchainCreateFlags, SwapchainCreateInfo, SwapchainUsageFlags, SystemId, Vector3f, Version, ViewConfigurationType, }; use surfman::{ Context as SurfmanContext, Device as SurfmanDevice, Error as SurfmanError, SurfaceTexture, }; use webxr_api; use webxr_api::util::{self, ClipPlanes}; use webxr_api::{ BaseSpace, Capture, ContextId, DeviceAPI, DiscoveryAPI, Display, Error, Event, EventBuffer, Floor, Frame, GLContexts, InputId, InputSource, LayerGrandManager, LayerId, LayerInit, LayerManager, LayerManagerAPI, LeftEye, Native, Quitter, RightEye, SelectKind, Session as WebXrSession, SessionBuilder, SessionInit, SessionMode, SubImage, SubImages, View, ViewerPose, Viewport, Viewports, Views, Visibility, WebXrSender, }; use crate::SurfmanGL; use crate::gl_utils::GlClearer; mod input; use input::OpenXRInput; mod graphics; mod interaction_profiles; use graphics::{GraphicsProvider, GraphicsProviderMethods}; #[cfg(target_os = "windows")] mod graphics_d3d11; #[cfg(target_os = "windows")] use graphics_d3d11::Backend; const HEIGHT: f32 = 1.4; const IDENTITY_POSE: Posef = Posef { orientation: Quaternionf { x: 0., y: 0., z: 0., w: 1., }, position: Vector3f { x: 0., y: 0., z: 0., }, }; const VIEW_INIT: openxr::View = openxr::View { pose: IDENTITY_POSE, fov: Fovf { angle_left: 0., angle_right: 0., angle_up: 0., angle_down: 0., }, }; // How much to downscale the view capture by. // This is used for performance reasons, to dedicate less texture memory to the camera. // Note that on an HL2 this allocates enough texture memory for "low power" mode, // not "high quality" (in the device portal under // Views > Mixed Reality Capture > Photo and Video Settings). const SECONDARY_VIEW_DOWNSCALE: i32 = 2; /// Provides a way to spawn and interact with context menus pub trait ContextMenuProvider: Send { /// Open a context menu, return a way to poll for the result fn open_context_menu(&self) -> Box; /// Clone self as a trait object fn clone_object(&self) -> Box; } /// A way to poll for the result of the context menu request pub trait ContextMenuFuture { fn poll(&self) -> ContextMenuResult; } /// The result of polling on a context menu request pub enum ContextMenuResult { /// Session should exit ExitSession, /// Dialog was dismissed Dismissed, /// User has not acted on dialog Pending, } #[derive(Default)] pub struct AppInfo { application_name: String, application_version: u32, engine_name: String, engine_version: u32, } impl AppInfo { pub fn new( application_name: &str, application_version: u32, engine_name: &str, engine_version: u32, ) -> AppInfo { Self { application_name: application_name.to_string(), application_version, engine_name: engine_name.to_string(), engine_version, } } } struct ViewInfo { view: openxr::View, extent: Extent2Di, cached_projection: Transform3D, } impl ViewInfo { fn set_view(&mut self, view: openxr::View, clip_planes: ClipPlanes) { self.view.pose = view.pose; if self.view.fov.angle_left != view.fov.angle_left || self.view.fov.angle_right != view.fov.angle_right || self.view.fov.angle_up != view.fov.angle_up || self.view.fov.angle_down != view.fov.angle_down { // It's fine if this happens occasionally, but if this happening very // often we should stop caching warn!("FOV changed, updating projection matrices"); self.view.fov = view.fov; self.recompute_projection(clip_planes); } } fn recompute_projection(&mut self, clip_planes: ClipPlanes) { self.cached_projection = fov_to_projection_matrix(&self.view.fov, clip_planes); } fn view(&self) -> View { View { transform: transform(&self.view.pose), projection: self.cached_projection, } } } pub struct OpenXrDiscovery { context_menu_provider: Option>, app_info: AppInfo, } impl OpenXrDiscovery { pub fn new( context_menu_provider: Option>, app_info: AppInfo, ) -> Self { Self { context_menu_provider, app_info, } } } pub struct CreatedInstance { instance: Instance, supports_hands: bool, supports_secondary: bool, system: SystemId, supports_mutable_fov: bool, supported_interaction_profiles: Vec<&'static str>, supports_passthrough: bool, supports_updating_framerate: bool, } pub fn create_instance( needs_hands: bool, needs_secondary: bool, needs_passthrough: bool, app_info: &AppInfo, ) -> Result { let entry = unsafe { Entry::load().map_err(|e| format!("Entry::load {:?}", e))? }; let supported = entry .enumerate_extensions() .map_err(|e| format!("Entry::enumerate_extensions {:?}", e))?; warn!("Available extensions:\n{:?}", supported); let mut supports_hands = needs_hands && supported.ext_hand_tracking; let supports_passthrough = needs_passthrough && supported.fb_passthrough; let supports_secondary = needs_secondary && supported.msft_secondary_view_configuration && supported.msft_first_person_observer; let supports_updating_framerate = supported.fb_display_refresh_rate; let app_info = ApplicationInfo { application_name: &app_info.application_name, application_version: app_info.application_version, engine_name: &app_info.engine_name, engine_version: app_info.engine_version, api_version: Version::new(1, 0, 36), }; let mut exts = ExtensionSet::default(); GraphicsProvider::enable_graphics_extensions(&mut exts); if supports_hands { exts.ext_hand_tracking = true; } if supports_secondary { exts.msft_secondary_view_configuration = true; exts.msft_first_person_observer = true; } if supports_passthrough { exts.fb_passthrough = true; } if supports_updating_framerate { exts.fb_display_refresh_rate = true; } let supported_interaction_profiles = get_supported_interaction_profiles(&supported, &mut exts); let instance = entry .create_instance(&app_info, &exts, &[]) .map_err(|e| format!("Entry::create_instance {:?}", e))?; let system = instance .system(FormFactor::HEAD_MOUNTED_DISPLAY) .map_err(|e| format!("Instance::system {:?}", e))?; if supports_hands { supports_hands |= instance .supports_hand_tracking(system) .map_err(|e| format!("Instance::supports_hand_tracking {:?}", e))?; } let supports_mutable_fov = { let properties = instance .view_configuration_properties(system, ViewConfigurationType::PRIMARY_STEREO) .map_err(|e| format!("Instance::view_configuration_properties {:?}", e))?; // Unfortunately we need to do a platform check here as just flipping the FOVs for the // composition layer is seemingly no longer sufficient. As long as windows sessions are // solely backed by D3D11, we'll need to apply the same inverted view + reversed winding // fix for SteamVR as well as Oculus. properties.fov_mutable && !cfg!(target_os = "windows") }; Ok(CreatedInstance { instance, supports_hands, supports_secondary, system, supports_mutable_fov, supported_interaction_profiles, supports_passthrough, supports_updating_framerate, }) } impl DiscoveryAPI for OpenXrDiscovery { fn request_session( &mut self, mode: SessionMode, init: &SessionInit, xr: SessionBuilder, ) -> Result { if self.supports_session(mode) { let needs_hands = init.feature_requested("hand-tracking"); let needs_secondary = init.feature_requested("secondary-views") && init.first_person_observer_view; let needs_passthrough = mode == SessionMode::ImmersiveAR; let instance = create_instance( needs_hands, needs_secondary, needs_passthrough, &self.app_info, ) .map_err(|e| Error::BackendSpecific(e))?; let mut supported_features = vec!["local-floor".into(), "bounded-floor".into()]; if instance.supports_hands { supported_features.push("hand-tracking".into()); } if instance.supports_secondary && init.first_person_observer_view { supported_features.push("secondary-views".into()); } let granted_features = init.validate(mode, &supported_features)?; let context_menu_provider = self.context_menu_provider.take(); xr.spawn(move |grand_manager| { OpenXrDevice::new( instance, granted_features, context_menu_provider, grand_manager, ) }) } else { Err(Error::NoMatchingDevice) } } fn supports_session(&self, mode: SessionMode) -> bool { let mut supports = false; // Determining AR support requires enumerating environment blend modes, // but this requires an already created XrInstance and SystemId. // We'll make a "default" instance here to check the blend modes, // then a proper one in request_session with hands/secondary support if needed. let needs_passthrough = mode == SessionMode::ImmersiveAR; if let Ok(instance) = create_instance(false, false, needs_passthrough, &self.app_info) { if let Ok(blend_modes) = instance.instance.enumerate_environment_blend_modes( instance.system, ViewConfigurationType::PRIMARY_STEREO, ) { if mode == SessionMode::ImmersiveAR { supports = blend_modes.contains(&EnvironmentBlendMode::ADDITIVE) || blend_modes.contains(&EnvironmentBlendMode::ALPHA_BLEND) || instance.supports_passthrough; } else if mode == SessionMode::ImmersiveVR { // Immersive VR sessions are not precluded by non-opaque blending supports = blend_modes.len() > 0; } } } supports } } struct OpenXrDevice { session: Arc>, instance: Instance, events: EventBuffer, frame_waiter: FrameWaiter, layer_manager: LayerManager, viewer_space: Space, shared_data: Arc>>, clip_planes: ClipPlanes, supports_secondary: bool, supports_mutable_fov: bool, supports_updating_framerate: bool, // input action_set: ActionSet, right_hand: OpenXRInput, left_hand: OpenXRInput, granted_features: Vec, context_menu_provider: Option>, context_menu_future: Option>, } /// Data that is shared between the openxr thread and the /// layer manager that runs in the webgl thread. struct SharedData { left: ViewInfo, right: ViewInfo, secondary: Option>, secondary_active: bool, primary_blend_mode: EnvironmentBlendMode, secondary_blend_mode: Option, frame_state: Option, space: Space, swapchain_sample_count: u32, } struct OpenXrLayerManager { session: Arc>, shared_data: Arc>>, frame_stream: FrameStream, layers: Vec<(ContextId, LayerId)>, openxr_layers: HashMap, clearer: GlClearer, _passthrough: Option, passthrough_layer: Option, } struct OpenXrLayer { swapchain: Swapchain, depth_stencil_texture: Option, size: Size2D, images: Vec<::SwapchainImage>, surface_textures: Vec>, waited: bool, } impl OpenXrLayerManager { fn new( session: Arc>, shared_data: Arc>>, frame_stream: FrameStream, should_reverse_winding: bool, _passthrough: Option, passthrough_layer: Option, ) -> OpenXrLayerManager { let layers = Vec::new(); let openxr_layers = HashMap::new(); let clearer = GlClearer::new(should_reverse_winding); OpenXrLayerManager { session, shared_data, frame_stream, layers, openxr_layers, clearer, _passthrough, passthrough_layer, } } } impl OpenXrLayer { fn new( swapchain: Swapchain, depth_stencil_texture: Option, size: Size2D, ) -> Result { let images = swapchain .enumerate_images() .map_err(|e| Error::BackendSpecific(format!("Session::enumerate_images {:?}", e)))?; let waited = false; let mut surface_textures = Vec::new(); surface_textures.resize_with(images.len(), || None); Ok(OpenXrLayer { swapchain, depth_stencil_texture, size, images, surface_textures, waited, }) } fn get_surface_texture( &mut self, device: &mut SurfmanDevice, context: &mut SurfmanContext, index: usize, ) -> Result<&SurfaceTexture, SurfmanError> { let result = self .surface_textures .get_mut(index) .ok_or(SurfmanError::Failed)?; if let Some(result) = result { return Ok(result); } let surface_texture = GraphicsProvider::surface_texture_from_swapchain_texture( self.images[index], device, context, &self.size.to_untyped(), )?; *result = Some(surface_texture); result.as_ref().ok_or(SurfmanError::Failed) } } impl LayerManagerAPI for OpenXrLayerManager { fn create_layer( &mut self, device: &mut SurfmanDevice, contexts: &mut dyn GLContexts, context_id: ContextId, init: LayerInit, ) -> Result { let guard = self.shared_data.lock().unwrap(); let data = guard.as_ref().unwrap(); // XXXManishearth should we be doing this, or letting Servo set the format? let formats = self.session.enumerate_swapchain_formats().map_err(|e| { Error::BackendSpecific(format!("Session::enumerate_swapchain_formats {:?}", e)) })?; let format = GraphicsProvider::pick_format(&formats); let texture_size = init.texture_size(&data.viewports()); let sample_count = data.swapchain_sample_count; let swapchain_create_info = SwapchainCreateInfo { create_flags: SwapchainCreateFlags::EMPTY, usage_flags: SwapchainUsageFlags::COLOR_ATTACHMENT | SwapchainUsageFlags::SAMPLED, width: texture_size.width as u32, height: texture_size.height as u32, format, sample_count, face_count: 1, array_size: 1, mip_count: 1, }; let swapchain = self .session .create_swapchain(&swapchain_create_info) .map_err(|e| Error::BackendSpecific(format!("Session::create_swapchain {:?}", e)))?; // TODO: Treat depth and stencil separately? // TODO: Use the openxr API for depth/stencil swap chains? let has_depth_stencil = match init { LayerInit::WebGLLayer { stencil, depth, .. } => stencil | depth, LayerInit::ProjectionLayer { stencil, depth, .. } => stencil | depth, }; let depth_stencil_texture = if has_depth_stencil { let gl = contexts .bindings(device, context_id) .ok_or(Error::NoMatchingDevice)?; unsafe { let depth_stencil_texture = gl.create_texture().ok(); gl.bind_texture(gl::TEXTURE_2D, depth_stencil_texture); gl.tex_image_2d( gl::TEXTURE_2D, 0, gl::DEPTH24_STENCIL8 as _, texture_size.width, texture_size.height, 0, gl::DEPTH_STENCIL, gl::UNSIGNED_INT_24_8, PixelUnpackData::Slice(None), ); depth_stencil_texture } } else { None }; let layer_id = LayerId::default(); let openxr_layer = OpenXrLayer::new(swapchain, depth_stencil_texture, texture_size)?; self.layers.push((context_id, layer_id)); self.openxr_layers.insert(layer_id, openxr_layer); Ok(layer_id) } fn destroy_layer( &mut self, device: &mut SurfmanDevice, contexts: &mut dyn GLContexts, context_id: ContextId, layer_id: LayerId, ) { self.clearer .destroy_layer(device, contexts, context_id, layer_id); self.layers.retain(|&ids| ids != (context_id, layer_id)); if let Some(mut layer) = self.openxr_layers.remove(&layer_id) { if let Some(depth_stencil_texture) = layer.depth_stencil_texture { let gl = contexts.bindings(device, context_id).unwrap(); unsafe { gl.delete_texture(depth_stencil_texture) }; } let mut context = contexts .context(device, context_id) .expect("missing GL context"); for surface_texture in mem::replace(&mut layer.surface_textures, vec![]) { if let Some(surface_texture) = surface_texture { let mut surface = device .destroy_surface_texture(&mut context, surface_texture) .unwrap(); device.destroy_surface(&mut context, &mut surface).unwrap(); } } } } fn layers(&self) -> &[(ContextId, LayerId)] { &self.layers[..] } fn end_frame( &mut self, _device: &mut SurfmanDevice, _contexts: &mut dyn GLContexts, layers: &[(ContextId, LayerId)], ) -> Result<(), Error> { let guard = self.shared_data.lock().unwrap(); let data = guard.as_ref().unwrap(); // At this point the frame contents have been rendered, so we can release access to the texture // in preparation for displaying it. for (_, openxr_layer) in &mut self.openxr_layers { if openxr_layer.waited { openxr_layer.swapchain.release_image().map_err(|e| { Error::BackendSpecific(format!("Session::release_image {:?}", e)) })?; openxr_layer.waited = false; } } let openxr_layers = &self.openxr_layers; // Invert the up/down angles so that openxr flips the texture in the y axis. // Additionally, swap between the L/R views to compensate for inverted up/down FOVs. // This has no effect in runtimes that don't support fovMutable let mut l_fov = data.left.view.fov; let mut r_fov = data.right.view.fov; if cfg!(target_os = "windows") { std::mem::swap(&mut l_fov.angle_up, &mut r_fov.angle_down); std::mem::swap(&mut r_fov.angle_up, &mut l_fov.angle_down); } let viewports = data.viewports(); let primary_views = layers .iter() .filter_map(|&(_, layer_id)| { let openxr_layer = openxr_layers.get(&layer_id)?; Some([ openxr::CompositionLayerProjectionView::new() .pose(data.left.view.pose) .fov(l_fov) .sub_image( openxr::SwapchainSubImage::new() .swapchain(&openxr_layer.swapchain) .image_array_index(0) .image_rect(image_rect(viewports.viewports[0])), ), openxr::CompositionLayerProjectionView::new() .pose(data.right.view.pose) .fov(r_fov) .sub_image( openxr::SwapchainSubImage::new() .swapchain(&openxr_layer.swapchain) .image_array_index(0) .image_rect(image_rect(viewports.viewports[1])), ), ]) }) .collect::>(); let primary_layers = primary_views .iter() .map(|views| { CompositionLayerProjection::new() .space(&data.space) .layer_flags(CompositionLayerFlags::BLEND_TEXTURE_SOURCE_ALPHA) .views(&views[..]) }) .collect::>(); let mut primary_layers = primary_layers .iter() .map(|layer| layer.deref()) .collect::>(); if let Some(passthrough_layer) = &self.passthrough_layer { let clp = CompositionLayerPassthroughFB { ty: CompositionLayerPassthroughFB::TYPE, next: std::ptr::null(), flags: CompositionLayerFlags::BLEND_TEXTURE_SOURCE_ALPHA, space: openxr::sys::Space::from_raw(0), layer_handle: *passthrough_layer.inner(), }; let passthrough_base = &clp as *const _ as *const CompositionLayerBase; unsafe { primary_layers.insert(0, &*passthrough_base); } } if let (Some(secondary), true) = (data.secondary.as_ref(), data.secondary_active) { let mut s_fov = secondary.view.fov; std::mem::swap(&mut s_fov.angle_up, &mut s_fov.angle_down); let secondary_views = layers .iter() .filter_map(|&(_, layer_id)| { let openxr_layer = openxr_layers.get(&layer_id)?; Some([openxr::CompositionLayerProjectionView::new() .pose(secondary.view.pose) .fov(s_fov) .sub_image( openxr::SwapchainSubImage::new() .swapchain(&openxr_layer.swapchain) .image_array_index(0) .image_rect(image_rect(viewports.viewports[2])), )]) }) .collect::>(); let secondary_layers = secondary_views .iter() .map(|views| { CompositionLayerProjection::new() .space(&data.space) .layer_flags(CompositionLayerFlags::BLEND_TEXTURE_SOURCE_ALPHA) .views(&views[..]) }) .collect::>(); let secondary_layers = secondary_layers .iter() .map(|layer| layer.deref()) .collect::>(); self.frame_stream .end_secondary( data.frame_state.as_ref().unwrap().predicted_display_time, data.primary_blend_mode, &primary_layers[..], SecondaryEndInfo { ty: ViewConfigurationType::SECONDARY_MONO_FIRST_PERSON_OBSERVER_MSFT, // XXXManishearth should we use the secondary layer's blend mode here, given // that the content will be using the primary blend mode? environment_blend_mode: data .secondary_blend_mode .unwrap_or(data.primary_blend_mode), layers: &secondary_layers[..], }, ) .map_err(|e| { Error::BackendSpecific(format!("FrameStream::end_secondary {:?}", e)) })?; } else { self.frame_stream .end( data.frame_state.as_ref().unwrap().predicted_display_time, data.primary_blend_mode, &primary_layers[..], ) .map_err(|e| Error::BackendSpecific(format!("FrameStream::end {:?}", e)))?; } Ok(()) } fn begin_frame( &mut self, device: &mut SurfmanDevice, contexts: &mut dyn GLContexts, layers: &[(ContextId, LayerId)], ) -> Result, Error> { let data_guard = self.shared_data.lock().unwrap(); let data = data_guard.as_ref().unwrap(); let openxr_layers = &mut self.openxr_layers; let clearer = &mut self.clearer; self.frame_stream .begin() .map_err(|e| Error::BackendSpecific(format!("FrameStream::begin {:?}", e)))?; layers .iter() .map(|&(context_id, layer_id)| { let context = contexts .context(device, context_id) .ok_or(Error::NoMatchingDevice)?; let openxr_layer = openxr_layers .get_mut(&layer_id) .ok_or(Error::NoMatchingDevice)?; let image = openxr_layer.swapchain.acquire_image().map_err(|e| { Error::BackendSpecific(format!("Swapchain::acquire_image {:?}", e)) })?; openxr_layer .swapchain .wait_image(openxr::Duration::INFINITE) .map_err(|e| { Error::BackendSpecific(format!("Swapchain::wait_image {:?}", e)) })?; openxr_layer.waited = true; let color_surface_texture = openxr_layer .get_surface_texture(device, context, image as usize) .map_err(|e| { Error::BackendSpecific(format!("Layer::get_surface_texture {:?}", e)) })?; let color_texture = device.surface_texture_object(color_surface_texture); let color_target = device.surface_gl_texture_target(); let depth_stencil_texture = openxr_layer.depth_stencil_texture; let texture_array_index = None; let origin = Point2D::new(0, 0); let texture_size = openxr_layer.size; let sub_image = Some(SubImage { color_texture: color_texture.map(|t| t.0), depth_stencil_texture: depth_stencil_texture.map(|t| t.0), texture_array_index, viewport: Rect::new(origin, texture_size), }); let view_sub_images = data .viewports() .viewports .iter() .map(|&viewport| SubImage { color_texture: color_texture.map(|t| t.0), depth_stencil_texture: depth_stencil_texture.map(|t| t.0), texture_array_index, viewport, }) .collect(); clearer.clear( device, contexts, context_id, layer_id, color_texture, color_target, openxr_layer.depth_stencil_texture, ); Ok(SubImages { layer_id, sub_image, view_sub_images, }) }) .collect() } } fn image_rect(viewport: Rect) -> openxr::Rect2Di { openxr::Rect2Di { extent: openxr::Extent2Di { height: viewport.size.height, width: viewport.size.width, }, offset: openxr::Offset2Di { x: viewport.origin.x, y: viewport.origin.y, }, } } impl OpenXrDevice { fn new( instance: CreatedInstance, granted_features: Vec, context_menu_provider: Option>, grand_manager: LayerGrandManager, ) -> Result { let CreatedInstance { instance, supports_hands, supports_secondary, system, supports_mutable_fov, supported_interaction_profiles, supports_passthrough, supports_updating_framerate, } = instance; let (init_tx, init_rx) = crossbeam_channel::unbounded(); let instance_clone = instance.clone(); let shared_data = Arc::new(Mutex::new(None)); let shared_data_clone = shared_data.clone(); let mut data = shared_data.lock().unwrap(); let layer_manager = grand_manager.create_layer_manager(move |device, _| { let (session, frame_waiter, frame_stream) = GraphicsProvider::create_session(device, &instance_clone, system)?; let (passthrough, passthrough_layer) = if supports_passthrough { let flags = PassthroughFlagsFB::IS_RUNNING_AT_CREATION; let purpose = PassthroughLayerPurposeFB::RECONSTRUCTION; let passthrough = session .create_passthrough(flags) .expect("Unable to initialize passthrough"); let passthrough_layer = session .create_passthrough_layer(&passthrough, flags, purpose) .expect("Failed to create passthrough layer"); (Some(passthrough), Some(passthrough_layer)) } else { (None, None) }; let session = Arc::new(session); init_tx .send((session.clone(), frame_waiter)) .map_err(|_| Error::CommunicationError)?; Ok(OpenXrLayerManager::new( session, shared_data_clone, frame_stream, !supports_mutable_fov, passthrough, passthrough_layer, )) })?; let (session, frame_waiter) = init_rx.recv().map_err(|_| Error::CommunicationError)?; // XXXPaul initialisation should happen on SessionStateChanged(Ready)? if supports_secondary { session .begin_with_secondary( ViewConfigurationType::PRIMARY_STEREO, &[ViewConfigurationType::SECONDARY_MONO_FIRST_PERSON_OBSERVER_MSFT], ) .map_err(|e| { Error::BackendSpecific(format!("Session::begin_with_secondary {:?}", e)) })?; } else { session .begin(ViewConfigurationType::PRIMARY_STEREO) .map_err(|e| Error::BackendSpecific(format!("Session::begin {:?}", e)))?; } let pose = Posef { orientation: Quaternionf { x: 0., y: 0., z: 0., w: 1., }, position: Vector3f { x: 0., y: 0., z: 0., }, }; let space = session .create_reference_space(ReferenceSpaceType::LOCAL, pose) .map_err(|e| { Error::BackendSpecific(format!("Session::create_reference_space {:?}", e)) })?; let viewer_space = session .create_reference_space(ReferenceSpaceType::VIEW, pose) .map_err(|e| { Error::BackendSpecific(format!("Session::create_reference_space {:?}", e)) })?; let view_configuration_type = ViewConfigurationType::PRIMARY_STEREO; let view_configurations = instance .enumerate_view_configuration_views(system, view_configuration_type) .map_err(|e| { Error::BackendSpecific(format!( "Session::enumerate_view_configuration_views {:?}", e )) })?; let left_view_configuration = view_configurations[0]; let right_view_configuration = view_configurations[1]; let left_extent = Extent2Di { width: left_view_configuration.recommended_image_rect_width as i32, height: left_view_configuration.recommended_image_rect_height as i32, }; let right_extent = Extent2Di { width: right_view_configuration.recommended_image_rect_width as i32, height: right_view_configuration.recommended_image_rect_height as i32, }; assert_eq!( left_view_configuration.recommended_image_rect_height, right_view_configuration.recommended_image_rect_height, ); let swapchain_sample_count = left_view_configuration.recommended_swapchain_sample_count; let secondary_active = false; let (secondary, secondary_blend_mode) = if supports_secondary { let view_configuration = *instance .enumerate_view_configuration_views( system, ViewConfigurationType::SECONDARY_MONO_FIRST_PERSON_OBSERVER_MSFT, ) .map_err(|e| { Error::BackendSpecific(format!( "Session::enumerate_view_configuration_views {:?}", e )) })? .get(0) .expect( "Session::enumerate_view_configuration_views() returned no secondary views", ); let secondary_blend_mode = instance .enumerate_environment_blend_modes( system, ViewConfigurationType::SECONDARY_MONO_FIRST_PERSON_OBSERVER_MSFT, ) .map_err(|e| { Error::BackendSpecific(format!( "Instance::enumerate_environment_blend_modes {:?}", e )) })?[0]; let secondary_extent = Extent2Di { width: view_configuration.recommended_image_rect_width as i32, height: view_configuration.recommended_image_rect_height as i32, }; let secondary = ViewInfo { view: VIEW_INIT, extent: secondary_extent, cached_projection: Transform3D::identity(), }; (Some(secondary), Some(secondary_blend_mode)) } else { (None, None) }; let primary_blend_mode = instance .enumerate_environment_blend_modes(system, view_configuration_type) .map_err(|e| { Error::BackendSpecific(format!( "Instance::enumerate_environment_blend_modes {:?}", e )) })?[0]; let left = ViewInfo { view: VIEW_INIT, extent: left_extent, cached_projection: Transform3D::identity(), }; let right = ViewInfo { view: VIEW_INIT, extent: right_extent, cached_projection: Transform3D::identity(), }; *data = Some(SharedData { frame_state: None, space, left, right, secondary, secondary_active, primary_blend_mode, secondary_blend_mode, swapchain_sample_count, }); drop(data); let (action_set, right_hand, left_hand) = OpenXRInput::setup_inputs( &instance, &session, supports_hands, supported_interaction_profiles, ); Ok(OpenXrDevice { instance, events: Default::default(), session, frame_waiter, viewer_space, clip_planes: Default::default(), supports_secondary, supports_mutable_fov, supports_updating_framerate, layer_manager, shared_data, action_set, right_hand, left_hand, granted_features, context_menu_provider, context_menu_future: None, }) } fn handle_openxr_events(&mut self) -> bool { use openxr::Event::*; let mut stopped = false; loop { let mut buffer = openxr::EventDataBuffer::new(); let event = match self.instance.poll_event(&mut buffer) { Ok(event) => event, Err(e) => { error!("Error polling events: {:?}", e); return false; }, }; match event { Some(SessionStateChanged(session_change)) => match session_change.state() { openxr::SessionState::EXITING | openxr::SessionState::LOSS_PENDING => { self.events.callback(Event::SessionEnd); return false; }, openxr::SessionState::STOPPING => { self.events .callback(Event::VisibilityChange(Visibility::Hidden)); if let Err(e) = self.session.end() { error!("Session failed to end on STOPPING: {:?}", e); } stopped = true; }, openxr::SessionState::READY if stopped => { self.events .callback(Event::VisibilityChange(Visibility::Visible)); if let Err(e) = self.session.begin(ViewConfigurationType::PRIMARY_STEREO) { error!("Session failed to begin on READY: {:?}", e); } stopped = false; }, openxr::SessionState::FOCUSED => { self.events .callback(Event::VisibilityChange(Visibility::Visible)); }, openxr::SessionState::VISIBLE => { self.events .callback(Event::VisibilityChange(Visibility::VisibleBlurred)); }, _ => { // FIXME: Handle other states }, }, Some(InstanceLossPending(_)) => { self.events.callback(Event::SessionEnd); return false; }, Some(InteractionProfileChanged(_)) => { let path = self.instance.string_to_path("/user/hand/right").unwrap(); let profile_path = self.session.current_interaction_profile(path).unwrap(); let profile = self.instance.path_to_string(profile_path); match profile { Ok(profile) => { let profiles = get_profiles_from_path(profile) .iter() .map(|s| s.to_string()) .collect(); let mut new_left = self.left_hand.input_source(); new_left.profiles.clone_from(&profiles); self.events .callback(Event::UpdateInput(new_left.id, new_left)); let mut new_right = self.right_hand.input_source(); new_right.profiles.clone_from(&profiles); self.events .callback(Event::UpdateInput(new_right.id, new_right)); }, Err(e) => { error!("Failed to get interaction profile: {:?}", e); }, } }, Some(ReferenceSpaceChangePending(e)) => { let base_space = match e.reference_space_type() { ReferenceSpaceType::VIEW => BaseSpace::Viewer, ReferenceSpaceType::LOCAL => BaseSpace::Local, ReferenceSpaceType::LOCAL_FLOOR => BaseSpace::Floor, ReferenceSpaceType::STAGE => BaseSpace::BoundedFloor, _ => unreachable!( "Should not be receiving change events for unsupported space types" ), }; let transform = transform(&e.pose_in_previous_space()); self.events .callback(Event::ReferenceSpaceChanged(base_space, transform)); }, Some(_) => { // FIXME: Handle other events }, None if stopped => { // XXXManishearth be able to handle exits during this time thread::sleep(Duration::from_millis(200)); }, None => { // No more events to process break; }, } } true } } impl SharedData { fn views(&self) -> Views { let left_view = self.left.view(); let right_view = self.right.view(); if let (Some(secondary), true) = (self.secondary.as_ref(), self.secondary_active) { // Note: we report the secondary view only when it is active let third_eye = secondary.view(); return Views::StereoCapture(left_view, right_view, third_eye); } Views::Stereo(left_view, right_view) } fn viewports(&self) -> Viewports { let left_vp = Rect::new( Point2D::zero(), Size2D::new(self.left.extent.width, self.left.extent.height), ); let right_vp = Rect::new( Point2D::new(self.left.extent.width, 0), Size2D::new(self.right.extent.width, self.right.extent.height), ); let mut viewports = vec![left_vp, right_vp]; // Note: we report the secondary viewport even when it is inactive if let Some(ref secondary) = self.secondary { let secondary_vp = Rect::new( Point2D::new(self.left.extent.width + self.right.extent.width, 0), Size2D::new(secondary.extent.width, secondary.extent.height) / SECONDARY_VIEW_DOWNSCALE, ); viewports.push(secondary_vp) } Viewports { viewports } } } impl DeviceAPI for OpenXrDevice { fn floor_transform(&self) -> Option> { let translation = Vector3D::new(0.0, HEIGHT, 0.0); Some(RigidTransform3D::from_translation(translation)) } fn viewports(&self) -> Viewports { self.shared_data .lock() .unwrap() .as_ref() .unwrap() .viewports() } fn create_layer(&mut self, context_id: ContextId, init: LayerInit) -> Result { self.layer_manager.create_layer(context_id, init) } fn destroy_layer(&mut self, context_id: ContextId, layer_id: LayerId) { self.layer_manager.destroy_layer(context_id, layer_id) } fn begin_animation_frame(&mut self, layers: &[(ContextId, LayerId)]) -> Option { if !self.handle_openxr_events() { warn!("no frame, session isn't running"); // Session is not running anymore. return None; } if let Some(ref context_menu_future) = self.context_menu_future { match context_menu_future.poll() { ContextMenuResult::ExitSession => { self.quit(); return None; }, ContextMenuResult::Dismissed => self.context_menu_future = None, ContextMenuResult::Pending => (), } } let (frame_state, secondary_state) = if self.supports_secondary { let (frame_state, secondary_state) = match self.frame_waiter.wait_secondary() { Ok(frame_state) => frame_state, Err(e) => { error!("Error waiting on frame: {:?}", e); return None; }, }; assert_eq!( secondary_state.ty, ViewConfigurationType::SECONDARY_MONO_FIRST_PERSON_OBSERVER_MSFT ); (frame_state, Some(secondary_state)) } else { match self.frame_waiter.wait() { Ok(frame_state) => (frame_state, None), Err(e) => { error!("Error waiting on frame: {:?}", e); return None; }, } }; // We get the subimages before grabbing the lock, // since otherwise we'll deadlock let sub_images = self.layer_manager.begin_frame(layers).ok()?; let mut guard = self.shared_data.lock().unwrap(); let data = guard.as_mut().unwrap(); // XXXManishearth should we check frame_state.should_render? let (_view_flags, mut views) = match self.session.locate_views( ViewConfigurationType::PRIMARY_STEREO, frame_state.predicted_display_time, &data.space, ) { Ok(data) => data, Err(e) => { error!("Error locating views: {:?}", e); return None; }, }; if !self.supports_mutable_fov { views.iter_mut().for_each(|v| { std::mem::swap(&mut v.fov.angle_up, &mut v.fov.angle_down); }); } data.left.set_view(views[0], self.clip_planes); data.right.set_view(views[1], self.clip_planes); let pose = match self .viewer_space .locate(&data.space, frame_state.predicted_display_time) { Ok(pose) => pose, Err(e) => { error!("Error locating viewer space: {:?}", e); return None; }, }; let transform = transform(&pose.pose); if let Some(secondary_state) = secondary_state.as_ref() { data.secondary_active = secondary_state.active; } if let (Some(secondary), true) = (data.secondary.as_mut(), data.secondary_active) { let view = match self.session.locate_views( ViewConfigurationType::SECONDARY_MONO_FIRST_PERSON_OBSERVER_MSFT, frame_state.predicted_display_time, &data.space, ) { Ok(v) => v.1[0], Err(e) => { error!("Error locating views: {:?}", e); return None; }, }; secondary.set_view(view, self.clip_planes); } let active_action_set = ActiveActionSet::new(&self.action_set); if let Err(e) = self.session.sync_actions(&[active_action_set]) { error!("Error syncing actions: {:?}", e); return None; } let mut right = self .right_hand .frame(&self.session, &frame_state, &data.space, &transform); let mut left = self .left_hand .frame(&self.session, &frame_state, &data.space, &transform); data.frame_state = Some(frame_state); let views = data.views(); if let Some(ref context_menu_provider) = self.context_menu_provider { if (left.menu_selected || right.menu_selected) && self.context_menu_future.is_none() { self.context_menu_future = Some(context_menu_provider.open_context_menu()); } else if self.context_menu_future.is_some() { // Do not surface input info whilst the context menu is open // We don't do this for the first frame after the context menu is opened // so that the appropriate select cancel events may fire right.frame.target_ray_origin = None; right.frame.grip_origin = None; left.frame.target_ray_origin = None; left.frame.grip_origin = None; right.select = None; right.squeeze = None; left.select = None; left.squeeze = None; } } let left_input_changed = left.frame.input_changed; let right_input_changed = right.frame.input_changed; let frame = Frame { pose: Some(ViewerPose { transform, views }), inputs: vec![right.frame, left.frame], events: vec![], sub_images, hit_test_results: vec![], predicted_display_time: frame_state.predicted_display_time.as_nanos() as f64, }; if let Some(right_select) = right.select { self.events.callback(Event::Select( InputId(0), SelectKind::Select, right_select, frame.clone(), )); } if let Some(right_squeeze) = right.squeeze { self.events.callback(Event::Select( InputId(0), SelectKind::Squeeze, right_squeeze, frame.clone(), )); } if let Some(left_select) = left.select { self.events.callback(Event::Select( InputId(1), SelectKind::Select, left_select, frame.clone(), )); } if let Some(left_squeeze) = left.squeeze { self.events.callback(Event::Select( InputId(1), SelectKind::Squeeze, left_squeeze, frame.clone(), )); } if left_input_changed { self.events .callback(Event::InputChanged(InputId(1), frame.inputs[1].clone())) } if right_input_changed { self.events .callback(Event::InputChanged(InputId(0), frame.inputs[0].clone())) } Some(frame) } fn end_animation_frame(&mut self, layers: &[(ContextId, LayerId)]) { // We tell OpenXR to display the frame in the layer manager. // Due to threading issues we can't call D3D11 APIs on the openxr thread as the // WebGL thread might be using the device simultaneously, so this method delegates // everything to the layer manager. let _ = self.layer_manager.end_frame(layers); } fn initial_inputs(&self) -> Vec { vec![ self.right_hand.input_source(), self.left_hand.input_source(), ] } fn set_event_dest(&mut self, dest: WebXrSender) { self.events.upgrade(dest) } fn quit(&mut self) { self.session.request_exit().unwrap(); loop { let mut buffer = openxr::EventDataBuffer::new(); let event = match self.instance.poll_event(&mut buffer) { Ok(e) => e, Err(e) => { error!("Error polling for event while quitting: {:?}", e); break; }, }; match event { Some(openxr::Event::SessionStateChanged(session_change)) => { match session_change.state() { openxr::SessionState::EXITING => { break; }, openxr::SessionState::STOPPING => { if let Err(e) = self.session.end() { error!("Session failed to end while STOPPING: {:?}", e); } }, _ => (), } }, _ => (), } thread::sleep(Duration::from_millis(30)); } self.events.callback(Event::SessionEnd); // We clear this data to remove the outstanding reference to XrSpace, // which keeps other OpenXR objects alive. *self.shared_data.lock().unwrap() = None; } fn set_quitter(&mut self, _: Quitter) { // the quitter is only needed if we have anything from outside the render // thread that can signal a quit. We don't. } fn update_clip_planes(&mut self, near: f32, far: f32) { self.clip_planes.update(near, far); } fn environment_blend_mode(&self) -> webxr_api::EnvironmentBlendMode { match self .shared_data .lock() .unwrap() .as_ref() .unwrap() .primary_blend_mode { EnvironmentBlendMode::OPAQUE => webxr_api::EnvironmentBlendMode::Opaque, EnvironmentBlendMode::ALPHA_BLEND => webxr_api::EnvironmentBlendMode::AlphaBlend, EnvironmentBlendMode::ADDITIVE => webxr_api::EnvironmentBlendMode::Additive, v => unimplemented!("unsupported blend mode: {:?}", v), } } fn granted_features(&self) -> &[String] { &self.granted_features } fn update_frame_rate(&mut self, rate: f32) -> f32 { if self.supports_updating_framerate { self.session .request_display_refresh_rate(rate) .expect("Failed to request display refresh rate"); self.session .get_display_refresh_rate() .expect("Failed to get display refresh rate") } else { -1.0 } } fn supported_frame_rates(&self) -> Vec { if self.supports_updating_framerate { self.session .enumerate_display_refresh_rates() .expect("Failed to enumerate display refresh rates") } else { vec![] } } fn reference_space_bounds(&self) -> Option>> { match self .session .reference_space_bounds_rect(ReferenceSpaceType::STAGE) { Ok(bounds) => { if let Some(bounds) = bounds { let point1 = Point2D::new(-bounds.width / 2., -bounds.height / 2.); let point2 = Point2D::new(-bounds.width / 2., bounds.height / 2.); let point3 = Point2D::new(bounds.width / 2., bounds.height / 2.); let point4 = Point2D::new(bounds.width / 2., -bounds.height / 2.); Some(vec![point1, point2, point3, point4]) } else { None } }, Err(_) => None, } } } fn transform(pose: &Posef) -> RigidTransform3D { let rotation = Rotation3D::quaternion( pose.orientation.x, pose.orientation.y, pose.orientation.z, pose.orientation.w, ); let translation = Vector3D::new(pose.position.x, pose.position.y, pose.position.z); RigidTransform3D::new(rotation, translation) } #[inline] fn fov_to_projection_matrix(fov: &Fovf, clip_planes: ClipPlanes) -> Transform3D { util::fov_to_projection_matrix( fov.angle_left, fov.angle_right, fov.angle_up, fov.angle_down, clip_planes, ) }