diff options
author | Martin Robinson <mrobinson@igalia.com> | 2025-01-30 20:07:35 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2025-01-30 19:07:35 +0000 |
commit | 534e78db5331fbfbad7e60d72a88e9aacdc11ee4 (patch) | |
tree | 3bcd217e0e7b7fd0c91d5406a81ea241ffc4ce06 /components/shared/webxr | |
parent | 64b40ea70065f949d1e281bd046c56d50312f2a7 (diff) | |
download | servo-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.toml | 28 | ||||
-rw-r--r-- | components/shared/webxr/device.rs | 114 | ||||
-rw-r--r-- | components/shared/webxr/error.rs | 21 | ||||
-rw-r--r-- | components/shared/webxr/events.rs | 80 | ||||
-rw-r--r-- | components/shared/webxr/frame.rs | 60 | ||||
-rw-r--r-- | components/shared/webxr/hand.rs | 122 | ||||
-rw-r--r-- | components/shared/webxr/hittest.rs | 179 | ||||
-rw-r--r-- | components/shared/webxr/input.rs | 74 | ||||
-rw-r--r-- | components/shared/webxr/layer.rs | 296 | ||||
-rw-r--r-- | components/shared/webxr/lib.rs | 175 | ||||
-rw-r--r-- | components/shared/webxr/mock.rs | 146 | ||||
-rw-r--r-- | components/shared/webxr/registry.rs | 262 | ||||
-rw-r--r-- | components/shared/webxr/session.rs | 531 | ||||
-rw-r--r-- | components/shared/webxr/space.rs | 28 | ||||
-rw-r--r-- | components/shared/webxr/util.rs | 129 | ||||
-rw-r--r-- | components/shared/webxr/view.rs | 170 |
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>>, +} |