WIP: feat(input): support the Switch 2 Pro Controller over USB, with HD rumble #2252

Draft
hckrman101 wants to merge 2 commits from hckrman101/feat/switch2-pro-controller into main
hckrman101 commented 2026-11-11 11:55:30 +00:00 (Migrated from github.com)

The Switch 2 Pro Controller doesn't work in Hookshot today: the SDL3 the Windows game ships has no driver for it (SDL's Switch 2 driver needs libusb), so the controller sends nothing. This PR adds a small driver of Hookshot's own for it over USB, with buttons, sticks, motion controls and the game's HD rumble, and nothing for players to install.

I don't have a Switch 2 Pro Controller, so none of this has run on the controller itself. Everything below is checked against the code it comes from, without hardware. If you have one, please try it (details at the end).

What it supports

  • Switch 2 Pro Controller over USB (057E:2069), on Windows and Linux. Bluetooth isn't supported (the controller uses a different protocol over Bluetooth LE, and SDL doesn't support it either).
  • Buttons and sticks, the same way the console sees them. B is the bottom face button and A the right one, as with the Switch Pro Controller. Stick calibration is read from the controller's flash (the factory values, or the user's own when there are any).
  • Home opens the Insert menu, as on other controllers. Capture and C (GameChat) are left to the system, as on the console.
  • GL and GR press nothing, which is what the console does until the player assigns them. HOS_SW2_GL / HOS_SW2_GR can assign them: set one to an input name (south east west north back start lstick rstick lshoulder rshoulder up down left right zl zr) and the button presses that input, so the Controls page can bind it like any other.
  • Motion controls through the existing Motion settings (sensitivity, invert, dead zone), with the controller's factory gyro bias taken off.
  • HD rumble: the game's own vibration values (both frequency bands, each side on its own actuator), with the same felt-amplitude curve and Rumble strength the Switch Pro Controller path already uses. HOS_HD_RUMBLE=0 plays the plain two-motor rumble at SDL's fixed frequencies instead.
  • Player 1 light turns on.
  • HOS_NO_SW2=1 turns all of this off and leaves the controller to SDL, as before.

How it works

  • host/sw2_pro.h is the protocol, with no I/O:
    • Setup: the calibration reads from flash, the init command sequence, and the player-light command.
    • Input report (format 0x05, 64 bytes every 4 ms): buttons, sticks, sensors, and the sensor-clock check.
    • HD rumble: the encoding and the output report.
    • USB descriptors: a small parser for them.
  • host/pad_sw2.c runs one thread per controller. It sets the controller up, reads it and sends its rumble:
    • Windows: the controller's setup interface (MI_01) gets Windows' own WinUSB driver automatically, because the controller asks for it in its descriptors (WebUSB tools rely on the same thing). Setup goes through winusb.dll; input and rumble go through the normal HID interface (hid.dll, setupapi.dll). All three are system DLLs, loaded at run time, so nothing changes in the build or link. If the setup interface has no WinUSB driver, a controller another program already started is still read.
    • Linux: usbfs (/dev/bus/usb), as SDL's libusb path does. Setup goes over the vendor interface, then the HID interface is taken from hid-generic (and given back when the controller goes away) and read directly. The device node has to belong to the logged-in user. Steam's udev rules (steam-devices) already do that for this controller, and SDL's own driver needs the same access. Without it, the log gives the one-line rule to add. An interface claimed by another program (Steam Input) is never taken.
  • host/hos_sdl.c hands these pads to host/pad_sdl.c as one more pad, so the dead zone, stick-rest guard, motion filter, rumble strength and bindings all apply unchanged. SDL's own view of the controller is left out of the list (by 057E:2069 only), and SDL's HIDAPI is told to ignore that one id (SDL_HIDAPI_IGNORE_DEVICES, added to anything the player set), so the controller never shows up twice. The Joy-Con 2 and the GameCube controller are untouched and stay SDL's.
  • Only 057E:2069 is ever opened. On Linux the device is picked from sysfs ids before anything is opened; on Windows, by its interface path. Every other controller, the Switch Pro Controller (057E:2009) included, goes through SDL exactly as before.

Why not a patched SDL with libusb, like Fritchii? We'd have to build and ship our own SDL3 with libusb for Windows, bundle SDL3 in the AppImage instead of using the system's, and re-apply an SDL patch every release, because SDL's Switch 2 driver can only play its two fixed rumble frequencies (a raw HD rumble effect was proposed to SDL and not merged). Its Linux path also needs the same usbfs access this does. The protocol is small enough to carry here instead, and players don't have to install anything.

Where it comes from (credit)

  • Protocol: ported from SDL's Switch 2 driver, src/joystick/hidapi/SDL_hidapi_switch2.c, by Vicki Pfau and Sam Lantinga for Valve (zlib license). That covers the init sequence, the flash addresses and calibration layout, the input report layout, the sensor scales and clock check, and the rumble sample encoding. The commits carry Co-authored-by lines for them.
  • HD rumble: from Fritchii PR #6 by 101Leafy (GPL-3.0). That covers the Pro Controller's HD rumble report (three 5 ms samples per actuator) and the frequency scale 10 Hz × 2^(code / 96), which they measured with the controller's own accelerometer (within about 1% from 20 to 357 Hz). There's no official documentation for that scale. The commits carry a Co-authored-by line for them.
  • Checked against, not copied:
    • Altureus/Windows-Switch2-Pro-Controller shows the setup interface comes up with WinUSB on Windows without Zadig, and that its captured rumble report has the same layout.
    • Qzq66/ns2pro-steamos-usb shows the rootless usbfs approach on Linux: claim the vendor interface, detach hid-generic, read the interrupt endpoint.

Tests (no controller needed)

tests/test_switch2_pro.py compiles the protocol and runs it next to copies of the functions it was ported from, on the same bytes:

  • IDs: only 057E:2069 matches. The Switch Pro Controller, Joy-Con, Joy-Con 2, GameCube controller and other vendors don't.
  • Input report against SDL's HandleSwitchProState: 20,000 random reports, with and without stick calibration. Every button, the d-pad, ZL/ZR and all four stick axes match exactly. The sensor values and SDL's sensor-clock check (34.8 vs 40 rad/s full scale) also match, and a flat controller reads 1 G.
  • HD rumble:
    • The bit packing matches SDL's EncodeHDRumble, checked on 20,000 random samples and on every bit.
    • Every one of the 1024 frequency codes comes back from its own frequency, and the codes match Fritchii's Switch2FrequencyCode.
    • Amplitudes are capped at SDL's maximum, the strength scaling works, and the report is byte-for-byte Fritchii's Pro Controller report.
    • The send rate is right: a change goes out at once, a rumble repeats every 12 ms, and silence is sent once.
    • The HOS_HD_RUMBLE=0 motor rumble matches SDL's own.
  • Setup against a fake controller: the flash reads happen in SDL's order, and the init sequence is byte-for-byte SDL's, then the player light. The serial, gyro bias and stick calibration are parsed, with the user's calibration taking priority over the factory one. Blank flash gives no bias, and a controller that stops answering partway through counts as not set up.
  • USB descriptors and Linux usbfs: the descriptor parser is checked, and the usbfs request numbers and struct sizes are checked against linux/usbdevice_fs.h.
  • Wiring:
    • Every hos_sdl_pad_* call hands Switch 2 pads to the new driver, SDL's own copy of the controller is skipped by its IDs alone, and pad_sdl.c sends it the game's vibration values.
    • Nothing is opened before the IDs are checked, and another program's claim on the controller is never taken.

tests/run_all.py on Linux: 357 passed, 4 failed, 13 skipped. The 4 failures are local environment problems in files this PR doesn't touch: two need zstandard, which the system Python here doesn't have (both pass with the bundled Python), one needs Tk, and one is a clang 23 warning in an existing test harness.

The Linux build is clean. The Windows files were compiled and linked with llvm-mingw: they import only kernel32, advapi32 and ole32 at link time; setupapi, winusb and hid are loaded at run time.

On Linux I also ran the device scan alone, with a Switch Pro Controller (057E:2009) plugged in over USB. It found no Switch 2 controller, opened nothing and started no thread.

Not tested

  • Everything on real hardware. That includes the setup over WinUSB and usbfs, the input reports, the sensor axes and signs, and the rumble.
  • Rumble strength. The game's amplitude 1.0 is mapped to SDL's strongest rumble on this controller (29000 of 65535, field value 453). How the console scales amplitudes for the Switch 2 controller isn't known. The test haptic of ProCon2Tool (a browser tool), as captured by the Altureus project, uses far smaller values (fields 2 to 9), so it may feel stronger or weaker than on the console.
  • Frequencies outside 20–357 Hz. The 96-steps-per-octave scale is only measured in that range.
  • Two controllers at once on Windows. Only one is handled for now: matching each setup interface to its HID interface is left until someone has two. Linux handles up to four.
  • Steam running with Steam Input on. Steam keeps the controller; this should log that and leave it alone, but that hasn't been seen.
  • The Joy-Con 2 and the GameCube controller. They aren't handled; they still go through SDL as before.

If you have a Switch 2 Pro Controller

Please plug it in with a USB cable and run the game. Then send hookshot_log.txt (the hid: lines) and tell us:

  • Do all buttons and both sticks do the right thing?
  • Does motion aiming move the right way?
  • Does rumble feel right (strength and pitch) compared with the console?

HOS_MOTION_DEBUG=1 logs the sensor readings twice a second.

On Linux, if the log says there is no access to the USB device, install Steam's steam-devices udev rules or add the rule the log gives.

The Switch 2 Pro Controller doesn't work in Hookshot today: the SDL3 the Windows game ships has no driver for it (SDL's Switch 2 driver needs libusb), so the controller sends nothing. This PR adds a small driver of Hookshot's own for it over USB, with buttons, sticks, motion controls and the game's HD rumble, and nothing for players to install. **I don't have a Switch 2 Pro Controller, so none of this has run on the controller itself.** Everything below is checked against the code it comes from, without hardware. If you have one, please try it (details at the end). ## What it supports - **Switch 2 Pro Controller over USB** (`057E:2069`), on Windows and Linux. Bluetooth isn't supported (the controller uses a different protocol over Bluetooth LE, and SDL doesn't support it either). - **Buttons and sticks**, the same way the console sees them. B is the bottom face button and A the right one, as with the Switch Pro Controller. Stick calibration is read from the controller's flash (the factory values, or the user's own when there are any). - **Home opens the Insert menu**, as on other controllers. **Capture and C (GameChat)** are left to the system, as on the console. - **GL and GR** press nothing, which is what the console does until the player assigns them. `HOS_SW2_GL` / `HOS_SW2_GR` can assign them: set one to an input name (`south east west north back start lstick rstick lshoulder rshoulder up down left right zl zr`) and the button presses that input, so the Controls page can bind it like any other. - **Motion controls** through the existing Motion settings (sensitivity, invert, dead zone), with the controller's factory gyro bias taken off. - **HD rumble:** the game's own vibration values (both frequency bands, each side on its own actuator), with the same felt-amplitude curve and Rumble strength the Switch Pro Controller path already uses. `HOS_HD_RUMBLE=0` plays the plain two-motor rumble at SDL's fixed frequencies instead. - **Player 1 light** turns on. - `HOS_NO_SW2=1` turns all of this off and leaves the controller to SDL, as before. ## How it works - `host/sw2_pro.h` is the protocol, with no I/O: - **Setup:** the calibration reads from flash, the init command sequence, and the player-light command. - **Input report** (format 0x05, 64 bytes every 4 ms): buttons, sticks, sensors, and the sensor-clock check. - **HD rumble:** the encoding and the output report. - **USB descriptors:** a small parser for them. - `host/pad_sw2.c` runs one thread per controller. It sets the controller up, reads it and sends its rumble: - **Windows:** the controller's setup interface (MI_01) gets Windows' own WinUSB driver automatically, because the controller asks for it in its descriptors (WebUSB tools rely on the same thing). Setup goes through `winusb.dll`; input and rumble go through the normal HID interface (`hid.dll`, `setupapi.dll`). All three are system DLLs, loaded at run time, so nothing changes in the build or link. If the setup interface has no WinUSB driver, a controller another program already started is still read. - **Linux:** usbfs (`/dev/bus/usb`), as SDL's libusb path does. Setup goes over the vendor interface, then the HID interface is taken from `hid-generic` (and given back when the controller goes away) and read directly. The device node has to belong to the logged-in user. Steam's udev rules (`steam-devices`) already do that for this controller, and SDL's own driver needs the same access. Without it, the log gives the one-line rule to add. An interface claimed by another program (Steam Input) is never taken. - `host/hos_sdl.c` hands these pads to `host/pad_sdl.c` as one more pad, so the dead zone, stick-rest guard, motion filter, rumble strength and bindings all apply unchanged. SDL's own view of the controller is left out of the list (by `057E:2069` only), and SDL's HIDAPI is told to ignore that one id (`SDL_HIDAPI_IGNORE_DEVICES`, added to anything the player set), so the controller never shows up twice. The Joy-Con 2 and the GameCube controller are untouched and stay SDL's. - Only `057E:2069` is ever opened. On Linux the device is picked from sysfs ids before anything is opened; on Windows, by its interface path. Every other controller, the Switch Pro Controller (`057E:2009`) included, goes through SDL exactly as before. Why not a patched SDL with libusb, like Fritchii? We'd have to build and ship our own SDL3 with libusb for Windows, bundle SDL3 in the AppImage instead of using the system's, and re-apply an SDL patch every release, because SDL's Switch 2 driver can only play its two fixed rumble frequencies (a raw HD rumble effect was proposed to SDL and not merged). Its Linux path also needs the same usbfs access this does. The protocol is small enough to carry here instead, and players don't have to install anything. ## Where it comes from (credit) - **Protocol:** ported from SDL's Switch 2 driver, [`src/joystick/hidapi/SDL_hidapi_switch2.c`](https://github.com/libsdl-org/SDL/blob/main/src/joystick/hidapi/SDL_hidapi_switch2.c), by Vicki Pfau and Sam Lantinga for Valve (zlib license). That covers the init sequence, the flash addresses and calibration layout, the input report layout, the sensor scales and clock check, and the rumble sample encoding. The commits carry `Co-authored-by` lines for them. - **HD rumble:** from [Fritchii PR #6](https://github.com/Frizti/Fritchii/pull/6) by 101Leafy (GPL-3.0). That covers the Pro Controller's HD rumble report (three 5 ms samples per actuator) and the frequency scale **10 Hz × 2^(code / 96)**, which they measured with the controller's own accelerometer (within about 1% from 20 to 357 Hz). There's no official documentation for that scale. The commits carry a `Co-authored-by` line for them. - **Checked against, not copied:** - [Altureus/Windows-Switch2-Pro-Controller](https://github.com/Altureus/Windows-Switch2-Pro-Controller) shows the setup interface comes up with WinUSB on Windows without Zadig, and that its captured rumble report has the same layout. - [Qzq66/ns2pro-steamos-usb](https://github.com/Qzq66/ns2pro-steamos-usb) shows the rootless usbfs approach on Linux: claim the vendor interface, detach `hid-generic`, read the interrupt endpoint. ## Tests (no controller needed) `tests/test_switch2_pro.py` compiles the protocol and runs it next to copies of the functions it was ported from, on the same bytes: - **IDs:** only `057E:2069` matches. The Switch Pro Controller, Joy-Con, Joy-Con 2, GameCube controller and other vendors don't. - **Input report against SDL's `HandleSwitchProState`:** 20,000 random reports, with and without stick calibration. Every button, the d-pad, ZL/ZR and all four stick axes match exactly. The sensor values and SDL's sensor-clock check (34.8 vs 40 rad/s full scale) also match, and a flat controller reads 1 G. - **HD rumble:** - The bit packing matches SDL's `EncodeHDRumble`, checked on 20,000 random samples and on every bit. - Every one of the 1024 frequency codes comes back from its own frequency, and the codes match Fritchii's `Switch2FrequencyCode`. - Amplitudes are capped at SDL's maximum, the strength scaling works, and the report is byte-for-byte Fritchii's Pro Controller report. - The send rate is right: a change goes out at once, a rumble repeats every 12 ms, and silence is sent once. - The `HOS_HD_RUMBLE=0` motor rumble matches SDL's own. - **Setup against a fake controller:** the flash reads happen in SDL's order, and the init sequence is byte-for-byte SDL's, then the player light. The serial, gyro bias and stick calibration are parsed, with the user's calibration taking priority over the factory one. Blank flash gives no bias, and a controller that stops answering partway through counts as not set up. - **USB descriptors and Linux usbfs:** the descriptor parser is checked, and the usbfs request numbers and struct sizes are checked against `linux/usbdevice_fs.h`. - **Wiring:** - Every `hos_sdl_pad_*` call hands Switch 2 pads to the new driver, SDL's own copy of the controller is skipped by its IDs alone, and `pad_sdl.c` sends it the game's vibration values. - Nothing is opened before the IDs are checked, and another program's claim on the controller is never taken. `tests/run_all.py` on Linux: 357 passed, 4 failed, 13 skipped. The 4 failures are local environment problems in files this PR doesn't touch: two need `zstandard`, which the system Python here doesn't have (both pass with the bundled Python), one needs Tk, and one is a clang 23 warning in an existing test harness. The Linux build is clean. The Windows files were compiled and linked with llvm-mingw: they import only kernel32, advapi32 and ole32 at link time; `setupapi`, `winusb` and `hid` are loaded at run time. On Linux I also ran the device scan alone, with a Switch Pro Controller (`057E:2009`) plugged in over USB. It found no Switch 2 controller, opened nothing and started no thread. ## Not tested - **Everything on real hardware.** That includes the setup over WinUSB and usbfs, the input reports, the sensor axes and signs, and the rumble. - **Rumble strength.** The game's amplitude 1.0 is mapped to SDL's strongest rumble on this controller (29000 of 65535, field value 453). How the console scales amplitudes for the Switch 2 controller isn't known. The test haptic of ProCon2Tool (a browser tool), as captured by the Altureus project, uses far smaller values (fields 2 to 9), so it may feel stronger or weaker than on the console. - **Frequencies outside 20–357 Hz.** The 96-steps-per-octave scale is only measured in that range. - **Two controllers at once on Windows.** Only one is handled for now: matching each setup interface to its HID interface is left until someone has two. Linux handles up to four. - **Steam running with Steam Input on.** Steam keeps the controller; this should log that and leave it alone, but that hasn't been seen. - **The Joy-Con 2 and the GameCube controller.** They aren't handled; they still go through SDL as before. ## If you have a Switch 2 Pro Controller Please plug it in with a USB cable and run the game. Then send `hookshot_log.txt` (the `hid:` lines) and tell us: - Do all buttons and both sticks do the right thing? - Does motion aiming move the right way? - Does rumble feel right (strength and pitch) compared with the console? `HOS_MOTION_DEBUG=1` logs the sensor readings twice a second. On Linux, if the log says there is no access to the USB device, install Steam's `steam-devices` udev rules or add the rule the log gives.
DamnFine815 commented 2026-11-11 17:50:39 +00:00 (Migrated from github.com)

Switch2Connect already allows wireless Switch 2 controllers to be used on Windows through USB/IP, emulating a genuine USB-connected Switch 2 Pro Controller.

Is it possible that the game recognizes it?

[Switch2Connect](https://github.com/TommyWabg/Switch2Connect) already allows wireless Switch 2 controllers to be used on Windows through USB/IP, emulating a genuine USB-connected Switch 2 Pro Controller. Is it possible that the game recognizes it?
DamnFine815 commented 2026-11-11 21:13:15 +00:00 (Migrated from github.com)

Are you able to send me a testbuild zip for Windows?
I can test it for you right now. :)

Are you able to send me a testbuild zip for Windows? I can test it for you right now. :)
Commenting is not possible because the repository is archived.
No description provided.