tablet: support tool-specific pressure offsets

If a tool wears out, it may have a pre-loaded pressure offset. In that case,
even when the tool is not physically in contact with the tablet surface it
will send pressure events.

Use automatic pressure offset detection, similar to what the X.Org wacom
driver does. On proximity-in, check the pressure and if the distance is above
50% of the range and the pressure is nonzero but below 20% of the range, use
that value as pressure offset.

Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
Reviewed-by: Ping Cheng <pingc@wacom.com>
This commit is contained in:
Peter Hutterer 2015-12-01 11:07:57 +10:00
parent 09456ebf23
commit db852ef0db
6 changed files with 448 additions and 4 deletions

View file

@ -92,4 +92,33 @@ if (value < min) {
}
@endcode
@section tablet-pressure-offset Pressure offset on worn-out tools
When a tool is used for an extended period it can wear down physically. A
worn-down tool may never return a zero pressure value. Even when hovering
above the surface, the pressure value returned by the tool is nonzero,
creating a fake surface touch and making interaction with the tablet less
predictable.
libinput automatically detects pressure offsets and rescales the remaining
pressure range into the available range, making pressure-offsets transparent
to the caller. A tool with a pressure offset will thus send a 0 pressure
value for the detected offset and nonzero pressure values for values higher
than that offset.
Some limitations apply to avoid misdetection of pressure offsets,
specifically:
- pressure offset is only detected on proximity in, and if a device is
capable of detection distances,
- pressure offset is only detected if the distance between the tool and the
tablet is high enough,
- pressure offset is only used if it is 20% or less of the pressure range
available to the tool. A pressure offset higher than 20% indicates either
a misdetection or a tool that should be replaced, and
- if a pressure value less than the current pressure offset is seen, the
offset resets to that value.
Pressure offsets are not detected on @ref LIBINPUT_TABLET_TOOL_TYPE_MOUSE
and @ref LIBINPUT_TABLET_TOOL_TYPE_LENS tools.
*/

View file

@ -21,6 +21,7 @@
* CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
#include "config.h"
#include "libinput-version.h"
#include "evdev-tablet.h"
#include <assert.h>
@ -202,7 +203,7 @@ tablet_update_tool(struct tablet_dispatch *tablet,
}
static inline double
normalize_pressure_dist_slider(const struct input_absinfo *absinfo)
normalize_dist_slider(const struct input_absinfo *absinfo)
{
double range = absinfo->maximum - absinfo->minimum;
double value = (absinfo->value - absinfo->minimum) / range;
@ -210,6 +211,18 @@ normalize_pressure_dist_slider(const struct input_absinfo *absinfo)
return value;
}
static inline double
normalize_pressure(const struct input_absinfo *absinfo,
struct libinput_tablet_tool *tool)
{
double range = absinfo->maximum - absinfo->minimum;
int offset = tool->has_pressure_offset ?
tool->pressure_offset : 0;
double value = (absinfo->value - offset - absinfo->minimum) / range;
return value;
}
static inline double
normalize_tilt(const struct input_absinfo *absinfo)
{
@ -398,10 +411,12 @@ tablet_check_notify_axes(struct tablet_dispatch *tablet,
axis_to_evcode(a));
switch (a) {
case LIBINPUT_TABLET_TOOL_AXIS_DISTANCE:
case LIBINPUT_TABLET_TOOL_AXIS_PRESSURE:
tablet->axes[a] = normalize_pressure(absinfo, tool);
break;
case LIBINPUT_TABLET_TOOL_AXIS_DISTANCE:
case LIBINPUT_TABLET_TOOL_AXIS_SLIDER:
tablet->axes[a] = normalize_pressure_dist_slider(absinfo);
tablet->axes[a] = normalize_dist_slider(absinfo);
break;
case LIBINPUT_TABLET_TOOL_AXIS_TILT_X:
case LIBINPUT_TABLET_TOOL_AXIS_TILT_Y:
@ -809,6 +824,8 @@ tablet_get_tool(struct tablet_dispatch *tablet,
.refcount = 1,
};
tool->pressure_offset = 0;
tool->has_pressure_offset = false;
tool_set_bits(tablet, tool);
list_insert(tool_list, &tool->link);
@ -922,6 +939,67 @@ sanitize_tablet_axes(struct tablet_dispatch *tablet)
set_bit(tablet->changed_axes, LIBINPUT_TABLET_TOOL_AXIS_ROTATION_Z);
}
static inline int
axis_range_percentage(const struct input_absinfo *a, int percent)
{
return (a->maximum - a->minimum) * percent/100 + a->minimum;
}
static void
detect_pressure_offset(struct tablet_dispatch *tablet,
struct evdev_device *device,
struct libinput_tablet_tool *tool)
{
const struct input_absinfo *pressure, *distance;
int offset;
if (!bit_is_set(tablet->changed_axes,
LIBINPUT_TABLET_TOOL_AXIS_PRESSURE))
return;
pressure = libevdev_get_abs_info(device->evdev, ABS_PRESSURE);
distance = libevdev_get_abs_info(device->evdev, ABS_DISTANCE);
if (!pressure || !distance)
return;
offset = pressure->value - pressure->minimum;
if (tool->has_pressure_offset) {
if (offset < tool->pressure_offset)
tool->pressure_offset = offset;
return;
}
/* we only set a pressure offset on proximity in */
if (!tablet_has_status(tablet, TABLET_TOOL_ENTERING_PROXIMITY))
return;
/* If we're closer than 50% of the distance axis, skip pressure
* offset detection, too likely to be wrong */
if (distance->value < axis_range_percentage(distance, 50))
return;
if (offset > axis_range_percentage(pressure, 20)) {
log_error(device->base.seat->libinput,
"Ignoring pressure offset greater than 20%% detected on tool %s (serial %#x). "
"See http://wayland.freedesktop.org/libinput/doc/%s/tablet-support.html\n",
tablet_tool_type_to_string(tool->type),
tool->serial,
LIBINPUT_VERSION);
return;
}
log_info(device->base.seat->libinput,
"Pressure offset detected on tool %s (serial %#x). "
"See http://wayland.freedesktop.org/libinput/doc/%s/tablet-support.html\n",
tablet_tool_type_to_string(tool->type),
tool->serial,
LIBINPUT_VERSION);
tool->pressure_offset = offset;
tool->has_pressure_offset = true;
}
static void
tablet_flush(struct tablet_dispatch *tablet,
struct evdev_device *device,
@ -946,6 +1024,7 @@ tablet_flush(struct tablet_dispatch *tablet,
tablet_set_status(tablet, TABLET_TOOL_LEAVING_CONTACT);
} else if (tablet_has_status(tablet, TABLET_AXES_UPDATED) ||
tablet_has_status(tablet, TABLET_TOOL_ENTERING_PROXIMITY)) {
detect_pressure_offset(tablet, device, tool);
sanitize_tablet_axes(tablet);
tablet_check_notify_axes(tablet, device, time, tool);

View file

@ -178,4 +178,25 @@ tablet_tool_to_evcode(enum libinput_tablet_tool_type type)
return code;
}
static inline const char *
tablet_tool_type_to_string(enum libinput_tablet_tool_type type)
{
const char *str;
switch (type) {
case LIBINPUT_TABLET_TOOL_TYPE_PEN: str = "pen"; break;
case LIBINPUT_TABLET_TOOL_TYPE_ERASER: str = "eraser"; break;
case LIBINPUT_TABLET_TOOL_TYPE_BRUSH: str = "brush"; break;
case LIBINPUT_TABLET_TOOL_TYPE_PENCIL: str = "pencil"; break;
case LIBINPUT_TABLET_TOOL_TYPE_AIRBRUSH: str = "airbrush"; break;
case LIBINPUT_TABLET_TOOL_TYPE_MOUSE: str = "mouse"; break;
case LIBINPUT_TABLET_TOOL_TYPE_LENS: str = "lens"; break;
default:
abort();
}
return str;
}
#endif

View file

@ -259,6 +259,9 @@ struct libinput_tablet_tool {
unsigned char buttons[NCHARS(KEY_MAX) + 1];
int refcount;
void *user_data;
int pressure_offset;
bool has_pressure_offset;
};
struct libinput_event {

View file

@ -37,6 +37,7 @@ static struct input_event proximity_in[] = {
{ .type = EV_ABS, .code = ABS_X, .value = LITEST_AUTO_ASSIGN },
{ .type = EV_ABS, .code = ABS_Y, .value = LITEST_AUTO_ASSIGN },
{ .type = EV_ABS, .code = ABS_DISTANCE, .value = LITEST_AUTO_ASSIGN },
{ .type = EV_ABS, .code = ABS_PRESSURE, .value = LITEST_AUTO_ASSIGN },
{ .type = EV_ABS, .code = ABS_TILT_X, .value = LITEST_AUTO_ASSIGN },
{ .type = EV_ABS, .code = ABS_TILT_Y, .value = LITEST_AUTO_ASSIGN },
{ .type = EV_ABS, .code = ABS_MISC, .value = 1050626 },

View file

@ -2313,7 +2313,7 @@ START_TEST(tablet_pressure_distance_exclusive)
struct libinput_event_tablet_tool *tev;
struct axis_replacement axes[] = {
{ ABS_DISTANCE, 10 },
{ ABS_PRESSURE, 20 },
{ ABS_PRESSURE, 20 }, /* see the litest device */
{ -1, -1 },
};
double pressure, distance;
@ -2548,6 +2548,310 @@ START_TEST(tablet_calibration_set_matrix)
}
END_TEST
START_TEST(tablet_pressure_offset)
{
struct litest_device *dev = litest_current_device();
struct libinput *li = dev->libinput;
struct libinput_event *event;
struct libinput_event_tablet_tool *tev;
struct axis_replacement axes[] = {
{ ABS_DISTANCE, 70 },
{ ABS_PRESSURE, 20 },
{ -1, -1 },
};
double pressure;
litest_tablet_proximity_in(dev, 5, 100, axes);
litest_drain_events(li);
axes[0].value = 0;
axes[1].value = 21;
litest_push_event_frame(dev);
litest_tablet_motion(dev, 70, 70, axes);
litest_event(dev, EV_KEY, BTN_TOUCH, 1);
litest_pop_event_frame(dev);
libinput_dispatch(li);
litest_drain_events(li);
axes[1].value = 20;
litest_tablet_motion(dev, 70, 70, axes);
libinput_dispatch(li);
event = libinput_get_event(li);
tev = litest_is_tablet_event(event,
LIBINPUT_EVENT_TABLET_TOOL_AXIS);
pressure = libinput_event_tablet_tool_get_axis_value(tev,
LIBINPUT_TABLET_TOOL_AXIS_PRESSURE);
ck_assert_double_eq(pressure, 0.0);
libinput_event_destroy(event);
litest_drain_events(li);
axes[1].value = 21;
litest_tablet_motion(dev, 70, 70, axes);
libinput_dispatch(li);
event = libinput_get_event(li);
tev = litest_is_tablet_event(event,
LIBINPUT_EVENT_TABLET_TOOL_AXIS);
pressure = libinput_event_tablet_tool_get_axis_value(tev,
LIBINPUT_TABLET_TOOL_AXIS_PRESSURE);
/* can't use the double_eq here, the pressure value is too tiny */
ck_assert(pressure > 0.0);
ck_assert(pressure < 1.0);
libinput_event_destroy(event);
}
END_TEST
START_TEST(tablet_pressure_offset_decrease)
{
struct litest_device *dev = litest_current_device();
struct libinput *li = dev->libinput;
struct libinput_event *event;
struct libinput_event_tablet_tool *tev;
struct axis_replacement axes[] = {
{ ABS_DISTANCE, 70 },
{ ABS_PRESSURE, 20 },
{ -1, -1 },
};
double pressure;
/* offset 20 on prox in */
litest_tablet_proximity_in(dev, 5, 100, axes);
litest_drain_events(li);
/* a reduced pressure value must reduce the offset */
axes[0].value = 0;
axes[1].value = 10;
litest_push_event_frame(dev);
litest_tablet_motion(dev, 70, 70, axes);
litest_event(dev, EV_KEY, BTN_TOUCH, 1);
litest_pop_event_frame(dev);
libinput_dispatch(li);
event = libinput_get_event(li);
tev = litest_is_tablet_event(event,
LIBINPUT_EVENT_TABLET_TOOL_AXIS);
pressure = libinput_event_tablet_tool_get_axis_value(tev,
LIBINPUT_TABLET_TOOL_AXIS_PRESSURE);
ck_assert_double_eq(pressure, 0.0);
libinput_event_destroy(event);
litest_drain_events(li);
axes[1].value = 11;
litest_tablet_motion(dev, 70, 70, axes);
libinput_dispatch(li);
event = libinput_get_event(li);
tev = litest_is_tablet_event(event,
LIBINPUT_EVENT_TABLET_TOOL_AXIS);
pressure = libinput_event_tablet_tool_get_axis_value(tev,
LIBINPUT_TABLET_TOOL_AXIS_PRESSURE);
/* can't use the double_eq here, the pressure value is too tiny */
ck_assert(pressure > 0.0);
ck_assert(pressure < 1.0);
libinput_event_destroy(event);
}
END_TEST
START_TEST(tablet_pressure_offset_increase)
{
struct litest_device *dev = litest_current_device();
struct libinput *li = dev->libinput;
struct libinput_event *event;
struct libinput_event_tablet_tool *tev;
struct axis_replacement axes[] = {
{ ABS_DISTANCE, 70 },
{ ABS_PRESSURE, 20 },
{ -1, -1 },
};
double pressure;
/* offset 20 on first prox in */
litest_tablet_proximity_in(dev, 5, 100, axes);
litest_tablet_proximity_out(dev);
litest_drain_events(li);
/* offset 30 on second prox in - must not change the offset */
axes[1].value = 30;
litest_tablet_proximity_in(dev, 5, 100, axes);
litest_drain_events(li);
axes[0].value = 0;
axes[1].value = 31;
litest_push_event_frame(dev);
litest_tablet_motion(dev, 70, 70, axes);
litest_event(dev, EV_KEY, BTN_TOUCH, 1);
litest_pop_event_frame(dev);
libinput_dispatch(li);
litest_drain_events(li);
axes[1].value = 30;
litest_tablet_motion(dev, 70, 70, axes);
libinput_dispatch(li);
event = libinput_get_event(li);
tev = litest_is_tablet_event(event,
LIBINPUT_EVENT_TABLET_TOOL_AXIS);
pressure = libinput_event_tablet_tool_get_axis_value(tev,
LIBINPUT_TABLET_TOOL_AXIS_PRESSURE);
/* can't use the double_eq here, the pressure value is too tiny */
ck_assert(pressure > 0.0);
ck_assert(pressure < 1.0);
libinput_event_destroy(event);
litest_drain_events(li);
axes[1].value = 20;
litest_tablet_motion(dev, 70, 70, axes);
libinput_dispatch(li);
event = libinput_get_event(li);
tev = litest_is_tablet_event(event,
LIBINPUT_EVENT_TABLET_TOOL_AXIS);
pressure = libinput_event_tablet_tool_get_axis_value(tev,
LIBINPUT_TABLET_TOOL_AXIS_PRESSURE);
ck_assert_double_eq(pressure, 0.0);
libinput_event_destroy(event);
}
END_TEST
START_TEST(tablet_pressure_offset_exceed_threshold)
{
struct litest_device *dev = litest_current_device();
struct libinput *li = dev->libinput;
struct libinput_event *event;
struct libinput_event_tablet_tool *tev;
struct axis_replacement axes[] = {
{ ABS_DISTANCE, 70 },
{ ABS_PRESSURE, 30 },
{ -1, -1 },
};
double pressure;
litest_drain_events(li);
litest_disable_log_handler(li);
litest_tablet_proximity_in(dev, 5, 100, axes);
libinput_dispatch(li);
event = libinput_get_event(li);
tev = litest_is_tablet_event(event,
LIBINPUT_EVENT_TABLET_TOOL_PROXIMITY);
pressure = libinput_event_tablet_tool_get_axis_value(tev,
LIBINPUT_TABLET_TOOL_AXIS_PRESSURE);
ck_assert_double_eq(pressure, 0.0);
libinput_event_destroy(event);
litest_restore_log_handler(li);
axes[0].value = 0;
axes[1].value = 31;
litest_push_event_frame(dev);
litest_tablet_motion(dev, 70, 70, axes);
litest_event(dev, EV_KEY, BTN_TOUCH, 1);
litest_pop_event_frame(dev);
libinput_dispatch(li);
litest_drain_events(li);
axes[1].value = 30;
litest_tablet_motion(dev, 70, 70, axes);
libinput_dispatch(li);
event = libinput_get_event(li);
tev = litest_is_tablet_event(event,
LIBINPUT_EVENT_TABLET_TOOL_AXIS);
pressure = libinput_event_tablet_tool_get_axis_value(tev,
LIBINPUT_TABLET_TOOL_AXIS_PRESSURE);
ck_assert_double_gt(pressure, 0.0);
libinput_event_destroy(event);
}
END_TEST
START_TEST(tablet_pressure_offset_none_for_zero_distance)
{
struct litest_device *dev = litest_current_device();
struct libinput *li = dev->libinput;
struct libinput_event *event;
struct libinput_event_tablet_tool *tev;
struct axis_replacement axes[] = {
{ ABS_DISTANCE, 0 },
{ ABS_PRESSURE, 20 },
{ -1, -1 },
};
double pressure;
litest_drain_events(li);
/* we're going straight to touch on proximity, make sure we don't
* offset the pressure here */
litest_push_event_frame(dev);
litest_tablet_proximity_in(dev, 5, 100, axes);
litest_event(dev, EV_KEY, BTN_TOUCH, 1);
litest_pop_event_frame(dev);
libinput_dispatch(li);
event = libinput_get_event(li);
tev = litest_is_tablet_event(event,
LIBINPUT_EVENT_TABLET_TOOL_PROXIMITY);
pressure = libinput_event_tablet_tool_get_axis_value(tev,
LIBINPUT_TABLET_TOOL_AXIS_PRESSURE);
ck_assert_double_gt(pressure, 0.0);
libinput_event_destroy(event);
}
END_TEST
START_TEST(tablet_pressure_offset_none_for_small_distance)
{
struct litest_device *dev = litest_current_device();
struct libinput *li = dev->libinput;
struct libinput_event *event;
struct libinput_event_tablet_tool *tev;
struct axis_replacement axes[] = {
{ ABS_DISTANCE, 20 },
{ ABS_PRESSURE, 20 },
{ -1, -1 },
};
double pressure;
/* stylus too close to the tablet on the proximity in, ignore any
* pressure offset */
litest_tablet_proximity_in(dev, 5, 100, axes);
litest_drain_events(li);
libinput_dispatch(li);
axes[0].value = 0;
axes[1].value = 21;
litest_push_event_frame(dev);
litest_tablet_motion(dev, 70, 70, axes);
litest_event(dev, EV_KEY, BTN_TOUCH, 1);
litest_pop_event_frame(dev);
litest_drain_events(li);
axes[1].value = 20;
litest_tablet_motion(dev, 70, 70, axes);
libinput_dispatch(li);
litest_wait_for_event_of_type(li,
LIBINPUT_EVENT_TABLET_TOOL_AXIS,
-1);
event = libinput_get_event(li);
tev = libinput_event_get_tablet_tool_event(event);
pressure = libinput_event_tablet_tool_get_axis_value(tev,
LIBINPUT_TABLET_TOOL_AXIS_PRESSURE);
ck_assert_double_gt(pressure, 0.0);
libinput_event_destroy(event);
}
END_TEST
void
litest_setup_tests(void)
{
@ -2596,4 +2900,11 @@ litest_setup_tests(void)
litest_add("tablet:calibration", tablet_calibration_has_matrix, LITEST_TABLET, LITEST_ANY);
litest_add("tablet:calibration", tablet_calibration_set_matrix, LITEST_TABLET, LITEST_ANY);
litest_add("tablet:calibration", tablet_calibration_set_matrix_delta, LITEST_TABLET, LITEST_ANY);
litest_add_for_device("tablet:pressure", tablet_pressure_offset, LITEST_WACOM_INTUOS);
litest_add_for_device("tablet:pressure", tablet_pressure_offset_decrease, LITEST_WACOM_INTUOS);
litest_add_for_device("tablet:pressure", tablet_pressure_offset_increase, LITEST_WACOM_INTUOS);
litest_add_for_device("tablet:pressure", tablet_pressure_offset_exceed_threshold, LITEST_WACOM_INTUOS);
litest_add_for_device("tablet:pressure", tablet_pressure_offset_none_for_zero_distance, LITEST_WACOM_INTUOS);
litest_add_for_device("tablet:pressure", tablet_pressure_offset_none_for_small_distance, LITEST_WACOM_INTUOS);
}