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