summaryrefslogtreecommitdiff
path: root/machines
diff options
context:
space:
mode:
Diffstat (limited to 'machines')
-rw-r--r--machines/common/default.nix2
-rw-r--r--machines/common/libinputcurve.nix105
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.
+ };
+}