macOS userspace gamepad driver. Plug in a controller, remap it, use it.
macOS has no kernel driver for gamepads. Windows ships XInput. Linux merged xpad.c into the kernel. On macOS, the last maintained general-purpose solution was Enjoyable, which hasn't had a commit since 2015 and doesn't support modern controllers, Apple Silicon, or current macOS.
This matters most to game studios and engine integrators (Unity, Unreal, custom engines) that need a stable, scriptable gamepad input layer, and to emulator users who want to use the controller they already own.
OpenJoystickDriver is to gamepads what OpenTabletDriver is to drawing tablets: a userspace driver that requires no kernel extension, with an open device registry that contributors can extend.
| Feature | Status |
|---|---|
| Xbox One / Series controllers (GIP protocol) | Working - hardware verified on Gamesir G7 SE |
| GIP authentication (CMD 0x06 sub-protocol) | Working - state machine with dummy auth payloads |
| Virtual HID gamepad (DriverKit extension) | Working - production output path on macOS 13+ |
| Virtual HID gamepad (IOHIDUserDevice user-space) | Working - optional compatibility mode (no reboot) |
| DualShock 4 (USB) | Implemented, untested (no PS4 hardware) |
| Generic USB HID gamepads | Basic fallback (standard HID usage page) |
| Button remapping | Working - JSON profiles per VID/PID |
| Stick → mouse, D-pad → arrow keys | Working |
| Menu bar app (SwiftUI) | Working |
CLI (--headless) |
Working |
| LaunchAgent (auto-start on login) | Working |
| Bluetooth | Not implemented |
| DualSense (PS5) | Not implemented |
| Switch Pro Controller | Not implemented |
- macOS 13 (Ventura) or later
- libusb - required for Xbox/GIP controllers
- Xcode Command Line Tools or a full Xcode installation (for
swift build)
brew install libusbgit clone https://github.com/OpenVara/OpenJoystickDriver.git
cd OpenJoystickDriver
./scripts/ojd signing install-profiles
./scripts/ojd signing configure
./scripts/ojd rebuild devThis builds a signed app bundle and installs it to /Applications/OpenJoystickDriver.app.
The daemon is managed as a LaunchAgent via SMAppService from inside the app/CLI.
One system permission is required:
- Input Monitoring (
System Settings > Privacy > Input Monitoring) - to read controller input
Grant it to the daemon binary (OpenJoystickDriverDaemon), not the GUI app.
Accessibility permission is not needed — the driver injects gamepad input via a virtual HID device (DriverKit extension, and optionally a user-space IOHIDUserDevice), not CGEvents.
Some SDL/IOKit apps ignore virtual devices with Transport="Virtual" (common for DriverKit virtual HID).
If a game/emulator can see your controller but won’t react to inputs, enable:
- Open the OpenJoystickDriver menu bar item →
Mode→Compatibility
This uses a user-space virtual controller (IOHIDUserDevice). It does not require a reboot. When enabled, OpenJoystickDriver routes output to the user-space device (and disables DriverKit output).
Note for development builds: Ad-hoc signed binaries get a new code identity on every
swift build. macOS ties TCC grants to the binary's code identity, so permissions reset after each rebuild. After rebuilding, re-grant both permissions and use--headless restartor the Restart Daemon button in the app. The Permissions view detects this state and shows a prompt automatically.To avoid this, sign with a real Apple Development certificate:
./scripts/ojd signing configure ./scripts/ojd build devFind your identity:
security find-identity -v -p codesigning
Launch OpenJoystickDriver from /usr/local/bin or Spotlight. It runs as a menu bar app.
- The menu bar popover shows:
- Driver status (and which backend is active)
- DriverKit install status + errors
- Mode: Auto / DriverKit / Compatibility
- Self-test and a log shortcut
If the LaunchAgent daemon cannot be managed in your current session (some shells/terminal sessions can’t talk to launchd properly), OpenJoystickDriver automatically falls back to an embedded backend so the driver still works.
All CLI commands use the --headless flag:
# Check permission states (via daemon if running, direct otherwise)
OpenJoystickDriver --headless status
# List connected controllers
OpenJoystickDriver --headless list
# Run the driver interactively (foreground, Ctrl+C to stop)
OpenJoystickDriver --headless run
# Print macOS version, permissions, USB devices, troubleshooting tips
OpenJoystickDriver --headless diagnose
# Daemon lifecycle
OpenJoystickDriver --headless install # Register as LaunchAgent
OpenJoystickDriver --headless start # Start the daemon
OpenJoystickDriver --headless restart # Restart the daemon
OpenJoystickDriver --headless uninstall # Remove LaunchAgent
# Virtual device toggles
OpenJoystickDriver --headless userspace status
OpenJoystickDriver --headless userspace on
OpenJoystickDriver --headless userspace off
# Output routing (DriverKit / user-space / both)
OpenJoystickDriver --headless output status
OpenJoystickDriver --headless output primary
OpenJoystickDriver --headless output secondary
OpenJoystickDriver --headless output both
# Virtual device input self-test (press buttons while it runs)
OpenJoystickDriver --headless selftest 5PCSX2 2.6.x uses SDL3 for controller input on macOS. If you can see the controller in a browser gamepad tester but PCSX2 won’t react to inputs, do this:
- Open PCSX2 → Settings → Controllers → Global Settings
- Ensure Enable SDL Input Source is checked
- Pick the device under Detected Devices (you should see
SDL-0 OpenJoystickDriver Virtual Gamepad) - Bind buttons/axes under Controller Port 1
If the controller only appears (or only works) when you enable Enable MFI Driver, that indicates PCSX2 is reading it through GameController.framework instead of SDL.
To debug whether SDL is receiving events at all, build and run the SDL3 probe:
./scripts/ojd diagnose sdl3 --seconds 10If the probe prints no axis/button events while you press inputs, SDL isn’t receiving input from the virtual device (PCSX2 SDL input will also fail).
Enable Compatibility mode in the menu bar app (Mode → Compatibility) and try again.
Some PCSX2 builds ship as Intel-only and run under Rosetta. SDL input behavior can differ between:
- native arm64 SDL3
- Intel (x86_64) SDL3 under Rosetta
This repo includes a script that runs both probes back-to-back:
./scripts/ojd diagnose pcsx2-latencyIf the native probe reports instant events but the PCSX2/Rosetta probe reports 0 devices (or very delayed events), the bottleneck is on the PCSX2/Rosetta SDL input path, not in OpenJoystickDriver.
-1ffffd15 is kIOReturnAborted (0xe00002eb). On macOS this commonly happens during
system-extension upgrades/replacements: IOKit aborts in-flight operations and your process
ends up holding a stale handle.
Fix (fast):
/Applications/OpenJoystickDriver.app/Contents/MacOS/OpenJoystickDriver --headless restartIf ./scripts/ojd diagnose dext reports stale sysext copies, a reboot cleans them up.
Two input paths, one per USB device class:
USB Class 0xFF (Vendor-Specific) → LibUSB / SwiftUSB → GIPParser (+ GIPAuthHandler)
USB Class 0x03 (HID) → IOKit / IOHIDManager → DS4Parser or GenericHIDParser
Both paths feed into a DevicePipeline actor - one per connected controller. Pipelines are isolated: an error in one controller's pipeline doesn't affect the others.
GIP controllers (Xbox One / Series) require a CMD 0x06 authentication handshake before they send input. GIPAuthHandler implements the state machine with dummy auth payloads (lenient enforcement allows cryptographically empty responses).
DextOutputDispatcher → DriverKit extension (IOUserHIDDevice + user-client IPC)
The DriverKit extension (OpenJoystickVirtualHIDDevice) registers as a system HID device and accepts 13-byte input reports from the daemon via user-client IPC. If the extension is not yet loaded, the dispatcher auto-retries on each input event until the connection succeeds.
The daemon exposes an XPC service (com.openjoystickdriver.xpc). The GUI and CLI connect to it for device listing, status queries, and profile changes. The daemon never depends on the GUI being open.
Profiles are stored at ~/Library/Application Support/OpenJoystickDriver/profiles/{VID}-{PID}.json.
Device support lives in two places:
Sources/OpenJoystickDriverKit/Resources/devices.json- VID/PID catalog and parser assignmentResources/Schemas/Devices/- per-device field layouts (for documentation and validation)
To add a new controller:
- Add an entry to
devices.jsonwith the VID, PID, and parser type ("gip","ds4", or"generic_hid") - If it uses a non-standard protocol, implement a new
InputParserconformance inSources/OpenJoystickDriverKit/Protocol/ - Add a device schema file to
Resources/Schemas/Devices/(optional but helpful for reviewers) - Add tests in
Tests/OpenJoystickDriverKitTests/
VID and PID values in JSON must be decimal integers, not hex strings.
# One-time signing setup (macOS 26+)
./scripts/ojd signing install-profiles
./scripts/ojd signing configure
# Full rebuild + deploy (reinstalls sysext; may require reboot)
./scripts/ojd rebuild dev
# Fast rebuild (does NOT reinstall/upgrade sysext; safe during streams / no reboot)
./scripts/ojd rebuild-fast dev
# Build universal release binaries, codesign, and notarize
OJD_ENV=release ./scripts/ojd rebuild release
OJD_ENV=release ./scripts/ojd notarize submit
# Lint (requires swiftlint)
./scripts/ojd lintmacOS 26+ enforces provisioning for certain entitlements (system extension / DriverKit). This repo expects:
- Provisioning profiles installed at
~/Library/MobileDevice/Provisioning Profiles/ - Two Keychain identities:
Apple Development: …(for dev builds + dext build step)Developer ID Application: …(for release signing + notarization)
The Team ID in the identity name (the (...) suffix) must match the provisioning profile’s Team ID. If you have multiple Apple Developer teams, it’s easy to create an Apple Development cert for the “wrong” team.
Sanity-check installed profiles (safe output; no identifiers printed):
./scripts/ojd signing audit "$HOME/Library/MobileDevice/Provisioning Profiles"/*.provisionprofileInstall profiles from ~/Documents/Profiles/ (or ~/Documents/profiles/):
./scripts/ojd signing install-profilesGenerate scripts/.env.dev and scripts/.env.release automatically (no heredocs / no copy-paste):
./scripts/ojd signing configureIf something fails, run the signing doctor first (prints safe info only):
./scripts/ojd signing doctorIf Keychain Access shows “not trusted” but security find-identity reports the identity as valid, you can usually ignore the UI.
If security find-identity reports 0 valid identities, you’re missing Apple’s intermediate CA certificates (WWDR / Developer ID). Get them from Apple’s Certificate Authority page and import them in Keychain Access (System keychain is fine):
Apple PKI index:
https://www.apple.com/certificateauthority/
Then re-check:
security find-identity -v -p codesigningIf security find-identity prints 0 valid identities found but Keychain Access shows your certs with private keys, your keychain file permissions are wrong (this can happen after migrations / restores).
Fix:
chmod 700 "$HOME/Library/Keychains"
chmod 600 "$HOME/Library/Keychains/login.keychain-db"Then log out/in (or reboot), and re-run security find-identity.
This repo uses xcrun notarytool with an Apple ID + an app-specific password.
Create an app-specific password at:
https://account.apple.com/ → Sign-In and Security → App-Specific Passwords
Put the values into scripts/.env.release:
NOTARIZE_APPLE_ID="you@example.com"NOTARIZE_PASSWORD="xxxx-xxxx-xxxx-xxxx"
Then run:
OJD_ENV=release ./scripts/ojd rebuild release
OJD_ENV=release ./scripts/ojd notarize submitSwift 6.2 strict concurrency is enforced. All warnings are errors. SwiftLint zero-suppression policy.
MIT - see LICENSE.