1use std::sync::atomic::{AtomicBool, Ordering};
6use std::sync::{Arc, Mutex};
7use std::thread;
8
9use euclid::{Point2D, RigidTransform3D};
10use profile_traits::generic_callback::GenericCallback as ProfileGenericCallback;
11use servo_base::generic_channel::GenericReceiver;
12use surfman::chains::SwapChains;
13use webxr_api::util::{self, ClipPlanes, HitTestList};
14use webxr_api::{
15 ApiSpace, BaseSpace, ContextId, DeviceAPI, DiscoveryAPI, EnvironmentBlendMode, Error, Event,
16 EventBuffer, Floor, Frame, FrameUpdateEvent, HitTestId, HitTestResult, HitTestSource, Input,
17 InputFrame, InputId, InputSource, LayerGrandManager, LayerId, LayerInit, LayerManager,
18 MockButton, MockDeviceInit, MockDeviceMsg, MockDiscoveryAPI, MockInputMsg, MockViewInit,
19 MockViewsInit, MockWorld, Native, Quitter, Ray, SelectEvent, SelectKind, Session,
20 SessionBuilder, SessionInit, SessionMode, Space, SubImages, View, Viewer, ViewerPose,
21 Viewports, Views,
22};
23
24use crate::{SurfmanGL, SurfmanLayerManager};
25
26pub struct HeadlessMockDiscovery {
27 enabled: Arc<AtomicBool>,
28}
29
30impl HeadlessMockDiscovery {
31 pub fn new(enabled: Arc<AtomicBool>) -> Self {
32 Self { enabled }
33 }
34}
35
36struct HeadlessDiscovery {
37 data: Arc<Mutex<HeadlessDeviceData>>,
38 supports_vr: bool,
39 supports_inline: bool,
40 supports_ar: bool,
41}
42
43struct InputInfo {
44 source: InputSource,
45 active: bool,
46 pointer: Option<RigidTransform3D<f32, Input, Native>>,
47 grip: Option<RigidTransform3D<f32, Input, Native>>,
48 clicking: bool,
49 buttons: Vec<MockButton>,
50}
51
52struct HeadlessDevice {
53 data: Arc<Mutex<HeadlessDeviceData>>,
54 id: u32,
55 hit_tests: HitTestList,
56 granted_features: Vec<String>,
57 grand_manager: LayerGrandManager<SurfmanGL>,
58 layer_manager: Option<LayerManager>,
59}
60
61struct PerSessionData {
62 id: u32,
63 mode: SessionMode,
64 clip_planes: ClipPlanes,
65 quitter: Option<Quitter>,
66 events: EventBuffer,
67 needs_vp_update: bool,
68}
69
70struct HeadlessDeviceData {
71 floor_transform: Option<RigidTransform3D<f32, Native, Floor>>,
72 viewer_origin: Option<RigidTransform3D<f32, Viewer, Native>>,
73 supported_features: Vec<String>,
74 views: MockViewsInit,
75 needs_floor_update: bool,
76 inputs: Vec<InputInfo>,
77 sessions: Vec<PerSessionData>,
78 disconnected: bool,
79 world: Option<MockWorld>,
80 next_id: u32,
81 bounds_geometry: Vec<Point2D<f32, Floor>>,
82}
83
84impl MockDiscoveryAPI<SurfmanGL> for HeadlessMockDiscovery {
85 fn simulate_device_connection(
86 &mut self,
87 init: MockDeviceInit,
88 receiver: GenericReceiver<MockDeviceMsg>,
89 ) -> Result<Box<dyn DiscoveryAPI<SurfmanGL>>, Error> {
90 if !self.enabled.load(Ordering::Relaxed) {
91 return Err(Error::NoMatchingDevice);
92 }
93
94 let viewer_origin = init.viewer_origin;
95 let floor_transform = init.floor_origin.map(|f| f.inverse());
96 let views = init.views.clone();
97 let data = HeadlessDeviceData {
98 floor_transform,
99 viewer_origin,
100 supported_features: init.supported_features,
101 views,
102 needs_floor_update: false,
103 inputs: vec![],
104 sessions: vec![],
105 disconnected: false,
106 world: init.world,
107 next_id: 0,
108 bounds_geometry: vec![],
109 };
110 let data = Arc::new(Mutex::new(data));
111 let data_ = data.clone();
112
113 thread::spawn(move || {
114 run_loop(receiver, data_);
115 });
116 Ok(Box::new(HeadlessDiscovery {
117 data,
118 supports_vr: init.supports_vr,
119 supports_inline: init.supports_inline,
120 supports_ar: init.supports_ar,
121 }))
122 }
123}
124
125fn run_loop(receiver: GenericReceiver<MockDeviceMsg>, data: Arc<Mutex<HeadlessDeviceData>>) {
126 while let Ok(msg) = receiver.recv() {
127 if !data.lock().expect("Mutex poisoned").handle_msg(msg) {
128 break;
129 }
130 }
131}
132
133impl DiscoveryAPI<SurfmanGL> for HeadlessDiscovery {
134 fn request_session(
135 &mut self,
136 mode: SessionMode,
137 init: &SessionInit,
138 xr: SessionBuilder<SurfmanGL>,
139 ) -> Result<Session, Error> {
140 if !self.supports_session(mode) {
141 return Err(Error::NoMatchingDevice);
142 }
143 let data = self.data.clone();
144 let mut d = data.lock().unwrap();
145 let id = d.next_id;
146 d.next_id += 1;
147 let per_session = PerSessionData {
148 id,
149 mode,
150 clip_planes: Default::default(),
151 quitter: Default::default(),
152 events: Default::default(),
153 needs_vp_update: false,
154 };
155 d.sessions.push(per_session);
156
157 let granted_features = init.validate(mode, &d.supported_features)?;
158 let layer_manager = None;
159 drop(d);
160 xr.spawn(move |grand_manager| {
161 Ok(HeadlessDevice {
162 data,
163 id,
164 granted_features,
165 hit_tests: HitTestList::default(),
166 grand_manager,
167 layer_manager,
168 })
169 })
170 }
171
172 fn supports_session(&self, mode: SessionMode) -> bool {
173 if self.data.lock().unwrap().disconnected {
174 return false;
175 }
176 match mode {
177 SessionMode::Inline => self.supports_inline,
178 SessionMode::ImmersiveVR => self.supports_vr,
179 SessionMode::ImmersiveAR => self.supports_ar,
180 }
181 }
182}
183
184fn view<Eye>(
185 init: MockViewInit<Eye>,
186 viewer: RigidTransform3D<f32, Viewer, Native>,
187 clip_planes: ClipPlanes,
188) -> View<Eye> {
189 let projection = if let Some((l, r, t, b)) = init.fov {
190 util::fov_to_projection_matrix(l, r, t, b, clip_planes)
191 } else {
192 init.projection
193 };
194
195 View {
196 transform: init.transform.inverse().then(&viewer),
197 projection,
198 }
199}
200
201impl HeadlessDevice {
202 fn with_per_session<R>(&self, f: impl FnOnce(&mut PerSessionData) -> R) -> R {
203 f(self
204 .data
205 .lock()
206 .unwrap()
207 .sessions
208 .iter_mut()
209 .find(|s| s.id == self.id)
210 .unwrap())
211 }
212
213 fn layer_manager(&mut self) -> Result<&mut LayerManager, Error> {
214 if let Some(ref mut manager) = self.layer_manager {
215 return Ok(manager);
216 }
217 let swap_chains = SwapChains::new();
218 let viewports = self.viewports();
219 let layer_manager = self
220 .grand_manager
221 .create_layer_manager(move |_| Ok(SurfmanLayerManager::new(viewports, swap_chains)))?;
222 self.layer_manager = Some(layer_manager);
223 Ok(self.layer_manager.as_mut().unwrap())
224 }
225}
226
227impl DeviceAPI for HeadlessDevice {
228 fn floor_transform(&self) -> Option<RigidTransform3D<f32, Native, Floor>> {
229 self.data.lock().unwrap().floor_transform
230 }
231
232 fn viewports(&self) -> Viewports {
233 let d = self.data.lock().unwrap();
234 let per_session = d.sessions.iter().find(|s| s.id == self.id).unwrap();
235 d.viewports(per_session.mode)
236 }
237
238 fn create_layer(&mut self, context_id: ContextId, init: LayerInit) -> Result<LayerId, Error> {
239 self.layer_manager()?.create_layer(context_id, init)
240 }
241
242 fn destroy_layer(&mut self, context_id: ContextId, layer_id: LayerId) {
243 self.layer_manager()
244 .unwrap()
245 .destroy_layer(context_id, layer_id)
246 }
247
248 fn begin_animation_frame(&mut self, layers: &[(ContextId, LayerId)]) -> Option<Frame> {
249 let sub_images = self.layer_manager().ok()?.begin_frame(layers).ok()?;
250 let mut data = self.data.lock().unwrap();
251 let mut frame = data.get_frame(
252 data.sessions.iter().find(|s| s.id == self.id).unwrap(),
253 sub_images,
254 );
255 let per_session = data.sessions.iter_mut().find(|s| s.id == self.id).unwrap();
256 if per_session.needs_vp_update {
257 per_session.needs_vp_update = false;
258 let mode = per_session.mode;
259 let vp = data.viewports(mode);
260 frame.events.push(FrameUpdateEvent::UpdateViewports(vp));
261 }
262 let events = self.hit_tests.commit_tests();
263 frame.events = events;
264
265 if let Some(ref world) = data.world {
266 for source in self.hit_tests.tests() {
267 let ray = data.native_ray(source.ray, source.space);
268 let ray = if let Some(ray) = ray { ray } else { break };
269 let hits = world
270 .regions
271 .iter()
272 .filter(|region| source.types.is_type(region.ty))
273 .flat_map(|region| ®ion.faces)
274 .filter_map(|triangle| triangle.intersect(ray))
275 .map(|space| HitTestResult {
276 space,
277 id: source.id,
278 });
279 frame.hit_test_results.extend(hits);
280 }
281 }
282
283 if data.needs_floor_update {
284 frame
285 .events
286 .push(FrameUpdateEvent::UpdateFloorTransform(data.floor_transform));
287 data.needs_floor_update = false;
288 }
289 Some(frame)
290 }
291
292 fn end_animation_frame(&mut self, layers: &[(ContextId, LayerId)]) {
293 let _ = self.layer_manager().unwrap().end_frame(layers);
294 thread::sleep(std::time::Duration::from_millis(20));
295 }
296
297 fn initial_inputs(&self) -> Vec<InputSource> {
298 vec![]
299 }
300
301 fn set_event_dest(&mut self, dest: ProfileGenericCallback<Event>) {
302 self.with_per_session(|s| s.events.upgrade(dest))
303 }
304
305 fn quit(&mut self) {
306 self.with_per_session(|s| s.events.callback(Event::SessionEnd))
307 }
308
309 fn set_quitter(&mut self, quitter: Quitter) {
310 self.with_per_session(|s| s.quitter = Some(quitter))
311 }
312
313 fn update_clip_planes(&mut self, near: f32, far: f32) {
314 self.with_per_session(|s| s.clip_planes.update(near, far));
315 }
316
317 fn granted_features(&self) -> &[String] {
318 &self.granted_features
319 }
320
321 fn request_hit_test(&mut self, source: HitTestSource) {
322 self.hit_tests.request_hit_test(source)
323 }
324
325 fn cancel_hit_test(&mut self, id: HitTestId) {
326 self.hit_tests.cancel_hit_test(id)
327 }
328
329 fn reference_space_bounds(&self) -> Option<Vec<Point2D<f32, Floor>>> {
330 let bounds = self.data.lock().unwrap().bounds_geometry.clone();
331 Some(bounds)
332 }
333
334 fn environment_blend_mode(&self) -> EnvironmentBlendMode {
335 let data = self.data.lock().unwrap();
336 let session = data
337 .sessions
338 .iter()
339 .find(|s| s.id == self.id)
340 .expect("Failed to find session for this device");
341
342 if session.mode == SessionMode::ImmersiveAR {
343 EnvironmentBlendMode::AlphaBlend
344 } else {
345 EnvironmentBlendMode::Opaque
346 }
347 }
348}
349
350macro_rules! with_all_sessions {
351 ($self:ident, |$s:ident| $e:expr) => {
352 for $s in &mut $self.sessions {
353 $e;
354 }
355 };
356}
357
358impl HeadlessDeviceData {
359 fn get_frame(&self, s: &PerSessionData, sub_images: Vec<SubImages>) -> Frame {
360 let views = self.views.clone();
361
362 let pose = self.viewer_origin.map(|transform| {
363 let views = if s.mode == SessionMode::Inline {
364 Views::Inline
365 } else {
366 match views {
367 MockViewsInit::Mono(one) => Views::Mono(view(one, transform, s.clip_planes)),
368 MockViewsInit::Stereo(one, two) => Views::Stereo(
369 view(one, transform, s.clip_planes),
370 view(two, transform, s.clip_planes),
371 ),
372 }
373 };
374
375 ViewerPose { transform, views }
376 });
377 let inputs = self
378 .inputs
379 .iter()
380 .filter(|i| i.active)
381 .map(|i| InputFrame {
382 id: i.source.id,
383 target_ray_origin: i.pointer,
384 grip_origin: i.grip,
385 pressed: false,
386 squeezed: false,
387 hand: None,
388 button_values: vec![],
389 axis_values: vec![],
390 input_changed: false,
391 })
392 .collect();
393 Frame {
394 pose,
395 inputs,
396 events: vec![],
397 sub_images,
398 hit_test_results: vec![],
399 predicted_display_time: 0.0,
400 }
401 }
402
403 fn viewports(&self, mode: SessionMode) -> Viewports {
404 let vec = if mode == SessionMode::Inline {
405 vec![]
406 } else {
407 match &self.views {
408 MockViewsInit::Mono(one) => vec![one.viewport],
409 MockViewsInit::Stereo(one, two) => vec![one.viewport, two.viewport],
410 }
411 };
412 Viewports { viewports: vec }
413 }
414
415 fn trigger_select(&mut self, id: InputId, kind: SelectKind, event: SelectEvent) {
416 for i in 0..self.sessions.len() {
417 let frame = self.get_frame(&self.sessions[i], Vec::new());
418 self.sessions[i]
419 .events
420 .callback(Event::Select(id, kind, event, frame));
421 }
422 }
423
424 fn handle_msg(&mut self, msg: MockDeviceMsg) -> bool {
425 match msg {
426 MockDeviceMsg::SetWorld(w) => self.world = Some(w),
427 MockDeviceMsg::ClearWorld => self.world = None,
428 MockDeviceMsg::SetViewerOrigin(viewer_origin) => {
429 self.viewer_origin = viewer_origin;
430 },
431 MockDeviceMsg::SetFloorOrigin(floor_origin) => {
432 self.floor_transform = floor_origin.map(|f| f.inverse());
433 self.needs_floor_update = true;
434 },
435 MockDeviceMsg::SetViews(views) => {
436 self.views = views;
437 with_all_sessions!(self, |s| {
438 s.needs_vp_update = true;
439 })
440 },
441 MockDeviceMsg::VisibilityChange(v) => {
442 with_all_sessions!(self, |s| s.events.callback(Event::VisibilityChange(v)))
443 },
444 MockDeviceMsg::AddInputSource(init) => {
445 self.inputs.push(InputInfo {
446 source: init.source.clone(),
447 pointer: init.pointer_origin,
448 grip: init.grip_origin,
449 active: true,
450 clicking: false,
451 buttons: init.supported_buttons,
452 });
453 with_all_sessions!(self, |s| s
454 .events
455 .callback(Event::AddInput(init.source.clone())))
456 },
457 MockDeviceMsg::MessageInputSource(id, msg) => {
458 if let Some(ref mut input) = self.inputs.iter_mut().find(|i| i.source.id == id) {
459 match msg {
460 MockInputMsg::SetHandedness(h) => {
461 input.source.handedness = h;
462 with_all_sessions!(self, |s| {
463 s.events
464 .callback(Event::UpdateInput(id, input.source.clone()))
465 });
466 },
467 MockInputMsg::SetProfiles(p) => {
468 input.source.profiles = p;
469 with_all_sessions!(self, |s| {
470 s.events
471 .callback(Event::UpdateInput(id, input.source.clone()))
472 });
473 },
474 MockInputMsg::SetTargetRayMode(t) => {
475 input.source.target_ray_mode = t;
476 with_all_sessions!(self, |s| {
477 s.events
478 .callback(Event::UpdateInput(id, input.source.clone()))
479 });
480 },
481 MockInputMsg::SetPointerOrigin(p) => input.pointer = p,
482 MockInputMsg::SetGripOrigin(p) => input.grip = p,
483 MockInputMsg::TriggerSelect(kind, event) => {
484 if !input.active {
485 return true;
486 }
487 let clicking = input.clicking;
488 input.clicking = event == SelectEvent::Start;
489 match event {
490 SelectEvent::Start => {
491 self.trigger_select(id, kind, event);
492 },
493 SelectEvent::End => {
494 if clicking {
495 self.trigger_select(id, kind, SelectEvent::Select);
496 } else {
497 self.trigger_select(id, kind, SelectEvent::End);
498 }
499 },
500 SelectEvent::Select => {
501 self.trigger_select(id, kind, SelectEvent::Start);
502 self.trigger_select(id, kind, SelectEvent::Select);
503 },
504 }
505 },
506 MockInputMsg::Disconnect => {
507 if input.active {
508 with_all_sessions!(self, |s| s
509 .events
510 .callback(Event::RemoveInput(input.source.id)));
511 input.active = false;
512 input.clicking = false;
513 }
514 },
515 MockInputMsg::Reconnect => {
516 if !input.active {
517 with_all_sessions!(self, |s| s
518 .events
519 .callback(Event::AddInput(input.source.clone())));
520 input.active = true;
521 }
522 },
523 MockInputMsg::SetSupportedButtons(buttons) => {
524 input.buttons = buttons;
525 with_all_sessions!(self, |s| s.events.callback(Event::UpdateInput(
526 input.source.id,
527 input.source.clone()
528 )));
529 },
530 MockInputMsg::UpdateButtonState(state) => {
531 if let Some(button) = input
532 .buttons
533 .iter_mut()
534 .find(|b| b.button_type == state.button_type)
535 {
536 *button = state;
537 }
538 },
539 }
540 }
541 },
542 MockDeviceMsg::Disconnect(s) => {
543 self.disconnected = true;
544 with_all_sessions!(self, |s| s.quitter.as_ref().map(|q| q.quit()));
545 let _ = s.send(());
547 return false;
548 },
549 MockDeviceMsg::SetBoundsGeometry(g) => {
550 self.bounds_geometry = g;
551 },
552 MockDeviceMsg::SimulateResetPose => {
553 with_all_sessions!(self, |s| s.events.callback(Event::ReferenceSpaceChanged(
554 BaseSpace::Local,
555 RigidTransform3D::identity()
556 )));
557 },
558 }
559 true
560 }
561
562 fn native_ray(&self, ray: Ray<ApiSpace>, space: Space) -> Option<Ray<Native>> {
563 let origin: RigidTransform3D<f32, ApiSpace, Native> = match space.base {
564 BaseSpace::Local => RigidTransform3D::identity(),
565 BaseSpace::Floor => self.floor_transform?.inverse().cast_unit(),
566 BaseSpace::Viewer => self.viewer_origin?.cast_unit(),
567 BaseSpace::BoundedFloor => self.floor_transform?.inverse().cast_unit(),
568 BaseSpace::TargetRay(id) => self
569 .inputs
570 .iter()
571 .find(|i| i.source.id == id)?
572 .pointer?
573 .cast_unit(),
574 BaseSpace::Grip(id) => self
575 .inputs
576 .iter()
577 .find(|i| i.source.id == id)?
578 .grip?
579 .cast_unit(),
580 BaseSpace::Joint(..) => panic!("Cannot request mocking backend with hands"),
581 };
582 let space_origin = space.offset.then(&origin);
583
584 let origin_rigid: RigidTransform3D<f32, ApiSpace, ApiSpace> = ray.origin.into();
585 Some(Ray {
586 origin: origin_rigid.then(&space_origin).translation,
587 direction: space_origin.rotation.transform_vector3d(ray.direction),
588 })
589 }
590}