1use std::sync::Arc;
2
3use hidpp::{
4 channel::HidppChannel,
5 device::Device,
6 feature::{
7 CreatableFeature as _,
8 haptic_feedback::{HapticFeedbackFeature, HapticIntensity, HapticWaveform},
9 },
10};
11
12use crate::backend::HidBackend;
13use crate::channel::route::DeviceRoute;
14use crate::{ChannelRegistry, SharedChannel};
15
16use super::{HidppOperation, WriteError, classify_hidpp_error, open_feature, with_route};
17
18async fn feature_on_channel(
19 channel: &Arc<HidppChannel>,
20 device_index: u8,
21) -> Result<Arc<HapticFeedbackFeature>, WriteError> {
22 let mut device = Device::new(Arc::clone(channel), device_index)
23 .await
24 .map_err(|_| WriteError::DeviceUnreachable {
25 index: device_index,
26 })?;
27 open_feature::<HapticFeedbackFeature>(&mut device).await
28}
29
30struct EpochGuarded<T> {
48 epoch: u64,
49 entry: Option<(usize, u8, T)>,
50}
51
52impl<T: Clone> EpochGuarded<T> {
53 const fn new() -> Self {
54 Self {
55 epoch: 0,
56 entry: None,
57 }
58 }
59
60 fn get(&self, ptr: usize, index: u8) -> Option<T> {
61 let (entry_ptr, entry_index, value) = self.entry.as_ref()?;
62 (*entry_ptr == ptr && *entry_index == index).then(|| value.clone())
63 }
64
65 fn store(
76 &mut self,
77 epoch: u64,
78 ptr: usize,
79 index: u8,
80 value: T,
81 still_current: impl FnOnce() -> bool,
82 ) {
83 if self.epoch == epoch && still_current() {
84 self.entry = Some((ptr, index, value));
85 }
86 }
87
88 fn clear(&mut self) {
89 self.epoch = self.epoch.wrapping_add(1);
90 self.entry = None;
91 }
92
93 fn clear_for(&mut self, ptr: usize) {
97 self.epoch = self.epoch.wrapping_add(1);
98 if self
99 .entry
100 .as_ref()
101 .is_some_and(|(entry_ptr, _, _)| *entry_ptr == ptr)
102 {
103 self.entry = None;
104 }
105 }
106}
107
108static CACHED_FEATURE: std::sync::Mutex<EpochGuarded<Arc<HapticFeedbackFeature>>> =
109 std::sync::Mutex::new(EpochGuarded::new());
110
111fn cache_epoch() -> u64 {
114 CACHED_FEATURE.lock().map_or(0, |guard| guard.epoch)
115}
116
117fn cached_feature(channel: &Arc<HidppChannel>, index: u8) -> Option<Arc<HapticFeedbackFeature>> {
118 let guard = CACHED_FEATURE.lock().ok()?;
119 guard.get(Arc::as_ptr(channel) as usize, index)
120}
121
122fn store_cached_feature(
125 epoch: u64,
126 registry: &ChannelRegistry,
127 shared: &SharedChannel,
128 feature: &Arc<HapticFeedbackFeature>,
129) {
130 if let Ok(mut guard) = CACHED_FEATURE.lock() {
131 guard.store(
132 epoch,
133 Arc::as_ptr(shared.channel()) as usize,
134 shared.device_index(),
135 Arc::clone(feature),
136 || registry.is_current(shared),
137 );
138 }
139}
140
141fn clear_cached_feature() {
142 if let Ok(mut guard) = CACHED_FEATURE.lock() {
143 guard.clear();
144 }
145}
146
147pub fn clear_haptic_feature_cache() {
155 clear_cached_feature();
156}
157
158pub(crate) fn clear_haptic_feature_cache_for(channel: &Arc<HidppChannel>) {
166 if let Ok(mut guard) = CACHED_FEATURE.lock() {
167 guard.clear_for(Arc::as_ptr(channel) as usize);
168 }
169}
170
171pub async fn ensure_haptics_armed_on(
179 registry: &ChannelRegistry,
180 shared: &SharedChannel,
181) -> Result<bool, WriteError> {
182 let channel = shared.channel();
183 let index = shared.device_index();
184 let feature = if let Some(feature) = cached_feature(channel, index) {
185 feature
186 } else {
187 let epoch = cache_epoch();
188 let feature = feature_on_channel(channel, index).await?;
189 store_cached_feature(epoch, registry, shared, &feature);
190 feature
191 };
192 let config = feature.get_configuration().await.map_err(|error| {
193 clear_cached_feature();
194 classify_hidpp_error(error, HidppOperation::PlayHaptic, HapticFeedbackFeature::ID)
195 })?;
196 let intensity = if config.intensity.get() == 0 {
197 HapticIntensity::new(25).unwrap_or(config.intensity)
198 } else {
199 config.intensity
200 };
201 if config.enabled && intensity == config.intensity {
202 return Ok(false);
203 }
204 feature
205 .set_configuration(true, intensity)
206 .await
207 .map_err(|error| {
208 clear_cached_feature();
209 classify_hidpp_error(error, HidppOperation::PlayHaptic, HapticFeedbackFeature::ID)
210 })?;
211 Ok(true)
212}
213
214pub async fn play_haptic_on(
220 registry: &ChannelRegistry,
221 shared: &SharedChannel,
222 waveform: HapticWaveform,
223) -> Result<(), WriteError> {
224 let channel = shared.channel();
225 let index = shared.device_index();
226 if let Some(feature) = cached_feature(channel, index) {
227 if feature.play(waveform).await.is_ok() {
228 return Ok(());
229 }
230 clear_cached_feature();
231 }
232 let epoch = cache_epoch();
233 let feature = feature_on_channel(channel, index).await?;
234 let result = feature.play(waveform).await.map_err(|error| {
235 classify_hidpp_error(error, HidppOperation::PlayHaptic, HapticFeedbackFeature::ID)
236 });
237 if result.is_ok() {
238 store_cached_feature(epoch, registry, shared, &feature);
239 }
240 result
241}
242
243pub async fn play_haptic(
245 backend: &dyn HidBackend,
246 route: &DeviceRoute,
247 waveform: HapticWaveform,
248) -> Result<(), WriteError> {
249 let index = route.device_index();
250 with_route(backend, route, move |channel| async move {
251 let feature = feature_on_channel(&channel, index).await?;
252 feature.play(waveform).await.map_err(|error| {
253 classify_hidpp_error(error, HidppOperation::PlayHaptic, HapticFeedbackFeature::ID)
254 })
255 })
256 .await
257}
258
259#[cfg(test)]
260mod tests {
261 use super::EpochGuarded;
262
263 const CURRENT: fn() -> bool = || true;
265 const RETIRED: fn() -> bool = || false;
267
268 #[test]
269 fn a_store_started_before_a_clear_is_discarded() {
270 let mut cache = EpochGuarded::new();
271 let epoch = cache.epoch;
272 cache.clear_for(0xA);
274 cache.store(epoch, 0xA, 2, "stale", CURRENT);
276 assert_eq!(cache.get(0xA, 2), None);
277 }
278
279 #[test]
292 fn a_store_for_a_channel_retired_before_the_open_is_discarded() {
293 let mut cache = EpochGuarded::new();
294 cache.clear_for(0xA);
296 let epoch = cache.epoch;
298
299 cache.store(epoch, 0xA, 2, "retired", RETIRED);
300
301 assert_eq!(
302 cache.get(0xA, 2),
303 None,
304 "a channel the enumerator has retired must never be cached again"
305 );
306 }
307
308 #[test]
309 fn a_store_with_a_current_epoch_lands() {
310 let mut cache = EpochGuarded::new();
311 cache.store(cache.epoch, 0xA, 2, "fresh", CURRENT);
312 assert_eq!(cache.get(0xA, 2), Some("fresh"));
313 assert_eq!(cache.get(0xB, 2), None);
314 assert_eq!(cache.get(0xA, 3), None);
315 }
316
317 #[test]
318 fn retiring_one_channel_keeps_anothers_entry_but_blocks_stale_stores() {
319 let mut cache = EpochGuarded::new();
320 cache.store(cache.epoch, 0xA, 2, "kept", CURRENT);
321 let epoch = cache.epoch;
322 cache.clear_for(0xB);
323 assert_eq!(cache.get(0xA, 2), Some("kept"));
324 cache.store(epoch, 0xB, 1, "stale", CURRENT);
325 assert_eq!(cache.get(0xB, 1), None);
326 }
327
328 #[test]
329 fn a_full_clear_empties_the_entry_and_blocks_stale_stores() {
330 let mut cache = EpochGuarded::new();
331 let epoch = cache.epoch;
332 cache.store(epoch, 0xA, 2, "cached", CURRENT);
333 cache.clear();
334 assert_eq!(cache.get(0xA, 2), None);
335 cache.store(epoch, 0xA, 2, "stale", CURRENT);
336 assert_eq!(cache.get(0xA, 2), None);
337 }
338}