Skip to content

Commit ed138c9

Browse files
feat(profiles): add Valve Steam Deck gamepad
Introduces a native Steam Deck controller profile (VID 0x28DE, PID 0x1205) across all supported backends: - New `GamepadProfileKind::steam_deck` enum value and `profiles::steam_deck()` factory - 64-byte vendor HID descriptor, native state packet packing (buttons, sticks, triggers, touch, motion) - `SteamDeckFeatureReportState` shared helper for feature-report initialization (serial query, desktop-mapping commands, rumble via `0xEB`) - Linux uhid backend: 4 ms periodic reports, SET/GET feature handling, serial seeding from stable_id - Windows backend: periodic state re-submission thread, feature-report responder, VHF queue dedup on button bytes - FreeBSD uses uinput path for Steam Deck (same as Xbox/Switch) - Four rear paddles (L4/R4/L5/R5) exposed via `supported_rear_paddle_count = 4`, Quick Access mapped to `misc1` - Protocol constant `LVH_WINDOWS_GAMEPAD_STEAM_DECK = 7`, broker validation updated - Full unit and consumer tests on Linux, Windows, and FreeBSD backends
1 parent 0cab073 commit ed138c9

36 files changed

Lines changed: 804 additions & 24 deletions

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@ behind backend implementations.
4343
## 🎮 Capabilities
4444

4545
- Gamepad profiles for generic HID, Xbox 360, Xbox One, Xbox Series,
46-
DualShock 4, DualSense, and Nintendo Switch Pro-style controllers.
47-
- Descriptor-driven PlayStation gamepads through Linux `uhid`; Generic, Xbox,
46+
DualShock 4, DualSense, Nintendo Switch Pro-style, and Steam Deck controllers.
47+
- Descriptor-driven PlayStation and Steam Deck gamepads through Linux `uhid`; Generic, Xbox,
4848
and Switch Pro gamepads plus keyboard, mouse, touchscreen, trackpad, and pen
4949
tablet devices through `uinput`.
5050
- Windows gamepads through a user-mode UMDF2 control driver backed by Virtual
@@ -149,6 +149,7 @@ Alternatives exist if `libvirtualhid` does not meet your needs.
149149
| DualShock 4 gamepad | ✅ | ✅ | ✅ | ❌ | ✅<sup><a href="#alternatives-note-4">4</a></sup> |
150150
| DualSense gamepad | ✅ | ❌ | ✅ | ✅ | ✅<sup><a href="#alternatives-note-4">4</a></sup> |
151151
| Nintendo Switch Pro-style gamepad | ✅ | ❌ | ✅ | ✅ | ✅<sup><a href="#alternatives-note-4">4</a></sup> |
152+
| Steam Deck gamepad | ✅ | ❌ | ❌ | ❌ | ❌ |
152153
| Rumble or output callbacks | ✅ | ❌ | ✅ | ✅ | ✅<sup><a href="#alternatives-note-4">4</a></sup> |
153154
| Data-driven profiles | ❌ | ❌ | ✅ | ❌ | ❌ |
154155
| Actively developed | ✅ | ❌ | ✅ | ✅ | ✅ |

docs/platform-support.md

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,11 @@ The VHF driver answers the calibration, pairing, and firmware feature reports
7575
used to initialize DualShock 4 and DualSense HIDAPI output. It also answers the
7676
Switch Pro USB and subcommand initialization sequence and accepts the native
7777
`0x30` input layout, so descriptor-aware consumers can initialize those
78-
controllers before sending their native output reports.
78+
controllers before sending their native output reports. The Steam Deck profile
79+
uses Valve's `0x28DE:0x1205` USB identity and native 64-byte state and feature
80+
reports. SDL/HIDAPI can therefore recognize it as a Steam Deck, disable its
81+
desktop mappings, and send native rumble without exposing Valve protocol details
82+
through the public C++ API.
7983

8084
See [Windows driver package](windows-driver.md) for build, install, validation,
8185
and signing details.
@@ -84,7 +88,7 @@ and signing details.
8488

8589
The Linux backend uses standard user-space kernel interfaces:
8690

87-
- `uhid` for descriptor-driven HID gamepads.
91+
- `uhid` for descriptor-driven HID gamepads, including Steam Deck.
8892
- `uinput` for Generic, Xbox 360, Xbox One, Xbox Series, and Switch Pro
8993
gamepads, plus keyboard, mouse, touchscreen, trackpad, and pen tablet
9094
devices.
@@ -131,13 +135,20 @@ buttons, Guide, L3, and R3 at their expected indices. D-pad directions are
131135
reported through the hat axes and exposed as logical buttons by standard
132136
gamepad consumers.
133137

134-
DualShock 4 and DualSense remain on `uhid` so their descriptors, motion,
138+
DualShock 4, DualSense, and Steam Deck remain on `uhid` so their descriptors, motion,
135139
touchpad, battery, feature reports, and profile-specific output reports stay
136140
available. The backend accepts PlayStation output through both UHID interrupt
137141
and control channels. Numbered control-channel output is normalized before
138142
parsing, whether the kernel includes the report number in the payload or
139143
provides it separately on the UHID event.
140144

145+
Steam Deck retains Valve's native USB identity and emits the 64-byte Deck state
146+
packet periodically so both the kernel `hid-steam` driver and SDL's direct
147+
HIDAPI path can initialize before the first client input arrives. The backend
148+
answers the unit-serial feature query used during Linux registration, accepts
149+
the desktop-mapping/settings commands used by SDL and `hid-steam`, and forwards
150+
native `0xEB` rumble requests through the portable output callback.
151+
141152
The backend opens `/dev/uhid` in nonblocking mode, matching the original
142153
asynchronous gamepad registration path. Its event reader is active before
143154
device registration begins, and creation does not report success until the
@@ -246,8 +257,8 @@ The FreeBSD backend uses the native evdev compatibility stack through
246257
FreeBSD path, and `/dev/uinput` for environments that provide the Linux-style
247258
alias. It supports the same uinput device categories as the Linux backend:
248259

249-
- Generic, Xbox 360, Xbox One, Xbox Series, DualShock 4, DualSense, and Switch
250-
Pro gamepads.
260+
- Generic, Xbox 360, Xbox One, Xbox Series, DualShock 4, DualSense, Switch Pro,
261+
and Steam Deck gamepads.
251262
- Keyboard and mouse devices, with X11/XTest available as a fallback.
252263
- Touchscreen, trackpad, and pen tablet devices.
253264

@@ -259,8 +270,8 @@ with the kernel HID bus. FreeBSD CUSE applications such as
259270
a `uhid(4)`-compatible character device for direct consumers, but that is a
260271
different integration surface and is not used by the current backend.
261272

262-
Generic, Xbox-family, Switch Pro, DualShock 4, and DualSense behavior therefore
263-
uses uinput. Ordinary buttons, sticks, analog triggers, and rumble are available,
273+
Generic, Xbox-family, Switch Pro, DualShock 4, DualSense, and Steam Deck behavior
274+
therefore uses uinput. Ordinary buttons, sticks, analog triggers, and rumble are available,
264275
but raw HID reports and descriptor-driven features are not.
265276

266277
For each created gamepad, `Gamepad::profile()` reports the effective FreeBSD

docs/streaming-host-integration.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,11 @@ The core API and adapter shape cover the major streaming-host requirements:
5151
- Rich controller metadata.
5252
- Gamepad output callbacks.
5353
- Keyboard and mouse input paths.
54-
- Linux PlayStation gamepads through `uhid`, Generic/Xbox/Switch Pro gamepads
55-
through `uinput`, and `uinput` keyboard/pointer devices.
54+
- Linux PlayStation and Steam Deck gamepads through `uhid`, Generic/Xbox/Switch
55+
Pro gamepads through `uinput`, and `uinput` keyboard/pointer devices.
5656
- Linux DualSense and DualShock 4 USB/Bluetooth report handling.
57+
- Native Steam Deck identity, input, feature-report initialization, touch/motion,
58+
rear-button, Quick Access, and rumble handling on Windows and Linux.
5759
- Linux touchscreen, trackpad, and pen tablet device types.
5860
- FreeBSD uinput gamepads and pointer devices, with basic PlayStation input and
5961
rumble but without Linux UHID-only PlayStation features.

docs/usage.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@ Built-in gamepad profiles and their platform-neutral default device names are:
212212
| DualShock 4 USB and Bluetooth | `(libvirtualhid) PS4 Controller` |
213213
| DualSense USB and Bluetooth | `(libvirtualhid) PS5 Controller` |
214214
| Nintendo Switch Pro | `(libvirtualhid) Nintendo Pro Controller` |
215+
| Steam Deck | `(libvirtualhid) Steam Deck Controller` |
215216
216217
Consumers may replace `DeviceProfile::name` before creating a gamepad, for
217218
example, to prepend an application name while preserving the default controller
@@ -226,6 +227,8 @@ Profiles advertise support for features such as rumble, trigger rumble, RGB
226227
LEDs, adaptive triggers, motion sensors, touchpads, battery state,
227228
profile-specific buttons, and raw output reports. Consumers should query profile and
228229
backend capabilities before warning users about unsupported client features.
229-
The `misc1` button represents Share/Capture/Mic Mute-style controls and is
230-
available on the generic, Xbox Series, DualSense, and Switch Pro profiles; Xbox
231-
360 and Xbox One do not advertise that extra button.
230+
The `misc1` button represents Share/Capture/Mic Mute/Quick Access-style controls
231+
and is available on the generic, Xbox Series, DualSense, Switch Pro, and Steam
232+
Deck profiles; Xbox 360 and Xbox One do not advertise that extra button. Steam
233+
Deck also exposes its two trackpads through the two portable touch contacts and
234+
maps its L4/R4/L5/R5 rear controls to the four paddle buttons.

docs/windows-driver.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,11 @@ gamepad.
381381
DualShock 4 and DualSense answer the calibration, pairing, and firmware feature
382382
requests used by their Windows HIDAPI initialization paths. Switch Pro answers
383383
the native USB and subcommand handshake and submits native `0x30` input reports.
384+
Steam Deck uses Valve's `VID_28DE&PID_1205` identity, responds to the unnumbered
385+
feature-report sequence used by SDL/HIDAPI, submits native 64-byte Deck state
386+
reports, and normalizes native `0xEB` rumble requests into the public callback.
387+
The driver queues a neutral Deck state before starting VHF so already-running
388+
consumers can receive the first packet within SDL's short endpoint-probe window.
384389
The built-in Generic profile is presented to Windows as a DirectInput PID
385390
Joystick with the complete output-report set required for DirectInput
386391
enumeration. Constant Force and Sine output is normalized to the portable

examples/gamepad_adapter.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ namespace {
3939
if (name == "switch") {
4040
return lvh::profiles::switch_pro();
4141
}
42+
if (name == "steamdeck") {
43+
return lvh::profiles::steam_deck();
44+
}
4245

4346
return std::nullopt;
4447
}
@@ -58,6 +61,7 @@ namespace {
5861
case switch_pro:
5962
return nintendo;
6063
case generic:
64+
case steam_deck:
6165
return unknown;
6266
}
6367

src/core/gamepad_adapter.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ namespace lvh {
4747
case xbox_series:
4848
case dualsense:
4949
case switch_pro:
50+
case steam_deck:
5051
return true;
5152
case xbox_360:
5253
case xbox_one:
@@ -88,6 +89,9 @@ namespace lvh {
8889
support.supports_battery = profile.capabilities.supports_battery;
8990
support.supports_misc1_button = supports_common_misc1_button(profile.gamepad_kind);
9091
support.supports_touchpad_button = profile.capabilities.supports_touchpad;
92+
if (profile.gamepad_kind == GamepadProfileKind::steam_deck) {
93+
support.supported_rear_paddle_count = 4U;
94+
}
9195

9296
return support;
9397
}

src/core/profiles.cpp

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ namespace lvh::profiles {
4141

4242
constexpr std::size_t switch_pro_output_report_size = 64;
4343

44+
constexpr std::size_t steam_deck_input_report_size = 64;
45+
46+
constexpr std::size_t steam_deck_feature_report_size = 64;
47+
4448
constexpr std::size_t dualshock4_usb_input_report_size = 64;
4549

4650
constexpr std::size_t dualshock4_usb_output_report_size = 32;
@@ -269,6 +273,40 @@ namespace lvh::profiles {
269273
return bytes_from_hex(descriptor);
270274
}
271275

276+
std::vector<std::uint8_t> make_steam_deck_report_descriptor() {
277+
// The native Steam Deck controller endpoint carries an unnumbered
278+
// 64-byte vendor input report and an unnumbered 64-byte feature report.
279+
return {
280+
0x06,
281+
0x00,
282+
0xFF, // Usage Page (Vendor Defined 0xFF00)
283+
0x09,
284+
0x01, // Usage (Vendor Usage 1)
285+
0xA1,
286+
0x01, // Collection (Application)
287+
0x15,
288+
0x00, // Logical Minimum (0)
289+
0x26,
290+
0xFF,
291+
0x00, // Logical Maximum (255)
292+
0x75,
293+
0x08, // Report Size (8)
294+
0x95,
295+
0x40, // Report Count (64)
296+
0x09,
297+
0x01, // Usage (Vendor Usage 1)
298+
0x81,
299+
0x02, // Input (Data,Var,Abs)
300+
0x09,
301+
0x02, // Usage (Vendor Usage 2)
302+
0x95,
303+
0x40, // Report Count (64)
304+
0xB1,
305+
0x02, // Feature (Data,Var,Abs)
306+
0xC0, // End Collection
307+
};
308+
}
309+
272310
std::vector<std::uint8_t> make_gamepad_report_descriptor(std::uint8_t report_id, bool supports_rumble) {
273311
std::vector<std::uint8_t> descriptor {
274312
0x05,
@@ -2036,6 +2074,28 @@ namespace lvh::profiles {
20362074
return profile;
20372075
}
20382076

2077+
DeviceProfile make_steam_deck_profile() {
2078+
DeviceProfile profile;
2079+
profile.device_type = DeviceType::gamepad;
2080+
profile.gamepad_kind = GamepadProfileKind::steam_deck;
2081+
profile.bus_type = BusType::usb;
2082+
profile.vendor_id = 0x28DE;
2083+
profile.product_id = 0x1205;
2084+
profile.version = 0x0100;
2085+
profile.report_id = 0;
2086+
profile.input_report_size = steam_deck_input_report_size;
2087+
profile.output_report_size = steam_deck_feature_report_size;
2088+
profile.name = "(libvirtualhid) Steam Deck Controller";
2089+
profile.manufacturer = "Valve Software";
2090+
profile.capabilities = {
2091+
.supports_rumble = true,
2092+
.supports_motion = true,
2093+
.supports_touchpad = true,
2094+
};
2095+
profile.report_descriptor = make_steam_deck_report_descriptor();
2096+
return profile;
2097+
}
2098+
20392099
DeviceProfile make_simple_profile(DeviceType device_type, std::string name, std::uint16_t product_id) {
20402100
DeviceProfile profile;
20412101
profile.device_type = device_type;
@@ -2124,6 +2184,10 @@ namespace lvh::profiles {
21242184
return make_switch_pro_profile();
21252185
}
21262186

2187+
DeviceProfile steam_deck() {
2188+
return make_steam_deck_profile();
2189+
}
2190+
21272191
DeviceProfile keyboard() {
21282192
return make_simple_profile(DeviceType::keyboard, "libvirtualhid Keyboard", 0x0002);
21292193
}
@@ -2160,6 +2224,8 @@ namespace lvh::profiles {
21602224
return dualsense();
21612225
case GamepadProfileKind::switch_pro:
21622226
return switch_pro();
2227+
case GamepadProfileKind::steam_deck:
2228+
return steam_deck();
21632229
}
21642230

21652231
return std::nullopt;
@@ -2174,6 +2240,7 @@ namespace lvh::profiles {
21742240
dualshock4(),
21752241
dualsense(),
21762242
switch_pro(),
2243+
steam_deck(),
21772244
};
21782245
}
21792246

0 commit comments

Comments
 (0)