/** * @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 #include #include #include #include #include #include #include #include // lib includes #include #include 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 byte_report(std::initializer_list 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(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(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(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 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 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 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( static_cast(report[12]) | static_cast(static_cast(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 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 set_effect {0x09, 0x21, 0xA1, 0x02, 0x85, 0x11}; constexpr std::array set_periodic {0x09, 0x6E, 0xA1, 0x02, 0x85, 0x14}; constexpr std::array set_constant {0x09, 0x73, 0xA1, 0x02, 0x85, 0x15}; constexpr std::array 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 block_load_feature {0x09, 0x89, 0xA1, 0x02, 0x85, 0x12}; constexpr std::array pool_feature {0x09, 0x7F, 0xA1, 0x02, 0x85, 0x13}; constexpr std::array 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 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 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 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 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 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 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 {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 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); }