Several months ago, Nintendo release their GameCube controller Adapter for WiiU, which allowed the use of GameCube controllers on the WiiU when playing Smash Brothers. However, the immediately obvious thing about it was the other end of the adapter, which featured two USB plugs. How difficult could it possibly be to get it to work on a computer?

It turns out, not very difficult. After just three days, ToadKing had figured out how to get it to work on Linux. Windows followed just a few days later. I expected OSX to be not far behind. Days, and then weeks passed to no avail. I finally decided - despite knowing essentially none of the skills required - to try and figure it all out myself.

An aside

Toadking made a reference to spending $60 to find out what one byte was. What does he mean by that? Well, even though the controller has USB plugs, it doesn't 'turn on' when you plug it in. You have to send it an initialisation packet before it will talk to whatever you've plugged it in to. This is the same for some other not-quite-standard devices too. For example, to turn on an Xbox One controller connected via USB, you have to send it four packets. They look like this, courtesy of the Tattiebogle Xbox controller driver:

{ 0x02, 0x20, 0x01, 0x1C, 0x7E, 0xED, 0x8B, 0x11, 0x0F, 0xA8, 0x00, 0x00, 0x5E, 0x04, 0xD1, 0x02, 0x01, 0x00, 0x01, 0x00, 0x17, 0x01, 0x02, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00 };
{ 0x05, 0x20, 0x00, 0x09, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x55, 0x53 };
{ 0x05, 0x20, 0x01, 0x01, 0x00 };
{ 0x0A, 0x20, 0x02, 0x03, 0x00, 0x01, 0x14 };

What does the equivalent packet look like for the Nintendo adapter?

{ 0x13 }

This is the byte that ToadKing was referring to. Thanks for making our lives easy, Nintendo!

Drivers on OSX

So, step one. How do drivers work on OSX? For inspiration, I searched around for projects that had already achieved similar things on OSX. There was the aforementioned Tattiebogle adapter, but that had a whole host of extra stuff around it that I didn't really have a desire to work through, especially given that I am unfamiliar with Objective C. I found another implementation which was a lot cleaner, and seemed to implement only the bare-bones needed for a kext.

A nice benefit of using this driver as my reference was that it used asynchronous reads from the device, rather than polling the device every few milliseconds. An implementation using this latter approach would introduce unnecessary lag during use, which is a particular bugbear of the Smash community - and justifiably so. Because so many people play Melee at a frame-perfect level, Melee tournaments go so far as using CRT TVs rather than the much easier to transport LCD TVs, so that they don't introduce lag during the analog-to-digital conversion LCD TVs do.

All I needed to do was implement a few functions that could be called by the operating system when the appropriate device was plugged in. Seems simple enough! The source made it clear I was going to have to write a HID report descriptor for the adapter though - and I had no clue what that was.

HID

HID stands for 'Human Interface Device' and is a universal standard for how such USB devices interact with a computer. The HID descriptor describes to the operating system the dials, switches, buttons and other such things that are on the device, and the order that it reports those features in.

Writing a HID descriptor is a total pain. The utilities you get told to use only work on Windows, and so I was left writing a descriptor by hand and using tools like this to verify I wasn't totally wrong. The biggest question in my mind was how to inform the OS that the device we had attached should actually represent multiple controllers.

Fortunately, the HID protocol has taken this into account. What you do is you group each device into a group known as a 'collection', and you can place multiples of these collections in a single HID report. The only caveat is the first byte in each description has to be a 'report id' so that each can be uniquely identified. This is explained in more detail in an excellent blogpost by Frank Zhao, which proved an invaluable resource as I attempted to teach myself. I ended up with a descriptor that looked like

unsigned char HIDReport[] = {
    0x05, 0x01,                    // USAGE_PAGE (Generic Desktop)
    0x09, 0x05,                    // USAGE (Game Pad)
    0xa1, 0x01,                    // COLLECTION (Application)
    0xa1, 0x00,                    //   COLLECTION (Physical)
    0x85, 0x01,                    //     REPORT_ID (1)
    0x05, 0x01,                    //     USAGE_PAGE (Generic Desktop)
    0x09, 0x30,                    //     USAGE (X)
    0x09, 0x31,                    //     USAGE (Y)
    0x09, 0x32,                    //     USAGE (Z)
    0x09, 0x33,                    //     USAGE (Rx)
    0x09, 0x34,                    //     USAGE (Ry)
    0x09, 0x35,                    //     USAGE (Rz)
    0x35, 0x00,                    //     PHYSICAL_MINIMUM (0)
    0x46, 0xff, 0x00,              //     PHYSICAL_MAXIMUM (255)
    0x15, 0x00,                    //     LOGICAL_MINIMUM (0)
    0x26, 0xff, 0x00,              //     LOGICAL_MAXIMUM (255)
    0x75, 0x08,                    //     REPORT_SIZE (8)
    0x95, 0x06,                    //     REPORT_COUNT (6)
    0x81, 0x02,                    //     INPUT (Data,Var,Abs)
    0x05, 0x09,                    //     USAGE_PAGE (Button)
    0x19, 0x01,                    //     USAGE_MINIMUM (Button 1)
    0x29, 0x0c,                    //     USAGE_MAXIMUM (Button 12)
    0x15, 0x00,                    //     LOGICAL_MINIMUM (0)
    0x25, 0x01,                    //     LOGICAL_MAXIMUM (1)
    0x95, 0x0c,                    //     REPORT_COUNT (12)
    0x75, 0x01,                    //     REPORT_SIZE (1)
    0x81, 0x02,                    //     INPUT (Data,Var,Abs)
    0x75, 0x01,                    //     REPORT_SIZE (1)
    0x95, 0x04,                    //     REPORT_COUNT (4)
    0x81, 0x03,                    //     INPUT (Cnst,Var,Abs)
    0xc0,                          //   END_COLLECTION
    0xc0,                          // END_COLLECTION

    0x05, 0x01,                    // USAGE_PAGE (Generic Desktop)
    0x09, 0x05,                    // USAGE (Game Pad)
    0xa1, 0x01,                    // COLLECTION (Application)
    0xa1, 0x00,                    //   COLLECTION (Physical)
    0x85, 0x02,                    //     REPORT_ID (2)
    0x05, 0x01,                    //     USAGE_PAGE (Generic Desktop)
    0x09, 0x30,                    //     USAGE (X)
    0x09, 0x31,                    //     USAGE (Y)
    0x09, 0x32,                    //     USAGE (Z)
    0x09, 0x33,                    //     USAGE (Rx)
    0x09, 0x34,                    //     USAGE (Ry)
    0x09, 0x35,                    //     USAGE (Rz)
    0x35, 0x00,                    //     PHYSICAL_MINIMUM (0)
    0x46, 0xff, 0x00,              //     PHYSICAL_MAXIMUM (255)
    0x15, 0x00,                    //     LOGICAL_MINIMUM (0)
    0x26, 0xff, 0x00,              //     LOGICAL_MAXIMUM (255)
    0x75, 0x08,                    //     REPORT_SIZE (8)
    0x95, 0x06,                    //     REPORT_COUNT (6)
    0x81, 0x02,                    //     INPUT (Data,Var,Abs)
    0x05, 0x09,                    //     USAGE_PAGE (Button)
    0x19, 0x01,                    //     USAGE_MINIMUM (Button 1)
    0x29, 0x0c,                    //     USAGE_MAXIMUM (Button 12)
    0x15, 0x00,                    //     LOGICAL_MINIMUM (0)
    0x25, 0x01,                    //     LOGICAL_MAXIMUM (1)
    0x95, 0x0c,                    //     REPORT_COUNT (12)
    0x75, 0x01,                    //     REPORT_SIZE (1)
    0x81, 0x02,                    //     INPUT (Data,Var,Abs)
    0x75, 0x01,                    //     REPORT_SIZE (1)
    0x95, 0x04,                    //     REPORT_COUNT (4)
    0x81, 0x03,                    //     INPUT (Cnst,Var,Abs)
    0xc0,                          //   END_COLLECTION
    0xc0,                          // END_COLLECTION

    0x05, 0x01,                    // USAGE_PAGE (Generic Desktop)
    0x09, 0x05,                    // USAGE (Game Pad)
    0xa1, 0x01,                    // COLLECTION (Application)
    0xa1, 0x00,                    //   COLLECTION (Physical)
    0x85, 0x03,                    //     REPORT_ID (3)
    0x05, 0x01,                    //     USAGE_PAGE (Generic Desktop)
    0x09, 0x30,                    //     USAGE (X)
    0x09, 0x31,                    //     USAGE (Y)
    0x09, 0x32,                    //     USAGE (Z)
    0x09, 0x33,                    //     USAGE (Rx)
    0x09, 0x34,                    //     USAGE (Ry)
    0x09, 0x35,                    //     USAGE (Rz)
    0x35, 0x00,                    //     PHYSICAL_MINIMUM (0)
    0x46, 0xff, 0x00,              //     PHYSICAL_MAXIMUM (255)
    0x15, 0x00,                    //     LOGICAL_MINIMUM (0)
    0x26, 0xff, 0x00,              //     LOGICAL_MAXIMUM (255)
    0x75, 0x08,                    //     REPORT_SIZE (8)
    0x95, 0x06,                    //     REPORT_COUNT (6)
    0x81, 0x02,                    //     INPUT (Data,Var,Abs)
    0x05, 0x09,                    //     USAGE_PAGE (Button)
    0x19, 0x01,                    //     USAGE_MINIMUM (Button 1)
    0x29, 0x0c,                    //     USAGE_MAXIMUM (Button 12)
    0x15, 0x00,                    //     LOGICAL_MINIMUM (0)
    0x25, 0x01,                    //     LOGICAL_MAXIMUM (1)
    0x95, 0x0c,                    //     REPORT_COUNT (12)
    0x75, 0x01,                    //     REPORT_SIZE (1)
    0x81, 0x02,                    //     INPUT (Data,Var,Abs)
    0x75, 0x01,                    //     REPORT_SIZE (1)
    0x95, 0x04,                    //     REPORT_COUNT (4)
    0x81, 0x03,                    //     INPUT (Cnst,Var,Abs)
    0xc0,                          //     END_COLLECTION
    0xc0,                          //   END_COLLECTION

    0x05, 0x01,                    // USAGE_PAGE (Generic Desktop)
    0x09, 0x05,                    //   USAGE (Game Pad)
    0xa1, 0x01,                    //   COLLECTION (Application)
    0xa1, 0x00,                    //     COLLECTION (Physical)
    0x85, 0x04,                    //     REPORT_ID (4)
    0x05, 0x01,                    //       USAGE_PAGE (Generic Desktop)
    0x09, 0x30,                    //       USAGE (X)
    0x09, 0x31,                    //       USAGE (Y)
    0x09, 0x32,                    //       USAGE (Z)
    0x09, 0x33,                    //       USAGE (Rx)
    0x09, 0x34,                    //       USAGE (Ry)
    0x09, 0x35,                    //       USAGE (Rz)
    0x35, 0x00,                    //       PHYSICAL_MINIMUM (0)
    0x46, 0xff, 0x00,              //       PHYSICAL_MAXIMUM (255)
    0x15, 0x00,                    //     LOGICAL_MINIMUM (0)
    0x26, 0xff, 0x00,              //     LOGICAL_MAXIMUM (255)
    0x75, 0x08,                    //       REPORT_SIZE (8)
    0x95, 0x06,                    //       REPORT_COUNT (6)
    0x81, 0x02,                    //       INPUT (Data,Var,Abs)
    0x05, 0x09,                    //     USAGE_PAGE (Button)
    0x19, 0x01,                    //     USAGE_MINIMUM (Button 1)
    0x29, 0x0c,                    //     USAGE_MAXIMUM (Button 12)
    0x15, 0x00,                    //     LOGICAL_MINIMUM (0)
    0x25, 0x01,                    //     LOGICAL_MAXIMUM (1)
    0x95, 0x0c,                    //     REPORT_COUNT (12)
    0x75, 0x01,                    //     REPORT_SIZE (1)
    0x81, 0x02,                    //     INPUT (Data,Var,Abs)
    0x75, 0x01,                    //     REPORT_SIZE (1)
    0x95, 0x04,                    //     REPORT_COUNT (4)
    0x81, 0x03,                    //     INPUT (Cnst,Var,Abs)

    0xc0,                          //   END_COLLECTION
    0xc0                           // END_COLLECTION
};

Each controller corresponds to a report ID (which is a byte long), six analog axes (left stick x and y, right stick x and y, and triggers), each with a full byte of resolution, followed by 12 buttons (four for the D-Pad, four face buttons, start, Z, and the trigger 'clicks') that are a bit each, and then four bits of padding so that the whole thing fits neatly into a whole number of bytes.

So now, all we have to do is give the OS this report descriptor, and then make sure the reports we supply are in this format, and everything should work!

Installing a kext even if Apple doesn't want you to

On OSX, a driver is bundled up in something called a kext, which stands for Kernel EXTension. While Apple frowns on users building their own kexts, in this case a kext is required as otherwise the normal HID driver will take control of the adapter. As mentioned above, it's not quite a normal HID device, and so the adapter won't work at all (there's that pesky initialisation packet).

Prior to OSX 10.9, this wasn't much of an issue. Since 10.9, however, Apple requires that all kexts are signed, which requires a developer license. As an alternative, you can disable signing, which is what I did, but this is a slight security risk and is annoying for distribution, having to explain to your users that this is required and what the consequences are. For now, however, I ploughed on accepting this to be a necessary evil.

Debugging

This had seemed surprisingly straightforward so far. Suspiciously so. My first attempt that compiled and installed correctly, however, had a significant bug. The first controller port worked perfectly. The second controller port? Less so. The same was true of the third and fourth ports, too. The program I was using to test this driver was Dolphin, the multiplatform Gamecube and Wii emulator. In Dolphin, when configuring the additional controllers, the button presses were detected, but when I tried to actually use them, it was only the first controller that was detected.

I (begrudgingly) paid for Joystick Show to see what was going on. This reported only one controller - but with 24 axes and 48 buttons. But OpenEmu showed four controllers. Indeed, OpenEmu was the only program I could find that showed four controllers, with most others agreeing with Joystick Show that I had only one controller plugged in.

Now, whenever Nintendo does something right, they always make a lot of money - as long as they are able to meet demand. The Gamecube adapter was a case where they were unable to meet demand, and as a result a third party stepped up to the plate. This Mayflash adapter boldly claimed to work on WiiU and PCs, which got me interested - if it worked on a PC without drivers, it must have (a) correct HID descriptor!

I managed to find a user online who had this adapter, and was disappointed to discover that its HID descriptor was almost identical to the one I had crafted myself. It even showed the same behaviour in Dolphin as my own driver. It is confirmed that it works in Windows - without drivers - so it looks like the fault lies with OSX in how it deals with multi-collection HID descriptors. A post on the Apple USB mailing list went unanswered, so I'm not really sure where I stand on this front.

I had heard rumours than an earlier (2-port) version of the Mayflash GameCube adapter worked correctly. Fed up with getting nowhere, I threw £12 away on Amazon to get one in my grubby little hands. The rumours were true - both ports worked correctly. A quick dump through USB prober proved disappointing, however - it was presenting as a device with multiple interfaces, each of which represented a single controller, and each of which had its own HID descriptor.

So maybe I could adopt that approach here. This is, after all, how wjoy allows the Wiimotes to be used so universally - by intercepting the Bluetooth devices' readouts, and then mapping them to virtual HID (VHID) devices. So I explored that approach - very briefly. Careful reading of the IOUSBInterface documentation didn't show any way for me to do it natively, so I started to go down the road of using VHID from wjoy to do it. Unfortunately, some of the packages used by VHID had other ideas:

#ifdef KERNEL
#error This file is not for kernel use
#endif

i.e. I was unable to use it in a kext. With no way to create extra interfaces, and no way to get the (demonstrated correct) HID descriptor to work, it looks like a kext-only solution is going to be somewhere between 'very difficult' and 'impossible' to pull off. I am happy to be proven wrong; getting a kext-only solution would be the best solution with the lowest lag, so I see myself coming back to this in the future. The implementation, as-is with only port one working correctly, can be found on GitHub. If you are only concerned about playing single player with the lowest amount of lag, I would even recommend that driver rather than the one I describe in the next section, which operates in userspace.

Capitulation

Unsurprisingly, I was not the only person with the will and the inclination to dive into all of this. A user on Reddit came up with a userspace driver, which had the distinction of having all four controller ports working. It did this by creating virtual HID devices using wjoy, as I explored above, but did it in userspace. Unfortunately, it worked by polling the USB device every 5ms and then updating the fake HID devices it created, which introduced a frame's worth of lag some of the time - something that would be totally unacceptable to the hardcore community, but probably fine for most people. Indeed, he got a lot of feedback on his driver, and presumably lots of people are happily using it. Another annoyance with this implementation, however, is by virtue of being a userspace driver, a program has to be run every time you want to use the device. Not ideal, from a user's perspective.

Fortunately, there is a (little-known?) way to get an executable to launch when a device is connected. This is the same way (I assume?) that iPhoto opens whenever you plug in your digital camera, and is known as a LaunchAgent. Writing one is pretty simple - so this solved my biggest gripe with the userspace solution of the user having to launch an executable every time.

I dove into the libusb documentation to figure out how to use async interrupts with libusb. Some significant missteps later, I had changed the userspace driver from polling the device every 5ms to using these asyncronous interrupts, greatly reducing the ceiling of the lag a user will experience. I am not comfortable with Objective C, which the driver was written in, so I've mushed together some C++ code with the Objective C. Hardly an elegant solution, but I feel like we're long past that point!

This driver still uses a kext, but one with no code inside. This is a well established trick to prevent the kernel grabbing the HID device with a generic HID driver, and allowing access to the device from userspace. Unfortunately, this kext still needs to be signed, or kext signing needs to be disabled, which is a security risk. I have registered with the Apple Developer Center so that I can sign this kext, paying £79 for the privilege. Hopefully I'll find some other use for the membership, but I'm not holding my breath.

Wrap up

This implementation of the driver seems to work well, and I've been slowly debugging issues on machines that are not my own with a handful of volunteers. All four controllers are visible and usable, both inside Dolphin and elsewhere (specficially, Steam games, such a Towerfall. Playing local Steam multiplayer with Gamecube controllers on my laptop was my original motivation for investigating all of this). It includes a helpful installation script, as well as an uninstallation script, the first of which was surprisingly fiddly to write correctly.

All in all, this has been an interesting experience, and I've certainly learned a lot about the lower-level workings of OSX and USB devices as I've reached this stage. I am hopeful that by publicising these efforts, someone who actually knows what they're doing will be able to figure out a way to make the kext-only implementation work with all four ports. Such an implementation would clearly be the best possible solution - but until then, I have at least made one that is good enough for my purposes!


Comments

comments powered by Disqus