aboutsummaryrefslogtreecommitdiffstats
path: root/components/script/dom/webxr/xrsession.rs
diff options
context:
space:
mode:
Diffstat (limited to 'components/script/dom/webxr/xrsession.rs')
-rw-r--r--components/script/dom/webxr/xrsession.rs1111
1 files changed, 1111 insertions, 0 deletions
diff --git a/components/script/dom/webxr/xrsession.rs b/components/script/dom/webxr/xrsession.rs
new file mode 100644
index 00000000000..920c94aaada
--- /dev/null
+++ b/components/script/dom/webxr/xrsession.rs
@@ -0,0 +1,1111 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+use std::cell::Cell;
+use std::collections::HashMap;
+use std::f64::consts::{FRAC_PI_2, PI};
+use std::rc::Rc;
+use std::{mem, ptr};
+
+use base::cross_process_instant::CrossProcessInstant;
+use dom_struct::dom_struct;
+use euclid::{RigidTransform3D, Transform3D, Vector3D};
+use ipc_channel::ipc::IpcReceiver;
+use ipc_channel::router::ROUTER;
+use js::jsapi::JSObject;
+use js::rust::MutableHandleValue;
+use js::typedarray::Float32Array;
+use profile_traits::ipc;
+use servo_atoms::Atom;
+use webxr_api::{
+ self, util, ApiSpace, ContextId as WebXRContextId, Display, EntityTypes, EnvironmentBlendMode,
+ Event as XREvent, Frame, FrameUpdateEvent, HitTestId, HitTestSource, InputFrame, InputId, Ray,
+ SelectEvent, SelectKind, Session, SessionId, View, Viewer, Visibility,
+};
+
+use crate::dom::bindings::trace::HashMapTracedValues;
+use crate::dom::bindings::buffer_source::create_buffer_source;
+use crate::dom::bindings::callback::ExceptionHandling;
+use crate::dom::bindings::cell::DomRefCell;
+use crate::dom::bindings::codegen::Bindings::NavigatorBinding::Navigator_Binding::NavigatorMethods;
+use crate::dom::bindings::codegen::Bindings::WindowBinding::Window_Binding::WindowMethods;
+use crate::dom::bindings::codegen::Bindings::XRHitTestSourceBinding::{
+ XRHitTestOptionsInit, XRHitTestTrackableType,
+};
+use crate::dom::bindings::codegen::Bindings::XRInputSourceArrayBinding::XRInputSourceArray_Binding::XRInputSourceArrayMethods;
+use crate::dom::bindings::codegen::Bindings::XRReferenceSpaceBinding::XRReferenceSpaceType;
+use crate::dom::bindings::codegen::Bindings::XRRenderStateBinding::{
+ XRRenderStateInit, XRRenderStateMethods,
+};
+use crate::dom::bindings::codegen::Bindings::XRSessionBinding::{
+ XREnvironmentBlendMode, XRFrameRequestCallback, XRInteractionMode, XRSessionMethods,
+ XRVisibilityState,
+};
+use crate::dom::bindings::codegen::Bindings::XRSystemBinding::XRSessionMode;
+use crate::dom::bindings::error::{Error, ErrorResult};
+use crate::dom::bindings::inheritance::Castable;
+use crate::dom::bindings::num::Finite;
+use crate::dom::bindings::refcounted::Trusted;
+use crate::dom::bindings::reflector::{reflect_dom_object, DomObject};
+use crate::dom::bindings::root::{Dom, DomRoot, MutDom, MutNullableDom};
+use crate::dom::bindings::utils::to_frozen_array;
+use crate::dom::event::Event;
+use crate::dom::eventtarget::EventTarget;
+use crate::dom::globalscope::GlobalScope;
+use crate::dom::promise::Promise;
+use crate::dom::xrboundedreferencespace::XRBoundedReferenceSpace;
+use crate::dom::xrframe::XRFrame;
+use crate::dom::xrhittestsource::XRHitTestSource;
+use crate::dom::xrinputsourcearray::XRInputSourceArray;
+use crate::dom::xrinputsourceevent::XRInputSourceEvent;
+use crate::dom::xrreferencespace::XRReferenceSpace;
+use crate::dom::xrreferencespaceevent::XRReferenceSpaceEvent;
+use crate::dom::xrrenderstate::XRRenderState;
+use crate::dom::xrrigidtransform::XRRigidTransform;
+use crate::dom::xrsessionevent::XRSessionEvent;
+use crate::dom::xrspace::XRSpace;
+use crate::realms::InRealm;
+use crate::script_runtime::JSContext;
+use crate::task_source::TaskSource;
+use crate::script_runtime::CanGc;
+
+#[dom_struct]
+pub struct XRSession {
+ eventtarget: EventTarget,
+ blend_mode: XREnvironmentBlendMode,
+ mode: XRSessionMode,
+ visibility_state: Cell<XRVisibilityState>,
+ viewer_space: MutNullableDom<XRSpace>,
+ #[ignore_malloc_size_of = "defined in webxr"]
+ #[no_trace]
+ session: DomRefCell<Session>,
+ frame_requested: Cell<bool>,
+ pending_render_state: MutNullableDom<XRRenderState>,
+ active_render_state: MutDom<XRRenderState>,
+ /// Cached projection matrix for inline sessions
+ #[no_trace]
+ inline_projection_matrix: DomRefCell<Transform3D<f32, Viewer, Display>>,
+
+ next_raf_id: Cell<i32>,
+ #[ignore_malloc_size_of = "closures are hard"]
+ raf_callback_list: DomRefCell<Vec<(i32, Option<Rc<XRFrameRequestCallback>>)>>,
+ #[ignore_malloc_size_of = "closures are hard"]
+ current_raf_callback_list: DomRefCell<Vec<(i32, Option<Rc<XRFrameRequestCallback>>)>>,
+ input_sources: Dom<XRInputSourceArray>,
+ // Any promises from calling end()
+ #[ignore_malloc_size_of = "promises are hard"]
+ end_promises: DomRefCell<Vec<Rc<Promise>>>,
+ /// <https://immersive-web.github.io/webxr/#ended>
+ ended: Cell<bool>,
+ #[ignore_malloc_size_of = "defined in webxr"]
+ #[no_trace]
+ next_hit_test_id: Cell<HitTestId>,
+ #[ignore_malloc_size_of = "defined in webxr"]
+ pending_hit_test_promises: DomRefCell<HashMapTracedValues<HitTestId, Rc<Promise>>>,
+ /// Opaque framebuffers need to know the session is "outside of a requestAnimationFrame"
+ /// <https://immersive-web.github.io/webxr/#opaque-framebuffer>
+ outside_raf: Cell<bool>,
+ #[ignore_malloc_size_of = "defined in webxr"]
+ #[no_trace]
+ input_frames: DomRefCell<HashMap<InputId, InputFrame>>,
+ framerate: Cell<f32>,
+ #[ignore_malloc_size_of = "promises are hard"]
+ update_framerate_promise: DomRefCell<Option<Rc<Promise>>>,
+ reference_spaces: DomRefCell<Vec<Dom<XRReferenceSpace>>>,
+}
+
+impl XRSession {
+ fn new_inherited(
+ session: Session,
+ render_state: &XRRenderState,
+ input_sources: &XRInputSourceArray,
+ mode: XRSessionMode,
+ ) -> XRSession {
+ XRSession {
+ eventtarget: EventTarget::new_inherited(),
+ blend_mode: session.environment_blend_mode().into(),
+ mode,
+ visibility_state: Cell::new(XRVisibilityState::Visible),
+ viewer_space: Default::default(),
+ session: DomRefCell::new(session),
+ frame_requested: Cell::new(false),
+ pending_render_state: MutNullableDom::new(None),
+ active_render_state: MutDom::new(render_state),
+ inline_projection_matrix: Default::default(),
+
+ next_raf_id: Cell::new(0),
+ raf_callback_list: DomRefCell::new(vec![]),
+ current_raf_callback_list: DomRefCell::new(vec![]),
+ input_sources: Dom::from_ref(input_sources),
+ end_promises: DomRefCell::new(vec![]),
+ ended: Cell::new(false),
+ next_hit_test_id: Cell::new(HitTestId(0)),
+ pending_hit_test_promises: DomRefCell::new(HashMapTracedValues::new()),
+ outside_raf: Cell::new(true),
+ input_frames: DomRefCell::new(HashMap::new()),
+ framerate: Cell::new(0.0),
+ update_framerate_promise: DomRefCell::new(None),
+ reference_spaces: DomRefCell::new(Vec::new()),
+ }
+ }
+
+ pub fn new(
+ global: &GlobalScope,
+ session: Session,
+ mode: XRSessionMode,
+ frame_receiver: IpcReceiver<Frame>,
+ ) -> DomRoot<XRSession> {
+ let ivfov = if mode == XRSessionMode::Inline {
+ Some(FRAC_PI_2)
+ } else {
+ None
+ };
+ let render_state = XRRenderState::new(global, 0.1, 1000.0, ivfov, None, Vec::new());
+ let input_sources = XRInputSourceArray::new(global);
+ let ret = reflect_dom_object(
+ Box::new(XRSession::new_inherited(
+ session,
+ &render_state,
+ &input_sources,
+ mode,
+ )),
+ global,
+ );
+ ret.attach_event_handler();
+ ret.setup_raf_loop(frame_receiver);
+ ret
+ }
+
+ pub fn with_session<R, F: FnOnce(&Session) -> R>(&self, with: F) -> R {
+ let session = self.session.borrow();
+ with(&session)
+ }
+
+ pub fn is_ended(&self) -> bool {
+ self.ended.get()
+ }
+
+ pub fn is_immersive(&self) -> bool {
+ self.mode != XRSessionMode::Inline
+ }
+
+ // https://immersive-web.github.io/layers/#feature-descriptor-layers
+ pub fn has_layers_feature(&self) -> bool {
+ // We do not support creating layers other than projection layers
+ // https://github.com/servo/servo/issues/27493
+ false
+ }
+
+ fn setup_raf_loop(&self, frame_receiver: IpcReceiver<Frame>) {
+ let this = Trusted::new(self);
+ let global = self.global();
+ let window = global.as_window();
+ let (task_source, canceller) = window
+ .task_manager()
+ .dom_manipulation_task_source_with_canceller();
+ ROUTER.add_typed_route(
+ frame_receiver,
+ Box::new(move |message| {
+ let frame: Frame = message.unwrap();
+ let time = CrossProcessInstant::now();
+ let this = this.clone();
+ let _ = task_source.queue_with_canceller(
+ task!(xr_raf_callback: move || {
+ this.root().raf_callback(frame, time);
+ }),
+ &canceller,
+ );
+ }),
+ );
+
+ self.session.borrow_mut().start_render_loop();
+ }
+
+ pub fn is_outside_raf(&self) -> bool {
+ self.outside_raf.get()
+ }
+
+ fn attach_event_handler(&self) {
+ let this = Trusted::new(self);
+ let global = self.global();
+ let window = global.as_window();
+ let (task_source, canceller) = window
+ .task_manager()
+ .dom_manipulation_task_source_with_canceller();
+ let (sender, receiver) = ipc::channel(global.time_profiler_chan().clone()).unwrap();
+
+ ROUTER.add_typed_route(
+ receiver.to_ipc_receiver(),
+ Box::new(move |message| {
+ let this = this.clone();
+ let _ = task_source.queue_with_canceller(
+ task!(xr_event_callback: move || {
+ this.root().event_callback(message.unwrap(), CanGc::note());
+ }),
+ &canceller,
+ );
+ }),
+ );
+
+ // request animation frame
+ self.session.borrow_mut().set_event_dest(sender);
+ }
+
+ // Must be called after the promise for session creation is resolved
+ // https://github.com/immersive-web/webxr/issues/961
+ //
+ // This enables content that assumes all input sources are accompanied
+ // by an inputsourceschange event to work properly. Without
+ pub fn setup_initial_inputs(&self) {
+ let initial_inputs = self.session.borrow().initial_inputs().to_owned();
+
+ if initial_inputs.is_empty() {
+ // do not fire an empty event
+ return;
+ }
+
+ let global = self.global();
+ let window = global.as_window();
+ let (task_source, canceller) = window
+ .task_manager()
+ .dom_manipulation_task_source_with_canceller();
+ let this = Trusted::new(self);
+ // Queue a task so that it runs after resolve()'s microtasks complete
+ // so that content has a chance to attach a listener for inputsourceschange
+ let _ = task_source.queue_with_canceller(
+ task!(session_initial_inputs: move || {
+ let this = this.root();
+ this.input_sources.add_input_sources(&this, &initial_inputs, CanGc::note());
+ }),
+ &canceller,
+ );
+ }
+
+ fn event_callback(&self, event: XREvent, can_gc: CanGc) {
+ match event {
+ XREvent::SessionEnd => {
+ // https://immersive-web.github.io/webxr/#shut-down-the-session
+ // Step 2
+ self.ended.set(true);
+ // Step 3-4
+ self.global().as_window().Navigator().Xr().end_session(self);
+ // Step 5: We currently do not have any such promises
+ // Step 6 is happening n the XR session
+ // https://immersive-web.github.io/webxr/#dom-xrsession-end step 3
+ for promise in self.end_promises.borrow_mut().drain(..) {
+ promise.resolve_native(&());
+ }
+ // Step 7
+ let event =
+ XRSessionEvent::new(&self.global(), atom!("end"), false, false, self, can_gc);
+ event.upcast::<Event>().fire(self.upcast(), can_gc);
+ },
+ XREvent::Select(input, kind, ty, frame) => {
+ use servo_atoms::Atom;
+ const START_ATOMS: [Atom; 2] = [atom!("selectstart"), atom!("squeezestart")];
+ const EVENT_ATOMS: [Atom; 2] = [atom!("select"), atom!("squeeze")];
+ const END_ATOMS: [Atom; 2] = [atom!("selectend"), atom!("squeezeend")];
+
+ // https://immersive-web.github.io/webxr/#primary-action
+ let source = self.input_sources.find(input);
+ let atom_index = if kind == SelectKind::Squeeze { 1 } else { 0 };
+ if let Some(source) = source {
+ let frame = XRFrame::new(&self.global(), self, frame);
+ frame.set_active(true);
+ if ty == SelectEvent::Start {
+ let event = XRInputSourceEvent::new(
+ &self.global(),
+ START_ATOMS[atom_index].clone(),
+ false,
+ false,
+ &frame,
+ &source,
+ can_gc,
+ );
+ event.upcast::<Event>().fire(self.upcast(), can_gc);
+ } else {
+ if ty == SelectEvent::Select {
+ let event = XRInputSourceEvent::new(
+ &self.global(),
+ EVENT_ATOMS[atom_index].clone(),
+ false,
+ false,
+ &frame,
+ &source,
+ can_gc,
+ );
+ event.upcast::<Event>().fire(self.upcast(), can_gc);
+ }
+ let event = XRInputSourceEvent::new(
+ &self.global(),
+ END_ATOMS[atom_index].clone(),
+ false,
+ false,
+ &frame,
+ &source,
+ can_gc,
+ );
+ event.upcast::<Event>().fire(self.upcast(), can_gc);
+ }
+ frame.set_active(false);
+ }
+ },
+ XREvent::VisibilityChange(v) => {
+ let v = match v {
+ Visibility::Visible => XRVisibilityState::Visible,
+ Visibility::VisibleBlurred => XRVisibilityState::Visible_blurred,
+ Visibility::Hidden => XRVisibilityState::Hidden,
+ };
+ self.visibility_state.set(v);
+ let event = XRSessionEvent::new(
+ &self.global(),
+ atom!("visibilitychange"),
+ false,
+ false,
+ self,
+ can_gc,
+ );
+ event.upcast::<Event>().fire(self.upcast(), can_gc);
+ // The page may be visible again, dirty the layers
+ // This also wakes up the event loop if necessary
+ self.dirty_layers();
+ },
+ XREvent::AddInput(info) => {
+ self.input_sources.add_input_sources(self, &[info], can_gc);
+ },
+ XREvent::RemoveInput(id) => {
+ self.input_sources.remove_input_source(self, id, can_gc);
+ },
+ XREvent::UpdateInput(id, source) => {
+ self.input_sources
+ .add_remove_input_source(self, id, source, can_gc);
+ },
+ XREvent::InputChanged(id, frame) => {
+ self.input_frames.borrow_mut().insert(id, frame);
+ },
+ XREvent::ReferenceSpaceChanged(base_space, transform) => {
+ self.reference_spaces
+ .borrow()
+ .iter()
+ .filter(|space| {
+ let base = match space.ty() {
+ XRReferenceSpaceType::Local => webxr_api::BaseSpace::Local,
+ XRReferenceSpaceType::Viewer => webxr_api::BaseSpace::Viewer,
+ XRReferenceSpaceType::Local_floor => webxr_api::BaseSpace::Floor,
+ XRReferenceSpaceType::Bounded_floor => {
+ webxr_api::BaseSpace::BoundedFloor
+ },
+ _ => panic!("unsupported reference space found"),
+ };
+ base == base_space
+ })
+ .for_each(|space| {
+ let offset = XRRigidTransform::new(&self.global(), transform, can_gc);
+ let event = XRReferenceSpaceEvent::new(
+ &self.global(),
+ atom!("reset"),
+ false,
+ false,
+ space,
+ Some(&*offset),
+ can_gc,
+ );
+ event.upcast::<Event>().fire(space.upcast(), can_gc);
+ });
+ },
+ }
+ }
+
+ /// <https://immersive-web.github.io/webxr/#xr-animation-frame>
+ fn raf_callback(&self, mut frame: Frame, time: CrossProcessInstant) {
+ debug!("WebXR RAF callback {:?}", frame);
+
+ // Step 1-2 happen in the xebxr device thread
+
+ // Step 3
+ if let Some(pending) = self.pending_render_state.take() {
+ // https://immersive-web.github.io/webxr/#apply-the-pending-render-state
+ // (Steps 1-4 are implicit)
+ // Step 5
+ self.active_render_state.set(&pending);
+ // Step 6-7: XXXManishearth handle inlineVerticalFieldOfView
+
+ if !self.is_immersive() {
+ self.update_inline_projection_matrix()
+ }
+ }
+
+ // TODO: how does this fit the webxr spec?
+ for event in frame.events.drain(..) {
+ self.handle_frame_event(event);
+ }
+
+ // Step 4
+ // TODO: what should this check be?
+ // This is checking that the new render state has the same
+ // layers as the frame.
+ // Related to https://github.com/immersive-web/webxr/issues/1051
+ if !self
+ .active_render_state
+ .get()
+ .has_sub_images(&frame.sub_images[..])
+ {
+ // If the frame has different layers than the render state,
+ // we just return early, drawing a blank frame.
+ // This can result in flickering when the render state is changed.
+ // TODO: it would be better to not render anything until the next frame.
+ warn!("Rendering blank XR frame");
+ self.session.borrow_mut().render_animation_frame();
+ return;
+ }
+
+ // Step 5: XXXManishearth handle inline session
+
+ // Step 6-7
+ {
+ let mut current = self.current_raf_callback_list.borrow_mut();
+ assert!(current.is_empty());
+ mem::swap(&mut *self.raf_callback_list.borrow_mut(), &mut current);
+ }
+
+ let time = self.global().performance().to_dom_high_res_time_stamp(time);
+ let frame = XRFrame::new(&self.global(), self, frame);
+
+ // Step 8-9
+ frame.set_active(true);
+ frame.set_animation_frame(true);
+
+ // Step 10
+ self.apply_frame_updates(&frame);
+
+ // TODO: how does this fit with the webxr and xr layers specs?
+ self.layers_begin_frame(&frame);
+
+ // Step 11-12
+ self.outside_raf.set(false);
+ let len = self.current_raf_callback_list.borrow().len();
+ for i in 0..len {
+ let callback = self.current_raf_callback_list.borrow()[i].1.clone();
+ if let Some(callback) = callback {
+ let _ = callback.Call__(time, &frame, ExceptionHandling::Report);
+ }
+ }
+ self.outside_raf.set(true);
+ *self.current_raf_callback_list.borrow_mut() = vec![];
+
+ // TODO: how does this fit with the webxr and xr layers specs?
+ self.layers_end_frame(&frame);
+
+ // Step 13
+ frame.set_active(false);
+
+ // TODO: how does this fit the webxr spec?
+ self.session.borrow_mut().render_animation_frame();
+ }
+
+ fn update_inline_projection_matrix(&self) {
+ debug_assert!(!self.is_immersive());
+ let render_state = self.active_render_state.get();
+ let size = if let Some(base) = render_state.GetBaseLayer() {
+ base.size()
+ } else {
+ return;
+ };
+ let mut clip_planes = util::ClipPlanes::default();
+ let near = *render_state.DepthNear() as f32;
+ let far = *render_state.DepthFar() as f32;
+ clip_planes.update(near, far);
+ let top = *render_state
+ .GetInlineVerticalFieldOfView()
+ .expect("IVFOV should be non null for inline sessions") /
+ 2.;
+ let top = near * top.tan() as f32;
+ let bottom = top;
+ let left = top * size.width as f32 / size.height as f32;
+ let right = left;
+ let matrix = util::frustum_to_projection_matrix(left, right, top, bottom, clip_planes);
+ *self.inline_projection_matrix.borrow_mut() = matrix;
+ }
+
+ /// Constructs a View suitable for inline sessions using the inlineVerticalFieldOfView and canvas size
+ pub fn inline_view(&self) -> View<Viewer> {
+ debug_assert!(!self.is_immersive());
+ View {
+ // Inline views have no offset
+ transform: RigidTransform3D::identity(),
+ projection: *self.inline_projection_matrix.borrow(),
+ }
+ }
+
+ pub fn session_id(&self) -> SessionId {
+ self.session.borrow().id()
+ }
+
+ pub fn dirty_layers(&self) {
+ if let Some(layer) = self.RenderState().GetBaseLayer() {
+ layer.context().mark_as_dirty();
+ }
+ }
+
+ // TODO: how does this align with the layers spec?
+ fn layers_begin_frame(&self, frame: &XRFrame) {
+ if let Some(layer) = self.active_render_state.get().GetBaseLayer() {
+ layer.begin_frame(frame);
+ }
+ self.active_render_state.get().with_layers(|layers| {
+ for layer in layers {
+ layer.begin_frame(frame);
+ }
+ });
+ }
+
+ // TODO: how does this align with the layers spec?
+ fn layers_end_frame(&self, frame: &XRFrame) {
+ if let Some(layer) = self.active_render_state.get().GetBaseLayer() {
+ layer.end_frame(frame);
+ }
+ self.active_render_state.get().with_layers(|layers| {
+ for layer in layers {
+ layer.end_frame(frame);
+ }
+ });
+ }
+
+ /// <https://immersive-web.github.io/webxr/#xrframe-apply-frame-updates>
+ fn apply_frame_updates(&self, _frame: &XRFrame) {
+ // <https://www.w3.org/TR/webxr-gamepads-module-1/#xrframe-apply-gamepad-frame-updates>
+ for (id, frame) in self.input_frames.borrow_mut().drain() {
+ let source = self.input_sources.find(id);
+ if let Some(source) = source {
+ source.update_gamepad_state(frame);
+ }
+ }
+ }
+
+ fn handle_frame_event(&self, event: FrameUpdateEvent) {
+ match event {
+ FrameUpdateEvent::HitTestSourceAdded(id) => {
+ if let Some(promise) = self.pending_hit_test_promises.borrow_mut().remove(&id) {
+ promise.resolve_native(&XRHitTestSource::new(&self.global(), id, self));
+ } else {
+ warn!(
+ "received hit test add request for unknown hit test {:?}",
+ id
+ )
+ }
+ },
+ _ => self.session.borrow_mut().apply_event(event),
+ }
+ }
+
+ /// <https://www.w3.org/TR/webxr/#apply-the-nominal-frame-rate>
+ fn apply_nominal_framerate(&self, rate: f32, can_gc: CanGc) {
+ if self.framerate.get() == rate || self.ended.get() {
+ return;
+ }
+
+ self.framerate.set(rate);
+
+ let event = XRSessionEvent::new(
+ &self.global(),
+ Atom::from("frameratechange"),
+ false,
+ false,
+ self,
+ can_gc,
+ );
+ event.upcast::<Event>().fire(self.upcast(), can_gc);
+ }
+}
+
+impl XRSessionMethods<crate::DomTypeHolder> for XRSession {
+ // https://immersive-web.github.io/webxr/#eventdef-xrsession-end
+ event_handler!(end, GetOnend, SetOnend);
+
+ // https://immersive-web.github.io/webxr/#eventdef-xrsession-select
+ event_handler!(select, GetOnselect, SetOnselect);
+
+ // https://immersive-web.github.io/webxr/#eventdef-xrsession-selectstart
+ event_handler!(selectstart, GetOnselectstart, SetOnselectstart);
+
+ // https://immersive-web.github.io/webxr/#eventdef-xrsession-selectend
+ event_handler!(selectend, GetOnselectend, SetOnselectend);
+
+ // https://immersive-web.github.io/webxr/#eventdef-xrsession-squeeze
+ event_handler!(squeeze, GetOnsqueeze, SetOnsqueeze);
+
+ // https://immersive-web.github.io/webxr/#eventdef-xrsession-squeezestart
+ event_handler!(squeezestart, GetOnsqueezestart, SetOnsqueezestart);
+
+ // https://immersive-web.github.io/webxr/#eventdef-xrsession-squeezeend
+ event_handler!(squeezeend, GetOnsqueezeend, SetOnsqueezeend);
+
+ // https://immersive-web.github.io/webxr/#eventdef-xrsession-visibilitychange
+ event_handler!(
+ visibilitychange,
+ GetOnvisibilitychange,
+ SetOnvisibilitychange
+ );
+
+ // https://immersive-web.github.io/webxr/#eventdef-xrsession-inputsourceschange
+ event_handler!(
+ inputsourceschange,
+ GetOninputsourceschange,
+ SetOninputsourceschange
+ );
+
+ // https://www.w3.org/TR/webxr/#dom-xrsession-onframeratechange
+ event_handler!(frameratechange, GetOnframeratechange, SetOnframeratechange);
+
+ // https://immersive-web.github.io/webxr/#dom-xrsession-renderstate
+ fn RenderState(&self) -> DomRoot<XRRenderState> {
+ self.active_render_state.get()
+ }
+
+ /// <https://immersive-web.github.io/webxr/#dom-xrsession-updaterenderstate>
+ fn UpdateRenderState(&self, init: &XRRenderStateInit, _: InRealm) -> ErrorResult {
+ // Step 2
+ if self.ended.get() {
+ return Err(Error::InvalidState);
+ }
+ // Step 3:
+ if let Some(Some(ref layer)) = init.baseLayer {
+ if Dom::from_ref(layer.session()) != Dom::from_ref(self) {
+ return Err(Error::InvalidState);
+ }
+ }
+
+ // Step 4:
+ if init.inlineVerticalFieldOfView.is_some() && self.is_immersive() {
+ return Err(Error::InvalidState);
+ }
+
+ // https://immersive-web.github.io/layers/#updaterenderstatechanges
+ // Step 1.
+ if init.baseLayer.is_some() && (self.has_layers_feature() || init.layers.is_some()) {
+ return Err(Error::NotSupported);
+ }
+
+ if let Some(Some(ref layers)) = init.layers {
+ // Step 2
+ for layer in layers {
+ let count = layers
+ .iter()
+ .filter(|other| other.layer_id() == layer.layer_id())
+ .count();
+ if count > 1 {
+ return Err(Error::Type(String::from("Duplicate entry in WebXR layers")));
+ }
+ }
+
+ // Step 3
+ for layer in layers {
+ if layer.session() != self {
+ return Err(Error::Type(String::from(
+ "Layer from different session in WebXR layers",
+ )));
+ }
+ }
+ }
+
+ // Step 4-5
+ let pending = self
+ .pending_render_state
+ .or_init(|| self.active_render_state.get().clone_object());
+
+ // Step 6
+ if let Some(ref layers) = init.layers {
+ let layers = layers.as_deref().unwrap_or_default();
+ pending.set_base_layer(None);
+ pending.set_layers(layers.iter().map(|x| &**x).collect());
+ let layers = layers
+ .iter()
+ .filter_map(|layer| {
+ let context_id = WebXRContextId::from(layer.context_id());
+ let layer_id = layer.layer_id()?;
+ Some((context_id, layer_id))
+ })
+ .collect();
+ self.session.borrow_mut().set_layers(layers);
+ }
+
+ // End of https://immersive-web.github.io/layers/#updaterenderstatechanges
+
+ if let Some(near) = init.depthNear {
+ let mut near = *near;
+ // Step 8 from #apply-the-pending-render-state
+ // this may need to be changed if backends wish to impose
+ // further constraints
+ if near < 0. {
+ near = 0.;
+ }
+ pending.set_depth_near(near);
+ }
+ if let Some(far) = init.depthFar {
+ let mut far = *far;
+ // Step 9 from #apply-the-pending-render-state
+ // this may need to be changed if backends wish to impose
+ // further constraints
+ // currently the maximum is infinity, so just check that
+ // the value is non-negative
+ if far < 0. {
+ far = 0.;
+ }
+ pending.set_depth_far(far);
+ }
+ if let Some(fov) = init.inlineVerticalFieldOfView {
+ let mut fov = *fov;
+ // Step 10 from #apply-the-pending-render-state
+ // this may need to be changed if backends wish to impose
+ // further constraints
+ if fov < 0. {
+ fov = 0.0001;
+ } else if fov > PI {
+ fov = PI - 0.0001;
+ }
+ pending.set_inline_vertical_fov(fov);
+ }
+ if let Some(ref layer) = init.baseLayer {
+ pending.set_base_layer(layer.as_deref());
+ pending.set_layers(Vec::new());
+ let layers = layer
+ .iter()
+ .filter_map(|layer| {
+ let context_id = WebXRContextId::from(layer.context_id());
+ let layer_id = layer.layer_id()?;
+ Some((context_id, layer_id))
+ })
+ .collect();
+ self.session.borrow_mut().set_layers(layers);
+ }
+
+ if init.depthFar.is_some() || init.depthNear.is_some() {
+ self.session
+ .borrow_mut()
+ .update_clip_planes(*pending.DepthNear() as f32, *pending.DepthFar() as f32);
+ }
+
+ Ok(())
+ }
+
+ /// <https://immersive-web.github.io/webxr/#dom-xrsession-requestanimationframe>
+ fn RequestAnimationFrame(&self, callback: Rc<XRFrameRequestCallback>) -> i32 {
+ // queue up RAF callback, obtain ID
+ let raf_id = self.next_raf_id.get();
+ self.next_raf_id.set(raf_id + 1);
+ self.raf_callback_list
+ .borrow_mut()
+ .push((raf_id, Some(callback)));
+
+ raf_id
+ }
+
+ /// <https://immersive-web.github.io/webxr/#dom-xrsession-cancelanimationframe>
+ fn CancelAnimationFrame(&self, frame: i32) {
+ let mut list = self.raf_callback_list.borrow_mut();
+ if let Some(pair) = list.iter_mut().find(|pair| pair.0 == frame) {
+ pair.1 = None;
+ }
+
+ let mut list = self.current_raf_callback_list.borrow_mut();
+ if let Some(pair) = list.iter_mut().find(|pair| pair.0 == frame) {
+ pair.1 = None;
+ }
+ }
+
+ /// <https://immersive-web.github.io/webxr/#dom-xrsession-environmentblendmode>
+ fn EnvironmentBlendMode(&self) -> XREnvironmentBlendMode {
+ self.blend_mode
+ }
+
+ /// <https://immersive-web.github.io/webxr/#dom-xrsession-visibilitystate>
+ fn VisibilityState(&self) -> XRVisibilityState {
+ self.visibility_state.get()
+ }
+
+ /// <https://immersive-web.github.io/webxr/#dom-xrsession-requestreferencespace>
+ fn RequestReferenceSpace(
+ &self,
+ ty: XRReferenceSpaceType,
+ comp: InRealm,
+ can_gc: CanGc,
+ ) -> Rc<Promise> {
+ let p = Promise::new_in_current_realm(comp, can_gc);
+
+ // https://immersive-web.github.io/webxr/#create-a-reference-space
+
+ // XXXManishearth reject based on session type
+ // https://github.com/immersive-web/webxr/blob/master/spatial-tracking-explainer.md#practical-usage-guidelines
+
+ if !self.is_immersive() &&
+ (ty == XRReferenceSpaceType::Bounded_floor || ty == XRReferenceSpaceType::Unbounded)
+ {
+ p.reject_error(Error::NotSupported);
+ return p;
+ }
+
+ match ty {
+ XRReferenceSpaceType::Unbounded => {
+ // XXXmsub2 figure out how to support this
+ p.reject_error(Error::NotSupported)
+ },
+ ty => {
+ if ty != XRReferenceSpaceType::Viewer &&
+ (!self.is_immersive() || ty != XRReferenceSpaceType::Local)
+ {
+ let s = ty.as_str();
+ if !self
+ .session
+ .borrow()
+ .granted_features()
+ .iter()
+ .any(|f| *f == s)
+ {
+ p.reject_error(Error::NotSupported);
+ return p;
+ }
+ }
+ if ty == XRReferenceSpaceType::Bounded_floor {
+ let space = XRBoundedReferenceSpace::new(&self.global(), self, can_gc);
+ self.reference_spaces
+ .borrow_mut()
+ .push(Dom::from_ref(space.reference_space()));
+ p.resolve_native(&space);
+ } else {
+ let space = XRReferenceSpace::new(&self.global(), self, ty, can_gc);
+ self.reference_spaces
+ .borrow_mut()
+ .push(Dom::from_ref(&*space));
+ p.resolve_native(&space);
+ }
+ },
+ }
+ p
+ }
+
+ /// <https://immersive-web.github.io/webxr/#dom-xrsession-inputsources>
+ fn InputSources(&self) -> DomRoot<XRInputSourceArray> {
+ DomRoot::from_ref(&*self.input_sources)
+ }
+
+ /// <https://immersive-web.github.io/webxr/#dom-xrsession-end>
+ fn End(&self, can_gc: CanGc) -> Rc<Promise> {
+ let global = self.global();
+ let p = Promise::new(&global, can_gc);
+ if self.ended.get() && self.end_promises.borrow().is_empty() {
+ // If the session has completely ended and all end promises have been resolved,
+ // don't queue up more end promises
+ //
+ // We need to check for end_promises being empty because `ended` is set
+ // before everything has been completely shut down, and we do not want to
+ // prematurely resolve the promise then
+ //
+ // However, if end_promises is empty, then all end() promises have already resolved,
+ // so the session has completely shut down and we should not queue up more promises
+ p.resolve_native(&());
+ return p;
+ }
+ self.end_promises.borrow_mut().push(p.clone());
+ // This is duplicated in event_callback since this should
+ // happen ASAP for end() but can happen later if the device
+ // shuts itself down
+ self.ended.set(true);
+ global.as_window().Navigator().Xr().end_session(self);
+ self.session.borrow_mut().end_session();
+ // Disconnect any still-attached XRInputSources
+ for source in 0..self.input_sources.Length() {
+ self.input_sources
+ .remove_input_source(self, InputId(source), can_gc);
+ }
+ p
+ }
+
+ // https://immersive-web.github.io/hit-test/#dom-xrsession-requesthittestsource
+ fn RequestHitTestSource(&self, options: &XRHitTestOptionsInit, can_gc: CanGc) -> Rc<Promise> {
+ let p = Promise::new(&self.global(), can_gc);
+
+ if !self
+ .session
+ .borrow()
+ .granted_features()
+ .iter()
+ .any(|f| f == "hit-test")
+ {
+ p.reject_error(Error::NotSupported);
+ return p;
+ }
+
+ let id = self.next_hit_test_id.get();
+ self.next_hit_test_id.set(HitTestId(id.0 + 1));
+
+ let space = options.space.space();
+ let ray = if let Some(ref ray) = options.offsetRay {
+ ray.ray()
+ } else {
+ Ray {
+ origin: Vector3D::new(0., 0., 0.),
+ direction: Vector3D::new(0., 0., -1.),
+ }
+ };
+
+ let mut types = EntityTypes::default();
+
+ if let Some(ref tys) = options.entityTypes {
+ for ty in tys {
+ match ty {
+ XRHitTestTrackableType::Point => types.point = true,
+ XRHitTestTrackableType::Plane => types.plane = true,
+ XRHitTestTrackableType::Mesh => types.mesh = true,
+ }
+ }
+ } else {
+ types.plane = true;
+ }
+
+ let source = HitTestSource {
+ id,
+ space,
+ ray,
+ types,
+ };
+ self.pending_hit_test_promises
+ .borrow_mut()
+ .insert(id, p.clone());
+
+ self.session.borrow().request_hit_test(source);
+
+ p
+ }
+
+ /// <https://www.w3.org/TR/webxr-ar-module-1/#dom-xrsession-interactionmode>
+ fn InteractionMode(&self) -> XRInteractionMode {
+ // Until Servo supports WebXR sessions on mobile phones or similar non-XR devices,
+ // this should always be world space
+ XRInteractionMode::World_space
+ }
+
+ /// <https://www.w3.org/TR/webxr/#dom-xrsession-framerate>
+ fn GetFrameRate(&self) -> Option<Finite<f32>> {
+ let session = self.session.borrow();
+ if self.mode == XRSessionMode::Inline || session.supported_frame_rates().is_empty() {
+ None
+ } else {
+ Finite::new(self.framerate.get())
+ }
+ }
+
+ /// <https://www.w3.org/TR/webxr/#dom-xrsession-supportedframerates>
+ fn GetSupportedFrameRates(&self, cx: JSContext) -> Option<Float32Array> {
+ let session = self.session.borrow();
+ if self.mode == XRSessionMode::Inline || session.supported_frame_rates().is_empty() {
+ None
+ } else {
+ let framerates = session.supported_frame_rates();
+ rooted!(in (*cx) let mut array = ptr::null_mut::<JSObject>());
+ Some(
+ create_buffer_source(cx, framerates, array.handle_mut())
+ .expect("Failed to construct supported frame rates array"),
+ )
+ }
+ }
+
+ /// <https://www.w3.org/TR/webxr/#dom-xrsession-enabledfeatures>
+ fn EnabledFeatures(&self, cx: JSContext, retval: MutableHandleValue) {
+ let session = self.session.borrow();
+ let features = session.granted_features();
+ to_frozen_array(features, cx, retval)
+ }
+
+ /// <https://www.w3.org/TR/webxr/#dom-xrsession-issystemkeyboardsupported>
+ fn IsSystemKeyboardSupported(&self) -> bool {
+ // Support for this only exists on Meta headsets (no desktop support)
+ // so this will always be false until that changes
+ false
+ }
+
+ /// <https://www.w3.org/TR/webxr/#dom-xrsession-updatetargetframerate>
+ fn UpdateTargetFrameRate(
+ &self,
+ rate: Finite<f32>,
+ comp: InRealm,
+ can_gc: CanGc,
+ ) -> Rc<Promise> {
+ let promise = Promise::new_in_current_realm(comp, can_gc);
+ {
+ let session = self.session.borrow();
+ let supported_frame_rates = session.supported_frame_rates();
+
+ if self.mode == XRSessionMode::Inline ||
+ supported_frame_rates.is_empty() ||
+ self.ended.get()
+ {
+ promise.reject_error(Error::InvalidState);
+ return promise;
+ }
+
+ if !supported_frame_rates.contains(&*rate) {
+ promise.reject_error(Error::Type("Provided framerate not supported".into()));
+ return promise;
+ }
+ }
+
+ *self.update_framerate_promise.borrow_mut() = Some(promise.clone());
+
+ let this = Trusted::new(self);
+ let global = self.global();
+ let window = global.as_window();
+ let (task_source, canceller) = window
+ .task_manager()
+ .dom_manipulation_task_source_with_canceller();
+ let (sender, receiver) = ipc::channel(global.time_profiler_chan().clone()).unwrap();
+
+ ROUTER.add_typed_route(
+ receiver.to_ipc_receiver(),
+ Box::new(move |message| {
+ let this = this.clone();
+ let _ = task_source.queue_with_canceller(
+ task!(update_session_framerate: move || {
+ let session = this.root();
+ session.apply_nominal_framerate(message.unwrap(), CanGc::note());
+ if let Some(promise) = session.update_framerate_promise.borrow_mut().take() {
+ promise.resolve_native(&());
+ };
+ }),
+ &canceller,
+ );
+ }),
+ );
+
+ self.session.borrow_mut().update_frame_rate(*rate, sender);
+
+ promise
+ }
+}
+
+// The pose of an object in native-space. Should never be exposed.
+pub type ApiPose = RigidTransform3D<f32, ApiSpace, webxr_api::Native>;
+// A transform between objects in some API-space
+pub type ApiRigidTransform = RigidTransform3D<f32, ApiSpace, ApiSpace>;
+
+#[derive(Clone, Copy)]
+pub struct BaseSpace;
+
+pub type BaseTransform = RigidTransform3D<f32, webxr_api::Native, BaseSpace>;
+
+#[allow(unsafe_code)]
+pub fn cast_transform<T, U, V, W>(
+ transform: RigidTransform3D<f32, T, U>,
+) -> RigidTransform3D<f32, V, W> {
+ unsafe { mem::transmute(transform) }
+}
+
+impl From<EnvironmentBlendMode> for XREnvironmentBlendMode {
+ fn from(x: EnvironmentBlendMode) -> Self {
+ match x {
+ EnvironmentBlendMode::Opaque => XREnvironmentBlendMode::Opaque,
+ EnvironmentBlendMode::AlphaBlend => XREnvironmentBlendMode::Alpha_blend,
+ EnvironmentBlendMode::Additive => XREnvironmentBlendMode::Additive,
+ }
+ }
+}