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/YTM_INPUT_ITEM_LEFT_STICK, ABS_HAT0XTM_INPUT_GAMEPAD_DPAD_RIGHT/LEFT.

Gamepad input mapping.

Gamepad input mapping.

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.

by Raphael De Vasconcelos Nascimento