Files
libvirtualhid/tests/unit/test_windows_protocol.cpp

761 lines
33 KiB
C++

/**
* @file tests/unit/test_windows_protocol.cpp
* @brief Unit tests for the Windows control protocol helpers.
*/
// local includes
#include "fixtures/fixtures.hpp"
#include "generic_pid_rumble.hpp"
#include "platform/windows/control_protocol.hpp"
// standard includes
#include <algorithm>
#include <array>
#include <chrono>
#include <cstdint>
#include <initializer_list>
#include <span>
#include <string>
#include <string_view>
#include <vector>
// lib includes
#include <libvirtualhid/profiles.hpp>
#include <libvirtualhid/report.hpp>
namespace {
lvh::DeviceProfile minimal_gamepad_profile() {
lvh::DeviceProfile profile;
profile.device_type = lvh::DeviceType::gamepad;
profile.gamepad_kind = lvh::GamepadProfileKind::generic;
profile.bus_type = lvh::BusType::usb;
profile.vendor_id = 0x1209;
profile.product_id = 0x0001;
profile.version = 0x0001;
profile.report_id = 1;
profile.input_report_size = 4;
profile.output_report_size = 2;
profile.name = "test gamepad";
profile.manufacturer = "test manufacturer";
profile.report_descriptor = {0x05, 0x01, 0x09, 0x05};
return profile;
}
std::vector<std::uint8_t> byte_report(std::initializer_list<std::uint8_t> values) {
return values;
}
LvhWindowsSessionToken test_session_token() {
LvhWindowsSessionToken token {};
for (std::uint8_t index = 0; index < LVH_WINDOWS_SESSION_TOKEN_SIZE; ++index) {
token.bytes[index] = static_cast<std::uint8_t>(0xA0U + index);
}
return token;
}
} // namespace
TEST(WindowsProtocolTest, ExposesStableProtocolConstants) {
EXPECT_STREQ(lvh::detail::windows::default_control_device_path.data(), R"(\\.\LibVirtualHid)");
EXPECT_STREQ(lvh::detail::windows::global_control_device_path.data(), R"(\\.\Global\LibVirtualHid)");
EXPECT_EQ(LVH_WINDOWS_CONTROL_PROTOCOL_VERSION, 5U);
EXPECT_EQ(LVH_WINDOWS_IOCTL_CREATE_DEVICE, 0x8000E000U);
EXPECT_EQ(LVH_WINDOWS_IOCTL_DESTROY_DEVICE, 0x8000E004U);
EXPECT_EQ(LVH_WINDOWS_IOCTL_SUBMIT_INPUT_REPORT, 0x8000E008U);
EXPECT_EQ(LVH_WINDOWS_IOCTL_READ_OUTPUT_REPORT, 0x8000600CU);
EXPECT_EQ(LVH_WINDOWS_IOCTL_RESET_DEVICES, 0x8000E010U);
EXPECT_EQ(LVH_WINDOWS_DEVICE_KEYBOARD, 3U);
EXPECT_EQ(LVH_WINDOWS_KEYBOARD_INPUT_REPORT_SIZE, 18U);
EXPECT_EQ(LVH_WINDOWS_KEYBOARD_OUTPUT_REPORT_SIZE, 1U);
EXPECT_EQ(sizeof(LvhWindowsDeviceHardwareIds), 14U);
EXPECT_EQ(sizeof(LvhWindowsDeviceReportSizes), 24U);
EXPECT_EQ(sizeof(LvhWindowsCreateDeviceRequest), 2502U);
EXPECT_EQ(sizeof(LvhWindowsSessionToken), 32U);
EXPECT_EQ(sizeof(LvhWindowsCreateDeviceResponse), 324U);
EXPECT_EQ(sizeof(LvhWindowsDestroyDeviceRequest), 48U);
EXPECT_EQ(sizeof(LvhWindowsResetDevicesRequest), 8U);
EXPECT_EQ(sizeof(LvhWindowsSubmitInputReportRequest), 312U);
EXPECT_EQ(sizeof(LvhWindowsOutputReportEvent), 280U);
EXPECT_EQ(sizeof(LvhWindowsXbox360InitializeRequest), 2550U);
EXPECT_EQ(sizeof(LvhWindowsXbox360InputState), 12U);
EXPECT_EQ(sizeof(LvhWindowsXbox360SubmitInputRequest), 60U);
EXPECT_EQ(sizeof(LvhWindowsXbox360ReadOutputRequest), 48U);
EXPECT_EQ(LVH_WINDOWS_IOCTL_XBOX360_INITIALIZE, 0x8000E400U);
EXPECT_EQ(LVH_WINDOWS_IOCTL_XBOX360_SUBMIT_INPUT, 0x8000E404U);
EXPECT_EQ(LVH_WINDOWS_IOCTL_XBOX360_READ_OUTPUT, 0x8000E408U);
EXPECT_STREQ(LVH_WINDOWS_XBOX360_ENUMERATOR, L"LibVirtualHid");
EXPECT_STREQ(LVH_WINDOWS_XBOX360_HARDWARE_ID, L"LIBVIRTUALHID_XBOX360");
}
TEST(WindowsProtocolTest, MapsBusTypesAndGamepadKinds) {
EXPECT_EQ(lvh::detail::windows::protocol_bus_type(lvh::BusType::unknown), LVH_WINDOWS_BUS_UNKNOWN);
EXPECT_EQ(lvh::detail::windows::protocol_bus_type(lvh::BusType::usb), LVH_WINDOWS_BUS_USB);
EXPECT_EQ(lvh::detail::windows::protocol_bus_type(lvh::BusType::bluetooth), LVH_WINDOWS_BUS_BLUETOOTH);
EXPECT_EQ(lvh::detail::windows::protocol_bus_type(static_cast<lvh::BusType>(255)), LVH_WINDOWS_BUS_UNKNOWN);
EXPECT_EQ(lvh::detail::windows::protocol_device_type(lvh::DeviceType::gamepad), LVH_WINDOWS_DEVICE_GAMEPAD);
EXPECT_EQ(lvh::detail::windows::protocol_device_type(lvh::DeviceType::keyboard), LVH_WINDOWS_DEVICE_KEYBOARD);
EXPECT_EQ(lvh::detail::windows::protocol_device_type(lvh::DeviceType::mouse), LVH_WINDOWS_DEVICE_MOUSE);
EXPECT_EQ(
lvh::detail::windows::protocol_gamepad_kind(lvh::GamepadProfileKind::generic),
LVH_WINDOWS_GAMEPAD_GENERIC
);
EXPECT_EQ(
lvh::detail::windows::protocol_gamepad_kind(lvh::GamepadProfileKind::xbox_360),
LVH_WINDOWS_GAMEPAD_XBOX_360
);
EXPECT_EQ(
lvh::detail::windows::protocol_gamepad_kind(lvh::GamepadProfileKind::xbox_one),
LVH_WINDOWS_GAMEPAD_XBOX_ONE
);
EXPECT_EQ(
lvh::detail::windows::protocol_gamepad_kind(lvh::GamepadProfileKind::xbox_series),
LVH_WINDOWS_GAMEPAD_XBOX_SERIES
);
EXPECT_EQ(
lvh::detail::windows::protocol_gamepad_kind(lvh::GamepadProfileKind::dualshock4),
LVH_WINDOWS_GAMEPAD_DUALSHOCK4
);
EXPECT_EQ(
lvh::detail::windows::protocol_gamepad_kind(lvh::GamepadProfileKind::dualsense),
LVH_WINDOWS_GAMEPAD_DUALSENSE
);
EXPECT_EQ(
lvh::detail::windows::protocol_gamepad_kind(lvh::GamepadProfileKind::switch_pro),
LVH_WINDOWS_GAMEPAD_SWITCH_PRO
);
EXPECT_EQ(
lvh::detail::windows::protocol_gamepad_kind(static_cast<lvh::GamepadProfileKind>(255)),
LVH_WINDOWS_GAMEPAD_GENERIC
);
}
TEST(WindowsProtocolTest, BuildsCapabilityFlags) {
lvh::GamepadProfileCapabilities capabilities;
EXPECT_EQ(lvh::detail::windows::gamepad_flags(capabilities), 0U);
capabilities.supports_rumble = true;
capabilities.supports_motion = true;
capabilities.supports_touchpad = true;
capabilities.supports_rgb_led = true;
capabilities.supports_battery = true;
capabilities.supports_adaptive_triggers = true;
const auto flags = lvh::detail::windows::gamepad_flags(capabilities);
EXPECT_NE(flags & LVH_WINDOWS_GAMEPAD_FLAG_SUPPORTS_RUMBLE, 0U);
EXPECT_NE(flags & LVH_WINDOWS_GAMEPAD_FLAG_SUPPORTS_MOTION, 0U);
EXPECT_NE(flags & LVH_WINDOWS_GAMEPAD_FLAG_SUPPORTS_TOUCHPAD, 0U);
EXPECT_NE(flags & LVH_WINDOWS_GAMEPAD_FLAG_SUPPORTS_RGB_LED, 0U);
EXPECT_NE(flags & LVH_WINDOWS_GAMEPAD_FLAG_SUPPORTS_BATTERY, 0U);
EXPECT_NE(flags & LVH_WINDOWS_GAMEPAD_FLAG_SUPPORTS_ADAPTIVE_TRIGGERS, 0U);
}
TEST(WindowsProtocolTest, CopyHelpersTruncateAndZeroFill) {
LvhWindowsCreateDeviceRequest create_request {};
create_request.name[0] = 'x';
create_request.name[1] = 'x';
create_request.name[2] = 'x';
create_request.name[3] = 'x';
create_request.name[4] = 'x';
const std::string oversized_name(LVH_WINDOWS_MAX_DEVICE_NAME_SIZE + 5U, 'a');
EXPECT_EQ(
lvh::detail::windows::copy_string(create_request.name, oversized_name),
LVH_WINDOWS_MAX_DEVICE_NAME_SIZE - 1U
);
EXPECT_EQ(
std::string_view {create_request.name.data()},
std::string_view {oversized_name}.substr(0U, LVH_WINDOWS_MAX_DEVICE_NAME_SIZE - 1U)
);
EXPECT_EQ(create_request.name[LVH_WINDOWS_MAX_DEVICE_NAME_SIZE - 1U], '\0');
LvhWindowsSubmitInputReportRequest submit_request {};
submit_request.report[0] = 0xFF;
submit_request.report[1] = 0xFF;
submit_request.report[2] = 0xFF;
submit_request.report[3] = 0xFF;
submit_request.report[4] = 0xFF;
const std::vector<std::uint8_t> source {1, 2};
EXPECT_EQ(lvh::detail::windows::copy_bytes(submit_request.report, source), 2U);
EXPECT_EQ(submit_request.report[0], 1U);
EXPECT_EQ(submit_request.report[1], 2U);
EXPECT_EQ(submit_request.report[2], 0U);
EXPECT_EQ(submit_request.report[3], 0U);
EXPECT_EQ(submit_request.report[4], 0U);
}
TEST(WindowsProtocolTest, PacksGamepadCreateRequest) {
lvh::CreateGamepadOptions options;
options.profile = lvh::profiles::dualsense_bluetooth();
options.metadata.stable_id = "client-0-controller-1";
const auto request = lvh::detail::windows::make_create_device_request(42, options);
EXPECT_EQ(request.version, LVH_WINDOWS_CONTROL_PROTOCOL_VERSION);
EXPECT_EQ(request.size, sizeof(request));
EXPECT_EQ(request.client_device_id, 42U);
EXPECT_EQ(request.device_type, LVH_WINDOWS_DEVICE_GAMEPAD);
EXPECT_EQ(request.bus_type, LVH_WINDOWS_BUS_BLUETOOTH);
EXPECT_EQ(request.gamepad_kind, LVH_WINDOWS_GAMEPAD_DUALSENSE);
EXPECT_EQ(request.hardware_ids.vendor_id, options.profile.vendor_id);
EXPECT_EQ(request.hardware_ids.product_id, options.profile.product_id);
EXPECT_EQ(request.hardware_ids.device_version, options.profile.version);
EXPECT_EQ(request.hardware_ids.report_id, options.profile.report_id);
EXPECT_EQ(request.report_sizes.input_report_size, options.profile.input_report_size);
EXPECT_EQ(request.report_sizes.output_report_size, options.profile.output_report_size);
EXPECT_EQ(request.report_sizes.report_descriptor_size, options.profile.report_descriptor.size());
EXPECT_EQ(request.report_descriptor[0], options.profile.report_descriptor[0]);
EXPECT_STREQ(request.name.data(), options.profile.name.c_str());
EXPECT_STREQ(request.manufacturer.data(), options.profile.manufacturer.c_str());
EXPECT_STREQ(request.stable_id.data(), options.metadata.stable_id.c_str());
EXPECT_NE(request.flags & LVH_WINDOWS_GAMEPAD_FLAG_SUPPORTS_RUMBLE, 0U);
EXPECT_NE(request.flags & LVH_WINDOWS_GAMEPAD_FLAG_SUPPORTS_MOTION, 0U);
EXPECT_NE(request.flags & LVH_WINDOWS_GAMEPAD_FLAG_SUPPORTS_TOUCHPAD, 0U);
EXPECT_NE(request.flags & LVH_WINDOWS_GAMEPAD_FLAG_SUPPORTS_BATTERY, 0U);
}
TEST(WindowsProtocolTest, PacksMouseCreateRequestWithoutGamepadPolicy) {
lvh::CreateMouseOptions options;
options.profile.device_type = lvh::DeviceType::mouse;
options.profile.gamepad_kind = lvh::GamepadProfileKind::generic;
options.profile.bus_type = lvh::BusType::bluetooth;
options.profile.vendor_id = 0x1234;
options.profile.product_id = 0x5678;
options.profile.version = 0x4321;
options.profile.report_id = 0U;
options.profile.input_report_size = LVH_WINDOWS_MOUSE_INPUT_REPORT_SIZE;
options.profile.output_report_size = 0U;
options.profile.name = "Configured mouse";
options.profile.manufacturer = "Configured manufacturer";
options.profile.report_descriptor = {0x05, 0x01, 0x09, 0x02};
options.profile.capabilities.supports_rumble = true;
options.stable_id = "configured-mouse";
const auto request = lvh::detail::windows::make_create_device_request(43U, options);
EXPECT_EQ(request.device_type, LVH_WINDOWS_DEVICE_MOUSE);
EXPECT_EQ(request.bus_type, LVH_WINDOWS_BUS_BLUETOOTH);
EXPECT_EQ(request.gamepad_kind, LVH_WINDOWS_GAMEPAD_GENERIC);
EXPECT_EQ(request.flags, 0U);
EXPECT_EQ(request.hardware_ids.vendor_id, 0x1234U);
EXPECT_EQ(request.hardware_ids.product_id, 0x5678U);
EXPECT_EQ(request.hardware_ids.device_version, 0x4321U);
EXPECT_EQ(request.hardware_ids.report_id, 0U);
EXPECT_EQ(request.report_sizes.input_report_size, LVH_WINDOWS_MOUSE_INPUT_REPORT_SIZE);
EXPECT_EQ(request.report_sizes.output_report_size, 0U);
EXPECT_STREQ(request.name.data(), "Configured mouse");
EXPECT_STREQ(request.manufacturer.data(), "Configured manufacturer");
EXPECT_STREQ(request.stable_id.data(), "configured-mouse");
}
TEST(WindowsProtocolTest, PacksKeyboardCreateRequestWithoutGamepadPolicy) {
lvh::CreateKeyboardOptions options;
options.profile.device_type = lvh::DeviceType::keyboard;
options.profile.gamepad_kind = lvh::GamepadProfileKind::generic;
options.profile.bus_type = lvh::BusType::usb;
options.profile.vendor_id = 0x1234;
options.profile.product_id = 0x5678;
options.profile.version = 0x4321;
options.profile.report_id = 0U;
options.profile.input_report_size = LVH_WINDOWS_KEYBOARD_INPUT_REPORT_SIZE;
options.profile.output_report_size = LVH_WINDOWS_KEYBOARD_OUTPUT_REPORT_SIZE;
options.profile.name = "Configured keyboard";
options.profile.manufacturer = "Configured manufacturer";
options.profile.report_descriptor = {0x05, 0x01, 0x09, 0x06};
options.profile.capabilities.supports_rumble = true;
options.stable_id = "configured-keyboard";
const auto request = lvh::detail::windows::make_create_device_request(44U, options);
EXPECT_EQ(request.device_type, LVH_WINDOWS_DEVICE_KEYBOARD);
EXPECT_EQ(request.bus_type, LVH_WINDOWS_BUS_USB);
EXPECT_EQ(request.gamepad_kind, LVH_WINDOWS_GAMEPAD_GENERIC);
EXPECT_EQ(request.flags, 0U);
EXPECT_EQ(request.hardware_ids.vendor_id, 0x1234U);
EXPECT_EQ(request.hardware_ids.product_id, 0x5678U);
EXPECT_EQ(request.hardware_ids.device_version, 0x4321U);
EXPECT_EQ(request.hardware_ids.report_id, 0U);
EXPECT_EQ(request.report_sizes.input_report_size, LVH_WINDOWS_KEYBOARD_INPUT_REPORT_SIZE);
EXPECT_EQ(request.report_sizes.output_report_size, LVH_WINDOWS_KEYBOARD_OUTPUT_REPORT_SIZE);
EXPECT_STREQ(request.name.data(), "Configured keyboard");
EXPECT_STREQ(request.manufacturer.data(), "Configured manufacturer");
EXPECT_STREQ(request.stable_id.data(), "configured-keyboard");
}
TEST(WindowsProtocolTest, UsesUsbFramingForPlayStationProfilesOnVhf) {
const std::array requested_profiles {
lvh::profiles::dualshock4(),
lvh::profiles::dualsense(),
};
const std::array usb_profiles {
lvh::profiles::dualshock4_usb(),
lvh::profiles::dualsense_usb(),
};
for (std::size_t index = 0; index < requested_profiles.size(); ++index) {
const auto &requested = requested_profiles[index];
const auto &usb = usb_profiles[index];
SCOPED_TRACE(requested.name);
ASSERT_EQ(requested.bus_type, lvh::BusType::bluetooth);
const auto effective = lvh::detail::windows::effective_vhf_gamepad_profile(requested);
EXPECT_EQ(effective.bus_type, lvh::BusType::usb);
EXPECT_EQ(effective.report_id, usb.report_id);
EXPECT_EQ(effective.input_report_size, usb.input_report_size);
EXPECT_EQ(effective.output_report_size, usb.output_report_size);
EXPECT_EQ(effective.report_descriptor, usb.report_descriptor);
EXPECT_EQ(effective.vendor_id, requested.vendor_id);
EXPECT_EQ(effective.product_id, requested.product_id);
EXPECT_EQ(effective.version, requested.version);
EXPECT_EQ(effective.name, requested.name);
EXPECT_EQ(effective.manufacturer, requested.manufacturer);
EXPECT_EQ(effective.capabilities.supports_rumble, requested.capabilities.supports_rumble);
EXPECT_EQ(effective.capabilities.supports_rgb_led, requested.capabilities.supports_rgb_led);
}
}
TEST(WindowsProtocolTest, PacksGenericUnknownBusGamepadCreateRequestWithoutOptionalFlags) {
lvh::CreateGamepadOptions options;
options.profile = minimal_gamepad_profile();
options.profile.bus_type = lvh::BusType::unknown;
options.profile.output_report_size = 0;
const auto request = lvh::detail::windows::make_create_device_request(7, options);
EXPECT_EQ(request.bus_type, LVH_WINDOWS_BUS_UNKNOWN);
EXPECT_EQ(request.gamepad_kind, LVH_WINDOWS_GAMEPAD_GENERIC);
EXPECT_EQ(request.flags, 0U);
EXPECT_EQ(request.report_sizes.output_report_size, 0U);
EXPECT_EQ(request.report_sizes.name_size, options.profile.name.size());
EXPECT_EQ(request.report_sizes.manufacturer_size, options.profile.manufacturer.size());
EXPECT_EQ(request.report_sizes.stable_id_size, 0U);
}
TEST(WindowsProtocolTest, PresentsXboxSeriesNativeWindowsIdentityAndReportShape) {
lvh::CreateGamepadOptions options;
options.profile = lvh::profiles::xbox_series();
const auto request = lvh::detail::windows::make_create_device_request(8, options);
const std::vector<std::uint8_t> descriptor(
request.report_descriptor.data(),
request.report_descriptor.data() + request.report_sizes.report_descriptor_size
);
EXPECT_EQ(request.bus_type, LVH_WINDOWS_BUS_USB);
EXPECT_EQ(request.hardware_ids.vendor_id, options.profile.vendor_id);
EXPECT_EQ(request.hardware_ids.product_id, lvh::detail::windows::xbox_series_windows_product_id);
EXPECT_EQ(request.hardware_ids.device_version, lvh::detail::windows::xbox_series_windows_device_version);
EXPECT_EQ(request.hardware_ids.report_id, options.profile.report_id);
EXPECT_EQ(request.report_sizes.input_report_size, options.profile.input_report_size);
EXPECT_EQ(request.report_sizes.output_report_size, options.profile.output_report_size);
EXPECT_EQ(request.report_sizes.input_report_size, lvh::detail::windows::xbox_series_windows_input_report_size);
EXPECT_EQ(request.report_sizes.output_report_size, lvh::detail::windows::xbox_series_windows_output_report_size);
EXPECT_EQ(descriptor, options.profile.report_descriptor);
constexpr std::array<std::uint8_t, 6> hidapi_button_range {0x05, 0x09, 0x19, 0x01, 0x29, 0x0C};
EXPECT_NE(std::ranges::search(descriptor, hidapi_button_range).begin(), descriptor.end());
}
TEST(WindowsProtocolTest, KeepsXboxSeriesNativeReportShape) {
using enum lvh::GamepadButton;
const auto profile = lvh::profiles::xbox_series();
lvh::GamepadState state;
state.buttons.set(a);
state.buttons.set(back);
state.buttons.set(start);
state.buttons.set(guide);
state.buttons.set(left_stick);
state.buttons.set(right_stick);
state.buttons.set(left_shoulder);
state.buttons.set(right_shoulder);
state.buttons.set(misc1);
const auto report = lvh::reports::pack_input_report(profile, state);
ASSERT_EQ(report.size(), lvh::detail::windows::xbox_series_windows_input_report_size);
const auto buttons = static_cast<std::uint16_t>(
static_cast<std::uint16_t>(report[12]) |
static_cast<std::uint16_t>(static_cast<std::uint16_t>(report[13]) << 8U)
);
EXPECT_NE(buttons & 0x0001U, 0U); // A.
EXPECT_NE(buttons & 0x0010U, 0U); // LB.
EXPECT_NE(buttons & 0x0020U, 0U); // RB.
EXPECT_NE(buttons & 0x0040U, 0U); // Back/View.
EXPECT_NE(buttons & 0x0080U, 0U); // Start/Menu.
EXPECT_NE(buttons & 0x0100U, 0U); // L3.
EXPECT_NE(buttons & 0x0200U, 0U); // R3.
EXPECT_NE(buttons & 0x0800U, 0U); // Share / misc1.
EXPECT_EQ(report[15], 0x01U); // Guide.
}
TEST(WindowsProtocolTest, PresentsBuiltInGenericControllerAsDirectInputPidJoystick) {
lvh::CreateGamepadOptions options;
options.profile = lvh::profiles::generic_gamepad();
const auto request = lvh::detail::windows::make_create_device_request(8, options);
const std::vector<std::uint8_t> descriptor(
request.report_descriptor.data(),
request.report_descriptor.data() + request.report_sizes.report_descriptor_size
);
ASSERT_GE(descriptor.size(), 6U);
EXPECT_EQ(descriptor[0], 0x05U);
EXPECT_EQ(descriptor[1], 0x01U);
EXPECT_EQ(descriptor[2], 0x09U);
EXPECT_EQ(descriptor[3], 0x04U); // Joystick TLC required by DirectInput PID.
EXPECT_EQ(descriptor[4], 0xA1U);
EXPECT_EQ(descriptor[5], 0x01U);
EXPECT_LE(descriptor.size(), LVH_WINDOWS_MAX_REPORT_DESCRIPTOR_SIZE);
EXPECT_EQ(request.report_sizes.output_report_size, lvh::detail::windows::generic_pid_output_report_size);
constexpr std::array<std::uint8_t, 6> set_effect {0x09, 0x21, 0xA1, 0x02, 0x85, 0x11};
constexpr std::array<std::uint8_t, 6> set_periodic {0x09, 0x6E, 0xA1, 0x02, 0x85, 0x14};
constexpr std::array<std::uint8_t, 6> set_constant {0x09, 0x73, 0xA1, 0x02, 0x85, 0x15};
constexpr std::array<std::uint8_t, 6> create_effect {0x09, 0xAB, 0xA1, 0x02, 0x85, 0x11};
EXPECT_NE(std::ranges::search(descriptor, set_effect).begin(), descriptor.end());
EXPECT_NE(std::ranges::search(descriptor, set_periodic).begin(), descriptor.end());
EXPECT_NE(std::ranges::search(descriptor, set_constant).begin(), descriptor.end());
EXPECT_NE(std::ranges::search(descriptor, create_effect).begin(), descriptor.end());
// HIDMaestro's working UMDF contract intentionally serves these through
// GetFeature without declaring the three Feature collections. Declaring
// them alongside Create New Effect is known to fault pid.dll CreateEffect.
constexpr std::array<std::uint8_t, 6> block_load_feature {0x09, 0x89, 0xA1, 0x02, 0x85, 0x12};
constexpr std::array<std::uint8_t, 6> pool_feature {0x09, 0x7F, 0xA1, 0x02, 0x85, 0x13};
constexpr std::array<std::uint8_t, 6> state_feature {0x09, 0x92, 0xA1, 0x02, 0x85, 0x14};
EXPECT_EQ(std::ranges::search(descriptor, block_load_feature).begin(), descriptor.end());
EXPECT_EQ(std::ranges::search(descriptor, pool_feature).begin(), descriptor.end());
EXPECT_EQ(std::ranges::search(descriptor, state_feature).begin(), descriptor.end());
// DirectInput requires the complete PID Output report set even though the
// backend only normalizes Constant and Sine effects as gamepad rumble.
constexpr std::array<std::uint8_t, 6> set_ramp {0x09, 0x74, 0xA1, 0x02, 0x85, 0x16};
EXPECT_NE(std::ranges::search(descriptor, set_ramp).begin(), descriptor.end());
// The public profile remains platform-neutral; Windows applies its transport contract at creation.
ASSERT_GE(options.profile.report_descriptor.size(), 4U);
EXPECT_EQ(options.profile.report_descriptor[3], 0x05U);
EXPECT_EQ(options.profile.output_report_size, 9U);
}
TEST(WindowsProtocolTest, InvertsOnlyWindowsGenericTriggerBytes) {
const std::vector<std::uint8_t> neutral {1, 0, 0, 128, 128, 128, 128, 0, 0};
const auto neutral_windows = lvh::detail::windows::make_generic_windows_input_report(neutral);
ASSERT_EQ(neutral_windows.size(), neutral.size());
EXPECT_EQ(neutral_windows[7], 255U);
EXPECT_EQ(neutral_windows[8], 255U);
const std::vector<std::uint8_t> active {1, 0, 0, 128, 128, 128, 128, 64, 255};
const auto active_windows = lvh::detail::windows::make_generic_windows_input_report(active);
EXPECT_EQ(active_windows[7], 191U);
EXPECT_EQ(active_windows[8], 0U);
EXPECT_EQ(
lvh::detail::windows::make_generic_windows_input_report(byte_report({1, 2, 3})),
byte_report({1, 2, 3})
);
}
TEST(WindowsProtocolTest, ImplementsGenericPidFeatureAllocationHandshake) {
using namespace lvh::detail::windows;
GenericPidFeatureState state;
ASSERT_TRUE(
state.handle_set_feature(
generic_pid_create_new_effect_report_id,
byte_report({0x11, 0x01, 0x00, 0x00})
)
);
const auto block_load = state.get_feature_report(generic_pid_block_load_report_id);
ASSERT_TRUE(block_load.has_value());
ASSERT_EQ(block_load->size(), 5U);
EXPECT_EQ((*block_load)[0], generic_pid_block_load_report_id);
EXPECT_EQ((*block_load)[1], 1U);
EXPECT_EQ((*block_load)[2], 1U); // Block Load Success.
const auto pool = state.get_feature_report(generic_pid_pool_report_id);
ASSERT_TRUE(pool.has_value());
ASSERT_EQ(pool->size(), 5U);
EXPECT_EQ((*pool)[3], generic_pid_max_effects);
EXPECT_EQ(std::byte {(*pool)[4]} & std::byte {0x01}, std::byte {0x01}); // Device-managed pool.
ASSERT_TRUE(
state.handle_output_report(
generic_pid_effect_operation_report_id,
byte_report({0x1A, 0x01, 0x01, 0x01})
)
);
const auto playing = state.get_feature_report(generic_pid_state_report_id);
ASSERT_TRUE(playing.has_value());
EXPECT_EQ((*playing)[1], 1U);
EXPECT_EQ(std::byte {(*playing)[2]} & std::byte {0x20}, std::byte {0x20});
ASSERT_TRUE(
state.handle_output_report(generic_pid_block_free_report_id, byte_report({0x1B, 0x01}))
);
const auto freed = state.get_feature_report(generic_pid_state_report_id);
ASSERT_TRUE(freed.has_value());
EXPECT_EQ((*freed)[1], 0U);
EXPECT_EQ(std::byte {(*freed)[2]} & std::byte {0x20}, std::byte {0x00});
EXPECT_FALSE(state.handle_set_feature(0xFF, byte_report({0xFF})));
EXPECT_FALSE(state.get_feature_report(0xFF).has_value());
}
TEST(WindowsProtocolTest, DecodesGenericPidConstantAndPeriodicEffectsAsRumble) {
using namespace lvh::detail::windows;
GenericPidRumbleState state;
std::vector<std::uint8_t> set_effect(generic_pid_output_report_size, 0);
set_effect[0] = generic_pid_set_effect_report_id;
set_effect[1] = 1; // Effect Block Index.
set_effect[2] = 1; // Constant Force.
set_effect[3] = 0xFF; // Infinite duration (Null).
set_effect[4] = 0xFF;
set_effect[11] = 255; // Effect gain.
auto update = state.handle_output_report(set_effect);
EXPECT_TRUE(update.recognized);
EXPECT_FALSE(update.rumble_changed);
update = state.handle_output_report(byte_report({generic_pid_set_constant_force_report_id, 1, 0x10, 0x27}));
EXPECT_TRUE(update.recognized);
EXPECT_FALSE(update.rumble_changed);
update = state.handle_output_report(byte_report({generic_pid_effect_operation_report_id, 1, 1, 1}));
EXPECT_TRUE(update.recognized);
EXPECT_TRUE(update.rumble_changed);
EXPECT_EQ(update.strength, 65535U);
update = state.handle_output_report(byte_report({generic_pid_device_gain_report_id, 128}));
EXPECT_TRUE(update.rumble_changed);
EXPECT_EQ(update.strength, 32896U);
update = state.handle_output_report(byte_report({generic_pid_effect_operation_report_id, 1, 3, 0}));
EXPECT_TRUE(update.rumble_changed);
EXPECT_EQ(update.strength, 0U);
std::ranges::fill(set_effect, 0);
set_effect[0] = generic_pid_set_effect_report_id;
set_effect[1] = 2;
set_effect[2] = 2; // Sine periodic.
set_effect[3] = 0xFF;
set_effect[4] = 0xFF;
set_effect[11] = 255;
EXPECT_TRUE(state.handle_output_report(set_effect).recognized);
EXPECT_TRUE(
state.handle_output_report(byte_report({generic_pid_set_periodic_report_id, 2, 0x88, 0x13})).recognized
);
update = state.handle_output_report(byte_report({generic_pid_effect_operation_report_id, 2, 1, 1}));
EXPECT_TRUE(update.rumble_changed);
EXPECT_GT(update.strength, 0U);
update = state.handle_output_report(byte_report({generic_pid_device_control_report_id, 2}));
EXPECT_TRUE(update.rumble_changed);
EXPECT_EQ(update.strength, 0U);
EXPECT_FALSE(state.handle_output_report(byte_report({0xFF})).recognized);
}
TEST(WindowsProtocolTest, HonorsGenericPidStartDelayDurationLoopCountAndExplicitStop) {
using namespace lvh::detail::windows;
using namespace std::chrono_literals;
GenericPidRumbleState state;
const auto start_time = GenericPidRumbleState::TimePoint {};
std::vector<std::uint8_t> set_effect(generic_pid_output_report_size, 0);
set_effect[0] = generic_pid_set_effect_report_id;
set_effect[1] = 1; // Effect Block Index.
set_effect[2] = 1; // Constant Force.
set_effect[3] = 100; // 100 ms duration.
set_effect[9] = 20; // 20 ms start delay.
set_effect[11] = 255; // Effect gain.
EXPECT_TRUE(state.handle_output_report(set_effect, start_time).recognized);
EXPECT_TRUE(
state
.handle_output_report(
byte_report({generic_pid_set_constant_force_report_id, 1, 0x10, 0x27}),
start_time
)
.recognized
);
auto update = state.handle_output_report(
byte_report({generic_pid_effect_operation_report_id, 1, 1, 2}),
start_time
);
EXPECT_TRUE(update.rumble_changed);
EXPECT_EQ(update.strength, 0U);
ASSERT_TRUE(state.next_transition().has_value());
EXPECT_EQ(*state.next_transition(), start_time + 20ms);
set_effect[9] = 30; // Reconfigure the pending start delay.
update = state.handle_output_report(set_effect, start_time + 5ms);
EXPECT_TRUE(update.rumble_changed);
EXPECT_EQ(update.strength, 0U);
ASSERT_TRUE(state.next_transition().has_value());
EXPECT_EQ(*state.next_transition(), start_time + 30ms);
update = state.advance(start_time + 29ms);
EXPECT_FALSE(update.rumble_changed);
update = state.advance(start_time + 30ms);
EXPECT_TRUE(update.rumble_changed);
EXPECT_EQ(update.strength, 65535U);
set_effect[3] = 150; // Extend each loop while the effect is active.
set_effect[9] = 30;
update = state.handle_output_report(set_effect, start_time + 40ms);
EXPECT_TRUE(update.rumble_changed);
EXPECT_EQ(update.strength, 65535U);
ASSERT_TRUE(state.next_transition().has_value());
EXPECT_EQ(*state.next_transition(), start_time + 330ms);
update = state.advance(start_time + 329ms);
EXPECT_FALSE(update.rumble_changed);
update = state.advance(start_time + 330ms);
EXPECT_TRUE(update.rumble_changed);
EXPECT_EQ(update.strength, 0U);
EXPECT_FALSE(state.next_transition().has_value());
EXPECT_FALSE(state.advance(start_time + 500ms).rumble_changed);
set_effect[3] = 0xFF; // Infinite duration (Null).
set_effect[4] = 0xFF;
set_effect[9] = 0;
EXPECT_TRUE(state.handle_output_report(set_effect, start_time + 500ms).recognized);
update = state.handle_output_report(
byte_report({generic_pid_effect_operation_report_id, 1, 1, 1}),
start_time + 500ms
);
EXPECT_TRUE(update.rumble_changed);
EXPECT_EQ(update.strength, 65535U);
EXPECT_FALSE(state.next_transition().has_value());
update = state.handle_output_report(
byte_report({generic_pid_effect_operation_report_id, 1, 3, 0}),
start_time + 501ms
);
EXPECT_TRUE(update.rumble_changed);
EXPECT_EQ(update.strength, 0U);
}
TEST(WindowsProtocolTest, TruncatesOversizedGamepadCreateRequestFields) {
lvh::CreateGamepadOptions options;
options.profile = minimal_gamepad_profile();
options.profile.input_report_size = LVH_WINDOWS_MAX_INPUT_REPORT_SIZE + 1U;
options.profile.output_report_size = LVH_WINDOWS_MAX_OUTPUT_REPORT_SIZE + 1U;
options.profile.report_descriptor.assign(LVH_WINDOWS_MAX_REPORT_DESCRIPTOR_SIZE + 5U, 0xAB);
options.profile.name.assign(LVH_WINDOWS_MAX_DEVICE_NAME_SIZE + 5U, 'n');
options.profile.manufacturer.assign(LVH_WINDOWS_MAX_MANUFACTURER_SIZE + 5U, 'm');
options.metadata.stable_id.assign(LVH_WINDOWS_MAX_STABLE_ID_SIZE + 5U, 's');
const auto request = lvh::detail::windows::make_create_device_request(9, options);
EXPECT_EQ(request.report_sizes.input_report_size, LVH_WINDOWS_MAX_INPUT_REPORT_SIZE);
EXPECT_EQ(request.report_sizes.output_report_size, LVH_WINDOWS_MAX_OUTPUT_REPORT_SIZE);
EXPECT_EQ(request.report_sizes.report_descriptor_size, LVH_WINDOWS_MAX_REPORT_DESCRIPTOR_SIZE);
EXPECT_EQ(request.report_descriptor[0], 0xABU);
EXPECT_EQ(request.report_descriptor[LVH_WINDOWS_MAX_REPORT_DESCRIPTOR_SIZE - 1U], 0xABU);
EXPECT_EQ(request.report_sizes.name_size, LVH_WINDOWS_MAX_DEVICE_NAME_SIZE - 1U);
EXPECT_EQ(request.name[LVH_WINDOWS_MAX_DEVICE_NAME_SIZE - 2U], 'n');
EXPECT_EQ(request.name[LVH_WINDOWS_MAX_DEVICE_NAME_SIZE - 1U], '\0');
EXPECT_EQ(request.report_sizes.manufacturer_size, LVH_WINDOWS_MAX_MANUFACTURER_SIZE - 1U);
EXPECT_EQ(request.manufacturer[LVH_WINDOWS_MAX_MANUFACTURER_SIZE - 2U], 'm');
EXPECT_EQ(request.manufacturer[LVH_WINDOWS_MAX_MANUFACTURER_SIZE - 1U], '\0');
EXPECT_EQ(request.report_sizes.stable_id_size, LVH_WINDOWS_MAX_STABLE_ID_SIZE - 1U);
EXPECT_EQ(request.stable_id[LVH_WINDOWS_MAX_STABLE_ID_SIZE - 2U], 's');
EXPECT_EQ(request.stable_id[LVH_WINDOWS_MAX_STABLE_ID_SIZE - 1U], '\0');
}
TEST(WindowsProtocolTest, PacksSubmitAndDestroyRequests) {
const std::vector<std::uint8_t> report {1, 2, 3, 4, 5};
const auto session_token = test_session_token();
const auto submit = lvh::detail::windows::make_submit_input_report_request(17, session_token, report);
EXPECT_EQ(submit.version, LVH_WINDOWS_CONTROL_PROTOCOL_VERSION);
EXPECT_EQ(submit.size, sizeof(submit));
EXPECT_EQ(submit.driver_device_id, 17U);
EXPECT_EQ(submit.session_token.bytes[0], session_token.bytes[0]);
EXPECT_EQ(submit.session_token.bytes[LVH_WINDOWS_SESSION_TOKEN_SIZE - 1U], session_token.bytes[LVH_WINDOWS_SESSION_TOKEN_SIZE - 1U]);
EXPECT_EQ(submit.report_size, report.size());
EXPECT_EQ(submit.report[0], report[0]);
EXPECT_EQ(submit.report[4], report[4]);
const auto destroy = lvh::detail::windows::make_destroy_device_request(17, session_token);
EXPECT_EQ(destroy.version, LVH_WINDOWS_CONTROL_PROTOCOL_VERSION);
EXPECT_EQ(destroy.size, sizeof(destroy));
EXPECT_EQ(destroy.driver_device_id, 17U);
EXPECT_EQ(destroy.session_token.bytes[0], session_token.bytes[0]);
EXPECT_EQ(destroy.session_token.bytes[LVH_WINDOWS_SESSION_TOKEN_SIZE - 1U], session_token.bytes[LVH_WINDOWS_SESSION_TOKEN_SIZE - 1U]);
const auto reset = lvh::detail::windows::make_reset_devices_request();
EXPECT_EQ(reset.version, LVH_WINDOWS_CONTROL_PROTOCOL_VERSION);
EXPECT_EQ(reset.size, sizeof(reset));
}
TEST(WindowsProtocolTest, PacksNativeXbox360InputState) {
using enum lvh::GamepadButton;
lvh::GamepadState state;
state.buttons.set(a);
state.buttons.set(y);
state.buttons.set(back);
state.buttons.set(guide);
state.buttons.set(dpad_up);
state.buttons.set(dpad_right);
state.left_stick = {-1.0F, 1.0F};
state.right_stick = {0.5F, -0.5F};
state.left_trigger = 0.25F;
state.right_trigger = 1.0F;
const auto token = test_session_token();
const auto request = lvh::detail::windows::make_xbox_360_submit_input_request(23U, token, state);
EXPECT_EQ(request.version, LVH_WINDOWS_XBOX360_PROTOCOL_VERSION);
EXPECT_EQ(request.size, sizeof(request));
EXPECT_EQ(request.driver_device_id, 23U);
EXPECT_EQ(request.session_token.bytes, token.bytes);
EXPECT_EQ(
request.state.buttons,
LVH_WINDOWS_XINPUT_A | LVH_WINDOWS_XINPUT_Y | LVH_WINDOWS_XINPUT_BACK |
LVH_WINDOWS_XINPUT_GUIDE | LVH_WINDOWS_XINPUT_DPAD_UP | LVH_WINDOWS_XINPUT_DPAD_RIGHT
);
EXPECT_EQ(request.state.left_trigger, 64U);
EXPECT_EQ(request.state.right_trigger, 255U);
EXPECT_EQ(request.state.left_thumb_x, -32768);
EXPECT_EQ(request.state.left_thumb_y, 32767);
EXPECT_EQ(request.state.right_thumb_x, 16384);
EXPECT_EQ(request.state.right_thumb_y, -16384);
}
TEST(WindowsProtocolTest, CompatibilityRequestHelpersUseEmptySessionToken) {
const auto submit = lvh::detail::windows::make_submit_input_report_request(17, std::vector<std::uint8_t> {1});
const auto destroy = lvh::detail::windows::make_destroy_device_request(17);
EXPECT_EQ(submit.session_token.bytes[0], 0U);
EXPECT_EQ(submit.session_token.bytes[LVH_WINDOWS_SESSION_TOKEN_SIZE - 1U], 0U);
EXPECT_EQ(destroy.session_token.bytes[0], 0U);
EXPECT_EQ(destroy.session_token.bytes[LVH_WINDOWS_SESSION_TOKEN_SIZE - 1U], 0U);
}
TEST(WindowsProtocolTest, SubmitInputReportTruncatesAndZeroFills) {
std::vector<std::uint8_t> oversized_report(LVH_WINDOWS_MAX_INPUT_REPORT_SIZE + 3U, 0x7F);
const auto oversized = lvh::detail::windows::make_submit_input_report_request(19, oversized_report);
EXPECT_EQ(oversized.report_size, LVH_WINDOWS_MAX_INPUT_REPORT_SIZE);
EXPECT_EQ(oversized.report[0], 0x7FU);
EXPECT_EQ(oversized.report[LVH_WINDOWS_MAX_INPUT_REPORT_SIZE - 1U], 0x7FU);
const auto empty = lvh::detail::windows::make_submit_input_report_request(19, {});
EXPECT_EQ(empty.report_size, 0U);
EXPECT_EQ(empty.report[0], 0U);
EXPECT_EQ(empty.report[LVH_WINDOWS_MAX_INPUT_REPORT_SIZE - 1U], 0U);
}