Skip to content

feat: XI2.1 smooth scrolling via xf86-input-neko scroll valuators#196

Open
hiroTamada wants to merge 6 commits intomainfrom
hiro/xi2-scroll
Open

feat: XI2.1 smooth scrolling via xf86-input-neko scroll valuators#196
hiroTamada wants to merge 6 commits intomainfrom
hiro/xi2-scroll

Conversation

@hiroTamada
Copy link
Copy Markdown
Contributor

@hiroTamada hiroTamada commented Apr 1, 2026

Checklist

  • Requires kernel/neko#hiro/xi2-scroll to be merged and the ghcr.io/kernel/neko/base:xi2-scroll image to be published
  • A description of the changes proposed in the pull request.
  • @mentions of the person or team responsible for reviewing proposed changes.

Summary

Replace the XTest button-event scroll path with XInput2.1 smooth scroll valuators on the xf86-input-neko Xorg driver. This fixes the fundamental scrolling problem: XTest could only fire discrete "notch" events (~100px per click in Chromium), making it impossible to achieve smooth trackpad scrolling. With XI2.1, we get pixel-precise 1:1 mapping between client trackpad deltas and browser scroll pixels.

Problem

The old XTest approach (xf86PostButtonEvent with buttons 4/5) had a fixed scroll quantum — each button press scrolled ~100px in Chromium with no sub-notch precision. This meant:

  • pixelsPerNotch too low → fires many button clicks → scrolls too fast
  • pixelsPerNotch too high → needs lots of delta before firing → feels jumpy/unresponsive
  • No middle ground exists because each click always scrolls the same fixed amount

Solution

Use XInput2.1 scroll valuators which provide continuous, sub-pixel scroll precision:

xf86-input-neko driver (neko.c):

  • Add vertical/horizontal scroll valuator axes (3, 4) with SetScrollValuator
  • Handle NEKO_SCROLL (0x80) messages via xf86PostMotionEventM instead of button events
  • Change device type from XI_TOUCHSCREEN to XI_MOUSE so Chromium respects scroll valuators

Client (video.vue / base.ts):

  • Remove PIXELS_PER_TICK quantization and 100ms throttle
  • Send raw pixel deltas batched per requestAnimationFrame
  • Use document-level wheel listener with passive: false for reliable preventDefault()
  • Extend binary wheel message to include controlKey byte (length=5)

Config:

  • Enable xinput driver in neko.yaml with socket path
  • Use neko base image with XI2 scroll support

Test results

Programmatic tests confirm exact 1:1 pixel mapping:

Client deltaY Actual scrollY Per-event
3 (tiny trackpad) 3px 3.0px
10 (small) 10px 10.0px
50 (medium) 50px 50.0px
200 (fast swipe) 200px 200.0px

Depends on

Made with Cursor


Note

Medium Risk
Changes the low-level mouse wheel pipeline end-to-end (client event handling, binary protocol, and Xorg input driver), so regressions could break scrolling or input behavior across browsers/hosts.

Overview
Adds an XI2.1 smooth-scrolling path by extending xf86-input-neko with vertical/horizontal scroll valuators and handling a new NEKO_SCROLL message type via motion events, including switching the device type to XI_MOUSE.

Updates the web client wheel handling to prevent default scrolling via a non-passive wheel listener, batch raw deltas per requestAnimationFrame (removing quantization/throttle), and sends an updated wheel payload including a controlKey byte.

Enables the new input driver via neko.yaml configuration (input socket), and annotates the container build to track the required upstream neko/base image tag.

Written by Cursor Bugbot for commit 11c1ba1. This will update automatically on new commits. Configure here.

Replace the XTest button-event scroll path (fixed ~100px quantum per
click) with XInput2.1 scroll valuators on the xf86-input-neko driver.
This gives pixel-precise 1:1 mapping between client trackpad deltas
and browser scroll pixels, eliminating the jumpiness of discrete
scroll notches.

Changes:
- neko.c: add vertical/horizontal scroll valuator axes (3, 4) with
  SetScrollValuator; handle NEKO_SCROLL messages via
  xf86PostMotionEventM instead of button events; change device type
  from XI_TOUCHSCREEN to XI_MOUSE so Chromium respects valuators
- video.vue: remove PIXELS_PER_TICK quantization and 100ms throttle;
  send raw pixel deltas batched per rAF; use document-level wheel
  listener with passive:false for reliable preventDefault
- base.ts: extend wheel binary message to include controlKey byte
  (PayloadScrollWithCtrl, length=5)
- neko.yaml: enable xinput driver with socket path
- Dockerfile: use neko base image with XI2 scroll support

Requires: kernel/neko branch hiro/xi2-scroll
Made-with: Cursor
- Scope wheel listener to overlay element instead of document to avoid
  blocking scrolling in chat/emoji/files panels
- Store handler reference and remove it in beforeDestroy to prevent leaks
- Re-integrate scroll sensitivity setting (1-100 range, 50 = 1x)
- Remove unused scroll_x/scroll_y fields from xf86-input-neko driver
- Revert Dockerfile FROM to existing neko base tag for CI (pending neko#12)

Made-with: Cursor

FROM ghcr.io/kernel/neko/base:3.0.8-v1.4.0 AS neko
# ^--- now has event.SYSTEM_PONG with legacy support to keepalive
# TODO: update to xi2-scroll tag once kernel/neko#12 is merged and tagged
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will update this once neko PR is merged

- Cancel any pending requestAnimationFrame in beforeDestroy to prevent
  accessing torn-down Vue instance
- Restore sendMousePos call in onWheel so cursor position is synced
  before scroll events (trackpad scrolling doesn't fire mousemove)

Made-with: Cursor
The default scroll setting is 10, so the divisor should be 10 (not 50)
to produce 1x sensitivity at the default value. Range: 1=0.1x to 100=10x.

Made-with: Cursor
Call preventDefault() before the locked check so the page doesn't
scroll underneath the video overlay when hosting but locked.

Made-with: Cursor
Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

- Subtract sent dx/dy from accumulators instead of resetting to zero,
  preserving fractional remainders for accurate sensitivity scaling
- Use assignment for ctrl flag to reflect latest event state, avoiding
  false positives from transient ctrl events within a RAF batch

Made-with: Cursor
@hiroTamada hiroTamada requested review from Sayan- and rgarcia April 1, 2026 20:42
Copy link
Copy Markdown
Contributor

@rgarcia rgarcia left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔥

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants