tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

LinuxGamepad.cpp (16457B)


      1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
      2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
      3 /* This Source Code Form is subject to the terms of the Mozilla Public
      4 * License, v. 2.0. If a copy of the MPL was not distributed with this
      5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      6 
      7 /*
      8 * LinuxGamepadService: An evdev backend for the GamepadService.
      9 *
     10 * Ref: https://www.kernel.org/doc/html/latest/input/gamepad.html
     11 */
     12 #include <glib.h>
     13 #include <linux/input.h>
     14 #include <stdint.h>
     15 #include <stdio.h>
     16 #include <sys/ioctl.h>
     17 #include <unistd.h>
     18 
     19 #include <algorithm>
     20 #include <cstddef>
     21 #include <unordered_map>
     22 
     23 #include "mozilla/Sprintf.h"
     24 #include "mozilla/Tainting.h"
     25 #include "mozilla/UniquePtr.h"
     26 #include "mozilla/dom/GamepadHandle.h"
     27 #include "mozilla/dom/GamepadPlatformService.h"
     28 #include "mozilla/dom/GamepadRemapping.h"
     29 #include "nscore.h"
     30 #include "udev.h"
     31 
     32 #define BITS_PER_LONG ((sizeof(unsigned long)) * 8)
     33 #define BITS_TO_LONGS(x) (((x) + BITS_PER_LONG - 1) / BITS_PER_LONG)
     34 
     35 namespace {
     36 
     37 using namespace mozilla::dom;
     38 using mozilla::MakeUnique;
     39 using mozilla::udev_device;
     40 using mozilla::udev_enumerate;
     41 using mozilla::udev_lib;
     42 using mozilla::udev_list_entry;
     43 using mozilla::udev_monitor;
     44 using mozilla::UniquePtr;
     45 
     46 static const char kEvdevPath[] = "/dev/input/event";
     47 
     48 static inline bool TestBit(const unsigned long* arr, size_t bit) {
     49  return !!(arr[bit / BITS_PER_LONG] & (1LL << (bit % BITS_PER_LONG)));
     50 }
     51 
     52 static inline double ScaleAxis(const input_absinfo& info, int value) {
     53  return 2.0 * (value - info.minimum) / (double)(info.maximum - info.minimum) -
     54         1.0;
     55 }
     56 
     57 // TODO: should find a USB identifier for each device so we can
     58 // provide something that persists across connect/disconnect cycles.
     59 struct Gamepad {
     60  GamepadHandle handle;
     61  bool isStandardGamepad = false;
     62  RefPtr<GamepadRemapper> remapper = nullptr;
     63  guint source_id = UINT_MAX;
     64  char idstring[256] = {0};
     65  char devpath[PATH_MAX] = {0};
     66  uint8_t key_map[KEY_MAX] = {0};
     67  uint8_t abs_map[ABS_MAX] = {0};
     68  std::unordered_map<uint16_t, input_absinfo> abs_info;
     69 };
     70 
     71 static inline bool LoadAbsInfo(int fd, Gamepad* gamepad, uint16_t code) {
     72  input_absinfo info{0};
     73  if (ioctl(fd, EVIOCGABS(code), &info) < 0) {
     74    return false;
     75  }
     76  if (info.minimum == info.maximum) {
     77    return false;
     78  }
     79  gamepad->abs_info.emplace(code, std::move(info));
     80  return true;
     81 }
     82 
     83 class LinuxGamepadService {
     84 public:
     85  LinuxGamepadService() : mMonitor(nullptr), mMonitorSourceID(0) {}
     86 
     87  void Startup();
     88  void Shutdown();
     89 
     90 private:
     91  void AddDevice(struct udev_device* dev);
     92  void RemoveDevice(struct udev_device* dev);
     93  void ScanForDevices();
     94  void AddMonitor();
     95  void RemoveMonitor();
     96  bool IsDeviceGamepad(struct udev_device* dev);
     97  void ReadUdevChange();
     98 
     99  // handler for data from /dev/input/eventN
    100  static gboolean OnGamepadData(GIOChannel* source, GIOCondition condition,
    101                                gpointer data);
    102 
    103  // handler for data from udev monitor
    104  static gboolean OnUdevMonitor(GIOChannel* source, GIOCondition condition,
    105                                gpointer data);
    106 
    107  udev_lib mUdev;
    108  struct udev_monitor* mMonitor;
    109  guint mMonitorSourceID;
    110  // Information about currently connected gamepads.
    111  AutoTArray<UniquePtr<Gamepad>, 4> mGamepads;
    112 };
    113 
    114 // singleton instance
    115 LinuxGamepadService* gService = nullptr;
    116 
    117 void LinuxGamepadService::AddDevice(struct udev_device* dev) {
    118  RefPtr<GamepadPlatformService> service =
    119      GamepadPlatformService::GetParentService();
    120  if (!service) {
    121    return;
    122  }
    123 
    124  const char* devpath = mUdev.udev_device_get_devnode(dev);
    125  if (!devpath) {
    126    return;
    127  }
    128 
    129  // Ensure that this device hasn't already been added.
    130  for (unsigned int i = 0; i < mGamepads.Length(); i++) {
    131    if (strcmp(mGamepads[i]->devpath, devpath) == 0) {
    132      return;
    133    }
    134  }
    135 
    136  auto gamepad = MakeUnique<Gamepad>();
    137  snprintf(gamepad->devpath, sizeof(gamepad->devpath), "%s", devpath);
    138  GIOChannel* channel = g_io_channel_new_file(devpath, "r", nullptr);
    139  if (!channel) {
    140    return;
    141  }
    142 
    143  g_io_channel_set_flags(channel, G_IO_FLAG_NONBLOCK, nullptr);
    144  g_io_channel_set_encoding(channel, nullptr, nullptr);
    145  g_io_channel_set_buffered(channel, FALSE);
    146  int fd = g_io_channel_unix_get_fd(channel);
    147 
    148  input_id id{0};
    149  if (ioctl(fd, EVIOCGID, &id) < 0) {
    150    return;
    151  }
    152 
    153  char name[128]{0};
    154  if (ioctl(fd, EVIOCGNAME(sizeof(name)), &name) < 0) {
    155    strcpy(name, "Unknown Device");
    156  }
    157 
    158  SprintfLiteral(gamepad->idstring, "%04" PRIx16 "-%04" PRIx16 "-%s", id.vendor,
    159                 id.product, name);
    160 
    161  unsigned long keyBits[BITS_TO_LONGS(KEY_CNT)] = {0};
    162  unsigned long absBits[BITS_TO_LONGS(ABS_CNT)] = {0};
    163  if (ioctl(fd, EVIOCGBIT(EV_KEY, sizeof(keyBits)), keyBits) < 0 ||
    164      ioctl(fd, EVIOCGBIT(EV_ABS, sizeof(absBits)), absBits) < 0) {
    165    return;
    166  }
    167 
    168  /* Here, we try to support even strange cases where proper semantic
    169   * BTN_GAMEPAD button are combined with arbitrary extra buttons. */
    170 
    171  /* These are mappings where the index is a CanonicalButtonIndex and the value
    172   * is an evdev code */
    173  const std::array<uint16_t, BUTTON_INDEX_COUNT> kStandardButtons = {
    174      /* BUTTON_INDEX_PRIMARY = */ BTN_SOUTH,
    175      /* BUTTON_INDEX_SECONDARY = */ BTN_EAST,
    176      /* BUTTON_INDEX_TERTIARY = */ BTN_WEST,
    177      /* BUTTON_INDEX_QUATERNARY = */ BTN_NORTH,
    178      /* BUTTON_INDEX_LEFT_SHOULDER = */ BTN_TL,
    179      /* BUTTON_INDEX_RIGHT_SHOULDER = */ BTN_TR,
    180      /* BUTTON_INDEX_LEFT_TRIGGER = */ BTN_TL2,
    181      /* BUTTON_INDEX_RIGHT_TRIGGER = */ BTN_TR2,
    182      /* BUTTON_INDEX_BACK_SELECT = */ BTN_SELECT,
    183      /* BUTTON_INDEX_START = */ BTN_START,
    184      /* BUTTON_INDEX_LEFT_THUMBSTICK = */ BTN_THUMBL,
    185      /* BUTTON_INDEX_RIGHT_THUMBSTICK = */ BTN_THUMBR,
    186      /* BUTTON_INDEX_DPAD_UP = */ BTN_DPAD_UP,
    187      /* BUTTON_INDEX_DPAD_DOWN = */ BTN_DPAD_DOWN,
    188      /* BUTTON_INDEX_DPAD_LEFT = */ BTN_DPAD_LEFT,
    189      /* BUTTON_INDEX_DPAD_RIGHT = */ BTN_DPAD_RIGHT,
    190      /* BUTTON_INDEX_META = */ BTN_MODE,
    191  };
    192  const std::array<uint16_t, AXIS_INDEX_COUNT> kStandardAxes = {
    193      /* AXIS_INDEX_LEFT_STICK_X = */ ABS_X,
    194      /* AXIS_INDEX_LEFT_STICK_Y = */ ABS_Y,
    195      /* AXIS_INDEX_RIGHT_STICK_X = */ ABS_RX,
    196      /* AXIS_INDEX_RIGHT_STICK_Y = */ ABS_RY,
    197  };
    198 
    199  /*
    200   * According to https://www.kernel.org/doc/html/latest/input/gamepad.html,
    201   * "All gamepads that follow the protocol described here map BTN_GAMEPAD",
    202   * so we can use it as a proxy for semantic buttons in general. If it's
    203   * enabled, we're probably going to be acting like a standard gamepad
    204   */
    205  uint32_t numButtons = 0;
    206  if (TestBit(keyBits, BTN_GAMEPAD)) {
    207    gamepad->isStandardGamepad = true;
    208    for (uint8_t button = 0; button < BUTTON_INDEX_COUNT; button++) {
    209      gamepad->key_map[kStandardButtons[button]] = button;
    210    }
    211    numButtons = BUTTON_INDEX_COUNT;
    212  }
    213 
    214  // Now, go through the non-semantic buttons and handle them as extras
    215  for (uint16_t key = 0; key < KEY_MAX; key++) {
    216    // Skip standard buttons
    217    if (gamepad->isStandardGamepad &&
    218        std::find(kStandardButtons.begin(), kStandardButtons.end(), key) !=
    219            kStandardButtons.end()) {
    220      continue;
    221    }
    222 
    223    if (TestBit(keyBits, key)) {
    224      gamepad->key_map[key] = numButtons++;
    225    }
    226  }
    227 
    228  uint32_t numAxes = 0;
    229  if (gamepad->isStandardGamepad) {
    230    for (uint8_t i = 0; i < AXIS_INDEX_COUNT; i++) {
    231      gamepad->abs_map[kStandardAxes[i]] = i;
    232      LoadAbsInfo(fd, gamepad.get(), kStandardAxes[i]);
    233    }
    234    numAxes = AXIS_INDEX_COUNT;
    235 
    236    // These are not real axis and get remapped to buttons.
    237    LoadAbsInfo(fd, gamepad.get(), ABS_HAT0X);
    238    LoadAbsInfo(fd, gamepad.get(), ABS_HAT0Y);
    239  }
    240 
    241  for (uint16_t i = 0; i < ABS_MAX; ++i) {
    242    if (gamepad->isStandardGamepad &&
    243        (std::find(kStandardAxes.begin(), kStandardAxes.end(), i) !=
    244             kStandardAxes.end() ||
    245         i == ABS_HAT0X || i == ABS_HAT0Y)) {
    246      continue;
    247    }
    248 
    249    if (TestBit(absBits, i)) {
    250      if (LoadAbsInfo(fd, gamepad.get(), i)) {
    251        gamepad->abs_map[i] = numAxes++;
    252      }
    253    }
    254  }
    255 
    256  if (numAxes == 0) {
    257    NS_WARNING("Gamepad with zero axes detected?");
    258  }
    259  if (numButtons == 0) {
    260    NS_WARNING("Gamepad with zero buttons detected?");
    261  }
    262 
    263  // NOTE: This almost always true, so we basically never use the remapping
    264  // code.
    265  if (gamepad->isStandardGamepad) {
    266    gamepad->handle =
    267        service->AddGamepad(gamepad->idstring, GamepadMappingType::Standard,
    268                            GamepadHand::_empty, numButtons, numAxes, 0, 0, 0);
    269  } else {
    270    bool defaultRemapper = false;
    271    RefPtr<GamepadRemapper> remapper =
    272        GetGamepadRemapper(id.vendor, id.product, defaultRemapper);
    273    MOZ_ASSERT(remapper);
    274    remapper->SetAxisCount(numAxes);
    275    remapper->SetButtonCount(numButtons);
    276 
    277    gamepad->handle = service->AddGamepad(
    278        gamepad->idstring, remapper->GetMappingType(), GamepadHand::_empty,
    279        remapper->GetButtonCount(), remapper->GetAxisCount(), 0,
    280        remapper->GetLightIndicatorCount(), remapper->GetTouchEventCount());
    281    gamepad->remapper = remapper.forget();
    282  }
    283  // TODO: Bug 680289, implement gamepad haptics for Linux.
    284  // TODO: Bug 1523355, implement gamepad lighindicator and touch for Linux.
    285 
    286  gamepad->source_id =
    287      g_io_add_watch(channel, GIOCondition(G_IO_IN | G_IO_ERR | G_IO_HUP),
    288                     OnGamepadData, gamepad.get());
    289  g_io_channel_unref(channel);
    290 
    291  mGamepads.AppendElement(std::move(gamepad));
    292 }
    293 
    294 void LinuxGamepadService::RemoveDevice(struct udev_device* dev) {
    295  RefPtr<GamepadPlatformService> service =
    296      GamepadPlatformService::GetParentService();
    297  if (!service) {
    298    return;
    299  }
    300 
    301  const char* devpath = mUdev.udev_device_get_devnode(dev);
    302  if (!devpath) {
    303    return;
    304  }
    305 
    306  for (unsigned int i = 0; i < mGamepads.Length(); i++) {
    307    if (strcmp(mGamepads[i]->devpath, devpath) == 0) {
    308      auto gamepad = std::move(mGamepads[i]);
    309      mGamepads.RemoveElementAt(i);
    310 
    311      g_source_remove(gamepad->source_id);
    312      service->RemoveGamepad(gamepad->handle);
    313 
    314      break;
    315    }
    316  }
    317 }
    318 
    319 void LinuxGamepadService::ScanForDevices() {
    320  struct udev_enumerate* en = mUdev.udev_enumerate_new(mUdev.udev);
    321  mUdev.udev_enumerate_add_match_subsystem(en, "input");
    322  mUdev.udev_enumerate_scan_devices(en);
    323 
    324  struct udev_list_entry* dev_list_entry;
    325  for (dev_list_entry = mUdev.udev_enumerate_get_list_entry(en);
    326       dev_list_entry != nullptr;
    327       dev_list_entry = mUdev.udev_list_entry_get_next(dev_list_entry)) {
    328    const char* path = mUdev.udev_list_entry_get_name(dev_list_entry);
    329    struct udev_device* dev =
    330        mUdev.udev_device_new_from_syspath(mUdev.udev, path);
    331    if (IsDeviceGamepad(dev)) {
    332      AddDevice(dev);
    333    }
    334 
    335    mUdev.udev_device_unref(dev);
    336  }
    337 
    338  mUdev.udev_enumerate_unref(en);
    339 }
    340 
    341 void LinuxGamepadService::AddMonitor() {
    342  // Add a monitor to watch for device changes
    343  mMonitor = mUdev.udev_monitor_new_from_netlink(mUdev.udev, "udev");
    344  if (!mMonitor) {
    345    // Not much we can do here.
    346    return;
    347  }
    348  mUdev.udev_monitor_filter_add_match_subsystem_devtype(mMonitor, "input",
    349                                                        nullptr);
    350 
    351  int monitor_fd = mUdev.udev_monitor_get_fd(mMonitor);
    352  GIOChannel* monitor_channel = g_io_channel_unix_new(monitor_fd);
    353  mMonitorSourceID = g_io_add_watch(monitor_channel,
    354                                    GIOCondition(G_IO_IN | G_IO_ERR | G_IO_HUP),
    355                                    OnUdevMonitor, nullptr);
    356  g_io_channel_unref(monitor_channel);
    357 
    358  mUdev.udev_monitor_enable_receiving(mMonitor);
    359 }
    360 
    361 void LinuxGamepadService::RemoveMonitor() {
    362  if (mMonitorSourceID) {
    363    g_source_remove(mMonitorSourceID);
    364    mMonitorSourceID = 0;
    365  }
    366  if (mMonitor) {
    367    mUdev.udev_monitor_unref(mMonitor);
    368    mMonitor = nullptr;
    369  }
    370 }
    371 
    372 void LinuxGamepadService::Startup() {
    373  // Don't bother starting up if libudev couldn't be loaded or initialized.
    374  if (!mUdev) {
    375    return;
    376  }
    377 
    378  AddMonitor();
    379  ScanForDevices();
    380 }
    381 
    382 void LinuxGamepadService::Shutdown() {
    383  for (unsigned int i = 0; i < mGamepads.Length(); i++) {
    384    g_source_remove(mGamepads[i]->source_id);
    385  }
    386  mGamepads.Clear();
    387  RemoveMonitor();
    388 }
    389 
    390 bool LinuxGamepadService::IsDeviceGamepad(struct udev_device* aDev) {
    391  if (!mUdev.udev_device_get_property_value(aDev, "ID_INPUT_JOYSTICK")) {
    392    return false;
    393  }
    394 
    395  const char* devpath = mUdev.udev_device_get_devnode(aDev);
    396  if (!devpath) {
    397    return false;
    398  }
    399 
    400  return strncmp(devpath, kEvdevPath, strlen(kEvdevPath)) == 0;
    401 }
    402 
    403 void LinuxGamepadService::ReadUdevChange() {
    404  struct udev_device* dev = mUdev.udev_monitor_receive_device(mMonitor);
    405  if (IsDeviceGamepad(dev)) {
    406    const char* action = mUdev.udev_device_get_action(dev);
    407    if (strcmp(action, "add") == 0) {
    408      AddDevice(dev);
    409    } else if (strcmp(action, "remove") == 0) {
    410      RemoveDevice(dev);
    411    }
    412  }
    413  mUdev.udev_device_unref(dev);
    414 }
    415 
    416 // static
    417 gboolean LinuxGamepadService::OnGamepadData(GIOChannel* source,
    418                                            GIOCondition condition,
    419                                            gpointer data) {
    420  RefPtr<GamepadPlatformService> service =
    421      GamepadPlatformService::GetParentService();
    422  if (!service) {
    423    return TRUE;
    424  }
    425  auto* gamepad = static_cast<Gamepad*>(data);
    426 
    427  // TODO: remove gamepad?
    428  if (condition & (G_IO_ERR | G_IO_HUP)) {
    429    return FALSE;
    430  }
    431 
    432  while (true) {
    433    struct input_event event{};
    434    gsize count;
    435    GError* err = nullptr;
    436    if (g_io_channel_read_chars(source, (gchar*)&event, sizeof(event), &count,
    437                                &err) != G_IO_STATUS_NORMAL ||
    438        count == 0) {
    439      break;
    440    }
    441 
    442    switch (event.type) {
    443      case EV_KEY:
    444        if (gamepad->isStandardGamepad) {
    445          service->NewButtonEvent(gamepad->handle, gamepad->key_map[event.code],
    446                                  !!event.value);
    447        } else {
    448          gamepad->remapper->RemapButtonEvent(
    449              gamepad->handle, gamepad->key_map[event.code], !!event.value);
    450        }
    451        break;
    452      case EV_ABS: {
    453        if (!gamepad->abs_info.count(event.code)) {
    454          continue;
    455        }
    456 
    457        double scaledValue =
    458            ScaleAxis(gamepad->abs_info[event.code], event.value);
    459        if (gamepad->isStandardGamepad) {
    460          switch (event.code) {
    461            case ABS_HAT0X:
    462              service->NewButtonEvent(gamepad->handle, BUTTON_INDEX_DPAD_LEFT,
    463                                      AxisNegativeAsButton(scaledValue));
    464              service->NewButtonEvent(gamepad->handle, BUTTON_INDEX_DPAD_RIGHT,
    465                                      AxisPositiveAsButton(scaledValue));
    466              break;
    467            case ABS_HAT0Y:
    468              service->NewButtonEvent(gamepad->handle, BUTTON_INDEX_DPAD_UP,
    469                                      AxisNegativeAsButton(scaledValue));
    470              service->NewButtonEvent(gamepad->handle, BUTTON_INDEX_DPAD_DOWN,
    471                                      AxisPositiveAsButton(scaledValue));
    472              break;
    473            default:
    474              service->NewAxisMoveEvent(
    475                  gamepad->handle, gamepad->abs_map[event.code], scaledValue);
    476              break;
    477          }
    478        } else {
    479          gamepad->remapper->RemapAxisMoveEvent(
    480              gamepad->handle, gamepad->abs_map[event.code], scaledValue);
    481        }
    482      } break;
    483    }
    484  }
    485 
    486  return TRUE;
    487 }
    488 
    489 // static
    490 gboolean LinuxGamepadService::OnUdevMonitor(GIOChannel* source,
    491                                            GIOCondition condition,
    492                                            gpointer data) {
    493  if (condition & (G_IO_ERR | G_IO_HUP)) {
    494    return FALSE;
    495  }
    496 
    497  gService->ReadUdevChange();
    498  return TRUE;
    499 }
    500 
    501 }  // namespace
    502 
    503 namespace mozilla::dom {
    504 
    505 void StartGamepadMonitoring() {
    506  if (gService) {
    507    return;
    508  }
    509  gService = new LinuxGamepadService();
    510  gService->Startup();
    511 }
    512 
    513 void StopGamepadMonitoring() {
    514  if (!gService) {
    515    return;
    516  }
    517  gService->Shutdown();
    518  delete gService;
    519  gService = nullptr;
    520 }
    521 
    522 void SetGamepadLightIndicatorColor(const Tainted<GamepadHandle>& aGamepadHandle,
    523                                   const Tainted<uint32_t>& aLightColorIndex,
    524                                   const uint8_t& aRed, const uint8_t& aGreen,
    525                                   const uint8_t& aBlue) {
    526  // TODO: Bug 1523355.
    527  NS_WARNING("Linux doesn't support gamepad light indicator.");
    528 }
    529 
    530 }  // namespace mozilla::dom