diff --git a/doc/tablet-support.dox b/doc/tablet-support.dox index 24d08d20..5468c6ff 100644 --- a/doc/tablet-support.dox +++ b/doc/tablet-support.dox @@ -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. + */ diff --git a/src/evdev-tablet.c b/src/evdev-tablet.c index 7f6eecb4..2f4d6804 100644 --- a/src/evdev-tablet.c +++ b/src/evdev-tablet.c @@ -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 @@ -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); diff --git a/src/evdev-tablet.h b/src/evdev-tablet.h index 162b5366..4dcbccc6 100644 --- a/src/evdev-tablet.h +++ b/src/evdev-tablet.h @@ -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 diff --git a/src/libinput-private.h b/src/libinput-private.h index 38a14b8f..f5b26483 100644 --- a/src/libinput-private.h +++ b/src/libinput-private.h @@ -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 { diff --git a/test/litest-device-wacom-intuos-tablet.c b/test/litest-device-wacom-intuos-tablet.c index 682a56c1..e1c9e3de 100644 --- a/test/litest-device-wacom-intuos-tablet.c +++ b/test/litest-device-wacom-intuos-tablet.c @@ -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 }, diff --git a/test/tablet.c b/test/tablet.c index 4cd5cef6..57f8083d 100644 --- a/test/tablet.c +++ b/test/tablet.c @@ -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); }