This was triggered by a simple plan: I want a configuration interface in
libinput that provides a sliding scale from -1 to 1 to adjust a device's
virtual speed from slowest to fastest, with 0 being the default for that
device. A user should not have to worry about the accel mechanism itself,
which may be different for any given device, all they need to know is that
the setting -0.5 means "halfway between default and 'holy cow this moves
like molasses!'". The utopia is of course that for any given acceleration
setting, every device feels equally fast (or slow).
In order to do that, I needed the right knobs to tweak.

The code we currently have in libinput is pretty much 1:1 what's used in the
X server. The X server sports a lot more configuration options, but what we
have in libinput 0.4.0 is essentially what the default acceleration settings
are in X. Armed with the knowledge that any
#define is a potential knob for configuration I went to investigate. There
are two defines that are labelled as adjustible parameters:

DEFAULT_THRESHOLD, set to 0.4

DEFAULT_ACCELERATION, set to 2.0

But what do they mean, exactly? And what exactly does a value of 0.4
represent?[side-note: threshold was 4 until I took the constant multiplier out,
it's now 0.4 upstream and all the graphs represent that.]

Pointer acceleration is nothing more than mapping some input data to some
potentially faster output data. How much faster depends on how fast the
device moves, and to get there one usually needs a couple of steps.
The trick of course is to make it predictable, so that despite the
acceleration, your brain thinks that the visible cursor is an extension of
your hand at all speeds.

Let's look at a high-level outline of our pointer acceleration code:

calculate the velocity of the current movement

use that velocity to calculate the acceleration factor

apply accel to dx/dy

smoothen out the dx/dy to avoid abrupt changes between two events

Calculating pointer speed

We don't just use dx/dy as values, rather, we use the pointer velocity.
There's a simple reason for that: dx/dy depends on the device's poll
rate (or interrupt frequency). A device that polls twice as often sends half
the dx/dy values in each event for the same physical speed.

Calculating the velocity is easy: divide dx/dy by the delta time.
We use a set of "trackers" that store previous dx/dy values with their
timestamp. As long as we get movement in the same
cardinal direction, we take those into account. So if we have 5 events in
direction NE, the speed is averaged over those 5 events, smoothing out
abrupt speed changes.

The acceleration function

The speed we just calculated is passed to the acceleration function
to calculate an acceleration factor.

Figure 1: Mapping of velocity in unit/ms to acceleration factor
(unitless). X axes here are labelled in units/ms and mm/s.

This function is the only place where DEFAULT_THRESHOLD/DEFAULT_ACCELERATION
are used, but they mostly just stretch the graph. The shape stays
the same.

The output of this function is a unit-less acceleration factor that is
applied to dx/dy. A factor of 1 means leaving dx/dy untouched, 0.5 is
half-speed, 2 is double-speed.

Let's look at the graph for the accel factor output (red): for very slow
speeds we have an acceleration factor < 1.0, i.e. we're slowing things
down. There is a distinct plateau up to the threshold of 0.4, after that it
shoots up to roughly a factor of 1.6 where it flattens out a bit until we
hit the max acceleration factor

Now we can also put units to the two defaults: Threshold is clearly
in units/ms, and the acceleration factor is simply a maximum. Whether those
are mentally easy to map is a different question.

We don't use the output of the function as-is, rather we smooth it out using
the Simpson's rule. The second (green) curve shows the accel factor after the smoothing
took effect. This is a contrived example, the tool to generate this data
simply increased the velocity, hence this particular line. For more random
data, see Figure 2.

Figure 2: Mapping of velocity in unit/ms to acceleration factor
(unitless) for a random data set. X axes here are labelled in units/ms and mm/s.

For the data set, I recorded the velocity from libinput while using
Firefox a bit.

The smoothing takes history into account, so the data points we get depend
on the usage. In this data set (and others I tested) we see that the
majority of the points still lie on or close to the pure function,
apparently the delta doesn't matter that much. Nonetheless, there are a few
points that suggest that the smoothing does take effect in some cases.

It's important to note that this is already the second smoothing to take
effect - remember that the velocity (may) average over multiple events and
thus smoothens the input data. However, the two smoothing effects somewhat
complement each other: velocity smoothing only happens when the pointer moves
consistently without much change, the Simpson's smoothing effect is most
pronounced when the pointer moves erratically.

Pointer speed mappings

The graph was produced by sending 30 events with the same constant speed,
then dividing by the number of events to reduce any effects tracker feeding
has at the initial couple of events.

The two lines show the actual output speed in mm/s and the gain in mm/s,
i.e. (output speed - input speed). We can see that the little nook where
the threshold kicks in and after the acceleration is linear. Look at Figure
1 again: the linear acceleration is caused by the acceleration factor
maxing out quickly.

Most of this graph is theoretical only though. On your average mouse you
don't usually get a delta greater than 10 or 15 and this graph covers the
theoretical range to 127. So you'd only ever be seeing the effect of up to
~120 mm/s. So a more realistic view of the graph is:

Same data as Figure 3, but zoomed to the realistic range.
We go from a linear speed increase (no
acceleration) to a quick bump once the threshold is hit and from then on to
a linear speed increase once the maximum acceleration is hit.

And to verify, the ratio of output speed : input speed:

Figure 5: Mapping of the unit-less gain of raw unaccelerated dx to accelerated dx,
i.e. the ratio of accelerated:unaccelerated.

Looks pretty much exactly like the pure acceleration function, which is
to be expected. What's important here though is that this is the effective
speed, not some mathematical abstraction. And it shows one limitation: we go
from 0 to full acceleration within really small window.

Again, this is the full theoretical range, the more realistic range is:

Figure 6: Mapping of the unit-less gain of raw unaccelerated dx to accelerated dx,
i.e. the ratio of accelerated:unaccelerated. Zoomed in to a max of 120 mm/s
(15 dx/event).

Same data as Figure 5, just zoomed in to a maximum of 120 mm/s. If we assume that 15
dx/event is roughly the maximum you can reach with a mouse you'll see that
we've reached maximum acceleration at a third of the maximum speed and the
window where we have adaptive acceleration is tiny.

Tweaking threshold/accel doesn't do that much. Below are the two graphs
representing the default (threshold=0.4, accel=2), a doubled threshold
(threshold=0.8, accel=2) and a doubled acceleration (threshold=0.4, accel=4).

Figure 9: Mapping raw unaccelerated dx to accelerated dx on a fixed
random data set, zoomed in to events 450-550 of that set.

This is more-or-less random movement reflecting some real-world usage. What
I find interesting is that it's very hard to see any areas where smoothing
takes visible effect. the accelerated curve largely looks like a stretched
input curve. tbh I'm not sure what I should've expected here and how to read
that, pointer acceleration data in real-world usage is notoriously hard to
visualize.

Summary

So in summary: I think there is room for improvement. We have no acceleration up
to the threshold, then we accelerate within too small a window. Acceleration
stops adjusting to the speed soon. This makes us lose precision and small
speed changes are punished quickly.

Increasing the threshold or the acceleration factor doesn't do that much.
Any increase in acceleration makes the mouse faster but the adaptive window
stays small. Any increase in threshold makes the acceleration kick in later,
but the adaptive window stays small.

We've already merged a number of fixes into libinput, but some more
work is needed. I think that to
get a good pointer acceleration we need to get a larger adaptive window
[Citation needed]. We're currently working on that (and figuring out how to
evaluate whatever changes we come up with).

A word on units

The biggest issue I was struggling with when trying to understand the code
was that of units. The code didn't document used units anywhere but it turns out that
everything was either in device units ("mickeys"), device units/ms or (in the case of
the acceleration factors) was unitless.

Device units are unfortunately a pretty useless base entity, only slightly more
precise than using the length of a piece of string. A device unit depends
on the device resolution and of course that differs between
devices. An average USB mouse tends to have 400 dpi (15.75 units/mm) but
it's common to have 800 dpi, 1000 dpi and gaming mice go up to 8200dpi. A touchpad
can have resolutions of 1092 dpi (43 u/mm), 3277 dpi (129 u/mm), etc. and
may even have different resolutions for x and y.

This explains why until commit e874d09b4 the touchpad felt slower than a
"normal" mouse. We scaled to a magic constant of 10 units/mm, before hitting the
pointer acceleration code. Now, as said above the mouse would likely have a
resolution of 15.75 units/mm, making it roughly 50% faster. The acceleration
would kick in earlier on the mouse, giving the touchpad and the mouse not
only different speeds but a different feel altogether.

Unfortunately, there is not much we can do about mice feeling different
depending on the resolution. To my knowledge there is no way to query the
resolution on a device. But for absolute devices that need pointer
acceleration (i.e. touchpads) we can normalize to a fake resolution of 400
dpi and base the acceleration code on that. This provides the same feel on
the mouse and the touchpad, as much as that is possible anyway.