Waitman Gobble

2020-11-01 9:59 pm

Getting the Firefox gamepad api to work on FreeBSD

Work In Process

The first issue is that the Firefox build system uses os_arch type to bring in the linux gamepad interface.

Since os_arch type doesn't match "Linux", on FreeBSD, the build defaults to using a generic gamepad interface which has the stubs but no code.

If you check about:config in Firefox and search for "gamepad" you will see it is enabled but it basically calls empty functions.

However, The Linux interface in Firefox uses libudev, which can be installed on FreeBSD, and it works with USB gamepads / Joysticks.

To get around this I changed the default to Linux. Note that Firefox doesn't actually link to libudev, it uses dlopen to dynamically load the library at runtime. If it can't load it, it fails silently. IE, you can build Firefox and run it even if you don't have libudev.

So little worry - it should build anyway, and run, if you don't have the libudev-devd port installed.

On FreeBSD, to get a USB game pad to work you have to do a little bit of work. Install libudev-devd, and webcamd. This requires linux support (it should bring it in as a dependency). You have to add the webcamd load in rc.conf per the instructions. The other thing you need to do it either create usb dev rules so your user has access, or (the easiest way) is to make your regular user a member of the webcamd group. This is because when you insert a USB gamepad/joystick it is taken by webcamd user with 660 permissions. So your regular user can't read or write (writing is probably used for vibration, etc. I haven't tested that).

So, there's another problem. The Firefox Linux gamepad implementation enumerates through all the input devices to see which ones are a "joystick".

It uses udevenumeratescan_devices which essentially boils down to a recursive scandir function. But libudev uses an RB tree (Red/Black) to manage the array.

I tested the libudev scandir function and it returns all the devices, including "js0". But using the library function to enumerate the device list the js0 device is not in the list. It should be the last item in the list, and it's the last item in scandir function. But RB_NEXT doesn't return it. I'm trying to track down the bug, it seems like it's not returning the last element. But that's not yet verified. It's not return js0 in the list of devices, so the LinuxGamePad never "sees" js0 device.

Here's the test code I made of the scandir function used by libudev, which indeed returns js0.

I added a print function in scandir_sub

...

static int
scandir_sub(char *path, int off, int rem, struct scan_ctx *ctx)
{
        DIR *dir;
        struct dirent *ent;

        dir = opendir(path);
        if (dir == NULL)
                return (errno == ENOENT ? 0 : -1);

        while ((ent = readdir(dir)) != NULL) {
                if (strcmp(ent->d_name, ".") == 0 ||
                    strcmp(ent->d_name, "..") == 0)
                        continue;

                int len = strlen(ent->d_name);
                if (len > rem)
                        continue;

                printf("%s\n",ent->d_name);
        }
        closedir(dir);
        return (0);
}

int
scandir_recursive(char *path, size_t len, struct scan_ctx *ctx)
{
        size_t root_len = strlen(path);

        return (scandir_sub(path, root_len, len - root_len - 1, ctx));
}

int main() {

        struct scan_ctx ctx;
        char path[255] =  "/dev/input/";
        int ret;
        ctx = (struct scan_ctx) {
                .recursive = true,
        };
        ret = scandir_recursive(path, sizeof(path), &ctx);
        return 0;
        
}
...

% clang -g -o testscan -DHAVE_STRCHRNUL -DHAVE_DEVINFO_H testscan.cc

% ./testscan
event0
event1
event2
event3
event4
event5
event6
event7
event8
event9
event10
js0

And here's using libudev to enumerate the device list. When you use enumerate it calls RB_NEXT to get the next element (defined in sys/tree.h)

#include <string>
#include <iostream>
#include <libudev.h>


int main() {
    struct udev* udev = udev_new();
    struct udev_enumerate* enumerate = udev_enumerate_new(udev);
    udev_enumerate_add_match_subsystem(enumerate, "input");
    udev_enumerate_scan_devices(enumerate);
    struct udev_list_entry* devices = udev_enumerate_get_list_entry(enumerate);
    struct udev_list_entry* dev_list_entry;
    udev_list_entry_foreach(dev_list_entry, devices) {
        const char* path = udev_list_entry_get_name(dev_list_entry);
        std::cout << path << std::endl;
    }
    udev_enumerate_unref(enumerate);
    return 0;
}


% clang++ -o enum enum.cc -I/usr/local/include -L/usr/local/lib -ludev
% ./enum 
/dev/input/event0
/dev/input/event1
/dev/input/event10
/dev/input/event2
/dev/input/event3
/dev/input/event4
/dev/input/event5
/dev/input/event6
/dev/input/event7
/dev/input/event8
/dev/input/event9

Note: /dev/input/js0 is missing!

This is causing a problem because LinuxGamePad.cpp checks to see if the device is a jsXXX for some reason.

In my opinion it's not necessary, but I'd like to know why it matters. Reading /dev/input/eventXXX gives you the exact same data as /dev/input/js0. You can test it with cat /dev/input/js0 or with a little python script:

import struct

infile_path = "/dev/input/js0"
EVENT_SIZE = struct.calcsize("LhBB")
file = open(infile_path, "rb")
event = file.read(EVENT_SIZE)
while event:
    print(struct.unpack("LhBB", event))
    (tv_msec,  value, type, number) = struct.unpack("LhBB", event)
    event = file.read(EVENT_SIZE)

change infile_path to "/dev/input/event10" or whatever your joystick shows up, and you get the same data.

In LinuxGamePad.cpp they check that the device is IDINPUTJOYSTICK, and can get the devnode. Then they check that it matches kJoystickPath.

static const char kJoystickPath[] = "/dev/input/js";

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;
}

Because udevenumeratescandevices does not return js0, the isgamepad returns false in Firefox dom/gamepad/linux/LinuxGamePad.cpp

Fixes that are potentially "hacky"

  1. in moz/gamepad/moz.build I set the default gamepad handler code to Linux / libudev. It is loading libudev dynamically and connecting to the joystick.
  2. in moz/gamepad/linux/LinuxGamePad.cpp I changed /dev/input/js to /dev/input/event. ie, static const char kJoystickPath[] = "/dev/input/event";

I added a logger function to moz/gamepad/linux/LinuxGamePad.cpp. It's logging found devices, then OK PASSED on connect.

% setenv MOZ_LOG_FILE log
% MOZ_LOG "gamepad:5,sync"
% firefox 

% tail log.moz_log
[Parent 77626: IPDL Background]: I/gamepad /dev/input/event11
[Parent 77626: IPDL Background]: I/gamepad OK PASSED: /dev/input/event11
[Parent 77626: Main Thread]: I/gamepad /dev/input/event12
[Parent 77626: Main Thread]: I/gamepad OK PASSED: /dev/input/event12

However, this is not 100% working yet, but making progress. I added some logging to Firefox gamepad code, which shows it's connecting to the joystick.

It's also successfully registering for notifications from udev. See the log file, the second entry for event12. It was event11 and i unplugged, then replugged the joystick so it was assigned event12.

But the test HTML file I'm using isn't working. I copied the test HTML file from the Mozilla site, so maybe it's out of date or not correct to begin with.

Copyright 2020 Waitman Gobble. Contact