webxr/headless/
mod.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use 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| &region.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                // notify the client that we're done disconnecting
528                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}