Could be fixed in the kernel, but these tablets are effectively abandoned and fixing them is a one-by-one issue. Let's put the infrastructure in place to have this fixed once for this type of device and move on.
Signed-off-by: Peter Hutterer <[email protected]> --- doc/device-configuration-via-udev.dox | 19 +++++ src/evdev-tablet.c | 113 +++++++++++++++++++++++++++++ src/evdev-tablet.h | 7 ++ src/evdev.c | 1 + src/evdev.h | 1 + test/litest-device-huion-pentablet.c | 15 ++-- test/litest.c | 6 ++ test/litest.h | 3 + test/test-tablet.c | 132 ++++++++++++++++++++++++++++++++++ udev/90-libinput-model-quirks.hwdb | 19 +++++ 10 files changed, 312 insertions(+), 4 deletions(-) diff --git a/doc/device-configuration-via-udev.dox b/doc/device-configuration-via-udev.dox index 2ebfa321..3050cd80 100644 --- a/doc/device-configuration-via-udev.dox +++ b/doc/device-configuration-via-udev.dox @@ -162,4 +162,23 @@ model quirks hwdb for instructions. This property must not be used for any other purpose, no specific behavior is guaranteed. +@subsection model_specific_configuration_huion_tablets Graphics tablets without BTN_TOOL_PEN proximity events + +On graphics tablets, the <b>BTN_TOOL_PEN</b> bit signals that the pen is in +detectable range and will send events. When the pen leaves the sensor range, +the bit must be unset to signal that the tablet is out of proximity again. +Some HUION PenTablet devices are buggy and do not send this event. To a +caller, it thus looks like the pen is constantly in proximity. This causes +unexpected behavior in applications that rely on tablet device proximity. + +The property <b>LIBINPUT_MODEL_TABLET_NO_PROXIMITY_OUT</b> may be set +by a user in a local hwdb file. This property designates the tablet +to be buggy and that libinput should work around this bug. + +Many of the affected tablets cannot be detected automatically by libinput +because HUION tablets reuse USB IDs. Local configuration is required to set +this property. Refer to the libinput model quirks hwdb for instructions. + +This property must not be used for any other purpose, no specific behavior +is guaranteed. */ diff --git a/src/evdev-tablet.c b/src/evdev-tablet.c index 77540496..057b498c 100644 --- a/src/evdev-tablet.c +++ b/src/evdev-tablet.c @@ -32,6 +32,10 @@ #include <libwacom/libwacom.h> #endif +/* The tablet sends events every ~2ms , 50ms should be plenty enough to + detect out-of-range */ +#define FORCED_PROXOUT_TIMEOUT ms2us(50) + #define tablet_set_status(tablet_,s_) (tablet_)->status |= (s_) #define tablet_unset_status(tablet_,s_) (tablet_)->status &= ~(s_) #define tablet_has_status(tablet_,s_) (!!((tablet_)->status & (s_))) @@ -1633,6 +1637,97 @@ tablet_reset_state(struct tablet_dispatch *tablet) sizeof(tablet->button_state)); } +static inline void +tablet_proximity_out_quirk_set_timer(struct tablet_dispatch *tablet, + uint64_t time) +{ + libinput_timer_set(&tablet->quirks.prox_out_timer, + time + FORCED_PROXOUT_TIMEOUT); +} + +static void +tablet_proximity_out_quirk_timer_func(uint64_t now, void *data) +{ + struct tablet_dispatch *tablet = data; + struct input_event events[2] = { + { .time = us2tv(now), + .type = EV_KEY, + .code = BTN_TOOL_PEN, + .value = 0 }, + { .time = us2tv(now), + .type = EV_SYN, + .code = SYN_REPORT, + .value = 0 }, + }; + struct input_event *e; + + if (tablet->quirks.last_event_time > now - FORCED_PROXOUT_TIMEOUT) { + tablet_proximity_out_quirk_set_timer(tablet, + tablet->quirks.last_event_time); + return; + } + + ARRAY_FOR_EACH(events, e) { + tablet->base.interface->process(&tablet->base, + tablet->device, + e, + now); + } + + tablet->quirks.proximity_out_forced = true; +} + +/** + * Handling for the proximity out workaround. Some tablets only send + * BTN_TOOL_PEN on the very first event, then leave it set even when the pen + * leaves the detectable range. To libinput this looks like we always have + * the pen in proximity. + * + * To avoid this, we set a timer on BTN_TOOL_PEN in. We expect the tablet to + * continuously send events, and while it's doing so we keep updating the + * timer. Once we go Xms without an event we assume proximity out and inject + * a BTN_TOOL_PEN event into the sequence through the timer func. + * + * We need to remember that we did that, on the first event after the + * timeout we need to inject a BTN_TOOL_PEN event again to force proximity + * in. + */ +static inline void +tablet_proximity_out_quirk_update(struct tablet_dispatch *tablet, + struct evdev_device *device, + struct input_event *e, + uint64_t time) +{ + if (!tablet->quirks.need_to_force_prox_out) + return; + + if (e->type == EV_SYN) { + /* If the timer function forced prox out before, + fake a BTN_TOOL_PEN event */ + if (tablet->quirks.proximity_out_forced) { + + struct input_event fake_event = { + .time = us2tv(time), + .type = EV_KEY, + .code = BTN_TOOL_PEN, + .value = 1, + }; + + tablet->base.interface->process(&tablet->base, + device, + &fake_event, + time); + tablet->quirks.proximity_out_forced = false; + } + tablet->quirks.last_event_time = time; + } else if (e->type == EV_KEY && e->code == BTN_TOOL_PEN) { + if (e->value) + tablet_proximity_out_quirk_set_timer(tablet, time); + else + libinput_timer_cancel(&tablet->quirks.prox_out_timer); + } +} + static void tablet_process(struct evdev_dispatch *dispatch, struct evdev_device *device, @@ -1641,6 +1736,9 @@ tablet_process(struct evdev_dispatch *dispatch, { struct tablet_dispatch *tablet = tablet_dispatch(dispatch); + /* Warning: this may inject events */ + tablet_proximity_out_quirk_update(tablet, device, e, time); + switch (e->type) { case EV_ABS: tablet_process_absolute(tablet, device, e, time); @@ -1683,6 +1781,9 @@ tablet_destroy(struct evdev_dispatch *dispatch) struct tablet_dispatch *tablet = tablet_dispatch(dispatch); struct libinput_tablet_tool *tool, *tmp; + libinput_timer_cancel(&tablet->quirks.prox_out_timer); + libinput_timer_destroy(&tablet->quirks.prox_out_timer); + list_for_each_safe(tool, tmp, &tablet->tool_list, link) { libinput_tablet_tool_unref(tool); } @@ -1722,6 +1823,7 @@ tablet_check_initial_proximity(struct evdev_device *device, struct evdev_dispatch *dispatch) { struct tablet_dispatch *tablet = tablet_dispatch(dispatch); + struct libinput *li = tablet_libinput_context(tablet); bool tool_in_prox = false; int code, state; enum libinput_tablet_tool_type tool; @@ -1743,6 +1845,8 @@ tablet_check_initial_proximity(struct evdev_device *device, return; tablet_update_tool(tablet, device, tool, state); + if (tablet->quirks.need_to_force_prox_out) + tablet_proximity_out_quirk_set_timer(tablet, libinput_now(li)); tablet->current_tool_id = libevdev_get_event_value(device->evdev, @@ -1922,6 +2026,15 @@ tablet_init(struct tablet_dispatch *tablet, tablet_set_status(tablet, TABLET_TOOL_OUT_OF_PROXIMITY); + if (device->model_flags & EVDEV_MODEL_TABLET_NO_PROXIMITY_OUT) { + tablet->quirks.need_to_force_prox_out = true; + libinput_timer_init(&tablet->quirks.prox_out_timer, + tablet_libinput_context(tablet), + "proxout", + tablet_proximity_out_quirk_timer_func, + tablet); + } + return 0; } diff --git a/src/evdev-tablet.h b/src/evdev-tablet.h index 7d17e366..ba05e88c 100644 --- a/src/evdev-tablet.h +++ b/src/evdev-tablet.h @@ -83,6 +83,13 @@ struct tablet_dispatch { /* The paired touch device on devices with both pen & touch */ struct evdev_device *touch_device; + + struct { + bool need_to_force_prox_out; + struct libinput_timer prox_out_timer; + bool proximity_out_forced; + uint64_t last_event_time; + } quirks; }; static inline struct tablet_dispatch* diff --git a/src/evdev.c b/src/evdev.c index 46f8ad57..a9ffd9de 100644 --- a/src/evdev.c +++ b/src/evdev.c @@ -2742,6 +2742,7 @@ evdev_read_model_flags(struct evdev_device *device) MODEL(HP_PAVILION_DM4_TOUCHPAD), MODEL(APPLE_TOUCHPAD_ONEBUTTON), MODEL(LOGITECH_MARBLE_MOUSE), + MODEL(TABLET_NO_PROXIMITY_OUT), #undef MODEL { "ID_INPUT_TRACKBALL", EVDEV_MODEL_TRACKBALL }, { NULL, EVDEV_MODEL_DEFAULT }, diff --git a/src/evdev.h b/src/evdev.h index 5192927c..bb4c3a03 100644 --- a/src/evdev.h +++ b/src/evdev.h @@ -127,6 +127,7 @@ enum evdev_device_model { EVDEV_MODEL_HP_PAVILION_DM4_TOUCHPAD = (1 << 24), EVDEV_MODEL_APPLE_TOUCHPAD_ONEBUTTON = (1 << 25), EVDEV_MODEL_LOGITECH_MARBLE_MOUSE = (1 << 26), + EVDEV_MODEL_TABLET_NO_PROXIMITY_OUT = (1 << 27), }; enum evdev_button_scroll_state { diff --git a/test/litest-device-huion-pentablet.c b/test/litest-device-huion-pentablet.c index e37d5c09..dbbdcb01 100644 --- a/test/litest-device-huion-pentablet.c +++ b/test/litest-device-huion-pentablet.c @@ -42,10 +42,6 @@ static struct input_event proximity_in[] = { }; static struct input_event proximity_out[] = { - { .type = EV_ABS, .code = ABS_X, .value = 0 }, - { .type = EV_ABS, .code = ABS_Y, .value = 0 }, - { .type = EV_KEY, .code = BTN_TOOL_PEN, .value = 0 }, - { .type = EV_SYN, .code = SYN_REPORT, .value = 0 }, { .type = -1, .code = -1 }, }; @@ -98,6 +94,16 @@ static int events[] = { -1, -1, }; +static const char udev_rule[] = +"ACTION==\"remove\", GOTO=\"huion_end\"\n" +"KERNEL!=\"event*\", GOTO=\"huion_end\"\n" +"ENV{ID_INPUT_TABLET}==\"\", GOTO=\"huion_end\"\n" +"\n" +"ATTRS{name}==\"litest HUION PenTablet Pen\"," +" ENV{LIBINPUT_MODEL_TABLET_NO_PROXIMITY_OUT}=\"1\"\n" +"\n" +"LABEL=\"huion_end\""; + struct litest_test_device litest_huion_tablet_device = { .type = LITEST_HUION_TABLET, .features = LITEST_TABLET, @@ -109,4 +115,5 @@ struct litest_test_device litest_huion_tablet_device = { .id = &input_id, .events = events, .absinfo = absinfo, + .udev_rule = udev_rule, }; diff --git a/test/litest.c b/test/litest.c index 1f238431..868dfb69 100644 --- a/test/litest.c +++ b/test/litest.c @@ -3340,6 +3340,12 @@ litest_timeout_trackpoint(void) } void +litest_timeout_tablet_proxout(void) +{ + msleep(70); +} + +void litest_push_event_frame(struct litest_device *dev) { litest_assert(dev->skip_ev_syn >= 0); diff --git a/test/litest.h b/test/litest.h index 679406dc..8f3c3bc9 100644 --- a/test/litest.h +++ b/test/litest.h @@ -730,6 +730,9 @@ void litest_timeout_trackpoint(void); void +litest_timeout_tablet_proxout(void); + +void litest_push_event_frame(struct litest_device *dev); void diff --git a/test/test-tablet.c b/test/test-tablet.c index a9ad9592..4a36c79a 100644 --- a/test/test-tablet.c +++ b/test/test-tablet.c @@ -250,6 +250,22 @@ START_TEST(tip_down_prox_in) } END_TEST +static inline bool +tablet_has_proxout_quirk(struct litest_device *dev) +{ + struct udev_device *udev_device; + bool has_quirk; + + udev_device = libinput_device_get_udev_device(dev->libinput_device); + + has_quirk = !!udev_device_get_property_value(udev_device, + "LIBINPUT_MODEL_TABLET_NO_PROXIMITY_OUT"); + + udev_device_unref(udev_device); + + return has_quirk; +} + START_TEST(tip_up_prox_out) { struct litest_device *dev = litest_current_device(); @@ -262,6 +278,9 @@ START_TEST(tip_up_prox_out) { -1, -1 } }; + if (tablet_has_proxout_quirk(dev)) + return; + litest_tablet_proximity_in(dev, 10, 10, axes); litest_event(dev, EV_KEY, BTN_TOUCH, 1); litest_event(dev, EV_SYN, SYN_REPORT, 0); @@ -616,6 +635,9 @@ START_TEST(tip_state_proximity) litest_tablet_proximity_out(dev); libinput_dispatch(li); + litest_timeout_tablet_proxout(); + libinput_dispatch(li); + event = libinput_get_event(li); tablet_event = litest_is_tablet_event(event, LIBINPUT_EVENT_TABLET_TOOL_PROXIMITY); @@ -808,6 +830,9 @@ START_TEST(proximity_in_out) litest_tablet_proximity_out(dev); libinput_dispatch(li); + litest_timeout_tablet_proxout(); + libinput_dispatch(li); + while ((event = libinput_get_event(li))) { if (libinput_event_get_type(event) == LIBINPUT_EVENT_TABLET_TOOL_PROXIMITY) { @@ -917,7 +942,9 @@ START_TEST(proximity_out_clear_buttons) litest_event(dev, EV_KEY, button, 1); litest_event(dev, EV_SYN, SYN_REPORT, 0); litest_tablet_proximity_out(dev); + libinput_dispatch(li); + litest_timeout_tablet_proxout(); libinput_dispatch(li); while ((event = libinput_get_event(li))) { @@ -1042,6 +1069,9 @@ START_TEST(proximity_has_axes) litest_tablet_proximity_out(dev); libinput_dispatch(li); + litest_timeout_tablet_proxout(); + libinput_dispatch(li); + event = libinput_get_event(li); tablet_event = litest_is_tablet_event(event, LIBINPUT_EVENT_TABLET_TOOL_PROXIMITY); @@ -2506,6 +2536,10 @@ START_TEST(tool_in_prox_before_start) litest_event(dev, EV_SYN, SYN_REPORT, 0); litest_assert_only_typed_events(li, LIBINPUT_EVENT_TABLET_TOOL_BUTTON); litest_tablet_proximity_out(dev); + libinput_dispatch(li); + + litest_timeout_tablet_proxout(); + libinput_dispatch(li); litest_wait_for_event_of_type(li, LIBINPUT_EVENT_TABLET_TOOL_PROXIMITY, @@ -4314,6 +4348,101 @@ START_TEST(cintiq_touch_arbitration_remove_tablet) } END_TEST +START_TEST(huion_static_btn_tool_pen) +{ + struct litest_device *dev = litest_current_device(); + struct libinput *li = dev->libinput; + int i; + + litest_drain_events(li); + + litest_event(dev, EV_ABS, ABS_X, 20000); + litest_event(dev, EV_ABS, ABS_Y, 20000); + litest_event(dev, EV_ABS, ABS_PRESSURE, 100); + litest_event(dev, EV_KEY, BTN_TOOL_PEN, 1); + litest_event(dev, EV_SYN, SYN_REPORT, 0); + litest_drain_events(li); + + for (i = 0; i < 10; i++) { + litest_event(dev, EV_ABS, ABS_X, 20000 + 10 * i); + litest_event(dev, EV_ABS, ABS_Y, 20000 - 10 * i); + litest_event(dev, EV_SYN, SYN_REPORT, 0); + libinput_dispatch(li); + } + litest_assert_only_typed_events(li, + LIBINPUT_EVENT_TABLET_TOOL_AXIS); + + /* Wait past the timeout to expect a proximity out */ + litest_timeout_tablet_proxout(); + libinput_dispatch(li); + litest_assert_tablet_proximity_event(li, + LIBINPUT_TABLET_TOOL_PROXIMITY_STATE_OUT); + libinput_dispatch(li); + + /* New events should fake a proximity in again */ + litest_event(dev, EV_ABS, ABS_X, 20000); + litest_event(dev, EV_ABS, ABS_Y, 20000); + litest_event(dev, EV_SYN, SYN_REPORT, 0); + libinput_dispatch(li); + litest_assert_tablet_proximity_event(li, + LIBINPUT_TABLET_TOOL_PROXIMITY_STATE_IN); + libinput_dispatch(li); + + for (i = 0; i < 10; i++) { + litest_event(dev, EV_ABS, ABS_X, 20000 + 10 * i); + litest_event(dev, EV_ABS, ABS_Y, 20000 - 10 * i); + litest_event(dev, EV_SYN, SYN_REPORT, 0); + libinput_dispatch(li); + } + litest_assert_only_typed_events(li, + LIBINPUT_EVENT_TABLET_TOOL_AXIS); + litest_timeout_tablet_proxout(); + libinput_dispatch(li); + litest_assert_tablet_proximity_event(li, + LIBINPUT_TABLET_TOOL_PROXIMITY_STATE_OUT); + libinput_dispatch(li); + + /* New events, just to ensure cleanup paths are correct */ + litest_event(dev, EV_ABS, ABS_X, 20000); + litest_event(dev, EV_ABS, ABS_Y, 20000); + litest_event(dev, EV_SYN, SYN_REPORT, 0); + libinput_dispatch(li); +} +END_TEST + +START_TEST(huion_static_btn_tool_pen_no_timeout_during_usage) +{ + struct litest_device *dev = litest_current_device(); + struct libinput *li = dev->libinput; + int i; + + litest_drain_events(li); + + litest_event(dev, EV_ABS, ABS_X, 20000); + litest_event(dev, EV_ABS, ABS_Y, 20000); + litest_event(dev, EV_ABS, ABS_PRESSURE, 100); + litest_event(dev, EV_KEY, BTN_TOOL_PEN, 1); + litest_event(dev, EV_SYN, SYN_REPORT, 0); + litest_drain_events(li); + + /* take longer than the no-activity timeout */ + for (i = 0; i < 50; i++) { + litest_event(dev, EV_ABS, ABS_X, 20000 + 10 * i); + litest_event(dev, EV_ABS, ABS_Y, 20000 - 10 * i); + litest_event(dev, EV_SYN, SYN_REPORT, 0); + libinput_dispatch(li); + msleep(5); + } + litest_assert_only_typed_events(li, + LIBINPUT_EVENT_TABLET_TOOL_AXIS); + litest_timeout_tablet_proxout(); + libinput_dispatch(li); + litest_assert_tablet_proximity_event(li, + LIBINPUT_TABLET_TOOL_PROXIMITY_STATE_OUT); + libinput_dispatch(li); +} +END_TEST + void litest_setup_tests_tablet(void) { @@ -4409,4 +4538,7 @@ litest_setup_tests_tablet(void) litest_add_for_device("tablet:touch-arbitration", cintiq_touch_arbitration_suspend_touch_device, LITEST_WACOM_CINTIQ_13HDT_FINGER); litest_add_for_device("tablet:touch-arbitration", cintiq_touch_arbitration_remove_touch, LITEST_WACOM_CINTIQ_13HDT_PEN); litest_add_for_device("tablet:touch-arbitration", cintiq_touch_arbitration_remove_tablet, LITEST_WACOM_CINTIQ_13HDT_FINGER); + + litest_add_for_device("tablet:quirks", huion_static_btn_tool_pen, LITEST_HUION_TABLET); + litest_add_for_device("tablet:quirks", huion_static_btn_tool_pen_no_timeout_during_usage, LITEST_HUION_TABLET); } diff --git a/udev/90-libinput-model-quirks.hwdb b/udev/90-libinput-model-quirks.hwdb index 72fcdca1..867ffa53 100644 --- a/udev/90-libinput-model-quirks.hwdb +++ b/udev/90-libinput-model-quirks.hwdb @@ -163,6 +163,25 @@ libinput:name:AlpsPS/2 ALPS GlidePoint:dmi:*svnHP:pnHPZBookStudioG3:* LIBINPUT_MODEL_HP_ZBOOK_STUDIO_G3=1 ########################################## +# HUION +########################################## +# +# HUION PenTablet device. Some of these devices send a BTN_TOOL_PEN event +# with value 1 on the first event received by the device but never send the +# matching BTN_TOOL_PEN value 0 event. The device appears as if it was +# permanently in proximity. +# +# If the tablet is affected by this bug, copy the two lines below into a new +# file +# /etc/udev/hwdb.d/90-libinput-huion-pentablet-proximity-quirk.hwdb, then run +# sudo udevadm hwdb --update and reboot. +# +# Note that HUION re-uses USB IDs for its devices, not ever HUION tablet is +# affected by this bug. +#libinput:name:PenTablet Pen:dmi:* +# LIBINPUT_MODEL_TABLET_NO_PROXIMITY_OUT=1 + +########################################## # LENOVO ########################################## -- 2.13.5 _______________________________________________ wayland-devel mailing list [email protected] https://lists.freedesktop.org/mailman/listinfo/wayland-devel
