Skip to main content

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 script_bindings::reflector::reflect_weak_referenceable_dom_object;
12use dom_struct::dom_struct;
13use js::context::JSContext;
14use js::realm::CurrentRealm;
15use euclid::{RigidTransform3D, Transform3D, Vector3D};
16use ipc_channel::ipc::IpcReceiver;
17use ipc_channel::router::ROUTER;
18use js::jsapi::JSObject;
19use js::rust::MutableHandleValue;
20use js::typedarray::HeapFloat32Array;
21use profile_traits::generic_callback::GenericCallback as ProfileGenericCallback;
22use rustc_hash::FxBuildHasher;
23use script_bindings::trace::RootedTraceableBox;
24use servo_base::cross_process_instant::CrossProcessInstant;
25use stylo_atoms::Atom;
26use webxr_api::{
27    self, ApiSpace, ContextId as WebXRContextId, Display, EntityTypes, EnvironmentBlendMode,
28    Event as XREvent, Frame, FrameUpdateEvent, HitTestId, HitTestSource, InputFrame, InputId, Ray,
29    SelectEvent, SelectKind, Session, SessionId, View, Viewer, Visibility, util,
30};
31
32use crate::conversions::Convert;
33use crate::canvas_context::CanvasContext;
34use crate::dom::bindings::trace::HashMapTracedValues;
35use crate::dom::bindings::buffer_source::create_buffer_source;
36use crate::dom::bindings::callback::ExceptionHandling;
37use script_bindings::cell::DomRefCell;
38use crate::dom::bindings::codegen::Bindings::NavigatorBinding::Navigator_Binding::NavigatorMethods;
39use crate::dom::bindings::codegen::Bindings::WindowBinding::Window_Binding::WindowMethods;
40use crate::dom::bindings::codegen::Bindings::XRHitTestSourceBinding::{
41    XRHitTestOptionsInit, XRHitTestTrackableType,
42};
43use crate::dom::bindings::codegen::Bindings::XRInputSourceArrayBinding::XRInputSourceArray_Binding::XRInputSourceArrayMethods;
44use crate::dom::bindings::codegen::Bindings::XRReferenceSpaceBinding::XRReferenceSpaceType;
45use crate::dom::bindings::codegen::Bindings::XRRenderStateBinding::{
46    XRRenderStateInit, XRRenderStateMethods,
47};
48use crate::dom::bindings::codegen::Bindings::XRSessionBinding::{
49    XREnvironmentBlendMode, XRFrameRequestCallback, XRInteractionMode, XRSessionMethods,
50    XRVisibilityState,
51};
52use crate::dom::bindings::codegen::Bindings::XRSystemBinding::XRSessionMode;
53use crate::dom::bindings::error::{Error, ErrorResult};
54use crate::dom::bindings::inheritance::Castable;
55use crate::dom::bindings::num::Finite;
56use crate::dom::bindings::refcounted::Trusted;
57use crate::dom::bindings::reflector::{DomGlobal};
58use crate::dom::bindings::root::{Dom, DomRoot, MutDom, MutNullableDom};
59use crate::dom::bindings::utils::to_frozen_array;
60use crate::dom::event::Event;
61use crate::dom::eventtarget::EventTarget;
62use crate::dom::promise::Promise;
63use crate::dom::window::Window;
64use crate::dom::xrboundedreferencespace::XRBoundedReferenceSpace;
65use crate::dom::xrframe::XRFrame;
66use crate::dom::xrhittestsource::XRHitTestSource;
67use crate::dom::xrinputsourcearray::XRInputSourceArray;
68use crate::dom::xrinputsourceevent::XRInputSourceEvent;
69use crate::dom::xrreferencespace::XRReferenceSpace;
70use crate::dom::xrreferencespaceevent::XRReferenceSpaceEvent;
71use crate::dom::xrrenderstate::XRRenderState;
72use crate::dom::xrrigidtransform::XRRigidTransform;
73use crate::dom::xrsessionevent::XRSessionEvent;
74use crate::dom::xrspace::XRSpace;
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        cx: &mut JSContext,
156        window: &Window,
157        session: Session,
158        mode: XRSessionMode,
159        frame_receiver: IpcReceiver<Frame>,
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(cx, window, 0.1, 1000.0, ivfov, None, Vec::new());
167        let input_sources = XRInputSourceArray::new(cx, window);
168        let ret = reflect_weak_referenceable_dom_object(
169            cx,
170            Rc::new(XRSession::new_inherited(
171                session,
172                &render_state,
173                &input_sources,
174                mode,
175            )),
176            window,
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 |cx| {
217                    this.root().raf_callback(cx, frame, time);
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 |cx| {
240                    this.root().event_callback(cx, message.unwrap());
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 |cx| {
269                let this = this.root();
270                this.input_sources.add_input_sources(cx, &this, &initial_inputs);
271            }));
272    }
273
274    fn event_callback(&self, cx: &mut JSContext, event: XREvent) {
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()
282                    .as_window()
283                    .Navigator(cx)
284                    .Xr(cx)
285                    .end_session(self);
286                // Step 5: We currently do not have any such promises
287                // Step 6 is happening n the XR session
288                // https://immersive-web.github.io/webxr/#dom-xrsession-end step 3
289                for promise in self.end_promises.borrow().iter() {
290                    promise.resolve_native(cx, &());
291                }
292                self.end_promises.safe_borrow_mut(cx.no_gc()).clear();
293                // Step 7
294                let event = XRSessionEvent::new(
295                    cx,
296                    self.global().as_window(),
297                    atom!("end"),
298                    false,
299                    false,
300                    self,
301                );
302                event.upcast::<Event>().fire(cx, self.upcast());
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(cx, self.global().as_window(), self, frame);
315                    frame.set_active(true);
316                    if ty == SelectEvent::Start {
317                        let event = XRInputSourceEvent::new(
318                            cx,
319                            self.global().as_window(),
320                            START_ATOMS[atom_index].clone(),
321                            false,
322                            false,
323                            &frame,
324                            &source,
325                        );
326                        event.upcast::<Event>().fire(cx, self.upcast());
327                    } else {
328                        if ty == SelectEvent::Select {
329                            let event = XRInputSourceEvent::new(
330                                cx,
331                                self.global().as_window(),
332                                EVENT_ATOMS[atom_index].clone(),
333                                false,
334                                false,
335                                &frame,
336                                &source,
337                            );
338                            event.upcast::<Event>().fire(cx, self.upcast());
339                        }
340                        let event = XRInputSourceEvent::new(
341                            cx,
342                            self.global().as_window(),
343                            END_ATOMS[atom_index].clone(),
344                            false,
345                            false,
346                            &frame,
347                            &source,
348                        );
349                        event.upcast::<Event>().fire(cx, self.upcast());
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                    cx,
363                    self.global().as_window(),
364                    atom!("visibilitychange"),
365                    false,
366                    false,
367                    self,
368                );
369                event.upcast::<Event>().fire(cx, self.upcast());
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(cx, self, &[info]);
376            },
377            XREvent::RemoveInput(id) => {
378                self.input_sources.remove_input_source(cx, self, id);
379            },
380            XREvent::UpdateInput(id, source) => {
381                self.input_sources
382                    .add_remove_input_source(cx, self, id, source);
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(cx, self.global().as_window(), transform);
406                        let event = XRReferenceSpaceEvent::new(
407                            cx,
408                            self.global().as_window(),
409                            atom!("reset"),
410                            false,
411                            false,
412                            space,
413                            Some(&*offset),
414                        );
415                        event.upcast::<Event>().fire(cx, space.upcast());
416                    });
417            },
418        }
419    }
420
421    /// <https://immersive-web.github.io/webxr/#xr-animation-frame>
422    fn raf_callback(&self, cx: &mut JSContext, mut frame: Frame, time: CrossProcessInstant) {
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(cx, event);
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
474            .global()
475            .performance(cx)
476            .to_dom_high_res_time_stamp(time);
477        let frame = XRFrame::new(cx, self.global().as_window(), self, frame);
478
479        // Step 8-9
480        frame.set_active(true);
481        frame.set_animation_frame(true);
482
483        // Step 10
484        self.apply_frame_updates(&frame);
485
486        // TODO: how does this fit with the webxr and xr layers specs?
487        self.layers_begin_frame(cx, &frame);
488
489        // Step 11-12
490        self.outside_raf.set(false);
491        let len = self.current_raf_callback_list.borrow().len();
492        for i in 0..len {
493            let callback = self.current_raf_callback_list.borrow()[i].1.clone();
494            if let Some(callback) = callback {
495                let _ = callback.Call__(cx, time, &frame, ExceptionHandling::Report);
496            }
497        }
498        self.outside_raf.set(true);
499        *self.current_raf_callback_list.borrow_mut() = vec![];
500
501        // TODO: how does this fit with the webxr and xr layers specs?
502        self.layers_end_frame(&frame);
503
504        // Step 13
505        frame.set_active(false);
506
507        // TODO: how does this fit the webxr spec?
508        self.session.borrow_mut().render_animation_frame();
509    }
510
511    fn update_inline_projection_matrix(&self) {
512        debug_assert!(!self.is_immersive());
513        let render_state = self.active_render_state.get();
514        let size = if let Some(base) = render_state.GetBaseLayer() {
515            base.size()
516        } else {
517            return;
518        };
519        let mut clip_planes = util::ClipPlanes::default();
520        let near = *render_state.DepthNear() as f32;
521        let far = *render_state.DepthFar() as f32;
522        clip_planes.update(near, far);
523        let top = *render_state
524            .GetInlineVerticalFieldOfView()
525            .expect("IVFOV should be non null for inline sessions") /
526            2.;
527        let top = near * top.tan() as f32;
528        let bottom = top;
529        let left = top * size.width as f32 / size.height as f32;
530        let right = left;
531        let matrix = util::frustum_to_projection_matrix(left, right, top, bottom, clip_planes);
532        *self.inline_projection_matrix.borrow_mut() = matrix;
533    }
534
535    /// Constructs a View suitable for inline sessions using the inlineVerticalFieldOfView and canvas size
536    pub(crate) fn inline_view(&self) -> View<Viewer> {
537        debug_assert!(!self.is_immersive());
538        View {
539            // Inline views have no offset
540            transform: RigidTransform3D::identity(),
541            projection: *self.inline_projection_matrix.borrow(),
542        }
543    }
544
545    pub(crate) fn session_id(&self) -> SessionId {
546        self.session.borrow().id()
547    }
548
549    pub(crate) fn dirty_layers(&self) {
550        if let Some(layer) = self.RenderState().GetBaseLayer() {
551            layer.context().mark_as_dirty();
552        }
553    }
554
555    // TODO: how does this align with the layers spec?
556    fn layers_begin_frame(&self, cx: &mut JSContext, frame: &XRFrame) {
557        if let Some(layer) = self.active_render_state.get().GetBaseLayer() {
558            layer.begin_frame(cx, frame);
559        }
560        self.active_render_state.get().with_layers(|layers| {
561            for layer in layers {
562                layer.begin_frame(cx, frame);
563            }
564        });
565    }
566
567    // TODO: how does this align with the layers spec?
568    fn layers_end_frame(&self, frame: &XRFrame) {
569        if let Some(layer) = self.active_render_state.get().GetBaseLayer() {
570            layer.end_frame(frame);
571        }
572        self.active_render_state.get().with_layers(|layers| {
573            for layer in layers {
574                layer.end_frame(frame);
575            }
576        });
577    }
578
579    /// <https://immersive-web.github.io/webxr/#xrframe-apply-frame-updates>
580    fn apply_frame_updates(&self, _frame: &XRFrame) {
581        // <https://www.w3.org/TR/webxr-gamepads-module-1/#xrframe-apply-gamepad-frame-updates>
582        for (id, frame) in self.input_frames.borrow_mut().drain() {
583            let source = self.input_sources.find(id);
584            if let Some(source) = source {
585                source.update_gamepad_state(frame);
586            }
587        }
588    }
589
590    fn handle_frame_event(&self, cx: &mut JSContext, event: FrameUpdateEvent) {
591        match event {
592            FrameUpdateEvent::HitTestSourceAdded(id) => {
593                if let Some(promise) = self.pending_hit_test_promises.borrow_mut().remove(&id) {
594                    let hit_test_source =
595                        XRHitTestSource::new(cx, self.global().as_window(), id, self);
596                    promise.resolve_native(cx, &hit_test_source);
597                } else {
598                    warn!(
599                        "received hit test add request for unknown hit test {:?}",
600                        id
601                    )
602                }
603            },
604            _ => self.session.borrow_mut().apply_event(event),
605        }
606    }
607
608    /// <https://www.w3.org/TR/webxr/#apply-the-nominal-frame-rate>
609    fn apply_nominal_framerate(&self, cx: &mut JSContext, rate: f32) {
610        if self.framerate.get() == rate || self.ended.get() {
611            return;
612        }
613
614        self.framerate.set(rate);
615
616        let event = XRSessionEvent::new(
617            cx,
618            self.global().as_window(),
619            Atom::from("frameratechange"),
620            false,
621            false,
622            self,
623        );
624        event.upcast::<Event>().fire(cx, self.upcast());
625    }
626}
627
628impl XRSessionMethods<crate::DomTypeHolder> for XRSession {
629    // https://immersive-web.github.io/webxr/#eventdef-xrsession-end
630    event_handler!(end, GetOnend, SetOnend);
631
632    // https://immersive-web.github.io/webxr/#eventdef-xrsession-select
633    event_handler!(select, GetOnselect, SetOnselect);
634
635    // https://immersive-web.github.io/webxr/#eventdef-xrsession-selectstart
636    event_handler!(selectstart, GetOnselectstart, SetOnselectstart);
637
638    // https://immersive-web.github.io/webxr/#eventdef-xrsession-selectend
639    event_handler!(selectend, GetOnselectend, SetOnselectend);
640
641    // https://immersive-web.github.io/webxr/#eventdef-xrsession-squeeze
642    event_handler!(squeeze, GetOnsqueeze, SetOnsqueeze);
643
644    // https://immersive-web.github.io/webxr/#eventdef-xrsession-squeezestart
645    event_handler!(squeezestart, GetOnsqueezestart, SetOnsqueezestart);
646
647    // https://immersive-web.github.io/webxr/#eventdef-xrsession-squeezeend
648    event_handler!(squeezeend, GetOnsqueezeend, SetOnsqueezeend);
649
650    // https://immersive-web.github.io/webxr/#eventdef-xrsession-visibilitychange
651    event_handler!(
652        visibilitychange,
653        GetOnvisibilitychange,
654        SetOnvisibilitychange
655    );
656
657    // https://immersive-web.github.io/webxr/#eventdef-xrsession-inputsourceschange
658    event_handler!(
659        inputsourceschange,
660        GetOninputsourceschange,
661        SetOninputsourceschange
662    );
663
664    // https://www.w3.org/TR/webxr/#dom-xrsession-onframeratechange
665    event_handler!(frameratechange, GetOnframeratechange, SetOnframeratechange);
666
667    /// <https://immersive-web.github.io/webxr/#dom-xrsession-renderstate>
668    fn RenderState(&self) -> DomRoot<XRRenderState> {
669        self.active_render_state.get()
670    }
671
672    /// <https://immersive-web.github.io/webxr/#dom-xrsession-updaterenderstate>
673    fn UpdateRenderState(&self, cx: &mut JSContext, init: &XRRenderStateInit) -> ErrorResult {
674        // Step 2
675        if self.ended.get() {
676            return Err(Error::InvalidState(None));
677        }
678        // Step 3:
679        if let Some(Some(ref layer)) = init.baseLayer &&
680            Dom::from_ref(layer.session()) != Dom::from_ref(self)
681        {
682            return Err(Error::InvalidState(None));
683        }
684
685        // Step 4:
686        if init.inlineVerticalFieldOfView.is_some() && self.is_immersive() {
687            return Err(Error::InvalidState(None));
688        }
689
690        // https://immersive-web.github.io/layers/#updaterenderstatechanges
691        // Step 1.
692        if init.baseLayer.is_some() && (self.has_layers_feature() || init.layers.is_some()) {
693            return Err(Error::NotSupported(None));
694        }
695
696        if let Some(Some(ref layers)) = init.layers {
697            // Step 2
698            for layer in layers {
699                let count = layers
700                    .iter()
701                    .filter(|other| other.layer_id() == layer.layer_id())
702                    .count();
703                if count > 1 {
704                    return Err(Error::Type(c"Duplicate entry in WebXR layers".to_owned()));
705                }
706            }
707
708            // Step 3
709            for layer in layers {
710                if layer.session() != self {
711                    return Err(Error::Type(
712                        c"Layer from different session in WebXR layers".to_owned(),
713                    ));
714                }
715            }
716        }
717
718        // Step 4-5
719        let pending = self
720            .pending_render_state
721            .or_init(|| self.active_render_state.get().clone_object(cx));
722
723        // Step 6
724        if let Some(ref layers) = init.layers {
725            let layers = layers.as_deref().unwrap_or_default();
726            pending.set_base_layer(None);
727            pending.set_layers(layers.iter().map(|x| &**x).collect());
728            let layers = layers
729                .iter()
730                .filter_map(|layer| {
731                    let context_id = WebXRContextId::from(layer.context_id());
732                    let layer_id = layer.layer_id()?;
733                    Some((context_id, layer_id))
734                })
735                .collect();
736            self.session.borrow_mut().set_layers(layers);
737        }
738
739        // End of https://immersive-web.github.io/layers/#updaterenderstatechanges
740
741        if let Some(near) = init.depthNear {
742            let mut near = *near;
743            // Step 8 from #apply-the-pending-render-state
744            // this may need to be changed if backends wish to impose
745            // further constraints
746            if near < 0. {
747                near = 0.;
748            }
749            pending.set_depth_near(near);
750        }
751        if let Some(far) = init.depthFar {
752            let mut far = *far;
753            // Step 9 from #apply-the-pending-render-state
754            // this may need to be changed if backends wish to impose
755            // further constraints
756            // currently the maximum is infinity, so just check that
757            // the value is non-negative
758            if far < 0. {
759                far = 0.;
760            }
761            pending.set_depth_far(far);
762        }
763        if let Some(fov) = init.inlineVerticalFieldOfView {
764            let mut fov = *fov;
765            // Step 10 from #apply-the-pending-render-state
766            // this may need to be changed if backends wish to impose
767            // further constraints
768            if fov < 0. {
769                fov = 0.0001;
770            } else if fov > PI {
771                fov = PI - 0.0001;
772            }
773            pending.set_inline_vertical_fov(fov);
774        }
775        if let Some(ref layer) = init.baseLayer {
776            pending.set_base_layer(layer.as_deref());
777            pending.set_layers(Vec::new());
778            let layers = layer
779                .iter()
780                .filter_map(|layer| {
781                    let context_id = WebXRContextId::from(layer.context_id());
782                    let layer_id = layer.layer_id()?;
783                    Some((context_id, layer_id))
784                })
785                .collect();
786            self.session.borrow_mut().set_layers(layers);
787        }
788
789        if init.depthFar.is_some() || init.depthNear.is_some() {
790            self.session
791                .borrow_mut()
792                .update_clip_planes(*pending.DepthNear() as f32, *pending.DepthFar() as f32);
793        }
794
795        Ok(())
796    }
797
798    /// <https://immersive-web.github.io/webxr/#dom-xrsession-requestanimationframe>
799    fn RequestAnimationFrame(&self, callback: Rc<XRFrameRequestCallback>) -> i32 {
800        // queue up RAF callback, obtain ID
801        let raf_id = self.next_raf_id.get();
802        self.next_raf_id.set(raf_id + 1);
803        self.raf_callback_list
804            .borrow_mut()
805            .push((raf_id, Some(callback)));
806
807        raf_id
808    }
809
810    /// <https://immersive-web.github.io/webxr/#dom-xrsession-cancelanimationframe>
811    fn CancelAnimationFrame(&self, frame: i32) {
812        let mut list = self.raf_callback_list.borrow_mut();
813        if let Some(pair) = list.iter_mut().find(|pair| pair.0 == frame) {
814            pair.1 = None;
815        }
816
817        let mut list = self.current_raf_callback_list.borrow_mut();
818        if let Some(pair) = list.iter_mut().find(|pair| pair.0 == frame) {
819            pair.1 = None;
820        }
821    }
822
823    /// <https://immersive-web.github.io/webxr/#dom-xrsession-environmentblendmode>
824    fn EnvironmentBlendMode(&self) -> XREnvironmentBlendMode {
825        self.blend_mode
826    }
827
828    /// <https://immersive-web.github.io/webxr/#dom-xrsession-visibilitystate>
829    fn VisibilityState(&self) -> XRVisibilityState {
830        self.visibility_state.get()
831    }
832
833    /// <https://immersive-web.github.io/webxr/#dom-xrsession-requestreferencespace>
834    fn RequestReferenceSpace(
835        &self,
836        cx: &mut CurrentRealm,
837        ty: XRReferenceSpaceType,
838    ) -> Rc<Promise> {
839        let p = Promise::new_in_realm(cx);
840
841        // https://immersive-web.github.io/webxr/#create-a-reference-space
842
843        // XXXManishearth reject based on session type
844        // https://github.com/immersive-web/webxr/blob/master/spatial-tracking-explainer.md#practical-usage-guidelines
845
846        if !self.is_immersive() &&
847            (ty == XRReferenceSpaceType::Bounded_floor || ty == XRReferenceSpaceType::Unbounded)
848        {
849            p.reject_error(cx, Error::NotSupported(None));
850            return p;
851        }
852
853        match ty {
854            XRReferenceSpaceType::Unbounded => {
855                // XXXmsub2 figure out how to support this
856                p.reject_error(cx, Error::NotSupported(None))
857            },
858            ty => {
859                if ty != XRReferenceSpaceType::Viewer &&
860                    (!self.is_immersive() || ty != XRReferenceSpaceType::Local)
861                {
862                    let s = ty.as_str();
863                    if !self
864                        .session
865                        .borrow()
866                        .granted_features()
867                        .iter()
868                        .any(|f| *f == s)
869                    {
870                        p.reject_error(cx, Error::NotSupported(None));
871                        return p;
872                    }
873                }
874                if ty == XRReferenceSpaceType::Bounded_floor {
875                    let space = XRBoundedReferenceSpace::new(cx, self.global().as_window(), self);
876                    self.reference_spaces
877                        .borrow_mut()
878                        .push(Dom::from_ref(space.reference_space()));
879                    p.resolve_native(cx, &space);
880                } else {
881                    let space = XRReferenceSpace::new(cx, self.global().as_window(), self, ty);
882                    self.reference_spaces
883                        .borrow_mut()
884                        .push(Dom::from_ref(&*space));
885                    p.resolve_native(cx, &space);
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, cx: &mut CurrentRealm) -> Rc<Promise> {
899        let p = Promise::new_in_realm(cx);
900        if self.ended.get() && self.end_promises.borrow().is_empty() {
901            // If the session has completely ended and all end promises have been resolved,
902            // don't queue up more end promises
903            //
904            // We need to check for end_promises being empty because `ended` is set
905            // before everything has been completely shut down, and we do not want to
906            // prematurely resolve the promise then
907            //
908            // However, if end_promises is empty, then all end() promises have already resolved,
909            // so the session has completely shut down and we should not queue up more promises
910            p.resolve_native(cx, &());
911            return p;
912        }
913        self.end_promises.borrow_mut().push(p.clone());
914        // This is duplicated in event_callback since this should
915        // happen ASAP for end() but can happen later if the device
916        // shuts itself down
917        self.ended.set(true);
918        self.global()
919            .as_window()
920            .Navigator(cx)
921            .Xr(cx)
922            .end_session(self);
923        self.session.borrow_mut().end_session();
924        // Disconnect any still-attached XRInputSources
925        for source in 0..self.input_sources.Length() {
926            self.input_sources
927                .remove_input_source(cx, self, InputId(source));
928        }
929        p
930    }
931
932    /// <https://immersive-web.github.io/hit-test/#dom-xrsession-requesthittestsource>
933    fn RequestHitTestSource(
934        &self,
935        cx: &mut CurrentRealm,
936        options: &XRHitTestOptionsInit,
937    ) -> Rc<Promise> {
938        let p = Promise::new_in_realm(cx);
939
940        if !self
941            .session
942            .borrow()
943            .granted_features()
944            .iter()
945            .any(|f| f == "hit-test")
946        {
947            p.reject_error(cx, Error::NotSupported(None));
948            return p;
949        }
950
951        let id = self.next_hit_test_id.get();
952        self.next_hit_test_id.set(HitTestId(id.0 + 1));
953
954        let space = options.space.space();
955        let ray = if let Some(ref ray) = options.offsetRay {
956            ray.ray()
957        } else {
958            Ray {
959                origin: Vector3D::new(0., 0., 0.),
960                direction: Vector3D::new(0., 0., -1.),
961            }
962        };
963
964        let mut types = EntityTypes::default();
965
966        if let Some(ref tys) = options.entityTypes {
967            for ty in tys {
968                match ty {
969                    XRHitTestTrackableType::Point => types.point = true,
970                    XRHitTestTrackableType::Plane => types.plane = true,
971                    XRHitTestTrackableType::Mesh => types.mesh = true,
972                }
973            }
974        } else {
975            types.plane = true;
976        }
977
978        let source = HitTestSource {
979            id,
980            space,
981            ray,
982            types,
983        };
984        self.pending_hit_test_promises
985            .borrow_mut()
986            .insert(id, p.clone());
987
988        self.session.borrow().request_hit_test(source);
989
990        p
991    }
992
993    /// <https://www.w3.org/TR/webxr-ar-module-1/#dom-xrsession-interactionmode>
994    fn InteractionMode(&self) -> XRInteractionMode {
995        // Until Servo supports WebXR sessions on mobile phones or similar non-XR devices,
996        // this should always be world space
997        XRInteractionMode::World_space
998    }
999
1000    /// <https://www.w3.org/TR/webxr/#dom-xrsession-framerate>
1001    fn GetFrameRate(&self) -> Option<Finite<f32>> {
1002        let session = self.session.borrow();
1003        if self.mode == XRSessionMode::Inline || session.supported_frame_rates().is_empty() {
1004            None
1005        } else {
1006            Finite::new(self.framerate.get())
1007        }
1008    }
1009
1010    /// <https://www.w3.org/TR/webxr/#dom-xrsession-supportedframerates>
1011    fn GetSupportedFrameRates(
1012        &self,
1013        cx: &mut JSContext,
1014    ) -> Option<RootedTraceableBox<HeapFloat32Array>> {
1015        let session = self.session.borrow();
1016        if self.mode == XRSessionMode::Inline || session.supported_frame_rates().is_empty() {
1017            None
1018        } else {
1019            let framerates = session.supported_frame_rates();
1020            rooted!(&in(cx) let mut array = ptr::null_mut::<JSObject>());
1021            Some(
1022                create_buffer_source(cx, framerates, array.handle_mut())
1023                    .expect("Failed to construct supported frame rates array"),
1024            )
1025        }
1026    }
1027
1028    /// <https://www.w3.org/TR/webxr/#dom-xrsession-enabledfeatures>
1029    fn EnabledFeatures(&self, cx: &mut JSContext, retval: MutableHandleValue) {
1030        let session = self.session.borrow();
1031        let features = session.granted_features();
1032        to_frozen_array(cx, features, retval)
1033    }
1034
1035    /// <https://www.w3.org/TR/webxr/#dom-xrsession-issystemkeyboardsupported>
1036    fn IsSystemKeyboardSupported(&self) -> bool {
1037        // Support for this only exists on Meta headsets (no desktop support)
1038        // so this will always be false until that changes
1039        false
1040    }
1041
1042    /// <https://www.w3.org/TR/webxr/#dom-xrsession-updatetargetframerate>
1043    fn UpdateTargetFrameRate(&self, cx: &mut CurrentRealm, rate: Finite<f32>) -> Rc<Promise> {
1044        let promise = Promise::new_in_realm(cx);
1045        {
1046            let session = self.session.borrow();
1047            let supported_frame_rates = session.supported_frame_rates();
1048
1049            if self.mode == XRSessionMode::Inline ||
1050                supported_frame_rates.is_empty() ||
1051                self.ended.get()
1052            {
1053                promise.reject_error(cx, Error::InvalidState(None));
1054                return promise;
1055            }
1056
1057            if !supported_frame_rates.contains(&*rate) {
1058                promise.reject_error(cx, Error::Type(c"Provided framerate not supported".into()));
1059                return promise;
1060            }
1061        }
1062
1063        *self.update_framerate_promise.borrow_mut() = Some(promise.clone());
1064
1065        let this = Trusted::new(self);
1066        let global = self.global();
1067        let task_source = global
1068            .task_manager()
1069            .dom_manipulation_task_source()
1070            .to_sendable();
1071
1072        let callback =
1073            ProfileGenericCallback::new(global.time_profiler_chan().clone(), move |message| {
1074                let this = this.clone();
1075                task_source.queue(task!(update_session_framerate: move |cx| {
1076                    let session = this.root();
1077                    session.apply_nominal_framerate(cx, message.unwrap());
1078                    if let Some(promise) = session.update_framerate_promise.borrow_mut().take() {
1079                        promise.resolve_native(cx, &());
1080                    };
1081                }));
1082            })
1083            .expect("Could not create callback");
1084
1085        self.session.borrow_mut().update_frame_rate(*rate, callback);
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#[expect(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}