/** * @file tests/unit/platform/test_virtualhid_input.cpp * @brief Tests for shared libvirtualhid input helpers. */ #include "../../tests_common.h" // standard includes #include #include #include #include #include #include #include #include #include // local includes #include "src/config.h" #include "src/globals.h" #include "src/platform/virtualhid_input.h" using namespace std::chrono_literals; using namespace std::literals; namespace { /** * @brief Expected touchpad support for a configured gamepad. */ struct gamepad_touchpad_case_t { std::string_view gamepad; ///< Configured gamepad name. bool expected; ///< Whether the configured gamepad supports touchpad input. }; /** * @brief Parameterized fixture that restores the configured gamepad after each test. */ class VirtualHidInputTest: public ::testing::TestWithParam { protected: /** * @brief Preserve the configured gamepad. */ void SetUp() override { original_gamepad = config::input.gamepad; } /** * @brief Restore the configured gamepad. */ void TearDown() override { config::input.gamepad = std::move(original_gamepad); } private: std::string original_gamepad; ///< Configured gamepad restored after each test. }; } // namespace TEST_P(VirtualHidInputTest, ReportsExpectedTouchpadSupport) { const auto &[gamepad, expected] = GetParam(); config::input.gamepad = gamepad; EXPECT_EQ(platf::virtualhid::configured_gamepad_supports_touchpad(), expected) << gamepad; } INSTANTIATE_TEST_SUITE_P( ConfiguredGamepads, VirtualHidInputTest, ::testing::Values( gamepad_touchpad_case_t {"auto"sv, true}, gamepad_touchpad_case_t {"generic"sv, false}, gamepad_touchpad_case_t {"x360"sv, false}, gamepad_touchpad_case_t {"xone"sv, false}, gamepad_touchpad_case_t {"xseries"sv, false}, gamepad_touchpad_case_t {"ds4"sv, true}, gamepad_touchpad_case_t {"ds5"sv, true}, gamepad_touchpad_case_t {"switch"sv, false} ), [](const ::testing::TestParamInfo &info) { return std::string {info.param.gamepad}; } ); namespace { /** * @brief Fixture backed by libvirtualhid's observable in-memory devices. */ class VirtualHidDeviceTest: public ::testing::Test { protected: /** * @brief Preserve input configuration and create fake virtual devices. */ void SetUp() override { original_input = config::input; context_ = std::make_unique(lvh::BackendKind::fake); client_ = std::make_unique(*context_); const auto *test_info = ::testing::UnitTest::GetInstance()->current_test_info(); const std::string queue_name = std::string {"virtualhid-input-test-"} + test_info->test_suite_name() + '-' + test_info->name(); feedback_queue_ = mail::man->queue(queue_name); ASSERT_NE(context_->runtime, nullptr); ASSERT_NE(context_->keyboard, nullptr); ASSERT_NE(context_->mouse, nullptr); ASSERT_NE(client_->touch, nullptr); ASSERT_NE(client_->pen, nullptr); } /** * @brief Restore process-wide input configuration. */ void TearDown() override { client_.reset(); context_.reset(); feedback_queue_.reset(); config::input = std::move(original_input); } /** * @brief Allocate a fake gamepad and return its state adapter. * * @param profile Configured Sunshine profile name. * @param type Client-reported controller type. * @param capabilities Client-reported capability flags. * @param global_index Global Sunshine gamepad slot. * @param client_index Client-relative gamepad index. * @return Allocated adapter, or `nullptr` when allocation fails. */ lvh::GamepadStateAdapter *allocate_gamepad( std::string_view profile, std::uint8_t type = LI_CTYPE_UNKNOWN, std::uint16_t capabilities = 0, int global_index = 0, std::uint8_t client_index = 3 ) { config::input.gamepad = profile; const platf::gamepad_id_t id {global_index, client_index}; const platf::gamepad_arrival_t metadata {type, capabilities, 0}; if (platf::virtualhid::alloc_gamepad(*context_, id, metadata, feedback_queue_) != 0) { ADD_FAILURE() << "Failed to allocate fake gamepad profile " << profile; return nullptr; } return platf::virtualhid::gamepad_adapter_for_testing(*context_, global_index); } /** * @brief Access the fake global virtual HID context. * * @return Pointer to the fixture-owned global context. */ platf::virtualhid::input_context_t *context() const { return context_.get(); } /** * @brief Access the fake per-client virtual HID context. * * @return Pointer to the fixture-owned client context. */ platf::virtualhid::client_context_t *client() const { return client_.get(); } /** * @brief Access the gamepad feedback queue. * * @return Reference to the fixture-owned feedback queue. */ platf::feedback_queue_t &feedback_queue() { return feedback_queue_; } private: config::input_t original_input; ///< Input configuration restored after each test. std::unique_ptr context_; ///< Shared fake virtual devices. std::unique_ptr client_; ///< Per-client fake touch and pen devices. platf::feedback_queue_t feedback_queue_; ///< Captured gamepad feedback messages. }; /** * @brief Auto-selection scenario for a client-reported gamepad. */ struct auto_profile_case_t { const char *name; ///< Stable parameter name. std::uint8_t type; ///< Client-reported controller type. std::uint16_t capabilities; ///< Client-reported capability flags. lvh::GamepadProfileKind expected; ///< Expected libvirtualhid profile. }; /** * @brief Parameterized fixture for automatic gamepad profile selection. */ class VirtualHidAutoProfileTest: public VirtualHidDeviceTest, public ::testing::WithParamInterface {}; } // namespace TEST_F(VirtualHidDeviceTest, CreatesEveryDeviceWithFakeRuntime) { EXPECT_EQ(context()->runtime->backend_kind(), lvh::BackendKind::fake); EXPECT_EQ(context()->runtime->capabilities().backend_name, "fake"); EXPECT_TRUE(context()->runtime->capabilities().supports_gamepad); EXPECT_TRUE(context()->keyboard->is_open()); EXPECT_TRUE(context()->mouse->is_open()); EXPECT_TRUE(client()->touch->is_open()); EXPECT_TRUE(client()->pen->is_open()); const auto runtime = platf::virtualhid::create_runtime(lvh::BackendKind::fake); ASSERT_NE(runtime, nullptr); EXPECT_EQ(runtime->backend_kind(), lvh::BackendKind::fake); } TEST_F(VirtualHidDeviceTest, ReportsStaticAndRuntimeGamepadChoices) { constexpr std::array expected_names {"auto"sv, "generic"sv, "x360"sv, "xone"sv, "xseries"sv, "ds4"sv, "ds5"sv, "switch"sv}; const auto static_gamepads = platf::virtualhid::static_supported_gamepads(); ASSERT_EQ(static_gamepads.size(), expected_names.size()); EXPECT_TRUE(static_gamepads.front().is_enabled); for (std::size_t index = 0; index < expected_names.size(); ++index) { EXPECT_EQ(static_gamepads[index].name, expected_names[index]); EXPECT_TRUE(static_gamepads[index].reason_disabled.empty()); } const auto available_gamepads = platf::virtualhid::supported_gamepads(context()->runtime.get()); ASSERT_EQ(available_gamepads.size(), expected_names.size()); for (const auto &gamepad : available_gamepads) { EXPECT_TRUE(gamepad.is_enabled) << gamepad.name; EXPECT_TRUE(gamepad.reason_disabled.empty()) << gamepad.name; } const auto no_runtime_gamepads = platf::virtualhid::supported_gamepads(nullptr, true); EXPECT_EQ(no_runtime_gamepads.size(), expected_names.size()); } TEST_F(VirtualHidDeviceTest, RejectsUnavailableAndInvalidGamepadSlots) { const platf::gamepad_arrival_t metadata {LI_CTYPE_XBOX, 0, 0}; const platf::gamepad_id_t valid_id {0, 0}; platf::virtualhid::input_context_t no_runtime {lvh::BackendKind::fake}; no_runtime.runtime.reset(); EXPECT_EQ(platf::virtualhid::alloc_gamepad(no_runtime, valid_id, metadata, nullptr), -1); EXPECT_EQ(platf::virtualhid::alloc_gamepad(*context(), {-1, 0}, metadata, nullptr), -1); EXPECT_EQ(platf::virtualhid::alloc_gamepad(*context(), {static_cast(context()->gamepads.size()), 0}, metadata, nullptr), -1); EXPECT_FALSE(platf::virtualhid::has_gamepad(*context(), -1)); EXPECT_FALSE(platf::virtualhid::has_gamepad(*context(), static_cast(context()->gamepads.size()))); EXPECT_EQ(platf::virtualhid::gamepad_adapter_for_testing(*context(), 0), nullptr); EXPECT_EQ(platf::virtualhid::rebind_gamepad(*context(), valid_id, feedback_queue()), -1); } TEST_F(VirtualHidDeviceTest, AllocatesManualProfileAndTranslatesFullState) { const auto active_devices_before_gamepad = context()->runtime->active_device_count(); config::input.ds4_back_as_touchpad_click = true; config::input.virtualhid_randomize_mac = false; const auto capabilities = static_cast(LI_CCAP_ACCEL | LI_CCAP_GYRO | LI_CCAP_TOUCHPAD | LI_CCAP_RGB_LED | LI_CCAP_BATTERY_STATE); auto *adapter = allocate_gamepad("ds5"sv, LI_CTYPE_PS, capabilities, 1, 4); ASSERT_NE(adapter, nullptr); ASSERT_NE(adapter->gamepad(), nullptr); EXPECT_EQ(context()->runtime->active_device_count(), active_devices_before_gamepad + 1); EXPECT_EQ(adapter->gamepad()->profile().gamepad_kind, lvh::GamepadProfileKind::dualsense); EXPECT_TRUE(adapter->gamepad()->profile().name.starts_with("Sunshine ")); const auto &metadata = adapter->gamepad()->metadata(); EXPECT_EQ(metadata.global_index, 1); EXPECT_EQ(metadata.client_relative_index, 4); EXPECT_EQ(metadata.client_type, lvh::ClientControllerType::playstation); EXPECT_TRUE(metadata.has_motion_sensors); EXPECT_TRUE(metadata.has_touchpad); EXPECT_TRUE(metadata.has_rgb_led); EXPECT_TRUE(metadata.has_battery); EXPECT_EQ(metadata.stable_id, "02:00:00:00:00:01"); const std::uint32_t button_flags = platf::DPAD_UP | platf::DPAD_DOWN | platf::DPAD_LEFT | platf::DPAD_RIGHT | platf::START | platf::BACK | platf::LEFT_STICK | platf::RIGHT_STICK | platf::LEFT_BUTTON | platf::RIGHT_BUTTON | platf::HOME | platf::A | platf::B | platf::X | platf::Y | platf::MISC_BUTTON; const platf::gamepad_state_t input_state { button_flags, 128, 255, std::numeric_limits::min(), std::numeric_limits::max(), -16384, 0, }; platf::virtualhid::gamepad_update(*context(), 1, input_state); const auto &state = adapter->state(); using enum lvh::GamepadButton; for (const auto button : {dpad_up, dpad_down, dpad_left, dpad_right, start, back, left_stick, right_stick, left_shoulder, right_shoulder, guide, a, b, x, y, misc1, touchpad}) { EXPECT_TRUE(state.buttons.test(button)); } EXPECT_FLOAT_EQ(state.left_stick.x, -1.0F); EXPECT_FLOAT_EQ(state.left_stick.y, 1.0F); EXPECT_FLOAT_EQ(state.right_stick.x, -0.5F); EXPECT_FLOAT_EQ(state.right_stick.y, 0.0F); EXPECT_FLOAT_EQ(state.left_trigger, 128.0F / 255.0F); EXPECT_FLOAT_EQ(state.right_trigger, 1.0F); config::input.ds4_back_as_touchpad_click = false; platf::virtualhid::gamepad_update(*context(), 1, {platf::BACK, 0, 0, 0, 0, 0, 0}); EXPECT_FALSE(adapter->state().buttons.test(touchpad)); platf::virtualhid::free_gamepad(*context(), 1); EXPECT_FALSE(platf::virtualhid::has_gamepad(*context(), 1)); EXPECT_EQ(context()->runtime->active_device_count(), active_devices_before_gamepad); platf::virtualhid::free_gamepad(*context(), 1); platf::virtualhid::gamepad_update(*context(), 1, input_state); } TEST_P(VirtualHidAutoProfileTest, SelectsProfileFromClientMetadata) { const auto &test_case = GetParam(); auto *adapter = allocate_gamepad("auto"sv, test_case.type, test_case.capabilities); ASSERT_NE(adapter, nullptr); ASSERT_NE(adapter->gamepad(), nullptr); EXPECT_EQ(adapter->gamepad()->profile().gamepad_kind, test_case.expected); } INSTANTIATE_TEST_SUITE_P( ClientMetadata, VirtualHidAutoProfileTest, ::testing::Values( auto_profile_case_t {"PlayStation", LI_CTYPE_PS, 0, lvh::GamepadProfileKind::dualsense}, auto_profile_case_t {"Nintendo", LI_CTYPE_NINTENDO, 0, lvh::GamepadProfileKind::switch_pro}, auto_profile_case_t {"Xbox", LI_CTYPE_XBOX, 0, lvh::GamepadProfileKind::xbox_series}, auto_profile_case_t {"UnknownMotion", LI_CTYPE_UNKNOWN, LI_CCAP_ACCEL, lvh::GamepadProfileKind::dualsense}, auto_profile_case_t {"UnknownTouchpad", LI_CTYPE_UNKNOWN, LI_CCAP_TOUCHPAD, lvh::GamepadProfileKind::dualsense}, auto_profile_case_t {"UnknownDefault", LI_CTYPE_UNKNOWN, 0, lvh::GamepadProfileKind::xbox_series} ), [](const ::testing::TestParamInfo &info) { return info.param.name; } ); TEST_F(VirtualHidDeviceTest, FallsBackForUnknownManualProfileAndRandomizesPlayStationIdentity) { auto *fallback = allocate_gamepad("not-a-profile"sv, LI_CTYPE_UNKNOWN, 0, 0); ASSERT_NE(fallback, nullptr); EXPECT_EQ(fallback->gamepad()->profile().gamepad_kind, lvh::GamepadProfileKind::xbox_series); EXPECT_EQ(fallback->gamepad()->metadata().client_type, lvh::ClientControllerType::unknown); EXPECT_EQ(fallback->gamepad()->metadata().stable_id, "sunshine-gamepad-0"); config::input.virtualhid_randomize_mac = true; auto *randomized = allocate_gamepad("ds4"sv, LI_CTYPE_PS, 0, 1); ASSERT_NE(randomized, nullptr); const auto &stable_id = randomized->gamepad()->metadata().stable_id; EXPECT_EQ(stable_id.size(), 17); EXPECT_TRUE(stable_id.starts_with("02:00:")); } TEST_F(VirtualHidDeviceTest, RoutesAndDeduplicatesGamepadFeedback) { auto *adapter = allocate_gamepad("xseries"sv, LI_CTYPE_XBOX); ASSERT_NE(adapter, nullptr); lvh::GamepadOutput output; output.kind = lvh::GamepadOutputKind::rumble; output.low_frequency_rumble = 100; output.high_frequency_rumble = 200; ASSERT_TRUE(adapter->dispatch_output(output).ok()); auto feedback = feedback_queue()->pop(10ms); ASSERT_TRUE(feedback); EXPECT_EQ(feedback->type, platf::gamepad_feedback_e::rumble); EXPECT_EQ(feedback->id, 3); EXPECT_EQ(feedback->data.rumble.lowfreq, 100); EXPECT_EQ(feedback->data.rumble.highfreq, 200); ASSERT_TRUE(adapter->dispatch_output(output).ok()); EXPECT_FALSE(feedback_queue()->pop(0ms)); output.high_frequency_rumble = 201; ASSERT_TRUE(adapter->dispatch_output(output).ok()); EXPECT_TRUE(feedback_queue()->pop(10ms)); output.kind = lvh::GamepadOutputKind::trigger_rumble; output.left_trigger_rumble = 300; output.right_trigger_rumble = 400; ASSERT_TRUE(adapter->dispatch_output(output).ok()); feedback = feedback_queue()->pop(10ms); ASSERT_TRUE(feedback); EXPECT_EQ(feedback->type, platf::gamepad_feedback_e::rumble_triggers); EXPECT_EQ(feedback->data.rumble_triggers.left_trigger, 300); EXPECT_EQ(feedback->data.rumble_triggers.right_trigger, 400); ASSERT_TRUE(adapter->dispatch_output(output).ok()); EXPECT_FALSE(feedback_queue()->pop(0ms)); ++output.right_trigger_rumble; ASSERT_TRUE(adapter->dispatch_output(output).ok()); EXPECT_TRUE(feedback_queue()->pop(10ms)); output.kind = lvh::GamepadOutputKind::rgb_led; output.red = 10; output.green = 20; output.blue = 30; ASSERT_TRUE(adapter->dispatch_output(output).ok()); feedback = feedback_queue()->pop(10ms); ASSERT_TRUE(feedback); EXPECT_EQ(feedback->type, platf::gamepad_feedback_e::set_rgb_led); EXPECT_EQ(feedback->data.rgb_led.r, 10); EXPECT_EQ(feedback->data.rgb_led.g, 20); EXPECT_EQ(feedback->data.rgb_led.b, 30); ASSERT_TRUE(adapter->dispatch_output(output).ok()); EXPECT_FALSE(feedback_queue()->pop(0ms)); ++output.blue; ASSERT_TRUE(adapter->dispatch_output(output).ok()); EXPECT_TRUE(feedback_queue()->pop(10ms)); output.kind = lvh::GamepadOutputKind::adaptive_triggers; output.adaptive_trigger_flags = 5; output.left_trigger_effect_type = 6; output.right_trigger_effect_type = 7; output.left_trigger_effect = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; output.right_trigger_effect = {9, 8, 7, 6, 5, 4, 3, 2, 1, 0}; ASSERT_TRUE(adapter->dispatch_output(output).ok()); feedback = feedback_queue()->pop(10ms); ASSERT_TRUE(feedback); EXPECT_EQ(feedback->type, platf::gamepad_feedback_e::set_adaptive_triggers); EXPECT_EQ(feedback->data.adaptive_triggers.event_flags, 5); EXPECT_EQ(feedback->data.adaptive_triggers.type_left, 6); EXPECT_EQ(feedback->data.adaptive_triggers.type_right, 7); EXPECT_EQ(feedback->data.adaptive_triggers.left, output.left_trigger_effect); EXPECT_EQ(feedback->data.adaptive_triggers.right, output.right_trigger_effect); auto resumed_feedback = mail::man->queue("virtualhid-input-test-resumed-feedback"); ASSERT_EQ(platf::virtualhid::rebind_gamepad(*context(), {0, 7}, resumed_feedback), 0); output.kind = lvh::GamepadOutputKind::rumble; output.low_frequency_rumble = 100; output.high_frequency_rumble = 201; ASSERT_TRUE(adapter->dispatch_output(output).ok()); feedback = resumed_feedback->pop(10ms); ASSERT_TRUE(feedback); EXPECT_EQ(feedback->type, platf::gamepad_feedback_e::rumble); EXPECT_EQ(feedback->id, 7); EXPECT_FALSE(feedback_queue()->pop(0ms)); output.kind = lvh::GamepadOutputKind::raw_report; ASSERT_TRUE(adapter->dispatch_output(output).ok()); EXPECT_FALSE(feedback_queue()->pop(0ms)); platf::virtualhid::free_gamepad(*context(), 0); const platf::gamepad_id_t id {0, 0}; const platf::gamepad_arrival_t metadata {LI_CTYPE_XBOX, 0, 0}; ASSERT_EQ(platf::virtualhid::alloc_gamepad(*context(), id, metadata, nullptr), 0); adapter = platf::virtualhid::gamepad_adapter_for_testing(*context(), 0); ASSERT_NE(adapter, nullptr); output.kind = lvh::GamepadOutputKind::rumble; ASSERT_TRUE(adapter->dispatch_output(output).ok()); } TEST_F(VirtualHidDeviceTest, TranslatesGamepadTouchMotionAndBattery) { const auto capabilities = static_cast(LI_CCAP_ACCEL | LI_CCAP_GYRO | LI_CCAP_TOUCHPAD | LI_CCAP_BATTERY_STATE); auto *adapter = allocate_gamepad("ds5"sv, LI_CTYPE_PS, capabilities); ASSERT_NE(adapter, nullptr); EXPECT_TRUE(feedback_queue()->pop(10ms)); EXPECT_TRUE(feedback_queue()->pop(10ms)); platf::gamepad_touch_t touch {{0, 3}, LI_TOUCH_EVENT_DOWN, 10, -0.25F, 1.25F, 1.0F}; platf::virtualhid::gamepad_touch(*context(), touch); EXPECT_EQ(adapter->state().touchpad_contacts[0].id, 0); EXPECT_TRUE(adapter->state().touchpad_contacts[0].active); EXPECT_FLOAT_EQ(adapter->state().touchpad_contacts[0].x, 0.0F); EXPECT_FLOAT_EQ(adapter->state().touchpad_contacts[0].y, 1.0F); touch.eventType = LI_TOUCH_EVENT_MOVE; touch.pressure = 0.25F; touch.x = 0.4F; touch.y = 0.6F; platf::virtualhid::gamepad_touch(*context(), touch); EXPECT_FALSE(adapter->state().touchpad_contacts[0].active); EXPECT_FLOAT_EQ(adapter->state().touchpad_contacts[0].x, 0.4F); touch.eventType = LI_TOUCH_EVENT_DOWN; touch.pointerId = 11; platf::virtualhid::gamepad_touch(*context(), touch); const auto full_touch_submit_count = adapter->gamepad()->submit_count(); touch.pointerId = 12; platf::virtualhid::gamepad_touch(*context(), touch); EXPECT_EQ(adapter->gamepad()->submit_count(), full_touch_submit_count); touch.eventType = LI_TOUCH_EVENT_MOVE; touch.pointerId = 99; platf::virtualhid::gamepad_touch(*context(), touch); EXPECT_EQ(adapter->gamepad()->submit_count(), full_touch_submit_count); touch.eventType = 0xFF; touch.pointerId = 10; platf::virtualhid::gamepad_touch(*context(), touch); EXPECT_EQ(adapter->gamepad()->submit_count(), full_touch_submit_count); touch.eventType = LI_TOUCH_EVENT_CANCEL; platf::virtualhid::gamepad_touch(*context(), touch); EXPECT_FALSE(adapter->state().touchpad_contacts[0].active); touch.eventType = LI_TOUCH_EVENT_UP; touch.pointerId = 11; platf::virtualhid::gamepad_touch(*context(), touch); EXPECT_FALSE(adapter->state().touchpad_contacts[1].active); touch.eventType = LI_TOUCH_EVENT_DOWN; touch.pointerId = 20; platf::virtualhid::gamepad_touch(*context(), touch); touch.pointerId = 21; platf::virtualhid::gamepad_touch(*context(), touch); touch.eventType = LI_TOUCH_EVENT_CANCEL_ALL; platf::virtualhid::gamepad_touch(*context(), touch); EXPECT_FALSE(adapter->state().touchpad_contacts[0].active); EXPECT_FALSE(adapter->state().touchpad_contacts[1].active); const platf::gamepad_motion_t acceleration {{0, 3}, LI_MOTION_TYPE_ACCEL, 1.0F, 2.0F, 3.0F}; platf::virtualhid::gamepad_motion(*context(), acceleration); ASSERT_TRUE(adapter->state().acceleration); EXPECT_FLOAT_EQ(adapter->state().acceleration->x, 1.0F); const platf::gamepad_motion_t gyroscope {{0, 3}, LI_MOTION_TYPE_GYRO, 4.0F, 5.0F, 6.0F}; platf::virtualhid::gamepad_motion(*context(), gyroscope); ASSERT_TRUE(adapter->state().gyroscope); EXPECT_FLOAT_EQ(adapter->state().gyroscope->z, 6.0F); const auto motion_submit_count = adapter->gamepad()->submit_count(); platf::virtualhid::gamepad_motion(*context(), {{0, 3}, 0xFF, 0.0F, 0.0F, 0.0F}); platf::virtualhid::gamepad_motion(*context(), {{7, 3}, LI_MOTION_TYPE_ACCEL, 0.0F, 0.0F, 0.0F}); EXPECT_EQ(adapter->gamepad()->submit_count(), motion_submit_count); platf::virtualhid::gamepad_battery(*context(), {{0, 3}, LI_BATTERY_STATE_DISCHARGING, 75}); ASSERT_TRUE(adapter->state().battery); EXPECT_EQ(adapter->state().battery->state, lvh::GamepadBatteryState::discharging); EXPECT_EQ(adapter->state().battery->percentage, 75); platf::virtualhid::gamepad_battery(*context(), {{0, 3}, LI_BATTERY_STATE_CHARGING, LI_BATTERY_PERCENTAGE_UNKNOWN}); EXPECT_EQ(adapter->state().battery->state, lvh::GamepadBatteryState::charging); EXPECT_EQ(adapter->state().battery->percentage, 100); platf::virtualhid::gamepad_battery(*context(), {{0, 3}, LI_BATTERY_STATE_FULL, 120}); EXPECT_EQ(adapter->state().battery->state, lvh::GamepadBatteryState::full); EXPECT_EQ(adapter->state().battery->percentage, 100); platf::virtualhid::gamepad_battery(*context(), {{0, 3}, LI_BATTERY_STATE_NOT_CHARGING, 50}); EXPECT_EQ(adapter->state().battery->state, lvh::GamepadBatteryState::charging_error); platf::virtualhid::gamepad_battery(*context(), {{0, 3}, LI_BATTERY_STATE_UNKNOWN, 0}); EXPECT_FALSE(adapter->state().battery); platf::virtualhid::gamepad_battery(*context(), {{0, 3}, LI_BATTERY_STATE_NOT_PRESENT, 0}); EXPECT_FALSE(adapter->state().battery); platf::virtualhid::gamepad_battery(*context(), {{7, 3}, LI_BATTERY_STATE_FULL, 100}); const auto unsupported_capabilities = static_cast(LI_CCAP_ACCEL | LI_CCAP_TOUCHPAD | LI_CCAP_RGB_LED); auto *unsupported = allocate_gamepad("xseries"sv, LI_CTYPE_XBOX, unsupported_capabilities, 1); ASSERT_NE(unsupported, nullptr); const auto unsupported_count = unsupported->gamepad()->submit_count(); platf::virtualhid::gamepad_touch(*context(), {{1, 3}, LI_TOUCH_EVENT_DOWN, 0, 0.5F, 0.5F, 1.0F}); EXPECT_EQ(unsupported->gamepad()->submit_count(), unsupported_count); } TEST_F(VirtualHidDeviceTest, TranslatesMouseAndKeyboardInput) { platf::virtualhid::move_mouse(*context(), -4, 7); auto mouse_event = context()->mouse->last_submitted_event(); EXPECT_EQ(mouse_event.kind, lvh::MouseEventKind::relative_motion); EXPECT_EQ(mouse_event.x, -4); EXPECT_EQ(mouse_event.y, 7); const platf::touch_port_t viewport {10, 20, 1920, 1080, 1920, 1080}; platf::virtualhid::abs_mouse(*context(), viewport, 10.6F, 20.4F); mouse_event = context()->mouse->last_submitted_event(); EXPECT_EQ(mouse_event.kind, lvh::MouseEventKind::absolute_motion); EXPECT_EQ(mouse_event.x, 11); EXPECT_EQ(mouse_event.y, 20); EXPECT_EQ(mouse_event.width, 1920); EXPECT_EQ(mouse_event.height, 1080); constexpr std::array button_cases { std::pair {BUTTON_LEFT, lvh::MouseButton::left}, std::pair {BUTTON_MIDDLE, lvh::MouseButton::middle}, std::pair {BUTTON_RIGHT, lvh::MouseButton::right}, std::pair {BUTTON_X1, lvh::MouseButton::side}, std::pair {BUTTON_X2, lvh::MouseButton::extra}, }; for (const auto &[button, expected] : button_cases) { platf::virtualhid::button_mouse(*context(), button, false); mouse_event = context()->mouse->last_submitted_event(); EXPECT_EQ(mouse_event.kind, lvh::MouseEventKind::button); EXPECT_EQ(mouse_event.button, expected); EXPECT_TRUE(mouse_event.pressed); platf::virtualhid::button_mouse(*context(), button, true); EXPECT_FALSE(context()->mouse->last_submitted_event().pressed); } const auto button_submit_count = context()->mouse->submit_count(); platf::virtualhid::button_mouse(*context(), 999, false); EXPECT_EQ(context()->mouse->submit_count(), button_submit_count); platf::virtualhid::scroll(*context(), 120); mouse_event = context()->mouse->last_submitted_event(); EXPECT_EQ(mouse_event.kind, lvh::MouseEventKind::vertical_scroll); EXPECT_EQ(mouse_event.high_resolution_scroll, 120); platf::virtualhid::hscroll(*context(), -240); mouse_event = context()->mouse->last_submitted_event(); EXPECT_EQ(mouse_event.kind, lvh::MouseEventKind::horizontal_scroll); EXPECT_EQ(mouse_event.high_resolution_scroll, -240); config::input.always_send_scancodes = true; platf::virtualhid::keyboard_update(*context(), 0x41, false, 0); auto keyboard_event = context()->keyboard->last_submitted_event(); EXPECT_EQ(keyboard_event.key_code, 0x41); EXPECT_TRUE(keyboard_event.pressed); #ifdef _WIN32 EXPECT_TRUE(keyboard_event.uses_normalized_key_code); EXPECT_TRUE(keyboard_event.prefer_native_scan_code); platf::virtualhid::keyboard_update(*context(), 0x41, true, SS_KBE_FLAG_NON_NORMALIZED); keyboard_event = context()->keyboard->last_submitted_event(); EXPECT_FALSE(keyboard_event.uses_normalized_key_code); #else EXPECT_FALSE(keyboard_event.uses_normalized_key_code); EXPECT_FALSE(keyboard_event.prefer_native_scan_code); platf::virtualhid::keyboard_update(*context(), 0x41, true, 0xFF); keyboard_event = context()->keyboard->last_submitted_event(); #endif EXPECT_FALSE(keyboard_event.pressed); const auto keyboard_submit_count = context()->keyboard->submit_count(); const std::string text = "Sunshine \u{2600}"; platf::virtualhid::unicode(*context(), text.data(), static_cast(text.size())); EXPECT_EQ(context()->keyboard->submit_count(), keyboard_submit_count + 1); platf::virtualhid::unicode(*context(), nullptr, 1); platf::virtualhid::unicode(*context(), text.data(), 0); EXPECT_EQ(context()->keyboard->submit_count(), keyboard_submit_count + 1); ASSERT_TRUE(context()->mouse->close().ok()); ASSERT_TRUE(context()->keyboard->close().ok()); platf::virtualhid::move_mouse(*context(), 1, 1); platf::virtualhid::keyboard_update(*context(), 0x41, false, 0); context()->mouse.reset(); context()->keyboard.reset(); platf::virtualhid::move_mouse(*context(), 1, 1); platf::virtualhid::abs_mouse(*context(), viewport, 1.0F, 1.0F); platf::virtualhid::button_mouse(*context(), BUTTON_LEFT, false); platf::virtualhid::scroll(*context(), 1); platf::virtualhid::hscroll(*context(), 1); platf::virtualhid::keyboard_update(*context(), 0x41, false, 0); platf::virtualhid::unicode(*context(), text.data(), static_cast(text.size())); } TEST_F(VirtualHidDeviceTest, TranslatesTouchscreenLifecycleAndGeometry) { const platf::touch_port_t viewport {10, 20, 800, 600, 400, 300}; platf::touch_input_t touch {LI_TOUCH_EVENT_DOWN, 100, 7, -0.1F, 1.1F, 1.2F, 15.0F, 10.0F}; platf::virtualhid::touch_update(*client(), viewport, touch); auto contact = client()->touch->last_submitted_contact(); EXPECT_EQ(contact.id, 7); EXPECT_FLOAT_EQ(contact.x, 0.0F); EXPECT_FLOAT_EQ(contact.y, 1.0F); EXPECT_FLOAT_EQ(contact.pressure, 1.0F); EXPECT_EQ(contact.orientation, 80); EXPECT_TRUE(contact.touching); EXPECT_EQ(contact.viewport.offset_x, 10); EXPECT_EQ(contact.viewport.offset_y, 20); EXPECT_EQ(contact.viewport.width, 800); EXPECT_EQ(contact.viewport.height, 600); EXPECT_FLOAT_EQ(contact.contact_major_axis, 15.0F); EXPECT_FLOAT_EQ(contact.contact_minor_axis, 10.0F); EXPECT_TRUE(client()->active_touches.contains(7)); touch.eventType = LI_TOUCH_EVENT_HOVER; touch.rotation = 300; touch.pointerId = 8; touch.pressureOrDistance = 0.25F; platf::virtualhid::touch_update(*client(), viewport, touch); contact = client()->touch->last_submitted_contact(); EXPECT_EQ(contact.orientation, -60); EXPECT_FALSE(contact.touching); EXPECT_TRUE(client()->active_touches.contains(8)); touch.eventType = LI_TOUCH_EVENT_UP; platf::virtualhid::touch_update(*client(), viewport, touch); EXPECT_FALSE(client()->active_touches.contains(8)); touch.eventType = LI_TOUCH_EVENT_CANCEL; touch.pointerId = 7; platf::virtualhid::touch_update(*client(), viewport, touch); EXPECT_FALSE(client()->active_touches.contains(7)); touch.eventType = LI_TOUCH_EVENT_MOVE; touch.rotation = LI_ROT_UNKNOWN; touch.pointerId = 9; platf::virtualhid::touch_update(*client(), viewport, touch); EXPECT_EQ(client()->touch->last_submitted_contact().orientation, 0); touch.eventType = LI_TOUCH_EVENT_HOVER_LEAVE; platf::virtualhid::touch_update(*client(), viewport, touch); EXPECT_FALSE(client()->active_touches.contains(9)); touch.eventType = LI_TOUCH_EVENT_DOWN; touch.pointerId = 10; platf::virtualhid::touch_update(*client(), viewport, touch); touch.pointerId = 11; platf::virtualhid::touch_update(*client(), viewport, touch); const auto before_cancel = client()->touch->submit_count(); touch.eventType = LI_TOUCH_EVENT_CANCEL_ALL; platf::virtualhid::touch_update(*client(), viewport, touch); EXPECT_TRUE(client()->active_touches.empty()); EXPECT_EQ(client()->touch->submit_count(), before_cancel + 2); const auto before_unknown = client()->touch->submit_count(); touch.eventType = 0xFF; platf::virtualhid::touch_update(*client(), viewport, touch); EXPECT_EQ(client()->touch->submit_count(), before_unknown); ASSERT_TRUE(client()->touch->close().ok()); touch.eventType = LI_TOUCH_EVENT_DOWN; platf::virtualhid::touch_update(*client(), viewport, touch); client()->touch.reset(); platf::virtualhid::touch_update(*client(), viewport, touch); } TEST_F(VirtualHidDeviceTest, TranslatesPenButtonsToolsAndTransitions) { const platf::touch_port_t viewport {1, 2, 1000, 500, 1000, 500}; platf::pen_input_t pen { LI_TOUCH_EVENT_DOWN, LI_TOOL_TYPE_PEN, static_cast(LI_PEN_BUTTON_PRIMARY | LI_PEN_BUTTON_SECONDARY), 45, 450, -0.25F, 1.25F, 1.5F, 0.0F, 0.0F, }; platf::virtualhid::pen_update(*client(), viewport, pen); auto state = client()->pen->last_submitted_tool(); EXPECT_EQ(state.tool, lvh::PenToolType::pen); EXPECT_FLOAT_EQ(state.x, 0.0F); EXPECT_FLOAT_EQ(state.y, 1.0F); EXPECT_FLOAT_EQ(state.pressure, 1.0F); EXPECT_FLOAT_EQ(state.distance, -1.0F); EXPECT_NEAR(state.tilt_x, -45.0F, 0.001F); EXPECT_NEAR(state.tilt_y, 0.0F, 0.001F); EXPECT_EQ(state.transition, lvh::PointerTransition::update); EXPECT_EQ(state.viewport.offset_x, 1); EXPECT_EQ(state.viewport.offset_y, 2); EXPECT_TRUE(client()->pressed_pen_buttons.contains(lvh::PenButton::primary)); EXPECT_TRUE(client()->pressed_pen_buttons.contains(lvh::PenButton::secondary)); const auto before_unchanged_buttons = client()->pen->submit_count(); platf::virtualhid::pen_update(*client(), viewport, pen); EXPECT_EQ(client()->pen->submit_count(), before_unchanged_buttons + 1); pen.eventType = LI_TOUCH_EVENT_HOVER; pen.toolType = LI_TOOL_TYPE_ERASER; pen.penButtons = LI_PEN_BUTTON_TERTIARY; pen.tilt = LI_TILT_UNKNOWN; pen.rotation = LI_ROT_UNKNOWN; pen.pressureOrDistance = 0.4F; platf::virtualhid::pen_update(*client(), viewport, pen); state = client()->pen->last_submitted_tool(); EXPECT_EQ(state.tool, lvh::PenToolType::eraser); EXPECT_FLOAT_EQ(state.pressure, -1.0F); EXPECT_FLOAT_EQ(state.distance, 0.4F); EXPECT_FLOAT_EQ(state.tilt_x, 0.0F); EXPECT_FLOAT_EQ(state.tilt_y, 0.0F); EXPECT_TRUE(client()->pressed_pen_buttons.contains(lvh::PenButton::tertiary)); EXPECT_FALSE(client()->pressed_pen_buttons.contains(lvh::PenButton::primary)); pen.eventType = LI_TOUCH_EVENT_UP; pen.toolType = LI_TOOL_TYPE_UNKNOWN; pen.penButtons = 0; platf::virtualhid::pen_update(*client(), viewport, pen); EXPECT_EQ(client()->pen->last_submitted_tool().tool, lvh::PenToolType::unchanged); EXPECT_EQ(client()->pen->last_submitted_tool().transition, lvh::PointerTransition::release); pen.eventType = LI_TOUCH_EVENT_HOVER_LEAVE; platf::virtualhid::pen_update(*client(), viewport, pen); EXPECT_EQ(client()->pen->last_submitted_tool().transition, lvh::PointerTransition::leave); pen.eventType = LI_TOUCH_EVENT_CANCEL; platf::virtualhid::pen_update(*client(), viewport, pen); EXPECT_EQ(client()->pen->last_submitted_tool().transition, lvh::PointerTransition::cancel); pen.eventType = LI_TOUCH_EVENT_DOWN; pen.penButtons = LI_PEN_BUTTON_PRIMARY; platf::virtualhid::pen_update(*client(), viewport, pen); ASSERT_FALSE(client()->pressed_pen_buttons.empty()); pen.eventType = LI_TOUCH_EVENT_CANCEL_ALL; platf::virtualhid::pen_update(*client(), viewport, pen); EXPECT_TRUE(client()->pressed_pen_buttons.empty()); EXPECT_EQ(client()->pen->last_submitted_tool().transition, lvh::PointerTransition::cancel); ASSERT_TRUE(client()->pen->close().ok()); pen.eventType = LI_TOUCH_EVENT_DOWN; platf::virtualhid::pen_update(*client(), viewport, pen); client()->pen.reset(); platf::virtualhid::pen_update(*client(), viewport, pen); } TEST_F(VirtualHidDeviceTest, PlatformWrappersForwardToVirtualHidContext) { auto platform_input = platf::input(); ASSERT_TRUE(platform_input); auto &platform_context = platf::virtualhid::get_input_context(platform_input); platform_context = platf::virtualhid::input_context_t {lvh::BackendKind::fake}; ASSERT_NE(platform_context.mouse, nullptr); ASSERT_NE(platform_context.keyboard, nullptr); const platf::touch_port_t viewport {0, 0, 1280, 720, 1280, 720}; platf::move_mouse(platform_input, 2, 3); EXPECT_EQ(platform_context.mouse->last_submitted_event().kind, lvh::MouseEventKind::relative_motion); platf::abs_mouse(platform_input, viewport, 40.0F, 50.0F); EXPECT_EQ(platform_context.mouse->last_submitted_event().kind, lvh::MouseEventKind::absolute_motion); platf::button_mouse(platform_input, BUTTON_LEFT, false); EXPECT_EQ(platform_context.mouse->last_submitted_event().kind, lvh::MouseEventKind::button); platf::scroll(platform_input, 120); EXPECT_EQ(platform_context.mouse->last_submitted_event().kind, lvh::MouseEventKind::vertical_scroll); platf::hscroll(platform_input, -120); EXPECT_EQ(platform_context.mouse->last_submitted_event().kind, lvh::MouseEventKind::horizontal_scroll); platf::keyboard_update(platform_input, 0x41, false, 0); EXPECT_EQ(platform_context.keyboard->last_submitted_event().key_code, 0x41); const std::string text = "wrapper"; const auto keyboard_count = platform_context.keyboard->submit_count(); platf::unicode(platform_input, text.data(), static_cast(text.size())); EXPECT_EQ(platform_context.keyboard->submit_count(), keyboard_count + 1); config::input.gamepad = "ds5"; const auto capabilities = static_cast(LI_CCAP_ACCEL | LI_CCAP_GYRO | LI_CCAP_TOUCHPAD | LI_CCAP_BATTERY_STATE); const platf::gamepad_id_t gamepad_id {0, 2}; const platf::gamepad_arrival_t gamepad_metadata {LI_CTYPE_PS, capabilities, 0}; ASSERT_EQ(platf::alloc_gamepad(platform_input, gamepad_id, gamepad_metadata, feedback_queue()), 0); auto *adapter = platf::virtualhid::gamepad_adapter_for_testing(platform_context, 0); ASSERT_NE(adapter, nullptr); platf::gamepad_update(platform_input, 0, {platf::A, 255, 0, 0, 0, 0, 0}); EXPECT_TRUE(adapter->state().buttons.test(lvh::GamepadButton::a)); platf::gamepad_touch(platform_input, {{0, 2}, LI_TOUCH_EVENT_DOWN, 1, 0.25F, 0.5F, 1.0F}); EXPECT_TRUE(adapter->state().touchpad_contacts[0].active); platf::gamepad_motion(platform_input, {{0, 2}, LI_MOTION_TYPE_ACCEL, 1.0F, 2.0F, 3.0F}); EXPECT_TRUE(adapter->state().acceleration.has_value()); platf::gamepad_battery(platform_input, {{0, 2}, LI_BATTERY_STATE_FULL, 100}); EXPECT_TRUE(adapter->state().battery.has_value()); const auto &supported = platf::supported_gamepads(std::addressof(platform_input)); ASSERT_FALSE(supported.empty()); EXPECT_TRUE(supported.front().is_enabled); EXPECT_FALSE(platf::supported_gamepads(nullptr).empty()); #ifdef __APPLE__ EXPECT_EQ(platf::get_capabilities() & platf::platform_caps::controller_touch, 0U); #else EXPECT_NE(platf::get_capabilities() & platf::platform_caps::controller_touch, 0U); #endif platf::free_gamepad(platform_input, 0); EXPECT_FALSE(platf::virtualhid::has_gamepad(platform_context, 0)); platf::free_gamepad(platform_input, 0); platf::gamepad_update(platform_input, 0, {}); platf::gamepad_touch(platform_input, {{0, 2}, LI_TOUCH_EVENT_DOWN, 1, 0.25F, 0.5F, 1.0F}); platf::gamepad_motion(platform_input, {{0, 2}, LI_MOTION_TYPE_ACCEL, 1.0F, 2.0F, 3.0F}); platf::gamepad_battery(platform_input, {{0, 2}, LI_BATTERY_STATE_FULL, 100}); auto platform_client = platf::allocate_client_input_context(platform_input); ASSERT_NE(platform_client, nullptr); auto &platform_client_context = platf::virtualhid::get_client_context(platform_client.get()); ASSERT_NE(platform_client_context.touch, nullptr); ASSERT_NE(platform_client_context.pen, nullptr); platf::touch_update(platform_client.get(), viewport, {LI_TOUCH_EVENT_DOWN, 0, 1, 0.25F, 0.5F, 1.0F, 2.0F, 1.0F}); EXPECT_EQ(platform_client_context.touch->last_submitted_contact().id, 1); platf::pen_update(platform_client.get(), viewport, {LI_TOUCH_EVENT_HOVER, LI_TOOL_TYPE_PEN, 0, LI_TILT_UNKNOWN, LI_ROT_UNKNOWN, 0.25F, 0.5F, 0.5F, 0.0F, 0.0F}); EXPECT_EQ(platform_client_context.pen->last_submitted_tool().tool, lvh::PenToolType::pen); platform_context.runtime.reset(); config::input.gamepad = "generic"; EXPECT_EQ(platf::alloc_gamepad(platform_input, gamepad_id, gamepad_metadata, nullptr), -1); }