portersky fafca4c265 feat: add Xbox One HID report layout and mapping
Phase 4 foundation, independent of the presentation mechanism:

- hid/hid_report.hpp: standard Xbox One report layout (64-byte input:
  buttons/guide/triggers/sticks; 64-byte output: motors), the HID
  report descriptor, and GIP state to report mapping.
- make_input_report(): remaps GIP button bits to the standard Xbox
  bitmask, scales 10-bit triggers to 8-bit, sticks pass through.
- parse_output_report(): extracts motor intensities from output
  reports for the rumble relay; malformed reports stop the motors.
- test_hid: unit tests for the mapping, scaling, and descriptor.

Report packing verified with a descriptor walker: input and output
are exactly 64 bytes each. Next step is presentation via DriverKit,
which needs a Developer ID with the DriverKit entitlement.

Co-Authored-By: qwen3.8-27b@q3_k_xl: implemented HID report layer
2026-08-29 15:39:03 +02:00
2026-08-17 14:18:03 +02:00
2026-08-29 13:36:54 +02:00
2026-08-17 14:18:03 +02:00

Xbox Wireless Dongle: macOS Port

User-space macOS app for the Xbox Wireless Dongle (MediaTek MT76xx). Ports the Linux kernel driver xone/ to macOS.

See PLAN.md for full architecture and implementation phases.


Building

Requires CMake ≥ 3.21, Xcode (or Command Line Tools), and Ninja. Swift is only supported by the Ninja and Xcode generators in CMake, so configure with one of them explicitly:

cmake -S . -B build -GNinja -DBUILD_TESTING=ON
ninja -C build
./build/xone_app
ninja -C build check   # build and run the test suite

Layout (mirrors ../refix/):

  • deps/: CMake modules: Platform.cmake, Flags.cmake, Sanitizers.cmake, FindUnity.cmake
  • src/{usb,mt76,gip,auth,hid}: C++ protocol stack (ported from medusalix/xone)
  • src/app/: Swift app entry point + C ABI bridge (include/app/xone_api.h)
  • src/cli/: C++ CLI (xone_cli) for manual protocol sequences
  • scripts/: download-firmware.sh (port of the upstream firmware fetcher)
  • tests/: Unity test suite (BUILD_TESTING=ON)

Investigation Checklist

These items need research on macOS before implementation can begin. Strike through or check off as each is resolved.

1. IOKit USB Access

  • IOUSBLib vs IOUSBFamily: Resolved: the SDK exposes only the struct-based IOUSBDeviceInterface / IOUSBInterfaceInterface (v197/v190) via IOCreatePlugInInterfaceForService() + QueryInterface. Implemented in src/usb/usb_transport.cpp.
  • USB device matching: Class match on IOUSBDevice plus a user-space VID/PID filter on the idVendor/idProduct properties (numeric registry matching proved unreliable). Verified with a physical dongle (PID 0x02E6).
  • Interface claiming: All interfaces are opened and pipes mapped by endpoint number + direction (EP 0x04 IN/OUT, EP 0x05 IN). Verified: opens cleanly with no driver conflict.
  • Async transfer latency: Resolved: the ReadPipeAsync pump (4 outstanding reads per IN pipe, resubmission in the completion handler) streams live controller input with no drops at game-report rates.
  • Device reconnect handling: Resolved: kIOTerminatedNotification with a PID filter fires on unplug and chip re-enumeration. Verified across firmware-load resets and repeated unplug/replug cycles.

2. Firmware Loading

  • Firmware binary availability: Resolved: scripts/download-firmware.sh (port of ../xone/install/firmware.sh) fetches the Windows Update CAB images and the hashes match the Linux driver. Images land in firmware/.
  • Firmware load sequence: Resolved: ported to src/mt76/mt76.cpp (control request to firmware mode, bulk transfer in 0x3800-byte chunks, MCU completion poll, chip reset). Verified on hardware: FCE shows the firmware running after load.
  • Post-firmware reconnect: Resolved: the chip re-enumerates with the same VID/PID; the app and CLI close, wait, and reopen the device. The full sequence runs end-to-end.

3. MT76 Register Access

  • Vendor request format: Resolved: the Linux values work unchanged via IOUSBDeviceInterface->DeviceRequest() (bRequest 0x84/0x86 for register R/W). All radio init traffic uses this path.
  • Register timing: Resolved: usleep() / clock_nanosleep() in user-space are sufficient; radio init and channel evaluation complete reliably.
  • EFUSE read: Resolved: the EFUSE sequence returns valid MAC address, chip ID, and TX power calibration data on macOS (xone_cli info).

4. 802.11 Frame Handling

  • Beacon construction: Resolved: beacons with the Microsoft OUI (00:50:f2) information element transmit correctly, including the chip header before the 802.11 frame. Controllers hear them and associate.
  • Frame encapsulation: Resolved: TX/RX chip headers handled per the Linux driver (send_wlan / process_wlan). Association, pairing, and encryption-enable management frames verified against hardware.
  • QoS data frames: RX path verified: controller input QoS frames are decrypted, fed to the GIP layer, and shown live in the app. Host to controller TX (rumble) verified on hardware via the app's Rumble button.

5. AES-CCMP Encryption

  • Crypto backend choice: CommonCrypto (SHA-256/HMAC), Security.framework (RSA PKCS#1), and a self-contained P-256 ECDH (no public macOS C API for EC key agreement). Implemented in src/auth/crypto.cpp.
  • CCMP mode implementation: Not needed on the host: AES-CCMP runs on the MT76 chip; the host only installs keys via WCID registers (xone_mt76_set_client_key).
  • ECDH key exchange: P-256 implemented and validated against OpenSSL-derived test vectors (tests/test_crypto.cpp).

6. Virtual HID Gamepad

  • HID Proxy Driver feasibility: Research Apple's HID Proxy Driver (DriverKit). Can we create a virtual Xbox controller that games recognize natively?
  • IOHIDDevice user-space alternative: Can we create a virtual HID device entirely in user-space? Test with IOHIDManager and see if games (Steam, Game Center) recognize it.
  • HID report descriptor: Write an Xbox 360/One-compatible HID report descriptor. Test with existing Xbox controller (via Bluetooth) to capture the exact report format macOS expects.
  • Force feedback: Can the virtual HID device receive rumble commands from games and relay them to the controller via GIP?

7. Core Audio (Headset Support)

  • Audio Unit setup: Test creating an AURenderCallback / AUOutputUnit for headset playback and AURecordingCallback / AUInputUnit for mic input.
  • Latency requirements: The GIP protocol sends audio in 8ms intervals. Can Core Audio maintain this latency without glitches?
  • Format negotiation: The headset negotiates audio format (sample rate, channels) via GIP. Map GIP audio formats to Core Audio AudioStreamBasicDescription.

8. Build System

  • CMake vs Xcode: Resolved: CMake with the Ninja generator compiles both the C++ stack and the Swift app (see Building). An Xcode project is only needed later for a DriverKit extension, if Phase 4 goes that route.
  • Minimum macOS version: Target 12.0 (Monterey) for modern IOKit/DriverKit. Verify all APIs are available.
  • Code signing: Development builds run unsigned on the local machine with no entitlements needed for IOKit USB access. DriverKit notarization and distribution signing deferred to the HID phase.
  • 5GHz channel restrictions: Channel evaluation picks a working channel and pairing is verified on hardware; no regulatory failures seen so far. Regional edge cases (blocked channels) remain untested.
  • Firmware license: The firmware binaries are from Microsoft Windows Update. Confirm they can be redistributed with the macOS port (the Linux driver includes them with a disclaimer).

10. Testing Hardware

  • Dongle: Xbox Wireless Dongle (PID 0x02FE preferred, 0x02E6 also works). In hand: PID 0x02E6 verified end-to-end.
  • Controller: Xbox One or Series X|S controller (for pairing and input testing). In hand: pairing and live input verified.
  • Headset: Xbox Wireless Headset (optional, for audio testing)
  • macOS machine: Intel or Apple Silicon (test both if possible, IOKit may differ)

Quick Reference

Linux API macOS Replacement Status
usb_control_msg() IOUSBDeviceInterface->DeviceRequest() Done (send_vendor_request)
usb_bulk_msg() WritePipe() / ReadPipeAsync() Done (bulk_write, reader thread)
usb_submit_urb() ReadPipeAsync() + CFRunLoopSource Done (reader thread)
kzalloc / kfree malloc / free Straightforward
spin_lock_irqsave std::mutex / std::condition_variable Done (usb_transport.cpp)
msleep / mdelay usleep() / clock_nanosleep() Done (radio init, channel eval)
crypto_shash_* CommonCrypto / Security.framework Done (auth/crypto.cpp)
input_register_device() HID Proxy Driver / IOHIDSystem ☐ Investigate
snd_pcm_* Core Audio (Audio Units) ☐ Investigate
request_firmware() File I/O (fopen/fread) Straightforward
cfg80211_* Nothing (no regulatory reporting) Remove
bus_register() Custom client management Redesign
device_create() / sysfs Nothing (no sysfs) Remove

Current Status

Working end-to-end on hardware:

  • Dongle probe/open, async read pump, vendor register R/W
  • Firmware download (scripts/download-firmware.sh) and load
  • EFUSE read, radio init, channel evaluation, beacon TX
  • Pairing mode, controller association, GIP handshake + auth
  • Live controller input (buttons, sticks, triggers) in the Swift app
  • Rumble test button (host to controller TX over the GIP data path)

Not done yet: virtual HID gamepad (Phase 4), headset audio (Phase 5).

xone_cli exercises the stack manually in a single dongle session:

./build/xone_cli info firmware firmware/xow_dongle.bin radio-init pair
S
Description
No description provided
Readme 648 KiB
Languages
C++ 86.7%
Swift 8.3%
CMake 2.9%
C 1.5%
Shell 0.6%