feat: XI2.1 smooth scrolling via xf86-input-neko scroll valuators#196
feat: XI2.1 smooth scrolling via xf86-input-neko scroll valuators#196hiroTamada wants to merge 7 commits intomainfrom
Conversation
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
images/chromium-headful/Dockerfile
Outdated
|
|
||
| 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 |
There was a problem hiding this comment.
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
- 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
## Summary Enable pixel-precise smooth scrolling by sending raw pixel deltas through the `xf86-input-neko` xinput driver instead of quantizing them into discrete XTest button events. ### Changes **`server/internal/desktop/xorg.go`** — `Scroll()` now sends raw pixel deltas directly to the xinput driver via Unix socket. Falls back to XTest if the driver connection fails. **`server/internal/webrtc/legacyhandler.go`** — Support `PayloadScrollWithCtrl` (length=5) binary message format that includes a `controlKey` byte alongside `deltaX`/`deltaY`. **`server/pkg/xinput/`** — Add `NEKO_SCROLL` (0x80) message type and `Scroll(deltaX, deltaY int32)` method to send scroll events over the Unix socket to the Xorg driver. **`server/pkg/xorg/xorg.c`** — Fix XTest fallback scroll direction (button 4/5 mapping was inverted). **`server/internal/config/desktop.go`** — Add `CDPScrollURL` config option (unused by default, for optional CDP scroll bypass). **`server/pkg/cdpscroll/`** — Optional CDP-based scroll client (gated behind `CDPScrollURL` config, not active by default). ### Context Used by [kernel/kernel-images#196](kernel/kernel-images#196) which extends the `xf86-input-neko` Xorg driver with XI2.1 scroll valuators. Together, this achieves 1:1 pixel mapping between client trackpad deltas and browser scroll. Made with [Cursor](https://cursor.com) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes desktop input/scroll handling (including modifier behavior and X11 button mapping), which can impact core interaction and has platform-specific edge cases. Failover paths mitigate risk but need validation across driver/no-driver setups. > > **Overview** > Adds XI2.1 *smooth/pixel-precise* scrolling support by routing scroll deltas through the `xinput` Unix-socket driver when `desktop.input.enabled` is set, including temporarily setting the Ctrl modifier before posting the event and falling back to XTest on driver errors. > > Extends the legacy WebRTC data-channel scroll payload to optionally include a Ctrl flag (`Length == 5`) while keeping the existing scroll format for compatibility, and updates logging accordingly. > > Expands `server/pkg/xinput` with a new `NEKO_SCROLL` message and `Driver.Scroll()` API (dummy returns a not-connected error), and fixes the XTest fallback scroll direction mapping in `server/pkg/xorg/xorg.c`. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit ae3594a. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
…ivity - Bump neko base image to 3.0.8-v1.5.0 (XI2.1 smooth scrolling support) - Default scroll_invert to false (works with --disable-features=ScrollLatching) - Add useDefineForClassFields: false to tsconfig for Vue 2 compatibility - Use document-level wheel listener with capture: true for reliable interception - Send scroll deltas directly without rAF batching or accumulation Made-with: Cursor
|
Note: With XI2.1 smooth scrolling, Chromium's scroll latching prevents nested scrolling from working (scroll events get "latched" to the first scrollable element and never propagate to parent containers). This is because XI2.1 continuous scroll events lack gesture phase information ( The fix is to pass This should be added to the default flags in |
|
@rgarcia heads up — a few changes since your approval:
Net diff is now +10/-31 (simpler than before). |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
| this.$client.sendData('wheel', { x, y }) | ||
| const sensitivity = this.scroll / 10 | ||
| const dx = Math.max(-32767, Math.min(32767, Math.round(x * sensitivity))) | ||
| const dy = Math.max(-32767, Math.min(32767, Math.round(y * sensitivity))) |
There was a problem hiding this comment.
Small scroll deltas silently lost at low sensitivity
Medium Severity
At low scroll settings (1–4, i.e. sensitivity 0.1x–0.4x), Math.round(x * sensitivity) rounds small trackpad deltas to zero, silently dropping them. For example, at scroll=1 a deltaY of 4px yields round(0.4)=0 — the event is discarded entirely. The old code guaranteed at least one tick via || Math.sign(x). The PR discussion explicitly states accumulators were added in commit 11c1ba1 to preserve fractional remainders, but they are absent from the final code, so repeated small gestures never accumulate into a scroll action.
There was a problem hiding this comment.
Not a concern in practice. The default scroll setting is 10 (sensitivity 1.0x), so deltas pass through unscaled. You'd need scroll set to 1–4 and sub-3px trackpad deltas for anything to be dropped — and at those settings, swallowing tiny deltas is arguably the intended behavior.
The || Math.sign(x) guarantee mentioned here didn't exist in the original code on main — the original onWheel sent raw deltas without sensitivity scaling.


Checklist
ghcr.io/kernel/neko/base:xi2-scrollimage to be publishedSummary
Replace the XTest button-event scroll path with XInput2.1 smooth scroll valuators on the
xf86-input-nekoXorg 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 (
xf86PostButtonEventwith buttons 4/5) had a fixed scroll quantum — each button press scrolled ~100px in Chromium with no sub-notch precision. This meant:pixelsPerNotchtoo low → fires many button clicks → scrolls too fastpixelsPerNotchtoo high → needs lots of delta before firing → feels jumpy/unresponsiveSolution
Use XInput2.1 scroll valuators which provide continuous, sub-pixel scroll precision:
xf86-input-neko driver (
neko.c):SetScrollValuatorNEKO_SCROLL(0x80) messages viaxf86PostMotionEventMinstead of button eventsXI_TOUCHSCREENtoXI_MOUSEso Chromium respects scroll valuatorsClient (
video.vue/base.ts):PIXELS_PER_TICKquantization and 100ms throttlerequestAnimationFramepassive: falsefor reliablepreventDefault()controlKeybyte (length=5)Config:
neko.yamlwith socket pathTest results
Programmatic tests confirm exact 1:1 pixel mapping:
Depends on
kernel/neko@hiro/xi2-scroll— server-side changes to send raw pixel deltas to xinput driverMade with Cursor
Note
Medium Risk
Touches the end-to-end input path (client wheel handling, binary protocol framing, and the Xorg
xf86-input-nekodriver), so regressions could break scrolling or other pointer interactions. Changes are localized but depend on consistent protocol/driver behavior across environments.Overview
Switches scrolling from the old discrete wheel/button-style behavior to XI2.1 smooth scrolling by sending raw wheel deltas through to new scroll valuators in the
xf86-input-nekoXorg driver.On the client, wheel capture is moved to a document-level non-passive listener (to reliably
preventDefault), scroll deltas are no longer quantized/throttled, andwheelmessages now include acontrolKeyflag; thescroll_invertdefault is flipped tofalse.On the runtime side, the image is bumped to
ghcr.io/kernel/neko/base:3.0.8-v1.5.0,neko.yamlenables the input socket, and the Xorg driver adds vertical/horizontal scroll axes (valuators 3/4), handlesNEKO_SCROLLvia motion events, and reports the device asXI_MOUSE.Written by Cursor Bugbot for commit c2fea7d. This will update automatically on new commits. Configure here.