Skip to content

Commit

Permalink
input: add gamepad support through evdev
Browse files Browse the repository at this point in the history
This is based on the existing SDL2 code, but without any additional
dependency on Linux.

Both buttons and axes are supported, but just like in the SDL2 code,
axes are transformed into buttons in order to fit into the existing mpv
code.  A possible future improvement would be to expose them as proper
axes, and take into account their movement.

I’ve tested this code on both PS3, PS4 and Switch controllers, in USB
and Bluetooth when possible, on Linux 5.18.

Unlike the SDL2 code, this supports more than one controller plugged in,
as it wasn’t much more code to handle that and it’s a useful feature to
me.
  • Loading branch information
linkmauve committed Jul 23, 2022
1 parent 6e4dd33 commit e65ec6f
Show file tree
Hide file tree
Showing 4 changed files with 388 additions and 3 deletions.
374 changes: 374 additions & 0 deletions input/evdev_gamepad.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,374 @@
/*
* This file is part of mpv.
*
* mpv is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* mpv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with mpv. If not, see <http://www.gnu.org/licenses/>.
*/

#include <dirent.h>
#include <fcntl.h>
#include <linux/input.h>
#include <linux/input-event-codes.h>
#include <stdatomic.h>
#include <stdbool.h>
#include <sys/eventfd.h>
#include <sys/ioctl.h>
#include <poll.h>
#include <unistd.h>
#include "common/common.h"
#include "common/msg.h"
#include "input.h"
#include "input/keycodes.h"

struct gamepad {
int fd;
int left_x_min, left_x_max;
int left_y_min, left_y_max;
int right_x_min, right_x_max;
int right_y_min, right_y_max;
int left_trigger, right_trigger;
char name[128];
};

#define MAX_GAMEPADS 4

static int event_fd = -1;

static const int button_map[][2] = {
{ BTN_SOUTH, MP_KEY_GAMEPAD_ACTION_DOWN },
{ BTN_EAST, MP_KEY_GAMEPAD_ACTION_RIGHT },
{ BTN_WEST, MP_KEY_GAMEPAD_ACTION_LEFT },
{ BTN_NORTH, MP_KEY_GAMEPAD_ACTION_UP },
{ BTN_SELECT, MP_KEY_GAMEPAD_BACK },
{ BTN_MODE, MP_KEY_GAMEPAD_MENU },
{ BTN_START, MP_KEY_GAMEPAD_START },
{ BTN_THUMBL, MP_KEY_GAMEPAD_LEFT_STICK },
{ BTN_THUMBR, MP_KEY_GAMEPAD_RIGHT_STICK },
{ BTN_TL, MP_KEY_GAMEPAD_LEFT_SHOULDER },
{ BTN_TR, MP_KEY_GAMEPAD_RIGHT_SHOULDER },
{ BTN_TL2, MP_KEY_GAMEPAD_LEFT_TRIGGER },
{ BTN_TR2, MP_KEY_GAMEPAD_RIGHT_TRIGGER },
{ BTN_DPAD_UP, MP_KEY_GAMEPAD_DPAD_UP },
{ BTN_DPAD_DOWN, MP_KEY_GAMEPAD_DPAD_DOWN },
{ BTN_DPAD_LEFT, MP_KEY_GAMEPAD_DPAD_LEFT },
{ BTN_DPAD_RIGHT, MP_KEY_GAMEPAD_DPAD_RIGHT },
};

static int lookup_button_mp_key(int evdev_key)
{
for (int i = 0; i < MP_ARRAY_SIZE(button_map); i++) {
if (button_map[i][0] == evdev_key) {
return button_map[i][1];
}
}
return -1;
}

static struct prev {
int left_x;
int left_y;
int right_x;
int right_y;
} prev;

static void handle_abs(struct mp_input_src *src, int code, int value, struct gamepad *gamepad)
{
int trigger;
switch (code) {
case ABS_HAT0X:
if (value == -1) {
mp_input_put_key(src->input_ctx, MP_KEY_GAMEPAD_DPAD_LEFT | MP_KEY_STATE_DOWN);
mp_input_put_key(src->input_ctx, MP_KEY_GAMEPAD_DPAD_RIGHT | MP_KEY_STATE_UP);
} else if (value == 0) {
mp_input_put_key(src->input_ctx, MP_KEY_GAMEPAD_DPAD_LEFT | MP_KEY_STATE_UP);
mp_input_put_key(src->input_ctx, MP_KEY_GAMEPAD_DPAD_RIGHT | MP_KEY_STATE_UP);
} else {
mp_input_put_key(src->input_ctx, MP_KEY_GAMEPAD_DPAD_LEFT | MP_KEY_STATE_UP);
mp_input_put_key(src->input_ctx, MP_KEY_GAMEPAD_DPAD_RIGHT | MP_KEY_STATE_DOWN);
}
break;
case ABS_HAT0Y:
if (value == -1) {
mp_input_put_key(src->input_ctx, MP_KEY_GAMEPAD_DPAD_UP | MP_KEY_STATE_DOWN);
mp_input_put_key(src->input_ctx, MP_KEY_GAMEPAD_DPAD_DOWN | MP_KEY_STATE_UP);
} else if (value == 0) {
mp_input_put_key(src->input_ctx, MP_KEY_GAMEPAD_DPAD_UP | MP_KEY_STATE_UP);
mp_input_put_key(src->input_ctx, MP_KEY_GAMEPAD_DPAD_DOWN | MP_KEY_STATE_UP);
} else {
mp_input_put_key(src->input_ctx, MP_KEY_GAMEPAD_DPAD_UP | MP_KEY_STATE_UP);
mp_input_put_key(src->input_ctx, MP_KEY_GAMEPAD_DPAD_DOWN | MP_KEY_STATE_DOWN);
}
break;
case ABS_X:
if (value < gamepad->left_x_min && prev.left_x != -1) {
mp_input_put_key(src->input_ctx, MP_KEY_GAMEPAD_LEFT_STICK_LEFT | MP_KEY_STATE_DOWN);
mp_input_put_key(src->input_ctx, MP_KEY_GAMEPAD_LEFT_STICK_RIGHT | MP_KEY_STATE_UP);
prev.left_x = -1;
} else if (value >= gamepad->left_x_min && value < gamepad->left_x_max && prev.left_x != 0) {
mp_input_put_key(src->input_ctx, MP_KEY_GAMEPAD_LEFT_STICK_LEFT | MP_KEY_STATE_UP);
mp_input_put_key(src->input_ctx, MP_KEY_GAMEPAD_LEFT_STICK_RIGHT | MP_KEY_STATE_UP);
prev.left_x = 0;
} else if (value >= gamepad->left_x_max && prev.left_x != 1) {
mp_input_put_key(src->input_ctx, MP_KEY_GAMEPAD_LEFT_STICK_LEFT | MP_KEY_STATE_UP);
mp_input_put_key(src->input_ctx, MP_KEY_GAMEPAD_LEFT_STICK_RIGHT | MP_KEY_STATE_DOWN);
prev.left_x = 1;
}
break;
case ABS_Y:
if (value < gamepad->left_y_min && prev.left_y != -1) {
mp_input_put_key(src->input_ctx, MP_KEY_GAMEPAD_LEFT_STICK_UP | MP_KEY_STATE_DOWN);
mp_input_put_key(src->input_ctx, MP_KEY_GAMEPAD_LEFT_STICK_DOWN | MP_KEY_STATE_UP);
prev.left_y = -1;
} else if (value >= gamepad->left_y_min && value < gamepad->left_y_max && prev.left_y != 0) {
mp_input_put_key(src->input_ctx, MP_KEY_GAMEPAD_LEFT_STICK_UP | MP_KEY_STATE_UP);
mp_input_put_key(src->input_ctx, MP_KEY_GAMEPAD_LEFT_STICK_DOWN | MP_KEY_STATE_UP);
prev.left_y = 0;
} else if (value >= gamepad->left_y_max && prev.left_y != 1) {
mp_input_put_key(src->input_ctx, MP_KEY_GAMEPAD_LEFT_STICK_UP | MP_KEY_STATE_UP);
mp_input_put_key(src->input_ctx, MP_KEY_GAMEPAD_LEFT_STICK_DOWN | MP_KEY_STATE_DOWN);
prev.left_y = 1;
}
break;
case ABS_RX:
if (value < gamepad->right_x_min && prev.right_x != -1) {
mp_input_put_key(src->input_ctx, MP_KEY_GAMEPAD_RIGHT_STICK_LEFT | MP_KEY_STATE_DOWN);
mp_input_put_key(src->input_ctx, MP_KEY_GAMEPAD_RIGHT_STICK_RIGHT | MP_KEY_STATE_UP);
prev.right_x = -1;
} else if (value >= gamepad->right_x_min && value < gamepad->right_x_max && prev.right_x != 0) {
mp_input_put_key(src->input_ctx, MP_KEY_GAMEPAD_RIGHT_STICK_LEFT | MP_KEY_STATE_UP);
mp_input_put_key(src->input_ctx, MP_KEY_GAMEPAD_RIGHT_STICK_RIGHT | MP_KEY_STATE_UP);
prev.right_x = 0;
} else if (value >= gamepad->right_x_max && prev.right_x != 1) {
mp_input_put_key(src->input_ctx, MP_KEY_GAMEPAD_RIGHT_STICK_LEFT | MP_KEY_STATE_UP);
mp_input_put_key(src->input_ctx, MP_KEY_GAMEPAD_RIGHT_STICK_RIGHT | MP_KEY_STATE_DOWN);
prev.right_x = 1;
}
break;
case ABS_RY:
if (value < gamepad->right_y_min && prev.right_y != -1) {
mp_input_put_key(src->input_ctx, MP_KEY_GAMEPAD_RIGHT_STICK_UP | MP_KEY_STATE_DOWN);
mp_input_put_key(src->input_ctx, MP_KEY_GAMEPAD_RIGHT_STICK_DOWN | MP_KEY_STATE_UP);
prev.right_y = -1;
} else if (value >= gamepad->right_y_min && value < gamepad->right_y_max && prev.right_y != 0) {
mp_input_put_key(src->input_ctx, MP_KEY_GAMEPAD_RIGHT_STICK_UP | MP_KEY_STATE_UP);
mp_input_put_key(src->input_ctx, MP_KEY_GAMEPAD_RIGHT_STICK_DOWN | MP_KEY_STATE_UP);
prev.right_y = 0;
} else if (value >= gamepad->right_y_max && prev.right_y != 1) {
mp_input_put_key(src->input_ctx, MP_KEY_GAMEPAD_RIGHT_STICK_UP | MP_KEY_STATE_UP);
mp_input_put_key(src->input_ctx, MP_KEY_GAMEPAD_RIGHT_STICK_DOWN | MP_KEY_STATE_DOWN);
prev.right_y = 1;
}
break;
case ABS_Z:
trigger = value >= gamepad->left_trigger ? MP_KEY_STATE_DOWN : MP_KEY_STATE_UP;
mp_input_put_key(src->input_ctx, MP_KEY_GAMEPAD_LEFT_TRIGGER | trigger);
break;
case ABS_RZ:
trigger = value >= gamepad->right_trigger ? MP_KEY_STATE_DOWN : MP_KEY_STATE_UP;
mp_input_put_key(src->input_ctx, MP_KEY_GAMEPAD_RIGHT_TRIGGER | trigger);
break;
}
}

static int test_device(struct mp_input_src *src, int dir_fd, char *name)
{
int fd = openat(dir_fd, name, O_RDWR | O_CLOEXEC);
if (fd < 0)
return -1;

#define BITS_PER_LONG (sizeof(unsigned long) * 8)
#define NBITS(x) ((((x)-1)/BITS_PER_LONG)+1)
#define EVDEV_OFF(x) ((x)%BITS_PER_LONG)
#define EVDEV_LONG(x) ((x)/BITS_PER_LONG)
#define test_bit(array, bit) ((array[EVDEV_LONG(bit)] >> EVDEV_OFF(bit)) & 1)

unsigned long keybit[NBITS(KEY_MAX)] = { 0 };
if (ioctl(fd, EVIOCGBIT(EV_KEY, sizeof(keybit)), keybit) < 0) {
close(fd);
return -1;
}

bool is_joystick = test_bit(keybit, BTN_SOUTH) && test_bit(keybit, BTN_EAST);
if (!is_joystick) {
MP_VERBOSE(src, "Device %s doesn't look like a gamepad, skipping.\n", name);
close(fd);
return -1;
}

return fd;
}

static void fill_gamepad(struct gamepad *gamepad)
{
/* If these axes are absent, there just won't be events for them so we
* can safely ignore the errors emitted here */
struct input_absinfo absinfo;
int fd = gamepad->fd;
if (ioctl(fd, EVIOCGABS(ABS_X), &absinfo) >= 0) {
int center = (absinfo.maximum - absinfo.minimum) / 2;
gamepad->left_x_min = absinfo.minimum + center / 2;
gamepad->left_x_max = absinfo.maximum - center / 2;
}
if (ioctl(fd, EVIOCGABS(ABS_Y), &absinfo) >= 0) {
int center = (absinfo.maximum - absinfo.minimum) / 2;
gamepad->left_y_min = absinfo.minimum + center / 2;
gamepad->left_y_max = absinfo.maximum - center / 2;
}
if (ioctl(fd, EVIOCGABS(ABS_Z), &absinfo) >= 0) {
gamepad->left_trigger = (absinfo.maximum - absinfo.minimum) / 2;
}
if (ioctl(fd, EVIOCGABS(ABS_RX), &absinfo) >= 0) {
int center = (absinfo.maximum - absinfo.minimum) / 2;
gamepad->right_x_min = absinfo.minimum + center / 2;
gamepad->right_x_max = absinfo.maximum - center / 2;
}
if (ioctl(fd, EVIOCGABS(ABS_RY), &absinfo) >= 0) {
int center = (absinfo.maximum - absinfo.minimum) / 2;
gamepad->right_y_min = absinfo.minimum + center / 2;
gamepad->right_y_max = absinfo.maximum - center / 2;
}
if (ioctl(fd, EVIOCGABS(ABS_RZ), &absinfo) >= 0) {
gamepad->right_trigger = (absinfo.maximum - absinfo.minimum) / 2;
}

if (ioctl(fd, EVIOCGNAME(sizeof(gamepad->name)), gamepad->name) < 0) {
gamepad->name[0] = '\0';
}
}

static int find_gamepads(struct mp_input_src *src, struct gamepad *gamepads, int max_gamepads)
{
DIR *dir = opendir("/dev/input");
if (!dir) {
MP_ERR(src, "opendir(\"/dev/input\") failed\n");
mp_input_src_init_done(src);
return -1;
}

int fd, num = 0;
struct dirent *dirent;
int dir_fd = dirfd(dir);
while ((dirent = readdir(dir))) {
if (strncmp(dirent->d_name, "event", 5) != 0)
continue;
if ((fd = test_device(src, dir_fd, dirent->d_name)) < 0)
continue;

gamepads[num].fd = fd;
fill_gamepad(&gamepads[num]);
MP_INFO(src, "Added controller: %s\n", gamepads[num].name);

num++;
if (num >= max_gamepads)
break;
}

return num;
}

static void request_cancel(struct mp_input_src *src)
{
MP_VERBOSE(src, "exiting...\n");
/* eventfd is basically a counter of how many events we wrote, we just need
* one here. */
eventfd_write(event_fd, 1);
}

static void uninit(struct mp_input_src *src)
{
MP_VERBOSE(src, "exited.\n");
}

static void close_gamepads(struct mp_input_src *src, struct gamepad *gamepads, int num)
{
while (num--) {
MP_INFO(src, "Removed controller: %s\n", gamepads[num].name);
close(gamepads[num].fd);
gamepads[num].fd = -1;
}
}

static void read_gamepad_thread(struct mp_input_src *src, void *param)
{
struct gamepad gamepads[MAX_GAMEPADS];
int num = find_gamepads(src, gamepads, MAX_GAMEPADS);
if (num == 0) {
MP_VERBOSE(src, "Couldn't find any gamepad.");
mp_input_src_init_done(src);
return;
}

event_fd = eventfd(0, EFD_CLOEXEC);
if (event_fd < 0) {
MP_ERR(src, "Couldn't create eventfd for gamepad.");
close_gamepads(src, gamepads, num);
mp_input_src_init_done(src);
return;
}

src->cancel = request_cancel;
src->uninit = uninit;

mp_input_src_init_done(src);

struct pollfd pfds[MAX_GAMEPADS + 1];
pfds[0].fd = event_fd;
pfds[0].events = POLLIN;
for (int i = 0; i < num; ++i) {
pfds[i + 1].fd = gamepads[i].fd;
pfds[i + 1].events = POLLIN;
}

int ret;
while ((ret = poll(pfds, num + 1, -1)) >= 0) {
/* First check whether we have to exit this thread. */
if (pfds[0].revents == POLLIN) {
eventfd_t value;
eventfd_read(event_fd, &value);
break;
}

/* Then handle a single gamepad, we’ll loop again if more than one has
* events for us. */
int i = 0, fd = -1;
for (; i < num; ++i) {
if (pfds[i + 1].revents == POLLIN) {
fd = pfds[i + 1].fd;
break;
}
}

struct input_event ev;
if (read(fd, &ev, sizeof(ev)) < 0) {
MP_ERR(src, "read() failed\n");
break;
}

if (ev.type == EV_KEY) {
const int key = lookup_button_mp_key(ev.code);
if (key != -1) {
const int value = ev.value ? MP_KEY_STATE_DOWN : MP_KEY_STATE_UP;
mp_input_put_key(src->input_ctx, key | value);
}
} else if (ev.type == EV_ABS) {
handle_abs(src, ev.code, ev.value, &gamepads[i]);
}
}

close_gamepads(src, gamepads, num);
}

void mp_input_evdev_gamepad_add(struct input_ctx *ictx)
{
mp_input_add_thread_src(ictx, NULL, read_gamepad_thread);
}
8 changes: 5 additions & 3 deletions input/input.c
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ const struct m_sub_options input_config = {
{"input-cursor", OPT_FLAG(enable_mouse_movements)},
{"input-vo-keyboard", OPT_FLAG(vo_key_input)},
{"input-media-keys", OPT_FLAG(use_media_keys)},
#if HAVE_SDL2_GAMEPAD
#if HAVE_SDL2_GAMEPAD || HAVE_EVDEV_GAMEPAD
{"input-gamepad", OPT_FLAG(use_gamepad)},
#endif
{"window-dragging", OPT_FLAG(allow_win_drag)},
Expand Down Expand Up @@ -1389,11 +1389,13 @@ void mp_input_load_config(struct input_ctx *ictx)
talloc_free(tmp);
}

#if HAVE_SDL2_GAMEPAD
if (ictx->opts->use_gamepad) {
#if HAVE_SDL2_GAMEPAD
mp_input_sdl_gamepad_add(ictx);
}
#elif HAVE_EVDEV_GAMEPAD
mp_input_evdev_gamepad_add(ictx);
#endif
}

input_unlock(ictx);
}
Expand Down
Loading

0 comments on commit e65ec6f

Please sign in to comment.