diff options
Diffstat (limited to 'machines/common')
| -rw-r--r-- | machines/common/default.nix | 2 | ||||
| -rw-r--r-- | machines/common/libinputcurve.nix | 105 |
2 files changed, 106 insertions, 1 deletions
diff --git a/machines/common/default.nix b/machines/common/default.nix index df8e60c..9af7dc0 100644 --- a/machines/common/default.nix +++ b/machines/common/default.nix @@ -2,11 +2,11 @@ { imports = [ ./base.nix + ./libinputcurve.nix ./pipewire.nix ./printer.nix ./rip.nix ./tailscale.nix ./user.nix - ./x11.nix ]; } diff --git a/machines/common/libinputcurve.nix b/machines/common/libinputcurve.nix new file mode 100644 index 0000000..8fefb9e --- /dev/null +++ b/machines/common/libinputcurve.nix @@ -0,0 +1,105 @@ +{ ... }: +# macOS trackpad pointer-acceleration curve, transplanted onto libinput. +# +# WHERE THE CURVE COMES FROM +# +# macOS accelerates one pointer report as (IOHIDFamily, IOHIDPointerAccelerator): +# +# velocity = floor(hypot(dx, dy)) * rate_multiplier +# mult = curve(velocity / (resolution / 67)) * (96/67) / velocity +# +# where curve() is a quartic up to TangentSpeedLinear, its tangent line up to +# TangentSpeedParabolicRoot, and the square root of a line beyond that. +# +# The coefficients below were recovered by fitting that form to ~2200 pointer +# reports captured from a real Apple trackpad (see ~/Downloads/code/rev), using +# the cursor's 1/256-point position deltas as ground truth rather than the +# integer CGEvent delta, which is rounded and cannot resolve better than +/-1: +# +# GainLinear 0.961648 TangentSpeedLinear 7.090 +# GainParabolic 0.829395 TangentSpeedParabolicRoot 17.978 +# GainCubic 0.122907 resolution 400 dpi @ 120 Hz +# GainQuartic 0 tracking index ~0.6875 +# +# The fit reproduces every measured velocity bin (v = 1..48 device units per +# report) to an rms of 2.4e-4 in gain. TangentSpeedParabolicRoot is the one +# value NOT measured -- the captures top out at 14.4 in/s, well below the +# roll-off -- so it is taken from the reported 32.2 in/s roll-off. It is +# corroborated: it puts the factor peak at 38.0 in/s against a reported ~40. +# +# TRANSLATION TO LIBINPUT +# +# libinput's custom profile has the same shape as Apple's algorithm -- both map +# an input speed to an output speed and use the ratio as the factor -- so the +# curve transplants directly. Only the units differ. libinput's x axis is +# *device units per millisecond* at the touchpad's own resolution (verified: +# tp_filter_motion passes x through raw, scaling only y for axis aspect), so +# the points below are specific to a 609.6 dpi (24 units/mm) touchpad. +# +# !! These numbers are tied to that resolution. magician's PIXA3854 is +# !! 609.6 dpi. A touchpad with a different resolution needs the table +# !! regenerated, or the curve will be wrong by the ratio of resolutions. +# !! Check with: libinput list-devices, or EVIOCGABS on the event node. +# !! To scope this to one machine instead, set services.libinput.touchpad.dev. +# +# WHAT DOES NOT TRANSFER +# +# Apple's floor() on velocity pins the gain flat below one device unit per +# report (< 0.30 in/s) and then steps it up 12% at exactly |d| = 1. libinput's +# f() is continuous and its points are uniformly spaced, so that discontinuity +# cannot be represented. What we get for free: f() runs linearly from the origin +# to the first knot, so the factor is constant across that first segment. The +# step below places the first knot at 0.3175 in/s -- essentially exactly Apple's +# 0.30 in/s plateau edge -- so the plateau's *width* is reproduced. Its value +# lands ~6% high, because continuity forbids the step. +# +# RANGE +# +# 64 points is libinput's hard maximum (libinput_config_accel_set_points rejects +# more), which forces a choice: uniform spacing means covering more speed costs +# low-speed resolution. 20 in/s is the sweet spot rather than a compromise -- +# past the last knot libinput extrapolates the final segment linearly, and +# Apple's curve is genuinely a straight line from 12.7 to 32.2 in/s, so the +# extrapolation is *exact* out to 30 in/s. Error only appears above the +# roll-off: +2.7% at 40 in/s, +16% at 60 in/s. Those are flick speeds where +# the pointer is being thrown across the screen anyway. +# +# Measured against the capture's own speed distribution: 0.22% weighted rms +# error, worst case 1.7% below 15 in/s. +let + # Overall speed trim. The curve's shape is independent of this; it scales + # cursor travel per finger inch uniformly. 1.0 maps one macOS point to one + # X11 pixel. Raise it if the pointer feels sluggish on a HiDPI screen that + # X is driving unscaled -- a macOS point is a *logical* unit, so an unscaled + # HiDPI X session covers less physical distance per point than macOS does. + gain = 1.0; + + # x = 0, step, 2*step, ... in touchpad device units per millisecond. + # step = 20 in/s / 63 intervals, at 609.6 dpi. + step = 0.193524; + + # f(x), the accelerated speed. libinput uses f(x)/x as the factor. + points = [ + 0.00000 0.03303 0.07349 0.12141 0.17680 0.23966 0.31000 0.38784 + 0.47319 0.56605 0.66644 0.77437 0.88985 1.01289 1.14350 1.28169 + 1.42748 1.58086 1.74186 1.91048 2.08674 2.27064 2.46220 2.66142 + 2.86832 3.08291 3.30520 3.53520 3.77291 4.01836 4.27155 4.53249 + 4.80119 5.07767 5.36193 5.65398 5.95384 6.26151 6.57702 6.90035 + 7.23154 7.56666 7.90177 8.23689 8.57201 8.90712 9.24224 9.57736 + 9.91247 10.24759 10.58270 10.91782 11.25294 11.58805 11.92317 12.25829 + 12.59340 12.92852 13.26364 13.59875 13.93387 14.26898 14.60410 14.93922 + ]; +in +{ + # services.libinput.enable defaults to services.xserver.enable, so this file + # is inert until X is turned on. + services.libinput.touchpad = { + accelProfile = "custom"; + accelStepMotion = step; + accelPointsMotion = map (p: p * gain) points; + + # Deliberately not setting accelSpeed: libinput ignores the speed setting + # entirely under the custom profile (custom_accelerator_set_speed only + # range-checks its argument and returns). The curve is the whole story. + }; +} |
