mirror of
https://github.com/LizardByte/Sunshine.git
synced 2026-11-02 18:26:58 +09:00
645 lines
28 KiB
C++
645 lines
28 KiB
C++
/**
|
|
* @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 <algorithm>
|
|
#include <array>
|
|
#include <chrono>
|
|
#include <cstdint>
|
|
#include <cstring>
|
|
#include <future>
|
|
#include <limits>
|
|
#include <memory>
|
|
#include <string>
|
|
#include <thread>
|
|
#include <vector>
|
|
|
|
// moonlight-common-c includes
|
|
extern "C" {
|
|
#include <moonlight-common-c/src/Input.h>
|
|
}
|
|
|
|
// 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<std::uint8_t> make_input_packet(std::uint32_t magic, std::uint32_t declared_size, std::size_t actual_size) {
|
|
std::vector<std::uint8_t> 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<class Packet>
|
|
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<safe::mail_raw_t>(), "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<std::uint32_t>(sizeof(packet) - sizeof(packet.header.size));
|
|
packet.header.magic = util::endian::little(magic);
|
|
std::vector<std::uint8_t> 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<std::uint32_t>::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<std::uint32_t>(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<input::input_t> empty_input;
|
|
EXPECT_EQ(input::testing::queued_input_packet_count(empty_input), 0);
|
|
|
|
auto stream_input = input::alloc(std::make_shared<safe::mail_raw_t>(), "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<std::uint32_t>(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<safe::mail_raw_t>(), "simultaneous-controller-packets");
|
|
ASSERT_NE(stream_input, nullptr);
|
|
std::vector<std::shared_ptr<input::input_t>> scheduled;
|
|
input::testing::set_input_task_sink([&](std::shared_ptr<input::input_t> input) {
|
|
scheduled.push_back(std::move(input));
|
|
});
|
|
|
|
SS_CONTROLLER_ARRIVAL_PACKET arrival {};
|
|
arrival.header.size = util::endian::big<std::uint32_t>(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<std::uint32_t>(sizeof(state) - sizeof(state.header.size));
|
|
state.header.magic = util::endian::little(MULTI_CONTROLLER_MAGIC_GEN5);
|
|
state.controllerNumber = 0;
|
|
state.activeGamepadMask = 1;
|
|
|
|
std::vector<std::uint8_t> 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<void> arrival_started;
|
|
std::promise<void> 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<std::uint8_t> 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<safe::mail_raw_t>(), "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<safe::mail_raw_t>(), "bounded-input-batch");
|
|
ASSERT_NE(stream_input, nullptr);
|
|
|
|
std::vector<std::shared_ptr<input::input_t>> scheduled;
|
|
input::testing::set_input_task_sink([&](std::shared_ptr<input::input_t> 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<input::input_t>) {
|
|
});
|
|
|
|
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<safe::mail_raw_t>();
|
|
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<input::input_t> 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<safe::mail_raw_t>();
|
|
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<safe::mail_raw_t>(), 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<std::uint16_t>(LI_CCAP_ACCEL | LI_CCAP_GYRO | LI_CCAP_RUMBLE);
|
|
auto first_mail = std::make_shared<safe::mail_raw_t>();
|
|
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<platf::gamepad_feedback_msg_t>(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<safe::mail_raw_t>();
|
|
auto resumed = input::alloc(resumed_mail, session_id);
|
|
ASSERT_EQ(resumed, stream_input);
|
|
auto feedback = resumed_mail->queue<platf::gamepad_feedback_msg_t>(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<std::uint32_t>(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<std::uint8_t> 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<safe::mail_raw_t>(), "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<safe::mail_raw_t>();
|
|
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<input::touch_port_t>(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<input::touch_port_t>(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);
|
|
}
|