use super::ffi::{
ABS_BRAKE, ABS_CNT, ABS_GAS, ABS_HAT0X, ABS_HAT0Y, ABS_HAT2X, ABS_HAT2Y, ABS_RX, ABS_RY,
ABS_RZ, ABS_X, ABS_Y, ABS_Z, AbsBits, AbsInfo, BTN_DPAD_DOWN, BTN_DPAD_LEFT, BTN_DPAD_RIGHT,
BTN_DPAD_UP, BTN_EAST, BTN_GAMEPAD, BTN_MODE, BTN_NORTH, BTN_SELECT, BTN_SOUTH, BTN_START,
BTN_THUMBL, BTN_THUMBR, BTN_TL, BTN_TL2, BTN_TR, BTN_TR2, BTN_WEST, InputId, KeyBits,
};
use crate::usb::{self, VENDOR_NINTENDO, VENDOR_SONY};
use crate::{GamepadSnapshot, PadAxis, PadButton, PadKind};
#[must_use]
pub fn stick_axis(value: i32, minimum: i32, maximum: i32) -> f32 {
let (value, minimum, maximum) = (i64::from(value), i64::from(minimum), i64::from(maximum));
let centre = minimum + (maximum - minimum + 1) / 2;
let scale = maximum - centre;
if scale <= 0 {
return 0.0;
}
((value - centre) as f64 / scale as f64).clamp(-1.0, 1.0) as f32
}
#[must_use]
pub fn trigger_axis(value: i32, minimum: i32, maximum: i32) -> f32 {
let (value, minimum, maximum) = (i64::from(value), i64::from(minimum), i64::from(maximum));
let range = maximum - minimum;
if range <= 0 {
return 0.0;
}
((value - minimum) as f64 / range as f64).clamp(0.0, 1.0) as f32
}
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct Probe {
pub(crate) id: InputId,
pub(crate) keys: KeyBits,
pub(crate) abs: AbsBits,
pub(crate) held: KeyBits,
pub(crate) axes: [AbsInfo; ABS_CNT],
}
impl Probe {
pub(crate) fn values(&self) -> [i32; ABS_CNT] {
self.axes.map(|info| info.value)
}
}
pub(crate) fn is_gamepad(keys: &KeyBits, abs: &AbsBits) -> bool {
keys.has(BTN_GAMEPAD) && abs.has(ABS_X) && abs.has(ABS_Y)
}
pub(crate) fn kind_of(id: InputId) -> PadKind {
usb::kind_of(id.vendor, id.product)
}
const COMMON_BUTTONS: [(u16, PadButton); 9] = [
(BTN_SOUTH, PadButton::South),
(BTN_EAST, PadButton::East),
(BTN_TL, PadButton::LeftShoulder),
(BTN_TR, PadButton::RightShoulder),
(BTN_THUMBL, PadButton::LeftStick),
(BTN_THUMBR, PadButton::RightStick),
(BTN_START, PadButton::Start),
(BTN_SELECT, PadButton::Select),
(BTN_MODE, PadButton::Guide),
];
const POSITIONAL_FACE: [(u16, PadButton); 2] =
[(BTN_NORTH, PadButton::North), (BTN_WEST, PadButton::West)];
const LETTERED_FACE: [(u16, PadButton); 2] =
[(BTN_NORTH, PadButton::West), (BTN_WEST, PadButton::North)];
const DPAD_KEYS: [(u16, PadButton); 4] = [
(BTN_DPAD_UP, PadButton::DpadUp),
(BTN_DPAD_DOWN, PadButton::DpadDown),
(BTN_DPAD_LEFT, PadButton::DpadLeft),
(BTN_DPAD_RIGHT, PadButton::DpadRight),
];
const STICKS: [(u16, PadAxis); 4] = [
(ABS_X, PadAxis::LeftX),
(ABS_Y, PadAxis::LeftY),
(ABS_RX, PadAxis::RightX),
(ABS_RY, PadAxis::RightY),
];
const TRIGGERS: [([u16; 3], u16, PadAxis); 2] = [
([ABS_Z, ABS_BRAKE, ABS_HAT2Y], BTN_TL2, PadAxis::LeftTrigger),
([ABS_RZ, ABS_GAS, ABS_HAT2X], BTN_TR2, PadAxis::RightTrigger),
];
const HAT_PRESS: f32 = 0.5;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Role {
Axis(PadAxis),
HatX,
HatY,
}
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct Layout {
pub(crate) kind: PadKind,
buttons: Vec<(u16, PadButton)>,
axes: Vec<(u16, Role, AbsInfo)>,
digital_triggers: Vec<(u16, PadAxis)>,
}
impl Layout {
pub(crate) fn new(probe: &Probe) -> Self {
let kind = kind_of(probe.id);
let has_key = |code: u16| probe.keys.has(code);
let has_abs = |code: u16| probe.abs.has(code);
let face = match probe.id.vendor {
VENDOR_SONY | VENDOR_NINTENDO => POSITIONAL_FACE,
_ => LETTERED_FACE,
};
let dpad_keys = DPAD_KEYS.iter().any(|&(code, _)| has_key(code));
let dpad: &[(u16, PadButton)] = if dpad_keys { &DPAD_KEYS } else { &[] };
let buttons = COMMON_BUTTONS
.iter()
.chain(&face)
.chain(dpad)
.copied()
.filter(|&(code, _)| has_key(code))
.collect();
let info = |code: u16| probe.axes[usize::from(code)];
let mut axes: Vec<_> = STICKS
.iter()
.filter(|&&(code, _)| has_abs(code))
.map(|&(code, axis)| (code, Role::Axis(axis), info(code)))
.collect();
let mut digital_triggers = Vec::new();
for (candidates, key, axis) in TRIGGERS {
match candidates.into_iter().find(|&code| has_abs(code)) {
Some(code) => axes.push((code, Role::Axis(axis), info(code))),
None if has_key(key) => digital_triggers.push((key, axis)),
None => {}
}
}
if !dpad_keys {
for (code, role) in [(ABS_HAT0X, Role::HatX), (ABS_HAT0Y, Role::HatY)] {
if has_abs(code) {
axes.push((code, role, info(code)));
}
}
}
Self {
kind,
buttons,
axes,
digital_triggers,
}
}
pub(crate) fn snapshot(&self, held: &KeyBits, values: &[i32; ABS_CNT]) -> GamepadSnapshot {
let mut snapshot = GamepadSnapshot::neutral(self.kind);
snapshot.buttons = self
.buttons
.iter()
.filter(|&&(code, _)| held.has(code))
.map(|&(_, button)| button)
.collect();
for &(code, axis) in &self.digital_triggers {
if held.has(code) {
snapshot.axes[axis as usize] = 1.0;
}
}
for &(code, role, info) in &self.axes {
let value = values[usize::from(code)];
let stick = stick_axis(value, info.minimum, info.maximum);
match role {
Role::Axis(axis @ (PadAxis::LeftTrigger | PadAxis::RightTrigger)) => {
snapshot.axes[axis as usize] = trigger_axis(value, info.minimum, info.maximum);
}
Role::Axis(axis @ (PadAxis::LeftY | PadAxis::RightY)) => {
snapshot.axes[axis as usize] = -stick;
}
Role::Axis(axis) => snapshot.axes[axis as usize] = stick,
Role::HatX if stick <= -HAT_PRESS => snapshot.buttons.insert(PadButton::DpadLeft),
Role::HatX if stick >= HAT_PRESS => snapshot.buttons.insert(PadButton::DpadRight),
Role::HatY if stick <= -HAT_PRESS => snapshot.buttons.insert(PadButton::DpadUp),
Role::HatY if stick >= HAT_PRESS => snapshot.buttons.insert(PadButton::DpadDown),
Role::HatX | Role::HatY => {}
}
}
snapshot
}
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use crate::PadButtons;
use crate::usb::{PRODUCT_STEAM_DECK, VENDOR_MICROSOFT, VENDOR_VALVE};
pub(crate) const XBOX: InputId = InputId {
bustype: 3,
vendor: VENDOR_MICROSOFT,
product: 0x028e,
version: 0,
};
const SONY: InputId = InputId {
bustype: 3,
vendor: VENDOR_SONY,
product: 0x0ce6,
version: 0,
};
pub(crate) const XPAD_STICK: (i32, i32) = (-32768, 32767);
pub(crate) const XPAD_TRIGGER: (i32, i32) = (0, 255);
pub(crate) fn probe(id: InputId, keys: &[u16], axes: &[(u16, (i32, i32))]) -> Probe {
let is_trigger = |code| TRIGGERS.iter().any(|(axes, _, _)| axes.contains(&code));
let mut probe = Probe {
id,
keys: KeyBits::EMPTY,
abs: AbsBits::EMPTY,
held: KeyBits::EMPTY,
axes: [AbsInfo::default(); ABS_CNT],
};
for &code in keys {
probe.keys.set(code, true);
}
for &(code, (minimum, maximum)) in axes {
probe.abs.set(code, true);
probe.axes[usize::from(code)] = AbsInfo {
value: if is_trigger(code) {
minimum
} else {
minimum + (maximum - minimum + 1) / 2
},
minimum,
maximum,
flat: 128,
..AbsInfo::default()
};
}
probe
}
pub(crate) const XPAD_KEYS: [u16; 11] = [
BTN_SOUTH, BTN_EAST, BTN_NORTH, BTN_WEST, BTN_TL, BTN_TR, BTN_SELECT, BTN_START, BTN_MODE,
BTN_THUMBL, BTN_THUMBR,
];
pub(crate) fn xpad() -> Probe {
probe(
XBOX,
&XPAD_KEYS,
&[
(ABS_X, XPAD_STICK),
(ABS_Y, XPAD_STICK),
(ABS_RX, XPAD_STICK),
(ABS_RY, XPAD_STICK),
(ABS_Z, XPAD_TRIGGER),
(ABS_RZ, XPAD_TRIGGER),
(ABS_HAT0X, (-1, 1)),
(ABS_HAT0Y, (-1, 1)),
],
)
}
fn snapshot(probe: &Probe, keys: &[u16], values: &[(u16, i32)]) -> GamepadSnapshot {
let mut held = KeyBits::EMPTY;
for &code in keys {
held.set(code, true);
}
let mut current = probe.values();
for &(code, value) in values {
current[usize::from(code)] = value;
}
Layout::new(probe).snapshot(&held, ¤t)
}
fn buttons(list: &[PadButton]) -> PadButtons {
list.iter().copied().collect()
}
#[test]
fn stick_values_normalise_against_known_ranges() {
assert_eq!(
stick_axis(-32768, -32768, 32767),
-1.0,
"the spare code clamps"
);
assert_eq!(stick_axis(-32767, -32768, 32767), -1.0);
assert_eq!(stick_axis(0, -32768, 32767), 0.0);
assert_eq!(stick_axis(32767, -32768, 32767), 1.0);
assert_eq!(
stick_axis(-16384, -32768, 32767),
-stick_axis(16384, -32768, 32767),
"one scale both ways"
);
assert_eq!(stick_axis(-32767, -32767, 32767), -1.0);
assert_eq!(stick_axis(0, -32767, 32767), 0.0);
assert_eq!(stick_axis(128, 0, 255), 0.0);
assert_eq!(stick_axis(255, 0, 255), 1.0);
assert_eq!(stick_axis(1, 0, 255), -1.0);
assert_eq!(stick_axis(0, 0, 255), -1.0, "the spare code clamps");
assert!((stick_axis(192, 0, 255) - 64.0 / 127.0).abs() < f32::EPSILON);
assert_eq!(stick_axis(-1, -1, 1), -1.0);
assert_eq!(stick_axis(40000, -32768, 32767), 1.0);
assert_eq!(stick_axis(5, 5, 5), 0.0);
assert_eq!(stick_axis(5, 10, 0), 0.0);
assert_eq!(stick_axis(i32::MAX, i32::MIN, i32::MAX), 1.0, "no overflow");
}
#[test]
fn trigger_values_normalise_against_known_ranges() {
assert_eq!(trigger_axis(0, 0, 255), 0.0);
assert_eq!(trigger_axis(255, 0, 255), 1.0);
assert!((trigger_axis(128, 0, 255) - 128.0 / 255.0).abs() < f32::EPSILON);
assert_eq!(trigger_axis(1023, 0, 1023), 1.0, "Xbox One's ten bits");
assert_eq!(trigger_axis(32767, 0, 32767), 1.0, "hid-steam's");
assert_eq!(trigger_axis(-5, 0, 255), 0.0, "clamped");
assert_eq!(trigger_axis(9, 9, 9), 0.0, "no extent");
}
#[test]
fn stick_y_is_flipped_to_up_and_flat_is_not_applied() {
let pad = xpad();
let pushed_up = snapshot(&pad, &[], &[(ABS_Y, -32768), (ABS_RY, -32768)]);
assert_eq!(pushed_up.axis(PadAxis::LeftY), 1.0, "evdev −Y is up");
assert_eq!(pushed_up.axis(PadAxis::RightY), 1.0);
let pulled_down = snapshot(&pad, &[], &[(ABS_Y, 32767), (ABS_RY, 32767)]);
assert_eq!(pulled_down.axis(PadAxis::LeftY), -1.0);
assert_eq!(pulled_down.axis(PadAxis::RightY), -1.0);
let right = snapshot(&pad, &[], &[(ABS_X, 32767), (ABS_RX, -32768)]);
assert_eq!(right.axis(PadAxis::LeftX), 1.0, "+X right, unflipped");
assert_eq!(right.axis(PadAxis::RightX), -1.0);
let inside_flat = snapshot(&pad, &[], &[(ABS_X, 64)]);
assert_eq!(pad.axes[usize::from(ABS_X)].flat, 128);
assert!((inside_flat.axis(PadAxis::LeftX) - 64.0 / 32767.0).abs() < f32::EPSILON);
let at_rest = snapshot(&pad, &[], &[]);
assert_eq!(at_rest, GamepadSnapshot::neutral(PadKind::Xbox));
}
#[test]
fn buttons_map_by_position_for_each_driver_family() {
let lettered = xpad();
for (code, button) in [
(BTN_SOUTH, PadButton::South),
(BTN_EAST, PadButton::East),
(BTN_NORTH, PadButton::West),
(BTN_WEST, PadButton::North),
(BTN_TL, PadButton::LeftShoulder),
(BTN_TR, PadButton::RightShoulder),
(BTN_THUMBL, PadButton::LeftStick),
(BTN_THUMBR, PadButton::RightStick),
(BTN_START, PadButton::Start),
(BTN_SELECT, PadButton::Select),
(BTN_MODE, PadButton::Guide),
] {
assert_eq!(
snapshot(&lettered, &[code], &[]).buttons,
buttons(&[button]),
"xpad {code:#x}"
);
}
let positional = probe(
SONY,
&XPAD_KEYS,
&[(ABS_X, XPAD_TRIGGER), (ABS_Y, XPAD_TRIGGER)],
);
assert_eq!(
snapshot(&positional, &[BTN_NORTH], &[]).buttons,
buttons(&[PadButton::North]),
"hid-playstation's triangle"
);
assert_eq!(
snapshot(&positional, &[BTN_WEST], &[]).buttons,
buttons(&[PadButton::West]),
"hid-playstation's square"
);
assert_eq!(snapshot(&positional, &[], &[]).kind, PadKind::PlayStation);
}
#[test]
fn the_hat_maps_onto_the_dpad() {
let pad = xpad();
for ((x, y), expected) in [
((0, -1), &[PadButton::DpadUp][..]),
((0, 1), &[PadButton::DpadDown]),
((-1, 0), &[PadButton::DpadLeft]),
((1, 0), &[PadButton::DpadRight]),
((1, -1), &[PadButton::DpadRight, PadButton::DpadUp]),
((0, 0), &[]),
] {
assert_eq!(
snapshot(&pad, &[], &[(ABS_HAT0X, x), (ABS_HAT0Y, y)]).buttons,
buttons(expected),
"hat at ({x}, {y})"
);
}
}
#[test]
fn dpad_keys_win_over_the_hat() {
let deck = probe(
InputId {
vendor: VENDOR_VALVE,
product: PRODUCT_STEAM_DECK,
..XBOX
},
&[
BTN_SOUTH,
BTN_DPAD_UP,
BTN_DPAD_DOWN,
BTN_DPAD_LEFT,
BTN_DPAD_RIGHT,
],
&[
(ABS_X, (-32767, 32767)),
(ABS_Y, (-32767, 32767)),
(ABS_HAT0X, (-32767, 32767)),
(ABS_HAT0Y, (-32767, 32767)),
],
);
for (code, button) in DPAD_KEYS {
assert_eq!(snapshot(&deck, &[code], &[]).buttons, buttons(&[button]));
}
let touched = snapshot(&deck, &[], &[(ABS_HAT0X, 32767), (ABS_HAT0Y, -32767)]);
assert!(touched.buttons.is_empty(), "{touched:?}");
assert_eq!(touched.kind, PadKind::SteamDeck);
}
#[test]
fn triggers_come_from_the_first_axis_present() {
let sticks = [(ABS_X, XPAD_STICK), (ABS_Y, XPAD_STICK)];
let both = probe(
XBOX,
&[BTN_SOUTH],
&[
sticks[0],
sticks[1],
(ABS_Z, XPAD_TRIGGER),
(ABS_RZ, XPAD_TRIGGER),
(ABS_BRAKE, (0, 1023)),
(ABS_GAS, (0, 1023)),
],
);
let pulled = snapshot(&both, &[], &[(ABS_Z, 255), (ABS_BRAKE, 0), (ABS_GAS, 1023)]);
assert_eq!(pulled.axis(PadAxis::LeftTrigger), 1.0, "ABS_Z first");
assert_eq!(pulled.axis(PadAxis::RightTrigger), 0.0, "ABS_GAS ignored");
let bluetooth = probe(
XBOX,
&[BTN_SOUTH],
&[
sticks[0],
sticks[1],
(ABS_BRAKE, (0, 1023)),
(ABS_GAS, (0, 1023)),
],
);
let pulled = snapshot(&bluetooth, &[], &[(ABS_BRAKE, 1023), (ABS_GAS, 0)]);
assert_eq!(pulled.axis(PadAxis::LeftTrigger), 1.0, "brake is left");
assert_eq!(pulled.axis(PadAxis::RightTrigger), 0.0);
let steam = probe(
XBOX,
&[BTN_SOUTH],
&[
sticks[0],
sticks[1],
(ABS_HAT2Y, (0, 32767)),
(ABS_HAT2X, (0, 32767)),
],
);
let pulled = snapshot(&steam, &[], &[(ABS_HAT2X, 32767), (ABS_HAT2Y, 0)]);
assert_eq!(pulled.axis(PadAxis::RightTrigger), 1.0, "HAT2X is right");
assert_eq!(pulled.axis(PadAxis::LeftTrigger), 0.0);
let digital = probe(
InputId {
vendor: VENDOR_NINTENDO,
..XBOX
},
&[BTN_SOUTH, BTN_TL2, BTN_TR2],
&sticks,
);
let pulled = snapshot(&digital, &[BTN_TR2], &[]);
assert_eq!(pulled.axis(PadAxis::RightTrigger), 1.0, "ZR held");
assert_eq!(pulled.axis(PadAxis::LeftTrigger), 0.0);
assert!(pulled.buttons.is_empty(), "a trigger, not a button");
}
#[test]
fn capability_bits_classify_pads() {
let class = |probe: &Probe| is_gamepad(&probe.keys, &probe.abs);
assert!(class(&xpad()), "an Xbox pad");
const KEY_ESC: u16 = 1;
const KEY_A: u16 = 30;
const KEY_SPACE: u16 = 57;
const BTN_LEFT: u16 = 0x110;
const BTN_TRIGGER: u16 = 0x120;
const BTN_TOUCH: u16 = 0x14a;
let axes = [(ABS_X, XPAD_STICK), (ABS_Y, XPAD_STICK)];
for (what, keys, axes) in [
("a keyboard", &[KEY_ESC, KEY_A, KEY_SPACE][..], &[][..]),
("a touchpad", &[BTN_LEFT, BTN_TOUCH], &axes[..]),
("a flight stick", &[BTN_TRIGGER], &axes),
(
"motion sensors",
&[],
&[
(ABS_X, XPAD_STICK),
(ABS_Y, XPAD_STICK),
(ABS_Z, XPAD_STICK),
],
),
("buttons with no stick", &[BTN_SOUTH, BTN_EAST], &[]),
("a pad missing ABS_Y", &[BTN_SOUTH], &[(ABS_X, XPAD_STICK)]),
] {
assert!(!class(&probe(XBOX, keys, axes)), "{what} is not a pad");
}
}
#[test]
fn vendor_and_product_name_the_family() {
let id = |vendor, product| InputId {
vendor,
product,
..XBOX
};
assert_eq!(kind_of(id(VENDOR_MICROSOFT, 0x028e)), PadKind::Xbox);
assert_eq!(
kind_of(id(VENDOR_VALVE, PRODUCT_STEAM_DECK)),
PadKind::SteamDeck
);
assert_eq!(
kind_of(id(VENDOR_VALVE, 0x11ff)),
PadKind::Xbox,
"Steam's virtual pad"
);
assert_eq!(kind_of(id(VENDOR_SONY, 0x0ce6)), PadKind::PlayStation);
assert_eq!(kind_of(id(VENDOR_NINTENDO, 0x2009)), PadKind::Switch);
assert_eq!(kind_of(id(0x1234, 0x5678)), PadKind::Generic);
}
}