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