Skip to content

Commit 8bef278

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 8bef278

28 files changed

Lines changed: 833 additions & 189 deletions

.github/workflows/ci-build.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ jobs:
184184
SUBSYSTEM=="hidraw", KERNEL=="hidraw*", ATTRS{phys}=="libvirtualhid/uhid/*", MODE="0666", TAG+="uaccess"
185185
SUBSYSTEM=="input", KERNEL=="event*", ATTRS{phys}=="libvirtualhid/uhid/*", MODE="0666", TAG+="uaccess"
186186
SUBSYSTEM=="hidraw", KERNEL=="hidraw*", ATTRS{name}=="(libvirtualhid)*", MODE="0666", TAG+="uaccess"
187+
SUBSYSTEM=="hidraw", KERNEL=="hidraw*", ATTRS{name}=="libvirtualhid*", MODE="0666", TAG+="uaccess"
187188
SUBSYSTEM=="input", KERNEL=="event*", ATTRS{name}=="(libvirtualhid)*", MODE="0666", TAG+="uaccess"
188189
SUBSYSTEM=="input", KERNEL=="event*", ATTRS{name}=="libvirtualhid*", MODE="0666", TAG+="uaccess"
189190
EOF

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: 20 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,15 @@ 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+
Its backend-only UHID identity advertises Bluetooth transport because SDL2's
184+
Linux HIDAPI rejects virtual `BUS_USB` HIDRAW devices without a physical USB
185+
parent in sysfs. The public profile and report framing remain unchanged. The
186+
backend answers Nintendo subcommand initialization reports, and native `0x30`
187+
input reports carry buttons, sticks, battery state, and three live IMU samples.
188+
The public acceleration and gyroscope units remain meters per second squared
189+
and degrees per second; the packer converts them to Nintendo's coordinate
190+
system and sensor scales.
189191

190192
Linux touchscreen and trackpad contacts use the lowest available multitouch
191193
slot while they are active. A newly placed contact receives a new tracking ID,
@@ -194,7 +196,11 @@ finger cannot overwrite another active finger in standard evdev consumers.
194196

195197
On descriptor-driven backends, native Switch Pro output reports `0x01` and
196198
`0x10` are decoded into the normalized low- and high-frequency rumble callback.
197-
The original native report remains available in `GamepadOutput::raw_report`.
199+
Set Player Lights subcommand `0x30` additionally produces a `player_leds`
200+
callback with separate solid and flashing states for the four indicators. The
201+
Set HOME Light subcommand `0x38` produces a grayscale `rgb_led` callback whose
202+
equal channels preserve the requested monochrome intensity. The original native
203+
report remains available in `GamepadOutput::raw_report`.
198204

199205
The optional `virtualhid_control` diagnostic UI uses SDL3 and Dear ImGui through
200206
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, HOME-light, and
58+
player-light 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: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -427,7 +427,11 @@ 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, and
433+
its monochrome HOME light is normalized as equal RGB channels so existing
434+
streaming LED feedback paths can preserve its intensity.
431435
The built-in Generic profile is presented to Windows as a DirectInput PID
432436
Joystick with the complete output-report set required for DirectInput
433437
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: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2031,7 +2031,13 @@ 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_rgb_led = true,
2038+
.supports_battery = true,
2039+
.supports_player_leds = true,
2040+
};
20352041
profile.report_descriptor = make_switch_pro_report_descriptor();
20362042
return profile;
20372043
}

src/core/report.cpp

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,30 @@ 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 std::uint8_t switch_set_home_light_subcommand = 0x38;
81+
82+
constexpr float switch_acceleration_scale = 4096.0F / 9.80665F;
83+
84+
constexpr float switch_gyroscope_scale = 14.2842F;
85+
86+
std::uint8_t decode_switch_home_light_intensity(std::uint8_t encoded_intensity) {
87+
const auto intensity = static_cast<std::uint8_t>(encoded_intensity >> 4U);
88+
if (intensity == 0U) {
89+
return 0U;
90+
}
91+
if (intensity <= 6U) {
92+
return static_cast<std::uint8_t>(std::lround(static_cast<float>(intensity) * 25.5F));
93+
}
94+
if (intensity == 15U) {
95+
return 255U;
96+
}
97+
98+
const auto normalized = (static_cast<float>(intensity) - 0.5F) / 15.0F;
99+
return static_cast<std::uint8_t>(std::lround(std::pow(normalized, 1.0F / 2.13F) * 255.0F));
100+
}
101+
78102
// SDL maps 16-bit rumble strengths to Nintendo's shared 101-step amplitude
79103
// scale. This is the inverse table for the packed high- and low-band values:
80104
// https://github.com/libsdl-org/SDL/blob/main/src/joystick/hidapi/SDL_hidapi_switch.c
@@ -1061,6 +1085,22 @@ namespace lvh::reports {
10611085
report[offset + 2U] = to_byte((y >> 4U) & 0xFFU);
10621086
}
10631087

1088+
void write_switch_imu_sample(
1089+
ByteReport &report,
1090+
std::size_t offset,
1091+
const Vector3 &acceleration,
1092+
const Vector3 &gyroscope
1093+
) {
1094+
// Nintendo's native coordinate system differs from the portable
1095+
// PlayStation-style coordinate system exposed by GamepadState.
1096+
write_i16(report, offset, scale_i16(-acceleration.z, switch_acceleration_scale));
1097+
write_i16(report, offset + 2U, scale_i16(-acceleration.x, switch_acceleration_scale));
1098+
write_i16(report, offset + 4U, scale_i16(acceleration.y, switch_acceleration_scale));
1099+
write_i16(report, offset + 6U, scale_i16(-gyroscope.z, switch_gyroscope_scale));
1100+
write_i16(report, offset + 8U, scale_i16(-gyroscope.x, switch_gyroscope_scale));
1101+
write_i16(report, offset + 10U, scale_i16(gyroscope.y, switch_gyroscope_scale));
1102+
}
1103+
10641104
std::byte switch_battery_and_connection(const std::optional<GamepadBattery> &battery) {
10651105
constexpr auto usb_connection = std::byte {0x01};
10661106
if (!battery.has_value()) {
@@ -1222,6 +1262,11 @@ namespace lvh::reports {
12221262
normalize_switch_stick_axis(normalized.right_stick.x),
12231263
normalize_switch_stick_axis(normalized.right_stick.y)
12241264
);
1265+
const auto acceleration = normalized.acceleration.value_or(Vector3 {.y = 9.80665F});
1266+
const auto gyroscope = normalized.gyroscope.value_or(Vector3 {});
1267+
for (const auto offset : {13U, 25U, 37U}) {
1268+
write_switch_imu_sample(report, offset, acceleration, gyroscope);
1269+
}
12251270
return to_uint8_report(report);
12261271
}
12271272

@@ -1309,6 +1354,35 @@ namespace lvh::reports {
13091354
output.high_frequency_rumble = rumble->high_frequency;
13101355
output.raw_report = report;
13111356
outputs.push_back(std::move(output));
1357+
}
1358+
if (
1359+
report.size() >= 12U && report[0] == switch_rumble_and_subcommand_output_report_id &&
1360+
report[10] == switch_set_player_lights_subcommand
1361+
) {
1362+
GamepadOutput output;
1363+
output.kind = GamepadOutputKind::player_leds;
1364+
const auto player_lights = std::byte {report[11]};
1365+
for (std::size_t index = 0; index < output.player_leds.size(); ++index) {
1366+
output.player_leds[index] = (player_lights & (std::byte {1} << index)) != zero_byte;
1367+
output.flashing_player_leds[index] =
1368+
(player_lights & (std::byte {1} << (index + 4U))) != zero_byte;
1369+
}
1370+
output.raw_report = report;
1371+
outputs.push_back(std::move(output));
1372+
}
1373+
if (
1374+
report.size() >= 15U && report[0] == switch_rumble_and_subcommand_output_report_id &&
1375+
report[10] == switch_set_home_light_subcommand
1376+
) {
1377+
GamepadOutput output;
1378+
output.kind = GamepadOutputKind::rgb_led;
1379+
output.red = decode_switch_home_light_intensity(report[12]);
1380+
output.green = output.red;
1381+
output.blue = output.red;
1382+
output.raw_report = report;
1383+
outputs.push_back(std::move(output));
1384+
}
1385+
if (!outputs.empty()) {
13121386
return outputs;
13131387
}
13141388
}

0 commit comments

Comments
 (0)