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 109ee138d8a1a8f1a342600bedcb942ec36e9927
Author: Cedric BAIL <[email protected]>
AuthorDate: Tue Aug 18 08:03:06 2026 -0600

    e_comp_wl - tell clients when their frame reached the screen
    
    wl_surface.frame answers "send me the next one". It does not answer "when
    did the last one land", and a video player needs the second to keep a
    picture in step with a soundtrack. Without wp_presentation both Firefox
    and Chromium fall back to inferring presentation time from the frame
    callback, which is a different moment and drifts.
    
    The bookkeeping mirrors the frame callbacks exactly - pending state,
    subsurface cache, client, and the drain in e_pixmap_image_clear - with one
    rule they do not have: every feedback object must end in exactly one of
    'presented' or 'discarded'. A content update replaced before it was drawn
    is discarded, and so is one whose surface state or client goes away. A
    client that gets neither event waits forever on an object it cannot free,
    which for the clients that ask for this protocol means the video stops.
    
    What is reported is narrower than what the protocol allows, and says so
    rather than rounding up. flags is 0: nothing here is locked to a vertical
    retrace, the timestamp is clock_gettime() rather than hardware, and Evas
    composites rather than scanning out. refresh is 0, which the protocol
    defines as "no useful prediction" - E renders when something changed, not
    on a fixed cadence, and inventing 16ms would read as a promise of a 60Hz
    beat that E does not keep. seq is 0: no retrace counter. Under a DRM
    backend with real flip completion these become real numbers without a
    client-visible change of shape.
    
    The timestamp is sampled at the drain rather than taken from
    ecore_loop_time_get(), which is when this main loop iteration began - a
    whole render before the moment being reported, and the error a client
    would be accumulating is exactly the one it is trying to measure.
    
    The test brackets each commit between two reads of the compositor's own
    advertised clock and requires the reported time to fall inside, because a
    plausible timestamp in the wrong clock domain is the way this protocol
    ships broken: everything works, the video just drifts.
    
    Co-Authored-By: Claude Opus 5 <[email protected]>
    Claude-Session: https://claude.ai/code/session_01FtoiXoSKUmZb6Aix6U3GZS
---
 src/bin/e_comp_wl.c                        |  18 ++
 src/bin/e_comp_wl.h                        |  25 +++
 src/bin/e_comp_wl_extensions.c             | 176 ++++++++++++++++
 src/bin/e_pixmap.c                         |  11 +
 src/bin/generated/meson.build              |   1 +
 src/tests/wayland/e_wl_testkit.c           |  20 ++
 src/tests/wayland/e_wl_testkit.h           |   6 +
 src/tests/wayland/globals.expected         |   1 +
 src/tests/wayland/meson.build              |   2 +
 src/tests/wayland/test_presentation_time.c | 327 +++++++++++++++++++++++++++++
 10 files changed, 587 insertions(+)

diff --git a/src/bin/e_comp_wl.c b/src/bin/e_comp_wl.c
index edf942b32..404935f94 100644
--- a/src/bin/e_comp_wl.c
+++ b/src/bin/e_comp_wl.c
@@ -2048,6 +2048,10 @@ _e_comp_wl_surface_state_finish(E_Comp_Wl_Surface_State *state)
    EINA_LIST_FREE(free_list, cb)
      wl_resource_destroy(cb);
 
+   /* A content update whose state is being thrown away is one nobody will
+    * ever see, so its feedbacks are discarded rather than left dangling. */
+   e_comp_wl_presentation_feedback_discard(&state->presentation_feedbacks);
+
    EINA_LIST_FREE(state->damages, dmg)
      eina_rectangle_free(dmg);
 
@@ -2489,6 +2493,11 @@ _e_comp_wl_surface_state_commit(E_Client *ec, E_Comp_Wl_Surface_State *state)
                                            state->frames);
    state->frames = NULL;
 
+   /* Same move for wp_presentation_feedback, with one extra rule the frame
+    * callbacks do not have: whatever was already waiting on the client is
+    * being superseded by this commit and has to be told so. */
+   e_comp_wl_presentation_feedback_take_over(ec, &state->presentation_feedbacks);
+
    /* put state damages into surface */
    if ((!e_comp->nocomp) && (ec->frame))
      {
@@ -3326,6 +3335,11 @@ _e_comp_wl_subsurface_commit_to_cache(E_Client *ec)
                                           cdata->pending.frames);
    cdata->pending.frames = NULL;
 
+   sdata->cached.presentation_feedbacks =
+     eina_list_merge(sdata->cached.presentation_feedbacks,
+                     cdata->pending.presentation_feedbacks);
+   cdata->pending.presentation_feedbacks = NULL;
+
    /* A synchronised subsurface applies its cached state, not its pending
     * state, so the crop and scale have to travel with it or they would sit in
     * pending until the subsurface went desynchronised. Only carry the
@@ -3858,6 +3872,10 @@ _e_comp_wl_client_cb_del(void *data EINA_UNUSED, E_Client *ec)
    EINA_LIST_FREE(free_list, cb)
      wl_resource_destroy(cb);
 
+   /* "or its surface destroyed" - the protocol names this case explicitly as
+    * a discard. */
+   e_comp_wl_presentation_feedback_discard(&ec->comp_data->presentation_feedbacks);
+
    if (ec->comp_data->surface)
      wl_resource_set_user_data(ec->comp_data->surface, NULL);
 
diff --git a/src/bin/e_comp_wl.h b/src/bin/e_comp_wl.h
index f79a77565..4bb6d1dca 100644
--- a/src/bin/e_comp_wl.h
+++ b/src/bin/e_comp_wl.h
@@ -115,6 +115,10 @@ struct _E_Comp_Wl_Surface_State
     * wp_viewport comes between the two, so the two kinds are kept apart until
     * the commit that knows the transform can bring them into one space. */
    Eina_List *damages, *buffer_damages, *frames;
+   /* wp_presentation_feedback resources asking about the content update this
+    * state will become. Double buffered exactly like frames, and for the same
+    * reason: the request names the *next* commit, not the surface. */
+   Eina_List *presentation_feedbacks;
    Eina_Tiler *input, *opaque;
    E_Comp_Wl_Viewport_State viewport;
    /* wl_surface.set_buffer_scale and set_buffer_transform. Double buffered
@@ -182,6 +186,10 @@ typedef struct E_Comp_Wl_Extension_Data
      {
         struct wl_global *global;
      } wp_viewporter;
+   struct
+     {
+        struct wl_global *global;
+     } wp_presentation;
    struct
      {
         struct wl_global *global;
@@ -478,6 +486,10 @@ struct _E_Comp_Wl_Client_Data
    E_Comp_Wl_Surface_State pending;
 
    Eina_List *frames;
+   /* wp_presentation_feedback for the content update that is committed but not
+    * yet drawn. Never more than one commit's worth: see
+    * e_comp_wl_presentation_feedback_take_over. */
+   Eina_List *presentation_feedbacks;
    Eina_List *constraints;
 
    struct
@@ -595,6 +607,19 @@ E_API void e_comp_wl_extension_pointer_unconstrain(E_Client *ec);
 E_API void e_comp_wl_extension_action_route_pid_allowed_set(uint32_t pid, Eina_Bool allow);
 E_API const void *e_comp_wl_extension_action_route_interface_get(int *version);
 
+/* wp_presentation_feedback bookkeeping, for the four places a content update
+ * can end: drawn, replaced before it was drawn, its surface state thrown away,
+ * or its client gone. Every feedback object must end in exactly one of
+ * 'presented' or 'discarded' - a client that gets neither waits forever for a
+ * frame it will never hear about, which for a video player means it stops
+ * scheduling. All three take the list by pointer and leave it NULL. */
+EINTERN void e_comp_wl_presentation_feedback_present(Eina_List **feedbacks);
+EINTERN void e_comp_wl_presentation_feedback_discard(Eina_List **feedbacks);
+/* Move a commit's feedbacks onto the client, discarding whatever was still
+ * waiting there: that older content update is being replaced before it was
+ * ever drawn, which is the protocol's definition of discarded. */
+EINTERN void e_comp_wl_presentation_feedback_take_over(E_Client *ec, Eina_List **from);
+
 E_API void e_comp_wl_notidle(void);
 E_API void e_comp_wl_idle_inhibit_add(void);
 E_API void e_comp_wl_idle_inhibit_del(void);
diff --git a/src/bin/e_comp_wl_extensions.c b/src/bin/e_comp_wl_extensions.c
index ef6588556..64a3bbf99 100644
--- a/src/bin/e_comp_wl_extensions.c
+++ b/src/bin/e_comp_wl_extensions.c
@@ -13,6 +13,9 @@
 #include "fractional-scale-v1-server-protocol.h"
 #include "idle-inhibit-unstable-v1-server-protocol.h"
 #include "keyboard-shortcuts-inhibit-unstable-v1-server-protocol.h"
+#include "presentation-time-server-protocol.h"
+
+#include <time.h>
 
 /* mutter uses 32, seems reasonable */
 #define HANDLE_LEN 32
@@ -1777,6 +1780,171 @@ static const struct zwp_keyboard_shortcuts_inhibit_manager_v1_interface _e_zwp_k
    _e_zwp_keyboard_shortcuts_inhibit_manager_v1_cb_inhibit_shortcuts,
 };
 
+/* wp_presentation.
+ *
+ * "When did the pixels I just committed actually reach the screen?" A video
+ * player needs the answer to keep a soundtrack in step with a picture, and
+ * without it Firefox and Chromium both fall back to guessing from the frame
+ * callback - which tells them the compositor is ready for the next frame, not
+ * when the last one was shown.
+ *
+ * What E can honestly answer here is narrower than what the protocol allows,
+ * and the events say so rather than rounding up:
+ *
+ *   flags is 0. Not vsync: nothing in this path is locked to a vertical
+ *   retrace, so tearing is not ruled out. Not hw_clock or hw_completion: the
+ *   timestamp is clock_gettime() sampled when the render finished, which the
+ *   protocol is explicit is not good enough for either flag. Not zero_copy:
+ *   Evas composites.
+ *
+ *   refresh is 0, which the protocol defines as "no useful prediction". E
+ *   renders when something changed rather than on a fixed cadence, and the
+ *   spec requires zero for an output without a constant refresh rate. An
+ *   invented 16ms would read to a client as a promise of a 60Hz beat that
+ *   E does not keep, and a client pacing video against it would drift.
+ *
+ *   seq is 0, which the protocol defines as "no vertical retrace counter".
+ *
+ * Under a DRM backend with real flip completion events these become real
+ * numbers and the flags become true; the shape here is what allows that
+ * without a client-visible change. What must not happen either way is a
+ * feedback object that never ends: the client is waiting on it.
+ */
+
+static void
+_e_wp_presentation_feedback_res_destroy(struct wl_resource *resource)
+{
+   E_Client *ec;
+
+   if (!(ec = wl_resource_get_user_data(resource))) return;
+   if (e_object_is_del(E_OBJECT(ec))) return;
+   if (!ec->comp_data) return;
+
+   ec->comp_data->presentation_feedbacks =
+     eina_list_remove(ec->comp_data->presentation_feedbacks, resource);
+   ec->comp_data->pending.presentation_feedbacks =
+     eina_list_remove(ec->comp_data->pending.presentation_feedbacks, resource);
+
+   if (!ec->comp_data->sub.data) return;
+   ec->comp_data->sub.data->cached.presentation_feedbacks =
+     eina_list_remove(ec->comp_data->sub.data->cached.presentation_feedbacks,
+                      resource);
+}
+
+/* 'presented' and 'discarded' are both destructor events: the object is gone
+ * once one of them is sent, and it is the compositor's job to destroy the
+ * resource. Both walks take the list into a temporary first, because
+ * wl_resource_destroy runs the handler above, which removes from the very list
+ * being walked. */
+EINTERN void
+e_comp_wl_presentation_feedback_present(Eina_List **feedbacks)
+{
+   Eina_List *list;
+   struct wl_resource *res;
+   struct timespec ts;
+   uint64_t sec;
+
+   if ((!feedbacks) || (!*feedbacks)) return;
+
+   list = *feedbacks;
+   *feedbacks = NULL;
+
+   /* CLOCK_MONOTONIC, because that is what the clock_id event promised at
+    * bind. Sampled here rather than taken from ecore_loop_time_get(), which
+    * is when this main loop iteration *began* - a whole render before the
+    * moment being reported. */
+   clock_gettime(CLOCK_MONOTONIC, &ts);
+   sec = (uint64_t)ts.tv_sec;
+
+   EINA_LIST_FREE(list, res)
+     {
+        wp_presentation_feedback_send_presented(res,
+          (uint32_t)(sec >> 32), (uint32_t)(sec & 0xffffffff),
+          (uint32_t)ts.tv_nsec,
+          0 /* refresh: not predictable, see above */,
+          0, 0 /* seq: no retrace counter */,
+          0 /* flags: none of them are true here */);
+        wl_resource_destroy(res);
+     }
+}
+
+EINTERN void
+e_comp_wl_presentation_feedback_discard(Eina_List **feedbacks)
+{
+   Eina_List *list;
+   struct wl_resource *res;
+
+   if ((!feedbacks) || (!*feedbacks)) return;
+
+   list = *feedbacks;
+   *feedbacks = NULL;
+
+   EINA_LIST_FREE(list, res)
+     {
+        wp_presentation_feedback_send_discarded(res);
+        wl_resource_destroy(res);
+     }
+}
+
+EINTERN void
+e_comp_wl_presentation_feedback_take_over(E_Client *ec, Eina_List **from)
+{
+   if (!ec->comp_data) return;
+
+   /* Anything still sitting on the client belongs to a content update that
+    * this one replaces before it was ever drawn - discarded, by definition.
+    *
+    * Except when the client is in e_comp->post_updates: it has been rendered
+    * and its 'presented' is one job away, so a commit arriving in that window
+    * supersedes nothing. Getting this wrong in that direction would report a
+    * dropped frame for a frame that was shown, which is worse than the
+    * alternative - a player drops quality in response to it. */
+   if (!ec->on_post_updates)
+     e_comp_wl_presentation_feedback_discard(&ec->comp_data->presentation_feedbacks);
+
+   ec->comp_data->presentation_feedbacks =
+     eina_list_merge(ec->comp_data->presentation_feedbacks, *from);
+   *from = NULL;
+}
+
+static void
+_e_wp_presentation_cb_destroy(struct wl_client *client EINA_UNUSED, struct wl_resource *resource)
+{
+   wl_resource_destroy(resource);
+}
+
+static void
+_e_wp_presentation_cb_feedback(struct wl_client *client, struct wl_resource *resource, struct wl_resource *surface, uint32_t callback)
+{
+   E_Client *ec;
+   struct wl_resource *res;
+
+   if (!(ec = wl_resource_get_user_data(surface))) return;
+   if (e_object_is_del(E_OBJECT(ec))) return;
+
+   res = wl_resource_create(client, &wp_presentation_feedback_interface,
+                            wl_resource_get_version(resource), callback);
+   if (!res)
+     {
+        wl_resource_post_no_memory(resource);
+        return;
+     }
+
+   /* No requests on this interface at all - it exists only to be sent one
+    * event and destroyed. */
+   wl_resource_set_implementation(res, NULL, ec,
+                                  _e_wp_presentation_feedback_res_destroy);
+
+   ec->comp_data->pending.presentation_feedbacks =
+     eina_list_append(ec->comp_data->pending.presentation_feedbacks, res);
+}
+
+static const struct wp_presentation_interface _e_wp_presentation_interface =
+{
+   _e_wp_presentation_cb_destroy,
+   _e_wp_presentation_cb_feedback,
+};
+
 #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) \
@@ -1805,6 +1973,13 @@ GLOBAL_BIND_CB(zxdg_output_manager_v1, zxdg_output_manager_v1_interface)
 GLOBAL_BIND_CB(wp_fractional_scale_manager_v1, wp_fractional_scale_manager_v1_interface)
 GLOBAL_BIND_CB(zwp_idle_inhibit_manager_v1, zwp_idle_inhibit_manager_v1_interface)
 GLOBAL_BIND_CB(zwp_keyboard_shortcuts_inhibit_manager_v1, zwp_keyboard_shortcuts_inhibit_manager_v1_interface)
+/* The clock_id event is sent on bind and never again - the protocol says the
+ * presentation clock does not change for the life of the connection, so a
+ * client is entitled to read it once and cache it. Anything sent later in
+ * e_comp_wl_presentation_feedback_present has to be in this clock domain. */
+GLOBAL_BIND_CB(wp_presentation, wp_presentation_interface,
+     wp_presentation_send_clock_id(res, CLOCK_MONOTONIC);
+)
 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);
@@ -1905,6 +2080,7 @@ e_comp_wl_extensions_init(void)
    GLOBAL_CREATE_OR_RETURN(action_route, action_route_interface, 1);
    GLOBAL_CREATE_OR_RETURN(xdg_activation_v1, xdg_activation_v1_interface, 1);
    GLOBAL_CREATE_OR_RETURN(wp_viewporter, wp_viewporter_interface, 1);
+   GLOBAL_CREATE_OR_RETURN(wp_presentation, wp_presentation_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/e_pixmap.c b/src/bin/e_pixmap.c
index e490f65c5..84e95f850 100644
--- a/src/bin/e_pixmap.c
+++ b/src/bin/e_pixmap.c
@@ -1105,6 +1105,17 @@ e_pixmap_image_clear(E_Pixmap *cp, Eina_Bool cache)
                   wl_callback_send_done(cb, t * 1000);
                   wl_resource_destroy(cb);
                }
+
+             /* The other half of the same moment: wl_surface.frame says "send
+              * me the next one", wp_presentation_feedback says "here is when
+              * the last one landed". They are answered together because this
+              * is the only point at which E knows a client's content has been
+              * drawn - but they carry different timestamps on purpose. The
+              * frame callback keeps the loop time it has always sent; the
+              * feedback samples the presentation clock here, because a client
+              * pacing video against a timestamp from before the render would
+              * accumulate exactly the error it is trying to measure. */
+             e_comp_wl_presentation_feedback_present(&cd->presentation_feedbacks);
           }
 #endif
         break;
diff --git a/src/bin/generated/meson.build b/src/bin/generated/meson.build
index a8c4790cf..256ffd61c 100644
--- a/src/bin/generated/meson.build
+++ b/src/bin/generated/meson.build
@@ -8,6 +8,7 @@ protos = [
   '@0@/unstable/pointer-constraints/pointer-constraints-unstable-v1.xml'.format(dir_wayland_protocols),
   '@0@/staging/xdg-activation/xdg-activation-v1.xml'.format(dir_wayland_protocols),
   '@0@/stable/viewporter/viewporter.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),
   '@0@/staging/fractional-scale/fractional-scale-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 c2dbe4843..c256e7896 100644
--- a/src/tests/wayland/e_wl_testkit.c
+++ b/src/tests/wayland/e_wl_testkit.c
@@ -493,6 +493,26 @@ tk_toplevel_painted(Tk_Toplevel *top, int *w, int *h)
    if (h) *h = top->h;
 }
 
+void
+tk_toplevel_repaint(Tk_Toplevel *top)
+{
+   struct wl_buffer *buffer;
+   int sw = top->w + (2 * top->shadow);
+   int sh = top->h + (2 * top->shadow);
+
+   buffer = _buffer_make(top->tk, sw, sh);
+   if (!buffer) tk_fail(top->tk, "cannot make a %dx%d buffer", sw, sh);
+
+   /* A fresh buffer and full damage every time, deliberately. Re-committing
+    * the same one, or committing no damage at all, is a content update the
+    * compositor is entitled to skip drawing - and a test that wants to know
+    * when a frame reached the screen must not be the reason there was no
+    * frame. */
+   wl_surface_attach(top->surface, buffer, 0, 0);
+   wl_surface_damage(top->surface, 0, 0, sw, sh);
+   wl_surface_commit(top->surface);
+}
+
 void
 tk_settle(Tk *tk)
 {
diff --git a/src/tests/wayland/e_wl_testkit.h b/src/tests/wayland/e_wl_testkit.h
index b353dc138..4c0686220 100644
--- a/src/tests/wayland/e_wl_testkit.h
+++ b/src/tests/wayland/e_wl_testkit.h
@@ -104,6 +104,12 @@ void tk_toplevel_configured(Tk_Toplevel *top, int *w, int *h, int *count);
  * asking whether the compositor and the client agree. */
 void tk_toplevel_painted(Tk_Toplevel *top, int *w, int *h);
 
+/* Commit a new frame at the size already being painted: attach a fresh
+ * buffer, damage all of it, commit. No configure involved and no waiting -
+ * for tests that need the compositor to actually draw something, as opposed
+ * to tests that need it to change something. */
+void tk_toplevel_repaint(Tk_Toplevel *top);
+
 /* The version the compositor advertised for a global, or 0 if it has none.
  * Ask this before tk_bind when the test is about a version, so a compositor
  * that offers too little is reported as such rather than measured silently at
diff --git a/src/tests/wayland/globals.expected b/src/tests/wayland/globals.expected
index e8dfe02f8..110ad0d2d 100644
--- a/src/tests/wayland/globals.expected
+++ b/src/tests/wayland/globals.expected
@@ -8,6 +8,7 @@ wl_shell	1
 wl_shm	>=1
 wl_subcompositor	1
 wp_fractional_scale_manager_v1	1
+wp_presentation	1
 wp_viewporter	1
 xdg_activation_v1	1
 xdg_wm_base	6
diff --git a/src/tests/wayland/meson.build b/src/tests/wayland/meson.build
index 200d1575c..9c03787bf 100644
--- a/src/tests/wayland/meson.build
+++ b/src/tests/wayland/meson.build
@@ -40,6 +40,7 @@ foreach p: [
   '@0@/unstable/idle-inhibit/idle-inhibit-unstable-v1.xml'.format(dir_wayland_protocols),
   '@0@/unstable/keyboard-shortcuts-inhibit/keyboard-shortcuts-inhibit-unstable-v1.xml'.format(dir_wayland_protocols),
   '@0@/unstable/linux-dmabuf/linux-dmabuf-unstable-v1.xml'.format(dir_wayland_protocols),
+  '@0@/stable/presentation-time/presentation-time.xml'.format(dir_wayland_protocols),
 ]
   test_proto_src += gen_scanner_client.process(p)
   test_proto_src += gen_scanner_impl.process(p)
@@ -81,6 +82,7 @@ wl_protocol_tests = [
   ['client-state', 'test_client_state.c'],
   ['client-move', 'test_client_move.c'],
   ['maximize-restore', 'test_maximize_restore.c'],
+  ['presentation-time', 'test_presentation_time.c'],
 ]
 
 # Shared plumbing: registry binding, toplevel construction, enumeration and a
diff --git a/src/tests/wayland/test_presentation_time.c b/src/tests/wayland/test_presentation_time.c
new file mode 100644
index 000000000..1680a7eb8
--- /dev/null
+++ b/src/tests/wayland/test_presentation_time.c
@@ -0,0 +1,327 @@
+/* wp_presentation: when did the frame I committed actually reach the screen?
+ *
+ * The protocol's whole contract is an accounting one. Every
+ * wp_presentation_feedback object is created for exactly one content update
+ * and must end in exactly one event - 'presented' with a timestamp, or
+ * 'discarded' if those pixels were never shown. Both are destructor events.
+ * A client that gets neither is left holding an object it is still waiting on,
+ * and for the clients that ask for this - video players pacing a picture
+ * against a soundtrack - waiting forever means the video stops.
+ *
+ * So this test counts. It does not assert that any particular commit was
+ * presented rather than discarded: which of the two is correct depends on
+ * whether the compositor got round to drawing, and a test that demanded one
+ * would be asserting a scheduling detail rather than the contract. It asserts
+ * that every feedback ends, ends once, and never both ways.
+ *
+ * The timestamps get their own scrutiny, because the easy way to implement
+ * this protocol is to send a number that is already lying around - and E has
+ * one lying around, ecore_loop_time_get(), which is when the current main loop
+ * iteration *began*. That is a whole render before the moment being reported,
+ * and it need not even be in the clock the compositor named in clock_id. A
+ * plausible-looking timestamp in the wrong clock domain is the failure this
+ * protocol is most likely to ship with and the hardest to notice afterwards:
+ * everything works, the video just drifts. So the test reads the compositor's
+ * own advertised clock itself, brackets each commit between two samples of it,
+ * and requires the reported time to fall inside.
+ */
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+
+#include "e_wl_testkit.h"
+#include "presentation-time-client-protocol.h"
+
+#define PROG "test-presentation-time"
+#define APP_ID "presentation-time"
+
+#define GEOM_W 300
+#define GEOM_H 200
+
+#define SETTLE_MS 5000
+
+/* How far outside the bracket a timestamp may fall before it is a failure.
+ * Not zero: the compositor samples its clock at a slightly different instant
+ * than we sample ours, and on a loaded machine the scheduler can put a real
+ * gap between the two. Generous enough that only a wrong *clock* fails - a
+ * CLOCK_REALTIME value read as CLOCK_MONOTONIC is out by decades, not
+ * milliseconds. */
+#define SLACK_NS (2ll * 1000000000ll)
+
+static Tk *tk;
+static clockid_t present_clock;
+static int have_clock_id;
+
+typedef struct
+{
+   struct wp_presentation_feedback *obj;
+   const char *label;
+   int presented;
+   int discarded;
+   int64_t committed_ns;    /* our clock, just before the commit */
+   int64_t deadline_ns;     /* our clock, once the event had arrived */
+   int64_t reported_ns;     /* what the compositor said */
+} Feedback;
+
+static int64_t
+now_ns(clockid_t clk)
+{
+   struct timespec ts;
+
+   if (clock_gettime(clk, &ts) < 0)
+     tk_fail(tk, "clock_gettime failed on clock %d - the compositor named a "
+                 "clock this machine does not have", (int)clk);
+   return ((int64_t)ts.tv_sec * 1000000000ll) + ts.tv_nsec;
+}
+
+/* ------------------------------------------------------------ wp_presentation */
+
+static void
+_presentation_clock_id(void *data, struct wp_presentation *p, uint32_t clk_id)
+{
+   (void)data; (void)p;
+   present_clock = (clockid_t)clk_id;
+   have_clock_id++;
+}
+
+static const struct wp_presentation_listener _presentation_listener =
+{
+   _presentation_clock_id
+};
+
+/* ---------------------------------------------------- wp_presentation_feedback */
+
+static void
+_fb_sync_output(void *data, struct wp_presentation_feedback *f, struct wl_output *o)
+{
+   (void)data; (void)f; (void)o;
+   /* Legal to send and legal to omit. Nothing to check: the protocol only
+    * requires that it come before 'presented', which the bindings enforce by
+    * ordering anyway. */
+}
+
+static void
+_fb_presented(void *data, struct wp_presentation_feedback *f,
+              uint32_t tv_sec_hi, uint32_t tv_sec_lo, uint32_t tv_nsec,
+              uint32_t refresh, uint32_t seq_hi, uint32_t seq_lo, uint32_t flags)
+{
+   Feedback *fb = data;
+   uint64_t sec;
+
+   (void)f; (void)refresh; (void)seq_hi; (void)seq_lo; (void)flags;
+
+   fb->presented++;
+   fb->deadline_ns = now_ns(present_clock);
+
+   if (tv_nsec > 999999999)
+     tk_fail(tk, "%s: presented with tv_nsec %u. The protocol requires "
+                 "[0, 999999999] - the seconds and nanoseconds parts have "
+                 "not been separated", fb->label, tv_nsec);
+
+   sec = ((uint64_t)tv_sec_hi << 32) | (uint64_t)tv_sec_lo;
+   fb->reported_ns = ((int64_t)sec * 1000000000ll) + (int64_t)tv_nsec;
+}
+
+static void
+_fb_discarded(void *data, struct wp_presentation_feedback *f)
+{
+   Feedback *fb = data;
+
+   (void)f;
+   fb->discarded++;
+}
+
+static const struct wp_presentation_feedback_listener _feedback_listener =
+{
+   _fb_sync_output,
+   _fb_presented,
+   _fb_discarded
+};
+
+static int
+_fb_done(const Feedback *fb)
+{
+   return (fb->presented + fb->discarded) > 0;
+}
+
+static void
+_fb_check(const Feedback *fb)
+{
+   if (!_fb_done(fb))
+     tk_fail(tk, "%s: neither presented nor discarded after %dms. The client "
+                 "is still waiting on this object and always will be - a video "
+                 "player in this position stops scheduling frames",
+             fb->label, SETTLE_MS);
+
+   if (fb->presented && fb->discarded)
+     tk_fail(tk, "%s: both presented and discarded. Each is a destructor "
+                 "event; the second was sent to an object that no longer "
+                 "exists", fb->label);
+
+   if (fb->presented > 1)
+     tk_fail(tk, "%s: presented %d times", fb->label, fb->presented);
+   if (fb->discarded > 1)
+     tk_fail(tk, "%s: discarded %d times", fb->label, fb->discarded);
+
+   if (!fb->presented) return;
+
+   if (fb->reported_ns < (fb->committed_ns - SLACK_NS))
+     tk_fail(tk, "%s: presented at %lld, but the commit it belongs to was not "
+                 "made until %lld - %.3fs earlier than the frame existed. "
+                 "Either the timestamp comes from before the render or it is "
+                 "not in clock %d, the one clock_id advertised",
+             fb->label, (long long)fb->reported_ns,
+             (long long)fb->committed_ns,
+             (fb->committed_ns - fb->reported_ns) / 1e9, (int)present_clock);
+
+   if (fb->reported_ns > (fb->deadline_ns + SLACK_NS))
+     tk_fail(tk, "%s: presented at %lld, which is %.3fs in the future - the "
+                 "event had already arrived at %lld. That is not clock %d",
+             fb->label, (long long)fb->reported_ns,
+             (fb->reported_ns - fb->deadline_ns) / 1e9,
+             (long long)fb->deadline_ns, (int)present_clock);
+}
+
+/* Ask for feedback on the next commit, then make one. Order matters and is
+ * the whole reason this is a helper: the request names the content update
+ * that the *following* commit carries, so a feedback created after the commit
+ * belongs to the one after that. */
+static void
+_feedback_commit(struct wp_presentation *pres, Tk_Toplevel *top,
+                 Feedback *fb, const char *label)
+{
+   memset(fb, 0, sizeof(*fb));
+   fb->label = label;
+   fb->committed_ns = now_ns(present_clock);
+   fb->obj = wp_presentation_feedback(pres, tk_toplevel_surface(top));
+   wp_presentation_feedback_add_listener(fb->obj, &_feedback_listener, fb);
+   tk_toplevel_repaint(top);
+}
+
+static void
+_wait_done(Feedback **fbs, int n, const char *what)
+{
+   struct timespec start, now;
+   int i, all;
+
+   clock_gettime(CLOCK_MONOTONIC, &start);
+   for (;;)
+     {
+        double elapsed;
+
+        tk_sync(tk);
+
+        all = 1;
+        for (i = 0; i < n; i++)
+          if (!_fb_done(fbs[i])) { all = 0; break; }
+        if (all) return;
+
+        clock_gettime(CLOCK_MONOTONIC, &now);
+        elapsed = ((now.tv_sec - start.tv_sec) * 1000.0) +
+                  ((now.tv_nsec - start.tv_nsec) / 1000000.0);
+        if (elapsed > SETTLE_MS) break;
+     }
+
+   /* Fall through to the per-object check, which says which one and why. */
+   for (i = 0; i < n; i++) _fb_check(fbs[i]);
+   tk_fail(tk, "%s: timed out, but every feedback looks finished - the wait "
+               "loop and the check disagree", what);
+}
+
+int
+main(void)
+{
+   struct wp_presentation *pres;
+   Tk_Toplevel *top;
+   Feedback rounds[4], a, b;
+   Feedback *set[4];
+   uint32_t version;
+   int i, presented = 0;
+
+   tk = tk_connect(PROG);
+
+   version = tk_global_version(tk, "wp_presentation");
+   if (version < 1)
+     tk_fail(tk, "no wp_presentation global. Without it a client has no way "
+                 "to learn when a frame was shown, only when the compositor "
+                 "wants the next one - which is not the same question and "
+                 "cannot be used to keep video in step with audio");
+   printf(PROG ": wp_presentation v%u\n", version);
+
+   pres = tk_bind(tk, &wp_presentation_interface, 1);
+   if (!pres) tk_fail(tk, "wp_presentation advertised but would not bind");
+   wp_presentation_add_listener(pres, &_presentation_listener, NULL);
+
+   /* clock_id is sent on bind and never again, so a client is entitled to
+    * read it once and cache it. If it never comes, every timestamp that
+    * follows is uninterpretable - there is no default to fall back on. */
+   tk_sync(tk);
+   if (!have_clock_id)
+     tk_fail(tk, "bound wp_presentation and got no clock_id. Every timestamp "
+                 "in this protocol is meaningless without knowing which clock "
+                 "it is in, and the protocol says the event comes at bind");
+   if (have_clock_id > 1)
+     tk_fail(tk, "clock_id sent %d times - the presentation clock is not "
+                 "allowed to change during the connection", have_clock_id);
+
+   /* Not a demand for CLOCK_MONOTONIC. The protocol says the identifier is
+    * platform dependent and any clock_gettime() clock will do; what it does
+    * not allow is naming one the client cannot read. */
+   (void)now_ns(present_clock);
+   printf(PROG ": presentation clock is %d%s\n", (int)present_clock,
+          (present_clock == CLOCK_MONOTONIC) ? " (CLOCK_MONOTONIC)" : "");
+
+   top = tk_toplevel_new(tk, APP_ID, "presentation time", GEOM_W, GEOM_H);
+   tk_expect(tk, APP_ID);
+
+   /* ------------------------------------- one commit at a time */
+
+   for (i = 0; i < (int)(sizeof(rounds) / sizeof(rounds[0])); i++)
+     {
+        static char labels[4][32];
+        Feedback *one;
+
+        snprintf(labels[i], sizeof(labels[i]), "frame %d", i + 1);
+        _feedback_commit(pres, top, &rounds[i], labels[i]);
+        _one_ = &rounds[i];
+        _wait_done(&one, 1, labels[i]);
+        _fb_check(&rounds[i]);
+        if (rounds[i].presented) presented++;
+     }
+
+   /* A compositor that discarded everything would satisfy every rule above
+    * while telling the client nothing. Four visible, fully damaged commits to
+    * a mapped window, each waited out on its own - if not one of them was
+    * drawn, the feature does not work whatever the accounting says. */
+   if (!presented)
+     tk_fail(tk, "%d commits to a mapped window, every one discarded and not "
+                 "one presented. The accounting is right and the protocol is "
+                 "useless: nothing ever reports a time",
+             (int)(sizeof(rounds) / sizeof(rounds[0])));
+   printf(PROG ": %d of %d commits presented, the rest discarded\n",
+          presented, (int)(sizeof(rounds) / sizeof(rounds[0])));
+
+   /* ------------------------------------- two commits, no wait between */
+
+   /* The superseding case, which is the one with a bug in it if there is one.
+    * Two content updates back to back: the first may or may not have been
+    * drawn before the second replaced it, so either outcome is correct for
+    * it - but it has to get one of them. Dropping the first feedback on the
+    * floor when a second commit arrives is the natural way to write this
+    * wrong, and it is invisible to any test that only ever has one
+    * outstanding. */
+   _feedback_commit(pres, top, &a, "back-to-back first");
+   _feedback_commit(pres, top, &b, "back-to-back second");
+   set[0] = &a; set[1] = &b;
+   _wait_done(set, 2, "two commits with no wait between them");
+   _fb_check(&a);
+   _fb_check(&b);
+   printf(PROG ": back-to-back commits - first %s, second %s\n",
+          a.presented ? "presented" : "discarded",
+          b.presented ? "presented" : "discarded");
+
+   printf(PROG ": ok\n");
+   tk_disconnect(tk);
+   return 0;
+}

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

Reply via email to