diff --git a/docs/platform-support.md b/docs/platform-support.md index d42cbb2..af11b97 100644 --- a/docs/platform-support.md +++ b/docs/platform-support.md @@ -157,6 +157,11 @@ the evdev layout used by Linux-native virtual-controller implementations and allows standard `FF_RUMBLE` effects without emulating the physical controller's proprietary initialization handshake. +Linux touchscreen and trackpad contacts use the lowest available multitouch +slot while they are active. A newly placed contact receives a new tracking ID, +including when it reuses a slot released by another contact, so replacing one +finger cannot overwrite another active finger in standard evdev consumers. + On descriptor-driven backends, native Switch Pro output reports `0x01` and `0x10` are decoded into the normalized low- and high-frequency rumble callback. The original native report remains available in `GamepadOutput::raw_report`. diff --git a/src/platform/linux/uhid_backend.cpp b/src/platform/linux/uhid_backend.cpp index ab85885..d64f5ab 100644 --- a/src/platform/linux/uhid_backend.cpp +++ b/src/platform/linux/uhid_backend.cpp @@ -85,6 +85,7 @@ namespace lvh::detail { constexpr auto touch_axis_max_x = 19200; constexpr auto touch_axis_max_y = 10800; constexpr auto touch_max_contacts = 16; + constexpr std::uint32_t touch_tracking_id_count = 65536U; constexpr auto touch_pressure_max = 253; constexpr auto tablet_pressure_max = 4096; constexpr auto tablet_distance_max = 1024; @@ -1655,7 +1656,9 @@ namespace lvh::detail { return status; } if (new_slot_) { - if (const auto status = emit_event(EV_ABS, ABS_MT_TRACKING_ID, *slot); !status.ok()) { + const auto tracking_id = next_tracking_id_; + next_tracking_id_ = (next_tracking_id_ + 1U) % touch_tracking_id_count; + if (const auto status = emit_event(EV_ABS, ABS_MT_TRACKING_ID, static_cast(tracking_id)); !status.ok()) { return status; } new_slot_ = false; @@ -1789,6 +1792,7 @@ namespace lvh::detail { std::string device_name_; std::map contacts_; int current_slot_ = -1; + std::uint32_t next_tracking_id_ = 0; bool new_slot_ = false; }; diff --git a/tests/fixtures/include/fixtures/linux_backend_test_hooks.hpp b/tests/fixtures/include/fixtures/linux_backend_test_hooks.hpp index 0e00dec..d52fafd 100644 --- a/tests/fixtures/include/fixtures/linux_backend_test_hooks.hpp +++ b/tests/fixtures/include/fixtures/linux_backend_test_hooks.hpp @@ -788,6 +788,14 @@ namespace lvh::detail::test { */ LinuxInputSubmissionResult linux_uinput_trackpad_multi_contact_pipe(); + /** + * @brief Release and replace one of two contacts through a pipe-backed uinput touch device. + * + * @param device_type Touchscreen or trackpad device type. + * @return Submission status and captured input events. + */ + LinuxInputSubmissionResult linux_uinput_touch_contact_reuse_pipe(DeviceType device_type); + /** * @brief Submit invalid touchscreen contacts through pipe-backed devices. * diff --git a/tests/fixtures/linux_backend_test_hooks.cpp b/tests/fixtures/linux_backend_test_hooks.cpp index df31b0e..d9b0f31 100644 --- a/tests/fixtures/linux_backend_test_hooks.cpp +++ b/tests/fixtures/linux_backend_test_hooks.cpp @@ -926,6 +926,33 @@ namespace lvh::detail::test { return create_fake_libevdev_device(DeviceType::gamepad, keep_fake_libevdev_successful, kind); } + template + LinuxInputSubmissionResult touch_contact_reuse_pipe() { + std::array descriptors {-1, -1}; + if (::pipe(descriptors.data()) != 0) { + return {system_error_status(ErrorCode::backend_failure, "failed to create pipe", errno), {}}; + } + + TouchDevice device {descriptors[1]}; + auto status = device.place_contact({.id = 0, .x = 0.1F, .y = 0.1F, .pressure = 0.5F}); + if (status.ok()) { + status = device.place_contact({.id = 1, .x = 0.2F, .y = 0.2F, .pressure = 0.5F}); + } + if (status.ok()) { + status = device.release_contact(0, PointerTransition::release); + } + if (status.ok()) { + status = device.place_contact({.id = 0, .x = 0.3F, .y = 0.3F, .pressure = 0.5F}); + } + if (status.ok()) { + status = device.place_contact({.id = 1, .x = 0.25F, .y = 0.25F, .pressure = 0.5F}); + } + static_cast(device.close()); + auto records = read_input_events_until_eof(descriptors[0]); + static_cast(::close(descriptors[0])); + return {std::move(status), std::move(records)}; + } + } // namespace std::string linux_copy_string_char_buffer(const std::string &source) { @@ -1384,6 +1411,20 @@ namespace lvh::detail::test { return {std::move(status), std::move(records)}; } + LinuxInputSubmissionResult linux_uinput_touch_contact_reuse_pipe(DeviceType device_type) { + switch (device_type) { + case DeviceType::touchscreen: + return touch_contact_reuse_pipe(); + case DeviceType::trackpad: + return touch_contact_reuse_pipe(); + default: + return { + OperationStatus::failure(ErrorCode::invalid_argument, "device type must be touchscreen or trackpad"), + {}, + }; + } + } + OperationStatus linux_uinput_touchscreen_invalid_contacts() { std::array descriptors {-1, -1}; if (::pipe(descriptors.data()) != 0) { diff --git a/tests/unit/test_linux_backend.cpp b/tests/unit/test_linux_backend.cpp index 0c44537..9342c2f 100644 --- a/tests/unit/test_linux_backend.cpp +++ b/tests/unit/test_linux_backend.cpp @@ -759,6 +759,31 @@ TEST_F(LinuxBackendTest, PipeBackedUinputTouchDevicesCoverStateTransitions) { EXPECT_EQ(lvh::detail::test::linux_uinput_pen_tablet_closed_status().code(), lvh::ErrorCode::device_closed); } +TEST_F(LinuxBackendTest, PipeBackedUinputTouchDevicesReuseSlotsWithNewTrackingIds) { + for (const auto device_type : {lvh::DeviceType::touchscreen, lvh::DeviceType::trackpad}) { + const auto result = lvh::detail::test::linux_uinput_touch_contact_reuse_pipe(device_type); + ASSERT_TRUE(result.status.ok()) << result.status.message(); + + std::vector selected_slots; + std::vector tracking_ids; + for (const auto &event : result.events) { + if (event.type == EV_ABS && event.code == ABS_MT_SLOT) { + selected_slots.push_back(event.value); + } else if (event.type == EV_ABS && event.code == ABS_MT_TRACKING_ID) { + tracking_ids.push_back(event.value); + } + } + + EXPECT_EQ(selected_slots, (std::vector {0, 1, 0, 1})); + EXPECT_EQ(tracking_ids, (std::vector {0, 1, -1, 2})); + } + + EXPECT_EQ( + lvh::detail::test::linux_uinput_touch_contact_reuse_pipe(lvh::DeviceType::mouse).status.code(), + lvh::ErrorCode::invalid_argument + ); +} + TEST_F(LinuxBackendTest, SocketpairBackedUhidGamepadRoundTripsEvents) { const auto result = lvh::detail::test::linux_uhid_socketpair_roundtrip(); EXPECT_TRUE(result.create_status.ok()) << result.create_status.message();