/* 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::cell::Cell; use std::collections::HashMap; use std::f64::consts::{FRAC_PI_2, PI}; use std::rc::Rc; use std::{mem, ptr}; use base::cross_process_instant::CrossProcessInstant; use dom_struct::dom_struct; use euclid::{RigidTransform3D, Transform3D, Vector3D}; use ipc_channel::ipc::IpcReceiver; use ipc_channel::router::ROUTER; use js::jsapi::JSObject; use js::rust::MutableHandleValue; use js::typedarray::Float32Array; use profile_traits::ipc; use stylo_atoms::Atom; use webxr_api::{ self, util, ApiSpace, ContextId as WebXRContextId, Display, EntityTypes, EnvironmentBlendMode, Event as XREvent, Frame, FrameUpdateEvent, HitTestId, HitTestSource, InputFrame, InputId, Ray, SelectEvent, SelectKind, Session, SessionId, View, Viewer, Visibility, }; use crate::conversions::Convert; use crate::canvas_context::CanvasContext; use crate::dom::bindings::trace::HashMapTracedValues; use crate::dom::bindings::buffer_source::create_buffer_source; use crate::dom::bindings::callback::ExceptionHandling; use crate::dom::bindings::cell::DomRefCell; use crate::dom::bindings::codegen::Bindings::NavigatorBinding::Navigator_Binding::NavigatorMethods; use crate::dom::bindings::codegen::Bindings::WindowBinding::Window_Binding::WindowMethods; use crate::dom::bindings::codegen::Bindings::XRHitTestSourceBinding::{ XRHitTestOptionsInit, XRHitTestTrackableType, }; use crate::dom::bindings::codegen::Bindings::XRInputSourceArrayBinding::XRInputSourceArray_Binding::XRInputSourceArrayMethods; use crate::dom::bindings::codegen::Bindings::XRReferenceSpaceBinding::XRReferenceSpaceType; use crate::dom::bindings::codegen::Bindings::XRRenderStateBinding::{ XRRenderStateInit, XRRenderStateMethods, }; use crate::dom::bindings::codegen::Bindings::XRSessionBinding::{ XREnvironmentBlendMode, XRFrameRequestCallback, XRInteractionMode, XRSessionMethods, XRVisibilityState, }; use crate::dom::bindings::codegen::Bindings::XRSystemBinding::XRSessionMode; use crate::dom::bindings::error::{Error, ErrorResult}; use crate::dom::bindings::inheritance::Castable; use crate::dom::bindings::num::Finite; use crate::dom::bindings::refcounted::Trusted; use crate::dom::bindings::reflector::{reflect_dom_object, DomGlobal}; use crate::dom::bindings::root::{Dom, DomRoot, MutDom, MutNullableDom}; use crate::dom::bindings::utils::to_frozen_array; use crate::dom::event::Event; use crate::dom::eventtarget::EventTarget; use crate::dom::globalscope::GlobalScope; use crate::dom::promise::Promise; use crate::dom::xrboundedreferencespace::XRBoundedReferenceSpace; use crate::dom::xrframe::XRFrame; use crate::dom::xrhittestsource::XRHitTestSource; use crate::dom::xrinputsourcearray::XRInputSourceArray; use crate::dom::xrinputsourceevent::XRInputSourceEvent; use crate::dom::xrreferencespace::XRReferenceSpace; use crate::dom::xrreferencespaceevent::XRReferenceSpaceEvent; use crate::dom::xrrenderstate::XRRenderState; use crate::dom::xrrigidtransform::XRRigidTransform; use crate::dom::xrsessionevent::XRSessionEvent; use crate::dom::xrspace::XRSpace; use crate::realms::InRealm; use crate::script_runtime::JSContext; use crate::script_runtime::CanGc; #[dom_struct] pub(crate) struct XRSession { eventtarget: EventTarget, blend_mode: XREnvironmentBlendMode, mode: XRSessionMode, visibility_state: Cell, viewer_space: MutNullableDom, #[ignore_malloc_size_of = "defined in webxr"] #[no_trace] session: DomRefCell, frame_requested: Cell, pending_render_state: MutNullableDom, active_render_state: MutDom, /// Cached projection matrix for inline sessions #[no_trace] inline_projection_matrix: DomRefCell>, next_raf_id: Cell, #[ignore_malloc_size_of = "closures are hard"] raf_callback_list: DomRefCell>)>>, #[ignore_malloc_size_of = "closures are hard"] current_raf_callback_list: DomRefCell>)>>, input_sources: Dom, // Any promises from calling end() #[ignore_malloc_size_of = "promises are hard"] end_promises: DomRefCell>>, /// ended: Cell, #[ignore_malloc_size_of = "defined in webxr"] #[no_trace] next_hit_test_id: Cell, #[ignore_malloc_size_of = "defined in webxr"] pending_hit_test_promises: DomRefCell>>, /// Opaque framebuffers need to know the session is "outside of a requestAnimationFrame" /// outside_raf: Cell, #[ignore_malloc_size_of = "defined in webxr"] #[no_trace] input_frames: DomRefCell>, framerate: Cell, #[ignore_malloc_size_of = "promises are hard"] update_framerate_promise: DomRefCell>>, reference_spaces: DomRefCell>>, } impl XRSession { fn new_inherited( session: Session, render_state: &XRRenderState, input_sources: &XRInputSourceArray, mode: XRSessionMode, ) -> XRSession { XRSession { eventtarget: EventTarget::new_inherited(), blend_mode: session.environment_blend_mode().convert(), mode, visibility_state: Cell::new(XRVisibilityState::Visible), viewer_space: Default::default(), session: DomRefCell::new(session), frame_requested: Cell::new(false), pending_render_state: MutNullableDom::new(None), active_render_state: MutDom::new(render_state), inline_projection_matrix: Default::default(), next_raf_id: Cell::new(0), raf_callback_list: DomRefCell::new(vec![]), current_raf_callback_list: DomRefCell::new(vec![]), input_sources: Dom::from_ref(input_sources), end_promises: DomRefCell::new(vec![]), ended: Cell::new(false), next_hit_test_id: Cell::new(HitTestId(0)), pending_hit_test_promises: DomRefCell::new(HashMapTracedValues::new()), outside_raf: Cell::new(true), input_frames: DomRefCell::new(HashMap::new()), framerate: Cell::new(0.0), update_framerate_promise: DomRefCell::new(None), reference_spaces: DomRefCell::new(Vec::new()), } } pub(crate) fn new( global: &GlobalScope, session: Session, mode: XRSessionMode, frame_receiver: IpcReceiver, can_gc: CanGc, ) -> DomRoot { let ivfov = if mode == XRSessionMode::Inline { Some(FRAC_PI_2) } else { None }; let render_state = XRRenderState::new(global, 0.1, 1000.0, ivfov, None, Vec::new(), can_gc); let input_sources = XRInputSourceArray::new(global, can_gc); let ret = reflect_dom_object( Box::new(XRSession::new_inherited( session, &render_state, &input_sources, mode, )), global, can_gc, ); ret.attach_event_handler(); ret.setup_raf_loop(frame_receiver); ret } pub(crate) fn with_session R>(&self, with: F) -> R { let session = self.session.borrow(); with(&session) } pub(crate) fn is_ended(&self) -> bool { self.ended.get() } pub(crate) fn is_immersive(&self) -> bool { self.mode != XRSessionMode::Inline } // https://immersive-web.github.io/layers/#feature-descriptor-layers pub(crate) fn has_layers_feature(&self) -> bool { // We do not support creating layers other than projection layers // https://github.com/servo/servo/issues/27493 false } fn setup_raf_loop(&self, frame_receiver: IpcReceiver) { let this = Trusted::new(self); let global = self.global(); let task_source = global .task_manager() .dom_manipulation_task_source() .to_sendable(); ROUTER.add_typed_route( frame_receiver, Box::new(move |message| { let frame: Frame = message.unwrap(); let time = CrossProcessInstant::now(); let this = this.clone(); task_source.queue(task!(xr_raf_callback: move || { this.root().raf_callback(frame, time, CanGc::note()); })); }), ); self.session.borrow_mut().start_render_loop(); } pub(crate) fn is_outside_raf(&self) -> bool { self.outside_raf.get() } fn attach_event_handler(&self) { let this = Trusted::new(self); let global = self.global(); let task_source = global .task_manager() .dom_manipulation_task_source() .to_sendable(); let (sender, receiver) = ipc::channel(global.time_profiler_chan().clone()).unwrap(); ROUTER.add_typed_route( receiver.to_ipc_receiver(), Box::new(move |message| { let this = this.clone(); task_source.queue(task!(xr_event_callback: move || { this.root().event_callback(message.unwrap(), CanGc::note()); })); }), ); // request animation frame self.session.borrow_mut().set_event_dest(sender); } // Must be called after the promise for session creation is resolved // https://github.com/immersive-web/webxr/issues/961 // // This enables content that assumes all input sources are accompanied // by an inputsourceschange event to work properly. Without pub(crate) fn setup_initial_inputs(&self) { let initial_inputs = self.session.borrow().initial_inputs().to_owned(); if initial_inputs.is_empty() { // do not fire an empty event return; } let this = Trusted::new(self); // Queue a task so that it runs after resolve()'s microtasks complete // so that content has a chance to attach a listener for inputsourceschange self.global() .task_manager() .dom_manipulation_task_source() .queue(task!(session_initial_inputs: move || { let this = this.root(); this.input_sources.add_input_sources(&this, &initial_inputs, CanGc::note()); })); } fn event_callback(&self, event: XREvent, can_gc: CanGc) { match event { XREvent::SessionEnd => { // https://immersive-web.github.io/webxr/#shut-down-the-session // Step 2 self.ended.set(true); // Step 3-4 self.global().as_window().Navigator().Xr().end_session(self); // Step 5: We currently do not have any such promises // Step 6 is happening n the XR session // https://immersive-web.github.io/webxr/#dom-xrsession-end step 3 for promise in self.end_promises.borrow_mut().drain(..) { promise.resolve_native(&(), can_gc); } // Step 7 let event = XRSessionEvent::new( self.global().as_window(), atom!("end"), false, false, self, can_gc, ); event.upcast::().fire(self.upcast(), can_gc); }, XREvent::Select(input, kind, ty, frame) => { use stylo_atoms::Atom; const START_ATOMS: [Atom; 2] = [atom!("selectstart"), atom!("squeezestart")]; const EVENT_ATOMS: [Atom; 2] = [atom!("select"), atom!("squeeze")]; const END_ATOMS: [Atom; 2] = [atom!("selectend"), atom!("squeezeend")]; // https://immersive-web.github.io/webxr/#primary-action let source = self.input_sources.find(input); let atom_index = if kind == SelectKind::Squeeze { 1 } else { 0 }; if let Some(source) = source { let frame = XRFrame::new(self.global().as_window(), self, frame, can_gc); frame.set_active(true); if ty == SelectEvent::Start { let event = XRInputSourceEvent::new( self.global().as_window(), START_ATOMS[atom_index].clone(), false, false, &frame, &source, can_gc, ); event.upcast::().fire(self.upcast(), can_gc); } else { if ty == SelectEvent::Select { let event = XRInputSourceEvent::new( self.global().as_window(), EVENT_ATOMS[atom_index].clone(), false, false, &frame, &source, can_gc, ); event.upcast::().fire(self.upcast(), can_gc); } let event = XRInputSourceEvent::new( self.global().as_window(), END_ATOMS[atom_index].clone(), false, false, &frame, &source, can_gc, ); event.upcast::().fire(self.upcast(), can_gc); } frame.set_active(false); } }, XREvent::VisibilityChange(v) => { let v = match v { Visibility::Visible => XRVisibilityState::Visible, Visibility::VisibleBlurred => XRVisibilityState::Visible_blurred, Visibility::Hidden => XRVisibilityState::Hidden, }; self.visibility_state.set(v); let event = XRSessionEvent::new( self.global().as_window(), atom!("visibilitychange"), false, false, self, can_gc, ); event.upcast::().fire(self.upcast(), can_gc); // The page may be visible again, dirty the layers // This also wakes up the event loop if necessary self.dirty_layers(); }, XREvent::AddInput(info) => { self.input_sources.add_input_sources(self, &[info], can_gc); }, XREvent::RemoveInput(id) => { self.input_sources.remove_input_source(self, id, can_gc); }, XREvent::UpdateInput(id, source) => { self.input_sources .add_remove_input_source(self, id, source, can_gc); }, XREvent::InputChanged(id, frame) => { self.input_frames.borrow_mut().insert(id, frame); }, XREvent::ReferenceSpaceChanged(base_space, transform) => { self.reference_spaces .borrow() .iter() .filter(|space| { let base = match space.ty() { XRReferenceSpaceType::Local => webxr_api::BaseSpace::Local, XRReferenceSpaceType::Viewer => webxr_api::BaseSpace::Viewer, XRReferenceSpaceType::Local_floor => webxr_api::BaseSpace::Floor, XRReferenceSpaceType::Bounded_floor => { webxr_api::BaseSpace::BoundedFloor }, _ => panic!("unsupported reference space found"), }; base == base_space }) .for_each(|space| { let offset = XRRigidTransform::new(self.global().as_window(), transform, can_gc); let event = XRReferenceSpaceEvent::new( self.global().as_window(), atom!("reset"), false, false, space, Some(&*offset), can_gc, ); event.upcast::().fire(space.upcast(), can_gc); }); }, } } /// fn raf_callback(&self, mut frame: Frame, time: CrossProcessInstant, can_gc: CanGc) { debug!("WebXR RAF callback {:?}", frame); // Step 1-2 happen in the xebxr device thread // Step 3 if let Some(pending) = self.pending_render_state.take() { // https://immersive-web.github.io/webxr/#apply-the-pending-render-state // (Steps 1-4 are implicit) // Step 5 self.active_render_state.set(&pending); // Step 6-7: XXXManishearth handle inlineVerticalFieldOfView if !self.is_immersive() { self.update_inline_projection_matrix() } } // TODO: how does this fit the webxr spec? for event in frame.events.drain(..) { self.handle_frame_event(event, can_gc); } // Step 4 // TODO: what should this check be? // This is checking that the new render state has the same // layers as the frame. // Related to https://github.com/immersive-web/webxr/issues/1051 if !self .active_render_state .get() .has_sub_images(&frame.sub_images[..]) { // If the frame has different layers than the render state, // we just return early, drawing a blank frame. // This can result in flickering when the render state is changed. // TODO: it would be better to not render anything until the next frame. warn!("Rendering blank XR frame"); self.session.borrow_mut().render_animation_frame(); return; } // Step 5: XXXManishearth handle inline session // Step 6-7 { let mut current = self.current_raf_callback_list.borrow_mut(); assert!(current.is_empty()); mem::swap(&mut *self.raf_callback_list.borrow_mut(), &mut current); } let time = self.global().performance().to_dom_high_res_time_stamp(time); let frame = XRFrame::new(self.global().as_window(), self, frame, CanGc::note()); // Step 8-9 frame.set_active(true); frame.set_animation_frame(true); // Step 10 self.apply_frame_updates(&frame); // TODO: how does this fit with the webxr and xr layers specs? self.layers_begin_frame(&frame); // Step 11-12 self.outside_raf.set(false); let len = self.current_raf_callback_list.borrow().len(); for i in 0..len { let callback = self.current_raf_callback_list.borrow()[i].1.clone(); if let Some(callback) = callback { let _ = callback.Call__(time, &frame, ExceptionHandling::Report, can_gc); } } self.outside_raf.set(true); *self.current_raf_callback_list.borrow_mut() = vec![]; // TODO: how does this fit with the webxr and xr layers specs? self.layers_end_frame(&frame); // Step 13 frame.set_active(false); // TODO: how does this fit the webxr spec? self.session.borrow_mut().render_animation_frame(); } fn update_inline_projection_matrix(&self) { debug_assert!(!self.is_immersive()); let render_state = self.active_render_state.get(); let size = if let Some(base) = render_state.GetBaseLayer() { base.size() } else { return; }; let mut clip_planes = util::ClipPlanes::default(); let near = *render_state.DepthNear() as f32; let far = *render_state.DepthFar() as f32; clip_planes.update(near, far); let top = *render_state .GetInlineVerticalFieldOfView() .expect("IVFOV should be non null for inline sessions") / 2.; let top = near * top.tan() as f32; let bottom = top; let left = top * size.width as f32 / size.height as f32; let right = left; let matrix = util::frustum_to_projection_matrix(left, right, top, bottom, clip_planes); *self.inline_projection_matrix.borrow_mut() = matrix; } /// Constructs a View suitable for inline sessions using the inlineVerticalFieldOfView and canvas size pub(crate) fn inline_view(&self) -> View { debug_assert!(!self.is_immersive()); View { // Inline views have no offset transform: RigidTransform3D::identity(), projection: *self.inline_projection_matrix.borrow(), } } pub(crate) fn session_id(&self) -> SessionId { self.session.borrow().id() } pub(crate) fn dirty_layers(&self) { if let Some(layer) = self.RenderState().GetBaseLayer() { layer.context().mark_as_dirty(); } } // TODO: how does this align with the layers spec? fn layers_begin_frame(&self, frame: &XRFrame) { if let Some(layer) = self.active_render_state.get().GetBaseLayer() { layer.begin_frame(frame); } self.active_render_state.get().with_layers(|layers| { for layer in layers { layer.begin_frame(frame); } }); } // TODO: how does this align with the layers spec? fn layers_end_frame(&self, frame: &XRFrame) { if let Some(layer) = self.active_render_state.get().GetBaseLayer() { layer.end_frame(frame); } self.active_render_state.get().with_layers(|layers| { for layer in layers { layer.end_frame(frame); } }); } /// fn apply_frame_updates(&self, _frame: &XRFrame) { // for (id, frame) in self.input_frames.borrow_mut().drain() { let source = self.input_sources.find(id); if let Some(source) = source { source.update_gamepad_state(frame); } } } fn handle_frame_event(&self, event: FrameUpdateEvent, can_gc: CanGc) { match event { FrameUpdateEvent::HitTestSourceAdded(id) => { if let Some(promise) = self.pending_hit_test_promises.borrow_mut().remove(&id) { promise.resolve_native( &XRHitTestSource::new(&self.global(), id, self, can_gc), can_gc, ); } else { warn!( "received hit test add request for unknown hit test {:?}", id ) } }, _ => self.session.borrow_mut().apply_event(event), } } /// fn apply_nominal_framerate(&self, rate: f32, can_gc: CanGc) { if self.framerate.get() == rate || self.ended.get() { return; } self.framerate.set(rate); let event = XRSessionEvent::new( self.global().as_window(), Atom::from("frameratechange"), false, false, self, can_gc, ); event.upcast::().fire(self.upcast(), can_gc); } } impl XRSessionMethods for XRSession { // https://immersive-web.github.io/webxr/#eventdef-xrsession-end event_handler!(end, GetOnend, SetOnend); // https://immersive-web.github.io/webxr/#eventdef-xrsession-select event_handler!(select, GetOnselect, SetOnselect); // https://immersive-web.github.io/webxr/#eventdef-xrsession-selectstart event_handler!(selectstart, GetOnselectstart, SetOnselectstart); // https://immersive-web.github.io/webxr/#eventdef-xrsession-selectend event_handler!(selectend, GetOnselectend, SetOnselectend); // https://immersive-web.github.io/webxr/#eventdef-xrsession-squeeze event_handler!(squeeze, GetOnsqueeze, SetOnsqueeze); // https://immersive-web.github.io/webxr/#eventdef-xrsession-squeezestart event_handler!(squeezestart, GetOnsqueezestart, SetOnsqueezestart); // https://immersive-web.github.io/webxr/#eventdef-xrsession-squeezeend event_handler!(squeezeend, GetOnsqueezeend, SetOnsqueezeend); // https://immersive-web.github.io/webxr/#eventdef-xrsession-visibilitychange event_handler!( visibilitychange, GetOnvisibilitychange, SetOnvisibilitychange ); // https://immersive-web.github.io/webxr/#eventdef-xrsession-inputsourceschange event_handler!( inputsourceschange, GetOninputsourceschange, SetOninputsourceschange ); // https://www.w3.org/TR/webxr/#dom-xrsession-onframeratechange event_handler!(frameratechange, GetOnframeratechange, SetOnframeratechange); // https://immersive-web.github.io/webxr/#dom-xrsession-renderstate fn RenderState(&self) -> DomRoot { self.active_render_state.get() } /// fn UpdateRenderState(&self, init: &XRRenderStateInit, _: InRealm) -> ErrorResult { // Step 2 if self.ended.get() { return Err(Error::InvalidState); } // Step 3: if let Some(Some(ref layer)) = init.baseLayer { if Dom::from_ref(layer.session()) != Dom::from_ref(self) { return Err(Error::InvalidState); } } // Step 4: if init.inlineVerticalFieldOfView.is_some() && self.is_immersive() { return Err(Error::InvalidState); } // https://immersive-web.github.io/layers/#updaterenderstatechanges // Step 1. if init.baseLayer.is_some() && (self.has_layers_feature() || init.layers.is_some()) { return Err(Error::NotSupported); } if let Some(Some(ref layers)) = init.layers { // Step 2 for layer in layers { let count = layers .iter() .filter(|other| other.layer_id() == layer.layer_id()) .count(); if count > 1 { return Err(Error::Type(String::from("Duplicate entry in WebXR layers"))); } } // Step 3 for layer in layers { if layer.session() != self { return Err(Error::Type(String::from( "Layer from different session in WebXR layers", ))); } } } // Step 4-5 let pending = self .pending_render_state .or_init(|| self.active_render_state.get().clone_object()); // Step 6 if let Some(ref layers) = init.layers { let layers = layers.as_deref().unwrap_or_default(); pending.set_base_layer(None); pending.set_layers(layers.iter().map(|x| &**x).collect()); let layers = layers .iter() .filter_map(|layer| { let context_id = WebXRContextId::from(layer.context_id()); let layer_id = layer.layer_id()?; Some((context_id, layer_id)) }) .collect(); self.session.borrow_mut().set_layers(layers); } // End of https://immersive-web.github.io/layers/#updaterenderstatechanges if let Some(near) = init.depthNear { let mut near = *near; // Step 8 from #apply-the-pending-render-state // this may need to be changed if backends wish to impose // further constraints if near < 0. { near = 0.; } pending.set_depth_near(near); } if let Some(far) = init.depthFar { let mut far = *far; // Step 9 from #apply-the-pending-render-state // this may need to be changed if backends wish to impose // further constraints // currently the maximum is infinity, so just check that // the value is non-negative if far < 0. { far = 0.; } pending.set_depth_far(far); } if let Some(fov) = init.inlineVerticalFieldOfView { let mut fov = *fov; // Step 10 from #apply-the-pending-render-state // this may need to be changed if backends wish to impose // further constraints if fov < 0. { fov = 0.0001; } else if fov > PI { fov = PI - 0.0001; } pending.set_inline_vertical_fov(fov); } if let Some(ref layer) = init.baseLayer { pending.set_base_layer(layer.as_deref()); pending.set_layers(Vec::new()); let layers = layer .iter() .filter_map(|layer| { let context_id = WebXRContextId::from(layer.context_id()); let layer_id = layer.layer_id()?; Some((context_id, layer_id)) }) .collect(); self.session.borrow_mut().set_layers(layers); } if init.depthFar.is_some() || init.depthNear.is_some() { self.session .borrow_mut() .update_clip_planes(*pending.DepthNear() as f32, *pending.DepthFar() as f32); } Ok(()) } /// fn RequestAnimationFrame(&self, callback: Rc) -> i32 { // queue up RAF callback, obtain ID let raf_id = self.next_raf_id.get(); self.next_raf_id.set(raf_id + 1); self.raf_callback_list .borrow_mut() .push((raf_id, Some(callback))); raf_id } /// fn CancelAnimationFrame(&self, frame: i32) { let mut list = self.raf_callback_list.borrow_mut(); if let Some(pair) = list.iter_mut().find(|pair| pair.0 == frame) { pair.1 = None; } let mut list = self.current_raf_callback_list.borrow_mut(); if let Some(pair) = list.iter_mut().find(|pair| pair.0 == frame) { pair.1 = None; } } /// fn EnvironmentBlendMode(&self) -> XREnvironmentBlendMode { self.blend_mode } /// fn VisibilityState(&self) -> XRVisibilityState { self.visibility_state.get() } /// fn RequestReferenceSpace( &self, ty: XRReferenceSpaceType, comp: InRealm, can_gc: CanGc, ) -> Rc { let p = Promise::new_in_current_realm(comp, can_gc); // https://immersive-web.github.io/webxr/#create-a-reference-space // XXXManishearth reject based on session type // https://github.com/immersive-web/webxr/blob/master/spatial-tracking-explainer.md#practical-usage-guidelines if !self.is_immersive() && (ty == XRReferenceSpaceType::Bounded_floor || ty == XRReferenceSpaceType::Unbounded) { p.reject_error(Error::NotSupported, can_gc); return p; } match ty { XRReferenceSpaceType::Unbounded => { // XXXmsub2 figure out how to support this p.reject_error(Error::NotSupported, can_gc) }, ty => { if ty != XRReferenceSpaceType::Viewer && (!self.is_immersive() || ty != XRReferenceSpaceType::Local) { let s = ty.as_str(); if !self .session .borrow() .granted_features() .iter() .any(|f| *f == s) { p.reject_error(Error::NotSupported, can_gc); return p; } } if ty == XRReferenceSpaceType::Bounded_floor { let space = XRBoundedReferenceSpace::new(self.global().as_window(), self, can_gc); self.reference_spaces .borrow_mut() .push(Dom::from_ref(space.reference_space())); p.resolve_native(&space, can_gc); } else { let space = XRReferenceSpace::new(self.global().as_window(), self, ty, can_gc); self.reference_spaces .borrow_mut() .push(Dom::from_ref(&*space)); p.resolve_native(&space, can_gc); } }, } p } /// fn InputSources(&self) -> DomRoot { DomRoot::from_ref(&*self.input_sources) } /// fn End(&self, can_gc: CanGc) -> Rc { let global = self.global(); let p = Promise::new(&global, can_gc); if self.ended.get() && self.end_promises.borrow().is_empty() { // If the session has completely ended and all end promises have been resolved, // don't queue up more end promises // // We need to check for end_promises being empty because `ended` is set // before everything has been completely shut down, and we do not want to // prematurely resolve the promise then // // However, if end_promises is empty, then all end() promises have already resolved, // so the session has completely shut down and we should not queue up more promises p.resolve_native(&(), can_gc); return p; } self.end_promises.borrow_mut().push(p.clone()); // This is duplicated in event_callback since this should // happen ASAP for end() but can happen later if the device // shuts itself down self.ended.set(true); global.as_window().Navigator().Xr().end_session(self); self.session.borrow_mut().end_session(); // Disconnect any still-attached XRInputSources for source in 0..self.input_sources.Length() { self.input_sources .remove_input_source(self, InputId(source), can_gc); } p } // https://immersive-web.github.io/hit-test/#dom-xrsession-requesthittestsource fn RequestHitTestSource(&self, options: &XRHitTestOptionsInit, can_gc: CanGc) -> Rc { let p = Promise::new(&self.global(), can_gc); if !self .session .borrow() .granted_features() .iter() .any(|f| f == "hit-test") { p.reject_error(Error::NotSupported, can_gc); return p; } let id = self.next_hit_test_id.get(); self.next_hit_test_id.set(HitTestId(id.0 + 1)); let space = options.space.space(); let ray = if let Some(ref ray) = options.offsetRay { ray.ray() } else { Ray { origin: Vector3D::new(0., 0., 0.), direction: Vector3D::new(0., 0., -1.), } }; let mut types = EntityTypes::default(); if let Some(ref tys) = options.entityTypes { for ty in tys { match ty { XRHitTestTrackableType::Point => types.point = true, XRHitTestTrackableType::Plane => types.plane = true, XRHitTestTrackableType::Mesh => types.mesh = true, } } } else { types.plane = true; } let source = HitTestSource { id, space, ray, types, }; self.pending_hit_test_promises .borrow_mut() .insert(id, p.clone()); self.session.borrow().request_hit_test(source); p } /// fn InteractionMode(&self) -> XRInteractionMode { // Until Servo supports WebXR sessions on mobile phones or similar non-XR devices, // this should always be world space XRInteractionMode::World_space } /// fn GetFrameRate(&self) -> Option> { let session = self.session.borrow(); if self.mode == XRSessionMode::Inline || session.supported_frame_rates().is_empty() { None } else { Finite::new(self.framerate.get()) } } /// fn GetSupportedFrameRates(&self, cx: JSContext, can_gc: CanGc) -> Option { let session = self.session.borrow(); if self.mode == XRSessionMode::Inline || session.supported_frame_rates().is_empty() { None } else { let framerates = session.supported_frame_rates(); rooted!(in (*cx) let mut array = ptr::null_mut::()); Some( create_buffer_source(cx, framerates, array.handle_mut(), can_gc) .expect("Failed to construct supported frame rates array"), ) } } /// fn EnabledFeatures(&self, cx: JSContext, can_gc: CanGc, retval: MutableHandleValue) { let session = self.session.borrow(); let features = session.granted_features(); to_frozen_array(features, cx, retval, can_gc) } /// fn IsSystemKeyboardSupported(&self) -> bool { // Support for this only exists on Meta headsets (no desktop support) // so this will always be false until that changes false } /// fn UpdateTargetFrameRate( &self, rate: Finite, comp: InRealm, can_gc: CanGc, ) -> Rc { let promise = Promise::new_in_current_realm(comp, can_gc); { let session = self.session.borrow(); let supported_frame_rates = session.supported_frame_rates(); if self.mode == XRSessionMode::Inline || supported_frame_rates.is_empty() || self.ended.get() { promise.reject_error(Error::InvalidState, can_gc); return promise; } if !supported_frame_rates.contains(&*rate) { promise.reject_error( Error::Type("Provided framerate not supported".into()), can_gc, ); return promise; } } *self.update_framerate_promise.borrow_mut() = Some(promise.clone()); let this = Trusted::new(self); let global = self.global(); let task_source = global .task_manager() .dom_manipulation_task_source() .to_sendable(); let (sender, receiver) = ipc::channel(global.time_profiler_chan().clone()).unwrap(); ROUTER.add_typed_route( receiver.to_ipc_receiver(), Box::new(move |message| { let this = this.clone(); task_source.queue(task!(update_session_framerate: move || { let session = this.root(); session.apply_nominal_framerate(message.unwrap(), CanGc::note()); if let Some(promise) = session.update_framerate_promise.borrow_mut().take() { promise.resolve_native(&(), CanGc::note()); }; })); }), ); self.session.borrow_mut().update_frame_rate(*rate, sender); promise } } // The pose of an object in native-space. Should never be exposed. pub(crate) type ApiPose = RigidTransform3D; // A transform between objects in some API-space pub(crate) type ApiRigidTransform = RigidTransform3D; #[derive(Clone, Copy)] pub(crate) struct BaseSpace; pub(crate) type BaseTransform = RigidTransform3D; #[allow(unsafe_code)] pub(crate) fn cast_transform( transform: RigidTransform3D, ) -> RigidTransform3D { unsafe { mem::transmute(transform) } } impl Convert for EnvironmentBlendMode { fn convert(self) -> XREnvironmentBlendMode { match self { EnvironmentBlendMode::Opaque => XREnvironmentBlendMode::Opaque, EnvironmentBlendMode::AlphaBlend => XREnvironmentBlendMode::Alpha_blend, EnvironmentBlendMode::Additive => XREnvironmentBlendMode::Additive, } } }