[PATCH libinput 2/4] filter: add a custom trackpoint accelerator

Peter Hutterer peter.hutterer at who-t.net
Wed Jul 12 06:06:16 UTC 2017


Switch to a pure factor with a max scaled after a function. The offset is just
0 now (will be removed eventually). Both are determined with a function based
on a linear/exponential regression of a sample set of data pairs.

Signed-off-by: Peter Hutterer <peter.hutterer at who-t.net>
---
 doc/pointer-acceleration.dox |  16 ++-
 src/evdev.c                  |   8 +-
 src/evdev.h                  |   1 +
 src/filter.c                 | 293 ++++++++++++++++++++++++++++++++++++-------
 src/filter.h                 |   5 +-
 tools/ptraccel-debug.c       |  38 +++++-
 6 files changed, 301 insertions(+), 60 deletions(-)

diff --git a/doc/pointer-acceleration.dox b/doc/pointer-acceleration.dox
index f6237c6b..57be6cdc 100644
--- a/doc/pointer-acceleration.dox
+++ b/doc/pointer-acceleration.dox
@@ -109,9 +109,16 @@ The image above shows the touchpad acceleration profile in comparison to the
 
 @section ptraccel-trackpoint Pointer acceleration on trackpoints
 
-Trackpoint hardware is quite varied in how it reacts to user pressure and
-unlike other devices it cannot easily be normalized for physical properties.
-Measuring pressure objectively across a variety of hardware is nontrivial.
+The main difference between trackpoint hardware and mice or touchpads is
+that trackpoint speed is a function of pressure rather than moving speed.
+But trackpoint hardware is quite varied in how it reacts to user pressure
+and unlike other devices it cannot easily be normalized for physical
+properties. Measuring pressure objectively across a variety of hardware is
+nontrivial.
+
+libinput's pointer acceleration is a function of the total available
+pressure range on a device.
+
 libinput relies on some sytem-wide configured properties, specifically the
 @ref udev_config. The two properties that influence trackpoint acceleration
 ````POINTINGSTICK_CONST_ACCEL```` which is a constant factor applied to the
@@ -125,9 +132,6 @@ builtin is expected to apply this to the device, i.e.  libinput does not
 handle this property. Once applied, the sensitivity adjusts the deltas
 coming out of the hardware.
 
-Trackpoint pointer acceleration uses the @ref ptraccel-low-dpi profile, with a
-constant acceleration factor taking the place of the DPI settings.
-
 @image html ptraccel-trackpoint.svg "Pointer acceleration curves for trackpoints"
 
 The image above shows the trackpoint acceleration profile in comparison to the
diff --git a/src/evdev.c b/src/evdev.c
index 0d861190..e07d5f7e 100644
--- a/src/evdev.c
+++ b/src/evdev.c
@@ -1990,7 +1990,7 @@ evdev_init_accel(struct evdev_device *device,
 	if (which == LIBINPUT_CONFIG_ACCEL_PROFILE_FLAT)
 		filter = create_pointer_accelerator_filter_flat(device->dpi);
 	else if (device->tags & EVDEV_TAG_TRACKPOINT)
-		filter = create_pointer_accelerator_filter_trackpoint(device->dpi);
+		filter = create_pointer_accelerator_filter_trackpoint(device->trackpoint_range);
 	else if (device->dpi < DEFAULT_MOUSE_DPI)
 		filter = create_pointer_accelerator_filter_linear_low_dpi(device->dpi);
 	else
@@ -2213,6 +2213,10 @@ evdev_get_trackpoint_dpi(struct evdev_device *device)
 	const char *trackpoint_accel;
 	double accel = DEFAULT_TRACKPOINT_ACCEL;
 
+	/*
+	 * parse the sensitivity property, and undo whatever it does.
+	 */
+
 	trackpoint_accel = udev_device_get_property_value(
 				device->udev_device, "POINTINGSTICK_CONST_ACCEL");
 	if (trackpoint_accel) {
@@ -2227,6 +2231,8 @@ evdev_get_trackpoint_dpi(struct evdev_device *device)
 		evdev_log_info(device, "set to const accel %.2f\n", accel);
 	}
 
+	device->trackpoint_range = 20; /* FIXME */
+
 	return DEFAULT_MOUSE_DPI / accel;
 }
 
diff --git a/src/evdev.h b/src/evdev.h
index b891f906..6524adf0 100644
--- a/src/evdev.h
+++ b/src/evdev.h
@@ -158,6 +158,7 @@ struct evdev_device {
 	bool is_mt;
 	bool is_suspended;
 	int dpi; /* HW resolution */
+	int trackpoint_range; /* trackpoint max delta */
 	struct ratelimit syn_drop_limit; /* ratelimit for SYN_DROPPED logging */
 	struct ratelimit nonpointer_rel_limit; /* ratelimit for REL_* events from non-pointer devices */
 	uint32_t model_flags;
diff --git a/src/filter.c b/src/filter.c
index 65e83197..022a7040 100644
--- a/src/filter.c
+++ b/src/filter.c
@@ -147,6 +147,12 @@ filter_get_type(struct motion_filter *filter)
 #define X230_MAGIC_SLOWDOWN 0.4			/* unitless */
 #define X230_TP_MAGIC_LOW_RES_FACTOR 4.0	/* unitless */
 
+/* Trackpoint acceleration */
+#define TRACKPOINT_DEFAULT_MAX_ACCEL 2.0	/* in units/us */
+#define TRACKPOINT_DEFAULT_MAX_DELTA 60
+/* As measured on a Lenovo T440 at kernel-default sensitivity 128 */
+#define TRACKPOINT_DEFAULT_RANGE 20		/* max value */
+
 /*
  * Pointer acceleration filter constants
  */
@@ -195,6 +201,20 @@ struct tablet_accelerator_flat {
 	       yres_scale; /* 1000dpi : tablet res */
 };
 
+struct trackpoint_accelerator {
+	struct motion_filter base;
+
+	struct device_float_coords history[4];
+	size_t history_size;
+
+	double scale_factor;
+	double max_accel;
+	double max_delta;
+
+	double incline; /* incline of the function */
+	double offset; /* offset of the function */
+};
+
 static void
 feed_trackers(struct pointer_accelerator *accel,
 	      const struct device_float_coords *delta,
@@ -904,38 +924,6 @@ touchpad_lenovo_x230_accel_profile(struct motion_filter *filter,
 	return factor * X230_MAGIC_SLOWDOWN / X230_TP_MAGIC_LOW_RES_FACTOR;
 }
 
-double
-trackpoint_accel_profile(struct motion_filter *filter,
-				void *data,
-				double speed_in, /* device units/µs */
-				uint64_t time)
-{
-	struct pointer_accelerator *accel_filter =
-		(struct pointer_accelerator *)filter;
-	double max_accel = accel_filter->accel; /* unitless factor */
-	double threshold = accel_filter->threshold; /* units/ms */
-	const double incline = accel_filter->incline;
-	double dpi_factor = accel_filter->dpi/(double)DEFAULT_MOUSE_DPI;
-	double factor;
-
-	/* dpi_factor is always < 1.0, increase max_accel, reduce
-	   the threshold so it kicks in earlier */
-	max_accel /= dpi_factor;
-	threshold *= dpi_factor;
-
-	/* see pointer_accel_profile_linear for a long description */
-	if (v_us2ms(speed_in) < 0.07)
-		factor = 10 * v_us2ms(speed_in) + 0.3;
-	else if (speed_in < threshold)
-		factor = 1;
-	else
-		factor = incline * v_us2ms(speed_in - threshold) + 1;
-
-	factor = min(max_accel, factor);
-
-	return factor;
-}
-
 struct motion_filter_interface accelerator_interface = {
 	.type = LIBINPUT_CONFIG_ACCEL_PROFILE_ADAPTIVE,
 	.filter = accelerator_filter_pre_normalized,
@@ -1063,30 +1051,245 @@ create_pointer_accelerator_filter_lenovo_x230(int dpi)
 	return &filter->base;
 }
 
+double
+trackpoint_accel_profile(struct motion_filter *filter,
+			 void *data,
+			 double delta)
+{
+	struct trackpoint_accelerator *accel_filter =
+		(struct trackpoint_accelerator *)filter;
+	const double max_accel = accel_filter->max_accel;
+	double factor;
+
+	delta = fabs(delta);
+
+	/* This is almost the equivalent of the xserver acceleration
+	   at sensitivity 128 and speed 0.0 */
+	factor = delta * accel_filter->incline + accel_filter->offset;
+	factor = min(factor, max_accel);
+
+	return factor;
+}
+
+/**
+ * Average the deltas, they are messy and can provide sequences like 7, 7,
+ * 9, 8, 14, 7, 9, 8 ... The outliers cause unpredictable jumps, so average
+ * them out.
+ */
+static inline struct device_float_coords
+trackpoint_average_delta(struct trackpoint_accelerator *filter,
+			 const struct device_float_coords *unaccelerated)
+{
+	size_t i;
+	struct device_float_coords avg;
+
+	memmove(&filter->history[1],
+		&filter->history[0],
+		sizeof(*filter->history) * (filter->history_size - 1));
+	filter->history[0] = *unaccelerated;
+
+	for (i = 0; i < filter->history_size; i++) {
+		avg.x += filter->history[i].x;
+		avg.y += filter->history[i].y;
+	}
+	avg.x /= filter->history_size;
+	avg.y /= filter->history_size;
+
+	return avg;
+}
+
+/**
+ * Undo any system-wide magic scaling, so we're behaving the same regardless
+ * of the trackpoint hardware. This way we can apply our profile independent
+ * of any other configuration that messes with things.
+ */
+static inline struct device_float_coords
+trackpoint_normalize_deltas(const struct trackpoint_accelerator *accel_filter,
+			    const struct device_float_coords *delta)
+{
+	struct device_float_coords scaled = *delta;
+
+	scaled.x *= accel_filter->scale_factor;
+	scaled.y *= accel_filter->scale_factor;
+
+	return scaled;
+}
+
+/**
+ * We set a max delta per event, to avoid extreme jumps once we exceed the
+ * expected pressure. Trackpoint hardware is inconsistent once the pressure
+ * gets high, so we can expect sequences like 30, 40, 35, 55, etc. This may
+ * be caused by difficulty keeping up high consistent pressures or just
+ * measuring errors in the hardware. Either way, we cap to a max delta so
+ * once we hit the high pressures, movement is capped and consistent.
+ */
+static inline struct normalized_coords
+trackpoint_clip_to_max_delta(const struct trackpoint_accelerator *accel_filter,
+			     struct normalized_coords coords)
+{
+	const double max_delta = accel_filter->max_delta;
+
+	if (abs(coords.x) > max_delta)
+		coords.x = copysign(max_delta, coords.x);
+	if (abs(coords.y) > max_delta)
+		coords.y = copysign(max_delta, coords.y);
+
+	return coords;
+}
+
+static struct normalized_coords
+trackpoint_accelerator_filter(struct motion_filter *filter,
+			      const struct device_float_coords *unaccelerated,
+			      void *data, uint64_t time)
+{
+	struct trackpoint_accelerator *accel_filter =
+		(struct trackpoint_accelerator *)filter;
+	struct device_float_coords scaled;
+	struct device_float_coords avg;
+	struct normalized_coords coords;
+	double f;
+	double delta;
+
+	scaled = trackpoint_normalize_deltas(accel_filter, unaccelerated);
+	avg = trackpoint_average_delta(accel_filter, &scaled);
+
+	delta = hypot(avg.x, avg.y);
+
+	f = trackpoint_accel_profile(filter, data, delta);
+
+	coords.x = avg.x * f;
+	coords.y = avg.y * f;
+
+	coords = trackpoint_clip_to_max_delta(accel_filter, coords);
+
+	return coords;
+}
+
+static struct normalized_coords
+trackpoint_accelerator_filter_noop(struct motion_filter *filter,
+				   const struct device_float_coords *unaccelerated,
+				   void *data, uint64_t time)
+{
+
+	struct trackpoint_accelerator *accel_filter =
+		(struct trackpoint_accelerator *)filter;
+	struct device_float_coords scaled;
+	struct device_float_coords avg;
+	struct normalized_coords coords;
+
+	scaled = trackpoint_normalize_deltas(accel_filter, unaccelerated);
+	avg = trackpoint_average_delta(accel_filter, &scaled);
+
+	coords.x = avg.x;
+	coords.y = avg.y;
+
+	coords = trackpoint_clip_to_max_delta(accel_filter, coords);
+
+	return coords;
+}
+
+static bool
+trackpoint_accelerator_set_speed(struct motion_filter *filter,
+				 double speed_adjustment)
+{
+	struct trackpoint_accelerator *accel_filter =
+		(struct trackpoint_accelerator*)filter;
+	double incline, offset, max;
+
+	assert(speed_adjustment >= -1.0 && speed_adjustment <= 1.0);
+
+	/* Helloooo, magic numbers.
+
+	   These numbers were obtained by finding an acceleration curve that
+	   provides precision at slow speeds but still provides a good
+	   acceleration at higher pressure - and a quick ramp-up to that
+	   acceleration.
+
+	   Trackpoints have built-in acceleration curves already, so we
+	   don't put a new function on top, we merely scale the output from
+	   those curves (re-calculating the pressure values from the
+	   firmware-defined curve and applying a new curve is unreliable).
+
+	   For that basic scaling, we assume a constant factor f based on
+	   the speed setting together with a maximum factor m (for this
+	   speed setting). Delta acceleration is thus:
+	      factor = max(m, f)
+	      accelerated_delta = delta * factor;
+
+	   Trial and error showed a couple of pairs that work well for the
+	   various speed settings (Lenovo T440, sensitivity 128):
+
+	       -1.0: f = 0.3, m = 1
+	       -0.5: f = 0.6, m = 2
+	        0.0: f = 1.0, m = 6
+	        0.5: f = 1.4, m = 8
+	        1.0: f = 1.9, m = 15
+
+	   Note: if f >= 2.0, some pixels are unaddressable
+
+	   Those pairs were fed into the linear/exponential regression tool
+	   at http://www.xuru.org/rt/LR.asp and show two functions that map
+	   speed settings to the respective f and m.
+	   Given a speed setting s in [-1.0, 1.0]
+		   f(s) = 0.8 * s + 1.04
+		   m(s) = 4.6 * e**(1.2 * s)
+	   These are close enough to the tested pairs.
+	*/
+
+	max = 4.6 * pow(M_E, 1.2 * speed_adjustment);
+	incline = 0.8 * speed_adjustment + 1.04;
+	offset = 0;
+
+	accel_filter->max_accel = max;
+	accel_filter->incline = incline;
+	accel_filter->offset = offset;
+	filter->speed_adjustment = speed_adjustment;
+
+	return true;
+}
+
+static void
+trackpoint_accelerator_destroy(struct motion_filter *filter)
+{
+	struct trackpoint_accelerator *accel_filter =
+		(struct trackpoint_accelerator *)filter;
+
+	free(accel_filter);
+}
+
 struct motion_filter_interface accelerator_interface_trackpoint = {
 	.type = LIBINPUT_CONFIG_ACCEL_PROFILE_ADAPTIVE,
-	.filter = accelerator_filter_unnormalized,
-	.filter_constant = accelerator_filter_noop,
-	.restart = accelerator_restart,
-	.destroy = accelerator_destroy,
-	.set_speed = accelerator_set_speed,
+	.filter = trackpoint_accelerator_filter,
+	.filter_constant = trackpoint_accelerator_filter_noop,
+	.restart = NULL,
+	.destroy = trackpoint_accelerator_destroy,
+	.set_speed = trackpoint_accelerator_set_speed,
 };
 
 struct motion_filter *
-create_pointer_accelerator_filter_trackpoint(int dpi)
+create_pointer_accelerator_filter_trackpoint(int max_hw_delta)
 {
-	struct pointer_accelerator *filter;
+	struct trackpoint_accelerator *filter;
 
-	filter = create_default_filter(dpi);
+	/* Trackpoints are special. They don't have a movement speed like a
+	 * mouse or a finger, instead they send a constant stream of events
+	 * based on the pressure applied.
+	 *
+	 * Physical ranges on a trackpoint are the max values for relative
+	 * deltas, but these are highly device-specific.
+	 *
+	 */
+
+	filter = zalloc(sizeof *filter);
 	if (!filter)
 		return NULL;
 
+	filter->history_size = ARRAY_LENGTH(filter->history);
+	filter->scale_factor = 1.0 * TRACKPOINT_DEFAULT_RANGE / max_hw_delta;
+	filter->max_accel = TRACKPOINT_DEFAULT_MAX_ACCEL;
+	filter->max_delta = TRACKPOINT_DEFAULT_MAX_DELTA;
+
 	filter->base.interface = &accelerator_interface_trackpoint;
-	filter->profile = trackpoint_accel_profile;
-	filter->threshold = DEFAULT_THRESHOLD;
-	filter->accel = DEFAULT_ACCELERATION;
-	filter->incline = DEFAULT_INCLINE;
-	filter->dpi = dpi;
 
 	return &filter->base;
 }
diff --git a/src/filter.h b/src/filter.h
index e24c20d4..60d3728a 100644
--- a/src/filter.h
+++ b/src/filter.h
@@ -120,7 +120,7 @@ struct motion_filter *
 create_pointer_accelerator_filter_lenovo_x230(int dpi);
 
 struct motion_filter *
-create_pointer_accelerator_filter_trackpoint(int dpi);
+create_pointer_accelerator_filter_trackpoint(int max_delta);
 
 struct motion_filter *
 create_pointer_accelerator_filter_tablet(int xres, int yres);
@@ -152,6 +152,5 @@ touchpad_lenovo_x230_accel_profile(struct motion_filter *filter,
 double
 trackpoint_accel_profile(struct motion_filter *filter,
 			 void *data,
-			 double speed_in,
-			 uint64_t time);
+			 double delta);
 #endif /* FILTER_H */
diff --git a/tools/ptraccel-debug.c b/tools/ptraccel-debug.c
index acb82c69..052769be 100644
--- a/tools/ptraccel-debug.c
+++ b/tools/ptraccel-debug.c
@@ -170,6 +170,24 @@ print_accel_func(struct motion_filter *filter,
 }
 
 static void
+print_accel_func_trackpoint(struct motion_filter *filter,
+			    int max)
+{
+	printf("# gnuplot:\n");
+	printf("# set xlabel \"deltas (units)\"\n");
+	printf("# set ylabel \"raw accel factor\"\n");
+	printf("# set style data lines\n");
+	printf("# plot \"gnuplot.data\" using 1:2 title 'accel factor'\n");
+	printf("#\n");
+	printf("# data: delta(units) factor\n");
+	for (double delta = 0; delta < max; delta += 0.2) {
+		double factor = trackpoint_accel_profile(filter, NULL, delta);
+
+		printf("%.2f %f\n", delta, factor);
+	}
+}
+
+static void
 usage(void)
 {
 	printf("Usage: %s [options] [dx1] [dx2] [...] > gnuplot.data\n", program_invocation_short_name);
@@ -184,6 +202,7 @@ usage(void)
 	       "--steps=<double>  ... in motion and delta modes only. Increase dx by step each round\n"
 	       "--speed=<double>  ... accel speed [-1, 1], default 0\n"
 	       "--dpi=<int>	... device resolution in DPI (default: 1000)\n"
+	       "--trackpoint_range=<int> ... range of the trackpoint deltas (default: 30)\n"
 	       "--filter=<linear|low-dpi|touchpad|x230|trackpoint> \n"
 	       "	linear	  ... the default motion filter\n"
 	       "	low-dpi	  ... low-dpi filter, use --dpi with this argument\n"
@@ -219,6 +238,7 @@ main(int argc, char **argv)
 	int dpi = 1000;
 	const char *filter_type = "linear";
 	accel_profile_func_t profile = NULL;
+	int tp_range_max = 20;
 
 	enum {
 		OPT_HELP = 1,
@@ -229,6 +249,7 @@ main(int argc, char **argv)
 		OPT_SPEED,
 		OPT_DPI,
 		OPT_FILTER,
+		OPT_TRACKPOINT_RANGE,
 	};
 
 	while (1) {
@@ -243,6 +264,7 @@ main(int argc, char **argv)
 			{"speed", 1, 0, OPT_SPEED },
 			{"dpi", 1, 0, OPT_DPI },
 			{"filter", 1, 0, OPT_FILTER },
+			{"trackpoint-range", 1, 0, OPT_TRACKPOINT_RANGE },
 			{0, 0, 0, 0}
 		};
 
@@ -300,6 +322,9 @@ main(int argc, char **argv)
 		case OPT_FILTER:
 			filter_type = optarg;
 			break;
+		case OPT_TRACKPOINT_RANGE:
+			tp_range_max = strtod(optarg, NULL);
+			break;
 		default:
 			usage();
 			exit(1);
@@ -320,8 +345,8 @@ main(int argc, char **argv)
 		filter = create_pointer_accelerator_filter_lenovo_x230(dpi);
 		profile = touchpad_lenovo_x230_accel_profile;
 	} else if (streq(filter_type, "trackpoint")) {
-		filter = create_pointer_accelerator_filter_trackpoint(dpi);
-		profile = trackpoint_accel_profile;
+		filter = create_pointer_accelerator_filter_trackpoint(tp_range_max);
+		profile = NULL; /* trackpoint is special */
 	} else {
 		fprintf(stderr, "Invalid filter type %s\n", filter_type);
 		return 1;
@@ -349,9 +374,12 @@ main(int argc, char **argv)
 			custom_deltas[nevents++] = strtod(argv[optind++], NULL);
 	}
 
-	if (print_accel)
-		print_accel_func(filter, profile, dpi);
-	else if (print_delta)
+	if (print_accel) {
+		if (!profile) /* trackpoint */
+			print_accel_func_trackpoint(filter, tp_range_max);
+		else
+			print_accel_func(filter, profile, dpi);
+	} else if (print_delta)
 		print_ptraccel_deltas(filter, step);
 	else if (print_motion)
 		print_ptraccel_movement(filter, nevents, max_dx, step);
-- 
2.13.0



More information about the wayland-devel mailing list