feat(macos/input): host-side virtual gamepad via IOHIDUserDevice#5203
feat(macos/input): host-side virtual gamepad via IOHIDUserDevice#5203Nottlespike wants to merge 2 commits into
Conversation
…evice
Upstream Sunshine on macOS has no host-side gamepad support: every
alloc_gamepad/gamepad_update/free_gamepad in src/platform/macos/input.cpp
is a stub that logs "Gamepad not yet implemented for MacOS" and returns
failure, and get_capabilities() returns 0. Moonlight clients send
gamepad events over the wire that the host silently drops.
This ports Lumen's working approach (MIT-licensed; trollzem/Lumen):
publish a virtual gamepad as an IOHIDUserDevice with a complete HID
report descriptor (16 buttons, 4-bit hat switch, 2x 8-bit triggers,
4x 16-bit signed stick axes). macOS's HID matching publishes the
device to userspace; games, emulators, GameController.framework, SDL,
and Steam all see it as a real USB Xbox-style controller (VID 0x1209,
PID 0x5853 — pid.codes open-source range, intentionally not in SDL's
known-controller database so it routes through generic mapping).
The IOHIDUserDevice creation path requires AMFI to be bypassed at
boot: `sudo nvram boot-args="amfi_get_out_of_my_way=1"` and reboot.
Without that, IOHIDUserDeviceCreateWithProperties fails the entitlement
check and the probe at input() construction time logs a clear
instruction. SIP can stay on. The DriverKit DEXT path (no AMFI flag,
requires Apple DriverKit entitlement application + signed
system-extension distribution) is the longer-term shipping story but
is multi-week work; this gets us to a working gamepad host today.
Changes:
- src/platform/macos/hid_gamepad.{h,m} (new): MRC Obj-C wrapper around
IOHIDUserDevice publishing the Xbox-style gamepad HID descriptor.
Maps Sunshine's 32-bit buttonFlags to the 16-bit HID button field +
4-bit hat switch + axes/triggers. Bounded 2s teardown via
IOHIDUserDeviceSetCancelHandler. QOS_CLASS_USER_INTERACTIVE serial
queue for report dispatch.
- src/platform/macos/input.{cpp -> mm}: renamed to Obj-C++ so the
gamepad path holds HIDGamepad strong references directly. Mouse/
keyboard injection paths unchanged. macos_input_t gains a 16-slot
HIDGamepad array + hid_gamepad_available flag (probed once at init).
alloc_gamepad finds the lowest free slot, createDevice's a virtual
gamepad, returns the slot index. free_gamepad disconnects +
releases. gamepad_update forwards state to the HIDGamepad's
updateState:. supported_gamepads now reports macos_hid /
macos_amfi_required depending on probe result.
- cmake/dependencies/macos.cmake: FIND_LIBRARY(IO_KIT_LIBRARY IOKit)
for IOHIDUserDevice* symbols.
- cmake/compile_definitions/macos.cmake: hid_gamepad.{h,m} added to
PLATFORM_TARGET_FILES, input.cpp -> input.mm, IO_KIT_LIBRARY linked.
- cmake/dependencies/ffmpeg.cmake: stage a small allow-list of FFmpeg
internal headers (h2645_parse.h, get_bits.h, etc.) into the dist
include tree at configure time. src/cbs.cpp uses libavcodec internals
that `make install` doesn't export; surfacing them inline avoids the
alternative of adding the FFmpeg source tree to the include path
wholesale (which collides with libc++'s "thread.h" and friends).
What this does not do:
- Rumble / haptic feedback (Sunshine's feedback_queue_t is accepted but
not forwarded — virtual HID has no force-feedback motor to drive).
- DualSense touchpad, gyro, adaptive triggers, LED.
- Battery state reporting.
These can be added later under the same HIDGamepad class if needed.
Verified locally: hid_gamepad.m compiles, _OBJC_CLASS_$_HIDGamepad is
linked into Sunshine.app, the AMFI-disabled probe runs at startup and
emits the right instruction line for users not yet on the bypass.
Functional end-to-end test (gamepad events from Moonlight client
actually reaching a game) requires AMFI bypass + reboot.
Lumen attribution: Lumen is MIT-licensed; this port preserves the
underlying design and copies the HID report descriptor verbatim.
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Enables macOS virtual gamepad support by implementing an IOHIDUserDevice-backed HID controller and wiring it into the existing macOS input backend.
Changes:
- Convert macOS input implementation to Objective-C++ and add HIDGamepad slot management (alloc/free/update).
- Add new IOHIDUserDevice-based virtual gamepad implementation (hid_gamepad.{h,m}) with a startup availability probe.
- Update macOS CMake configuration to compile/link the new Objective-C sources and link against IOKit.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/platform/macos/input.cpp | Adds HIDGamepad integration and startup probing (intended to be Objective-C++ source). |
| src/platform/macos/hid_gamepad.m | Implements IOHIDUserDevice virtual gamepad creation, report descriptor, and report updates. |
| src/platform/macos/hid_gamepad.h | Declares HIDGamepad interface and packed report struct for IOHIDUserDevice. |
| cmake/dependencies/macos.cmake | Adds IOKit dependency for IOHIDUserDevice symbols. |
| cmake/compile_definitions/macos.cmake | Links IOKit and adds new macOS target sources (hid_gamepad + input.mm). |
Comments suppressed due to low confidence (1)
cmake/compile_definitions/macos.cmake:1
- CMake now references
src/platform/macos/input.mm, but the diff shown is againstsrc/platform/macos/input.cpp. If the file isn’t actually renamed in the PR (git mv), builds will fail due to a missing source file. Ensure the file is truly renamed toinput.mm(and removed/renamed from.cpp) so the build system and repository paths match.
# macos specific compile definitions
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| @kIOHIDVendorIDKey: @(0x1209), // Generic (pid.codes open-source VID) | ||
| @kIOHIDProductIDKey: @(0x5853), // Not in SDL's known controller database | ||
| @kIOHIDReportDescriptorKey: [NSData dataWithBytes:kHIDReportDescriptor | ||
| length:sizeof(kHIDReportDescriptor)], |
| @property (nonatomic, assign) int gamepadIndex; | ||
| @property (nonatomic, assign) BOOL isConnected; | ||
| @property (nonatomic, assign) IOHIDUserDeviceRef hidDevice; | ||
| @property (nonatomic, strong) dispatch_queue_t hidQueue; |
| * @details Creates a virtual Xbox-style gamepad recognized by macOS Game Controller framework. | ||
| * Requires SIP to be disabled for IOHIDUserDevice creation to succeed | ||
| * (bypasses com.apple.developer.hid.virtual.device entitlement check). |
| if (!_hidDevice) { | ||
| NSLog(@"[HIDGamepad] Failed to create IOHIDUserDevice for gamepad %d", _gamepadIndex); | ||
| _hidQueue = nil; | ||
| return NO; | ||
| } |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 94bc048033
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
|
||
| macos_input->gamepads[i] = pad; | ||
| BOOST_LOG(info) << "alloc_gamepad: slot "sv << i << " allocated (IOHIDUserDevice virtual gamepad)"sv; | ||
| return i; |
There was a problem hiding this comment.
Return success code from alloc_gamepad
When a second controller connects and the free slot is 1 or higher, src/input.cpp treats any nonzero return from platf::alloc_gamepad() as allocation failure and leaves gamepad.id unset. This line returns the slot number after already creating and storing the HID device, so additional controllers are ignored by the input layer and the created virtual device remains allocated until shutdown; the other backends return 0 on success and use id.globalIndex as the slot.
Useful? React with 👍 / 👎.
| static std::vector<supported_gamepad_t> gamepads; | ||
| if (!initialized) { | ||
| const BOOL hid_ok = [HIDGamepad isAvailable]; | ||
| gamepads.push_back({"hid", true, hid_ok ? "gamepads.macos_hid" : "gamepads.macos_amfi_required"}); |
There was a problem hiding this comment.
Mark HID option disabled when probing fails
When AMFI is still enabled, hid_ok is false and alloc_gamepad() will always return -1, but this entry is still pushed with is_enabled=true. The startup probe in src/input.cpp only warns when there are no enabled non-auto gamepads, and the config UI also relies on this enabled flag, so macOS reports an available gamepad option even though every allocation will fail in that environment.
Useful? React with 👍 / 👎.
Fixes brought forward from Copilot + Codex inline review on LizardByte#5203: 1. **alloc_gamepad return contract** (Codex P2). The platform interface in src/platform/common.h:831 documents "return 0 on success". src/input.cpp:897 calls `if (platf::alloc_gamepad(...))` and treats any non-zero return as failure. The prior implementation returned the slot index, which would misclassify every gamepad after slot 0 as a failure — only the first controller would actually work. Switch to keying the HIDGamepad slot off id.globalIndex (which Sunshine allocates from a bitmask before calling us) and return 0 on success, -1 on failure, matching the Windows / Linux contract. 2. **supported_gamepads is_enabled mirrors probe** (Codex P2). When AMFI is enabled and HIDGamepad isAvailable returns NO, the entry was still being pushed with is_enabled=true, so the web UI would offer a controller type that would immediately fail alloc_gamepad. Flip is_enabled to match the probe result so the UI surfaces the correct state. 3. **SIP → AMFI terminology fix** (Copilot). The hid_gamepad.{h,m} header doc comments said "Requires SIP to be disabled". The actual gating mechanism is AMFI (Apple Mobile File Integrity), not SIP (System Integrity Protection). Updated both files' doc blocks + the isAvailable method doc to reference AMFI consistently. The user-facing instruction line in input.mm and the PR body already correctly referenced amfi_get_out_of_my_way; this just brings the header docs in line. Not addressed (with rationale): - Copilot: "@kIOHIDVendorIDKey produces NSString of token name" — this is incorrect. The IOKit headers `#define kIOHIDVendorIDKey "VendorID"` as a plain C string literal, so `@kIOHIDVendorIDKey` expands to `@"VendorID"` which is a valid NSString literal. The dictionary construction is correct as written. Verified empirically: the runtime IOHIDUserDeviceCreateWithProperties call succeeds when AMFI is bypassed and returns NULL with no key-related diagnostics when not. - Copilot: "@Property strong in MRC". Clang treats `strong` as a synonym for `retain` under MRC, so this compiles and behaves correctly. We match Lumen's original on this for traceability; converting to `retain` is a cosmetic change. - Copilot: "NSLog vs BOOST_LOG". hid_gamepad.m is plain Objective-C (.m) by design — adding BOOST_LOG would require renaming to .mm and pulling Boost log headers into a hot-path file for log lines that fire only during device create/teardown (16x at most per session). Defendable consistency: input.mm uses BOOST_LOG; hid_gamepad.m uses NSLog for its low-volume device-lifecycle messages.
|
Pushed Real bugs fixed (3):
Defended (3) — included in commit body for record:
All addressed in 3 files, +55/-26 lines. The fix for #1 is the important one — without it, multi-gamepad scenarios would fail silently. Thanks for the careful pass. |
|



Description
Replaces the macOS gamepad stubs in
src/platform/macos/input.cppwith a working virtual gamepad implementation backed byIOHIDUserDevice. Moonlight clients' gamepad events have been silently dropped on the macOS host since the port shipped; this lights up the path end-to-end.Approach: publish a virtual HID device with a complete Xbox-style report descriptor (16 buttons, 4-bit hat switch, two 8-bit triggers, four 16-bit signed stick axes). macOS's HID matching layer publishes the device to userspace;
GameController.framework, SDL, Steam, and apps consuming raw IOKit HID all see it as a real USB controller.VID
0x1209/ PID0x5853— pid.codes open-source VID range, intentionally not in SDL's known-controller database so it routes through generic GameController mapping.Required boot flag
IOHIDUserDeviceCreateWithPropertiesreturns a NULL device unlesscom.apple.developer.hid.virtual.deviceis granted, and that entitlement is only honored when AMFI is bypassed at boot:```
sudo nvram boot-args="amfi_get_out_of_my_way=1" # plus any existing flags
reboot
```
SIP can stay on. The implementation probes once at
platf::input()construction; without the flag,alloc_gamepadreturns-1and the startup log emits a clear instruction line. This matches behavior on Linux whenuinputis unavailable.A DriverKit DEXT (no boot flag, requires Apple DriverKit entitlement approval + signed system-extension distribution) would be the longer-term shipping story but is multi-week work. Submitting the IOHIDUserDevice path first so the feature gap closes today.
Changes
src/platform/macos/hid_gamepad.{h,m}(new): MRC Obj-C wrapper aroundIOHIDUserDevice. Defines the gamepad HID report descriptor (Usage Page Generic Desktop / Usage Joystick to dodge SDL's GCController-only path on known controller IDs). Maps Sunshine's 32-bitbuttonFlagsto the 16-bit HID button field plus a 4-bit hat switch. Bounded 2 s teardown viaIOHIDUserDeviceSetCancelHandler. Report dispatch on a dedicatedQOS_CLASS_USER_INTERACTIVEserial queue.src/platform/macos/input.cpp→input.mm: renamed so the gamepad path holdsHIDGamepadstrong references directly without a C bridge. Mouse/keyboard injection paths are unchanged.macos_input_tgains a 16-slotHIDGamepad *array plus ahid_gamepad_availableflag (probed once at init).alloc_gamepadfinds the lowest free slot, creates a virtual device, returns the slot index.free_gamepaddisconnects and releases.gamepad_updateforwards state to the slot'sHIDGamepad.supported_gamepadsreportsmacos_hidormacos_amfi_requireddepending on probe result.cmake/dependencies/macos.cmake: `FIND_LIBRARY(IO_KIT_LIBRARY IOKit)` for the new symbols.cmake/compile_definitions/macos.cmake: addhid_gamepad.{h,m}toPLATFORM_TARGET_FILES, renameinput.cpp→input.mm, linkIO_KIT_LIBRARY.What this does NOT do
All four are additive to the existing `HIDGamepad` class and can ship in follow-up PRs.
Attribution
The IOHIDUserDevice approach and the HID report descriptor are derived from Lumen, an Apple-Silicon-focused Sunshine fork. Lumen is MIT-licensed. The HID report descriptor is copied verbatim with attribution in the source file header; the surrounding lifecycle/integration code is original to this PR.
Verification
Independence
Stands alone. Does not depend on any other open PR. No interaction with the SCK / EDR / ProRes work (separate PRs).