aboutsummaryrefslogtreecommitdiffstats
path: root/components/shared/webxr
diff options
context:
space:
mode:
authorMartin Robinson <mrobinson@igalia.com>2025-01-30 20:07:35 +0100
committerGitHub <noreply@github.com>2025-01-30 19:07:35 +0000
commit534e78db5331fbfbad7e60d72a88e9aacdc11ee4 (patch)
tree3bcd217e0e7b7fd0c91d5406a81ea241ffc4ce06 /components/shared/webxr
parent64b40ea70065f949d1e281bd046c56d50312f2a7 (diff)
downloadservo-534e78db5331fbfbad7e60d72a88e9aacdc11ee4.tar.gz
servo-534e78db5331fbfbad7e60d72a88e9aacdc11ee4.zip
Merge webxr repository (#35228)
Signed-off-by: Martin Robinson <mrobinson@igalia.com>
Diffstat (limited to 'components/shared/webxr')
-rw-r--r--components/shared/webxr/Cargo.toml28
-rw-r--r--components/shared/webxr/device.rs114
-rw-r--r--components/shared/webxr/error.rs21
-rw-r--r--components/shared/webxr/events.rs80
-rw-r--r--components/shared/webxr/frame.rs60
-rw-r--r--components/shared/webxr/hand.rs122
-rw-r--r--components/shared/webxr/hittest.rs179
-rw-r--r--components/shared/webxr/input.rs74
-rw-r--r--components/shared/webxr/layer.rs296
-rw-r--r--components/shared/webxr/lib.rs175
-rw-r--r--components/shared/webxr/mock.rs146
-rw-r--r--components/shared/webxr/registry.rs262
-rw-r--r--components/shared/webxr/session.rs531
-rw-r--r--components/shared/webxr/space.rs28
-rw-r--r--components/shared/webxr/util.rs129
-rw-r--r--components/shared/webxr/view.rs170
16 files changed, 2415 insertions, 0 deletions
diff --git a/components/shared/webxr/Cargo.toml b/components/shared/webxr/Cargo.toml
new file mode 100644
index 00000000000..47caee00131
--- /dev/null
+++ b/components/shared/webxr/Cargo.toml
@@ -0,0 +1,28 @@
+[package]
+name = "webxr-api"
+version = "0.0.1"
+authors = ["The Servo Project Developers"]
+edition = "2018"
+
+homepage = "https://github.com/servo/webxr"
+repository = "https://github.com/servo/webxr"
+keywords = ["ar", "headset", "openxr", "vr", "webxr"]
+license = "MPL-2.0"
+
+description = '''A safe Rust API that provides a way to interact with
+virtual reality and augmented reality devices and integration with OpenXR.
+The API is inspired by the WebXR Device API (https://www.w3.org/TR/webxr/)
+but adapted to Rust design patterns.'''
+
+[lib]
+path = "lib.rs"
+
+[features]
+ipc = ["serde", "ipc-channel", "euclid/serde"]
+
+[dependencies]
+euclid = "0.22"
+ipc-channel = { version = "0.19", optional = true }
+log = "0.4"
+serde = { version = "1.0", optional = true }
+time = { version = "0.1", optional = true }
diff --git a/components/shared/webxr/device.rs b/components/shared/webxr/device.rs
new file mode 100644
index 00000000000..65f34d8560b
--- /dev/null
+++ b/components/shared/webxr/device.rs
@@ -0,0 +1,114 @@
+/* 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/. */
+
+//! Traits to be implemented by backends
+
+use crate::ContextId;
+use crate::EnvironmentBlendMode;
+use crate::Error;
+use crate::Event;
+use crate::Floor;
+use crate::Frame;
+use crate::HitTestId;
+use crate::HitTestSource;
+use crate::InputSource;
+use crate::LayerId;
+use crate::LayerInit;
+use crate::Native;
+use crate::Quitter;
+use crate::Sender;
+use crate::Session;
+use crate::SessionBuilder;
+use crate::SessionInit;
+use crate::SessionMode;
+use crate::Viewports;
+
+use euclid::{Point2D, RigidTransform3D};
+
+/// A trait for discovering XR devices
+pub trait DiscoveryAPI<GL>: 'static {
+ fn request_session(
+ &mut self,
+ mode: SessionMode,
+ init: &SessionInit,
+ xr: SessionBuilder<GL>,
+ ) -> Result<Session, Error>;
+ fn supports_session(&self, mode: SessionMode) -> bool;
+}
+
+/// A trait for using an XR device
+pub trait DeviceAPI: 'static {
+ /// Create a new layer
+ fn create_layer(&mut self, context_id: ContextId, init: LayerInit) -> Result<LayerId, Error>;
+
+ /// Destroy a layer
+ fn destroy_layer(&mut self, context_id: ContextId, layer_id: LayerId);
+
+ /// The transform from native coordinates to the floor.
+ fn floor_transform(&self) -> Option<RigidTransform3D<f32, Native, Floor>>;
+
+ fn viewports(&self) -> Viewports;
+
+ /// Begin an animation frame.
+ fn begin_animation_frame(&mut self, layers: &[(ContextId, LayerId)]) -> Option<Frame>;
+
+ /// End an animation frame, render the layer to the device, and block waiting for the next frame.
+ fn end_animation_frame(&mut self, layers: &[(ContextId, LayerId)]);
+
+ /// Inputs registered with the device on initialization. More may be added, which
+ /// should be communicated through a yet-undecided event mechanism
+ fn initial_inputs(&self) -> Vec<InputSource>;
+
+ /// Sets the event handling channel
+ fn set_event_dest(&mut self, dest: Sender<Event>);
+
+ /// Quit the session
+ fn quit(&mut self);
+
+ fn set_quitter(&mut self, quitter: Quitter);
+
+ fn update_clip_planes(&mut self, near: f32, far: f32);
+
+ fn environment_blend_mode(&self) -> EnvironmentBlendMode {
+ // for VR devices, override for AR
+ EnvironmentBlendMode::Opaque
+ }
+
+ fn granted_features(&self) -> &[String];
+
+ fn request_hit_test(&mut self, _source: HitTestSource) {
+ panic!("This device does not support requesting hit tests");
+ }
+
+ fn cancel_hit_test(&mut self, _id: HitTestId) {
+ panic!("This device does not support hit tests");
+ }
+
+ fn update_frame_rate(&mut self, rate: f32) -> f32 {
+ rate
+ }
+
+ fn supported_frame_rates(&self) -> Vec<f32> {
+ Vec::new()
+ }
+
+ fn reference_space_bounds(&self) -> Option<Vec<Point2D<f32, Floor>>> {
+ None
+ }
+}
+
+impl<GL: 'static> DiscoveryAPI<GL> for Box<dyn DiscoveryAPI<GL>> {
+ fn request_session(
+ &mut self,
+ mode: SessionMode,
+ init: &SessionInit,
+ xr: SessionBuilder<GL>,
+ ) -> Result<Session, Error> {
+ (&mut **self).request_session(mode, init, xr)
+ }
+
+ fn supports_session(&self, mode: SessionMode) -> bool {
+ (&**self).supports_session(mode)
+ }
+}
diff --git a/components/shared/webxr/error.rs b/components/shared/webxr/error.rs
new file mode 100644
index 00000000000..86822f6fb7a
--- /dev/null
+++ b/components/shared/webxr/error.rs
@@ -0,0 +1,21 @@
+/* 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/. */
+
+#[cfg(feature = "ipc")]
+use serde::{Deserialize, Serialize};
+
+/// Errors that can be produced by XR.
+
+// TODO: this is currently incomplete!
+
+#[derive(Debug)]
+#[cfg_attr(feature = "ipc", derive(Serialize, Deserialize))]
+pub enum Error {
+ NoMatchingDevice,
+ CommunicationError,
+ ThreadCreationError,
+ InlineSession,
+ UnsupportedFeature(String),
+ BackendSpecific(String),
+}
diff --git a/components/shared/webxr/events.rs b/components/shared/webxr/events.rs
new file mode 100644
index 00000000000..338913464ba
--- /dev/null
+++ b/components/shared/webxr/events.rs
@@ -0,0 +1,80 @@
+/* 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 euclid::RigidTransform3D;
+
+use crate::ApiSpace;
+use crate::BaseSpace;
+use crate::Frame;
+use crate::InputFrame;
+use crate::InputId;
+use crate::InputSource;
+use crate::SelectEvent;
+use crate::SelectKind;
+use crate::Sender;
+
+#[derive(Clone, Debug)]
+#[cfg_attr(feature = "ipc", derive(serde::Serialize, serde::Deserialize))]
+pub enum Event {
+ /// Input source connected
+ AddInput(InputSource),
+ /// Input source disconnected
+ RemoveInput(InputId),
+ /// Input updated (this is a disconnect+reconnect)
+ UpdateInput(InputId, InputSource),
+ /// Session ended by device
+ SessionEnd,
+ /// Session focused/blurred/etc
+ VisibilityChange(Visibility),
+ /// Selection started / ended
+ Select(InputId, SelectKind, SelectEvent, Frame),
+ /// Input from an input source has changed
+ InputChanged(InputId, InputFrame),
+ /// Reference space has changed
+ ReferenceSpaceChanged(BaseSpace, RigidTransform3D<f32, ApiSpace, ApiSpace>),
+}
+
+#[derive(Copy, Clone, Debug)]
+#[cfg_attr(feature = "ipc", derive(serde::Serialize, serde::Deserialize))]
+pub enum Visibility {
+ /// Session fully displayed to user
+ Visible,
+ /// Session still visible, but is not the primary focus
+ VisibleBlurred,
+ /// Session not visible
+ Hidden,
+}
+
+/// Convenience structure for buffering up events
+/// when no event callback has been set
+pub enum EventBuffer {
+ Buffered(Vec<Event>),
+ Sink(Sender<Event>),
+}
+
+impl Default for EventBuffer {
+ fn default() -> Self {
+ EventBuffer::Buffered(vec![])
+ }
+}
+
+impl EventBuffer {
+ pub fn callback(&mut self, event: Event) {
+ match *self {
+ EventBuffer::Buffered(ref mut events) => events.push(event),
+ EventBuffer::Sink(ref dest) => {
+ let _ = dest.send(event);
+ }
+ }
+ }
+
+ pub fn upgrade(&mut self, dest: Sender<Event>) {
+ if let EventBuffer::Buffered(ref mut events) = *self {
+ for event in events.drain(..) {
+ let _ = dest.send(event);
+ }
+ }
+ *self = EventBuffer::Sink(dest)
+ }
+}
diff --git a/components/shared/webxr/frame.rs b/components/shared/webxr/frame.rs
new file mode 100644
index 00000000000..2589953ecba
--- /dev/null
+++ b/components/shared/webxr/frame.rs
@@ -0,0 +1,60 @@
+/* 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 crate::Floor;
+use crate::HitTestId;
+use crate::HitTestResult;
+use crate::InputFrame;
+use crate::Native;
+use crate::SubImages;
+use crate::Viewer;
+use crate::Viewports;
+use crate::Views;
+
+use euclid::RigidTransform3D;
+
+/// The per-frame data that is provided by the device.
+/// https://www.w3.org/TR/webxr/#xrframe
+// TODO: other fields?
+#[derive(Clone, Debug)]
+#[cfg_attr(feature = "ipc", derive(serde::Serialize, serde::Deserialize))]
+pub struct Frame {
+ /// The pose information of the viewer
+ pub pose: Option<ViewerPose>,
+ /// Frame information for each connected input source
+ pub inputs: Vec<InputFrame>,
+
+ /// Events that occur with the frame.
+ pub events: Vec<FrameUpdateEvent>,
+
+ /// The subimages to render to
+ pub sub_images: Vec<SubImages>,
+
+ /// The hit test results for this frame, if any
+ pub hit_test_results: Vec<HitTestResult>,
+
+ /// The average point in time this XRFrame is expected to be displayed on the devices' display
+ pub predicted_display_time: f64,
+}
+
+#[derive(Clone, Debug)]
+#[cfg_attr(feature = "ipc", derive(serde::Serialize, serde::Deserialize))]
+pub enum FrameUpdateEvent {
+ UpdateFloorTransform(Option<RigidTransform3D<f32, Native, Floor>>),
+ UpdateViewports(Viewports),
+ HitTestSourceAdded(HitTestId),
+}
+
+#[derive(Clone, Debug)]
+#[cfg_attr(feature = "ipc", derive(serde::Serialize, serde::Deserialize))]
+pub struct ViewerPose {
+ /// The transform from the viewer to native coordinates
+ ///
+ /// This is equivalent to the pose of the viewer in native coordinates.
+ /// This is the inverse of the view matrix.
+ pub transform: RigidTransform3D<f32, Viewer, Native>,
+
+ // The various views
+ pub views: Views,
+}
diff --git a/components/shared/webxr/hand.rs b/components/shared/webxr/hand.rs
new file mode 100644
index 00000000000..fa6e8fafe80
--- /dev/null
+++ b/components/shared/webxr/hand.rs
@@ -0,0 +1,122 @@
+use crate::Native;
+use euclid::RigidTransform3D;
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
+#[cfg_attr(feature = "ipc", derive(serde::Serialize, serde::Deserialize))]
+pub struct HandSpace;
+
+#[derive(Clone, Debug, Default)]
+#[cfg_attr(feature = "ipc", derive(serde::Serialize, serde::Deserialize))]
+pub struct Hand<J> {
+ pub wrist: Option<J>,
+ pub thumb_metacarpal: Option<J>,
+ pub thumb_phalanx_proximal: Option<J>,
+ pub thumb_phalanx_distal: Option<J>,
+ pub thumb_phalanx_tip: Option<J>,
+ pub index: Finger<J>,
+ pub middle: Finger<J>,
+ pub ring: Finger<J>,
+ pub little: Finger<J>,
+}
+
+#[derive(Clone, Debug, Default)]
+#[cfg_attr(feature = "ipc", derive(serde::Serialize, serde::Deserialize))]
+pub struct Finger<J> {
+ pub metacarpal: Option<J>,
+ pub phalanx_proximal: Option<J>,
+ pub phalanx_intermediate: Option<J>,
+ pub phalanx_distal: Option<J>,
+ pub phalanx_tip: Option<J>,
+}
+
+#[derive(Copy, Clone, Debug)]
+#[cfg_attr(feature = "ipc", derive(serde::Serialize, serde::Deserialize))]
+pub struct JointFrame {
+ pub pose: RigidTransform3D<f32, HandSpace, Native>,
+ pub radius: f32,
+}
+
+impl Default for JointFrame {
+ fn default() -> Self {
+ Self {
+ pose: RigidTransform3D::identity(),
+ radius: 0.,
+ }
+ }
+}
+
+impl<J> Hand<J> {
+ pub fn map<R>(&self, map: impl (Fn(&Option<J>, Joint) -> Option<R>) + Copy) -> Hand<R> {
+ Hand {
+ wrist: map(&self.wrist, Joint::Wrist),
+ thumb_metacarpal: map(&self.thumb_metacarpal, Joint::ThumbMetacarpal),
+ thumb_phalanx_proximal: map(&self.thumb_phalanx_proximal, Joint::ThumbPhalanxProximal),
+ thumb_phalanx_distal: map(&self.thumb_phalanx_distal, Joint::ThumbPhalanxDistal),
+ thumb_phalanx_tip: map(&self.thumb_phalanx_tip, Joint::ThumbPhalanxTip),
+ index: self.index.map(|f, j| map(f, Joint::Index(j))),
+ middle: self.middle.map(|f, j| map(f, Joint::Middle(j))),
+ ring: self.ring.map(|f, j| map(f, Joint::Ring(j))),
+ little: self.little.map(|f, j| map(f, Joint::Little(j))),
+ }
+ }
+
+ pub fn get(&self, joint: Joint) -> Option<&J> {
+ match joint {
+ Joint::Wrist => self.wrist.as_ref(),
+ Joint::ThumbMetacarpal => self.thumb_metacarpal.as_ref(),
+ Joint::ThumbPhalanxProximal => self.thumb_phalanx_proximal.as_ref(),
+ Joint::ThumbPhalanxDistal => self.thumb_phalanx_distal.as_ref(),
+ Joint::ThumbPhalanxTip => self.thumb_phalanx_tip.as_ref(),
+ Joint::Index(f) => self.index.get(f),
+ Joint::Middle(f) => self.middle.get(f),
+ Joint::Ring(f) => self.ring.get(f),
+ Joint::Little(f) => self.little.get(f),
+ }
+ }
+}
+
+impl<J> Finger<J> {
+ pub fn map<R>(&self, map: impl (Fn(&Option<J>, FingerJoint) -> Option<R>) + Copy) -> Finger<R> {
+ Finger {
+ metacarpal: map(&self.metacarpal, FingerJoint::Metacarpal),
+ phalanx_proximal: map(&self.phalanx_proximal, FingerJoint::PhalanxProximal),
+ phalanx_intermediate: map(&self.phalanx_intermediate, FingerJoint::PhalanxIntermediate),
+ phalanx_distal: map(&self.phalanx_distal, FingerJoint::PhalanxDistal),
+ phalanx_tip: map(&self.phalanx_tip, FingerJoint::PhalanxTip),
+ }
+ }
+
+ pub fn get(&self, joint: FingerJoint) -> Option<&J> {
+ match joint {
+ FingerJoint::Metacarpal => self.metacarpal.as_ref(),
+ FingerJoint::PhalanxProximal => self.phalanx_proximal.as_ref(),
+ FingerJoint::PhalanxIntermediate => self.phalanx_intermediate.as_ref(),
+ FingerJoint::PhalanxDistal => self.phalanx_distal.as_ref(),
+ FingerJoint::PhalanxTip => self.phalanx_tip.as_ref(),
+ }
+ }
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+#[cfg_attr(feature = "ipc", derive(serde::Serialize, serde::Deserialize))]
+pub enum FingerJoint {
+ Metacarpal,
+ PhalanxProximal,
+ PhalanxIntermediate,
+ PhalanxDistal,
+ PhalanxTip,
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+#[cfg_attr(feature = "ipc", derive(serde::Serialize, serde::Deserialize))]
+pub enum Joint {
+ Wrist,
+ ThumbMetacarpal,
+ ThumbPhalanxProximal,
+ ThumbPhalanxDistal,
+ ThumbPhalanxTip,
+ Index(FingerJoint),
+ Middle(FingerJoint),
+ Ring(FingerJoint),
+ Little(FingerJoint),
+}
diff --git a/components/shared/webxr/hittest.rs b/components/shared/webxr/hittest.rs
new file mode 100644
index 00000000000..3e56ff8c357
--- /dev/null
+++ b/components/shared/webxr/hittest.rs
@@ -0,0 +1,179 @@
+use crate::ApiSpace;
+use crate::Native;
+use crate::Space;
+use euclid::Point3D;
+use euclid::RigidTransform3D;
+use euclid::Rotation3D;
+use euclid::Vector3D;
+use std::f32::EPSILON;
+use std::iter::FromIterator;
+
+#[derive(Clone, Copy, Debug)]
+#[cfg_attr(feature = "ipc", derive(serde::Serialize, serde::Deserialize))]
+/// https://immersive-web.github.io/hit-test/#xrray
+pub struct Ray<Space> {
+ /// The origin of the ray
+ pub origin: Vector3D<f32, Space>,
+ /// The direction of the ray. Must be normalized.
+ pub direction: Vector3D<f32, Space>,
+}
+
+#[derive(Clone, Copy, Debug)]
+#[cfg_attr(feature = "ipc", derive(serde::Serialize, serde::Deserialize))]
+/// https://immersive-web.github.io/hit-test/#enumdef-xrhittesttrackabletype
+pub enum EntityType {
+ Point,
+ Plane,
+ Mesh,
+}
+
+#[derive(Copy, Clone, Debug)]
+#[cfg_attr(feature = "ipc", derive(serde::Serialize, serde::Deserialize))]
+/// https://immersive-web.github.io/hit-test/#dictdef-xrhittestoptionsinit
+pub struct HitTestSource {
+ pub id: HitTestId,
+ pub space: Space,
+ pub ray: Ray<ApiSpace>,
+ pub types: EntityTypes,
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
+#[cfg_attr(feature = "ipc", derive(serde::Serialize, serde::Deserialize))]
+pub struct HitTestId(pub u32);
+
+#[derive(Copy, Clone, Debug, Default)]
+#[cfg_attr(feature = "ipc", derive(serde::Serialize, serde::Deserialize))]
+/// Vec<EntityType>, but better
+pub struct EntityTypes {
+ pub point: bool,
+ pub plane: bool,
+ pub mesh: bool,
+}
+
+#[derive(Copy, Clone, Debug)]
+#[cfg_attr(feature = "ipc", derive(serde::Serialize, serde::Deserialize))]
+pub struct HitTestResult {
+ pub id: HitTestId,
+ pub space: RigidTransform3D<f32, HitTestSpace, Native>,
+}
+
+#[derive(Clone, Copy, Debug)]
+#[cfg_attr(feature = "ipc", derive(serde::Serialize, serde::Deserialize))]
+/// The coordinate space of a hit test result
+pub struct HitTestSpace;
+
+#[derive(Copy, Clone, Debug)]
+#[cfg_attr(feature = "ipc", derive(serde::Serialize, serde::Deserialize))]
+pub struct Triangle {
+ pub first: Point3D<f32, Native>,
+ pub second: Point3D<f32, Native>,
+ pub third: Point3D<f32, Native>,
+}
+
+impl EntityTypes {
+ pub fn is_type(self, ty: EntityType) -> bool {
+ match ty {
+ EntityType::Point => self.point,
+ EntityType::Plane => self.plane,
+ EntityType::Mesh => self.mesh,
+ }
+ }
+
+ pub fn add_type(&mut self, ty: EntityType) {
+ match ty {
+ EntityType::Point => self.point = true,
+ EntityType::Plane => self.plane = true,
+ EntityType::Mesh => self.mesh = true,
+ }
+ }
+}
+
+impl FromIterator<EntityType> for EntityTypes {
+ fn from_iter<T>(iter: T) -> Self
+ where
+ T: IntoIterator<Item = EntityType>,
+ {
+ iter.into_iter().fold(Default::default(), |mut acc, e| {
+ acc.add_type(e);
+ acc
+ })
+ }
+}
+
+impl Triangle {
+ /// https://en.wikipedia.org/wiki/M%C3%B6ller%E2%80%93Trumbore_intersection_algorithm
+ pub fn intersect(
+ self,
+ ray: Ray<Native>,
+ ) -> Option<RigidTransform3D<f32, HitTestSpace, Native>> {
+ let Triangle {
+ first: v0,
+ second: v1,
+ third: v2,
+ } = self;
+
+ let edge1 = v1 - v0;
+ let edge2 = v2 - v0;
+
+ let h = ray.direction.cross(edge2);
+ let a = edge1.dot(h);
+ if a > -EPSILON && a < EPSILON {
+ // ray is parallel to triangle
+ return None;
+ }
+
+ let f = 1. / a;
+
+ let s = ray.origin - v0.to_vector();
+
+ // barycentric coordinate of intersection point u
+ let u = f * s.dot(h);
+ // barycentric coordinates have range (0, 1)
+ if u < 0. || u > 1. {
+ // the intersection is outside the triangle
+ return None;
+ }
+
+ let q = s.cross(edge1);
+ // barycentric coordinate of intersection point v
+ let v = f * ray.direction.dot(q);
+
+ // barycentric coordinates have range (0, 1)
+ // and their sum must not be greater than 1
+ if v < 0. || u + v > 1. {
+ // the intersection is outside the triangle
+ return None;
+ }
+
+ let t = f * edge2.dot(q);
+
+ if t > EPSILON {
+ let origin = ray.origin + ray.direction * t;
+
+ // this is not part of the Möller-Trumbore algorithm, the hit test spec
+ // requires it has an orientation such that the Y axis points along
+ // the triangle normal
+ let normal = edge1.cross(edge2).normalize();
+ let y = Vector3D::new(0., 1., 0.);
+ let dot = normal.dot(y);
+ let rotation = if dot > -EPSILON && dot < EPSILON {
+ // vectors are parallel, return the vector itself
+ // XXXManishearth it's possible for the vectors to be
+ // antiparallel, unclear if normals need to be flipped
+ Rotation3D::identity()
+ } else {
+ let axis = normal.cross(y);
+ let cos = normal.dot(y);
+ // This is Rotation3D::around_axis(axis.normalize(), theta), however
+ // that is just Rotation3D::quaternion(axis.normalize().xyz * sin, cos),
+ // which is Rotation3D::quaternion(cross, dot)
+ Rotation3D::quaternion(axis.x, axis.y, axis.z, cos)
+ };
+
+ return Some(RigidTransform3D::new(rotation, origin));
+ }
+
+ // triangle is behind ray
+ None
+ }
+}
diff --git a/components/shared/webxr/input.rs b/components/shared/webxr/input.rs
new file mode 100644
index 00000000000..9fcd2a18554
--- /dev/null
+++ b/components/shared/webxr/input.rs
@@ -0,0 +1,74 @@
+/* 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 crate::Hand;
+use crate::Input;
+use crate::JointFrame;
+use crate::Native;
+
+use euclid::RigidTransform3D;
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
+#[cfg_attr(feature = "ipc", derive(serde::Serialize, serde::Deserialize))]
+pub struct InputId(pub u32);
+
+#[derive(Copy, Clone, Debug)]
+#[cfg_attr(feature = "ipc", derive(serde::Serialize, serde::Deserialize))]
+pub enum Handedness {
+ None,
+ Left,
+ Right,
+}
+
+#[derive(Copy, Clone, Debug)]
+#[cfg_attr(feature = "ipc", derive(serde::Serialize, serde::Deserialize))]
+pub enum TargetRayMode {
+ Gaze,
+ TrackedPointer,
+ Screen,
+ TransientPointer,
+}
+
+#[derive(Clone, Debug)]
+#[cfg_attr(feature = "ipc", derive(serde::Serialize, serde::Deserialize))]
+pub struct InputSource {
+ pub handedness: Handedness,
+ pub target_ray_mode: TargetRayMode,
+ pub id: InputId,
+ pub supports_grip: bool,
+ pub hand_support: Option<Hand<()>>,
+ pub profiles: Vec<String>,
+}
+
+#[derive(Clone, Debug)]
+#[cfg_attr(feature = "ipc", derive(serde::Serialize, serde::Deserialize))]
+pub struct InputFrame {
+ pub id: InputId,
+ pub target_ray_origin: Option<RigidTransform3D<f32, Input, Native>>,
+ pub grip_origin: Option<RigidTransform3D<f32, Input, Native>>,
+ pub pressed: bool,
+ pub hand: Option<Box<Hand<JointFrame>>>,
+ pub squeezed: bool,
+ pub button_values: Vec<f32>,
+ pub axis_values: Vec<f32>,
+ pub input_changed: bool,
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+#[cfg_attr(feature = "ipc", derive(serde::Serialize, serde::Deserialize))]
+pub enum SelectEvent {
+ /// Selection started
+ Start,
+ /// Selection ended *without* it being a contiguous select event
+ End,
+ /// Selection ended *with* it being a contiguous select event
+ Select,
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+#[cfg_attr(feature = "ipc", derive(serde::Serialize, serde::Deserialize))]
+pub enum SelectKind {
+ Select,
+ Squeeze,
+}
diff --git a/components/shared/webxr/layer.rs b/components/shared/webxr/layer.rs
new file mode 100644
index 00000000000..b0a607f290f
--- /dev/null
+++ b/components/shared/webxr/layer.rs
@@ -0,0 +1,296 @@
+/* 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 crate::Error;
+use crate::Viewport;
+use crate::Viewports;
+
+use euclid::Rect;
+use euclid::Size2D;
+
+use std::fmt::Debug;
+use std::sync::atomic::AtomicUsize;
+use std::sync::atomic::Ordering;
+
+#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
+#[cfg_attr(feature = "ipc", derive(Deserialize, Serialize))]
+pub struct ContextId(pub u64);
+
+#[cfg(feature = "ipc")]
+use serde::{Deserialize, Serialize};
+
+pub trait GLTypes {
+ type Device;
+ type Context;
+ type Bindings;
+}
+
+pub trait GLContexts<GL: GLTypes> {
+ fn bindings(&mut self, device: &GL::Device, context_id: ContextId) -> Option<&GL::Bindings>;
+ fn context(&mut self, device: &GL::Device, context_id: ContextId) -> Option<&mut GL::Context>;
+}
+
+impl GLTypes for () {
+ type Bindings = ();
+ type Device = ();
+ type Context = ();
+}
+
+impl GLContexts<()> for () {
+ fn context(&mut self, _: &(), _: ContextId) -> Option<&mut ()> {
+ Some(self)
+ }
+
+ fn bindings(&mut self, _: &(), _: ContextId) -> Option<&()> {
+ Some(self)
+ }
+}
+
+pub trait LayerGrandManagerAPI<GL: GLTypes> {
+ fn create_layer_manager(&self, factory: LayerManagerFactory<GL>)
+ -> Result<LayerManager, Error>;
+
+ fn clone_layer_grand_manager(&self) -> LayerGrandManager<GL>;
+}
+
+pub struct LayerGrandManager<GL>(Box<dyn Send + LayerGrandManagerAPI<GL>>);
+
+impl<GL: GLTypes> Clone for LayerGrandManager<GL> {
+ fn clone(&self) -> Self {
+ self.0.clone_layer_grand_manager()
+ }
+}
+
+impl<GL> Debug for LayerGrandManager<GL> {
+ fn fmt(&self, fmt: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
+ "LayerGrandManager(...)".fmt(fmt)
+ }
+}
+
+impl<GL: GLTypes> LayerGrandManager<GL> {
+ pub fn new<GM>(grand_manager: GM) -> LayerGrandManager<GL>
+ where
+ GM: 'static + Send + LayerGrandManagerAPI<GL>,
+ {
+ LayerGrandManager(Box::new(grand_manager))
+ }
+
+ pub fn create_layer_manager<F, M>(&self, factory: F) -> Result<LayerManager, Error>
+ where
+ F: 'static + Send + FnOnce(&mut GL::Device, &mut dyn GLContexts<GL>) -> Result<M, Error>,
+ M: 'static + LayerManagerAPI<GL>,
+ {
+ self.0
+ .create_layer_manager(LayerManagerFactory::new(factory))
+ }
+}
+
+pub trait LayerManagerAPI<GL: GLTypes> {
+ fn create_layer(
+ &mut self,
+ device: &mut GL::Device,
+ contexts: &mut dyn GLContexts<GL>,
+ context_id: ContextId,
+ init: LayerInit,
+ ) -> Result<LayerId, Error>;
+
+ fn destroy_layer(
+ &mut self,
+ device: &mut GL::Device,
+ contexts: &mut dyn GLContexts<GL>,
+ context_id: ContextId,
+ layer_id: LayerId,
+ );
+
+ fn layers(&self) -> &[(ContextId, LayerId)];
+
+ fn begin_frame(
+ &mut self,
+ device: &mut GL::Device,
+ contexts: &mut dyn GLContexts<GL>,
+ layers: &[(ContextId, LayerId)],
+ ) -> Result<Vec<SubImages>, Error>;
+
+ fn end_frame(
+ &mut self,
+ device: &mut GL::Device,
+ contexts: &mut dyn GLContexts<GL>,
+ layers: &[(ContextId, LayerId)],
+ ) -> Result<(), Error>;
+}
+
+pub struct LayerManager(Box<dyn Send + LayerManagerAPI<()>>);
+
+impl Debug for LayerManager {
+ fn fmt(&self, fmt: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
+ "LayerManager(...)".fmt(fmt)
+ }
+}
+
+impl LayerManager {
+ pub fn create_layer(
+ &mut self,
+ context_id: ContextId,
+ init: LayerInit,
+ ) -> Result<LayerId, Error> {
+ self.0.create_layer(&mut (), &mut (), context_id, init)
+ }
+
+ pub fn destroy_layer(&mut self, context_id: ContextId, layer_id: LayerId) {
+ self.0.destroy_layer(&mut (), &mut (), context_id, layer_id);
+ }
+
+ pub fn begin_frame(
+ &mut self,
+ layers: &[(ContextId, LayerId)],
+ ) -> Result<Vec<SubImages>, Error> {
+ self.0.begin_frame(&mut (), &mut (), layers)
+ }
+
+ pub fn end_frame(&mut self, layers: &[(ContextId, LayerId)]) -> Result<(), Error> {
+ self.0.end_frame(&mut (), &mut (), layers)
+ }
+}
+
+impl LayerManager {
+ pub fn new<M>(manager: M) -> LayerManager
+ where
+ M: 'static + Send + LayerManagerAPI<()>,
+ {
+ LayerManager(Box::new(manager))
+ }
+}
+
+impl Drop for LayerManager {
+ fn drop(&mut self) {
+ log::debug!("Dropping LayerManager");
+ for (context_id, layer_id) in self.0.layers().to_vec() {
+ self.destroy_layer(context_id, layer_id);
+ }
+ }
+}
+
+pub struct LayerManagerFactory<GL: GLTypes>(
+ Box<
+ dyn Send
+ + FnOnce(
+ &mut GL::Device,
+ &mut dyn GLContexts<GL>,
+ ) -> Result<Box<dyn LayerManagerAPI<GL>>, Error>,
+ >,
+);
+
+impl<GL: GLTypes> Debug for LayerManagerFactory<GL> {
+ fn fmt(&self, fmt: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
+ "LayerManagerFactory(...)".fmt(fmt)
+ }
+}
+
+impl<GL: GLTypes> LayerManagerFactory<GL> {
+ pub fn new<F, M>(factory: F) -> LayerManagerFactory<GL>
+ where
+ F: 'static + Send + FnOnce(&mut GL::Device, &mut dyn GLContexts<GL>) -> Result<M, Error>,
+ M: 'static + LayerManagerAPI<GL>,
+ {
+ LayerManagerFactory(Box::new(move |device, contexts| {
+ Ok(Box::new(factory(device, contexts)?))
+ }))
+ }
+
+ pub fn build(
+ self,
+ device: &mut GL::Device,
+ contexts: &mut dyn GLContexts<GL>,
+ ) -> Result<Box<dyn LayerManagerAPI<GL>>, Error> {
+ (self.0)(device, contexts)
+ }
+}
+
+#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
+#[cfg_attr(feature = "ipc", derive(Deserialize, Serialize))]
+pub struct LayerId(usize);
+
+static NEXT_LAYER_ID: AtomicUsize = AtomicUsize::new(0);
+
+impl LayerId {
+ pub fn new() -> LayerId {
+ LayerId(NEXT_LAYER_ID.fetch_add(1, Ordering::SeqCst))
+ }
+}
+
+#[derive(Copy, Clone, Debug)]
+#[cfg_attr(feature = "ipc", derive(Deserialize, Serialize))]
+pub enum LayerInit {
+ // https://www.w3.org/TR/webxr/#dictdef-xrwebgllayerinit
+ WebGLLayer {
+ antialias: bool,
+ depth: bool,
+ stencil: bool,
+ alpha: bool,
+ ignore_depth_values: bool,
+ framebuffer_scale_factor: f32,
+ },
+ // https://immersive-web.github.io/layers/#xrprojectionlayerinittype
+ ProjectionLayer {
+ depth: bool,
+ stencil: bool,
+ alpha: bool,
+ scale_factor: f32,
+ },
+ // TODO: other layer types
+}
+
+impl LayerInit {
+ pub fn texture_size(&self, viewports: &Viewports) -> Size2D<i32, Viewport> {
+ match self {
+ LayerInit::WebGLLayer {
+ framebuffer_scale_factor: scale,
+ ..
+ }
+ | LayerInit::ProjectionLayer {
+ scale_factor: scale,
+ ..
+ } => {
+ let native_size = viewports
+ .viewports
+ .iter()
+ .fold(Rect::zero(), |acc, view| acc.union(view))
+ .size;
+ (native_size.to_f32() * *scale).to_i32()
+ }
+ }
+ }
+}
+
+/// https://immersive-web.github.io/layers/#enumdef-xrlayerlayout
+#[derive(Copy, Clone, Debug)]
+#[cfg_attr(feature = "ipc", derive(Deserialize, Serialize))]
+pub enum LayerLayout {
+ // TODO: Default
+ // Allocates one texture
+ Mono,
+ // Allocates one texture, which is split in half vertically, giving two subimages
+ StereoLeftRight,
+ // Allocates one texture, which is split in half horizonally, giving two subimages
+ StereoTopBottom,
+}
+
+#[derive(Clone, Debug)]
+#[cfg_attr(feature = "ipc", derive(Deserialize, Serialize))]
+pub struct SubImages {
+ pub layer_id: LayerId,
+ pub sub_image: Option<SubImage>,
+ pub view_sub_images: Vec<SubImage>,
+}
+
+/// https://immersive-web.github.io/layers/#xrsubimagetype
+#[derive(Clone, Debug)]
+#[cfg_attr(feature = "ipc", derive(Deserialize, Serialize))]
+pub struct SubImage {
+ pub color_texture: u32,
+ // TODO: make this Option<NonZeroU32>
+ pub depth_stencil_texture: Option<u32>,
+ pub texture_array_index: Option<u32>,
+ pub viewport: Rect<i32, Viewport>,
+}
diff --git a/components/shared/webxr/lib.rs b/components/shared/webxr/lib.rs
new file mode 100644
index 00000000000..9acad34e0e5
--- /dev/null
+++ b/components/shared/webxr/lib.rs
@@ -0,0 +1,175 @@
+/* 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/. */
+
+//! This crate defines the Rust API for WebXR. It is implemented by the `webxr` crate.
+
+mod device;
+mod error;
+mod events;
+mod frame;
+mod hand;
+mod hittest;
+mod input;
+mod layer;
+mod mock;
+mod registry;
+mod session;
+mod space;
+pub mod util;
+mod view;
+
+pub use device::DeviceAPI;
+pub use device::DiscoveryAPI;
+
+pub use error::Error;
+
+pub use events::Event;
+pub use events::EventBuffer;
+pub use events::Visibility;
+
+pub use frame::Frame;
+pub use frame::FrameUpdateEvent;
+pub use frame::ViewerPose;
+
+pub use hand::Finger;
+pub use hand::FingerJoint;
+pub use hand::Hand;
+pub use hand::HandSpace;
+pub use hand::Joint;
+pub use hand::JointFrame;
+
+pub use hittest::EntityType;
+pub use hittest::EntityTypes;
+pub use hittest::HitTestId;
+pub use hittest::HitTestResult;
+pub use hittest::HitTestSource;
+pub use hittest::HitTestSpace;
+pub use hittest::Ray;
+pub use hittest::Triangle;
+
+pub use input::Handedness;
+pub use input::InputFrame;
+pub use input::InputId;
+pub use input::InputSource;
+pub use input::SelectEvent;
+pub use input::SelectKind;
+pub use input::TargetRayMode;
+
+pub use layer::ContextId;
+pub use layer::GLContexts;
+pub use layer::GLTypes;
+pub use layer::LayerGrandManager;
+pub use layer::LayerGrandManagerAPI;
+pub use layer::LayerId;
+pub use layer::LayerInit;
+pub use layer::LayerLayout;
+pub use layer::LayerManager;
+pub use layer::LayerManagerAPI;
+pub use layer::LayerManagerFactory;
+pub use layer::SubImage;
+pub use layer::SubImages;
+
+pub use mock::MockButton;
+pub use mock::MockButtonType;
+pub use mock::MockDeviceInit;
+pub use mock::MockDeviceMsg;
+pub use mock::MockDiscoveryAPI;
+pub use mock::MockInputInit;
+pub use mock::MockInputMsg;
+pub use mock::MockRegion;
+pub use mock::MockViewInit;
+pub use mock::MockViewsInit;
+pub use mock::MockWorld;
+
+pub use registry::MainThreadRegistry;
+pub use registry::MainThreadWaker;
+pub use registry::Registry;
+
+pub use session::EnvironmentBlendMode;
+pub use session::MainThreadSession;
+pub use session::Quitter;
+pub use session::Session;
+pub use session::SessionBuilder;
+pub use session::SessionId;
+pub use session::SessionInit;
+pub use session::SessionMode;
+pub use session::SessionThread;
+
+pub use space::ApiSpace;
+pub use space::BaseSpace;
+pub use space::Space;
+
+pub use view::Capture;
+pub use view::CubeBack;
+pub use view::CubeBottom;
+pub use view::CubeLeft;
+pub use view::CubeRight;
+pub use view::CubeTop;
+pub use view::Display;
+pub use view::Floor;
+pub use view::Input;
+pub use view::LeftEye;
+pub use view::Native;
+pub use view::RightEye;
+pub use view::SomeEye;
+pub use view::View;
+pub use view::Viewer;
+pub use view::Viewport;
+pub use view::Viewports;
+pub use view::Views;
+pub use view::CUBE_BACK;
+pub use view::CUBE_BOTTOM;
+pub use view::CUBE_LEFT;
+pub use view::CUBE_RIGHT;
+pub use view::CUBE_TOP;
+pub use view::LEFT_EYE;
+pub use view::RIGHT_EYE;
+pub use view::VIEWER;
+
+#[cfg(feature = "ipc")]
+use std::thread;
+
+use std::time::Duration;
+
+#[cfg(feature = "ipc")]
+pub use ipc_channel::ipc::IpcSender as Sender;
+
+#[cfg(feature = "ipc")]
+pub use ipc_channel::ipc::IpcReceiver as Receiver;
+
+#[cfg(feature = "ipc")]
+pub use ipc_channel::ipc::channel;
+
+#[cfg(not(feature = "ipc"))]
+pub use std::sync::mpsc::{Receiver, RecvTimeoutError, Sender};
+
+#[cfg(not(feature = "ipc"))]
+pub fn channel<T>() -> Result<(Sender<T>, Receiver<T>), ()> {
+ Ok(std::sync::mpsc::channel())
+}
+
+#[cfg(not(feature = "ipc"))]
+pub fn recv_timeout<T>(receiver: &Receiver<T>, timeout: Duration) -> Result<T, RecvTimeoutError> {
+ receiver.recv_timeout(timeout)
+}
+
+#[cfg(feature = "ipc")]
+pub fn recv_timeout<T>(
+ receiver: &Receiver<T>,
+ timeout: Duration,
+) -> Result<T, ipc_channel::ipc::TryRecvError>
+where
+ T: serde::Serialize + for<'a> serde::Deserialize<'a>,
+{
+ // Sigh, polling, sigh.
+ let mut delay = timeout / 1000;
+ while delay < timeout {
+ if let Ok(msg) = receiver.try_recv() {
+ return Ok(msg);
+ }
+ thread::sleep(delay);
+ delay = delay * 2;
+ }
+ receiver.try_recv()
+}
diff --git a/components/shared/webxr/mock.rs b/components/shared/webxr/mock.rs
new file mode 100644
index 00000000000..91c15bae44b
--- /dev/null
+++ b/components/shared/webxr/mock.rs
@@ -0,0 +1,146 @@
+/* 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 crate::DiscoveryAPI;
+use crate::Display;
+use crate::EntityType;
+use crate::Error;
+use crate::Floor;
+use crate::Handedness;
+use crate::Input;
+use crate::InputId;
+use crate::InputSource;
+use crate::LeftEye;
+use crate::Native;
+use crate::Receiver;
+use crate::RightEye;
+use crate::SelectEvent;
+use crate::SelectKind;
+use crate::Sender;
+use crate::TargetRayMode;
+use crate::Triangle;
+use crate::Viewer;
+use crate::Viewport;
+use crate::Visibility;
+
+use euclid::{Point2D, Rect, RigidTransform3D, Transform3D};
+
+#[cfg(feature = "ipc")]
+use serde::{Deserialize, Serialize};
+
+/// A trait for discovering mock XR devices
+pub trait MockDiscoveryAPI<GL>: 'static {
+ fn simulate_device_connection(
+ &mut self,
+ init: MockDeviceInit,
+ receiver: Receiver<MockDeviceMsg>,
+ ) -> Result<Box<dyn DiscoveryAPI<GL>>, Error>;
+}
+
+#[derive(Clone, Debug)]
+#[cfg_attr(feature = "ipc", derive(Serialize, Deserialize))]
+pub struct MockDeviceInit {
+ pub floor_origin: Option<RigidTransform3D<f32, Floor, Native>>,
+ pub supports_inline: bool,
+ pub supports_vr: bool,
+ pub supports_ar: bool,
+ pub viewer_origin: Option<RigidTransform3D<f32, Viewer, Native>>,
+ pub views: MockViewsInit,
+ pub supported_features: Vec<String>,
+ pub world: Option<MockWorld>,
+}
+
+#[derive(Clone, Debug)]
+#[cfg_attr(feature = "ipc", derive(Serialize, Deserialize))]
+pub struct MockViewInit<Eye> {
+ pub transform: RigidTransform3D<f32, Viewer, Eye>,
+ pub projection: Transform3D<f32, Eye, Display>,
+ pub viewport: Rect<i32, Viewport>,
+ /// field of view values, in radians
+ pub fov: Option<(f32, f32, f32, f32)>,
+}
+
+#[derive(Clone, Debug)]
+#[cfg_attr(feature = "ipc", derive(Serialize, Deserialize))]
+pub enum MockViewsInit {
+ Mono(MockViewInit<Viewer>),
+ Stereo(MockViewInit<LeftEye>, MockViewInit<RightEye>),
+}
+
+#[derive(Debug)]
+#[cfg_attr(feature = "ipc", derive(Serialize, Deserialize))]
+pub enum MockDeviceMsg {
+ SetViewerOrigin(Option<RigidTransform3D<f32, Viewer, Native>>),
+ SetFloorOrigin(Option<RigidTransform3D<f32, Floor, Native>>),
+ SetViews(MockViewsInit),
+ AddInputSource(MockInputInit),
+ MessageInputSource(InputId, MockInputMsg),
+ VisibilityChange(Visibility),
+ SetWorld(MockWorld),
+ ClearWorld,
+ Disconnect(Sender<()>),
+ SetBoundsGeometry(Vec<Point2D<f32, Floor>>),
+ SimulateResetPose,
+}
+
+#[derive(Clone, Debug)]
+#[cfg_attr(feature = "ipc", derive(Serialize, Deserialize))]
+pub struct MockInputInit {
+ pub source: InputSource,
+ pub pointer_origin: Option<RigidTransform3D<f32, Input, Native>>,
+ pub grip_origin: Option<RigidTransform3D<f32, Input, Native>>,
+ pub supported_buttons: Vec<MockButton>,
+}
+
+#[derive(Debug)]
+#[cfg_attr(feature = "ipc", derive(Serialize, Deserialize))]
+pub enum MockInputMsg {
+ SetHandedness(Handedness),
+ SetTargetRayMode(TargetRayMode),
+ SetProfiles(Vec<String>),
+ SetPointerOrigin(Option<RigidTransform3D<f32, Input, Native>>),
+ SetGripOrigin(Option<RigidTransform3D<f32, Input, Native>>),
+ /// Note: SelectEvent::Select here refers to a complete Select event,
+ /// not just the end event, i.e. it refers to
+ /// https://immersive-web.github.io/webxr-test-api/#dom-fakexrinputcontroller-simulateselect
+ TriggerSelect(SelectKind, SelectEvent),
+ Disconnect,
+ Reconnect,
+ SetSupportedButtons(Vec<MockButton>),
+ UpdateButtonState(MockButton),
+}
+
+#[derive(Clone, Debug)]
+#[cfg_attr(feature = "ipc", derive(Serialize, Deserialize))]
+pub struct MockRegion {
+ pub faces: Vec<Triangle>,
+ pub ty: EntityType,
+}
+
+#[derive(Clone, Debug)]
+#[cfg_attr(feature = "ipc", derive(Serialize, Deserialize))]
+pub struct MockWorld {
+ pub regions: Vec<MockRegion>,
+}
+
+#[derive(Clone, Debug, PartialEq)]
+#[cfg_attr(feature = "ipc", derive(Serialize, Deserialize))]
+pub enum MockButtonType {
+ Grip,
+ Touchpad,
+ Thumbstick,
+ OptionalButton,
+ OptionalThumbstick,
+}
+
+#[derive(Clone, Debug)]
+#[cfg_attr(feature = "ipc", derive(Serialize, Deserialize))]
+pub struct MockButton {
+ pub button_type: MockButtonType,
+ pub pressed: bool,
+ pub touched: bool,
+ pub pressed_value: f32,
+ pub x_value: f32,
+ pub y_value: f32,
+}
diff --git a/components/shared/webxr/registry.rs b/components/shared/webxr/registry.rs
new file mode 100644
index 00000000000..337bb80a8a2
--- /dev/null
+++ b/components/shared/webxr/registry.rs
@@ -0,0 +1,262 @@
+/* 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 crate::DiscoveryAPI;
+use crate::Error;
+use crate::Frame;
+use crate::GLTypes;
+use crate::LayerGrandManager;
+use crate::MainThreadSession;
+use crate::MockDeviceInit;
+use crate::MockDeviceMsg;
+use crate::MockDiscoveryAPI;
+use crate::Receiver;
+use crate::Sender;
+use crate::Session;
+use crate::SessionBuilder;
+use crate::SessionId;
+use crate::SessionInit;
+use crate::SessionMode;
+
+use log::warn;
+
+#[cfg(feature = "ipc")]
+use serde::{Deserialize, Serialize};
+
+#[derive(Clone)]
+#[cfg_attr(feature = "ipc", derive(Serialize, Deserialize))]
+pub struct Registry {
+ sender: Sender<RegistryMsg>,
+ waker: MainThreadWakerImpl,
+}
+
+pub struct MainThreadRegistry<GL> {
+ discoveries: Vec<Box<dyn DiscoveryAPI<GL>>>,
+ sessions: Vec<Box<dyn MainThreadSession>>,
+ mocks: Vec<Box<dyn MockDiscoveryAPI<GL>>>,
+ sender: Sender<RegistryMsg>,
+ receiver: Receiver<RegistryMsg>,
+ waker: MainThreadWakerImpl,
+ grand_manager: LayerGrandManager<GL>,
+ next_session_id: u32,
+}
+
+pub trait MainThreadWaker: 'static + Send {
+ fn clone_box(&self) -> Box<dyn MainThreadWaker>;
+ fn wake(&self);
+}
+
+impl Clone for Box<dyn MainThreadWaker> {
+ fn clone(&self) -> Self {
+ self.clone_box()
+ }
+}
+
+#[derive(Clone)]
+#[cfg_attr(feature = "ipc", derive(Serialize, Deserialize))]
+struct MainThreadWakerImpl {
+ #[cfg(feature = "ipc")]
+ sender: Sender<()>,
+ #[cfg(not(feature = "ipc"))]
+ waker: Box<dyn MainThreadWaker>,
+}
+
+#[cfg(feature = "ipc")]
+impl MainThreadWakerImpl {
+ fn new(waker: Box<dyn MainThreadWaker>) -> Result<MainThreadWakerImpl, Error> {
+ let (sender, receiver) = crate::channel().or(Err(Error::CommunicationError))?;
+ ipc_channel::router::ROUTER.add_typed_route(receiver, Box::new(move |_| waker.wake()));
+ Ok(MainThreadWakerImpl { sender })
+ }
+
+ fn wake(&self) {
+ let _ = self.sender.send(());
+ }
+}
+
+#[cfg(not(feature = "ipc"))]
+impl MainThreadWakerImpl {
+ fn new(waker: Box<dyn MainThreadWaker>) -> Result<MainThreadWakerImpl, Error> {
+ Ok(MainThreadWakerImpl { waker })
+ }
+
+ pub fn wake(&self) {
+ self.waker.wake()
+ }
+}
+
+impl Registry {
+ pub fn supports_session(&mut self, mode: SessionMode, dest: Sender<Result<(), Error>>) {
+ let _ = self.sender.send(RegistryMsg::SupportsSession(mode, dest));
+ self.waker.wake();
+ }
+
+ pub fn request_session(
+ &mut self,
+ mode: SessionMode,
+ init: SessionInit,
+ dest: Sender<Result<Session, Error>>,
+ animation_frame_handler: Sender<Frame>,
+ ) {
+ let _ = self.sender.send(RegistryMsg::RequestSession(
+ mode,
+ init,
+ dest,
+ animation_frame_handler,
+ ));
+ self.waker.wake();
+ }
+
+ pub fn simulate_device_connection(
+ &mut self,
+ init: MockDeviceInit,
+ dest: Sender<Result<Sender<MockDeviceMsg>, Error>>,
+ ) {
+ let _ = self
+ .sender
+ .send(RegistryMsg::SimulateDeviceConnection(init, dest));
+ self.waker.wake();
+ }
+}
+
+impl<GL: 'static + GLTypes> MainThreadRegistry<GL> {
+ pub fn new(
+ waker: Box<dyn MainThreadWaker>,
+ grand_manager: LayerGrandManager<GL>,
+ ) -> Result<Self, Error> {
+ let (sender, receiver) = crate::channel().or(Err(Error::CommunicationError))?;
+ let discoveries = Vec::new();
+ let sessions = Vec::new();
+ let mocks = Vec::new();
+ let waker = MainThreadWakerImpl::new(waker)?;
+ Ok(MainThreadRegistry {
+ discoveries,
+ sessions,
+ mocks,
+ sender,
+ receiver,
+ waker,
+ grand_manager,
+ next_session_id: 0,
+ })
+ }
+
+ pub fn registry(&self) -> Registry {
+ Registry {
+ sender: self.sender.clone(),
+ waker: self.waker.clone(),
+ }
+ }
+
+ pub fn register<D>(&mut self, discovery: D)
+ where
+ D: DiscoveryAPI<GL>,
+ {
+ self.discoveries.push(Box::new(discovery));
+ }
+
+ pub fn register_mock<D>(&mut self, discovery: D)
+ where
+ D: MockDiscoveryAPI<GL>,
+ {
+ self.mocks.push(Box::new(discovery));
+ }
+
+ pub fn run_on_main_thread<S>(&mut self, session: S)
+ where
+ S: MainThreadSession,
+ {
+ self.sessions.push(Box::new(session));
+ }
+
+ pub fn run_one_frame(&mut self) {
+ while let Ok(msg) = self.receiver.try_recv() {
+ self.handle_msg(msg);
+ }
+ for session in &mut self.sessions {
+ session.run_one_frame();
+ }
+ self.sessions.retain(|session| session.running());
+ }
+
+ pub fn running(&self) -> bool {
+ self.sessions.iter().any(|session| session.running())
+ }
+
+ fn handle_msg(&mut self, msg: RegistryMsg) {
+ match msg {
+ RegistryMsg::SupportsSession(mode, dest) => {
+ let _ = dest.send(self.supports_session(mode));
+ }
+ RegistryMsg::RequestSession(mode, init, dest, raf_sender) => {
+ let _ = dest.send(self.request_session(mode, init, raf_sender));
+ }
+ RegistryMsg::SimulateDeviceConnection(init, dest) => {
+ let _ = dest.send(self.simulate_device_connection(init));
+ }
+ }
+ }
+
+ fn supports_session(&mut self, mode: SessionMode) -> Result<(), Error> {
+ for discovery in &self.discoveries {
+ if discovery.supports_session(mode) {
+ return Ok(());
+ }
+ }
+ Err(Error::NoMatchingDevice)
+ }
+
+ fn request_session(
+ &mut self,
+ mode: SessionMode,
+ init: SessionInit,
+ raf_sender: Sender<Frame>,
+ ) -> Result<Session, Error> {
+ for discovery in &mut self.discoveries {
+ if discovery.supports_session(mode) {
+ let raf_sender = raf_sender.clone();
+ let id = SessionId(self.next_session_id);
+ self.next_session_id += 1;
+ let xr = SessionBuilder::new(
+ &mut self.sessions,
+ raf_sender,
+ self.grand_manager.clone(),
+ id,
+ );
+ match discovery.request_session(mode, &init, xr) {
+ Ok(session) => return Ok(session),
+ Err(err) => warn!("XR device error {:?}", err),
+ }
+ }
+ }
+ warn!("no device could support the session");
+ Err(Error::NoMatchingDevice)
+ }
+
+ fn simulate_device_connection(
+ &mut self,
+ init: MockDeviceInit,
+ ) -> Result<Sender<MockDeviceMsg>, Error> {
+ for mock in &mut self.mocks {
+ let (sender, receiver) = crate::channel().or(Err(Error::CommunicationError))?;
+ if let Ok(discovery) = mock.simulate_device_connection(init.clone(), receiver) {
+ self.discoveries.insert(0, discovery);
+ return Ok(sender);
+ }
+ }
+ Err(Error::NoMatchingDevice)
+ }
+}
+
+#[cfg_attr(feature = "ipc", derive(Serialize, Deserialize))]
+enum RegistryMsg {
+ RequestSession(
+ SessionMode,
+ SessionInit,
+ Sender<Result<Session, Error>>,
+ Sender<Frame>,
+ ),
+ SupportsSession(SessionMode, Sender<Result<(), Error>>),
+ SimulateDeviceConnection(MockDeviceInit, Sender<Result<Sender<MockDeviceMsg>, Error>>),
+}
diff --git a/components/shared/webxr/session.rs b/components/shared/webxr/session.rs
new file mode 100644
index 00000000000..be731b8c243
--- /dev/null
+++ b/components/shared/webxr/session.rs
@@ -0,0 +1,531 @@
+/* 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 crate::channel;
+use crate::ContextId;
+use crate::DeviceAPI;
+use crate::Error;
+use crate::Event;
+use crate::Floor;
+use crate::Frame;
+use crate::FrameUpdateEvent;
+use crate::HitTestId;
+use crate::HitTestSource;
+use crate::InputSource;
+use crate::LayerGrandManager;
+use crate::LayerId;
+use crate::LayerInit;
+use crate::Native;
+use crate::Receiver;
+use crate::Sender;
+use crate::Viewport;
+use crate::Viewports;
+
+use euclid::Point2D;
+use euclid::Rect;
+use euclid::RigidTransform3D;
+use euclid::Size2D;
+
+use log::warn;
+
+use std::thread;
+use std::time::Duration;
+
+#[cfg(feature = "ipc")]
+use serde::{Deserialize, Serialize};
+
+// How long to wait for an rAF.
+static TIMEOUT: Duration = Duration::from_millis(5);
+
+/// https://www.w3.org/TR/webxr/#xrsessionmode-enum
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+#[cfg_attr(feature = "ipc", derive(Serialize, Deserialize))]
+pub enum SessionMode {
+ Inline,
+ ImmersiveVR,
+ ImmersiveAR,
+}
+
+/// https://immersive-web.github.io/webxr/#dictdef-xrsessioninit
+#[derive(Clone, Debug, Eq, PartialEq)]
+#[cfg_attr(feature = "ipc", derive(Serialize, Deserialize))]
+pub struct SessionInit {
+ pub required_features: Vec<String>,
+ pub optional_features: Vec<String>,
+ /// Secondary views are enabled with the `secondary-view` feature
+ /// but for performance reasons we also ask users to enable this pref
+ /// for now.
+ pub first_person_observer_view: bool,
+}
+
+impl SessionInit {
+ /// Helper function for validating a list of requested features against
+ /// a list of supported features for a given mode
+ pub fn validate(&self, mode: SessionMode, supported: &[String]) -> Result<Vec<String>, Error> {
+ for f in &self.required_features {
+ // viewer and local in immersive are granted by default
+ // https://immersive-web.github.io/webxr/#default-features
+ if f == "viewer" || (f == "local" && mode != SessionMode::Inline) {
+ continue;
+ }
+
+ if !supported.contains(f) {
+ return Err(Error::UnsupportedFeature(f.into()));
+ }
+ }
+ let mut granted = self.required_features.clone();
+ for f in &self.optional_features {
+ if f == "viewer"
+ || (f == "local" && mode != SessionMode::Inline)
+ || supported.contains(f)
+ {
+ granted.push(f.clone());
+ }
+ }
+
+ Ok(granted)
+ }
+
+ pub fn feature_requested(&self, f: &str) -> bool {
+ self.required_features
+ .iter()
+ .chain(self.optional_features.iter())
+ .find(|x| *x == f)
+ .is_some()
+ }
+}
+
+/// https://immersive-web.github.io/webxr-ar-module/#xrenvironmentblendmode-enum
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+#[cfg_attr(feature = "ipc", derive(Serialize, Deserialize))]
+pub enum EnvironmentBlendMode {
+ Opaque,
+ AlphaBlend,
+ Additive,
+}
+
+// The messages that are sent from the content thread to the session thread.
+#[derive(Debug)]
+#[cfg_attr(feature = "ipc", derive(Serialize, Deserialize))]
+enum SessionMsg {
+ CreateLayer(ContextId, LayerInit, Sender<Result<LayerId, Error>>),
+ DestroyLayer(ContextId, LayerId),
+ SetLayers(Vec<(ContextId, LayerId)>),
+ SetEventDest(Sender<Event>),
+ UpdateClipPlanes(/* near */ f32, /* far */ f32),
+ StartRenderLoop,
+ RenderAnimationFrame,
+ RequestHitTest(HitTestSource),
+ CancelHitTest(HitTestId),
+ UpdateFrameRate(f32, Sender<f32>),
+ Quit,
+ GetBoundsGeometry(Sender<Option<Vec<Point2D<f32, Floor>>>>),
+}
+
+#[cfg_attr(feature = "ipc", derive(Serialize, Deserialize))]
+#[derive(Clone)]
+pub struct Quitter {
+ sender: Sender<SessionMsg>,
+}
+
+impl Quitter {
+ pub fn quit(&self) {
+ let _ = self.sender.send(SessionMsg::Quit);
+ }
+}
+
+/// An object that represents an XR session.
+/// This is owned by the content thread.
+/// https://www.w3.org/TR/webxr/#xrsession-interface
+#[cfg_attr(feature = "ipc", derive(Serialize, Deserialize))]
+pub struct Session {
+ floor_transform: Option<RigidTransform3D<f32, Native, Floor>>,
+ viewports: Viewports,
+ sender: Sender<SessionMsg>,
+ environment_blend_mode: EnvironmentBlendMode,
+ initial_inputs: Vec<InputSource>,
+ granted_features: Vec<String>,
+ id: SessionId,
+ supported_frame_rates: Vec<f32>,
+}
+
+#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
+#[cfg_attr(feature = "ipc", derive(Deserialize, Serialize))]
+pub struct SessionId(pub(crate) u32);
+
+impl Session {
+ pub fn id(&self) -> SessionId {
+ self.id
+ }
+
+ pub fn floor_transform(&self) -> Option<RigidTransform3D<f32, Native, Floor>> {
+ self.floor_transform.clone()
+ }
+
+ pub fn reference_space_bounds(&self) -> Option<Vec<Point2D<f32, Floor>>> {
+ let (sender, receiver) = channel().ok()?;
+ let _ = self.sender.send(SessionMsg::GetBoundsGeometry(sender));
+ receiver.recv().ok()?
+ }
+
+ pub fn initial_inputs(&self) -> &[InputSource] {
+ &self.initial_inputs
+ }
+
+ pub fn environment_blend_mode(&self) -> EnvironmentBlendMode {
+ self.environment_blend_mode
+ }
+
+ pub fn viewports(&self) -> &[Rect<i32, Viewport>] {
+ &self.viewports.viewports
+ }
+
+ /// A resolution large enough to contain all the viewports.
+ /// https://immersive-web.github.io/webxr/#recommended-webgl-framebuffer-resolution
+ ///
+ /// Returns None if the session is inline
+ pub fn recommended_framebuffer_resolution(&self) -> Option<Size2D<i32, Viewport>> {
+ self.viewports()
+ .iter()
+ .fold(None::<Rect<_, _>>, |acc, vp| {
+ Some(acc.map(|a| a.union(vp)).unwrap_or(*vp))
+ })
+ .map(|rect| Size2D::new(rect.max_x(), rect.max_y()))
+ }
+
+ pub fn create_layer(&self, context_id: ContextId, init: LayerInit) -> Result<LayerId, Error> {
+ let (sender, receiver) = channel().map_err(|_| Error::CommunicationError)?;
+ let _ = self
+ .sender
+ .send(SessionMsg::CreateLayer(context_id, init, sender));
+ receiver.recv().map_err(|_| Error::CommunicationError)?
+ }
+
+ /// Destroy a layer
+ pub fn destroy_layer(&self, context_id: ContextId, layer_id: LayerId) {
+ let _ = self
+ .sender
+ .send(SessionMsg::DestroyLayer(context_id, layer_id));
+ }
+
+ pub fn set_layers(&self, layers: Vec<(ContextId, LayerId)>) {
+ let _ = self.sender.send(SessionMsg::SetLayers(layers));
+ }
+
+ pub fn start_render_loop(&mut self) {
+ let _ = self.sender.send(SessionMsg::StartRenderLoop);
+ }
+
+ pub fn update_clip_planes(&mut self, near: f32, far: f32) {
+ let _ = self.sender.send(SessionMsg::UpdateClipPlanes(near, far));
+ }
+
+ pub fn set_event_dest(&mut self, dest: Sender<Event>) {
+ let _ = self.sender.send(SessionMsg::SetEventDest(dest));
+ }
+
+ pub fn render_animation_frame(&mut self) {
+ let _ = self.sender.send(SessionMsg::RenderAnimationFrame);
+ }
+
+ pub fn end_session(&mut self) {
+ let _ = self.sender.send(SessionMsg::Quit);
+ }
+
+ pub fn apply_event(&mut self, event: FrameUpdateEvent) {
+ match event {
+ FrameUpdateEvent::UpdateFloorTransform(floor) => self.floor_transform = floor,
+ FrameUpdateEvent::UpdateViewports(vp) => self.viewports = vp,
+ FrameUpdateEvent::HitTestSourceAdded(_) => (),
+ }
+ }
+
+ pub fn granted_features(&self) -> &[String] {
+ &self.granted_features
+ }
+
+ pub fn request_hit_test(&self, source: HitTestSource) {
+ let _ = self.sender.send(SessionMsg::RequestHitTest(source));
+ }
+
+ pub fn cancel_hit_test(&self, id: HitTestId) {
+ let _ = self.sender.send(SessionMsg::CancelHitTest(id));
+ }
+
+ pub fn update_frame_rate(&mut self, rate: f32, sender: Sender<f32>) {
+ let _ = self.sender.send(SessionMsg::UpdateFrameRate(rate, sender));
+ }
+
+ pub fn supported_frame_rates(&self) -> &[f32] {
+ &self.supported_frame_rates
+ }
+}
+
+#[derive(PartialEq)]
+enum RenderState {
+ NotInRenderLoop,
+ InRenderLoop,
+ PendingQuit,
+}
+
+/// For devices that want to do their own thread management, the `SessionThread` type is exposed.
+pub struct SessionThread<Device> {
+ receiver: Receiver<SessionMsg>,
+ sender: Sender<SessionMsg>,
+ layers: Vec<(ContextId, LayerId)>,
+ pending_layers: Option<Vec<(ContextId, LayerId)>>,
+ frame_count: u64,
+ frame_sender: Sender<Frame>,
+ running: bool,
+ device: Device,
+ id: SessionId,
+ render_state: RenderState,
+}
+
+impl<Device> SessionThread<Device>
+where
+ Device: DeviceAPI,
+{
+ pub fn new(
+ mut device: Device,
+ frame_sender: Sender<Frame>,
+ id: SessionId,
+ ) -> Result<Self, Error> {
+ let (sender, receiver) = crate::channel().or(Err(Error::CommunicationError))?;
+ device.set_quitter(Quitter {
+ sender: sender.clone(),
+ });
+ let frame_count = 0;
+ let running = true;
+ let layers = Vec::new();
+ let pending_layers = None;
+ Ok(SessionThread {
+ sender,
+ receiver,
+ device,
+ layers,
+ pending_layers,
+ frame_count,
+ frame_sender,
+ running,
+ id,
+ render_state: RenderState::NotInRenderLoop,
+ })
+ }
+
+ pub fn new_session(&mut self) -> Session {
+ let floor_transform = self.device.floor_transform();
+ let viewports = self.device.viewports();
+ let sender = self.sender.clone();
+ let initial_inputs = self.device.initial_inputs();
+ let environment_blend_mode = self.device.environment_blend_mode();
+ let granted_features = self.device.granted_features().into();
+ let supported_frame_rates = self.device.supported_frame_rates();
+ Session {
+ floor_transform,
+ viewports,
+ sender,
+ initial_inputs,
+ environment_blend_mode,
+ granted_features,
+ id: self.id,
+ supported_frame_rates,
+ }
+ }
+
+ pub fn run(&mut self) {
+ loop {
+ if let Ok(msg) = self.receiver.recv() {
+ if !self.handle_msg(msg) {
+ self.running = false;
+ break;
+ }
+ } else {
+ break;
+ }
+ }
+ }
+
+ fn handle_msg(&mut self, msg: SessionMsg) -> bool {
+ log::debug!("processing {:?}", msg);
+ match msg {
+ SessionMsg::SetEventDest(dest) => {
+ self.device.set_event_dest(dest);
+ }
+ SessionMsg::RequestHitTest(source) => {
+ self.device.request_hit_test(source);
+ }
+ SessionMsg::CancelHitTest(id) => {
+ self.device.cancel_hit_test(id);
+ }
+ SessionMsg::CreateLayer(context_id, layer_init, sender) => {
+ let result = self.device.create_layer(context_id, layer_init);
+ let _ = sender.send(result);
+ }
+ SessionMsg::DestroyLayer(context_id, layer_id) => {
+ self.layers.retain(|&(_, other_id)| layer_id != other_id);
+ self.device.destroy_layer(context_id, layer_id);
+ }
+ SessionMsg::SetLayers(layers) => {
+ self.pending_layers = Some(layers);
+ }
+ SessionMsg::StartRenderLoop => {
+ if let Some(layers) = self.pending_layers.take() {
+ self.layers = layers;
+ }
+ let frame = match self.device.begin_animation_frame(&self.layers[..]) {
+ Some(frame) => frame,
+ None => {
+ warn!("Device stopped providing frames, exiting");
+ return false;
+ }
+ };
+ self.render_state = RenderState::InRenderLoop;
+ let _ = self.frame_sender.send(frame);
+ }
+ SessionMsg::UpdateClipPlanes(near, far) => self.device.update_clip_planes(near, far),
+ SessionMsg::RenderAnimationFrame => {
+ self.frame_count += 1;
+
+ self.device.end_animation_frame(&self.layers[..]);
+
+ if self.render_state == RenderState::PendingQuit {
+ self.quit();
+ return false;
+ }
+
+ if let Some(layers) = self.pending_layers.take() {
+ self.layers = layers;
+ }
+ #[allow(unused_mut)]
+ let mut frame = match self.device.begin_animation_frame(&self.layers[..]) {
+ Some(frame) => frame,
+ None => {
+ warn!("Device stopped providing frames, exiting");
+ return false;
+ }
+ };
+
+ let _ = self.frame_sender.send(frame);
+ }
+ SessionMsg::UpdateFrameRate(rate, sender) => {
+ let new_framerate = self.device.update_frame_rate(rate);
+ let _ = sender.send(new_framerate);
+ }
+ SessionMsg::Quit => {
+ if self.render_state == RenderState::NotInRenderLoop {
+ self.quit();
+ return false;
+ } else {
+ self.render_state = RenderState::PendingQuit;
+ }
+ }
+ SessionMsg::GetBoundsGeometry(sender) => {
+ let bounds = self.device.reference_space_bounds();
+ let _ = sender.send(bounds);
+ }
+ }
+ true
+ }
+
+ fn quit(&mut self) {
+ self.render_state = RenderState::NotInRenderLoop;
+ self.device.quit();
+ }
+}
+
+/// Devices that need to can run sessions on the main thread.
+pub trait MainThreadSession: 'static {
+ fn run_one_frame(&mut self);
+ fn running(&self) -> bool;
+}
+
+impl<Device> MainThreadSession for SessionThread<Device>
+where
+ Device: DeviceAPI,
+{
+ fn run_one_frame(&mut self) {
+ let frame_count = self.frame_count;
+ while frame_count == self.frame_count && self.running {
+ if let Ok(msg) = crate::recv_timeout(&self.receiver, TIMEOUT) {
+ self.running = self.handle_msg(msg);
+ } else {
+ break;
+ }
+ }
+ }
+
+ fn running(&self) -> bool {
+ self.running
+ }
+}
+
+/// A type for building XR sessions
+pub struct SessionBuilder<'a, GL> {
+ sessions: &'a mut Vec<Box<dyn MainThreadSession>>,
+ frame_sender: Sender<Frame>,
+ layer_grand_manager: LayerGrandManager<GL>,
+ id: SessionId,
+}
+
+impl<'a, GL: 'static> SessionBuilder<'a, GL> {
+ pub fn id(&self) -> SessionId {
+ self.id
+ }
+
+ pub(crate) fn new(
+ sessions: &'a mut Vec<Box<dyn MainThreadSession>>,
+ frame_sender: Sender<Frame>,
+ layer_grand_manager: LayerGrandManager<GL>,
+ id: SessionId,
+ ) -> Self {
+ SessionBuilder {
+ sessions,
+ frame_sender,
+ layer_grand_manager,
+ id,
+ }
+ }
+
+ /// For devices which are happy to hand over thread management to webxr.
+ pub fn spawn<Device, Factory>(self, factory: Factory) -> Result<Session, Error>
+ where
+ Factory: 'static + FnOnce(LayerGrandManager<GL>) -> Result<Device, Error> + Send,
+ Device: DeviceAPI,
+ {
+ let (acks, ackr) = crate::channel().or(Err(Error::CommunicationError))?;
+ let frame_sender = self.frame_sender;
+ let layer_grand_manager = self.layer_grand_manager;
+ let id = self.id;
+ thread::spawn(move || {
+ match factory(layer_grand_manager)
+ .and_then(|device| SessionThread::new(device, frame_sender, id))
+ {
+ Ok(mut thread) => {
+ let session = thread.new_session();
+ let _ = acks.send(Ok(session));
+ thread.run();
+ }
+ Err(err) => {
+ let _ = acks.send(Err(err));
+ }
+ }
+ });
+ ackr.recv().unwrap_or(Err(Error::CommunicationError))
+ }
+
+ /// For devices that need to run on the main thread.
+ pub fn run_on_main_thread<Device, Factory>(self, factory: Factory) -> Result<Session, Error>
+ where
+ Factory: 'static + FnOnce(LayerGrandManager<GL>) -> Result<Device, Error>,
+ Device: DeviceAPI,
+ {
+ let device = factory(self.layer_grand_manager)?;
+ let frame_sender = self.frame_sender;
+ let mut session_thread = SessionThread::new(device, frame_sender, self.id)?;
+ let session = session_thread.new_session();
+ self.sessions.push(Box::new(session_thread));
+ Ok(session)
+ }
+}
diff --git a/components/shared/webxr/space.rs b/components/shared/webxr/space.rs
new file mode 100644
index 00000000000..4ab116c5b90
--- /dev/null
+++ b/components/shared/webxr/space.rs
@@ -0,0 +1,28 @@
+use crate::InputId;
+use crate::Joint;
+use euclid::RigidTransform3D;
+
+#[derive(Clone, Copy, Debug)]
+#[cfg_attr(feature = "ipc", derive(serde::Serialize, serde::Deserialize))]
+/// A stand-in type for "the space isn't statically known since
+/// it comes from client side code"
+pub struct ApiSpace;
+
+#[derive(Clone, Copy, Debug, PartialEq)]
+#[cfg_attr(feature = "ipc", derive(serde::Serialize, serde::Deserialize))]
+pub enum BaseSpace {
+ Local,
+ Floor,
+ Viewer,
+ BoundedFloor,
+ TargetRay(InputId),
+ Grip(InputId),
+ Joint(InputId, Joint),
+}
+
+#[derive(Clone, Copy, Debug)]
+#[cfg_attr(feature = "ipc", derive(serde::Serialize, serde::Deserialize))]
+pub struct Space {
+ pub base: BaseSpace,
+ pub offset: RigidTransform3D<f32, ApiSpace, ApiSpace>,
+}
diff --git a/components/shared/webxr/util.rs b/components/shared/webxr/util.rs
new file mode 100644
index 00000000000..e6342d42faf
--- /dev/null
+++ b/components/shared/webxr/util.rs
@@ -0,0 +1,129 @@
+use crate::FrameUpdateEvent;
+use crate::HitTestId;
+use crate::HitTestSource;
+use euclid::Transform3D;
+
+#[derive(Clone, Copy, Debug)]
+#[cfg_attr(feature = "ipc", derive(serde::Serialize, serde::Deserialize))]
+pub struct ClipPlanes {
+ pub near: f32,
+ pub far: f32,
+ /// Was there an update that needs propagation to the client?
+ update: bool,
+}
+
+impl Default for ClipPlanes {
+ fn default() -> Self {
+ ClipPlanes {
+ near: 0.1,
+ far: 1000.,
+ update: false,
+ }
+ }
+}
+
+impl ClipPlanes {
+ pub fn update(&mut self, near: f32, far: f32) {
+ self.near = near;
+ self.far = far;
+ self.update = true;
+ }
+
+ /// Checks for and clears the pending update flag
+ pub fn recently_updated(&mut self) -> bool {
+ if self.update {
+ self.update = false;
+ true
+ } else {
+ false
+ }
+ }
+}
+
+#[derive(Clone, Debug, Default)]
+#[cfg_attr(feature = "ipc", derive(serde::Serialize, serde::Deserialize))]
+/// Holds on to hit tests
+pub struct HitTestList {
+ tests: Vec<HitTestSource>,
+ uncommitted_tests: Vec<HitTestSource>,
+}
+
+impl HitTestList {
+ pub fn request_hit_test(&mut self, source: HitTestSource) {
+ self.uncommitted_tests.push(source)
+ }
+
+ pub fn commit_tests(&mut self) -> Vec<FrameUpdateEvent> {
+ let mut events = vec![];
+ for test in self.uncommitted_tests.drain(..) {
+ events.push(FrameUpdateEvent::HitTestSourceAdded(test.id));
+ self.tests.push(test);
+ }
+ events
+ }
+
+ pub fn tests(&self) -> &[HitTestSource] {
+ &self.tests
+ }
+
+ pub fn cancel_hit_test(&mut self, id: HitTestId) {
+ self.tests.retain(|s| s.id != id);
+ self.uncommitted_tests.retain(|s| s.id != id);
+ }
+}
+
+#[inline]
+/// Construct a projection matrix given the four angles from the center for the faces of the viewing frustum
+pub fn fov_to_projection_matrix<T, U>(
+ left: f32,
+ right: f32,
+ top: f32,
+ bottom: f32,
+ clip_planes: ClipPlanes,
+) -> Transform3D<f32, T, U> {
+ let near = clip_planes.near;
+ // XXXManishearth deal with infinite planes
+ let left = left.tan() * near;
+ let right = right.tan() * near;
+ let top = top.tan() * near;
+ let bottom = bottom.tan() * near;
+
+ frustum_to_projection_matrix(left, right, top, bottom, clip_planes)
+}
+
+#[inline]
+/// Construct matrix given the actual extent of the viewing frustum on the near plane
+pub fn frustum_to_projection_matrix<T, U>(
+ left: f32,
+ right: f32,
+ top: f32,
+ bottom: f32,
+ clip_planes: ClipPlanes,
+) -> Transform3D<f32, T, U> {
+ let near = clip_planes.near;
+ let far = clip_planes.far;
+
+ let w = right - left;
+ let h = top - bottom;
+ let d = far - near;
+
+ // Column-major order
+ Transform3D::new(
+ 2. * near / w,
+ 0.,
+ 0.,
+ 0.,
+ 0.,
+ 2. * near / h,
+ 0.,
+ 0.,
+ (right + left) / w,
+ (top + bottom) / h,
+ -(far + near) / d,
+ -1.,
+ 0.,
+ 0.,
+ -2. * far * near / d,
+ 0.,
+ )
+}
diff --git a/components/shared/webxr/view.rs b/components/shared/webxr/view.rs
new file mode 100644
index 00000000000..566748f8a7a
--- /dev/null
+++ b/components/shared/webxr/view.rs
@@ -0,0 +1,170 @@
+/* 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/. */
+
+//! This crate uses `euclid`'s typed units, and exposes different coordinate spaces.
+
+use euclid::Rect;
+use euclid::RigidTransform3D;
+use euclid::Transform3D;
+
+#[cfg(feature = "ipc")]
+use serde::{Deserialize, Serialize};
+
+use std::marker::PhantomData;
+
+/// The coordinate space of the viewer
+/// https://immersive-web.github.io/webxr/#dom-xrreferencespacetype-viewer
+#[derive(Clone, Copy, Debug)]
+#[cfg_attr(feature = "ipc", derive(Serialize, Deserialize))]
+pub enum Viewer {}
+
+/// The coordinate space of the floor
+/// https://immersive-web.github.io/webxr/#dom-xrreferencespacetype-local-floor
+#[derive(Clone, Copy, Debug)]
+#[cfg_attr(feature = "ipc", derive(Serialize, Deserialize))]
+pub enum Floor {}
+
+/// The coordinate space of the left eye
+/// https://immersive-web.github.io/webxr/#dom-xreye-left
+#[derive(Clone, Copy, Debug)]
+#[cfg_attr(feature = "ipc", derive(Serialize, Deserialize))]
+pub enum LeftEye {}
+
+/// The coordinate space of the right eye
+/// https://immersive-web.github.io/webxr/#dom-xreye-right
+#[derive(Clone, Copy, Debug)]
+#[cfg_attr(feature = "ipc", derive(Serialize, Deserialize))]
+pub enum RightEye {}
+
+/// The coordinate space of the left frustrum of a cubemap
+#[derive(Clone, Copy, Debug)]
+#[cfg_attr(feature = "ipc", derive(Serialize, Deserialize))]
+pub enum CubeLeft {}
+
+/// The coordinate space of the right frustrum of a cubemap
+#[derive(Clone, Copy, Debug)]
+#[cfg_attr(feature = "ipc", derive(Serialize, Deserialize))]
+pub enum CubeRight {}
+
+/// The coordinate space of the top frustrum of a cubemap
+#[derive(Clone, Copy, Debug)]
+#[cfg_attr(feature = "ipc", derive(Serialize, Deserialize))]
+pub enum CubeTop {}
+
+/// The coordinate space of the bottom frustrum of a cubemap
+#[derive(Clone, Copy, Debug)]
+#[cfg_attr(feature = "ipc", derive(Serialize, Deserialize))]
+pub enum CubeBottom {}
+
+/// The coordinate space of the back frustrum of a cubemap
+#[derive(Clone, Copy, Debug)]
+#[cfg_attr(feature = "ipc", derive(Serialize, Deserialize))]
+pub enum CubeBack {}
+
+/// Pattern-match on eyes
+#[derive(Clone, Copy, Debug)]
+#[cfg_attr(feature = "ipc", derive(Serialize, Deserialize))]
+pub struct SomeEye<Eye>(u8, PhantomData<Eye>);
+pub const LEFT_EYE: SomeEye<LeftEye> = SomeEye(0, PhantomData);
+pub const RIGHT_EYE: SomeEye<RightEye> = SomeEye(1, PhantomData);
+pub const VIEWER: SomeEye<Viewer> = SomeEye(2, PhantomData);
+pub const CUBE_LEFT: SomeEye<CubeLeft> = SomeEye(3, PhantomData);
+pub const CUBE_RIGHT: SomeEye<CubeRight> = SomeEye(4, PhantomData);
+pub const CUBE_TOP: SomeEye<CubeTop> = SomeEye(5, PhantomData);
+pub const CUBE_BOTTOM: SomeEye<CubeBottom> = SomeEye(6, PhantomData);
+pub const CUBE_BACK: SomeEye<CubeBack> = SomeEye(7, PhantomData);
+
+impl<Eye1, Eye2> PartialEq<SomeEye<Eye2>> for SomeEye<Eye1> {
+ fn eq(&self, rhs: &SomeEye<Eye2>) -> bool {
+ self.0 == rhs.0
+ }
+}
+
+/// The native 3D coordinate space of the device
+/// This is not part of the webvr specification.
+#[derive(Clone, Copy, Debug)]
+#[cfg_attr(feature = "ipc", derive(Serialize, Deserialize))]
+pub enum Native {}
+
+/// The normalized device coordinate space, where the display
+/// is from (-1,-1) to (1,1).
+// TODO: are we OK assuming that we can use the same coordinate system for all displays?
+#[derive(Clone, Copy, Debug)]
+#[cfg_attr(feature = "ipc", derive(Serialize, Deserialize))]
+pub enum Display {}
+
+/// The unnormalized device coordinate space, where the display
+/// is from (0,0) to (w,h), measured in pixels.
+// TODO: are we OK assuming that we can use the same coordinate system for all displays?
+#[derive(Clone, Copy, Debug)]
+#[cfg_attr(feature = "ipc", derive(Serialize, Deserialize))]
+pub enum Viewport {}
+
+/// The coordinate space of an input device
+#[derive(Clone, Copy, Debug)]
+#[cfg_attr(feature = "ipc", derive(Serialize, Deserialize))]
+pub enum Input {}
+
+/// The coordinate space of a secondary capture view
+#[derive(Clone, Copy, Debug)]
+#[cfg_attr(feature = "ipc", derive(Serialize, Deserialize))]
+pub enum Capture {}
+
+/// For each eye, the pose of that eye,
+/// its projection onto its display.
+/// For stereo displays, we have a `View<LeftEye>` and a `View<RightEye>`.
+/// For mono displays, we hagve a `View<Viewer>`
+/// https://immersive-web.github.io/webxr/#xrview
+#[derive(Clone, Debug)]
+#[cfg_attr(feature = "ipc", derive(Serialize, Deserialize))]
+pub struct View<Eye> {
+ pub transform: RigidTransform3D<f32, Eye, Native>,
+ pub projection: Transform3D<f32, Eye, Display>,
+}
+
+impl<Eye> Default for View<Eye> {
+ fn default() -> Self {
+ View {
+ transform: RigidTransform3D::identity(),
+ projection: Transform3D::identity(),
+ }
+ }
+}
+
+impl<Eye> View<Eye> {
+ pub fn cast_unit<NewEye>(&self) -> View<NewEye> {
+ View {
+ transform: self.transform.cast_unit(),
+ projection: Transform3D::from_untyped(&self.projection.to_untyped()),
+ }
+ }
+}
+
+/// Whether a device is mono or stereo, and the views it supports.
+#[derive(Clone, Debug)]
+#[cfg_attr(feature = "ipc", derive(Serialize, Deserialize))]
+pub enum Views {
+ /// Mono view for inline VR, viewport and projection matrices are calculated by client
+ Inline,
+ Mono(View<Viewer>),
+ Stereo(View<LeftEye>, View<RightEye>),
+ StereoCapture(View<LeftEye>, View<RightEye>, View<Capture>),
+ Cubemap(
+ View<Viewer>,
+ View<CubeLeft>,
+ View<CubeRight>,
+ View<CubeTop>,
+ View<CubeBottom>,
+ View<CubeBack>,
+ ),
+}
+
+/// A list of viewports per-eye in the order of fields in Views.
+///
+/// Not all must be in active use.
+#[derive(Clone, Debug)]
+#[cfg_attr(feature = "ipc", derive(Serialize, Deserialize))]
+pub struct Viewports {
+ pub viewports: Vec<Rect<i32, Viewport>>,
+}