* Re: [PATCH] HID: multitouch: Fix Yoga Book 9 14IAH10 touchscreen misclassification
2026-04-02 18:29 [PATCH] HID: multitouch: Fix Yoga Book 9 14IAH10 touchscreen misclassification Dave Carey
@ 2026-04-03 13:02 ` Benjamin Tissoires
[not found] ` <CALPvROSB4y0UsPvF5-ZS=_rGmj1NgM6QvBAbHO13bkgpAwQSyA@mail.gmail.com>
2026-04-13 12:58 ` [PATCH v2] " Dave Carey
1 sibling, 1 reply; 4+ messages in thread
From: Benjamin Tissoires @ 2026-04-03 13:02 UTC (permalink / raw)
To: Dave Carey; +Cc: jikos, linux-input, linux-kernel
Hi Dave,
On Apr 02 2026, Dave Carey wrote:
> The Lenovo Yoga Book 9 14IAH10 (83KJ) uses a composite USB HID device
> (17EF:6161) where three descriptor quirks combine to cause hid-multitouch
> to incorrectly set INPUT_PROP_BUTTONPAD on both touchscreen nodes, making
> libinput treat them as indirect clickpads rather than direct touchscreens.
>
> Quirk 1: The HID_DG_TOUCHSCREEN application collection contains
> HID_UP_BUTTON usages (stylus barrel buttons). The generic heuristic in
> mt_touch_input_mapping() treats any touchscreen-with-buttons as a
> touchpad, setting INPUT_MT_POINTER.
>
> Quirk 2: A HID_DG_TOUCHPAD collection ("Emulated Touchpad") sets
> INPUT_MT_POINTER unconditionally in mt_allocate_application().
>
> Quirk 3: The HID_DG_BUTTONTYPE feature report (0x51) returns
> MT_BUTTONTYPE_CLICKPAD, directly setting td->is_buttonpad = true.
>
> These combine to produce INPUT_PROP_BUTTONPAD on the touchscreen input
> nodes. libinput treats the devices as indirect clickpads and suppresses
> direct touch events, leaving the touchscreens non-functional under
> KDE/Wayland.
This looks like a completely borked report descriptor. Out of curiosity,
do you know if there is a specific Windows driver for it or if it's
using the plain generic driver there.
The reasoning is that if it's using the generic win driver, we are
probably doing something wrong, and we need to fix it in a more generic
way.
>
> Additionally, the firmware resets if any USB control request is received
> during the CDC ACM initialization window. The existing GET_REPORT call
> in mt_check_input_mode() during probe triggers this reset.
Ouch, even better :(
>
> Fix by extending MT_QUIRK_YOGABOOK9I (already defined for the earlier
> Yoga Book 9i) to guard all three BUTTONPAD heuristics and skip the
> HID_DG_BUTTONTYPE GET_REPORT during probe for this device.
Really not a big fan of the approach taken here: We are sprinkling the
code with special quirks for one particular device and that makes
everything worse.
I would much prefer a report descriptor fixup where:
- we drop the HID_UP_BUTTON
- we drop the HID_DG_TOUCHPAD collection entirely
- we drop the HID_DG_BUTTONTYPE feature entirely
- we drop the Win8 blob feature as well to prevent queries during
initialization.
For ease of development I would recomend working with a separate HID-BPF
program instead of a in-kernel fix, but we already have a .report_fixup
here, so I wouldn't mind having the fix here as well.
Cheers,
Benjamin
>
> Signed-off-by: Dave Carey <carvsdriver@gmail.com>
> Tested-by: Dave Carey <carvsdriver@gmail.com>
> ---
> drivers/hid/hid-multitouch.c | 34 +++++++++++++++++++++++++++-------
> 1 file changed, 27 insertions(+), 7 deletions(-)
>
> diff --git a/drivers/hid/hid-multitouch.c b/drivers/hid/hid-multitouch.c
> index e82a3c4e5..1bef32b1d 100644
> --- a/drivers/hid/hid-multitouch.c
> +++ b/drivers/hid/hid-multitouch.c
> @@ -549,7 +549,14 @@ static void mt_feature_mapping(struct hid_device *hdev,
>
> switch (usage->hid) {
> case HID_DG_CONTACTMAX:
> - mt_get_feature(hdev, field->report);
> + /*
> + * Yoga Book 9: skip GET_REPORT during probe; the firmware
> + * resets if it receives any control request before the init
> + * Output report is sent (within ~1.18s of USB enumeration).
> + * Logical maximum from the descriptor is used as the fallback.
> + */
> + if (!(td->mtclass.quirks & MT_QUIRK_YOGABOOK9I))
> + mt_get_feature(hdev, field->report);
>
> td->maxcontacts = field->value[0];
> if (!td->maxcontacts &&
> @@ -566,6 +573,10 @@ static void mt_feature_mapping(struct hid_device *hdev,
> break;
> }
>
> + /* Yoga Book 9 reports Clickpad but is a direct touchscreen */
> + if (td->mtclass.quirks & MT_QUIRK_YOGABOOK9I)
> + break;
> +
> mt_get_feature(hdev, field->report);
> switch (field->value[usage->usage_index]) {
> case MT_BUTTONTYPE_CLICKPAD:
> @@ -579,7 +590,9 @@ static void mt_feature_mapping(struct hid_device *hdev,
> break;
> case 0xff0000c5:
> /* Retrieve the Win8 blob once to enable some devices */
> - if (usage->usage_index == 0)
> + /* Yoga Book 9: skip; firmware resets before init if queried */
> + if (usage->usage_index == 0 &&
> + !(td->mtclass.quirks & MT_QUIRK_YOGABOOK9I))
> mt_get_feature(hdev, field->report);
> break;
> }
> @@ -644,8 +657,11 @@ static struct mt_application *mt_allocate_application(struct mt_device *td,
>
> /*
> * Model touchscreens providing buttons as touchpads.
> + * Yoga Book 9 has an emulated touchpad but its touch surfaces
> + * are direct screens, not indirect pointers.
> */
> - if (application == HID_DG_TOUCHPAD) {
> + if (application == HID_DG_TOUCHPAD &&
> + !(td->mtclass.quirks & MT_QUIRK_YOGABOOK9I)) {
> mt_application->mt_flags |= INPUT_MT_POINTER;
> td->inputmode_value = MT_INPUTMODE_TOUCHPAD;
> }
> @@ -802,11 +818,15 @@ static int mt_touch_input_mapping(struct hid_device *hdev, struct hid_input *hi,
>
> /*
> * Model touchscreens providing buttons as touchpads.
> + * Skip for Yoga Book 9 which has stylus buttons inside
> + * touchscreen collections, not physical touchpad buttons.
> */
> if (field->application == HID_DG_TOUCHSCREEN &&
> (usage->hid & HID_USAGE_PAGE) == HID_UP_BUTTON) {
> - app->mt_flags |= INPUT_MT_POINTER;
> - td->inputmode_value = MT_INPUTMODE_TOUCHPAD;
> + if (!(app->quirks & MT_QUIRK_YOGABOOK9I)) {
> + app->mt_flags |= INPUT_MT_POINTER;
> + td->inputmode_value = MT_INPUTMODE_TOUCHPAD;
> + }
> }
>
> /* count the buttons on touchpads */
> @@ -1420,7 +1440,6 @@ static int mt_touch_input_configured(struct hid_device *hdev,
> */
> if (cls->quirks & MT_QUIRK_APPLE_TOUCHBAR)
> app->mt_flags |= INPUT_MT_DIRECT;
> -
> if (cls->is_indirect)
> app->mt_flags |= INPUT_MT_POINTER;
>
> @@ -1432,7 +1451,8 @@ static int mt_touch_input_configured(struct hid_device *hdev,
>
> /* check for clickpads */
> if ((app->mt_flags & INPUT_MT_POINTER) &&
> - (app->buttons_count == 1))
> + (app->buttons_count == 1) &&
> + !(app->quirks & MT_QUIRK_YOGABOOK9I))
> td->is_buttonpad = true;
>
> if (td->is_buttonpad)
> --
> 2.53.0
>
>
^ permalink raw reply [flat|nested] 4+ messages in thread* [PATCH v2] HID: multitouch: Fix Yoga Book 9 14IAH10 touchscreen misclassification
2026-04-02 18:29 [PATCH] HID: multitouch: Fix Yoga Book 9 14IAH10 touchscreen misclassification Dave Carey
2026-04-03 13:02 ` Benjamin Tissoires
@ 2026-04-13 12:58 ` Dave Carey
1 sibling, 0 replies; 4+ messages in thread
From: Dave Carey @ 2026-04-13 12:58 UTC (permalink / raw)
To: linux-input; +Cc: jikos, bentiss, Dave Carey
The Lenovo Yoga Book 9 14IAH10 (83KJ) (17EF:6161) firmware includes a
HID_DG_TOUCHPAD application collection designed for the Windows inbox HID
driver's Win8 PTP touchpad mode. On Linux the HID_DG_TOUCHSCREEN
collections provide the correct direct-touch interface. The presence of
the touchpad collection causes hid-multitouch to misclassify the
touchscreen nodes as indirect buttonpads, leaving them non-functional.
Within the touchpad collection:
- HID_UP_BUTTON usages trigger the touchscreen-with-buttons heuristic
that sets INPUT_MT_POINTER on the touchscreen applications.
- The HID_DG_TOUCHPAD application itself sets INPUT_MT_POINTER via
mt_allocate_application(), propagating to all touchscreen nodes.
- A HID_DG_BUTTONTYPE feature (report 0x51) returns MT_BUTTONTYPE_CLICKPAD,
setting td->is_buttonpad = true for the entire device.
Additionally, the firmware resets if any USB control request arrives while
the CDC-ACM interface is initialising (~1.18 s after enumeration).
The Win8 compliance blob (0xff00:0xc5) and Contact Count Max feature
reports in the touchscreen collections trigger GET_REPORT calls at probe
that hit this window. Surface Switch (0x57) and Button Switch (0x58)
feature reports are sent by mt_set_modes() on every input-device open and
close, repeatedly hitting this window throughout device lifetime.
The firmware also leaves a persistent ghost contact in its contact buffer
(contact ID 2, fixed coordinates, tip always asserted) on every enumeration.
This ghost occupies a multitouch slot and prevents KWin from seeing a clean
finger-lift, causing stuck touch state. The ghost is cleared when Input
Mode is set via HID_REQ_SET_REPORT at probe.
Fix using a report descriptor fixup in mt_report_fixup() and a class
definition update:
1. Remove the entire HID_DG_TOUCHPAD application collection. Parsing
HID short items from its header to the matching End Collection and
closing the gap with memmove eliminates all three BUTTONPAD
heuristics and the feature reports within the collection.
2. Neutralize the Win8 compliance blob feature reports remaining in the
touchscreen collections by changing Usage Page 0xff00 to 0x0f00,
preventing the case 0xff0000c5 branch in mt_feature_mapping() from
issuing GET_REPORT.
3. Neutralize the Contact Count Max feature reports by changing usage
0x55 to 0x00; set maxcontacts = 10 in the class definition so the
driver uses the correct contact limit without querying the device.
4. Neutralize Surface Switch (0x57) and Button Switch (0x58) feature
report usages in the Device Configuration collection so mt_set_modes()
does not issue HID_REQ_SET_REPORT for these on every input-device
open/close. Input Mode (0x52) is intentionally left intact: the single
HID_REQ_SET_REPORT at probe flushes the firmware's contact buffer and
clears the persistent ghost contact. By probe time the cdc-acm driver
has already satisfied the CDC-ACM init watchdog (~130 ms), so this
request arrives safely after the reset window has closed.
5. Add MT_QUIRK_NOT_SEEN_MEANS_UP to the MT_CLS_YOGABOOK9I class so that
contacts not present in a frame are released via INPUT_MT_DROP_UNUSED,
preventing stale multitouch slots from lingering if the firmware omits
a contact from a report.
Signed-off-by: Dave Carey <carvsdriver@gmail.com>
Tested-by: Dave Carey <carvsdriver@gmail.com>
---
Re: Benjamin's question about the Windows driver — the device uses
Windows' generic inbox drivers: usbser.sys binds interface 0 (CDC ACM)
and the generic HID class driver handles the HID interfaces. The
HID_DG_TOUCHPAD collection is in the descriptor for Windows' PTP inbox
touchpad path, not for a custom driver. On Windows the system routes
input through the touchpad application; on Linux hid-multitouch sees
both the touchscreen and touchpad applications and gets confused by the
touchpad one. The descriptor fixup removes it from Linux's view.
Changes in v2:
- Replace per-callsite MT_QUIRK_YOGABOOK9I guards with a single
mt_yogabook9_fixup() function called from mt_report_fixup().
- Drop the HID_DG_TOUCHPAD application collection entirely via memmove,
eliminating all three BUTTONPAD heuristics at source rather than
suppressing their effects at each callsite.
- Neutralize Win8 compliance blob GET_REPORT triggers by changing
Usage Page 0xff00 to 0x0f00 in the descriptor.
- Neutralize Contact Count Max GET_REPORT trigger (usage 0x55 -> 0x00);
set maxcontacts = 10 in the class definition.
- Neutralize Surface Switch (0x57) and Button Switch (0x58) SET_REPORT
triggers; retain Input Mode (0x52) so the single probe-time SET_REPORT
flushes the firmware contact buffer and clears a persistent ghost contact.
- Add MT_QUIRK_NOT_SEEN_MEANS_UP to the MT_CLS_YOGABOOK9I class.
drivers/hid/hid-multitouch.c | 146 ++++++++++++++++++++++++++++++++++-
1 file changed, 145 insertions(+), 1 deletion(-)
diff --git a/drivers/hid/hid-multitouch.c b/drivers/hid/hid-multitouch.c
index e82a3c4e5..ec04dbafb 100644
--- a/drivers/hid/hid-multitouch.c
+++ b/drivers/hid/hid-multitouch.c
@@ -443,11 +443,13 @@ static const struct mt_class mt_classes[] = {
MT_QUIRK_CONTACT_CNT_ACCURATE,
},
{ .name = MT_CLS_YOGABOOK9I,
- .quirks = MT_QUIRK_ALWAYS_VALID |
+ .quirks = MT_QUIRK_NOT_SEEN_MEANS_UP |
+ MT_QUIRK_ALWAYS_VALID |
MT_QUIRK_FORCE_MULTI_INPUT |
MT_QUIRK_SEPARATE_APP_REPORT |
MT_QUIRK_HOVERING |
MT_QUIRK_YOGABOOK9I,
+ .maxcontacts = 10,
.export_all_inputs = true
},
{ .name = MT_CLS_EGALAX_P80H84,
@@ -1566,6 +1568,144 @@ static int mt_event(struct hid_device *hid, struct hid_field *field,
return 0;
}
+/*
+ * Yoga Book 9 14IAH10 descriptor fixup.
+ *
+ * The device includes a HID_DG_TOUCHPAD application collection designed for
+ * the Windows inbox HID driver's Win8 PTP touchpad mode. On Linux we want
+ * only the HID_DG_TOUCHSCREEN collections. The touchpad collection (and the
+ * HID_DG_BUTTONTYPE and Win8 compliance blob features it contains) must be
+ * removed so hid-multitouch does not misclassify the touchscreen nodes as
+ * indirect buttonpads.
+ *
+ * The firmware also resets if any USB control request is received while the
+ * CDC-ACM interface is initialising (~1.18 s after enumeration). Dropping
+ * the Win8 blob and Contact Count Max feature reports prevents the
+ * GET_REPORT calls that hid-multitouch issues at probe.
+ */
+static void mt_yogabook9_fixup(struct hid_device *hdev, __u8 *rdesc,
+ unsigned int *size)
+{
+ /* Usage Page (Digitizer), Usage (Touch Pad), Collection (Application) */
+ static const __u8 tp_app_hdr[] = { 0x05, 0x0d, 0x09, 0x05, 0xa1, 0x01 };
+ /* Vendor Usage Page 0xff00 (Win8 compliance blob header) */
+ static const __u8 win8_page[] = { 0x06, 0x00, 0xff };
+ /* Usage (Contact Count Max = 0x55) */
+ static const __u8 ccmax_usage[] = { 0x09, 0x55 };
+ unsigned int i;
+
+ /*
+ * Step 1: find and remove the Touch Pad application collection.
+ * Walk HID short items from the collection header to its matching
+ * End Collection, then close the gap with memmove.
+ */
+ for (i = 0; i + sizeof(tp_app_hdr) <= *size; i++) {
+ if (memcmp(rdesc + i, tp_app_hdr, sizeof(tp_app_hdr)) == 0) {
+ __u8 *start = rdesc + i;
+ __u8 *coll_end = NULL;
+ __u8 *p = start;
+ unsigned int drop;
+ int depth = 0;
+
+ while (p < rdesc + *size) {
+ __u8 b = *p;
+ int ds = b & 3;
+ int item_len;
+
+ if (b == 0xfe) { /* long item */
+ if (p + 2 >= rdesc + *size)
+ break;
+ item_len = p[1] + 3;
+ } else {
+ item_len = (ds == 3) ? 5 : ds + 1;
+ }
+ if (p + item_len > rdesc + *size)
+ break;
+
+ if ((b & 0xfc) == 0xa0)
+ depth++; /* Collection */
+ else if (b == 0xc0) {
+ depth--; /* End Collection */
+ if (depth == 0) {
+ coll_end = p;
+ break;
+ }
+ }
+ p += item_len;
+ }
+
+ if (!coll_end) {
+ hid_err(hdev,
+ "Yoga Book 9: Touch Pad End Collection not found\n");
+ break;
+ }
+
+ drop = coll_end - start + 1;
+ memmove(start, coll_end + 1, rdesc + *size - coll_end - 1);
+ *size -= drop;
+ hid_dbg(hdev,
+ "Yoga Book 9: dropped Touch Pad collection (%u bytes)\n",
+ drop);
+ break;
+ }
+ }
+
+ /*
+ * Step 2: neutralize Win8 compliance blob feature reports remaining
+ * in the touchscreen collections. Change Usage Page 0xff00 to 0x0f00
+ * so the case 0xff0000c5 branch in mt_feature_mapping() is not reached
+ * and no GET_REPORT is issued.
+ */
+ for (i = 0; i + sizeof(win8_page) <= *size; i++) {
+ if (memcmp(rdesc + i, win8_page, sizeof(win8_page)) == 0) {
+ rdesc[i + 2] = 0x0f; /* 0xff00 -> 0x0f00 */
+ hid_dbg(hdev,
+ "Yoga Book 9: neutralized Win8 blob at offset %u\n",
+ i);
+ }
+ }
+
+ /*
+ * Step 3: neutralize Contact Count Max feature reports. Change usage
+ * 0x55 (HID_DG_CONTACTMAX) to 0x00 so mt_feature_mapping() does not
+ * issue GET_REPORT. The class maxcontacts field provides the value.
+ */
+ for (i = 0; i + sizeof(ccmax_usage) <= *size; i++) {
+ if (memcmp(rdesc + i, ccmax_usage, sizeof(ccmax_usage)) == 0) {
+ rdesc[i + 1] = 0x00;
+ hid_dbg(hdev,
+ "Yoga Book 9: neutralized ContactMax at offset %u\n",
+ i);
+ }
+ }
+
+ /*
+ * Step 4: neutralize Surface Switch (0x57) and Button Switch (0x58)
+ * feature report usages in the Device Configuration collection.
+ * mt_set_modes() issues HID_REQ_SET_REPORT for these on every
+ * input-device open/close; those repeated control requests hit the
+ * firmware's CDC-ACM init window and trigger resets.
+ *
+ * Input Mode (0x52) is intentionally left intact. mt_set_modes()
+ * sends it once at probe to set the device into touchscreen mode,
+ * which flushes the firmware's contact buffer and clears a persistent
+ * ghost contact (cid 2, fixed coordinates) that otherwise appears on
+ * every enumeration. By probe time cdc_acm has already satisfied the
+ * CDC-ACM init watchdog (~130 ms), so the single SET_REPORT for Input
+ * Mode arrives safely after the reset window has closed.
+ */
+ for (i = 0; i + 2 <= *size; i++) {
+ if (rdesc[i] == 0x09 &&
+ (rdesc[i + 1] == 0x57 ||
+ rdesc[i + 1] == 0x58)) {
+ hid_dbg(hdev,
+ "Yoga Book 9: neutralized set-modes usage 0x%02x at offset %u\n",
+ rdesc[i + 1], i);
+ rdesc[i + 1] = 0x00;
+ }
+ }
+}
+
static const __u8 *mt_report_fixup(struct hid_device *hdev, __u8 *rdesc,
unsigned int *size)
{
@@ -1595,6 +1735,10 @@ got: %x\n",
}
}
+ if (hdev->vendor == USB_VENDOR_ID_LENOVO &&
+ hdev->product == USB_DEVICE_ID_LENOVO_YOGABOOK9I)
+ mt_yogabook9_fixup(hdev, rdesc, size);
+
return rdesc;
}
base-commit: 705c735d0ef7701cf9ded290545345a8c9b8bd7e
--
2.53.0
^ permalink raw reply related [flat|nested] 4+ messages in thread