A controller my Mac couldn’t see
This started as a fun little side project. I wanted to play some N64 games on my Mac with OpenEmu, so I bought a PowerA wired Xbox Series X controller. The box said “Windows only.” I figured that just meant nobody had tested it on a Mac.
It turned out to mean more than that. I plugged it in, and nothing happened. Not in OpenEmu, not anywhere.
Why macOS ignores it
macOS ships with a driver for Xbox controllers, but it only matches Microsoft’s own vendor ID and four product IDs. Controllers from PowerA, PDP, Hori and other brands speak the exact same protocol (Microsoft calls it GIP), but they use their own vendor IDs. So macOS just ignores them. They never show up as input devices, and OpenEmu can’t see them.
The fix OpenEmu’s wiki suggests, the 360Controller driver, hasn’t worked since Big Sur. There’s an open request for this support, and lots of people with the same controller and no way to use it.
What I built
Rather than wait for Apple, I built a new device handler for OpenEmu, modelled on the ones it already has for Switch Pro and PS4 controllers:
- It finds controllers by what they are, not who made them. It matches on the USB class codes every GIP controller reports, so it doesn’t need a list of brands.
- It talks to the controller directly over USB. OpenEmu isn’t sandboxed, so it can open the controller’s USB interface from the app itself. No driver to install, no system extension to approve. It sends the same startup sequence Linux’s
xpaddriver uses. - It plugs into what’s already there. Input comes through as the same events OpenEmu already understands, using its existing Xbox One controller profile. Every system’s Xbox preset just works, with fully analog sticks and triggers.
- It survives real use. Hot-plugging works, including yanking the cable mid-game.
- It stays out of Apple’s way. Official Microsoft controllers are left to Apple’s driver. The handler reads the list of supported devices from that driver, so the two never fight over the same controller.
The trickiest part was sharing. Only one process can hold a USB interface at a time, but OpenEmu reads input in two places: the game process while you play, and the main app while you set up your controls. So the handler only holds the controller while its process actually needs input, and the two hand it back and forth. When OpenEmu is idle, it lets go entirely, so Steam and other apps can use the controller too.
Mario Kart 64, analog steering, on a controller the box said wouldn’t work.
Along the way I also fixed a small bug on OpenEmu’s Controls screen that affects every controller. When you release a stick after binding a direction, it can spring past center, and that overshoot was getting bound to the next key.
Offering it back
OpenEmu is open source, and this felt like something that should live in the project, not in my fork. So instead of opening a pull request out of nowhere, I opened an issue offering a working, tested implementation and asking the maintainers whether they’d accept it, and how they’d like it shaped. There’s a short demo video in the issue.
The work is split into two proposed pull requests: the controller handler in OpenEmu’s SDK, and the Controls screen fix in the app. In the meantime, there’s a beta branch that builds against the SDK version OpenEmu currently uses, if you have one of these controllers and don’t want to wait.
What surprised me
I built this with Claude Code, and the whole thing took a couple of hours. That’s not a timeframe I’d normally expect for working out a USB protocol on a platform I don’t write drivers for. I’d have filed this under “someday” a year ago.
What made it work wasn’t speed for its own sake. It was being able to stay at the level I’m good at: what should happen when you unplug mid-game, who should own the controller when, what a maintainer would want to see before saying yes. The low-level details came along with it.
And now I get to play Mario Kart 64.