Skip to content

Add game controller emulation (HID, Xbox 360, Switch Pro, DualSense)#218

Draft
anagnorisis2peripeteia wants to merge 6 commits into
pikvm:masterfrom
anagnorisis2peripeteia:feature/hid-gamepad
Draft

Add game controller emulation (HID, Xbox 360, Switch Pro, DualSense)#218
anagnorisis2peripeteia wants to merge 6 commits into
pikvm:masterfrom
anagnorisis2peripeteia:feature/hid-gamepad

Conversation

@anagnorisis2peripeteia
Copy link
Copy Markdown

@anagnorisis2peripeteia anagnorisis2peripeteia commented May 30, 2026

Game Controller Emulation for PiKVM

Adds four gamepad emulation modes with multi-controller support (up to 4 simultaneous controllers) to the OTG backend.

Controller Modes

Mode Controller VID:PID Host Driver Console Support
hid Generic HID PiKVM default hid-generic N/A
xinput Xbox 360 045E:028E xpad Xbox, PC
switchpro Switch Pro 057E:2009 hid-nintendo Switch 1+2, PC
dualsense DualSense 054C:0CE6 hid-playstation PC/Steam only

Multi-Controller Support

Up to 4 gamepad slots, each a separate FunctionFS instance with its own USB endpoints (2 per controller).

Endpoint budget (9 total on Pi):

  • Default (2 gamepads + kbd + 2 mice + MSD): 9/9 endpoints
  • Couch co-op (4 gamepads, kbd/mouse/MSD disabled via kvmd-otgconf): 8/9

Slots 0-1 start enabled by default; 2-3 disabled. Toggle at runtime:

[root@pikvm ~]# kvmd-otgconf -d hid.usb0 hid.usb1 hid.usb2 mass_storage.usb0
[root@pikvm ~]# kvmd-otgconf -e ffs.gamepad2 ffs.gamepad3

Browser Gamepad API (navigator.getGamepads()[0..3]) maps to slots via the existing HID WebSocket. Binary opcode 6 extended: 9 bytes (legacy, index=0) or 10 bytes (index prefix 0-3). Backward compatible.

Architecture

  • Generic HID (gamepad.py): standard f_hid gadget
  • XInput (xinput.py): FunctionFS, vendor class 0xFF/0x5D/0x01, 20-byte reports
  • Switch Pro (switchpro.py): FunctionFS, HID class, full 0x80 handshake + SPI flash calibration
  • DualSense (dualsense.py): FunctionFS, HID class, GET_REPORT feature replies

Console auth notes

  • Switch 1/2: No auth. Pro Controller works on both via backwards compat.
  • PS5: Requires Sony crypto IC. DualSense mode is PC/Steam only.
  • Xbox: No auth for wired 360.

What is NOT tested

  • Real Pi hardware (spikes tested on Lima Ubuntu 24.04 VM with dummy_hcd)
  • Composite gadget coexistence with all 4 gamepads simultaneously
  • Switch/PS5 console verification (only Linux host drivers verified)
  • Web frontend visual gamepad overlay

Adds a USB HID gamepad alongside the keyboard and mouse using the same f_hid gadget mechanism. Disabled by default; enable by setting kvmd.hid.gamepad.device and otg.devices.hid.gamepad.start.

- apps/otg/hid/gamepad.py: 16-button report descriptor with two analog sticks, two analog triggers and a D-pad hat (9-byte report)
- plugins/hid/otg/gamepad.py: GamepadProcess streaming full-state reports
- plugins/hid: BaseHid.send_gamepad_event() with a no-op default for non-OTG backends
- apps/kvmd/api/hid.py: WS 'gamepad' event and binary opcode 6
- _scheme.py, validators/hid.py and udev rules (hidg3 -> kvmd-hid-gamepad)
Adds web/share/js/kvm/gamepad.js, a Gamepad module that polls the browser Gamepad API (navigator.getGamepads()) and sends full-state 'gamepad' HID events while the target exposes an online gamepad device. Wired into hid.js alongside Keyboard/Mouse (construct, setSocket, state sync, releaseAll).
XInput controllers are USB vendor-specific (class 0xFF/0x5D/0x01), not HID, so they cannot use f_hid. This adds an XInput mode for the optional gamepad, implemented as a FunctionFS function serviced from user space.

- plugins/hid/otg/xinput.py: XInputProcess streams the 20-byte 360 report, services the ffs endpoints and binds the UDC. A FunctionFS function has no endpoints until its descriptors are written, so the bind is deferred from kvmd-otg to the servicer. Same send_*/get_state interface as GamepadProcess.
- apps/otg: add_xinput() creates and mounts the ffs.xinput function; in XInput mode the gadget presents as 045E:028E (so xpad/consoles bind it) and the UDC bind is deferred.
- plugins/hid/otg: kvmd.hid.gamepad.mode (hid|xinput) selects the backend.

Validated standalone on dummy_hcd (enumerates as an Xbox 360 pad, xpad binds, live reports via jstest). Full in-daemon test pending. Requires python-functionfs.
@anagnorisis2peripeteia anagnorisis2peripeteia changed the title Add an optional generic USB HID gamepad Add an optional USB gamepad: generic HID and Xbox 360 (XInput) May 30, 2026
Two new FunctionFS-based gamepad backends alongside the existing
generic HID and XInput modes:

- switchpro: Nintendo Switch Pro Controller (057E:2009). Full
  hid-nintendo handshake (0x80 USB commands + 0x01 subcommands
  with SPI flash calibration reads). Works on Switch 1, Switch 2
  (backwards compat), and PC.

- dualsense: Sony DualSense / PS5 controller (054C:0CE6). Responds
  to hid-playstation feature report probes (0x09 MAC, 0x05 cal,
  0x20 firmware). PC/Steam only (PS5 console auth requires Sony
  signing IC).

Both use the same send_state_event/get_state interface as
XInputProcess, selected via gamepad.mode config.
@anagnorisis2peripeteia anagnorisis2peripeteia changed the title Add an optional USB gamepad: generic HID and Xbox 360 (XInput) Add game controller emulation (HID, Xbox 360, Switch Pro, DualSense) Jun 1, 2026
Refactors the single gamepad slot into an array of up to 4 indexed
gamepad processes. Each is a separate FunctionFS instance with its
own USB endpoints (2 per controller: IN + OUT).

Config: gamepad.count (default 2, max 4) + gamepad.ffs_base path.
Slots 0-1 start enabled by default; 2-3 start disabled (can be
toggled at runtime via kvmd-otgconf).

Endpoint budget (9 total on Pi):
  - kbd(1) + mouse(1) + rel_mouse(1) + MSD(2) + 2 gamepads(4) = 9
  - Disable kbd/mouse/MSD via kvmd-otgconf for 4 gamepads (couch co-op)

Binary WS opcode 6 extended: 9-byte legacy (index=0) or 10-byte
(index prefix, 0-3). JSON "gamepad" event gains "index" field.
Backward compatible: existing 9-byte payloads still work as index 0.
Two input-path optimizations:

1. USB endpoint bInterval=1 (1ms/1000Hz polling) for XInput and
   DualSense modes. The host reads gamepad reports up to 1000 times
   per second instead of the previous 125Hz (8ms) or 250Hz (4ms).
   Switch Pro stays at bInterval=8 to match real hardware behavior.

2. Browser gamepad poller switched from requestAnimationFrame (16ms,
   tied to display refresh) to setInterval(4) (4ms, ~250Hz). This is
   the minimum reliable timer interval browsers allow. Combined with
   change-detection, reports are only sent when state actually changes.

Also: the poller now reads ALL connected gamepads (up to 4) per tick
and sends each with the 10-byte indexed binary format (opcode 6 with
index prefix), matching the multi-controller backend from the previous
commit.

Net effect: worst-case input latency drops from ~24ms (16ms browser
poll + 8ms USB poll) to ~5ms (4ms browser poll + 1ms USB poll).
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.

1 participant