From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:47:55 +0200 Subject: Adding upstream version 124.0.1. Signed-off-by: Daniel Baumann --- dom/gamepad/linux/LinuxGamepad.cpp | 529 +++++++++++++++++++++++++++++++++++++ 1 file changed, 529 insertions(+) create mode 100644 dom/gamepad/linux/LinuxGamepad.cpp (limited to 'dom/gamepad/linux/LinuxGamepad.cpp') diff --git a/dom/gamepad/linux/LinuxGamepad.cpp b/dom/gamepad/linux/LinuxGamepad.cpp new file mode 100644 index 0000000000..e7a7071b86 --- /dev/null +++ b/dom/gamepad/linux/LinuxGamepad.cpp @@ -0,0 +1,529 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * 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 +#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; +using mozilla::MakeUnique; +using mozilla::udev_device; +using mozilla::udev_enumerate; +using mozilla::udev_lib; +using mozilla::udev_list_entry; +using mozilla::udev_monitor; +using mozilla::UniquePtr; + +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. +struct Gamepad { + GamepadHandle handle; + 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: + LinuxGamepadService() : mMonitor(nullptr), mMonitorSourceID(0) {} + + void Startup(); + void Shutdown(); + + private: + void AddDevice(struct udev_device* dev); + void RemoveDevice(struct udev_device* dev); + void ScanForDevices(); + void AddMonitor(); + void RemoveMonitor(); + bool IsDeviceGamepad(struct udev_device* dev); + void ReadUdevChange(); + + // handler for data from /dev/input/eventN + static gboolean OnGamepadData(GIOChannel* source, GIOCondition condition, + gpointer data); + + // handler for data from udev monitor + static gboolean OnUdevMonitor(GIOChannel* source, GIOCondition condition, + gpointer data); + + udev_lib mUdev; + struct udev_monitor* mMonitor; + guint mMonitorSourceID; + // Information about currently connected gamepads. + AutoTArray, 4> mGamepads; +}; + +// singleton instance +LinuxGamepadService* gService = nullptr; + +void LinuxGamepadService::AddDevice(struct udev_device* dev) { + RefPtr service = + GamepadPlatformService::GetParentService(); + if (!service) { + return; + } + + const char* devpath = mUdev.udev_device_get_devnode(dev); + if (!devpath) { + return; + } + + // Ensure that this device hasn't already been added. + for (unsigned int i = 0; i < mGamepads.Length(); i++) { + if (strcmp(mGamepads[i]->devpath, devpath) == 0) { + return; + } + } + + auto gamepad = MakeUnique(); + snprintf(gamepad->devpath, sizeof(gamepad->devpath), "%s", devpath); + GIOChannel* channel = g_io_channel_new_file(devpath, "r", nullptr); + if (!channel) { + return; + } + + g_io_channel_set_flags(channel, G_IO_FLAG_NONBLOCK, nullptr); + g_io_channel_set_encoding(channel, nullptr, nullptr); + g_io_channel_set_buffered(channel, FALSE); + int fd = g_io_channel_unix_get_fd(channel); + + input_id id{0}; + if (ioctl(fd, EVIOCGID, &id) < 0) { + return; + } + + 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++; + } + } + + 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; + + // 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 = + g_io_add_watch(channel, GIOCondition(G_IO_IN | G_IO_ERR | G_IO_HUP), + OnGamepadData, gamepad.get()); + g_io_channel_unref(channel); + + mGamepads.AppendElement(std::move(gamepad)); +} + +void LinuxGamepadService::RemoveDevice(struct udev_device* dev) { + RefPtr service = + GamepadPlatformService::GetParentService(); + if (!service) { + return; + } + + const char* devpath = mUdev.udev_device_get_devnode(dev); + if (!devpath) { + return; + } + + for (unsigned int i = 0; i < mGamepads.Length(); i++) { + if (strcmp(mGamepads[i]->devpath, devpath) == 0) { + auto gamepad = std::move(mGamepads[i]); + mGamepads.RemoveElementAt(i); + + g_source_remove(gamepad->source_id); + service->RemoveGamepad(gamepad->handle); + + break; + } + } +} + +void LinuxGamepadService::ScanForDevices() { + struct udev_enumerate* en = mUdev.udev_enumerate_new(mUdev.udev); + mUdev.udev_enumerate_add_match_subsystem(en, "input"); + mUdev.udev_enumerate_scan_devices(en); + + struct udev_list_entry* dev_list_entry; + for (dev_list_entry = mUdev.udev_enumerate_get_list_entry(en); + dev_list_entry != nullptr; + dev_list_entry = mUdev.udev_list_entry_get_next(dev_list_entry)) { + 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 (IsDeviceGamepad(dev)) { + AddDevice(dev); + } + + mUdev.udev_device_unref(dev); + } + + mUdev.udev_enumerate_unref(en); +} + +void LinuxGamepadService::AddMonitor() { + // Add a monitor to watch for device changes + mMonitor = mUdev.udev_monitor_new_from_netlink(mUdev.udev, "udev"); + if (!mMonitor) { + // Not much we can do here. + return; + } + mUdev.udev_monitor_filter_add_match_subsystem_devtype(mMonitor, "input", + nullptr); + + int monitor_fd = mUdev.udev_monitor_get_fd(mMonitor); + GIOChannel* monitor_channel = g_io_channel_unix_new(monitor_fd); + mMonitorSourceID = g_io_add_watch(monitor_channel, + GIOCondition(G_IO_IN | G_IO_ERR | G_IO_HUP), + OnUdevMonitor, nullptr); + g_io_channel_unref(monitor_channel); + + mUdev.udev_monitor_enable_receiving(mMonitor); +} + +void LinuxGamepadService::RemoveMonitor() { + if (mMonitorSourceID) { + g_source_remove(mMonitorSourceID); + mMonitorSourceID = 0; + } + if (mMonitor) { + mUdev.udev_monitor_unref(mMonitor); + mMonitor = nullptr; + } +} + +void LinuxGamepadService::Startup() { + // Don't bother starting up if libudev couldn't be loaded or initialized. + if (!mUdev) { + return; + } + + AddMonitor(); + ScanForDevices(); +} + +void LinuxGamepadService::Shutdown() { + for (unsigned int i = 0; i < mGamepads.Length(); i++) { + g_source_remove(mGamepads[i]->source_id); + } + mGamepads.Clear(); + RemoveMonitor(); +} + +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(aDev); + if (!devpath) { + return false; + } + + return strncmp(devpath, kEvdevPath, strlen(kEvdevPath)) == 0; +} + +void LinuxGamepadService::ReadUdevChange() { + struct udev_device* dev = mUdev.udev_monitor_receive_device(mMonitor); + 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) { + RemoveDevice(dev); + } + } + mUdev.udev_device_unref(dev); +} + +// static +gboolean LinuxGamepadService::OnGamepadData(GIOChannel* source, + GIOCondition condition, + gpointer data) { + RefPtr service = + GamepadPlatformService::GetParentService(); + if (!service) { + return TRUE; + } + auto* gamepad = static_cast(data); + + // TODO: remove gamepad? + if (condition & (G_IO_ERR | G_IO_HUP)) { + return FALSE; + } + + while (true) { + struct input_event event {}; + gsize count; + GError* err = nullptr; + if (g_io_channel_read_chars(source, (gchar*)&event, sizeof(event), &count, + &err) != G_IO_STATUS_NORMAL || + count == 0) { + break; + } + + switch (event.type) { + 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; + } + } + + return TRUE; +} + +// static +gboolean LinuxGamepadService::OnUdevMonitor(GIOChannel* source, + GIOCondition condition, + gpointer data) { + if (condition & (G_IO_ERR | G_IO_HUP)) { + return FALSE; + } + + gService->ReadUdevChange(); + return TRUE; +} + +} // namespace + +namespace mozilla::dom { + +void StartGamepadMonitoring() { + if (gService) { + return; + } + gService = new LinuxGamepadService(); + gService->Startup(); +} + +void StopGamepadMonitoring() { + if (!gService) { + return; + } + gService->Shutdown(); + delete gService; + gService = nullptr; +} + +void SetGamepadLightIndicatorColor(const Tainted& aGamepadHandle, + const Tainted& aLightColorIndex, + const uint8_t& aRed, const uint8_t& aGreen, + const uint8_t& aBlue) { + // TODO: Bug 1523355. + NS_WARNING("Linux doesn't support gamepad light indicator."); +} + +} // namespace mozilla::dom -- cgit v1.2.3