X server pointer acceleration analysis – part 1
X server pointer acceleration analysis - part 1


This post is part of a four part series:
Part 1,
Part 2,
Part 3,
Part 4.


Over the last few days, I once again tried to tackle pointer
acceleration. After all, I still get plenty of complaints about how terrible
libinput is and how the world was so much better without it. So I once more
tried to understand the X server's pointer
acceleration code. Note: the evdev driver doesn't do any acceleration, it's
all handled in the server. Synaptics will come in part two, so this here focuses mostly on pointer
acceleration for mice/trackpoints.

After a few failed attempts of live analysis [1], I finally succeeded extracting the pointer
acceleration code into something that could be visualised. That helped me a
great deal in going back and understanding the various bits and how they fit
together.

The approach was: copy the ptrveloc.(c|h) files into a new project, set up
a meson.build file, #define all the bits that are assumed to be there and
voila, here's your library. Now we can build basic analysis tools provided
we initialise all the structs the pointer accel code needs correctly. I think
I succeeded. The git repo is here if anyone
wants to check the data. All scripts to generate the data files are in the
repository.

A note on language: the terms "speed" and "velocity" are subtly different but
for this post the difference doesn't matter. The code uses "velocity" but "speed" is
more natural to talk about, so just assume equivalence.

The X server acceleration code

There are 15 configuration options for pointer acceleration
(ConstantDeceleration, AdaptiveDeceleration, AccelerationProfile,
ExpectedRate, VelocityTrackerCount, Softening, VelocityScale, VelocityReset,
VelocityInitialRange, VelocityRelDiff, VelocityAbsDiff,
AccelerationProfileAveraging, AccelerationNumerator,
AccelerationDenominator, AccelerationThreshold). Basically, every number
is exposed as configurable knob. The acceleration code is a product of a
time when we were handing out configuration options like
participation medals at a children's footy tournament. Assume that for the
rest of this blog post, every behavioural description ends with "unless
specific configuration combinations apply". In reality, I think only four
options are commonly used: AccelerationNumerator, AccelerationDenominator,
AccelerationThreshold, and ConstantDeceleration. These four have immediate effects on
the pointer movement and thus it's easy to do trial-and-error configuration.

The server has different acceleration profiles (called the 'pointer transfer
function' in the literature). Each profile is a function that converts
speed into a factor. That factor is then combined with other things like 
constant deceleration, but eventually our output delta forms as:
deltaout(x, y) = deltain(x, y) * factor * deceleration

The output delta is passed back to the server and the pointer saunters
over by few pixels, happily bumping into any screen edge on the way. 

The input for the acceleration profile is a speed in mickeys,
a threshold (in mickeys) and a max accel factor (unitless). Mickeys are a bit
tricky. This means the acceleration is device-specific, the
deltas for a mouse at 1000 dpi are 20% larger than the deltas for a mouse at
800 dpi (assuming same physical distance and speed). The "Resolution" option in evdev
can work around this, but by default this means that the acceleration factor
is (on average) higher for high-resolution mice for the same physical
movement. It also means that that xorg.conf snippet you found on
stackoverflow probably does not do the same on your device.

The second problem with mickeys is that they require a frequency to map to a
physical speed. If a device sends events every N ms, delta/N gives us a
speed in units/ms. But we need mickeys for the profiles. Devices
generally have a fixed reporting rate and the speed of each mickey is the same as
(units/ms * reporting rate). This rate defaults to 10 in the server
(the VelocityScaling default value) and thus matches a device
reporting at 100Hz (a discussion of this comes later). All graphs below were
generated with this default value.

Back to the profile function and how it works: The threshold
(usually) defines the mimimum speed at which acceleration kicks in. The
max accel factor (usually) limits the acceleration. So the simplest
algorithm is
 if (velocity < threshold)
     return base_velocity;
 factor = calculate_factor(velocity);
 if (factor > max_accel)
     return max_accel;
 return factor;

In reality, things are somewhere between this simple and "whoops, what have we done".

Diagram generation

Diagrams were generated by gnuplot, parsing .dat files generated by the
ptrveloc tool in the git repo. Helper scripts to regenerate all data
are in the repo too. Default values unless otherwise specified:

 threshold: 4
 accel: 2
 dpi: 1000 (used for converting units to mm)
 constant deceleration: 1
 profile: classic

All diagrams are limited to 100 mm/s and a factor of 5 so they are directly comparable.
From earlier testing I found movements above over 300 mm/s are rare, once you hit 500 mm/s the
acceleration doesn't really matter that much anymore, you're going to hit the screen
edge anyway.

Acceleration profiles

The server provides a number of profiles, but I have seen very little evidence
that people use anything but the default "Classic" profile. Synaptics installs a
device-specific profile. Below is a comparison of the profiles just so you
get a rough idea what each profile does. For this post, I'll focus on the default
Classic only.



First thing to point out here that if you want to have your pointer travel to
Mars, the linear profile is what you should choose. This profile is unusable
without further configuration to bring the incline to a more sensible level.
Only the simple and limited profiles have a maximum factor, all others
increase acceleration indefinitely. The faster you go, the more it
accelerates the movement. I find them completely unusable at anything but
low speeds.

The classic profile transparently maps to the simple profile, so the curves are identical.

Anyway, as said above, profile changes are rare. The one we care about is
the default profile: the classic profile which transparently maps to the
simple profile (SimpleSmoothProfile() in the source).


Looks like there's a bug in the profile formula.
At the threshold value it jumps from 1 to 1.5 before the curve kicks in.
This code was added in ~2008, apparently no-one noticed this in a decade.

The profile has deceleration (accel factor < 1 and thus decreasing
the deltas) at slow speeds. This provides extra precision at slow speeds
without compromising pointer speed at higher physical speeds.

The effect of config options

Ok, now let's look at the classic profile and the configuration options.
What happens when we change the threshold?


First thing that sticks out: one of these is not like the others. The
classic profile changes to the polynomial profile at thresholds less than
1.0. *shrug* I think there's some historical reason, I didn't chase it up.

Otherwise, the threshold not only defines when acceleration starts
kicking in but it also affects steepness of the curve. So higher threshold
also means acceleration kicks in slower as the speed increases. It has no
effect on the low-speed deceleration.

What happens when we change the max accel factor? This factor is actually
set via the AccelerationNumerator and AccelerationDenominator options
(because floats used to be more expensive than buying a house). At
runtime, the Xlib function of your choice is XChangePointerControl().
That's what all the traditional config tools use (xset, your desktop
environment pre-libinput, etc.).


First thing that sticks out: one is not like the others. When max
acceleration is 0, the factor is always zero for speeds exceeding the
threshold. No user impact though, the server discards factors of 0.0 and
leaves the input delta as-is.

Otherwise it's relatively unexciting, it changes the maximum acceleration
without changing the incline of the function. And it has no effect on
deceleration. Because the curves aren't linear ones, they don't overlap 100%
but meh, whatever. The higher values are cut off in this view, but they just look like a larger version of the visible 2 and 4 curves.

Next config option: ConstantDeceleration. This one is handled outside of the
profile but at the code is easy-enough to follow, it's a basic multiplier
applied together with the factor. (I cheated and just did this in gnuplot
directly)


Easy to see what happens with the curve here, it simply stretches vertically
without changing the properties of the curve itself. If the deceleration is
greater than 1, we get constant acceleration instead.

All this means with the default profile, we have 3 ways of adjusting it.
What we can't directly change is the incline, i.e. the actual process of
acceleration remains the same.

Velocity calculation

As mentioned above, the profile applies to a velocity so obviously we need
to calculate that first. This is done by storing each delta and looking at
their direction and individual velocity. As long as the direction remains
roughly the same and the velocity between deltas doesn't change too much,
the velocity is averaged across multiple deltas - up to 16 in the default config.
Of course you can change whether this averaging applies, the max time
deltas or velocity deltas, etc. I'm honestly not sure anyone ever used any
of these options intentionally or with any real success.


Velocity scaling was explained above (units/ms * reporting rate). The default 
value for the reporting rate is 10, equivalent to 100Hz.
Of the 155 frequencies currently defined in 70-mouse.hwdb, only one
is 100 Hz. The most common one here is 125Hz, followed by 1000Hz followed by
166Hz and 142Hz. Now, the vast majority of devices don't have an entry in
the hwdb file, so this data does not represent a significant sample set. But
for modern mice, the default velocity scale of 10 is probably off between 25% 
and a factor 10. While this doesn't mean much for the local example
(users generally just move the numbers around until they're happy enough) it
means that the actual values are largely meaningless for anyone but those
with the same hardware.
Of note: the synaptics driver automatically sets VelocityScale to 80Hz. This
is correct for the vast majority of touchpads.


Epilogue

The graphs above show the X server's pointer acceleration for mice, trackballs
and other devices and the effects of the configuration toggles. I purposely did
not put any specific analysis in and/or comparison to libinput. That will come in a
future post.




[1] I still have a branch somewhere where the server prints yaml to the log
file which can then be extracted by shell scripts, passed on to python for
processing and ++++ out of cheese error. redo from start ++++

