Files
libvirtualhid/tests/unit/test_windows_vhf_input_report_queue.cpp

329 lines
12 KiB
C++

// SPDX-FileCopyrightText: 2026 LIZARDBYTE LLC
// SPDX-License-Identifier: LicenseRef-LizardByte-SAL-1.0
/**
* @file tests/unit/test_windows_vhf_input_report_queue.cpp
* @brief Tests for Windows VHF input-report coalescing.
*/
// standard includes
#include <cstddef>
#include <cstdint>
#include <vector>
// library includes
#include <gtest/gtest.h>
#include <libvirtualhid/profiles.hpp>
#include <libvirtualhid/report.hpp>
// local includes
#include "vhf_input_report_queue.hpp"
namespace {
using lvh::detail::windows::vhf_max_pending_input_reports;
using lvh::detail::windows::VhfInputReportQueue;
std::vector<std::uint8_t> make_report(std::size_t size, std::uint8_t report_id) {
auto report = std::vector<std::uint8_t>(size, 0U);
if (report_id != 0U) {
report[0] = report_id;
}
return report;
}
std::vector<std::uint8_t> make_mouse_report(
std::uint8_t buttons,
std::int16_t x,
std::int16_t y,
std::int8_t wheel,
std::int8_t pan
) {
const auto encode_i16 = [](std::vector<std::uint8_t> &report, std::size_t offset, std::int16_t value) {
const auto encoded = static_cast<std::uint16_t>(value);
report[offset] = static_cast<std::uint8_t>(encoded & 0xFFU);
report[offset + 1U] = static_cast<std::uint8_t>(encoded >> 8U);
};
std::vector<std::uint8_t> report(LVH_WINDOWS_MOUSE_INPUT_REPORT_SIZE, 0U);
report[0] = buttons;
encode_i16(report, 1U, x);
encode_i16(report, 3U, y);
report[5] = static_cast<std::uint8_t>(wheel);
report[6] = static_cast<std::uint8_t>(pan);
return report;
}
TEST(WindowsVhfInputReportQueueTest, CollapsesAnAxisBurstToItsLatestState) {
VhfInputReportQueue queue {LVH_WINDOWS_DEVICE_GAMEPAD, LVH_WINDOWS_GAMEPAD_GENERIC, LVH_WINDOWS_BUS_USB, 1U};
auto latest = make_report(9U, 1U);
for (std::uint8_t axis = 0U; axis < 64U; ++axis) {
latest[3] = axis;
queue.push(latest);
}
ASSERT_EQ(queue.size(), 1U);
EXPECT_EQ(queue.pop(), latest);
EXPECT_TRUE(queue.empty());
}
TEST(WindowsVhfInputReportQueueTest, PreservesButtonTransitionsWhileCoalescingTheirAxes) {
VhfInputReportQueue queue {LVH_WINDOWS_DEVICE_GAMEPAD, LVH_WINDOWS_GAMEPAD_GENERIC, LVH_WINDOWS_BUS_USB, 1U};
auto neutral = make_report(9U, 1U);
neutral[3] = 10U;
auto pressed = neutral;
pressed[1] = 1U;
pressed[3] = 20U;
auto pressed_latest = pressed;
pressed_latest[3] = 30U;
auto released = pressed_latest;
released[1] = 0U;
released[3] = 40U;
queue.push(neutral);
queue.push(pressed);
queue.push(pressed_latest);
queue.push(released);
ASSERT_EQ(queue.size(), 3U);
EXPECT_EQ(queue.pop(), neutral);
EXPECT_EQ(queue.pop(), pressed_latest);
EXPECT_EQ(queue.pop(), released);
}
struct ProfileCase {
lvh::DeviceProfile profile;
std::uint32_t gamepad_kind;
std::uint32_t bus_type;
bool supports_touch;
};
TEST(WindowsVhfInputReportQueueTest, RecognizesContinuousAndDiscreteStateForEveryVhfProfile) {
const std::vector profiles {
ProfileCase {lvh::profiles::generic_gamepad(), LVH_WINDOWS_GAMEPAD_GENERIC, LVH_WINDOWS_BUS_USB, false},
ProfileCase {lvh::profiles::xbox_one(), LVH_WINDOWS_GAMEPAD_XBOX_ONE, LVH_WINDOWS_BUS_USB, false},
ProfileCase {lvh::profiles::xbox_series(), LVH_WINDOWS_GAMEPAD_XBOX_SERIES, LVH_WINDOWS_BUS_USB, false},
ProfileCase {lvh::profiles::switch_pro(), LVH_WINDOWS_GAMEPAD_SWITCH_PRO, LVH_WINDOWS_BUS_USB, false},
ProfileCase {lvh::profiles::dualshock4_usb(), LVH_WINDOWS_GAMEPAD_DUALSHOCK4, LVH_WINDOWS_BUS_USB, true},
ProfileCase {lvh::profiles::dualshock4_bluetooth(), LVH_WINDOWS_GAMEPAD_DUALSHOCK4, LVH_WINDOWS_BUS_BLUETOOTH, true},
ProfileCase {lvh::profiles::dualsense_usb(), LVH_WINDOWS_GAMEPAD_DUALSENSE, LVH_WINDOWS_BUS_USB, true},
ProfileCase {lvh::profiles::dualsense_bluetooth(), LVH_WINDOWS_GAMEPAD_DUALSENSE, LVH_WINDOWS_BUS_BLUETOOTH, true},
};
for (const auto &test_case : profiles) {
SCOPED_TRACE(test_case.profile.name);
VhfInputReportQueue queue {
LVH_WINDOWS_DEVICE_GAMEPAD,
test_case.gamepad_kind,
test_case.bus_type,
test_case.profile.report_id,
};
auto state = lvh::GamepadState {};
const auto initial = lvh::reports::pack_input_report(test_case.profile, state);
state.left_stick.x = 0.75F;
const auto moved = lvh::reports::pack_input_report(test_case.profile, state);
state.buttons.set(lvh::GamepadButton::a);
const auto button_changed = lvh::reports::pack_input_report(test_case.profile, state);
ASSERT_FALSE(initial.empty());
ASSERT_FALSE(moved.empty());
ASSERT_FALSE(button_changed.empty());
queue.push(initial);
queue.push(moved);
EXPECT_EQ(queue.size(), 1U);
queue.push(button_changed);
EXPECT_EQ(queue.size(), 2U);
if (test_case.supports_touch) {
state.touchpad_contacts[0] = {.id = 7U, .active = true, .x = 0.25F, .y = 0.5F};
const auto touch_started = lvh::reports::pack_input_report(test_case.profile, state);
state.touchpad_contacts[0].x = 0.75F;
const auto touch_moved = lvh::reports::pack_input_report(test_case.profile, state);
queue.push(touch_started);
EXPECT_EQ(queue.size(), 3U);
queue.push(touch_moved);
EXPECT_EQ(queue.size(), 3U);
state.touchpad_contacts[0].active = false;
queue.push(lvh::reports::pack_input_report(test_case.profile, state));
EXPECT_EQ(queue.size(), 4U);
}
}
}
TEST(WindowsVhfInputReportQueueTest, CoalescesContinuousTriggerMovementAndPreservesNativeThresholdChanges) {
struct TriggerCase {
lvh::DeviceProfile profile;
std::uint32_t gamepad_kind;
std::uint32_t bus_type;
bool exposes_trigger_button;
};
const std::vector profiles {
TriggerCase {lvh::profiles::generic_gamepad(), LVH_WINDOWS_GAMEPAD_GENERIC, LVH_WINDOWS_BUS_USB, false},
TriggerCase {lvh::profiles::xbox_one(), LVH_WINDOWS_GAMEPAD_XBOX_ONE, LVH_WINDOWS_BUS_USB, false},
TriggerCase {lvh::profiles::xbox_series(), LVH_WINDOWS_GAMEPAD_XBOX_SERIES, LVH_WINDOWS_BUS_USB, false},
TriggerCase {lvh::profiles::switch_pro(), LVH_WINDOWS_GAMEPAD_SWITCH_PRO, LVH_WINDOWS_BUS_USB, true},
TriggerCase {lvh::profiles::dualshock4_usb(), LVH_WINDOWS_GAMEPAD_DUALSHOCK4, LVH_WINDOWS_BUS_USB, true},
TriggerCase {lvh::profiles::dualshock4_bluetooth(), LVH_WINDOWS_GAMEPAD_DUALSHOCK4, LVH_WINDOWS_BUS_BLUETOOTH, true},
TriggerCase {lvh::profiles::dualsense_usb(), LVH_WINDOWS_GAMEPAD_DUALSENSE, LVH_WINDOWS_BUS_USB, true},
TriggerCase {lvh::profiles::dualsense_bluetooth(), LVH_WINDOWS_GAMEPAD_DUALSENSE, LVH_WINDOWS_BUS_BLUETOOTH, true},
};
for (const auto &test_case : profiles) {
SCOPED_TRACE(test_case.profile.name);
VhfInputReportQueue queue {
LVH_WINDOWS_DEVICE_GAMEPAD,
test_case.gamepad_kind,
test_case.bus_type,
test_case.profile.report_id,
};
auto state = lvh::GamepadState {};
state.left_trigger = 0.1F;
queue.push(lvh::reports::pack_input_report(test_case.profile, state));
state.left_trigger = 0.9F;
queue.push(lvh::reports::pack_input_report(test_case.profile, state));
EXPECT_EQ(queue.size(), 1U);
state.left_trigger = 0.0F;
queue.push(lvh::reports::pack_input_report(test_case.profile, state));
EXPECT_EQ(queue.size(), test_case.exposes_trigger_button ? 2U : 1U);
}
}
TEST(WindowsVhfInputReportQueueTest, PrioritizesSwitchProtocolRepliesWithoutCoalescingThem) {
VhfInputReportQueue queue {LVH_WINDOWS_DEVICE_GAMEPAD, LVH_WINDOWS_GAMEPAD_SWITCH_PRO, LVH_WINDOWS_BUS_USB, 0x30U};
auto controller_state = make_report(64U, 0x30U);
auto first_reply = make_report(64U, 0x21U);
auto second_reply = first_reply;
first_reply[15] = 1U;
second_reply[15] = 2U;
queue.push(controller_state);
queue.push(first_reply);
queue.push(second_reply);
ASSERT_EQ(queue.size(), 3U);
EXPECT_EQ(queue.pop(), first_reply);
EXPECT_EQ(queue.pop(), second_reply);
EXPECT_EQ(queue.pop(), controller_state);
}
TEST(WindowsVhfInputReportQueueTest, BoundsSwitchProtocolReplyHistory) {
VhfInputReportQueue queue {LVH_WINDOWS_DEVICE_GAMEPAD, LVH_WINDOWS_GAMEPAD_SWITCH_PRO, LVH_WINDOWS_BUS_USB, 0x30U};
constexpr auto submitted_reports = vhf_max_pending_input_reports + 8U;
for (std::size_t index = 0U; index < submitted_reports; ++index) {
auto reply = make_report(64U, 0x21U);
reply[15] = static_cast<std::uint8_t>(index);
queue.push(std::move(reply));
}
ASSERT_EQ(queue.size(), vhf_max_pending_input_reports);
const auto first = queue.pop();
ASSERT_TRUE(first.has_value());
EXPECT_EQ((*first)[15], 8U);
}
TEST(WindowsVhfInputReportQueueTest, KeepsOnlyTheNewestBoundedTransitionHistory) {
VhfInputReportQueue queue {LVH_WINDOWS_DEVICE_GAMEPAD, LVH_WINDOWS_GAMEPAD_GENERIC, LVH_WINDOWS_BUS_USB, 1U};
constexpr auto submitted_reports = vhf_max_pending_input_reports + 8U;
for (std::size_t index = 0U; index < submitted_reports; ++index) {
auto report = make_report(9U, 1U);
report[1] = static_cast<std::uint8_t>(index % 2U);
report[3] = static_cast<std::uint8_t>(index);
queue.push(std::move(report));
}
ASSERT_EQ(queue.size(), vhf_max_pending_input_reports);
const auto first = queue.pop();
ASSERT_TRUE(first.has_value());
EXPECT_EQ((*first)[3], 8U);
auto last = first;
while (!queue.empty()) {
last = queue.pop();
}
ASSERT_TRUE(last.has_value());
EXPECT_EQ((*last)[3], submitted_reports - 1U);
}
TEST(WindowsVhfInputReportQueueTest, KeepsMalformedAndUnknownReportsDistinctAndCanClearThem) {
VhfInputReportQueue malformed_queue {LVH_WINDOWS_DEVICE_GAMEPAD, LVH_WINDOWS_GAMEPAD_GENERIC, LVH_WINDOWS_BUS_USB, 0U};
malformed_queue.push({});
malformed_queue.push({});
malformed_queue.push({0U});
EXPECT_EQ(malformed_queue.size(), 3U);
malformed_queue.clear();
EXPECT_TRUE(malformed_queue.empty());
EXPECT_FALSE(malformed_queue.pop().has_value());
constexpr auto unknown_gamepad_kind = 0xFFFFFFFFU;
VhfInputReportQueue unknown_queue {LVH_WINDOWS_DEVICE_GAMEPAD, unknown_gamepad_kind, LVH_WINDOWS_BUS_UNKNOWN, 0U};
unknown_queue.push(make_report(9U, 0U));
unknown_queue.push(make_report(9U, 0U));
EXPECT_EQ(unknown_queue.size(), 2U);
}
TEST(WindowsVhfInputReportQueueTest, AccumulatesRelativeMouseReportsWithoutLosingLargeDeltas) {
VhfInputReportQueue queue {
LVH_WINDOWS_DEVICE_MOUSE,
LVH_WINDOWS_GAMEPAD_GENERIC,
LVH_WINDOWS_BUS_USB,
0U,
};
queue.push(make_mouse_report(0U, 30000, -30000, 100, -100));
queue.push(make_mouse_report(0U, 30000, -30000, 100, -100));
ASSERT_EQ(queue.size(), 1U);
EXPECT_EQ(queue.pop(), make_mouse_report(0U, 32767, -32768, 127, -127));
EXPECT_EQ(queue.pop(), make_mouse_report(0U, 27233, -27232, 73, -73));
EXPECT_TRUE(queue.empty());
}
TEST(WindowsVhfInputReportQueueTest, PreservesKeyboardTransitions) {
VhfInputReportQueue queue {
LVH_WINDOWS_DEVICE_KEYBOARD,
LVH_WINDOWS_GAMEPAD_GENERIC,
LVH_WINDOWS_BUS_USB,
0U,
};
auto neutral = make_report(LVH_WINDOWS_KEYBOARD_INPUT_REPORT_SIZE, 0U);
auto pressed = neutral;
pressed[2] = 0x04U;
auto shifted = pressed;
shifted[0] = 0x02U;
queue.push(neutral);
queue.push(pressed);
queue.push(shifted);
queue.push(neutral);
ASSERT_EQ(queue.size(), 4U);
EXPECT_EQ(queue.pop(), neutral);
EXPECT_EQ(queue.pop(), pressed);
EXPECT_EQ(queue.pop(), shifted);
EXPECT_EQ(queue.pop(), neutral);
EXPECT_TRUE(queue.empty());
}
TEST(WindowsVhfInputReportQueueTest, PreservesMouseButtonTransitions) {
VhfInputReportQueue queue {
LVH_WINDOWS_DEVICE_MOUSE,
LVH_WINDOWS_GAMEPAD_GENERIC,
LVH_WINDOWS_BUS_USB,
0U,
};
queue.push(make_mouse_report(0U, 5, 0, 0, 0));
queue.push(make_mouse_report(1U, 0, 0, 0, 0));
queue.push(make_mouse_report(1U, 7, 0, 0, 0));
queue.push(make_mouse_report(0U, 0, 0, 0, 0));
ASSERT_EQ(queue.size(), 3U);
EXPECT_EQ(queue.pop(), make_mouse_report(0U, 5, 0, 0, 0));
EXPECT_EQ(queue.pop(), make_mouse_report(1U, 7, 0, 0, 0));
EXPECT_EQ(queue.pop(), make_mouse_report(0U, 0, 0, 0, 0));
}
} // namespace