script/dom/webxr/
xrsession.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::collections::HashMap;
7use std::f64::consts::{FRAC_PI_2, PI};
8use std::rc::Rc;
9use std::{mem, ptr};
10
11use base::cross_process_instant::CrossProcessInstant;
12use dom_struct::dom_struct;
13use euclid::{RigidTransform3D, Transform3D, Vector3D};
14use ipc_channel::ipc::IpcReceiver;
15use ipc_channel::router::ROUTER;
16use js::jsapi::JSObject;
17use js::rust::MutableHandleValue;
18use js::typedarray::Float32Array;
19use profile_traits::ipc;
20use stylo_atoms::Atom;
21use webxr_api::{
22    self, ApiSpace, ContextId as WebXRContextId, Display, EntityTypes, EnvironmentBlendMode,
23    Event as XREvent, Frame, FrameUpdateEvent, HitTestId, HitTestSource, InputFrame, InputId, Ray,
24    SelectEvent, SelectKind, Session, SessionId, View, Viewer, Visibility, util,
25};
26
27use crate::conversions::Convert;
28use crate::canvas_context::CanvasContext;
29use crate::dom::bindings::trace::HashMapTracedValues;
30use crate::dom::bindings::buffer_source::create_buffer_source;
31use crate::dom::bindings::callback::ExceptionHandling;
32use crate::dom::bindings::cell::DomRefCell;
33use crate::dom::bindings::codegen::Bindings::NavigatorBinding::Navigator_Binding::NavigatorMethods;
34use crate::dom::bindings::codegen::Bindings::WindowBinding::Window_Binding::WindowMethods;
35use crate::dom::bindings::codegen::Bindings::XRHitTestSourceBinding::{
36    XRHitTestOptionsInit, XRHitTestTrackableType,
37};
38use crate::dom::bindings::codegen::Bindings::XRInputSourceArrayBinding::XRInputSourceArray_Binding::XRInputSourceArrayMethods;
39use crate::dom::bindings::codegen::Bindings::XRReferenceSpaceBinding::XRReferenceSpaceType;
40use crate::dom::bindings::codegen::Bindings::XRRenderStateBinding::{
41    XRRenderStateInit, XRRenderStateMethods,
42};
43use crate::dom::bindings::codegen::Bindings::XRSessionBinding::{
44    XREnvironmentBlendMode, XRFrameRequestCallback, XRInteractionMode, XRSessionMethods,
45    XRVisibilityState,
46};
47use crate::dom::bindings::codegen::Bindings::XRSystemBinding::XRSessionMode;
48use crate::dom::bindings::error::{Error, ErrorResult};
49use crate::dom::bindings::inheritance::Castable;
50use crate::dom::bindings::num::Finite;
51use crate::dom::bindings::refcounted::Trusted;
52use crate::dom::bindings::reflector::{reflect_dom_object, DomGlobal};
53use crate::dom::bindings::root::{Dom, DomRoot, MutDom, MutNullableDom};
54use crate::dom::bindings::utils::to_frozen_array;
55use crate::dom::event::Event;
56use crate::dom::eventtarget::EventTarget;
57use crate::dom::promise::Promise;
58use crate::dom::window::Window;
59use crate::dom::xrboundedreferencespace::XRBoundedReferenceSpace;
60use crate::dom::xrframe::XRFrame;
61use crate::dom::xrhittestsource::XRHitTestSource;
62use crate::dom::xrinputsourcearray::XRInputSourceArray;
63use crate::dom::xrinputsourceevent::XRInputSourceEvent;
64use crate::dom::xrreferencespace::XRReferenceSpace;
65use crate::dom::xrreferencespaceevent::XRReferenceSpaceEvent;
66use crate::dom::xrrenderstate::XRRenderState;
67use crate::dom::xrrigidtransform::XRRigidTransform;
68use crate::dom::xrsessionevent::XRSessionEvent;
69use crate::dom::xrspace::XRSpace;
70use crate::realms::InRealm;
71use crate::script_runtime::JSContext;
72use crate::script_runtime::CanGc;
73
74#[dom_struct]
75pub(crate) struct XRSession {
76    eventtarget: EventTarget,
77    blend_mode: XREnvironmentBlendMode,
78    mode: XRSessionMode,
79    visibility_state: Cell<XRVisibilityState>,
80    viewer_space: MutNullableDom<XRSpace>,
81    #[ignore_malloc_size_of = "defined in webxr"]
82    #[no_trace]
83    session: DomRefCell<Session>,
84    frame_requested: Cell<bool>,
85    pending_render_state: MutNullableDom<XRRenderState>,
86    active_render_state: MutDom<XRRenderState>,
87    /// Cached projection matrix for inline sessions
88    #[no_trace]
89    inline_projection_matrix: DomRefCell<Transform3D<f32, Viewer, Display>>,
90
91    next_raf_id: Cell<i32>,
92    #[ignore_malloc_size_of = "closures are hard"]
93    raf_callback_list: DomRefCell<Vec<(i32, Option<Rc<XRFrameRequestCallback>>)>>,
94    #[ignore_malloc_size_of = "closures are hard"]
95    current_raf_callback_list: DomRefCell<Vec<(i32, Option<Rc<XRFrameRequestCallback>>)>>,
96    input_sources: Dom<XRInputSourceArray>,
97    // Any promises from calling end()
98    #[ignore_malloc_size_of = "promises are hard"]
99    end_promises: DomRefCell<Vec<Rc<Promise>>>,
100    /// <https://immersive-web.github.io/webxr/#ended>
101    ended: Cell<bool>,
102    #[ignore_malloc_size_of = "defined in webxr"]
103    #[no_trace]
104    next_hit_test_id: Cell<HitTestId>,
105    #[ignore_malloc_size_of = "defined in webxr"]
106    pending_hit_test_promises: DomRefCell<HashMapTracedValues<HitTestId, Rc<Promise>>>,
107    /// Opaque framebuffers need to know the session is "outside of a requestAnimationFrame"
108    /// <https://immersive-web.github.io/webxr/#opaque-framebuffer>
109    outside_raf: Cell<bool>,
110    #[ignore_malloc_size_of = "defined in webxr"]
111    #[no_trace]
112    input_frames: DomRefCell<HashMap<InputId, InputFrame>>,
113    framerate: Cell<f32>,
114    #[ignore_malloc_size_of = "promises are hard"]
115    update_framerate_promise: DomRefCell<Option<Rc<Promise>>>,
116    reference_spaces: DomRefCell<Vec<Dom<XRReferenceSpace>>>,
117}
118
119impl XRSession {
120    fn new_inherited(
121        session: Session,
122        render_state: &XRRenderState,
123        input_sources: &XRInputSourceArray,
124        mode: XRSessionMode,
125    ) -> XRSession {
126        XRSession {
127            eventtarget: EventTarget::new_inherited(),
128            blend_mode: session.environment_blend_mode().convert(),
129            mode,
130            visibility_state: Cell::new(XRVisibilityState::Visible),
131            viewer_space: Default::default(),
132            session: DomRefCell::new(session),
133            frame_requested: Cell::new(false),
134            pending_render_state: MutNullableDom::new(None),
135            active_render_state: MutDom::new(render_state),
136            inline_projection_matrix: Default::default(),
137
138            next_raf_id: Cell::new(0),
139            raf_callback_list: DomRefCell::new(vec![]),
140            current_raf_callback_list: DomRefCell::new(vec![]),
141            input_sources: Dom::from_ref(input_sources),
142            end_promises: DomRefCell::new(vec![]),
143            ended: Cell::new(false),
144            next_hit_test_id: Cell::new(HitTestId(0)),
145            pending_hit_test_promises: DomRefCell::new(HashMapTracedValues::new()),
146            outside_raf: Cell::new(true),
147            input_frames: DomRefCell::new(HashMap::new()),
148            framerate: Cell::new(0.0),
149            update_framerate_promise: DomRefCell::new(None),
150            reference_spaces: DomRefCell::new(Vec::new()),
151        }
152    }
153
154    pub(crate) fn new(
155        window: &Window,
156        session: Session,
157        mode: XRSessionMode,
158        frame_receiver: IpcReceiver<Frame>,
159        can_gc: CanGc,
160    ) -> DomRoot<XRSession> {
161        let ivfov = if mode == XRSessionMode::Inline {
162            Some(FRAC_PI_2)
163        } else {
164            None
165        };
166        let render_state = XRRenderState::new(window, 0.1, 1000.0, ivfov, None, Vec::new(), can_gc);
167        let input_sources = XRInputSourceArray::new(window, can_gc);
168        let ret = reflect_dom_object(
169            Box::new(XRSession::new_inherited(
170                session,
171                &render_state,
172                &input_sources,
173                mode,
174            )),
175            window,
176            can_gc,
177        );
178        ret.attach_event_handler();
179        ret.setup_raf_loop(frame_receiver);
180        ret
181    }
182
183    pub(crate) fn with_session<R, F: FnOnce(&Session) -> R>(&self, with: F) -> R {
184        let session = self.session.borrow();
185        with(&session)
186    }
187
188    pub(crate) fn is_ended(&self) -> bool {
189        self.ended.get()
190    }
191
192    pub(crate) fn is_immersive(&self) -> bool {
193        self.mode != XRSessionMode::Inline
194    }
195
196    // https://immersive-web.github.io/layers/#feature-descriptor-layers
197    pub(crate) fn has_layers_feature(&self) -> bool {
198        // We do not support creating layers other than projection layers
199        // https://github.com/servo/servo/issues/27493
200        false
201    }
202
203    fn setup_raf_loop(&self, frame_receiver: IpcReceiver<Frame>) {
204        let this = Trusted::new(self);
205        let global = self.global();
206        let task_source = global
207            .task_manager()
208            .dom_manipulation_task_source()
209            .to_sendable();
210        ROUTER.add_typed_route(
211            frame_receiver,
212            Box::new(move |message| {
213                let frame: Frame = message.unwrap();
214                let time = CrossProcessInstant::now();
215                let this = this.clone();
216                task_source.queue(task!(xr_raf_callback: move || {
217                    this.root().raf_callback(frame, time, CanGc::note());
218                }));
219            }),
220        );
221
222        self.session.borrow_mut().start_render_loop();
223    }
224
225    pub(crate) fn is_outside_raf(&self) -> bool {
226        self.outside_raf.get()
227    }
228
229    fn attach_event_handler(&self) {
230        let this = Trusted::new(self);
231        let global = self.global();
232        let task_source = global
233            .task_manager()
234            .dom_manipulation_task_source()
235            .to_sendable();
236        let (sender, receiver) = ipc::channel(global.time_profiler_chan().clone()).unwrap();
237
238        ROUTER.add_typed_route(
239            receiver.to_ipc_receiver(),
240            Box::new(move |message| {
241                let this = this.clone();
242                task_source.queue(task!(xr_event_callback: move || {
243                    this.root().event_callback(message.unwrap(), CanGc::note());
244                }));
245            }),
246        );
247
248        // request animation frame
249        self.session.borrow_mut().set_event_dest(sender);
250    }
251
252    // Must be called after the promise for session creation is resolved
253    // https://github.com/immersive-web/webxr/issues/961
254    //
255    // This enables content that assumes all input sources are accompanied
256    // by an inputsourceschange event to work properly. Without
257    pub(crate) fn setup_initial_inputs(&self) {
258        let initial_inputs = self.session.borrow().initial_inputs().to_owned();
259
260        if initial_inputs.is_empty() {
261            // do not fire an empty event
262            return;
263        }
264
265        let this = Trusted::new(self);
266        // Queue a task so that it runs after resolve()'s microtasks complete
267        // so that content has a chance to attach a listener for inputsourceschange
268        self.global()
269            .task_manager()
270            .dom_manipulation_task_source()
271            .queue(task!(session_initial_inputs: move || {
272                let this = this.root();
273                this.input_sources.add_input_sources(&this, &initial_inputs, CanGc::note());
274            }));
275    }
276
277    fn event_callback(&self, event: XREvent, can_gc: CanGc) {
278        match event {
279            XREvent::SessionEnd => {
280                // https://immersive-web.github.io/webxr/#shut-down-the-session
281                // Step 2
282                self.ended.set(true);
283                // Step 3-4
284                self.global().as_window().Navigator().Xr().end_session(self);
285                // Step 5: We currently do not have any such promises
286                // Step 6 is happening n the XR session
287                // https://immersive-web.github.io/webxr/#dom-xrsession-end step 3
288                for promise in self.end_promises.borrow_mut().drain(..) {
289                    promise.resolve_native(&(), can_gc);
290                }
291                // Step 7
292                let event = XRSessionEvent::new(
293                    self.global().as_window(),
294                    atom!("end"),
295                    false,
296                    false,
297                    self,
298                    can_gc,
299                );
300                event.upcast::<Event>().fire(self.upcast(), can_gc);
301            },
302            XREvent::Select(input, kind, ty, frame) => {
303                use stylo_atoms::Atom;
304                const START_ATOMS: [Atom; 2] = [atom!("selectstart"), atom!("squeezestart")];
305                const EVENT_ATOMS: [Atom; 2] = [atom!("select"), atom!("squeeze")];
306                const END_ATOMS: [Atom; 2] = [atom!("selectend"), atom!("squeezeend")];
307
308                // https://immersive-web.github.io/webxr/#primary-action
309                let source = self.input_sources.find(input);
310                let atom_index = if kind == SelectKind::Squeeze { 1 } else { 0 };
311                if let Some(source) = source {
312                    let frame = XRFrame::new(self.global().as_window(), self, frame, can_gc);
313                    frame.set_active(true);
314                    if ty == SelectEvent::Start {
315                        let event = XRInputSourceEvent::new(
316                            self.global().as_window(),
317                            START_ATOMS[atom_index].clone(),
318                            false,
319                            false,
320                            &frame,
321                            &source,
322                            can_gc,
323                        );
324                        event.upcast::<Event>().fire(self.upcast(), can_gc);
325                    } else {
326                        if ty == SelectEvent::Select {
327                            let event = XRInputSourceEvent::new(
328                                self.global().as_window(),
329                                EVENT_ATOMS[atom_index].clone(),
330                                false,
331                                false,
332                                &frame,
333                                &source,
334                                can_gc,
335                            );
336                            event.upcast::<Event>().fire(self.upcast(), can_gc);
337                        }
338                        let event = XRInputSourceEvent::new(
339                            self.global().as_window(),
340                            END_ATOMS[atom_index].clone(),
341                            false,
342                            false,
343                            &frame,
344                            &source,
345                            can_gc,
346                        );
347                        event.upcast::<Event>().fire(self.upcast(), can_gc);
348                    }
349                    frame.set_active(false);
350                }
351            },
352            XREvent::VisibilityChange(v) => {
353                let v = match v {
354                    Visibility::Visible => XRVisibilityState::Visible,
355                    Visibility::VisibleBlurred => XRVisibilityState::Visible_blurred,
356                    Visibility::Hidden => XRVisibilityState::Hidden,
357                };
358                self.visibility_state.set(v);
359                let event = XRSessionEvent::new(
360                    self.global().as_window(),
361                    atom!("visibilitychange"),
362                    false,
363                    false,
364                    self,
365                    can_gc,
366                );
367                event.upcast::<Event>().fire(self.upcast(), can_gc);
368                // The page may be visible again, dirty the layers
369                // This also wakes up the event loop if necessary
370                self.dirty_layers();
371            },
372            XREvent::AddInput(info) => {
373                self.input_sources.add_input_sources(self, &[info], can_gc);
374            },
375            XREvent::RemoveInput(id) => {
376                self.input_sources.remove_input_source(self, id, can_gc);
377            },
378            XREvent::UpdateInput(id, source) => {
379                self.input_sources
380                    .add_remove_input_source(self, id, source, can_gc);
381            },
382            XREvent::InputChanged(id, frame) => {
383                self.input_frames.borrow_mut().insert(id, frame);
384            },
385            XREvent::ReferenceSpaceChanged(base_space, transform) => {
386                self.reference_spaces
387                    .borrow()
388                    .iter()
389                    .filter(|space| {
390                        let base = match space.ty() {
391                            XRReferenceSpaceType::Local => webxr_api::BaseSpace::Local,
392                            XRReferenceSpaceType::Viewer => webxr_api::BaseSpace::Viewer,
393                            XRReferenceSpaceType::Local_floor => webxr_api::BaseSpace::Floor,
394                            XRReferenceSpaceType::Bounded_floor => {
395                                webxr_api::BaseSpace::BoundedFloor
396                            },
397                            _ => panic!("unsupported reference space found"),
398                        };
399                        base == base_space
400                    })
401                    .for_each(|space| {
402                        let offset =
403                            XRRigidTransform::new(self.global().as_window(), transform, can_gc);
404                        let event = XRReferenceSpaceEvent::new(
405                            self.global().as_window(),
406                            atom!("reset"),
407                            false,
408                            false,
409                            space,
410                            Some(&*offset),
411                            can_gc,
412                        );
413                        event.upcast::<Event>().fire(space.upcast(), can_gc);
414                    });
415            },
416        }
417    }
418
419    /// <https://immersive-web.github.io/webxr/#xr-animation-frame>
420    fn raf_callback(&self, mut frame: Frame, time: CrossProcessInstant, can_gc: CanGc) {
421        debug!("WebXR RAF callback {:?}", frame);
422
423        // Step 1-2 happen in the xebxr device thread
424
425        // Step 3
426        if let Some(pending) = self.pending_render_state.take() {
427            // https://immersive-web.github.io/webxr/#apply-the-pending-render-state
428            // (Steps 1-4 are implicit)
429            // Step 5
430            self.active_render_state.set(&pending);
431            // Step 6-7: XXXManishearth handle inlineVerticalFieldOfView
432
433            if !self.is_immersive() {
434                self.update_inline_projection_matrix()
435            }
436        }
437
438        // TODO: how does this fit the webxr spec?
439        for event in frame.events.drain(..) {
440            self.handle_frame_event(event, can_gc);
441        }
442
443        // Step 4
444        // TODO: what should this check be?
445        // This is checking that the new render state has the same
446        // layers as the frame.
447        // Related to https://github.com/immersive-web/webxr/issues/1051
448        if !self
449            .active_render_state
450            .get()
451            .has_sub_images(&frame.sub_images[..])
452        {
453            // If the frame has different layers than the render state,
454            // we just return early, drawing a blank frame.
455            // This can result in flickering when the render state is changed.
456            // TODO: it would be better to not render anything until the next frame.
457            warn!("Rendering blank XR frame");
458            self.session.borrow_mut().render_animation_frame();
459            return;
460        }
461
462        // Step 5: XXXManishearth handle inline session
463
464        // Step 6-7
465        {
466            let mut current = self.current_raf_callback_list.borrow_mut();
467            assert!(current.is_empty());
468            mem::swap(&mut *self.raf_callback_list.borrow_mut(), &mut current);
469        }
470
471        let time = self.global().performance().to_dom_high_res_time_stamp(time);
472        let frame = XRFrame::new(self.global().as_window(), self, frame, CanGc::note());
473
474        // Step 8-9
475        frame.set_active(true);
476        frame.set_animation_frame(true);
477
478        // Step 10
479        self.apply_frame_updates(&frame);
480
481        // TODO: how does this fit with the webxr and xr layers specs?
482        self.layers_begin_frame(&frame);
483
484        // Step 11-12
485        self.outside_raf.set(false);
486        let len = self.current_raf_callback_list.borrow().len();
487        for i in 0..len {
488            let callback = self.current_raf_callback_list.borrow()[i].1.clone();
489            if let Some(callback) = callback {
490                let _ = callback.Call__(time, &frame, ExceptionHandling::Report, can_gc);
491            }
492        }
493        self.outside_raf.set(true);
494        *self.current_raf_callback_list.borrow_mut() = vec![];
495
496        // TODO: how does this fit with the webxr and xr layers specs?
497        self.layers_end_frame(&frame);
498
499        // Step 13
500        frame.set_active(false);
501
502        // TODO: how does this fit the webxr spec?
503        self.session.borrow_mut().render_animation_frame();
504    }
505
506    fn update_inline_projection_matrix(&self) {
507        debug_assert!(!self.is_immersive());
508        let render_state = self.active_render_state.get();
509        let size = if let Some(base) = render_state.GetBaseLayer() {
510            base.size()
511        } else {
512            return;
513        };
514        let mut clip_planes = util::ClipPlanes::default();
515        let near = *render_state.DepthNear() as f32;
516        let far = *render_state.DepthFar() as f32;
517        clip_planes.update(near, far);
518        let top = *render_state
519            .GetInlineVerticalFieldOfView()
520            .expect("IVFOV should be non null for inline sessions") /
521            2.;
522        let top = near * top.tan() as f32;
523        let bottom = top;
524        let left = top * size.width as f32 / size.height as f32;
525        let right = left;
526        let matrix = util::frustum_to_projection_matrix(left, right, top, bottom, clip_planes);
527        *self.inline_projection_matrix.borrow_mut() = matrix;
528    }
529
530    /// Constructs a View suitable for inline sessions using the inlineVerticalFieldOfView and canvas size
531    pub(crate) fn inline_view(&self) -> View<Viewer> {
532        debug_assert!(!self.is_immersive());
533        View {
534            // Inline views have no offset
535            transform: RigidTransform3D::identity(),
536            projection: *self.inline_projection_matrix.borrow(),
537        }
538    }
539
540    pub(crate) fn session_id(&self) -> SessionId {
541        self.session.borrow().id()
542    }
543
544    pub(crate) fn dirty_layers(&self) {
545        if let Some(layer) = self.RenderState().GetBaseLayer() {
546            layer.context().mark_as_dirty();
547        }
548    }
549
550    // TODO: how does this align with the layers spec?
551    fn layers_begin_frame(&self, frame: &XRFrame) {
552        if let Some(layer) = self.active_render_state.get().GetBaseLayer() {
553            layer.begin_frame(frame);
554        }
555        self.active_render_state.get().with_layers(|layers| {
556            for layer in layers {
557                layer.begin_frame(frame);
558            }
559        });
560    }
561
562    // TODO: how does this align with the layers spec?
563    fn layers_end_frame(&self, frame: &XRFrame) {
564        if let Some(layer) = self.active_render_state.get().GetBaseLayer() {
565            layer.end_frame(frame);
566        }
567        self.active_render_state.get().with_layers(|layers| {
568            for layer in layers {
569                layer.end_frame(frame);
570            }
571        });
572    }
573
574    /// <https://immersive-web.github.io/webxr/#xrframe-apply-frame-updates>
575    fn apply_frame_updates(&self, _frame: &XRFrame) {
576        // <https://www.w3.org/TR/webxr-gamepads-module-1/#xrframe-apply-gamepad-frame-updates>
577        for (id, frame) in self.input_frames.borrow_mut().drain() {
578            let source = self.input_sources.find(id);
579            if let Some(source) = source {
580                source.update_gamepad_state(frame);
581            }
582        }
583    }
584
585    fn handle_frame_event(&self, event: FrameUpdateEvent, can_gc: CanGc) {
586        match event {
587            FrameUpdateEvent::HitTestSourceAdded(id) => {
588                if let Some(promise) = self.pending_hit_test_promises.borrow_mut().remove(&id) {
589                    promise.resolve_native(
590                        &XRHitTestSource::new(self.global().as_window(), id, self, can_gc),
591                        can_gc,
592                    );
593                } else {
594                    warn!(
595                        "received hit test add request for unknown hit test {:?}",
596                        id
597                    )
598                }
599            },
600            _ => self.session.borrow_mut().apply_event(event),
601        }
602    }
603
604    /// <https://www.w3.org/TR/webxr/#apply-the-nominal-frame-rate>
605    fn apply_nominal_framerate(&self, rate: f32, can_gc: CanGc) {
606        if self.framerate.get() == rate || self.ended.get() {
607            return;
608        }
609
610        self.framerate.set(rate);
611
612        let event = XRSessionEvent::new(
613            self.global().as_window(),
614            Atom::from("frameratechange"),
615            false,
616            false,
617            self,
618            can_gc,
619        );
620        event.upcast::<Event>().fire(self.upcast(), can_gc);
621    }
622}
623
624impl XRSessionMethods<crate::DomTypeHolder> for XRSession {
625    // https://immersive-web.github.io/webxr/#eventdef-xrsession-end
626    event_handler!(end, GetOnend, SetOnend);
627
628    // https://immersive-web.github.io/webxr/#eventdef-xrsession-select
629    event_handler!(select, GetOnselect, SetOnselect);
630
631    // https://immersive-web.github.io/webxr/#eventdef-xrsession-selectstart
632    event_handler!(selectstart, GetOnselectstart, SetOnselectstart);
633
634    // https://immersive-web.github.io/webxr/#eventdef-xrsession-selectend
635    event_handler!(selectend, GetOnselectend, SetOnselectend);
636
637    // https://immersive-web.github.io/webxr/#eventdef-xrsession-squeeze
638    event_handler!(squeeze, GetOnsqueeze, SetOnsqueeze);
639
640    // https://immersive-web.github.io/webxr/#eventdef-xrsession-squeezestart
641    event_handler!(squeezestart, GetOnsqueezestart, SetOnsqueezestart);
642
643    // https://immersive-web.github.io/webxr/#eventdef-xrsession-squeezeend
644    event_handler!(squeezeend, GetOnsqueezeend, SetOnsqueezeend);
645
646    // https://immersive-web.github.io/webxr/#eventdef-xrsession-visibilitychange
647    event_handler!(
648        visibilitychange,
649        GetOnvisibilitychange,
650        SetOnvisibilitychange
651    );
652
653    // https://immersive-web.github.io/webxr/#eventdef-xrsession-inputsourceschange
654    event_handler!(
655        inputsourceschange,
656        GetOninputsourceschange,
657        SetOninputsourceschange
658    );
659
660    // https://www.w3.org/TR/webxr/#dom-xrsession-onframeratechange
661    event_handler!(frameratechange, GetOnframeratechange, SetOnframeratechange);
662
663    // https://immersive-web.github.io/webxr/#dom-xrsession-renderstate
664    fn RenderState(&self) -> DomRoot<XRRenderState> {
665        self.active_render_state.get()
666    }
667
668    /// <https://immersive-web.github.io/webxr/#dom-xrsession-updaterenderstate>
669    fn UpdateRenderState(&self, init: &XRRenderStateInit, _: InRealm) -> ErrorResult {
670        // Step 2
671        if self.ended.get() {
672            return Err(Error::InvalidState);
673        }
674        // Step 3:
675        if let Some(Some(ref layer)) = init.baseLayer {
676            if Dom::from_ref(layer.session()) != Dom::from_ref(self) {
677                return Err(Error::InvalidState);
678            }
679        }
680
681        // Step 4:
682        if init.inlineVerticalFieldOfView.is_some() && self.is_immersive() {
683            return Err(Error::InvalidState);
684        }
685
686        // https://immersive-web.github.io/layers/#updaterenderstatechanges
687        // Step 1.
688        if init.baseLayer.is_some() && (self.has_layers_feature() || init.layers.is_some()) {
689            return Err(Error::NotSupported);
690        }
691
692        if let Some(Some(ref layers)) = init.layers {
693            // Step 2
694            for layer in layers {
695                let count = layers
696                    .iter()
697                    .filter(|other| other.layer_id() == layer.layer_id())
698                    .count();
699                if count > 1 {
700                    return Err(Error::Type(String::from("Duplicate entry in WebXR layers")));
701                }
702            }
703
704            // Step 3
705            for layer in layers {
706                if layer.session() != self {
707                    return Err(Error::Type(String::from(
708                        "Layer from different session in WebXR layers",
709                    )));
710                }
711            }
712        }
713
714        // Step 4-5
715        let pending = self
716            .pending_render_state
717            .or_init(|| self.active_render_state.get().clone_object());
718
719        // Step 6
720        if let Some(ref layers) = init.layers {
721            let layers = layers.as_deref().unwrap_or_default();
722            pending.set_base_layer(None);
723            pending.set_layers(layers.iter().map(|x| &**x).collect());
724            let layers = layers
725                .iter()
726                .filter_map(|layer| {
727                    let context_id = WebXRContextId::from(layer.context_id());
728                    let layer_id = layer.layer_id()?;
729                    Some((context_id, layer_id))
730                })
731                .collect();
732            self.session.borrow_mut().set_layers(layers);
733        }
734
735        // End of https://immersive-web.github.io/layers/#updaterenderstatechanges
736
737        if let Some(near) = init.depthNear {
738            let mut near = *near;
739            // Step 8 from #apply-the-pending-render-state
740            // this may need to be changed if backends wish to impose
741            // further constraints
742            if near < 0. {
743                near = 0.;
744            }
745            pending.set_depth_near(near);
746        }
747        if let Some(far) = init.depthFar {
748            let mut far = *far;
749            // Step 9 from #apply-the-pending-render-state
750            // this may need to be changed if backends wish to impose
751            // further constraints
752            // currently the maximum is infinity, so just check that
753            // the value is non-negative
754            if far < 0. {
755                far = 0.;
756            }
757            pending.set_depth_far(far);
758        }
759        if let Some(fov) = init.inlineVerticalFieldOfView {
760            let mut fov = *fov;
761            // Step 10 from #apply-the-pending-render-state
762            // this may need to be changed if backends wish to impose
763            // further constraints
764            if fov < 0. {
765                fov = 0.0001;
766            } else if fov > PI {
767                fov = PI - 0.0001;
768            }
769            pending.set_inline_vertical_fov(fov);
770        }
771        if let Some(ref layer) = init.baseLayer {
772            pending.set_base_layer(layer.as_deref());
773            pending.set_layers(Vec::new());
774            let layers = layer
775                .iter()
776                .filter_map(|layer| {
777                    let context_id = WebXRContextId::from(layer.context_id());
778                    let layer_id = layer.layer_id()?;
779                    Some((context_id, layer_id))
780                })
781                .collect();
782            self.session.borrow_mut().set_layers(layers);
783        }
784
785        if init.depthFar.is_some() || init.depthNear.is_some() {
786            self.session
787                .borrow_mut()
788                .update_clip_planes(*pending.DepthNear() as f32, *pending.DepthFar() as f32);
789        }
790
791        Ok(())
792    }
793
794    /// <https://immersive-web.github.io/webxr/#dom-xrsession-requestanimationframe>
795    fn RequestAnimationFrame(&self, callback: Rc<XRFrameRequestCallback>) -> i32 {
796        // queue up RAF callback, obtain ID
797        let raf_id = self.next_raf_id.get();
798        self.next_raf_id.set(raf_id + 1);
799        self.raf_callback_list
800            .borrow_mut()
801            .push((raf_id, Some(callback)));
802
803        raf_id
804    }
805
806    /// <https://immersive-web.github.io/webxr/#dom-xrsession-cancelanimationframe>
807    fn CancelAnimationFrame(&self, frame: i32) {
808        let mut list = self.raf_callback_list.borrow_mut();
809        if let Some(pair) = list.iter_mut().find(|pair| pair.0 == frame) {
810            pair.1 = None;
811        }
812
813        let mut list = self.current_raf_callback_list.borrow_mut();
814        if let Some(pair) = list.iter_mut().find(|pair| pair.0 == frame) {
815            pair.1 = None;
816        }
817    }
818
819    /// <https://immersive-web.github.io/webxr/#dom-xrsession-environmentblendmode>
820    fn EnvironmentBlendMode(&self) -> XREnvironmentBlendMode {
821        self.blend_mode
822    }
823
824    /// <https://immersive-web.github.io/webxr/#dom-xrsession-visibilitystate>
825    fn VisibilityState(&self) -> XRVisibilityState {
826        self.visibility_state.get()
827    }
828
829    /// <https://immersive-web.github.io/webxr/#dom-xrsession-requestreferencespace>
830    fn RequestReferenceSpace(
831        &self,
832        ty: XRReferenceSpaceType,
833        comp: InRealm,
834        can_gc: CanGc,
835    ) -> Rc<Promise> {
836        let p = Promise::new_in_current_realm(comp, can_gc);
837
838        // https://immersive-web.github.io/webxr/#create-a-reference-space
839
840        // XXXManishearth reject based on session type
841        // https://github.com/immersive-web/webxr/blob/master/spatial-tracking-explainer.md#practical-usage-guidelines
842
843        if !self.is_immersive() &&
844            (ty == XRReferenceSpaceType::Bounded_floor || ty == XRReferenceSpaceType::Unbounded)
845        {
846            p.reject_error(Error::NotSupported, can_gc);
847            return p;
848        }
849
850        match ty {
851            XRReferenceSpaceType::Unbounded => {
852                // XXXmsub2 figure out how to support this
853                p.reject_error(Error::NotSupported, can_gc)
854            },
855            ty => {
856                if ty != XRReferenceSpaceType::Viewer &&
857                    (!self.is_immersive() || ty != XRReferenceSpaceType::Local)
858                {
859                    let s = ty.as_str();
860                    if !self
861                        .session
862                        .borrow()
863                        .granted_features()
864                        .iter()
865                        .any(|f| *f == s)
866                    {
867                        p.reject_error(Error::NotSupported, can_gc);
868                        return p;
869                    }
870                }
871                if ty == XRReferenceSpaceType::Bounded_floor {
872                    let space =
873                        XRBoundedReferenceSpace::new(self.global().as_window(), self, can_gc);
874                    self.reference_spaces
875                        .borrow_mut()
876                        .push(Dom::from_ref(space.reference_space()));
877                    p.resolve_native(&space, can_gc);
878                } else {
879                    let space = XRReferenceSpace::new(self.global().as_window(), self, ty, can_gc);
880                    self.reference_spaces
881                        .borrow_mut()
882                        .push(Dom::from_ref(&*space));
883                    p.resolve_native(&space, can_gc);
884                }
885            },
886        }
887        p
888    }
889
890    /// <https://immersive-web.github.io/webxr/#dom-xrsession-inputsources>
891    fn InputSources(&self) -> DomRoot<XRInputSourceArray> {
892        DomRoot::from_ref(&*self.input_sources)
893    }
894
895    /// <https://immersive-web.github.io/webxr/#dom-xrsession-end>
896    fn End(&self, can_gc: CanGc) -> Rc<Promise> {
897        let global = self.global();
898        let p = Promise::new(&global, can_gc);
899        if self.ended.get() && self.end_promises.borrow().is_empty() {
900            // If the session has completely ended and all end promises have been resolved,
901            // don't queue up more end promises
902            //
903            // We need to check for end_promises being empty because `ended` is set
904            // before everything has been completely shut down, and we do not want to
905            // prematurely resolve the promise then
906            //
907            // However, if end_promises is empty, then all end() promises have already resolved,
908            // so the session has completely shut down and we should not queue up more promises
909            p.resolve_native(&(), can_gc);
910            return p;
911        }
912        self.end_promises.borrow_mut().push(p.clone());
913        // This is duplicated in event_callback since this should
914        // happen ASAP for end() but can happen later if the device
915        // shuts itself down
916        self.ended.set(true);
917        global.as_window().Navigator().Xr().end_session(self);
918        self.session.borrow_mut().end_session();
919        // Disconnect any still-attached XRInputSources
920        for source in 0..self.input_sources.Length() {
921            self.input_sources
922                .remove_input_source(self, InputId(source), can_gc);
923        }
924        p
925    }
926
927    // https://immersive-web.github.io/hit-test/#dom-xrsession-requesthittestsource
928    fn RequestHitTestSource(&self, options: &XRHitTestOptionsInit, can_gc: CanGc) -> Rc<Promise> {
929        let p = Promise::new(&self.global(), can_gc);
930
931        if !self
932            .session
933            .borrow()
934            .granted_features()
935            .iter()
936            .any(|f| f == "hit-test")
937        {
938            p.reject_error(Error::NotSupported, can_gc);
939            return p;
940        }
941
942        let id = self.next_hit_test_id.get();
943        self.next_hit_test_id.set(HitTestId(id.0 + 1));
944
945        let space = options.space.space();
946        let ray = if let Some(ref ray) = options.offsetRay {
947            ray.ray()
948        } else {
949            Ray {
950                origin: Vector3D::new(0., 0., 0.),
951                direction: Vector3D::new(0., 0., -1.),
952            }
953        };
954
955        let mut types = EntityTypes::default();
956
957        if let Some(ref tys) = options.entityTypes {
958            for ty in tys {
959                match ty {
960                    XRHitTestTrackableType::Point => types.point = true,
961                    XRHitTestTrackableType::Plane => types.plane = true,
962                    XRHitTestTrackableType::Mesh => types.mesh = true,
963                }
964            }
965        } else {
966            types.plane = true;
967        }
968
969        let source = HitTestSource {
970            id,
971            space,
972            ray,
973            types,
974        };
975        self.pending_hit_test_promises
976            .borrow_mut()
977            .insert(id, p.clone());
978
979        self.session.borrow().request_hit_test(source);
980
981        p
982    }
983
984    /// <https://www.w3.org/TR/webxr-ar-module-1/#dom-xrsession-interactionmode>
985    fn InteractionMode(&self) -> XRInteractionMode {
986        // Until Servo supports WebXR sessions on mobile phones or similar non-XR devices,
987        // this should always be world space
988        XRInteractionMode::World_space
989    }
990
991    /// <https://www.w3.org/TR/webxr/#dom-xrsession-framerate>
992    fn GetFrameRate(&self) -> Option<Finite<f32>> {
993        let session = self.session.borrow();
994        if self.mode == XRSessionMode::Inline || session.supported_frame_rates().is_empty() {
995            None
996        } else {
997            Finite::new(self.framerate.get())
998        }
999    }
1000
1001    /// <https://www.w3.org/TR/webxr/#dom-xrsession-supportedframerates>
1002    fn GetSupportedFrameRates(&self, cx: JSContext, can_gc: CanGc) -> Option<Float32Array> {
1003        let session = self.session.borrow();
1004        if self.mode == XRSessionMode::Inline || session.supported_frame_rates().is_empty() {
1005            None
1006        } else {
1007            let framerates = session.supported_frame_rates();
1008            rooted!(in (*cx) let mut array = ptr::null_mut::<JSObject>());
1009            Some(
1010                create_buffer_source(cx, framerates, array.handle_mut(), can_gc)
1011                    .expect("Failed to construct supported frame rates array"),
1012            )
1013        }
1014    }
1015
1016    /// <https://www.w3.org/TR/webxr/#dom-xrsession-enabledfeatures>
1017    fn EnabledFeatures(&self, cx: JSContext, can_gc: CanGc, retval: MutableHandleValue) {
1018        let session = self.session.borrow();
1019        let features = session.granted_features();
1020        to_frozen_array(features, cx, retval, can_gc)
1021    }
1022
1023    /// <https://www.w3.org/TR/webxr/#dom-xrsession-issystemkeyboardsupported>
1024    fn IsSystemKeyboardSupported(&self) -> bool {
1025        // Support for this only exists on Meta headsets (no desktop support)
1026        // so this will always be false until that changes
1027        false
1028    }
1029
1030    /// <https://www.w3.org/TR/webxr/#dom-xrsession-updatetargetframerate>
1031    fn UpdateTargetFrameRate(
1032        &self,
1033        rate: Finite<f32>,
1034        comp: InRealm,
1035        can_gc: CanGc,
1036    ) -> Rc<Promise> {
1037        let promise = Promise::new_in_current_realm(comp, can_gc);
1038        {
1039            let session = self.session.borrow();
1040            let supported_frame_rates = session.supported_frame_rates();
1041
1042            if self.mode == XRSessionMode::Inline ||
1043                supported_frame_rates.is_empty() ||
1044                self.ended.get()
1045            {
1046                promise.reject_error(Error::InvalidState, can_gc);
1047                return promise;
1048            }
1049
1050            if !supported_frame_rates.contains(&*rate) {
1051                promise.reject_error(
1052                    Error::Type("Provided framerate not supported".into()),
1053                    can_gc,
1054                );
1055                return promise;
1056            }
1057        }
1058
1059        *self.update_framerate_promise.borrow_mut() = Some(promise.clone());
1060
1061        let this = Trusted::new(self);
1062        let global = self.global();
1063        let task_source = global
1064            .task_manager()
1065            .dom_manipulation_task_source()
1066            .to_sendable();
1067        let (sender, receiver) = ipc::channel(global.time_profiler_chan().clone()).unwrap();
1068
1069        ROUTER.add_typed_route(
1070            receiver.to_ipc_receiver(),
1071            Box::new(move |message| {
1072                let this = this.clone();
1073                task_source.queue(task!(update_session_framerate: move || {
1074                    let session = this.root();
1075                    session.apply_nominal_framerate(message.unwrap(), CanGc::note());
1076                    if let Some(promise) = session.update_framerate_promise.borrow_mut().take() {
1077                        promise.resolve_native(&(), CanGc::note());
1078                    };
1079                }));
1080            }),
1081        );
1082
1083        self.session.borrow_mut().update_frame_rate(*rate, sender);
1084
1085        promise
1086    }
1087}
1088
1089// The pose of an object in native-space. Should never be exposed.
1090pub(crate) type ApiPose = RigidTransform3D<f32, ApiSpace, webxr_api::Native>;
1091// A transform between objects in some API-space
1092pub(crate) type ApiRigidTransform = RigidTransform3D<f32, ApiSpace, ApiSpace>;
1093
1094#[derive(Clone, Copy)]
1095pub(crate) struct BaseSpace;
1096
1097pub(crate) type BaseTransform = RigidTransform3D<f32, webxr_api::Native, BaseSpace>;
1098
1099#[allow(unsafe_code)]
1100pub(crate) fn cast_transform<T, U, V, W>(
1101    transform: RigidTransform3D<f32, T, U>,
1102) -> RigidTransform3D<f32, V, W> {
1103    unsafe { mem::transmute(transform) }
1104}
1105
1106impl Convert<XREnvironmentBlendMode> for EnvironmentBlendMode {
1107    fn convert(self) -> XREnvironmentBlendMode {
1108        match self {
1109            EnvironmentBlendMode::Opaque => XREnvironmentBlendMode::Opaque,
1110            EnvironmentBlendMode::AlphaBlend => XREnvironmentBlendMode::Alpha_blend,
1111            EnvironmentBlendMode::Additive => XREnvironmentBlendMode::Additive,
1112        }
1113    }
1114}