This is an automated email from the git hooks/post-receive script.

git pushed a commit to branch wl/browser-all
in repository enlightenment.

View the commit online.

commit 4eb9b034e7aa0f461c252d24e8387b892111222e
Author: Cedric BAIL <[email protected]>
AuthorDate: Tue Aug 18 23:53:46 2026 -0600

    e_comp_wl_input - wl_seat.capabilities is a change notification
    
    "Sent whenever a seat gains or loses the pointer, keyboard or touch
    capabilities" - so it is sent on a change, and a client is entitled to
    read one as "the set of devices on this seat is now different from the
    set you have". Creating a wl_pointer in response is the obvious thing to
    do, and it is what wlcs's own client does.
    
    E sent it to every bound seat resource whenever a *new* client bound the
    seat, which is neither a gain nor a loss. The client that had been
    running for five minutes was told its devices had changed when nothing
    had, made a second wl_pointer, and from then on received every pointer
    event twice on two objects that both believed they held the pointer
    focus.
    
    Two changes, both of which have to be there:
    
      * binding sends the capabilities to the resource that just bound, not
        to everyone. That is the actual bug;
      * the broadcast sends only when the value has changed, so a call that
        toggles a capability to the value it already had stays quiet. That
        is not the same bug, but it is the same mistake one layer down.
    
    Found while making the text-input wlcs tests run at all: every one of
    them with two clients and a pointer failed on "Pointer tried to enter
    surface X without first leaving surface X", which is what a duplicated
    wl_pointer looks like from the far end.
    
    The test is two connections and a count. Nothing in it changes the
    seat's devices, so the first connection must see exactly one
    capabilities event no matter what the second connection does; reverting
    either half of the fix makes it fail, and it was checked against a build
    with each removed.
---
 src/bin/e_comp_wl_input.c                  |  43 +++++++++--
 src/tests/wayland/meson.build              |   1 +
 src/tests/wayland/test_seat_capabilities.c | 112 +++++++++++++++++++++++++++++
 3 files changed, 151 insertions(+), 5 deletions(-)

diff --git a/src/bin/e_comp_wl_input.c b/src/bin/e_comp_wl_input.c
index 4deeff6a3..a55aa5903 100644
--- a/src/bin/e_comp_wl_input.c
+++ b/src/bin/e_comp_wl_input.c
@@ -19,11 +19,9 @@ static struct xkb_context *cached_context;
 static struct xkb_keymap *cached_keymap;
 static xkb_layout_index_t choosen_group;
 
-static void
-_e_comp_wl_input_update_seat_caps(void)
+static enum wl_seat_capability
+_e_comp_wl_input_seat_caps(void)
 {
-   Eina_List *l;
-   struct wl_resource *res;
    enum wl_seat_capability caps = 0;
 
    if (e_comp_wl->ptr.enabled)
@@ -33,6 +31,33 @@ _e_comp_wl_input_update_seat_caps(void)
    if (e_comp_wl->touch.enabled)
      caps |= WL_SEAT_CAPABILITY_TOUCH;
 
+   return caps;
+}
+
+/* What every bound seat resource has last been told, so that a call which
+ * changes nothing sends nothing. -1 is not a capability bitmask, so the first
+ * call always sends. */
+static int _seat_caps_sent = -1;
+
+/* Tell every client the seat's devices changed.
+ *
+ * "Sent whenever a seat gains or loses the pointer, keyboard or touch
+ * capabilities" - wl_seat.capabilities. So it is sent on a change, and only on
+ * a change. A client is within its rights to read the event as "the set of
+ * devices is now different from what you had" and act on it; one that creates
+ * a wl_pointer each time it arrives ends up with two, and from then on
+ * receives every pointer event twice on two objects that both believe they
+ * hold the focus. */
+static void
+_e_comp_wl_input_update_seat_caps(void)
+{
+   Eina_List *l;
+   struct wl_resource *res;
+   enum wl_seat_capability caps = _e_comp_wl_input_seat_caps();
+
+   if ((int)caps == _seat_caps_sent) return;
+   _seat_caps_sent = (int)caps;
+
    EINA_LIST_FOREACH(e_comp_wl->seat.resources, l, res)
      wl_seat_send_capabilities(res, caps);
 }
@@ -319,7 +344,15 @@ _e_comp_wl_input_cb_bind_seat(struct wl_client *client, void *data EINA_UNUSED,
                                   e_comp->wl_comp_data,
                                   _e_comp_wl_input_cb_unbind_seat);
 
-   _e_comp_wl_input_update_seat_caps();
+   /* To this resource, not to every seat resource in the session. A second
+    * client connecting is not the seat gaining or losing a device, and the
+    * clients already running have no business hearing about it - the one that
+    * bound five minutes ago would be told its devices changed when nothing
+    * did. Every wlcs test with two clients and a pointer failed on exactly
+    * that: the first client made a second wl_pointer when the second client
+    * connected, and then saw two enter events for one crossing. */
+   wl_seat_send_capabilities(res, _e_comp_wl_input_seat_caps());
+
    /* This client's own version, not seat.version: that field holds whoever
     * bound last, and two clients on different versions are now the normal
     * case rather than a curiosity - GTK binds 5, Firefox and Chromium 8. */
diff --git a/src/tests/wayland/meson.build b/src/tests/wayland/meson.build
index b313c0e34..4271d6bb7 100644
--- a/src/tests/wayland/meson.build
+++ b/src/tests/wayland/meson.build
@@ -90,6 +90,7 @@ wl_protocol_tests = [
   ['xdg-foreign', 'test_xdg_foreign.c'],
   ['content-type', 'test_content_type.c'],
   ['toplevel-drag', 'test_toplevel_drag.c'],
+  ['seat-capabilities', 'test_seat_capabilities.c'],
 ]
 
 # Shared plumbing: registry binding, toplevel construction, enumeration and a
diff --git a/src/tests/wayland/test_seat_capabilities.c b/src/tests/wayland/test_seat_capabilities.c
new file mode 100644
index 000000000..b4db0a9fb
--- /dev/null
+++ b/src/tests/wayland/test_seat_capabilities.c
@@ -0,0 +1,112 @@
+/* wl_seat.capabilities is a change notification, not a greeting.
+ *
+ * "Sent whenever a seat gains or loses the pointer, keyboard or touch
+ * capabilities" - so a client is entitled to read one as "the set of devices
+ * on this seat is now different from the set you have" and act accordingly.
+ * Creating a wl_pointer in response is the obvious thing to do, and it is what
+ * wlcs's own client does.
+ *
+ * E used to send it to every bound seat resource whenever a *new* client bound
+ * the seat, which is neither a gain nor a loss. The client that had been
+ * running made a second wl_pointer for a change that never happened, and from
+ * then on received every pointer event twice, on two objects that both
+ * believed they held the pointer focus. Every wlcs test with two clients and a
+ * pointer failed on it once anything made them run at all.
+ *
+ * The test is two connections and a count. Nothing here changes the seat's
+ * devices, so the first connection must see exactly one capabilities event no
+ * matter what the second connection does.
+ */
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "e_wl_testkit.h"
+
+#define PROG "test-seat-capabilities"
+
+typedef struct
+{
+   struct wl_seat *seat;
+   int events;
+   uint32_t caps;
+} Seat_Watch;
+
+static void
+_caps(void *data, struct wl_seat *seat, uint32_t caps)
+{
+   Seat_Watch *w = data;
+
+   (void)seat;
+   w->events++;
+   w->caps = caps;
+}
+
+static void
+_name(void *d, struct wl_seat *s, const char *n)
+{ (void)d; (void)s; (void)n; }
+
+static const struct wl_seat_listener _seat_listener = { _caps, _name };
+
+static void
+_seat_watch(Tk *tk, Seat_Watch *w)
+{
+   uint32_t version = tk_global_version(tk, "wl_seat");
+
+   if (!version) tk_fail(tk, "no wl_seat");
+   w->seat = tk_bind(tk, &wl_seat_interface, version);
+   if (!w->seat) tk_fail(tk, "wl_seat advertised but would not bind");
+   wl_seat_add_listener(w->seat, &_seat_listener, w);
+   tk_sync(tk);
+}
+
+int
+main(void)
+{
+   Tk *a, *b;
+   Seat_Watch wa = { 0 }, wb = { 0 };
+   uint32_t caps_after_first;
+
+   a = tk_connect(PROG "-a");
+   _seat_watch(a, &wa);
+
+   if (wa.events != 1)
+     tk_fail(a, "first client saw %d capabilities events on binding the seat, "
+                "expected exactly 1", wa.events);
+   caps_after_first = wa.caps;
+
+   /* A second client arrives. This is the whole test: nothing about the seat
+    * has changed, so the first client must hear nothing. */
+   b = tk_connect(PROG "-b");
+   _seat_watch(b, &wb);
+
+   if (wb.events != 1)
+     tk_fail(b, "second client saw %d capabilities events on binding the seat, "
+                "expected exactly 1", wb.events);
+
+   tk_sync(a);
+   tk_sync(a);
+
+   if (wa.events != 1)
+     tk_fail(a, "first client saw %d capabilities events after a second client "
+                "bound the seat, expected 1 - a client that creates a "
+                "wl_pointer for each one now has two, and gets every pointer "
+                "event twice", wa.events);
+
+   if (wa.caps != caps_after_first)
+     tk_fail(a, "capabilities changed from 0x%x to 0x%x without any device "
+                "being added or removed", caps_after_first, wa.caps);
+
+   /* Both clients must agree about what the seat has; they are looking at the
+    * same seat. A compositor that computes the value per bind rather than from
+    * one place can get this wrong without ever sending a spurious event. */
+   if (wa.caps != wb.caps)
+     tk_fail(a, "the two clients were told different capabilities for the same "
+                "seat: 0x%x and 0x%x", wa.caps, wb.caps);
+
+   printf(PROG ": ok - one capabilities event each, caps 0x%x\n", wa.caps);
+
+   tk_disconnect(b);
+   tk_disconnect(a);
+   return 0;
+}

-- 
To stop receiving notification emails like this one, please contact
the administrator of this repository.

Reply via email to