From 5a288c4922fe389b5add4c33e4fd4ff51cf8da37 Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Tue, 8 Sep 2026 00:15:45 -0400 Subject: [PATCH] fix(linux): split relative and absolute mouse devices --- docs/platform-support.md | 22 +- docs/streaming-host-integration.md | 5 + src/platform/linux/uhid_backend.cpp | 227 ++++++++++++++---- .../fixtures/linux_backend_test_hooks.hpp | 35 +++ tests/fixtures/linux_backend_test_hooks.cpp | 71 +++++- tests/unit/test_linux_backend.cpp | 76 +++++- tests/unit/test_linux_consumers.cpp | 59 ++++- 7 files changed, 410 insertions(+), 85 deletions(-) diff --git a/docs/platform-support.md b/docs/platform-support.md index f496e75..d9f4c33 100644 --- a/docs/platform-support.md +++ b/docs/platform-support.md @@ -140,12 +140,22 @@ The Linux backend uses standard user-space kernel interfaces: - X11/XTest only as a keyboard and mouse fallback when `uinput` cannot be used and an X11 session is available. -The uinput mouse advertises the legacy `REL_WHEEL` and `REL_HWHEEL` axes together -with their high-resolution counterparts when the platform provides them. It -accumulates high-resolution input independently for each axis and emits a legacy -detent for every 120 accumulated units. This follows the Linux input protocol, -lets libinput recognize the device as wheel-capable, and prevents libinput from -reserving the physical middle button for button scrolling. +One public mouse handle uses separate relative and absolute uinput devices. The +relative device exposes `REL_X`, `REL_Y`, buttons, and scroll axes. The absolute +device exposes `ABS_X`, `ABS_Y`, buttons, and `INPUT_PROP_DIRECT`, without +relative axes, so libinput and the X11 libinput driver deliver absolute pointer +motion instead of discarding it from a mouse-class relative device. Buttons are +routed to the device that most recently received motion, while releases remain +on the device that received the matching press. Scroll always uses the relative +device. + +The relative uinput mouse advertises the legacy `REL_WHEEL` and `REL_HWHEEL` +axes together with their high-resolution counterparts when the platform +provides them. It accumulates high-resolution input independently for each axis +and emits a legacy detent for every 120 accumulated units. This follows the +Linux input protocol, lets libinput recognize the device as wheel-capable, and +prevents libinput from reserving the physical middle button for button +scrolling. Gamepad support normally prefers `uhid` because descriptors, raw HID identity, feature reports, and output reports matter for controller compatibility. Xbox diff --git a/docs/streaming-host-integration.md b/docs/streaming-host-integration.md index 9df5e6d..bccaa8a 100644 --- a/docs/streaming-host-integration.md +++ b/docs/streaming-host-integration.md @@ -23,6 +23,11 @@ A streaming host should be able to: - Use keyboard and mouse APIs for relative mouse, absolute mouse, buttons, wheel, horizontal wheel, key events, and Unicode text input. +On Linux and FreeBSD, one mouse handle may represent separate relative and +absolute uinput nodes. Consumers should keep using the platform-neutral mouse +API; the backend routes motion and matching button transitions to the correct +node. + `libvirtualhid` should not own the host application's network transport, packet schema, configuration model, controller assignment policy, or status API. diff --git a/src/platform/linux/uhid_backend.cpp b/src/platform/linux/uhid_backend.cpp index 8f01d32..b2e0a2c 100644 --- a/src/platform/linux/uhid_backend.cpp +++ b/src/platform/linux/uhid_backend.cpp @@ -1312,6 +1312,11 @@ namespace lvh::detail { std::int32_t remainder = 0; }; + enum class UinputMouseDeviceKind { + relative, + absolute, + }; + LegacyScrollConversion accumulated_legacy_scroll(std::int32_t remainder, std::int32_t distance) { const auto total = static_cast(remainder) + distance; return { @@ -1338,7 +1343,11 @@ namespace lvh::detail { } protected: - OperationStatus create_uinput_device(const DeviceProfile &profile, DeviceId id); + OperationStatus create_uinput_device( + const DeviceProfile &profile, + DeviceId id, + UinputMouseDeviceKind mouse_kind = UinputMouseDeviceKind::relative + ); std::vector uinput_device_nodes(const std::string &device_name) const; @@ -1484,16 +1493,10 @@ namespace lvh::detail { return OperationStatus::success(); } - OperationStatus configure_evdev_mouse(libevdev *device) { + OperationStatus configure_evdev_mouse(libevdev *device, UinputMouseDeviceKind kind) { if (const auto status = enable_evdev_type(device, EV_KEY, "mouse button events"); !status.ok()) { return status; } - if (const auto status = enable_evdev_type(device, EV_REL, "relative mouse events"); !status.ok()) { - return status; - } - if (const auto status = enable_evdev_type(device, EV_ABS, "absolute mouse events"); !status.ok()) { - return status; - } for (const auto button : {BTN_LEFT, BTN_RIGHT, BTN_MIDDLE, BTN_SIDE, BTN_EXTRA}) { if (const auto status = enable_evdev_code(device, EV_KEY, button, "mouse button"); !status.ok()) { @@ -1501,6 +1504,26 @@ namespace lvh::detail { } } + if (kind == UinputMouseDeviceKind::absolute) { + if (const auto status = enable_evdev_property(device, INPUT_PROP_DIRECT, "direct input"); !status.ok()) { + return status; + } + if (const auto status = enable_evdev_type(device, EV_ABS, "absolute mouse events"); !status.ok()) { + return status; + } + + auto x = make_absinfo(0, absolute_axis_max); + if (const auto status = enable_evdev_code(device, EV_ABS, ABS_X, "absolute mouse axis", &x); !status.ok()) { + return status; + } + auto y = make_absinfo(0, absolute_axis_max); + return enable_evdev_code(device, EV_ABS, ABS_Y, "absolute mouse axis", &y); + } + + if (const auto status = enable_evdev_type(device, EV_REL, "relative mouse events"); !status.ok()) { + return status; + } + for (const auto code : {REL_X, REL_Y}) { if (const auto status = enable_evdev_code(device, EV_REL, code, "relative mouse axis"); !status.ok()) { return status; @@ -1525,12 +1548,7 @@ namespace lvh::detail { } #endif - auto x = make_absinfo(0, absolute_axis_max); - if (const auto status = enable_evdev_code(device, EV_ABS, ABS_X, "absolute mouse axis", &x); !status.ok()) { - return status; - } - auto y = make_absinfo(0, absolute_axis_max); - return enable_evdev_code(device, EV_ABS, ABS_Y, "absolute mouse axis", &y); + return OperationStatus::success(); } OperationStatus configure_evdev_touch_axes(libevdev *device) { @@ -1768,14 +1786,18 @@ namespace lvh::detail { return configure_evdev_gamepad_force_feedback(device, supports_rumble); } - OperationStatus configure_evdev_device(libevdev *device, const DeviceProfile &profile) { + OperationStatus configure_evdev_device( + libevdev *device, + const DeviceProfile &profile, + UinputMouseDeviceKind mouse_kind = UinputMouseDeviceKind::relative + ) { switch (profile.device_type) { using enum DeviceType; case keyboard: return configure_evdev_keyboard(device); case mouse: - return configure_evdev_mouse(device); + return configure_evdev_mouse(device, mouse_kind); case touchscreen: return configure_evdev_touchscreen(device); case trackpad: @@ -1792,7 +1814,12 @@ namespace lvh::detail { return OperationStatus::failure(ErrorCode::unsupported_profile, "unsupported uinput device type"); } - UinputCreationResult create_libevdev_uinput_device(int fd, const DeviceProfile &profile, DeviceId id) { + UinputCreationResult create_libevdev_uinput_device( + int fd, + const DeviceProfile &profile, + DeviceId id, + UinputMouseDeviceKind mouse_kind = UinputMouseDeviceKind::relative + ) { if (fd < 0) { return {OperationStatus::failure(ErrorCode::backend_failure, "uinput file descriptor is closed"), nullptr}; } @@ -1822,7 +1849,7 @@ namespace lvh::detail { libevdev_set_id_product(device.get(), product_id); libevdev_set_id_version(device.get(), profile.version); - if (const auto status = configure_evdev_device(device.get(), profile); !status.ok()) { + if (const auto status = configure_evdev_device(device.get(), profile, mouse_kind); !status.ok()) { return {status, nullptr}; } @@ -1840,8 +1867,12 @@ namespace lvh::detail { return {OperationStatus::success(), uinput_device}; } - OperationStatus UinputDevice::create_uinput_device(const DeviceProfile &profile, DeviceId id) { - auto result = create_libevdev_uinput_device(fd_, profile, id); + OperationStatus UinputDevice::create_uinput_device( + const DeviceProfile &profile, + DeviceId id, + UinputMouseDeviceKind mouse_kind + ) { + auto result = create_libevdev_uinput_device(fd_, profile, id, mouse_kind); if (!result.status.ok()) { return result.status; } @@ -1971,24 +2002,67 @@ namespace lvh::detail { }; /** - * @brief Backend mouse backed by one Linux uinput file descriptor. + * @brief One motion-specific Linux uinput device used by a backend mouse. */ - class UinputMouse final: public BackendMouse, private UinputDevice { + class UinputMouseDevice final: private UinputDevice { public: - explicit UinputMouse(int file_descriptor): + explicit UinputMouseDevice(int file_descriptor): UinputDevice {file_descriptor} {} + OperationStatus create(DeviceId id, const DeviceProfile &profile, UinputMouseDeviceKind kind) { + device_name_ = profile.name; + return create_uinput_device(profile, id, kind); + } + + OperationStatus emit(std::uint16_t type, std::uint16_t code, std::int32_t value) { + return emit_event(type, code, value); + } + + OperationStatus synchronize() { + return sync(); + } + + OperationStatus close(const std::string &description) { + return close_uinput(description); + } + + bool open() const { + return is_open(); + } + + std::vector device_nodes() const { + return uinput_device_nodes(device_name_); + } + + private: + std::string device_name_; + }; + + /** + * @brief Backend mouse backed by separate relative and absolute Linux uinput devices. + */ + class UinputMouse final: public BackendMouse { + public: + UinputMouse(int relative_file_descriptor, int absolute_file_descriptor): + relative_device_ {relative_file_descriptor}, + absolute_device_ {absolute_file_descriptor} {} + ~UinputMouse() override { static_cast(close()); } OperationStatus create(DeviceId id, const CreateMouseOptions &options) { - device_name_ = options.profile.name; - return create_uinput_device(options.profile, id); + if (const auto status = relative_device_.create(id, options.profile, UinputMouseDeviceKind::relative); !status.ok()) { + return status; + } + + auto absolute_profile = options.profile; + absolute_profile.name += " (Absolute)"; + return absolute_device_.create(id, absolute_profile, UinputMouseDeviceKind::absolute); } OperationStatus submit(const MouseEvent &event) override { - if (!is_open()) { + if (!relative_device_.open() || !absolute_device_.open()) { return OperationStatus::failure(ErrorCode::device_closed, "uinput mouse is closed"); } @@ -2011,47 +2085,107 @@ namespace lvh::detail { } OperationStatus close() override { - return close_uinput("uinput mouse"); + const auto relative_status = relative_device_.close("relative uinput mouse"); + const auto absolute_status = absolute_device_.close("absolute uinput mouse"); + return relative_status.ok() ? absolute_status : relative_status; } std::vector device_nodes() const override { - return uinput_device_nodes(device_name_); + auto nodes = relative_device_.device_nodes(); + const auto absolute_nodes = absolute_device_.device_nodes(); + nodes.insert(nodes.end(), absolute_nodes.begin(), absolute_nodes.end()); + return nodes; } private: - std::string device_name_; + UinputMouseDevice relative_device_; + UinputMouseDevice absolute_device_; + UinputMouseDeviceKind last_motion_device_ = UinputMouseDeviceKind::relative; + std::byte relative_buttons_down_ {}; + std::byte absolute_buttons_down_ {}; std::int32_t vertical_scroll_remainder_ = 0; std::int32_t horizontal_scroll_remainder_ = 0; + UinputMouseDevice &device(UinputMouseDeviceKind kind) { + return kind == UinputMouseDeviceKind::absolute ? absolute_device_ : relative_device_; + } + + std::byte &buttons_down(UinputMouseDeviceKind kind) { + return kind == UinputMouseDeviceKind::absolute ? absolute_buttons_down_ : relative_buttons_down_; + } + + static std::byte button_mask(MouseButton button) { + auto index = std::to_underlying(button); + if (index > std::to_underlying(MouseButton::extra)) { + index = std::to_underlying(MouseButton::left); + } + return std::byte {1} << index; + } + + UinputMouseDeviceKind button_device(const MouseEvent &event) const { + if (!event.pressed) { + const auto mask = button_mask(event.button); + if ((relative_buttons_down_ & mask) != std::byte {0}) { + return UinputMouseDeviceKind::relative; + } + if ((absolute_buttons_down_ & mask) != std::byte {0}) { + return UinputMouseDeviceKind::absolute; + } + } + return last_motion_device_; + } + OperationStatus submit_relative_motion(const MouseEvent &event) { + auto &relative = device(UinputMouseDeviceKind::relative); if (event.x != 0) { - if (const auto status = emit_event(EV_REL, REL_X, event.x); !status.ok()) { + if (const auto status = relative.emit(EV_REL, REL_X, event.x); !status.ok()) { return status; } } if (event.y != 0) { - if (const auto status = emit_event(EV_REL, REL_Y, event.y); !status.ok()) { + if (const auto status = relative.emit(EV_REL, REL_Y, event.y); !status.ok()) { return status; } } - return sync(); + if (const auto status = relative.synchronize(); !status.ok()) { + return status; + } + last_motion_device_ = UinputMouseDeviceKind::relative; + return OperationStatus::success(); } OperationStatus submit_absolute_motion(const MouseEvent &event) { - if (const auto status = emit_event(EV_ABS, ABS_X, scale_absolute_axis(event.x, event.width)); !status.ok()) { + auto &absolute = device(UinputMouseDeviceKind::absolute); + if (const auto status = absolute.emit(EV_ABS, ABS_X, scale_absolute_axis(event.x, event.width)); !status.ok()) { return status; } - if (const auto status = emit_event(EV_ABS, ABS_Y, scale_absolute_axis(event.y, event.height)); !status.ok()) { + if (const auto status = absolute.emit(EV_ABS, ABS_Y, scale_absolute_axis(event.y, event.height)); !status.ok()) { return status; } - return sync(); + if (const auto status = absolute.synchronize(); !status.ok()) { + return status; + } + last_motion_device_ = UinputMouseDeviceKind::absolute; + return OperationStatus::success(); } OperationStatus submit_button(const MouseEvent &event) { - if (const auto status = emit_event(EV_KEY, static_cast(mouse_button_to_linux(event.button)), event.pressed ? 1 : 0); !status.ok()) { + const auto kind = button_device(event); + auto &target = device(kind); + if (const auto status = target.emit(EV_KEY, static_cast(mouse_button_to_linux(event.button)), event.pressed ? 1 : 0); !status.ok()) { return status; } - return sync(); + if (const auto status = target.synchronize(); !status.ok()) { + return status; + } + + const auto mask = button_mask(event.button); + if (event.pressed) { + buttons_down(kind) |= mask; + } else { + buttons_down(kind) &= ~mask; + } + return OperationStatus::success(); } OperationStatus submit_vertical_scroll(std::int32_t distance) { @@ -2068,18 +2202,19 @@ namespace lvh::detail { std::uint16_t legacy_code, std::optional high_resolution_code ) { + auto &relative = device(UinputMouseDeviceKind::relative); const auto converted = accumulated_legacy_scroll(remainder, distance); if (converted.detents != 0) { - if (const auto status = emit_event(EV_REL, legacy_code, converted.detents); !status.ok()) { + if (const auto status = relative.emit(EV_REL, legacy_code, converted.detents); !status.ok()) { return status; } } if (high_resolution_code.has_value()) { - if (const auto status = emit_event(EV_REL, *high_resolution_code, distance); !status.ok()) { + if (const auto status = relative.emit(EV_REL, *high_resolution_code, distance); !status.ok()) { return status; } } - if (const auto status = sync(); !status.ok()) { + if (const auto status = relative.synchronize(); !status.ok()) { return status; } remainder = converted.remainder; @@ -3821,12 +3956,18 @@ namespace lvh::detail { } BackendMouseCreationResult create_mouse(DeviceId id, const CreateMouseOptions &options) override { - const auto fd = open_uinput(O_RDWR | O_CLOEXEC | O_NONBLOCK); - if (fd < 0) { + const auto relative_fd = open_uinput(O_RDWR | O_CLOEXEC | O_NONBLOCK); + if (relative_fd < 0) { + return create_xtest_mouse(); + } + + const auto absolute_fd = open_uinput(O_RDWR | O_CLOEXEC | O_NONBLOCK); + if (absolute_fd < 0) { + static_cast(system_close(relative_fd)); return create_xtest_mouse(); } - auto mouse = std::make_unique(fd); + auto mouse = std::make_unique(relative_fd, absolute_fd); if (const auto status = mouse->create(id, options); !status.ok()) { static_cast(mouse->close()); auto fallback = create_xtest_mouse(); diff --git a/tests/fixtures/include/fixtures/linux_backend_test_hooks.hpp b/tests/fixtures/include/fixtures/linux_backend_test_hooks.hpp index f26ea1c..84c2cee 100644 --- a/tests/fixtures/include/fixtures/linux_backend_test_hooks.hpp +++ b/tests/fixtures/include/fixtures/linux_backend_test_hooks.hpp @@ -52,6 +52,26 @@ namespace lvh::detail::test { std::vector events; }; + /** + * @brief Result from a split relative/absolute pipe-backed mouse submission. + */ + struct LinuxMouseInputSubmissionResult { + /** + * @brief Submit operation status. + */ + OperationStatus status; + + /** + * @brief Events written to the relative mouse device. + */ + std::vector relative_events; + + /** + * @brief Events written to the absolute mouse device. + */ + std::vector absolute_events; + }; + /** * @brief Result from a fake uinput Xbox force-feedback exchange. */ @@ -915,6 +935,14 @@ namespace lvh::detail::test { */ LinuxInputSubmissionResult linux_uinput_mouse_submit_pipe_sequence(const std::vector &events); + /** + * @brief Submit mouse events to separate relative and absolute pipe-backed devices. + * + * @param events Mouse events to submit in order. + * @return Submission status and the events captured from each device. + */ + LinuxMouseInputSubmissionResult linux_uinput_mouse_submit_split_pipe_sequence(const std::vector &events); + /** * @brief Place and release a contact through a pipe-backed uinput touchscreen. * @@ -1252,6 +1280,13 @@ namespace lvh::detail::test { */ LinuxLibevdevCreationResult linux_uinput_create_fake_libevdev_device(DeviceType device_type); + /** + * @brief Create the absolute half of a uinput mouse through the fake libevdev recorder. + * + * @return Recorded fake libevdev construction result. + */ + LinuxLibevdevCreationResult linux_uinput_create_fake_absolute_mouse_device(); + /** * @brief Create a uinput gamepad through the fake libevdev recorder. * diff --git a/tests/fixtures/linux_backend_test_hooks.cpp b/tests/fixtures/linux_backend_test_hooks.cpp index 48639db..0111bb2 100644 --- a/tests/fixtures/linux_backend_test_hooks.cpp +++ b/tests/fixtures/linux_backend_test_hooks.cpp @@ -835,7 +835,8 @@ namespace lvh::detail::test { OperationStatus create_uinput_device_by_type( int fd, DeviceType device_type, - std::optional gamepad_kind = std::nullopt + std::optional gamepad_kind = std::nullopt, + UinputMouseDeviceKind mouse_kind = UinputMouseDeviceKind::relative ) { switch (device_type) { case DeviceType::keyboard: @@ -849,8 +850,8 @@ namespace lvh::detail::test { { CreateMouseOptions options; options.profile = profile_for_uinput_device_type(device_type); - UinputMouse mouse {fd}; - return mouse.create(1, options); + UinputMouseDevice mouse {fd}; + return mouse.create(1, options.profile, mouse_kind); } case DeviceType::touchscreen: { @@ -889,7 +890,8 @@ namespace lvh::detail::test { LinuxLibevdevCreationResult create_fake_libevdev_device( DeviceType device_type, ConfigureFailure configure_failure, - std::optional gamepad_kind = std::nullopt + std::optional gamepad_kind = std::nullopt, + UinputMouseDeviceKind mouse_kind = UinputMouseDeviceKind::relative ) { LinuxTestSyscalls syscalls; syscalls.override_libevdev = true; @@ -903,7 +905,7 @@ namespace lvh::detail::test { return result; } - result.status = create_uinput_device_by_type(fd, device_type, gamepad_kind); + result.status = create_uinput_device_by_type(fd, device_type, gamepad_kind, mouse_kind); if (!syscalls.libevdev_devices.empty()) { const auto &device = syscalls.libevdev_devices.back(); result.name = device.name; @@ -1059,7 +1061,7 @@ namespace lvh::detail::test { std::size_t linux_empty_device_nodes_count() { UhidGamepad gamepad {-1}; UinputKeyboard keyboard {-1}; - UinputMouse mouse {-1}; + UinputMouse mouse {-1, -1}; UinputTouchscreen touchscreen {-1}; UinputTrackpad trackpad {-1}; UinputPenTablet pen_tablet {-1}; @@ -1304,17 +1306,17 @@ namespace lvh::detail::test { CreateMouseOptions options; options.profile = profiles::mouse(); - UinputMouse mouse {-1}; + UinputMouse mouse {-1, -1}; return mouse.create(1, options); } OperationStatus linux_uinput_mouse_submit_invalid_fd(const MouseEvent &event) { - UinputMouse mouse {-1}; + UinputMouse mouse {-1, -1}; return mouse.submit(event); } OperationStatus linux_uinput_mouse_submit_after_close() { - UinputMouse mouse {-1}; + UinputMouse mouse {-1, -1}; static_cast(mouse.close()); return mouse.submit({.kind = MouseEventKind::relative_motion, .x = 1, .y = 1}); } @@ -1329,7 +1331,14 @@ namespace lvh::detail::test { return {system_error_status(ErrorCode::backend_failure, "failed to create pipe", errno), {}}; } - UinputMouse mouse {descriptors[1]}; + const auto absolute_descriptor = ::dup(descriptors[1]); + if (absolute_descriptor < 0) { + static_cast(::close(descriptors[0])); + static_cast(::close(descriptors[1])); + return {system_error_status(ErrorCode::backend_failure, "failed to duplicate pipe", errno), {}}; + } + + UinputMouse mouse {descriptors[1], absolute_descriptor}; auto status = OperationStatus::success(); for (const auto &event : events) { status = mouse.submit(event); @@ -1343,6 +1352,35 @@ namespace lvh::detail::test { return {std::move(status), std::move(records)}; } + LinuxMouseInputSubmissionResult linux_uinput_mouse_submit_split_pipe_sequence(const std::vector &events) { + std::array relative_descriptors {-1, -1}; + if (::pipe(relative_descriptors.data()) != 0) { + return {system_error_status(ErrorCode::backend_failure, "failed to create relative mouse pipe", errno), {}, {}}; + } + + std::array absolute_descriptors {-1, -1}; + if (::pipe(absolute_descriptors.data()) != 0) { + static_cast(::close(relative_descriptors[0])); + static_cast(::close(relative_descriptors[1])); + return {system_error_status(ErrorCode::backend_failure, "failed to create absolute mouse pipe", errno), {}, {}}; + } + + UinputMouse mouse {relative_descriptors[1], absolute_descriptors[1]}; + auto status = OperationStatus::success(); + for (const auto &event : events) { + status = mouse.submit(event); + if (!status.ok()) { + break; + } + } + static_cast(mouse.close()); + auto relative_records = read_input_events_until_eof(relative_descriptors[0]); + auto absolute_records = read_input_events_until_eof(absolute_descriptors[0]); + static_cast(::close(relative_descriptors[0])); + static_cast(::close(absolute_descriptors[0])); + return {std::move(status), std::move(relative_records), std::move(absolute_records)}; + } + LinuxInputSubmissionResult linux_uinput_touchscreen_contact_pipe(const TouchContact &contact) { std::array descriptors {-1, -1}; if (::pipe(descriptors.data()) != 0) { @@ -2618,6 +2656,15 @@ namespace lvh::detail::test { return create_fake_libevdev_device(device_type); } + LinuxLibevdevCreationResult linux_uinput_create_fake_absolute_mouse_device() { + return create_fake_libevdev_device( + DeviceType::mouse, + keep_fake_libevdev_successful, + std::nullopt, + UinputMouseDeviceKind::absolute + ); + } + LinuxLibevdevCreationResult linux_uinput_create_fake_gamepad(GamepadProfileKind kind) { return create_fake_libevdev_gamepad(kind); } @@ -2739,7 +2786,7 @@ namespace lvh::detail::test { syscalls.override_ioctl = true; ScopedLinuxTestSyscalls scoped_syscalls {syscalls}; - UinputMouse mouse {fake_fd}; + UinputMouse mouse {fake_fd, fake_fd}; return mouse.submit(event); } @@ -2750,7 +2797,7 @@ namespace lvh::detail::test { syscalls.override_ioctl = true; ScopedLinuxTestSyscalls scoped_syscalls {syscalls}; - UinputMouse mouse {fake_fd}; + UinputMouse mouse {fake_fd, fake_fd}; return mouse.submit(event); } diff --git a/tests/unit/test_linux_backend.cpp b/tests/unit/test_linux_backend.cpp index e3761f3..5f39285 100644 --- a/tests/unit/test_linux_backend.cpp +++ b/tests/unit/test_linux_backend.cpp @@ -647,6 +647,49 @@ TEST_F(LinuxBackendTest, PipeBackedUinputMouseEmitsEvents) { EXPECT_EQ(result.events.back().type, EV_SYN); } +TEST_F(LinuxBackendTest, PipeBackedUinputMouseRoutesMotionAndButtonsAcrossSplitDevices) { + const std::vector events { + {.kind = lvh::MouseEventKind::absolute_motion, .x = 50, .y = 25, .width = 100, .height = 100}, + {.kind = lvh::MouseEventKind::button, .button = lvh::MouseButton::side, .pressed = true}, + {.kind = lvh::MouseEventKind::relative_motion, .x = 5, .y = -2}, + {.kind = lvh::MouseEventKind::button, .button = lvh::MouseButton::side, .pressed = false}, + {.kind = lvh::MouseEventKind::button, .button = lvh::MouseButton::extra, .pressed = true}, + {.kind = lvh::MouseEventKind::button, .button = lvh::MouseButton::extra, .pressed = false}, + }; + const auto result = lvh::detail::test::linux_uinput_mouse_submit_split_pipe_sequence(events); + ASSERT_TRUE(result.status.ok()) << result.status.message(); + + ASSERT_EQ(result.absolute_events.size(), 7U); + EXPECT_EQ(result.absolute_events[0].type, EV_ABS); + EXPECT_EQ(result.absolute_events[0].code, ABS_X); + EXPECT_EQ(result.absolute_events[1].type, EV_ABS); + EXPECT_EQ(result.absolute_events[1].code, ABS_Y); + EXPECT_EQ(result.absolute_events[3].type, EV_KEY); + EXPECT_EQ(result.absolute_events[3].code, BTN_SIDE); + EXPECT_EQ(result.absolute_events[3].value, 1); + EXPECT_EQ(result.absolute_events[5].type, EV_KEY); + EXPECT_EQ(result.absolute_events[5].code, BTN_SIDE); + EXPECT_EQ(result.absolute_events[5].value, 0); + EXPECT_TRUE(std::ranges::none_of(result.absolute_events, [](const auto &event) { + return event.type == EV_REL; + })); + + ASSERT_EQ(result.relative_events.size(), 7U); + EXPECT_EQ(result.relative_events[0].type, EV_REL); + EXPECT_EQ(result.relative_events[0].code, REL_X); + EXPECT_EQ(result.relative_events[1].type, EV_REL); + EXPECT_EQ(result.relative_events[1].code, REL_Y); + EXPECT_EQ(result.relative_events[3].type, EV_KEY); + EXPECT_EQ(result.relative_events[3].code, BTN_EXTRA); + EXPECT_EQ(result.relative_events[3].value, 1); + EXPECT_EQ(result.relative_events[5].type, EV_KEY); + EXPECT_EQ(result.relative_events[5].code, BTN_EXTRA); + EXPECT_EQ(result.relative_events[5].value, 0); + EXPECT_TRUE(std::ranges::none_of(result.relative_events, [](const auto &event) { + return event.type == EV_ABS; + })); +} + TEST_F(LinuxBackendTest, PipeBackedUinputMouseAccumulatesLegacyScrollDetentsPerAxis) { const std::vector events { {.kind = lvh::MouseEventKind::vertical_scroll, .high_resolution_scroll = 60}, @@ -1193,22 +1236,31 @@ TEST_F(LinuxBackendTest, FakeUinputConstructionCoversCapabilitiesAndFailureBranc EXPECT_EQ(xbox_360_button_slots[index], expected_xbox_360_button_slots[index]) << "button slot " << index; } - const auto mouse = lvh::detail::test::linux_uinput_create_fake_libevdev_device(lvh::DeviceType::mouse); - ASSERT_TRUE(mouse.status.ok()) << mouse.status.message(); - EXPECT_TRUE(has_type(mouse, EV_KEY)); - EXPECT_TRUE(has_type(mouse, EV_REL)); - EXPECT_TRUE(has_type(mouse, EV_ABS)); - EXPECT_NE(find_code(mouse, EV_KEY, BTN_LEFT), nullptr); - EXPECT_NE(find_code(mouse, EV_REL, REL_X), nullptr); - EXPECT_NE(find_code(mouse, EV_REL, REL_WHEEL), nullptr); - EXPECT_NE(find_code(mouse, EV_REL, REL_HWHEEL), nullptr); + const auto relative_mouse = lvh::detail::test::linux_uinput_create_fake_libevdev_device(lvh::DeviceType::mouse); + ASSERT_TRUE(relative_mouse.status.ok()) << relative_mouse.status.message(); + EXPECT_TRUE(has_type(relative_mouse, EV_KEY)); + EXPECT_TRUE(has_type(relative_mouse, EV_REL)); + EXPECT_FALSE(has_type(relative_mouse, EV_ABS)); + EXPECT_FALSE(has_property(relative_mouse, INPUT_PROP_DIRECT)); + EXPECT_NE(find_code(relative_mouse, EV_KEY, BTN_LEFT), nullptr); + EXPECT_NE(find_code(relative_mouse, EV_REL, REL_X), nullptr); + EXPECT_NE(find_code(relative_mouse, EV_REL, REL_WHEEL), nullptr); + EXPECT_NE(find_code(relative_mouse, EV_REL, REL_HWHEEL), nullptr); #if defined(REL_WHEEL_HI_RES) - EXPECT_NE(find_code(mouse, EV_REL, REL_WHEEL_HI_RES), nullptr); + EXPECT_NE(find_code(relative_mouse, EV_REL, REL_WHEEL_HI_RES), nullptr); #endif #if defined(REL_HWHEEL_HI_RES) - EXPECT_NE(find_code(mouse, EV_REL, REL_HWHEEL_HI_RES), nullptr); + EXPECT_NE(find_code(relative_mouse, EV_REL, REL_HWHEEL_HI_RES), nullptr); #endif - const auto *mouse_x = find_code(mouse, EV_ABS, ABS_X); + + const auto absolute_mouse = lvh::detail::test::linux_uinput_create_fake_absolute_mouse_device(); + ASSERT_TRUE(absolute_mouse.status.ok()) << absolute_mouse.status.message(); + EXPECT_TRUE(has_type(absolute_mouse, EV_KEY)); + EXPECT_FALSE(has_type(absolute_mouse, EV_REL)); + EXPECT_TRUE(has_type(absolute_mouse, EV_ABS)); + EXPECT_TRUE(has_property(absolute_mouse, INPUT_PROP_DIRECT)); + EXPECT_NE(find_code(absolute_mouse, EV_KEY, BTN_LEFT), nullptr); + const auto *mouse_x = find_code(absolute_mouse, EV_ABS, ABS_X); ASSERT_NE(mouse_x, nullptr); EXPECT_TRUE(mouse_x->has_absinfo); EXPECT_EQ(mouse_x->maximum, 65535); diff --git a/tests/unit/test_linux_consumers.cpp b/tests/unit/test_linux_consumers.cpp index 8ef7337..a2283fc 100644 --- a/tests/unit/test_linux_consumers.cpp +++ b/tests/unit/test_linux_consumers.cpp @@ -1149,7 +1149,7 @@ TEST_F(LinuxConsumerTest, LibinputSeesUinputKeyboardKeys) { EXPECT_EQ(libinput_event_keyboard_get_key_state(keyboard_event), LIBINPUT_KEY_STATE_RELEASED); } -TEST_F(LinuxConsumerTest, LibinputSeesUinputMouseMotionAndButtons) { +TEST_F(LinuxConsumerTest, LibinputSeesSplitUinputMouseMotionAndButtons) { ASSERT_TRUE(HasReadableWritableDeviceNode("/dev/uinput")); lvh::RuntimeOptions runtime_options; @@ -1165,42 +1165,77 @@ TEST_F(LinuxConsumerTest, LibinputSeesUinputMouseMotionAndButtons) { auto created = runtime->create_mouse(options); ASSERT_TRUE(created) << created.status.message(); - const auto node = wait_for_readable_event_node(options.profile.name); - ASSERT_TRUE(node) << "libinput mouse event node was not readable for " << options.profile.name; + const auto relative_node = wait_for_readable_event_node(options.profile.name); + ASSERT_TRUE(relative_node) << "libinput relative mouse event node was not readable for " << options.profile.name; + const auto absolute_name = options.profile.name + " (Absolute)"; + const auto absolute_node = wait_for_readable_event_node(absolute_name); + ASSERT_TRUE(absolute_node) << "libinput absolute mouse event node was not readable for " << absolute_name; - auto context = create_libinput_context(*node); - ASSERT_NE(context.get(), nullptr) << "libinput could not open " << node->string(); + auto relative_context = create_libinput_context(*relative_node); + ASSERT_NE(relative_context.get(), nullptr) << "libinput could not open " << relative_node->string(); + auto absolute_context = create_libinput_context(*absolute_node); + ASSERT_NE(absolute_context.get(), nullptr) << "libinput could not open " << absolute_node->string(); - auto event = wait_for_libinput_event(context.get(), {LIBINPUT_EVENT_DEVICE_ADDED}); + auto event = wait_for_libinput_event(relative_context.get(), {LIBINPUT_EVENT_DEVICE_ADDED}); ASSERT_NE(event.get(), nullptr); auto *device = libinput_event_get_device(event.get()); ASSERT_NE(device, nullptr); EXPECT_TRUE(libinput_device_has_capability(device, LIBINPUT_DEVICE_CAP_POINTER)); EXPECT_EQ(libinput_device_config_scroll_get_method(device), LIBINPUT_CONFIG_SCROLL_NO_SCROLL); - ASSERT_TRUE(created.mouse->move_relative(25, -10).ok()); - event = wait_for_libinput_event(context.get(), {LIBINPUT_EVENT_POINTER_MOTION}); + event = wait_for_libinput_event(absolute_context.get(), {LIBINPUT_EVENT_DEVICE_ADDED}); + ASSERT_NE(event.get(), nullptr); + device = libinput_event_get_device(event.get()); + ASSERT_NE(device, nullptr); + EXPECT_TRUE(libinput_device_has_capability(device, LIBINPUT_DEVICE_CAP_POINTER)); + + ASSERT_TRUE(created.mouse->move_absolute(50, 25, 100, 100).ok()); + event = wait_for_libinput_event(absolute_context.get(), {LIBINPUT_EVENT_POINTER_MOTION_ABSOLUTE}); ASSERT_NE(event.get(), nullptr); auto *pointer_event = libinput_event_get_pointer_event(event.get()); ASSERT_NE(pointer_event, nullptr); - EXPECT_DOUBLE_EQ(libinput_event_pointer_get_dx_unaccelerated(pointer_event), 25.0); - EXPECT_DOUBLE_EQ(libinput_event_pointer_get_dy_unaccelerated(pointer_event), -10.0); + EXPECT_NEAR(libinput_event_pointer_get_absolute_x_transformed(pointer_event, 100), 50.0, 0.1); + EXPECT_NEAR(libinput_event_pointer_get_absolute_y_transformed(pointer_event, 100), 25.0, 0.1); ASSERT_TRUE(created.mouse->button(lvh::MouseButton::middle, true).ok()); - event = wait_for_libinput_event(context.get(), {LIBINPUT_EVENT_POINTER_BUTTON}); + event = wait_for_libinput_event(absolute_context.get(), {LIBINPUT_EVENT_POINTER_BUTTON}); ASSERT_NE(event.get(), nullptr); pointer_event = libinput_event_get_pointer_event(event.get()); ASSERT_NE(pointer_event, nullptr); EXPECT_EQ(libinput_event_pointer_get_button(pointer_event), BTN_MIDDLE); EXPECT_EQ(libinput_event_pointer_get_button_state(pointer_event), LIBINPUT_BUTTON_STATE_PRESSED); + ASSERT_TRUE(created.mouse->move_relative(25, -10).ok()); + event = wait_for_libinput_event(relative_context.get(), {LIBINPUT_EVENT_POINTER_MOTION}); + ASSERT_NE(event.get(), nullptr); + pointer_event = libinput_event_get_pointer_event(event.get()); + ASSERT_NE(pointer_event, nullptr); + EXPECT_DOUBLE_EQ(libinput_event_pointer_get_dx_unaccelerated(pointer_event), 25.0); + EXPECT_DOUBLE_EQ(libinput_event_pointer_get_dy_unaccelerated(pointer_event), -10.0); + ASSERT_TRUE(created.mouse->button(lvh::MouseButton::middle, false).ok()); - event = wait_for_libinput_event(context.get(), {LIBINPUT_EVENT_POINTER_BUTTON}); + event = wait_for_libinput_event(absolute_context.get(), {LIBINPUT_EVENT_POINTER_BUTTON}); ASSERT_NE(event.get(), nullptr); pointer_event = libinput_event_get_pointer_event(event.get()); ASSERT_NE(pointer_event, nullptr); EXPECT_EQ(libinput_event_pointer_get_button(pointer_event), BTN_MIDDLE); EXPECT_EQ(libinput_event_pointer_get_button_state(pointer_event), LIBINPUT_BUTTON_STATE_RELEASED); + + ASSERT_TRUE(created.mouse->button(lvh::MouseButton::extra, true).ok()); + event = wait_for_libinput_event(relative_context.get(), {LIBINPUT_EVENT_POINTER_BUTTON}); + ASSERT_NE(event.get(), nullptr); + pointer_event = libinput_event_get_pointer_event(event.get()); + ASSERT_NE(pointer_event, nullptr); + EXPECT_EQ(libinput_event_pointer_get_button(pointer_event), BTN_EXTRA); + EXPECT_EQ(libinput_event_pointer_get_button_state(pointer_event), LIBINPUT_BUTTON_STATE_PRESSED); + + ASSERT_TRUE(created.mouse->button(lvh::MouseButton::extra, false).ok()); + event = wait_for_libinput_event(relative_context.get(), {LIBINPUT_EVENT_POINTER_BUTTON}); + ASSERT_NE(event.get(), nullptr); + pointer_event = libinput_event_get_pointer_event(event.get()); + ASSERT_NE(pointer_event, nullptr); + EXPECT_EQ(libinput_event_pointer_get_button(pointer_event), BTN_EXTRA); + EXPECT_EQ(libinput_event_pointer_get_button_state(pointer_event), LIBINPUT_BUTTON_STATE_RELEASED); } TEST_F(LinuxConsumerTest, LibinputSeesUinputTouchscreenContacts) {