Gamepad Implementation on Linux
Hey everyone! If you remember my last post, one of the missing features was joystick support on Linux, which I’d say is pretty much essential for any game engine. We have gamepad support working on The Machinery now and, in this post, I want to share the implementation process, what you need to know to support gamepads in an application, and talk a little about the input system in the engine. I had never worked with joystick programming before and this is the research I did about the subject and some notes I took during development, so if I missed some point or misinterpreted some information, please let me know in the comments.
My first step was to visit the Linux Kernel Documentation
https://www.kernel.org/doc/html/latest/input/joydev/joystick-api.html,
which has an important note: “This document describes legacy js
interface. Newer clients are
encouraged to switch to the generic event (evdev
) interface.”, so let’s discover what it means.
Linux I/O follows a model where everything is a file, a concept called universality of I/O,
letting you read from a device the same way you read from a normal file. The evdev
kernel
interface gives us access to input devices events through /dev/input/eventX
files, so basically,
all you need to do is to read() from these files, which can also be
done in a non-blocking fashion using select() or
poll().
struct input_event {
struct timeval time;
unsigned short type;
unsigned short code;
unsigned int value;
};
struct input_event events[NUM_EVENTS];
int32_t num_events = read(fd, events, sizeof(events));
On top of that, we have libevedev
which is a wrapper for evdev
devices,
libinput
that helps with commonly used devices, and finally, X11 and Wayland. Note that libinput
and the
Window System don’t provide access to joystick events.
For the gamepad implementation, I decided to go only with the evdev
interface, partly because I
wanted to learn more about it, and partly because we are dealing with only one kind of device, which
I believe is a simple use case. Anyway, whether we are using libevedev
or not, we still need to
understand the evdev
kernel interface.
We know that we need to open a file and read from it, but which one? The /dev/input/eventX
file
names don’t provide any clues. The good thing is that udev
creates more descriptive symlinks in /dev/input/by-id
and dev/input/by-path
paths, although I’m
not 100% sure how udev
creates these links. What’s nice is that joystick devices are listed in
these paths by names ending with -event-joystick
(https://wiki.archlinux.org/index.php/Gamepad).
With that in mind we can use
tm_os_api->file_system->directory_entries()
to find already connected devices, and
tm_os_api->file_system->detailed_changes()
to monitor new ones.
Before starting to process device events, we need to gather some basic information. This can be done
using the ioctl()
system call, which
provides a way to do some operations that fall out of the standard I/O model. You can have a look at
all IOCTLs supplied by the event interface in the
input.h Linux
header, and understand a little more about them in the article Using the Input Subsystem, Part
II. For our purposes, we have to use the EVIOCGBIT
ioctl to check if the joystick device follows the Linux Gamepad
Specification (if the controller has the
BTN_GAMEPAD
key), find out which axes are supported by the device, and use EVIOCABS
ioctl to
get specific information about them.
Here are some examples of how to use these calls to get the information needed. Note that event types and codes can be found in the input-event-codes.h header:
static char *gamepad_button_names[BTN_THUMBR - BTN_GAMEPAD + 1] = {
[BTN_A - BTN_GAMEPAD] = "BTN_GAMEPAD/BTN_SOUTH/BTN_A",
[BTN_B - BTN_GAMEPAD] = "BTN_EAST/BTN_B",
[BTN_C - BTN_GAMEPAD] = "BTN_C",
[BTN_X - BTN_GAMEPAD] = "BTN_NORTH/BTN_X",
[BTN_Y - BTN_GAMEPAD] = "BTN_WEST/BTN_Y",
[BTN_Z - BTN_GAMEPAD] = "BTN_Z",
[BTN_TL - BTN_GAMEPAD] = "BTN_TL",
[BTN_TR - BTN_GAMEPAD] = "BTN_TR2",
[BTN_TL2 - BTN_GAMEPAD] = "BTN_TL2",
[BTN_TR2 - BTN_GAMEPAD] = "BTN_TR2",
[BTN_SELECT - BTN_GAMEPAD] = "BTN_SELECT",
[BTN_START - BTN_GAMEPAD] = "BTN_START",
[BTN_MODE - BTN_GAMEPAD] = "BTN_MODE",
[BTN_THUMBL - BTN_GAMEPAD] = "BTN_THUMBL",
[BTN_THUMBR - BTN_GAMEPAD] = "BTN_THUMBR"
};
uint32_t *supported_keys = NULL;
// Bit Array with KEY_CNT bits
tm_carray_temp_resize(supported_keys, tm_uint32_div_ceil(KEY_CNT, 32), ta);
if (ioctl(gamepad->fd.opaque, EVIOCGBIT(EV_KEY, KEY_MAX), supported_keys) >= 0) {
for (uint32_t c = BTN_GAMEPAD; c < BTN_THUMBR + 1; ++c) {
uint32_t idx = c / 32;
uint32_t pos = c % 32;
if (supported_keys[idx] & (1 << pos)) {
TM_LOG("name: %s\n", gamepad_button_names[c - BTN_GAMEPAD]);
}
}
}
typedef struct controller_absinfo_t
{
float value;
float max;
float min;
float deadzone;
} controller_absinfo_t;
static char *gamepad_axis_names[ABS_HAT3Y - ABS_X + 1] = {
[ABS_X] = "ABS_X",
[ABS_Y] = "ABS_Y",
[ABS_RX] ="ABS_RX",
[ABS_RY] = "ABS_RY",
[ABS_Z] = "ABS_Z",
[ABS_RZ] = "ABS_RZ",
[ABS_HAT0X] = "ABS_HAT0X",
[ABS_HAT0Y] = "ABS_HAT0Y",
[ABS_HAT1X] = "ABS_HAT1X",
[ABS_HAT1Y] = "ABS_HAT1Y",
[ABS_HAT2X] = "ABS_HAT2X",
[ABS_HAT2Y] = "ABS_HAT2Y",
};
uint32_t *supported_axis = NULL;
tm_carray_temp_resize(supported_axis, tm_uint32_div_ceil(ABS_CNT, 32), ta);
if (ioctl(gamepad->fd.opaque, EVIOCGBIT(EV_ABS, ABS_MAX), supported_axis) == -1)
return;
for (uint32_t c = ABS_X; c < ABS_HAT3Y + 1; ++c) {
idx = c / 32;
pos = c % 32;
if (supported_axis[idx] & (1 << pos)) {
TM_LOG("name: %s\n", gamepad_axis_names[c]);
controller_absinfo_t axis_info = { 0 };
struct input_absinfo absinfo = { 0 };
if (ioctl(gamepad->fd.opaque, EVIOCGABS(c), &absinfo) == -1)
continue;
axis_info.max = (float)absinfo.maximum;
axis_info.min = (float)absinfo.minimum;
axis_info.value = (float)absinfo.value;
axis_info.deadzone = (float)absinfo.flat;
TM_LOG("max: %f, min: %f, value: %f, deadzone: %f\n", axis_info.max, axis_info.min, axis_info.value, axis_info.deadzone);
}
}
To support a device using the
tm_input_api
you
need to create a
tm_input_source_i
interface, which provides details about the input device and supplies events to the application
using the
tm_input_source_i->events()
callback. We need to poll for events using evdev
, so we’ll do it in this callback. Below, you can
see how to do it using a circular buffer to keep events around:
static void poll()
{
struct input_event events[64];
for (uint32_t d = 0; d < NUM_LINUX_GAMEPADS; ++d) {
gamepad_controller_t *gamepad = &gamepads[d];
if (!gamepad->connected)
continue;
fd_set fds;
FD_ZERO(&fds);
FD_SET(gamepad->fd.opaque, &fds);
struct timeval timout;
timout.tv_sec = 0;
timout.tv_usec = 0;
bool any_changes = select(FD_SETSIZE, &fds, NULL, NULL, &timout) > 0;
if (any_changes) {
int32_t r = read(gamepad->fd.opaque, events, sizeof(events));
if (r < (int32_t)sizeof(struct input_event))
continue;
uint32_t num_events = (uint32_t)(r / sizeof(struct input_event));
for (uint32_t e = 0; e < num_events; ++e) {
struct input_event *ev = &events[e];
if (ev->type == EV_KEY) {
TM_LOG("key: %s value: %d\n", gamepad_button_names[ev->code - BTN_GAMEPAD], ev->value);
uint64_t tmid = button_map[ev->code - BTN_GAMEPAD];
tm_input_event_t btn_ev = {
.time = tm_os_api->time->now().opaque,
.source = &linux_gamepad_input_source,
.controller_id = gamepad->index,
.item_id = tmid,
.type = TM_INPUT_EVENT_TYPE_DATA_CHANGE,
.data.f.x = (float)ev->value,
};
gamepad_events[gamepad_events_n % LINUX_GAMEPAD_BUFFER_SIZE] = btn_ev;
}
}
}
}
}
The last detail I think is worth mentioning is the mapping between evdev
codes and
tm_input_gamepad_item
.
As you can note in the figure below, buttons can be directly mapped, but for axes, we have 1:1, 2:1,
and 1:2 mappings, e.g: ABS_X/Y
→ TM_INPUT_ITEM_LEFT_STICK
, ABS_HAT0X
→
TM_INPUT_GAMEPAD_DPAD_RIGHT/LEFT
.
This is not really a problem as we can have an if/else
for each specific axis, but I wanted to
keep the array/table approach using the evdev
code as a key, so for each entry in the array we
have a positive/negative mapping for the 1:2 case and a component index for the 2:1 case. This way
we can do something like the following in the poll()
code:
typedef struct axis_map_info_t {
uint8_t positive_map;
uint8_t negative_map;
uint8_t component_index;
} axis_map_info_t;
static axis_map_info_t axis_map[ABS_HAT3Y - ABS_X + 1] = {
[ABS_X] = { TM_INPUT_GAMEPAD_ITEM_LEFT_STICK, TM_INPUT_GAMEPAD_ITEM_LEFT_STICK, 0 },
[ABS_Y] = { TM_INPUT_GAMEPAD_ITEM_LEFT_STICK, TM_INPUT_GAMEPAD_ITEM_LEFT_STICK, 1 },
[ABS_HAT0X] = { TM_INPUT_GAMEPAD_ITEM_DPAD_RIGHT, TM_INPUT_GAMEPAD_ITEM_DPAD_LEFT, 0 },
};
uint64_t tmid = ev->value >= 0 ? axis_map[ev->code].positive_map : axis_map[ev->code].negative_map;
tm_vec2_t v;
v.x = axis_map[ev->code].component_index == 0 ? value : gamepad_state[gamepad->index][tmid].data.f.x;
v.y = axis_map[ev->code].component_index == 1 ? value : gamepad_state[gamepad->index][tmid].data.f.y;
Wrap up
I hope this information can be helpful, either if you want to create your implementation or just understand a little more about gamepad programming. To me, it was a nice experience and not as hard as I thought that would be when I started.