Skip to main content

Module evdev

Module evdev 

Source
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 GamepadId and the PadKind its 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_REPORT whose mapped snapshot differs from the last one reported — change-only, as xinput does and for the same reasons: the seam’s snapshot is a level, and an identical one carries nothing.
  • Disconnected when a read fails: ENODEV is the ordinary unplug and is not an error; anything else also returns an EvdevError. 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

evdevseam
BTN_SOUTH, BTN_EASTSouth, 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_TRLeftShoulder, RightShoulder
BTN_THUMBL, BTN_THUMBRLeftStick, RightStick
BTN_START, BTN_SELECT, BTN_MODEStart, Select, Guide
BTN_DPAD_*, or else ABS_HAT0X/YDpadUp/Down/Left/Right
ABS_X, ABS_Y, ABS_RX, ABS_RYthe sticks, Y negated (evdev’s +Y is down)
ABS_Z / ABS_BRAKE / ABS_HAT2YLeftTrigger, first present; BTN_TL2 if none
ABS_RZ / ABS_GAS / ABS_HAT2XRightTrigger, 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§

EvdevError
What went wrong reading evdev. Each variant carries the OS error’s text.

Constants§

RESCAN_INTERVAL
How long /dev/input goes unlisted between scans for new pads.

Functions§

stick_axis
A stick axis from its input_absinfo range to −1…1, before any Y flip.
trigger_axis
A trigger from its input_absinfo range to 0…1, minimum at rest, clamped. A range with no extent reads 0.