From: Ulf Brosziewski Subject: pms/synaptics: Improve the handling of multitouch input To: "tech@openbsd.org" Date: Sun, 11 Oct 2026 15:00:17 +0200 Many, or most, Synaptics touchpads offer a variant of their PS/2 mode that can report the coordinates of two touches. With this patch, which is a rewrite and extension of a proposal from Marc Lasch, pms enables that mode. If used with the ws driver in X, it provides MT-support, in a somewhat limited form. Some changes that might interest some users are: - On clickpads, click-and-drag inputs with two fingers work independently of the order of touches. - If a bottom area is configured - which is a default for clickpads - a finger (thumb) that is resting in that area doesn't block pointer movement, tapping inputs and two-finger-scrolling. - For clickpads reporting an appropriate size, the "mtbuttons" feature is available. - The MT-filter for two-finger scrolling is stricter, which may help to avoid unintended scrolling. The fingers must move, very roughly, in parallel. (This does not apply if there is an additional touch in the bottom area.) After a three-finger touch, the tracking function of wsmouse may misidentify MT-slots because of the incomplete inputs. That's no problem with respect to the pointer if the contact count is kept in sync, but the driver could, in turn, misidentify "thumb contacts". The patch includes a modification of wstpad.c. It checks and updates the relevant state continuously, which avoids that problem. This change affects all MT- touchpads, I think it's a better and cleaner method anyway. Many tests - ideally with reports including the pms line of the dmesg - and OKs would be welcome. Index: dev/pckbc/pms.c =================================================================== RCS file: /cvs/src/sys/dev/pckbc/pms.c,v retrieving revision 1.102 diff -u -p -r1.102 pms.c --- dev/pckbc/pms.c 15 Jul 2025 13:40:02 -0000 1.102 +++ dev/pckbc/pms.c 11 Oct 2026 12:41:44 -0000 @@ -98,6 +98,10 @@ struct synaptics_softc { u_int sec_buttons; + int extw; + int lastw; + struct mtpoint mtpoints[2]; + #define SYNAPTICS_PRESSURE_HI 30 #define SYNAPTICS_PRESSURE_LO 25 #define SYNAPTICS_PRESSURE SYNAPTICS_PRESSURE_HI @@ -1067,11 +1071,15 @@ synaptics_get_hwinfo(struct pms_softc *s hw->hw_type = (syn->ext_capabilities & SYNAPTICS_EXT_CAP_CLICKPAD) ? WSMOUSEHW_CLICKPAD : WSMOUSEHW_TOUCHPAD; + if (syn->ext_capabilities & SYNAPTICS_EXT_CAP_REPORTS_V) { + hw->mt_slots = 2; + hw->flags |= WSMOUSEHW_MT_TRACKING; + } + if (resolution & SYNAPTICS_RESOLUTION_VALID) { hw->h_res = SYNAPTICS_RESOLUTION_X(resolution); hw->v_res = SYNAPTICS_RESOLUTION_Y(resolution); } - hw->x_min = (min_coords ? SYNAPTICS_X_LIMIT(min_coords) : SYNAPTICS_XMIN_BEZEL); hw->y_min = (min_coords ? @@ -1210,15 +1218,20 @@ pms_enable_synaptics(struct pms_softc *s } /* - * Enable absolute mode, plain W-mode and "advanced gesture mode" - * (AGM), if possible. AGM, which seems to be a prerequisite for the - * extended W-mode, might not always be necessary here, but at least - * some older Synaptics models do not report finger counts without it. + * Enable absolute mode, W-mode and "advanced gesture mode" (AGM), + * if possible. Extended W-mode is enabled for models with the + * REPORTS_V capability. The packet handler ignores the V bits, + * but that capability should imply that EW-mode packets report + * "secondary" touches reliably enough for handling them as + * multitouch inputs. */ mode = SYNAPTICS_ABSOLUTE_MODE | SYNAPTICS_HIGH_RATE; - if (syn->capabilities & SYNAPTICS_CAP_EXTENDED) + if (syn->capabilities & SYNAPTICS_CAP_EXTENDED) { mode |= SYNAPTICS_W_MODE; - else if (SYNAPTICS_ID_MAJOR(syn->identify) >= 4) + /* For EW-mode, add the DISABLE_GESTURE bit. */ + if (syn->ext_capabilities & SYNAPTICS_EXT_CAP_REPORTS_V) + mode |= SYNAPTICS_DISABLE_GESTURE; + } else if (SYNAPTICS_ID_MAJOR(syn->identify) >= 4) mode |= SYNAPTICS_DISABLE_GESTURE; if (synaptics_set_mode(sc, mode, 0)) goto err; @@ -1294,7 +1307,7 @@ pms_proc_synaptics(struct pms_softc *sc) { struct synaptics_softc *syn = sc->synaptics; u_int buttons; - int x, y, z, w, fingerwidth; + int x, y, z, w, fingerwidth, pktcode; w = ((sc->packet[0] & 0x30) >> 2) | ((sc->packet[0] & 0x04) >> 1) | ((sc->packet[3] & 0x04) >> 2); @@ -1322,8 +1335,22 @@ pms_proc_synaptics(struct pms_softc *sc) if ((sc->sc_dev_enable & PMS_DEV_PRIMARY) == 0) return; - if (w == 2) - return; /* EW-mode packets are not expected here. */ + if (w == 2) { + pktcode = (sc->packet[5] & 0x30) >> 4; + if (pktcode == 1) { /* "secondary finger packet" */ + syn->mtpoints[1].x = ((sc->packet[4] & 0x0f) << 9) | + (sc->packet[1] << 1); + syn->mtpoints[1].y = ((sc->packet[4] & 0xf0) << 5) | + (sc->packet[2] << 1); + syn->mtpoints[1].pressure = ((sc->packet[3] & 0x30) | + (sc->packet[5] & 0x0f)) << 1; + } else if (pktcode == 2) { /* "finger state information" */ + /* extw-reporting starts with (extw >= 4), and + stops with a packet that reports (extw < 4). */ + syn->extw = sc->packet[1] & 0x0f; + } + return; + } x = ((sc->packet[3] & 0x10) << 8) | ((sc->packet[1] & 0x0f) << 8) | sc->packet[4]; @@ -1388,7 +1415,39 @@ pms_proc_synaptics(struct pms_softc *sc) w = 0; } wsmouse_set(sc->sc_wsmousedev, WSMOUSE_TOUCH_WIDTH, fingerwidth, 0); - WSMOUSE_TOUCH(sc->sc_wsmousedev, buttons, x, y, z, w); + + if (syn->ext_capabilities & SYNAPTICS_EXT_CAP_REPORTS_V) { + if (w) { + syn->mtpoints[0].x = x; + syn->mtpoints[0].y = y; + syn->mtpoints[0].pressure = z; + } + if (syn->extw) { + if (w < 3) + syn->extw = 0; + else + w = syn->extw; + } + /* + * Transitions to or from contacts with three or more fingers + * may change the pair of tracked touches and make the tracking + * state inconsistent, but they do not affect the pointer if + * wsmouse registers the changes of the contact count. + * With (w > 1), do nothing if a touch has just been released; + * it can happen that the EW coordinates are not in sync. + */ + if ((w || syn->lastw) && !(w > 1 && w < syn->lastw)) { + wsmouse_mtframe(sc->sc_wsmousedev, syn->mtpoints, + min(w, 2)); + wsmouse_set(sc->sc_wsmousedev, WSMOUSE_MT_COUNT, w, 0); + } + syn->lastw = w; + } else { + wsmouse_position(sc->sc_wsmousedev, x, y); + wsmouse_touch(sc->sc_wsmousedev, z, w); + } + wsmouse_buttons(sc->sc_wsmousedev, buttons); + wsmouse_input_sync(sc->sc_wsmousedev); } void Index: dev/wscons/wsmouse.c =================================================================== RCS file: /cvs/src/sys/dev/wscons/wsmouse.c,v retrieving revision 1.76 diff -u -p -r1.76 wsmouse.c --- dev/wscons/wsmouse.c 18 Jul 2025 17:34:29 -0000 1.76 +++ dev/wscons/wsmouse.c 11 Oct 2026 12:41:44 -0000 @@ -777,6 +777,9 @@ wsmouse_set(struct device *sc, enum wsmo input->touch.sync |= SYNC_TOUCH_WIDTH; } return; + case WSMOUSE_MT_COUNT: + input->mt.mt_count = value; + return; case WSMOUSE_MT_REL_X: value += mts->pos.x; /* fall through */ case WSMOUSE_MT_ABS_X: @@ -947,7 +950,7 @@ wsmouse_mt_convert(struct device *sc) pressure = 0; } - wsmouse_touch(sc, pressure, mt->num_touches); + wsmouse_touch(sc, pressure, max(mt->num_touches, mt->mt_count)); } void Index: dev/wscons/wsmouseinput.h =================================================================== RCS file: /cvs/src/sys/dev/wscons/wsmouseinput.h,v retrieving revision 1.15 diff -u -p -r1.15 wsmouseinput.h --- dev/wscons/wsmouseinput.h 21 Mar 2021 16:20:49 -0000 1.15 +++ dev/wscons/wsmouseinput.h 11 Oct 2026 12:41:44 -0000 @@ -90,6 +90,10 @@ struct mt_state { int num_touches; + /* If set, this number includes touches + not reported by wsmouse_mtstate(). */ + int mt_count; + /* pointer control */ u_int ptr; u_int ptr_cycle; Index: dev/wscons/wsmousevar.h =================================================================== RCS file: /cvs/src/sys/dev/wscons/wsmousevar.h,v retrieving revision 1.15 diff -u -p -r1.15 wsmousevar.h --- dev/wscons/wsmousevar.h 18 Jun 2017 13:21:48 -0000 1.15 +++ dev/wscons/wsmousevar.h 11 Oct 2026 12:41:45 -0000 @@ -135,6 +135,7 @@ enum wsmouseval { WSMOUSE_PRESSURE, WSMOUSE_CONTACTS, WSMOUSE_TOUCH_WIDTH, + WSMOUSE_MT_COUNT, WSMOUSE_MT_REL_X, WSMOUSE_MT_ABS_X, WSMOUSE_MT_REL_Y, Index: dev/wscons/wstpad.c =================================================================== RCS file: /cvs/src/sys/dev/wscons/wstpad.c,v retrieving revision 1.35 diff -u -p -r1.35 wstpad.c --- dev/wscons/wstpad.c 30 Jan 2025 08:53:29 -0000 1.35 +++ dev/wscons/wstpad.c 11 Oct 2026 12:41:45 -0000 @@ -104,7 +104,6 @@ enum tpad_cmd { #define R_EDGE (1 << 1) #define T_EDGE (1 << 2) #define B_EDGE (1 << 3) -#define THUMB (1 << 4) #define EDGES (L_EDGE | R_EDGE | T_EDGE | B_EDGE) @@ -115,6 +114,13 @@ enum tpad_cmd { */ #define CENTERED(t) (((t)->flags & (L_EDGE | R_EDGE | T_EDGE)) == 0) +/* + * On MT-touchpads, two-finger scrolling requires that the touches are + * moving in parallel; there is one exception for pms/synaptics. + */ +#define F2SCROLL_FILTER(input) \ + ((input)->mt.num_slots > 2 || (input)->tp->ignore == 0) + enum touchstates { TOUCH_NONE, TOUCH_BEGIN, @@ -544,7 +550,7 @@ wstpad_f2scroll(struct wsmouseinput *inp if (dx || dy) { centered = CENTERED(tp->t); - if (IS_MT(tp)) { + if (IS_MT(tp) && F2SCROLL_FILTER(input)) { t2 = get_2nd_touch(input); if (t2 == NULL) return; @@ -1102,25 +1108,21 @@ wstpad_mt_inputs(struct wsmouseinput *in } /* - * Identify "thumb" contacts in the bottom area. The identification - * has three stages: - * 1. If exactly one of two or more touches is in the bottom area, it + * Identify "thumb" contacts in the bottom area. + * + * If exactly one of two or more touches is in the bottom area, it * is masked, which means it does not receive pointer control as long - * as there are alternatives. Once set, the mask will only be cleared - * when the touch is released. - * Tap detection ignores a masked touch if it does not participate in - * a tap gesture. - * 2. If the pointer-controlling touch is moving stably while a masked - * touch in the bottom area is resting, or only moving minimally, the - * pointer mask is copied to tp->ignore. In this stage, the masked - * touch does not block pointer movement, and it is ignored by - * wstpad_f2scroll(). - * Decisions are made more or less immediately, there may be errors - * in edge cases. If a fast or long upward movement is detected, - * tp->ignore is cleared. There is no other transition from stage 2 - * to scrolling, or vice versa, for a pair of touches. - * 3. If tp->ignore is set and the touch is resting, it is marked as - * thumb, and it will be ignored until it ends. + * as there are alternatives. Tap detection ignores a masked touch + * if it does not participate in a tap gesture. + + * If the pointer-controlling touch is moving stably while a masked + * touch in the bottom area is resting or only moving minimally, the + * pointer mask is copied to tp->ignore. In this state, the masked + * touch does not block pointer movement and two-finger scrolling. + * + * The 'ignore' mask is cleared if the touch is moving with a medium + * or higher speed. Both masks are cleared if it leaves the bottom + * area. */ void wstpad_mt_masks(struct wsmouseinput *input) @@ -1131,51 +1133,42 @@ wstpad_mt_masks(struct wsmouseinput *inp u_int mask; int slot; - tp->ignore &= input->mt.touches; + tp->ignore &= input->mt.ptr_mask; - if (tp->contacts < 2) + if (tp->edge.bottom == INT_MIN) return; - if (tp->ignore) { - slot = ffs(tp->ignore) - 1; + if (input->mt.ptr_mask) { + slot = ffs(input->mt.ptr_mask) - 1; t = &tp->tpad_touches[slot]; - if (t->flags & THUMB) - return; - if (t->dir < 0 && wstpad_is_stable(input, t)) { - t->flags |= THUMB; - return; - } /* The edge.low area is a bit larger than the bottom area. */ - if (t->y >= tp->edge.low || (NORTH(t->dir) && - magnitude(input, t->pos->dx, t->pos->dy) >= MAG_MEDIUM)) - tp->ignore = 0; + if (t->y >= tp->edge.low) + tp->ignore = (input->mt.ptr_mask = 0); + } + + if (tp->ignore && t->dir >= 0 && + magnitude(input, t->pos->dx, t->pos->dy) >= MAG_MEDIUM) { + tp->ignore = 0; + t->flags &= ~B_EDGE; return; } + if (tp->contacts < 2) + return; + if (input->mt.ptr_mask == 0) { mask = ~0; FOREACHBIT(input->mt.touches, slot) { t = &tp->tpad_touches[slot]; - if (t->flags & B_EDGE) { + if (t->y <= tp->edge.bottom) { mask &= (1 << slot); input->mt.ptr_mask = mask; } } } - if ((input->mt.ptr_mask & ~input->mt.ptr) - && !(tp->scroll.dz || tp->scroll.dw) - && tp->t->dir >= 0 - && wstpad_is_stable(input, tp->t)) { - - slot = ffs(input->mt.ptr_mask) - 1; - t = &tp->tpad_touches[slot]; - - if (t->y >= tp->edge.low) - return; - - if (!wstpad_is_stable(input, t)) - return; + if (tp->ignore == 0 && (input->mt.ptr_mask & ~input->mt.ptr) && + tp->t->dir >= 0 && wstpad_is_stable(input, tp->t)) { /* Default hysteresis limits are low. Make a strict check. */ pos = tp->t->pos; @@ -1183,6 +1176,8 @@ wstpad_mt_masks(struct wsmouseinput *inp && abs(pos->acc_dy) < 3 * input->filter.v.hysteresis) return; + slot = ffs(input->mt.ptr_mask) - 1; + t = &tp->tpad_touches[slot]; if (t->dir >= 0) { /* Treat t as thumb if it is slow while tp->t is fast. */ if (magnitude(input, t->pos->dx, t->pos->dy) > MAG_LOW @@ -1191,6 +1186,9 @@ wstpad_mt_masks(struct wsmouseinput *inp } tp->ignore = input->mt.ptr_mask; + + /* If t receives pointer control, ignore slow movement. */ + t->flags |= B_EDGE; } }