Smooth scroll hardware report (MX Master 4, macOS): synthetic output bypasses OS wheel acceleration — fast spins travel less than with smoothing off
- Dominant language
- Rust
- Stars
- 21k
- Forks
- 675
- Avg merge
- 2d 5h
- Merged PRs (30d)
- 172
Description
## Summary
PR #975 shipped the smooth-scroll motion model with the note *"Not runtime-tested on hardware. Verify with `smooth_scroll = true`"* — this is that hardware verification, from an MX Master 4 on macOS (Sequoia 15.7, Mac Studio), wheel in permanent free-spin mode.
The plumbing works exactly as designed: per-source state, one balanced Began/Changed/Ended lifecycle, fail-open under pressure, absolute-timestamp evaluation — all solid, and `smooth_scroll = false` remains clean passthrough. But the **motion model itself makes scrolling feel much worse than either native or the commercial reference** (SmoothScroll.app): subjectively "too slow — takes too many turns of the wheel," and the measurements below show why.
## Measurements
Method: a listen-only CGEventTap at the annotated session tap (i.e. observing what applications actually receive), summing `pointDeltaAxis1` pixels — the OS fills this with its acceleration curve applied for line events, and it's the literal pixel distance for continuous events, so it's apples-to-apples "how far the page traveled." Same physical gestures repeated across three configurations, same machine/mouse/page.
**Fast gesture (three hard free-spin flicks, ~5 s):**
| Configuration | Output events | Distance delivered | Peak px/event |
|---|---|---|---|
| Native (no smoothing anywhere) | 201 line events (1,550 raw ticks) | **16,608 px** | 93 |
| OpenLogi `smooth_scroll = true` | 631 phased continuous | **9,640 px** | 128 |
| SmoothScroll.app 1.7.6 (defaults) | 180 continuous | **61,961 px** | 677 |
**Slow gentle scrolling (~10–12 s):** native ≈ 1,200 px, OpenLogi ≈ 3,000 px, SmoothScroll ≈ 6,100 px (slow gestures are harder to reproduce exactly, so treat these as indicative; the fast row is the controlled one).
Two things fall out of the fast row:
1. **Smoothing on travels 42% *less* than smoothing off.** Native line ticks get macOS's wheel acceleration (measured avg ~10.7 px/tick across the flick, ~9× on the fastest ticks). OpenLogi's synthetic continuous output bypasses that curve — by design of the CG event type, not a bug — and the model conserves raw tick distance (~8 px/tick, flat) while adding no acceleration of its own. So the harder you spin, the further behind native you fall. Enabling the feature reduces scroll throughput.
2. **The commercial reference travels ~6.4× further on the same gesture.** SmoothScroll.app also bypasses OS acceleration (it emits phaseless continuous events too) — but it replaces it: a per-tick **step size** (90 px default) and a **tick-rate acceleration** (ticks arriving within 70 ms compound, capped at 7×). That's the entire gap.
A second, smaller gap: the 100 ms smoothstep with a hard stop reads as an abrupt halt. SmoothScroll's curve (Michael Herf's published "pulse": brief damped-force rise, long exponential viscous tail — https://stereopsis.com/stopping/) runs ~360 ms with a tail-to-head ratio of 3, so successive flicks blend into a glide and stops decay naturally.
## Proposal
Keep everything about the current architecture (per-source map, single output lifecycle, worker/queue, trackpad rejection, finite conserve-distance evaluation from absolute timestamps). Change only the per-tick motion math, roughly:
- **Step size:** each wheel tick animates `step` px instead of its raw ~8 px (default in the 60–90 px range).
- **Tick-rate acceleration:** ticks arriving within `accel_window` of the previous tick scale super-linearly, capped at `accel_max`. This restores (and can exceed) the OS acceleration the synthetic output bypasses. Needs per-source state so independent producers don't cross-contaminate (the issue that helped sink #1012).
- **Pulse-shaped finite curve, ~300–400 ms:** replace the 100 ms smoothstep with Herf's two-phase pulse (damped rise + exponential tail). Still finite, still conserves net distance, still evaluated from absolute timestamps — overlapping ticks compound into a glide, and "momentum" is just the tail, no separate mechanism.
- **Config surface:** `smooth_scroll_step`, `smooth_scroll_duration`, `smooth_scroll_acceleration` (+ window), with 3–4 named presets in the GUI (Snappy / Balanced / Glide) and `smooth_scroll = true` continuing to mean the default preset, so existing configs don't break.
Reference constants known-good on hardware (SmoothScroll.app defaults, years of daily use): step 90 px, 360 ms, accel window 70 ms, accel max 7×, tail:head 3.
Licensing note: the model is implementable from Herf's published algorithm description and its MIT-licensed reference implementation (gblazex/smoothscroll-for-websites); nothing needs to come from NC-licensed projects (Mos et al.).
## Scope notes
- Trackpad input stays native (current typed rejection already handles this).
- Possible follow-up, not part of this proposal: a `smooth_scroll_excluded_apps` list — animated synthetic events are known to break zoom-on-scroll apps and remote desktops (SmoothScroll ships a default exclusion list for this reason). Current behavior is fine to ship first.
- Windows hi-res wheel deltas (#884) are adjacent but orthogonal; nothing here changes per-platform injection.
## Offer
Happy to implement this as a PR series against the current code (core model + config first, GUI presets second), with deterministic unit tests in the existing style (synthetic tick traces: ratchet, hi-res free-spin bursts, reversal, sparse frames, cancellation, net-distance conservation). I have the hardware (MX Master 4 free-spin + MX Master 3S) and the commercial reference side-by-side for A/B tuning.
---
Contributor guide
No contributing guide indexed for this repository
Research direction
No file paths are named. Start by reviewing PR #975 and the current smooth-scroll motion model, including its per-source state, worker/queue, and absolute-timestamp evaluation; use the existing deterministic test style with synthetic tick traces for bursts, reversal, sparse frames, cancellation, and distance conservation. Verify smooth_scroll=true on the listed MX hardware, and consider the work done when the revised model, configuration, presets, and tests cover the stated behavior.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- desktop, performance
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 38/100