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 9302461a8a77fe5d1d9156fa8027ebdf21f10a77
Author: Cedric BAIL <[email protected]>
AuthorDate: Tue Aug 18 13:32:18 2026 -0600

    e_comp_wl - accept a client's content type hint
    
    wp_content_type_manager_v1. A client tags a surface as photo, video or
    game; a compositor may adapt - hold off on an effect, favour timing
    accuracy over latency, set the DRM "content type" property on a connector
    so a television stops applying its own motion interpolation to a game.
    
    E stores the hint and acts on none of it, and that is a complete
    implementation rather than a stub. The protocol asks a compositor to
    accept the tag and says every use of it is optional; storing it is what
    lets a DRM backend or a compositing policy read it later without needing
    a protocol change first.
    
    The value is double buffered, which is easy to miss because a hint feels
    like it should apply at once. It does not: it lands in the pending
    surface state beside the viewport and the buffer scale, and takes effect
    on the next wl_surface.commit alongside the buffer it describes.
    Destroying the object is defined as setting none with the same double
    buffering, so the changed flag is separate from the value rather than
    inferred from it being none.
    
    Two things are refused. A second wp_content_type_v1 on one surface is the
    error the protocol names; a value outside the enum is a malformed
    request, because there are four defined values and storing a fifth leaves
    a number nothing downstream can interpret.
    
    Both refusals are what the test is for - there is no behaviour to observe
    otherwise - and each was checked against a build with that guard removed,
    because an error path nothing exercises is indistinguishable from one
    that is not there.
    
    Co-Authored-By: Claude Opus 5 <[email protected]>
    Claude-Session: https://claude.ai/code/session_01FtoiXoSKUmZb6Aix6U3GZS
---
 src/bin/e_comp_wl.c                   |  14 ++++
 src/bin/e_comp_wl.h                   |  16 ++++
 src/bin/e_comp_wl_extensions.c        | 120 ++++++++++++++++++++++++++++
 src/bin/generated/meson.build         |   1 +
 src/tests/wayland/e_wl_testkit.c      |  13 +++
 src/tests/wayland/e_wl_testkit.h      |   6 ++
 src/tests/wayland/globals.expected    |   1 +
 src/tests/wayland/meson.build         |   2 +
 src/tests/wayland/test_content_type.c | 146 ++++++++++++++++++++++++++++++++++
 9 files changed, 319 insertions(+)

diff --git a/src/bin/e_comp_wl.c b/src/bin/e_comp_wl.c
index 86871632c..f50b7f0ef 100644
--- a/src/bin/e_comp_wl.c
+++ b/src/bin/e_comp_wl.c
@@ -2164,6 +2164,14 @@ _e_comp_wl_surface_state_commit(E_Client *ec, E_Comp_Wl_Surface_State *state)
         ec->comp_data->viewport = state->viewport;
         state->viewport_changed = 0;
      }
+   /* wp_content_type_v1 is double buffered too, and destroying the object
+    * counts as setting it back to none - which is why the flag is separate
+    * from the value rather than inferred from it. */
+   if (state->content_type_changed)
+     {
+        ec->comp_data->content_type = state->content_type;
+        state->content_type_changed = 0;
+     }
    ec->comp_data->buffer_scale = state->buffer_scale;
    ec->comp_data->buffer_transform = state->buffer_transform;
 
@@ -3347,6 +3355,12 @@ _e_comp_wl_subsurface_commit_to_cache(E_Client *ec)
         sdata->cached.viewport_changed = 1;
         cdata->pending.viewport_changed = 0;
      }
+   if (cdata->pending.content_type_changed)
+     {
+        sdata->cached.content_type = cdata->pending.content_type;
+        sdata->cached.content_type_changed = 1;
+        cdata->pending.content_type_changed = 0;
+     }
    sdata->cached.buffer_scale = cdata->pending.buffer_scale;
    sdata->cached.buffer_transform = cdata->pending.buffer_transform;
 
diff --git a/src/bin/e_comp_wl.h b/src/bin/e_comp_wl.h
index 36bde7b59..346422593 100644
--- a/src/bin/e_comp_wl.h
+++ b/src/bin/e_comp_wl.h
@@ -133,11 +133,18 @@ struct _E_Comp_Wl_Surface_State
     * the request arrives rather than here. */
    int buffer_scale;
    int buffer_transform;
+   /* wp_content_type_v1.set_content_type, which the protocol makes double
+    * buffered like everything else here. Range-checked when the request
+    * arrives rather than here. */
+   int content_type;
    Eina_Bool new_attach E_BITFIELD;
    Eina_Bool has_data E_BITFIELD;
    /* Whether viewport above is a request waiting for a commit, as opposed to
     * the copy that is already in effect. */
    Eina_Bool viewport_changed E_BITFIELD;
+   /* Same question for content_type. Needed separately from "is it none",
+    * because going back to none is itself a double-buffered change. */
+   Eina_Bool content_type_changed E_BITFIELD;
 };
 
 struct _E_Comp_Wl_Subsurf_Data
@@ -207,6 +214,10 @@ typedef struct E_Comp_Wl_Extension_Data
      {
         struct wl_global *global;
      } wp_single_pixel_buffer_manager_v1;
+   struct
+     {
+        struct wl_global *global;
+     } wp_content_type_manager_v1;
    struct
      {
         struct wl_global *global;
@@ -447,6 +458,11 @@ struct _E_Comp_Wl_Client_Data
     * exactly one per surface and has an error for the second, so the surface
     * is where the answer to "is there already one" lives. */
    struct wl_resource *fractional_scale_resource;
+   /* wp_content_type_v1: at most one per surface, and the content type it
+    * last committed. A hint only - E stores it and nothing acts on it yet,
+    * which is the whole of what the protocol requires of a compositor. */
+   struct wl_resource *content_type_resource;
+   int content_type;
    /* The one zwp_keyboard_shortcuts_inhibitor_v1 this surface is allowed for
     * E's single seat. The protocol has an already_inhibited error for the
     * second, so the surface is where "is there one" has to live. */
diff --git a/src/bin/e_comp_wl_extensions.c b/src/bin/e_comp_wl_extensions.c
index 8e800e72a..853d71786 100644
--- a/src/bin/e_comp_wl_extensions.c
+++ b/src/bin/e_comp_wl_extensions.c
@@ -16,6 +16,7 @@
 #include "keyboard-shortcuts-inhibit-unstable-v1-server-protocol.h"
 #include "presentation-time-server-protocol.h"
 #include "single-pixel-buffer-v1-server-protocol.h"
+#include "content-type-v1-server-protocol.h"
 
 #include <time.h>
 
@@ -2149,6 +2150,123 @@ static const struct wp_single_pixel_buffer_manager_v1_interface _e_wp_single_pix
    _e_wp_single_pixel_buffer_manager_cb_create_u32_rgba_buffer,
 };
 
+/* wp_content_type_manager_v1.
+ *
+ * A client tags a surface as photo, video or game and the compositor may
+ * adapt - hold off on an effect, favour timing accuracy over latency, set the
+ * DRM "content type" property on a connector so a television stops applying
+ * its own motion interpolation to a game.
+ *
+ * E stores the hint and acts on none of it, and that is a complete
+ * implementation rather than a stub: the protocol asks a compositor to accept
+ * the tag and says every use of it is optional. Storing it is what lets
+ * anything later - a DRM backend, a compositing policy - read it without
+ * needing a protocol change first. What would *not* be complete is accepting
+ * a value outside the enum, so that is refused.
+ *
+ * The value is double buffered, which is easy to miss because a hint feels
+ * like it should apply at once. It does not: it lands in the pending surface
+ * state and takes effect on the next wl_surface.commit, alongside the buffer
+ * it describes. Destroying the object is defined as setting none *with the
+ * same double buffering*, which is why the changed flag is kept separately
+ * from the value rather than inferred from it being none.
+ */
+
+static void
+_e_wp_content_type_v1_cb_destroy(struct wl_client *client EINA_UNUSED, struct wl_resource *resource)
+{
+   wl_resource_destroy(resource);
+}
+
+static void
+_e_wp_content_type_v1_cb_set_content_type(struct wl_client *client EINA_UNUSED, struct wl_resource *resource, uint32_t content_type)
+{
+   E_Client *ec = wl_resource_get_user_data(resource);
+
+   /* Inert once the surface has gone, which the protocol calls for by name.
+    * Same guard every other per-surface object in this file uses. */
+   if ((!ec) || e_object_is_del(E_OBJECT(ec)) || (!ec->comp_data)) return;
+
+   if (content_type > WP_CONTENT_TYPE_V1_TYPE_GAME)
+     {
+        /* The argument names an enum, so a value outside it is a malformed
+         * request rather than a hint we happen not to understand. Storing it
+         * would leave a number nothing can interpret where later code will
+         * expect one of four. */
+        wl_resource_post_error(resource, WL_DISPLAY_ERROR_INVALID_METHOD,
+                               "content type %u is not a wp_content_type_v1.type",
+                               content_type);
+        return;
+     }
+
+   ec->comp_data->pending.content_type = (int)content_type;
+   ec->comp_data->pending.content_type_changed = 1;
+}
+
+static const struct wp_content_type_v1_interface _e_wp_content_type_v1_interface =
+{
+   _e_wp_content_type_v1_cb_destroy,
+   _e_wp_content_type_v1_cb_set_content_type,
+};
+
+static void
+_e_wp_content_type_v1_res_destroy(struct wl_resource *resource)
+{
+   E_Client *ec = wl_resource_get_user_data(resource);
+
+   if ((!ec) || e_object_is_del(E_OBJECT(ec)) || (!ec->comp_data)) return;
+   if (ec->comp_data->content_type_resource != resource) return;
+
+   ec->comp_data->content_type_resource = NULL;
+   /* "Switch back to not specifying the content type of this surface. This is
+    * equivalent to setting the content type to none, including double
+    * buffering semantics." - scheduled, then, not applied. */
+   ec->comp_data->pending.content_type = WP_CONTENT_TYPE_V1_TYPE_NONE;
+   ec->comp_data->pending.content_type_changed = 1;
+}
+
+static void
+_e_wp_content_type_manager_v1_cb_destroy(struct wl_client *client EINA_UNUSED, struct wl_resource *resource)
+{
+   wl_resource_destroy(resource);
+}
+
+static void
+_e_wp_content_type_manager_v1_cb_get_surface_content_type(struct wl_client *client, struct wl_resource *resource, uint32_t id, struct wl_resource *surface)
+{
+   E_Client *ec;
+   struct wl_resource *res;
+
+   if (!(ec = wl_resource_get_user_data(surface))) return;
+   if (e_object_is_del(E_OBJECT(ec)) || (!ec->comp_data)) return;
+
+   if (ec->comp_data->content_type_resource)
+     {
+        wl_resource_post_error(resource,
+                               WP_CONTENT_TYPE_MANAGER_V1_ERROR_ALREADY_CONSTRUCTED,
+                               "wl_surface already has a wp_content_type_v1");
+        return;
+     }
+
+   res = wl_resource_create(client, &wp_content_type_v1_interface,
+                            wl_resource_get_version(resource), id);
+   if (!res)
+     {
+        wl_resource_post_no_memory(resource);
+        return;
+     }
+
+   wl_resource_set_implementation(res, &_e_wp_content_type_v1_interface, ec,
+                                  _e_wp_content_type_v1_res_destroy);
+   ec->comp_data->content_type_resource = res;
+}
+
+static const struct wp_content_type_manager_v1_interface _e_wp_content_type_manager_v1_interface =
+{
+   _e_wp_content_type_manager_v1_cb_destroy,
+   _e_wp_content_type_manager_v1_cb_get_surface_content_type,
+};
+
 #define GLOBAL_BIND_CB(NAME, IFACE, ...) \
 static void \
 _e_comp_wl_##NAME##_cb_bind(struct wl_client *client, void *data EINA_UNUSED, uint32_t version, uint32_t id) \
@@ -2187,6 +2305,7 @@ GLOBAL_BIND_CB(wp_presentation, wp_presentation_interface,
      wp_presentation_send_clock_id(res, CLOCK_MONOTONIC);
 )
 GLOBAL_BIND_CB(wp_single_pixel_buffer_manager_v1, wp_single_pixel_buffer_manager_v1_interface)
+GLOBAL_BIND_CB(wp_content_type_manager_v1, wp_content_type_manager_v1_interface)
 GLOBAL_BIND_CB(action_route, action_route_interface,
      e_binding_key_list_cb = _action_route_key_list_cb;
      key_bindings = eina_hash_string_superfast_new(NULL);
@@ -2292,6 +2411,7 @@ e_comp_wl_extensions_init(void)
    GLOBAL_CREATE_OR_RETURN(wp_viewporter, wp_viewporter_interface, 1);
    GLOBAL_CREATE_OR_RETURN(wp_presentation, wp_presentation_interface, 1);
    GLOBAL_CREATE_OR_RETURN(wp_single_pixel_buffer_manager_v1, wp_single_pixel_buffer_manager_v1_interface, 1);
+   GLOBAL_CREATE_OR_RETURN(wp_content_type_manager_v1, wp_content_type_manager_v1_interface, 1);
    GLOBAL_CREATE_OR_RETURN(zxdg_output_manager_v1, zxdg_output_manager_v1_interface, 3);
    GLOBAL_CREATE_OR_RETURN(wp_fractional_scale_manager_v1, wp_fractional_scale_manager_v1_interface, 1);
    GLOBAL_CREATE_OR_RETURN(zwp_idle_inhibit_manager_v1, zwp_idle_inhibit_manager_v1_interface, 1);
diff --git a/src/bin/generated/meson.build b/src/bin/generated/meson.build
index 99316e55d..23598c0a3 100644
--- a/src/bin/generated/meson.build
+++ b/src/bin/generated/meson.build
@@ -10,6 +10,7 @@ protos = [
   '@0@/staging/xdg-activation/xdg-activation-v1.xml'.format(dir_wayland_protocols),
   '@0@/stable/viewporter/viewporter.xml'.format(dir_wayland_protocols),
   '@0@/staging/single-pixel-buffer/single-pixel-buffer-v1.xml'.format(dir_wayland_protocols),
+  '@0@/staging/content-type/content-type-v1.xml'.format(dir_wayland_protocols),
   '@0@/stable/presentation-time/presentation-time.xml'.format(dir_wayland_protocols),
   '@0@/unstable/xdg-output/xdg-output-unstable-v1.xml'.format(dir_wayland_protocols),
   '@0@/unstable/primary-selection/primary-selection-unstable-v1.xml'.format(dir_wayland_protocols),
diff --git a/src/tests/wayland/e_wl_testkit.c b/src/tests/wayland/e_wl_testkit.c
index c256e7896..60aefe193 100644
--- a/src/tests/wayland/e_wl_testkit.c
+++ b/src/tests/wayland/e_wl_testkit.c
@@ -478,6 +478,19 @@ tk_toplevel_surface(Tk_Toplevel *top)
    return top->surface;
 }
 
+int
+tk_check_error(Tk *tk)
+{
+   /* Plain wl_display_roundtrip rather than tk_sync: tk_sync goes through
+    * wl_test and exits the process the moment the connection fails, which is
+    * right for every test that is not about provoking an error and useless
+    * for one that is. After a protocol error nothing on the connection works,
+    * so there is nothing left to ask the compositor - the answer is already
+    * local, in the display's error state. */
+   wl_display_roundtrip(tk->disp);
+   return wl_display_get_error(tk->disp) != 0;
+}
+
 void
 tk_toplevel_configured(Tk_Toplevel *top, int *w, int *h, int *count)
 {
diff --git a/src/tests/wayland/e_wl_testkit.h b/src/tests/wayland/e_wl_testkit.h
index 4c0686220..e2653b401 100644
--- a/src/tests/wayland/e_wl_testkit.h
+++ b/src/tests/wayland/e_wl_testkit.h
@@ -126,6 +126,12 @@ void *tk_bind(Tk *tk, const struct wl_interface *iface, uint32_t version);
  * requests were read. */
 void tk_sync(Tk *tk);
 
+/* Round-trip and report whether the connection has failed, instead of exiting
+ * the way tk_sync does. For the tests that deliberately provoke a protocol
+ * error: killing the connection is the compositor's *correct* answer there,
+ * and it is the only way a client can observe one. */
+int tk_check_error(Tk *tk);
+
 /* Sync until a change that needs the client's cooperation has completed: one
  * round trip to carry the compositor's configure out and our commit back,
  * one more for the compositor to apply it. Still no sleeping anywhere. */
diff --git a/src/tests/wayland/globals.expected b/src/tests/wayland/globals.expected
index c74012ab5..d3489d992 100644
--- a/src/tests/wayland/globals.expected
+++ b/src/tests/wayland/globals.expected
@@ -7,6 +7,7 @@ wl_seat	8
 wl_shell	1
 wl_shm	>=1
 wl_subcompositor	1
+wp_content_type_manager_v1	1
 wp_fractional_scale_manager_v1	1
 wp_presentation	1
 wp_single_pixel_buffer_manager_v1	1
diff --git a/src/tests/wayland/meson.build b/src/tests/wayland/meson.build
index 0fdb4ba4a..040e89b94 100644
--- a/src/tests/wayland/meson.build
+++ b/src/tests/wayland/meson.build
@@ -43,6 +43,7 @@ foreach p: [
   '@0@/stable/presentation-time/presentation-time.xml'.format(dir_wayland_protocols),
   '@0@/unstable/xdg-foreign/xdg-foreign-unstable-v1.xml'.format(dir_wayland_protocols),
   '@0@/unstable/xdg-foreign/xdg-foreign-unstable-v2.xml'.format(dir_wayland_protocols),
+  '@0@/staging/content-type/content-type-v1.xml'.format(dir_wayland_protocols),
 ]
   test_proto_src += gen_scanner_client.process(p)
   test_proto_src += gen_scanner_impl.process(p)
@@ -86,6 +87,7 @@ wl_protocol_tests = [
   ['maximize-restore', 'test_maximize_restore.c'],
   ['presentation-time', 'test_presentation_time.c'],
   ['xdg-foreign', 'test_xdg_foreign.c'],
+  ['content-type', 'test_content_type.c'],
 ]
 
 # Shared plumbing: registry binding, toplevel construction, enumeration and a
diff --git a/src/tests/wayland/test_content_type.c b/src/tests/wayland/test_content_type.c
new file mode 100644
index 000000000..0a5a633ca
--- /dev/null
+++ b/src/tests/wayland/test_content_type.c
@@ -0,0 +1,146 @@
+/* wp_content_type_v1: a client saying what kind of thing it is showing.
+ *
+ * E stores the hint and acts on none of it, which is what the protocol asks
+ * of a compositor - every use of the tag is explicitly optional. That makes
+ * this test unusual: there is no behaviour to observe, so what is checked is
+ * the part a client *can* observe going wrong, which is the protocol contract
+ * around the hint rather than the hint itself.
+ *
+ * Three things, each of which a careless implementation gets wrong:
+ *
+ *   * a second wp_content_type_v1 on the same surface is the error the
+ *     protocol names, already_constructed. A compositor that just overwrites
+ *     its stored resource leaks the first one and answers nothing;
+ *   * a value outside the enum is a malformed request, not a hint to store.
+ *     There are four valid values and nothing says what a fifth would mean;
+ *   * every legal value is accepted, and the object can be destroyed and
+ *     remade - destroy is defined as "set none", not as "detach", so a
+ *     compositor that forgets to clear its slot refuses the remake with
+ *     already_constructed and the client is stuck for the life of the
+ *     surface.
+ *
+ * A protocol error kills the connection, so the two error cases each need a
+ * connection of their own - which is also the only honest way to assert them:
+ * "the compositor disconnected us" is the observation, and a test sharing one
+ * connection could only ever check the first.
+ */
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "e_wl_testkit.h"
+#include "content-type-v1-client-protocol.h"
+
+#define PROG "test-content-type"
+#define APP_ID "content-type"
+
+static struct wp_content_type_manager_v1 *
+_manager_get(Tk *tk)
+{
+   struct wp_content_type_manager_v1 *mgr;
+
+   if (tk_global_version(tk, "wp_content_type_manager_v1") < 1)
+     tk_fail(tk, "no wp_content_type_manager_v1 - a client has no way to say "
+                 "it is playing video rather than showing a still, which is "
+                 "the difference between wanting accurate timing and wanting "
+                 "low latency");
+
+   mgr = tk_bind(tk, &wp_content_type_manager_v1_interface, 1);
+   if (!mgr) tk_fail(tk, "advertised but would not bind");
+   return mgr;
+}
+
+int
+main(void)
+{
+   Tk *tk;
+   Tk_Toplevel *top;
+   struct wp_content_type_manager_v1 *mgr;
+   struct wp_content_type_v1 *ct;
+   int i;
+   static const uint32_t all[] =
+     {
+        WP_CONTENT_TYPE_V1_TYPE_NONE,
+        WP_CONTENT_TYPE_V1_TYPE_PHOTO,
+        WP_CONTENT_TYPE_V1_TYPE_VIDEO,
+        WP_CONTENT_TYPE_V1_TYPE_GAME,
+     };
+
+   /* ------------------------------------------- the ordinary path */
+
+   tk = tk_connect(PROG);
+   mgr = _manager_get(tk);
+   top = tk_toplevel_new(tk, APP_ID, "content type", 200, 150);
+   tk_settle(tk);
+   tk_expect(tk, APP_ID);
+
+   /* Every legal value, each on its own commit, because the value is double
+    * buffered and a compositor that applies it immediately would pass a test
+    * that never committed. */
+   for (i = 0; i < (int)(sizeof(all) / sizeof(all[0])); i++)
+     {
+        ct = wp_content_type_manager_v1_get_surface_content_type(
+               mgr, tk_toplevel_surface(top));
+        wp_content_type_v1_set_content_type(ct, all[i]);
+        wl_surface_commit(tk_toplevel_surface(top));
+
+        /* tk_check_error rather than tk_sync: tk_sync exits with
+         * "disconnected during sync" and the reason is lost. Here the reason
+         * is the whole point. */
+        if (tk_check_error(tk))
+          tk_fail(tk, "setting content type %u was refused, and it is one of "
+                      "the four the protocol defines", all[i]);
+
+        /* Destroy and remake. "Switch back to not specifying the content
+         * type" is a set, not a detach, so the surface must be free to have
+         * another one - a compositor that forgets to clear its slot fails
+         * here on the second iteration and nowhere else. */
+        wp_content_type_v1_destroy(ct);
+        wl_surface_commit(tk_toplevel_surface(top));
+
+        if (tk_check_error(tk))
+          tk_fail(tk, "the connection died destroying the content type object "
+                      "for value %u", all[i]);
+     }
+   printf(PROG ": all four types accepted, object remade after each\n");
+   tk_disconnect(tk);
+
+   /* ------------------------------- two objects on one surface */
+
+   tk = tk_connect(PROG);
+   mgr = _manager_get(tk);
+   top = tk_toplevel_new(tk, APP_ID, "content type", 200, 150);
+   tk_settle(tk);
+
+   wp_content_type_manager_v1_get_surface_content_type(mgr, tk_toplevel_surface(top));
+   wp_content_type_manager_v1_get_surface_content_type(mgr, tk_toplevel_surface(top));
+
+   if (!tk_check_error(tk))
+     tk_fail(tk, "a second wp_content_type_v1 on the same surface was "
+                 "accepted. The protocol names this one already_constructed, "
+                 "and a compositor that instead overwrites its slot has "
+                 "quietly leaked the first object");
+   printf(PROG ": a second object on one surface is refused\n");
+   tk_disconnect(tk);
+
+   /* ------------------------------- a value outside the enum */
+
+   tk = tk_connect(PROG);
+   mgr = _manager_get(tk);
+   top = tk_toplevel_new(tk, APP_ID, "content type", 200, 150);
+   tk_settle(tk);
+
+   ct = wp_content_type_manager_v1_get_surface_content_type(mgr, tk_toplevel_surface(top));
+   wp_content_type_v1_set_content_type(ct, 4242);
+
+   if (!tk_check_error(tk))
+     tk_fail(tk, "content type 4242 was accepted. There are four valid "
+                 "values and nothing defines a fifth, so this is a malformed "
+                 "request - storing it leaves a number nothing downstream "
+                 "can interpret");
+   printf(PROG ": a value outside the enum is refused\n");
+   tk_disconnect(tk);
+
+   printf(PROG ": ok\n");
+   return 0;
+}

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

Reply via email to