From 91365cfeaced95d8260f192b837a8f3f4f778105 Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Wed, 19 Aug 2026 13:35:53 -0400 Subject: [PATCH 1/2] fix: DualShock4 and DualSense improvements --- docs/platform-support.md | 11 ++++ docs/usage.md | 4 ++ src/core/profiles.cpp | 4 +- src/core/report.cpp | 33 +++++++++--- src/include/libvirtualhid/profiles.hpp | 12 ++++- src/platform/linux/uhid_backend.cpp | 53 ++++++++++--------- .../fixtures/linux_backend_test_hooks.hpp | 4 +- tests/fixtures/linux_backend_test_hooks.cpp | 32 ++++++++++- tests/unit/test_linux_backend.cpp | 3 +- tests/unit/test_profiles.cpp | 28 ++++++++-- tests/unit/test_report.cpp | 24 +++++++++ 11 files changed, 163 insertions(+), 45 deletions(-) diff --git a/docs/platform-support.md b/docs/platform-support.md index af11b97..68b4b04 100644 --- a/docs/platform-support.md +++ b/docs/platform-support.md @@ -138,6 +138,17 @@ and control channels. Numbered control-channel output is normalized before parsing, whether the kernel includes the report number in the payload or provides it separately on the UHID event. +The default DualShock 4 and DualSense profiles use Bluetooth framing, avoiding +the parent-USB checks that can make virtual USB devices appear late in Steam. +Explicit USB and Bluetooth factories remain available for consumers that +require a particular transport. DualSense motion packing preserves the public +meters-per-second-squared and degrees-per-second units while applying the same +raw sensor calibration used by Inputtino. Periodic PlayStation reports are +repacked at 100 Hz so their sequence number and sensor timestamp continue to +advance even when controller state is unchanged. Periodic and application +submissions are serialized so a repeated report cannot restore stale motion +state after a newer application report. + The backend opens `/dev/uhid` in nonblocking mode, matching the original asynchronous gamepad registration path. Its event reader is active before device registration begins, and creation does not report success until the diff --git a/docs/usage.md b/docs/usage.md index 9de3c37..8afc110 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -217,6 +217,10 @@ Consumers may replace `DeviceProfile::name` before creating a gamepad, for example, to prepend an application name while preserving the default controller identity across platform backends. +`profiles::dualshock4()` and `profiles::dualsense()` select Bluetooth framing +for reliable native-controller discovery. Consumers can use the corresponding +`_usb()` or `_bluetooth()` factory when the transport must be explicit. + The platform-neutral Generic HID descriptor reports the D-pad as buttons 13 through 16 in the input report. Linux may still route that profile through `uinput`, where the backend exposes those same logical directions through the diff --git a/src/core/profiles.cpp b/src/core/profiles.cpp index 598da10..67f843f 100644 --- a/src/core/profiles.cpp +++ b/src/core/profiles.cpp @@ -2095,7 +2095,7 @@ namespace lvh::profiles { } DeviceProfile dualshock4() { - return dualshock4_usb(); + return dualshock4_bluetooth(); } DeviceProfile dualshock4_usb() { @@ -2107,7 +2107,7 @@ namespace lvh::profiles { } DeviceProfile dualsense() { - return dualsense_usb(); + return dualsense_bluetooth(); } DeviceProfile dualsense_usb() { diff --git a/src/core/report.cpp b/src/core/report.cpp index 318cf5c..c8c1960 100644 --- a/src/core/report.cpp +++ b/src/core/report.cpp @@ -6,11 +6,13 @@ // standard includes #include #include +#include #include #include #include #include #include +#include #include #include #include @@ -61,6 +63,10 @@ namespace lvh::reports { constexpr auto dualsense_flag2_compatible_vibration = std::byte {0x04}; + constexpr auto dualsense_acceleration_scale = 9.80665F * 100.0F; + + constexpr auto dualsense_gyroscope_scale = 1145.0F * std::numbers::pi_v / 180.0F; + constexpr std::uint8_t switch_rumble_and_subcommand_output_report_id = 0x01; constexpr std::uint8_t switch_rumble_only_output_report_id = 0x10; @@ -613,6 +619,19 @@ namespace lvh::reports { return static_cast((static_cast(elapsed) * 3U) / 16U); } + std::uint8_t dualsense_sequence_number() { + static std::atomic_uint32_t sequence_number = 0; + return static_cast((sequence_number.fetch_add(1U, std::memory_order_relaxed) + 1U) % 255U); + } + + std::uint32_t dualsense_sensor_timestamp() { + const auto elapsed = std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch() + ) + .count(); + return static_cast(static_cast(elapsed) / 333U); + } + std::vector pack_dualshock4_input_report(const DeviceProfile &profile, const GamepadState &state) { const auto is_bluetooth = profile.bus_type == BusType::bluetooth; const auto payload_offset = is_bluetooth ? 3U : 1U; @@ -722,6 +741,7 @@ namespace lvh::reports { report[payload_offset + 3U] = to_byte(normalize_u8_axis(-normalized.right_stick.y)); report[payload_offset + 4U] = to_byte(normalize_trigger(normalized.left_trigger)); report[payload_offset + 5U] = to_byte(normalize_trigger(normalized.right_trigger)); + report[payload_offset + 6U] = to_byte(dualsense_sequence_number()); report[payload_offset + 7U] = to_byte(hat_from_buttons(normalized.buttons)); if (normalized.buttons.test(GamepadButton::x)) { @@ -773,15 +793,16 @@ namespace lvh::reports { } if (normalized.gyroscope) { - write_i16(report, payload_offset + 15U, scale_i16(normalized.gyroscope->x, 1145.0F)); - write_i16(report, payload_offset + 17U, scale_i16(normalized.gyroscope->y, 1145.0F)); - write_i16(report, payload_offset + 19U, scale_i16(normalized.gyroscope->z, 1145.0F)); + write_i16(report, payload_offset + 15U, scale_i16(normalized.gyroscope->x, dualsense_gyroscope_scale)); + write_i16(report, payload_offset + 17U, scale_i16(normalized.gyroscope->y, dualsense_gyroscope_scale)); + write_i16(report, payload_offset + 19U, scale_i16(normalized.gyroscope->z, dualsense_gyroscope_scale)); } if (normalized.acceleration) { - write_i16(report, payload_offset + 21U, scale_i16(normalized.acceleration->x, 100.0F)); - write_i16(report, payload_offset + 23U, scale_i16(normalized.acceleration->y, 100.0F)); - write_i16(report, payload_offset + 25U, scale_i16(normalized.acceleration->z, 100.0F)); + write_i16(report, payload_offset + 21U, scale_i16(normalized.acceleration->x, dualsense_acceleration_scale)); + write_i16(report, payload_offset + 23U, scale_i16(normalized.acceleration->y, dualsense_acceleration_scale)); + write_i16(report, payload_offset + 25U, scale_i16(normalized.acceleration->z, dualsense_acceleration_scale)); } + write_u32(report, payload_offset + 27U, dualsense_sensor_timestamp()); write_dualsense_touch_contact(report, payload_offset + 32U, normalized.touchpad_contacts[0]); write_dualsense_touch_contact(report, payload_offset + 36U, normalized.touchpad_contacts[1]); diff --git a/src/include/libvirtualhid/profiles.hpp b/src/include/libvirtualhid/profiles.hpp index 0ca6a4e..4888cb3 100644 --- a/src/include/libvirtualhid/profiles.hpp +++ b/src/include/libvirtualhid/profiles.hpp @@ -44,7 +44,11 @@ namespace lvh::profiles { /** * @brief Create the PlayStation DualShock 4-compatible gamepad profile. * - * @return Default DualShock 4-compatible device profile. + * The default uses Bluetooth framing because Linux native-controller + * consumers discover virtual DualShock 4 devices more reliably through that + * transport. Use `dualshock4_usb()` when USB framing is explicitly required. + * + * @return Bluetooth DualShock 4-compatible device profile. */ DeviceProfile dualshock4(); @@ -65,7 +69,11 @@ namespace lvh::profiles { /** * @brief Create the PlayStation DualSense-compatible gamepad profile. * - * @return Default DualSense-compatible device profile. + * The default uses Bluetooth framing because Linux native-controller + * consumers discover virtual DualSense devices more reliably through that + * transport. Use `dualsense_usb()` when USB framing is explicitly required. + * + * @return Bluetooth DualSense-compatible device profile. */ DeviceProfile dualsense(); diff --git a/src/platform/linux/uhid_backend.cpp b/src/platform/linux/uhid_backend.cpp index d64f5ab..eaffb3e 100644 --- a/src/platform/linux/uhid_backend.cpp +++ b/src/platform/linux/uhid_backend.cpp @@ -2815,8 +2815,8 @@ namespace lvh::detail { std::memcpy(request.rd_data, options.profile.report_descriptor.data(), options.profile.report_descriptor.size()); profile_ = options.profile; { - std::lock_guard lock {report_mutex_}; - last_report_ = reports::pack_input_report(profile_, {}); + std::lock_guard lock {state_mutex_}; + last_state_ = {}; } { @@ -2848,25 +2848,13 @@ namespace lvh::detail { } OperationStatus submit( - const GamepadState & /*state*/, + const GamepadState &state, const std::vector &report ) override { - if (!open_) { - return OperationStatus::failure(ErrorCode::device_closed, "UHID gamepad is closed"); - } - - uhid_event event {}; - if (report.size() > sizeof(event.u.input2.data)) { - return OperationStatus::failure(ErrorCode::invalid_argument, "HID input report is too large for UHID"); - } - - event.type = UHID_INPUT2; - event.u.input2.size = static_cast(report.size()); - std::memcpy(event.u.input2.data, report.data(), report.size()); - auto status = write_event(event); + std::lock_guard lock {state_mutex_}; + auto status = write_input_report(report); if (status.ok()) { - std::lock_guard lock {report_mutex_}; - last_report_ = report; + last_state_ = state; } return status; } @@ -2922,6 +2910,22 @@ namespace lvh::detail { } private: + OperationStatus write_input_report(const std::vector &report) { + if (!open_) { + return OperationStatus::failure(ErrorCode::device_closed, "UHID gamepad is closed"); + } + + uhid_event event {}; + if (report.size() > sizeof(event.u.input2.data)) { + return OperationStatus::failure(ErrorCode::invalid_argument, "HID input report is too large for UHID"); + } + + event.type = UHID_INPUT2; + event.u.input2.size = static_cast(report.size()); + std::memcpy(event.u.input2.data, report.data(), report.size()); + return write_event(event); + } + OperationStatus write_event(const uhid_event &event) { using enum ErrorCode; @@ -3042,13 +3046,10 @@ namespace lvh::detail { break; } - std::vector report; - { - std::lock_guard lock {report_mutex_}; - report = last_report_; - } + std::lock_guard lock {state_mutex_}; + const auto report = reports::pack_input_report(profile_, last_state_); if (!report.empty()) { - static_cast(submit({}, report)); + static_cast(write_input_report(report)); } } } @@ -3173,7 +3174,7 @@ namespace lvh::detail { std::string physical_id_; std::string unique_id_; std::array playstation_mac_address_ {}; - std::vector last_report_; + GamepadState last_state_; std::atomic_bool open_ = true; std::atomic_bool running_ = false; std::jthread reader_; @@ -3183,7 +3184,7 @@ namespace lvh::detail { bool started_ = false; bool reader_exited_ = false; std::mutex write_mutex_; - std::mutex report_mutex_; + std::mutex state_mutex_; std::mutex callback_mutex_; OutputCallback output_callback_; }; diff --git a/tests/fixtures/include/fixtures/linux_backend_test_hooks.hpp b/tests/fixtures/include/fixtures/linux_backend_test_hooks.hpp index d52fafd..6742901 100644 --- a/tests/fixtures/include/fixtures/linux_backend_test_hooks.hpp +++ b/tests/fixtures/include/fixtures/linux_backend_test_hooks.hpp @@ -212,9 +212,9 @@ namespace lvh::detail::test { bool saw_dualshock4_feature_crc = false; /** - * @brief Whether the peer observed a Bluetooth-framed DualSense input report. + * @brief Whether periodic Bluetooth DualSense reports preserved motion and advanced sensor metadata. */ - bool saw_dualsense_bluetooth_input = false; + bool saw_dualsense_bluetooth_input_with_live_sensor_metadata = false; /** * @brief Whether the peer observed a Bluetooth-framed DualShock 4 input report. diff --git a/tests/fixtures/linux_backend_test_hooks.cpp b/tests/fixtures/linux_backend_test_hooks.cpp index d9b0f31..62834aa 100644 --- a/tests/fixtures/linux_backend_test_hooks.cpp +++ b/tests/fixtures/linux_backend_test_hooks.cpp @@ -1698,13 +1698,42 @@ namespace lvh::detail::test { UhidGamepad gamepad {descriptors[0]}; auto event = create_started_profile_uhid_gamepad(gamepad, 9, options, descriptors[1], BUS_BLUETOOTH, result); + GamepadState motion_state; + motion_state.acceleration = Vector3 {.x = 1.0F, .y = 2.0F, .z = 3.0F}; + motion_state.gyroscope = Vector3 {.x = 4.0F, .y = 5.0F, .z = 6.0F}; + const auto submitted_report = reports::pack_input_report(options.profile, motion_state); + result.submit_status = gamepad.submit(motion_state, submitted_report); + + bool first_input_report_valid = false; + std::uint8_t first_sequence = 0; + std::uint32_t first_sensor_timestamp = 0; + std::array first_sensor_values {}; if (read_uhid_event_type(descriptors[1], UHID_INPUT2, event)) { const auto report_size = static_cast(event.u.input2.size); if (report_size == options.profile.input_report_size && event.u.input2.data[0] == 0x31) { const auto crc_offset = report_size - 4U; const auto expected_crc = crc32(std::span {event.u.input2.data, crc_offset}, playstation_crc_seed(0xA1)); const auto actual_crc = read_u32_le(event.u.input2.data + crc_offset); - result.saw_dualsense_bluetooth_input = expected_crc == actual_crc; + std::copy_n(event.u.input2.data + 17U, first_sensor_values.size(), first_sensor_values.begin()); + first_input_report_valid = expected_crc == actual_crc && std::equal( + first_sensor_values.begin(), + first_sensor_values.end(), + submitted_report.begin() + 17 + ); + first_sequence = event.u.input2.data[8]; + first_sensor_timestamp = read_u32_le(event.u.input2.data + 29U); + } + } + + if (read_uhid_event_type(descriptors[1], UHID_INPUT2, event)) { + const auto report_size = static_cast(event.u.input2.size); + if (report_size == options.profile.input_report_size && event.u.input2.data[0] == 0x31) { + const auto crc_offset = report_size - 4U; + const auto expected_crc = crc32(std::span {event.u.input2.data, crc_offset}, playstation_crc_seed(0xA1)); + const auto actual_crc = read_u32_le(event.u.input2.data + crc_offset); + result.saw_dualsense_bluetooth_input_with_live_sensor_metadata = + first_input_report_valid && expected_crc == actual_crc && event.u.input2.data[8] != first_sequence && + read_u32_le(event.u.input2.data + 29U) != first_sensor_timestamp && std::equal(first_sensor_values.begin(), first_sensor_values.end(), event.u.input2.data + 17U); } } @@ -1732,7 +1761,6 @@ namespace lvh::detail::test { result.close_status = gamepad.close(); static_cast(::close(descriptors[1])); - result.submit_status = OperationStatus::success(); return result; } diff --git a/tests/unit/test_linux_backend.cpp b/tests/unit/test_linux_backend.cpp index 9342c2f..70214eb 100644 --- a/tests/unit/test_linux_backend.cpp +++ b/tests/unit/test_linux_backend.cpp @@ -822,11 +822,12 @@ TEST_F(LinuxBackendTest, SocketpairBackedDualSenseRepliesToFeatureReports) { TEST_F(LinuxBackendTest, SocketpairBackedDualSenseBluetoothFramesReports) { const auto result = lvh::detail::test::linux_dualsense_bluetooth_uhid_socketpair_reports(); EXPECT_TRUE(result.create_status.ok()) << result.create_status.message(); + EXPECT_TRUE(result.submit_status.ok()) << result.submit_status.message(); EXPECT_TRUE(result.close_status.ok()) << result.close_status.message(); EXPECT_TRUE(result.creation.saw_create); EXPECT_TRUE(result.creation.waited_for_start); EXPECT_EQ(result.creation.name, "Wireless Controller"); - EXPECT_TRUE(result.saw_dualsense_bluetooth_input); + EXPECT_TRUE(result.saw_dualsense_bluetooth_input_with_live_sensor_metadata); EXPECT_TRUE(result.saw_dualsense_pairing); EXPECT_TRUE(result.saw_dualsense_feature_crc); } diff --git a/tests/unit/test_profiles.cpp b/tests/unit/test_profiles.cpp index 040f0bd..4b603bf 100644 --- a/tests/unit/test_profiles.cpp +++ b/tests/unit/test_profiles.cpp @@ -176,8 +176,10 @@ TEST(ProfileTest, StreamingControllerProfilesArePresent) { EXPECT_EQ(dualshock4.vendor_id, 0x054C); EXPECT_EQ(dualshock4.product_id, 0x05C4); EXPECT_EQ(dualshock4.version, 0x0100); - EXPECT_EQ(dualshock4.input_report_size, 64U); - EXPECT_EQ(dualshock4.output_report_size, 32U); + EXPECT_EQ(dualshock4.bus_type, lvh::BusType::bluetooth); + EXPECT_EQ(dualshock4.report_id, 0x11); + EXPECT_EQ(dualshock4.input_report_size, 78U); + EXPECT_EQ(dualshock4.output_report_size, 78U); EXPECT_TRUE(dualshock4.capabilities.supports_motion); EXPECT_TRUE(dualshock4.capabilities.supports_touchpad); EXPECT_TRUE(dualshock4.capabilities.supports_rgb_led); @@ -193,9 +195,20 @@ TEST(ProfileTest, StreamingControllerProfilesArePresent) { EXPECT_EQ(dualshock4_bluetooth.report_id, 0x11); EXPECT_EQ(dualshock4_bluetooth.input_report_size, 78U); EXPECT_EQ(dualshock4_bluetooth.output_report_size, 78U); - EXPECT_NE(dualshock4_bluetooth.report_descriptor, dualshock4.report_descriptor); + EXPECT_EQ(dualshock4_bluetooth.report_descriptor, dualshock4.report_descriptor); + + const auto dualshock4_usb = lvh::profiles::dualshock4_usb(); + EXPECT_EQ(dualshock4_usb.bus_type, lvh::BusType::usb); + EXPECT_EQ(dualshock4_usb.report_id, 0x01); + EXPECT_EQ(dualshock4_usb.input_report_size, 64U); + EXPECT_EQ(dualshock4_usb.output_report_size, 32U); + EXPECT_NE(dualshock4_usb.report_descriptor, dualshock4.report_descriptor); EXPECT_EQ(dualsense.vendor_id, 0x054C); + EXPECT_EQ(dualsense.bus_type, lvh::BusType::bluetooth); + EXPECT_EQ(dualsense.report_id, 0x31); + EXPECT_EQ(dualsense.input_report_size, 78U); + EXPECT_EQ(dualsense.output_report_size, 78U); EXPECT_TRUE(dualsense.capabilities.supports_motion); EXPECT_TRUE(dualsense.capabilities.supports_touchpad); EXPECT_TRUE(dualsense.capabilities.supports_rgb_led); @@ -210,7 +223,14 @@ TEST(ProfileTest, StreamingControllerProfilesArePresent) { EXPECT_EQ(dualsense_bluetooth.report_id, 0x31); EXPECT_EQ(dualsense_bluetooth.input_report_size, 78U); EXPECT_EQ(dualsense_bluetooth.output_report_size, 78U); - EXPECT_NE(dualsense_bluetooth.report_descriptor, dualsense.report_descriptor); + EXPECT_EQ(dualsense_bluetooth.report_descriptor, dualsense.report_descriptor); + + const auto dualsense_usb = lvh::profiles::dualsense_usb(); + EXPECT_EQ(dualsense_usb.bus_type, lvh::BusType::usb); + EXPECT_EQ(dualsense_usb.report_id, 0x01); + EXPECT_EQ(dualsense_usb.input_report_size, 64U); + EXPECT_EQ(dualsense_usb.output_report_size, 48U); + EXPECT_NE(dualsense_usb.report_descriptor, dualsense.report_descriptor); EXPECT_EQ(switch_pro.vendor_id, 0x057E); EXPECT_EQ(switch_pro.product_id, 0x2009); diff --git a/tests/unit/test_report.cpp b/tests/unit/test_report.cpp index a8f3037..8ea005d 100644 --- a/tests/unit/test_report.cpp +++ b/tests/unit/test_report.cpp @@ -50,6 +50,10 @@ namespace { return static_cast(low | static_cast(high << 8U)); } + std::int16_t read_i16_le(std::span bytes, std::size_t offset) { + return static_cast(read_u16_le(bytes, offset)); + } + lvh::GamepadState make_active_gamepad_state() { using enum lvh::GamepadButton; @@ -232,12 +236,32 @@ TEST(ReportTest, PacksDualSenseUsbReport) { EXPECT_EQ(report[5], 255); EXPECT_EQ(report[8] & 0x20, 0x20); EXPECT_EQ(report[9] & 0x05, 0x05); + EXPECT_EQ(read_i16_le(report, 16U), 80); + EXPECT_EQ(read_i16_le(report, 18U), 100); + EXPECT_EQ(read_i16_le(report, 20U), 120); + EXPECT_EQ(read_i16_le(report, 22U), 981); + EXPECT_EQ(read_i16_le(report, 24U), 1961); + EXPECT_EQ(read_i16_le(report, 26U), 2942); + EXPECT_NE(read_u32_le(report, 28U), 0U); EXPECT_EQ(report[33] & 0x7F, 3); EXPECT_EQ(report[33] & 0x80, 0); EXPECT_EQ(report[53] & 0x0F, 8); EXPECT_EQ(report[53] >> 4, 1); } +TEST(ReportTest, AdvancesDualSenseSensorMetadata) { + const auto profile = lvh::profiles::dualsense_bluetooth(); + + const auto first = lvh::reports::pack_input_report(profile, {}); + const auto second = lvh::reports::pack_input_report(profile, {}); + + ASSERT_EQ(first.size(), profile.input_report_size); + ASSERT_EQ(second.size(), profile.input_report_size); + EXPECT_NE(first[8], second[8]); + EXPECT_NE(read_u32_le(first, 29U), 0U); + EXPECT_GE(read_u32_le(second, 29U), read_u32_le(first, 29U)); +} + TEST(ReportTest, PacksDualSenseBluetoothReportWithCrc) { const auto profile = lvh::profiles::dualsense_bluetooth(); From 0c38b9ff65959f4cf36c18d8beaf1dd995c4b487 Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Wed, 19 Aug 2026 20:59:41 -0400 Subject: [PATCH 2/2] Set DS4 BT HID-present flag and align tests DualShock 4 Bluetooth input reports now set the HID-present header bit (0x80) while keeping existing BT framing and CRC behavior. This enables HIDAPI/SDL consumers to accept live input after hotplug on Linux. Updated report and Linux consumer tests to assert the new behavior, and refreshed platform-support docs to describe the BT report contract and unchanged transport boundaries. --- docs/platform-support.md | 12 +++++++----- src/core/report.cpp | 5 +++++ tests/fixtures/linux_backend_test_hooks.cpp | 6 +++++- tests/unit/test_linux_consumers.cpp | 3 +-- tests/unit/test_report.cpp | 1 + 5 files changed, 19 insertions(+), 8 deletions(-) diff --git a/docs/platform-support.md b/docs/platform-support.md index 68b4b04..76e2a2c 100644 --- a/docs/platform-support.md +++ b/docs/platform-support.md @@ -141,8 +141,11 @@ provides it separately on the UHID event. The default DualShock 4 and DualSense profiles use Bluetooth framing, avoiding the parent-USB checks that can make virtual USB devices appear late in Steam. Explicit USB and Bluetooth factories remain available for consumers that -require a particular transport. DualSense motion packing preserves the public -meters-per-second-squared and degrees-per-second units while applying the same +require a particular transport. DualShock 4 Bluetooth input reports set the +HID-present header flag required by HIDAPI consumers and include the transport +CRC, so a running consumer can accept live input after hotplug. DualSense motion +packing preserves the public meters-per-second-squared and degrees-per-second +units while applying the same raw sensor calibration used by Inputtino. Periodic PlayStation reports are repacked at 100 Hz so their sequence number and sensor timestamp continue to advance even when controller state is unchanged. Periodic and application @@ -158,9 +161,8 @@ a controller before its kernel HID device has started. On Linux, DualShock 4 and DualSense emit Sony's native `Wireless Controller` product name for Steam HID discovery. The requested USB or Bluetooth bus, -descriptor, and report framing remain unchanged; in particular, the default -DualShock 4 profile stays on its USB report contract. This transport-only name -is confined to the Linux backend; public profile names, Windows names, and VHF +descriptor, and report framing remain unchanged. This transport-only name is +confined to the Linux backend; public profile names, Windows names, and VHF behavior are unchanged. Switch Pro keeps its Nintendo identity on the Linux uinput path. This follows diff --git a/src/core/report.cpp b/src/core/report.cpp index c8c1960..d0fa18b 100644 --- a/src/core/report.cpp +++ b/src/core/report.cpp @@ -33,6 +33,8 @@ namespace lvh::reports { constexpr auto dualshock4_bt_input_report_id = std::byte {0x11}; + constexpr auto dualshock4_bt_input_hid_present = std::byte {0x80}; + constexpr auto dualshock4_bt_output_report_id = std::byte {0x11}; constexpr auto dualshock4_output_hwctl_crc32 = std::byte {0x40}; @@ -646,6 +648,9 @@ namespace lvh::reports { ByteReport report(profile.input_report_size, zero_byte); report[0] = is_bluetooth ? dualshock4_bt_input_report_id : to_byte(profile.report_id); + if (is_bluetooth) { + report[1] = dualshock4_bt_input_hid_present; + } report[payload_offset + 0U] = to_byte(normalize_u8_axis(normalized.left_stick.x)); report[payload_offset + 1U] = to_byte(normalize_u8_axis(-normalized.left_stick.y)); diff --git a/tests/fixtures/linux_backend_test_hooks.cpp b/tests/fixtures/linux_backend_test_hooks.cpp index 62834aa..91345e3 100644 --- a/tests/fixtures/linux_backend_test_hooks.cpp +++ b/tests/fixtures/linux_backend_test_hooks.cpp @@ -1868,7 +1868,11 @@ namespace lvh::detail::test { if (read_uhid_event_type(descriptors[1], UHID_INPUT2, event)) { const auto report_size = static_cast(event.u.input2.size); - if (report_size == options.profile.input_report_size && event.u.input2.data[0] == 0x11) { + if ( + report_size == options.profile.input_report_size && + event.u.input2.data[0] == 0x11 && + (event.u.input2.data[1] & 0x80U) != 0U + ) { const auto crc_offset = report_size - 4U; const auto expected_crc = crc32(std::span {event.u.input2.data, crc_offset}, playstation_crc_seed(0xA1)); const auto actual_crc = read_u32_le(event.u.input2.data + crc_offset); diff --git a/tests/unit/test_linux_consumers.cpp b/tests/unit/test_linux_consumers.cpp index 675fa4f..b733de7 100644 --- a/tests/unit/test_linux_consumers.cpp +++ b/tests/unit/test_linux_consumers.cpp @@ -886,7 +886,7 @@ TEST_F(LinuxConsumerTest, SdlSeesDualShock4UsbControllerBehavior) { }); } -TEST_F(LinuxConsumerTest, SdlSeesDualShock4BluetoothControllerDiscovery) { +TEST_F(LinuxConsumerTest, SdlSeesDualShock4BluetoothControllerBehavior) { ASSERT_TRUE(HasReadableWritableDeviceNode("/dev/uhid")); run_sdl_playstation_controller_test({ @@ -895,7 +895,6 @@ TEST_F(LinuxConsumerTest, SdlSeesDualShock4BluetoothControllerDiscovery) { .stable_id = "02:00:00:00:00:04", .minimum_buttons = 10, .minimum_axes = 4, - .expect_live_input = false, }); } diff --git a/tests/unit/test_report.cpp b/tests/unit/test_report.cpp index 8ea005d..06a73a4 100644 --- a/tests/unit/test_report.cpp +++ b/tests/unit/test_report.cpp @@ -350,6 +350,7 @@ TEST(ReportTest, PacksDualShock4BluetoothReportWithCrc) { ASSERT_EQ(report.size(), profile.input_report_size); EXPECT_EQ(report[0], 0x11); + EXPECT_EQ(report[1], 0x80); EXPECT_EQ(report[3], 128); EXPECT_EQ(report[4], 128); EXPECT_EQ(report[9], 0x02);