Skip to content

feat: classify USB-direct controllers by interface descriptor, not a VID:PID allowlist - #150

Merged
emir-hasanbegovic merged 1 commit into
mainfrom
feat/usb-direct-descriptor-classification
Jul 22, 2026
Merged

feat: classify USB-direct controllers by interface descriptor, not a VID:PID allowlist#150
emir-hasanbegovic merged 1 commit into
mainfrom
feat/usb-direct-descriptor-classification

Conversation

@emir-hasanbegovic

Copy link
Copy Markdown
Contributor

Problem

USB Direct mode recognized a controller only if its exact VID:PID was in the native device table. An XInput controller under an unlisted PID — e.g. an 8BitDo Ultimate 2.4g dongle in X-input mode — fell through to the generic-HID guess and failed to claim (silent garbage or a probe timeout), even though the OS xpad driver handles it fine (which is why it works in Moonlight). The table already held ~200 XInput/GIP rows that all map to the same two parsers — encoding one bit the interface descriptor states directly.

Separately, the Kotlin side claimed the first HID/vendor interface with an interrupt-IN, which on a composite Xbox 360 controller can be the audio interface (0x5D/0x03) rather than the gamepad.

Fix — classify by the USB interface descriptor, not a VID:PID allowlist

  • usbparsers::classifyDevice(vid, pid, class, subclass, protocol) (pure, host-tested): a known-table entry still wins (so every listed device keeps its exact parser/init/quirks and the Sony/Switch vendor report formats); otherwise the descriptor decides — 0xFF/0x5D/0x01 → XInput, 0xFF/0x47/0xD0 → Xbox One GIP (power-on), class 0x03 → HID.
  • Plumb the interface triple through attachUsbDevice (Kotlin → JNI → usbhost::attachDevice).
  • Ranked interface selection so the gamepad interface is chosen over the Xbox 360 audio/expansion (0x5D/0x02, 0x5D/0x03) and security (0xFD) interfaces, which are never claimed.

Any standards-compliant wired XInput / GIP / HID controller now works in Direct mode without a table entry; the 8BitDo dongle is a consequence, not a special case, and its rumble + analog triggers come with the XInput path (which the DInput path can't express). Everything is sourced from the Linux xpad driver and [MS-GIPUSB] — no guessed bytes.

Auto-claim is unchanged: isVerifiedFastLane still keys on the kKnown table only, so descriptor-classified devices are reached only through an explicit Direct pick (dashboard / wizard "Try Direct"), never auto-claimed.

Tests

  • 6 C++ classifyDevice cases (usb_parsers_test.cpp): table-wins-over-descriptor, wired XInput / GIP / HID by triple, the 8BitDo dongle triple → XInput without a table entry, unknown-vendor fallback.
  • Kotlin disambiguation test (UsbGamepadManagerTest.kt): a composite Xbox 360 device enumerated audio-first must claim the gamepad interface — verified to fail against the old first-match selection.
  • Full local CI green: clang-format, ktlint, detekt, lint, testDebugUnitTest, nativeTest (174/174), assembleDebug.

Scope (deliberate, not hacked)

Out of scope because doing them correctly needs the physical hardware to verify (shipping a guessed implementation would violate the "no guessing" bar): the Microsoft Xbox 360 wireless-receiver (0x5D/0x81) presence/unwrap state machine, and deeper HID multi-report muxing. Existing table entries for those keep working unchanged.

…VID:PID allowlist

Direct mode recognized a controller only if its exact VID:PID was in the
native table, so an XInput pad under an unlisted PID -- e.g. an 8BitDo
Ultimate 2.4g dongle in X-input mode -- fell through to the generic-HID guess
and failed to claim, even though the OS xpad driver handles it fine. The
Kotlin side also claimed the first HID/vendor interface with an interrupt-IN,
which on a composite Xbox 360 controller can be the audio interface.

Classify by the USB interface descriptor instead:

- New pure usbparsers::classifyDevice(vid, pid, class, subclass, protocol): a
  known-table entry still wins (preserving every listed device's
  parser/init/quirks and the Sony/Switch vendor report formats); otherwise
  0xFF/0x5D/0x01 -> XInput, 0xFF/0x47/0xD0 -> Xbox One GIP (power-on),
  class 0x03 -> HID. Host-tested.
- Plumb the interface triple through attachUsbDevice (Kotlin -> JNI ->
  usbhost::attachDevice).
- Rank interface selection so the gamepad interface is chosen over the
  Xbox 360 audio/expansion (0x5D/0x02, 0x5D/0x03) and security (0xFD)
  interfaces, which are never claimed.

Any standards-compliant wired XInput / GIP / HID controller now works in
Direct mode without a table entry; the 8BitDo dongle is a consequence, not a
special case, and its rumble + analog triggers come with the XInput path.
Auto-claim is unchanged -- descriptor-classified devices are reached only
through an explicit Direct pick, never auto-claimed.

Sourced from the Linux xpad driver and [MS-GIPUSB]. Scoped out (needs the
hardware to verify, not guessed): the Microsoft Xbox 360 wireless-receiver
(0x5D/0x81) presence/unwrap state machine, and deeper HID multi-report
muxing. Existing table entries for those keep working unchanged.
@emir-hasanbegovic
emir-hasanbegovic force-pushed the feat/usb-direct-descriptor-classification branch from 2df107a to 4817394 Compare July 22, 2026 16:13
@emir-hasanbegovic
emir-hasanbegovic merged commit d76c659 into main Jul 22, 2026
9 checks passed
@emir-hasanbegovic
emir-hasanbegovic deleted the feat/usb-direct-descriptor-classification branch July 22, 2026 16:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants