Source code

Revision control

Copy as Markdown

Other Tools

/* -*- 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.
*
*/
#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,
};
/*
* "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