/* 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 crate::dom::bindings::cell::DomRefCell; use crate::dom::bindings::codegen::Bindings::XRSystemBinding::XRSessionInit; use crate::dom::bindings::codegen::Bindings::XRSystemBinding::{XRSessionMode, XRSystemMethods}; use crate::dom::bindings::conversions::{ConversionResult, FromJSValConvertible}; use crate::dom::bindings::error::Error; use crate::dom::bindings::inheritance::Castable; use crate::dom::bindings::refcounted::{Trusted, TrustedPromise}; use crate::dom::bindings::reflector::{reflect_dom_object, DomObject}; use crate::dom::bindings::root::{Dom, DomRoot, MutNullableDom}; use crate::dom::bindings::trace::RootedTraceableBox; use crate::dom::eventtarget::EventTarget; use crate::dom::gamepad::Gamepad; use crate::dom::globalscope::GlobalScope; use crate::dom::promise::Promise; use crate::dom::window::Window; use crate::dom::xrsession::XRSession; use crate::dom::xrtest::XRTest; use crate::realms::InRealm; use crate::script_thread::ScriptThread; use crate::task_source::TaskSource; use dom_struct::dom_struct; use ipc_channel::ipc::{self as ipc_crate, IpcReceiver}; use ipc_channel::router::ROUTER; use msg::constellation_msg::PipelineId; use profile_traits::ipc; use servo_config::pref; use std::cell::Cell; use std::rc::Rc; use webxr_api::{Error as XRError, Frame, Session, SessionInit, SessionMode}; #[dom_struct] pub struct XRSystem { eventtarget: EventTarget, gamepads: DomRefCell>>, pending_immersive_session: Cell, active_immersive_session: MutNullableDom, active_inline_sessions: DomRefCell>>, test: MutNullableDom, #[no_trace] pipeline: PipelineId, } impl XRSystem { fn new_inherited(pipeline: PipelineId) -> XRSystem { XRSystem { eventtarget: EventTarget::new_inherited(), gamepads: DomRefCell::new(Vec::new()), pending_immersive_session: Cell::new(false), active_immersive_session: Default::default(), active_inline_sessions: DomRefCell::new(Vec::new()), test: Default::default(), pipeline, } } pub fn new(window: &Window) -> DomRoot { reflect_dom_object( Box::new(XRSystem::new_inherited(window.pipeline_id())), window, ) } pub fn pending_or_active_session(&self) -> bool { self.pending_immersive_session.get() || self.active_immersive_session.get().is_some() } pub fn set_pending(&self) { self.pending_immersive_session.set(true) } pub fn set_active_immersive_session(&self, session: &XRSession) { // XXXManishearth when we support non-immersive (inline) sessions we should // ensure they never reach these codepaths self.pending_immersive_session.set(false); self.active_immersive_session.set(Some(session)) } /// https://immersive-web.github.io/webxr/#ref-for-eventdef-xrsession-end pub fn end_session(&self, session: &XRSession) { // Step 3 if let Some(active) = self.active_immersive_session.get() { if Dom::from_ref(&*active) == Dom::from_ref(session) { self.active_immersive_session.set(None); // Dirty the canvas, since it has been skipping this step whilst in immersive // mode session.dirty_layers(); } } self.active_inline_sessions .borrow_mut() .retain(|sess| Dom::from_ref(&**sess) != Dom::from_ref(session)); } } impl Into for XRSessionMode { fn into(self) -> SessionMode { match self { XRSessionMode::Immersive_vr => SessionMode::ImmersiveVR, XRSessionMode::Immersive_ar => SessionMode::ImmersiveAR, XRSessionMode::Inline => SessionMode::Inline, } } } impl XRSystemMethods for XRSystem { /// https://immersive-web.github.io/webxr/#dom-xr-issessionsupported fn IsSessionSupported(&self, mode: XRSessionMode) -> Rc { // XXXManishearth this should select an XR device first let promise = Promise::new(&self.global()); let mut trusted = Some(TrustedPromise::new(promise.clone())); let global = self.global(); let window = global.as_window(); let (task_source, canceller) = window .task_manager() .dom_manipulation_task_source_with_canceller(); let (sender, receiver) = ipc::channel(global.time_profiler_chan().clone()).unwrap(); ROUTER.add_route( receiver.to_opaque(), Box::new(move |message| { // router doesn't know this is only called once let trusted = if let Some(trusted) = trusted.take() { trusted } else { error!("supportsSession callback called twice!"); return; }; let message: Result<(), webxr_api::Error> = if let Ok(message) = message.to() { message } else { error!("supportsSession callback given incorrect payload"); return; }; if let Ok(()) = message { let _ = task_source.queue_with_canceller(trusted.resolve_task(true), &canceller); } else { let _ = task_source.queue_with_canceller(trusted.resolve_task(false), &canceller); }; }), ); window .webxr_registry() .supports_session(mode.into(), sender); promise } /// https://immersive-web.github.io/webxr/#dom-xr-requestsession #[allow(unsafe_code)] fn RequestSession( &self, mode: XRSessionMode, init: RootedTraceableBox, comp: InRealm, ) -> Rc { let global = self.global(); let window = global.as_window(); let promise = Promise::new_in_current_realm(comp); if mode != XRSessionMode::Inline { if !ScriptThread::is_user_interacting() { if pref!(dom.webxr.unsafe_assume_user_intent) { warn!("The dom.webxr.unsafe-assume-user-intent preference assumes user intent to enter WebXR."); } else { promise.reject_error(Error::Security); return promise; } } if self.pending_or_active_session() { promise.reject_error(Error::InvalidState); return promise; } self.set_pending(); } let mut required_features = vec![]; let mut optional_features = vec![]; let cx = GlobalScope::get_cx(); // We are supposed to include "viewer" and on immersive devices "local" // by default here, but this is handled directly in requestReferenceSpace() if let Some(ref r) = init.requiredFeatures { for feature in r { unsafe { if let Ok(ConversionResult::Success(s)) = String::from_jsval(*cx, feature.handle(), ()) { required_features.push(s) } else { warn!("Unable to convert required feature to string"); if mode != XRSessionMode::Inline { self.pending_immersive_session.set(false); } promise.reject_error(Error::NotSupported); return promise; } } } } if let Some(ref o) = init.optionalFeatures { for feature in o { unsafe { if let Ok(ConversionResult::Success(s)) = String::from_jsval(*cx, feature.handle(), ()) { optional_features.push(s) } else { warn!("Unable to convert optional feature to string"); } } } } let init = SessionInit { required_features, optional_features, first_person_observer_view: pref!(dom.webxr.first_person_observer_view), }; let mut trusted = Some(TrustedPromise::new(promise.clone())); let this = Trusted::new(self); let (task_source, canceller) = window .task_manager() .dom_manipulation_task_source_with_canceller(); let (sender, receiver) = ipc::channel(global.time_profiler_chan().clone()).unwrap(); let (frame_sender, frame_receiver) = ipc_crate::channel().unwrap(); let mut frame_receiver = Some(frame_receiver); ROUTER.add_route( receiver.to_opaque(), Box::new(move |message| { // router doesn't know this is only called once let trusted = trusted.take().unwrap(); let this = this.clone(); let frame_receiver = frame_receiver.take().unwrap(); let message: Result = if let Ok(message) = message.to() { message } else { error!("requestSession callback given incorrect payload"); return; }; let _ = task_source.queue_with_canceller( task!(request_session: move || { this.root().session_obtained(message, trusted.root(), mode, frame_receiver); }), &canceller, ); }), ); window .webxr_registry() .request_session(mode.into(), init, sender, frame_sender); promise } // https://github.com/immersive-web/webxr-test-api/blob/master/explainer.md fn Test(&self) -> DomRoot { self.test.or_init(|| XRTest::new(&self.global())) } } impl XRSystem { fn session_obtained( &self, response: Result, promise: Rc, mode: XRSessionMode, frame_receiver: IpcReceiver, ) { let session = match response { Ok(session) => session, Err(e) => { warn!("Error requesting XR session: {:?}", e); if mode != XRSessionMode::Inline { self.pending_immersive_session.set(false); } promise.reject_error(Error::NotSupported); return; }, }; let session = XRSession::new(&self.global(), session, mode, frame_receiver); if mode == XRSessionMode::Inline { self.active_inline_sessions .borrow_mut() .push(Dom::from_ref(&*session)); } else { self.set_active_immersive_session(&session); } promise.resolve_native(&session); // https://github.com/immersive-web/webxr/issues/961 // This must be called _after_ the promise is resolved session.setup_initial_inputs(); } // https://github.com/immersive-web/navigation/issues/10 pub fn dispatch_sessionavailable(&self) { let xr = Trusted::new(self); let global = self.global(); let window = global.as_window(); window .task_manager() .dom_manipulation_task_source() .queue( task!(fire_sessionavailable_event: move || { // The sessionavailable event indicates user intent to enter an XR session let xr = xr.root(); let interacting = ScriptThread::is_user_interacting(); ScriptThread::set_user_interacting(true); xr.upcast::().fire_bubbling_event(atom!("sessionavailable")); ScriptThread::set_user_interacting(interacting); }), window.upcast(), ) .unwrap(); } }