diff --git a/.github/workflows/xorg_driver_test.yml b/.github/workflows/xorg_driver_test.yml new file mode 100644 index 000000000..2d88e9b17 --- /dev/null +++ b/.github/workflows/xorg_driver_test.yml @@ -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 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; diff --git a/utils/xorg-deps/xf86-input-neko/tests/README.md b/utils/xorg-deps/xf86-input-neko/tests/README.md new file mode 100644 index 000000000..914317156 --- /dev/null +++ b/utils/xorg-deps/xf86-input-neko/tests/README.md @@ -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. diff --git a/utils/xorg-deps/xf86-input-neko/tests/run-integration.sh b/utils/xorg-deps/xf86-input-neko/tests/run-integration.sh new file mode 100755 index 000000000..6931d9f2b --- /dev/null +++ b/utils/xorg-deps/xf86-input-neko/tests/run-integration.sh @@ -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" <"$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 diff --git a/utils/xorg-deps/xf86-input-neko/tests/scroll-integration.c b/utils/xorg-deps/xf86-input-neko/tests/scroll-integration.c new file mode 100644 index 000000000..44dbdf31f --- /dev/null +++ b/utils/xorg-deps/xf86-input-neko/tests/scroll-integration.c @@ -0,0 +1,426 @@ +/* Integration test for an isolated Xorg server running xf86-input-neko. + * Never run against an interactive desktop: this creates a window and moves + * its pointer. The caller owns the disposable Xorg process and socket. + */ +#define _POSIX_C_SOURCE 200809L +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define NEKO_SCROLL 0x80 +#define TIMEOUT_MS 3000 +#define QUIET_MS 100 +#define MAX_EVENTS 64 + +static Display *legacy, *xi; +static int opcode, device_id, socket_fd; +static Window window; +static const char *current_test = "setup"; + +struct sample { + unsigned int mask; + double values[8]; + unsigned int id; +}; +struct capture { + int raw_motion_count, motion_count; + struct sample raw_motion[MAX_EVENTS]; + int raw_touch_count[3], touch_count[3]; + struct sample raw_touch[3], touch[3]; + int press[8], release[8]; +}; + +static void fail(const char *format, ...) +{ + va_list args; + fprintf(stderr, "FAIL %s: ", current_test); + va_start(args, format); + vfprintf(stderr, format, args); + va_end(args); + fputc('\n', stderr); + exit(EXIT_FAILURE); +} + +static int xerror(Display *display, XErrorEvent *event) +{ + char message[256]; + XGetErrorText(display, event->error_code, message, sizeof(message)); + fail("X error %s (request %u.%u)", message, + event->request_code, event->minor_code); + return 0; +} + +static long long now_ms(void) +{ + struct timespec ts; + if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0) + fail("clock_gettime: %s", strerror(errno)); + return (long long)ts.tv_sec * 1000 + ts.tv_nsec / 1000000; +} + +static struct sample sample_valuators(const XIValuatorState *state, + const double *values, unsigned int id) +{ + struct sample result = { .id = id }; + int next = 0; + for (int axis = 0; axis < state->mask_len * 8; ++axis) { + if (!XIMaskIsSet(state->mask, axis)) + continue; + if (axis >= 8) + fail("unexpected valuator %d", axis); + result.mask |= 1u << axis; + result.values[axis] = values[next++]; + } + return result; +} + +static int drain(struct capture *capture) +{ + int processed = 0; + while (XPending(legacy)) { + XEvent event; + XNextEvent(legacy, &event); + processed++; + if (event.type == ButtonPress && event.xbutton.button < 8) + capture->press[event.xbutton.button]++; + if (event.type == ButtonRelease && event.xbutton.button < 8) + capture->release[event.xbutton.button]++; + } + while (XPending(xi)) { + XEvent event; + XNextEvent(xi, &event); + processed++; + if (event.type != GenericEvent || event.xcookie.extension != opcode || + !XGetEventData(xi, &event.xcookie)) + continue; + int type = event.xcookie.evtype; + if (type == XI_RawMotion || + (type >= XI_RawTouchBegin && type <= XI_RawTouchEnd)) { + XIRawEvent *raw = event.xcookie.data; + if (raw->sourceid == device_id) { + struct sample sample = sample_valuators(&raw->valuators, + raw->raw_values, + raw->detail); + if (type == XI_RawMotion) { + if (capture->raw_motion_count == MAX_EVENTS) + fail("too many raw motion events"); + capture->raw_motion[capture->raw_motion_count++] = sample; + } else { + int index = type - XI_RawTouchBegin; + capture->raw_touch_count[index]++; + capture->raw_touch[index] = sample; + } + } + } else if (type == XI_Motion || + (type >= XI_TouchBegin && type <= XI_TouchEnd)) { + XIDeviceEvent *dev = event.xcookie.data; + if (dev->sourceid == device_id) { + if (type == XI_Motion) { + capture->motion_count++; + } else { + int index = type - XI_TouchBegin; + capture->touch_count[index]++; + capture->touch[index] = sample_valuators(&dev->valuators, + dev->valuators.values, + dev->detail); + } + } + } + XFreeEventData(xi, &event.xcookie); + } + return processed; +} + +/* Wait for driver output, then drain both X clients to quiescence. The socket + * write is asynchronous, so XSync alone would not be a completion barrier. */ +static struct capture collect(int expect_output) +{ + struct capture capture = {0}; + 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] + + capture.raw_touch_count[1] + capture.raw_touch_count[2]; + if (processed || !quiet_since) + quiet_since = now; + if ((received && now - quiet_since >= QUIET_MS) || now >= deadline) + return capture; + long long remaining = deadline - now; + int timeout = remaining < QUIET_MS ? (int)remaining : QUIET_MS; + struct pollfd fds[2] = { + { .fd = ConnectionNumber(legacy), .events = POLLIN }, + { .fd = ConnectionNumber(xi), .events = POLLIN } + }; + if (poll(fds, 2, timeout) < 0 && errno != EINTR) + fail("poll: %s", strerror(errno)); + } +} + +static void send_frame(uint8_t type, uint16_t id, int32_t x, int32_t y, + uint8_t pressure) +{ + unsigned char frame[12] = { type, id & 0xff, id >> 8 }; + for (int byte = 0; byte < 4; ++byte) { + frame[3 + byte] = (uint32_t)x >> (byte * 8); + frame[7 + byte] = (uint32_t)y >> (byte * 8); + } + frame[11] = pressure; + /* Keep one message in one write: split-frame tolerance is a separate, + * pre-existing protocol defect and is deliberately not hidden here. */ + ssize_t written; + do { + written = send(socket_fd, frame, sizeof(frame), MSG_NOSIGNAL); + } while (written < 0 && errno == EINTR); + if (written != (ssize_t)sizeof(frame)) + fail("socket write returned %zd: %s", written, + written < 0 ? strerror(errno) : "short write"); +} + +static void pointer_position(int *x, int *y) +{ + Window root, child; + int win_x, win_y; + unsigned int mask; + if (!XQueryPointer(legacy, DefaultRootWindow(legacy), &root, &child, + x, y, &win_x, &win_y, &mask)) + fail("pointer is on another screen"); +} + +static void check_buttons(const struct capture *capture, + int up, int down, int left, int right) +{ + int expected[8] = { 0, 0, 0, 0, up, down, left, right }; + for (int button = 4; button <= 7; ++button) { + if (capture->press[button] != expected[button] || + capture->release[button] != expected[button]) + fail("button %d: press/release %d/%d, expected %d/%d", button, + capture->press[button], capture->release[button], + expected[button], expected[button]); + } +} + +static void scroll_case(const char *name, int32_t x, int32_t y, + int up, int down, int left, int right) +{ + current_test = name; + int before_x, before_y, after_x, after_y; + pointer_position(&before_x, &before_y); + send_frame(NEKO_SCROLL, 0, x, y, 0); + struct capture capture = collect(x || y); + int expected_count = (x || y) ? 1 : 0; + if (capture.raw_motion_count != expected_count || + capture.motion_count != expected_count) + fail("raw/cooked motion count %d/%d, expected %d/%d", + capture.raw_motion_count, capture.motion_count, + expected_count, expected_count); + if (expected_count) { + struct sample *sample = &capture.raw_motion[0]; + unsigned int mask = (y ? 1u << 3 : 0) | (x ? 1u << 4 : 0); + if (sample->mask != mask || sample->values[3] != y || + sample->values[4] != x) + fail("raw mask/deltas %#x %.3f/%.3f, expected %#x %d/%d", + sample->mask, sample->values[4], sample->values[3], mask, x, y); + } + for (int i = 0; i < 3; ++i) + if (capture.touch_count[i] || capture.raw_touch_count[i]) + fail("scroll generated a touch event"); + check_buttons(&capture, up, down, left, right); + pointer_position(&after_x, &after_y); + if (before_x != after_x || before_y != after_y) + fail("scroll moved pointer from %d,%d to %d,%d", before_x, before_y, + after_x, after_y); + printf("PASS %s\n", name); +} + +static unsigned int touch_case(const char *name, uint8_t type, + int32_t x, int32_t y, uint8_t pressure) +{ + current_test = name; + send_frame(type, 0x1234, x, y, pressure); + struct capture capture = collect(1); + int index = type - XI_TouchBegin; + if (capture.raw_touch_count[index] != 1 || capture.touch_count[index] != 1) + fail("raw/cooked touch count %d/%d, expected 1/1", + capture.raw_touch_count[index], capture.touch_count[index]); + struct sample *raw = &capture.raw_touch[index]; + struct sample *cooked = &capture.touch[index]; + 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 (!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) + if (i != index && (capture.touch_count[i] || capture.raw_touch_count[i])) + fail("unexpected touch event type"); + check_buttons(&capture, 0, 0, 0, 0); + printf("PASS %s\n", name); + return cooked->id; +} + +static void verify_device(const char *name) +{ + int count; + XIDeviceInfo *devices = XIQueryDevice(xi, XIAllDevices, &count); + if (!devices) + fail("XIQueryDevice failed"); + XIDeviceInfo *device = NULL; + for (int i = 0; i < count; ++i) + if (strcmp(devices[i].name, name) == 0) { + if (device) + fail("duplicate device name %s", name); + device = &devices[i]; + } + if (!device) + fail("device %s not found", name); + if (!device->enabled || device->use != XISlavePointer) + fail("device must be an enabled, attached slave pointer"); + device_id = device->deviceid; + int axes = 0, scrolls = 0, touches = 0; + unsigned int axis_mask = 0; + for (int i = 0; i < device->num_classes; ++i) { + XIAnyClassInfo *info = device->classes[i]; + if (info->type == XIValuatorClass) { + XIValuatorClassInfo *axis = (XIValuatorClassInfo *)info; + if (axis->number < 0 || axis->number >= 5) + fail("unexpected axis %d", axis->number); + int mode = axis->number < 3 ? XIModeAbsolute : XIModeRelative; + if (axis->mode != mode) + fail("axis %d mode %d, expected %d", axis->number, axis->mode, mode); + if (axis->number >= 3) { + const char *label = axis->number == 3 ? "Rel Vert Scroll" : "Rel Horiz Scroll"; + if (axis->label != XInternAtom(xi, label, True)) + fail("axis %d has incorrect scroll label", axis->number); + } + axes++; + axis_mask |= 1u << axis->number; + } else if (info->type == XIScrollClass) { + XIScrollClassInfo *scroll = (XIScrollClassInfo *)info; + int type = scroll->number == 3 ? XIScrollTypeVertical : XIScrollTypeHorizontal; + if ((scroll->number != 3 && scroll->number != 4) || + scroll->scroll_type != type || scroll->increment != 120.0 || + !(scroll->flags & XIScrollFlagPreferred) || + (scroll->flags & XIScrollFlagNoEmulation)) + fail("invalid scroll metadata for axis %d", scroll->number); + scrolls++; + } else if (info->type == XITouchClass) { + XITouchClassInfo *touch = (XITouchClassInfo *)info; + if (touch->mode != XIDirectTouch || touch->num_touches != 10) + fail("invalid touch metadata"); + touches++; + } + } + if (axes != 5 || axis_mask != 31 || scrolls != 2 || touches != 1) + fail("class counts axes/scroll/touch %d/%d/%d, expected 5/2/1", + axes, scrolls, touches); + printf("PASS XIQueryDevice: %s (id %d), 5 axes, two 120-unit scroll classes, 10 direct touches\n", + name, device_id); + XIFreeDeviceInfo(devices); +} + +int main(int argc, char **argv) +{ + if (argc != 3 && argc != 4) { + fprintf(stderr, "Usage: %s DISPLAY SOCKET [DEVICE_NAME]\n", argv[0]); + return EXIT_FAILURE; + } + XSetErrorHandler(xerror); + legacy = XOpenDisplay(argv[1]); + xi = XOpenDisplay(argv[1]); + if (!legacy || !xi) + fail("cannot open disposable Xorg display %s", argv[1]); + int event, error, major = 2, minor = 2; + if (!XQueryExtension(xi, "XInputExtension", &opcode, &event, &error) || + XIQueryVersion(xi, &major, &minor) != Success) + fail("XI2.2 is required"); + verify_device(argc == 4 ? argv[3] : "dummy_touchscreen"); + + int screen = DefaultScreen(legacy); + XSetWindowAttributes attrs = { .override_redirect = True }; + window = XCreateWindow(legacy, RootWindow(legacy, screen), 0, 0, + DisplayWidth(legacy, screen), DisplayHeight(legacy, screen), + 0, CopyFromParent, InputOutput, CopyFromParent, + CWOverrideRedirect, &attrs); + XSelectInput(legacy, window, ButtonPressMask | ButtonReleaseMask | PointerMotionMask); + XMapRaised(legacy, window); + XWarpPointer(legacy, None, window, 0, 0, 0, 0, 80, 80); + XSync(legacy, False); + + unsigned char cooked[XIMaskLen(XI_LASTEVENT)] = {0}; + XISetMask(cooked, XI_Motion); + XISetMask(cooked, XI_TouchBegin); + XISetMask(cooked, XI_TouchUpdate); + XISetMask(cooked, XI_TouchEnd); + XIEventMask selected = { XIAllMasterDevices, sizeof(cooked), cooked }; + XISelectEvents(xi, window, &selected, 1); + unsigned char raw[XIMaskLen(XI_LASTEVENT)] = {0}; + XISetMask(raw, XI_RawMotion); + XISetMask(raw, XI_RawTouchBegin); + XISetMask(raw, XI_RawTouchUpdate); + XISetMask(raw, XI_RawTouchEnd); + selected.mask = raw; + selected.mask_len = sizeof(raw); + XISelectEvents(xi, DefaultRootWindow(xi), &selected, 1); + XSync(xi, False); + (void)collect(0); + + struct sockaddr_un address = { .sun_family = AF_UNIX }; + if (strlen(argv[2]) >= sizeof(address.sun_path)) + fail("socket path too long"); + strcpy(address.sun_path, argv[2]); + socket_fd = socket(AF_UNIX, SOCK_STREAM, 0); + if (socket_fd < 0 || connect(socket_fd, (struct sockaddr *)&address, sizeof(address)) < 0) + fail("cannot connect socket %s: %s", argv[2], strerror(errno)); + + scroll_case("down one notch", 0, 120, 0, 1, 0, 0); + scroll_case("up one notch", 0, -120, 1, 0, 0, 0); + scroll_case("right one notch", 120, 0, 0, 0, 0, 1); + scroll_case("left one notch", -120, 0, 0, 0, 1, 0); + scroll_case("diagonal right/up", 120, -120, 1, 0, 0, 1); + scroll_case("diagonal left/down", -120, 120, 0, 1, 1, 0); + scroll_case("zero is a no-op", 0, 0, 0, 0, 0, 0); + scroll_case("quarter notch 1", 0, 30, 0, 0, 0, 0); + scroll_case("quarter notch 2", 0, 30, 0, 0, 0, 0); + scroll_case("quarter notch 3", 0, 30, 0, 0, 0, 0); + scroll_case("quarter notch 4 accumulates", 0, 30, 0, 1, 0, 0); + scroll_case("reverse accumulated notch", 0, -120, 1, 0, 0, 0); + + unsigned int touch_id = touch_case("touch begin after scroll", XI_TouchBegin, 16384, 16384, 128); + scroll_case("scroll during touch", 120, 0, 0, 0, 0, 1); + if (touch_case("touch update after scroll", XI_TouchUpdate, 17000, 17000, 64) != touch_id) + fail("touch ID changed on update"); + if (touch_case("touch end sentinel", XI_TouchEnd, -1, -1, 0) != touch_id) + fail("touch ID changed on end"); + scroll_case("scroll after touch", -120, 0, 0, 0, 1, 0); + + close(socket_fd); + XDestroyWindow(legacy, window); + XCloseDisplay(xi); + XCloseDisplay(legacy); + puts("PASS all isolated xf86-input-neko scroll/touch integration checks"); + return EXIT_SUCCESS; +}