script/dom/webxr/
xrsystem.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::cell::Cell;
6use std::rc::Rc;
7
8use base::id::PipelineId;
9use dom_struct::dom_struct;
10use ipc_channel::ipc::{self as ipc_crate, IpcReceiver};
11use ipc_channel::router::ROUTER;
12use profile_traits::ipc;
13use servo_config::pref;
14use webxr_api::{Error as XRError, Frame, Session, SessionInit, SessionMode};
15
16use crate::conversions::Convert;
17use crate::dom::bindings::cell::DomRefCell;
18use crate::dom::bindings::codegen::Bindings::XRSystemBinding::{
19    XRSessionInit, XRSessionMode, XRSystemMethods,
20};
21use crate::dom::bindings::conversions::{ConversionResult, SafeFromJSValConvertible};
22use crate::dom::bindings::error::Error;
23use crate::dom::bindings::inheritance::Castable;
24use crate::dom::bindings::refcounted::{Trusted, TrustedPromise};
25use crate::dom::bindings::reflector::{DomGlobal, reflect_dom_object};
26use crate::dom::bindings::root::{Dom, DomRoot, MutNullableDom};
27use crate::dom::bindings::trace::RootedTraceableBox;
28use crate::dom::eventtarget::EventTarget;
29use crate::dom::gamepad::Gamepad;
30use crate::dom::globalscope::GlobalScope;
31use crate::dom::promise::Promise;
32use crate::dom::window::Window;
33use crate::dom::xrsession::XRSession;
34use crate::dom::xrtest::XRTest;
35use crate::realms::InRealm;
36use crate::script_runtime::CanGc;
37use crate::script_thread::ScriptThread;
38
39#[dom_struct]
40pub(crate) struct XRSystem {
41    eventtarget: EventTarget,
42    gamepads: DomRefCell<Vec<Dom<Gamepad>>>,
43    pending_immersive_session: Cell<bool>,
44    active_immersive_session: MutNullableDom<XRSession>,
45    active_inline_sessions: DomRefCell<Vec<Dom<XRSession>>>,
46    test: MutNullableDom<XRTest>,
47    #[no_trace]
48    pipeline: PipelineId,
49}
50
51impl XRSystem {
52    fn new_inherited(pipeline: PipelineId) -> XRSystem {
53        XRSystem {
54            eventtarget: EventTarget::new_inherited(),
55            gamepads: DomRefCell::new(Vec::new()),
56            pending_immersive_session: Cell::new(false),
57            active_immersive_session: Default::default(),
58            active_inline_sessions: DomRefCell::new(Vec::new()),
59            test: Default::default(),
60            pipeline,
61        }
62    }
63
64    pub(crate) fn new(window: &Window, can_gc: CanGc) -> DomRoot<XRSystem> {
65        reflect_dom_object(
66            Box::new(XRSystem::new_inherited(window.pipeline_id())),
67            window,
68            can_gc,
69        )
70    }
71
72    pub(crate) fn pending_or_active_session(&self) -> bool {
73        self.pending_immersive_session.get() || self.active_immersive_session.get().is_some()
74    }
75
76    pub(crate) fn set_pending(&self) {
77        self.pending_immersive_session.set(true)
78    }
79
80    pub(crate) fn set_active_immersive_session(&self, session: &XRSession) {
81        // XXXManishearth when we support non-immersive (inline) sessions we should
82        // ensure they never reach these codepaths
83        self.pending_immersive_session.set(false);
84        self.active_immersive_session.set(Some(session))
85    }
86
87    /// <https://immersive-web.github.io/webxr/#ref-for-eventdef-xrsession-end>
88    pub(crate) fn end_session(&self, session: &XRSession) {
89        // Step 3
90        if let Some(active) = self.active_immersive_session.get() {
91            if Dom::from_ref(&*active) == Dom::from_ref(session) {
92                self.active_immersive_session.set(None);
93                // Dirty the canvas, since it has been skipping this step whilst in immersive
94                // mode
95                session.dirty_layers();
96            }
97        }
98        self.active_inline_sessions
99            .borrow_mut()
100            .retain(|sess| Dom::from_ref(&**sess) != Dom::from_ref(session));
101    }
102}
103
104impl Convert<SessionMode> for XRSessionMode {
105    fn convert(self) -> SessionMode {
106        match self {
107            XRSessionMode::Immersive_vr => SessionMode::ImmersiveVR,
108            XRSessionMode::Immersive_ar => SessionMode::ImmersiveAR,
109            XRSessionMode::Inline => SessionMode::Inline,
110        }
111    }
112}
113
114impl XRSystemMethods<crate::DomTypeHolder> for XRSystem {
115    /// <https://immersive-web.github.io/webxr/#dom-xr-issessionsupported>
116    fn IsSessionSupported(&self, mode: XRSessionMode, can_gc: CanGc) -> Rc<Promise> {
117        // XXXManishearth this should select an XR device first
118        let promise = Promise::new(&self.global(), can_gc);
119        let mut trusted = Some(TrustedPromise::new(promise.clone()));
120        let global = self.global();
121        let task_source = global
122            .task_manager()
123            .dom_manipulation_task_source()
124            .to_sendable();
125        let (sender, receiver) = ipc::channel(global.time_profiler_chan().clone()).unwrap();
126        ROUTER.add_typed_route(
127            receiver.to_ipc_receiver(),
128            Box::new(move |message| {
129                // router doesn't know this is only called once
130                let trusted = if let Some(trusted) = trusted.take() {
131                    trusted
132                } else {
133                    error!("supportsSession callback called twice!");
134                    return;
135                };
136                let message: Result<(), webxr_api::Error> = if let Ok(message) = message {
137                    message
138                } else {
139                    error!("supportsSession callback given incorrect payload");
140                    return;
141                };
142                if let Ok(()) = message {
143                    task_source.queue(trusted.resolve_task(true));
144                } else {
145                    task_source.queue(trusted.resolve_task(false));
146                };
147            }),
148        );
149        if let Some(mut r) = global.as_window().webxr_registry() {
150            r.supports_session(mode.convert(), sender);
151        }
152
153        promise
154    }
155
156    /// <https://immersive-web.github.io/webxr/#dom-xr-requestsession>
157    #[allow(unsafe_code)]
158    fn RequestSession(
159        &self,
160        mode: XRSessionMode,
161        init: RootedTraceableBox<XRSessionInit>,
162        comp: InRealm,
163        can_gc: CanGc,
164    ) -> Rc<Promise> {
165        let global = self.global();
166        let window = global.as_window();
167        let promise = Promise::new_in_current_realm(comp, can_gc);
168
169        if mode != XRSessionMode::Inline {
170            if !ScriptThread::is_user_interacting() {
171                if pref!(dom_webxr_unsafe_assume_user_intent) {
172                    warn!(
173                        "The dom.webxr.unsafe-assume-user-intent preference assumes user intent to enter WebXR."
174                    );
175                } else {
176                    promise.reject_error(Error::Security, can_gc);
177                    return promise;
178                }
179            }
180
181            if self.pending_or_active_session() {
182                promise.reject_error(Error::InvalidState, can_gc);
183                return promise;
184            }
185
186            self.set_pending();
187        }
188
189        let mut required_features = vec![];
190        let mut optional_features = vec![];
191        let cx = GlobalScope::get_cx();
192
193        if let Some(ref r) = init.requiredFeatures {
194            for feature in r {
195                if let Ok(ConversionResult::Success(s)) =
196                    String::safe_from_jsval(cx, feature.handle(), ())
197                {
198                    required_features.push(s)
199                } else {
200                    warn!("Unable to convert required feature to string");
201                    if mode != XRSessionMode::Inline {
202                        self.pending_immersive_session.set(false);
203                    }
204                    promise.reject_error(Error::NotSupported, can_gc);
205                    return promise;
206                }
207            }
208        }
209
210        if let Some(ref o) = init.optionalFeatures {
211            for feature in o {
212                if let Ok(ConversionResult::Success(s)) =
213                    String::safe_from_jsval(cx, feature.handle(), ())
214                {
215                    optional_features.push(s)
216                } else {
217                    warn!("Unable to convert optional feature to string");
218                }
219            }
220        }
221
222        if !required_features.contains(&"viewer".to_string()) {
223            required_features.push("viewer".to_string());
224        }
225
226        if !required_features.contains(&"local".to_string()) && mode != XRSessionMode::Inline {
227            required_features.push("local".to_string());
228        }
229
230        let init = SessionInit {
231            required_features,
232            optional_features,
233            first_person_observer_view: pref!(dom_webxr_first_person_observer_view),
234        };
235
236        let mut trusted = Some(TrustedPromise::new(promise.clone()));
237        let this = Trusted::new(self);
238        let task_source = global
239            .task_manager()
240            .dom_manipulation_task_source()
241            .to_sendable();
242        let (sender, receiver) = ipc::channel(global.time_profiler_chan().clone()).unwrap();
243        let (frame_sender, frame_receiver) = ipc_crate::channel().unwrap();
244        let mut frame_receiver = Some(frame_receiver);
245        ROUTER.add_typed_route(
246            receiver.to_ipc_receiver(),
247            Box::new(move |message| {
248                // router doesn't know this is only called once
249                let trusted = trusted.take().unwrap();
250                let this = this.clone();
251                let frame_receiver = frame_receiver.take().unwrap();
252                let message: Result<Session, webxr_api::Error> = if let Ok(message) = message {
253                    message
254                } else {
255                    error!("requestSession callback given incorrect payload");
256                    return;
257                };
258                task_source.queue(task!(request_session: move || {
259                    this.root().session_obtained(message, trusted.root(), mode, frame_receiver, CanGc::note());
260                }));
261            }),
262        );
263        if let Some(mut r) = window.webxr_registry() {
264            r.request_session(mode.convert(), init, sender, frame_sender);
265        }
266        promise
267    }
268
269    // https://github.com/immersive-web/webxr-test-api/blob/master/explainer.md
270    fn Test(&self) -> DomRoot<XRTest> {
271        self.test
272            .or_init(|| XRTest::new(&self.global(), CanGc::note()))
273    }
274}
275
276impl XRSystem {
277    fn session_obtained(
278        &self,
279        response: Result<Session, XRError>,
280        promise: Rc<Promise>,
281        mode: XRSessionMode,
282        frame_receiver: IpcReceiver<Frame>,
283        can_gc: CanGc,
284    ) {
285        let session = match response {
286            Ok(session) => session,
287            Err(e) => {
288                warn!("Error requesting XR session: {:?}", e);
289                if mode != XRSessionMode::Inline {
290                    self.pending_immersive_session.set(false);
291                }
292                promise.reject_error(Error::NotSupported, can_gc);
293                return;
294            },
295        };
296        let session = XRSession::new(
297            self.global().as_window(),
298            session,
299            mode,
300            frame_receiver,
301            CanGc::note(),
302        );
303        if mode == XRSessionMode::Inline {
304            self.active_inline_sessions
305                .borrow_mut()
306                .push(Dom::from_ref(&*session));
307        } else {
308            self.set_active_immersive_session(&session);
309        }
310        promise.resolve_native(&session, can_gc);
311        // https://github.com/immersive-web/webxr/issues/961
312        // This must be called _after_ the promise is resolved
313        session.setup_initial_inputs();
314    }
315
316    // https://github.com/immersive-web/navigation/issues/10
317    pub(crate) fn dispatch_sessionavailable(&self) {
318        let xr = Trusted::new(self);
319        self.global()
320            .task_manager()
321            .dom_manipulation_task_source()
322            .queue(
323                task!(fire_sessionavailable_event: move || {
324                    // The sessionavailable event indicates user intent to enter an XR session
325                    let xr = xr.root();
326                    let interacting = ScriptThread::is_user_interacting();
327                    ScriptThread::set_user_interacting(true);
328                    xr.upcast::<EventTarget>().fire_bubbling_event(atom!("sessionavailable"), CanGc::note());
329                    ScriptThread::set_user_interacting(interacting);
330                })
331            );
332    }
333}