/** * @file tests/unit/test_macos_broker_protocol.cpp * @brief macOS broker protocol and built-in profile capacity checks. */ #include "platform/macos/broker/io.hpp" #include "platform/macos/macos_xbox_transport.hpp" #include #include #include #include #include #include #include #include #include TEST(MacosBrokerProtocolTest, BuiltInProfilesFitBrokerTransport) { auto profiles = lvh::profiles::built_in_gamepad_profiles(); profiles.push_back(lvh::profiles::dualshock4_usb()); profiles.push_back(lvh::profiles::dualshock4_bluetooth()); profiles.push_back(lvh::profiles::dualsense_usb()); profiles.push_back(lvh::profiles::dualsense_bluetooth()); ASSERT_GE(profiles.size(), 11U); for (const auto &profile : profiles) { SCOPED_TRACE(profile.name); EXPECT_FALSE(profile.report_descriptor.empty()); EXPECT_LE(profile.report_descriptor.size(), lvh::detail::macos_broker::max_descriptor_size); EXPECT_GT(profile.input_report_size, 0U); EXPECT_LE(profile.input_report_size, lvh::detail::macos_broker::max_report_size); EXPECT_LE(profile.output_report_size, lvh::detail::macos_broker::max_report_size); const auto transport = lvh::detail::macos::xbox_transport_profile(profile); EXPECT_LE(transport.report_descriptor.size(), lvh::detail::macos_broker::max_descriptor_size); EXPECT_LE(transport.input_report_size, lvh::detail::macos_broker::max_report_size); EXPECT_LE(transport.output_report_size, lvh::detail::macos_broker::max_report_size); } } TEST(MacosBrokerProtocolTest, TransfersVersionedMessagesWithoutTruncation) { std::array sockets {}; ASSERT_EQ(::socketpair(AF_UNIX, SOCK_STREAM, 0, sockets.data()), 0); lvh::detail::macos_broker::Message sent; sent.type = lvh::detail::macos_broker::MessageType::create; sent.descriptor_size = 3; sent.data[0] = 0x05; sent.data[1] = 0x01; sent.data[2] = 0x09; bool sent_ok = false; std::jthread sender {[&sent_ok, &sockets, &sent] { sent_ok = lvh::detail::macos_broker::send_message(sockets[0], sent); }}; lvh::detail::macos_broker::Message received; EXPECT_TRUE(lvh::detail::macos_broker::receive_message(sockets[1], received)); sender.join(); EXPECT_TRUE(sent_ok); EXPECT_EQ(received.type, sent.type); EXPECT_EQ(received.descriptor_size, 3U); EXPECT_EQ(received.data[2], 0x09); ::close(sockets[0]); ::close(sockets[1]); } TEST(MacosBrokerProtocolTest, XboxTransportUsesSteamMacOSIdentityAndButtonLayout) { using enum lvh::GamepadButton; const auto requested = lvh::profiles::xbox_360(); const auto transport = lvh::detail::macos::xbox_transport_profile(requested); EXPECT_EQ(transport.vendor_id, 0x045E); EXPECT_EQ(transport.product_id, 0x028E); EXPECT_EQ(transport.version, 0x0114); EXPECT_EQ(transport.input_report_size, 9U); EXPECT_EQ(transport.report_id, 1); EXPECT_NE(transport.report_descriptor, requested.report_descriptor); const std::array buttons {a, b, x, y, left_shoulder, right_shoulder, left_stick, right_stick, start, back, guide, dpad_up, dpad_down, dpad_left, dpad_right, misc1}; for (std::size_t bit = 0; bit < buttons.size(); ++bit) { lvh::GamepadState state; state.buttons.set(buttons[bit]); const auto report = lvh::detail::macos::xbox_transport_input_report(state); ASSERT_EQ(report.size(), transport.input_report_size); const auto flags = std::bitset<16> {report[1] + 256U * report[2]}; EXPECT_TRUE(flags.test(bit)); EXPECT_EQ(flags.count(), 1U); } } TEST(MacosBrokerProtocolTest, XboxOneAndSeriesUseDistinctIdentitiesAndGipInput) { for (const auto &requested : {lvh::profiles::xbox_one(), lvh::profiles::xbox_series()}) { SCOPED_TRACE(requested.name); const auto transport = lvh::detail::macos::xbox_transport_profile(requested); EXPECT_EQ(transport.vendor_id, 0x045E); EXPECT_EQ(transport.product_id, requested.gamepad_kind == lvh::GamepadProfileKind::xbox_one ? 0x0B20 : 0x0B13); EXPECT_EQ(transport.version, 0x0513); EXPECT_EQ(transport.bus_type, lvh::BusType::bluetooth); EXPECT_EQ(transport.report_id, lvh::detail::macos::xbox_gip_input_command); EXPECT_EQ(transport.input_report_size, 25U); EXPECT_EQ(transport.output_report_size, 9U); EXPECT_NE(transport.report_descriptor, requested.report_descriptor); lvh::GamepadState state; state.buttons.set(lvh::GamepadButton::a); state.buttons.set(lvh::GamepadButton::start); state.buttons.set(lvh::GamepadButton::dpad_left); state.buttons.set(lvh::GamepadButton::misc1); state.left_stick = {1.0F, -1.0F}; const auto original = lvh::reports::pack_input_report(requested, state); const auto report = lvh::detail::macos::xbox_gip_transport_input_report( state, original, requested.gamepad_kind == lvh::GamepadProfileKind::xbox_series ); ASSERT_EQ(report.size(), transport.input_report_size); EXPECT_EQ(report[0], 0x20U); EXPECT_EQ(report[3], 16U); EXPECT_EQ(report[4], 0x14U); // Start and A. EXPECT_EQ(report[5], 0x04U); // D-pad left. EXPECT_EQ(report[10], 0xFFU); // Left X fully right. EXPECT_EQ(report[11], 0x7FU); EXPECT_EQ(report[12], 0x00U); // Left Y is inverted for Steam's GIP decoder. EXPECT_EQ(report[13], 0x80U); EXPECT_EQ(report[18], requested.gamepad_kind == lvh::GamepadProfileKind::xbox_series ? 1U : 0U); EXPECT_EQ(report[20], 0x07U); // Separate Guide packet. EXPECT_EQ(report[21], 0x20U); EXPECT_EQ(report[23], 1U); EXPECT_EQ(report[24], 0U); } } TEST(MacosBrokerProtocolTest, XboxGipTransportMapsEveryButtonAndGuide) { using enum lvh::GamepadButton; struct ButtonCase { lvh::GamepadButton button; std::size_t index; std::uint8_t mask; }; const std::array buttons { ButtonCase {start, 4, 0x04}, ButtonCase {back, 4, 0x08}, ButtonCase {a, 4, 0x10}, ButtonCase {b, 4, 0x20}, ButtonCase {x, 4, 0x40}, ButtonCase {y, 4, 0x80}, ButtonCase {dpad_up, 5, 0x01}, ButtonCase {dpad_down, 5, 0x02}, ButtonCase {dpad_left, 5, 0x04}, ButtonCase {dpad_right, 5, 0x08}, ButtonCase {left_shoulder, 5, 0x10}, ButtonCase {right_shoulder, 5, 0x20}, ButtonCase {left_stick, 5, 0x40}, ButtonCase {right_stick, 5, 0x80}, ButtonCase {misc1, 18, 0x01}, ButtonCase {guide, 24, 0x01}, }; for (const auto &item : buttons) { lvh::GamepadState state; state.buttons.set(item.button); const auto packed = lvh::reports::pack_input_report(lvh::profiles::xbox_series(), state); const auto report = lvh::detail::macos::xbox_gip_transport_input_report(state, packed, true); ASSERT_EQ(report.size(), 25U); EXPECT_EQ(report[item.index], item.mask); } } TEST(MacosBrokerProtocolTest, XboxGipRumbleUsesFourMotorOutputDecoder) { const auto profile = lvh::detail::macos::xbox_transport_profile(lvh::profiles::xbox_series()); const std::vector wired { 0x09, 0x00, 0x00, 0x09, 0x00, 0x0F, 25, 50, 75, 100, 0xFF, 0x00, 0xEB, }; const auto outputs = lvh::detail::macos::xbox_transport_output_reports(profile, wired); ASSERT_EQ(outputs.size(), 2U); EXPECT_EQ(outputs[0].kind, lvh::GamepadOutputKind::rumble); EXPECT_GT(outputs[0].low_frequency_rumble, 0U); EXPECT_GT(outputs[0].high_frequency_rumble, outputs[0].low_frequency_rumble); EXPECT_EQ(outputs[1].kind, lvh::GamepadOutputKind::trigger_rumble); EXPECT_GT(outputs[1].left_trigger_rumble, 0U); EXPECT_GT(outputs[1].right_trigger_rumble, outputs[1].left_trigger_rumble); EXPECT_EQ(outputs[0].raw_report, wired); EXPECT_EQ(outputs[1].raw_report, wired); const std::vector bluetooth {0x03, 0x0F, 25, 50, 75, 100, 0xFF, 0x00, 0xEB}; const auto wireless_outputs = lvh::detail::macos::xbox_transport_output_reports(profile, bluetooth); ASSERT_EQ(wireless_outputs.size(), 2U); EXPECT_EQ(wireless_outputs[0].low_frequency_rumble, outputs[0].low_frequency_rumble); EXPECT_EQ(wireless_outputs[1].left_trigger_rumble, outputs[1].left_trigger_rumble); } TEST(MacosBrokerProtocolTest, Xbox360TransportChangesOnlyButtonsAndVerticalAxes) { lvh::GamepadState state; state.left_stick = {1.0F, -1.0F}; state.right_stick = {-1.0F, 1.0F}; state.left_trigger = 0.5F; state.right_trigger = 1.0F; state.buttons.set(lvh::GamepadButton::back); const auto shared = lvh::reports::pack_input_report(lvh::profiles::xbox_360(), state); const auto transport = lvh::detail::macos::xbox_transport_input_report(state); ASSERT_EQ(shared.size(), 9U); ASSERT_EQ(transport.size(), 9U); EXPECT_EQ(shared[1], 0x40U); EXPECT_EQ(transport[2], 0x02U); for (std::size_t index = 3; index < shared.size(); ++index) { if (index == 4U || index == 7U) { EXPECT_EQ(transport[index], 0xFFU - shared[index]); } else { EXPECT_EQ(transport[index], shared[index]); } } } TEST(MacosBrokerProtocolTest, XboxVerticalSticksUsePositiveUpOnMacOS) { lvh::GamepadState state; state.left_stick.y = 1.0F; state.right_stick.y = -1.0F; const auto xbox_360 = lvh::detail::macos::xbox_transport_input_report(state); ASSERT_EQ(xbox_360.size(), 9U); EXPECT_EQ(xbox_360[4], 0xFFU); EXPECT_EQ(xbox_360[7], 0x00U); for (const auto &profile : {lvh::profiles::xbox_one(), lvh::profiles::xbox_series()}) { const auto packed = lvh::reports::pack_input_report(profile, state); const auto report = lvh::detail::macos::xbox_gip_transport_input_report( state, packed, profile.gamepad_kind == lvh::GamepadProfileKind::xbox_series ); ASSERT_EQ(report.size(), 25U); EXPECT_EQ(report[12], 0xFFU); EXPECT_EQ(report[13], 0x7FU); EXPECT_EQ(report[16], 0x00U); EXPECT_EQ(report[17], 0x80U); } } TEST(MacosBrokerProtocolTest, XboxTransportLeavesCustomHidProfilesUntouched) { auto custom = lvh::profiles::xbox_360(); custom.vendor_id = 0x1209; const auto transport = lvh::detail::macos::xbox_transport_profile(custom); EXPECT_FALSE(lvh::detail::macos::uses_xbox_transport(custom)); EXPECT_EQ(transport.vendor_id, custom.vendor_id); EXPECT_EQ(transport.product_id, custom.product_id); EXPECT_EQ(transport.report_descriptor, custom.report_descriptor); }