From 6b93c0c7405805d0f90369cd19c6ed57a2880a2a Mon Sep 17 00:00:00 2001 From: Mike Bannister Date: Tue, 1 Sep 2026 08:01:02 -0400 Subject: [PATCH 1/2] fix: sync xf86-input-neko with the scroll driver kernel-images ships The vendored driver here has no scroll support: a NEKO_SCROLL (0x80) message falls through to xf86PostTouchEvent. The copy that kernel-images builds and ships registers XI2 scroll valuators for it, and Dockerfile.tmpl builds this copy into the base image, so a standalone base-image deployment with the driver enabled gets a driver that cannot scroll. Take src/neko.c verbatim from kernel-images at 18e75774 (kernel-images PR #196): scroll valuators 3 and 4 registered with SCROLL_INCREMENT 120, NEKO_SCROLL posted through xf86PostMotionEventM, the touch class held at its three axes, and the device announced as XI_MOUSE rather than XI_TOUCHSCREEN so Chromium honours the scroll valuators. Document the message and the unit in the README. Co-Authored-By: Claude Fable 5 --- utils/xorg-deps/xf86-input-neko/README.md | 14 ++++ utils/xorg-deps/xf86-input-neko/src/neko.c | 76 ++++++++++++++-------- 2 files changed, 63 insertions(+), 27 deletions(-) diff --git a/utils/xorg-deps/xf86-input-neko/README.md b/utils/xorg-deps/xf86-input-neko/README.md index 5ed64d0ab..1de477888 100644 --- a/utils/xorg-deps/xf86-input-neko/README.md +++ b/utils/xorg-deps/xf86-input-neko/README.md @@ -17,3 +17,17 @@ Done. To _uninstall_, again go inside the extracted directory, and do sudo make uninstall + +### scroll events +Besides XI2 touch events, the driver accepts a `NEKO_SCROLL` (`0x80`) message +whose `x` and `y` fields carry relative scroll deltas in XI2.1 scroll units, +120 units per wheel notch (`SCROLL_INCREMENT`). They are posted through the +device's `REL_HSCROLL`/`REL_VSCROLL` scroll valuators, so XI2.1-aware clients +get smooth scrolling while the X server emulates wheel buttons 4-7 for the +rest. The device registers as `XI_MOUSE` rather than `XI_TOUCHSCREEN` so that +Chromium honours the scroll valuators. The server sends deltas in these units, so its XTest fallback has to divide +by the same increment to replay them as wheel clicks. + +This is the same driver source that ships in +[kernel-images](https://github.com/kernel/kernel-images) +(`images/chromium-headful/xorg-deps/xf86-input-neko`). diff --git a/utils/xorg-deps/xf86-input-neko/src/neko.c b/utils/xorg-deps/xf86-input-neko/src/neko.c index 3d8f88d61..21bf9d61e 100644 --- a/utils/xorg-deps/xf86-input-neko/src/neko.c +++ b/utils/xorg-deps/xf86-input-neko/src/neko.c @@ -54,10 +54,15 @@ #include #include #include +#include #include -#define MAX_USED_VALUATORS 3 /* x, y, pressure */ -#define TOUCH_MAX_SLOTS 10 /* max number of simultaneous touches */ +#define MAX_USED_VALUATORS 5 /* x, y, pressure, v-scroll, h-scroll */ +#define TOUCH_VALUATORS 3 /* touch only uses x, y, pressure */ +#define TOUCH_MAX_SLOTS 10 /* max number of simultaneous touches */ + +#define NEKO_SCROLL 0x80 +#define SCROLL_INCREMENT 120.0 struct neko_message { @@ -149,16 +154,25 @@ ReadInput(InputInfoPtr pInfo) ValuatorMask *m = priv->valuators; valuator_mask_zero(m); - // do not send valuators if x and y are -1 - if (msg.x != -1 && msg.y != -1) + if (msg.type == NEKO_SCROLL) { - valuator_mask_set_double(m, 0, msg.x); - valuator_mask_set_double(m, 1, msg.y); - valuator_mask_set_double(m, 2, msg.pressure); + if (msg.y != 0) + valuator_mask_set_double(m, 3, (double)msg.y); + if (msg.x != 0) + valuator_mask_set_double(m, 4, (double)msg.x); + xf86PostMotionEventM(pInfo->dev, FALSE, m); + } + else + { + // do not send valuators if x and y are -1 + if (msg.x != -1 && msg.y != -1) + { + valuator_mask_set_double(m, 0, msg.x); + valuator_mask_set_double(m, 1, msg.y); + valuator_mask_set_double(m, 2, msg.pressure); + } + xf86PostTouchEvent(pInfo->dev, msg.touchId, msg.type, 0, m); } - - // TODO: extend to other types, such as keyboard and mouse - xf86PostTouchEvent(pInfo->dev, msg.touchId, msg.type, 0, m); } /* Close socket. */ @@ -181,11 +195,11 @@ InitTouch(InputInfoPtr pInfo) struct neko_priv *priv = pInfo->private; const int nbtns = 11; - const int naxes = 3; + const int naxes = MAX_USED_VALUATORS; /* x, y, pressure, v-scroll, h-scroll */ unsigned char map[nbtns + 1]; Atom btn_labels[nbtns]; - Atom axis_labels[naxes]; + Atom axis_labels[MAX_USED_VALUATORS]; // init button map memset(map, 0, sizeof(map)); @@ -209,10 +223,12 @@ InitTouch(InputInfoPtr pInfo) btn_labels[10] = XIGetKnownProperty(BTN_LABEL_PROP_BTN_BACK); // init axis labels - memset(axis_labels, 0, ARRAY_SIZE(axis_labels) * sizeof(Atom)); + memset(axis_labels, 0, sizeof(axis_labels)); axis_labels[0] = XIGetKnownProperty(AXIS_LABEL_PROP_ABS_MT_POSITION_X); axis_labels[1] = XIGetKnownProperty(AXIS_LABEL_PROP_ABS_MT_POSITION_Y); axis_labels[2] = XIGetKnownProperty(AXIS_LABEL_PROP_ABS_MT_PRESSURE); + axis_labels[3] = XIGetKnownProperty(AXIS_LABEL_PROP_REL_VSCROLL); + axis_labels[4] = XIGetKnownProperty(AXIS_LABEL_PROP_REL_HSCROLL); /* initialize mouse emulation valuators */ if (InitPointerDeviceStruct((DevicePtr)pInfo->dev, @@ -274,22 +290,28 @@ InitTouch(InputInfoPtr pInfo) priv->pmax + 1, /* max_res */ Absolute); - /* - The mode field is either XIDirectTouch for direct−input touch devices - such as touchscreens or XIDependentTouch for indirect input devices such - as touchpads. For XIDirectTouch devices, touch events are sent to window - at the position the touch occured. For XIDependentTouch devices, touch - events are sent to the window at the position of the device's sprite. - - The num_touches field defines the maximum number of simultaneous touches - the device supports. A num_touches of 0 means the maximum number of - simultaneous touches is undefined or unspecified. This field should be - used as a guide only, devices will lie about their capabilities. - */ + /* scroll valuator axes — relative, so min=max=0 */ + xf86InitValuatorAxisStruct(pInfo->dev, 3, + axis_labels[3], + NO_AXIS_LIMITS, NO_AXIS_LIMITS, /* no limits for scroll */ + 0, 0, 0, + Relative); + SetScrollValuator(pInfo->dev, 3, SCROLL_TYPE_VERTICAL, + SCROLL_INCREMENT, SCROLL_FLAG_PREFERRED); + + xf86InitValuatorAxisStruct(pInfo->dev, 4, + axis_labels[4], + NO_AXIS_LIMITS, NO_AXIS_LIMITS, + 0, 0, 0, + Relative); + SetScrollValuator(pInfo->dev, 4, SCROLL_TYPE_HORIZONTAL, + SCROLL_INCREMENT, SCROLL_FLAG_PREFERRED); + + /* Touch class only uses the first 3 axes (x, y, pressure). */ if (InitTouchClassDeviceStruct(pInfo->dev, priv->slots, XIDirectTouch, - naxes) == FALSE) + TOUCH_VALUATORS) == FALSE) { xf86IDrvMsg(pInfo, X_ERROR, "unable to allocate TouchClassDeviceStruct\n"); @@ -354,7 +376,7 @@ PreInit(__attribute__ ((unused)) InputDriverPtr drv, return BadValue; } - pInfo->type_name = (char*)XI_TOUCHSCREEN; + pInfo->type_name = (char*)XI_MOUSE; pInfo->device_control = DeviceControl; pInfo->read_input = NULL; pInfo->control_proc = NULL; From 80ebaae4edde5b08d6b8539641a69296cb32be3a Mon Sep 17 00:00:00 2001 From: Mike Bannister Date: Mon, 5 Oct 2026 08:52:05 -0700 Subject: [PATCH 2/2] test: account for Xorg queue wakeups and archived bullseye packages --- .github/workflows/xorg_driver_test.yml | 14 ++++++++++++++ utils/xorg-deps/xf86-input-neko/tests/README.md | 8 +++++++- .../xf86-input-neko/tests/run-integration.sh | 2 +- .../xf86-input-neko/tests/scroll-integration.c | 14 ++++++++++++-- 4 files changed, 34 insertions(+), 4 deletions(-) diff --git a/.github/workflows/xorg_driver_test.yml b/.github/workflows/xorg_driver_test.yml index e25654cd7..2d88e9b17 100644 --- a/.github/workflows/xorg_driver_test.yml +++ b/.github/workflows/xorg_driver_test.yml @@ -17,12 +17,26 @@ jobs: driver: name: Build and test driver (${{ matrix.debian }}) runs-on: ubuntu-latest + timeout-minutes: 15 + env: + DEBIAN_FRONTEND: noninteractive container: debian:${{ matrix.debian }}-slim strategy: fail-fast: false matrix: debian: [bullseye, bookworm] steps: + # Bullseye LTS ended in August 2026 and its security packages were removed + # before archival completed (Debian bug #1147093). Keep this compatibility + # test reproducible using Debian's signed final-LTS snapshot. + - name: Select archived bullseye package snapshot + if: matrix.debian == 'bullseye' + run: | + rm -f /etc/apt/sources.list.d/* + printf '%s\n' \ + 'deb [check-valid-until=no] http://snapshot.debian.org/archive/debian/20260831T235959Z/ bullseye main' \ + 'deb [check-valid-until=no] http://snapshot.debian.org/archive/debian-security/20260831T235959Z/ bullseye-security main' \ + > /etc/apt/sources.list - name: Install build and isolated Xorg dependencies run: | apt-get update diff --git a/utils/xorg-deps/xf86-input-neko/tests/README.md b/utils/xorg-deps/xf86-input-neko/tests/README.md index 01115022e..914317156 100644 --- a/utils/xorg-deps/xf86-input-neko/tests/README.md +++ b/utils/xorg-deps/xf86-input-neko/tests/README.md @@ -57,7 +57,13 @@ zero. No other client should write to the input socket or grab touch/pointer events. Each fixture is sent as one 12-byte write; stream-fragment handling is an existing driver limitation, not covered by this success-path suite. -The test waits up to three seconds for asynchronous input and then drains both +The inherited driver posts from a bespoke thread without waking Xorg's main +loop. The test sends periodic XNoOp requests, as an active X client would, to +wake that loop. It still asserts observed events; it does not validate delivery +on a completely idle server. Xorg itself restores X/Y mask bits on the sentinel +touch-end event, so the raw end mask is expected to contain those two axes. + +The runner enforces a 30-second timeout. The test waits up to three seconds for asynchronous input and then drains both X clients to quiescence. A successful socket write or `XSync` alone is not considered proof of event delivery. diff --git a/utils/xorg-deps/xf86-input-neko/tests/run-integration.sh b/utils/xorg-deps/xf86-input-neko/tests/run-integration.sh index 0a0fc7c51..6931d9f2b 100755 --- a/utils/xorg-deps/xf86-input-neko/tests/run-integration.sh +++ b/utils/xorg-deps/xf86-input-neko/tests/run-integration.sh @@ -73,4 +73,4 @@ for attempt in $(seq 1 100); do sleep 0.1 done [ "$ready" = true ] || { echo 'Timed out starting isolated Xorg' >&2; exit 1; } -"$work/scroll-integration" ":$(cat "$work/display")" "$work/neko.sock" dummy_touchscreen +timeout 30s "$work/scroll-integration" ":$(cat "$work/display")" "$work/neko.sock" dummy_touchscreen diff --git a/utils/xorg-deps/xf86-input-neko/tests/scroll-integration.c b/utils/xorg-deps/xf86-input-neko/tests/scroll-integration.c index efd55244c..44dbdf31f 100644 --- a/utils/xorg-deps/xf86-input-neko/tests/scroll-integration.c +++ b/utils/xorg-deps/xf86-input-neko/tests/scroll-integration.c @@ -150,6 +150,12 @@ static struct capture collect(int expect_output) long long deadline = now_ms() + (expect_output ? TIMEOUT_MS : QUIET_MS * 2); long long quiet_since = 0; for (;;) { + /* This driver posts from a bespoke thread without waking Xorg's main + * loop. A request supplies that wake, as normal active clients do. + * This is not an input-delivery barrier: still wait for actual events. + */ + XNoOp(xi); + XFlush(xi); int processed = drain(&capture); long long now = now_ms(); int received = capture.raw_motion_count + capture.raw_touch_count[0] + @@ -257,11 +263,15 @@ static unsigned int touch_case(const char *name, uint8_t type, capture.raw_touch_count[index], capture.touch_count[index]); struct sample *raw = &capture.raw_touch[index]; struct sample *cooked = &capture.touch[index]; - unsigned int expected_mask = (x == -1 && y == -1) ? 0 : 7; + int sentinel = x == -1 && y == -1; + /* Xorg restores cached X/Y into the processed TouchEnd mask and merges + * those bits into the raw event too, even when the driver supplied none. + */ + unsigned int expected_mask = sentinel ? 3 : 7; if (raw->mask != expected_mask || (cooked->mask & ~7u)) fail("touch mask raw/cooked %#x/%#x, expected raw %#x and only axes 0-2", raw->mask, cooked->mask, expected_mask); - if (expected_mask && (raw->values[0] != x || raw->values[1] != y || + if (!sentinel && (raw->values[0] != x || raw->values[1] != y || raw->values[2] != pressure)) fail("raw touch coordinates/pressure differ from fixture"); for (int i = 0; i < 3; ++i)