Skip to content

Commit 6f19b7c

Browse files
Add native Switch Pro IMU and player LED support
Route Linux Switch Pro controllers through descriptor-driven UHID, share the native handshake/reply protocol with Windows, and pack live IMU samples into Switch input reports. The API, control UI, tests, and docs now expose Switch Pro player LED output support and document the updated Linux backend behavior.
1 parent 61c0c19 commit 6f19b7c

26 files changed

Lines changed: 625 additions & 186 deletions

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,14 +45,14 @@ behind backend implementations.
4545

4646
- Gamepad profiles for generic HID, Xbox 360, Xbox One, Xbox Series,
4747
DualShock 4, DualSense, and Nintendo Switch Pro-style controllers.
48-
- Descriptor-driven PlayStation gamepads through Linux `uhid`; Generic, Xbox,
49-
and Switch Pro gamepads plus keyboard, mouse, touchscreen, trackpad, and pen
50-
tablet devices through `uinput`.
48+
- Descriptor-driven PlayStation and Switch Pro gamepads through Linux `uhid`;
49+
Generic and Xbox gamepads plus keyboard, mouse, touchscreen, trackpad, and
50+
pen tablet devices through `uinput`.
5151
- Windows gamepads, keyboards, and Raw Input-visible mice through a user-mode
5252
UMDF2 control driver backed by Virtual HID Framework, with Win32 keyboard and
5353
mouse fallbacks when the licensed driver path is unavailable.
54-
- Output callbacks for profile-specific feedback such as rumble, LEDs,
55-
adaptive triggers, and raw HID output reports when available.
54+
- Output callbacks for profile-specific feedback such as rumble, RGB and player
55+
LEDs, adaptive triggers, and raw HID output reports when available.
5656
- An optional `virtualhid_control` native UI tool for creating, removing,
5757
controlling, and inspecting test gamepads and mice through the public C++ API.
5858
- CMake consumption through installed packages, vendored source,

docs/end-user-gamepad-guide.md

Lines changed: 11 additions & 5 deletions
Large diffs are not rendered by default.

docs/platform-support.md

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -101,26 +101,23 @@ and signing details.
101101

102102
The Linux backend uses standard user-space kernel interfaces:
103103

104-
- `uhid` for descriptor-driven HID gamepads.
105-
- `uinput` for Generic, Xbox 360, Xbox One, Xbox Series, and Switch Pro
106-
gamepads, plus keyboard, mouse, touchscreen, trackpad, and pen tablet
107-
devices.
104+
- `uhid` for descriptor-driven PlayStation and Switch Pro gamepads.
105+
- `uinput` for Generic, Xbox 360, Xbox One, and Xbox Series gamepads, plus
106+
keyboard, mouse, touchscreen, trackpad, and pen tablet devices.
108107
- `libevdev` internally for uinput device construction.
109108
- X11/XTest only as a keyboard and mouse fallback when `uinput` cannot be used
110109
and an X11 session is available.
111110

112111
Gamepad support normally prefers `uhid` because descriptors, raw HID identity,
113112
feature reports, and output reports matter for controller compatibility.
114-
Generic, Xbox-family, and Switch Pro profiles instead use `uinput` so SDL,
113+
Generic and Xbox-family profiles instead use `uinput` so SDL,
115114
Steam, browser Gamepad API implementations, and other evdev consumers receive
116115
canonical Linux gamepad events. Face buttons, shoulders, menu buttons, stick
117116
clicks, and Guide use their native evdev codes; sticks use absolute axes. Every
118117
uinput gamepad exposes its directional pad through `ABS_HAT0X` and `ABS_HAT0Y`.
119118
Generic and Xbox triggers remain independent analog `ABS_Z` and `ABS_RZ` axes.
120-
Switch Pro uses the Nintendo face-button
121-
positions, button events for ZL/ZR, and `BTN_Z` for Capture. Profiles with rumble
122-
support normalize rumble, constant, periodic, and ramp uinput force-feedback
123-
effects back into the public callback. Each requested playback repetition
119+
Profiles with rumble support normalize rumble, constant, periodic, and ramp
120+
uinput force-feedback effects back into the public callback. Each requested playback repetition
124121
restarts the effect's ramp and envelope timing. A zero-length effect remains
125122
active until its explicit stop event, matching the infinite-effect contract used
126123
by SDL and Steam. The Linux backend lets a new uinput device settle before
@@ -182,10 +179,14 @@ descriptor, and report framing remain unchanged. This transport-only name is
182179
confined to the Linux backend; public profile names, Windows names, and VHF
183180
behavior are unchanged.
184181

185-
Switch Pro keeps its Nintendo identity on the Linux uinput path. This follows
186-
the evdev layout used by Linux-native virtual-controller implementations and
187-
allows standard `FF_RUMBLE` effects without emulating the physical controller's
188-
proprietary initialization handshake.
182+
Switch Pro uses Linux `uhid` with its native Nintendo descriptor and identity.
183+
It advertises `BUS_VIRTUAL` so `hid-generic` and HIDAPI consumers can use the
184+
descriptor without the kernel claiming the virtual endpoint as physical
185+
Nintendo hardware. The backend answers Nintendo USB and subcommand
186+
initialization reports, and native `0x30` input reports carry buttons, sticks,
187+
battery state, and three live IMU samples. The public acceleration and
188+
gyroscope units remain meters per second squared and degrees per second; the
189+
packer converts them to Nintendo's coordinate system and sensor scales.
189190

190191
Linux touchscreen and trackpad contacts use the lowest available multitouch
191192
slot while they are active. A newly placed contact receives a new tracking ID,
@@ -194,7 +195,9 @@ finger cannot overwrite another active finger in standard evdev consumers.
194195

195196
On descriptor-driven backends, native Switch Pro output reports `0x01` and
196197
`0x10` are decoded into the normalized low- and high-frequency rumble callback.
197-
The original native report remains available in `GamepadOutput::raw_report`.
198+
Set Player Lights subcommand `0x30` additionally produces a `player_leds`
199+
callback with separate solid and flashing states for the four indicators. The
200+
original native report remains available in `GamepadOutput::raw_report`.
198201

199202
The optional `virtualhid_control` diagnostic UI uses SDL3 and Dear ImGui through
200203
the repository CPM lockfile. It is intended to stay on the same UI framework for

docs/streaming-host-integration.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@ A streaming host should be able to:
1313
indexes.
1414
- Submit incremental button, axis, trigger, touchpad, motion, and battery
1515
updates without recreating a device.
16-
- Receive output callbacks for rumble, LEDs, adaptive triggers, trigger rumble,
17-
and raw output reports where the selected profile supports them.
16+
- Receive output callbacks for rumble, RGB and player LEDs, adaptive triggers,
17+
trigger rumble, and raw output reports where the selected profile supports
18+
them.
1819
- Query profile and backend capabilities before warning users about unsupported
1920
client features.
2021
- Read device nodes and platform paths when a downstream consumer or diagnostic
@@ -51,8 +52,10 @@ The core API and adapter shape cover the major streaming-host requirements:
5152
- Rich controller metadata.
5253
- Gamepad output callbacks.
5354
- Keyboard and mouse input paths.
54-
- Linux PlayStation gamepads through `uhid`, Generic/Xbox/Switch Pro gamepads
55-
through `uinput`, and `uinput` keyboard/pointer devices.
55+
- Linux PlayStation and Switch Pro gamepads through `uhid`, Generic/Xbox
56+
gamepads through `uinput`, and `uinput` keyboard/pointer devices.
57+
- Native Switch Pro motion, initialization replies, rumble, and player-light
58+
output handling on Linux and Windows descriptor-driven backends.
5659
- Linux DualSense and DualShock 4 USB/Bluetooth report handling.
5760
- Linux touchscreen, trackpad, and pen tablet device types.
5861
- FreeBSD uinput gamepads and pointer devices, with basic PlayStation input and

docs/usage.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,8 @@ default when a static target is available.
105105
The UI can create and remove gamepads from the built-in profiles, submit
106106
buttons, sticks, triggers, and battery state, show backend and profile
107107
capabilities, list device nodes reported for UI-created devices, and display
108-
normalized gamepad output such as rumble, RGB LED, adaptive trigger, trigger
109-
rumble, and raw report events delivered through the normal callback path. Button
108+
normalized gamepad output such as rumble, RGB LED, player LED, adaptive trigger,
109+
trigger rumble, and raw report events delivered through the normal callback path. Button
110110
controls are momentary by default, so they behave like physical gamepad buttons;
111111
on Windows, the UI also displays broker license status and can activate,
112112
refresh, or deactivate a machine license without elevation. Windows UMDF
@@ -229,10 +229,11 @@ through 16 in the input report. Linux may still route that profile through
229229
`uinput`, where the backend exposes those same logical directions through the
230230
standard `ABS_HAT0X` and `ABS_HAT0Y` axes.
231231
232-
Profiles advertise support for features such as rumble, trigger rumble, RGB
233-
LEDs, adaptive triggers, motion sensors, touchpads, battery state,
234-
profile-specific buttons, and raw output reports. Consumers should query profile and
235-
backend capabilities before warning users about unsupported client features.
232+
Profiles advertise support for features such as rumble, trigger rumble, RGB and
233+
player LEDs, adaptive triggers, motion sensors, touchpads, battery state,
234+
profile-specific buttons, and raw output reports. Consumers should query
235+
profile and backend capabilities before warning users about unsupported client
236+
features.
236237
The `misc1` button represents Share/Capture/Mic Mute-style controls and is
237238
available on the generic, Xbox Series, DualSense, and Switch Pro profiles; Xbox
238239
360 and Xbox One do not advertise that extra button.

docs/windows-driver.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -427,7 +427,9 @@ gamepad.
427427

428428
DualShock 4 and DualSense answer the calibration, pairing, and firmware feature
429429
requests used by their Windows HIDAPI initialization paths. Switch Pro answers
430-
the native USB and subcommand handshake and submits native `0x30` input reports.
430+
the native USB and subcommand handshake and submits native `0x30` input reports
431+
with three live IMU samples. Its Set Player Lights subcommand is normalized into
432+
solid and flashing player-indicator output states for the creating runtime.
431433
The built-in Generic profile is presented to Windows as a DirectInput PID
432434
Joystick with the complete output-report set required for DirectInput
433435
enumeration. Constant Force and Sine output is normalized to the portable

src/core/gamepad_adapter.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ namespace lvh {
8383
support.supports_rumble = profile.capabilities.supports_rumble;
8484
support.supports_rgb_led = profile.capabilities.supports_rgb_led;
8585
support.supports_adaptive_triggers = profile.capabilities.supports_adaptive_triggers;
86+
support.supports_player_leds = profile.capabilities.supports_player_leds;
8687
support.supports_motion = profile.capabilities.supports_motion;
8788
support.supports_touchpad = profile.capabilities.supports_touchpad;
8889
support.supports_battery = profile.capabilities.supports_battery;
@@ -142,6 +143,8 @@ namespace lvh {
142143
return support.supports_rgb_led;
143144
case adaptive_triggers:
144145
return support.supports_adaptive_triggers;
146+
case player_leds:
147+
return support.supports_player_leds;
145148
case raw_report:
146149
return profile.output_report_size > 0U;
147150
}

src/core/profiles.cpp

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2031,7 +2031,12 @@ namespace lvh::profiles {
20312031
profile.output_report_size = switch_pro_output_report_size;
20322032
profile.name = "(libvirtualhid) Nintendo Pro Controller";
20332033
profile.manufacturer = "Nintendo Co., Ltd.";
2034-
profile.capabilities = {.supports_rumble = true, .supports_motion = true, .supports_battery = true};
2034+
profile.capabilities = {
2035+
.supports_rumble = true,
2036+
.supports_motion = true,
2037+
.supports_battery = true,
2038+
.supports_player_leds = true,
2039+
};
20352040
profile.report_descriptor = make_switch_pro_report_descriptor();
20362041
return profile;
20372042
}

src/core/report.cpp

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,12 @@ namespace lvh::reports {
7575

7676
constexpr std::size_t switch_rumble_output_report_size = 10;
7777

78+
constexpr std::uint8_t switch_set_player_lights_subcommand = 0x30;
79+
80+
constexpr float switch_acceleration_scale = 4096.0F / 9.80665F;
81+
82+
constexpr float switch_gyroscope_scale = 14.2842F;
83+
7884
// SDL maps 16-bit rumble strengths to Nintendo's shared 101-step amplitude
7985
// scale. This is the inverse table for the packed high- and low-band values:
8086
// https://github.com/libsdl-org/SDL/blob/main/src/joystick/hidapi/SDL_hidapi_switch.c
@@ -1061,6 +1067,22 @@ namespace lvh::reports {
10611067
report[offset + 2U] = to_byte((y >> 4U) & 0xFFU);
10621068
}
10631069

1070+
void write_switch_imu_sample(
1071+
ByteReport &report,
1072+
std::size_t offset,
1073+
const Vector3 &acceleration,
1074+
const Vector3 &gyroscope
1075+
) {
1076+
// Nintendo's native coordinate system differs from the portable
1077+
// PlayStation-style coordinate system exposed by GamepadState.
1078+
write_i16(report, offset, scale_i16(-acceleration.z, switch_acceleration_scale));
1079+
write_i16(report, offset + 2U, scale_i16(-acceleration.x, switch_acceleration_scale));
1080+
write_i16(report, offset + 4U, scale_i16(acceleration.y, switch_acceleration_scale));
1081+
write_i16(report, offset + 6U, scale_i16(-gyroscope.z, switch_gyroscope_scale));
1082+
write_i16(report, offset + 8U, scale_i16(-gyroscope.x, switch_gyroscope_scale));
1083+
write_i16(report, offset + 10U, scale_i16(gyroscope.y, switch_gyroscope_scale));
1084+
}
1085+
10641086
std::byte switch_battery_and_connection(const std::optional<GamepadBattery> &battery) {
10651087
constexpr auto usb_connection = std::byte {0x01};
10661088
if (!battery.has_value()) {
@@ -1222,6 +1244,11 @@ namespace lvh::reports {
12221244
normalize_switch_stick_axis(normalized.right_stick.x),
12231245
normalize_switch_stick_axis(normalized.right_stick.y)
12241246
);
1247+
const auto acceleration = normalized.acceleration.value_or(Vector3 {.y = 9.80665F});
1248+
const auto gyroscope = normalized.gyroscope.value_or(Vector3 {});
1249+
for (const auto offset : {13U, 25U, 37U}) {
1250+
write_switch_imu_sample(report, offset, acceleration, gyroscope);
1251+
}
12251252
return to_uint8_report(report);
12261253
}
12271254

@@ -1309,6 +1336,23 @@ namespace lvh::reports {
13091336
output.high_frequency_rumble = rumble->high_frequency;
13101337
output.raw_report = report;
13111338
outputs.push_back(std::move(output));
1339+
}
1340+
if (
1341+
report.size() >= 12U && report[0] == switch_rumble_and_subcommand_output_report_id &&
1342+
report[10] == switch_set_player_lights_subcommand
1343+
) {
1344+
GamepadOutput output;
1345+
output.kind = GamepadOutputKind::player_leds;
1346+
const auto player_lights = std::byte {report[11]};
1347+
for (std::size_t index = 0; index < output.player_leds.size(); ++index) {
1348+
output.player_leds[index] = (player_lights & (std::byte {1} << index)) != zero_byte;
1349+
output.flashing_player_leds[index] =
1350+
(player_lights & (std::byte {1} << (index + 4U))) != zero_byte;
1351+
}
1352+
output.raw_report = report;
1353+
outputs.push_back(std::move(output));
1354+
}
1355+
if (!outputs.empty()) {
13121356
return outputs;
13131357
}
13141358
}

src/include/libvirtualhid/gamepad_adapter.hpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,11 @@ namespace lvh {
6969
* @brief Number of rear paddle buttons exposed by the profile.
7070
*/
7171
std::uint8_t supported_rear_paddle_count = 0;
72+
73+
/**
74+
* @brief Whether the profile supports player indicator LED output.
75+
*/
76+
bool supports_player_leds = false;
7277
};
7378

7479
/**

0 commit comments

Comments
 (0)