/** * @file tests/unit/test_input.cpp * @brief Tests for input packet batching, retained stream input, and virtual gamepad lifecycle behavior. */ // test includes #include "../tests_common.h" // standard includes #include #include #include #include #include #include #include #include #include #include #include // moonlight-common-c includes extern "C" { #include } // local includes #include "src/config.h" #include "src/input.h" #include "src/platform/virtualhid_input.h" #include "src/utility.h" namespace { /** * @brief Protocol metadata for a fixed-size input packet. */ struct input_packet_spec_t { std::uint32_t magic; ///< Packet type identifier. std::size_t size; ///< Complete fixed packet size. }; /** * @brief Create raw input bytes with a caller-selected header and buffer size. * * @param magic Packet type identifier. * @param declared_size Packet size declared after the size field. * @param actual_size Number of bytes available in the packet buffer. * @return Raw packet bytes. */ std::vector make_input_packet(std::uint32_t magic, std::uint32_t declared_size, std::size_t actual_size) { std::vector packet(actual_size); const NV_INPUT_HEADER header { util::endian::big(declared_size), util::endian::little(magic), }; if (!packet.empty()) { std::memcpy(packet.data(), &header, std::min(packet.size(), sizeof(header))); } return packet; } /** * @brief Verify batching a repeated packet around a potentially different packet. * * @tparam Packet Fixed-size protocol packet type. * @param original Packet repeated before and after the different packet. * @param different Packet that may introduce a batching boundary. * @param magic Protocol identifier shared by the packets. * @param expected Expected number of dispatched packets. */ template void expect_packet_dispatches(Packet original, Packet different, std::uint32_t magic, std::size_t expected) { SCOPED_TRACE(magic); auto stream_input = input::alloc(std::make_shared(), "packet-batching"); ASSERT_NE(stream_input, nullptr); std::size_t dispatch_count = 0; input::testing::set_input_packet_hook([magic, &dispatch_count](std::uint32_t dispatched_magic) { EXPECT_EQ(dispatched_magic, magic); ++dispatch_count; }); for (auto packet : {original, different, original}) { packet.header.size = util::endian::big(sizeof(packet) - sizeof(packet.header.size)); packet.header.magic = util::endian::little(magic); std::vector bytes(sizeof(packet)); std::memcpy(bytes.data(), &packet, sizeof(packet)); input::passthrough(stream_input, std::move(bytes)); } input::testing::process_queued_messages(stream_input); input::testing::set_input_packet_hook({}); EXPECT_EQ(dispatch_count, expected); EXPECT_EQ(input::testing::queued_input_packet_count(stream_input), 0); } /** * @brief Fixture that installs a fake global virtual input backend. */ class InputGamepadSessionTest: public ::testing::Test { protected: /** * @brief Preserve configuration and install observable fake devices. */ void SetUp() override { original_input_ = config::input; config::input.controller = true; config::input.mouse = true; config::input.gamepad = "xseries"; auto platform_input = platf::input(); ASSERT_TRUE(platform_input); auto &context = platf::virtualhid::get_input_context(platform_input); context = platf::virtualhid::input_context_t {lvh::BackendKind::fake}; context_ = &context; runtime_ = context.runtime.get(); ASSERT_NE(runtime_, nullptr); input::testing::set_platform_input(std::move(platform_input)); } /** * @brief Destroy retained test sessions and restore configuration. */ void TearDown() override { input::testing::set_input_packet_hook({}); input::testing::set_input_task_sink({}); input::terminate_gamepads(); input::testing::set_platform_input({}); context_ = nullptr; runtime_ = nullptr; config::input = std::move(original_input_); } /** * @brief Access the fake runtime installed for the current test. * * @return Fake libvirtualhid runtime. */ lvh::Runtime &runtime() const { return *runtime_; } /** * @brief Access the shared libvirtualhid input context installed for the test. * * @return Fake input context. */ platf::virtualhid::input_context_t &context() const { return *context_; } private: platf::virtualhid::input_context_t *context_ = nullptr; ///< Fake input context installed in the global backend. lvh::Runtime *runtime_ = nullptr; ///< Fake runtime installed in the global input backend. config::input_t original_input_; ///< Input configuration restored after each test. }; } // namespace TEST(InputPacketValidationTest, RejectsEveryBufferShorterThanTheHeader) { for (std::size_t actual_size = 0; actual_size < sizeof(NV_INPUT_HEADER); ++actual_size) { const auto packet = make_input_packet(UTF8_TEXT_EVENT_MAGIC, sizeof(std::uint32_t), actual_size); EXPECT_FALSE(input::testing::is_valid_input_packet(packet)) << "actual_size=" << actual_size; } } TEST(InputPacketValidationTest, RejectsDeclaredSizesOutsideTheAvailableBuffer) { constexpr std::uint32_t unknown_magic = 0x12345678; for (std::uint32_t declared_size = 0; declared_size < sizeof(std::uint32_t); ++declared_size) { const auto packet = make_input_packet(unknown_magic, declared_size, sizeof(NV_INPUT_HEADER)); EXPECT_FALSE(input::testing::is_valid_input_packet(packet)) << "declared_size=" << declared_size; } const auto truncated_packet = make_input_packet(unknown_magic, sizeof(std::uint32_t) + 1, sizeof(NV_INPUT_HEADER)); EXPECT_FALSE(input::testing::is_valid_input_packet(truncated_packet)); const auto overflowing_size = make_input_packet( unknown_magic, std::numeric_limits::max(), sizeof(NV_INPUT_HEADER) ); EXPECT_FALSE(input::testing::is_valid_input_packet(overflowing_size)); } TEST(InputPacketValidationTest, ValidatesUnicodePacketBoundsWithoutOverflow) { const auto empty_packet = make_input_packet(UTF8_TEXT_EVENT_MAGIC, sizeof(std::uint32_t), sizeof(NV_INPUT_HEADER)); EXPECT_TRUE(input::testing::is_valid_input_packet(empty_packet)); constexpr std::uint32_t maximum_declared_size = sizeof(std::uint32_t) + UTF8_TEXT_EVENT_MAX_COUNT; const auto maximum_packet = make_input_packet( UTF8_TEXT_EVENT_MAGIC, maximum_declared_size, sizeof(std::uint32_t) + maximum_declared_size ); EXPECT_TRUE(input::testing::is_valid_input_packet(maximum_packet)); const auto oversized_text = make_input_packet( UTF8_TEXT_EVENT_MAGIC, maximum_declared_size + 1, sizeof(std::uint32_t) + maximum_declared_size + 1 ); EXPECT_FALSE(input::testing::is_valid_input_packet(oversized_text)); const auto truncated_text = make_input_packet(UTF8_TEXT_EVENT_MAGIC, maximum_declared_size, sizeof(NV_INPUT_HEADER)); EXPECT_FALSE(input::testing::is_valid_input_packet(truncated_text)); } TEST(InputPacketValidationTest, EnforcesEveryFixedPacketSize) { constexpr std::array packet_specs { input_packet_spec_t {MOUSE_MOVE_REL_MAGIC_GEN5, sizeof(NV_REL_MOUSE_MOVE_PACKET)}, input_packet_spec_t {MOUSE_MOVE_ABS_MAGIC, sizeof(NV_ABS_MOUSE_MOVE_PACKET)}, input_packet_spec_t {MOUSE_BUTTON_DOWN_EVENT_MAGIC_GEN5, sizeof(NV_MOUSE_BUTTON_PACKET)}, input_packet_spec_t {MOUSE_BUTTON_UP_EVENT_MAGIC_GEN5, sizeof(NV_MOUSE_BUTTON_PACKET)}, input_packet_spec_t {SCROLL_MAGIC_GEN5, sizeof(NV_SCROLL_PACKET)}, input_packet_spec_t {SS_HSCROLL_MAGIC, sizeof(SS_HSCROLL_PACKET)}, input_packet_spec_t {KEY_DOWN_EVENT_MAGIC, sizeof(NV_KEYBOARD_PACKET)}, input_packet_spec_t {KEY_UP_EVENT_MAGIC, sizeof(NV_KEYBOARD_PACKET)}, input_packet_spec_t {MULTI_CONTROLLER_MAGIC_GEN5, sizeof(NV_MULTI_CONTROLLER_PACKET)}, input_packet_spec_t {SS_TOUCH_MAGIC, sizeof(SS_TOUCH_PACKET)}, input_packet_spec_t {SS_PEN_MAGIC, sizeof(SS_PEN_PACKET)}, input_packet_spec_t {SS_CONTROLLER_ARRIVAL_MAGIC, sizeof(SS_CONTROLLER_ARRIVAL_PACKET)}, input_packet_spec_t {SS_CONTROLLER_TOUCH_MAGIC, sizeof(SS_CONTROLLER_TOUCH_PACKET)}, input_packet_spec_t {SS_CONTROLLER_MOTION_MAGIC, sizeof(SS_CONTROLLER_MOTION_PACKET)}, input_packet_spec_t {SS_CONTROLLER_BATTERY_MAGIC, sizeof(SS_CONTROLLER_BATTERY_PACKET)}, }; for (const auto &[magic, size] : packet_specs) { const auto declared_size = static_cast(size - sizeof(std::uint32_t)); const auto valid_packet = make_input_packet(magic, declared_size, size); EXPECT_TRUE(input::testing::is_valid_input_packet(valid_packet)) << "magic=" << magic; const auto trailing_bytes = make_input_packet(magic, declared_size, size + 8); EXPECT_TRUE(input::testing::is_valid_input_packet(trailing_bytes)) << "magic=" << magic; const auto wrong_declared_size = make_input_packet(magic, declared_size - 1, size); EXPECT_FALSE(input::testing::is_valid_input_packet(wrong_declared_size)) << "magic=" << magic; const auto oversized_declared_size = make_input_packet(magic, declared_size + 1, size + 1); EXPECT_FALSE(input::testing::is_valid_input_packet(oversized_declared_size)) << "magic=" << magic; const auto truncated_packet = make_input_packet(magic, declared_size, size - 1); EXPECT_FALSE(input::testing::is_valid_input_packet(truncated_packet)) << "magic=" << magic; } } TEST(InputPacketValidationTest, PreservesUnknownPacketHandlingWithinDeclaredBounds) { constexpr std::uint32_t unknown_magic = 0x12345678; const auto packet = make_input_packet(unknown_magic, sizeof(std::uint32_t), sizeof(NV_INPUT_HEADER)); EXPECT_TRUE(input::testing::is_valid_input_packet(packet)); } TEST_F(InputGamepadSessionTest, RejectsMalformedBatchablePacketsAtQueueIngress) { ASSERT_FALSE(task_pool.running()); const std::shared_ptr empty_input; EXPECT_EQ(input::testing::queued_input_packet_count(empty_input), 0); auto stream_input = input::alloc(std::make_shared(), "packet-validation-client"); ASSERT_NE(stream_input, nullptr); auto short_header = make_input_packet(SS_PEN_MAGIC, sizeof(std::uint32_t), sizeof(NV_INPUT_HEADER) - 1); input::passthrough(stream_input, std::move(short_header)); EXPECT_EQ(input::testing::queued_input_packet_count(stream_input), 0); constexpr std::array batchable_packet_specs { input_packet_spec_t {MOUSE_MOVE_REL_MAGIC_GEN5, sizeof(NV_REL_MOUSE_MOVE_PACKET)}, input_packet_spec_t {MOUSE_MOVE_ABS_MAGIC, sizeof(NV_ABS_MOUSE_MOVE_PACKET)}, input_packet_spec_t {SCROLL_MAGIC_GEN5, sizeof(NV_SCROLL_PACKET)}, input_packet_spec_t {SS_HSCROLL_MAGIC, sizeof(SS_HSCROLL_PACKET)}, input_packet_spec_t {MULTI_CONTROLLER_MAGIC_GEN5, sizeof(NV_MULTI_CONTROLLER_PACKET)}, input_packet_spec_t {SS_TOUCH_MAGIC, sizeof(SS_TOUCH_PACKET)}, input_packet_spec_t {SS_PEN_MAGIC, sizeof(SS_PEN_PACKET)}, input_packet_spec_t {SS_CONTROLLER_TOUCH_MAGIC, sizeof(SS_CONTROLLER_TOUCH_PACKET)}, input_packet_spec_t {SS_CONTROLLER_MOTION_MAGIC, sizeof(SS_CONTROLLER_MOTION_PACKET)}, }; std::size_t valid_packet_count = 0; for (const auto &[magic, size] : batchable_packet_specs) { const auto declared_size = static_cast(size - sizeof(std::uint32_t)); auto truncated_packet = make_input_packet(magic, declared_size, size - 1); input::passthrough(stream_input, std::move(truncated_packet)); EXPECT_EQ(input::testing::queued_input_packet_count(stream_input), valid_packet_count) << "magic=" << magic; auto valid_packet = make_input_packet(magic, declared_size, size); input::passthrough(stream_input, std::move(valid_packet)); ++valid_packet_count; EXPECT_EQ(input::testing::queued_input_packet_count(stream_input), valid_packet_count) << "magic=" << magic; auto oversized_declared_size = make_input_packet(magic, declared_size + 1, size + 1); input::passthrough(stream_input, std::move(oversized_declared_size)); EXPECT_EQ(input::testing::queued_input_packet_count(stream_input), valid_packet_count) << "magic=" << magic; } } TEST_F(InputGamepadSessionTest, SerializesArrivalAndStatePacketsForOneController) { ASSERT_FALSE(task_pool.running()); config::input.gamepad = "auto"; auto stream_input = input::alloc(std::make_shared(), "simultaneous-controller-packets"); ASSERT_NE(stream_input, nullptr); std::vector> scheduled; input::testing::set_input_task_sink([&](std::shared_ptr input) { scheduled.push_back(std::move(input)); }); SS_CONTROLLER_ARRIVAL_PACKET arrival {}; arrival.header.size = util::endian::big(sizeof(arrival) - sizeof(arrival.header.size)); arrival.header.magic = util::endian::little(SS_CONTROLLER_ARRIVAL_MAGIC); arrival.controllerNumber = 0; arrival.type = LI_CTYPE_NINTENDO; NV_MULTI_CONTROLLER_PACKET state {}; state.header.size = util::endian::big(sizeof(state) - sizeof(state.header.size)); state.header.magic = util::endian::little(MULTI_CONTROLLER_MAGIC_GEN5); state.controllerNumber = 0; state.activeGamepadMask = 1; std::vector arrival_bytes(sizeof(arrival)); std::memcpy(arrival_bytes.data(), &arrival, sizeof(arrival)); input::passthrough(stream_input, std::move(arrival_bytes)); ASSERT_EQ(scheduled.size(), 1); EXPECT_EQ(scheduled.front(), stream_input); std::promise arrival_started; std::promise release_arrival; const auto release_signal = release_arrival.get_future().share(); input::testing::set_input_packet_hook([&](std::uint32_t magic) { if (magic == SS_CONTROLLER_ARRIVAL_MAGIC) { arrival_started.set_value(); release_signal.wait(); } }); std::jthread worker {input::testing::process_queued_messages, stream_input}; const auto arrival_status = arrival_started.get_future().wait_for(std::chrono::seconds {5}); if (arrival_status != std::future_status::ready) { release_arrival.set_value(); worker.join(); FAIL() << "Controller arrival packet did not reach the dispatch hook"; } std::vector state_bytes(sizeof(state)); std::memcpy(state_bytes.data(), &state, sizeof(state)); input::passthrough(stream_input, std::move(state_bytes)); EXPECT_EQ(scheduled.size(), 1); EXPECT_EQ(input::testing::queued_input_packet_count(stream_input), 1); auto other_input = input::alloc(std::make_shared(), "independent-input-stream"); if (!other_input) { release_arrival.set_value(); worker.join(); FAIL() << "Could not create independent input stream"; } input::passthrough(other_input, make_input_packet(0x12345678, sizeof(std::uint32_t), sizeof(NV_INPUT_HEADER))); EXPECT_EQ(scheduled.size(), 2); if (scheduled.size() == 2) { EXPECT_EQ(scheduled.back(), other_input); } const auto active_before_gamepad = runtime().active_device_count(); release_arrival.set_value(); worker.join(); input::testing::set_input_packet_hook({}); input::testing::process_queued_messages(other_input); EXPECT_EQ(input::testing::queued_input_packet_count(stream_input), 0); EXPECT_EQ(input::testing::queued_input_packet_count(other_input), 0); EXPECT_EQ(runtime().active_device_count(), active_before_gamepad + 1); const auto gamepad_id = input::testing::gamepad_id(stream_input, 0); ASSERT_GE(gamepad_id, 0); const auto *adapter = platf::virtualhid::gamepad_adapter_for_testing(context(), gamepad_id); ASSERT_NE(adapter, nullptr); ASSERT_NE(adapter->gamepad(), nullptr); EXPECT_EQ(adapter->gamepad()->profile().gamepad_kind, lvh::GamepadProfileKind::switch_pro); } TEST_F(InputGamepadSessionTest, YieldsAfterBoundedPacketBatch) { ASSERT_FALSE(task_pool.running()); auto stream_input = input::alloc(std::make_shared(), "bounded-input-batch"); ASSERT_NE(stream_input, nullptr); std::vector> scheduled; input::testing::set_input_task_sink([&](std::shared_ptr input) { scheduled.push_back(std::move(input)); }); constexpr std::uint32_t unknown_magic = 0x12345678; constexpr std::size_t packet_count = 33; for (std::size_t n = 0; n < packet_count; ++n) { input::passthrough(stream_input, make_input_packet(unknown_magic, sizeof(std::uint32_t), sizeof(NV_INPUT_HEADER))); } ASSERT_EQ(scheduled.size(), 1); input::testing::process_queued_messages(scheduled.front()); EXPECT_EQ(input::testing::queued_input_packet_count(stream_input), 1); ASSERT_EQ(scheduled.size(), 2); input::testing::process_queued_messages(scheduled.back()); EXPECT_EQ(input::testing::queued_input_packet_count(stream_input), 0); EXPECT_EQ(scheduled.size(), 2); } TEST_F(InputGamepadSessionTest, BatchesContinuousPacketsWithoutCrossingStateChanges) { ASSERT_FALSE(task_pool.running()); config::input.mouse = false; config::input.controller = false; config::input.native_pen_touch = false; input::testing::set_input_task_sink([](std::shared_ptr) { }); NV_MULTI_CONTROLLER_PACKET controller {}; expect_packet_dispatches(controller, controller, MULTI_CONTROLLER_MAGIC_GEN5, 1); auto changed_controller = controller; changed_controller.controllerNumber = 1; expect_packet_dispatches(controller, changed_controller, MULTI_CONTROLLER_MAGIC_GEN5, 2); changed_controller = controller; changed_controller.activeGamepadMask = 1; expect_packet_dispatches(controller, changed_controller, MULTI_CONTROLLER_MAGIC_GEN5, 3); changed_controller = controller; changed_controller.buttonFlags = 1; expect_packet_dispatches(controller, changed_controller, MULTI_CONTROLLER_MAGIC_GEN5, 3); changed_controller = controller; changed_controller.buttonFlags2 = 1; expect_packet_dispatches(controller, changed_controller, MULTI_CONTROLLER_MAGIC_GEN5, 3); SS_TOUCH_PACKET touch {}; touch.eventType = LI_TOUCH_EVENT_MOVE; expect_packet_dispatches(touch, touch, SS_TOUCH_MAGIC, 1); auto changed_touch = touch; changed_touch.pointerId = 1; expect_packet_dispatches(touch, changed_touch, SS_TOUCH_MAGIC, 2); changed_touch = touch; changed_touch.eventType = LI_TOUCH_EVENT_DOWN; expect_packet_dispatches(touch, changed_touch, SS_TOUCH_MAGIC, 3); changed_touch.eventType = LI_TOUCH_EVENT_HOVER; expect_packet_dispatches(touch, changed_touch, SS_TOUCH_MAGIC, 3); SS_PEN_PACKET pen {}; pen.eventType = LI_TOUCH_EVENT_MOVE; expect_packet_dispatches(pen, pen, SS_PEN_MAGIC, 1); auto changed_pen = pen; changed_pen.eventType = LI_TOUCH_EVENT_DOWN; expect_packet_dispatches(pen, changed_pen, SS_PEN_MAGIC, 3); changed_pen.eventType = LI_TOUCH_EVENT_HOVER; expect_packet_dispatches(pen, changed_pen, SS_PEN_MAGIC, 3); changed_pen = pen; changed_pen.penButtons = 1; expect_packet_dispatches(pen, changed_pen, SS_PEN_MAGIC, 3); changed_pen = pen; changed_pen.toolType = 1; expect_packet_dispatches(pen, changed_pen, SS_PEN_MAGIC, 3); SS_CONTROLLER_TOUCH_PACKET controller_touch {}; controller_touch.eventType = LI_TOUCH_EVENT_MOVE; expect_packet_dispatches(controller_touch, controller_touch, SS_CONTROLLER_TOUCH_MAGIC, 1); auto changed_controller_touch = controller_touch; changed_controller_touch.controllerNumber = 1; expect_packet_dispatches(controller_touch, changed_controller_touch, SS_CONTROLLER_TOUCH_MAGIC, 2); changed_controller_touch = controller_touch; changed_controller_touch.pointerId = 1; expect_packet_dispatches(controller_touch, changed_controller_touch, SS_CONTROLLER_TOUCH_MAGIC, 2); changed_controller_touch = controller_touch; changed_controller_touch.eventType = LI_TOUCH_EVENT_DOWN; expect_packet_dispatches(controller_touch, changed_controller_touch, SS_CONTROLLER_TOUCH_MAGIC, 3); changed_controller_touch.eventType = LI_TOUCH_EVENT_HOVER; expect_packet_dispatches(controller_touch, changed_controller_touch, SS_CONTROLLER_TOUCH_MAGIC, 3); SS_CONTROLLER_MOTION_PACKET motion {}; expect_packet_dispatches(motion, motion, SS_CONTROLLER_MOTION_MAGIC, 1); auto changed_motion = motion; changed_motion.controllerNumber = 1; expect_packet_dispatches(motion, changed_motion, SS_CONTROLLER_MOTION_MAGIC, 2); changed_motion = motion; changed_motion.motionType = 1; expect_packet_dispatches(motion, changed_motion, SS_CONTROLLER_MOTION_MAGIC, 2); } TEST_F(InputGamepadSessionTest, ReusesGamepadsAcrossPauseAndDestroysThemOnTermination) { const std::string session_id = "paired-client-certificate"; auto first_mail = std::make_shared(); auto first = input::alloc(first_mail, session_id); ASSERT_NE(first, nullptr); const auto active_devices_before_gamepad = runtime().active_device_count(); const platf::gamepad_arrival_t metadata {LI_CTYPE_XBOX, 0, 0}; const auto original_id = input::testing::alloc_gamepad(first, 0, metadata); ASSERT_GE(original_id, 0); EXPECT_EQ(runtime().active_device_count(), active_devices_before_gamepad + 1); const std::weak_ptr paused = first; first.reset(); EXPECT_FALSE(paused.expired()); EXPECT_EQ(runtime().active_device_count(), active_devices_before_gamepad + 1); auto resumed_mail = std::make_shared(); auto resumed = input::alloc(resumed_mail, session_id); ASSERT_NE(resumed, nullptr); EXPECT_EQ(paused.lock(), resumed); EXPECT_EQ(input::testing::gamepad_id(resumed, 0), original_id); EXPECT_EQ(runtime().active_device_count(), active_devices_before_gamepad + 1); input::terminate_gamepads(session_id); EXPECT_EQ(input::testing::gamepad_id(resumed, 0), -1); EXPECT_EQ(runtime().active_device_count(), active_devices_before_gamepad); auto replacement = input::alloc(std::make_shared(), session_id); EXPECT_NE(replacement, resumed); } TEST_F(InputGamepadSessionTest, ReplaysMotionAndRumbleAfterResumedControllerArrival) { config::input.gamepad = "ds5"; const std::string session_id = "late-controller-arrival"; const auto capabilities = static_cast(LI_CCAP_ACCEL | LI_CCAP_GYRO | LI_CCAP_RUMBLE); auto first_mail = std::make_shared(); auto stream_input = input::alloc(first_mail, session_id); const auto id = input::testing::alloc_gamepad(stream_input, 0, {LI_CTYPE_PS, capabilities, 0}); ASSERT_GE(id, 0); auto *adapter = platf::virtualhid::gamepad_adapter_for_testing(context(), id); ASSERT_NE(adapter, nullptr); const auto device_count = runtime().active_device_count(); auto old_queue = first_mail->queue(mail::gamepad_feedback); while (old_queue->pop(std::chrono::milliseconds {0})) { // Discard initial motion requests before simulating a reconnect. } for (int reconnect = 0; reconnect < 3; ++reconnect) { auto resumed_mail = std::make_shared(); auto resumed = input::alloc(resumed_mail, session_id); ASSERT_EQ(resumed, stream_input); auto feedback = resumed_mail->queue(mail::gamepad_feedback); // Model the client discarding requests received before controller assignment. ASSERT_TRUE(feedback->pop(std::chrono::milliseconds {0})); ASSERT_TRUE(feedback->pop(std::chrono::milliseconds {0})); ASSERT_FALSE(feedback->pop(std::chrono::milliseconds {0})); lvh::GamepadOutput output; output.kind = lvh::GamepadOutputKind::rumble; output.low_frequency_rumble = 1234; output.high_frequency_rumble = 5678; ASSERT_TRUE(adapter->dispatch_output(output).ok()); ASSERT_TRUE(feedback->pop(std::chrono::milliseconds {0})); SS_CONTROLLER_ARRIVAL_PACKET arrival {}; arrival.header.size = util::endian::big(sizeof(arrival) - sizeof(arrival.header.size)); arrival.header.magic = util::endian::little(SS_CONTROLLER_ARRIVAL_MAGIC); arrival.controllerNumber = 0; arrival.type = LI_CTYPE_PS; arrival.capabilities = util::endian::little(capabilities); std::vector bytes(sizeof(arrival)); std::memcpy(bytes.data(), &arrival, sizeof(arrival)); input::passthrough(resumed, std::move(bytes)); input::testing::process_queued_messages(resumed); for (const auto motion_type : {LI_MOTION_TYPE_ACCEL, LI_MOTION_TYPE_GYRO}) { const auto request = feedback->pop(std::chrono::milliseconds {0}); ASSERT_TRUE(request); EXPECT_EQ(request->type, platf::gamepad_feedback_e::set_motion_event_state); EXPECT_EQ(request->id, 0); EXPECT_EQ(request->data.motion_event_state.motion_type, motion_type); EXPECT_EQ(request->data.motion_event_state.report_rate, 100); } // Identical feedback sent before client readiness must not suppress this rumble. ASSERT_TRUE(adapter->dispatch_output(output).ok()); const auto rumble = feedback->pop(std::chrono::milliseconds {0}); ASSERT_TRUE(rumble); EXPECT_EQ(rumble->type, platf::gamepad_feedback_e::rumble); EXPECT_EQ(rumble->data.rumble.lowfreq, 1234); EXPECT_EQ(rumble->data.rumble.highfreq, 5678); EXPECT_FALSE(old_queue->pop(std::chrono::milliseconds {0})); EXPECT_EQ(input::testing::gamepad_id(resumed, 0), id); EXPECT_EQ(platf::virtualhid::gamepad_adapter_for_testing(context(), id), adapter); EXPECT_EQ(runtime().active_device_count(), device_count); old_queue = std::move(feedback); } } TEST_F(InputGamepadSessionTest, ReportsMissingRetainedBackendOnArrival) { auto stream_input = input::alloc(std::make_shared(), "missing-retained-backend"); const platf::gamepad_arrival_t metadata {LI_CTYPE_XBOX, 0, 0}; const auto id = input::testing::alloc_gamepad(stream_input, 0, metadata); ASSERT_GE(id, 0); platf::virtualhid::free_gamepad(context(), id); EXPECT_EQ(input::testing::alloc_gamepad(stream_input, 0, metadata), -1); } TEST_F(InputGamepadSessionTest, RefreshesSharedVirtualInputAfterLicenseStateChanges) { ASSERT_NE(context().keyboard, nullptr); ASSERT_NE(context().mouse, nullptr); const auto original_keyboard_id = context().keyboard->device_id(); const auto original_mouse_id = context().mouse->device_id(); const auto active_devices = runtime().active_device_count(); input::refresh_virtual_input(); ASSERT_NE(context().keyboard, nullptr); ASSERT_NE(context().mouse, nullptr); EXPECT_NE(context().keyboard->device_id(), original_keyboard_id); EXPECT_NE(context().mouse->device_id(), original_mouse_id); EXPECT_EQ(runtime().active_device_count(), active_devices); } TEST_F(InputGamepadSessionTest, RetargetsRelativeMouseFromLatestStreamGeometry) { auto session_mail = std::make_shared(); auto stream_input = input::alloc(session_mail, "relative-mouse-client"); ASSERT_NE(stream_input, nullptr); input::touch_port_t left_viewport { { .offset_x = -1920, .offset_y = 0, .width = 1920, .height = 1080, .logical_width = 1920, .logical_height = 1080, .env_offset_x = -1920, .env_offset_y = 0, }, 3840, 1080, 0.0F, 0.0F, 1.0F, 1.0F, 3840, 1080, }; const auto initial_mouse_id = context().mouse->device_id(); session_mail->event(mail::touch_port)->raise(left_viewport); input::testing::send_relative_mouse_packet(stream_input, 12, -7); const auto left_mouse_id = context().mouse->device_id(); EXPECT_NE(left_mouse_id, initial_mouse_id); auto mouse_event = context().mouse->last_submitted_event(); EXPECT_EQ(mouse_event.kind, lvh::MouseEventKind::relative_motion); EXPECT_EQ(mouse_event.x, 12); EXPECT_EQ(mouse_event.y, -7); EXPECT_EQ(context().mouse_desktop.offset_x, -1920); EXPECT_EQ(context().mouse_desktop.width, 3840); EXPECT_EQ(context().mouse_viewport.offset_x, -1920); EXPECT_EQ(context().mouse_viewport.width, 1920); left_viewport.offset_x = 0; session_mail->event(mail::touch_port)->raise(left_viewport); input::testing::send_relative_mouse_packet(stream_input, -5, 9); EXPECT_NE(context().mouse->device_id(), left_mouse_id); mouse_event = context().mouse->last_submitted_event(); EXPECT_EQ(mouse_event.kind, lvh::MouseEventKind::relative_motion); EXPECT_EQ(mouse_event.x, -5); EXPECT_EQ(mouse_event.y, 9); EXPECT_EQ(context().mouse_viewport.offset_x, 0); }