Expand description
The Linux gamepad backend: evdev, polled, onto the gamepad seam.
Evdev::new names /dev/input, and Evdev::poll reads every open pad
and hands what changed to a closure as GamepadEvents — the same shape
as xinput, so a game consumes it the same way:
use crcbl_input::{GamepadEvent, Stick};
let mut pads = crcbl_input::evdev::Evdev::new();
// Once a frame, before the frame's input is read:
pads.poll(|event| {
if let GamepadEvent::State { id, snapshot } = event {
let (x, y) = snapshot.stick(Stick::Left); // raw, +Y up, no dead zone
println!("pad {} left stick at ({x}, {y})", id.0);
}
})?;§Which nodes are pads
Every /dev/input/event* node is opened read-only and non-blocking and
probed: EVIOCGBIT for its keys and axes, and it is a pad if it has
BTN_GAMEPAD and both ABS_X and ABS_Y. A node that is not a pad is
closed and not probed again while the same node stays in the directory. A
node that cannot be opened — a keyboard’s is usually root-only, and a
pad’s is too for the moment before udev grants the seat user access — is
skipped without an error and tried again at the next scan, since what cannot
be opened cannot be told apart from a pad. A pad whose node the user
cannot read is therefore never found: desktop distributions and SteamOS
grant it through udev’s uaccess tag, which systemd’s rules put on
joysticks.
§What it reports
- Connected when a pad node appears, with a fresh
GamepadIdand thePadKindits USB vendor and product name, then a State if the pad is not at rest (the probe reads what is held and where every axis is). - State at each
SYN_REPORTwhose mapped snapshot differs from the last one reported — change-only, asxinputdoes and for the same reasons: the seam’s snapshot is a level, and an identical one carries nothing. - Disconnected when a read fails:
ENODEVis the ordinary unplug and is not an error; anything else also returns anEvdevError. A pad plugged back in is a new node and a new id.
After SYN_DROPPED — the kernel’s buffer overflowed and events were lost —
everything up to the next SYN_REPORT is discarded and the pad’s keys and
axes are read afresh, as the evdev docs prescribe.
§New pads are found by re-scanning
Open pads are read on every poll. /dev/input is listed again only once
RESCAN_INTERVAL has passed since the last scan, so a pad plugged in is
reported at most that late — the same trade as xinput’s re-probe, and
the same one second. inotify would report it at once, at the price of one
more descriptor and its own event parsing; nothing has needed it yet.
§The mapping
| evdev | seam |
|---|---|
BTN_SOUTH, BTN_EAST | South, East |
BTN_NORTH (BTN_X), BTN_WEST (BTN_Y) | West, North on Xbox-style drivers; North, West on Sony and Nintendo ones (below) |
BTN_TL, BTN_TR | LeftShoulder, RightShoulder |
BTN_THUMBL, BTN_THUMBR | LeftStick, RightStick |
BTN_START, BTN_SELECT, BTN_MODE | Start, Select, Guide |
BTN_DPAD_*, or else ABS_HAT0X/Y | DpadUp/Down/Left/Right |
ABS_X, ABS_Y, ABS_RX, ABS_RY | the sticks, Y negated (evdev’s +Y is down) |
ABS_Z / ABS_BRAKE / ABS_HAT2Y | LeftTrigger, first present; BTN_TL2 if none |
ABS_RZ / ABS_GAS / ABS_HAT2X | RightTrigger, first present; BTN_TR2 if none |
Sticks are scaled by stick_axis and triggers by trigger_axis, each
from the axis’s own EVIOCGABS minimum and maximum. The driver’s flat is
read but not applied: the seam’s axes are raw and the dead zone is the
binding’s (gamepad.rs).
The top and left face buttons depend on the driver, the one place the
kernel disagrees with itself. The header spells BTN_NORTH and BTN_X as
one code, and BTN_WEST and BTN_Y as another; hid-playstation,
hid-sony and hid-nintendo send the positional names, while xpad,
Steam Input’s virtual pad and hid-steam send the Xbox letters, so their
X — the left button — arrives as BTN_NORTH. The split is by USB vendor:
Sony and Nintendo positional, everything else lettered. Other drivers that
disagree are the “quirk zoo” the input design scopes out; the table
grows by demand.
§Steam Input’s virtual pad is read like any other
Valve’s vendor id, 0x28DE, is not filtered out. Without Steam Input,
Steam’s gamepad emulation reaching this backend as an ordinary controller
is how a game launched from Steam on a Deck gets the built-in controls. That
emulated pad is the only copy: per hid-steam’s source, the driver
removes its own gamepad node while Steam holds the controller’s hidraw
node, so one physical pad is not reported twice. Unverified on a Deck. A
game that turns Steam Input on (crcbl-steam’s SteamPads) would see the
pad twice, once from each, so crcbl::engine::steam::steam_input replaces
this backend rather than polling beside it; a vendor filter here is what
would let the two run together, and docs/backlog.md carries it.
§Not on other targets
This module is compiled on Linux (and into every target’s tests, which is how the mapping, the layouts and the poller are checked on the Windows and macOS runners); only the device access is Linux-only. No other target gets a stand-in that answers “no pads”.
Structs§
- Evdev
- The evdev pads on this machine, and their connection state.
Enums§
- Evdev
Error - What went wrong reading evdev. Each variant carries the OS error’s text.
Constants§
- RESCAN_
INTERVAL - How long
/dev/inputgoes unlisted between scans for new pads.
Functions§
- stick_
axis - A stick axis from its
input_absinforange to −1…1, before any Y flip. - trigger_
axis - A trigger from its
input_absinforange to 0…1,minimumat rest, clamped. A range with no extent reads 0.