diff --git a/docs/platform-support.md b/docs/platform-support.md index af11b97..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. @@ -138,6 +149,20 @@ 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. 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 +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 @@ -147,9 +172,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/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..d0fa18b 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 @@ -31,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}; @@ -61,6 +65,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 +621,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; @@ -627,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)); @@ -722,6 +746,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 +798,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..4db46a9 100644 --- a/src/include/libvirtualhid/profiles.hpp +++ b/src/include/libvirtualhid/profiles.hpp @@ -44,7 +44,14 @@ 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. 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. */ DeviceProfile dualshock4(); @@ -65,7 +72,14 @@ 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. 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. */ DeviceProfile dualsense(); 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/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/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/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/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/linux_backend_test_hooks.cpp b/tests/fixtures/linux_backend_test_hooks.cpp index d9b0f31..91345e3 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; } @@ -1840,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/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_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_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_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..06a73a4 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(); @@ -326,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); 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();