diff options
Diffstat (limited to 'components/script/dom/webxr')
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 + } +} |