529 lines
16 KiB
C++
529 lines
16 KiB
C++
/* -*- 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 <algorithm>
|
|
#include <unordered_map>
|
|
#include <cstddef>
|
|
|
|
#include <glib.h>
|
|
#include <linux/input.h>
|
|
#include <stdio.h>
|
|
#include <stdint.h>
|
|
#include <sys/ioctl.h>
|
|
#include <unistd.h>
|
|
#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<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:
|
|
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<UniquePtr<Gamepad>, 4> mGamepads;
|
|
};
|
|
|
|
// singleton instance
|
|
LinuxGamepadService* gService = nullptr;
|
|
|
|
void LinuxGamepadService::AddDevice(struct udev_device* dev) {
|
|
RefPtr<GamepadPlatformService> 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<Gamepad>();
|
|
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<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++;
|
|
}
|
|
}
|
|
|
|
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<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 =
|
|
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<GamepadPlatformService> 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<GamepadPlatformService> service =
|
|
GamepadPlatformService::GetParentService();
|
|
if (!service) {
|
|
return TRUE;
|
|
}
|
|
auto* gamepad = static_cast<Gamepad*>(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<GamepadHandle>& aGamepadHandle,
|
|
const Tainted<uint32_t>& 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
|