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