Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/platform-support.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
6 changes: 5 additions & 1 deletion src/platform/linux/uhid_backend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<std::int32_t>(tracking_id)); !status.ok()) {
return status;
}
new_slot_ = false;
Expand Down Expand Up @@ -1789,6 +1792,7 @@ namespace lvh::detail {
std::string device_name_;
std::map<std::int32_t, int> contacts_;
int current_slot_ = -1;
std::uint32_t next_tracking_id_ = 0;
bool new_slot_ = false;
};

Expand Down
8 changes: 8 additions & 0 deletions tests/fixtures/include/fixtures/linux_backend_test_hooks.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
41 changes: 41 additions & 0 deletions tests/fixtures/linux_backend_test_hooks.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -926,6 +926,33 @@ namespace lvh::detail::test {
return create_fake_libevdev_device(DeviceType::gamepad, keep_fake_libevdev_successful, kind);
}

template<typename TouchDevice>
LinuxInputSubmissionResult touch_contact_reuse_pipe() {
std::array<int, 2> 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<void>(device.close());
auto records = read_input_events_until_eof(descriptors[0]);
static_cast<void>(::close(descriptors[0]));
return {std::move(status), std::move(records)};
}

} // namespace

std::string linux_copy_string_char_buffer(const std::string &source) {
Expand Down Expand Up @@ -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<UinputTouchscreen>();
case DeviceType::trackpad:
return touch_contact_reuse_pipe<UinputTrackpad>();
default:
return {
OperationStatus::failure(ErrorCode::invalid_argument, "device type must be touchscreen or trackpad"),
{},
};
}
}

OperationStatus linux_uinput_touchscreen_invalid_contacts() {
std::array<int, 2> descriptors {-1, -1};
if (::pipe(descriptors.data()) != 0) {
Expand Down
25 changes: 25 additions & 0 deletions tests/unit/test_linux_backend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::int32_t> selected_slots;
std::vector<std::int32_t> 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<std::int32_t> {0, 1, 0, 1}));
EXPECT_EQ(tracking_ids, (std::vector<std::int32_t> {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();
Expand Down
Loading