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;
6
7use dom_struct::dom_struct;
8use ipc_channel::ipc::{self as ipc_crate, IpcReceiver};
9use ipc_channel::router::ROUTER;
10use js::context::JSContext;
11use js::realm::CurrentRealm;
12use profile_traits::generic_callback::GenericCallback as ProfileGenericCallback;
13use profile_traits::ipc;
14use script_bindings::cell::DomRefCell;
15use script_bindings::reflector::reflect_dom_object;
16use servo_base::id::PipelineId;
17use servo_config::pref;
18use webxr_api::{Error as XRError, Frame, Session, SessionInit, SessionMode};
19
20use crate::conversions::Convert;
21use crate::dom::bindings::codegen::Bindings::XRSystemBinding::{
22    XRSessionInit, XRSessionMode, XRSystemMethods,
23};
24use crate::dom::bindings::conversions::{ConversionResult, FromJSValConvertible};
25use crate::dom::bindings::error::Error;
26use crate::dom::bindings::inheritance::Castable;
27use crate::dom::bindings::refcounted::{Trusted, TrustedPromise};
28use crate::dom::bindings::reflector::DomGlobal;
29use crate::dom::bindings::root::{Dom, DomRoot, MutNullableDom};
30use crate::dom::bindings::trace::RootedTraceableBox;
31use crate::dom::eventtarget::EventTarget;
32use crate::dom::gamepad::Gamepad;
33use crate::dom::promise::{Promise, RootedPromise};
34use crate::dom::window::Window;
35use crate::dom::xrsession::XRSession;
36use crate::dom::xrtest::XRTest;
37use crate::event_loop::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(cx: &mut JSContext, window: &Window) -> DomRoot<XRSystem> {
65        reflect_dom_object(
66            cx,
67            Box::new(XRSystem::new_inherited(window.pipeline_id())),
68            window,
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            Dom::from_ref(&*active) == Dom::from_ref(session)
92        {
93            self.active_immersive_session.set(None);
94            // Dirty the canvas, since it has been skipping this step whilst in immersive
95            // mode
96            session.dirty_layers();
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, cx: &mut CurrentRealm, mode: XRSessionMode) -> RootedPromise {
117        // XXXManishearth this should select an XR device first
118        let promise = Promise::new_in_realm_rooted(cx);
119        let mut trusted = Some(TrustedPromise::from(&promise));
120        let global = self.global();
121        let task_source = global
122            .task_manager()
123            .dom_manipulation_task_source()
124            .to_sendable();
125
126        let callback = ProfileGenericCallback::new(move |message| {
127            // router doesn't know this is only called once
128            let trusted = if let Some(trusted) = trusted.take() {
129                trusted
130            } else {
131                error!("supportsSession callback called twice!");
132                return;
133            };
134            let message: Result<(), webxr_api::Error> = if let Ok(message) = message {
135                message
136            } else {
137                error!("supportsSession callback given incorrect payload");
138                return;
139            };
140            if let Ok(()) = message {
141                task_source.queue(trusted.resolve_task(true));
142            } else {
143                task_source.queue(trusted.resolve_task(false));
144            };
145        })
146        .expect("Could not create callback");
147
148        if let Some(mut r) = global.as_window().webxr_registry() {
149            r.supports_session(mode.convert(), callback);
150        }
151
152        promise
153    }
154
155    /// <https://immersive-web.github.io/webxr/#dom-xr-requestsession>
156    fn RequestSession(
157        &self,
158        realm: &mut CurrentRealm,
159        mode: XRSessionMode,
160        init: RootedTraceableBox<XRSessionInit>,
161    ) -> RootedPromise {
162        let global = self.global();
163        let window = global.as_window();
164        let promise = Promise::new_in_realm_rooted(realm);
165
166        if mode != XRSessionMode::Inline {
167            if !ScriptThread::is_user_interacting() {
168                if pref!(dom_webxr_unsafe_assume_user_intent) {
169                    warn!(
170                        "The dom.webxr.unsafe-assume-user-intent preference assumes user intent to enter WebXR."
171                    );
172                } else {
173                    promise.reject_error(realm, Error::Security(None));
174                    return promise;
175                }
176            }
177
178            if self.pending_or_active_session() {
179                promise.reject_error(realm, Error::InvalidState(None));
180                return promise;
181            }
182
183            self.set_pending();
184        }
185
186        let mut required_features = vec![];
187        let mut optional_features = vec![];
188
189        if let Some(ref r) = init.requiredFeatures {
190            for feature in r {
191                if let Ok(ConversionResult::Success(s)) =
192                    String::from_jsval(realm, feature.handle(), ())
193                {
194                    required_features.push(s)
195                } else {
196                    warn!("Unable to convert required feature to string");
197                    if mode != XRSessionMode::Inline {
198                        self.pending_immersive_session.set(false);
199                    }
200                    promise.reject_error(realm, Error::NotSupported(None));
201                    return promise;
202                }
203            }
204        }
205
206        if let Some(ref o) = init.optionalFeatures {
207            for feature in o {
208                if let Ok(ConversionResult::Success(s)) =
209                    String::from_jsval(realm, feature.handle(), ())
210                {
211                    optional_features.push(s)
212                } else {
213                    warn!("Unable to convert optional feature to string");
214                }
215            }
216        }
217
218        if !required_features.contains(&"viewer".to_string()) {
219            required_features.push("viewer".to_string());
220        }
221
222        if !required_features.contains(&"local".to_string()) && mode != XRSessionMode::Inline {
223            required_features.push("local".to_string());
224        }
225
226        let init = SessionInit {
227            required_features,
228            optional_features,
229            first_person_observer_view: pref!(dom_webxr_first_person_observer_view),
230        };
231
232        let mut trusted = Some(TrustedPromise::from(&promise));
233        let this = Trusted::new(self);
234        let task_source = global
235            .task_manager()
236            .dom_manipulation_task_source()
237            .to_sendable();
238        let (sender, receiver) = ipc::channel(global.time_profiler_chan().clone()).unwrap();
239        let (frame_sender, frame_receiver) = ipc_crate::channel().unwrap();
240        let mut frame_receiver = Some(frame_receiver);
241        ROUTER.add_typed_route(
242            receiver.to_ipc_receiver(),
243            Box::new(move |message| {
244                // router doesn't know this is only called once
245                let trusted = trusted.take().unwrap();
246                let this = this.clone();
247                let frame_receiver = frame_receiver.take().unwrap();
248                let message: Result<Session, webxr_api::Error> = if let Ok(message) = message {
249                    message
250                } else {
251                    error!("requestSession callback given incorrect payload");
252                    return;
253                };
254                task_source.queue(task!(request_session: move |cx| {
255                    this.root().session_obtained(cx, message, &trusted.root(cx), mode, frame_receiver);
256                }));
257            }),
258        );
259        if let Some(mut r) = window.webxr_registry() {
260            r.request_session(mode.convert(), init, sender, frame_sender);
261        }
262        promise
263    }
264
265    /// <https://github.com/immersive-web/webxr-test-api/blob/master/explainer.md>
266    fn Test(&self, cx: &mut JSContext) -> DomRoot<XRTest> {
267        self.test.or_init(|| XRTest::new(cx, &self.global()))
268    }
269}
270
271impl XRSystem {
272    fn session_obtained(
273        &self,
274        cx: &mut JSContext,
275        response: Result<Session, XRError>,
276        promise: &RootedPromise,
277        mode: XRSessionMode,
278        frame_receiver: IpcReceiver<Frame>,
279    ) {
280        let session = match response {
281            Ok(session) => session,
282            Err(e) => {
283                warn!("Error requesting XR session: {:?}", e);
284                if mode != XRSessionMode::Inline {
285                    self.pending_immersive_session.set(false);
286                }
287                promise.reject_error(cx, Error::NotSupported(None));
288                return;
289            },
290        };
291        let session = XRSession::new(cx, self.global().as_window(), session, mode, frame_receiver);
292        if mode == XRSessionMode::Inline {
293            self.active_inline_sessions
294                .borrow_mut()
295                .push(Dom::from_ref(&*session));
296        } else {
297            self.set_active_immersive_session(&session);
298        }
299        promise.resolve_native(cx, &session);
300        // https://github.com/immersive-web/webxr/issues/961
301        // This must be called _after_ the promise is resolved
302        session.setup_initial_inputs();
303    }
304
305    // https://github.com/immersive-web/navigation/issues/10
306    pub(crate) fn dispatch_sessionavailable(&self) {
307        let xr = Trusted::new(self);
308        self.global()
309            .task_manager()
310            .dom_manipulation_task_source()
311            .queue(task!(fire_sessionavailable_event: move |cx| {
312                // The sessionavailable event indicates user intent to enter an XR session
313                let xr = xr.root();
314                    let _guard = ScriptThread::user_interacting_guard();
315                    xr.upcast::<EventTarget>().fire_bubbling_event(cx, atom!("sessionavailable"));
316            }));
317    }
318}