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/3] 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/3] 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); From 1137f89786963de55e89c2666f35086a37c49835 Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Thu, 20 Aug 2026 11:16:21 -0400 Subject: [PATCH 3/3] fix: align Windows PS profiles with USB VHF Map the default Bluetooth DualShock 4 and DualSense profiles to effective USB framing on the Windows VHF backend so the exposed descriptor, reports, and `Gamepad::profile()` match what HIDAPI, Steam, and SDL can actually use. Update the public docs/runtime comments to explain the backend-selected effective transport, refresh the emulated DualSense firmware report to match the base controller series/version, and extend Windows protocol/backend/consumer tests to cover the new transport behavior plus RGB LED output handling. --- docs/platform-support.md | 11 ++ src/include/libvirtualhid/profiles.hpp | 10 +- src/include/libvirtualhid/runtime.hpp | 7 +- src/platform/windows/control_protocol.hpp | 28 +++++ src/platform/windows/windows_backend.cpp | 8 +- src/shared/playstation_feature_reports.hpp | 42 +++---- .../fixtures/windows_backend_test_hooks.hpp | 8 ++ tests/fixtures/windows_backend_test_hooks.cpp | 30 +++++ tests/unit/test_windows_backend.cpp | 21 ++++ tests/unit/test_windows_consumers.cpp | 104 +++++++++++------- tests/unit/test_windows_driver_protocol.cpp | 4 + tests/unit/test_windows_protocol.cpp | 33 ++++++ 12 files changed, 244 insertions(+), 62 deletions(-) diff --git a/docs/platform-support.md b/docs/platform-support.md index 76e2a2c..d12c7eb 100644 --- a/docs/platform-support.md +++ b/docs/platform-support.md @@ -77,6 +77,17 @@ Switch Pro USB and subcommand initialization sequence and accepts the native `0x30` input layout, so descriptor-aware consumers can initialize those controllers before sending their native output reports. +Windows VHF devices do not expose a Bluetooth transport identity to HIDAPI. +The Windows backend therefore reports DualShock 4 and DualSense requests as +effective USB profiles through `Gamepad::profile()` and uses the matching USB +descriptor, input reports, output reports, and feature-report framing. This +keeps Steam and SDL's transport detection aligned with the reports accepted by +the driver, including rumble and RGB LED output. The DualSense firmware feature +report identifies the base controller's `0x0004` software series and current +`0x0630` device software instead of reporting DualSense Edge series `0x0044` +with the older `0x0154` revision. Linux keeps the Bluetooth defaults described +below. + See [Windows driver package](windows-driver.md) for build, install, validation, and signing details. diff --git a/src/include/libvirtualhid/profiles.hpp b/src/include/libvirtualhid/profiles.hpp index 4888cb3..4db46a9 100644 --- a/src/include/libvirtualhid/profiles.hpp +++ b/src/include/libvirtualhid/profiles.hpp @@ -46,7 +46,10 @@ namespace lvh::profiles { * * 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. + * transport. Backends may select a different effective transport when their + * native virtual HID stack cannot expose Bluetooth identity; query + * `Gamepad::profile()` after creation. Use `dualshock4_usb()` when USB + * framing is explicitly required. * * @return Bluetooth DualShock 4-compatible device profile. */ @@ -71,7 +74,10 @@ namespace lvh::profiles { * * 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. + * transport. Backends may select a different effective transport when their + * native virtual HID stack cannot expose Bluetooth identity; query + * `Gamepad::profile()` after creation. Use `dualsense_usb()` when USB framing + * is explicitly required. * * @return Bluetooth DualSense-compatible device profile. */ diff --git a/src/include/libvirtualhid/runtime.hpp b/src/include/libvirtualhid/runtime.hpp index 7ad029d..efdda3f 100644 --- a/src/include/libvirtualhid/runtime.hpp +++ b/src/include/libvirtualhid/runtime.hpp @@ -54,9 +54,12 @@ namespace lvh { virtual DeviceId device_id() const = 0; /** - * @brief Get the profile used to create this device. + * @brief Get the effective profile exposed by the backend for this device. * - * @return Device profile. + * A backend may adjust transport-specific profile fields when its native + * device stack cannot represent the requested transport directly. + * + * @return Effective device profile. */ virtual const DeviceProfile &profile() const = 0; diff --git a/src/platform/windows/control_protocol.hpp b/src/platform/windows/control_protocol.hpp index c401022..e22a47b 100644 --- a/src/platform/windows/control_protocol.hpp +++ b/src/platform/windows/control_protocol.hpp @@ -13,6 +13,7 @@ #include #include #include +#include #include // driver includes @@ -20,6 +21,7 @@ #include "lvh_windows_protocol.h" // local includes +#include #include namespace lvh::detail::windows { @@ -94,6 +96,32 @@ namespace lvh::detail::windows { return LVH_WINDOWS_GAMEPAD_GENERIC; } + inline DeviceProfile effective_vhf_gamepad_profile(const DeviceProfile &requested_profile) { + if (requested_profile.bus_type != BusType::bluetooth) { + return requested_profile; + } + + auto usb_profile = DeviceProfile {}; + switch (requested_profile.gamepad_kind) { + case GamepadProfileKind::dualshock4: + usb_profile = profiles::dualshock4_usb(); + break; + case GamepadProfileKind::dualsense: + usb_profile = profiles::dualsense_usb(); + break; + default: + return requested_profile; + } + + auto effective_profile = requested_profile; + effective_profile.bus_type = usb_profile.bus_type; + effective_profile.report_id = usb_profile.report_id; + effective_profile.input_report_size = usb_profile.input_report_size; + effective_profile.output_report_size = usb_profile.output_report_size; + effective_profile.report_descriptor = std::move(usb_profile.report_descriptor); + return effective_profile; + } + template std::uint32_t copy_string(std::array &target, std::string_view source) { std::ranges::fill(target, '\0'); diff --git a/src/platform/windows/windows_backend.cpp b/src/platform/windows/windows_backend.cpp index d129fa4..a9c2a92 100644 --- a/src/platform/windows/windows_backend.cpp +++ b/src/platform/windows/windows_backend.cpp @@ -2013,7 +2013,13 @@ namespace lvh::detail { }; } - return context_->create_gamepad(id, options); + auto effective_options = options; + effective_options.profile = windows::effective_vhf_gamepad_profile(options.profile); + auto result = context_->create_gamepad(id, effective_options); + if (result) { + result.effective_profile = std::move(effective_options.profile); + } + return result; } BackendKeyboardCreationResult create_keyboard( diff --git a/src/shared/playstation_feature_reports.hpp b/src/shared/playstation_feature_reports.hpp index 70afbfd..ac8b5d2 100644 --- a/src/shared/playstation_feature_reports.hpp +++ b/src/shared/playstation_feature_reports.hpp @@ -218,40 +218,42 @@ namespace lvh::detail::playstation_feature_reports { 0x00, }; + // Captured from a current base DualSense (PID 0x0CE6). Keep the software + // series and firmware version consistent with the emulated product ID. inline constexpr std::array dualsense_firmware_info { 0x20, 0x4A, 0x75, - 0x6E, + 0x6C, 0x20, - 0x31, - 0x39, + 0x20, + 0x34, 0x20, 0x32, 0x30, 0x32, - 0x33, + 0x35, 0x31, - 0x34, + 0x30, 0x3A, - 0x34, - 0x37, + 0x31, + 0x30, 0x3A, 0x33, - 0x34, + 0x32, 0x03, 0x00, - 0x44, + 0x04, 0x00, - 0x08, - 0x02, + 0x10, + 0x13, 0x00, - 0x01, - 0x36, 0x00, + 0x2A, 0x00, + 0x10, + 0x01, 0x01, - 0xC1, 0xC8, 0x00, 0x00, @@ -263,17 +265,17 @@ namespace lvh::detail::playstation_feature_reports { 0x00, 0x00, 0x00, - 0x54, - 0x01, - 0x00, + 0x30, + 0x06, 0x00, - 0x14, 0x00, + 0x3C, 0x00, + 0x01, 0x00, - 0x0B, + 0x0A, 0x00, - 0x01, + 0x02, 0x00, 0x06, 0x00, diff --git a/tests/fixtures/include/fixtures/windows_backend_test_hooks.hpp b/tests/fixtures/include/fixtures/windows_backend_test_hooks.hpp index 4967caa..f5976cf 100644 --- a/tests/fixtures/include/fixtures/windows_backend_test_hooks.hpp +++ b/tests/fixtures/include/fixtures/windows_backend_test_hooks.hpp @@ -51,6 +51,13 @@ namespace lvh::detail::test { std::vector strengths; }; + struct WindowsPlayStationTransportResult { + OperationStatus dualshock4_status; + OperationStatus dualsense_status; + DeviceProfile dualshock4_effective_profile; + DeviceProfile dualsense_effective_profile; + }; + struct WindowsBackendUtilityResult { std::vector default_device_paths; std::vector custom_device_paths; @@ -164,6 +171,7 @@ namespace lvh::detail::test { }; WindowsBackendLifecycleResult windows_backend_fake_channel_lifecycle(); + WindowsPlayStationTransportResult windows_backend_playstation_transport(); WindowsGenericPidOrderingResult windows_backend_generic_pid_callback_ordering(); WindowsBackendFailureResult windows_backend_fake_channel_failures(); WindowsBackendUtilityResult windows_backend_fake_channel_utilities(); diff --git a/tests/fixtures/windows_backend_test_hooks.cpp b/tests/fixtures/windows_backend_test_hooks.cpp index e7381f6..b3442dc 100644 --- a/tests/fixtures/windows_backend_test_hooks.cpp +++ b/tests/fixtures/windows_backend_test_hooks.cpp @@ -494,6 +494,36 @@ namespace lvh::detail { return result; } + WindowsPlayStationTransportResult windows_backend_playstation_transport() { + WindowsPlayStationTransportResult result; + auto command_state = std::make_shared(); + auto event_state = std::make_shared(); + auto backend = make_fake_windows_backend(command_state, event_state); + + CreateGamepadOptions options; + options.profile = profiles::dualshock4(); + auto dualshock4 = backend->create_gamepad(40, options); + result.dualshock4_status = dualshock4.status; + if (dualshock4.effective_profile.has_value()) { + result.dualshock4_effective_profile = std::move(*dualshock4.effective_profile); + } + if (dualshock4.gamepad) { + static_cast(dualshock4.gamepad->close()); + } + + options.profile = profiles::dualsense(); + auto dualsense = backend->create_gamepad(41, options); + result.dualsense_status = dualsense.status; + if (dualsense.effective_profile.has_value()) { + result.dualsense_effective_profile = std::move(*dualsense.effective_profile); + } + if (dualsense.gamepad) { + static_cast(dualsense.gamepad->close()); + } + + return result; + } + WindowsGenericPidOrderingResult windows_backend_generic_pid_callback_ordering() { WindowsGenericPidOrderingResult result; auto command_state = std::make_shared(); diff --git a/tests/unit/test_windows_backend.cpp b/tests/unit/test_windows_backend.cpp index 993a710..d2fc45e 100644 --- a/tests/unit/test_windows_backend.cpp +++ b/tests/unit/test_windows_backend.cpp @@ -28,6 +28,9 @@ #include "fixtures/fixtures.hpp" #include "fixtures/windows_backend_test_hooks.hpp" +// lib includes +#include + // standard includes #include #include @@ -94,6 +97,24 @@ TEST_F(WindowsBackendTest, FakeChannelExercisesLifecycleSubmitCloseAndOutput) { EXPECT_EQ(result.last_output.raw_report[0], 0x03U); } +TEST_F(WindowsBackendTest, PlayStationDefaultsUseEffectiveUsbProfiles) { + const auto result = lvh::detail::test::windows_backend_playstation_transport(); + const auto dualshock4_usb = lvh::profiles::dualshock4_usb(); + const auto dualsense_usb = lvh::profiles::dualsense_usb(); + + ASSERT_TRUE(result.dualshock4_status.ok()) << result.dualshock4_status.message(); + EXPECT_EQ(result.dualshock4_effective_profile.bus_type, lvh::BusType::usb); + EXPECT_EQ(result.dualshock4_effective_profile.report_descriptor, dualshock4_usb.report_descriptor); + EXPECT_EQ(result.dualshock4_effective_profile.input_report_size, dualshock4_usb.input_report_size); + EXPECT_EQ(result.dualshock4_effective_profile.output_report_size, dualshock4_usb.output_report_size); + + ASSERT_TRUE(result.dualsense_status.ok()) << result.dualsense_status.message(); + EXPECT_EQ(result.dualsense_effective_profile.bus_type, lvh::BusType::usb); + EXPECT_EQ(result.dualsense_effective_profile.report_descriptor, dualsense_usb.report_descriptor); + EXPECT_EQ(result.dualsense_effective_profile.input_report_size, dualsense_usb.input_report_size); + EXPECT_EQ(result.dualsense_effective_profile.output_report_size, dualsense_usb.output_report_size); +} + TEST_F(WindowsBackendTest, GenericPidTimerCannotDeliverStaleStopAfterNewStart) { const auto result = lvh::detail::test::windows_backend_generic_pid_callback_ordering(); diff --git a/tests/unit/test_windows_consumers.cpp b/tests/unit/test_windows_consumers.cpp index a451e05..4ba26aa 100644 --- a/tests/unit/test_windows_consumers.cpp +++ b/tests/unit/test_windows_consumers.cpp @@ -317,35 +317,6 @@ namespace { return report; } - std::optional wait_for_rumble_output( - std::mutex &output_mutex, - std::condition_variable &output_ready, - const std::vector &outputs, - std::size_t &next_output, - bool expect_nonzero - ) { - const auto matches = [expect_nonzero](const lvh::GamepadOutput &output) { - const auto has_strength = output.low_frequency_rumble > 0U || output.high_frequency_rumble > 0U; - return output.kind == lvh::GamepadOutputKind::rumble && has_strength == expect_nonzero; - }; - const auto find_next_match = [&outputs, &next_output, &matches] { - return std::find_if( - outputs.begin() + static_cast(next_output), - outputs.end(), - matches - ); - }; - - if (std::unique_lock lock {output_mutex}; output_ready.wait_for(lock, 5s, [&outputs, &find_next_match] { - return find_next_match() != outputs.end(); - })) { - const auto match = find_next_match(); - next_output = static_cast(std::distance(outputs.begin(), match)) + 1U; - return *match; - } - return std::nullopt; - } - HidInterfacePaths current_gamepad_interface_paths() { HidInterfacePaths paths; for (const auto &hid_interface : enumerate_gamepad_interfaces()) { @@ -367,10 +338,44 @@ namespace { } std::optional wait_for_rumble(bool expect_nonzero) { - return wait_for_rumble_output(mutex_, ready_, outputs_, next_output_, expect_nonzero); + return wait_for([expect_nonzero](const lvh::GamepadOutput &output) { + const auto has_strength = output.low_frequency_rumble > 0U || output.high_frequency_rumble > 0U; + return output.kind == lvh::GamepadOutputKind::rumble && has_strength == expect_nonzero; + }); + } + + std::optional wait_for_rgb_led( + std::uint8_t red, + std::uint8_t green, + std::uint8_t blue + ) { + return wait_for([red, green, blue](const lvh::GamepadOutput &output) { + return output.kind == lvh::GamepadOutputKind::rgb_led && output.red == red && output.green == green && + output.blue == blue; + }); } private: + template + std::optional wait_for(Predicate matches) { + const auto find_next_match = [this, &matches] { + return std::find_if( + outputs_.begin() + static_cast(next_output_), + outputs_.end(), + matches + ); + }; + + if (std::unique_lock lock {mutex_}; ready_.wait_for(lock, 5s, [this, &find_next_match] { + return find_next_match() != outputs_.end(); + })) { + const auto match = find_next_match(); + next_output_ = static_cast(std::distance(outputs_.begin(), match)) + 1U; + return *match; + } + return std::nullopt; + } + std::mutex mutex_; std::condition_variable ready_; std::vector outputs_; @@ -450,7 +455,7 @@ namespace { } // namespace #if defined(LIBVIRTUALHID_TEST_HAS_SDL3) -TEST_F(WindowsConsumerTest, SdlHidapiRumbleReachesPlayStationAndSwitchCallbacks) { +TEST_F(WindowsConsumerTest, SdlHidapiOutputReachesDefaultPlayStationAndSwitchCallbacks) { SdlGamepadSubsystem sdl; ASSERT_TRUE(sdl.initialized()) << SDL_GetError(); @@ -462,8 +467,8 @@ TEST_F(WindowsConsumerTest, SdlHidapiRumbleReachesPlayStationAndSwitchCallbacks) << "The installed libvirtualhid Windows driver is required for this integration test"; const std::array profiles { - lvh::profiles::dualshock4_usb(), - lvh::profiles::dualsense_usb(), + lvh::profiles::dualshock4(), + lvh::profiles::dualsense(), lvh::profiles::switch_pro(), }; for (const auto &profile : profiles) { @@ -475,6 +480,13 @@ TEST_F(WindowsConsumerTest, SdlHidapiRumbleReachesPlayStationAndSwitchCallbacks) options.metadata.stable_id = "02:11:22:33:44:55"; auto created = lvh::GamepadStateAdapter::create(*runtime, options); ASSERT_TRUE(created) << created.status.message(); + ASSERT_NE(created.adapter->gamepad(), nullptr); + if ( + profile.gamepad_kind == lvh::GamepadProfileKind::dualshock4 || + profile.gamepad_kind == lvh::GamepadProfileKind::dualsense + ) { + EXPECT_EQ(created.adapter->gamepad()->profile().bus_type, lvh::BusType::usb); + } GamepadOutputCapture output_capture; output_capture.attach(*created.adapter); @@ -484,6 +496,15 @@ TEST_F(WindowsConsumerTest, SdlHidapiRumbleReachesPlayStationAndSwitchCallbacks) ASSERT_NE(joystick, nullptr) << SDL_GetError(); const auto properties = SDL_GetGamepadProperties(gamepad.get()); ASSERT_NE(properties, 0U) << SDL_GetError(); + if ( + profile.gamepad_kind == lvh::GamepadProfileKind::dualshock4 || + profile.gamepad_kind == lvh::GamepadProfileKind::dualsense + ) { + ASSERT_TRUE(SDL_GetBooleanProperty(properties, SDL_PROP_GAMEPAD_CAP_RGB_LED_BOOLEAN, false)) << SDL_GetError(); + ASSERT_TRUE(SDL_SetGamepadLED(gamepad.get(), 0x12U, 0x34U, 0x56U)) << SDL_GetError(); + const auto rgb_led = output_capture.wait_for_rgb_led(0x12U, 0x34U, 0x56U); + ASSERT_TRUE(rgb_led.has_value()) << "No normalized RGB LED callback followed SDL HIDAPI LED output"; + } ASSERT_TRUE(SDL_GetBooleanProperty(properties, SDL_PROP_GAMEPAD_CAP_RUMBLE_BOOLEAN, false)) << SDL_GetError(); ASSERT_TRUE(SDL_RumbleGamepad(gamepad.get(), 0x5678U, 0x1234U, 1000U)) << SDL_GetError(); @@ -514,7 +535,7 @@ TEST_F(WindowsConsumerTest, NativePlayStationFeatureAndOutputReportsReachOwningR const std::array test_cases { NativePlayStationCase { - .profile = lvh::profiles::dualshock4_usb(), + .profile = lvh::profiles::dualshock4(), .calibration_report_id = 0x02, .pairing_report_id = 0x12, .firmware_report_id = 0xA3, @@ -526,11 +547,11 @@ TEST_F(WindowsConsumerTest, NativePlayStationFeatureAndOutputReportsReachOwningR .minimum_feature_report_size = 49U, }, NativePlayStationCase { - .profile = lvh::profiles::dualsense_usb(), + .profile = lvh::profiles::dualsense(), .calibration_report_id = 0x05, .pairing_report_id = 0x09, .firmware_report_id = 0x20, - .firmware_prefix = {'J', 'u', 'n'}, + .firmware_prefix = {'J', 'u', 'l'}, .output_report_id = 0x02, .valid_flags_offset = 1U, .right_motor_offset = 3U, @@ -560,12 +581,15 @@ TEST_F(WindowsConsumerTest, NativePlayStationFeatureAndOutputReportsReachOwningR options.metadata.stable_id = "02:11:22:33:44:55"; auto created = lvh::GamepadStateAdapter::create(*runtime, options); ASSERT_TRUE(created) << profile.name << ": " << created.status.message(); + ASSERT_NE(created.adapter->gamepad(), nullptr); + const auto &effective_profile = created.adapter->gamepad()->profile(); + ASSERT_EQ(effective_profile.bus_type, lvh::BusType::usb); GamepadOutputCapture output_capture; output_capture.attach(*created.adapter); const auto hid_interface = wait_for_new_interface(previous_paths, profile.vendor_id, profile.product_id); ASSERT_TRUE(hid_interface.has_value()) << "The VHF " << profile.name << " HID interface was not enumerated"; - ASSERT_EQ(hid_interface->output_report_size, profile.output_report_size); + ASSERT_EQ(hid_interface->output_report_size, effective_profile.output_report_size); Handle hid {CreateFileW( hid_interface->path.c_str(), @@ -605,6 +629,12 @@ TEST_F(WindowsConsumerTest, NativePlayStationFeatureAndOutputReportsReachOwningR << profile.name << " firmware GetFeature failed: " << GetLastError(); EXPECT_EQ(firmware[0], test_case.firmware_report_id); EXPECT_TRUE(std::ranges::equal(test_case.firmware_prefix, std::span {firmware}.subspan(1U, 3U))); + if (profile.gamepad_kind == lvh::GamepadProfileKind::dualsense) { + EXPECT_EQ(firmware[22], 0x04U); + EXPECT_EQ(firmware[23], 0x00U); + EXPECT_EQ(firmware[44], 0x30U); + EXPECT_EQ(firmware[45], 0x06U); + } std::vector report(hid_interface->output_report_size, 0); report[0] = test_case.output_report_id; diff --git a/tests/unit/test_windows_driver_protocol.cpp b/tests/unit/test_windows_driver_protocol.cpp index 0b3bbea..782e58d 100644 --- a/tests/unit/test_windows_driver_protocol.cpp +++ b/tests/unit/test_windows_driver_protocol.cpp @@ -255,6 +255,10 @@ TEST_F(WindowsDriverProtocolTest, DualSenseReturnsCalibrationPairingAndFirmwareF ASSERT_TRUE(firmware.has_value()); EXPECT_EQ(firmware->size(), 64U); EXPECT_EQ(firmware->front(), 0x20); + EXPECT_EQ(firmware->at(22), 0x04); + EXPECT_EQ(firmware->at(23), 0x00); + EXPECT_EQ(firmware->at(44), 0x30); + EXPECT_EQ(firmware->at(45), 0x06); EXPECT_FALSE(lvh::detail::windows::make_playstation_feature_report(request, 0xFF).has_value()); } diff --git a/tests/unit/test_windows_protocol.cpp b/tests/unit/test_windows_protocol.cpp index 3f60f00..052d7e4 100644 --- a/tests/unit/test_windows_protocol.cpp +++ b/tests/unit/test_windows_protocol.cpp @@ -200,6 +200,39 @@ TEST(WindowsProtocolTest, PacksGamepadCreateRequest) { EXPECT_NE(request.flags & LVH_WINDOWS_GAMEPAD_FLAG_SUPPORTS_BATTERY, 0U); } +TEST(WindowsProtocolTest, UsesUsbFramingForPlayStationProfilesOnVhf) { + const std::array requested_profiles { + lvh::profiles::dualshock4(), + lvh::profiles::dualsense(), + }; + const std::array usb_profiles { + lvh::profiles::dualshock4_usb(), + lvh::profiles::dualsense_usb(), + }; + + for (std::size_t index = 0; index < requested_profiles.size(); ++index) { + const auto &requested = requested_profiles[index]; + const auto &usb = usb_profiles[index]; + SCOPED_TRACE(requested.name); + ASSERT_EQ(requested.bus_type, lvh::BusType::bluetooth); + + const auto effective = lvh::detail::windows::effective_vhf_gamepad_profile(requested); + + EXPECT_EQ(effective.bus_type, lvh::BusType::usb); + EXPECT_EQ(effective.report_id, usb.report_id); + EXPECT_EQ(effective.input_report_size, usb.input_report_size); + EXPECT_EQ(effective.output_report_size, usb.output_report_size); + EXPECT_EQ(effective.report_descriptor, usb.report_descriptor); + EXPECT_EQ(effective.vendor_id, requested.vendor_id); + EXPECT_EQ(effective.product_id, requested.product_id); + EXPECT_EQ(effective.version, requested.version); + EXPECT_EQ(effective.name, requested.name); + EXPECT_EQ(effective.manufacturer, requested.manufacturer); + EXPECT_EQ(effective.capabilities.supports_rumble, requested.capabilities.supports_rumble); + EXPECT_EQ(effective.capabilities.supports_rgb_led, requested.capabilities.supports_rgb_led); + } +} + TEST(WindowsProtocolTest, PacksGenericUnknownBusGamepadCreateRequestWithoutOptionalFlags) { lvh::CreateGamepadOptions options; options.profile = minimal_gamepad_profile();