Skip to content
Draft
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
55 changes: 55 additions & 0 deletions .github/workflows/xorg_driver_test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
name: Test Xorg input driver

on:
push:
paths:
- utils/xorg-deps/xf86-input-neko/**
- .github/workflows/xorg_driver_test.yml
pull_request:
paths:
- utils/xorg-deps/xf86-input-neko/**
- .github/workflows/xorg_driver_test.yml

permissions:
contents: read

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
apt-get install -y --no-install-recommends \
ca-certificates git gcc pkgconf autoconf automake libtool make \
xorg-dev xutils-dev xserver-xorg-core xserver-xorg-video-dummy
- uses: actions/checkout@v4
- name: Build the vendored driver
working-directory: utils/xorg-deps/xf86-input-neko
run: |
./autogen.sh --prefix=/usr
./configure
make -j2
- name: Test real XI2 scroll and touch events
working-directory: utils/xorg-deps/xf86-input-neko
run: ./tests/run-integration.sh
14 changes: 14 additions & 0 deletions utils/xorg-deps/xf86-input-neko/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`).
76 changes: 49 additions & 27 deletions utils/xorg-deps/xf86-input-neko/src/neko.c
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,15 @@
#include <X11/keysym.h>
#include <mipointer.h>
#include <xserver-properties.h>
#include <inputstr.h>
#include <pthread.h>

#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
{
Expand Down Expand Up @@ -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. */
Expand All @@ -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));
Expand All @@ -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,
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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;
Expand Down
72 changes: 72 additions & 0 deletions utils/xorg-deps/xf86-input-neko/tests/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Isolated driver integration test

`scroll-integration.c` connects to a **fresh, disposable Xorg server** with the
built `neko_drv.so` and a dummy video driver. Do not point it at an interactive
desktop. It creates a full-screen test window, moves that server's pointer, and
injects scroll and touch fixtures through the driver's Unix socket.

Run the complete check on a machine with Xorg, the dummy video driver, and
X11/XI client development packages:

```sh
./autogen.sh --prefix=/usr
./configure
make -j2
./tests/run-integration.sh
```

The runner creates and cleans up its own display, configuration, module path,
and socket. CI runs this on Debian bullseye (the base-image default) and bookworm.

To use another disposable test server, build with the X11 and XI client development packages:

```sh
cc -std=c99 -O2 -Wall -Wextra -Werror \
tests/scroll-integration.c -o /tmp/neko-scroll-integration \
$(pkg-config --cflags --libs x11 xi)
/tmp/neko-scroll-integration "$TEST_DISPLAY" "$TEST_SOCKET" dummy_touchscreen
```

The optional third argument is the configured input device's `Identifier`;
it defaults to `dummy_touchscreen`. The runner must load the just-built module,
wait for Xorg and the socket to be ready, and stop Xorg on both success and
failure. Xvfb alone cannot load an Xorg input module and is not a substitute.

## Coverage

- XI2.2 negotiation and actual `XIQueryDevice` metadata: five valuators, absolute
axes 0–2, relative scroll axes 3–4, correct labels, vertical/horizontal scroll
classes with increment 120 and preferred legacy emulation, ten direct touches
- Signed vertical/horizontal and diagonal scroll fixtures, using the production
12-byte little-endian socket layout
- Exact raw deltas and valuator masks, one cooked motion event per nonzero
fixture, no touch events from scroll, and no pointer displacement
- Wheel button 4–7 press/release counts from a separate legacy X client
- Four 30-unit deltas accumulating into one legacy wheel notch; zero as a no-op
- Touch begin/update/end, stable touch IDs, unchanged raw coordinates/pressure,
end-without-coordinates sentinel, and separation of touch/scroll masks when
the two event types are interleaved

The XI2 touch class does not expose a touch-axis count. The test verifies the
three-axis touch contract through event masks instead.

## Boundaries

Start a new Xorg instance for each test run so scroll accumulators begin at
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 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.

These checks do not establish Chromium rendering behavior, Ctrl-wheel modifier
ordering across the separate X11 and driver sockets, reconnection behavior,
or thread/teardown safety. Those require separate integration or stress tests.
76 changes: 76 additions & 0 deletions utils/xorg-deps/xf86-input-neko/tests/run-integration.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
#!/bin/sh
# Build first with ./autogen.sh && ./configure && make.
# This runner always creates its own disposable Xorg server.
set -eu
cd "$(dirname "$0")/.."
module=$(pwd)/src/.libs/neko_drv.so
[ -f "$module" ] || { echo 'Build neko_drv.so before running this test' >&2; exit 1; }
work=$(mktemp -d /tmp/neko-xorg-test.XXXXXX)
pid=
cleanup() {
status=$?
trap - EXIT HUP INT TERM
if [ -n "$pid" ]; then kill "$pid" 2>/dev/null || true; wait "$pid" 2>/dev/null || true; fi
if [ "$status" -ne 0 ]; then cat "$work/Xorg.log" "$work/stderr.log" >&2 2>/dev/null || true; fi
rm -rf "$work"
exit "$status"
}
trap cleanup EXIT HUP INT TERM
mkdir -p "$work/modules/input"
ln -s "$module" "$work/modules/input/neko_drv.so"
cat > "$work/xorg.conf" <<CONFIG
Section "ServerFlags"
Option "AutoAddDevices" "false"
Option "AutoEnableDevices" "false"
Option "AllowMouseOpenFail" "true"
EndSection
Section "Files"
ModulePath "$work/modules"
ModulePath "/usr/lib/xorg/modules"
EndSection
Section "Device"
Identifier "dummy_video"
Driver "dummy"
VideoRam 256000
EndSection
Section "Monitor"
Identifier "dummy_monitor"
HorizSync 5.0-1000.0
VertRefresh 5.0-200.0
Modeline "1280x720" 74.50 1280 1344 1472 1664 720 723 728 748
EndSection
Section "Screen"
Identifier "dummy_screen"
Device "dummy_video"
Monitor "dummy_monitor"
DefaultDepth 24
SubSection "Display"
Depth 24
Modes "1280x720"
EndSubSection
EndSection
Section "InputDevice"
Identifier "dummy_touchscreen"
Driver "neko"
Option "SocketName" "$work/neko.sock"
EndSection
Section "ServerLayout"
Identifier "test_layout"
Screen "dummy_screen"
InputDevice "dummy_touchscreen" "CorePointer"
EndSection
CONFIG
${CC:-cc} -std=c99 -O2 -Wall -Wextra -Werror tests/scroll-integration.c \
-o "$work/scroll-integration" $(pkg-config --cflags --libs x11 xi)
Xorg -config "$work/xorg.conf" -logfile "$work/Xorg.log" \
-displayfd 3 -nolisten tcp -noreset -novtswitch -sharevts \
3>"$work/display" >"$work/stderr.log" 2>&1 &
pid=$!
ready=false
for attempt in $(seq 1 100); do
if ! kill -0 "$pid" 2>/dev/null; then echo 'Xorg exited before becoming ready' >&2; exit 1; fi
if [ -s "$work/display" ] && [ -S "$work/neko.sock" ]; then ready=true; break; fi
sleep 0.1
done
[ "$ready" = true ] || { echo 'Timed out starting isolated Xorg' >&2; exit 1; }
timeout 30s "$work/scroll-integration" ":$(cat "$work/display")" "$work/neko.sock" dummy_touchscreen
Loading