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