diff --git a/docs/todo.md b/docs/todo.md index 4d7cd65..5fd5ec7 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -78,37 +78,5 @@ Proposed solution: ## PlayStation and Nintendo Rumble -Status: unresolved or not fully validated for DualShock 4, DualSense, and -Switch Pro-style virtual gamepads in consumer paths. - -The profiles expose the native descriptors and the Windows backend answers the -initialization feature reports needed by HIDAPI-style clients. The remaining -gap is output behavior after consumers open the virtual controllers: rumble is -not reliably reaching the public `GamepadOutput` callback for PlayStation 4, -PlayStation 5, and Nintendo-style gamepads in manual validation. - -Proposed solution: - -1. Capture physical-controller output traffic for DualShock 4, DualSense, and - Switch Pro over the relevant transports used by Steam, SDL, and browser HID - testers. -2. Compare those output and feature reports against the virtual backend's - accepted reports, including report IDs, leading zero/report-ID conventions, - initialization order, and transport-specific payload variants. -3. Extend the report parsers and backend output routing only where a captured - consumer report proves the missing behavior. -4. Add regression tests for each accepted rumble payload shape before relying - on manual testing. -5. Validate with at least one real consumer per platform path. For Steam, the - required proof is a received virtual-control output report or a normalized - rumble callback after triggering controller rumble in Steam's tester. - -Implementation notes: - -- DualShock 4 and DualSense output paths may differ between USB, Bluetooth, and - HIDAPI-initialized reports. -- Switch Pro output may require the native initialization sequence to complete - before rumble packets are accepted. -- Linux `uhid` and Windows VHF have different report-ID delivery behavior, so - both prefixed and unprefixed payloads need explicit coverage where consumers - actually send them. +Status: native output-path validation and the remaining Steam and Chromium +consumer gaps are tracked in [GitHub issue #80](https://github.com/LizardByte/libvirtualhid/issues/80). diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 03c9dd1..d588464 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -133,6 +133,14 @@ elseif(WIN32) hid setupapi winhttp) + + if(TARGET SDL3::SDL3-static) + target_compile_definitions(${TEST_BINARY} PRIVATE LIBVIRTUALHID_TEST_HAS_SDL3=1) + target_link_libraries(${TEST_BINARY} PRIVATE SDL3::SDL3-static) + elseif(TARGET SDL3::SDL3) + target_compile_definitions(${TEST_BINARY} PRIVATE LIBVIRTUALHID_TEST_HAS_SDL3=1) + target_link_libraries(${TEST_BINARY} PRIVATE SDL3::SDL3) + endif() endif() if(CMAKE_SYSTEM_NAME STREQUAL "Linux" AND LIBVIRTUALHID_ENABLE_XTEST AND X11_FOUND AND X11_XTest_FOUND) diff --git a/tests/unit/test_windows_consumers.cpp b/tests/unit/test_windows_consumers.cpp index 368d75e..a451e05 100644 --- a/tests/unit/test_windows_consumers.cpp +++ b/tests/unit/test_windows_consumers.cpp @@ -21,6 +21,10 @@ #include #include +#if defined(LIBVIRTUALHID_TEST_HAS_SDL3) + #include +#endif + // standard includes #include #include @@ -373,8 +377,127 @@ namespace { std::size_t next_output_ = 0; }; +#if defined(LIBVIRTUALHID_TEST_HAS_SDL3) + using SdlGamepad = std::unique_ptr; + + class SdlGamepadSubsystem { + public: + SdlGamepadSubsystem() { + SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1"); + SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI, "1"); + SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS4, "1"); + SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS5, "1"); + SDL_SetHint(SDL_HINT_JOYSTICK_ENHANCED_REPORTS, "1"); + initialized_ = SDL_Init(SDL_INIT_GAMEPAD | SDL_INIT_JOYSTICK | SDL_INIT_EVENTS); + } + + SdlGamepadSubsystem(const SdlGamepadSubsystem &) = delete; + SdlGamepadSubsystem &operator=(const SdlGamepadSubsystem &) = delete; + + ~SdlGamepadSubsystem() { + if (initialized_) { + SDL_Quit(); + } + } + + bool initialized() const { + return initialized_; + } + + private: + bool initialized_ = false; + }; + + std::set current_sdl_gamepads() { + auto count = 0; + auto *gamepads = SDL_GetGamepads(&count); + std::set result; + if (gamepads != nullptr) { + result.insert(gamepads, gamepads + count); + SDL_free(gamepads); + } + return result; + } + + SdlGamepad wait_for_new_sdl_gamepad( + const std::set &previous_gamepads, + std::uint16_t vendor_id, + std::uint16_t product_id + ) { + const auto deadline = std::chrono::steady_clock::now() + 10s; + while (std::chrono::steady_clock::now() < deadline) { + SDL_UpdateGamepads(); + auto count = 0; + auto *gamepads = SDL_GetGamepads(&count); + for (auto index = 0; index < count; ++index) { + const auto gamepad_id = gamepads[index]; + if ( + !previous_gamepads.contains(gamepad_id) && SDL_GetGamepadVendorForID(gamepad_id) == vendor_id && + SDL_GetGamepadProductForID(gamepad_id) == product_id + ) { + auto *opened = SDL_OpenGamepad(gamepad_id); + SDL_free(gamepads); + return {opened, &SDL_CloseGamepad}; + } + } + SDL_free(gamepads); + std::this_thread::sleep_for(100ms); + } + return {nullptr, &SDL_CloseGamepad}; + } +#endif + } // namespace +#if defined(LIBVIRTUALHID_TEST_HAS_SDL3) +TEST_F(WindowsConsumerTest, SdlHidapiRumbleReachesPlayStationAndSwitchCallbacks) { + SdlGamepadSubsystem sdl; + ASSERT_TRUE(sdl.initialized()) << SDL_GetError(); + + lvh::RuntimeOptions runtime_options; + runtime_options.backend = lvh::BackendKind::platform_default; + auto runtime = lvh::Runtime::create(runtime_options); + ASSERT_NE(runtime, nullptr); + ASSERT_TRUE(runtime->capabilities().supports_gamepad) + << "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::switch_pro(), + }; + for (const auto &profile : profiles) { + SCOPED_TRACE(profile.name); + const auto previous_gamepads = current_sdl_gamepads(); + + lvh::CreateGamepadOptions options; + options.profile = profile; + options.metadata.stable_id = "02:11:22:33:44:55"; + auto created = lvh::GamepadStateAdapter::create(*runtime, options); + ASSERT_TRUE(created) << created.status.message(); + GamepadOutputCapture output_capture; + output_capture.attach(*created.adapter); + + auto gamepad = wait_for_new_sdl_gamepad(previous_gamepads, profile.vendor_id, profile.product_id); + ASSERT_NE(gamepad.get(), nullptr) << SDL_GetError(); + auto *joystick = SDL_GetGamepadJoystick(gamepad.get()); + ASSERT_NE(joystick, nullptr) << SDL_GetError(); + const auto properties = SDL_GetGamepadProperties(gamepad.get()); + ASSERT_NE(properties, 0U) << SDL_GetError(); + 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(); + + const auto rumble = output_capture.wait_for_rumble(true); + ASSERT_TRUE(rumble.has_value()) << "No normalized rumble callback followed SDL HIDAPI rumble"; + EXPECT_GT(rumble->low_frequency_rumble, 0U); + EXPECT_GT(rumble->high_frequency_rumble, 0U); + + gamepad.reset(); + ASSERT_TRUE(created.adapter->close().ok()); + } +} +#endif + TEST_F(WindowsConsumerTest, NativePlayStationFeatureAndOutputReportsReachOwningRuntime) { struct NativePlayStationCase { lvh::DeviceProfile profile;