Skip to content

Commit f1138b3

Browse files
Remap DS4 USB to Bluetooth for UHID
A virtual UHID device has no physical USB ancestor, so steam-devices cannot match its USB-only DualShock 4 hidraw rule. When a USB DS4 profile is requested on Linux, the backend now transparently substitutes the Bluetooth descriptor and report framing so steam-devices' Bluetooth rule matches instead. The public profile name, vendor/product IDs, and all non-Linux behavior are preserved. Adds a test hook and unit tests to verify the effective profile selection.
1 parent def6ff0 commit f1138b3

5 files changed

Lines changed: 74 additions & 7 deletions

File tree

docs/platform-support.md

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -139,11 +139,14 @@ parsing, whether the kernel includes the report number in the payload or
139139
provides it separately on the UHID event.
140140

141141
On Linux, the UHID transport emits Sony's native `Wireless Controller` product
142-
name for DualShock 4 and DualSense devices. This keeps Steam's PlayStation HID
143-
detection compatible even when a streaming host customizes the public profile
144-
name. The requested USB or Bluetooth descriptor and report framing are left
145-
unchanged. This identity adjustment is confined to the Linux backend; Windows
146-
profile names and VHF behavior are unchanged.
142+
name for DualShock 4 and DualSense devices. The default USB DualShock 4 profile
143+
is instantiated with its Bluetooth descriptor and report framing because a
144+
virtual UHID device has no physical USB ancestor for the USB-only DualShock 4
145+
rule shipped by `steam-devices`; its Bluetooth rule matches the virtual HID
146+
ancestor directly. `Gamepad::profile()` reports this effective transport.
147+
DualSense keeps its requested USB or Bluetooth framing. These adjustments are
148+
confined to the Linux backend; Windows profile names and VHF behavior are
149+
unchanged.
147150

148151
Switch Pro keeps its Nintendo identity on the Linux uinput path. This follows
149152
the evdev layout used by Linux-native virtual-controller implementations and

src/platform/linux/uhid_backend.cpp

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,28 @@ namespace lvh::detail {
313313
}
314314
return profile.name;
315315
}
316+
317+
std::optional<DeviceProfile> effective_uhid_gamepad_profile(const DeviceProfile &requested_profile) {
318+
if (
319+
requested_profile.gamepad_kind != GamepadProfileKind::dualshock4 ||
320+
requested_profile.bus_type == BusType::bluetooth
321+
) {
322+
return std::nullopt;
323+
}
324+
325+
// A UHID device has no physical USB ancestor, so steam-devices cannot
326+
// apply its USB-only DS4 hidraw rule. Its Bluetooth rule matches the
327+
// virtual HID ancestor directly. Keep the public DS4 profile neutral and
328+
// change only the Linux transport descriptor and report framing.
329+
const auto transport_profile = profiles::dualshock4_bluetooth();
330+
auto effective_profile = requested_profile;
331+
effective_profile.bus_type = transport_profile.bus_type;
332+
effective_profile.report_id = transport_profile.report_id;
333+
effective_profile.input_report_size = transport_profile.input_report_size;
334+
effective_profile.output_report_size = transport_profile.output_report_size;
335+
effective_profile.report_descriptor = transport_profile.report_descriptor;
336+
return effective_profile;
337+
}
316338
#endif
317339

318340
std::uint16_t to_uinput_bus(BusType bus_type) {
@@ -3200,13 +3222,19 @@ namespace lvh::detail {
32003222
return {system_error_status(ErrorCode::backend_unavailable, "failed to open /dev/uhid", errno), nullptr};
32013223
}
32023224

3225+
auto effective_options = options;
3226+
auto effective_profile = effective_uhid_gamepad_profile(options.profile);
3227+
if (effective_profile.has_value()) {
3228+
effective_options.profile = *effective_profile;
3229+
}
3230+
32033231
auto gamepad = std::make_unique<UhidGamepad>(fd);
3204-
if (const auto status = gamepad->create(id, options); !status.ok()) {
3232+
if (const auto status = gamepad->create(id, effective_options); !status.ok()) {
32053233
static_cast<void>(gamepad->close());
32063234
return {status, nullptr};
32073235
}
32083236

3209-
return {OperationStatus::success(), std::move(gamepad)};
3237+
return {OperationStatus::success(), std::move(gamepad), std::move(effective_profile)};
32103238
#else
32113239
return {
32123240
OperationStatus::failure(ErrorCode::unsupported_profile, "gamepad profile requires Linux UHID"),

tests/fixtures/include/fixtures/linux_backend_test_hooks.hpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,14 @@ namespace lvh::detail::test {
427427
*/
428428
std::uint16_t linux_gamepad_uhid_bus(GamepadProfileKind kind);
429429

430+
/**
431+
* @brief Select the effective Linux UHID profile for a requested gamepad profile.
432+
*
433+
* @param profile Requested gamepad profile.
434+
* @return Backend-adjusted gamepad profile.
435+
*/
436+
DeviceProfile linux_effective_uhid_gamepad_profile(const DeviceProfile &profile);
437+
430438
/**
431439
* @brief Translate a bus type to a Linux uinput bus code.
432440
*

tests/fixtures/linux_backend_test_hooks.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -878,6 +878,10 @@ namespace lvh::detail::test {
878878
return profile ? to_uhid_bus(*profile) : to_uhid_bus(BusType::unknown);
879879
}
880880

881+
DeviceProfile linux_effective_uhid_gamepad_profile(const DeviceProfile &profile) {
882+
return effective_uhid_gamepad_profile(profile).value_or(profile);
883+
}
884+
881885
std::uint16_t linux_uinput_bus(BusType bus_type) {
882886
return to_uinput_bus(bus_type);
883887
}

tests/unit/test_linux_backend.cpp

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,30 @@ TEST_F(LinuxBackendTest, TranslatesMouseButtonsAndBusTypes) {
125125
EXPECT_EQ(lvh::detail::test::linux_gamepad_uhid_bus(lvh::GamepadProfileKind::switch_pro), BUS_VIRTUAL);
126126
EXPECT_EQ(lvh::detail::test::linux_uinput_bus(lvh::BusType::bluetooth), BUS_BLUETOOTH);
127127

128+
auto dualshock4 = lvh::profiles::dualshock4_usb();
129+
dualshock4.name = "Streaming host DS4";
130+
const auto effective_dualshock4 = lvh::detail::test::linux_effective_uhid_gamepad_profile(dualshock4);
131+
const auto dualshock4_bluetooth = lvh::profiles::dualshock4_bluetooth();
132+
EXPECT_EQ(effective_dualshock4.bus_type, lvh::BusType::bluetooth);
133+
EXPECT_EQ(effective_dualshock4.report_id, dualshock4_bluetooth.report_id);
134+
EXPECT_EQ(effective_dualshock4.input_report_size, dualshock4_bluetooth.input_report_size);
135+
EXPECT_EQ(effective_dualshock4.output_report_size, dualshock4_bluetooth.output_report_size);
136+
EXPECT_EQ(effective_dualshock4.report_descriptor, dualshock4_bluetooth.report_descriptor);
137+
EXPECT_EQ(effective_dualshock4.name, dualshock4.name);
138+
EXPECT_EQ(effective_dualshock4.vendor_id, dualshock4.vendor_id);
139+
EXPECT_EQ(effective_dualshock4.product_id, dualshock4.product_id);
140+
141+
const auto requested_dualshock4_bluetooth = lvh::profiles::dualshock4_bluetooth();
142+
EXPECT_EQ(
143+
lvh::detail::test::linux_effective_uhid_gamepad_profile(requested_dualshock4_bluetooth).report_descriptor,
144+
requested_dualshock4_bluetooth.report_descriptor
145+
);
146+
147+
const auto dualsense = lvh::profiles::dualsense_usb();
148+
const auto effective_dualsense = lvh::detail::test::linux_effective_uhid_gamepad_profile(dualsense);
149+
EXPECT_EQ(effective_dualsense.bus_type, lvh::BusType::usb);
150+
EXPECT_EQ(effective_dualsense.report_descriptor, dualsense.report_descriptor);
151+
128152
EXPECT_EQ(lvh::detail::test::linux_pen_tool(lvh::PenToolType::pen), BTN_TOOL_PEN);
129153
EXPECT_EQ(lvh::detail::test::linux_pen_tool(lvh::PenToolType::eraser), BTN_TOOL_RUBBER);
130154
EXPECT_EQ(lvh::detail::test::linux_pen_tool(lvh::PenToolType::brush), BTN_TOOL_BRUSH);

0 commit comments

Comments
 (0)