From 761ca4cf49f85a7553725d2d2a73730e1b57f595 Mon Sep 17 00:00:00 2001 From: Tom Schuster Date: Thu, 11 Jan 2024 20:15:22 +0000 Subject: [PATCH] Bug 1680982 - Implement Linux/Unix Gamepad support using evdev. r=stransky Based on work by: Val Packett and coolreader18 . Differential Revision: https://phabricator.services.mozilla.com/D197865 --- dom/gamepad/GamepadRemapping.cpp | 41 ---- dom/gamepad/GamepadRemapping.h | 35 ++++ dom/gamepad/linux/LinuxGamepad.cpp | 299 ++++++++++++++++++++++------- dom/gamepad/moz.build | 2 +- 4 files changed, 269 insertions(+), 108 deletions(-) diff --git a/dom/gamepad/GamepadRemapping.cpp b/dom/gamepad/GamepadRemapping.cpp index 042f8a115e31..2c3ec252f55f 100644 --- a/dom/gamepad/GamepadRemapping.cpp +++ b/dom/gamepad/GamepadRemapping.cpp @@ -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(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; diff --git a/dom/gamepad/GamepadRemapping.h b/dom/gamepad/GamepadRemapping.h index 8b2038889fb2..0a508fe13491 100644 --- a/dom/gamepad/GamepadRemapping.h +++ b/dom/gamepad/GamepadRemapping.h @@ -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) diff --git a/dom/gamepad/linux/LinuxGamepad.cpp b/dom/gamepad/linux/LinuxGamepad.cpp index deee47b9d267..e7a7071b867a 100644 --- a/dom/gamepad/linux/LinuxGamepad.cpp +++ b/dom/gamepad/linux/LinuxGamepad.cpp @@ -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 +#include #include #include -#include +#include #include #include #include @@ -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 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 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 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 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 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(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; diff --git a/dom/gamepad/moz.build b/dom/gamepad/moz.build index 5d5cfbfa203a..db77ef8da68b 100644 --- a/dom/gamepad/moz.build +++ b/dom/gamepad/moz.build @@ -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"]