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