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