1use super::ffi::{
8 ABS_BRAKE, ABS_CNT, ABS_GAS, ABS_HAT0X, ABS_HAT0Y, ABS_HAT2X, ABS_HAT2Y, ABS_RX, ABS_RY,
9 ABS_RZ, ABS_X, ABS_Y, ABS_Z, AbsBits, AbsInfo, BTN_DPAD_DOWN, BTN_DPAD_LEFT, BTN_DPAD_RIGHT,
10 BTN_DPAD_UP, BTN_EAST, BTN_GAMEPAD, BTN_MODE, BTN_NORTH, BTN_SELECT, BTN_SOUTH, BTN_START,
11 BTN_THUMBL, BTN_THUMBR, BTN_TL, BTN_TL2, BTN_TR, BTN_TR2, BTN_WEST, InputId, KeyBits,
12};
13use crate::usb::{self, VENDOR_NINTENDO, VENDOR_SONY};
14use crate::{GamepadSnapshot, PadAxis, PadButton, PadKind};
15
16#[must_use]
28pub fn stick_axis(value: i32, minimum: i32, maximum: i32) -> f32 {
29 let (value, minimum, maximum) = (i64::from(value), i64::from(minimum), i64::from(maximum));
30 let centre = minimum + (maximum - minimum + 1) / 2;
31 let scale = maximum - centre;
32 if scale <= 0 {
33 return 0.0;
34 }
35 ((value - centre) as f64 / scale as f64).clamp(-1.0, 1.0) as f32
36}
37
38#[must_use]
41pub fn trigger_axis(value: i32, minimum: i32, maximum: i32) -> f32 {
42 let (value, minimum, maximum) = (i64::from(value), i64::from(minimum), i64::from(maximum));
43 let range = maximum - minimum;
44 if range <= 0 {
45 return 0.0;
46 }
47 ((value - minimum) as f64 / range as f64).clamp(0.0, 1.0) as f32
48}
49
50#[derive(Clone, Debug, PartialEq)]
53pub(crate) struct Probe {
54 pub(crate) id: InputId,
56 pub(crate) keys: KeyBits,
58 pub(crate) abs: AbsBits,
60 pub(crate) held: KeyBits,
62 pub(crate) axes: [AbsInfo; ABS_CNT],
65}
66
67impl Probe {
68 pub(crate) fn values(&self) -> [i32; ABS_CNT] {
70 self.axes.map(|info| info.value)
71 }
72}
73
74pub(crate) fn is_gamepad(keys: &KeyBits, abs: &AbsBits) -> bool {
80 keys.has(BTN_GAMEPAD) && abs.has(ABS_X) && abs.has(ABS_Y)
81}
82
83pub(crate) fn kind_of(id: InputId) -> PadKind {
85 usb::kind_of(id.vendor, id.product)
86}
87
88const COMMON_BUTTONS: [(u16, PadButton); 9] = [
90 (BTN_SOUTH, PadButton::South),
91 (BTN_EAST, PadButton::East),
92 (BTN_TL, PadButton::LeftShoulder),
93 (BTN_TR, PadButton::RightShoulder),
94 (BTN_THUMBL, PadButton::LeftStick),
95 (BTN_THUMBR, PadButton::RightStick),
96 (BTN_START, PadButton::Start),
97 (BTN_SELECT, PadButton::Select),
98 (BTN_MODE, PadButton::Guide),
99];
100
101const POSITIONAL_FACE: [(u16, PadButton); 2] =
104 [(BTN_NORTH, PadButton::North), (BTN_WEST, PadButton::West)];
105
106const LETTERED_FACE: [(u16, PadButton); 2] =
111 [(BTN_NORTH, PadButton::West), (BTN_WEST, PadButton::North)];
112
113const DPAD_KEYS: [(u16, PadButton); 4] = [
115 (BTN_DPAD_UP, PadButton::DpadUp),
116 (BTN_DPAD_DOWN, PadButton::DpadDown),
117 (BTN_DPAD_LEFT, PadButton::DpadLeft),
118 (BTN_DPAD_RIGHT, PadButton::DpadRight),
119];
120
121const STICKS: [(u16, PadAxis); 4] = [
123 (ABS_X, PadAxis::LeftX),
124 (ABS_Y, PadAxis::LeftY),
125 (ABS_RX, PadAxis::RightX),
126 (ABS_RY, PadAxis::RightY),
127];
128
129const TRIGGERS: [([u16; 3], u16, PadAxis); 2] = [
135 ([ABS_Z, ABS_BRAKE, ABS_HAT2Y], BTN_TL2, PadAxis::LeftTrigger),
136 ([ABS_RZ, ABS_GAS, ABS_HAT2X], BTN_TR2, PadAxis::RightTrigger),
137];
138
139const HAT_PRESS: f32 = 0.5;
143
144#[derive(Clone, Copy, Debug, PartialEq, Eq)]
146enum Role {
147 Axis(PadAxis),
149 HatX,
151 HatY,
153}
154
155#[derive(Clone, Debug, PartialEq)]
158pub(crate) struct Layout {
159 pub(crate) kind: PadKind,
161 buttons: Vec<(u16, PadButton)>,
163 axes: Vec<(u16, Role, AbsInfo)>,
165 digital_triggers: Vec<(u16, PadAxis)>,
167}
168
169impl Layout {
170 pub(crate) fn new(probe: &Probe) -> Self {
177 let kind = kind_of(probe.id);
178 let has_key = |code: u16| probe.keys.has(code);
179 let has_abs = |code: u16| probe.abs.has(code);
180 let face = match probe.id.vendor {
181 VENDOR_SONY | VENDOR_NINTENDO => POSITIONAL_FACE,
182 _ => LETTERED_FACE,
183 };
184 let dpad_keys = DPAD_KEYS.iter().any(|&(code, _)| has_key(code));
185 let dpad: &[(u16, PadButton)] = if dpad_keys { &DPAD_KEYS } else { &[] };
186 let buttons = COMMON_BUTTONS
187 .iter()
188 .chain(&face)
189 .chain(dpad)
190 .copied()
191 .filter(|&(code, _)| has_key(code))
192 .collect();
193
194 let info = |code: u16| probe.axes[usize::from(code)];
195 let mut axes: Vec<_> = STICKS
196 .iter()
197 .filter(|&&(code, _)| has_abs(code))
198 .map(|&(code, axis)| (code, Role::Axis(axis), info(code)))
199 .collect();
200 let mut digital_triggers = Vec::new();
201 for (candidates, key, axis) in TRIGGERS {
202 match candidates.into_iter().find(|&code| has_abs(code)) {
203 Some(code) => axes.push((code, Role::Axis(axis), info(code))),
204 None if has_key(key) => digital_triggers.push((key, axis)),
205 None => {}
206 }
207 }
208 if !dpad_keys {
209 for (code, role) in [(ABS_HAT0X, Role::HatX), (ABS_HAT0Y, Role::HatY)] {
210 if has_abs(code) {
211 axes.push((code, role, info(code)));
212 }
213 }
214 }
215 Self {
216 kind,
217 buttons,
218 axes,
219 digital_triggers,
220 }
221 }
222
223 pub(crate) fn snapshot(&self, held: &KeyBits, values: &[i32; ABS_CNT]) -> GamepadSnapshot {
227 let mut snapshot = GamepadSnapshot::neutral(self.kind);
228 snapshot.buttons = self
229 .buttons
230 .iter()
231 .filter(|&&(code, _)| held.has(code))
232 .map(|&(_, button)| button)
233 .collect();
234 for &(code, axis) in &self.digital_triggers {
235 if held.has(code) {
236 snapshot.axes[axis as usize] = 1.0;
237 }
238 }
239 for &(code, role, info) in &self.axes {
240 let value = values[usize::from(code)];
241 let stick = stick_axis(value, info.minimum, info.maximum);
242 match role {
243 Role::Axis(axis @ (PadAxis::LeftTrigger | PadAxis::RightTrigger)) => {
244 snapshot.axes[axis as usize] = trigger_axis(value, info.minimum, info.maximum);
245 }
246 Role::Axis(axis @ (PadAxis::LeftY | PadAxis::RightY)) => {
247 snapshot.axes[axis as usize] = -stick;
248 }
249 Role::Axis(axis) => snapshot.axes[axis as usize] = stick,
250 Role::HatX if stick <= -HAT_PRESS => snapshot.buttons.insert(PadButton::DpadLeft),
251 Role::HatX if stick >= HAT_PRESS => snapshot.buttons.insert(PadButton::DpadRight),
252 Role::HatY if stick <= -HAT_PRESS => snapshot.buttons.insert(PadButton::DpadUp),
253 Role::HatY if stick >= HAT_PRESS => snapshot.buttons.insert(PadButton::DpadDown),
254 Role::HatX | Role::HatY => {}
255 }
256 }
257 snapshot
258 }
259}
260
261#[cfg(test)]
262pub(crate) mod tests {
263 use super::*;
264 use crate::PadButtons;
265 use crate::usb::{PRODUCT_STEAM_DECK, VENDOR_MICROSOFT, VENDOR_VALVE};
266
267 pub(crate) const XBOX: InputId = InputId {
268 bustype: 3,
269 vendor: VENDOR_MICROSOFT,
270 product: 0x028e,
271 version: 0,
272 };
273 const SONY: InputId = InputId {
274 bustype: 3,
275 vendor: VENDOR_SONY,
276 product: 0x0ce6,
277 version: 0,
278 };
279
280 pub(crate) const XPAD_STICK: (i32, i32) = (-32768, 32767);
282 pub(crate) const XPAD_TRIGGER: (i32, i32) = (0, 255);
284
285 pub(crate) fn probe(id: InputId, keys: &[u16], axes: &[(u16, (i32, i32))]) -> Probe {
289 let is_trigger = |code| TRIGGERS.iter().any(|(axes, _, _)| axes.contains(&code));
290 let mut probe = Probe {
291 id,
292 keys: KeyBits::EMPTY,
293 abs: AbsBits::EMPTY,
294 held: KeyBits::EMPTY,
295 axes: [AbsInfo::default(); ABS_CNT],
296 };
297 for &code in keys {
298 probe.keys.set(code, true);
299 }
300 for &(code, (minimum, maximum)) in axes {
301 probe.abs.set(code, true);
302 probe.axes[usize::from(code)] = AbsInfo {
303 value: if is_trigger(code) {
304 minimum
305 } else {
306 minimum + (maximum - minimum + 1) / 2
307 },
308 minimum,
309 maximum,
310 flat: 128,
311 ..AbsInfo::default()
312 };
313 }
314 probe
315 }
316
317 pub(crate) const XPAD_KEYS: [u16; 11] = [
319 BTN_SOUTH, BTN_EAST, BTN_NORTH, BTN_WEST, BTN_TL, BTN_TR, BTN_SELECT, BTN_START, BTN_MODE,
320 BTN_THUMBL, BTN_THUMBR,
321 ];
322
323 pub(crate) fn xpad() -> Probe {
325 probe(
326 XBOX,
327 &XPAD_KEYS,
328 &[
329 (ABS_X, XPAD_STICK),
330 (ABS_Y, XPAD_STICK),
331 (ABS_RX, XPAD_STICK),
332 (ABS_RY, XPAD_STICK),
333 (ABS_Z, XPAD_TRIGGER),
334 (ABS_RZ, XPAD_TRIGGER),
335 (ABS_HAT0X, (-1, 1)),
336 (ABS_HAT0Y, (-1, 1)),
337 ],
338 )
339 }
340
341 fn snapshot(probe: &Probe, keys: &[u16], values: &[(u16, i32)]) -> GamepadSnapshot {
344 let mut held = KeyBits::EMPTY;
345 for &code in keys {
346 held.set(code, true);
347 }
348 let mut current = probe.values();
349 for &(code, value) in values {
350 current[usize::from(code)] = value;
351 }
352 Layout::new(probe).snapshot(&held, ¤t)
353 }
354
355 fn buttons(list: &[PadButton]) -> PadButtons {
356 list.iter().copied().collect()
357 }
358
359 #[test]
362 fn stick_values_normalise_against_known_ranges() {
363 assert_eq!(
365 stick_axis(-32768, -32768, 32767),
366 -1.0,
367 "the spare code clamps"
368 );
369 assert_eq!(stick_axis(-32767, -32768, 32767), -1.0);
370 assert_eq!(stick_axis(0, -32768, 32767), 0.0);
371 assert_eq!(stick_axis(32767, -32768, 32767), 1.0);
372 assert_eq!(
373 stick_axis(-16384, -32768, 32767),
374 -stick_axis(16384, -32768, 32767),
375 "one scale both ways"
376 );
377 assert_eq!(stick_axis(-32767, -32767, 32767), -1.0);
379 assert_eq!(stick_axis(0, -32767, 32767), 0.0);
380 assert_eq!(stick_axis(128, 0, 255), 0.0);
382 assert_eq!(stick_axis(255, 0, 255), 1.0);
383 assert_eq!(stick_axis(1, 0, 255), -1.0);
384 assert_eq!(stick_axis(0, 0, 255), -1.0, "the spare code clamps");
385 assert!((stick_axis(192, 0, 255) - 64.0 / 127.0).abs() < f32::EPSILON);
386 assert_eq!(stick_axis(-1, -1, 1), -1.0);
388 assert_eq!(stick_axis(40000, -32768, 32767), 1.0);
389 assert_eq!(stick_axis(5, 5, 5), 0.0);
391 assert_eq!(stick_axis(5, 10, 0), 0.0);
392 assert_eq!(stick_axis(i32::MAX, i32::MIN, i32::MAX), 1.0, "no overflow");
393 }
394
395 #[test]
396 fn trigger_values_normalise_against_known_ranges() {
397 assert_eq!(trigger_axis(0, 0, 255), 0.0);
398 assert_eq!(trigger_axis(255, 0, 255), 1.0);
399 assert!((trigger_axis(128, 0, 255) - 128.0 / 255.0).abs() < f32::EPSILON);
400 assert_eq!(trigger_axis(1023, 0, 1023), 1.0, "Xbox One's ten bits");
401 assert_eq!(trigger_axis(32767, 0, 32767), 1.0, "hid-steam's");
402 assert_eq!(trigger_axis(-5, 0, 255), 0.0, "clamped");
403 assert_eq!(trigger_axis(9, 9, 9), 0.0, "no extent");
404 }
405
406 #[test]
410 fn stick_y_is_flipped_to_up_and_flat_is_not_applied() {
411 let pad = xpad();
412 let pushed_up = snapshot(&pad, &[], &[(ABS_Y, -32768), (ABS_RY, -32768)]);
413 assert_eq!(pushed_up.axis(PadAxis::LeftY), 1.0, "evdev −Y is up");
414 assert_eq!(pushed_up.axis(PadAxis::RightY), 1.0);
415 let pulled_down = snapshot(&pad, &[], &[(ABS_Y, 32767), (ABS_RY, 32767)]);
416 assert_eq!(pulled_down.axis(PadAxis::LeftY), -1.0);
417 assert_eq!(pulled_down.axis(PadAxis::RightY), -1.0);
418 let right = snapshot(&pad, &[], &[(ABS_X, 32767), (ABS_RX, -32768)]);
419 assert_eq!(right.axis(PadAxis::LeftX), 1.0, "+X right, unflipped");
420 assert_eq!(right.axis(PadAxis::RightX), -1.0);
421
422 let inside_flat = snapshot(&pad, &[], &[(ABS_X, 64)]);
423 assert_eq!(pad.axes[usize::from(ABS_X)].flat, 128);
424 assert!((inside_flat.axis(PadAxis::LeftX) - 64.0 / 32767.0).abs() < f32::EPSILON);
425
426 let at_rest = snapshot(&pad, &[], &[]);
427 assert_eq!(at_rest, GamepadSnapshot::neutral(PadKind::Xbox));
428 }
429
430 #[test]
433 fn buttons_map_by_position_for_each_driver_family() {
434 let lettered = xpad();
435 for (code, button) in [
436 (BTN_SOUTH, PadButton::South),
437 (BTN_EAST, PadButton::East),
438 (BTN_NORTH, PadButton::West),
439 (BTN_WEST, PadButton::North),
440 (BTN_TL, PadButton::LeftShoulder),
441 (BTN_TR, PadButton::RightShoulder),
442 (BTN_THUMBL, PadButton::LeftStick),
443 (BTN_THUMBR, PadButton::RightStick),
444 (BTN_START, PadButton::Start),
445 (BTN_SELECT, PadButton::Select),
446 (BTN_MODE, PadButton::Guide),
447 ] {
448 assert_eq!(
449 snapshot(&lettered, &[code], &[]).buttons,
450 buttons(&[button]),
451 "xpad {code:#x}"
452 );
453 }
454
455 let positional = probe(
456 SONY,
457 &XPAD_KEYS,
458 &[(ABS_X, XPAD_TRIGGER), (ABS_Y, XPAD_TRIGGER)],
459 );
460 assert_eq!(
461 snapshot(&positional, &[BTN_NORTH], &[]).buttons,
462 buttons(&[PadButton::North]),
463 "hid-playstation's triangle"
464 );
465 assert_eq!(
466 snapshot(&positional, &[BTN_WEST], &[]).buttons,
467 buttons(&[PadButton::West]),
468 "hid-playstation's square"
469 );
470 assert_eq!(snapshot(&positional, &[], &[]).kind, PadKind::PlayStation);
471 }
472
473 #[test]
476 fn the_hat_maps_onto_the_dpad() {
477 let pad = xpad();
478 for ((x, y), expected) in [
479 ((0, -1), &[PadButton::DpadUp][..]),
480 ((0, 1), &[PadButton::DpadDown]),
481 ((-1, 0), &[PadButton::DpadLeft]),
482 ((1, 0), &[PadButton::DpadRight]),
483 ((1, -1), &[PadButton::DpadRight, PadButton::DpadUp]),
484 ((0, 0), &[]),
485 ] {
486 assert_eq!(
487 snapshot(&pad, &[], &[(ABS_HAT0X, x), (ABS_HAT0Y, y)]).buttons,
488 buttons(expected),
489 "hat at ({x}, {y})"
490 );
491 }
492 }
493
494 #[test]
497 fn dpad_keys_win_over_the_hat() {
498 let deck = probe(
499 InputId {
500 vendor: VENDOR_VALVE,
501 product: PRODUCT_STEAM_DECK,
502 ..XBOX
503 },
504 &[
505 BTN_SOUTH,
506 BTN_DPAD_UP,
507 BTN_DPAD_DOWN,
508 BTN_DPAD_LEFT,
509 BTN_DPAD_RIGHT,
510 ],
511 &[
512 (ABS_X, (-32767, 32767)),
513 (ABS_Y, (-32767, 32767)),
514 (ABS_HAT0X, (-32767, 32767)),
515 (ABS_HAT0Y, (-32767, 32767)),
516 ],
517 );
518 for (code, button) in DPAD_KEYS {
519 assert_eq!(snapshot(&deck, &[code], &[]).buttons, buttons(&[button]));
520 }
521 let touched = snapshot(&deck, &[], &[(ABS_HAT0X, 32767), (ABS_HAT0Y, -32767)]);
522 assert!(touched.buttons.is_empty(), "{touched:?}");
523 assert_eq!(touched.kind, PadKind::SteamDeck);
524 }
525
526 #[test]
530 fn triggers_come_from_the_first_axis_present() {
531 let sticks = [(ABS_X, XPAD_STICK), (ABS_Y, XPAD_STICK)];
532 let both = probe(
533 XBOX,
534 &[BTN_SOUTH],
535 &[
536 sticks[0],
537 sticks[1],
538 (ABS_Z, XPAD_TRIGGER),
539 (ABS_RZ, XPAD_TRIGGER),
540 (ABS_BRAKE, (0, 1023)),
541 (ABS_GAS, (0, 1023)),
542 ],
543 );
544 let pulled = snapshot(&both, &[], &[(ABS_Z, 255), (ABS_BRAKE, 0), (ABS_GAS, 1023)]);
545 assert_eq!(pulled.axis(PadAxis::LeftTrigger), 1.0, "ABS_Z first");
546 assert_eq!(pulled.axis(PadAxis::RightTrigger), 0.0, "ABS_GAS ignored");
547
548 let bluetooth = probe(
549 XBOX,
550 &[BTN_SOUTH],
551 &[
552 sticks[0],
553 sticks[1],
554 (ABS_BRAKE, (0, 1023)),
555 (ABS_GAS, (0, 1023)),
556 ],
557 );
558 let pulled = snapshot(&bluetooth, &[], &[(ABS_BRAKE, 1023), (ABS_GAS, 0)]);
559 assert_eq!(pulled.axis(PadAxis::LeftTrigger), 1.0, "brake is left");
560 assert_eq!(pulled.axis(PadAxis::RightTrigger), 0.0);
561
562 let steam = probe(
563 XBOX,
564 &[BTN_SOUTH],
565 &[
566 sticks[0],
567 sticks[1],
568 (ABS_HAT2Y, (0, 32767)),
569 (ABS_HAT2X, (0, 32767)),
570 ],
571 );
572 let pulled = snapshot(&steam, &[], &[(ABS_HAT2X, 32767), (ABS_HAT2Y, 0)]);
573 assert_eq!(pulled.axis(PadAxis::RightTrigger), 1.0, "HAT2X is right");
574 assert_eq!(pulled.axis(PadAxis::LeftTrigger), 0.0);
575
576 let digital = probe(
577 InputId {
578 vendor: VENDOR_NINTENDO,
579 ..XBOX
580 },
581 &[BTN_SOUTH, BTN_TL2, BTN_TR2],
582 &sticks,
583 );
584 let pulled = snapshot(&digital, &[BTN_TR2], &[]);
585 assert_eq!(pulled.axis(PadAxis::RightTrigger), 1.0, "ZR held");
586 assert_eq!(pulled.axis(PadAxis::LeftTrigger), 0.0);
587 assert!(pulled.buttons.is_empty(), "a trigger, not a button");
588 }
589
590 #[test]
593 fn capability_bits_classify_pads() {
594 let class = |probe: &Probe| is_gamepad(&probe.keys, &probe.abs);
595 assert!(class(&xpad()), "an Xbox pad");
596 const KEY_ESC: u16 = 1;
597 const KEY_A: u16 = 30;
598 const KEY_SPACE: u16 = 57;
599 const BTN_LEFT: u16 = 0x110;
600 const BTN_TRIGGER: u16 = 0x120;
601 const BTN_TOUCH: u16 = 0x14a;
602 let axes = [(ABS_X, XPAD_STICK), (ABS_Y, XPAD_STICK)];
603 for (what, keys, axes) in [
604 ("a keyboard", &[KEY_ESC, KEY_A, KEY_SPACE][..], &[][..]),
605 ("a touchpad", &[BTN_LEFT, BTN_TOUCH], &axes[..]),
606 ("a flight stick", &[BTN_TRIGGER], &axes),
607 (
608 "motion sensors",
609 &[],
610 &[
611 (ABS_X, XPAD_STICK),
612 (ABS_Y, XPAD_STICK),
613 (ABS_Z, XPAD_STICK),
614 ],
615 ),
616 ("buttons with no stick", &[BTN_SOUTH, BTN_EAST], &[]),
617 ("a pad missing ABS_Y", &[BTN_SOUTH], &[(ABS_X, XPAD_STICK)]),
618 ] {
619 assert!(!class(&probe(XBOX, keys, axes)), "{what} is not a pad");
620 }
621 }
622
623 #[test]
624 fn vendor_and_product_name_the_family() {
625 let id = |vendor, product| InputId {
626 vendor,
627 product,
628 ..XBOX
629 };
630 assert_eq!(kind_of(id(VENDOR_MICROSOFT, 0x028e)), PadKind::Xbox);
631 assert_eq!(
632 kind_of(id(VENDOR_VALVE, PRODUCT_STEAM_DECK)),
633 PadKind::SteamDeck
634 );
635 assert_eq!(
636 kind_of(id(VENDOR_VALVE, 0x11ff)),
637 PadKind::Xbox,
638 "Steam's virtual pad"
639 );
640 assert_eq!(kind_of(id(VENDOR_SONY, 0x0ce6)), PadKind::PlayStation);
641 assert_eq!(kind_of(id(VENDOR_NINTENDO, 0x2009)), PadKind::Switch);
642 assert_eq!(kind_of(id(0x1234, 0x5678)), PadKind::Generic);
643 }
644}