Skip to main content

repame_shell/
haptics.rs

1use std::collections::{HashMap, HashSet};
2
3use repame_input::{HapticBus, HapticSink};
4use repose_app::ReposeRuntime;
5use repose_core::input::GamepadId;
6use web_time::{Duration, Instant};
7
8/// Length of each rumble upload. The backend sends a fresh force-feedback
9/// effect per call, so uploads are spaced out rather than repeated per frame.
10pub const RUMBLE_UPLOAD_MS: u32 = 1000;
11
12/// Upload again once this much of [`RUMBLE_UPLOAD_MS`] has elapsed, which
13/// keeps a held effect alive without restarting it every frame.
14pub const RUMBLE_REFRESH: Duration = Duration::from_millis(800);
15
16/// Strengths below this count as stopped.
17const MOTOR_EPSILON: f32 = 0.001;
18
19#[derive(Clone, Copy, Debug)]
20struct Live {
21    strong: f32,
22    weak: f32,
23    uploaded: Instant,
24}
25
26/// Routes [`HapticBus`] motors to `ReposeRuntime::request_rumble`. Uploads
27/// only on change and on refresh, and stops devices the bus dropped, since
28/// the bus stops mentioning a device rather than zeroing it.
29#[derive(Default)]
30pub struct RumbleBridge {
31    live: HashMap<u64, Live>,
32    queued: Vec<(GamepadId, f32, f32, u32)>,
33}
34
35impl RumbleBridge {
36    pub fn new() -> Self {
37        Self::default()
38    }
39
40    /// Feed every motor the bus is running, stopping what it dropped.
41    pub fn sync(&mut self, bus: &HapticBus) {
42        let now = Instant::now();
43        self.sync_at(bus, now);
44    }
45
46    /// [`RumbleBridge::sync`] against an explicit clock.
47    pub fn sync_at(&mut self, bus: &HapticBus, now: Instant) {
48        let active: HashSet<u64> = bus.active_devices().collect();
49        let stopped: Vec<u64> = self
50            .live
51            .keys()
52            .copied()
53            .filter(|device| !active.contains(device))
54            .collect();
55        for device in stopped {
56            self.live.remove(&device);
57            self.queued.push((GamepadId(device as u32), 0.0, 0.0, 0));
58        }
59        for device in active {
60            let (strong, weak) = bus.motors(device);
61            self.set(device, strong, weak, now);
62        }
63    }
64
65    /// Hand the queued rumble to the runtime. Call once per frame after
66    /// [`RumbleBridge::sync`]; the runner drains the runtime queue in turn.
67    pub fn forward_to(&mut self, runtime: &mut ReposeRuntime) {
68        for (id, strong, weak, duration_ms) in self.drain() {
69            runtime.request_rumble(id, strong, weak, duration_ms);
70        }
71    }
72
73    pub fn drain(&mut self) -> Vec<(GamepadId, f32, f32, u32)> {
74        std::mem::take(&mut self.queued)
75    }
76
77    fn set(&mut self, device: u64, strong: f32, weak: f32, now: Instant) {
78        let strong = clamp_motor(strong);
79        let weak = clamp_motor(weak);
80        let stopped = strong <= MOTOR_EPSILON && weak <= MOTOR_EPSILON;
81        let stale = self
82            .live
83            .get(&device)
84            .is_some_and(|live| now.duration_since(live.uploaded) >= RUMBLE_REFRESH);
85        let changed = self
86            .live
87            .get(&device)
88            .is_none_or(|live| live.strong != strong || live.weak != weak);
89        if !stopped && !stale && !changed {
90            return;
91        }
92        if stopped {
93            if self.live.remove(&device).is_some() {
94                self.queued.push((GamepadId(device as u32), 0.0, 0.0, 0));
95            }
96            return;
97        }
98        self.live.insert(
99            device,
100            Live {
101                strong,
102                weak,
103                uploaded: now,
104            },
105        );
106        self.queued
107            .push((GamepadId(device as u32), strong, weak, RUMBLE_UPLOAD_MS));
108    }
109}
110
111impl HapticSink for RumbleBridge {
112    fn apply(&mut self, device: u64, strong: f32, weak: f32) {
113        let now = Instant::now();
114        self.set(device, strong, weak, now);
115    }
116}
117
118fn clamp_motor(value: f32) -> f32 {
119    if value.is_nan() {
120        return 0.0;
121    }
122    value.clamp(0.0, 1.0)
123}