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
8pub const RUMBLE_UPLOAD_MS: u32 = 1000;
11
12pub const RUMBLE_REFRESH: Duration = Duration::from_millis(800);
15
16const 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#[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 pub fn sync(&mut self, bus: &HapticBus) {
42 let now = Instant::now();
43 self.sync_at(bus, now);
44 }
45
46 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 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}