Merge webxr repository (#35228)

Signed-off-by: Martin Robinson <mrobinson@igalia.com>
This commit is contained in:
Martin Robinson 2025-01-30 20:07:35 +01:00 committed by GitHub
parent 64b40ea700
commit 534e78db53
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 7303 additions and 2 deletions

View file

@ -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 }

View file

@ -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)
}
}

View file

@ -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),
}

View file

@ -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)
}
}

View file

@ -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,
}

View file

@ -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),
}

View file

@ -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
}
}

View file

@ -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,
}

View file

@ -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>,
}

View file

@ -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()
}

View file

@ -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,
}

View file

@ -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>>),
}

View file

@ -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)
}
}

View file

@ -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>,
}

View file

@ -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.,
)
}

View file

@ -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>>,
}