Files
Sunshine/tests/unit/platform/test_virtualhid_input.cpp

851 lines
38 KiB
C++

/**
* @file tests/unit/platform/test_virtualhid_input.cpp
* @brief Tests for shared libvirtualhid input helpers.
*/
#include "../../tests_common.h"
// standard includes
#include <array>
#include <chrono>
#include <cstdint>
#include <limits>
#include <memory>
#include <string>
#include <string_view>
#include <tuple>
#include <utility>
// 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<gamepad_touchpad_case_t> {
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<gamepad_touchpad_case_t> &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<platf::virtualhid::input_context_t>(lvh::BackendKind::fake);
client_ = std::make_unique<platf::virtualhid::client_context_t>(*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<platf::gamepad_feedback_msg_t>(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<platf::virtualhid::input_context_t> context_; ///< Shared fake virtual devices.
std::unique_ptr<platf::virtualhid::client_context_t> 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<auto_profile_case_t> {};
} // 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<int>(context()->gamepads.size()), 0}, metadata, nullptr), -1);
EXPECT_FALSE(platf::virtualhid::has_gamepad(*context(), -1));
EXPECT_FALSE(platf::virtualhid::has_gamepad(*context(), static_cast<int>(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<std::uint16_t>(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<std::int16_t>::min(),
std::numeric_limits<std::int16_t>::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<auto_profile_case_t> &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<platf::gamepad_feedback_msg_t>("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<std::uint16_t>(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<std::uint16_t>(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<int>(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<int>(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<std::uint8_t>(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<int>(text.size()));
EXPECT_EQ(platform_context.keyboard->submit_count(), keyboard_count + 1);
config::input.gamepad = "ds5";
const auto capabilities = static_cast<std::uint16_t>(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);
}