Bug 1680982 - Implement Linux/Unix Gamepad support using evdev. r=stransky

Based on work by: Val Packett <val@packett.cool> and coolreader18
<coolreader18@gmail.com>.

Differential Revision: https://phabricator.services.mozilla.com/D197865
This commit is contained in:
Tom Schuster 2024-01-11 20:15:22 +00:00
parent e0c8b40945
commit 761ca4cf49
4 changed files with 269 additions and 108 deletions

View file

@ -19,53 +19,12 @@
namespace mozilla::dom {
// Follow the canonical ordering recommendation for the "Standard Gamepad"
// from https://www.w3.org/TR/gamepad/#remapping.
enum CanonicalButtonIndex {
BUTTON_INDEX_PRIMARY,
BUTTON_INDEX_SECONDARY,
BUTTON_INDEX_TERTIARY,
BUTTON_INDEX_QUATERNARY,
BUTTON_INDEX_LEFT_SHOULDER,
BUTTON_INDEX_RIGHT_SHOULDER,
BUTTON_INDEX_LEFT_TRIGGER,
BUTTON_INDEX_RIGHT_TRIGGER,
BUTTON_INDEX_BACK_SELECT,
BUTTON_INDEX_START,
BUTTON_INDEX_LEFT_THUMBSTICK,
BUTTON_INDEX_RIGHT_THUMBSTICK,
BUTTON_INDEX_DPAD_UP,
BUTTON_INDEX_DPAD_DOWN,
BUTTON_INDEX_DPAD_LEFT,
BUTTON_INDEX_DPAD_RIGHT,
BUTTON_INDEX_META,
BUTTON_INDEX_COUNT
};
enum CanonicalAxisIndex {
AXIS_INDEX_LEFT_STICK_X,
AXIS_INDEX_LEFT_STICK_Y,
AXIS_INDEX_RIGHT_STICK_X,
AXIS_INDEX_RIGHT_STICK_Y,
AXIS_INDEX_COUNT
};
const float BUTTON_THRESHOLD_VALUE = 0.1f;
float NormalizeTouch(long aValue, long aMin, long aMax) {
return (2.f * (aValue - aMin) / static_cast<float>(aMax - aMin)) - 1.f;
}
bool AxisNegativeAsButton(float input) {
const float value = (input < -0.5f) ? 1.f : 0.f;
return value > BUTTON_THRESHOLD_VALUE;
}
bool AxisPositiveAsButton(float input) {
const float value = (input > 0.5f) ? 1.f : 0.f;
return value > BUTTON_THRESHOLD_VALUE;
}
double AxisToButtonValue(double aValue) {
// Mapping axis value range from (-1, +1) to (0, +1).
return (aValue + 1.0f) * 0.5f;

View file

@ -98,6 +98,41 @@ enum class GamepadId : uint32_t {
kVendor2836Product0001 = 0x28360001,
};
// Follow the canonical ordering recommendation for the "Standard Gamepad"
// from https://www.w3.org/TR/gamepad/#remapping.
enum CanonicalButtonIndex {
BUTTON_INDEX_PRIMARY,
BUTTON_INDEX_SECONDARY,
BUTTON_INDEX_TERTIARY,
BUTTON_INDEX_QUATERNARY,
BUTTON_INDEX_LEFT_SHOULDER,
BUTTON_INDEX_RIGHT_SHOULDER,
BUTTON_INDEX_LEFT_TRIGGER,
BUTTON_INDEX_RIGHT_TRIGGER,
BUTTON_INDEX_BACK_SELECT,
BUTTON_INDEX_START,
BUTTON_INDEX_LEFT_THUMBSTICK,
BUTTON_INDEX_RIGHT_THUMBSTICK,
BUTTON_INDEX_DPAD_UP,
BUTTON_INDEX_DPAD_DOWN,
BUTTON_INDEX_DPAD_LEFT,
BUTTON_INDEX_DPAD_RIGHT,
BUTTON_INDEX_META,
BUTTON_INDEX_COUNT
};
enum CanonicalAxisIndex {
AXIS_INDEX_LEFT_STICK_X,
AXIS_INDEX_LEFT_STICK_Y,
AXIS_INDEX_RIGHT_STICK_X,
AXIS_INDEX_RIGHT_STICK_Y,
AXIS_INDEX_COUNT
};
static inline bool AxisNegativeAsButton(double input) { return input < -0.5; }
static inline bool AxisPositiveAsButton(double input) { return input > 0.5; }
class GamepadRemapper {
NS_INLINE_DECL_THREADSAFE_REFCOUNTING(GamepadRemapper)

View file

@ -5,15 +5,16 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/*
* LinuxGamepadService: A Linux backend for the GamepadService.
* Derived from the kernel documentation at
* http://www.kernel.org/doc/Documentation/input/joystick-api.txt
* LinuxGamepadService: An evdev backend for the GamepadService.
*
* Ref: https://www.kernel.org/doc/html/latest/input/gamepad.html
*/
#include <algorithm>
#include <unordered_map>
#include <cstddef>
#include <glib.h>
#include <linux/joystick.h>
#include <linux/input.h>
#include <stdio.h>
#include <stdint.h>
#include <sys/ioctl.h>
@ -21,10 +22,15 @@
#include "nscore.h"
#include "mozilla/dom/GamepadHandle.h"
#include "mozilla/dom/GamepadPlatformService.h"
#include "mozilla/dom/GamepadRemapping.h"
#include "mozilla/Tainting.h"
#include "mozilla/UniquePtr.h"
#include "mozilla/Sprintf.h"
#include "udev.h"
#define BITS_PER_LONG ((sizeof(unsigned long)) * 8)
#define BITS_TO_LONGS(x) (((x) + BITS_PER_LONG - 1) / BITS_PER_LONG)
namespace {
using namespace mozilla::dom;
@ -36,19 +42,42 @@ using mozilla::udev_list_entry;
using mozilla::udev_monitor;
using mozilla::UniquePtr;
static const float kMaxAxisValue = 32767.0;
static const char kJoystickPath[] = "/dev/input/js";
static const char kEvdevPath[] = "/dev/input/event";
static inline bool TestBit(const unsigned long* arr, size_t bit) {
return !!(arr[bit / BITS_PER_LONG] & (1LL << (bit % BITS_PER_LONG)));
}
static inline double ScaleAxis(const input_absinfo& info, int value) {
return 2.0 * (value - info.minimum) / (double)(info.maximum - info.minimum) -
1.0;
}
// TODO: should find a USB identifier for each device so we can
// provide something that persists across connect/disconnect cycles.
typedef struct {
struct Gamepad {
GamepadHandle handle;
guint source_id;
int numAxes;
int numButtons;
char idstring[256];
char devpath[PATH_MAX];
} Gamepad;
bool isStandardGamepad = false;
RefPtr<GamepadRemapper> remapper = nullptr;
guint source_id = UINT_MAX;
char idstring[256] = {0};
char devpath[PATH_MAX] = {0};
uint8_t key_map[KEY_MAX] = {0};
uint8_t abs_map[ABS_MAX] = {0};
std::unordered_map<uint16_t, input_absinfo> abs_info;
};
static inline bool LoadAbsInfo(int fd, Gamepad* gamepad, uint16_t code) {
input_absinfo info{0};
if (ioctl(fd, EVIOCGABS(code), &info) < 0) {
return false;
}
if (info.minimum == info.maximum) {
return false;
}
gamepad->abs_info.emplace(code, std::move(info));
return true;
}
class LinuxGamepadService {
public:
@ -63,10 +92,10 @@ class LinuxGamepadService {
void ScanForDevices();
void AddMonitor();
void RemoveMonitor();
bool is_gamepad(struct udev_device* dev);
bool IsDeviceGamepad(struct udev_device* dev);
void ReadUdevChange();
// handler for data from /dev/input/jsN
// handler for data from /dev/input/eventN
static gboolean OnGamepadData(GIOChannel* source, GIOCondition condition,
gpointer data);
@ -114,37 +143,143 @@ void LinuxGamepadService::AddDevice(struct udev_device* dev) {
g_io_channel_set_encoding(channel, nullptr, nullptr);
g_io_channel_set_buffered(channel, FALSE);
int fd = g_io_channel_unix_get_fd(channel);
char name[128];
if (ioctl(fd, JSIOCGNAME(sizeof(name)), &name) == -1) {
strcpy(name, "unknown");
input_id id{0};
if (ioctl(fd, EVIOCGID, &id) < 0) {
return;
}
const char* vendor_id =
mUdev.udev_device_get_property_value(dev, "ID_VENDOR_ID");
const char* model_id =
mUdev.udev_device_get_property_value(dev, "ID_MODEL_ID");
if (!vendor_id || !model_id) {
struct udev_device* parent =
mUdev.udev_device_get_parent_with_subsystem_devtype(dev, "input",
nullptr);
if (parent) {
vendor_id = mUdev.udev_device_get_sysattr_value(parent, "id/vendor");
model_id = mUdev.udev_device_get_sysattr_value(parent, "id/product");
char name[128]{0};
if (ioctl(fd, EVIOCGNAME(sizeof(name)), &name) < 0) {
strcpy(name, "Unknown Device");
}
SprintfLiteral(gamepad->idstring, "%04" PRIx16 "-%04" PRIx16 "-%s", id.vendor,
id.product, name);
unsigned long keyBits[BITS_TO_LONGS(KEY_CNT)] = {0};
unsigned long absBits[BITS_TO_LONGS(ABS_CNT)] = {0};
if (ioctl(fd, EVIOCGBIT(EV_KEY, sizeof(keyBits)), keyBits) < 0 ||
ioctl(fd, EVIOCGBIT(EV_ABS, sizeof(absBits)), absBits) < 0) {
return;
}
/* Here, we try to support even strange cases where proper semantic
* BTN_GAMEPAD button are combined with arbitrary extra buttons. */
/* These are mappings where the index is a CanonicalButtonIndex and the value
* is an evdev code */
const std::array<uint16_t, BUTTON_INDEX_COUNT> kStandardButtons = {
/* BUTTON_INDEX_PRIMARY = */ BTN_SOUTH,
/* BUTTON_INDEX_SECONDARY = */ BTN_EAST,
/* BUTTON_INDEX_TERTIARY = */ BTN_WEST,
/* BUTTON_INDEX_QUATERNARY = */ BTN_NORTH,
/* BUTTON_INDEX_LEFT_SHOULDER = */ BTN_TL,
/* BUTTON_INDEX_RIGHT_SHOULDER = */ BTN_TR,
/* BUTTON_INDEX_LEFT_TRIGGER = */ BTN_TL2,
/* BUTTON_INDEX_RIGHT_TRIGGER = */ BTN_TR2,
/* BUTTON_INDEX_BACK_SELECT = */ BTN_SELECT,
/* BUTTON_INDEX_START = */ BTN_START,
/* BUTTON_INDEX_LEFT_THUMBSTICK = */ BTN_THUMBL,
/* BUTTON_INDEX_RIGHT_THUMBSTICK = */ BTN_THUMBR,
/* BUTTON_INDEX_DPAD_UP = */ BTN_DPAD_UP,
/* BUTTON_INDEX_DPAD_DOWN = */ BTN_DPAD_DOWN,
/* BUTTON_INDEX_DPAD_LEFT = */ BTN_DPAD_LEFT,
/* BUTTON_INDEX_DPAD_RIGHT = */ BTN_DPAD_RIGHT,
/* BUTTON_INDEX_META = */ BTN_MODE,
};
const std::array<uint16_t, AXIS_INDEX_COUNT> kStandardAxes = {
/* AXIS_INDEX_LEFT_STICK_X = */ ABS_X,
/* AXIS_INDEX_LEFT_STICK_Y = */ ABS_Y,
/* AXIS_INDEX_RIGHT_STICK_X = */ ABS_RX,
/* AXIS_INDEX_RIGHT_STICK_Y = */ ABS_RY,
};
/*
* According to https://www.kernel.org/doc/html/latest/input/gamepad.html,
* "All gamepads that follow the protocol described here map BTN_GAMEPAD",
* so we can use it as a proxy for semantic buttons in general. If it's
* enabled, we're probably going to be acting like a standard gamepad
*/
uint32_t numButtons = 0;
if (TestBit(keyBits, BTN_GAMEPAD)) {
gamepad->isStandardGamepad = true;
for (uint8_t button = 0; button < BUTTON_INDEX_COUNT; button++) {
gamepad->key_map[kStandardButtons[button]] = button;
}
numButtons = BUTTON_INDEX_COUNT;
}
// Now, go through the non-semantic buttons and handle them as extras
for (uint16_t key = 0; key < KEY_MAX; key++) {
// Skip standard buttons
if (gamepad->isStandardGamepad &&
std::find(kStandardButtons.begin(), kStandardButtons.end(), key) !=
kStandardButtons.end()) {
continue;
}
if (TestBit(keyBits, key)) {
gamepad->key_map[key] = numButtons++;
}
}
snprintf(gamepad->idstring, sizeof(gamepad->idstring), "%s-%s-%s",
vendor_id ? vendor_id : "unknown", model_id ? model_id : "unknown",
name);
char numAxes = 0, numButtons = 0;
ioctl(fd, JSIOCGAXES, &numAxes);
gamepad->numAxes = numAxes;
ioctl(fd, JSIOCGBUTTONS, &numButtons);
gamepad->numButtons = numButtons;
uint32_t numAxes = 0;
if (gamepad->isStandardGamepad) {
for (uint8_t i = 0; i < AXIS_INDEX_COUNT; i++) {
gamepad->abs_map[kStandardAxes[i]] = i;
LoadAbsInfo(fd, gamepad.get(), kStandardAxes[i]);
}
numAxes = AXIS_INDEX_COUNT;
gamepad->handle = service->AddGamepad(
gamepad->idstring, mozilla::dom::GamepadMappingType::_empty,
mozilla::dom::GamepadHand::_empty, gamepad->numButtons, gamepad->numAxes,
0, 0, 0); // TODO: Bug 680289, implement gamepad haptics for Linux.
// These are not real axis and get remapped to buttons.
LoadAbsInfo(fd, gamepad.get(), ABS_HAT0X);
LoadAbsInfo(fd, gamepad.get(), ABS_HAT0Y);
}
for (uint16_t i = 0; i < ABS_MAX; ++i) {
if (gamepad->isStandardGamepad &&
(std::find(kStandardAxes.begin(), kStandardAxes.end(), i) !=
kStandardAxes.end() ||
i == ABS_HAT0X || i == ABS_HAT0Y)) {
continue;
}
if (TestBit(absBits, i)) {
if (LoadAbsInfo(fd, gamepad.get(), i)) {
gamepad->abs_map[i] = numAxes++;
}
}
}
if (numAxes == 0) {
NS_WARNING("Gamepad with zero axes detected?");
}
if (numButtons == 0) {
NS_WARNING("Gamepad with zero buttons detected?");
}
// NOTE: This almost always true, so we basically never use the remapping
// code.
if (gamepad->isStandardGamepad) {
gamepad->handle =
service->AddGamepad(gamepad->idstring, GamepadMappingType::Standard,
GamepadHand::_empty, numButtons, numAxes, 0, 0, 0);
} else {
bool defaultRemapper = false;
RefPtr<GamepadRemapper> remapper =
GetGamepadRemapper(id.vendor, id.product, defaultRemapper);
MOZ_ASSERT(remapper);
remapper->SetAxisCount(numAxes);
remapper->SetButtonCount(numButtons);
gamepad->handle = service->AddGamepad(
gamepad->idstring, remapper->GetMappingType(), GamepadHand::_empty,
remapper->GetButtonCount(), remapper->GetAxisCount(), 0,
remapper->GetLightIndicatorCount(), remapper->GetTouchEventCount());
gamepad->remapper = remapper.forget();
}
// TODO: Bug 680289, implement gamepad haptics for Linux.
// TODO: Bug 1523355, implement gamepad lighindicator and touch for Linux.
gamepad->source_id =
@ -192,7 +327,7 @@ void LinuxGamepadService::ScanForDevices() {
const char* path = mUdev.udev_list_entry_get_name(dev_list_entry);
struct udev_device* dev =
mUdev.udev_device_new_from_syspath(mUdev.udev, path);
if (is_gamepad(dev)) {
if (IsDeviceGamepad(dev)) {
AddDevice(dev);
}
@ -235,7 +370,9 @@ void LinuxGamepadService::RemoveMonitor() {
void LinuxGamepadService::Startup() {
// Don't bother starting up if libudev couldn't be loaded or initialized.
if (!mUdev) return;
if (!mUdev) {
return;
}
AddMonitor();
ScanForDevices();
@ -249,25 +386,23 @@ void LinuxGamepadService::Shutdown() {
RemoveMonitor();
}
bool LinuxGamepadService::is_gamepad(struct udev_device* dev) {
if (!mUdev.udev_device_get_property_value(dev, "ID_INPUT_JOYSTICK"))
bool LinuxGamepadService::IsDeviceGamepad(struct udev_device* aDev) {
if (!mUdev.udev_device_get_property_value(aDev, "ID_INPUT_JOYSTICK")) {
return false;
}
const char* devpath = mUdev.udev_device_get_devnode(dev);
const char* devpath = mUdev.udev_device_get_devnode(aDev);
if (!devpath) {
return false;
}
if (strncmp(kJoystickPath, devpath, sizeof(kJoystickPath) - 1) != 0) {
return false;
}
return true;
return strncmp(devpath, kEvdevPath, strlen(kEvdevPath)) == 0;
}
void LinuxGamepadService::ReadUdevChange() {
struct udev_device* dev = mUdev.udev_monitor_receive_device(mMonitor);
const char* action = mUdev.udev_device_get_action(dev);
if (is_gamepad(dev)) {
if (IsDeviceGamepad(dev)) {
const char* action = mUdev.udev_device_get_action(dev);
if (strcmp(action, "add") == 0) {
AddDevice(dev);
} else if (strcmp(action, "remove") == 0) {
@ -289,10 +424,12 @@ gboolean LinuxGamepadService::OnGamepadData(GIOChannel* source,
auto* gamepad = static_cast<Gamepad*>(data);
// TODO: remove gamepad?
if (condition & G_IO_ERR || condition & G_IO_HUP) return FALSE;
if (condition & (G_IO_ERR | G_IO_HUP)) {
return FALSE;
}
while (true) {
struct js_event event;
struct input_event event {};
gsize count;
GError* err = nullptr;
if (g_io_channel_read_chars(source, (gchar*)&event, sizeof(event), &count,
@ -301,19 +438,47 @@ gboolean LinuxGamepadService::OnGamepadData(GIOChannel* source,
break;
}
// TODO: store device state?
if (event.type & JS_EVENT_INIT) {
continue;
}
switch (event.type) {
case JS_EVENT_BUTTON:
service->NewButtonEvent(gamepad->handle, event.number, !!event.value);
break;
case JS_EVENT_AXIS:
service->NewAxisMoveEvent(gamepad->handle, event.number,
((float)event.value) / kMaxAxisValue);
case EV_KEY:
if (gamepad->isStandardGamepad) {
service->NewButtonEvent(gamepad->handle, gamepad->key_map[event.code],
!!event.value);
} else {
gamepad->remapper->RemapButtonEvent(
gamepad->handle, gamepad->key_map[event.code], !!event.value);
}
break;
case EV_ABS: {
if (!gamepad->abs_info.count(event.code)) {
continue;
}
double scaledValue =
ScaleAxis(gamepad->abs_info[event.code], event.value);
if (gamepad->isStandardGamepad) {
switch (event.code) {
case ABS_HAT0X:
service->NewButtonEvent(gamepad->handle, BUTTON_INDEX_DPAD_LEFT,
AxisNegativeAsButton(scaledValue));
service->NewButtonEvent(gamepad->handle, BUTTON_INDEX_DPAD_RIGHT,
AxisPositiveAsButton(scaledValue));
break;
case ABS_HAT0Y:
service->NewButtonEvent(gamepad->handle, BUTTON_INDEX_DPAD_UP,
AxisNegativeAsButton(scaledValue));
service->NewButtonEvent(gamepad->handle, BUTTON_INDEX_DPAD_DOWN,
AxisPositiveAsButton(scaledValue));
break;
default:
service->NewAxisMoveEvent(
gamepad->handle, gamepad->abs_map[event.code], scaledValue);
break;
}
} else {
gamepad->remapper->RemapAxisMoveEvent(
gamepad->handle, gamepad->abs_map[event.code], scaledValue);
}
} break;
}
}
@ -324,7 +489,9 @@ gboolean LinuxGamepadService::OnGamepadData(GIOChannel* source,
gboolean LinuxGamepadService::OnUdevMonitor(GIOChannel* source,
GIOCondition condition,
gpointer data) {
if (condition & G_IO_ERR || condition & G_IO_HUP) return FALSE;
if (condition & (G_IO_ERR | G_IO_HUP)) {
return FALSE;
}
gService->ReadUdevChange();
return TRUE;

View file

@ -59,7 +59,7 @@ elif CONFIG["MOZ_WIDGET_TOOLKIT"] == "windows":
UNIFIED_SOURCES += ["windows/WindowsGamepad.cpp"]
elif CONFIG["MOZ_WIDGET_TOOLKIT"] == "android":
UNIFIED_SOURCES += ["android/AndroidGamepad.cpp"]
elif CONFIG["OS_ARCH"] == "Linux":
elif CONFIG["OS_ARCH"] in ("Linux", "FreeBSD", "DragonFly"):
UNIFIED_SOURCES += ["linux/LinuxGamepad.cpp"]
else:
UNIFIED_SOURCES += ["fallback/FallbackGamepad.cpp"]