bug 604039 - Linux gamepad backend. r=karlt
authorTed Mielczarek <ted.mielczarek@gmail.com>
Wed, 03 Aug 2011 14:12:08 -0400
changeset 125678 f04511d857697dc51921aabd644e81f1c44ef50e
parent 125677 2e774935eaec1d6092023fc63c31fad61cd76ebf
child 125679 6873f9a7c4d62e112774ae86fc2205aba09483a6
push id24461
push useremorley@mozilla.com
push dateThu, 21 Mar 2013 11:51:51 +0000
treeherdermozilla-central@a73a2b5c423b [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerskarlt
bugs604039
milestone22.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
bug 604039 - Linux gamepad backend. r=karlt
configure.in
hal/Makefile.in
hal/linux/LinuxGamepad.cpp
hal/linux/udev.h
--- a/configure.in
+++ b/configure.in
@@ -5963,16 +5963,23 @@ if test "$MOZ_GAMEPAD"; then
         else
             MOZ_GAMEPAD=
         fi
         if test -z "$MOZ_GAMEPAD"; then
            AC_MSG_ERROR([Couldn't find the DirectX SDK, needed for gamepad support. Please install it or, reconfigure with --disable-gamepad to disable gamepad support.])
         fi
         MOZ_GAMEPAD_BACKEND=windows
         ;;
+    Linux)
+        MOZ_CHECK_HEADER([linux/joystick.h])
+        if test "$ac_cv_header_linux_joystick_h" != "yes"; then
+          AC_MSG_ERROR([Can't find header linux/joystick.h, needed for gamepad support. Please install Linux kernel headers or reconfigure with --disable-gamepad to disable gamepad support.])
+        fi
+        MOZ_GAMEPAD_BACKEND=linux
+        ;;
     *)
         ;;
    esac
 
   AC_DEFINE(MOZ_GAMEPAD)
 fi
 AC_SUBST(MOZ_GAMEPAD)
 AC_SUBST(MOZ_GAMEPAD_BACKEND)
--- a/hal/Makefile.in
+++ b/hal/Makefile.in
@@ -46,16 +46,19 @@ ifeq (stub,$(MOZ_GAMEPAD_BACKEND))
 CPPSRCS += FallbackGamepad.cpp
 endif
 ifeq (cocoa,$(MOZ_GAMEPAD_BACKEND))
 CPPSRCS += CocoaGamepad.cpp
 endif
 ifeq (windows,$(MOZ_GAMEPAD_BACKEND))
 CPPSRCS += WindowsGamepad.cpp
 endif
+ifeq (linux,$(MOZ_GAMEPAD_BACKEND))
+CPPSRCS += LinuxGamepad.cpp
+endif
 
 ifeq (android,$(MOZ_WIDGET_TOOLKIT))
 CPPSRCS += \
   AndroidHal.cpp \
   AndroidSensor.cpp \
   FallbackPower.cpp \
   FallbackAlarm.cpp \
   $(NULL)
new file mode 100644
--- /dev/null
+++ b/hal/linux/LinuxGamepad.cpp
@@ -0,0 +1,364 @@
+/* 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: A Linux backend for the GamepadService.
+ * Derived from the kernel documentation at
+ * http://www.kernel.org/doc/Documentation/input/joystick-api.txt
+ */
+#include <algorithm>
+#include <cstddef>
+
+#include <glib.h>
+#include <stdio.h>
+#include <stdint.h>
+#include <sys/ioctl.h>
+#include <unistd.h>
+
+#include "nscore.h"
+#include "mozilla/dom/GamepadService.h"
+#include "udev.h"
+
+// Include this later because it also does #define JS_VERSION
+#include <linux/joystick.h>
+
+namespace {
+
+using mozilla::dom::GamepadService;
+using mozilla::udev_lib;
+using mozilla::udev_device;
+using mozilla::udev_list_entry;
+using mozilla::udev_enumerate;
+using mozilla::udev_monitor;
+
+static const float kMaxAxisValue = 32767.0;
+static const char kJoystickPath[] = "/dev/input/js";
+
+//TODO: should find a USB identifier for each device so we can
+// provide something that persists across connect/disconnect cycles.
+typedef struct {
+  int index;
+  guint source_id;
+  int numAxes;
+  int numButtons;
+  char idstring[128];
+  char devpath[PATH_MAX];
+} Gamepad;
+
+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 is_gamepad(struct udev_device* dev);
+  void ReadUdevChange();
+
+  // handler for data from /dev/input/jsN
+  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.
+  nsAutoTArray<Gamepad,4> mGamepads;
+};
+
+// singleton instance
+LinuxGamepadService* gService = nullptr;
+
+void
+LinuxGamepadService::AddDevice(struct udev_device* dev)
+{
+  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;
+    }
+  }
+
+  Gamepad 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);
+  char name[128];
+  if (ioctl(fd, JSIOCGNAME(sizeof(name)), &name) == -1) {
+    strcpy(name, "unknown");
+  }
+  const char* vendor_id =
+    mUdev.udev_device_get_property_value(dev, "ID_VENDOR_ID");
+  const char* model_id =
+    mUdev.udev_device_get_property_value(dev, "ID_MODEL_ID");
+  snprintf(gamepad.idstring, sizeof(gamepad.idstring),
+           "%s-%s-%s",
+           vendor_id ? vendor_id : "unknown",
+           model_id ? model_id : "unknown",
+           name);
+
+  char numAxes = 0, numButtons = 0;
+  ioctl(fd, JSIOCGAXES, &numAxes);
+  gamepad.numAxes = numAxes;
+  ioctl(fd, JSIOCGBUTTONS, &numButtons);
+  gamepad.numButtons = numButtons;
+
+  nsRefPtr<GamepadService> service(GamepadService::GetService());
+  gamepad.index = service->AddGamepad(gamepad.idstring,
+                                      gamepad.numButtons,
+                                      gamepad.numAxes);
+
+  gamepad.source_id =
+    g_io_add_watch(channel,
+                   GIOCondition(G_IO_IN | G_IO_ERR | G_IO_HUP),
+                   OnGamepadData,
+                   GINT_TO_POINTER(gamepad.index));
+  g_io_channel_unref(channel);
+
+  mGamepads.AppendElement(gamepad);
+}
+
+void
+LinuxGamepadService::RemoveDevice(struct udev_device* dev)
+{
+  const char* devpath = mUdev.udev_device_get_devnode(dev);
+  if (!devpath) {
+    return;
+  }
+
+  nsRefPtr<GamepadService> service(GamepadService::GetService());
+  for (unsigned int i = 0; i < mGamepads.Length(); i++) {
+    if (strcmp(mGamepads[i].devpath, devpath) == 0) {
+      g_source_remove(mGamepads[i].source_id);
+      service->RemoveGamepad(mGamepads[i].index);
+      mGamepads.RemoveElementAt(i);
+      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 (is_gamepad(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::is_gamepad(struct udev_device* dev)
+{
+  if (!mUdev.udev_device_get_property_value(dev, "ID_INPUT_JOYSTICK"))
+    return false;
+
+  const char* devpath = mUdev.udev_device_get_devnode(dev);
+  if (!devpath) {
+    return false;
+  }
+  if (strncmp(kJoystickPath, devpath, sizeof(kJoystickPath) - 1) != 0) {
+    return false;
+  }
+
+  return true;
+}
+
+void
+LinuxGamepadService::ReadUdevChange()
+{
+  struct udev_device* dev =
+    mUdev.udev_monitor_receive_device(mMonitor);
+  const char* action = mUdev.udev_device_get_action(dev);
+  if (is_gamepad(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)
+{
+  int index = GPOINTER_TO_INT(data);
+  //TODO: remove gamepad?
+  if (condition & G_IO_ERR || condition & G_IO_HUP)
+    return FALSE;
+
+  while (true) {
+    struct js_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;
+    }
+
+    //TODO: store device state?
+    if (event.type & JS_EVENT_INIT) {
+      continue;
+    }
+
+    nsRefPtr<GamepadService> service(GamepadService::GetService());
+    switch (event.type) {
+    case JS_EVENT_BUTTON:
+      service->NewButtonEvent(index, event.number, !!event.value);
+      break;
+    case JS_EVENT_AXIS:
+      service->NewAxisMoveEvent(index, event.number,
+                                ((float)event.value) / kMaxAxisValue);
+      break;
+    }
+  }
+
+  return TRUE;
+}
+
+// static
+gboolean
+LinuxGamepadService::OnUdevMonitor(GIOChannel* source,
+                                   GIOCondition condition,
+                                   gpointer data)
+{
+  if (condition & G_IO_ERR || condition & G_IO_HUP)
+    return FALSE;
+
+  gService->ReadUdevChange();
+  return TRUE;
+}
+
+} // namespace
+
+namespace mozilla {
+namespace hal_impl {
+
+void StartMonitoringGamepadStatus()
+{
+  if (!gService) {
+    gService = new LinuxGamepadService();
+    gService->Startup();
+  }
+}
+
+void StopMonitoringGamepadStatus()
+{
+  if (gService) {
+    gService->Shutdown();
+    delete gService;
+    gService = nullptr;
+  }
+}
+
+} // namespace hal_impl
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/hal/linux/udev.h
@@ -0,0 +1,117 @@
+/* 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/. */
+
+/*
+ * This file defines a wrapper around libudev so we can avoid
+ * linking directly to it and use dlopen instead.
+ */
+
+#ifndef HAL_LINUX_UDEV_H_
+#define HAL_LINUX_UDEV_H_
+
+#include <dlfcn.h>
+
+namespace mozilla {
+
+struct udev;
+struct udev_device;
+struct udev_enumerate;
+struct udev_list_entry;
+struct udev_monitor;
+
+class udev_lib {
+ public:
+  udev_lib() : lib(dlopen("libudev.so.0", RTLD_LAZY | RTLD_GLOBAL)),
+               udev(NULL) {
+    if (lib && LoadSymbols())
+      udev = udev_new();
+  }
+
+  ~udev_lib() {
+    if (udev) {
+      udev_unref(udev);
+    }
+
+    if (lib) {
+      dlclose(lib);
+    }
+  }
+
+  operator bool() {
+    return udev;
+  }
+
+ private:
+  bool LoadSymbols() {
+#define DLSYM(s) \
+  do { \
+    s = (typeof(s))dlsym(lib, #s); \
+    if (!s) return false; \
+  } while (0)
+
+    DLSYM(udev_new);
+    DLSYM(udev_unref);
+    DLSYM(udev_device_unref);
+    DLSYM(udev_device_new_from_syspath);
+    DLSYM(udev_device_get_devnode);
+    DLSYM(udev_device_get_property_value);
+    DLSYM(udev_device_get_action);
+    DLSYM(udev_enumerate_new);
+    DLSYM(udev_enumerate_unref);
+    DLSYM(udev_enumerate_add_match_subsystem);
+    DLSYM(udev_enumerate_scan_devices);
+    DLSYM(udev_enumerate_get_list_entry);
+    DLSYM(udev_list_entry_get_next);
+    DLSYM(udev_list_entry_get_name);
+    DLSYM(udev_monitor_new_from_netlink);
+    DLSYM(udev_monitor_filter_add_match_subsystem_devtype);
+    DLSYM(udev_monitor_enable_receiving);
+    DLSYM(udev_monitor_get_fd);
+    DLSYM(udev_monitor_receive_device);
+    DLSYM(udev_monitor_unref);
+#undef DLSYM
+    return true;
+  }
+
+  void* lib;
+
+ public:
+  struct udev* udev;
+
+  // Function pointers returned from dlsym.
+  struct udev* (*udev_new)(void);
+  void (*udev_unref)(struct udev*);
+
+  void (*udev_device_unref)(struct udev_device*);
+  struct udev_device* (*udev_device_new_from_syspath)(struct udev*,
+                                                      const char*);
+  const char* (*udev_device_get_devnode)(struct udev_device*);
+  const char* (*udev_device_get_property_value)(struct udev_device*,
+                                                const char*);
+  const char* (*udev_device_get_action)(struct udev_device*);
+
+  struct udev_enumerate* (*udev_enumerate_new)(struct udev*);
+  void (*udev_enumerate_unref)(struct udev_enumerate*);
+  int (*udev_enumerate_add_match_subsystem)(struct udev_enumerate*,
+                                            const char*);
+  int (*udev_enumerate_scan_devices)(struct udev_enumerate*);
+  struct udev_list_entry* (*udev_enumerate_get_list_entry)
+    (struct udev_enumerate*);
+
+  struct udev_list_entry* (*udev_list_entry_get_next)(struct udev_list_entry *);
+  const char* (*udev_list_entry_get_name)(struct udev_list_entry*);
+
+  struct udev_monitor* (*udev_monitor_new_from_netlink)(struct udev*,
+                                                        const char*);
+  int (*udev_monitor_filter_add_match_subsystem_devtype)
+    (struct udev_monitor*, const char*, const char*);
+  int (*udev_monitor_enable_receiving)(struct udev_monitor*);
+  int (*udev_monitor_get_fd)(struct udev_monitor*);
+  struct udev_device* (*udev_monitor_receive_device)(struct udev_monitor*);
+  void (*udev_monitor_unref)(struct udev_monitor*);
+};
+
+} // namespace mozilla
+
+#endif // HAL_LINUX_UDEV_H_