Skip to main content

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