aboutsummaryrefslogtreecommitdiffstats
path: root/components/script/dom/webxr
diff options
context:
space:
mode:
Diffstat (limited to 'components/script/dom/webxr')
-rw-r--r--components/script/dom/webxr/fakexrdevice.rs368
-rw-r--r--components/script/dom/webxr/fakexrinputcontroller.rs194
-rw-r--r--components/script/dom/webxr/mod.rs44
-rw-r--r--components/script/dom/webxr/xrboundedreferencespace.rs91
-rw-r--r--components/script/dom/webxr/xrcompositionlayer.rs12
-rw-r--r--components/script/dom/webxr/xrcubelayer.rs12
-rw-r--r--components/script/dom/webxr/xrcylinderlayer.rs12
-rw-r--r--components/script/dom/webxr/xrequirectlayer.rs12
-rw-r--r--components/script/dom/webxr/xrframe.rs294
-rw-r--r--components/script/dom/webxr/xrhand.rs176
-rw-r--r--components/script/dom/webxr/xrhittestresult.rs54
-rw-r--r--components/script/dom/webxr/xrhittestsource.rs53
-rw-r--r--components/script/dom/webxr/xrinputsource.rs186
-rw-r--r--components/script/dom/webxr/xrinputsourcearray.rs152
-rw-r--r--components/script/dom/webxr/xrinputsourceevent.rs116
-rw-r--r--components/script/dom/webxr/xrinputsourceschangeevent.rs140
-rw-r--r--components/script/dom/webxr/xrjointpose.rs51
-rw-r--r--components/script/dom/webxr/xrjointspace.rs83
-rw-r--r--components/script/dom/webxr/xrlayer.rs77
-rw-r--r--components/script/dom/webxr/xrlayerevent.rs77
-rw-r--r--components/script/dom/webxr/xrmediabinding.rs101
-rw-r--r--components/script/dom/webxr/xrpose.rs65
-rw-r--r--components/script/dom/webxr/xrprojectionlayer.rs12
-rw-r--r--components/script/dom/webxr/xrquadlayer.rs12
-rw-r--r--components/script/dom/webxr/xrray.rs174
-rw-r--r--components/script/dom/webxr/xrreferencespace.rs153
-rw-r--r--components/script/dom/webxr/xrreferencespaceevent.rs121
-rw-r--r--components/script/dom/webxr/xrrenderstate.rs155
-rw-r--r--components/script/dom/webxr/xrrigidtransform.rs188
-rw-r--r--components/script/dom/webxr/xrsession.rs1111
-rw-r--r--components/script/dom/webxr/xrsessionevent.rs100
-rw-r--r--components/script/dom/webxr/xrspace.rs120
-rw-r--r--components/script/dom/webxr/xrsubimage.rs23
-rw-r--r--components/script/dom/webxr/xrsystem.rs334
-rw-r--r--components/script/dom/webxr/xrtest.rs235
-rw-r--r--components/script/dom/webxr/xrview.rs137
-rw-r--r--components/script/dom/webxr/xrviewerpose.rs196
-rw-r--r--components/script/dom/webxr/xrviewport.rs54
-rw-r--r--components/script/dom/webxr/xrwebglbinding.rs168
-rw-r--r--components/script/dom/webxr/xrwebgllayer.rs367
-rw-r--r--components/script/dom/webxr/xrwebglsubimage.rs49
41 files changed, 6079 insertions, 0 deletions
diff --git a/components/script/dom/webxr/fakexrdevice.rs b/components/script/dom/webxr/fakexrdevice.rs
new file mode 100644
index 00000000000..7024aa9dc65
--- /dev/null
+++ b/components/script/dom/webxr/fakexrdevice.rs
@@ -0,0 +1,368 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+use std::cell::Cell;
+use std::rc::Rc;
+
+use dom_struct::dom_struct;
+use euclid::{Point2D, Point3D, Rect, RigidTransform3D, Rotation3D, Size2D, Transform3D, Vector3D};
+use ipc_channel::ipc::IpcSender;
+use ipc_channel::router::ROUTER;
+use profile_traits::ipc;
+use webxr_api::{
+ EntityType, Handedness, InputId, InputSource, MockDeviceMsg, MockInputInit, MockRegion,
+ MockViewInit, MockViewsInit, MockWorld, TargetRayMode, Triangle, Visibility,
+};
+
+use crate::dom::bindings::codegen::Bindings::DOMPointBinding::DOMPointInit;
+use crate::dom::bindings::codegen::Bindings::FakeXRDeviceBinding::{
+ FakeXRBoundsPoint, FakeXRDeviceMethods, FakeXRRegionType, FakeXRRigidTransformInit,
+ FakeXRViewInit, FakeXRWorldInit,
+};
+use crate::dom::bindings::codegen::Bindings::FakeXRInputControllerBinding::FakeXRInputSourceInit;
+use crate::dom::bindings::codegen::Bindings::XRInputSourceBinding::{
+ XRHandedness, XRTargetRayMode,
+};
+use crate::dom::bindings::codegen::Bindings::XRSessionBinding::XRVisibilityState;
+use crate::dom::bindings::codegen::Bindings::XRViewBinding::XREye;
+use crate::dom::bindings::error::{Error, Fallible};
+use crate::dom::bindings::refcounted::TrustedPromise;
+use crate::dom::bindings::reflector::{reflect_dom_object, DomObject, Reflector};
+use crate::dom::bindings::root::DomRoot;
+use crate::dom::fakexrinputcontroller::{init_to_mock_buttons, FakeXRInputController};
+use crate::dom::globalscope::GlobalScope;
+use crate::dom::promise::Promise;
+use crate::script_runtime::CanGc;
+use crate::task_source::TaskSource;
+
+#[dom_struct]
+pub struct FakeXRDevice {
+ reflector: Reflector,
+ #[ignore_malloc_size_of = "defined in ipc-channel"]
+ #[no_trace]
+ sender: IpcSender<MockDeviceMsg>,
+ #[ignore_malloc_size_of = "defined in webxr-api"]
+ #[no_trace]
+ next_input_id: Cell<InputId>,
+}
+
+impl FakeXRDevice {
+ pub fn new_inherited(sender: IpcSender<MockDeviceMsg>) -> FakeXRDevice {
+ FakeXRDevice {
+ reflector: Reflector::new(),
+ sender,
+ next_input_id: Cell::new(InputId(0)),
+ }
+ }
+
+ pub fn new(global: &GlobalScope, sender: IpcSender<MockDeviceMsg>) -> DomRoot<FakeXRDevice> {
+ reflect_dom_object(Box::new(FakeXRDevice::new_inherited(sender)), global)
+ }
+
+ pub fn disconnect(&self, sender: IpcSender<()>) {
+ let _ = self.sender.send(MockDeviceMsg::Disconnect(sender));
+ }
+}
+
+pub fn view<Eye>(view: &FakeXRViewInit) -> Fallible<MockViewInit<Eye>> {
+ if view.projectionMatrix.len() != 16 || view.viewOffset.position.len() != 3 {
+ return Err(Error::Type("Incorrectly sized array".into()));
+ }
+
+ let mut proj = [0.; 16];
+ let v: Vec<_> = view.projectionMatrix.iter().map(|x| **x).collect();
+ proj.copy_from_slice(&v);
+ let projection = Transform3D::from_array(proj);
+
+ // spec defines offsets as origins, but mock API expects the inverse transform
+ let transform = get_origin(&view.viewOffset)?.inverse();
+
+ let size = Size2D::new(view.resolution.width, view.resolution.height);
+ let origin = match view.eye {
+ XREye::Right => Point2D::new(size.width, 0),
+ _ => Point2D::zero(),
+ };
+ let viewport = Rect::new(origin, size);
+
+ let fov = view.fieldOfView.as_ref().map(|fov| {
+ (
+ fov.leftDegrees.to_radians(),
+ fov.rightDegrees.to_radians(),
+ fov.upDegrees.to_radians(),
+ fov.downDegrees.to_radians(),
+ )
+ });
+
+ Ok(MockViewInit {
+ projection,
+ transform,
+ viewport,
+ fov,
+ })
+}
+
+pub fn get_views(views: &[FakeXRViewInit]) -> Fallible<MockViewsInit> {
+ match views.len() {
+ 1 => Ok(MockViewsInit::Mono(view(&views[0])?)),
+ 2 => {
+ let (left, right) = match (views[0].eye, views[1].eye) {
+ (XREye::Left, XREye::Right) => (&views[0], &views[1]),
+ (XREye::Right, XREye::Left) => (&views[1], &views[0]),
+ _ => return Err(Error::NotSupported),
+ };
+ Ok(MockViewsInit::Stereo(view(left)?, view(right)?))
+ },
+ _ => Err(Error::NotSupported),
+ }
+}
+
+pub fn get_origin<T, U>(
+ origin: &FakeXRRigidTransformInit,
+) -> Fallible<RigidTransform3D<f32, T, U>> {
+ if origin.position.len() != 3 || origin.orientation.len() != 4 {
+ return Err(Error::Type("Incorrectly sized array".into()));
+ }
+ let p = Vector3D::new(
+ *origin.position[0],
+ *origin.position[1],
+ *origin.position[2],
+ );
+ let o = Rotation3D::unit_quaternion(
+ *origin.orientation[0],
+ *origin.orientation[1],
+ *origin.orientation[2],
+ *origin.orientation[3],
+ );
+
+ Ok(RigidTransform3D::new(o, p))
+}
+
+pub fn get_point<T>(pt: &DOMPointInit) -> Point3D<f32, T> {
+ Point3D::new(pt.x / pt.w, pt.y / pt.w, pt.z / pt.w).cast()
+}
+
+pub fn get_world(world: &FakeXRWorldInit) -> Fallible<MockWorld> {
+ let regions = world
+ .hitTestRegions
+ .iter()
+ .map(|region| {
+ let ty = region.type_.into();
+ let faces = region
+ .faces
+ .iter()
+ .map(|face| {
+ if face.vertices.len() != 3 {
+ return Err(Error::Type(
+ "Incorrectly sized array for triangle list".into(),
+ ));
+ }
+
+ Ok(Triangle {
+ first: get_point(&face.vertices[0]),
+ second: get_point(&face.vertices[1]),
+ third: get_point(&face.vertices[2]),
+ })
+ })
+ .collect::<Fallible<Vec<_>>>()?;
+ Ok(MockRegion { faces, ty })
+ })
+ .collect::<Fallible<Vec<_>>>()?;
+
+ Ok(MockWorld { regions })
+}
+
+impl From<FakeXRRegionType> for EntityType {
+ fn from(x: FakeXRRegionType) -> Self {
+ match x {
+ FakeXRRegionType::Point => EntityType::Point,
+ FakeXRRegionType::Plane => EntityType::Plane,
+ FakeXRRegionType::Mesh => EntityType::Mesh,
+ }
+ }
+}
+
+impl FakeXRDeviceMethods<crate::DomTypeHolder> for FakeXRDevice {
+ /// <https://github.com/immersive-web/webxr-test-api/blob/master/explainer.md>
+ fn SetViews(
+ &self,
+ views: Vec<FakeXRViewInit>,
+ _secondary_views: Option<Vec<FakeXRViewInit>>,
+ ) -> Fallible<()> {
+ let _ = self
+ .sender
+ .send(MockDeviceMsg::SetViews(get_views(&views)?));
+ // TODO: Support setting secondary views for mock backend
+ Ok(())
+ }
+
+ /// <https://immersive-web.github.io/webxr-test-api/#dom-fakexrdevice-setviewerorigin>
+ fn SetViewerOrigin(
+ &self,
+ origin: &FakeXRRigidTransformInit,
+ _emulated_position: bool,
+ ) -> Fallible<()> {
+ let _ = self
+ .sender
+ .send(MockDeviceMsg::SetViewerOrigin(Some(get_origin(origin)?)));
+ Ok(())
+ }
+
+ /// <https://immersive-web.github.io/webxr-test-api/#dom-fakexrdevice-clearviewerorigin>
+ fn ClearViewerOrigin(&self) {
+ let _ = self.sender.send(MockDeviceMsg::SetViewerOrigin(None));
+ }
+
+ /// <https://immersive-web.github.io/webxr-test-api/#dom-fakexrdevice-clearfloororigin>
+ fn ClearFloorOrigin(&self) {
+ let _ = self.sender.send(MockDeviceMsg::SetFloorOrigin(None));
+ }
+
+ /// <https://immersive-web.github.io/webxr-test-api/#dom-fakexrdevice-setfloororigin>
+ fn SetFloorOrigin(&self, origin: &FakeXRRigidTransformInit) -> Fallible<()> {
+ let _ = self
+ .sender
+ .send(MockDeviceMsg::SetFloorOrigin(Some(get_origin(origin)?)));
+ Ok(())
+ }
+
+ /// <https://immersive-web.github.io/webxr-test-api/#dom-fakexrdevice-clearworld>
+ fn ClearWorld(&self) {
+ let _ = self.sender.send(MockDeviceMsg::ClearWorld);
+ }
+
+ /// <https://immersive-web.github.io/webxr-test-api/#dom-fakexrdevice-setworld>
+ fn SetWorld(&self, world: &FakeXRWorldInit) -> Fallible<()> {
+ let _ = self.sender.send(MockDeviceMsg::SetWorld(get_world(world)?));
+ Ok(())
+ }
+
+ /// <https://immersive-web.github.io/webxr-test-api/#dom-fakexrdevice-simulatevisibilitychange>
+ fn SimulateVisibilityChange(&self, v: XRVisibilityState) {
+ let v = match v {
+ XRVisibilityState::Visible => Visibility::Visible,
+ XRVisibilityState::Visible_blurred => Visibility::VisibleBlurred,
+ XRVisibilityState::Hidden => Visibility::Hidden,
+ };
+ let _ = self.sender.send(MockDeviceMsg::VisibilityChange(v));
+ }
+
+ /// <https://immersive-web.github.io/webxr-test-api/#dom-fakexrdevice-simulateinputsourceconnection>
+ fn SimulateInputSourceConnection(
+ &self,
+ init: &FakeXRInputSourceInit,
+ ) -> Fallible<DomRoot<FakeXRInputController>> {
+ let id = self.next_input_id.get();
+ self.next_input_id.set(InputId(id.0 + 1));
+
+ let handedness = init.handedness.into();
+ let target_ray_mode = init.targetRayMode.into();
+
+ let pointer_origin = Some(get_origin(&init.pointerOrigin)?);
+
+ let grip_origin = if let Some(ref g) = init.gripOrigin {
+ Some(get_origin(g)?)
+ } else {
+ None
+ };
+
+ let profiles = init.profiles.iter().cloned().map(String::from).collect();
+
+ let mut supported_buttons = vec![];
+ if let Some(ref buttons) = init.supportedButtons {
+ supported_buttons.extend(init_to_mock_buttons(buttons));
+ }
+
+ let source = InputSource {
+ handedness,
+ target_ray_mode,
+ id,
+ supports_grip: true,
+ profiles,
+ hand_support: None,
+ };
+
+ let init = MockInputInit {
+ source,
+ pointer_origin,
+ grip_origin,
+ supported_buttons,
+ };
+
+ let global = self.global();
+ let _ = self.sender.send(MockDeviceMsg::AddInputSource(init));
+
+ let controller = FakeXRInputController::new(&global, self.sender.clone(), id);
+
+ Ok(controller)
+ }
+
+ /// <https://immersive-web.github.io/webxr-test-api/#dom-fakexrdevice-disconnect>
+ fn Disconnect(&self, can_gc: CanGc) -> Rc<Promise> {
+ let global = self.global();
+ let p = Promise::new(&global, can_gc);
+ let mut trusted = Some(TrustedPromise::new(p.clone()));
+ let (task_source, canceller) = global
+ .as_window()
+ .task_manager()
+ .dom_manipulation_task_source_with_canceller();
+ let (sender, receiver) = ipc::channel(global.time_profiler_chan().clone()).unwrap();
+
+ ROUTER.add_typed_route(
+ receiver.to_ipc_receiver(),
+ Box::new(move |_| {
+ let trusted = trusted
+ .take()
+ .expect("disconnect callback called multiple times");
+ let _ = task_source.queue_with_canceller(trusted.resolve_task(()), &canceller);
+ }),
+ );
+ self.disconnect(sender);
+ p
+ }
+
+ /// <https://immersive-web.github.io/webxr-test-api/#dom-fakexrdevice-setboundsgeometry>
+ fn SetBoundsGeometry(&self, bounds_coodinates: Vec<FakeXRBoundsPoint>) -> Fallible<()> {
+ if bounds_coodinates.len() < 3 {
+ return Err(Error::Type(
+ "Bounds geometry must contain at least 3 points".into(),
+ ));
+ }
+ let coords = bounds_coodinates
+ .iter()
+ .map(|coord| {
+ let x = *coord.x.unwrap() as f32;
+ let y = *coord.z.unwrap() as f32;
+ Point2D::new(x, y)
+ })
+ .collect();
+ let _ = self.sender.send(MockDeviceMsg::SetBoundsGeometry(coords));
+ Ok(())
+ }
+
+ /// <https://immersive-web.github.io/webxr-test-api/#dom-fakexrdevice-simulateresetpose>
+ fn SimulateResetPose(&self) {
+ let _ = self.sender.send(MockDeviceMsg::SimulateResetPose);
+ }
+}
+
+impl From<XRHandedness> for Handedness {
+ fn from(h: XRHandedness) -> Self {
+ match h {
+ XRHandedness::None => Handedness::None,
+ XRHandedness::Left => Handedness::Left,
+ XRHandedness::Right => Handedness::Right,
+ }
+ }
+}
+
+impl From<XRTargetRayMode> for TargetRayMode {
+ fn from(t: XRTargetRayMode) -> Self {
+ match t {
+ XRTargetRayMode::Gaze => TargetRayMode::Gaze,
+ XRTargetRayMode::Tracked_pointer => TargetRayMode::TrackedPointer,
+ XRTargetRayMode::Screen => TargetRayMode::Screen,
+ XRTargetRayMode::Transient_pointer => TargetRayMode::TransientPointer,
+ }
+ }
+}
diff --git a/components/script/dom/webxr/fakexrinputcontroller.rs b/components/script/dom/webxr/fakexrinputcontroller.rs
new file mode 100644
index 00000000000..a9bf8038f5e
--- /dev/null
+++ b/components/script/dom/webxr/fakexrinputcontroller.rs
@@ -0,0 +1,194 @@
+/* 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 dom_struct::dom_struct;
+use ipc_channel::ipc::IpcSender;
+use webxr_api::{
+ Handedness, InputId, MockButton, MockButtonType, MockDeviceMsg, MockInputMsg, SelectEvent,
+ SelectKind, TargetRayMode,
+};
+
+use crate::dom::bindings::codegen::Bindings::FakeXRDeviceBinding::FakeXRRigidTransformInit;
+use crate::dom::bindings::codegen::Bindings::FakeXRInputControllerBinding::{
+ FakeXRButtonStateInit, FakeXRButtonType, FakeXRInputControllerMethods,
+};
+use crate::dom::bindings::codegen::Bindings::XRInputSourceBinding::{
+ XRHandedness, XRTargetRayMode,
+};
+use crate::dom::bindings::error::{Error, Fallible};
+use crate::dom::bindings::reflector::{reflect_dom_object, Reflector};
+use crate::dom::bindings::root::DomRoot;
+use crate::dom::bindings::str::DOMString;
+use crate::dom::fakexrdevice::get_origin;
+use crate::dom::globalscope::GlobalScope;
+
+#[dom_struct]
+pub struct FakeXRInputController {
+ reflector: Reflector,
+ #[ignore_malloc_size_of = "defined in ipc-channel"]
+ #[no_trace]
+ sender: IpcSender<MockDeviceMsg>,
+ #[ignore_malloc_size_of = "defined in webxr-api"]
+ #[no_trace]
+ id: InputId,
+}
+
+impl FakeXRInputController {
+ pub fn new_inherited(sender: IpcSender<MockDeviceMsg>, id: InputId) -> FakeXRInputController {
+ FakeXRInputController {
+ reflector: Reflector::new(),
+ sender,
+ id,
+ }
+ }
+
+ pub fn new(
+ global: &GlobalScope,
+ sender: IpcSender<MockDeviceMsg>,
+ id: InputId,
+ ) -> DomRoot<FakeXRInputController> {
+ reflect_dom_object(
+ Box::new(FakeXRInputController::new_inherited(sender, id)),
+ global,
+ )
+ }
+
+ fn send_message(&self, msg: MockInputMsg) {
+ let _ = self
+ .sender
+ .send(MockDeviceMsg::MessageInputSource(self.id, msg));
+ }
+}
+
+impl FakeXRInputControllerMethods<crate::DomTypeHolder> for FakeXRInputController {
+ /// <https://immersive-web.github.io/webxr-test-api/#dom-fakexrinputcontroller-setpointerorigin>
+ fn SetPointerOrigin(&self, origin: &FakeXRRigidTransformInit, _emulated: bool) -> Fallible<()> {
+ self.send_message(MockInputMsg::SetPointerOrigin(Some(get_origin(origin)?)));
+ Ok(())
+ }
+
+ /// <https://immersive-web.github.io/webxr-test-api/#dom-fakexrinputcontroller-setgriporigin>
+ fn SetGripOrigin(&self, origin: &FakeXRRigidTransformInit, _emulated: bool) -> Fallible<()> {
+ self.send_message(MockInputMsg::SetGripOrigin(Some(get_origin(origin)?)));
+ Ok(())
+ }
+
+ /// <https://immersive-web.github.io/webxr-test-api/#dom-fakexrinputcontroller-cleargriporigin>
+ fn ClearGripOrigin(&self) {
+ self.send_message(MockInputMsg::SetGripOrigin(None))
+ }
+
+ /// <https://immersive-web.github.io/webxr-test-api/#dom-fakexrinputcontroller-disconnect>
+ fn Disconnect(&self) {
+ self.send_message(MockInputMsg::Disconnect)
+ }
+
+ /// <https://immersive-web.github.io/webxr-test-api/#dom-fakexrinputcontroller-reconnect>
+ fn Reconnect(&self) {
+ self.send_message(MockInputMsg::Reconnect)
+ }
+
+ /// <https://immersive-web.github.io/webxr-test-api/#dom-fakexrinputcontroller-startselection>
+ fn StartSelection(&self) {
+ self.send_message(MockInputMsg::TriggerSelect(
+ SelectKind::Select,
+ SelectEvent::Start,
+ ))
+ }
+
+ /// <https://immersive-web.github.io/webxr-test-api/#dom-fakexrinputcontroller-endselection>
+ fn EndSelection(&self) {
+ self.send_message(MockInputMsg::TriggerSelect(
+ SelectKind::Select,
+ SelectEvent::End,
+ ))
+ }
+
+ /// <https://immersive-web.github.io/webxr-test-api/#dom-fakexrinputcontroller-simulateselect>
+ fn SimulateSelect(&self) {
+ self.send_message(MockInputMsg::TriggerSelect(
+ SelectKind::Select,
+ SelectEvent::Select,
+ ))
+ }
+
+ /// <https://immersive-web.github.io/webxr-test-api/#dom-fakexrinputcontroller-sethandedness>
+ fn SetHandedness(&self, handedness: XRHandedness) {
+ let h = match handedness {
+ XRHandedness::None => Handedness::None,
+ XRHandedness::Left => Handedness::Left,
+ XRHandedness::Right => Handedness::Right,
+ };
+ self.send_message(MockInputMsg::SetHandedness(h));
+ }
+
+ /// <https://immersive-web.github.io/webxr-test-api/#dom-fakexrinputcontroller-settargetraymode>
+ fn SetTargetRayMode(&self, target_ray_mode: XRTargetRayMode) {
+ let t = match target_ray_mode {
+ XRTargetRayMode::Gaze => TargetRayMode::Gaze,
+ XRTargetRayMode::Tracked_pointer => TargetRayMode::TrackedPointer,
+ XRTargetRayMode::Screen => TargetRayMode::Screen,
+ XRTargetRayMode::Transient_pointer => TargetRayMode::TransientPointer,
+ };
+ self.send_message(MockInputMsg::SetTargetRayMode(t));
+ }
+
+ /// <https://immersive-web.github.io/webxr-test-api/#dom-fakexrinputcontroller-setprofiles>
+ fn SetProfiles(&self, profiles: Vec<DOMString>) {
+ let t = profiles.into_iter().map(String::from).collect();
+ self.send_message(MockInputMsg::SetProfiles(t));
+ }
+
+ /// <https://immersive-web.github.io/webxr-test-api/#dom-fakexrinputcontroller-setsupportedbuttons>
+ fn SetSupportedButtons(&self, supported_buttons: Vec<FakeXRButtonStateInit>) {
+ let supported = init_to_mock_buttons(&supported_buttons);
+ self.send_message(MockInputMsg::SetSupportedButtons(supported));
+ }
+
+ /// <https://immersive-web.github.io/webxr-test-api/#dom-fakexrinputcontroller-updatebuttonstate>
+ fn UpdateButtonState(&self, button_state: &FakeXRButtonStateInit) -> Fallible<()> {
+ // https://immersive-web.github.io/webxr-test-api/#validate-a-button-state
+ if (button_state.pressed || *button_state.pressedValue > 0.0) && !button_state.touched {
+ return Err(Error::Type("Pressed button must also be touched".into()));
+ }
+ if *button_state.pressedValue < 0.0 {
+ return Err(Error::Type("Pressed value must be non-negative".into()));
+ }
+
+ // TODO: Steps 3-5 of updateButtonState
+ // Passing the one WPT test that utilizes this will require additional work
+ // to specify gamepad button/axes list lengths, as well as passing that info
+ // to the constructor of XRInputSource
+
+ Ok(())
+ }
+}
+
+impl From<FakeXRButtonType> for MockButtonType {
+ fn from(b: FakeXRButtonType) -> Self {
+ match b {
+ FakeXRButtonType::Grip => MockButtonType::Grip,
+ FakeXRButtonType::Touchpad => MockButtonType::Touchpad,
+ FakeXRButtonType::Thumbstick => MockButtonType::Thumbstick,
+ FakeXRButtonType::Optional_button => MockButtonType::OptionalButton,
+ FakeXRButtonType::Optional_thumbstick => MockButtonType::OptionalThumbstick,
+ }
+ }
+}
+
+/// <https://immersive-web.github.io/webxr-test-api/#parse-supported-buttons>
+pub fn init_to_mock_buttons(buttons: &[FakeXRButtonStateInit]) -> Vec<MockButton> {
+ let supported: Vec<MockButton> = buttons
+ .iter()
+ .map(|b| MockButton {
+ button_type: b.buttonType.into(),
+ pressed: b.pressed,
+ touched: b.touched,
+ pressed_value: *b.pressedValue,
+ x_value: *b.xValue,
+ y_value: *b.yValue,
+ })
+ .collect();
+ supported
+}
diff --git a/components/script/dom/webxr/mod.rs b/components/script/dom/webxr/mod.rs
new file mode 100644
index 00000000000..6faee016890
--- /dev/null
+++ b/components/script/dom/webxr/mod.rs
@@ -0,0 +1,44 @@
+/* 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/. */
+
+pub mod fakexrdevice;
+pub mod fakexrinputcontroller;
+pub mod xrboundedreferencespace;
+pub mod xrcompositionlayer;
+pub mod xrcubelayer;
+pub mod xrcylinderlayer;
+pub mod xrequirectlayer;
+pub mod xrframe;
+pub mod xrhand;
+pub mod xrhittestresult;
+pub mod xrhittestsource;
+pub mod xrinputsource;
+pub mod xrinputsourcearray;
+pub mod xrinputsourceevent;
+pub mod xrinputsourceschangeevent;
+pub mod xrjointpose;
+pub mod xrjointspace;
+pub mod xrlayer;
+pub mod xrlayerevent;
+pub mod xrmediabinding;
+pub mod xrpose;
+pub mod xrprojectionlayer;
+pub mod xrquadlayer;
+pub mod xrray;
+pub mod xrreferencespace;
+pub mod xrreferencespaceevent;
+pub mod xrrenderstate;
+pub mod xrrigidtransform;
+pub mod xrsession;
+pub mod xrsessionevent;
+pub mod xrspace;
+pub mod xrsubimage;
+pub mod xrsystem;
+pub mod xrtest;
+pub mod xrview;
+pub mod xrviewerpose;
+pub mod xrviewport;
+pub mod xrwebglbinding;
+pub mod xrwebgllayer;
+pub mod xrwebglsubimage;
diff --git a/components/script/dom/webxr/xrboundedreferencespace.rs b/components/script/dom/webxr/xrboundedreferencespace.rs
new file mode 100644
index 00000000000..1434e23e6e1
--- /dev/null
+++ b/components/script/dom/webxr/xrboundedreferencespace.rs
@@ -0,0 +1,91 @@
+/* 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 dom_struct::dom_struct;
+use js::rust::MutableHandleValue;
+
+use crate::dom::bindings::codegen::Bindings::XRBoundedReferenceSpaceBinding::XRBoundedReferenceSpaceMethods;
+use crate::dom::bindings::codegen::Bindings::XRReferenceSpaceBinding::XRReferenceSpaceType;
+use crate::dom::bindings::reflector::{reflect_dom_object, DomObject};
+use crate::dom::bindings::root::{Dom, DomRoot};
+use crate::dom::bindings::utils::to_frozen_array;
+use crate::dom::dompointreadonly::DOMPointReadOnly;
+use crate::dom::globalscope::GlobalScope;
+use crate::dom::xrreferencespace::XRReferenceSpace;
+use crate::dom::xrrigidtransform::XRRigidTransform;
+use crate::dom::xrsession::XRSession;
+use crate::script_runtime::{CanGc, JSContext};
+
+#[dom_struct]
+pub struct XRBoundedReferenceSpace {
+ reference_space: XRReferenceSpace,
+ offset: Dom<XRRigidTransform>,
+}
+
+impl XRBoundedReferenceSpace {
+ pub fn new_inherited(
+ session: &XRSession,
+ offset: &XRRigidTransform,
+ ) -> XRBoundedReferenceSpace {
+ XRBoundedReferenceSpace {
+ reference_space: XRReferenceSpace::new_inherited(
+ session,
+ offset,
+ XRReferenceSpaceType::Bounded_floor,
+ ),
+ offset: Dom::from_ref(offset),
+ }
+ }
+
+ #[allow(unused)]
+ pub fn new(
+ global: &GlobalScope,
+ session: &XRSession,
+ can_gc: CanGc,
+ ) -> DomRoot<XRBoundedReferenceSpace> {
+ let offset = XRRigidTransform::identity(global, can_gc);
+ Self::new_offset(global, session, &offset)
+ }
+
+ #[allow(unused)]
+ pub fn new_offset(
+ global: &GlobalScope,
+ session: &XRSession,
+ offset: &XRRigidTransform,
+ ) -> DomRoot<XRBoundedReferenceSpace> {
+ reflect_dom_object(
+ Box::new(XRBoundedReferenceSpace::new_inherited(session, offset)),
+ global,
+ )
+ }
+
+ pub fn reference_space(&self) -> &XRReferenceSpace {
+ &self.reference_space
+ }
+}
+
+impl XRBoundedReferenceSpaceMethods<crate::DomTypeHolder> for XRBoundedReferenceSpace {
+ /// <https://www.w3.org/TR/webxr/#dom-xrboundedreferencespace-boundsgeometry>
+ fn BoundsGeometry(&self, cx: JSContext, can_gc: CanGc, retval: MutableHandleValue) {
+ if let Some(bounds) = self.reference_space.get_bounds() {
+ let points: Vec<DomRoot<DOMPointReadOnly>> = bounds
+ .into_iter()
+ .map(|point| {
+ DOMPointReadOnly::new(
+ &self.global(),
+ point.x.into(),
+ 0.0,
+ point.y.into(),
+ 1.0,
+ can_gc,
+ )
+ })
+ .collect();
+
+ to_frozen_array(&points, cx, retval)
+ } else {
+ to_frozen_array::<DomRoot<DOMPointReadOnly>>(&[], cx, retval)
+ }
+ }
+}
diff --git a/components/script/dom/webxr/xrcompositionlayer.rs b/components/script/dom/webxr/xrcompositionlayer.rs
new file mode 100644
index 00000000000..fbb71149716
--- /dev/null
+++ b/components/script/dom/webxr/xrcompositionlayer.rs
@@ -0,0 +1,12 @@
+/* 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 dom_struct::dom_struct;
+
+use crate::dom::xrlayer::XRLayer;
+
+#[dom_struct]
+pub struct XRCompositionLayer {
+ xr_layer: XRLayer,
+}
diff --git a/components/script/dom/webxr/xrcubelayer.rs b/components/script/dom/webxr/xrcubelayer.rs
new file mode 100644
index 00000000000..587d088e88b
--- /dev/null
+++ b/components/script/dom/webxr/xrcubelayer.rs
@@ -0,0 +1,12 @@
+/* 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 dom_struct::dom_struct;
+
+use crate::dom::xrcompositionlayer::XRCompositionLayer;
+
+#[dom_struct]
+pub struct XRCubeLayer {
+ composition_layer: XRCompositionLayer,
+}
diff --git a/components/script/dom/webxr/xrcylinderlayer.rs b/components/script/dom/webxr/xrcylinderlayer.rs
new file mode 100644
index 00000000000..f468380bb36
--- /dev/null
+++ b/components/script/dom/webxr/xrcylinderlayer.rs
@@ -0,0 +1,12 @@
+/* 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 dom_struct::dom_struct;
+
+use crate::dom::xrcompositionlayer::XRCompositionLayer;
+
+#[dom_struct]
+pub struct XRCylinderLayer {
+ composition_layer: XRCompositionLayer,
+}
diff --git a/components/script/dom/webxr/xrequirectlayer.rs b/components/script/dom/webxr/xrequirectlayer.rs
new file mode 100644
index 00000000000..25cc04595ef
--- /dev/null
+++ b/components/script/dom/webxr/xrequirectlayer.rs
@@ -0,0 +1,12 @@
+/* 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 dom_struct::dom_struct;
+
+use crate::dom::xrcompositionlayer::XRCompositionLayer;
+
+#[dom_struct]
+pub struct XREquirectLayer {
+ composition_layer: XRCompositionLayer,
+}
diff --git a/components/script/dom/webxr/xrframe.rs b/components/script/dom/webxr/xrframe.rs
new file mode 100644
index 00000000000..f3a8a263fd3
--- /dev/null
+++ b/components/script/dom/webxr/xrframe.rs
@@ -0,0 +1,294 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+use std::cell::Cell;
+
+use dom_struct::dom_struct;
+use js::gc::CustomAutoRooterGuard;
+use js::typedarray::Float32Array;
+use webxr_api::{Frame, LayerId, SubImages};
+
+use crate::dom::bindings::codegen::Bindings::XRFrameBinding::XRFrameMethods;
+use crate::dom::bindings::error::Error;
+use crate::dom::bindings::inheritance::Castable;
+use crate::dom::bindings::num::Finite;
+use crate::dom::bindings::reflector::{reflect_dom_object, DomObject, Reflector};
+use crate::dom::bindings::root::{Dom, DomRoot};
+use crate::dom::globalscope::GlobalScope;
+use crate::dom::xrhittestresult::XRHitTestResult;
+use crate::dom::xrhittestsource::XRHitTestSource;
+use crate::dom::xrjointpose::XRJointPose;
+use crate::dom::xrjointspace::XRJointSpace;
+use crate::dom::xrpose::XRPose;
+use crate::dom::xrreferencespace::XRReferenceSpace;
+use crate::dom::xrsession::{ApiPose, XRSession};
+use crate::dom::xrspace::XRSpace;
+use crate::dom::xrviewerpose::XRViewerPose;
+use crate::script_runtime::CanGc;
+
+#[dom_struct]
+pub struct XRFrame {
+ reflector_: Reflector,
+ session: Dom<XRSession>,
+ #[ignore_malloc_size_of = "defined in webxr_api"]
+ #[no_trace]
+ data: Frame,
+ active: Cell<bool>,
+ animation_frame: Cell<bool>,
+}
+
+impl XRFrame {
+ fn new_inherited(session: &XRSession, data: Frame) -> XRFrame {
+ XRFrame {
+ reflector_: Reflector::new(),
+ session: Dom::from_ref(session),
+ data,
+ active: Cell::new(false),
+ animation_frame: Cell::new(false),
+ }
+ }
+
+ pub fn new(global: &GlobalScope, session: &XRSession, data: Frame) -> DomRoot<XRFrame> {
+ reflect_dom_object(Box::new(XRFrame::new_inherited(session, data)), global)
+ }
+
+ /// <https://immersive-web.github.io/webxr/#xrframe-active>
+ pub fn set_active(&self, active: bool) {
+ self.active.set(active);
+ }
+
+ /// <https://immersive-web.github.io/webxr/#xrframe-animationframe>
+ pub fn set_animation_frame(&self, animation_frame: bool) {
+ self.animation_frame.set(animation_frame);
+ }
+
+ pub fn get_pose(&self, space: &XRSpace) -> Option<ApiPose> {
+ space.get_pose(&self.data)
+ }
+
+ pub fn get_sub_images(&self, layer_id: LayerId) -> Option<&SubImages> {
+ self.data
+ .sub_images
+ .iter()
+ .find(|sub_images| sub_images.layer_id == layer_id)
+ }
+}
+
+impl XRFrameMethods<crate::DomTypeHolder> for XRFrame {
+ /// <https://immersive-web.github.io/webxr/#dom-xrframe-session>
+ fn Session(&self) -> DomRoot<XRSession> {
+ DomRoot::from_ref(&self.session)
+ }
+
+ /// <https://www.w3.org/TR/webxr/#dom-xrframe-predicteddisplaytime>
+ fn PredictedDisplayTime(&self) -> Finite<f64> {
+ // TODO: If inline, return the same value
+ // as the timestamp passed to XRFrameRequestCallback
+ Finite::new(self.data.predicted_display_time)
+ .expect("Failed to create predictedDisplayTime")
+ }
+
+ /// <https://immersive-web.github.io/webxr/#dom-xrframe-getviewerpose>
+ fn GetViewerPose(
+ &self,
+ reference: &XRReferenceSpace,
+ can_gc: CanGc,
+ ) -> Result<Option<DomRoot<XRViewerPose>>, Error> {
+ if self.session != reference.upcast::<XRSpace>().session() {
+ return Err(Error::InvalidState);
+ }
+
+ if !self.active.get() || !self.animation_frame.get() {
+ return Err(Error::InvalidState);
+ }
+
+ let to_base = if let Some(to_base) = reference.get_base_transform(&self.data) {
+ to_base
+ } else {
+ return Ok(None);
+ };
+ let viewer_pose = if let Some(pose) = self.data.pose.as_ref() {
+ pose
+ } else {
+ return Ok(None);
+ };
+ Ok(Some(XRViewerPose::new(
+ &self.global(),
+ &self.session,
+ to_base,
+ viewer_pose,
+ can_gc,
+ )))
+ }
+
+ /// <https://immersive-web.github.io/webxr/#dom-xrframe-getpose>
+ fn GetPose(
+ &self,
+ space: &XRSpace,
+ base_space: &XRSpace,
+ can_gc: CanGc,
+ ) -> Result<Option<DomRoot<XRPose>>, Error> {
+ if self.session != space.session() || self.session != base_space.session() {
+ return Err(Error::InvalidState);
+ }
+ if !self.active.get() {
+ return Err(Error::InvalidState);
+ }
+ let space = if let Some(space) = self.get_pose(space) {
+ space
+ } else {
+ return Ok(None);
+ };
+ let base_space = if let Some(r) = self.get_pose(base_space) {
+ r
+ } else {
+ return Ok(None);
+ };
+ let pose = space.then(&base_space.inverse());
+ Ok(Some(XRPose::new(&self.global(), pose, can_gc)))
+ }
+
+ /// <https://immersive-web.github.io/webxr/#dom-xrframe-getpose>
+ fn GetJointPose(
+ &self,
+ space: &XRJointSpace,
+ base_space: &XRSpace,
+ can_gc: CanGc,
+ ) -> Result<Option<DomRoot<XRJointPose>>, Error> {
+ if self.session != space.upcast::<XRSpace>().session() ||
+ self.session != base_space.session()
+ {
+ return Err(Error::InvalidState);
+ }
+ if !self.active.get() {
+ return Err(Error::InvalidState);
+ }
+ let joint_frame = if let Some(frame) = space.frame(&self.data) {
+ frame
+ } else {
+ return Ok(None);
+ };
+ let base_space = if let Some(r) = self.get_pose(base_space) {
+ r
+ } else {
+ return Ok(None);
+ };
+ let pose = joint_frame.pose.then(&base_space.inverse());
+ Ok(Some(XRJointPose::new(
+ &self.global(),
+ pose.cast_unit(),
+ Some(joint_frame.radius),
+ can_gc,
+ )))
+ }
+
+ /// <https://immersive-web.github.io/hit-test/#dom-xrframe-gethittestresults>
+ fn GetHitTestResults(&self, source: &XRHitTestSource) -> Vec<DomRoot<XRHitTestResult>> {
+ self.data
+ .hit_test_results
+ .iter()
+ .filter(|r| r.id == source.id())
+ .map(|r| XRHitTestResult::new(&self.global(), *r, self))
+ .collect()
+ }
+
+ #[allow(unsafe_code)]
+ /// <https://www.w3.org/TR/webxr-hand-input-1/#dom-xrframe-filljointradii>
+ fn FillJointRadii(
+ &self,
+ joint_spaces: Vec<DomRoot<XRJointSpace>>,
+ mut radii: CustomAutoRooterGuard<Float32Array>,
+ ) -> Result<bool, Error> {
+ if !self.active.get() {
+ return Err(Error::InvalidState);
+ }
+
+ for joint_space in &joint_spaces {
+ if self.session != joint_space.upcast::<XRSpace>().session() {
+ return Err(Error::InvalidState);
+ }
+ }
+
+ if joint_spaces.len() > radii.len() {
+ return Err(Error::Type(
+ "Length of radii does not match length of joint spaces".to_string(),
+ ));
+ }
+
+ let mut radii_vec = radii.to_vec();
+ let mut all_valid = true;
+ radii_vec.iter_mut().enumerate().for_each(|(i, radius)| {
+ if let Some(joint_frame) = joint_spaces
+ .get(i)
+ .and_then(|joint_space| joint_space.frame(&self.data))
+ {
+ *radius = joint_frame.radius;
+ } else {
+ all_valid = false;
+ }
+ });
+
+ if !all_valid {
+ radii_vec.fill(f32::NAN);
+ }
+
+ radii.update(&radii_vec);
+
+ Ok(all_valid)
+ }
+
+ #[allow(unsafe_code)]
+ /// <https://www.w3.org/TR/webxr-hand-input-1/#dom-xrframe-fillposes>
+ fn FillPoses(
+ &self,
+ spaces: Vec<DomRoot<XRSpace>>,
+ base_space: &XRSpace,
+ mut transforms: CustomAutoRooterGuard<Float32Array>,
+ ) -> Result<bool, Error> {
+ if !self.active.get() {
+ return Err(Error::InvalidState);
+ }
+
+ for space in &spaces {
+ if self.session != space.session() {
+ return Err(Error::InvalidState);
+ }
+ }
+
+ if self.session != base_space.session() {
+ return Err(Error::InvalidState);
+ }
+
+ if spaces.len() * 16 > transforms.len() {
+ return Err(Error::Type(
+ "Transforms array length does not match 16 * spaces length".to_string(),
+ ));
+ }
+
+ let mut transforms_vec = transforms.to_vec();
+ let mut all_valid = true;
+ spaces.iter().enumerate().for_each(|(i, space)| {
+ let Some(joint_pose) = self.get_pose(space) else {
+ all_valid = false;
+ return;
+ };
+ let Some(base_pose) = self.get_pose(base_space) else {
+ all_valid = false;
+ return;
+ };
+ let pose = joint_pose.then(&base_pose.inverse());
+ let elements = pose.to_transform();
+ let elements_arr = elements.to_array();
+ transforms_vec[i * 16..(i + 1) * 16].copy_from_slice(&elements_arr);
+ });
+
+ if !all_valid {
+ transforms_vec.fill(f32::NAN);
+ }
+
+ transforms.update(&transforms_vec);
+
+ Ok(all_valid)
+ }
+}
diff --git a/components/script/dom/webxr/xrhand.rs b/components/script/dom/webxr/xrhand.rs
new file mode 100644
index 00000000000..b73e9cddb35
--- /dev/null
+++ b/components/script/dom/webxr/xrhand.rs
@@ -0,0 +1,176 @@
+/* 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 dom_struct::dom_struct;
+use webxr_api::{FingerJoint, Hand, Joint};
+
+use crate::dom::bindings::codegen::Bindings::XRHandBinding::{XRHandJoint, XRHandMethods};
+use crate::dom::bindings::iterable::Iterable;
+use crate::dom::bindings::reflector::{reflect_dom_object, Reflector};
+use crate::dom::bindings::root::{Dom, DomRoot};
+use crate::dom::globalscope::GlobalScope;
+use crate::dom::xrinputsource::XRInputSource;
+use crate::dom::xrjointspace::XRJointSpace;
+
+const JOINT_SPACE_MAP: [(XRHandJoint, Joint); 25] = [
+ (XRHandJoint::Wrist, Joint::Wrist),
+ (XRHandJoint::Thumb_metacarpal, Joint::ThumbMetacarpal),
+ (
+ XRHandJoint::Thumb_phalanx_proximal,
+ Joint::ThumbPhalanxProximal,
+ ),
+ (XRHandJoint::Thumb_phalanx_distal, Joint::ThumbPhalanxDistal),
+ (XRHandJoint::Thumb_tip, Joint::ThumbPhalanxTip),
+ (
+ XRHandJoint::Index_finger_metacarpal,
+ Joint::Index(FingerJoint::Metacarpal),
+ ),
+ (
+ XRHandJoint::Index_finger_phalanx_proximal,
+ Joint::Index(FingerJoint::PhalanxProximal),
+ ),
+ (XRHandJoint::Index_finger_phalanx_intermediate, {
+ Joint::Index(FingerJoint::PhalanxIntermediate)
+ }),
+ (
+ XRHandJoint::Index_finger_phalanx_distal,
+ Joint::Index(FingerJoint::PhalanxDistal),
+ ),
+ (
+ XRHandJoint::Index_finger_tip,
+ Joint::Index(FingerJoint::PhalanxTip),
+ ),
+ (
+ XRHandJoint::Middle_finger_metacarpal,
+ Joint::Middle(FingerJoint::Metacarpal),
+ ),
+ (
+ XRHandJoint::Middle_finger_phalanx_proximal,
+ Joint::Middle(FingerJoint::PhalanxProximal),
+ ),
+ (XRHandJoint::Middle_finger_phalanx_intermediate, {
+ Joint::Middle(FingerJoint::PhalanxIntermediate)
+ }),
+ (
+ XRHandJoint::Middle_finger_phalanx_distal,
+ Joint::Middle(FingerJoint::PhalanxDistal),
+ ),
+ (
+ XRHandJoint::Middle_finger_tip,
+ Joint::Middle(FingerJoint::PhalanxTip),
+ ),
+ (
+ XRHandJoint::Ring_finger_metacarpal,
+ Joint::Ring(FingerJoint::Metacarpal),
+ ),
+ (
+ XRHandJoint::Ring_finger_phalanx_proximal,
+ Joint::Ring(FingerJoint::PhalanxProximal),
+ ),
+ (XRHandJoint::Ring_finger_phalanx_intermediate, {
+ Joint::Ring(FingerJoint::PhalanxIntermediate)
+ }),
+ (
+ XRHandJoint::Ring_finger_phalanx_distal,
+ Joint::Ring(FingerJoint::PhalanxDistal),
+ ),
+ (
+ XRHandJoint::Ring_finger_tip,
+ Joint::Ring(FingerJoint::PhalanxTip),
+ ),
+ (
+ XRHandJoint::Pinky_finger_metacarpal,
+ Joint::Little(FingerJoint::Metacarpal),
+ ),
+ (
+ XRHandJoint::Pinky_finger_phalanx_proximal,
+ Joint::Little(FingerJoint::PhalanxProximal),
+ ),
+ (XRHandJoint::Pinky_finger_phalanx_intermediate, {
+ Joint::Little(FingerJoint::PhalanxIntermediate)
+ }),
+ (
+ XRHandJoint::Pinky_finger_phalanx_distal,
+ Joint::Little(FingerJoint::PhalanxDistal),
+ ),
+ (
+ XRHandJoint::Pinky_finger_tip,
+ Joint::Little(FingerJoint::PhalanxTip),
+ ),
+];
+
+#[dom_struct]
+pub struct XRHand {
+ reflector_: Reflector,
+ #[ignore_malloc_size_of = "defined in webxr"]
+ source: Dom<XRInputSource>,
+ #[ignore_malloc_size_of = "partially defind in webxr"]
+ #[custom_trace]
+ spaces: Hand<Dom<XRJointSpace>>,
+}
+
+impl XRHand {
+ fn new_inherited(source: &XRInputSource, spaces: &Hand<DomRoot<XRJointSpace>>) -> XRHand {
+ XRHand {
+ reflector_: Reflector::new(),
+ source: Dom::from_ref(source),
+ spaces: spaces.map(|j, _| j.as_ref().map(|j| Dom::from_ref(&**j))),
+ }
+ }
+
+ pub fn new(global: &GlobalScope, source: &XRInputSource, support: Hand<()>) -> DomRoot<XRHand> {
+ let id = source.id();
+ let session = source.session();
+ let spaces = support.map(|field, joint| {
+ let hand_joint = JOINT_SPACE_MAP
+ .iter()
+ .find(|&&(_, value)| value == joint)
+ .map(|&(hand_joint, _)| hand_joint)
+ .expect("Invalid joint name");
+ field.map(|_| XRJointSpace::new(global, session, id, joint, hand_joint))
+ });
+ reflect_dom_object(Box::new(XRHand::new_inherited(source, &spaces)), global)
+ }
+}
+
+impl XRHandMethods<crate::DomTypeHolder> for XRHand {
+ /// <https://github.com/immersive-web/webxr-hands-input/blob/master/explainer.md>
+ fn Size(&self) -> u32 {
+ XRHandJoint::Pinky_finger_tip as u32 + 1
+ }
+
+ /// <https://github.com/immersive-web/webxr-hands-input/blob/master/explainer.md>
+ fn Get(&self, joint_name: XRHandJoint) -> DomRoot<XRJointSpace> {
+ let joint = JOINT_SPACE_MAP
+ .iter()
+ .find(|&&(key, _)| key == joint_name)
+ .map(|&(_, joint)| joint)
+ .expect("Invalid joint name");
+ self.spaces
+ .get(joint)
+ .map(|j| DomRoot::from_ref(&**j))
+ .expect("Failed to get joint pose")
+ }
+}
+
+impl Iterable for XRHand {
+ type Key = XRHandJoint;
+ type Value = DomRoot<XRJointSpace>;
+
+ fn get_iterable_length(&self) -> u32 {
+ JOINT_SPACE_MAP.len() as u32
+ }
+
+ fn get_value_at_index(&self, n: u32) -> DomRoot<XRJointSpace> {
+ let joint = JOINT_SPACE_MAP[n as usize].1;
+ self.spaces
+ .get(joint)
+ .map(|j| DomRoot::from_ref(&**j))
+ .expect("Failed to get joint pose")
+ }
+
+ fn get_key_at_index(&self, n: u32) -> XRHandJoint {
+ JOINT_SPACE_MAP[n as usize].0
+ }
+}
diff --git a/components/script/dom/webxr/xrhittestresult.rs b/components/script/dom/webxr/xrhittestresult.rs
new file mode 100644
index 00000000000..faca7dad3a3
--- /dev/null
+++ b/components/script/dom/webxr/xrhittestresult.rs
@@ -0,0 +1,54 @@
+/* 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 dom_struct::dom_struct;
+use webxr_api::HitTestResult;
+
+use crate::dom::bindings::codegen::Bindings::XRHitTestResultBinding::XRHitTestResultMethods;
+use crate::dom::bindings::reflector::{reflect_dom_object, DomObject, Reflector};
+use crate::dom::bindings::root::{Dom, DomRoot};
+use crate::dom::globalscope::GlobalScope;
+use crate::dom::xrframe::XRFrame;
+use crate::dom::xrpose::XRPose;
+use crate::dom::xrspace::XRSpace;
+use crate::script_runtime::CanGc;
+
+#[dom_struct]
+pub struct XRHitTestResult {
+ reflector_: Reflector,
+ #[ignore_malloc_size_of = "defined in webxr"]
+ #[no_trace]
+ result: HitTestResult,
+ frame: Dom<XRFrame>,
+}
+
+impl XRHitTestResult {
+ fn new_inherited(result: HitTestResult, frame: &XRFrame) -> XRHitTestResult {
+ XRHitTestResult {
+ reflector_: Reflector::new(),
+ result,
+ frame: Dom::from_ref(frame),
+ }
+ }
+
+ pub fn new(
+ global: &GlobalScope,
+ result: HitTestResult,
+ frame: &XRFrame,
+ ) -> DomRoot<XRHitTestResult> {
+ reflect_dom_object(
+ Box::new(XRHitTestResult::new_inherited(result, frame)),
+ global,
+ )
+ }
+}
+
+impl XRHitTestResultMethods<crate::DomTypeHolder> for XRHitTestResult {
+ // https://immersive-web.github.io/hit-test/#dom-xrhittestresult-getpose
+ fn GetPose(&self, base: &XRSpace, can_gc: CanGc) -> Option<DomRoot<XRPose>> {
+ let base = self.frame.get_pose(base)?;
+ let pose = self.result.space.then(&base.inverse());
+ Some(XRPose::new(&self.global(), pose.cast_unit(), can_gc))
+ }
+}
diff --git a/components/script/dom/webxr/xrhittestsource.rs b/components/script/dom/webxr/xrhittestsource.rs
new file mode 100644
index 00000000000..5210bfa84a9
--- /dev/null
+++ b/components/script/dom/webxr/xrhittestsource.rs
@@ -0,0 +1,53 @@
+/* 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 dom_struct::dom_struct;
+use webxr_api::HitTestId;
+
+use crate::dom::bindings::codegen::Bindings::XRHitTestSourceBinding::XRHitTestSourceMethods;
+use crate::dom::bindings::reflector::{reflect_dom_object, Reflector};
+use crate::dom::bindings::root::{Dom, DomRoot};
+use crate::dom::globalscope::GlobalScope;
+use crate::dom::xrsession::XRSession;
+
+#[dom_struct]
+pub struct XRHitTestSource {
+ reflector_: Reflector,
+ #[ignore_malloc_size_of = "defined in webxr"]
+ #[no_trace]
+ id: HitTestId,
+ session: Dom<XRSession>,
+}
+
+impl XRHitTestSource {
+ fn new_inherited(id: HitTestId, session: &XRSession) -> XRHitTestSource {
+ XRHitTestSource {
+ reflector_: Reflector::new(),
+ id,
+ session: Dom::from_ref(session),
+ }
+ }
+
+ pub fn new(
+ global: &GlobalScope,
+ id: HitTestId,
+ session: &XRSession,
+ ) -> DomRoot<XRHitTestSource> {
+ reflect_dom_object(
+ Box::new(XRHitTestSource::new_inherited(id, session)),
+ global,
+ )
+ }
+
+ pub fn id(&self) -> HitTestId {
+ self.id
+ }
+}
+
+impl XRHitTestSourceMethods<crate::DomTypeHolder> for XRHitTestSource {
+ // https://immersive-web.github.io/hit-test/#dom-xrhittestsource-cancel
+ fn Cancel(&self) {
+ self.session.with_session(|s| s.cancel_hit_test(self.id));
+ }
+}
diff --git a/components/script/dom/webxr/xrinputsource.rs b/components/script/dom/webxr/xrinputsource.rs
new file mode 100644
index 00000000000..c7155d343d2
--- /dev/null
+++ b/components/script/dom/webxr/xrinputsource.rs
@@ -0,0 +1,186 @@
+/* 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 dom_struct::dom_struct;
+use js::conversions::ToJSValConvertible;
+use js::jsapi::Heap;
+use js::jsval::{JSVal, UndefinedValue};
+use js::rust::MutableHandleValue;
+use script_traits::GamepadSupportedHapticEffects;
+use webxr_api::{Handedness, InputFrame, InputId, InputSource, TargetRayMode};
+
+use crate::dom::bindings::codegen::Bindings::XRInputSourceBinding::{
+ XRHandedness, XRInputSourceMethods, XRTargetRayMode,
+};
+use crate::dom::bindings::reflector::{reflect_dom_object, DomObject, Reflector};
+use crate::dom::bindings::root::{Dom, DomRoot, MutNullableDom};
+use crate::dom::gamepad::Gamepad;
+use crate::dom::globalscope::GlobalScope;
+use crate::dom::xrhand::XRHand;
+use crate::dom::xrsession::XRSession;
+use crate::dom::xrspace::XRSpace;
+use crate::realms::enter_realm;
+use crate::script_runtime::{CanGc, JSContext};
+
+#[dom_struct]
+pub struct XRInputSource {
+ reflector: Reflector,
+ session: Dom<XRSession>,
+ #[ignore_malloc_size_of = "Defined in rust-webxr"]
+ #[no_trace]
+ info: InputSource,
+ target_ray_space: MutNullableDom<XRSpace>,
+ grip_space: MutNullableDom<XRSpace>,
+ hand: MutNullableDom<XRHand>,
+ #[ignore_malloc_size_of = "mozjs"]
+ profiles: Heap<JSVal>,
+ gamepad: DomRoot<Gamepad>,
+}
+
+impl XRInputSource {
+ pub fn new_inherited(
+ global: &GlobalScope,
+ session: &XRSession,
+ info: InputSource,
+ can_gc: CanGc,
+ ) -> XRInputSource {
+ // <https://www.w3.org/TR/webxr-gamepads-module-1/#gamepad-differences>
+ let gamepad = Gamepad::new(
+ global,
+ 0,
+ "".into(),
+ "xr-standard".into(),
+ (-1.0, 1.0),
+ (0.0, 1.0),
+ GamepadSupportedHapticEffects {
+ supports_dual_rumble: false,
+ supports_trigger_rumble: false,
+ },
+ true,
+ can_gc,
+ );
+ XRInputSource {
+ reflector: Reflector::new(),
+ session: Dom::from_ref(session),
+ info,
+ target_ray_space: Default::default(),
+ grip_space: Default::default(),
+ hand: Default::default(),
+ profiles: Heap::default(),
+ gamepad,
+ }
+ }
+
+ #[allow(unsafe_code)]
+ pub fn new(
+ global: &GlobalScope,
+ session: &XRSession,
+ info: InputSource,
+ can_gc: CanGc,
+ ) -> DomRoot<XRInputSource> {
+ let source = reflect_dom_object(
+ Box::new(XRInputSource::new_inherited(global, session, info, can_gc)),
+ global,
+ );
+
+ let _ac = enter_realm(global);
+ let cx = GlobalScope::get_cx();
+ unsafe {
+ rooted!(in(*cx) let mut profiles = UndefinedValue());
+ source.info.profiles.to_jsval(*cx, profiles.handle_mut());
+ source.profiles.set(profiles.get());
+ }
+ source
+ }
+
+ pub fn id(&self) -> InputId {
+ self.info.id
+ }
+
+ pub fn session(&self) -> &XRSession {
+ &self.session
+ }
+
+ pub fn update_gamepad_state(&self, frame: InputFrame) {
+ frame
+ .button_values
+ .iter()
+ .enumerate()
+ .for_each(|(i, value)| {
+ self.gamepad.map_and_normalize_buttons(i, *value as f64);
+ });
+ frame.axis_values.iter().enumerate().for_each(|(i, value)| {
+ self.gamepad.map_and_normalize_axes(i, *value as f64);
+ });
+ }
+
+ pub fn gamepad(&self) -> &DomRoot<Gamepad> {
+ &self.gamepad
+ }
+}
+
+impl XRInputSourceMethods<crate::DomTypeHolder> for XRInputSource {
+ /// <https://immersive-web.github.io/webxr/#dom-xrinputsource-handedness>
+ fn Handedness(&self) -> XRHandedness {
+ match self.info.handedness {
+ Handedness::None => XRHandedness::None,
+ Handedness::Left => XRHandedness::Left,
+ Handedness::Right => XRHandedness::Right,
+ }
+ }
+
+ /// <https://immersive-web.github.io/webxr/#dom-xrinputsource-targetraymode>
+ fn TargetRayMode(&self) -> XRTargetRayMode {
+ match self.info.target_ray_mode {
+ TargetRayMode::Gaze => XRTargetRayMode::Gaze,
+ TargetRayMode::TrackedPointer => XRTargetRayMode::Tracked_pointer,
+ TargetRayMode::Screen => XRTargetRayMode::Screen,
+ TargetRayMode::TransientPointer => XRTargetRayMode::Transient_pointer,
+ }
+ }
+
+ /// <https://immersive-web.github.io/webxr/#dom-xrinputsource-targetrayspace>
+ fn TargetRaySpace(&self) -> DomRoot<XRSpace> {
+ self.target_ray_space.or_init(|| {
+ let global = self.global();
+ XRSpace::new_inputspace(&global, &self.session, self, false)
+ })
+ }
+
+ /// <https://immersive-web.github.io/webxr/#dom-xrinputsource-gripspace>
+ fn GetGripSpace(&self) -> Option<DomRoot<XRSpace>> {
+ if self.info.supports_grip {
+ Some(self.grip_space.or_init(|| {
+ let global = self.global();
+ XRSpace::new_inputspace(&global, &self.session, self, true)
+ }))
+ } else {
+ None
+ }
+ }
+ // https://immersive-web.github.io/webxr/#dom-xrinputsource-profiles
+ fn Profiles(&self, _cx: JSContext, mut retval: MutableHandleValue) {
+ retval.set(self.profiles.get())
+ }
+
+ /// <https://www.w3.org/TR/webxr/#dom-xrinputsource-skiprendering>
+ fn SkipRendering(&self) -> bool {
+ // Servo is not currently supported anywhere that would allow for skipped
+ // controller rendering.
+ false
+ }
+
+ /// <https://www.w3.org/TR/webxr-gamepads-module-1/#xrinputsource-interface>
+ fn GetGamepad(&self) -> Option<DomRoot<Gamepad>> {
+ Some(DomRoot::from_ref(&*self.gamepad))
+ }
+
+ // https://github.com/immersive-web/webxr-hands-input/blob/master/explainer.md
+ fn GetHand(&self) -> Option<DomRoot<XRHand>> {
+ self.info.hand_support.as_ref().map(|hand| {
+ self.hand
+ .or_init(|| XRHand::new(&self.global(), self, hand.clone()))
+ })
+ }
+}
diff --git a/components/script/dom/webxr/xrinputsourcearray.rs b/components/script/dom/webxr/xrinputsourcearray.rs
new file mode 100644
index 00000000000..18059db822a
--- /dev/null
+++ b/components/script/dom/webxr/xrinputsourcearray.rs
@@ -0,0 +1,152 @@
+/* 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 dom_struct::dom_struct;
+use webxr_api::{InputId, InputSource};
+
+use crate::dom::bindings::cell::DomRefCell;
+use crate::dom::bindings::codegen::Bindings::XRInputSourceArrayBinding::XRInputSourceArrayMethods;
+use crate::dom::bindings::inheritance::Castable;
+use crate::dom::bindings::reflector::{reflect_dom_object, DomObject, Reflector};
+use crate::dom::bindings::root::{Dom, DomRoot};
+use crate::dom::event::Event;
+use crate::dom::globalscope::GlobalScope;
+use crate::dom::xrinputsource::XRInputSource;
+use crate::dom::xrinputsourceschangeevent::XRInputSourcesChangeEvent;
+use crate::dom::xrsession::XRSession;
+use crate::script_runtime::CanGc;
+
+#[dom_struct]
+pub struct XRInputSourceArray {
+ reflector_: Reflector,
+ input_sources: DomRefCell<Vec<Dom<XRInputSource>>>,
+}
+
+impl XRInputSourceArray {
+ fn new_inherited() -> XRInputSourceArray {
+ XRInputSourceArray {
+ reflector_: Reflector::new(),
+ input_sources: DomRefCell::new(vec![]),
+ }
+ }
+
+ pub fn new(global: &GlobalScope) -> DomRoot<XRInputSourceArray> {
+ reflect_dom_object(Box::new(XRInputSourceArray::new_inherited()), global)
+ }
+
+ pub fn add_input_sources(&self, session: &XRSession, inputs: &[InputSource], can_gc: CanGc) {
+ let global = self.global();
+
+ let mut added = vec![];
+ for info in inputs {
+ // This is quadratic, but won't be a problem for the only case
+ // where we add multiple input sources (the initial input sources case)
+ debug_assert!(
+ !self
+ .input_sources
+ .borrow()
+ .iter()
+ .any(|i| i.id() == info.id),
+ "Should never add a duplicate input id!"
+ );
+ let input = XRInputSource::new(&global, session, info.clone(), can_gc);
+ self.input_sources.borrow_mut().push(Dom::from_ref(&input));
+ added.push(input);
+ }
+
+ let event = XRInputSourcesChangeEvent::new(
+ &global,
+ atom!("inputsourceschange"),
+ false,
+ true,
+ session,
+ &added,
+ &[],
+ can_gc,
+ );
+ event.upcast::<Event>().fire(session.upcast(), can_gc);
+ }
+
+ pub fn remove_input_source(&self, session: &XRSession, id: InputId, can_gc: CanGc) {
+ let global = self.global();
+ let removed = if let Some(i) = self.input_sources.borrow().iter().find(|i| i.id() == id) {
+ i.gamepad().update_connected(false, false, can_gc);
+ [DomRoot::from_ref(&**i)]
+ } else {
+ return;
+ };
+
+ let event = XRInputSourcesChangeEvent::new(
+ &global,
+ atom!("inputsourceschange"),
+ false,
+ true,
+ session,
+ &[],
+ &removed,
+ can_gc,
+ );
+ self.input_sources.borrow_mut().retain(|i| i.id() != id);
+ event.upcast::<Event>().fire(session.upcast(), can_gc);
+ }
+
+ pub fn add_remove_input_source(
+ &self,
+ session: &XRSession,
+ id: InputId,
+ info: InputSource,
+ can_gc: CanGc,
+ ) {
+ let global = self.global();
+ let root;
+ let removed = if let Some(i) = self.input_sources.borrow().iter().find(|i| i.id() == id) {
+ i.gamepad().update_connected(false, false, can_gc);
+ root = [DomRoot::from_ref(&**i)];
+ &root as &[_]
+ } else {
+ warn!("Could not find removed input source with id {:?}", id);
+ &[]
+ };
+ self.input_sources.borrow_mut().retain(|i| i.id() != id);
+ let input = XRInputSource::new(&global, session, info, can_gc);
+ self.input_sources.borrow_mut().push(Dom::from_ref(&input));
+
+ let added = [input];
+
+ let event = XRInputSourcesChangeEvent::new(
+ &global,
+ atom!("inputsourceschange"),
+ false,
+ true,
+ session,
+ &added,
+ removed,
+ can_gc,
+ );
+ event.upcast::<Event>().fire(session.upcast(), can_gc);
+ }
+
+ pub fn find(&self, id: InputId) -> Option<DomRoot<XRInputSource>> {
+ self.input_sources
+ .borrow()
+ .iter()
+ .find(|x| x.id() == id)
+ .map(|x| DomRoot::from_ref(&**x))
+ }
+}
+
+impl XRInputSourceArrayMethods<crate::DomTypeHolder> for XRInputSourceArray {
+ /// <https://immersive-web.github.io/webxr/#dom-xrinputsourcearray-length>
+ fn Length(&self) -> u32 {
+ self.input_sources.borrow().len() as u32
+ }
+
+ /// <https://immersive-web.github.io/webxr/#xrinputsourcearray>
+ fn IndexedGetter(&self, n: u32) -> Option<DomRoot<XRInputSource>> {
+ self.input_sources
+ .borrow()
+ .get(n as usize)
+ .map(|x| DomRoot::from_ref(&**x))
+ }
+}
diff --git a/components/script/dom/webxr/xrinputsourceevent.rs b/components/script/dom/webxr/xrinputsourceevent.rs
new file mode 100644
index 00000000000..cc694f8f99c
--- /dev/null
+++ b/components/script/dom/webxr/xrinputsourceevent.rs
@@ -0,0 +1,116 @@
+/* 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 dom_struct::dom_struct;
+use js::rust::HandleObject;
+use servo_atoms::Atom;
+
+use crate::dom::bindings::codegen::Bindings::EventBinding::Event_Binding::EventMethods;
+use crate::dom::bindings::codegen::Bindings::XRInputSourceEventBinding::{
+ self, XRInputSourceEventMethods,
+};
+use crate::dom::bindings::error::Fallible;
+use crate::dom::bindings::inheritance::Castable;
+use crate::dom::bindings::reflector::{reflect_dom_object_with_proto, DomObject};
+use crate::dom::bindings::root::{Dom, DomRoot};
+use crate::dom::bindings::str::DOMString;
+use crate::dom::event::Event;
+use crate::dom::globalscope::GlobalScope;
+use crate::dom::window::Window;
+use crate::dom::xrframe::XRFrame;
+use crate::dom::xrinputsource::XRInputSource;
+use crate::script_runtime::CanGc;
+
+#[dom_struct]
+pub struct XRInputSourceEvent {
+ event: Event,
+ frame: Dom<XRFrame>,
+ source: Dom<XRInputSource>,
+}
+
+impl XRInputSourceEvent {
+ #[allow(crown::unrooted_must_root)]
+ fn new_inherited(frame: &XRFrame, source: &XRInputSource) -> XRInputSourceEvent {
+ XRInputSourceEvent {
+ event: Event::new_inherited(),
+ frame: Dom::from_ref(frame),
+ source: Dom::from_ref(source),
+ }
+ }
+
+ pub fn new(
+ global: &GlobalScope,
+ type_: Atom,
+ bubbles: bool,
+ cancelable: bool,
+ frame: &XRFrame,
+ source: &XRInputSource,
+ can_gc: CanGc,
+ ) -> DomRoot<XRInputSourceEvent> {
+ Self::new_with_proto(
+ global, None, type_, bubbles, cancelable, frame, source, can_gc,
+ )
+ }
+
+ #[allow(clippy::too_many_arguments)]
+ fn new_with_proto(
+ global: &GlobalScope,
+ proto: Option<HandleObject>,
+ type_: Atom,
+ bubbles: bool,
+ cancelable: bool,
+ frame: &XRFrame,
+ source: &XRInputSource,
+ can_gc: CanGc,
+ ) -> DomRoot<XRInputSourceEvent> {
+ let trackevent = reflect_dom_object_with_proto(
+ Box::new(XRInputSourceEvent::new_inherited(frame, source)),
+ global,
+ proto,
+ can_gc,
+ );
+ {
+ let event = trackevent.upcast::<Event>();
+ event.init_event(type_, bubbles, cancelable);
+ }
+ trackevent
+ }
+}
+
+impl XRInputSourceEventMethods<crate::DomTypeHolder> for XRInputSourceEvent {
+ // https://immersive-web.github.io/webxr/#dom-xrinputsourceevent-xrinputsourceevent
+ fn Constructor(
+ window: &Window,
+ proto: Option<HandleObject>,
+ can_gc: CanGc,
+ type_: DOMString,
+ init: &XRInputSourceEventBinding::XRInputSourceEventInit,
+ ) -> Fallible<DomRoot<XRInputSourceEvent>> {
+ Ok(XRInputSourceEvent::new_with_proto(
+ &window.global(),
+ proto,
+ Atom::from(type_),
+ init.parent.bubbles,
+ init.parent.cancelable,
+ &init.frame,
+ &init.inputSource,
+ can_gc,
+ ))
+ }
+
+ // https://immersive-web.github.io/webxr/#dom-xrinputsourceeventinit-frame
+ fn Frame(&self) -> DomRoot<XRFrame> {
+ DomRoot::from_ref(&*self.frame)
+ }
+
+ // https://immersive-web.github.io/webxr/#dom-xrinputsourceeventinit-inputsource
+ fn InputSource(&self) -> DomRoot<XRInputSource> {
+ DomRoot::from_ref(&*self.source)
+ }
+
+ // https://dom.spec.whatwg.org/#dom-event-istrusted
+ fn IsTrusted(&self) -> bool {
+ self.event.IsTrusted()
+ }
+}
diff --git a/components/script/dom/webxr/xrinputsourceschangeevent.rs b/components/script/dom/webxr/xrinputsourceschangeevent.rs
new file mode 100644
index 00000000000..4429f7d545e
--- /dev/null
+++ b/components/script/dom/webxr/xrinputsourceschangeevent.rs
@@ -0,0 +1,140 @@
+/* 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 dom_struct::dom_struct;
+use js::jsapi::Heap;
+use js::jsval::JSVal;
+use js::rust::{HandleObject, MutableHandleValue};
+use servo_atoms::Atom;
+
+use crate::dom::bindings::codegen::Bindings::EventBinding::Event_Binding::EventMethods;
+use crate::dom::bindings::codegen::Bindings::XRInputSourcesChangeEventBinding::{
+ self, XRInputSourcesChangeEventMethods,
+};
+use crate::dom::bindings::inheritance::Castable;
+use crate::dom::bindings::reflector::{reflect_dom_object_with_proto, DomObject};
+use crate::dom::bindings::root::{Dom, DomRoot};
+use crate::dom::bindings::str::DOMString;
+use crate::dom::bindings::utils::to_frozen_array;
+use crate::dom::event::Event;
+use crate::dom::globalscope::GlobalScope;
+use crate::dom::window::Window;
+use crate::dom::xrinputsource::XRInputSource;
+use crate::dom::xrsession::XRSession;
+use crate::realms::enter_realm;
+use crate::script_runtime::{CanGc, JSContext};
+
+#[dom_struct]
+pub struct XRInputSourcesChangeEvent {
+ event: Event,
+ session: Dom<XRSession>,
+ #[ignore_malloc_size_of = "mozjs"]
+ added: Heap<JSVal>,
+ #[ignore_malloc_size_of = "mozjs"]
+ removed: Heap<JSVal>,
+}
+
+impl XRInputSourcesChangeEvent {
+ #[allow(crown::unrooted_must_root)]
+ fn new_inherited(session: &XRSession) -> XRInputSourcesChangeEvent {
+ XRInputSourcesChangeEvent {
+ event: Event::new_inherited(),
+ session: Dom::from_ref(session),
+ added: Heap::default(),
+ removed: Heap::default(),
+ }
+ }
+
+ #[allow(clippy::too_many_arguments)]
+ pub fn new(
+ global: &GlobalScope,
+ type_: Atom,
+ bubbles: bool,
+ cancelable: bool,
+ session: &XRSession,
+ added: &[DomRoot<XRInputSource>],
+ removed: &[DomRoot<XRInputSource>],
+ can_gc: CanGc,
+ ) -> DomRoot<XRInputSourcesChangeEvent> {
+ Self::new_with_proto(
+ global, None, type_, bubbles, cancelable, session, added, removed, can_gc,
+ )
+ }
+
+ #[allow(unsafe_code)]
+ #[allow(clippy::too_many_arguments)]
+ fn new_with_proto(
+ global: &GlobalScope,
+ proto: Option<HandleObject>,
+ type_: Atom,
+ bubbles: bool,
+ cancelable: bool,
+ session: &XRSession,
+ added: &[DomRoot<XRInputSource>],
+ removed: &[DomRoot<XRInputSource>],
+ can_gc: CanGc,
+ ) -> DomRoot<XRInputSourcesChangeEvent> {
+ let changeevent = reflect_dom_object_with_proto(
+ Box::new(XRInputSourcesChangeEvent::new_inherited(session)),
+ global,
+ proto,
+ can_gc,
+ );
+ {
+ let event = changeevent.upcast::<Event>();
+ event.init_event(type_, bubbles, cancelable);
+ }
+ let _ac = enter_realm(global);
+ let cx = GlobalScope::get_cx();
+ rooted!(in(*cx) let mut frozen_val: JSVal);
+ to_frozen_array(added, cx, frozen_val.handle_mut());
+ changeevent.added.set(*frozen_val);
+ to_frozen_array(removed, cx, frozen_val.handle_mut());
+ changeevent.removed.set(*frozen_val);
+ changeevent
+ }
+}
+
+impl XRInputSourcesChangeEventMethods<crate::DomTypeHolder> for XRInputSourcesChangeEvent {
+ // https://immersive-web.github.io/webxr/#dom-xrinputsourceschangeevent-xrinputsourceschangeevent
+ fn Constructor(
+ window: &Window,
+ proto: Option<HandleObject>,
+ can_gc: CanGc,
+ type_: DOMString,
+ init: &XRInputSourcesChangeEventBinding::XRInputSourcesChangeEventInit,
+ ) -> DomRoot<XRInputSourcesChangeEvent> {
+ XRInputSourcesChangeEvent::new_with_proto(
+ &window.global(),
+ proto,
+ Atom::from(type_),
+ init.parent.bubbles,
+ init.parent.cancelable,
+ &init.session,
+ &init.added,
+ &init.removed,
+ can_gc,
+ )
+ }
+
+ // https://immersive-web.github.io/webxr/#dom-xrinputsourceschangeevent-session
+ fn Session(&self) -> DomRoot<XRSession> {
+ DomRoot::from_ref(&*self.session)
+ }
+
+ // https://immersive-web.github.io/webxr/#dom-xrinputsourceschangeevent-added
+ fn Added(&self, _cx: JSContext, mut retval: MutableHandleValue) {
+ retval.set(self.added.get())
+ }
+
+ // https://immersive-web.github.io/webxr/#dom-xrinputsourceschangeevent-removed
+ fn Removed(&self, _cx: JSContext, mut retval: MutableHandleValue) {
+ retval.set(self.removed.get())
+ }
+
+ // https://dom.spec.whatwg.org/#dom-event-istrusted
+ fn IsTrusted(&self) -> bool {
+ self.event.IsTrusted()
+ }
+}
diff --git a/components/script/dom/webxr/xrjointpose.rs b/components/script/dom/webxr/xrjointpose.rs
new file mode 100644
index 00000000000..9d1ed301486
--- /dev/null
+++ b/components/script/dom/webxr/xrjointpose.rs
@@ -0,0 +1,51 @@
+/* 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 dom_struct::dom_struct;
+
+use crate::dom::bindings::codegen::Bindings::XRJointPoseBinding::XRJointPoseMethods;
+use crate::dom::bindings::num::Finite;
+use crate::dom::bindings::reflector::reflect_dom_object;
+use crate::dom::bindings::root::DomRoot;
+use crate::dom::globalscope::GlobalScope;
+use crate::dom::xrpose::XRPose;
+use crate::dom::xrrigidtransform::XRRigidTransform;
+use crate::dom::xrsession::ApiRigidTransform;
+use crate::script_runtime::CanGc;
+
+#[dom_struct]
+pub struct XRJointPose {
+ pose: XRPose,
+ radius: Option<f32>,
+}
+
+impl XRJointPose {
+ fn new_inherited(transform: &XRRigidTransform, radius: Option<f32>) -> XRJointPose {
+ XRJointPose {
+ pose: XRPose::new_inherited(transform),
+ radius,
+ }
+ }
+
+ #[allow(unsafe_code)]
+ pub fn new(
+ global: &GlobalScope,
+ pose: ApiRigidTransform,
+ radius: Option<f32>,
+ can_gc: CanGc,
+ ) -> DomRoot<XRJointPose> {
+ let transform = XRRigidTransform::new(global, pose, can_gc);
+ reflect_dom_object(
+ Box::new(XRJointPose::new_inherited(&transform, radius)),
+ global,
+ )
+ }
+}
+
+impl XRJointPoseMethods<crate::DomTypeHolder> for XRJointPose {
+ /// <https://immersive-web.github.io/webxr/#dom-XRJointPose-views>
+ fn GetRadius(&self) -> Option<Finite<f32>> {
+ self.radius.map(Finite::wrap)
+ }
+}
diff --git a/components/script/dom/webxr/xrjointspace.rs b/components/script/dom/webxr/xrjointspace.rs
new file mode 100644
index 00000000000..8d8dc9309f7
--- /dev/null
+++ b/components/script/dom/webxr/xrjointspace.rs
@@ -0,0 +1,83 @@
+/* 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 dom_struct::dom_struct;
+use euclid::RigidTransform3D;
+use webxr_api::{BaseSpace, Frame, InputId, Joint, JointFrame, Space};
+
+use crate::dom::bindings::codegen::Bindings::XRHandBinding::XRHandJoint;
+use crate::dom::bindings::codegen::Bindings::XRJointSpaceBinding::XRJointSpaceMethods;
+use crate::dom::bindings::reflector::reflect_dom_object;
+use crate::dom::bindings::root::DomRoot;
+use crate::dom::globalscope::GlobalScope;
+use crate::dom::xrsession::{ApiPose, XRSession};
+use crate::dom::xrspace::XRSpace;
+
+#[dom_struct]
+pub struct XRJointSpace {
+ xrspace: XRSpace,
+ #[ignore_malloc_size_of = "defined in rust-webxr"]
+ #[no_trace]
+ input: InputId,
+ #[ignore_malloc_size_of = "defined in rust-webxr"]
+ #[no_trace]
+ joint: Joint,
+ hand_joint: XRHandJoint,
+}
+
+impl XRJointSpace {
+ pub fn new_inherited(
+ session: &XRSession,
+ input: InputId,
+ joint: Joint,
+ hand_joint: XRHandJoint,
+ ) -> XRJointSpace {
+ XRJointSpace {
+ xrspace: XRSpace::new_inherited(session),
+ input,
+ joint,
+ hand_joint,
+ }
+ }
+
+ #[allow(unused)]
+ pub fn new(
+ global: &GlobalScope,
+ session: &XRSession,
+ input: InputId,
+ joint: Joint,
+ hand_joint: XRHandJoint,
+ ) -> DomRoot<XRJointSpace> {
+ reflect_dom_object(
+ Box::new(Self::new_inherited(session, input, joint, hand_joint)),
+ global,
+ )
+ }
+
+ pub fn space(&self) -> Space {
+ let base = BaseSpace::Joint(self.input, self.joint);
+ let offset = RigidTransform3D::identity();
+ Space { base, offset }
+ }
+
+ pub fn frame<'a>(&self, frame: &'a Frame) -> Option<&'a JointFrame> {
+ frame
+ .inputs
+ .iter()
+ .find(|i| i.id == self.input)
+ .and_then(|i| i.hand.as_ref())
+ .and_then(|h| h.get(self.joint))
+ }
+
+ pub fn get_pose(&self, frame: &Frame) -> Option<ApiPose> {
+ self.frame(frame).map(|f| f.pose).map(|t| t.cast_unit())
+ }
+}
+
+impl XRJointSpaceMethods<crate::DomTypeHolder> for XRJointSpace {
+ /// <https://www.w3.org/TR/webxr-hand-input-1/#xrjointspace-jointname>
+ fn JointName(&self) -> XRHandJoint {
+ self.hand_joint
+ }
+}
diff --git a/components/script/dom/webxr/xrlayer.rs b/components/script/dom/webxr/xrlayer.rs
new file mode 100644
index 00000000000..3dc3b4dfeea
--- /dev/null
+++ b/components/script/dom/webxr/xrlayer.rs
@@ -0,0 +1,77 @@
+/* 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 canvas_traits::webgl::WebGLContextId;
+use dom_struct::dom_struct;
+use webxr_api::LayerId;
+
+use crate::dom::bindings::inheritance::Castable;
+use crate::dom::bindings::root::Dom;
+use crate::dom::eventtarget::EventTarget;
+use crate::dom::webglrenderingcontext::WebGLRenderingContext;
+use crate::dom::xrframe::XRFrame;
+use crate::dom::xrsession::XRSession;
+use crate::dom::xrwebgllayer::XRWebGLLayer;
+
+#[dom_struct]
+pub struct XRLayer {
+ event_target: EventTarget,
+ session: Dom<XRSession>,
+ context: Dom<WebGLRenderingContext>,
+ /// If none, the session is inline (the composition disabled flag is true)
+ /// and this is a XRWebGLLayer.
+ #[ignore_malloc_size_of = "Layer ids don't heap-allocate"]
+ #[no_trace]
+ layer_id: Option<LayerId>,
+}
+
+impl XRLayer {
+ #[allow(dead_code)]
+ pub fn new_inherited(
+ session: &XRSession,
+ context: &WebGLRenderingContext,
+ layer_id: Option<LayerId>,
+ ) -> XRLayer {
+ XRLayer {
+ event_target: EventTarget::new_inherited(),
+ session: Dom::from_ref(session),
+ context: Dom::from_ref(context),
+ layer_id,
+ }
+ }
+
+ pub(crate) fn layer_id(&self) -> Option<LayerId> {
+ self.layer_id
+ }
+
+ pub(crate) fn context_id(&self) -> WebGLContextId {
+ self.context.context_id()
+ }
+
+ pub(crate) fn context(&self) -> &WebGLRenderingContext {
+ &self.context
+ }
+
+ pub(crate) fn session(&self) -> &XRSession {
+ &self.session
+ }
+
+ pub fn begin_frame(&self, frame: &XRFrame) -> Option<()> {
+ // TODO: Implement this for other layer types
+ if let Some(this) = self.downcast::<XRWebGLLayer>() {
+ this.begin_frame(frame)
+ } else {
+ unimplemented!()
+ }
+ }
+
+ pub fn end_frame(&self, frame: &XRFrame) -> Option<()> {
+ // TODO: Implement this for other layer types
+ if let Some(this) = self.downcast::<XRWebGLLayer>() {
+ this.end_frame(frame)
+ } else {
+ unimplemented!()
+ }
+ }
+}
diff --git a/components/script/dom/webxr/xrlayerevent.rs b/components/script/dom/webxr/xrlayerevent.rs
new file mode 100644
index 00000000000..136aac87450
--- /dev/null
+++ b/components/script/dom/webxr/xrlayerevent.rs
@@ -0,0 +1,77 @@
+/* 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 dom_struct::dom_struct;
+use js::rust::HandleObject;
+use servo_atoms::Atom;
+
+use crate::dom::bindings::codegen::Bindings::EventBinding::Event_Binding::EventMethods;
+use crate::dom::bindings::codegen::Bindings::XRLayerEventBinding::{
+ XRLayerEventInit, XRLayerEventMethods,
+};
+use crate::dom::bindings::reflector::reflect_dom_object_with_proto;
+use crate::dom::bindings::root::{Dom, DomRoot};
+use crate::dom::bindings::str::DOMString;
+use crate::dom::event::Event;
+use crate::dom::window::Window;
+use crate::dom::xrlayer::XRLayer;
+use crate::script_runtime::CanGc;
+
+// https://w3c.github.io/uievents/#interface-uievent
+#[dom_struct]
+pub struct XRLayerEvent {
+ event: Event,
+ layer: Dom<XRLayer>,
+}
+
+impl XRLayerEvent {
+ pub fn new_inherited(layer: &XRLayer) -> XRLayerEvent {
+ XRLayerEvent {
+ event: Event::new_inherited(),
+ layer: Dom::from_ref(layer),
+ }
+ }
+
+ fn new(
+ window: &Window,
+ proto: Option<HandleObject>,
+ layer: &XRLayer,
+ can_gc: CanGc,
+ ) -> DomRoot<XRLayerEvent> {
+ reflect_dom_object_with_proto(
+ Box::new(XRLayerEvent::new_inherited(layer)),
+ window,
+ proto,
+ can_gc,
+ )
+ }
+}
+
+impl XRLayerEventMethods<crate::DomTypeHolder> for XRLayerEvent {
+ // https://immersive-web.github.io/layers/#dom-xrlayerevent-xrlayerevent
+ fn Constructor(
+ window: &Window,
+ proto: Option<HandleObject>,
+ can_gc: CanGc,
+ type_: DOMString,
+ init: &XRLayerEventInit,
+ ) -> DomRoot<XRLayerEvent> {
+ let event = XRLayerEvent::new(window, proto, &init.layer, can_gc);
+ let type_ = Atom::from(type_);
+ let bubbles = init.parent.bubbles;
+ let cancelable = init.parent.cancelable;
+ event.event.init_event(type_, bubbles, cancelable);
+ event
+ }
+
+ // https://immersive-web.github.io/layers/#dom-xrlayerevent-layer
+ fn Layer(&self) -> DomRoot<XRLayer> {
+ DomRoot::from_ref(&self.layer)
+ }
+
+ // https://dom.spec.whatwg.org/#dom-event-istrusted
+ fn IsTrusted(&self) -> bool {
+ self.event.IsTrusted()
+ }
+}
diff --git a/components/script/dom/webxr/xrmediabinding.rs b/components/script/dom/webxr/xrmediabinding.rs
new file mode 100644
index 00000000000..14eec1fb354
--- /dev/null
+++ b/components/script/dom/webxr/xrmediabinding.rs
@@ -0,0 +1,101 @@
+/* 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 dom_struct::dom_struct;
+use js::rust::HandleObject;
+
+use crate::dom::bindings::codegen::Bindings::XRMediaBindingBinding::XRMediaBinding_Binding::XRMediaBindingMethods;
+use crate::dom::bindings::codegen::Bindings::XRMediaBindingBinding::XRMediaLayerInit;
+use crate::dom::bindings::error::{Error, Fallible};
+use crate::dom::bindings::reflector::{reflect_dom_object_with_proto, Reflector};
+use crate::dom::bindings::root::{Dom, DomRoot};
+use crate::dom::htmlvideoelement::HTMLVideoElement;
+use crate::dom::window::Window;
+use crate::dom::xrcylinderlayer::XRCylinderLayer;
+use crate::dom::xrequirectlayer::XREquirectLayer;
+use crate::dom::xrquadlayer::XRQuadLayer;
+use crate::dom::xrsession::XRSession;
+use crate::script_runtime::CanGc;
+
+#[dom_struct]
+pub struct XRMediaBinding {
+ reflector: Reflector,
+ session: Dom<XRSession>,
+}
+
+impl XRMediaBinding {
+ pub fn new_inherited(session: &XRSession) -> XRMediaBinding {
+ XRMediaBinding {
+ reflector: Reflector::new(),
+ session: Dom::from_ref(session),
+ }
+ }
+
+ fn new(
+ global: &Window,
+ proto: Option<HandleObject>,
+ session: &XRSession,
+ can_gc: CanGc,
+ ) -> DomRoot<XRMediaBinding> {
+ reflect_dom_object_with_proto(
+ Box::new(XRMediaBinding::new_inherited(session)),
+ global,
+ proto,
+ can_gc,
+ )
+ }
+}
+
+impl XRMediaBindingMethods<crate::DomTypeHolder> for XRMediaBinding {
+ /// <https://immersive-web.github.io/layers/#dom-xrmediabinding-xrmediabinding>
+ fn Constructor(
+ global: &Window,
+ proto: Option<HandleObject>,
+ can_gc: CanGc,
+ session: &XRSession,
+ ) -> Fallible<DomRoot<XRMediaBinding>> {
+ // Step 1.
+ if session.is_ended() {
+ return Err(Error::InvalidState);
+ }
+
+ // Step 2.
+ if !session.is_immersive() {
+ return Err(Error::InvalidState);
+ }
+
+ // Steps 3-5.
+ Ok(XRMediaBinding::new(global, proto, session, can_gc))
+ }
+
+ /// <https://immersive-web.github.io/layers/#dom-xrmediabinding-createquadlayer>
+ fn CreateQuadLayer(
+ &self,
+ _: &HTMLVideoElement,
+ _: &XRMediaLayerInit,
+ ) -> Fallible<DomRoot<XRQuadLayer>> {
+ // https://github.com/servo/servo/issues/27493
+ Err(Error::NotSupported)
+ }
+
+ /// <https://immersive-web.github.io/layers/#dom-xrmediabinding-createcylinderlayer>
+ fn CreateCylinderLayer(
+ &self,
+ _: &HTMLVideoElement,
+ _: &XRMediaLayerInit,
+ ) -> Fallible<DomRoot<XRCylinderLayer>> {
+ // https://github.com/servo/servo/issues/27493
+ Err(Error::NotSupported)
+ }
+
+ /// <https://immersive-web.github.io/layers/#dom-xrmediabinding-createequirectlayer>
+ fn CreateEquirectLayer(
+ &self,
+ _: &HTMLVideoElement,
+ _: &XRMediaLayerInit,
+ ) -> Fallible<DomRoot<XREquirectLayer>> {
+ // https://github.com/servo/servo/issues/27493
+ Err(Error::NotSupported)
+ }
+}
diff --git a/components/script/dom/webxr/xrpose.rs b/components/script/dom/webxr/xrpose.rs
new file mode 100644
index 00000000000..dea8aa62dc4
--- /dev/null
+++ b/components/script/dom/webxr/xrpose.rs
@@ -0,0 +1,65 @@
+/* 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 dom_struct::dom_struct;
+
+use crate::dom::bindings::codegen::Bindings::XRPoseBinding::XRPoseMethods;
+use crate::dom::bindings::reflector::{reflect_dom_object, Reflector};
+use crate::dom::bindings::root::{Dom, DomRoot};
+use crate::dom::dompointreadonly::DOMPointReadOnly;
+use crate::dom::globalscope::GlobalScope;
+use crate::dom::xrrigidtransform::XRRigidTransform;
+use crate::dom::xrsession::ApiRigidTransform;
+use crate::script_runtime::CanGc;
+
+#[dom_struct]
+pub struct XRPose {
+ reflector_: Reflector,
+ transform: Dom<XRRigidTransform>,
+}
+
+impl XRPose {
+ pub fn new_inherited(transform: &XRRigidTransform) -> XRPose {
+ XRPose {
+ reflector_: Reflector::new(),
+ transform: Dom::from_ref(transform),
+ }
+ }
+
+ #[allow(unused)]
+ pub fn new(
+ global: &GlobalScope,
+ transform: ApiRigidTransform,
+ can_gc: CanGc,
+ ) -> DomRoot<XRPose> {
+ let transform = XRRigidTransform::new(global, transform, can_gc);
+ reflect_dom_object(Box::new(XRPose::new_inherited(&transform)), global)
+ }
+}
+
+impl XRPoseMethods<crate::DomTypeHolder> for XRPose {
+ /// <https://immersive-web.github.io/webxr/#dom-xrpose-transform>
+ fn Transform(&self) -> DomRoot<XRRigidTransform> {
+ DomRoot::from_ref(&self.transform)
+ }
+
+ /// <https://www.w3.org/TR/webxr/#dom-xrpose-linearvelocity>
+ fn GetLinearVelocity(&self) -> Option<DomRoot<DOMPointReadOnly>> {
+ // TODO: Expose from webxr crate
+ None
+ }
+
+ /// <https://www.w3.org/TR/webxr/#dom-xrpose-angularvelocity>
+ fn GetAngularVelocity(&self) -> Option<DomRoot<DOMPointReadOnly>> {
+ // TODO: Expose from webxr crate
+ None
+ }
+
+ /// <https://www.w3.org/TR/webxr/#dom-xrpose-emulatedposition>
+ fn EmulatedPosition(&self) -> bool {
+ // There are currently no instances in which we would need to rely
+ // on emulation for reporting pose, so return false.
+ false
+ }
+}
diff --git a/components/script/dom/webxr/xrprojectionlayer.rs b/components/script/dom/webxr/xrprojectionlayer.rs
new file mode 100644
index 00000000000..489c3a9e706
--- /dev/null
+++ b/components/script/dom/webxr/xrprojectionlayer.rs
@@ -0,0 +1,12 @@
+/* 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 dom_struct::dom_struct;
+
+use crate::dom::xrcompositionlayer::XRCompositionLayer;
+
+#[dom_struct]
+pub struct XRProjectionLayer {
+ composition_layer: XRCompositionLayer,
+}
diff --git a/components/script/dom/webxr/xrquadlayer.rs b/components/script/dom/webxr/xrquadlayer.rs
new file mode 100644
index 00000000000..dd93aea9c0a
--- /dev/null
+++ b/components/script/dom/webxr/xrquadlayer.rs
@@ -0,0 +1,12 @@
+/* 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 dom_struct::dom_struct;
+
+use crate::dom::xrcompositionlayer::XRCompositionLayer;
+
+#[dom_struct]
+pub struct XRQuadLayer {
+ composition_layer: XRCompositionLayer,
+}
diff --git a/components/script/dom/webxr/xrray.rs b/components/script/dom/webxr/xrray.rs
new file mode 100644
index 00000000000..4936edd0acb
--- /dev/null
+++ b/components/script/dom/webxr/xrray.rs
@@ -0,0 +1,174 @@
+/* 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 dom_struct::dom_struct;
+use euclid::{Angle, RigidTransform3D, Rotation3D, Vector3D};
+use js::rust::HandleObject;
+use js::typedarray::{Float32, Float32Array};
+use webxr_api::{ApiSpace, Ray};
+
+use crate::dom::bindings::buffer_source::HeapBufferSource;
+use crate::dom::bindings::codegen::Bindings::DOMPointBinding::DOMPointInit;
+use crate::dom::bindings::codegen::Bindings::XRRayBinding::{XRRayDirectionInit, XRRayMethods};
+use crate::dom::bindings::error::{Error, Fallible};
+use crate::dom::bindings::reflector::{reflect_dom_object_with_proto, DomObject, Reflector};
+use crate::dom::bindings::root::DomRoot;
+use crate::dom::dompointreadonly::DOMPointReadOnly;
+use crate::dom::globalscope::GlobalScope;
+use crate::dom::window::Window;
+use crate::dom::xrrigidtransform::XRRigidTransform;
+use crate::script_runtime::{CanGc, JSContext};
+
+#[dom_struct]
+pub struct XRRay {
+ reflector_: Reflector,
+ #[ignore_malloc_size_of = "defined in webxr"]
+ #[no_trace]
+ ray: Ray<ApiSpace>,
+ #[ignore_malloc_size_of = "defined in mozjs"]
+ matrix: HeapBufferSource<Float32>,
+}
+
+impl XRRay {
+ fn new_inherited(ray: Ray<ApiSpace>) -> XRRay {
+ XRRay {
+ reflector_: Reflector::new(),
+ ray,
+ matrix: HeapBufferSource::default(),
+ }
+ }
+
+ fn new(
+ global: &GlobalScope,
+ proto: Option<HandleObject>,
+ ray: Ray<ApiSpace>,
+ can_gc: CanGc,
+ ) -> DomRoot<XRRay> {
+ reflect_dom_object_with_proto(Box::new(XRRay::new_inherited(ray)), global, proto, can_gc)
+ }
+
+ pub fn ray(&self) -> Ray<ApiSpace> {
+ self.ray
+ }
+}
+
+impl XRRayMethods<crate::DomTypeHolder> for XRRay {
+ /// <https://immersive-web.github.io/hit-test/#dom-xrray-xrray>
+ fn Constructor(
+ window: &Window,
+ proto: Option<HandleObject>,
+ can_gc: CanGc,
+ origin: &DOMPointInit,
+ direction: &XRRayDirectionInit,
+ ) -> Fallible<DomRoot<Self>> {
+ if origin.w != 1.0 {
+ return Err(Error::Type("Origin w coordinate must be 1".into()));
+ }
+ if *direction.w != 0.0 {
+ return Err(Error::Type("Direction w coordinate must be 0".into()));
+ }
+ if *direction.x == 0.0 && *direction.y == 0.0 && *direction.z == 0.0 {
+ return Err(Error::Type(
+ "Direction vector cannot have zero length".into(),
+ ));
+ }
+
+ let origin = Vector3D::new(origin.x as f32, origin.y as f32, origin.z as f32);
+ let direction = Vector3D::new(
+ *direction.x as f32,
+ *direction.y as f32,
+ *direction.z as f32,
+ )
+ .normalize();
+
+ Ok(Self::new(
+ &window.global(),
+ proto,
+ Ray { origin, direction },
+ can_gc,
+ ))
+ }
+
+ /// <https://immersive-web.github.io/hit-test/#dom-xrray-xrray-transform>
+ fn Constructor_(
+ window: &Window,
+ proto: Option<HandleObject>,
+ can_gc: CanGc,
+ transform: &XRRigidTransform,
+ ) -> Fallible<DomRoot<Self>> {
+ let transform = transform.transform();
+ let origin = transform.translation;
+ let direction = transform
+ .rotation
+ .transform_vector3d(Vector3D::new(0., 0., -1.));
+
+ Ok(Self::new(
+ &window.global(),
+ proto,
+ Ray { origin, direction },
+ can_gc,
+ ))
+ }
+
+ /// <https://immersive-web.github.io/hit-test/#dom-xrray-origin>
+ fn Origin(&self, can_gc: CanGc) -> DomRoot<DOMPointReadOnly> {
+ DOMPointReadOnly::new(
+ &self.global(),
+ self.ray.origin.x as f64,
+ self.ray.origin.y as f64,
+ self.ray.origin.z as f64,
+ 1.,
+ can_gc,
+ )
+ }
+
+ /// <https://immersive-web.github.io/hit-test/#dom-xrray-direction>
+ fn Direction(&self, can_gc: CanGc) -> DomRoot<DOMPointReadOnly> {
+ DOMPointReadOnly::new(
+ &self.global(),
+ self.ray.direction.x as f64,
+ self.ray.direction.y as f64,
+ self.ray.direction.z as f64,
+ 0.,
+ can_gc,
+ )
+ }
+
+ /// <https://immersive-web.github.io/hit-test/#dom-xrray-matrix>
+ fn Matrix(&self, _cx: JSContext) -> Float32Array {
+ // https://immersive-web.github.io/hit-test/#xrray-obtain-the-matrix
+ if !self.matrix.is_initialized() {
+ // Step 1
+ let z = Vector3D::new(0., 0., -1.);
+ // Step 2
+ let axis = z.cross(self.ray.direction);
+ // Step 3
+ let cos_angle = z.dot(self.ray.direction);
+ // Step 4
+ let rotation = if cos_angle > -1. && cos_angle < 1. {
+ Rotation3D::around_axis(axis, Angle::radians(cos_angle.acos()))
+ } else if cos_angle == -1. {
+ let axis = Vector3D::new(1., 0., 0.);
+ Rotation3D::around_axis(axis, Angle::radians(cos_angle.acos()))
+ } else {
+ Rotation3D::identity()
+ };
+ // Step 5
+ let translation = self.ray.origin;
+ // Step 6
+ // According to the spec all matrices are column-major,
+ // however euclid uses row vectors so we use .to_array()
+ let arr = RigidTransform3D::new(rotation, translation)
+ .to_transform()
+ .to_array();
+ self.matrix
+ .set_data(_cx, &arr)
+ .expect("Failed to set matrix data on XRRAy.")
+ }
+
+ self.matrix
+ .get_buffer()
+ .expect("Failed to get matrix from XRRay.")
+ }
+}
diff --git a/components/script/dom/webxr/xrreferencespace.rs b/components/script/dom/webxr/xrreferencespace.rs
new file mode 100644
index 00000000000..6f8c409edbc
--- /dev/null
+++ b/components/script/dom/webxr/xrreferencespace.rs
@@ -0,0 +1,153 @@
+/* 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 dom_struct::dom_struct;
+use euclid::{Point2D, RigidTransform3D};
+use webxr_api::{self, Floor, Frame, Space};
+
+use crate::dom::bindings::codegen::Bindings::XRReferenceSpaceBinding::{
+ XRReferenceSpaceMethods, XRReferenceSpaceType,
+};
+use crate::dom::bindings::inheritance::Castable;
+use crate::dom::bindings::reflector::{reflect_dom_object, DomObject};
+use crate::dom::bindings::root::{Dom, DomRoot};
+use crate::dom::globalscope::GlobalScope;
+use crate::dom::xrrigidtransform::XRRigidTransform;
+use crate::dom::xrsession::{cast_transform, ApiPose, BaseTransform, XRSession};
+use crate::dom::xrspace::XRSpace;
+use crate::script_runtime::CanGc;
+
+#[dom_struct]
+pub struct XRReferenceSpace {
+ xrspace: XRSpace,
+ offset: Dom<XRRigidTransform>,
+ ty: XRReferenceSpaceType,
+}
+
+impl XRReferenceSpace {
+ pub fn new_inherited(
+ session: &XRSession,
+ offset: &XRRigidTransform,
+ ty: XRReferenceSpaceType,
+ ) -> XRReferenceSpace {
+ XRReferenceSpace {
+ xrspace: XRSpace::new_inherited(session),
+ offset: Dom::from_ref(offset),
+ ty,
+ }
+ }
+
+ #[allow(unused)]
+ pub fn new(
+ global: &GlobalScope,
+ session: &XRSession,
+ ty: XRReferenceSpaceType,
+ can_gc: CanGc,
+ ) -> DomRoot<XRReferenceSpace> {
+ let offset = XRRigidTransform::identity(global, can_gc);
+ Self::new_offset(global, session, ty, &offset)
+ }
+
+ #[allow(unused)]
+ pub fn new_offset(
+ global: &GlobalScope,
+ session: &XRSession,
+ ty: XRReferenceSpaceType,
+ offset: &XRRigidTransform,
+ ) -> DomRoot<XRReferenceSpace> {
+ reflect_dom_object(
+ Box::new(XRReferenceSpace::new_inherited(session, offset, ty)),
+ global,
+ )
+ }
+
+ pub fn space(&self) -> Space {
+ let base = match self.ty {
+ XRReferenceSpaceType::Local => webxr_api::BaseSpace::Local,
+ XRReferenceSpaceType::Viewer => webxr_api::BaseSpace::Viewer,
+ XRReferenceSpaceType::Local_floor => webxr_api::BaseSpace::Floor,
+ XRReferenceSpaceType::Bounded_floor => webxr_api::BaseSpace::BoundedFloor,
+ _ => panic!("unsupported reference space found"),
+ };
+ let offset = self.offset.transform();
+ Space { base, offset }
+ }
+
+ pub fn ty(&self) -> XRReferenceSpaceType {
+ self.ty
+ }
+}
+
+impl XRReferenceSpaceMethods<crate::DomTypeHolder> for XRReferenceSpace {
+ /// <https://immersive-web.github.io/webxr/#dom-xrreferencespace-getoffsetreferencespace>
+ fn GetOffsetReferenceSpace(&self, new: &XRRigidTransform, can_gc: CanGc) -> DomRoot<Self> {
+ let offset = new.transform().then(&self.offset.transform());
+ let offset = XRRigidTransform::new(&self.global(), offset, can_gc);
+ Self::new_offset(
+ &self.global(),
+ self.upcast::<XRSpace>().session(),
+ self.ty,
+ &offset,
+ )
+ }
+
+ // https://www.w3.org/TR/webxr/#dom-xrreferencespace-onreset
+ event_handler!(reset, GetOnreset, SetOnreset);
+}
+
+impl XRReferenceSpace {
+ /// Get a transform that can be used to locate the base space
+ ///
+ /// This is equivalent to `get_pose(self).inverse()` (in column vector notation),
+ /// but with better types
+ pub fn get_base_transform(&self, base_pose: &Frame) -> Option<BaseTransform> {
+ let pose = self.get_pose(base_pose)?;
+ Some(pose.inverse().cast_unit())
+ }
+
+ /// Gets pose represented by this space
+ ///
+ /// The reference origin used is common between all
+ /// get_pose calls for spaces from the same device, so this can be used to compare
+ /// with other spaces
+ pub fn get_pose(&self, base_pose: &Frame) -> Option<ApiPose> {
+ let pose = self.get_unoffset_pose(base_pose)?;
+ let offset = self.offset.transform();
+ // pose is a transform from the unoffset space to native space,
+ // offset is a transform from offset space to unoffset space,
+ // we want a transform from unoffset space to native space,
+ // which is pose * offset in column vector notation
+ Some(offset.then(&pose))
+ }
+
+ /// Gets pose represented by this space
+ ///
+ /// Does not apply originOffset, use get_viewer_pose instead if you need it
+ pub fn get_unoffset_pose(&self, base_pose: &Frame) -> Option<ApiPose> {
+ match self.ty {
+ XRReferenceSpaceType::Local => {
+ // The eye-level pose is basically whatever the headset pose was at t=0, which
+ // for most devices is (0, 0, 0)
+ Some(RigidTransform3D::identity())
+ },
+ XRReferenceSpaceType::Local_floor | XRReferenceSpaceType::Bounded_floor => {
+ let native_to_floor = self
+ .upcast::<XRSpace>()
+ .session()
+ .with_session(|s| s.floor_transform())?;
+ Some(cast_transform(native_to_floor.inverse()))
+ },
+ XRReferenceSpaceType::Viewer => {
+ Some(cast_transform(base_pose.pose.as_ref()?.transform))
+ },
+ _ => unimplemented!(),
+ }
+ }
+
+ pub fn get_bounds(&self) -> Option<Vec<Point2D<f32, Floor>>> {
+ self.upcast::<XRSpace>()
+ .session()
+ .with_session(|s| s.reference_space_bounds())
+ }
+}
diff --git a/components/script/dom/webxr/xrreferencespaceevent.rs b/components/script/dom/webxr/xrreferencespaceevent.rs
new file mode 100644
index 00000000000..e75aabe14a1
--- /dev/null
+++ b/components/script/dom/webxr/xrreferencespaceevent.rs
@@ -0,0 +1,121 @@
+/* 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 dom_struct::dom_struct;
+use js::rust::HandleObject;
+use servo_atoms::Atom;
+
+use crate::dom::bindings::codegen::Bindings::EventBinding::Event_Binding::EventMethods;
+use crate::dom::bindings::codegen::Bindings::XRReferenceSpaceEventBinding::{
+ XRReferenceSpaceEventInit, XRReferenceSpaceEventMethods,
+};
+use crate::dom::bindings::error::Fallible;
+use crate::dom::bindings::inheritance::Castable;
+use crate::dom::bindings::reflector::{reflect_dom_object_with_proto, DomObject};
+use crate::dom::bindings::root::{Dom, DomRoot};
+use crate::dom::bindings::str::DOMString;
+use crate::dom::event::Event;
+use crate::dom::globalscope::GlobalScope;
+use crate::dom::window::Window;
+use crate::dom::xrreferencespace::XRReferenceSpace;
+use crate::dom::xrrigidtransform::XRRigidTransform;
+use crate::script_runtime::CanGc;
+
+#[dom_struct]
+pub struct XRReferenceSpaceEvent {
+ event: Event,
+ space: Dom<XRReferenceSpace>,
+ transform: Option<Dom<XRRigidTransform>>,
+}
+
+impl XRReferenceSpaceEvent {
+ #[allow(crown::unrooted_must_root)]
+ fn new_inherited(
+ space: &XRReferenceSpace,
+ transform: Option<&XRRigidTransform>,
+ ) -> XRReferenceSpaceEvent {
+ XRReferenceSpaceEvent {
+ event: Event::new_inherited(),
+ space: Dom::from_ref(space),
+ transform: transform.map(Dom::from_ref),
+ }
+ }
+
+ pub fn new(
+ global: &GlobalScope,
+ type_: Atom,
+ bubbles: bool,
+ cancelable: bool,
+ space: &XRReferenceSpace,
+ transform: Option<&XRRigidTransform>,
+ can_gc: CanGc,
+ ) -> DomRoot<XRReferenceSpaceEvent> {
+ Self::new_with_proto(
+ global, None, type_, bubbles, cancelable, space, transform, can_gc,
+ )
+ }
+
+ #[allow(clippy::too_many_arguments)]
+ fn new_with_proto(
+ global: &GlobalScope,
+ proto: Option<HandleObject>,
+ type_: Atom,
+ bubbles: bool,
+ cancelable: bool,
+ space: &XRReferenceSpace,
+ transform: Option<&XRRigidTransform>,
+ can_gc: CanGc,
+ ) -> DomRoot<XRReferenceSpaceEvent> {
+ let trackevent = reflect_dom_object_with_proto(
+ Box::new(XRReferenceSpaceEvent::new_inherited(space, transform)),
+ global,
+ proto,
+ can_gc,
+ );
+ {
+ let event = trackevent.upcast::<Event>();
+ event.init_event(type_, bubbles, cancelable);
+ }
+ trackevent
+ }
+}
+
+impl XRReferenceSpaceEventMethods<crate::DomTypeHolder> for XRReferenceSpaceEvent {
+ /// <https://www.w3.org/TR/webxr/#dom-xrreferencespaceevent-xrreferencespaceevent>
+ fn Constructor(
+ window: &Window,
+ proto: Option<HandleObject>,
+ can_gc: CanGc,
+ type_: DOMString,
+ init: &XRReferenceSpaceEventInit,
+ ) -> Fallible<DomRoot<XRReferenceSpaceEvent>> {
+ Ok(XRReferenceSpaceEvent::new_with_proto(
+ &window.global(),
+ proto,
+ Atom::from(type_),
+ init.parent.bubbles,
+ init.parent.cancelable,
+ &init.referenceSpace,
+ init.transform.as_deref(),
+ can_gc,
+ ))
+ }
+
+ /// <https://www.w3.org/TR/webxr/#dom-xrreferencespaceeventinit-session>
+ fn ReferenceSpace(&self) -> DomRoot<XRReferenceSpace> {
+ DomRoot::from_ref(&*self.space)
+ }
+
+ /// <https://www.w3.org/TR/webxr/#dom-xrreferencespaceevent-transform>
+ fn GetTransform(&self) -> Option<DomRoot<XRRigidTransform>> {
+ self.transform
+ .as_ref()
+ .map(|transform| DomRoot::from_ref(&**transform))
+ }
+
+ /// <https://dom.spec.whatwg.org/#dom-event-istrusted>
+ fn IsTrusted(&self) -> bool {
+ self.event.IsTrusted()
+ }
+}
diff --git a/components/script/dom/webxr/xrrenderstate.rs b/components/script/dom/webxr/xrrenderstate.rs
new file mode 100644
index 00000000000..cf6976404d6
--- /dev/null
+++ b/components/script/dom/webxr/xrrenderstate.rs
@@ -0,0 +1,155 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+use std::cell::Cell;
+
+use dom_struct::dom_struct;
+use js::rust::MutableHandleValue;
+use webxr_api::SubImages;
+
+use crate::dom::bindings::cell::DomRefCell;
+use crate::dom::bindings::codegen::Bindings::XRRenderStateBinding::XRRenderStateMethods;
+use crate::dom::bindings::num::Finite;
+use crate::dom::bindings::reflector::{reflect_dom_object, DomObject, Reflector};
+use crate::dom::bindings::root::{Dom, DomRoot, MutNullableDom};
+use crate::dom::bindings::utils::to_frozen_array;
+use crate::dom::globalscope::GlobalScope;
+use crate::dom::xrlayer::XRLayer;
+use crate::dom::xrwebgllayer::XRWebGLLayer;
+use crate::script_runtime::JSContext;
+
+#[dom_struct]
+pub struct XRRenderState {
+ reflector_: Reflector,
+ depth_near: Cell<f64>,
+ depth_far: Cell<f64>,
+ inline_vertical_fov: Cell<Option<f64>>,
+ base_layer: MutNullableDom<XRWebGLLayer>,
+ layers: DomRefCell<Vec<Dom<XRLayer>>>,
+}
+
+impl XRRenderState {
+ pub fn new_inherited(
+ depth_near: f64,
+ depth_far: f64,
+ inline_vertical_fov: Option<f64>,
+ layer: Option<&XRWebGLLayer>,
+ layers: Vec<&XRLayer>,
+ ) -> XRRenderState {
+ debug_assert!(layer.is_none() || layers.is_empty());
+ XRRenderState {
+ reflector_: Reflector::new(),
+ depth_near: Cell::new(depth_near),
+ depth_far: Cell::new(depth_far),
+ inline_vertical_fov: Cell::new(inline_vertical_fov),
+ base_layer: MutNullableDom::new(layer),
+ layers: DomRefCell::new(layers.into_iter().map(Dom::from_ref).collect()),
+ }
+ }
+
+ pub fn new(
+ global: &GlobalScope,
+ depth_near: f64,
+ depth_far: f64,
+ inline_vertical_fov: Option<f64>,
+ layer: Option<&XRWebGLLayer>,
+ layers: Vec<&XRLayer>,
+ ) -> DomRoot<XRRenderState> {
+ reflect_dom_object(
+ Box::new(XRRenderState::new_inherited(
+ depth_near,
+ depth_far,
+ inline_vertical_fov,
+ layer,
+ layers,
+ )),
+ global,
+ )
+ }
+
+ pub fn clone_object(&self) -> DomRoot<Self> {
+ XRRenderState::new(
+ &self.global(),
+ self.depth_near.get(),
+ self.depth_far.get(),
+ self.inline_vertical_fov.get(),
+ self.base_layer.get().as_deref(),
+ self.layers.borrow().iter().map(|x| &**x).collect(),
+ )
+ }
+
+ pub fn set_depth_near(&self, depth: f64) {
+ self.depth_near.set(depth)
+ }
+ pub fn set_depth_far(&self, depth: f64) {
+ self.depth_far.set(depth)
+ }
+ pub fn set_inline_vertical_fov(&self, fov: f64) {
+ debug_assert!(self.inline_vertical_fov.get().is_some());
+ self.inline_vertical_fov.set(Some(fov))
+ }
+ pub fn set_base_layer(&self, layer: Option<&XRWebGLLayer>) {
+ self.base_layer.set(layer)
+ }
+ pub fn set_layers(&self, layers: Vec<&XRLayer>) {
+ *self.layers.borrow_mut() = layers.into_iter().map(Dom::from_ref).collect();
+ }
+ pub fn with_layers<F, R>(&self, f: F) -> R
+ where
+ F: FnOnce(&[Dom<XRLayer>]) -> R,
+ {
+ let layers = self.layers.borrow();
+ f(&layers)
+ }
+ pub fn has_sub_images(&self, sub_images: &[SubImages]) -> bool {
+ if let Some(base_layer) = self.base_layer.get() {
+ match sub_images.len() {
+ // For inline sessions, there may be a base layer, but it won't have a framebuffer
+ 0 => base_layer.layer_id().is_none(),
+ // For immersive sessions, the base layer will have a framebuffer,
+ // so we make sure the layer id's match up
+ 1 => base_layer.layer_id() == Some(sub_images[0].layer_id),
+ _ => false,
+ }
+ } else {
+ // The layers API is only for immersive sessions
+ let layers = self.layers.borrow();
+ sub_images.len() == layers.len() &&
+ sub_images
+ .iter()
+ .zip(layers.iter())
+ .all(|(sub_image, layer)| Some(sub_image.layer_id) == layer.layer_id())
+ }
+ }
+}
+
+impl XRRenderStateMethods<crate::DomTypeHolder> for XRRenderState {
+ /// <https://immersive-web.github.io/webxr/#dom-xrrenderstate-depthnear>
+ fn DepthNear(&self) -> Finite<f64> {
+ Finite::wrap(self.depth_near.get())
+ }
+
+ /// <https://immersive-web.github.io/webxr/#dom-xrrenderstate-depthfar>
+ fn DepthFar(&self) -> Finite<f64> {
+ Finite::wrap(self.depth_far.get())
+ }
+
+ /// <https://immersive-web.github.io/webxr/#dom-xrrenderstate-inlineverticalfieldofview>
+ fn GetInlineVerticalFieldOfView(&self) -> Option<Finite<f64>> {
+ self.inline_vertical_fov.get().map(Finite::wrap)
+ }
+
+ /// <https://immersive-web.github.io/webxr/#dom-xrrenderstate-baselayer>
+ fn GetBaseLayer(&self) -> Option<DomRoot<XRWebGLLayer>> {
+ self.base_layer.get()
+ }
+
+ /// <https://immersive-web.github.io/layers/#dom-xrrenderstate-layers>
+ fn Layers(&self, cx: JSContext, retval: MutableHandleValue) {
+ // TODO: cache this array?
+ let layers = self.layers.borrow();
+ let layers: Vec<&XRLayer> = layers.iter().map(|x| &**x).collect();
+ to_frozen_array(&layers[..], cx, retval)
+ }
+}
diff --git a/components/script/dom/webxr/xrrigidtransform.rs b/components/script/dom/webxr/xrrigidtransform.rs
new file mode 100644
index 00000000000..e215c17cf4e
--- /dev/null
+++ b/components/script/dom/webxr/xrrigidtransform.rs
@@ -0,0 +1,188 @@
+/* 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 dom_struct::dom_struct;
+use euclid::{RigidTransform3D, Rotation3D, Vector3D};
+use js::rust::HandleObject;
+use js::typedarray::{Float32, Float32Array};
+
+use crate::dom::bindings::buffer_source::HeapBufferSource;
+use crate::dom::bindings::codegen::Bindings::DOMPointBinding::DOMPointInit;
+use crate::dom::bindings::codegen::Bindings::XRRigidTransformBinding::XRRigidTransformMethods;
+use crate::dom::bindings::error::{Error, Fallible};
+use crate::dom::bindings::reflector::{reflect_dom_object_with_proto, DomObject, Reflector};
+use crate::dom::bindings::root::{DomRoot, MutNullableDom};
+use crate::dom::dompointreadonly::DOMPointReadOnly;
+use crate::dom::globalscope::GlobalScope;
+use crate::dom::window::Window;
+use crate::dom::xrsession::ApiRigidTransform;
+use crate::script_runtime::{CanGc, JSContext};
+
+#[dom_struct]
+pub struct XRRigidTransform {
+ reflector_: Reflector,
+ position: MutNullableDom<DOMPointReadOnly>,
+ orientation: MutNullableDom<DOMPointReadOnly>,
+ #[ignore_malloc_size_of = "defined in euclid"]
+ #[no_trace]
+ transform: ApiRigidTransform,
+ inverse: MutNullableDom<XRRigidTransform>,
+ #[ignore_malloc_size_of = "defined in mozjs"]
+ matrix: HeapBufferSource<Float32>,
+}
+
+impl XRRigidTransform {
+ fn new_inherited(transform: ApiRigidTransform) -> XRRigidTransform {
+ XRRigidTransform {
+ reflector_: Reflector::new(),
+ position: MutNullableDom::default(),
+ orientation: MutNullableDom::default(),
+ transform,
+ inverse: MutNullableDom::default(),
+ matrix: HeapBufferSource::default(),
+ }
+ }
+
+ pub fn new(
+ global: &GlobalScope,
+ transform: ApiRigidTransform,
+ can_gc: CanGc,
+ ) -> DomRoot<XRRigidTransform> {
+ Self::new_with_proto(global, None, transform, can_gc)
+ }
+
+ fn new_with_proto(
+ global: &GlobalScope,
+ proto: Option<HandleObject>,
+ transform: ApiRigidTransform,
+ can_gc: CanGc,
+ ) -> DomRoot<XRRigidTransform> {
+ reflect_dom_object_with_proto(
+ Box::new(XRRigidTransform::new_inherited(transform)),
+ global,
+ proto,
+ can_gc,
+ )
+ }
+
+ pub fn identity(window: &GlobalScope, can_gc: CanGc) -> DomRoot<XRRigidTransform> {
+ let transform = RigidTransform3D::identity();
+ XRRigidTransform::new(window, transform, can_gc)
+ }
+}
+
+impl XRRigidTransformMethods<crate::DomTypeHolder> for XRRigidTransform {
+ // https://immersive-web.github.io/webxr/#dom-xrrigidtransform-xrrigidtransform
+ fn Constructor(
+ window: &Window,
+ proto: Option<HandleObject>,
+ can_gc: CanGc,
+ position: &DOMPointInit,
+ orientation: &DOMPointInit,
+ ) -> Fallible<DomRoot<Self>> {
+ if position.w != 1.0 {
+ return Err(Error::Type(format!(
+ "XRRigidTransform must be constructed with a position that has a w value of of 1.0, not {}",
+ position.w
+ )));
+ }
+
+ if !position.x.is_finite() ||
+ !position.y.is_finite() ||
+ !position.z.is_finite() ||
+ !position.w.is_finite()
+ {
+ return Err(Error::Type(
+ "Position must not contain non-finite values".into(),
+ ));
+ }
+
+ if !orientation.x.is_finite() ||
+ !orientation.y.is_finite() ||
+ !orientation.z.is_finite() ||
+ !orientation.w.is_finite()
+ {
+ return Err(Error::Type(
+ "Orientation must not contain non-finite values".into(),
+ ));
+ }
+
+ let translate = Vector3D::new(position.x as f32, position.y as f32, position.z as f32);
+ let rotate = Rotation3D::unit_quaternion(
+ orientation.x as f32,
+ orientation.y as f32,
+ orientation.z as f32,
+ orientation.w as f32,
+ );
+
+ if !rotate.i.is_finite() {
+ // if quaternion has zero norm, we'll get an infinite or NaN
+ // value for each element. This is preferable to checking for zero.
+ return Err(Error::InvalidState);
+ }
+ let transform = RigidTransform3D::new(rotate, translate);
+ Ok(XRRigidTransform::new_with_proto(
+ &window.global(),
+ proto,
+ transform,
+ can_gc,
+ ))
+ }
+
+ // https://immersive-web.github.io/webxr/#dom-xrrigidtransform-position
+ fn Position(&self, can_gc: CanGc) -> DomRoot<DOMPointReadOnly> {
+ self.position.or_init(|| {
+ let t = &self.transform.translation;
+ DOMPointReadOnly::new(
+ &self.global(),
+ t.x.into(),
+ t.y.into(),
+ t.z.into(),
+ 1.0,
+ can_gc,
+ )
+ })
+ }
+ // https://immersive-web.github.io/webxr/#dom-xrrigidtransform-orientation
+ fn Orientation(&self, can_gc: CanGc) -> DomRoot<DOMPointReadOnly> {
+ self.orientation.or_init(|| {
+ let r = &self.transform.rotation;
+ DOMPointReadOnly::new(
+ &self.global(),
+ r.i.into(),
+ r.j.into(),
+ r.k.into(),
+ r.r.into(),
+ can_gc,
+ )
+ })
+ }
+ // https://immersive-web.github.io/webxr/#dom-xrrigidtransform-inverse
+ fn Inverse(&self, can_gc: CanGc) -> DomRoot<XRRigidTransform> {
+ self.inverse.or_init(|| {
+ let transform = XRRigidTransform::new(&self.global(), self.transform.inverse(), can_gc);
+ transform.inverse.set(Some(self));
+ transform
+ })
+ }
+ // https://immersive-web.github.io/webxr/#dom-xrrigidtransform-matrix
+ fn Matrix(&self, _cx: JSContext) -> Float32Array {
+ if !self.matrix.is_initialized() {
+ self.matrix
+ .set_data(_cx, &self.transform.to_transform().to_array())
+ .expect("Failed to set on data on transform's internal matrix.")
+ }
+
+ self.matrix
+ .get_buffer()
+ .expect("Failed to get transform's internal matrix.")
+ }
+}
+
+impl XRRigidTransform {
+ /// <https://immersive-web.github.io/webxr/#dom-xrpose-transform>
+ pub fn transform(&self) -> ApiRigidTransform {
+ self.transform
+ }
+}
diff --git a/components/script/dom/webxr/xrsession.rs b/components/script/dom/webxr/xrsession.rs
new file mode 100644
index 00000000000..920c94aaada
--- /dev/null
+++ b/components/script/dom/webxr/xrsession.rs
@@ -0,0 +1,1111 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+use std::cell::Cell;
+use std::collections::HashMap;
+use std::f64::consts::{FRAC_PI_2, PI};
+use std::rc::Rc;
+use std::{mem, ptr};
+
+use base::cross_process_instant::CrossProcessInstant;
+use dom_struct::dom_struct;
+use euclid::{RigidTransform3D, Transform3D, Vector3D};
+use ipc_channel::ipc::IpcReceiver;
+use ipc_channel::router::ROUTER;
+use js::jsapi::JSObject;
+use js::rust::MutableHandleValue;
+use js::typedarray::Float32Array;
+use profile_traits::ipc;
+use servo_atoms::Atom;
+use webxr_api::{
+ self, util, ApiSpace, ContextId as WebXRContextId, Display, EntityTypes, EnvironmentBlendMode,
+ Event as XREvent, Frame, FrameUpdateEvent, HitTestId, HitTestSource, InputFrame, InputId, Ray,
+ SelectEvent, SelectKind, Session, SessionId, View, Viewer, Visibility,
+};
+
+use crate::dom::bindings::trace::HashMapTracedValues;
+use crate::dom::bindings::buffer_source::create_buffer_source;
+use crate::dom::bindings::callback::ExceptionHandling;
+use crate::dom::bindings::cell::DomRefCell;
+use crate::dom::bindings::codegen::Bindings::NavigatorBinding::Navigator_Binding::NavigatorMethods;
+use crate::dom::bindings::codegen::Bindings::WindowBinding::Window_Binding::WindowMethods;
+use crate::dom::bindings::codegen::Bindings::XRHitTestSourceBinding::{
+ XRHitTestOptionsInit, XRHitTestTrackableType,
+};
+use crate::dom::bindings::codegen::Bindings::XRInputSourceArrayBinding::XRInputSourceArray_Binding::XRInputSourceArrayMethods;
+use crate::dom::bindings::codegen::Bindings::XRReferenceSpaceBinding::XRReferenceSpaceType;
+use crate::dom::bindings::codegen::Bindings::XRRenderStateBinding::{
+ XRRenderStateInit, XRRenderStateMethods,
+};
+use crate::dom::bindings::codegen::Bindings::XRSessionBinding::{
+ XREnvironmentBlendMode, XRFrameRequestCallback, XRInteractionMode, XRSessionMethods,
+ XRVisibilityState,
+};
+use crate::dom::bindings::codegen::Bindings::XRSystemBinding::XRSessionMode;
+use crate::dom::bindings::error::{Error, ErrorResult};
+use crate::dom::bindings::inheritance::Castable;
+use crate::dom::bindings::num::Finite;
+use crate::dom::bindings::refcounted::Trusted;
+use crate::dom::bindings::reflector::{reflect_dom_object, DomObject};
+use crate::dom::bindings::root::{Dom, DomRoot, MutDom, MutNullableDom};
+use crate::dom::bindings::utils::to_frozen_array;
+use crate::dom::event::Event;
+use crate::dom::eventtarget::EventTarget;
+use crate::dom::globalscope::GlobalScope;
+use crate::dom::promise::Promise;
+use crate::dom::xrboundedreferencespace::XRBoundedReferenceSpace;
+use crate::dom::xrframe::XRFrame;
+use crate::dom::xrhittestsource::XRHitTestSource;
+use crate::dom::xrinputsourcearray::XRInputSourceArray;
+use crate::dom::xrinputsourceevent::XRInputSourceEvent;
+use crate::dom::xrreferencespace::XRReferenceSpace;
+use crate::dom::xrreferencespaceevent::XRReferenceSpaceEvent;
+use crate::dom::xrrenderstate::XRRenderState;
+use crate::dom::xrrigidtransform::XRRigidTransform;
+use crate::dom::xrsessionevent::XRSessionEvent;
+use crate::dom::xrspace::XRSpace;
+use crate::realms::InRealm;
+use crate::script_runtime::JSContext;
+use crate::task_source::TaskSource;
+use crate::script_runtime::CanGc;
+
+#[dom_struct]
+pub struct XRSession {
+ eventtarget: EventTarget,
+ blend_mode: XREnvironmentBlendMode,
+ mode: XRSessionMode,
+ visibility_state: Cell<XRVisibilityState>,
+ viewer_space: MutNullableDom<XRSpace>,
+ #[ignore_malloc_size_of = "defined in webxr"]
+ #[no_trace]
+ session: DomRefCell<Session>,
+ frame_requested: Cell<bool>,
+ pending_render_state: MutNullableDom<XRRenderState>,
+ active_render_state: MutDom<XRRenderState>,
+ /// Cached projection matrix for inline sessions
+ #[no_trace]
+ inline_projection_matrix: DomRefCell<Transform3D<f32, Viewer, Display>>,
+
+ next_raf_id: Cell<i32>,
+ #[ignore_malloc_size_of = "closures are hard"]
+ raf_callback_list: DomRefCell<Vec<(i32, Option<Rc<XRFrameRequestCallback>>)>>,
+ #[ignore_malloc_size_of = "closures are hard"]
+ current_raf_callback_list: DomRefCell<Vec<(i32, Option<Rc<XRFrameRequestCallback>>)>>,
+ input_sources: Dom<XRInputSourceArray>,
+ // Any promises from calling end()
+ #[ignore_malloc_size_of = "promises are hard"]
+ end_promises: DomRefCell<Vec<Rc<Promise>>>,
+ /// <https://immersive-web.github.io/webxr/#ended>
+ ended: Cell<bool>,
+ #[ignore_malloc_size_of = "defined in webxr"]
+ #[no_trace]
+ next_hit_test_id: Cell<HitTestId>,
+ #[ignore_malloc_size_of = "defined in webxr"]
+ pending_hit_test_promises: DomRefCell<HashMapTracedValues<HitTestId, Rc<Promise>>>,
+ /// Opaque framebuffers need to know the session is "outside of a requestAnimationFrame"
+ /// <https://immersive-web.github.io/webxr/#opaque-framebuffer>
+ outside_raf: Cell<bool>,
+ #[ignore_malloc_size_of = "defined in webxr"]
+ #[no_trace]
+ input_frames: DomRefCell<HashMap<InputId, InputFrame>>,
+ framerate: Cell<f32>,
+ #[ignore_malloc_size_of = "promises are hard"]
+ update_framerate_promise: DomRefCell<Option<Rc<Promise>>>,
+ reference_spaces: DomRefCell<Vec<Dom<XRReferenceSpace>>>,
+}
+
+impl XRSession {
+ fn new_inherited(
+ session: Session,
+ render_state: &XRRenderState,
+ input_sources: &XRInputSourceArray,
+ mode: XRSessionMode,
+ ) -> XRSession {
+ XRSession {
+ eventtarget: EventTarget::new_inherited(),
+ blend_mode: session.environment_blend_mode().into(),
+ mode,
+ visibility_state: Cell::new(XRVisibilityState::Visible),
+ viewer_space: Default::default(),
+ session: DomRefCell::new(session),
+ frame_requested: Cell::new(false),
+ pending_render_state: MutNullableDom::new(None),
+ active_render_state: MutDom::new(render_state),
+ inline_projection_matrix: Default::default(),
+
+ next_raf_id: Cell::new(0),
+ raf_callback_list: DomRefCell::new(vec![]),
+ current_raf_callback_list: DomRefCell::new(vec![]),
+ input_sources: Dom::from_ref(input_sources),
+ end_promises: DomRefCell::new(vec![]),
+ ended: Cell::new(false),
+ next_hit_test_id: Cell::new(HitTestId(0)),
+ pending_hit_test_promises: DomRefCell::new(HashMapTracedValues::new()),
+ outside_raf: Cell::new(true),
+ input_frames: DomRefCell::new(HashMap::new()),
+ framerate: Cell::new(0.0),
+ update_framerate_promise: DomRefCell::new(None),
+ reference_spaces: DomRefCell::new(Vec::new()),
+ }
+ }
+
+ pub fn new(
+ global: &GlobalScope,
+ session: Session,
+ mode: XRSessionMode,
+ frame_receiver: IpcReceiver<Frame>,
+ ) -> DomRoot<XRSession> {
+ let ivfov = if mode == XRSessionMode::Inline {
+ Some(FRAC_PI_2)
+ } else {
+ None
+ };
+ let render_state = XRRenderState::new(global, 0.1, 1000.0, ivfov, None, Vec::new());
+ let input_sources = XRInputSourceArray::new(global);
+ let ret = reflect_dom_object(
+ Box::new(XRSession::new_inherited(
+ session,
+ &render_state,
+ &input_sources,
+ mode,
+ )),
+ global,
+ );
+ ret.attach_event_handler();
+ ret.setup_raf_loop(frame_receiver);
+ ret
+ }
+
+ pub fn with_session<R, F: FnOnce(&Session) -> R>(&self, with: F) -> R {
+ let session = self.session.borrow();
+ with(&session)
+ }
+
+ pub fn is_ended(&self) -> bool {
+ self.ended.get()
+ }
+
+ pub fn is_immersive(&self) -> bool {
+ self.mode != XRSessionMode::Inline
+ }
+
+ // https://immersive-web.github.io/layers/#feature-descriptor-layers
+ pub fn has_layers_feature(&self) -> bool {
+ // We do not support creating layers other than projection layers
+ // https://github.com/servo/servo/issues/27493
+ false
+ }
+
+ fn setup_raf_loop(&self, frame_receiver: IpcReceiver<Frame>) {
+ let this = Trusted::new(self);
+ let global = self.global();
+ let window = global.as_window();
+ let (task_source, canceller) = window
+ .task_manager()
+ .dom_manipulation_task_source_with_canceller();
+ ROUTER.add_typed_route(
+ frame_receiver,
+ Box::new(move |message| {
+ let frame: Frame = message.unwrap();
+ let time = CrossProcessInstant::now();
+ let this = this.clone();
+ let _ = task_source.queue_with_canceller(
+ task!(xr_raf_callback: move || {
+ this.root().raf_callback(frame, time);
+ }),
+ &canceller,
+ );
+ }),
+ );
+
+ self.session.borrow_mut().start_render_loop();
+ }
+
+ pub fn is_outside_raf(&self) -> bool {
+ self.outside_raf.get()
+ }
+
+ fn attach_event_handler(&self) {
+ let this = Trusted::new(self);
+ let global = self.global();
+ let window = global.as_window();
+ let (task_source, canceller) = window
+ .task_manager()
+ .dom_manipulation_task_source_with_canceller();
+ let (sender, receiver) = ipc::channel(global.time_profiler_chan().clone()).unwrap();
+
+ ROUTER.add_typed_route(
+ receiver.to_ipc_receiver(),
+ Box::new(move |message| {
+ let this = this.clone();
+ let _ = task_source.queue_with_canceller(
+ task!(xr_event_callback: move || {
+ this.root().event_callback(message.unwrap(), CanGc::note());
+ }),
+ &canceller,
+ );
+ }),
+ );
+
+ // request animation frame
+ self.session.borrow_mut().set_event_dest(sender);
+ }
+
+ // Must be called after the promise for session creation is resolved
+ // https://github.com/immersive-web/webxr/issues/961
+ //
+ // This enables content that assumes all input sources are accompanied
+ // by an inputsourceschange event to work properly. Without
+ pub fn setup_initial_inputs(&self) {
+ let initial_inputs = self.session.borrow().initial_inputs().to_owned();
+
+ if initial_inputs.is_empty() {
+ // do not fire an empty event
+ return;
+ }
+
+ let global = self.global();
+ let window = global.as_window();
+ let (task_source, canceller) = window
+ .task_manager()
+ .dom_manipulation_task_source_with_canceller();
+ let this = Trusted::new(self);
+ // Queue a task so that it runs after resolve()'s microtasks complete
+ // so that content has a chance to attach a listener for inputsourceschange
+ let _ = task_source.queue_with_canceller(
+ task!(session_initial_inputs: move || {
+ let this = this.root();
+ this.input_sources.add_input_sources(&this, &initial_inputs, CanGc::note());
+ }),
+ &canceller,
+ );
+ }
+
+ fn event_callback(&self, event: XREvent, can_gc: CanGc) {
+ match event {
+ XREvent::SessionEnd => {
+ // https://immersive-web.github.io/webxr/#shut-down-the-session
+ // Step 2
+ self.ended.set(true);
+ // Step 3-4
+ self.global().as_window().Navigator().Xr().end_session(self);
+ // Step 5: We currently do not have any such promises
+ // Step 6 is happening n the XR session
+ // https://immersive-web.github.io/webxr/#dom-xrsession-end step 3
+ for promise in self.end_promises.borrow_mut().drain(..) {
+ promise.resolve_native(&());
+ }
+ // Step 7
+ let event =
+ XRSessionEvent::new(&self.global(), atom!("end"), false, false, self, can_gc);
+ event.upcast::<Event>().fire(self.upcast(), can_gc);
+ },
+ XREvent::Select(input, kind, ty, frame) => {
+ use servo_atoms::Atom;
+ const START_ATOMS: [Atom; 2] = [atom!("selectstart"), atom!("squeezestart")];
+ const EVENT_ATOMS: [Atom; 2] = [atom!("select"), atom!("squeeze")];
+ const END_ATOMS: [Atom; 2] = [atom!("selectend"), atom!("squeezeend")];
+
+ // https://immersive-web.github.io/webxr/#primary-action
+ let source = self.input_sources.find(input);
+ let atom_index = if kind == SelectKind::Squeeze { 1 } else { 0 };
+ if let Some(source) = source {
+ let frame = XRFrame::new(&self.global(), self, frame);
+ frame.set_active(true);
+ if ty == SelectEvent::Start {
+ let event = XRInputSourceEvent::new(
+ &self.global(),
+ START_ATOMS[atom_index].clone(),
+ false,
+ false,
+ &frame,
+ &source,
+ can_gc,
+ );
+ event.upcast::<Event>().fire(self.upcast(), can_gc);
+ } else {
+ if ty == SelectEvent::Select {
+ let event = XRInputSourceEvent::new(
+ &self.global(),
+ EVENT_ATOMS[atom_index].clone(),
+ false,
+ false,
+ &frame,
+ &source,
+ can_gc,
+ );
+ event.upcast::<Event>().fire(self.upcast(), can_gc);
+ }
+ let event = XRInputSourceEvent::new(
+ &self.global(),
+ END_ATOMS[atom_index].clone(),
+ false,
+ false,
+ &frame,
+ &source,
+ can_gc,
+ );
+ event.upcast::<Event>().fire(self.upcast(), can_gc);
+ }
+ frame.set_active(false);
+ }
+ },
+ XREvent::VisibilityChange(v) => {
+ let v = match v {
+ Visibility::Visible => XRVisibilityState::Visible,
+ Visibility::VisibleBlurred => XRVisibilityState::Visible_blurred,
+ Visibility::Hidden => XRVisibilityState::Hidden,
+ };
+ self.visibility_state.set(v);
+ let event = XRSessionEvent::new(
+ &self.global(),
+ atom!("visibilitychange"),
+ false,
+ false,
+ self,
+ can_gc,
+ );
+ event.upcast::<Event>().fire(self.upcast(), can_gc);
+ // The page may be visible again, dirty the layers
+ // This also wakes up the event loop if necessary
+ self.dirty_layers();
+ },
+ XREvent::AddInput(info) => {
+ self.input_sources.add_input_sources(self, &[info], can_gc);
+ },
+ XREvent::RemoveInput(id) => {
+ self.input_sources.remove_input_source(self, id, can_gc);
+ },
+ XREvent::UpdateInput(id, source) => {
+ self.input_sources
+ .add_remove_input_source(self, id, source, can_gc);
+ },
+ XREvent::InputChanged(id, frame) => {
+ self.input_frames.borrow_mut().insert(id, frame);
+ },
+ XREvent::ReferenceSpaceChanged(base_space, transform) => {
+ self.reference_spaces
+ .borrow()
+ .iter()
+ .filter(|space| {
+ let base = match space.ty() {
+ XRReferenceSpaceType::Local => webxr_api::BaseSpace::Local,
+ XRReferenceSpaceType::Viewer => webxr_api::BaseSpace::Viewer,
+ XRReferenceSpaceType::Local_floor => webxr_api::BaseSpace::Floor,
+ XRReferenceSpaceType::Bounded_floor => {
+ webxr_api::BaseSpace::BoundedFloor
+ },
+ _ => panic!("unsupported reference space found"),
+ };
+ base == base_space
+ })
+ .for_each(|space| {
+ let offset = XRRigidTransform::new(&self.global(), transform, can_gc);
+ let event = XRReferenceSpaceEvent::new(
+ &self.global(),
+ atom!("reset"),
+ false,
+ false,
+ space,
+ Some(&*offset),
+ can_gc,
+ );
+ event.upcast::<Event>().fire(space.upcast(), can_gc);
+ });
+ },
+ }
+ }
+
+ /// <https://immersive-web.github.io/webxr/#xr-animation-frame>
+ fn raf_callback(&self, mut frame: Frame, time: CrossProcessInstant) {
+ debug!("WebXR RAF callback {:?}", frame);
+
+ // Step 1-2 happen in the xebxr device thread
+
+ // Step 3
+ if let Some(pending) = self.pending_render_state.take() {
+ // https://immersive-web.github.io/webxr/#apply-the-pending-render-state
+ // (Steps 1-4 are implicit)
+ // Step 5
+ self.active_render_state.set(&pending);
+ // Step 6-7: XXXManishearth handle inlineVerticalFieldOfView
+
+ if !self.is_immersive() {
+ self.update_inline_projection_matrix()
+ }
+ }
+
+ // TODO: how does this fit the webxr spec?
+ for event in frame.events.drain(..) {
+ self.handle_frame_event(event);
+ }
+
+ // Step 4
+ // TODO: what should this check be?
+ // This is checking that the new render state has the same
+ // layers as the frame.
+ // Related to https://github.com/immersive-web/webxr/issues/1051
+ if !self
+ .active_render_state
+ .get()
+ .has_sub_images(&frame.sub_images[..])
+ {
+ // If the frame has different layers than the render state,
+ // we just return early, drawing a blank frame.
+ // This can result in flickering when the render state is changed.
+ // TODO: it would be better to not render anything until the next frame.
+ warn!("Rendering blank XR frame");
+ self.session.borrow_mut().render_animation_frame();
+ return;
+ }
+
+ // Step 5: XXXManishearth handle inline session
+
+ // Step 6-7
+ {
+ let mut current = self.current_raf_callback_list.borrow_mut();
+ assert!(current.is_empty());
+ mem::swap(&mut *self.raf_callback_list.borrow_mut(), &mut current);
+ }
+
+ let time = self.global().performance().to_dom_high_res_time_stamp(time);
+ let frame = XRFrame::new(&self.global(), self, frame);
+
+ // Step 8-9
+ frame.set_active(true);
+ frame.set_animation_frame(true);
+
+ // Step 10
+ self.apply_frame_updates(&frame);
+
+ // TODO: how does this fit with the webxr and xr layers specs?
+ self.layers_begin_frame(&frame);
+
+ // Step 11-12
+ self.outside_raf.set(false);
+ let len = self.current_raf_callback_list.borrow().len();
+ for i in 0..len {
+ let callback = self.current_raf_callback_list.borrow()[i].1.clone();
+ if let Some(callback) = callback {
+ let _ = callback.Call__(time, &frame, ExceptionHandling::Report);
+ }
+ }
+ self.outside_raf.set(true);
+ *self.current_raf_callback_list.borrow_mut() = vec![];
+
+ // TODO: how does this fit with the webxr and xr layers specs?
+ self.layers_end_frame(&frame);
+
+ // Step 13
+ frame.set_active(false);
+
+ // TODO: how does this fit the webxr spec?
+ self.session.borrow_mut().render_animation_frame();
+ }
+
+ fn update_inline_projection_matrix(&self) {
+ debug_assert!(!self.is_immersive());
+ let render_state = self.active_render_state.get();
+ let size = if let Some(base) = render_state.GetBaseLayer() {
+ base.size()
+ } else {
+ return;
+ };
+ let mut clip_planes = util::ClipPlanes::default();
+ let near = *render_state.DepthNear() as f32;
+ let far = *render_state.DepthFar() as f32;
+ clip_planes.update(near, far);
+ let top = *render_state
+ .GetInlineVerticalFieldOfView()
+ .expect("IVFOV should be non null for inline sessions") /
+ 2.;
+ let top = near * top.tan() as f32;
+ let bottom = top;
+ let left = top * size.width as f32 / size.height as f32;
+ let right = left;
+ let matrix = util::frustum_to_projection_matrix(left, right, top, bottom, clip_planes);
+ *self.inline_projection_matrix.borrow_mut() = matrix;
+ }
+
+ /// Constructs a View suitable for inline sessions using the inlineVerticalFieldOfView and canvas size
+ pub fn inline_view(&self) -> View<Viewer> {
+ debug_assert!(!self.is_immersive());
+ View {
+ // Inline views have no offset
+ transform: RigidTransform3D::identity(),
+ projection: *self.inline_projection_matrix.borrow(),
+ }
+ }
+
+ pub fn session_id(&self) -> SessionId {
+ self.session.borrow().id()
+ }
+
+ pub fn dirty_layers(&self) {
+ if let Some(layer) = self.RenderState().GetBaseLayer() {
+ layer.context().mark_as_dirty();
+ }
+ }
+
+ // TODO: how does this align with the layers spec?
+ fn layers_begin_frame(&self, frame: &XRFrame) {
+ if let Some(layer) = self.active_render_state.get().GetBaseLayer() {
+ layer.begin_frame(frame);
+ }
+ self.active_render_state.get().with_layers(|layers| {
+ for layer in layers {
+ layer.begin_frame(frame);
+ }
+ });
+ }
+
+ // TODO: how does this align with the layers spec?
+ fn layers_end_frame(&self, frame: &XRFrame) {
+ if let Some(layer) = self.active_render_state.get().GetBaseLayer() {
+ layer.end_frame(frame);
+ }
+ self.active_render_state.get().with_layers(|layers| {
+ for layer in layers {
+ layer.end_frame(frame);
+ }
+ });
+ }
+
+ /// <https://immersive-web.github.io/webxr/#xrframe-apply-frame-updates>
+ fn apply_frame_updates(&self, _frame: &XRFrame) {
+ // <https://www.w3.org/TR/webxr-gamepads-module-1/#xrframe-apply-gamepad-frame-updates>
+ for (id, frame) in self.input_frames.borrow_mut().drain() {
+ let source = self.input_sources.find(id);
+ if let Some(source) = source {
+ source.update_gamepad_state(frame);
+ }
+ }
+ }
+
+ fn handle_frame_event(&self, event: FrameUpdateEvent) {
+ match event {
+ FrameUpdateEvent::HitTestSourceAdded(id) => {
+ if let Some(promise) = self.pending_hit_test_promises.borrow_mut().remove(&id) {
+ promise.resolve_native(&XRHitTestSource::new(&self.global(), id, self));
+ } else {
+ warn!(
+ "received hit test add request for unknown hit test {:?}",
+ id
+ )
+ }
+ },
+ _ => self.session.borrow_mut().apply_event(event),
+ }
+ }
+
+ /// <https://www.w3.org/TR/webxr/#apply-the-nominal-frame-rate>
+ fn apply_nominal_framerate(&self, rate: f32, can_gc: CanGc) {
+ if self.framerate.get() == rate || self.ended.get() {
+ return;
+ }
+
+ self.framerate.set(rate);
+
+ let event = XRSessionEvent::new(
+ &self.global(),
+ Atom::from("frameratechange"),
+ false,
+ false,
+ self,
+ can_gc,
+ );
+ event.upcast::<Event>().fire(self.upcast(), can_gc);
+ }
+}
+
+impl XRSessionMethods<crate::DomTypeHolder> for XRSession {
+ // https://immersive-web.github.io/webxr/#eventdef-xrsession-end
+ event_handler!(end, GetOnend, SetOnend);
+
+ // https://immersive-web.github.io/webxr/#eventdef-xrsession-select
+ event_handler!(select, GetOnselect, SetOnselect);
+
+ // https://immersive-web.github.io/webxr/#eventdef-xrsession-selectstart
+ event_handler!(selectstart, GetOnselectstart, SetOnselectstart);
+
+ // https://immersive-web.github.io/webxr/#eventdef-xrsession-selectend
+ event_handler!(selectend, GetOnselectend, SetOnselectend);
+
+ // https://immersive-web.github.io/webxr/#eventdef-xrsession-squeeze
+ event_handler!(squeeze, GetOnsqueeze, SetOnsqueeze);
+
+ // https://immersive-web.github.io/webxr/#eventdef-xrsession-squeezestart
+ event_handler!(squeezestart, GetOnsqueezestart, SetOnsqueezestart);
+
+ // https://immersive-web.github.io/webxr/#eventdef-xrsession-squeezeend
+ event_handler!(squeezeend, GetOnsqueezeend, SetOnsqueezeend);
+
+ // https://immersive-web.github.io/webxr/#eventdef-xrsession-visibilitychange
+ event_handler!(
+ visibilitychange,
+ GetOnvisibilitychange,
+ SetOnvisibilitychange
+ );
+
+ // https://immersive-web.github.io/webxr/#eventdef-xrsession-inputsourceschange
+ event_handler!(
+ inputsourceschange,
+ GetOninputsourceschange,
+ SetOninputsourceschange
+ );
+
+ // https://www.w3.org/TR/webxr/#dom-xrsession-onframeratechange
+ event_handler!(frameratechange, GetOnframeratechange, SetOnframeratechange);
+
+ // https://immersive-web.github.io/webxr/#dom-xrsession-renderstate
+ fn RenderState(&self) -> DomRoot<XRRenderState> {
+ self.active_render_state.get()
+ }
+
+ /// <https://immersive-web.github.io/webxr/#dom-xrsession-updaterenderstate>
+ fn UpdateRenderState(&self, init: &XRRenderStateInit, _: InRealm) -> ErrorResult {
+ // Step 2
+ if self.ended.get() {
+ return Err(Error::InvalidState);
+ }
+ // Step 3:
+ if let Some(Some(ref layer)) = init.baseLayer {
+ if Dom::from_ref(layer.session()) != Dom::from_ref(self) {
+ return Err(Error::InvalidState);
+ }
+ }
+
+ // Step 4:
+ if init.inlineVerticalFieldOfView.is_some() && self.is_immersive() {
+ return Err(Error::InvalidState);
+ }
+
+ // https://immersive-web.github.io/layers/#updaterenderstatechanges
+ // Step 1.
+ if init.baseLayer.is_some() && (self.has_layers_feature() || init.layers.is_some()) {
+ return Err(Error::NotSupported);
+ }
+
+ if let Some(Some(ref layers)) = init.layers {
+ // Step 2
+ for layer in layers {
+ let count = layers
+ .iter()
+ .filter(|other| other.layer_id() == layer.layer_id())
+ .count();
+ if count > 1 {
+ return Err(Error::Type(String::from("Duplicate entry in WebXR layers")));
+ }
+ }
+
+ // Step 3
+ for layer in layers {
+ if layer.session() != self {
+ return Err(Error::Type(String::from(
+ "Layer from different session in WebXR layers",
+ )));
+ }
+ }
+ }
+
+ // Step 4-5
+ let pending = self
+ .pending_render_state
+ .or_init(|| self.active_render_state.get().clone_object());
+
+ // Step 6
+ if let Some(ref layers) = init.layers {
+ let layers = layers.as_deref().unwrap_or_default();
+ pending.set_base_layer(None);
+ pending.set_layers(layers.iter().map(|x| &**x).collect());
+ let layers = layers
+ .iter()
+ .filter_map(|layer| {
+ let context_id = WebXRContextId::from(layer.context_id());
+ let layer_id = layer.layer_id()?;
+ Some((context_id, layer_id))
+ })
+ .collect();
+ self.session.borrow_mut().set_layers(layers);
+ }
+
+ // End of https://immersive-web.github.io/layers/#updaterenderstatechanges
+
+ if let Some(near) = init.depthNear {
+ let mut near = *near;
+ // Step 8 from #apply-the-pending-render-state
+ // this may need to be changed if backends wish to impose
+ // further constraints
+ if near < 0. {
+ near = 0.;
+ }
+ pending.set_depth_near(near);
+ }
+ if let Some(far) = init.depthFar {
+ let mut far = *far;
+ // Step 9 from #apply-the-pending-render-state
+ // this may need to be changed if backends wish to impose
+ // further constraints
+ // currently the maximum is infinity, so just check that
+ // the value is non-negative
+ if far < 0. {
+ far = 0.;
+ }
+ pending.set_depth_far(far);
+ }
+ if let Some(fov) = init.inlineVerticalFieldOfView {
+ let mut fov = *fov;
+ // Step 10 from #apply-the-pending-render-state
+ // this may need to be changed if backends wish to impose
+ // further constraints
+ if fov < 0. {
+ fov = 0.0001;
+ } else if fov > PI {
+ fov = PI - 0.0001;
+ }
+ pending.set_inline_vertical_fov(fov);
+ }
+ if let Some(ref layer) = init.baseLayer {
+ pending.set_base_layer(layer.as_deref());
+ pending.set_layers(Vec::new());
+ let layers = layer
+ .iter()
+ .filter_map(|layer| {
+ let context_id = WebXRContextId::from(layer.context_id());
+ let layer_id = layer.layer_id()?;
+ Some((context_id, layer_id))
+ })
+ .collect();
+ self.session.borrow_mut().set_layers(layers);
+ }
+
+ if init.depthFar.is_some() || init.depthNear.is_some() {
+ self.session
+ .borrow_mut()
+ .update_clip_planes(*pending.DepthNear() as f32, *pending.DepthFar() as f32);
+ }
+
+ Ok(())
+ }
+
+ /// <https://immersive-web.github.io/webxr/#dom-xrsession-requestanimationframe>
+ fn RequestAnimationFrame(&self, callback: Rc<XRFrameRequestCallback>) -> i32 {
+ // queue up RAF callback, obtain ID
+ let raf_id = self.next_raf_id.get();
+ self.next_raf_id.set(raf_id + 1);
+ self.raf_callback_list
+ .borrow_mut()
+ .push((raf_id, Some(callback)));
+
+ raf_id
+ }
+
+ /// <https://immersive-web.github.io/webxr/#dom-xrsession-cancelanimationframe>
+ fn CancelAnimationFrame(&self, frame: i32) {
+ let mut list = self.raf_callback_list.borrow_mut();
+ if let Some(pair) = list.iter_mut().find(|pair| pair.0 == frame) {
+ pair.1 = None;
+ }
+
+ let mut list = self.current_raf_callback_list.borrow_mut();
+ if let Some(pair) = list.iter_mut().find(|pair| pair.0 == frame) {
+ pair.1 = None;
+ }
+ }
+
+ /// <https://immersive-web.github.io/webxr/#dom-xrsession-environmentblendmode>
+ fn EnvironmentBlendMode(&self) -> XREnvironmentBlendMode {
+ self.blend_mode
+ }
+
+ /// <https://immersive-web.github.io/webxr/#dom-xrsession-visibilitystate>
+ fn VisibilityState(&self) -> XRVisibilityState {
+ self.visibility_state.get()
+ }
+
+ /// <https://immersive-web.github.io/webxr/#dom-xrsession-requestreferencespace>
+ fn RequestReferenceSpace(
+ &self,
+ ty: XRReferenceSpaceType,
+ comp: InRealm,
+ can_gc: CanGc,
+ ) -> Rc<Promise> {
+ let p = Promise::new_in_current_realm(comp, can_gc);
+
+ // https://immersive-web.github.io/webxr/#create-a-reference-space
+
+ // XXXManishearth reject based on session type
+ // https://github.com/immersive-web/webxr/blob/master/spatial-tracking-explainer.md#practical-usage-guidelines
+
+ if !self.is_immersive() &&
+ (ty == XRReferenceSpaceType::Bounded_floor || ty == XRReferenceSpaceType::Unbounded)
+ {
+ p.reject_error(Error::NotSupported);
+ return p;
+ }
+
+ match ty {
+ XRReferenceSpaceType::Unbounded => {
+ // XXXmsub2 figure out how to support this
+ p.reject_error(Error::NotSupported)
+ },
+ ty => {
+ if ty != XRReferenceSpaceType::Viewer &&
+ (!self.is_immersive() || ty != XRReferenceSpaceType::Local)
+ {
+ let s = ty.as_str();
+ if !self
+ .session
+ .borrow()
+ .granted_features()
+ .iter()
+ .any(|f| *f == s)
+ {
+ p.reject_error(Error::NotSupported);
+ return p;
+ }
+ }
+ if ty == XRReferenceSpaceType::Bounded_floor {
+ let space = XRBoundedReferenceSpace::new(&self.global(), self, can_gc);
+ self.reference_spaces
+ .borrow_mut()
+ .push(Dom::from_ref(space.reference_space()));
+ p.resolve_native(&space);
+ } else {
+ let space = XRReferenceSpace::new(&self.global(), self, ty, can_gc);
+ self.reference_spaces
+ .borrow_mut()
+ .push(Dom::from_ref(&*space));
+ p.resolve_native(&space);
+ }
+ },
+ }
+ p
+ }
+
+ /// <https://immersive-web.github.io/webxr/#dom-xrsession-inputsources>
+ fn InputSources(&self) -> DomRoot<XRInputSourceArray> {
+ DomRoot::from_ref(&*self.input_sources)
+ }
+
+ /// <https://immersive-web.github.io/webxr/#dom-xrsession-end>
+ fn End(&self, can_gc: CanGc) -> Rc<Promise> {
+ let global = self.global();
+ let p = Promise::new(&global, can_gc);
+ if self.ended.get() && self.end_promises.borrow().is_empty() {
+ // If the session has completely ended and all end promises have been resolved,
+ // don't queue up more end promises
+ //
+ // We need to check for end_promises being empty because `ended` is set
+ // before everything has been completely shut down, and we do not want to
+ // prematurely resolve the promise then
+ //
+ // However, if end_promises is empty, then all end() promises have already resolved,
+ // so the session has completely shut down and we should not queue up more promises
+ p.resolve_native(&());
+ return p;
+ }
+ self.end_promises.borrow_mut().push(p.clone());
+ // This is duplicated in event_callback since this should
+ // happen ASAP for end() but can happen later if the device
+ // shuts itself down
+ self.ended.set(true);
+ global.as_window().Navigator().Xr().end_session(self);
+ self.session.borrow_mut().end_session();
+ // Disconnect any still-attached XRInputSources
+ for source in 0..self.input_sources.Length() {
+ self.input_sources
+ .remove_input_source(self, InputId(source), can_gc);
+ }
+ p
+ }
+
+ // https://immersive-web.github.io/hit-test/#dom-xrsession-requesthittestsource
+ fn RequestHitTestSource(&self, options: &XRHitTestOptionsInit, can_gc: CanGc) -> Rc<Promise> {
+ let p = Promise::new(&self.global(), can_gc);
+
+ if !self
+ .session
+ .borrow()
+ .granted_features()
+ .iter()
+ .any(|f| f == "hit-test")
+ {
+ p.reject_error(Error::NotSupported);
+ return p;
+ }
+
+ let id = self.next_hit_test_id.get();
+ self.next_hit_test_id.set(HitTestId(id.0 + 1));
+
+ let space = options.space.space();
+ let ray = if let Some(ref ray) = options.offsetRay {
+ ray.ray()
+ } else {
+ Ray {
+ origin: Vector3D::new(0., 0., 0.),
+ direction: Vector3D::new(0., 0., -1.),
+ }
+ };
+
+ let mut types = EntityTypes::default();
+
+ if let Some(ref tys) = options.entityTypes {
+ for ty in tys {
+ match ty {
+ XRHitTestTrackableType::Point => types.point = true,
+ XRHitTestTrackableType::Plane => types.plane = true,
+ XRHitTestTrackableType::Mesh => types.mesh = true,
+ }
+ }
+ } else {
+ types.plane = true;
+ }
+
+ let source = HitTestSource {
+ id,
+ space,
+ ray,
+ types,
+ };
+ self.pending_hit_test_promises
+ .borrow_mut()
+ .insert(id, p.clone());
+
+ self.session.borrow().request_hit_test(source);
+
+ p
+ }
+
+ /// <https://www.w3.org/TR/webxr-ar-module-1/#dom-xrsession-interactionmode>
+ fn InteractionMode(&self) -> XRInteractionMode {
+ // Until Servo supports WebXR sessions on mobile phones or similar non-XR devices,
+ // this should always be world space
+ XRInteractionMode::World_space
+ }
+
+ /// <https://www.w3.org/TR/webxr/#dom-xrsession-framerate>
+ fn GetFrameRate(&self) -> Option<Finite<f32>> {
+ let session = self.session.borrow();
+ if self.mode == XRSessionMode::Inline || session.supported_frame_rates().is_empty() {
+ None
+ } else {
+ Finite::new(self.framerate.get())
+ }
+ }
+
+ /// <https://www.w3.org/TR/webxr/#dom-xrsession-supportedframerates>
+ fn GetSupportedFrameRates(&self, cx: JSContext) -> Option<Float32Array> {
+ let session = self.session.borrow();
+ if self.mode == XRSessionMode::Inline || session.supported_frame_rates().is_empty() {
+ None
+ } else {
+ let framerates = session.supported_frame_rates();
+ rooted!(in (*cx) let mut array = ptr::null_mut::<JSObject>());
+ Some(
+ create_buffer_source(cx, framerates, array.handle_mut())
+ .expect("Failed to construct supported frame rates array"),
+ )
+ }
+ }
+
+ /// <https://www.w3.org/TR/webxr/#dom-xrsession-enabledfeatures>
+ fn EnabledFeatures(&self, cx: JSContext, retval: MutableHandleValue) {
+ let session = self.session.borrow();
+ let features = session.granted_features();
+ to_frozen_array(features, cx, retval)
+ }
+
+ /// <https://www.w3.org/TR/webxr/#dom-xrsession-issystemkeyboardsupported>
+ fn IsSystemKeyboardSupported(&self) -> bool {
+ // Support for this only exists on Meta headsets (no desktop support)
+ // so this will always be false until that changes
+ false
+ }
+
+ /// <https://www.w3.org/TR/webxr/#dom-xrsession-updatetargetframerate>
+ fn UpdateTargetFrameRate(
+ &self,
+ rate: Finite<f32>,
+ comp: InRealm,
+ can_gc: CanGc,
+ ) -> Rc<Promise> {
+ let promise = Promise::new_in_current_realm(comp, can_gc);
+ {
+ let session = self.session.borrow();
+ let supported_frame_rates = session.supported_frame_rates();
+
+ if self.mode == XRSessionMode::Inline ||
+ supported_frame_rates.is_empty() ||
+ self.ended.get()
+ {
+ promise.reject_error(Error::InvalidState);
+ return promise;
+ }
+
+ if !supported_frame_rates.contains(&*rate) {
+ promise.reject_error(Error::Type("Provided framerate not supported".into()));
+ return promise;
+ }
+ }
+
+ *self.update_framerate_promise.borrow_mut() = Some(promise.clone());
+
+ let this = Trusted::new(self);
+ let global = self.global();
+ let window = global.as_window();
+ let (task_source, canceller) = window
+ .task_manager()
+ .dom_manipulation_task_source_with_canceller();
+ let (sender, receiver) = ipc::channel(global.time_profiler_chan().clone()).unwrap();
+
+ ROUTER.add_typed_route(
+ receiver.to_ipc_receiver(),
+ Box::new(move |message| {
+ let this = this.clone();
+ let _ = task_source.queue_with_canceller(
+ task!(update_session_framerate: move || {
+ let session = this.root();
+ session.apply_nominal_framerate(message.unwrap(), CanGc::note());
+ if let Some(promise) = session.update_framerate_promise.borrow_mut().take() {
+ promise.resolve_native(&());
+ };
+ }),
+ &canceller,
+ );
+ }),
+ );
+
+ self.session.borrow_mut().update_frame_rate(*rate, sender);
+
+ promise
+ }
+}
+
+// The pose of an object in native-space. Should never be exposed.
+pub type ApiPose = RigidTransform3D<f32, ApiSpace, webxr_api::Native>;
+// A transform between objects in some API-space
+pub type ApiRigidTransform = RigidTransform3D<f32, ApiSpace, ApiSpace>;
+
+#[derive(Clone, Copy)]
+pub struct BaseSpace;
+
+pub type BaseTransform = RigidTransform3D<f32, webxr_api::Native, BaseSpace>;
+
+#[allow(unsafe_code)]
+pub fn cast_transform<T, U, V, W>(
+ transform: RigidTransform3D<f32, T, U>,
+) -> RigidTransform3D<f32, V, W> {
+ unsafe { mem::transmute(transform) }
+}
+
+impl From<EnvironmentBlendMode> for XREnvironmentBlendMode {
+ fn from(x: EnvironmentBlendMode) -> Self {
+ match x {
+ EnvironmentBlendMode::Opaque => XREnvironmentBlendMode::Opaque,
+ EnvironmentBlendMode::AlphaBlend => XREnvironmentBlendMode::Alpha_blend,
+ EnvironmentBlendMode::Additive => XREnvironmentBlendMode::Additive,
+ }
+ }
+}
diff --git a/components/script/dom/webxr/xrsessionevent.rs b/components/script/dom/webxr/xrsessionevent.rs
new file mode 100644
index 00000000000..fda299a9f2a
--- /dev/null
+++ b/components/script/dom/webxr/xrsessionevent.rs
@@ -0,0 +1,100 @@
+/* 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 dom_struct::dom_struct;
+use js::rust::HandleObject;
+use servo_atoms::Atom;
+
+use crate::dom::bindings::codegen::Bindings::EventBinding::Event_Binding::EventMethods;
+use crate::dom::bindings::codegen::Bindings::XRSessionEventBinding::{self, XRSessionEventMethods};
+use crate::dom::bindings::error::Fallible;
+use crate::dom::bindings::inheritance::Castable;
+use crate::dom::bindings::reflector::{reflect_dom_object_with_proto, DomObject};
+use crate::dom::bindings::root::{Dom, DomRoot};
+use crate::dom::bindings::str::DOMString;
+use crate::dom::event::Event;
+use crate::dom::globalscope::GlobalScope;
+use crate::dom::window::Window;
+use crate::dom::xrsession::XRSession;
+use crate::script_runtime::CanGc;
+
+#[dom_struct]
+pub struct XRSessionEvent {
+ event: Event,
+ session: Dom<XRSession>,
+}
+
+impl XRSessionEvent {
+ #[allow(crown::unrooted_must_root)]
+ fn new_inherited(session: &XRSession) -> XRSessionEvent {
+ XRSessionEvent {
+ event: Event::new_inherited(),
+ session: Dom::from_ref(session),
+ }
+ }
+
+ pub fn new(
+ global: &GlobalScope,
+ type_: Atom,
+ bubbles: bool,
+ cancelable: bool,
+ session: &XRSession,
+ can_gc: CanGc,
+ ) -> DomRoot<XRSessionEvent> {
+ Self::new_with_proto(global, None, type_, bubbles, cancelable, session, can_gc)
+ }
+
+ fn new_with_proto(
+ global: &GlobalScope,
+ proto: Option<HandleObject>,
+ type_: Atom,
+ bubbles: bool,
+ cancelable: bool,
+ session: &XRSession,
+ can_gc: CanGc,
+ ) -> DomRoot<XRSessionEvent> {
+ let trackevent = reflect_dom_object_with_proto(
+ Box::new(XRSessionEvent::new_inherited(session)),
+ global,
+ proto,
+ can_gc,
+ );
+ {
+ let event = trackevent.upcast::<Event>();
+ event.init_event(type_, bubbles, cancelable);
+ }
+ trackevent
+ }
+}
+
+impl XRSessionEventMethods<crate::DomTypeHolder> for XRSessionEvent {
+ // https://immersive-web.github.io/webxr/#dom-xrsessionevent-xrsessionevent
+ fn Constructor(
+ window: &Window,
+ proto: Option<HandleObject>,
+ can_gc: CanGc,
+ type_: DOMString,
+ init: &XRSessionEventBinding::XRSessionEventInit,
+ ) -> Fallible<DomRoot<XRSessionEvent>> {
+ Ok(XRSessionEvent::new_with_proto(
+ &window.global(),
+ proto,
+ Atom::from(type_),
+ init.parent.bubbles,
+ init.parent.cancelable,
+ &init.session,
+ can_gc,
+ ))
+ }
+
+ // https://immersive-web.github.io/webxr/#dom-xrsessioneventinit-session
+ fn Session(&self) -> DomRoot<XRSession> {
+ DomRoot::from_ref(&*self.session)
+ }
+
+ // https://dom.spec.whatwg.org/#dom-event-istrusted
+ fn IsTrusted(&self) -> bool {
+ self.event.IsTrusted()
+ }
+}
diff --git a/components/script/dom/webxr/xrspace.rs b/components/script/dom/webxr/xrspace.rs
new file mode 100644
index 00000000000..d810d5c07f0
--- /dev/null
+++ b/components/script/dom/webxr/xrspace.rs
@@ -0,0 +1,120 @@
+/* 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 dom_struct::dom_struct;
+use euclid::RigidTransform3D;
+use webxr_api::{BaseSpace, Frame, Space};
+
+use crate::dom::bindings::inheritance::Castable;
+use crate::dom::bindings::reflector::reflect_dom_object;
+use crate::dom::bindings::root::{Dom, DomRoot, MutNullableDom};
+use crate::dom::eventtarget::EventTarget;
+use crate::dom::globalscope::GlobalScope;
+use crate::dom::xrinputsource::XRInputSource;
+use crate::dom::xrjointspace::XRJointSpace;
+use crate::dom::xrreferencespace::XRReferenceSpace;
+use crate::dom::xrsession::{cast_transform, ApiPose, XRSession};
+
+#[dom_struct]
+pub struct XRSpace {
+ eventtarget: EventTarget,
+ session: Dom<XRSession>,
+ input_source: MutNullableDom<XRInputSource>,
+ /// If we're an input space, are we an aim space or a grip space?
+ is_grip_space: bool,
+}
+
+impl XRSpace {
+ pub fn new_inherited(session: &XRSession) -> XRSpace {
+ XRSpace {
+ eventtarget: EventTarget::new_inherited(),
+ session: Dom::from_ref(session),
+ input_source: Default::default(),
+ is_grip_space: false,
+ }
+ }
+
+ fn new_inputspace_inner(
+ session: &XRSession,
+ input: &XRInputSource,
+ is_grip_space: bool,
+ ) -> XRSpace {
+ XRSpace {
+ eventtarget: EventTarget::new_inherited(),
+ session: Dom::from_ref(session),
+ input_source: MutNullableDom::new(Some(input)),
+ is_grip_space,
+ }
+ }
+
+ pub fn new_inputspace(
+ global: &GlobalScope,
+ session: &XRSession,
+ input: &XRInputSource,
+ is_grip_space: bool,
+ ) -> DomRoot<XRSpace> {
+ reflect_dom_object(
+ Box::new(XRSpace::new_inputspace_inner(session, input, is_grip_space)),
+ global,
+ )
+ }
+
+ pub fn space(&self) -> Space {
+ if let Some(rs) = self.downcast::<XRReferenceSpace>() {
+ rs.space()
+ } else if let Some(j) = self.downcast::<XRJointSpace>() {
+ j.space()
+ } else if let Some(source) = self.input_source.get() {
+ let base = if self.is_grip_space {
+ BaseSpace::Grip(source.id())
+ } else {
+ BaseSpace::TargetRay(source.id())
+ };
+ Space {
+ base,
+ offset: RigidTransform3D::identity(),
+ }
+ } else {
+ panic!("invalid space found")
+ }
+ }
+}
+
+impl XRSpace {
+ /// Gets pose represented by this space
+ ///
+ /// The reference origin used is common between all
+ /// get_pose calls for spaces from the same device, so this can be used to compare
+ /// with other spaces
+ pub fn get_pose(&self, base_pose: &Frame) -> Option<ApiPose> {
+ if let Some(reference) = self.downcast::<XRReferenceSpace>() {
+ reference.get_pose(base_pose)
+ } else if let Some(joint) = self.downcast::<XRJointSpace>() {
+ joint.get_pose(base_pose)
+ } else if let Some(source) = self.input_source.get() {
+ // XXXManishearth we should be able to request frame information
+ // for inputs when necessary instead of always loading it
+ //
+ // Also, the below code is quadratic, so this API may need an overhaul anyway
+ let id = source.id();
+ // XXXManishearth once we have dynamic inputs we'll need to handle this better
+ let frame = base_pose
+ .inputs
+ .iter()
+ .find(|i| i.id == id)
+ .expect("no input found");
+ if self.is_grip_space {
+ frame.grip_origin.map(cast_transform)
+ } else {
+ frame.target_ray_origin.map(cast_transform)
+ }
+ } else {
+ unreachable!()
+ }
+ }
+
+ pub fn session(&self) -> &XRSession {
+ &self.session
+ }
+}
diff --git a/components/script/dom/webxr/xrsubimage.rs b/components/script/dom/webxr/xrsubimage.rs
new file mode 100644
index 00000000000..53d2e3ca697
--- /dev/null
+++ b/components/script/dom/webxr/xrsubimage.rs
@@ -0,0 +1,23 @@
+/* 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 dom_struct::dom_struct;
+
+use crate::dom::bindings::codegen::Bindings::XRSubImageBinding::XRSubImage_Binding::XRSubImageMethods;
+use crate::dom::bindings::reflector::Reflector;
+use crate::dom::bindings::root::{Dom, DomRoot};
+use crate::dom::xrviewport::XRViewport;
+
+#[dom_struct]
+pub struct XRSubImage {
+ reflector: Reflector,
+ viewport: Dom<XRViewport>,
+}
+
+impl XRSubImageMethods<crate::DomTypeHolder> for XRSubImage {
+ /// <https://immersive-web.github.io/layers/#dom-xrsubimage-viewport>
+ fn Viewport(&self) -> DomRoot<XRViewport> {
+ DomRoot::from_ref(&self.viewport)
+ }
+}
diff --git a/components/script/dom/webxr/xrsystem.rs b/components/script/dom/webxr/xrsystem.rs
new file mode 100644
index 00000000000..21705357e0b
--- /dev/null
+++ b/components/script/dom/webxr/xrsystem.rs
@@ -0,0 +1,334 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+use std::cell::Cell;
+use std::rc::Rc;
+
+use base::id::PipelineId;
+use dom_struct::dom_struct;
+use ipc_channel::ipc::{self as ipc_crate, IpcReceiver};
+use ipc_channel::router::ROUTER;
+use profile_traits::ipc;
+use servo_config::pref;
+use webxr_api::{Error as XRError, Frame, Session, SessionInit, SessionMode};
+
+use crate::dom::bindings::cell::DomRefCell;
+use crate::dom::bindings::codegen::Bindings::XRSystemBinding::{
+ XRSessionInit, XRSessionMode, XRSystemMethods,
+};
+use crate::dom::bindings::conversions::{ConversionResult, FromJSValConvertible};
+use crate::dom::bindings::error::Error;
+use crate::dom::bindings::inheritance::Castable;
+use crate::dom::bindings::refcounted::{Trusted, TrustedPromise};
+use crate::dom::bindings::reflector::{reflect_dom_object, DomObject};
+use crate::dom::bindings::root::{Dom, DomRoot, MutNullableDom};
+use crate::dom::bindings::trace::RootedTraceableBox;
+use crate::dom::eventtarget::EventTarget;
+use crate::dom::gamepad::Gamepad;
+use crate::dom::globalscope::GlobalScope;
+use crate::dom::promise::Promise;
+use crate::dom::window::Window;
+use crate::dom::xrsession::XRSession;
+use crate::dom::xrtest::XRTest;
+use crate::realms::InRealm;
+use crate::script_runtime::CanGc;
+use crate::script_thread::ScriptThread;
+use crate::task_source::TaskSource;
+
+#[dom_struct]
+pub struct XRSystem {
+ eventtarget: EventTarget,
+ gamepads: DomRefCell<Vec<Dom<Gamepad>>>,
+ pending_immersive_session: Cell<bool>,
+ active_immersive_session: MutNullableDom<XRSession>,
+ active_inline_sessions: DomRefCell<Vec<Dom<XRSession>>>,
+ test: MutNullableDom<XRTest>,
+ #[no_trace]
+ pipeline: PipelineId,
+}
+
+impl XRSystem {
+ fn new_inherited(pipeline: PipelineId) -> XRSystem {
+ XRSystem {
+ eventtarget: EventTarget::new_inherited(),
+ gamepads: DomRefCell::new(Vec::new()),
+ pending_immersive_session: Cell::new(false),
+ active_immersive_session: Default::default(),
+ active_inline_sessions: DomRefCell::new(Vec::new()),
+ test: Default::default(),
+ pipeline,
+ }
+ }
+
+ pub fn new(window: &Window) -> DomRoot<XRSystem> {
+ reflect_dom_object(
+ Box::new(XRSystem::new_inherited(window.pipeline_id())),
+ window,
+ )
+ }
+
+ pub fn pending_or_active_session(&self) -> bool {
+ self.pending_immersive_session.get() || self.active_immersive_session.get().is_some()
+ }
+
+ pub fn set_pending(&self) {
+ self.pending_immersive_session.set(true)
+ }
+
+ pub fn set_active_immersive_session(&self, session: &XRSession) {
+ // XXXManishearth when we support non-immersive (inline) sessions we should
+ // ensure they never reach these codepaths
+ self.pending_immersive_session.set(false);
+ self.active_immersive_session.set(Some(session))
+ }
+
+ /// <https://immersive-web.github.io/webxr/#ref-for-eventdef-xrsession-end>
+ pub fn end_session(&self, session: &XRSession) {
+ // Step 3
+ if let Some(active) = self.active_immersive_session.get() {
+ if Dom::from_ref(&*active) == Dom::from_ref(session) {
+ self.active_immersive_session.set(None);
+ // Dirty the canvas, since it has been skipping this step whilst in immersive
+ // mode
+ session.dirty_layers();
+ }
+ }
+ self.active_inline_sessions
+ .borrow_mut()
+ .retain(|sess| Dom::from_ref(&**sess) != Dom::from_ref(session));
+ }
+}
+
+impl From<XRSessionMode> for SessionMode {
+ fn from(mode: XRSessionMode) -> SessionMode {
+ match mode {
+ XRSessionMode::Immersive_vr => SessionMode::ImmersiveVR,
+ XRSessionMode::Immersive_ar => SessionMode::ImmersiveAR,
+ XRSessionMode::Inline => SessionMode::Inline,
+ }
+ }
+}
+
+impl XRSystemMethods<crate::DomTypeHolder> for XRSystem {
+ /// <https://immersive-web.github.io/webxr/#dom-xr-issessionsupported>
+ fn IsSessionSupported(&self, mode: XRSessionMode, can_gc: CanGc) -> Rc<Promise> {
+ // XXXManishearth this should select an XR device first
+ let promise = Promise::new(&self.global(), can_gc);
+ let mut trusted = Some(TrustedPromise::new(promise.clone()));
+ let global = self.global();
+ let window = global.as_window();
+ let (task_source, canceller) = window
+ .task_manager()
+ .dom_manipulation_task_source_with_canceller();
+ let (sender, receiver) = ipc::channel(global.time_profiler_chan().clone()).unwrap();
+ ROUTER.add_typed_route(
+ receiver.to_ipc_receiver(),
+ Box::new(move |message| {
+ // router doesn't know this is only called once
+ let trusted = if let Some(trusted) = trusted.take() {
+ trusted
+ } else {
+ error!("supportsSession callback called twice!");
+ return;
+ };
+ let message: Result<(), webxr_api::Error> = if let Ok(message) = message {
+ message
+ } else {
+ error!("supportsSession callback given incorrect payload");
+ return;
+ };
+ if let Ok(()) = message {
+ let _ =
+ task_source.queue_with_canceller(trusted.resolve_task(true), &canceller);
+ } else {
+ let _ =
+ task_source.queue_with_canceller(trusted.resolve_task(false), &canceller);
+ };
+ }),
+ );
+ if let Some(mut r) = window.webxr_registry() {
+ r.supports_session(mode.into(), sender);
+ }
+
+ promise
+ }
+
+ /// <https://immersive-web.github.io/webxr/#dom-xr-requestsession>
+ #[allow(unsafe_code)]
+ fn RequestSession(
+ &self,
+ mode: XRSessionMode,
+ init: RootedTraceableBox<XRSessionInit>,
+ comp: InRealm,
+ can_gc: CanGc,
+ ) -> Rc<Promise> {
+ let global = self.global();
+ let window = global.as_window();
+ let promise = Promise::new_in_current_realm(comp, can_gc);
+
+ if mode != XRSessionMode::Inline {
+ if !ScriptThread::is_user_interacting() {
+ if pref!(dom.webxr.unsafe_assume_user_intent) {
+ warn!("The dom.webxr.unsafe-assume-user-intent preference assumes user intent to enter WebXR.");
+ } else {
+ promise.reject_error(Error::Security);
+ return promise;
+ }
+ }
+
+ if self.pending_or_active_session() {
+ promise.reject_error(Error::InvalidState);
+ return promise;
+ }
+
+ self.set_pending();
+ }
+
+ let mut required_features = vec![];
+ let mut optional_features = vec![];
+ let cx = GlobalScope::get_cx();
+
+ if let Some(ref r) = init.requiredFeatures {
+ for feature in r {
+ unsafe {
+ if let Ok(ConversionResult::Success(s)) =
+ String::from_jsval(*cx, feature.handle(), ())
+ {
+ required_features.push(s)
+ } else {
+ warn!("Unable to convert required feature to string");
+ if mode != XRSessionMode::Inline {
+ self.pending_immersive_session.set(false);
+ }
+ promise.reject_error(Error::NotSupported);
+ return promise;
+ }
+ }
+ }
+ }
+
+ if let Some(ref o) = init.optionalFeatures {
+ for feature in o {
+ unsafe {
+ if let Ok(ConversionResult::Success(s)) =
+ String::from_jsval(*cx, feature.handle(), ())
+ {
+ optional_features.push(s)
+ } else {
+ warn!("Unable to convert optional feature to string");
+ }
+ }
+ }
+ }
+
+ if !required_features.contains(&"viewer".to_string()) {
+ required_features.push("viewer".to_string());
+ }
+
+ if !required_features.contains(&"local".to_string()) && mode != XRSessionMode::Inline {
+ required_features.push("local".to_string());
+ }
+
+ let init = SessionInit {
+ required_features,
+ optional_features,
+ first_person_observer_view: pref!(dom.webxr.first_person_observer_view),
+ };
+
+ let mut trusted = Some(TrustedPromise::new(promise.clone()));
+ let this = Trusted::new(self);
+ let (task_source, canceller) = window
+ .task_manager()
+ .dom_manipulation_task_source_with_canceller();
+ let (sender, receiver) = ipc::channel(global.time_profiler_chan().clone()).unwrap();
+ let (frame_sender, frame_receiver) = ipc_crate::channel().unwrap();
+ let mut frame_receiver = Some(frame_receiver);
+ ROUTER.add_typed_route(
+ receiver.to_ipc_receiver(),
+ Box::new(move |message| {
+ // router doesn't know this is only called once
+ let trusted = trusted.take().unwrap();
+ let this = this.clone();
+ let frame_receiver = frame_receiver.take().unwrap();
+ let message: Result<Session, webxr_api::Error> = if let Ok(message) = message {
+ message
+ } else {
+ error!("requestSession callback given incorrect payload");
+ return;
+ };
+ let _ = task_source.queue_with_canceller(
+ task!(request_session: move || {
+ this.root().session_obtained(message, trusted.root(), mode, frame_receiver);
+ }),
+ &canceller,
+ );
+ }),
+ );
+ if let Some(mut r) = window.webxr_registry() {
+ r.request_session(mode.into(), init, sender, frame_sender);
+ }
+ promise
+ }
+
+ // https://github.com/immersive-web/webxr-test-api/blob/master/explainer.md
+ fn Test(&self) -> DomRoot<XRTest> {
+ self.test.or_init(|| XRTest::new(&self.global()))
+ }
+}
+
+impl XRSystem {
+ fn session_obtained(
+ &self,
+ response: Result<Session, XRError>,
+ promise: Rc<Promise>,
+ mode: XRSessionMode,
+ frame_receiver: IpcReceiver<Frame>,
+ ) {
+ let session = match response {
+ Ok(session) => session,
+ Err(e) => {
+ warn!("Error requesting XR session: {:?}", e);
+ if mode != XRSessionMode::Inline {
+ self.pending_immersive_session.set(false);
+ }
+ promise.reject_error(Error::NotSupported);
+ return;
+ },
+ };
+ let session = XRSession::new(&self.global(), session, mode, frame_receiver);
+ if mode == XRSessionMode::Inline {
+ self.active_inline_sessions
+ .borrow_mut()
+ .push(Dom::from_ref(&*session));
+ } else {
+ self.set_active_immersive_session(&session);
+ }
+ promise.resolve_native(&session);
+ // https://github.com/immersive-web/webxr/issues/961
+ // This must be called _after_ the promise is resolved
+ session.setup_initial_inputs();
+ }
+
+ // https://github.com/immersive-web/navigation/issues/10
+ pub fn dispatch_sessionavailable(&self) {
+ let xr = Trusted::new(self);
+ let global = self.global();
+ let window = global.as_window();
+ window
+ .task_manager()
+ .dom_manipulation_task_source()
+ .queue(
+ task!(fire_sessionavailable_event: move || {
+ // The sessionavailable event indicates user intent to enter an XR session
+ let xr = xr.root();
+ let interacting = ScriptThread::is_user_interacting();
+ ScriptThread::set_user_interacting(true);
+ xr.upcast::<EventTarget>().fire_bubbling_event(atom!("sessionavailable"), CanGc::note());
+ ScriptThread::set_user_interacting(interacting);
+ }),
+ window.upcast(),
+ )
+ .unwrap();
+ }
+}
diff --git a/components/script/dom/webxr/xrtest.rs b/components/script/dom/webxr/xrtest.rs
new file mode 100644
index 00000000000..0117ab5c3ce
--- /dev/null
+++ b/components/script/dom/webxr/xrtest.rs
@@ -0,0 +1,235 @@
+/* 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 Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+use std::rc::Rc;
+
+use dom_struct::dom_struct;
+use ipc_channel::ipc::IpcSender;
+use ipc_channel::router::ROUTER;
+use js::jsval::JSVal;
+use profile_traits::ipc;
+use webxr_api::{self, Error as XRError, MockDeviceInit, MockDeviceMsg};
+
+use crate::dom::bindings::callback::ExceptionHandling;
+use crate::dom::bindings::cell::DomRefCell;
+use crate::dom::bindings::codegen::Bindings::FunctionBinding::Function;
+use crate::dom::bindings::codegen::Bindings::XRSystemBinding::XRSessionMode;
+use crate::dom::bindings::codegen::Bindings::XRTestBinding::{FakeXRDeviceInit, XRTestMethods};
+use crate::dom::bindings::refcounted::{Trusted, TrustedPromise};
+use crate::dom::bindings::reflector::{reflect_dom_object, DomObject, Reflector};
+use crate::dom::bindings::root::{Dom, DomRoot};
+use crate::dom::fakexrdevice::{get_origin, get_views, get_world, FakeXRDevice};
+use crate::dom::globalscope::GlobalScope;
+use crate::dom::promise::Promise;
+use crate::script_runtime::CanGc;
+use crate::script_thread::ScriptThread;
+use crate::task_source::TaskSource;
+
+#[dom_struct]
+pub struct XRTest {
+ reflector: Reflector,
+ devices_connected: DomRefCell<Vec<Dom<FakeXRDevice>>>,
+}
+
+impl XRTest {
+ pub fn new_inherited() -> XRTest {
+ XRTest {
+ reflector: Reflector::new(),
+ devices_connected: DomRefCell::new(vec![]),
+ }
+ }
+
+ pub fn new(global: &GlobalScope) -> DomRoot<XRTest> {
+ reflect_dom_object(Box::new(XRTest::new_inherited()), global)
+ }
+
+ fn device_obtained(
+ &self,
+ response: Result<IpcSender<MockDeviceMsg>, XRError>,
+ trusted: TrustedPromise,
+ ) {
+ let promise = trusted.root();
+ if let Ok(sender) = response {
+ let device = FakeXRDevice::new(&self.global(), sender);
+ self.devices_connected
+ .borrow_mut()
+ .push(Dom::from_ref(&device));
+ promise.resolve_native(&device);
+ } else {
+ promise.reject_native(&());
+ }
+ }
+}
+
+impl XRTestMethods<crate::DomTypeHolder> for XRTest {
+ /// <https://github.com/immersive-web/webxr-test-api/blob/master/explainer.md>
+ #[allow(unsafe_code)]
+ fn SimulateDeviceConnection(&self, init: &FakeXRDeviceInit, can_gc: CanGc) -> Rc<Promise> {
+ let global = self.global();
+ let p = Promise::new(&global, can_gc);
+
+ let origin = if let Some(ref o) = init.viewerOrigin {
+ match get_origin(o) {
+ Ok(origin) => Some(origin),
+ Err(e) => {
+ p.reject_error(e);
+ return p;
+ },
+ }
+ } else {
+ None
+ };
+
+ let floor_origin = if let Some(ref o) = init.floorOrigin {
+ match get_origin(o) {
+ Ok(origin) => Some(origin),
+ Err(e) => {
+ p.reject_error(e);
+ return p;
+ },
+ }
+ } else {
+ None
+ };
+
+ let views = match get_views(&init.views) {
+ Ok(views) => views,
+ Err(e) => {
+ p.reject_error(e);
+ return p;
+ },
+ };
+
+ let supported_features = if let Some(ref s) = init.supportedFeatures {
+ s.iter().cloned().map(String::from).collect()
+ } else {
+ vec![]
+ };
+
+ let world = if let Some(ref w) = init.world {
+ let w = match get_world(w) {
+ Ok(w) => w,
+ Err(e) => {
+ p.reject_error(e);
+ return p;
+ },
+ };
+ Some(w)
+ } else {
+ None
+ };
+
+ let (mut supports_inline, mut supports_vr, mut supports_ar) = (false, false, false);
+
+ if let Some(ref modes) = init.supportedModes {
+ for mode in modes {
+ match mode {
+ XRSessionMode::Immersive_vr => supports_vr = true,
+ XRSessionMode::Immersive_ar => supports_ar = true,
+ XRSessionMode::Inline => supports_inline = true,
+ }
+ }
+ }
+
+ let init = MockDeviceInit {
+ viewer_origin: origin,
+ views,
+ supports_inline,
+ supports_vr,
+ supports_ar,
+ floor_origin,
+ supported_features,
+ world,
+ };
+
+ let global = self.global();
+ let window = global.as_window();
+ let this = Trusted::new(self);
+ let mut trusted = Some(TrustedPromise::new(p.clone()));
+
+ let (task_source, canceller) = window
+ .task_manager()
+ .dom_manipulation_task_source_with_canceller();
+ let (sender, receiver) = ipc::channel(global.time_profiler_chan().clone()).unwrap();
+
+ ROUTER.add_typed_route(
+ receiver.to_ipc_receiver(),
+ Box::new(move |message| {
+ let trusted = trusted
+ .take()
+ .expect("SimulateDeviceConnection callback called twice");
+ let this = this.clone();
+ let message =
+ message.expect("SimulateDeviceConnection callback given incorrect payload");
+
+ let _ = task_source.queue_with_canceller(
+ task!(request_session: move || {
+ this.root().device_obtained(message, trusted);
+ }),
+ &canceller,
+ );
+ }),
+ );
+ if let Some(mut r) = window.webxr_registry() {
+ r.simulate_device_connection(init, sender);
+ }
+
+ p
+ }
+
+ /// <https://github.com/immersive-web/webxr-test-api/blob/master/explainer.md>
+ fn SimulateUserActivation(&self, f: Rc<Function>) {
+ ScriptThread::set_user_interacting(true);
+ rooted!(in(*GlobalScope::get_cx()) let mut value: JSVal);
+ let _ = f.Call__(vec![], value.handle_mut(), ExceptionHandling::Rethrow);
+ ScriptThread::set_user_interacting(false);
+ }
+
+ /// <https://github.com/immersive-web/webxr-test-api/blob/master/explainer.md>
+ fn DisconnectAllDevices(&self, can_gc: CanGc) -> Rc<Promise> {
+ // XXXManishearth implement device disconnection and session ending
+ let global = self.global();
+ let p = Promise::new(&global, can_gc);
+ let mut devices = self.devices_connected.borrow_mut();
+ if devices.is_empty() {
+ p.resolve_native(&());
+ } else {
+ let mut len = devices.len();
+
+ let (sender, receiver) = ipc::channel(global.time_profiler_chan().clone()).unwrap();
+ let mut rooted_devices: Vec<_> =
+ devices.iter().map(|x| DomRoot::from_ref(&**x)).collect();
+ devices.clear();
+
+ let mut trusted = Some(TrustedPromise::new(p.clone()));
+ let (task_source, canceller) = global
+ .as_window()
+ .task_manager()
+ .dom_manipulation_task_source_with_canceller();
+
+ ROUTER.add_typed_route(
+ receiver.to_ipc_receiver(),
+ Box::new(move |_| {
+ len -= 1;
+ if len == 0 {
+ let trusted = trusted
+ .take()
+ .expect("DisconnectAllDevices disconnected more devices than expected");
+ let _ =
+ task_source.queue_with_canceller(trusted.resolve_task(()), &canceller);
+ }
+ }),
+ );
+
+ for device in rooted_devices.drain(..) {
+ device.disconnect(sender.clone());
+ }
+ };
+ p
+ }
+}
diff --git a/components/script/dom/webxr/xrview.rs b/components/script/dom/webxr/xrview.rs
new file mode 100644
index 00000000000..89c5f7d7868
--- /dev/null
+++ b/components/script/dom/webxr/xrview.rs
@@ -0,0 +1,137 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+use std::cell::Cell;
+
+use dom_struct::dom_struct;
+use euclid::RigidTransform3D;
+use js::typedarray::{Float32, Float32Array};
+use webxr_api::{ApiSpace, View};
+
+use crate::dom::bindings::buffer_source::HeapBufferSource;
+use crate::dom::bindings::codegen::Bindings::XRViewBinding::{XREye, XRViewMethods};
+use crate::dom::bindings::num::Finite;
+use crate::dom::bindings::reflector::{reflect_dom_object, Reflector};
+use crate::dom::bindings::root::{Dom, DomRoot};
+use crate::dom::globalscope::GlobalScope;
+use crate::dom::xrrigidtransform::XRRigidTransform;
+use crate::dom::xrsession::{cast_transform, BaseSpace, BaseTransform, XRSession};
+use crate::script_runtime::{CanGc, JSContext};
+
+#[dom_struct]
+pub struct XRView {
+ reflector_: Reflector,
+ session: Dom<XRSession>,
+ eye: XREye,
+ viewport_index: usize,
+ #[ignore_malloc_size_of = "mozjs"]
+ proj: HeapBufferSource<Float32>,
+ #[ignore_malloc_size_of = "defined in rust-webxr"]
+ #[no_trace]
+ view: View<ApiSpace>,
+ transform: Dom<XRRigidTransform>,
+ requested_viewport_scale: Cell<f64>,
+}
+
+impl XRView {
+ fn new_inherited(
+ session: &XRSession,
+ transform: &XRRigidTransform,
+ eye: XREye,
+ viewport_index: usize,
+ view: View<ApiSpace>,
+ ) -> XRView {
+ XRView {
+ reflector_: Reflector::new(),
+ session: Dom::from_ref(session),
+ eye,
+ viewport_index,
+ proj: HeapBufferSource::default(),
+ view,
+ transform: Dom::from_ref(transform),
+ requested_viewport_scale: Cell::new(1.0),
+ }
+ }
+
+ pub fn new<V: Copy>(
+ global: &GlobalScope,
+ session: &XRSession,
+ view: &View<V>,
+ eye: XREye,
+ viewport_index: usize,
+ to_base: &BaseTransform,
+ can_gc: CanGc,
+ ) -> DomRoot<XRView> {
+ let transform: RigidTransform3D<f32, V, BaseSpace> = view.transform.then(to_base);
+ let transform = XRRigidTransform::new(global, cast_transform(transform), can_gc);
+
+ reflect_dom_object(
+ Box::new(XRView::new_inherited(
+ session,
+ &transform,
+ eye,
+ viewport_index,
+ view.cast_unit(),
+ )),
+ global,
+ )
+ }
+
+ pub fn session(&self) -> &XRSession {
+ &self.session
+ }
+
+ pub fn viewport_index(&self) -> usize {
+ self.viewport_index
+ }
+}
+
+impl XRViewMethods<crate::DomTypeHolder> for XRView {
+ /// <https://immersive-web.github.io/webxr/#dom-xrview-eye>
+ fn Eye(&self) -> XREye {
+ self.eye
+ }
+
+ /// <https://immersive-web.github.io/webxr/#dom-xrview-projectionmatrix>
+ fn ProjectionMatrix(&self, _cx: JSContext) -> Float32Array {
+ if !self.proj.is_initialized() {
+ let cx = GlobalScope::get_cx();
+ // row_major since euclid uses row vectors
+ let proj = self.view.projection.to_array();
+ self.proj
+ .set_data(cx, &proj)
+ .expect("Failed to set projection matrix.")
+ }
+ self.proj
+ .get_buffer()
+ .expect("Failed to get projection matrix.")
+ }
+
+ /// <https://immersive-web.github.io/webxr/#dom-xrview-transform>
+ fn Transform(&self) -> DomRoot<XRRigidTransform> {
+ DomRoot::from_ref(&self.transform)
+ }
+
+ /// <https://www.w3.org/TR/webxr/#dom-xrview-recommendedviewportscale>
+ fn GetRecommendedViewportScale(&self) -> Option<Finite<f64>> {
+ // Just return 1.0 since we currently will always use full-sized viewports
+ Finite::new(1.0)
+ }
+
+ /// <https://www.w3.org/TR/webxr/#dom-xrview-requestviewportscale>
+ fn RequestViewportScale(&self, scale: Option<Finite<f64>>) {
+ if let Some(scale) = scale {
+ if *scale > 0.0 {
+ let clamped_scale = scale.clamp(0.0, 1.0);
+ self.requested_viewport_scale.set(clamped_scale);
+ }
+ }
+ }
+
+ /// <https://www.w3.org/TR/webxr-ar-module-1/#dom-xrview-isfirstpersonobserver>
+ fn IsFirstPersonObserver(&self) -> bool {
+ // Servo is not currently supported anywhere that supports this, so return false
+ false
+ }
+}
diff --git a/components/script/dom/webxr/xrviewerpose.rs b/components/script/dom/webxr/xrviewerpose.rs
new file mode 100644
index 00000000000..47b166ab375
--- /dev/null
+++ b/components/script/dom/webxr/xrviewerpose.rs
@@ -0,0 +1,196 @@
+/* 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 dom_struct::dom_struct;
+use euclid::RigidTransform3D;
+use js::conversions::ToJSValConvertible;
+use js::jsapi::Heap;
+use js::jsval::{JSVal, UndefinedValue};
+use js::rust::MutableHandleValue;
+use webxr_api::{Viewer, ViewerPose, Views};
+
+use crate::dom::bindings::codegen::Bindings::XRViewBinding::XREye;
+use crate::dom::bindings::codegen::Bindings::XRViewerPoseBinding::XRViewerPoseMethods;
+use crate::dom::bindings::reflector::reflect_dom_object;
+use crate::dom::bindings::root::DomRoot;
+use crate::dom::globalscope::GlobalScope;
+use crate::dom::xrpose::XRPose;
+use crate::dom::xrrigidtransform::XRRigidTransform;
+use crate::dom::xrsession::{cast_transform, BaseSpace, BaseTransform, XRSession};
+use crate::dom::xrview::XRView;
+use crate::realms::enter_realm;
+use crate::script_runtime::{CanGc, JSContext};
+
+#[dom_struct]
+pub struct XRViewerPose {
+ pose: XRPose,
+ #[ignore_malloc_size_of = "mozjs"]
+ views: Heap<JSVal>,
+}
+
+impl XRViewerPose {
+ fn new_inherited(transform: &XRRigidTransform) -> XRViewerPose {
+ XRViewerPose {
+ pose: XRPose::new_inherited(transform),
+ views: Heap::default(),
+ }
+ }
+
+ #[allow(unsafe_code)]
+ pub fn new(
+ global: &GlobalScope,
+ session: &XRSession,
+ to_base: BaseTransform,
+ viewer_pose: &ViewerPose,
+ can_gc: CanGc,
+ ) -> DomRoot<XRViewerPose> {
+ let _ac = enter_realm(global);
+ rooted_vec!(let mut views);
+ match &viewer_pose.views {
+ Views::Inline => views.push(XRView::new(
+ global,
+ session,
+ &session.inline_view(),
+ XREye::None,
+ 0,
+ &to_base,
+ can_gc,
+ )),
+ Views::Mono(view) => views.push(XRView::new(
+ global,
+ session,
+ view,
+ XREye::None,
+ 0,
+ &to_base,
+ can_gc,
+ )),
+ Views::Stereo(left, right) => {
+ views.push(XRView::new(
+ global,
+ session,
+ left,
+ XREye::Left,
+ 0,
+ &to_base,
+ can_gc,
+ ));
+ views.push(XRView::new(
+ global,
+ session,
+ right,
+ XREye::Right,
+ 1,
+ &to_base,
+ can_gc,
+ ));
+ },
+ Views::StereoCapture(left, right, third_eye) => {
+ views.push(XRView::new(
+ global,
+ session,
+ left,
+ XREye::Left,
+ 0,
+ &to_base,
+ can_gc,
+ ));
+ views.push(XRView::new(
+ global,
+ session,
+ right,
+ XREye::Right,
+ 1,
+ &to_base,
+ can_gc,
+ ));
+ views.push(XRView::new(
+ global,
+ session,
+ third_eye,
+ XREye::None,
+ 2,
+ &to_base,
+ can_gc,
+ ));
+ },
+ Views::Cubemap(front, left, right, top, bottom, back) => {
+ views.push(XRView::new(
+ global,
+ session,
+ front,
+ XREye::None,
+ 0,
+ &to_base,
+ can_gc,
+ ));
+ views.push(XRView::new(
+ global,
+ session,
+ left,
+ XREye::None,
+ 1,
+ &to_base,
+ can_gc,
+ ));
+ views.push(XRView::new(
+ global,
+ session,
+ right,
+ XREye::None,
+ 2,
+ &to_base,
+ can_gc,
+ ));
+ views.push(XRView::new(
+ global,
+ session,
+ top,
+ XREye::None,
+ 3,
+ &to_base,
+ can_gc,
+ ));
+ views.push(XRView::new(
+ global,
+ session,
+ bottom,
+ XREye::None,
+ 4,
+ &to_base,
+ can_gc,
+ ));
+ views.push(XRView::new(
+ global,
+ session,
+ back,
+ XREye::None,
+ 5,
+ &to_base,
+ can_gc,
+ ));
+ },
+ };
+ let transform: RigidTransform3D<f32, Viewer, BaseSpace> =
+ viewer_pose.transform.then(&to_base);
+ let transform = XRRigidTransform::new(global, cast_transform(transform), can_gc);
+ let pose = reflect_dom_object(Box::new(XRViewerPose::new_inherited(&transform)), global);
+
+ let cx = GlobalScope::get_cx();
+ unsafe {
+ rooted!(in(*cx) let mut jsval = UndefinedValue());
+ views.to_jsval(*cx, jsval.handle_mut());
+ pose.views.set(jsval.get());
+ }
+
+ pose
+ }
+}
+
+impl XRViewerPoseMethods<crate::DomTypeHolder> for XRViewerPose {
+ /// <https://immersive-web.github.io/webxr/#dom-xrviewerpose-views>
+ fn Views(&self, _cx: JSContext, mut retval: MutableHandleValue) {
+ retval.set(self.views.get())
+ }
+}
diff --git a/components/script/dom/webxr/xrviewport.rs b/components/script/dom/webxr/xrviewport.rs
new file mode 100644
index 00000000000..8ac28519620
--- /dev/null
+++ b/components/script/dom/webxr/xrviewport.rs
@@ -0,0 +1,54 @@
+/* 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 dom_struct::dom_struct;
+use euclid::Rect;
+use webxr_api::Viewport;
+
+use crate::dom::bindings::codegen::Bindings::XRViewportBinding::XRViewportMethods;
+use crate::dom::bindings::reflector::{reflect_dom_object, Reflector};
+use crate::dom::bindings::root::DomRoot;
+use crate::dom::globalscope::GlobalScope;
+
+#[dom_struct]
+pub struct XRViewport {
+ reflector_: Reflector,
+ #[no_trace]
+ viewport: Rect<i32, Viewport>,
+}
+
+impl XRViewport {
+ fn new_inherited(viewport: Rect<i32, Viewport>) -> XRViewport {
+ XRViewport {
+ reflector_: Reflector::new(),
+ viewport,
+ }
+ }
+
+ pub fn new(global: &GlobalScope, viewport: Rect<i32, Viewport>) -> DomRoot<XRViewport> {
+ reflect_dom_object(Box::new(XRViewport::new_inherited(viewport)), global)
+ }
+}
+
+impl XRViewportMethods<crate::DomTypeHolder> for XRViewport {
+ /// <https://immersive-web.github.io/webxr/#dom-xrviewport-x>
+ fn X(&self) -> i32 {
+ self.viewport.origin.x
+ }
+
+ /// <https://immersive-web.github.io/webxr/#dom-xrviewport-y>
+ fn Y(&self) -> i32 {
+ self.viewport.origin.y
+ }
+
+ /// <https://immersive-web.github.io/webxr/#dom-xrviewport-width>
+ fn Width(&self) -> i32 {
+ self.viewport.size.width
+ }
+
+ /// <https://immersive-web.github.io/webxr/#dom-xrviewport-height>
+ fn Height(&self) -> i32 {
+ self.viewport.size.height
+ }
+}
diff --git a/components/script/dom/webxr/xrwebglbinding.rs b/components/script/dom/webxr/xrwebglbinding.rs
new file mode 100644
index 00000000000..c2d652bb81d
--- /dev/null
+++ b/components/script/dom/webxr/xrwebglbinding.rs
@@ -0,0 +1,168 @@
+/* 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 dom_struct::dom_struct;
+use js::rust::HandleObject;
+
+use crate::dom::bindings::codegen::Bindings::XRViewBinding::XREye;
+use crate::dom::bindings::codegen::Bindings::XRWebGLBindingBinding::XRWebGLBinding_Binding::XRWebGLBindingMethods;
+use crate::dom::bindings::codegen::Bindings::WebGLRenderingContextBinding::WebGLRenderingContext_Binding::WebGLRenderingContextMethods;
+use crate::dom::bindings::codegen::Bindings::XRWebGLBindingBinding::{
+ XRCubeLayerInit, XRCylinderLayerInit, XREquirectLayerInit, XRProjectionLayerInit,
+ XRQuadLayerInit, XRTextureType,
+};
+use crate::dom::bindings::codegen::UnionTypes::WebGLRenderingContextOrWebGL2RenderingContext;
+use crate::dom::bindings::error::{Error, Fallible};
+use crate::dom::bindings::reflector::{reflect_dom_object_with_proto, Reflector};
+use crate::dom::bindings::root::{Dom, DomRoot};
+use crate::dom::webglrenderingcontext::WebGLRenderingContext;
+use crate::dom::window::Window;
+use crate::dom::xrcompositionlayer::XRCompositionLayer;
+use crate::dom::xrcubelayer::XRCubeLayer;
+use crate::dom::xrcylinderlayer::XRCylinderLayer;
+use crate::dom::xrequirectlayer::XREquirectLayer;
+use crate::dom::xrframe::XRFrame;
+use crate::dom::xrprojectionlayer::XRProjectionLayer;
+use crate::dom::xrquadlayer::XRQuadLayer;
+use crate::dom::xrsession::XRSession;
+use crate::dom::xrview::XRView;
+use crate::dom::xrwebglsubimage::XRWebGLSubImage;
+use crate::script_runtime::CanGc;
+
+#[dom_struct]
+pub struct XRWebGLBinding {
+ reflector: Reflector,
+ session: Dom<XRSession>,
+ context: Dom<WebGLRenderingContext>,
+}
+
+impl XRWebGLBinding {
+ pub fn new_inherited(session: &XRSession, context: &WebGLRenderingContext) -> XRWebGLBinding {
+ XRWebGLBinding {
+ reflector: Reflector::new(),
+ session: Dom::from_ref(session),
+ context: Dom::from_ref(context),
+ }
+ }
+
+ fn new(
+ global: &Window,
+ proto: Option<HandleObject>,
+ session: &XRSession,
+ context: &WebGLRenderingContext,
+ can_gc: CanGc,
+ ) -> DomRoot<XRWebGLBinding> {
+ reflect_dom_object_with_proto(
+ Box::new(XRWebGLBinding::new_inherited(session, context)),
+ global,
+ proto,
+ can_gc,
+ )
+ }
+}
+
+impl XRWebGLBindingMethods<crate::DomTypeHolder> for XRWebGLBinding {
+ /// <https://immersive-web.github.io/layers/#dom-xrwebglbinding-xrwebglbinding>
+ fn Constructor(
+ global: &Window,
+ proto: Option<HandleObject>,
+ can_gc: CanGc,
+ session: &XRSession,
+ context: WebGLRenderingContextOrWebGL2RenderingContext,
+ ) -> Fallible<DomRoot<XRWebGLBinding>> {
+ let context = match context {
+ WebGLRenderingContextOrWebGL2RenderingContext::WebGLRenderingContext(ctx) => ctx,
+ WebGLRenderingContextOrWebGL2RenderingContext::WebGL2RenderingContext(ctx) => {
+ ctx.base_context()
+ },
+ };
+ // Step 2
+ if session.is_ended() {
+ return Err(Error::InvalidState);
+ }
+
+ // step 3
+ if context.IsContextLost() {
+ return Err(Error::InvalidState);
+ }
+
+ // Step 4
+ if !session.is_immersive() {
+ return Err(Error::InvalidState);
+ };
+
+ // Step 5 throw an InvalidStateError If context’s XR compatible boolean is false.
+
+ Ok(XRWebGLBinding::new(
+ global, proto, session, &context, can_gc,
+ ))
+ }
+
+ /// <https://immersive-web.github.io/layers/#dom-xrwebglbinding-createprojectionlayer>
+ fn CreateProjectionLayer(
+ &self,
+ _: XRTextureType,
+ _: &XRProjectionLayerInit,
+ ) -> Fallible<DomRoot<XRProjectionLayer>> {
+ // https://github.com/servo/servo/issues/27468
+ Err(Error::NotSupported)
+ }
+
+ /// <https://immersive-web.github.io/layers/#dom-xrwebglbinding-createquadlayer>
+ fn CreateQuadLayer(
+ &self,
+ _: XRTextureType,
+ _: &Option<XRQuadLayerInit>,
+ ) -> Fallible<DomRoot<XRQuadLayer>> {
+ // https://github.com/servo/servo/issues/27493
+ Err(Error::NotSupported)
+ }
+
+ /// <https://immersive-web.github.io/layers/#dom-xrwebglbinding-createcylinderlayer>
+ fn CreateCylinderLayer(
+ &self,
+ _: XRTextureType,
+ _: &Option<XRCylinderLayerInit>,
+ ) -> Fallible<DomRoot<XRCylinderLayer>> {
+ // https://github.com/servo/servo/issues/27493
+ Err(Error::NotSupported)
+ }
+
+ /// <https://immersive-web.github.io/layers/#dom-xrwebglbinding-createequirectlayer>
+ fn CreateEquirectLayer(
+ &self,
+ _: XRTextureType,
+ _: &Option<XREquirectLayerInit>,
+ ) -> Fallible<DomRoot<XREquirectLayer>> {
+ // https://github.com/servo/servo/issues/27493
+ Err(Error::NotSupported)
+ }
+
+ /// <https://immersive-web.github.io/layers/#dom-xrwebglbinding-createcubelayer>
+ fn CreateCubeLayer(&self, _: &Option<XRCubeLayerInit>) -> Fallible<DomRoot<XRCubeLayer>> {
+ // https://github.com/servo/servo/issues/27493
+ Err(Error::NotSupported)
+ }
+
+ /// <https://immersive-web.github.io/layers/#dom-xrwebglbinding-getsubimage>
+ fn GetSubImage(
+ &self,
+ _: &XRCompositionLayer,
+ _: &XRFrame,
+ _: XREye,
+ ) -> Fallible<DomRoot<XRWebGLSubImage>> {
+ // https://github.com/servo/servo/issues/27468
+ Err(Error::NotSupported)
+ }
+
+ /// <https://immersive-web.github.io/layers/#dom-xrwebglbinding-getviewsubimage>
+ fn GetViewSubImage(
+ &self,
+ _: &XRProjectionLayer,
+ _: &XRView,
+ ) -> Fallible<DomRoot<XRWebGLSubImage>> {
+ // https://github.com/servo/servo/issues/27468
+ Err(Error::NotSupported)
+ }
+}
diff --git a/components/script/dom/webxr/xrwebgllayer.rs b/components/script/dom/webxr/xrwebgllayer.rs
new file mode 100644
index 00000000000..031bd4b2f4d
--- /dev/null
+++ b/components/script/dom/webxr/xrwebgllayer.rs
@@ -0,0 +1,367 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+use std::convert::TryInto;
+
+use canvas_traits::webgl::{WebGLCommand, WebGLContextId, WebGLTextureId};
+use dom_struct::dom_struct;
+use euclid::{Rect, Size2D};
+use js::rust::HandleObject;
+use webxr_api::{ContextId as WebXRContextId, LayerId, LayerInit, Viewport};
+
+use crate::dom::bindings::codegen::Bindings::WebGL2RenderingContextBinding::WebGL2RenderingContextConstants as constants;
+use crate::dom::bindings::codegen::Bindings::WebGLRenderingContextBinding::WebGLRenderingContextMethods;
+use crate::dom::bindings::codegen::Bindings::XRWebGLLayerBinding::{
+ XRWebGLLayerInit, XRWebGLLayerMethods, XRWebGLRenderingContext,
+};
+use crate::dom::bindings::codegen::UnionTypes::HTMLCanvasElementOrOffscreenCanvas;
+use crate::dom::bindings::error::{Error, Fallible};
+use crate::dom::bindings::inheritance::Castable;
+use crate::dom::bindings::num::Finite;
+use crate::dom::bindings::reflector::{reflect_dom_object_with_proto, DomObject};
+use crate::dom::bindings::root::{Dom, DomRoot};
+use crate::dom::globalscope::GlobalScope;
+use crate::dom::webglframebuffer::WebGLFramebuffer;
+use crate::dom::webglobject::WebGLObject;
+use crate::dom::webglrenderingcontext::WebGLRenderingContext;
+use crate::dom::webgltexture::WebGLTexture;
+use crate::dom::window::Window;
+use crate::dom::xrframe::XRFrame;
+use crate::dom::xrlayer::XRLayer;
+use crate::dom::xrsession::XRSession;
+use crate::dom::xrview::XRView;
+use crate::dom::xrviewport::XRViewport;
+use crate::script_runtime::CanGc;
+
+impl<'a> From<&'a XRWebGLLayerInit> for LayerInit {
+ fn from(init: &'a XRWebGLLayerInit) -> LayerInit {
+ LayerInit::WebGLLayer {
+ alpha: init.alpha,
+ antialias: init.antialias,
+ depth: init.depth,
+ stencil: init.stencil,
+ framebuffer_scale_factor: *init.framebufferScaleFactor as f32,
+ ignore_depth_values: init.ignoreDepthValues,
+ }
+ }
+}
+
+#[dom_struct]
+pub struct XRWebGLLayer {
+ xr_layer: XRLayer,
+ antialias: bool,
+ depth: bool,
+ stencil: bool,
+ alpha: bool,
+ ignore_depth_values: bool,
+ /// If none, this is an inline session (the composition disabled flag is true)
+ framebuffer: Option<Dom<WebGLFramebuffer>>,
+}
+
+impl XRWebGLLayer {
+ pub fn new_inherited(
+ session: &XRSession,
+ context: &WebGLRenderingContext,
+ init: &XRWebGLLayerInit,
+ framebuffer: Option<&WebGLFramebuffer>,
+ layer_id: Option<LayerId>,
+ ) -> XRWebGLLayer {
+ XRWebGLLayer {
+ xr_layer: XRLayer::new_inherited(session, context, layer_id),
+ antialias: init.antialias,
+ depth: init.depth,
+ stencil: init.stencil,
+ alpha: init.alpha,
+ ignore_depth_values: init.ignoreDepthValues,
+ framebuffer: framebuffer.map(Dom::from_ref),
+ }
+ }
+
+ #[allow(clippy::too_many_arguments)]
+ fn new(
+ global: &GlobalScope,
+ proto: Option<HandleObject>,
+ session: &XRSession,
+ context: &WebGLRenderingContext,
+ init: &XRWebGLLayerInit,
+ framebuffer: Option<&WebGLFramebuffer>,
+ layer_id: Option<LayerId>,
+ can_gc: CanGc,
+ ) -> DomRoot<XRWebGLLayer> {
+ reflect_dom_object_with_proto(
+ Box::new(XRWebGLLayer::new_inherited(
+ session,
+ context,
+ init,
+ framebuffer,
+ layer_id,
+ )),
+ global,
+ proto,
+ can_gc,
+ )
+ }
+
+ pub fn layer_id(&self) -> Option<LayerId> {
+ self.xr_layer.layer_id()
+ }
+
+ pub fn context_id(&self) -> WebGLContextId {
+ self.xr_layer.context_id()
+ }
+
+ pub fn session(&self) -> &XRSession {
+ self.xr_layer.session()
+ }
+
+ pub fn size(&self) -> Size2D<u32, Viewport> {
+ if let Some(framebuffer) = self.framebuffer.as_ref() {
+ let size = framebuffer.size().unwrap_or((0, 0));
+ Size2D::new(
+ size.0.try_into().unwrap_or(0),
+ size.1.try_into().unwrap_or(0),
+ )
+ } else {
+ let size = match self.context().Canvas() {
+ HTMLCanvasElementOrOffscreenCanvas::HTMLCanvasElement(canvas) => canvas.get_size(),
+ HTMLCanvasElementOrOffscreenCanvas::OffscreenCanvas(canvas) => {
+ let size = canvas.get_size();
+ Size2D::new(
+ size.width.try_into().unwrap_or(0),
+ size.height.try_into().unwrap_or(0),
+ )
+ },
+ };
+ Size2D::from_untyped(size)
+ }
+ }
+
+ fn texture_target(&self) -> u32 {
+ if cfg!(target_os = "macos") {
+ glow::TEXTURE_RECTANGLE
+ } else {
+ glow::TEXTURE_2D
+ }
+ }
+
+ pub fn begin_frame(&self, frame: &XRFrame) -> Option<()> {
+ debug!("XRWebGLLayer begin frame");
+ let framebuffer = self.framebuffer.as_ref()?;
+ let context = framebuffer.upcast::<WebGLObject>().context();
+ let sub_images = frame.get_sub_images(self.layer_id()?)?;
+ let session = self.session();
+ // TODO: Cache this texture
+ let color_texture_id =
+ WebGLTextureId::maybe_new(sub_images.sub_image.as_ref()?.color_texture)?;
+ let color_texture = WebGLTexture::new_webxr(context, color_texture_id, session);
+ let target = self.texture_target();
+
+ // Save the current bindings
+ let saved_framebuffer = context.get_draw_framebuffer_slot().get();
+ let saved_framebuffer_target = framebuffer.target();
+ let saved_texture_id = context
+ .textures()
+ .active_texture_slot(target, context.webgl_version())
+ .ok()
+ .and_then(|slot| slot.get().map(|texture| texture.id()));
+
+ // We have to pick a framebuffer target.
+ // If there is a draw framebuffer, we use its target,
+ // otherwise we just use DRAW_FRAMEBUFFER.
+ let framebuffer_target = saved_framebuffer
+ .as_ref()
+ .and_then(|fb| fb.target())
+ .unwrap_or(constants::DRAW_FRAMEBUFFER);
+
+ // Update the attachments
+ context.send_command(WebGLCommand::BindTexture(target, Some(color_texture_id)));
+ framebuffer.bind(framebuffer_target);
+ framebuffer
+ .texture2d_even_if_opaque(
+ constants::COLOR_ATTACHMENT0,
+ self.texture_target(),
+ Some(&color_texture),
+ 0,
+ )
+ .ok()?;
+ if let Some(id) = sub_images.sub_image.as_ref()?.depth_stencil_texture {
+ // TODO: Cache this texture
+ let depth_stencil_texture_id = WebGLTextureId::maybe_new(id)?;
+ let depth_stencil_texture =
+ WebGLTexture::new_webxr(context, depth_stencil_texture_id, session);
+ framebuffer
+ .texture2d_even_if_opaque(
+ constants::DEPTH_STENCIL_ATTACHMENT,
+ constants::TEXTURE_2D,
+ Some(&depth_stencil_texture),
+ 0,
+ )
+ .ok()?;
+ }
+
+ // Restore the old bindings
+ context.send_command(WebGLCommand::BindTexture(target, saved_texture_id));
+ if let Some(framebuffer_target) = saved_framebuffer_target {
+ framebuffer.bind(framebuffer_target);
+ }
+ if let Some(framebuffer) = saved_framebuffer {
+ framebuffer.bind(framebuffer_target);
+ }
+ Some(())
+ }
+
+ pub fn end_frame(&self, _frame: &XRFrame) -> Option<()> {
+ debug!("XRWebGLLayer end frame");
+ // TODO: invalidate the old texture
+ let framebuffer = self.framebuffer.as_ref()?;
+ // TODO: rebind the current bindings
+ framebuffer.bind(constants::FRAMEBUFFER);
+ framebuffer
+ .texture2d_even_if_opaque(constants::COLOR_ATTACHMENT0, self.texture_target(), None, 0)
+ .ok()?;
+ framebuffer
+ .texture2d_even_if_opaque(
+ constants::DEPTH_STENCIL_ATTACHMENT,
+ constants::DEPTH_STENCIL_ATTACHMENT,
+ None,
+ 0,
+ )
+ .ok()?;
+ framebuffer.upcast::<WebGLObject>().context().Flush();
+ Some(())
+ }
+
+ pub(crate) fn context(&self) -> &WebGLRenderingContext {
+ self.xr_layer.context()
+ }
+}
+
+impl XRWebGLLayerMethods<crate::DomTypeHolder> for XRWebGLLayer {
+ /// <https://immersive-web.github.io/webxr/#dom-xrwebgllayer-xrwebgllayer>
+ fn Constructor(
+ global: &Window,
+ proto: Option<HandleObject>,
+ can_gc: CanGc,
+ session: &XRSession,
+ context: XRWebGLRenderingContext,
+ init: &XRWebGLLayerInit,
+ ) -> Fallible<DomRoot<Self>> {
+ let context = match context {
+ XRWebGLRenderingContext::WebGLRenderingContext(ctx) => ctx,
+ XRWebGLRenderingContext::WebGL2RenderingContext(ctx) => ctx.base_context(),
+ };
+
+ // Step 2
+ if session.is_ended() {
+ return Err(Error::InvalidState);
+ }
+ // XXXManishearth step 3: throw error if context is lost
+ // XXXManishearth step 4: check XR compat flag for immersive sessions
+
+ let (framebuffer, layer_id) = if session.is_immersive() {
+ // Step 9.2. "Initialize layer’s framebuffer to a new opaque framebuffer created with context."
+ let size = session
+ .with_session(|session| session.recommended_framebuffer_resolution())
+ .ok_or(Error::Operation)?;
+ let framebuffer = WebGLFramebuffer::maybe_new_webxr(session, &context, size)
+ .ok_or(Error::Operation)?;
+
+ // Step 9.3. "Allocate and initialize resources compatible with session’s XR device,
+ // including GPU accessible memory buffers, as required to support the compositing of layer."
+ let context_id = WebXRContextId::from(context.context_id());
+ let layer_init = LayerInit::from(init);
+ let layer_id = session
+ .with_session(|session| session.create_layer(context_id, layer_init))
+ .map_err(|_| Error::Operation)?;
+
+ // Step 9.4: "If layer’s resources were unable to be created for any reason,
+ // throw an OperationError and abort these steps."
+ (Some(framebuffer), Some(layer_id))
+ } else {
+ (None, None)
+ };
+
+ // Ensure that we finish setting up this layer before continuing.
+ context.Finish();
+
+ // Step 10. "Return layer."
+ Ok(XRWebGLLayer::new(
+ &global.global(),
+ proto,
+ session,
+ &context,
+ init,
+ framebuffer.as_deref(),
+ layer_id,
+ can_gc,
+ ))
+ }
+
+ /// <https://www.w3.org/TR/webxr/#dom-xrwebgllayer-getnativeframebufferscalefactor>
+ fn GetNativeFramebufferScaleFactor(_window: &Window, session: &XRSession) -> Finite<f64> {
+ let value: f64 = if session.is_ended() { 0.0 } else { 1.0 };
+ Finite::wrap(value)
+ }
+
+ /// <https://immersive-web.github.io/webxr/#dom-xrwebgllayer-antialias>
+ fn Antialias(&self) -> bool {
+ self.antialias
+ }
+
+ /// <https://immersive-web.github.io/webxr/#dom-xrwebgllayer-ignoredepthvalues>
+ fn IgnoreDepthValues(&self) -> bool {
+ self.ignore_depth_values
+ }
+
+ /// <https://www.w3.org/TR/webxr/#dom-xrwebgllayer-fixedfoveation>
+ fn GetFixedFoveation(&self) -> Option<Finite<f32>> {
+ // Fixed foveation is only available on Quest/Pico headset runtimes
+ None
+ }
+
+ /// <https://www.w3.org/TR/webxr/#dom-xrwebgllayer-fixedfoveation>
+ fn SetFixedFoveation(&self, _value: Option<Finite<f32>>) {
+ // no-op until fixed foveation is supported
+ }
+
+ /// <https://immersive-web.github.io/webxr/#dom-xrwebgllayer-framebuffer>
+ fn GetFramebuffer(&self) -> Option<DomRoot<WebGLFramebuffer>> {
+ self.framebuffer.as_ref().map(|x| DomRoot::from_ref(&**x))
+ }
+
+ /// <https://immersive-web.github.io/webxr/#dom-xrwebgllayer-framebufferwidth>
+ fn FramebufferWidth(&self) -> u32 {
+ self.size().width
+ }
+
+ /// <https://immersive-web.github.io/webxr/#dom-xrwebgllayer-framebufferheight>
+ fn FramebufferHeight(&self) -> u32 {
+ self.size().height
+ }
+
+ /// <https://immersive-web.github.io/webxr/#dom-xrwebgllayer-getviewport>
+ fn GetViewport(&self, view: &XRView) -> Option<DomRoot<XRViewport>> {
+ if self.session() != view.session() {
+ return None;
+ }
+
+ let index = view.viewport_index();
+
+ let viewport = self.session().with_session(|s| {
+ // Inline sessions
+ if s.viewports().is_empty() {
+ Rect::from_size(self.size().to_i32())
+ } else {
+ s.viewports()[index]
+ }
+ });
+
+ // NOTE: According to spec, viewport sizes should be recalculated here if the
+ // requested viewport scale has changed. However, existing browser implementations
+ // don't seem to do this for stereoscopic immersive sessions.
+ // Revisit if Servo gets support for handheld AR/VR via ARCore/ARKit
+
+ Some(XRViewport::new(&self.global(), viewport))
+ }
+}
diff --git a/components/script/dom/webxr/xrwebglsubimage.rs b/components/script/dom/webxr/xrwebglsubimage.rs
new file mode 100644
index 00000000000..acdcd22f4a9
--- /dev/null
+++ b/components/script/dom/webxr/xrwebglsubimage.rs
@@ -0,0 +1,49 @@
+/* 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 dom_struct::dom_struct;
+use euclid::Size2D;
+use webxr_api::Viewport;
+
+use crate::dom::bindings::codegen::Bindings::XRWebGLSubImageBinding::XRWebGLSubImage_Binding::XRWebGLSubImageMethods;
+use crate::dom::bindings::root::{Dom, DomRoot};
+use crate::dom::webgltexture::WebGLTexture;
+use crate::dom::xrsubimage::XRSubImage;
+
+#[dom_struct]
+pub struct XRWebGLSubImage {
+ xr_sub_image: XRSubImage,
+ color_texture: Dom<WebGLTexture>,
+ depth_stencil_texture: Option<Dom<WebGLTexture>>,
+ image_index: Option<u32>,
+ #[no_trace]
+ size: Size2D<u32, Viewport>,
+}
+
+impl XRWebGLSubImageMethods<crate::DomTypeHolder> for XRWebGLSubImage {
+ /// <https://immersive-web.github.io/layers/#dom-xrwebglsubimage-colortexture>
+ fn ColorTexture(&self) -> DomRoot<WebGLTexture> {
+ DomRoot::from_ref(&self.color_texture)
+ }
+
+ /// <https://immersive-web.github.io/layers/#dom-xrwebglsubimage-depthstenciltexture>
+ fn GetDepthStencilTexture(&self) -> Option<DomRoot<WebGLTexture>> {
+ self.depth_stencil_texture.as_deref().map(DomRoot::from_ref)
+ }
+
+ /// <https://immersive-web.github.io/layers/#dom-xrwebglsubimage-imageindex>
+ fn GetImageIndex(&self) -> Option<u32> {
+ self.image_index
+ }
+
+ /// <https://immersive-web.github.io/layers/#dom-xrwebglsubimage-texturewidth>
+ fn TextureWidth(&self) -> u32 {
+ self.size.width
+ }
+
+ /// <https://immersive-web.github.io/layers/#dom-xrwebglsubimage-textureheight>
+ fn TextureHeight(&self) -> u32 {
+ self.size.height
+ }
+}