There are two gamepad drivers, joydev and evedev.
Very inaccurate definitions of what driver is and how drivers operates:
In linux, driver is a program runned with lots of priviliges, that creates a file in /dev
and implement handlers for read/write requests on this file.
That means that you can communicate with any device with text editor!
Me, reading data from /dev/input/js0 while pushing random buttons on the gamepad
Joydev is an old, legacy driver. It still works, still is present on most systems. But it lacks certain features like OS-level gamepad calibration and do not support some crazy joysticks.
The good thing about joydev - it is crazy simple.
Some docs from linux kernel: https://www.kernel.org/doc/Documentation/input/joystick-api.txt
Basically there is a file, "/dev/input/js0". And to read joystick events - just open the file and read some bytes out of it, with normal reading procedure.
Bytes will represent a sturct like this:
#[derive(Debug, Clone, Copy)]
#[repr(C)]
struct JsEvent {
time: u32,
value: i16,
type_: u8,
number: u8,
}
let mut f = File::open("/dev/input/js0").unwrap();
loop {
let mut b: [u8; std::mem::size_of::<JsEvent>()] = [0; std::mem::size_of::<JsEvent>()];
f.read_exact(&mut b).unwrap();
let event: JsEvent = unsafe { std::mem::transmute(b) };
if (event.type_ & JsEventType::Axis as u8) != 0 {
let value = event.value as f32 / std::u16::MAX as f32 * 2.;
println!("Axis number {}: changed value to {}", event.number, value);
}
if (event.type_ & JsEventType::Button as u8) != 0 {
println!("Digital button number {}: changed value to {}", event.number, event.value != 0);
}
}
So this is the all the code needed to get some data out of the joystick. Nice!
Evdev clearly is less joy to use.
Evdev creates an event file in /dev/input/event*.
The only article I found, and a really good one: https://ourmachinery.com/post/gamepad-implementation-on-linux/
Main difference and a challenge introduced by this /dev/input/event* files - normal reading procedures are not enough anymore.
In linux, there are 3 syscalls to work with files.
read
, write
and ioctl
.
read/write are the "normal" one. In rust we usually call them with some nice functions std::fs
.
ioctl
is a weird one, introduced when read/write became not sufficient for linux.
Itresting side note: there were an alternative! Plan9, more pure unix-like system, managed to avoid ioctl
weirdness and used just read/write. http://catb.org/~esr/writings/taoup/html/plan9.html.
Plan9 is really cool! Not only device drivers, but all the system's services operates as files with just read/write interface. You can do things like cat /dev/screen > screenshot
to capture a screenshot, how cool is that?
Anyways, back to linux.
libc
ioctl declaration
pub unsafe extern "C" fn ioctl(
fd: c_int,
request: c_ulong,
...
) -> c_int
C docs: https://man7.org/linux/man-pages/man2/ioctl.2.html
request
is a device specific number and, technically, any device driver may use any notation for request
and accept any amount of arguments as ...
.
However there is a common notation for input/output devices. In linux kernel there are tons of macros to build a request ID and drivers follow this notation.
If we boil down those C macros to a nice rust's const fn
we will get something like this:
/// Encode an ioctl command.
pub const fn ioc(dir: u64, type_: u64, nr: u64, size: u64) -> u64 {
(((dir as u32) << DIRSHIFT)
| ((type_ as u32) << TYPESHIFT)
| ((nr as u32) << NRSHIFT)
| ((size as u32) << SIZESHIFT)) as u64
}
And most ioctl
call will look like
libc::ioctl(file_descriptor, ioc(dir, type_, nr, size), &mut data);
dir
/type_
/nr
/size
are real parameters describing ioctl
command (I have no clues why they use smart macroses instead of 4 u8 function arguments, legacy, I guess?).
On top of this there are lots of macros like EVIOCGABS
. Check them out here: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/include/uapi/linux/input.h#n129
#define EVIOCGABS(abs) _IOR('E', 0x40 + (abs), struct input_absinfo) /* get abs value/limits */
They look like this, and boiled down to const fn will look something like this:
pub const fn eviocgabs(abs: u32) -> u64 {
ioc(READ | WRITE, b'E', 0x40 + abs, std::mem::size_of::<input_absinfo>() as _)
}
This eviocgabs
is going to be used later like this:
libc::ioctl(fd, eviocgabs(code), &mut axis_data);
It is an ioctl
call with a request for some absolute value, and the argument is a pointer to a result data.
And, finally, back to the gamepad implementation. To get our axis/buttons state we need to do 3 steps:
- go through all /dev/input/event* and figure who is the gamepad
- ask the gamepad about axises/buttons metadata
- and finally get the data itself
Now I am mostly based on GLFW https://github.com/glfw/glfw/blob/master/src/linux_joystick.c implementation (the best docs on evdev's gamepad I found)
For the first step - just try to open all the /dev/input/event* files, and whoever will successefully open - probably is some sort of an input device.
For the second step we will need a few ioctl
call to get axis/button maps. And device who will not give that map - is not a gamepad.
let mut key_bits: [u8; (KEY_CNT as usize + 7) / 8] = [0; (KEY_CNT as usize + 7) / 8];
if libc::ioctl(fd, eviocgbit(std::mem::size_of(key_bits)), key_bits.as_mut_ptr()) < 0 {
panic!();
}
This is the way to get key's binding, for example. There are lots of ioctl
calls and metadata to acquire, but thats the idea - feed some pointers to ioctl
, get all the metadata, validate that this is a gamepad and we know what are the axis.
And the third, last step - back to normal reading routing, pretty much like joydev.
In my implementation I already used a lot of libc
stuff to work with ioctl
, so this time it would be libc::read
instad of std::fs::read
, but this is just implementation details, it is certainly doable with std::fs::read
.
loop {
let mut event = InputEvent::default();
if libc::read(
self.fd,
&mut event as *mut _ as *mut _,
std::mem::size_of_val(&e),
) < 0
{
// handle disconnect
return;
}
if event.type_ == EV_KEY as _ {
println!("Digital button number {}: changed value to {}", event.code, event.value != 0);
}
if event.type_ == EV_ABS as _ {
let info = ..; // thing we got from ioctl while joystick initialisation
let value = value / (info.maximum - info.minimum); // not exactly correct formula, but thats the idea
println!("Axis number {}: changed value to {}", event.code, value);
}
}
Intresting that code
in event supposed to be a gamepad's button code, defined in https://github.com/torvalds/linux/blob/master/include/uapi/linux/input-event-codes.h#L379
But in reality it is not. Some gampeads just assigns random codes to random buttons.
What most libraries do instead (SDL, GLFW) - during gamepad initialisation they assign a number to each of a presented button.
And than they use a hardcoded, per-gamepad map https://github.com/gabomdq/SDL_GameControllerDB to go from this 0..buttons_count identifier to a real button, like BTN_A/BTN_B.