Skip to content

Commit 6b06ea9

Browse files
fix(linux): assign unique touch tracking IDs on slot reuse (#87)
1 parent b6f192e commit 6b06ea9

5 files changed

Lines changed: 84 additions & 1 deletion

File tree

docs/platform-support.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,11 @@ the evdev layout used by Linux-native virtual-controller implementations and
157157
allows standard `FF_RUMBLE` effects without emulating the physical controller's
158158
proprietary initialization handshake.
159159

160+
Linux touchscreen and trackpad contacts use the lowest available multitouch
161+
slot while they are active. A newly placed contact receives a new tracking ID,
162+
including when it reuses a slot released by another contact, so replacing one
163+
finger cannot overwrite another active finger in standard evdev consumers.
164+
160165
On descriptor-driven backends, native Switch Pro output reports `0x01` and
161166
`0x10` are decoded into the normalized low- and high-frequency rumble callback.
162167
The original native report remains available in `GamepadOutput::raw_report`.

src/platform/linux/uhid_backend.cpp

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ namespace lvh::detail {
8585
constexpr auto touch_axis_max_x = 19200;
8686
constexpr auto touch_axis_max_y = 10800;
8787
constexpr auto touch_max_contacts = 16;
88+
constexpr std::uint32_t touch_tracking_id_count = 65536U;
8889
constexpr auto touch_pressure_max = 253;
8990
constexpr auto tablet_pressure_max = 4096;
9091
constexpr auto tablet_distance_max = 1024;
@@ -1655,7 +1656,9 @@ namespace lvh::detail {
16551656
return status;
16561657
}
16571658
if (new_slot_) {
1658-
if (const auto status = emit_event(EV_ABS, ABS_MT_TRACKING_ID, *slot); !status.ok()) {
1659+
const auto tracking_id = next_tracking_id_;
1660+
next_tracking_id_ = (next_tracking_id_ + 1U) % touch_tracking_id_count;
1661+
if (const auto status = emit_event(EV_ABS, ABS_MT_TRACKING_ID, static_cast<std::int32_t>(tracking_id)); !status.ok()) {
16591662
return status;
16601663
}
16611664
new_slot_ = false;
@@ -1789,6 +1792,7 @@ namespace lvh::detail {
17891792
std::string device_name_;
17901793
std::map<std::int32_t, int> contacts_;
17911794
int current_slot_ = -1;
1795+
std::uint32_t next_tracking_id_ = 0;
17921796
bool new_slot_ = false;
17931797
};
17941798

tests/fixtures/include/fixtures/linux_backend_test_hooks.hpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -788,6 +788,14 @@ namespace lvh::detail::test {
788788
*/
789789
LinuxInputSubmissionResult linux_uinput_trackpad_multi_contact_pipe();
790790

791+
/**
792+
* @brief Release and replace one of two contacts through a pipe-backed uinput touch device.
793+
*
794+
* @param device_type Touchscreen or trackpad device type.
795+
* @return Submission status and captured input events.
796+
*/
797+
LinuxInputSubmissionResult linux_uinput_touch_contact_reuse_pipe(DeviceType device_type);
798+
791799
/**
792800
* @brief Submit invalid touchscreen contacts through pipe-backed devices.
793801
*

tests/fixtures/linux_backend_test_hooks.cpp

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -926,6 +926,33 @@ namespace lvh::detail::test {
926926
return create_fake_libevdev_device(DeviceType::gamepad, keep_fake_libevdev_successful, kind);
927927
}
928928

929+
template<typename TouchDevice>
930+
LinuxInputSubmissionResult touch_contact_reuse_pipe() {
931+
std::array<int, 2> descriptors {-1, -1};
932+
if (::pipe(descriptors.data()) != 0) {
933+
return {system_error_status(ErrorCode::backend_failure, "failed to create pipe", errno), {}};
934+
}
935+
936+
TouchDevice device {descriptors[1]};
937+
auto status = device.place_contact({.id = 0, .x = 0.1F, .y = 0.1F, .pressure = 0.5F});
938+
if (status.ok()) {
939+
status = device.place_contact({.id = 1, .x = 0.2F, .y = 0.2F, .pressure = 0.5F});
940+
}
941+
if (status.ok()) {
942+
status = device.release_contact(0, PointerTransition::release);
943+
}
944+
if (status.ok()) {
945+
status = device.place_contact({.id = 0, .x = 0.3F, .y = 0.3F, .pressure = 0.5F});
946+
}
947+
if (status.ok()) {
948+
status = device.place_contact({.id = 1, .x = 0.25F, .y = 0.25F, .pressure = 0.5F});
949+
}
950+
static_cast<void>(device.close());
951+
auto records = read_input_events_until_eof(descriptors[0]);
952+
static_cast<void>(::close(descriptors[0]));
953+
return {std::move(status), std::move(records)};
954+
}
955+
929956
} // namespace
930957

931958
std::string linux_copy_string_char_buffer(const std::string &source) {
@@ -1384,6 +1411,20 @@ namespace lvh::detail::test {
13841411
return {std::move(status), std::move(records)};
13851412
}
13861413

1414+
LinuxInputSubmissionResult linux_uinput_touch_contact_reuse_pipe(DeviceType device_type) {
1415+
switch (device_type) {
1416+
case DeviceType::touchscreen:
1417+
return touch_contact_reuse_pipe<UinputTouchscreen>();
1418+
case DeviceType::trackpad:
1419+
return touch_contact_reuse_pipe<UinputTrackpad>();
1420+
default:
1421+
return {
1422+
OperationStatus::failure(ErrorCode::invalid_argument, "device type must be touchscreen or trackpad"),
1423+
{},
1424+
};
1425+
}
1426+
}
1427+
13871428
OperationStatus linux_uinput_touchscreen_invalid_contacts() {
13881429
std::array<int, 2> descriptors {-1, -1};
13891430
if (::pipe(descriptors.data()) != 0) {

tests/unit/test_linux_backend.cpp

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -759,6 +759,31 @@ TEST_F(LinuxBackendTest, PipeBackedUinputTouchDevicesCoverStateTransitions) {
759759
EXPECT_EQ(lvh::detail::test::linux_uinput_pen_tablet_closed_status().code(), lvh::ErrorCode::device_closed);
760760
}
761761

762+
TEST_F(LinuxBackendTest, PipeBackedUinputTouchDevicesReuseSlotsWithNewTrackingIds) {
763+
for (const auto device_type : {lvh::DeviceType::touchscreen, lvh::DeviceType::trackpad}) {
764+
const auto result = lvh::detail::test::linux_uinput_touch_contact_reuse_pipe(device_type);
765+
ASSERT_TRUE(result.status.ok()) << result.status.message();
766+
767+
std::vector<std::int32_t> selected_slots;
768+
std::vector<std::int32_t> tracking_ids;
769+
for (const auto &event : result.events) {
770+
if (event.type == EV_ABS && event.code == ABS_MT_SLOT) {
771+
selected_slots.push_back(event.value);
772+
} else if (event.type == EV_ABS && event.code == ABS_MT_TRACKING_ID) {
773+
tracking_ids.push_back(event.value);
774+
}
775+
}
776+
777+
EXPECT_EQ(selected_slots, (std::vector<std::int32_t> {0, 1, 0, 1}));
778+
EXPECT_EQ(tracking_ids, (std::vector<std::int32_t> {0, 1, -1, 2}));
779+
}
780+
781+
EXPECT_EQ(
782+
lvh::detail::test::linux_uinput_touch_contact_reuse_pipe(lvh::DeviceType::mouse).status.code(),
783+
lvh::ErrorCode::invalid_argument
784+
);
785+
}
786+
762787
TEST_F(LinuxBackendTest, SocketpairBackedUhidGamepadRoundTripsEvents) {
763788
const auto result = lvh::detail::test::linux_uhid_socketpair_roundtrip();
764789
EXPECT_TRUE(result.create_status.ok()) << result.create_status.message();

0 commit comments

Comments
 (0)