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