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