Pointer acceleration bugs
I finally found a few hours to look into the libinput pointer acceleration code and decided to dump my notes here, short of having some other place that's convenient. In short, I found fairly obvious bugs in all but the touchpad pointer acceleration...
The below applies to bafffb7e.
This is not a discussion on whether the pointer acceleration curve/profile is good or not, we're assuming that the intended approach makes sense and are just looking for bugs. Discussions on other accel methods need to happen elsewhere.
libinput pointer acceleration
The pointer acceleration code is abstracted away into the various filter*
files and functions. Entry point is always filter_dispatch()
which is the
actual "turn this into accelerated delta" function. The output is always a delta normalized to 1000dpi.
Filters vs profiles
As a general rule, libinput differs between a "filter" and a "profile". The profile is merely a function that converts velocity to a unitless factor (e.g. "40 units/mm mean factor 2"). The filter takes in a delta in device-native units (and a timestamp) and returns a modified and normalized (to 1000dpi) delta. The filter may use the profile function to modify the delta (e.g. flat acceleration doesn't need to).
All acceleration filters have const
equivalent (entry point: filter_dispatch_constant()
) which is supposed to
return the delta without acceleration applied but in the same coordinate
system. In theory, constant and accel should be identical whenever the accel
function applies a factor 1.
We have four device types that care about acceleration: mice, touchpads, trackpoints and tablets (in relative mode)
Filter implementations
We have several backends that may use acceleration
- fallback (mouse and trackpoints) which passes data in device float coordinates to the filter code
- touchpad which passes device-coordinates coordinates to the filter code. Where xres != yres, yres is scaled to xres so the filter code only needs to deal with one resolution.
- tablets: pass the delta as-is in device coordinates to the filter code
So correctly all four device types pass device coordinates in their native units to the filter code. Motion to filters is always dispatched with the event timestamp.
We have multiple acceleration functions:
- "flat" for mice + trackpoints
- "linear" the
adaptive
filter as named in the API like this - "trackpoint" which is the default accel for trackpoints (the adaptive one)
- "low-dpi" the default (adaptive) one for mice < 1000dpi
- "touchpad flat" for touchpads
- "x230" which is an adaptive one for just the lenovo x230 (ignoring this one here, not touching it)
- "touchpad" which is the adaptive one for touchpads
- this one may be initialized with some smoothing values for bluetooth touchpads
- "tablet" for tablets only
The device to filter table is:
Device Type | Adaptive | Flat |
---|---|---|
mouse | linear |
flat |
low-dpi mouse | low_dpi |
flat |
trackpoint | trackpoint |
flat |
touchpad | touchpad |
touchpad-flat |
tablet | tablet |
n/a |
The return values of the filter is a normalized coordinate pair that is sent to the client as delta.
The flat filter
The flat filter simply multiplies the incoming delta in device coordinates with the given fixed value. The value goes from 0 to 2 on the assumption that these are mouse deltas and an acceleration of >2 results in skipping every second pixel, making interaction difficult/pointless.
It doesn't have a profile function since it's a flat filter.
The const
equivalent normalizes the data - buggy.
Bugs:
-
Normalizing makes the two values incompatible - it probably didn't matter since most mice are 1000dpi?? The touchpad-flat filter does this correctly but had a similar bug fixed in 92233df5. -
users that wants flat pointer acceleration likely want the same 1:1 behaviour between physical and virtual movement. Having the factor apply to pointer motion but not scrolling (the constant bits) means that's off and worse, there's no way to adjust that.
The trackpoint filter
Unlike mice (but like the flat profile), trackpoints don't normalize data since there's nothing to normalize against.
The trackpoint 'adaptive' filter has a custom trial-and-error curve that provides some acceleration factor. That's combined with the multiplier to provide the delta. The curve is just that, a curve, it's flat enough that having this as a linear one might work too.
For the constant the factor is simply dropped but the magic multiplier still happens.
This filter seems correct.
Bugs:
-
Where a trackpoint is using the flat profile, the trackpoint multiplier is not used, see #796 This can make the trackpoint pretty useless. Probably needed here: splitting the flat filter into a trackpoint-flat filter
The touchpad flat filter
The touchpad flat filter implementation normalizes the touchpad DPI and then returns the resulting value, times the magic slowdown. For the const filter the same is true but without the accel factor.
The slowdown is to roughly 30% of actual speed, that's a trial-and-error value.
Bugs:
-
users that want flat pointer acceleration likely want the same 1:1 behaviour between physical and virtual movement. Having the factor apply to pointer motion but not scrolling (the constant bits) means that's off and worse, there's no way to adjust that.
The mouse filter
The mouse filter takes in device coordinates and converts it to normalized coords - the acceleration profile is then applied to those normalized coordinates (i.e. the trackers and calculated velocity is all in normalized coords).
This filter makes use of trackers which are just a tuple of unaccelerated deltas + timestamps. The tracker is fed with the current delta and the velocity calculation is done as the velocity across multiple deltas that are "similar" (i.e. didn't change direction significantly and didn't change "too much").
Except that most of this code is a noop anyway because unless the "AttrUseVelocityAveraging" quirk is set we have a grand total of 2 trackers, i.e. we don't actually average. We don't ship that quirk for any device.
Anyway, we take that velocity and apply the pointer accel profile func to it - using the Simpson's rule to smooth from the last velocity. So we sort-of average twice - once because we calculate the velocity across multiple deltas (except we don't, see above...) and then we smooth the returned factor.
The profile func itself normalizes the delta - again! buggy! - and applies a fairly simple linear-ish function. That function has a plateau so there's a speed range where the accel factor is 1.0.
The constant function returns just the normalized coordinates without any factors applied. That is correct behaviour. Notably: this one also doesn't feed the trackers to keep the motion history separate between accelerated/nonaccelerated motion.
Bugs:
-
the tracker velocity diff is in units/us but we are using normalized values -
normalized twice: we pre-normalize the data, pass that to the profile which normalizes it again. For a 2500dpi mouse this means the speed is actually 0.4 * speed
, so the curve is massively stretched, the deceleration will apply to a larger range and acceleration kicks in much later.
The touchpad filter
The touchpad filter takes in device coordinates and uses the same approach as the mouse filter, except it doesn't normalize before acceleration. So the trackers etc. are all in device units, not normalized coords. Otherwise it's the same with the tracker velocity + speed calculation.
The profile func does the conversion to normalized units before doing the factor calculation. The factor always has the magic slowdown applied, so this looks to be correct.
There's a baseline for an accel factor, except that for historical reasons
that baseline is actually at 0.9 (not 1.0) and the factor has the
TP_MAGIC_SLOWDOWN
applied anyway (so actually at 0.9 * 0.29).
The const filter normalizes and applies that same 0.9 * 0.29 factor to provide data similar to that baseline. That appears to be correct.
This should be fixed by incorporating everything into the function correctly, but that's likely to break other things so lets not.
The low-dpi filter
The low-dpi filter only applies to devices with MOUSE_DPI
less than 1000.
That's a few trackballs for example.
The filter takes in device units and accelerates the device units with the same tracker + simpsons accel as the mouse/touchpad code. The return value is never normalized, so a 1/1 from the 400dpi device at accel factor ends up at 1/1. That was the point of the low-dpi, initially everything got scaled but then low-dpi mice won't function well since they get scaled up.
The profile takes the DPI into account to calculate when acceleration kicks in but it has the same plateau with a factor 1.
The constant function normalizes the data - buggy.
Bugs:
-
the tracker velocity diff is in units/us -
Normalizing makes the two values incompatible, what would be a 1/1 pointer motion becomes a 2.5/2.5 scoll motion on a MOUSE_DPI
400 device (1000/4000).