diff options
author | Martin Robinson <mrobinson@igalia.com> | 2025-01-30 20:07:35 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2025-01-30 19:07:35 +0000 |
commit | 534e78db5331fbfbad7e60d72a88e9aacdc11ee4 (patch) | |
tree | 3bcd217e0e7b7fd0c91d5406a81ea241ffc4ce06 /components/webxr/headless | |
parent | 64b40ea70065f949d1e281bd046c56d50312f2a7 (diff) | |
download | servo-534e78db5331fbfbad7e60d72a88e9aacdc11ee4.tar.gz servo-534e78db5331fbfbad7e60d72a88e9aacdc11ee4.zip |
Merge webxr repository (#35228)
Signed-off-by: Martin Robinson <mrobinson@igalia.com>
Diffstat (limited to 'components/webxr/headless')
-rw-r--r-- | components/webxr/headless/mod.rs | 564 |
1 files changed, 564 insertions, 0 deletions
diff --git a/components/webxr/headless/mod.rs b/components/webxr/headless/mod.rs new file mode 100644 index 00000000000..7a8fea01f28 --- /dev/null +++ b/components/webxr/headless/mod.rs @@ -0,0 +1,564 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +use crate::SurfmanGL; +use crate::SurfmanLayerManager; +use euclid::{Point2D, RigidTransform3D}; +use std::sync::{Arc, Mutex}; +use std::thread; +use surfman::chains::SwapChains; +use webxr_api::util::{self, ClipPlanes, HitTestList}; +use webxr_api::{ + ApiSpace, BaseSpace, ContextId, DeviceAPI, DiscoveryAPI, Error, Event, EventBuffer, Floor, + Frame, FrameUpdateEvent, HitTestId, HitTestResult, HitTestSource, Input, InputFrame, InputId, + InputSource, LayerGrandManager, LayerId, LayerInit, LayerManager, MockButton, MockDeviceInit, + MockDeviceMsg, MockDiscoveryAPI, MockInputMsg, MockViewInit, MockViewsInit, MockWorld, Native, + Quitter, Ray, Receiver, SelectEvent, SelectKind, Sender, Session, SessionBuilder, SessionInit, + SessionMode, Space, SubImages, View, Viewer, ViewerPose, Viewports, Views, +}; + +pub struct HeadlessMockDiscovery {} + +struct HeadlessDiscovery { + data: Arc<Mutex<HeadlessDeviceData>>, + supports_vr: bool, + supports_inline: bool, + supports_ar: bool, +} + +struct InputInfo { + source: InputSource, + active: bool, + pointer: Option<RigidTransform3D<f32, Input, Native>>, + grip: Option<RigidTransform3D<f32, Input, Native>>, + clicking: bool, + buttons: Vec<MockButton>, +} + +struct HeadlessDevice { + data: Arc<Mutex<HeadlessDeviceData>>, + id: u32, + hit_tests: HitTestList, + granted_features: Vec<String>, + grand_manager: LayerGrandManager<SurfmanGL>, + layer_manager: Option<LayerManager>, +} + +struct PerSessionData { + id: u32, + mode: SessionMode, + clip_planes: ClipPlanes, + quitter: Option<Quitter>, + events: EventBuffer, + needs_vp_update: bool, +} + +struct HeadlessDeviceData { + floor_transform: Option<RigidTransform3D<f32, Native, Floor>>, + viewer_origin: Option<RigidTransform3D<f32, Viewer, Native>>, + supported_features: Vec<String>, + views: MockViewsInit, + needs_floor_update: bool, + inputs: Vec<InputInfo>, + sessions: Vec<PerSessionData>, + disconnected: bool, + world: Option<MockWorld>, + next_id: u32, + bounds_geometry: Vec<Point2D<f32, Floor>>, +} + +impl MockDiscoveryAPI<SurfmanGL> for HeadlessMockDiscovery { + fn simulate_device_connection( + &mut self, + init: MockDeviceInit, + receiver: Receiver<MockDeviceMsg>, + ) -> Result<Box<dyn DiscoveryAPI<SurfmanGL>>, Error> { + let viewer_origin = init.viewer_origin.clone(); + let floor_transform = init.floor_origin.map(|f| f.inverse()); + let views = init.views.clone(); + let data = HeadlessDeviceData { + floor_transform, + viewer_origin, + supported_features: init.supported_features, + views, + needs_floor_update: false, + inputs: vec![], + sessions: vec![], + disconnected: false, + world: init.world, + next_id: 0, + bounds_geometry: vec![], + }; + let data = Arc::new(Mutex::new(data)); + let data_ = data.clone(); + + thread::spawn(move || { + run_loop(receiver, data_); + }); + Ok(Box::new(HeadlessDiscovery { + data, + supports_vr: init.supports_vr, + supports_inline: init.supports_inline, + supports_ar: init.supports_ar, + })) + } +} + +fn run_loop(receiver: Receiver<MockDeviceMsg>, data: Arc<Mutex<HeadlessDeviceData>>) { + while let Ok(msg) = receiver.recv() { + if !data.lock().expect("Mutex poisoned").handle_msg(msg) { + break; + } + } +} + +impl DiscoveryAPI<SurfmanGL> for HeadlessDiscovery { + fn request_session( + &mut self, + mode: SessionMode, + init: &SessionInit, + xr: SessionBuilder<SurfmanGL>, + ) -> Result<Session, Error> { + if !self.supports_session(mode) { + return Err(Error::NoMatchingDevice); + } + let data = self.data.clone(); + let mut d = data.lock().unwrap(); + let id = d.next_id; + d.next_id += 1; + let per_session = PerSessionData { + id, + mode, + clip_planes: Default::default(), + quitter: Default::default(), + events: Default::default(), + needs_vp_update: false, + }; + d.sessions.push(per_session); + + let granted_features = init.validate(mode, &d.supported_features)?; + let layer_manager = None; + drop(d); + xr.spawn(move |grand_manager| { + Ok(HeadlessDevice { + data, + id, + granted_features, + hit_tests: HitTestList::default(), + grand_manager, + layer_manager, + }) + }) + } + + fn supports_session(&self, mode: SessionMode) -> bool { + if self.data.lock().unwrap().disconnected { + return false; + } + match mode { + SessionMode::Inline => self.supports_inline, + SessionMode::ImmersiveVR => self.supports_vr, + SessionMode::ImmersiveAR => self.supports_ar, + } + } +} + +fn view<Eye>( + init: MockViewInit<Eye>, + viewer: RigidTransform3D<f32, Viewer, Native>, + clip_planes: ClipPlanes, +) -> View<Eye> { + let projection = if let Some((l, r, t, b)) = init.fov { + util::fov_to_projection_matrix(l, r, t, b, clip_planes) + } else { + init.projection + }; + + View { + transform: init.transform.inverse().then(&viewer), + projection, + } +} + +impl HeadlessDevice { + fn with_per_session<R>(&self, f: impl FnOnce(&mut PerSessionData) -> R) -> R { + f(self + .data + .lock() + .unwrap() + .sessions + .iter_mut() + .find(|s| s.id == self.id) + .unwrap()) + } + + fn layer_manager(&mut self) -> Result<&mut LayerManager, Error> { + if let Some(ref mut manager) = self.layer_manager { + return Ok(manager); + } + let swap_chains = SwapChains::new(); + let viewports = self.viewports(); + let layer_manager = self.grand_manager.create_layer_manager(move |_, _| { + Ok(SurfmanLayerManager::new(viewports, swap_chains)) + })?; + self.layer_manager = Some(layer_manager); + Ok(self.layer_manager.as_mut().unwrap()) + } +} + +impl DeviceAPI for HeadlessDevice { + fn floor_transform(&self) -> Option<RigidTransform3D<f32, Native, Floor>> { + self.data.lock().unwrap().floor_transform.clone() + } + + fn viewports(&self) -> Viewports { + let d = self.data.lock().unwrap(); + let per_session = d.sessions.iter().find(|s| s.id == self.id).unwrap(); + d.viewports(per_session.mode) + } + + fn create_layer(&mut self, context_id: ContextId, init: LayerInit) -> Result<LayerId, Error> { + self.layer_manager()?.create_layer(context_id, init) + } + + fn destroy_layer(&mut self, context_id: ContextId, layer_id: LayerId) { + self.layer_manager() + .unwrap() + .destroy_layer(context_id, layer_id) + } + + fn begin_animation_frame(&mut self, layers: &[(ContextId, LayerId)]) -> Option<Frame> { + let sub_images = self.layer_manager().ok()?.begin_frame(layers).ok()?; + let mut data = self.data.lock().unwrap(); + let mut frame = data.get_frame( + data.sessions.iter().find(|s| s.id == self.id).unwrap(), + sub_images, + ); + let per_session = data.sessions.iter_mut().find(|s| s.id == self.id).unwrap(); + if per_session.needs_vp_update { + per_session.needs_vp_update = false; + let mode = per_session.mode; + let vp = data.viewports(mode); + frame.events.push(FrameUpdateEvent::UpdateViewports(vp)); + } + let events = self.hit_tests.commit_tests(); + frame.events = events; + + if let Some(ref world) = data.world { + for source in self.hit_tests.tests() { + let ray = data.native_ray(source.ray, source.space); + let ray = if let Some(ray) = ray { ray } else { break }; + let hits = world + .regions + .iter() + .filter(|region| source.types.is_type(region.ty)) + .flat_map(|region| ®ion.faces) + .filter_map(|triangle| triangle.intersect(ray)) + .map(|space| HitTestResult { + space, + id: source.id, + }); + frame.hit_test_results.extend(hits); + } + } + + if data.needs_floor_update { + frame.events.push(FrameUpdateEvent::UpdateFloorTransform( + data.floor_transform.clone(), + )); + data.needs_floor_update = false; + } + Some(frame) + } + + fn end_animation_frame(&mut self, layers: &[(ContextId, LayerId)]) { + let _ = self.layer_manager().unwrap().end_frame(layers); + thread::sleep(std::time::Duration::from_millis(20)); + } + + fn initial_inputs(&self) -> Vec<InputSource> { + vec![] + } + + fn set_event_dest(&mut self, dest: Sender<Event>) { + self.with_per_session(|s| s.events.upgrade(dest)) + } + + fn quit(&mut self) { + self.with_per_session(|s| s.events.callback(Event::SessionEnd)) + } + + fn set_quitter(&mut self, quitter: Quitter) { + self.with_per_session(|s| s.quitter = Some(quitter)) + } + + fn update_clip_planes(&mut self, near: f32, far: f32) { + self.with_per_session(|s| s.clip_planes.update(near, far)); + } + + fn granted_features(&self) -> &[String] { + &self.granted_features + } + + fn request_hit_test(&mut self, source: HitTestSource) { + self.hit_tests.request_hit_test(source) + } + + fn cancel_hit_test(&mut self, id: HitTestId) { + self.hit_tests.cancel_hit_test(id) + } + + fn reference_space_bounds(&self) -> Option<Vec<Point2D<f32, Floor>>> { + let bounds = self.data.lock().unwrap().bounds_geometry.clone(); + Some(bounds) + } +} + +impl HeadlessMockDiscovery { + pub fn new() -> HeadlessMockDiscovery { + HeadlessMockDiscovery {} + } +} + +macro_rules! with_all_sessions { + ($self:ident, |$s:ident| $e:expr) => { + for $s in &mut $self.sessions { + $e; + } + }; +} + +impl HeadlessDeviceData { + fn get_frame(&self, s: &PerSessionData, sub_images: Vec<SubImages>) -> Frame { + let views = self.views.clone(); + + let pose = self.viewer_origin.map(|transform| { + let views = if s.mode == SessionMode::Inline { + Views::Inline + } else { + match views { + MockViewsInit::Mono(one) => Views::Mono(view(one, transform, s.clip_planes)), + MockViewsInit::Stereo(one, two) => Views::Stereo( + view(one, transform, s.clip_planes), + view(two, transform, s.clip_planes), + ), + } + }; + + ViewerPose { transform, views } + }); + let inputs = self + .inputs + .iter() + .filter(|i| i.active) + .map(|i| InputFrame { + id: i.source.id, + target_ray_origin: i.pointer, + grip_origin: i.grip, + pressed: false, + squeezed: false, + hand: None, + button_values: vec![], + axis_values: vec![], + input_changed: false, + }) + .collect(); + Frame { + pose, + inputs, + events: vec![], + sub_images, + hit_test_results: vec![], + predicted_display_time: 0.0, + } + } + + fn viewports(&self, mode: SessionMode) -> Viewports { + let vec = if mode == SessionMode::Inline { + vec![] + } else { + match &self.views { + MockViewsInit::Mono(one) => vec![one.viewport], + MockViewsInit::Stereo(one, two) => vec![one.viewport, two.viewport], + } + }; + Viewports { viewports: vec } + } + + fn trigger_select(&mut self, id: InputId, kind: SelectKind, event: SelectEvent) { + for i in 0..self.sessions.len() { + let frame = self.get_frame(&self.sessions[i], Vec::new()); + self.sessions[i] + .events + .callback(Event::Select(id, kind, event, frame)); + } + } + + fn handle_msg(&mut self, msg: MockDeviceMsg) -> bool { + match msg { + MockDeviceMsg::SetWorld(w) => self.world = Some(w), + MockDeviceMsg::ClearWorld => self.world = None, + MockDeviceMsg::SetViewerOrigin(viewer_origin) => { + self.viewer_origin = viewer_origin; + } + MockDeviceMsg::SetFloorOrigin(floor_origin) => { + self.floor_transform = floor_origin.map(|f| f.inverse()); + self.needs_floor_update = true; + } + MockDeviceMsg::SetViews(views) => { + self.views = views; + with_all_sessions!(self, |s| { + s.needs_vp_update = true; + }) + } + MockDeviceMsg::VisibilityChange(v) => { + with_all_sessions!(self, |s| s.events.callback(Event::VisibilityChange(v))) + } + MockDeviceMsg::AddInputSource(init) => { + self.inputs.push(InputInfo { + source: init.source.clone(), + pointer: init.pointer_origin, + grip: init.grip_origin, + active: true, + clicking: false, + buttons: init.supported_buttons, + }); + with_all_sessions!(self, |s| s + .events + .callback(Event::AddInput(init.source.clone()))) + } + MockDeviceMsg::MessageInputSource(id, msg) => { + if let Some(ref mut input) = self.inputs.iter_mut().find(|i| i.source.id == id) { + match msg { + MockInputMsg::SetHandedness(h) => { + input.source.handedness = h; + with_all_sessions!(self, |s| { + s.events + .callback(Event::UpdateInput(id, input.source.clone())) + }); + } + MockInputMsg::SetProfiles(p) => { + input.source.profiles = p; + with_all_sessions!(self, |s| { + s.events + .callback(Event::UpdateInput(id, input.source.clone())) + }); + } + MockInputMsg::SetTargetRayMode(t) => { + input.source.target_ray_mode = t; + with_all_sessions!(self, |s| { + s.events + .callback(Event::UpdateInput(id, input.source.clone())) + }); + } + MockInputMsg::SetPointerOrigin(p) => input.pointer = p, + MockInputMsg::SetGripOrigin(p) => input.grip = p, + MockInputMsg::TriggerSelect(kind, event) => { + if !input.active { + return true; + } + let clicking = input.clicking; + input.clicking = event == SelectEvent::Start; + match event { + SelectEvent::Start => { + self.trigger_select(id, kind, event); + } + SelectEvent::End => { + if clicking { + self.trigger_select(id, kind, SelectEvent::Select); + } else { + self.trigger_select(id, kind, SelectEvent::End); + } + } + SelectEvent::Select => { + self.trigger_select(id, kind, SelectEvent::Start); + self.trigger_select(id, kind, SelectEvent::Select); + } + } + } + MockInputMsg::Disconnect => { + if input.active { + with_all_sessions!(self, |s| s + .events + .callback(Event::RemoveInput(input.source.id))); + input.active = false; + input.clicking = false; + } + } + MockInputMsg::Reconnect => { + if !input.active { + with_all_sessions!(self, |s| s + .events + .callback(Event::AddInput(input.source.clone()))); + input.active = true; + } + } + MockInputMsg::SetSupportedButtons(buttons) => { + input.buttons = buttons; + with_all_sessions!(self, |s| s.events.callback(Event::UpdateInput( + input.source.id, + input.source.clone() + ))); + } + MockInputMsg::UpdateButtonState(state) => { + if let Some(button) = input + .buttons + .iter_mut() + .find(|b| b.button_type == state.button_type) + { + *button = state; + } + } + } + } + } + MockDeviceMsg::Disconnect(s) => { + self.disconnected = true; + with_all_sessions!(self, |s| s.quitter.as_ref().map(|q| q.quit())); + // notify the client that we're done disconnecting + let _ = s.send(()); + return false; + } + MockDeviceMsg::SetBoundsGeometry(g) => { + self.bounds_geometry = g; + } + MockDeviceMsg::SimulateResetPose => { + with_all_sessions!(self, |s| s.events.callback(Event::ReferenceSpaceChanged( + BaseSpace::Local, + RigidTransform3D::identity() + ))); + } + } + true + } + + fn native_ray(&self, ray: Ray<ApiSpace>, space: Space) -> Option<Ray<Native>> { + let origin: RigidTransform3D<f32, ApiSpace, Native> = match space.base { + BaseSpace::Local => RigidTransform3D::identity(), + BaseSpace::Floor => self.floor_transform?.inverse().cast_unit(), + BaseSpace::Viewer => self.viewer_origin?.cast_unit(), + BaseSpace::BoundedFloor => self.floor_transform?.inverse().cast_unit(), + BaseSpace::TargetRay(id) => self + .inputs + .iter() + .find(|i| i.source.id == id)? + .pointer? + .cast_unit(), + BaseSpace::Grip(id) => self + .inputs + .iter() + .find(|i| i.source.id == id)? + .grip? + .cast_unit(), + BaseSpace::Joint(..) => panic!("Cannot request mocking backend with hands"), + }; + let space_origin = space.offset.then(&origin); + + let origin_rigid: RigidTransform3D<f32, ApiSpace, ApiSpace> = ray.origin.into(); + Some(Ray { + origin: origin_rigid.then(&space_origin).translation, + direction: space_origin.rotation.transform_vector3d(ray.direction), + }) + } +} |