diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index 2ac6c6c..1473894 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -181,7 +181,8 @@ jobs: echo "::warning::${kernel_modules_package} is unavailable; relying on the runner image kernel modules." fi sudo tee /etc/udev/rules.d/99-libvirtualhid-ci.rules >/dev/null <<'EOF' - SUBSYSTEM=="hidraw", KERNEL=="hidraw*", ENV{HID_PHYS}=="libvirtualhid/uhid/*", MODE="0666", TAG+="uaccess" + SUBSYSTEM=="hidraw", KERNEL=="hidraw*", ATTRS{phys}=="libvirtualhid/uhid/*", MODE="0666", TAG+="uaccess" + SUBSYSTEM=="input", KERNEL=="event*", ATTRS{phys}=="libvirtualhid/uhid/*", MODE="0666", TAG+="uaccess" SUBSYSTEM=="hidraw", KERNEL=="hidraw*", ATTRS{name}=="(libvirtualhid)*", MODE="0666", TAG+="uaccess" SUBSYSTEM=="input", KERNEL=="event*", ATTRS{name}=="(libvirtualhid)*", MODE="0666", TAG+="uaccess" SUBSYSTEM=="input", KERNEL=="event*", ATTRS{name}=="libvirtualhid*", MODE="0666", TAG+="uaccess" diff --git a/docs/platform-support.md b/docs/platform-support.md index 3f4c3f2..d42cbb2 100644 --- a/docs/platform-support.md +++ b/docs/platform-support.md @@ -138,6 +138,20 @@ and control channels. Numbered control-channel output is normalized before parsing, whether the kernel includes the report number in the payload or provides it separately on the UHID event. +The backend opens `/dev/uhid` in nonblocking mode, matching the original +asynchronous gamepad registration path. Its event reader is active before +device registration begins, and creation does not report success until the +kernel returns `UHID_START`. This keeps control-channel initialization +available throughout registration and prevents streaming hosts from publishing +a controller before its kernel HID device has started. + +On Linux, DualShock 4 and DualSense emit Sony's native `Wireless Controller` +product name for Steam HID discovery. The requested USB or Bluetooth bus, +descriptor, and report framing remain unchanged; in particular, the default +DualShock 4 profile stays on its USB report contract. This transport-only name +is confined to the Linux backend; public profile names, Windows names, and VHF +behavior are unchanged. + Switch Pro keeps its Nintendo identity on the Linux uinput path. This follows the evdev layout used by Linux-native virtual-controller implementations and allows standard `FF_RUMBLE` effects without emulating the physical controller's @@ -170,9 +184,19 @@ KERNEL=="uinput", SUBSYSTEM=="misc", OPTIONS+="static_node=uinput", GROUP="input KERNEL=="uhid", GROUP="input", MODE="0660", TAG+="uaccess" ``` -Consuming applications may also install name-matched rules for stable virtual -device names when generated `hidraw` or `input` nodes must be accessible to the -session user: +UHID gamepads use a stable `libvirtualhid/uhid/*` physical path even when the +library is compiled directly into a consuming application. Match that path for +generated `hidraw` and input event nodes because native profiles such as +DualShock 4 and DualSense intentionally do not retain the application's product +name: + +```udev +KERNEL=="hidraw*", ATTRS{phys}=="libvirtualhid/uhid/*", GROUP="input", MODE="0660", TAG+="uaccess" +SUBSYSTEM=="input", KERNEL=="event*", ATTRS{phys}=="libvirtualhid/uhid/*", GROUP="input", MODE="0660", TAG+="uaccess" +``` + +Consuming applications may additionally install name-matched rules for stable +virtual device names, including uinput-backed gamepads: ```udev KERNEL=="hidraw*", ATTRS{name}=="Your App Controller*", GROUP="input", MODE="0660", TAG+="uaccess" diff --git a/src/platform/linux/uhid_backend.cpp b/src/platform/linux/uhid_backend.cpp index 4756490..ab85885 100644 --- a/src/platform/linux/uhid_backend.cpp +++ b/src/platform/linux/uhid_backend.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -99,6 +100,7 @@ namespace lvh::detail { #if defined(__linux__) namespace ps = playstation_feature_reports; constexpr auto playstation_periodic_report_ms = 10; + constexpr auto uhid_start_timeout = std::chrono::seconds {5}; #endif int system_access(const char *path, int mode) { @@ -303,6 +305,16 @@ namespace lvh::detail { } return to_uhid_bus(profile.bus_type); } + + std::string_view uhid_gamepad_name(const DeviceProfile &profile) { + // Steam's PlayStation HID path expects Sony's native product name. Keep + // consumer branding out of the Linux transport identity while preserving + // the requested descriptor, bus, and report framing. + if (is_playstation_profile(profile.gamepad_kind)) { + return "Wireless Controller"; + } + return profile.name; + } #endif std::uint16_t to_uinput_bus(BusType bus_type) { @@ -2787,7 +2799,8 @@ namespace lvh::detail { } physical_id_ = std::format("libvirtualhid/uhid/{}", id); - copy_string(request.name, options.profile.name); + device_name_ = uhid_gamepad_name(options.profile); + copy_string(request.name, device_name_); copy_string(request.phys, physical_id_); copy_string(request.uniq, unique_id_); request.rd_size = static_cast(options.profile.report_descriptor.size()); @@ -2797,20 +2810,31 @@ namespace lvh::detail { request.version = options.profile.version; std::memcpy(request.rd_data, options.profile.report_descriptor.data(), options.profile.report_descriptor.size()); profile_ = options.profile; - device_name_ = options.profile.name; { std::lock_guard lock {report_mutex_}; last_report_ = reports::pack_input_report(profile_, {}); } - if (const auto status = write_event(event); !status.ok()) { - return status; + { + std::lock_guard lock {lifecycle_mutex_}; + started_ = false; + reader_exited_ = false; } - running_ = true; reader_ = std::jthread {[this](std::stop_token stop_token) { read_loop(stop_token); }}; + + if (const auto status = write_event(event); !status.ok()) { + stop_reader(); + return status; + } + + if (const auto status = wait_for_start(); !status.ok()) { + stop_reader(); + return status; + } + if (is_playstation_profile(profile_.gamepad_kind)) { periodic_reporter_ = std::jthread {[this](std::stop_token stop_token) { periodic_report_loop(stop_token); @@ -2914,47 +2938,65 @@ namespace lvh::detail { return OperationStatus::success(); } - void read_loop(std::stop_token stop_token) { - while (!stop_token.stop_requested() && running_) { - pollfd descriptor {}; - descriptor.fd = fd_; - descriptor.events = POLLIN; + enum class ReadEventResult { + event, + retry, + stop, + }; - const auto result = system_poll(&descriptor, 1, poll_timeout_ms); - if (result < 0) { - if (errno == EINTR) { - continue; - } - return; - } - if (result == 0) { - continue; - } - if ((descriptor.revents & (POLLERR | POLLHUP | POLLNVAL)) != 0) { - return; - } - if ((descriptor.revents & POLLIN) == 0) { - continue; - } + ReadEventResult read_event(uhid_event &event) const { + pollfd descriptor {}; + descriptor.fd = fd_; + descriptor.events = POLLIN; + const auto result = system_poll(&descriptor, 1, poll_timeout_ms); + if (result < 0) { + return errno == EINTR ? ReadEventResult::retry : ReadEventResult::stop; + } + if ((descriptor.revents & (POLLERR | POLLHUP | POLLNVAL)) != 0) { + return ReadEventResult::stop; + } + if (result == 0 || (descriptor.revents & POLLIN) == 0) { + return ReadEventResult::retry; + } + + const auto result_read = system_read(fd_, std::as_writable_bytes(std::span {&event, 1U})); + if (result_read < 0) { + return errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR ? + ReadEventResult::retry : + ReadEventResult::stop; + } + return result_read == 0 ? ReadEventResult::stop : ReadEventResult::event; + } + + void read_loop(std::stop_token stop_token) { + while (!stop_token.stop_requested() && running_) { uhid_event event {}; - const auto read_result = system_read(fd_, std::as_writable_bytes(std::span {&event, 1U})); - if (read_result < 0) { - if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR) { - continue; - } - return; + const auto result = read_event(event); + if (result == ReadEventResult::stop) { + break; } - if (read_result == 0) { - return; + if (result == ReadEventResult::event) { + handle_event(event); } + } - handle_event(event); + { + std::lock_guard lock {lifecycle_mutex_}; + reader_exited_ = true; } + lifecycle_condition_.notify_all(); } void handle_event(const uhid_event &event) { switch (event.type) { + case UHID_START: + { + std::lock_guard lock {lifecycle_mutex_}; + started_ = true; + } + lifecycle_condition_.notify_all(); + break; case UHID_OUTPUT: dispatch_output_report(event.u.output.data, event.u.output.size); break; @@ -2970,6 +3012,25 @@ namespace lvh::detail { } } + void stop_reader() { + running_ = false; + if (reader_.joinable()) { + reader_.request_stop(); + reader_.join(); + } + } + + OperationStatus wait_for_start() { + if (std::unique_lock lock {lifecycle_mutex_}; !lifecycle_condition_.wait_for(lock, uhid_start_timeout, [this]() { + return started_ || reader_exited_; + })) { + return OperationStatus::failure(ErrorCode::backend_failure, "timed out waiting for UHID_START"); + } else if (!started_) { + return OperationStatus::failure(ErrorCode::backend_failure, "UHID reader stopped before UHID_START"); + } + return OperationStatus::success(); + } + void periodic_report_loop(std::stop_token stop_token) { while (!stop_token.stop_requested() && running_) { std::this_thread::sleep_for(std::chrono::milliseconds {playstation_periodic_report_ms}); @@ -3113,6 +3174,10 @@ namespace lvh::detail { std::atomic_bool running_ = false; std::jthread reader_; std::jthread periodic_reporter_; + std::mutex lifecycle_mutex_; + std::condition_variable lifecycle_condition_; + bool started_ = false; + bool reader_exited_ = false; std::mutex write_mutex_; std::mutex report_mutex_; std::mutex callback_mutex_; @@ -3185,7 +3250,7 @@ namespace lvh::detail { } #if defined(__linux__) - const auto fd = system_open(uhid_path, O_RDWR | O_CLOEXEC); + const auto fd = system_open(uhid_path, O_RDWR | O_CLOEXEC | O_NONBLOCK); if (fd < 0) { return {system_error_status(ErrorCode::backend_unavailable, "failed to open /dev/uhid", errno), nullptr}; } diff --git a/tests/fixtures/include/fixtures/linux_backend_test_hooks.hpp b/tests/fixtures/include/fixtures/linux_backend_test_hooks.hpp index 5051ae1..0e00dec 100644 --- a/tests/fixtures/include/fixtures/linux_backend_test_hooks.hpp +++ b/tests/fixtures/include/fixtures/linux_backend_test_hooks.hpp @@ -102,6 +102,41 @@ namespace lvh::detail::test { std::uint64_t remaining = 0; }; + /** + * @brief UHID device-creation observations captured by a socketpair peer. + */ + struct LinuxUhidCreationObservation { + /** + * @brief Whether the peer observed a create event. + */ + bool saw_create = false; + + /** + * @brief Whether create remained pending until the peer sent UHID_START. + */ + bool waited_for_start = false; + + /** + * @brief Product name carried by the observed create event. + */ + std::string name; + }; + + /** + * @brief UHID output callback observations captured during a round trip. + */ + struct LinuxUhidOutputObservation { + /** + * @brief Number of output callbacks received. + */ + std::size_t callback_count = 0; + + /** + * @brief Last output callback payload. + */ + GamepadOutput last; + }; + /** * @brief Result from a socketpair-backed UHID lifecycle test. */ @@ -122,9 +157,9 @@ namespace lvh::detail::test { OperationStatus close_status; /** - * @brief Whether the peer observed a create event. + * @brief Device-creation observations. */ - bool saw_create = false; + LinuxUhidCreationObservation creation; /** * @brief Whether the peer observed an input report event. @@ -186,6 +221,11 @@ namespace lvh::detail::test { */ bool saw_dualshock4_bluetooth_input = false; + /** + * @brief Whether the peer observed a USB-framed DualShock 4 input report. + */ + bool saw_dualshock4_usb_input = false; + /** * @brief Whether the peer observed a set-report reply. */ @@ -197,14 +237,9 @@ namespace lvh::detail::test { bool saw_destroy = false; /** - * @brief Number of output callbacks received. - */ - std::size_t output_callback_count = 0; - - /** - * @brief Last output callback payload. + * @brief Output callback observations. */ - GamepadOutput last_output; + LinuxUhidOutputObservation output; }; /** @@ -830,6 +865,13 @@ namespace lvh::detail::test { */ OperationStatus linux_backend_gamepad_fake_open_failure(); + /** + * @brief Capture the flags used to open UHID for a descriptor-driven gamepad. + * + * @return Flags passed to `open()` for `/dev/uhid`. + */ + int linux_backend_gamepad_open_flags(); + /** * @brief Try creating a Linux backend gamepad while fake UHID creation fails. * diff --git a/tests/fixtures/linux_backend_test_hooks.cpp b/tests/fixtures/linux_backend_test_hooks.cpp index f3aec9c..df31b0e 100644 --- a/tests/fixtures/linux_backend_test_hooks.cpp +++ b/tests/fixtures/linux_backend_test_hooks.cpp @@ -120,6 +120,7 @@ namespace lvh::detail::test { int access_result = 0; bool override_open = false; int open_result = 100000; + int last_open_flags = 0; bool override_write = false; std::atomic_int write_call_count = 0; int fail_write_call = -1; @@ -138,6 +139,7 @@ namespace lvh::detail::test { std::vector read_results; std::vector read_errors; uhid_event read_event {}; + std::vector read_events; std::vector read_input_events; ff_effect uploaded_ff_effect {}; std::vector uploaded_ff_effects; @@ -206,6 +208,9 @@ int lvh_linux_test_access(const char *path, int mode) { } int lvh_linux_test_open(const char *path, int flags) { + if (lvh::detail::test::active_test_syscalls() != nullptr) { + lvh::detail::test::active_test_syscalls()->last_open_flags = flags; + } if (lvh::detail::test::active_test_syscalls() != nullptr && lvh::detail::test::active_test_syscalls()->override_open) { if (lvh::detail::test::active_test_syscalls()->open_result < 0) { errno = ENOENT; @@ -305,7 +310,11 @@ std::ptrdiff_t lvh_linux_test_read(int fd, std::byte *buffer, std::size_t size) } const auto bytes = std::min(static_cast(result), std::min(size, sizeof(uhid_event))); - std::memcpy(buffer, &lvh::detail::test::active_test_syscalls()->read_event, bytes); + const auto &read_events = lvh::detail::test::active_test_syscalls()->read_events; + const auto &event = call_index < read_events.size() ? + read_events[call_index] : + lvh::detail::test::active_test_syscalls()->read_event; + std::memcpy(buffer, &event, bytes); return static_cast(bytes); } @@ -679,6 +688,60 @@ namespace lvh::detail::test { return false; } + OperationStatus create_started_uhid_gamepad( + UhidGamepad &gamepad, + DeviceId id, + const CreateGamepadOptions &options, + int peer_fd, + uhid_event &create_event, + bool &saw_create, + bool &waited_for_start + ) { + auto create_status = OperationStatus::failure(ErrorCode::backend_failure, "UHID create thread did not run"); + std::atomic_bool create_returned = false; + std::jthread create_thread {[&]() { + create_status = gamepad.create(id, options); + create_returned = true; + }}; + + saw_create = read_uhid_event_type(peer_fd, UHID_CREATE2, create_event); + std::this_thread::sleep_for(std::chrono::milliseconds {20}); + waited_for_start = !create_returned; + + uhid_event start_event {}; + start_event.type = UHID_START; + static_cast(write_uhid_event(peer_fd, start_event)); + create_thread.join(); + return create_status; + } + + uhid_event create_started_profile_uhid_gamepad( + UhidGamepad &gamepad, + DeviceId id, + const CreateGamepadOptions &options, + int peer_fd, + std::uint16_t expected_bus, + LinuxUhidRoundTripResult &result + ) { + uhid_event event {}; + result.create_status = create_started_uhid_gamepad( + gamepad, + id, + options, + peer_fd, + event, + result.creation.saw_create, + result.creation.waited_for_start + ); + if (result.creation.saw_create) { + result.creation.saw_create = event.u.create2.vendor == options.profile.vendor_id && + event.u.create2.product == options.profile.product_id && + event.u.create2.bus == expected_bus; + result.creation.name = reinterpret_cast(event.u.create2.name); + } + return event; + } + std::uint32_t read_u32_le(const std::uint8_t *buffer) { return static_cast(buffer[0]) | (static_cast(buffer[1]) << 8U) | @@ -708,6 +771,16 @@ namespace lvh::detail::test { OperationStatus run_fake_uhid_read_loop(LinuxTestSyscalls &syscalls, int expected_poll_calls) { syscalls.override_write = true; + syscalls.override_poll = true; + syscalls.override_read = true; + syscalls.poll_results.insert(syscalls.poll_results.begin(), 1); + syscalls.poll_revents.insert(syscalls.poll_revents.begin(), POLLIN); + syscalls.poll_errors.insert(syscalls.poll_errors.begin(), 0); + syscalls.read_results.insert(syscalls.read_results.begin(), static_cast(sizeof(uhid_event))); + syscalls.read_errors.insert(syscalls.read_errors.begin(), 0); + uhid_event start_event {}; + start_event.type = UHID_START; + syscalls.read_events.insert(syscalls.read_events.begin(), start_event); ScopedLinuxTestSyscalls scoped_syscalls {syscalls}; @@ -724,7 +797,7 @@ namespace lvh::detail::test { return status; } - const auto saw_expected_polls = wait_for_poll_calls(syscalls, expected_poll_calls); + const auto saw_expected_polls = wait_for_poll_calls(syscalls, expected_poll_calls + 1); const auto close_status = gamepad.close(); if (!saw_expected_polls) { return OperationStatus::failure(ErrorCode::backend_failure, "fake UHID read loop did not consume the scripted poll calls"); @@ -1410,17 +1483,11 @@ namespace lvh::detail::test { options.metadata.stable_id = "linux-uhid-roundtrip"; UhidGamepad gamepad {descriptors[0]}; - result.create_status = gamepad.create(7, options); - - uhid_event event {}; - if (read_uhid_event(descriptors[1], event)) { - result.saw_create = - event.type == UHID_CREATE2 && event.u.create2.vendor == profile.vendor_id && event.u.create2.product == profile.product_id; - } + auto event = create_started_profile_uhid_gamepad(gamepad, 7, options, descriptors[1], BUS_USB, result); gamepad.set_output_callback([&result](const GamepadOutput &output) { - ++result.output_callback_count; - result.last_output = output; + ++result.output.callback_count; + result.output.last = output; }); event = {}; @@ -1490,21 +1557,16 @@ namespace lvh::detail::test { CreateGamepadOptions options; options.profile = profiles::dualsense_usb(); + options.profile.name = "Sunshine (libvirtualhid) PS5 Controller"; options.metadata.stable_id = "02:03:04:05:06:07"; UhidGamepad gamepad {descriptors[0]}; - result.create_status = gamepad.create(8, options); - - uhid_event event {}; - if (read_uhid_event_type(descriptors[1], UHID_CREATE2, event)) { - result.saw_create = event.u.create2.vendor == options.profile.vendor_id && - event.u.create2.product == options.profile.product_id; - } + auto event = create_started_profile_uhid_gamepad(gamepad, 8, options, descriptors[1], BUS_USB, result); gamepad.set_output_callback([&result](const GamepadOutput &output) { if (output.kind == GamepadOutputKind::rumble) { - ++result.output_callback_count; - result.last_output = output; + ++result.output.callback_count; + result.output.last = output; } }); @@ -1589,17 +1651,11 @@ namespace lvh::detail::test { CreateGamepadOptions options; options.profile = profiles::dualsense_bluetooth(); + options.profile.name = "Sunshine (libvirtualhid) PS5 Controller"; options.metadata.stable_id = "02:03:04:05:06:07"; UhidGamepad gamepad {descriptors[0]}; - result.create_status = gamepad.create(9, options); - - uhid_event event {}; - if (read_uhid_event_type(descriptors[1], UHID_CREATE2, event)) { - result.saw_create = event.u.create2.vendor == options.profile.vendor_id && - event.u.create2.product == options.profile.product_id && - event.u.create2.bus == BUS_BLUETOOTH; - } + auto event = create_started_profile_uhid_gamepad(gamepad, 9, options, descriptors[1], BUS_BLUETOOTH, result); if (read_uhid_event_type(descriptors[1], UHID_INPUT2, event)) { const auto report_size = static_cast(event.u.input2.size); @@ -1651,21 +1707,23 @@ namespace lvh::detail::test { CreateGamepadOptions options; options.profile = profiles::dualshock4_usb(); + options.profile.name = "Sunshine (libvirtualhid) PS4 Controller"; options.metadata.stable_id = "02:03:04:05:06:07"; UhidGamepad gamepad {descriptors[0]}; - result.create_status = gamepad.create(10, options); + auto event = create_started_profile_uhid_gamepad(gamepad, 10, options, descriptors[1], BUS_USB, result); + result.creation.saw_create = result.creation.saw_create && + event.u.create2.rd_size == options.profile.report_descriptor.size(); - uhid_event event {}; - if (read_uhid_event_type(descriptors[1], UHID_CREATE2, event)) { - result.saw_create = event.u.create2.vendor == options.profile.vendor_id && - event.u.create2.product == options.profile.product_id; + if (read_uhid_event_type(descriptors[1], UHID_INPUT2, event)) { + result.saw_dualshock4_usb_input = + event.u.input2.size == options.profile.input_report_size && event.u.input2.data[0] == options.profile.report_id; } gamepad.set_output_callback([&result](const GamepadOutput &output) { if (output.kind == GamepadOutputKind::rumble) { - ++result.output_callback_count; - result.last_output = output; + ++result.output.callback_count; + result.output.last = output; } }); @@ -1733,17 +1791,11 @@ namespace lvh::detail::test { CreateGamepadOptions options; options.profile = profiles::dualshock4_bluetooth(); + options.profile.name = "Sunshine (libvirtualhid) PS4 Controller"; options.metadata.stable_id = "02:03:04:05:06:07"; UhidGamepad gamepad {descriptors[0]}; - result.create_status = gamepad.create(11, options); - - uhid_event event {}; - if (read_uhid_event_type(descriptors[1], UHID_CREATE2, event)) { - result.saw_create = event.u.create2.vendor == options.profile.vendor_id && - event.u.create2.product == options.profile.product_id && - event.u.create2.bus == BUS_BLUETOOTH; - } + auto event = create_started_profile_uhid_gamepad(gamepad, 11, options, descriptors[1], BUS_BLUETOOTH, result); if (read_uhid_event_type(descriptors[1], UHID_INPUT2, event)) { const auto report_size = static_cast(event.u.input2.size); @@ -1883,6 +1935,21 @@ namespace lvh::detail::test { return backend.create_gamepad(1, options).status; } + int linux_backend_gamepad_open_flags() { + LinuxTestSyscalls syscalls; + syscalls.override_access = true; + syscalls.override_open = true; + syscalls.open_result = -1; + ScopedLinuxTestSyscalls scoped_syscalls {syscalls}; + + LinuxUhidBackend backend; + + CreateGamepadOptions options; + options.profile = profiles::dualshock4_usb(); + static_cast(backend.create_gamepad(1, options)); + return syscalls.last_open_flags; + } + OperationStatus linux_backend_gamepad_fake_create_failure() { LinuxTestSyscalls syscalls; enable_fake_device_syscalls(syscalls); diff --git a/tests/unit/test_linux_backend.cpp b/tests/unit/test_linux_backend.cpp index e74bef0..0c44537 100644 --- a/tests/unit/test_linux_backend.cpp +++ b/tests/unit/test_linux_backend.cpp @@ -15,6 +15,7 @@ #include // platform includes +#include #include #if defined(LIBVIRTUALHID_HAVE_XTEST) #include @@ -763,37 +764,43 @@ TEST_F(LinuxBackendTest, SocketpairBackedUhidGamepadRoundTripsEvents) { EXPECT_TRUE(result.create_status.ok()) << result.create_status.message(); EXPECT_TRUE(result.submit_status.ok()) << result.submit_status.message(); EXPECT_TRUE(result.close_status.ok()) << result.close_status.message(); - EXPECT_TRUE(result.saw_create); + EXPECT_TRUE(result.creation.saw_create); + EXPECT_TRUE(result.creation.waited_for_start); + EXPECT_EQ(result.creation.name, lvh::profiles::xbox_360().name); EXPECT_TRUE(result.saw_input); EXPECT_TRUE(result.saw_get_report_reply); EXPECT_TRUE(result.saw_set_report_reply); EXPECT_TRUE(result.saw_destroy); - EXPECT_GE(result.output_callback_count, 2U); - EXPECT_EQ(result.last_output.kind, lvh::GamepadOutputKind::rumble); - EXPECT_EQ(result.last_output.low_frequency_rumble, 0x5678); - EXPECT_EQ(result.last_output.high_frequency_rumble, 0x1234); + EXPECT_GE(result.output.callback_count, 2U); + EXPECT_EQ(result.output.last.kind, lvh::GamepadOutputKind::rumble); + EXPECT_EQ(result.output.last.low_frequency_rumble, 0x5678); + EXPECT_EQ(result.output.last.high_frequency_rumble, 0x1234); } TEST_F(LinuxBackendTest, SocketpairBackedDualSenseRepliesToFeatureReports) { const auto result = lvh::detail::test::linux_dualsense_uhid_socketpair_reports(); EXPECT_TRUE(result.create_status.ok()) << result.create_status.message(); EXPECT_TRUE(result.close_status.ok()) << result.close_status.message(); - EXPECT_TRUE(result.saw_create); + EXPECT_TRUE(result.creation.saw_create); + EXPECT_TRUE(result.creation.waited_for_start); + EXPECT_EQ(result.creation.name, "Wireless Controller"); EXPECT_TRUE(result.saw_dualsense_calibration); EXPECT_TRUE(result.saw_dualsense_pairing); EXPECT_TRUE(result.saw_dualsense_firmware); EXPECT_TRUE(result.saw_set_report_reply); - ASSERT_GE(result.output_callback_count, 1U); - EXPECT_EQ(result.last_output.kind, lvh::GamepadOutputKind::rumble); - EXPECT_EQ(result.last_output.low_frequency_rumble, 0x5656); - EXPECT_EQ(result.last_output.high_frequency_rumble, 0x1212); + ASSERT_GE(result.output.callback_count, 1U); + EXPECT_EQ(result.output.last.kind, lvh::GamepadOutputKind::rumble); + EXPECT_EQ(result.output.last.low_frequency_rumble, 0x5656); + EXPECT_EQ(result.output.last.high_frequency_rumble, 0x1212); } TEST_F(LinuxBackendTest, SocketpairBackedDualSenseBluetoothFramesReports) { const auto result = lvh::detail::test::linux_dualsense_bluetooth_uhid_socketpair_reports(); EXPECT_TRUE(result.create_status.ok()) << result.create_status.message(); EXPECT_TRUE(result.close_status.ok()) << result.close_status.message(); - EXPECT_TRUE(result.saw_create); + EXPECT_TRUE(result.creation.saw_create); + EXPECT_TRUE(result.creation.waited_for_start); + EXPECT_EQ(result.creation.name, "Wireless Controller"); EXPECT_TRUE(result.saw_dualsense_bluetooth_input); EXPECT_TRUE(result.saw_dualsense_pairing); EXPECT_TRUE(result.saw_dualsense_feature_crc); @@ -803,22 +810,27 @@ TEST_F(LinuxBackendTest, SocketpairBackedDualShock4RepliesToFeatureReports) { const auto result = lvh::detail::test::linux_dualshock4_uhid_socketpair_reports(); EXPECT_TRUE(result.create_status.ok()) << result.create_status.message(); EXPECT_TRUE(result.close_status.ok()) << result.close_status.message(); - EXPECT_TRUE(result.saw_create); + EXPECT_TRUE(result.creation.saw_create); + EXPECT_TRUE(result.creation.waited_for_start); + EXPECT_EQ(result.creation.name, "Wireless Controller"); + EXPECT_TRUE(result.saw_dualshock4_usb_input); EXPECT_TRUE(result.saw_dualshock4_calibration); EXPECT_TRUE(result.saw_dualshock4_pairing); EXPECT_TRUE(result.saw_dualshock4_firmware); EXPECT_TRUE(result.saw_set_report_reply); - ASSERT_GE(result.output_callback_count, 1U); - EXPECT_EQ(result.last_output.kind, lvh::GamepadOutputKind::rumble); - EXPECT_EQ(result.last_output.low_frequency_rumble, 0x5656); - EXPECT_EQ(result.last_output.high_frequency_rumble, 0x1212); + ASSERT_GE(result.output.callback_count, 1U); + EXPECT_EQ(result.output.last.kind, lvh::GamepadOutputKind::rumble); + EXPECT_EQ(result.output.last.low_frequency_rumble, 0x5656); + EXPECT_EQ(result.output.last.high_frequency_rumble, 0x1212); } TEST_F(LinuxBackendTest, SocketpairBackedDualShock4BluetoothFramesReports) { const auto result = lvh::detail::test::linux_dualshock4_bluetooth_uhid_socketpair_reports(); EXPECT_TRUE(result.create_status.ok()) << result.create_status.message(); EXPECT_TRUE(result.close_status.ok()) << result.close_status.message(); - EXPECT_TRUE(result.saw_create); + EXPECT_TRUE(result.creation.saw_create); + EXPECT_TRUE(result.creation.waited_for_start); + EXPECT_EQ(result.creation.name, "Wireless Controller"); EXPECT_TRUE(result.saw_dualshock4_bluetooth_input); EXPECT_TRUE(result.saw_dualshock4_calibration); EXPECT_TRUE(result.saw_dualshock4_pairing); @@ -897,6 +909,13 @@ TEST_F(LinuxBackendTest, FakeLinuxBackendCreatesAllDeviceTypes) { EXPECT_TRUE(result.pen_tablet_close_status.ok()) << result.pen_tablet_close_status.message(); } +TEST_F(LinuxBackendTest, OpensUhidGamepadsNonblocking) { + const auto flags = lvh::detail::test::linux_backend_gamepad_open_flags(); + EXPECT_EQ(flags & O_ACCMODE, O_RDWR); + EXPECT_NE(flags & O_CLOEXEC, 0); + EXPECT_NE(flags & O_NONBLOCK, 0); +} + TEST_F(LinuxBackendTest, FakeUhidSyscallsCoverFailureBranches) { EXPECT_EQ(lvh::detail::test::linux_uhid_submit_fake_write_failure().code(), lvh::ErrorCode::backend_failure); EXPECT_EQ(lvh::detail::test::linux_uhid_submit_fake_short_write().code(), lvh::ErrorCode::backend_failure); diff --git a/tests/unit/test_linux_consumers.cpp b/tests/unit/test_linux_consumers.cpp index 729b168..675fa4f 100644 --- a/tests/unit/test_linux_consumers.cpp +++ b/tests/unit/test_linux_consumers.cpp @@ -53,6 +53,7 @@ namespace { using LibinputContext = std::unique_ptr; using LibinputEvent = std::unique_ptr; using SdlGameController = std::unique_ptr; + constexpr std::string_view playstation_uhid_name = "Wireless Controller"; /** * @brief SDL-visible gamepad case. @@ -108,6 +109,10 @@ namespace { return std::format("libvirtualhid {} {}", suffix, ::getpid()); } + bool is_playstation_profile(lvh::GamepadProfileKind kind) { + return kind == lvh::GamepadProfileKind::dualshock4 || kind == lvh::GamepadProfileKind::dualsense; + } + std::optional read_first_line(const std::filesystem::path &path) { std::ifstream file {path}; if (!file) { @@ -500,7 +505,9 @@ namespace { const auto expected_profile = [&test_case]() { auto profile = test_case.profile; - profile.name = unique_device_name(test_case.name_suffix); + profile.name = is_playstation_profile(profile.gamepad_kind) ? + std::string {playstation_uhid_name} : + unique_device_name(test_case.name_suffix); if (test_case.expected_vendor_id.has_value()) { profile.vendor_id = *test_case.expected_vendor_id; }