[PATCH libinput 5/5] evdev: add new debouncing code

Peter Hutterer peter.hutterer at who-t.net
Tue Nov 14 04:13:01 UTC 2017


The current debouncing code monitors events and switches on when events are
too close together. From then on, any event can be delayed.

Vicente Bergas provided an algorithm that avoids most of these delays:
on a button state change we now forward the change without delay but start a
timer. If the button changes state during that timer, the changes are
ignored. On timer expiry, events are sent to match the hardware state
with the client's view of the device. This is only done if needed.

Thus, a press-release sequence of: PRP sends a single press event, a sequence of
PRPR sends press and then the release at the end of the timeout. The timeout
is short enough that the delay should not be noticeable.

This new mode is called the 'bounce' mode. The old mode is now referred to as
'spurious' mode and only covers the case of a button held down that loses
contact. It works as before, monitoring a button for these spurious contact
losses and switching on. When on, button release events are delayed as before.

The whole button debouncing moves to a state machine which makes debugging a
lot easier. See the accompanying SVG for the diagram.

Signed-off-by: Peter Hutterer <peter.hutterer at who-t.net>
---
Note: this patch is missing the diff that adds the svg file, for that file
see: 
https://github.com/whot/libinput/blob/wip/button-debouncing-v3/doc/button-debouncing-state-machine.svg

 doc/button-debouncing-state-machine.svg |   2 +
 doc/button_debouncing.dox               |  25 +-
 meson.build                             |   1 +
 src/evdev-debounce.c                    | 564 ++++++++++++++++++++++++++++++++
 src/evdev-fallback.c                    | 260 +--------------
 src/evdev-fallback.h                    | 102 +++++-
 test/litest.c                           |   2 +-
 test/test-pointer.c                     | 248 ++++++++++----
 8 files changed, 884 insertions(+), 320 deletions(-)
 create mode 100644 doc/button-debouncing-state-machine.svg
 create mode 100644 src/evdev-debounce.c


diff --git a/doc/button_debouncing.dox b/doc/button_debouncing.dox
index fd52986e..9e6198bb 100644
--- a/doc/button_debouncing.dox
+++ b/doc/button_debouncing.dox
@@ -9,12 +9,25 @@ though the user only pressed or clicked the button once. This effect can be
 counteracted by "debouncing" the buttons, usually by ignoring erroneous
 events.
 
-libinput has a built-in debouncing for hardware defects. This feature is
-available for all button-base devices but not active by default. When
-libinput detects a faulty button on a device, debouncing is enabled and a
-warning is printed to the log. Subsequent button events are handled
-correctly in that bouncing button events are ignored, a user should thus see
-the expected behavior.
+libinput provides two methods of debouncing buttons, referred to as the
+"bounce" and "spurious" methods:
+
+- In the "bounce" method, libinput monitors hardware bouncing on button
+  state changes, i.e. when a user clicks or releases a button. For example,
+  if a user presses a button but the hardware generates a
+  press-release-press sequence in quick succession, libinput ignores the
+  release and second press event. This method is always enabled.
+- in the "spurious" method, libinput detects spurious releases of a button
+  while the button is physically held down by the user. These releases are
+  immediately followed by a press event. libinput monitors for these events
+  and ignores the release and press event. This method is disabled by
+  default and enables once libinput detects the first faulty event sequence.
+
+The "bounce" method guarantees that all press events are delivered
+immediately and most release events are delivered immediately. The
+"spurious" method requires that release events are delayed, libinput thus
+does not enable this method unless a faulty event sequence is detected. A
+message is printed to the log when spurious deboucing was detected.
 
 Note that libinput's debouncing intended to correct hardware damage or
 substandard hardware. Debouncing is also used as an accessibility feature
diff --git a/meson.build b/meson.build
index 1dc0b7d4..ca200cae 100644
--- a/meson.build
+++ b/meson.build
@@ -156,6 +156,7 @@ src_libinput = [
 	'src/libinput-private.h',
 	'src/evdev.c',
 	'src/evdev.h',
+	'src/evdev-debounce.c',
 	'src/evdev-fallback.c',
 	'src/evdev-fallback.h',
 	'src/evdev-middle-button.c',
diff --git a/src/evdev-debounce.c b/src/evdev-debounce.c
new file mode 100644
index 00000000..fbc3c530
--- /dev/null
+++ b/src/evdev-debounce.c
@@ -0,0 +1,564 @@
+/*
+ * Copyright © 2017 Red Hat, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "config.h"
+
+#include "evdev-fallback.h"
+
+/* Debounce cases to handle
+     P ... button press
+     R ... button release
+     ---|  timeout duration
+
+     'normal' .... event sent when it happens
+     'filtered' .. event is not sent (but may be sent later)
+     'delayed' ... event is sent with wall-clock delay
+
+
+   1) P---| R		P normal, R normal
+   2) R---| P		R normal, P normal
+   3) P---R--| P	P normal, R filtered, delayed, P normal
+   4) R---P--| R	R normal, P filtered, delayed, R normal
+   4.1) P---| R--P--|	P normal, R filtered
+   5) P--R-P-| R	P normal, R filtered, P filtered, R normal
+   6) R--P-R-| P	R normal, P filtered, R filtered, P normal
+   7) P--R--|
+          ---P-|	P normal, R filtered, P filtered
+   8) R--P--|
+          ---R-|	R normal, P filtered, R filtered
+
+   1, 2 are the normal click cases without debouncing taking effect
+   3, 4 are fast clicks where the second event is delivered with a delay
+   5, 6 are contact bounces, fast
+   7, 8 are contact bounces, slow
+
+   4.1 is a special case with the same event sequence as 4 but we want to
+   filter the *release* event out, it's a button losing contact while being
+   held down.
+
+   7 and 8 are cases where the first event happens within the first timeout
+   but the second event is outside that timeout (but within the timeout of
+   the second event). These cases are currently unhandled.
+*/
+
+
+enum debounce_event {
+	DEBOUNCE_EVENT_PRESS = 50,
+	DEBOUNCE_EVENT_RELEASE,
+	DEBOUNCE_EVENT_TIMEOUT,
+	DEBOUNCE_EVENT_TIMEOUT_SHORT,
+	DEBOUNCE_EVENT_OTHERBUTTON,
+};
+
+static inline const char *
+debounce_state_to_str(enum debounce_state state)
+{
+	switch(state) {
+	CASE_RETURN_STRING(DEBOUNCE_STATE_IS_UP);
+	CASE_RETURN_STRING(DEBOUNCE_STATE_IS_DOWN);
+	CASE_RETURN_STRING(DEBOUNCE_STATE_DOWN_WAITING);
+	CASE_RETURN_STRING(DEBOUNCE_STATE_RELEASE_PENDING);
+	CASE_RETURN_STRING(DEBOUNCE_STATE_RELEASE_DELAYED);
+	CASE_RETURN_STRING(DEBOUNCE_STATE_RELEASE_WAITING);
+	CASE_RETURN_STRING(DEBOUNCE_STATE_MAYBE_SPURIOUS);
+	CASE_RETURN_STRING(DEBOUNCE_STATE_RELEASED);
+	CASE_RETURN_STRING(DEBOUNCE_STATE_PRESS_PENDING);
+	}
+
+	return NULL;
+}
+
+static inline const char*
+debounce_event_to_str(enum debounce_event event)
+{
+	switch(event) {
+	CASE_RETURN_STRING(DEBOUNCE_EVENT_PRESS);
+	CASE_RETURN_STRING(DEBOUNCE_EVENT_RELEASE);
+	CASE_RETURN_STRING(DEBOUNCE_EVENT_TIMEOUT);
+	CASE_RETURN_STRING(DEBOUNCE_EVENT_TIMEOUT_SHORT);
+	CASE_RETURN_STRING(DEBOUNCE_EVENT_OTHERBUTTON);
+	}
+	return NULL;
+}
+
+static inline void
+log_debounce_bug(struct fallback_dispatch *fallback, enum debounce_event event)
+{
+	evdev_log_bug_libinput(fallback->device,
+			       "invalid debounce event %s in state %s\n",
+			       debounce_event_to_str(event),
+			       debounce_state_to_str(fallback->debounce.state));
+
+}
+
+static inline void
+debounce_set_state(struct fallback_dispatch *fallback,
+		   enum debounce_state new_state)
+{
+	assert(new_state >= DEBOUNCE_STATE_IS_UP &&
+	       new_state <= DEBOUNCE_STATE_PRESS_PENDING);
+
+	fallback->debounce.state = new_state;
+}
+
+static inline void
+debounce_set_timer(struct fallback_dispatch *fallback,
+		   uint64_t time)
+{
+	const int DEBOUNCE_TIMEOUT_BOUNCE = ms2us(25);
+
+	libinput_timer_set(&fallback->debounce.timer,
+			   time + DEBOUNCE_TIMEOUT_BOUNCE);
+}
+
+static inline void
+debounce_set_timer_short(struct fallback_dispatch *fallback,
+			 uint64_t time)
+{
+	const int DEBOUNCE_TIMEOUT_SPURIOUS = ms2us(12);
+
+	libinput_timer_set(&fallback->debounce.timer_short,
+			   time + DEBOUNCE_TIMEOUT_SPURIOUS);
+}
+
+static inline void
+debounce_cancel_timer(struct fallback_dispatch *fallback)
+{
+	libinput_timer_cancel(&fallback->debounce.timer);
+}
+
+static inline void
+debounce_cancel_timer_short(struct fallback_dispatch *fallback)
+{
+	libinput_timer_cancel(&fallback->debounce.timer_short);
+}
+
+static inline void
+debounce_enable_spurious(struct fallback_dispatch *fallback)
+{
+	if (fallback->debounce.spurious_enabled)
+		evdev_log_bug_libinput(fallback->device,
+				       "tried to enable spurious debouncing twice\n");
+
+	fallback->debounce.spurious_enabled = true;
+	evdev_log_info(fallback->device,
+		       "Enabling spurious button debouncing, "
+		       "see %sbutton_debouncing.html for details\n",
+		       HTTP_DOC_LINK);
+}
+
+static void
+debounce_notify_button(struct fallback_dispatch *fallback,
+		       enum libinput_button_state state)
+{
+	struct evdev_device *device = fallback->device;
+	unsigned int code = fallback->debounce.button_code;
+	uint64_t time = fallback->debounce.button_time;
+
+	code = evdev_to_left_handed(device, code);
+
+	evdev_pointer_notify_physical_button(device, time, code, state);
+}
+
+static void
+debounce_is_up_handle_event(struct fallback_dispatch *fallback, enum debounce_event event, uint64_t time)
+{
+	switch (event) {
+	case DEBOUNCE_EVENT_PRESS:
+		fallback->debounce.button_time = time;
+		debounce_set_timer(fallback, time);
+		debounce_set_state(fallback, DEBOUNCE_STATE_DOWN_WAITING);
+		debounce_notify_button(fallback,
+				       LIBINPUT_BUTTON_STATE_PRESSED);
+		break;
+	case DEBOUNCE_EVENT_RELEASE:
+	case DEBOUNCE_EVENT_TIMEOUT:
+	case DEBOUNCE_EVENT_TIMEOUT_SHORT:
+		log_debounce_bug(fallback, event);
+		break;
+	case DEBOUNCE_EVENT_OTHERBUTTON:
+		break;
+	}
+}
+
+static void
+debounce_is_down_handle_event(struct fallback_dispatch *fallback, enum debounce_event event, uint64_t time)
+{
+	switch (event) {
+	case DEBOUNCE_EVENT_PRESS:
+		log_debounce_bug(fallback, event);
+		break;
+	case DEBOUNCE_EVENT_RELEASE:
+		fallback->debounce.button_time = time;
+		debounce_set_timer(fallback, time);
+		debounce_set_timer_short(fallback, time);
+		if (fallback->debounce.spurious_enabled) {
+			debounce_set_state(fallback, DEBOUNCE_STATE_RELEASE_DELAYED);
+		} else {
+			debounce_set_state(fallback, DEBOUNCE_STATE_RELEASE_WAITING);
+			debounce_notify_button(fallback,
+					       LIBINPUT_BUTTON_STATE_RELEASED);
+		}
+		break;
+	case DEBOUNCE_EVENT_TIMEOUT:
+	case DEBOUNCE_EVENT_TIMEOUT_SHORT:
+		log_debounce_bug(fallback, event);
+		break;
+	case DEBOUNCE_EVENT_OTHERBUTTON:
+		break;
+	}
+}
+
+static void
+debounce_down_waiting_handle_event(struct fallback_dispatch *fallback, enum debounce_event event, uint64_t time)
+{
+	switch (event) {
+	case DEBOUNCE_EVENT_PRESS:
+		log_debounce_bug(fallback, event);
+		break;
+	case DEBOUNCE_EVENT_RELEASE:
+		debounce_set_state(fallback, DEBOUNCE_STATE_RELEASE_PENDING);
+		/* Note: In the debouncing RPR case, we use the last
+		 * release's time stamp */
+		fallback->debounce.button_time = time;
+		break;
+	case DEBOUNCE_EVENT_TIMEOUT:
+		debounce_set_state(fallback, DEBOUNCE_STATE_IS_DOWN);
+		break;
+	case DEBOUNCE_EVENT_TIMEOUT_SHORT:
+		log_debounce_bug(fallback, event);
+		break;
+	case DEBOUNCE_EVENT_OTHERBUTTON:
+		debounce_set_state(fallback, DEBOUNCE_STATE_IS_DOWN);
+		break;
+	}
+}
+
+static void
+debounce_release_pending_handle_event(struct fallback_dispatch *fallback, enum debounce_event event, uint64_t time)
+{
+	switch (event) {
+	case DEBOUNCE_EVENT_PRESS:
+		debounce_set_state(fallback, DEBOUNCE_STATE_DOWN_WAITING);
+		break;
+	case DEBOUNCE_EVENT_RELEASE:
+	case DEBOUNCE_EVENT_TIMEOUT_SHORT:
+		log_debounce_bug(fallback, event);
+		break;
+	case DEBOUNCE_EVENT_TIMEOUT:
+	case DEBOUNCE_EVENT_OTHERBUTTON:
+		debounce_set_state(fallback, DEBOUNCE_STATE_IS_UP);
+		debounce_notify_button(fallback,
+				       LIBINPUT_BUTTON_STATE_RELEASED);
+		break;
+	}
+}
+
+static void
+debounce_release_delayed_handle_event(struct fallback_dispatch *fallback, enum debounce_event event, uint64_t time)
+{
+	switch (event) {
+	case DEBOUNCE_EVENT_PRESS:
+		debounce_set_state(fallback, DEBOUNCE_STATE_IS_DOWN);
+		debounce_cancel_timer(fallback);
+		debounce_cancel_timer_short(fallback);
+		break;
+	case DEBOUNCE_EVENT_RELEASE:
+	case DEBOUNCE_EVENT_TIMEOUT:
+		log_debounce_bug(fallback, event);
+		break;
+	case DEBOUNCE_EVENT_TIMEOUT_SHORT:
+		debounce_set_state(fallback, DEBOUNCE_STATE_RELEASE_WAITING);
+		debounce_notify_button(fallback,
+				       LIBINPUT_BUTTON_STATE_RELEASED);
+		break;
+	case DEBOUNCE_EVENT_OTHERBUTTON:
+		debounce_set_state(fallback, DEBOUNCE_STATE_IS_UP);
+		debounce_notify_button(fallback,
+				       LIBINPUT_BUTTON_STATE_RELEASED);
+		break;
+	}
+}
+
+static void
+debounce_release_waiting_handle_event(struct fallback_dispatch *fallback, enum debounce_event event, uint64_t time)
+{
+	switch (event) {
+	case DEBOUNCE_EVENT_PRESS:
+		/* Note: in a bouncing PRP case, we use the last press
+		 * event time */
+		fallback->debounce.button_time = time;
+		debounce_set_state(fallback, DEBOUNCE_STATE_MAYBE_SPURIOUS);
+		break;
+	case DEBOUNCE_EVENT_RELEASE:
+		log_debounce_bug(fallback, event);
+		break;
+	case DEBOUNCE_EVENT_TIMEOUT:
+		debounce_set_state(fallback, DEBOUNCE_STATE_IS_UP);
+		break;
+	case DEBOUNCE_EVENT_TIMEOUT_SHORT:
+		debounce_set_state(fallback, DEBOUNCE_STATE_RELEASED);
+		break;
+	case DEBOUNCE_EVENT_OTHERBUTTON:
+		debounce_set_state(fallback, DEBOUNCE_STATE_IS_UP);
+		break;
+	}
+}
+
+static void
+debounce_maybe_spurious_handle_event(struct fallback_dispatch *fallback, enum debounce_event event, uint64_t time)
+{
+	switch (event) {
+	case DEBOUNCE_EVENT_PRESS:
+		log_debounce_bug(fallback, event);
+		break;
+	case DEBOUNCE_EVENT_RELEASE:
+		debounce_set_state(fallback, DEBOUNCE_STATE_RELEASE_WAITING);
+		break;
+	case DEBOUNCE_EVENT_TIMEOUT:
+		log_debounce_bug(fallback, event);
+		break;
+	case DEBOUNCE_EVENT_TIMEOUT_SHORT:
+		debounce_cancel_timer(fallback);
+		debounce_set_state(fallback, DEBOUNCE_STATE_IS_DOWN);
+		debounce_enable_spurious(fallback);
+		debounce_notify_button(fallback,
+				       LIBINPUT_BUTTON_STATE_PRESSED);
+		break;
+	case DEBOUNCE_EVENT_OTHERBUTTON:
+		debounce_set_state(fallback, DEBOUNCE_STATE_IS_UP);
+		debounce_notify_button(fallback,
+				       LIBINPUT_BUTTON_STATE_PRESSED);
+		break;
+	}
+}
+
+static void
+debounce_released_handle_event(struct fallback_dispatch *fallback, enum debounce_event event, uint64_t time)
+{
+	switch (event) {
+	case DEBOUNCE_EVENT_PRESS:
+		/* Note: in a debouncing PRP case, we use the last press'
+		 * time */
+		fallback->debounce.button_time = time;
+		debounce_set_state(fallback, DEBOUNCE_STATE_PRESS_PENDING);
+		break;
+	case DEBOUNCE_EVENT_RELEASE:
+	case DEBOUNCE_EVENT_TIMEOUT_SHORT:
+		log_debounce_bug(fallback, event);
+		break;
+	case DEBOUNCE_EVENT_TIMEOUT:
+	case DEBOUNCE_EVENT_OTHERBUTTON:
+		debounce_set_state(fallback, DEBOUNCE_STATE_IS_UP);
+		break;
+	}
+}
+
+static void
+debounce_press_pending_event(struct fallback_dispatch *fallback, enum debounce_event event, uint64_t time)
+{
+	switch (event) {
+	case DEBOUNCE_EVENT_PRESS:
+		log_debounce_bug(fallback, event);
+		break;
+	case DEBOUNCE_EVENT_RELEASE:
+		debounce_set_state(fallback, DEBOUNCE_STATE_RELEASED);
+		break;
+	case DEBOUNCE_EVENT_TIMEOUT_SHORT:
+		log_debounce_bug(fallback, event);
+		break;
+	case DEBOUNCE_EVENT_TIMEOUT:
+	case DEBOUNCE_EVENT_OTHERBUTTON:
+		debounce_set_state(fallback, DEBOUNCE_STATE_IS_UP);
+		debounce_notify_button(fallback,
+				       LIBINPUT_BUTTON_STATE_PRESSED);
+		break;
+	}
+}
+
+static void
+debounce_handle_event(struct fallback_dispatch *fallback,
+		      enum debounce_event event,
+		      uint64_t time)
+{
+	enum debounce_state current = fallback->debounce.state;
+
+	if (event == DEBOUNCE_EVENT_OTHERBUTTON) {
+		debounce_cancel_timer(fallback);
+		debounce_cancel_timer_short(fallback);
+	}
+
+	switch(current) {
+	case DEBOUNCE_STATE_IS_UP:
+		debounce_is_up_handle_event(fallback, event, time);
+		break;
+	case DEBOUNCE_STATE_IS_DOWN:
+		debounce_is_down_handle_event(fallback, event, time);
+		break;
+	case DEBOUNCE_STATE_DOWN_WAITING:
+		debounce_down_waiting_handle_event(fallback, event, time);
+		break;
+	case DEBOUNCE_STATE_RELEASE_PENDING:
+		debounce_release_pending_handle_event(fallback, event, time);
+		break;
+	case DEBOUNCE_STATE_RELEASE_DELAYED:
+		debounce_release_delayed_handle_event(fallback, event, time);
+		break;
+	case DEBOUNCE_STATE_RELEASE_WAITING:
+		debounce_release_waiting_handle_event(fallback, event, time);
+		break;
+	case DEBOUNCE_STATE_MAYBE_SPURIOUS:
+		debounce_maybe_spurious_handle_event(fallback, event, time);
+		break;
+	case DEBOUNCE_STATE_RELEASED:
+		debounce_released_handle_event(fallback, event, time);
+		break;
+	case DEBOUNCE_STATE_PRESS_PENDING:
+		debounce_press_pending_event(fallback, event, time);
+		break;
+	}
+
+	evdev_log_debug(fallback->device,
+			"debounce state: %s → %s → %s\n",
+			debounce_state_to_str(current),
+			debounce_event_to_str(event),
+			debounce_state_to_str(fallback->debounce.state));
+}
+
+void
+fallback_debounce_handle_state(struct fallback_dispatch *dispatch,
+			       uint64_t time)
+{
+	unsigned int changed[16] = {0}; /* event codes of changed buttons */
+	size_t nchanged = 0;
+	bool flushed = false;
+
+	for (unsigned int code = 0; code <= KEY_MAX; code++) {
+		if (get_key_type(code) != KEY_TYPE_BUTTON)
+			continue;
+
+		if (hw_key_has_changed(dispatch, code))
+			changed[nchanged++] = code;
+
+		/* If you manage to press more than 16 buttons in the same
+		 * frame, we just quietly ignore the rest of them */
+		if (nchanged == ARRAY_LENGTH(changed))
+			break;
+	}
+
+	/* If we have more than one button this frame or a different button,
+	 * flush the state machine with otherbutton */
+	if (nchanged > 1 ||
+	    changed[0] != dispatch->debounce.button_code) {
+		debounce_handle_event(dispatch,
+				      DEBOUNCE_EVENT_OTHERBUTTON,
+				      time);
+		flushed = true;
+	}
+
+	/* The state machine has some pre-conditions:
+	 * - the IS_DOWN and IS_UP states are neutral entry states without
+	 *   any timeouts
+	 * - a OTHERBUTTON event always flushes the state to IS_DOWN or
+	 *   IS_UP
+	 */
+
+	for (size_t i = 0; i < nchanged; i++) {
+		bool is_down = hw_is_key_down(dispatch, changed[i]);
+
+		if (flushed) {
+			debounce_set_state(dispatch,
+					   !is_down ?
+						   DEBOUNCE_STATE_IS_DOWN :
+						   DEBOUNCE_STATE_IS_UP);
+			flushed = false;
+		}
+
+
+		dispatch->debounce.button_code = changed[i];
+		debounce_handle_event(dispatch,
+				      is_down ?
+					      DEBOUNCE_EVENT_PRESS :
+					      DEBOUNCE_EVENT_RELEASE,
+				      time);
+
+		/* if we have more than one event, we flush the state
+		 * machine immediately after the event itself */
+		if (nchanged > 1) {
+			debounce_handle_event(dispatch,
+					      DEBOUNCE_EVENT_OTHERBUTTON,
+					      time);
+			flushed = true;
+		}
+
+	}
+}
+
+static void
+debounce_timeout(uint64_t now, void *data)
+{
+	struct evdev_device *device = data;
+	struct fallback_dispatch *dispatch =
+		fallback_dispatch(device->dispatch);
+
+	debounce_handle_event(dispatch, DEBOUNCE_EVENT_TIMEOUT, now);
+}
+
+static void
+debounce_timeout_short(uint64_t now, void *data)
+{
+	struct evdev_device *device = data;
+	struct fallback_dispatch *dispatch =
+		fallback_dispatch(device->dispatch);
+
+	debounce_handle_event(dispatch, DEBOUNCE_EVENT_TIMEOUT_SHORT, now);
+}
+
+void
+fallback_init_debounce(struct fallback_dispatch *dispatch)
+{
+	struct evdev_device *device = dispatch->device;
+	char timer_name[64];
+
+	dispatch->debounce.state = DEBOUNCE_STATE_IS_UP;
+
+	snprintf(timer_name,
+		 sizeof(timer_name),
+		 "%s debounce short",
+		 evdev_device_get_sysname(device));
+	libinput_timer_init(&dispatch->debounce.timer_short,
+			    evdev_libinput_context(device),
+			    timer_name,
+			    debounce_timeout_short,
+			    device);
+
+	snprintf(timer_name,
+		 sizeof(timer_name),
+		 "%s debounce",
+		 evdev_device_get_sysname(device));
+	libinput_timer_init(&dispatch->debounce.timer,
+			    evdev_libinput_context(device),
+			    timer_name,
+			    debounce_timeout,
+			    device);
+}
diff --git a/src/evdev-fallback.c b/src/evdev-fallback.c
index 70cbd3e1..7bfcaf90 100644
--- a/src/evdev-fallback.c
+++ b/src/evdev-fallback.c
@@ -32,49 +32,6 @@
 
 #define	DEBOUNCE_TIME ms2us(12)
 
-enum key_type {
-	KEY_TYPE_NONE,
-	KEY_TYPE_KEY,
-	KEY_TYPE_BUTTON,
-};
-
-static void
-hw_set_key_down(struct fallback_dispatch *dispatch, int code, int pressed)
-{
-	long_set_bit_state(dispatch->hw_key_mask, code, pressed);
-}
-
-static bool
-hw_key_has_changed(struct fallback_dispatch *dispatch, int code)
-{
-	return long_bit_is_set(dispatch->hw_key_mask, code) !=
-		long_bit_is_set(dispatch->last_hw_key_mask, code);
-}
-
-static void
-hw_key_update_last_state(struct fallback_dispatch *dispatch)
-{
-	static_assert(sizeof(dispatch->hw_key_mask) ==
-		      sizeof(dispatch->last_hw_key_mask),
-		      "Mismatching key mask size");
-
-	memcpy(dispatch->last_hw_key_mask,
-	       dispatch->hw_key_mask,
-	       sizeof(dispatch->hw_key_mask));
-}
-
-static bool
-hw_is_key_down(struct fallback_dispatch *dispatch, int code)
-{
-	return long_bit_is_set(dispatch->hw_key_mask, code);
-}
-
-static int
-get_key_down_count(struct evdev_device *device, int code)
-{
-	return device->key_count[code];
-}
-
 static void
 fallback_keyboard_notify_key(struct fallback_dispatch *dispatch,
 			     struct evdev_device *device,
@@ -495,41 +452,6 @@ fallback_flush_st_up(struct fallback_dispatch *dispatch,
 	return true;
 }
 
-static enum key_type
-get_key_type(uint16_t code)
-{
-	switch (code) {
-	case BTN_TOOL_PEN:
-	case BTN_TOOL_RUBBER:
-	case BTN_TOOL_BRUSH:
-	case BTN_TOOL_PENCIL:
-	case BTN_TOOL_AIRBRUSH:
-	case BTN_TOOL_MOUSE:
-	case BTN_TOOL_LENS:
-	case BTN_TOOL_QUINTTAP:
-	case BTN_TOOL_DOUBLETAP:
-	case BTN_TOOL_TRIPLETAP:
-	case BTN_TOOL_QUADTAP:
-	case BTN_TOOL_FINGER:
-	case BTN_TOUCH:
-		return KEY_TYPE_NONE;
-	}
-
-	if (code >= KEY_ESC && code <= KEY_MICMUTE)
-		return KEY_TYPE_KEY;
-	if (code >= BTN_MISC && code <= BTN_GEAR_UP)
-		return KEY_TYPE_BUTTON;
-	if (code >= KEY_OK && code <= KEY_LIGHTS_TOGGLE)
-		return KEY_TYPE_KEY;
-	if (code >= BTN_DPAD_UP && code <= BTN_DPAD_RIGHT)
-		return KEY_TYPE_BUTTON;
-	if (code >= KEY_ALS_TOGGLE && code <= KEY_ONSCREEN_KEYBOARD)
-		return KEY_TYPE_KEY;
-	if (code >= BTN_TRIGGER_HAPPY && code <= BTN_TRIGGER_HAPPY40)
-		return KEY_TYPE_BUTTON;
-	return KEY_TYPE_NONE;
-}
-
 static void
 fallback_process_touch_button(struct fallback_dispatch *dispatch,
 			      struct evdev_device *device,
@@ -541,149 +463,6 @@ fallback_process_touch_button(struct fallback_dispatch *dispatch,
 }
 
 static inline void
-fallback_flush_debounce(struct fallback_dispatch *dispatch,
-			struct evdev_device *device)
-{
-	int code = dispatch->debounce.button_code;
-	int button;
-
-	if (dispatch->debounce.state != DEBOUNCE_ACTIVE)
-		return;
-
-	if (hw_is_key_down(dispatch, code)) {
-		button = evdev_to_left_handed(device, code);
-		evdev_pointer_notify_physical_button(device,
-						     dispatch->debounce.button_up_time,
-						     button,
-						     LIBINPUT_BUTTON_STATE_RELEASED);
-		hw_set_key_down(dispatch, code, 0);
-		hw_key_update_last_state(dispatch);
-	}
-
-	dispatch->debounce.state = DEBOUNCE_ON;
-}
-
-static void
-fallback_debounce_timeout(uint64_t now, void *data)
-{
-	struct evdev_device *device = data;
-	struct fallback_dispatch *dispatch =
-		fallback_dispatch(device->dispatch);
-
-	fallback_flush_debounce(dispatch, device);
-}
-
-static bool
-fallback_filter_debounce_press(struct fallback_dispatch *dispatch,
-			       struct evdev_device *device,
-			       struct input_event *e,
-			       uint64_t time)
-{
-	bool filter = false;
-	uint64_t tdelta;
-
-	/* If other button is pressed while we're holding back the release,
-	 * flush the pending release (if any) and continue. We don't handle
-	 * this situation, if you have a mouse that needs per-button
-	 * debouncing, consider writing to santa for a new mouse.
-	 */
-	if (e->code != dispatch->debounce.button_code) {
-		if (dispatch->debounce.state == DEBOUNCE_ACTIVE) {
-			libinput_timer_cancel(&dispatch->debounce.timer);
-			fallback_flush_debounce(dispatch, device);
-		}
-		return false;
-	}
-
-	tdelta = time - dispatch->debounce.button_up_time;
-	assert((int64_t)tdelta >= 0);
-
-	if (tdelta < DEBOUNCE_TIME) {
-		switch (dispatch->debounce.state) {
-		case DEBOUNCE_INIT:
-			/* This is the first time we debounce, enable proper debouncing
-			   from now on but filter this press event */
-			filter = true;
-			evdev_log_info(device,
-				       "Enabling button debouncing, "
-				       "see %sbutton_debouncing.html for details\n",
-				       HTTP_DOC_LINK);
-			dispatch->debounce.state = DEBOUNCE_NEEDED;
-			break;
-		case DEBOUNCE_NEEDED:
-		case DEBOUNCE_ON:
-			break;
-		/* If a release event is pending and, filter press
-		 * events until we flushed the release */
-		case DEBOUNCE_ACTIVE:
-			filter = true;
-			break;
-		}
-	} else if (dispatch->debounce.state == DEBOUNCE_ACTIVE) {
-		/* call libinput_dispatch() more frequently */
-		evdev_log_bug_client(device,
-				     "Debouncing still active past timeout\n");
-	}
-
-	return filter;
-}
-
-static bool
-fallback_filter_debounce_release(struct fallback_dispatch *dispatch,
-				 struct input_event *e,
-				 uint64_t time)
-{
-	bool filter = false;
-
-	dispatch->debounce.button_code = e->code;
-	dispatch->debounce.button_up_time = time;
-
-	switch (dispatch->debounce.state) {
-	case DEBOUNCE_INIT:
-		break;
-	case DEBOUNCE_NEEDED:
-		filter = true;
-		dispatch->debounce.state = DEBOUNCE_ON;
-		break;
-	case DEBOUNCE_ON:
-		libinput_timer_set(&dispatch->debounce.timer,
-				   time + DEBOUNCE_TIME);
-		filter = true;
-		dispatch->debounce.state = DEBOUNCE_ACTIVE;
-		break;
-	case DEBOUNCE_ACTIVE:
-		filter = true;
-		break;
-	}
-
-	return filter;
-}
-
-static bool
-fallback_filter_debounce(struct fallback_dispatch *dispatch,
-			 struct evdev_device *device,
-			 struct input_event *e, uint64_t time)
-{
-	bool filter = false;
-
-	/* Behavior: we monitor the time deltas between release and press
-	 * events. Proper debouncing is disabled on init, but the first
-	 * time we see a bouncing press event we enable it.
-	 *
-	 * The first bounced event is simply discarded, which ends up in the
-	 * button being released sooner than it should be. Subsequent button
-	 * presses are timer-based and thus released a bit later because we
-	 * then wait for a timeout before we post the release event.
-	 */
-	if (e->value)
-		filter = fallback_filter_debounce_press(dispatch, device, e, time);
-	else
-		filter = fallback_filter_debounce_release(dispatch, e, time);
-
-	return filter;
-}
-
-static inline void
 fallback_process_key(struct fallback_dispatch *dispatch,
 		     struct evdev_device *device,
 		     struct input_event *e, uint64_t time)
@@ -712,20 +491,11 @@ fallback_process_key(struct fallback_dispatch *dispatch,
 	case KEY_TYPE_NONE:
 		break;
 	case KEY_TYPE_KEY:
-		if ((e->value && hw_is_key_down(dispatch, e->code)) ||
-		    (e->value == 0 && !hw_is_key_down(dispatch, e->code)))
-			return;
-
-		dispatch->pending_event |= EVDEV_KEY;
-		break;
 	case KEY_TYPE_BUTTON:
-		/* FIXME: should move to handle_state */
-		if (fallback_filter_debounce(dispatch, device, e, time))
-			return;
-
 		if ((e->value && hw_is_key_down(dispatch, e->code)) ||
 		    (e->value == 0 && !hw_is_key_down(dispatch, e->code)))
 			return;
+
 		dispatch->pending_event |= EVDEV_KEY;
 		break;
 	}
@@ -1055,6 +825,7 @@ fallback_handle_state(struct fallback_dispatch *dispatch,
 
 	/* Buttons and keys */
 	if (dispatch->pending_event & EVDEV_KEY) {
+		bool want_debounce = false;
 		for (unsigned int code = 0; code <= KEY_MAX; code++) {
 			bool new_state;
 
@@ -1077,17 +848,14 @@ fallback_handle_state(struct fallback_dispatch *dispatch,
 							     LIBINPUT_KEY_STATE_RELEASED);
 				break;
 			case KEY_TYPE_BUTTON:
-				evdev_pointer_notify_physical_button(
-						     device,
-						     time,
-						     evdev_to_left_handed(device, code),
-						     new_state ?
-							     LIBINPUT_BUTTON_STATE_PRESSED :
-							     LIBINPUT_BUTTON_STATE_RELEASED);
+				want_debounce = true;
 				break;
 			}
-
 		}
+
+		if (want_debounce)
+			fallback_debounce_handle_state(dispatch, time);
+
 		hw_key_update_last_state(dispatch);
 	}
 
@@ -1299,6 +1067,8 @@ fallback_interface_destroy(struct evdev_dispatch *evdev_dispatch)
 
 	libinput_timer_cancel(&dispatch->debounce.timer);
 	libinput_timer_destroy(&dispatch->debounce.timer);
+	libinput_timer_cancel(&dispatch->debounce.timer_short);
+	libinput_timer_destroy(&dispatch->debounce.timer_short);
 	free(dispatch->mt.slots);
 	free(dispatch);
 }
@@ -1672,7 +1442,6 @@ fallback_dispatch_create(struct libinput_device *libinput_device)
 {
 	struct evdev_device *device = evdev_device(libinput_device);
 	struct fallback_dispatch *dispatch;
-	char timer_name[64];
 
 	dispatch = zalloc(sizeof *dispatch);
 	dispatch->device = evdev_device(libinput_device);
@@ -1722,14 +1491,7 @@ fallback_dispatch_create(struct libinput_device *libinput_device)
 					want_config);
 	}
 
-	snprintf(timer_name,
-		 sizeof(timer_name),
-		 "%s debounce",
-		 evdev_device_get_sysname(device));
-	libinput_timer_init(&dispatch->debounce.timer,
-			    evdev_libinput_context(device),
-			    timer_name,
-			    fallback_debounce_timeout,
-			    device);
+	fallback_init_debounce(dispatch);
+
 	return &dispatch->base;
 }
diff --git a/src/evdev-fallback.h b/src/evdev-fallback.h
index ba121c08..0d9e247e 100644
--- a/src/evdev-fallback.h
+++ b/src/evdev-fallback.h
@@ -31,6 +31,18 @@
 
 #include "evdev.h"
 
+enum debounce_state {
+	DEBOUNCE_STATE_IS_UP = 100,
+	DEBOUNCE_STATE_IS_DOWN,
+	DEBOUNCE_STATE_DOWN_WAITING,
+	DEBOUNCE_STATE_RELEASE_PENDING,
+	DEBOUNCE_STATE_RELEASE_DELAYED,
+	DEBOUNCE_STATE_RELEASE_WAITING,
+	DEBOUNCE_STATE_MAYBE_SPURIOUS,
+	DEBOUNCE_STATE_RELEASED,
+	DEBOUNCE_STATE_PRESS_PENDING,
+};
+
 struct fallback_dispatch {
 	struct evdev_dispatch base;
 	struct evdev_device *device;
@@ -85,10 +97,16 @@ struct fallback_dispatch {
 	bool ignore_events;
 
 	struct {
+#if 0
 		enum evdev_debounce_state state;
-		unsigned int button_code;
 		uint64_t button_up_time;
+#endif
+		unsigned int button_code;
+		uint64_t button_time;
 		struct libinput_timer timer;
+		struct libinput_timer timer_short;
+		enum debounce_state state;
+		bool spurious_enabled;
 	} debounce;
 
 	struct {
@@ -119,4 +137,86 @@ fallback_dispatch(struct evdev_dispatch *dispatch)
 	return container_of(dispatch, struct fallback_dispatch, base);
 }
 
+enum key_type {
+	KEY_TYPE_NONE,
+	KEY_TYPE_KEY,
+	KEY_TYPE_BUTTON,
+};
+
+static inline enum key_type
+get_key_type(uint16_t code)
+{
+	switch (code) {
+	case BTN_TOOL_PEN:
+	case BTN_TOOL_RUBBER:
+	case BTN_TOOL_BRUSH:
+	case BTN_TOOL_PENCIL:
+	case BTN_TOOL_AIRBRUSH:
+	case BTN_TOOL_MOUSE:
+	case BTN_TOOL_LENS:
+	case BTN_TOOL_QUINTTAP:
+	case BTN_TOOL_DOUBLETAP:
+	case BTN_TOOL_TRIPLETAP:
+	case BTN_TOOL_QUADTAP:
+	case BTN_TOOL_FINGER:
+	case BTN_TOUCH:
+		return KEY_TYPE_NONE;
+	}
+
+	if (code >= KEY_ESC && code <= KEY_MICMUTE)
+		return KEY_TYPE_KEY;
+	if (code >= BTN_MISC && code <= BTN_GEAR_UP)
+		return KEY_TYPE_BUTTON;
+	if (code >= KEY_OK && code <= KEY_LIGHTS_TOGGLE)
+		return KEY_TYPE_KEY;
+	if (code >= BTN_DPAD_UP && code <= BTN_DPAD_RIGHT)
+		return KEY_TYPE_BUTTON;
+	if (code >= KEY_ALS_TOGGLE && code <= KEY_ONSCREEN_KEYBOARD)
+		return KEY_TYPE_KEY;
+	if (code >= BTN_TRIGGER_HAPPY && code <= BTN_TRIGGER_HAPPY40)
+		return KEY_TYPE_BUTTON;
+	return KEY_TYPE_NONE;
+}
+
+static inline void
+hw_set_key_down(struct fallback_dispatch *dispatch, int code, int pressed)
+{
+	long_set_bit_state(dispatch->hw_key_mask, code, pressed);
+}
+
+static inline bool
+hw_key_has_changed(struct fallback_dispatch *dispatch, int code)
+{
+	return long_bit_is_set(dispatch->hw_key_mask, code) !=
+		long_bit_is_set(dispatch->last_hw_key_mask, code);
+}
+
+static inline void
+hw_key_update_last_state(struct fallback_dispatch *dispatch)
+{
+	static_assert(sizeof(dispatch->hw_key_mask) ==
+		      sizeof(dispatch->last_hw_key_mask),
+		      "Mismatching key mask size");
+
+	memcpy(dispatch->last_hw_key_mask,
+	       dispatch->hw_key_mask,
+	       sizeof(dispatch->hw_key_mask));
+}
+
+static inline bool
+hw_is_key_down(struct fallback_dispatch *dispatch, int code)
+{
+	return long_bit_is_set(dispatch->hw_key_mask, code);
+}
+
+static inline int
+get_key_down_count(struct evdev_device *device, int code)
+{
+	return device->key_count[code];
+}
+
+void fallback_init_debounce(struct fallback_dispatch *dispatch);
+void fallback_debounce_handle_state(struct fallback_dispatch *dispatch,
+				    uint64_t time);
+
 #endif
diff --git a/test/litest.c b/test/litest.c
index 9819bed9..a447c81c 100644
--- a/test/litest.c
+++ b/test/litest.c
@@ -3182,7 +3182,7 @@ litest_timeout_tapndrag(void)
 void
 litest_timeout_debounce(void)
 {
-	msleep(15);
+	msleep(30);
 }
 
 void
diff --git a/test/test-pointer.c b/test/test-pointer.c
index 136aa48c..7324c0f6 100644
--- a/test/test-pointer.c
+++ b/test/test-pointer.c
@@ -2110,92 +2110,176 @@ START_TEST(pointer_time_usec)
 }
 END_TEST
 
-START_TEST(debounce)
+START_TEST(debounce_bounce)
 {
 	struct litest_device *dev = litest_current_device();
 	struct libinput *li = dev->libinput;
+	unsigned int button = _i; /* ranged test */
+
+	if (!libinput_device_pointer_has_button(dev->libinput_device,
+						button))
+		return;
 
 	litest_disable_middleemu(dev);
+	disable_button_scrolling(dev);
 	litest_drain_events(li);
 
-	litest_event(dev, EV_KEY, BTN_LEFT, 1);
+	litest_event(dev, EV_KEY, button, 1);
 	litest_event(dev, EV_SYN, SYN_REPORT, 0);
-	litest_event(dev, EV_KEY, BTN_LEFT, 0);
-	litest_event(dev, EV_SYN, SYN_REPORT, 0);
-
-	/* expect debouncing on now, this event is ignored */
-	litest_event(dev, EV_KEY, BTN_LEFT, 1);
+	litest_event(dev, EV_KEY, button, 0);
 	litest_event(dev, EV_SYN, SYN_REPORT, 0);
-	litest_event(dev, EV_KEY, BTN_LEFT, 0);
+	litest_event(dev, EV_KEY, button, 1);
 	litest_event(dev, EV_SYN, SYN_REPORT, 0);
-
+	libinput_dispatch(li);
+	litest_timeout_debounce();
 	libinput_dispatch(li);
 
 	litest_assert_button_event(li,
-				   BTN_LEFT,
+				   button,
 				   LIBINPUT_BUTTON_STATE_PRESSED);
+	litest_assert_empty_queue(li);
+
+	litest_event(dev, EV_KEY, button, 0);
+	litest_event(dev, EV_SYN, SYN_REPORT, 0);
+	litest_event(dev, EV_KEY, button, 1);
+	litest_event(dev, EV_SYN, SYN_REPORT, 0);
+	litest_event(dev, EV_KEY, button, 0);
+	litest_event(dev, EV_SYN, SYN_REPORT, 0);
+	libinput_dispatch(li);
+	litest_timeout_debounce();
+	libinput_dispatch(li);
+
 	litest_assert_button_event(li,
-				   BTN_LEFT,
+				   button,
 				   LIBINPUT_BUTTON_STATE_RELEASED);
 
 	litest_assert_empty_queue(li);
 }
 END_TEST
 
-START_TEST(debounce_timer)
+START_TEST(debounce_bounce_check_immediate)
 {
 	struct litest_device *dev = litest_current_device();
 	struct libinput *li = dev->libinput;
 
 	litest_disable_middleemu(dev);
+	disable_button_scrolling(dev);
 	litest_drain_events(li);
 
+	/* Press must be sent without delay */
 	litest_event(dev, EV_KEY, BTN_LEFT, 1);
 	litest_event(dev, EV_SYN, SYN_REPORT, 0);
+	litest_assert_button_event(li,
+				   BTN_LEFT,
+				   LIBINPUT_BUTTON_STATE_PRESSED);
+	litest_timeout_debounce();
+	litest_assert_empty_queue(li);
+
+	/* held down & past timeout, we expect releases to be immediate */
+
 	litest_event(dev, EV_KEY, BTN_LEFT, 0);
 	litest_event(dev, EV_SYN, SYN_REPORT, 0);
+	litest_assert_button_event(li,
+				   BTN_LEFT,
+				   LIBINPUT_BUTTON_STATE_RELEASED);
 
-	/* expect debouncing on now, this event is ignored */
+	litest_timeout_debounce();
+	litest_assert_empty_queue(li);
+}
+END_TEST
+
+/* Triggers the event sequence that initializes the spurious
+ * debouncing behavior */
+static inline void
+debounce_trigger_spurious(struct litest_device *dev, struct libinput *li)
+{
 	litest_event(dev, EV_KEY, BTN_LEFT, 1);
 	litest_event(dev, EV_SYN, SYN_REPORT, 0);
+	libinput_dispatch(li);
+	litest_timeout_debounce();
+	libinput_dispatch(li);
+
+	litest_assert_button_event(li,
+				   BTN_LEFT,
+				   LIBINPUT_BUTTON_STATE_PRESSED);
+
 	litest_event(dev, EV_KEY, BTN_LEFT, 0);
 	litest_event(dev, EV_SYN, SYN_REPORT, 0);
 	libinput_dispatch(li);
+	litest_event(dev, EV_KEY, BTN_LEFT, 1);
+	litest_event(dev, EV_SYN, SYN_REPORT, 0);
+	libinput_dispatch(li);
 
 	litest_timeout_debounce();
+	libinput_dispatch(li);
+
+	litest_assert_button_event(li,
+				   BTN_LEFT,
+				   LIBINPUT_BUTTON_STATE_RELEASED);
+	litest_assert_button_event(li,
+				   BTN_LEFT,
+				   LIBINPUT_BUTTON_STATE_PRESSED);
+
+	/* gets filtered now */
+	litest_event(dev, EV_KEY, BTN_LEFT, 0);
+	litest_event(dev, EV_SYN, SYN_REPORT, 0);
+	libinput_dispatch(li);
+	litest_timeout_debounce();
+	libinput_dispatch(li);
+	litest_assert_button_event(li,
+				   BTN_LEFT,
+				   LIBINPUT_BUTTON_STATE_RELEASED);
+	litest_assert_empty_queue(li);
+}
+
+START_TEST(debounce_spurious)
+{
+	struct litest_device *dev = litest_current_device();
+	struct libinput *li = dev->libinput;
+	unsigned int button = _i; /* ranged test */
+
+	if (!libinput_device_pointer_has_button(dev->libinput_device,
+						button))
+		return;
+
+	litest_disable_middleemu(dev);
+	disable_button_scrolling(dev);
 	litest_drain_events(li);
 
+	debounce_trigger_spurious(dev, li);
+
 	for (int i = 0; i < 3; i++) {
-		litest_event(dev, EV_KEY, BTN_LEFT, 1);
+		litest_event(dev, EV_KEY, button, 1);
 		litest_event(dev, EV_SYN, SYN_REPORT, 0);
 		libinput_dispatch(li);
 		litest_timeout_debounce();
+		libinput_dispatch(li);
 
 		/* Not all devices can disable middle button emulation, time out on
 		 * middle button here to make sure the initial button press event
 		 * was flushed.
 		 */
-		libinput_dispatch(li);
 		litest_timeout_middlebutton();
 		libinput_dispatch(li);
+
 		litest_assert_button_event(li,
-					   BTN_LEFT,
+					   button,
 					   LIBINPUT_BUTTON_STATE_PRESSED);
 
 		/* bouncy bouncy bouncy */
-		litest_event(dev, EV_KEY, BTN_LEFT, 0);
+		litest_event(dev, EV_KEY, button, 0);
 		litest_event(dev, EV_SYN, SYN_REPORT, 0);
-		litest_event(dev, EV_KEY, BTN_LEFT, 1);
+		litest_event(dev, EV_KEY, button, 1);
 		litest_event(dev, EV_SYN, SYN_REPORT, 0);
 		litest_assert_empty_queue(li);
 
-		litest_event(dev, EV_KEY, BTN_LEFT, 0);
+		litest_event(dev, EV_KEY, button, 0);
 		litest_event(dev, EV_SYN, SYN_REPORT, 0);
 		libinput_dispatch(li);
 		litest_timeout_debounce();
 		libinput_dispatch(li);
 		litest_assert_button_event(li,
-					   BTN_LEFT,
+					   button,
 					   LIBINPUT_BUTTON_STATE_RELEASED);
 
 		litest_assert_empty_queue(li);
@@ -2203,7 +2287,7 @@ START_TEST(debounce_timer)
 }
 END_TEST
 
-START_TEST(debounce_multibounce)
+START_TEST(debounce_spurious_multibounce)
 {
 	struct litest_device *dev = litest_current_device();
 	struct libinput *li = dev->libinput;
@@ -2211,15 +2295,7 @@ START_TEST(debounce_multibounce)
 	litest_disable_middleemu(dev);
 	litest_drain_events(li);
 
-	/* enable debouncing */
-	litest_event(dev, EV_KEY, BTN_LEFT, 1);
-	litest_event(dev, EV_SYN, SYN_REPORT, 0);
-	litest_event(dev, EV_KEY, BTN_LEFT, 0);
-	litest_event(dev, EV_SYN, SYN_REPORT, 0);
-	litest_event(dev, EV_KEY, BTN_LEFT, 1);
-	litest_event(dev, EV_SYN, SYN_REPORT, 0);
-	litest_event(dev, EV_KEY, BTN_LEFT, 0);
-	litest_event(dev, EV_SYN, SYN_REPORT, 0);
+	debounce_trigger_spurious(dev, li);
 	litest_drain_events(li);
 
 	/* Let's assume our button has ventricular fibrilation and sends a
@@ -2230,6 +2306,8 @@ START_TEST(debounce_multibounce)
 
 	litest_event(dev, EV_KEY, BTN_LEFT, 1);
 	litest_event(dev, EV_SYN, SYN_REPORT, 0);
+	libinput_dispatch(li);
+	litest_timeout_debounce();
 
 	/* Not all devices can disable middle button emulation, time out on
 	 * middle button here to make sure the initial button press event
@@ -2268,20 +2346,34 @@ START_TEST(debounce_multibounce)
 }
 END_TEST
 
-START_TEST(debounce_no_debounce_for_otherbutton)
+START_TEST(debounce_spurious_dont_enable_on_otherbutton)
 {
 	struct litest_device *dev = litest_current_device();
+	struct libinput_device *device = dev->libinput_device;
 	struct libinput *li = dev->libinput;
 
+	if (!libinput_device_config_middle_emulation_is_available(device))
+		return;
+
 	litest_disable_middleemu(dev);
+	disable_button_scrolling(dev);
 	litest_drain_events(li);
 
+	/* Don't trigger spurious debouncing on otherbutton events */
 	litest_event(dev, EV_KEY, BTN_LEFT, 1);
 	litest_event(dev, EV_SYN, SYN_REPORT, 0);
+	libinput_dispatch(li);
+	litest_timeout_debounce();
+	libinput_dispatch(li);
+
 	litest_event(dev, EV_KEY, BTN_LEFT, 0);
 	litest_event(dev, EV_SYN, SYN_REPORT, 0);
 	litest_event(dev, EV_KEY, BTN_RIGHT, 1);
 	litest_event(dev, EV_SYN, SYN_REPORT, 0);
+	litest_event(dev, EV_KEY, BTN_LEFT, 1);
+	litest_event(dev, EV_SYN, SYN_REPORT, 0);
+	litest_event(dev, EV_KEY, BTN_LEFT, 0);
+	litest_event(dev, EV_SYN, SYN_REPORT, 0);
 	litest_event(dev, EV_KEY, BTN_RIGHT, 0);
 	litest_event(dev, EV_SYN, SYN_REPORT, 0);
 
@@ -2298,42 +2390,67 @@ START_TEST(debounce_no_debounce_for_otherbutton)
 				   BTN_RIGHT,
 				   LIBINPUT_BUTTON_STATE_PRESSED);
 	litest_assert_button_event(li,
+				   BTN_LEFT,
+				   LIBINPUT_BUTTON_STATE_PRESSED);
+	litest_assert_button_event(li,
+				   BTN_LEFT,
+				   LIBINPUT_BUTTON_STATE_RELEASED);
+	litest_assert_button_event(li,
 				   BTN_RIGHT,
 				   LIBINPUT_BUTTON_STATE_RELEASED);
 
 	litest_assert_empty_queue(li);
+
+	/* Expect release to be immediate */
+	litest_event(dev, EV_KEY, BTN_LEFT, 1);
+	litest_event(dev, EV_SYN, SYN_REPORT, 0);
+	libinput_dispatch(li);
+	litest_timeout_debounce();
+	libinput_dispatch(li);
+
+	litest_event(dev, EV_KEY, BTN_LEFT, 0);
+	litest_event(dev, EV_SYN, SYN_REPORT, 0);
+	libinput_dispatch(li);
+	litest_assert_button_event(li,
+				   BTN_LEFT,
+				   LIBINPUT_BUTTON_STATE_PRESSED);
+	litest_assert_button_event(li,
+				   BTN_LEFT,
+				   LIBINPUT_BUTTON_STATE_RELEASED);
 }
 END_TEST
 
-START_TEST(debounce_cancel_debounce_otherbutton)
+START_TEST(debounce_spurious_cancel_debounce_otherbutton)
 {
 	struct litest_device *dev = litest_current_device();
+	struct libinput_device *device = dev->libinput_device;
 	struct libinput *li = dev->libinput;
 
+	if (!libinput_device_config_middle_emulation_is_available(device))
+		return;
+
 	litest_disable_middleemu(dev);
+	disable_button_scrolling(dev);
 	litest_drain_events(li);
 
+	debounce_trigger_spurious(dev, li);
+
 	litest_event(dev, EV_KEY, BTN_LEFT, 1);
 	litest_event(dev, EV_SYN, SYN_REPORT, 0);
+	libinput_dispatch(li);
+	litest_timeout_debounce();
+	libinput_dispatch(li);
+
+	/* spurious debouncing is on but the release should get flushed by
+	 * the other button */
 	litest_event(dev, EV_KEY, BTN_LEFT, 0);
 	litest_event(dev, EV_SYN, SYN_REPORT, 0);
-	litest_event(dev, EV_KEY, BTN_LEFT, 1);
-	litest_event(dev, EV_SYN, SYN_REPORT, 0);
-	litest_event(dev, EV_KEY, BTN_LEFT, 0);
+	litest_event(dev, EV_KEY, BTN_RIGHT, 1);
 	litest_event(dev, EV_SYN, SYN_REPORT, 0);
-
-	litest_drain_events(li);
-
 	litest_event(dev, EV_KEY, BTN_LEFT, 1);
 	litest_event(dev, EV_SYN, SYN_REPORT, 0);
 	litest_event(dev, EV_KEY, BTN_LEFT, 0);
 	litest_event(dev, EV_SYN, SYN_REPORT, 0);
-	litest_event(dev, EV_KEY, BTN_LEFT, 1);
-	litest_event(dev, EV_SYN, SYN_REPORT, 0);
-	/* release is now held back, press was ignored,
-	 * other button should flush the release */
-	litest_event(dev, EV_KEY, BTN_RIGHT, 1);
-	litest_event(dev, EV_SYN, SYN_REPORT, 0);
 	litest_event(dev, EV_KEY, BTN_RIGHT, 0);
 	litest_event(dev, EV_SYN, SYN_REPORT, 0);
 
@@ -2350,6 +2467,12 @@ START_TEST(debounce_cancel_debounce_otherbutton)
 				   BTN_RIGHT,
 				   LIBINPUT_BUTTON_STATE_PRESSED);
 	litest_assert_button_event(li,
+				   BTN_LEFT,
+				   LIBINPUT_BUTTON_STATE_PRESSED);
+	litest_assert_button_event(li,
+				   BTN_LEFT,
+				   LIBINPUT_BUTTON_STATE_RELEASED);
+	litest_assert_button_event(li,
 				   BTN_RIGHT,
 				   LIBINPUT_BUTTON_STATE_RELEASED);
 
@@ -2357,31 +2480,28 @@ START_TEST(debounce_cancel_debounce_otherbutton)
 }
 END_TEST
 
-START_TEST(debounce_switch_to_otherbutton)
+START_TEST(debounce_spurious_switch_to_otherbutton)
 {
 	struct litest_device *dev = litest_current_device();
+	struct libinput_device *device = dev->libinput_device;
 	struct libinput *li = dev->libinput;
 
+	if (!libinput_device_config_middle_emulation_is_available(device))
+		return;
+
 	litest_drain_events(li);
+	debounce_trigger_spurious(dev, li);
 
 	litest_event(dev, EV_KEY, BTN_LEFT, 1);
 	litest_event(dev, EV_SYN, SYN_REPORT, 0);
-	litest_event(dev, EV_KEY, BTN_LEFT, 0);
-	litest_event(dev, EV_SYN, SYN_REPORT, 0);
-	litest_event(dev, EV_KEY, BTN_LEFT, 1);
-	litest_event(dev, EV_SYN, SYN_REPORT, 0);
-	litest_event(dev, EV_KEY, BTN_LEFT, 0);
-	litest_event(dev, EV_SYN, SYN_REPORT, 0);
+	libinput_dispatch(li);
+	litest_timeout_debounce();
+	libinput_dispatch(li);
 
-	litest_drain_events(li);
 
-	litest_event(dev, EV_KEY, BTN_LEFT, 1);
-	litest_event(dev, EV_SYN, SYN_REPORT, 0);
 	litest_event(dev, EV_KEY, BTN_LEFT, 0);
 	litest_event(dev, EV_SYN, SYN_REPORT, 0);
-	litest_event(dev, EV_KEY, BTN_LEFT, 1);
-	litest_event(dev, EV_SYN, SYN_REPORT, 0);
-	/* release is now held back, press was ignored,
+	/* release is now held back,
 	 * other button should flush the release */
 	litest_event(dev, EV_KEY, BTN_RIGHT, 1);
 	litest_event(dev, EV_SYN, SYN_REPORT, 0);
@@ -2419,6 +2539,7 @@ litest_setup_tests_pointer(void)
 {
 	struct range axis_range = {ABS_X, ABS_Y + 1};
 	struct range compass = {0, 7}; /* cardinal directions */
+	struct range buttons = {BTN_LEFT, BTN_TASK + 1};
 
 	litest_add("pointer:motion", pointer_motion_relative, LITEST_RELATIVE, LITEST_POINTINGSTICK);
 	litest_add_for_device("pointer:motion", pointer_motion_relative_zero, LITEST_MOUSE);
@@ -2481,10 +2602,11 @@ litest_setup_tests_pointer(void)
 
 	litest_add("pointer:time", pointer_time_usec, LITEST_RELATIVE, LITEST_ANY);
 
-	litest_add("pointer:debounce", debounce, LITEST_BUTTON, LITEST_TOUCHPAD);
-	litest_add("pointer:debounce", debounce_timer, LITEST_BUTTON, LITEST_TOUCHPAD);
-	litest_add("pointer:debounce", debounce_multibounce, LITEST_BUTTON, LITEST_TOUCHPAD);
-	litest_add("pointer:debounce_otherbutton", debounce_no_debounce_for_otherbutton, LITEST_BUTTON, LITEST_TOUCHPAD);
-	litest_add("pointer:debounce_otherbutton", debounce_cancel_debounce_otherbutton, LITEST_BUTTON, LITEST_TOUCHPAD);
-	litest_add("pointer:debounce_otherbutton", debounce_switch_to_otherbutton, LITEST_BUTTON, LITEST_TOUCHPAD);
+	litest_add_ranged("pointer:debounce", debounce_bounce, LITEST_BUTTON, LITEST_TOUCHPAD, &buttons);
+	litest_add("pointer:debounce", debounce_bounce_check_immediate, LITEST_BUTTON, LITEST_TOUCHPAD);
+	litest_add_ranged("pointer:debounce", debounce_spurious, LITEST_BUTTON, LITEST_TOUCHPAD, &buttons);
+	litest_add("pointer:debounce", debounce_spurious_multibounce, LITEST_BUTTON, LITEST_TOUCHPAD);
+	litest_add("pointer:debounce_otherbutton", debounce_spurious_dont_enable_on_otherbutton, LITEST_BUTTON, LITEST_TOUCHPAD);
+	litest_add("pointer:debounce_otherbutton", debounce_spurious_cancel_debounce_otherbutton, LITEST_BUTTON, LITEST_TOUCHPAD);
+	litest_add("pointer:debounce_otherbutton", debounce_spurious_switch_to_otherbutton, LITEST_BUTTON, LITEST_TOUCHPAD);
 }
-- 
2.13.6



More information about the wayland-devel mailing list