/** * @file tests/unit/test_profiles.cpp * @brief Unit tests for built-in gamepad profiles. */ // standard includes #include #include #include #include #include #include // local includes #include "fixtures/fixtures.hpp" #include #include namespace { struct PackedButtonCase { lvh::GamepadButton button; std::uint16_t bit; }; struct PackedByteButtonCase { lvh::GamepadButton button; std::size_t offset; std::uint8_t mask; }; template void expect_descriptor_contains( const lvh::DeviceProfile &profile, const std::array &expected ) { const auto match = std::ranges::search(profile.report_descriptor, expected); EXPECT_NE(match.begin(), profile.report_descriptor.end()); } std::uint16_t read_u16_le(const std::vector &report, std::size_t offset) { const auto low = std::byte {report[offset]}; const auto high = std::byte {report[offset + 1U]}; return static_cast( std::to_integer(low) | (std::to_integer(high) << 8U) ); } void expect_packed_button_bits( const lvh::DeviceProfile &profile, const std::vector &button_cases, std::size_t report_offset ) { for (const auto &[button, bit] : button_cases) { lvh::GamepadState state; state.buttons.set(button); const auto report = lvh::reports::pack_input_report(profile, state); ASSERT_GT(report.size(), report_offset + 1U); EXPECT_EQ(read_u16_le(report, report_offset), static_cast(1U << bit)) << "logical button " << static_cast(std::to_underlying(button)); } } } // namespace TEST(ProfileTest, BuiltInProfilesHaveDescriptors) { const auto profiles = lvh::profiles::built_in_gamepad_profiles(); ASSERT_GE(profiles.size(), 4U); for (const auto &profile : profiles) { EXPECT_EQ(profile.device_type, lvh::DeviceType::gamepad); EXPECT_FALSE(profile.name.empty()); EXPECT_NE(profile.vendor_id, 0); EXPECT_NE(profile.product_id, 0); EXPECT_GE(profile.input_report_size, 9U); EXPECT_FALSE(profile.report_descriptor.empty()); } } TEST(ProfileTest, BuiltInProfilesUseDefaultDeviceNames) { EXPECT_EQ(lvh::profiles::generic_gamepad().name, "(libvirtualhid) Generic Controller"); EXPECT_EQ(lvh::profiles::xbox_360().name, "(libvirtualhid) X-Box 360 Controller"); EXPECT_EQ(lvh::profiles::xbox_one().name, "(libvirtualhid) X-Box One Controller"); EXPECT_EQ(lvh::profiles::xbox_series().name, "(libvirtualhid) X-Box Series Controller"); EXPECT_EQ(lvh::profiles::dualshock4().name, "(libvirtualhid) PS4 Controller"); EXPECT_EQ(lvh::profiles::dualsense().name, "(libvirtualhid) PS5 Controller"); EXPECT_EQ(lvh::profiles::switch_pro().name, "(libvirtualhid) Nintendo Pro Controller"); } TEST(ProfileTest, StreamingControllerProfilesArePresent) { const auto xbox_one = lvh::profiles::xbox_one(); const auto dualshock4 = lvh::profiles::dualshock4(); const auto dualsense = lvh::profiles::dualsense(); const auto switch_pro = lvh::profiles::switch_pro(); EXPECT_EQ(xbox_one.vendor_id, 0x045E); EXPECT_EQ(xbox_one.product_id, 0x02EA); EXPECT_EQ(xbox_one.bus_type, lvh::BusType::usb); EXPECT_EQ(xbox_one.manufacturer, "Microsoft"); EXPECT_TRUE(xbox_one.capabilities.supports_rumble); EXPECT_TRUE(xbox_one.capabilities.supports_trigger_rumble); EXPECT_TRUE(xbox_one.capabilities.supports_battery); EXPECT_EQ(xbox_one.report_id, 0); EXPECT_EQ(xbox_one.input_report_size, 17U); EXPECT_EQ(xbox_one.output_report_size, 8U); const auto xbox_series = lvh::profiles::xbox_series(); EXPECT_EQ(xbox_series.vendor_id, 0x045E); EXPECT_EQ(xbox_series.product_id, 0x0B12); EXPECT_EQ(xbox_series.bus_type, lvh::BusType::usb); EXPECT_EQ(xbox_series.manufacturer, "Microsoft"); EXPECT_EQ(xbox_series.report_id, 0); EXPECT_EQ(xbox_series.input_report_size, 17U); EXPECT_EQ(xbox_series.output_report_size, 8U); EXPECT_TRUE(xbox_series.capabilities.supports_trigger_rumble); EXPECT_TRUE(xbox_series.capabilities.supports_battery); const std::array xbox_gip_stick_axis_descriptor { 0x15, 0x00, 0x27, 0xFF, 0xFF, 0x00, 0x00, 0x95, 0x02, 0x75, 0x10, }; EXPECT_TRUE( std::ranges::search(xbox_one.report_descriptor, xbox_gip_stick_axis_descriptor).begin() != xbox_one.report_descriptor.end() ); const std::array right_stick_usage_descriptor { 0x09, 0x33, 0x09, 0x34, 0x15, 0x00, 0x27, 0xFF, 0xFF, }; EXPECT_TRUE( std::ranges::search(xbox_one.report_descriptor, right_stick_usage_descriptor).begin() != xbox_one.report_descriptor.end() ); const std::array trigger_usage_descriptor { 0x09, 0x32, 0x15, 0x00, 0x26, 0xFF, 0x03, 0x95, 0x01, }; EXPECT_TRUE( std::ranges::search(xbox_one.report_descriptor, trigger_usage_descriptor).begin() != xbox_one.report_descriptor.end() ); const std::array right_trigger_usage_descriptor { 0x09, 0x35, 0x15, 0x00, 0x26, 0xFF, 0x03, 0x95, 0x01, }; EXPECT_TRUE( std::ranges::search(xbox_one.report_descriptor, right_trigger_usage_descriptor).begin() != xbox_one.report_descriptor.end() ); EXPECT_EQ(dualshock4.vendor_id, 0x054C); EXPECT_EQ(dualshock4.product_id, 0x05C4); EXPECT_EQ(dualshock4.version, 0x0100); EXPECT_EQ(dualshock4.bus_type, lvh::BusType::bluetooth); EXPECT_EQ(dualshock4.report_id, 0x11); EXPECT_EQ(dualshock4.input_report_size, 78U); EXPECT_EQ(dualshock4.output_report_size, 78U); EXPECT_TRUE(dualshock4.capabilities.supports_motion); EXPECT_TRUE(dualshock4.capabilities.supports_touchpad); EXPECT_TRUE(dualshock4.capabilities.supports_rgb_led); EXPECT_TRUE(dualshock4.capabilities.supports_battery); EXPECT_FALSE(dualshock4.capabilities.supports_adaptive_triggers); EXPECT_EQ(dualshock4.manufacturer, "Sony Computer Entertainment"); const auto dualshock4_bluetooth = lvh::profiles::dualshock4_bluetooth(); EXPECT_EQ(dualshock4_bluetooth.bus_type, lvh::BusType::bluetooth); EXPECT_EQ(dualshock4_bluetooth.version, 0x0100); EXPECT_EQ(dualshock4_bluetooth.name, "(libvirtualhid) PS4 Controller"); EXPECT_EQ(dualshock4_bluetooth.manufacturer, "Sony Computer Entertainment"); EXPECT_EQ(dualshock4_bluetooth.report_id, 0x11); EXPECT_EQ(dualshock4_bluetooth.input_report_size, 78U); EXPECT_EQ(dualshock4_bluetooth.output_report_size, 78U); EXPECT_EQ(dualshock4_bluetooth.report_descriptor, dualshock4.report_descriptor); const auto dualshock4_usb = lvh::profiles::dualshock4_usb(); EXPECT_EQ(dualshock4_usb.bus_type, lvh::BusType::usb); EXPECT_EQ(dualshock4_usb.report_id, 0x01); EXPECT_EQ(dualshock4_usb.input_report_size, 64U); EXPECT_EQ(dualshock4_usb.output_report_size, 32U); EXPECT_NE(dualshock4_usb.report_descriptor, dualshock4.report_descriptor); EXPECT_EQ(dualsense.vendor_id, 0x054C); EXPECT_EQ(dualsense.bus_type, lvh::BusType::bluetooth); EXPECT_EQ(dualsense.report_id, 0x31); EXPECT_EQ(dualsense.input_report_size, 78U); EXPECT_EQ(dualsense.output_report_size, 78U); EXPECT_TRUE(dualsense.capabilities.supports_motion); EXPECT_TRUE(dualsense.capabilities.supports_touchpad); EXPECT_TRUE(dualsense.capabilities.supports_rgb_led); EXPECT_TRUE(dualsense.capabilities.supports_adaptive_triggers); EXPECT_GT(dualsense.input_report_size, 14U); EXPECT_GT(dualsense.output_report_size, 5U); EXPECT_EQ(dualsense.manufacturer, "Sony Interactive Entertainment"); const auto dualsense_bluetooth = lvh::profiles::dualsense_bluetooth(); EXPECT_EQ(dualsense_bluetooth.bus_type, lvh::BusType::bluetooth); EXPECT_EQ(dualsense_bluetooth.name, "(libvirtualhid) PS5 Controller"); EXPECT_EQ(dualsense_bluetooth.report_id, 0x31); EXPECT_EQ(dualsense_bluetooth.input_report_size, 78U); EXPECT_EQ(dualsense_bluetooth.output_report_size, 78U); EXPECT_EQ(dualsense_bluetooth.report_descriptor, dualsense.report_descriptor); const auto dualsense_usb = lvh::profiles::dualsense_usb(); EXPECT_EQ(dualsense_usb.bus_type, lvh::BusType::usb); EXPECT_EQ(dualsense_usb.report_id, 0x01); EXPECT_EQ(dualsense_usb.input_report_size, 64U); EXPECT_EQ(dualsense_usb.output_report_size, 48U); EXPECT_NE(dualsense_usb.report_descriptor, dualsense.report_descriptor); EXPECT_EQ(switch_pro.vendor_id, 0x057E); EXPECT_EQ(switch_pro.product_id, 0x2009); EXPECT_EQ(switch_pro.version, 0x0210); EXPECT_EQ(switch_pro.manufacturer, "Nintendo Co., Ltd."); EXPECT_EQ(switch_pro.report_id, 0x30); EXPECT_EQ(switch_pro.input_report_size, 64U); EXPECT_EQ(switch_pro.output_report_size, 64U); EXPECT_TRUE(switch_pro.capabilities.supports_rumble); EXPECT_TRUE(switch_pro.capabilities.supports_motion); EXPECT_TRUE(switch_pro.capabilities.supports_rgb_led); EXPECT_TRUE(switch_pro.capabilities.supports_battery); const auto generic = lvh::profiles::generic_gamepad(); const std::array standard_button_descriptor { 0x05, 0x09, // Usage Page (Button) 0x19, 0x01, // Usage Minimum (Button 1) 0x29, 0x10, // Usage Maximum (Button 16) 0x15, 0x00, 0x25, 0x01, 0x75, 0x01, 0x95, 0x10, 0x81, 0x02, }; EXPECT_TRUE( std::ranges::search(generic.report_descriptor, standard_button_descriptor).begin() != generic.report_descriptor.end() ); const std::array standard_axis_order { 0x09, 0x30, 0x09, 0x31, 0x09, 0x33, 0x09, 0x34, 0x09, 0x32, 0x09, 0x35, }; EXPECT_TRUE( std::ranges::search(generic.report_descriptor, standard_axis_order).begin() != generic.report_descriptor.end() ); EXPECT_NE(switch_pro.report_descriptor, generic.report_descriptor); const std::array switch_pro_report_id_descriptor {0x85, 0x30}; EXPECT_TRUE( std::ranges::search(switch_pro.report_descriptor, switch_pro_report_id_descriptor).begin() != switch_pro.report_descriptor.end() ); } TEST(ProfileTest, NativeControllerDescriptorsUseContiguousButtonUsages) { const auto xbox_one = lvh::profiles::xbox_one(); const auto xbox_series = lvh::profiles::xbox_series(); const auto switch_pro = lvh::profiles::switch_pro(); constexpr std::array xbox_one_buttons { 0x05, 0x09, 0x19, 0x01, 0x29, 0x0A, 0x95, 0x0A, 0x75, 0x01, 0x81, 0x02, }; constexpr std::array xbox_series_buttons { 0x05, 0x09, 0x19, 0x01, 0x29, 0x0C, 0x95, 0x0C, 0x75, 0x01, 0x81, 0x02, }; constexpr std::array switch_primary_buttons { 0x05, 0x09, 0x19, 0x01, 0x29, 0x0A, 0x15, 0x00, 0x25, 0x01, 0x75, 0x01, 0x95, 0x0A, 0x55, 0x00, 0x65, 0x00, 0x81, 0x02, }; constexpr std::array switch_system_buttons { 0x05, 0x09, 0x19, 0x0B, 0x29, 0x0E, 0x15, 0x00, 0x25, 0x01, 0x75, 0x01, 0x95, 0x04, 0x81, 0x02, }; constexpr std::array switch_compatibility_buttons { 0x05, 0x09, 0x19, 0x0F, 0x29, 0x12, 0x15, 0x00, 0x25, 0x01, 0x75, 0x01, 0x95, 0x04, 0x81, 0x02, }; expect_descriptor_contains(xbox_one, xbox_one_buttons); expect_descriptor_contains(xbox_series, xbox_series_buttons); expect_descriptor_contains(switch_pro, switch_primary_buttons); expect_descriptor_contains(switch_pro, switch_system_buttons); expect_descriptor_contains(switch_pro, switch_compatibility_buttons); } TEST(ProfileTest, NativeControllerPackedButtonsUseExpectedBitSlots) { using enum lvh::GamepadButton; const auto xbox_one = lvh::profiles::xbox_one(); const auto xbox_series = lvh::profiles::xbox_series(); const auto switch_pro = lvh::profiles::switch_pro(); const std::vector xbox_button_cases { {a, 0U}, {b, 1U}, {x, 2U}, {y, 3U}, {left_shoulder, 4U}, {right_shoulder, 5U}, {back, 6U}, {start, 7U}, {left_stick, 8U}, {right_stick, 9U}, }; expect_packed_button_bits(xbox_one, xbox_button_cases, 12U); expect_packed_button_bits(xbox_series, xbox_button_cases, 12U); expect_packed_button_bits(xbox_series, {{misc1, 11U}}, 12U); for (const auto &profile : {xbox_one, xbox_series}) { lvh::GamepadState state; state.buttons.set(guide); const auto report = lvh::reports::pack_input_report(profile, state); ASSERT_GT(report.size(), 15U); EXPECT_EQ(read_u16_le(report, 12U), 0U); EXPECT_EQ(report[15], 1U); } const std::vector switch_button_cases { {a, 3U, 0x08U}, {b, 3U, 0x04U}, {x, 3U, 0x02U}, {y, 3U, 0x01U}, {right_shoulder, 3U, 0x40U}, {back, 4U, 0x01U}, {start, 4U, 0x02U}, {right_stick, 4U, 0x04U}, {left_stick, 4U, 0x08U}, {guide, 4U, 0x10U}, {misc1, 4U, 0x20U}, {dpad_down, 5U, 0x01U}, {dpad_up, 5U, 0x02U}, {dpad_right, 5U, 0x04U}, {dpad_left, 5U, 0x08U}, {left_shoulder, 5U, 0x40U}, }; for (const auto &[button, offset, mask] : switch_button_cases) { lvh::GamepadState state; state.buttons.set(button); const auto report = lvh::reports::pack_input_report(switch_pro, state); ASSERT_GT(report.size(), offset); EXPECT_EQ(report[offset], mask) << "logical button " << static_cast(std::to_underlying(button)); } lvh::GamepadState trigger_state; trigger_state.left_trigger = 1.0F; trigger_state.right_trigger = 1.0F; const auto trigger_report = lvh::reports::pack_input_report(switch_pro, trigger_state); ASSERT_GT(trigger_report.size(), 5U); EXPECT_EQ(trigger_report[3], 0x80U); EXPECT_EQ(trigger_report[5], 0x80U); } TEST(ProfileTest, SwitchProPacksNativeFullControllerState) { const auto profile = lvh::profiles::switch_pro(); lvh::GamepadState state; state.left_stick = {.x = 1.0F, .y = 0.0F}; state.right_stick = {.x = 0.0F, .y = -1.0F}; const auto report = lvh::reports::pack_input_report(profile, state); ASSERT_EQ(report.size(), 64U); EXPECT_EQ(report[0], 0x30U); EXPECT_EQ(report[2], 0x81U); // Nintendo packs each stick as two little-endian 12-bit values across three bytes. EXPECT_EQ(report[6], 0xFFU); EXPECT_EQ(report[7], 0x0FU); EXPECT_EQ(report[8], 0x80U); EXPECT_EQ(report[9], 0x00U); EXPECT_EQ(report[10], 0x08U); EXPECT_EQ(report[11], 0x00U); state.battery = lvh::GamepadBattery {.state = lvh::GamepadBatteryState::full, .percentage = 100}; EXPECT_EQ(lvh::reports::pack_input_report(profile, state)[2], 0x81U); state.battery = lvh::GamepadBattery {.state = lvh::GamepadBatteryState::charging, .percentage = 50}; EXPECT_EQ(lvh::reports::pack_input_report(profile, state)[2], 0x51U); } TEST(ProfileTest, RumbleProfilesExposeOutputReports) { const auto generic = lvh::profiles::generic_gamepad(); const auto xbox_360 = lvh::profiles::xbox_360(); const auto xbox_one = lvh::profiles::xbox_one(); const auto xbox_series = lvh::profiles::xbox_series(); EXPECT_TRUE(generic.capabilities.supports_rumble); EXPECT_EQ(generic.output_report_size, 9U); constexpr std::array pid_rumble_descriptor { 0x05, 0x0F, 0x09, 0x97, 0x15, 0x00, 0x25, 0x01, 0x75, 0x04, 0x95, 0x01, 0x91, 0x02, 0x15, 0x00, }; expect_descriptor_contains(generic, pid_rumble_descriptor); EXPECT_TRUE(xbox_360.capabilities.supports_rumble); EXPECT_EQ(xbox_360.output_report_size, 5U); ASSERT_GE(xbox_360.report_descriptor.size(), 3U); EXPECT_EQ(xbox_360.report_descriptor[xbox_360.report_descriptor.size() - 3U], 0x91); EXPECT_EQ(xbox_360.report_descriptor[xbox_360.report_descriptor.size() - 2U], 0x02); EXPECT_EQ(xbox_360.report_descriptor.back(), 0xC0); EXPECT_EQ(xbox_one.output_report_size, 8U); EXPECT_EQ(xbox_series.output_report_size, 8U); expect_descriptor_contains(xbox_one, pid_rumble_descriptor); expect_descriptor_contains(xbox_series, pid_rumble_descriptor); } TEST(ProfileTest, CanFindProfileByKind) { const auto profile = lvh::profiles::gamepad_profile(lvh::GamepadProfileKind::xbox_series); ASSERT_TRUE(profile.has_value()); EXPECT_EQ(profile->gamepad_kind, lvh::GamepadProfileKind::xbox_series); } TEST(ProfileTest, PointerProfilesArePresent) { const auto keyboard = lvh::profiles::keyboard(); const auto mouse = lvh::profiles::mouse(); const auto touchscreen = lvh::profiles::touchscreen(); const auto trackpad = lvh::profiles::trackpad(); const auto pen_tablet = lvh::profiles::pen_tablet(); EXPECT_EQ(keyboard.device_type, lvh::DeviceType::keyboard); EXPECT_FALSE(keyboard.name.empty()); EXPECT_NE(keyboard.vendor_id, 0); EXPECT_NE(keyboard.product_id, 0); EXPECT_EQ(mouse.device_type, lvh::DeviceType::mouse); EXPECT_FALSE(mouse.name.empty()); EXPECT_NE(mouse.vendor_id, 0); EXPECT_NE(mouse.product_id, 0); EXPECT_EQ(touchscreen.device_type, lvh::DeviceType::touchscreen); EXPECT_FALSE(touchscreen.name.empty()); EXPECT_NE(touchscreen.product_id, 0); EXPECT_EQ(trackpad.device_type, lvh::DeviceType::trackpad); EXPECT_FALSE(trackpad.name.empty()); EXPECT_NE(trackpad.product_id, 0); EXPECT_EQ(pen_tablet.device_type, lvh::DeviceType::pen_tablet); EXPECT_FALSE(pen_tablet.name.empty()); EXPECT_NE(pen_tablet.product_id, 0); }