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