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 6afb439c9107e0cb7c9f65b4e18e985d7e145e0d
Author: Cedric BAIL <[email protected]>
AuthorDate: Mon Aug 17 13:42:37 2026 -0600
e_comp_wl - implement zwp_linux_dmabuf_v1 version 4
Version 4 stops answering "what can you import" and starts answering
"what should you allocate, and where". The format and modifier events are
not superseded but forbidden from 4 on, so a client that binds 4 learns
through feedback objects or not at all.
The point of the change is the device. A client that renders on one GPU
and hands the buffer to a compositor importing on another gets a copy
through system memory every frame, and before feedback there was no way
for it to find out. main_device is how it finds out - which is why E
offers version 4 only when it can name its own DRM node, and stays at 3
otherwise. A client takes that number, resolves it and allocates there,
so naming the wrong node sends it to the wrong GPU. On a single-GPU
machine any guess is right, which is what makes guessing tempting and
wrong: it would be correct everywhere it does not matter and wrong in the
one case the protocol was written for.
The device comes from evasglQueryDrmDevice, new in EVAS_GL_API_VERSION 8,
beside the two dmabuf queries E already used. Nothing else in E or EFL
knew it: the EGLDisplay lives inside Evas.
One tranche, targeting the main device, no scanout flag. E composites
everything through the GL engine and never puts a client buffer on a KMS
plane, so anything it can import is equally good and an order of
preference would be invented information. The format table is built once
into a sealed memfd - the protocol forbids mutating a table after sending
it, and the seals are what make that true rather than merely intended.
WHAT IS NOT VERIFIED, and why. None of this code runs in any test
configuration available here. linux_dmabuf_setup is called only when
neither dmabuf_disable nor dmabuf_proxy is set, and every nested backend
sets one: wl_buffer and wl_x11 disable it outright, wl_wl proxies. Only
the DRM backend - E as the session compositor on a TTY - ever reaches it.
So the in-tree test skips everywhere it can be run, and is written to say
so with a reason rather than pass vacuously.
What is verified: the device query itself, measured on this hardware
through a standalone Evas_GL probe, returns 226:128 and names
/dev/dri/renderD128. And the full wlcs suite is unchanged at 770/13,
which says this does no harm where it does not run.
The wl_x11 disable is da23b852e, from 2016: "too hard to get right and
nobody will notice its absence". Both halves have expired - dmabuf is the
path browsers use for GPU rendering - and lifting it is the way to make
this testable. That is its own change and its own evidence, not this one.
---
src/bin/e_comp_wl.c | 26 +++
src/bin/e_comp_wl_dmabuf.c | 332 ++++++++++++++++++++++++++++-
src/tests/wayland/e_wl_testkit.c | 53 +++++
src/tests/wayland/e_wl_testkit.h | 16 ++
src/tests/wayland/meson.build | 2 +
src/tests/wayland/test_dmabuf_feedback.c | 350 +++++++++++++++++++++++++++++++
6 files changed, 777 insertions(+), 2 deletions(-)
diff --git a/src/bin/e_comp_wl.c b/src/bin/e_comp_wl.c
index 2a8ceda14..52a5af722 100644
--- a/src/bin/e_comp_wl.c
+++ b/src/bin/e_comp_wl.c
@@ -4328,6 +4328,32 @@ e_comp_wl_query_dmabuf_modifiers(int format, int max_modifiers, uint64_t *modifi
#endif
}
+/* The DRM node E imports dmabufs on, as a dev_t widened to a fixed size.
+ *
+ * This is what zwp_linux_dmabuf_v1 version 4 calls the main device, and it is
+ * the one number that made v4 impossible to implement until EFL grew a way to
+ * ask for it. Only Evas can answer: the EGLDisplay lives inside the GL engine,
+ * and the device is two EGL queries and a stat() away from there.
+ *
+ * Failure here is ordinary rather than exceptional - there is no EGL under the
+ * software engine or under GLX, and a driver need not implement
+ * EGL_EXT_device_query. Every such case must come back false so the caller
+ * keeps to version 3 instead of advertising feedback it would have to invent.
+ * A wrong device is worse than no device: the client resolves it and allocates
+ * there. */
+Eina_Bool
+e_comp_wl_query_dmabuf_device(uint64_t *device)
+{
+ *device = 0;
+#if EVAS_GL_API_VERSION >= 8
+ if (!e_comp_wl->wl.glapi) return EINA_FALSE;
+ if (!e_comp_wl->wl.glapi->evasglQueryDrmDevice) return EINA_FALSE;
+ return e_comp_wl->wl.glapi->evasglQueryDrmDevice(e_comp_wl->wl.gl, device);
+#else
+ return EINA_FALSE;
+#endif
+}
+
static void
_e_comp_wl_gl_init(void)
{
diff --git a/src/bin/e_comp_wl_dmabuf.c b/src/bin/e_comp_wl_dmabuf.c
index 95bb10ed7..2ffee828e 100644
--- a/src/bin/e_comp_wl_dmabuf.c
+++ b/src/bin/e_comp_wl_dmabuf.c
@@ -35,6 +35,8 @@
#include "e.h"
#include <sys/mman.h>
+#include <unistd.h>
+#include <fcntl.h>
#include <assert.h>
@@ -44,6 +46,11 @@
# define DRM_FORMAT_MOD_LINEAR 0
#endif
+/* Defined in e_comp_wl.c, which is where the Evas_GL handle lives. Declared
+ * here rather than in a header for the same reason e_pixmap.c declares its two
+ * siblings that way: they are one wire between two files in the same binary. */
+extern Eina_Bool e_comp_wl_query_dmabuf_device(uint64_t *device);
+
void
linux_dmabuf_buffer_ref(struct linux_dmabuf_buffer *buffer)
{
@@ -478,10 +485,299 @@ linux_dmabuf_buffer_get_user_data(struct linux_dmabuf_buffer *buffer)
return buffer->user_data;
}
+
+/* ------------------------------------------------------- dmabuf feedback
+ *
+ * zwp_linux_dmabuf_v1 version 4 stops answering "what can you import" and
+ * starts answering "what should you allocate, and where". The format and
+ * modifier events are not merely superseded, they are forbidden from version 4
+ * on, so a client that binds v4 is told through feedback objects or not at all.
+ *
+ * The point of the change is the device. A client that renders on one GPU and
+ * hands the buffer to a compositor importing on another gets a copy through
+ * system memory on every frame, and before feedback there was no way for it to
+ * find out. main_device is how it finds out, which is why E declines to
+ * advertise v4 at all when it cannot name its own device - see
+ * linux_dmabuf_setup below.
+ *
+ * What E does not do is direct scan-out: nothing here ever puts a client
+ * buffer on a KMS plane. So there is exactly one tranche, targeting the main
+ * device, with no scanout flag - which is a complete and honest answer for a
+ * compositor that composites everything, not a stub. A second tranche would be
+ * a claim about hardware behaviour E has no way to make good on.
+ */
+
+/* Format table shared by every feedback object: a tightly packed array of
+ * {u32 format, u32 padding, u64 modifier}, 16 bytes an entry, in a memfd the
+ * clients map read-only.
+ *
+ * Built once. The protocol forbids mutating a table after sending it, so a
+ * table that could change would have to become a new fd and a fresh round of
+ * parameters to every client holding feedback. E's format list comes from the
+ * GL engine and does not change while the compositor runs, so the simple
+ * version is also the correct one - but the seals are what make that true
+ * rather than merely intended, and they are why the fd can be handed to
+ * untrusted clients at all.
+ *
+ * The fd stays open for as long as the compositor runs, and there is no
+ * function here to close it. That matches the global it belongs to - see
+ * linux_dmabuf_setup's own note that globals are only reaped when the
+ * wl_display is destroyed - and a teardown path nothing can reach would be
+ * dead code pretending to be tidiness. */
+typedef struct
+{
+ uint32_t format;
+ uint32_t padding;
+ uint64_t modifier;
+} Dmabuf_Table_Entry;
+
+static int dmabuf_table_fd = -1;
+static size_t dmabuf_table_size = 0;
+static uint16_t dmabuf_table_entries = 0;
+static uint64_t dmabuf_main_device = 0;
+
+/* Collect every format/modifier pair the GL engine will import, and seal it
+ * into a memfd. Returns false if there is nothing to say, which is a reason
+ * not to offer v4 rather than something to paper over. */
+static Eina_Bool
+_dmabuf_table_build(void)
+{
+ Eina_Array *entries;
+ Dmabuf_Table_Entry *e;
+ int *formats = NULL;
+ int num_formats = 0, i, j;
+ unsigned int n;
+ void *map;
+ int fd;
+
+ if (dmabuf_table_fd >= 0) return EINA_TRUE;
+
+ if (!e_pixmap_dmabuf_formats_query(&formats, &num_formats)) return EINA_FALSE;
+ if (num_formats <= 0)
+ {
+ free(formats);
+ return EINA_FALSE;
+ }
+
+ entries = eina_array_new(64);
+ for (i = 0; i < num_formats; i++)
+ {
+ uint64_t *modifiers = NULL;
+ int num_modifiers = 0;
+
+ if (!e_pixmap_dmabuf_modifiers_query(formats[i], &modifiers, &num_modifiers))
+ num_modifiers = 0;
+
+ /* A format with no explicit modifiers is still usable - it means the
+ * layout is whatever the dmabuf itself carries. Say so with
+ * DRM_FORMAT_MOD_INVALID rather than dropping the format, which is
+ * what the version 3 path above does for the same case. */
+ if (num_modifiers == 0)
+ {
+ e = malloc(sizeof(*e));
+ if (e)
+ {
+ e->format = (uint32_t)formats[i];
+ e->padding = 0;
+ e->modifier = DRM_FORMAT_MOD_INVALID;
+ eina_array_push(entries, e);
+ }
+ }
+ for (j = 0; j < num_modifiers; j++)
+ {
+ e = malloc(sizeof(*e));
+ if (!e) continue;
+ e->format = (uint32_t)formats[i];
+ e->padding = 0;
+ e->modifier = modifiers[j];
+ eina_array_push(entries, e);
+ }
+ free(modifiers);
+ }
+ free(formats);
+
+ n = eina_array_count(entries);
+ /* tranche_formats indexes the table with 16-bit values, so a table that
+ * cannot be indexed is not a table. Nothing observed comes close, but
+ * truncating silently would be the kind of cap that reads as coverage. */
+ if ((n == 0) || (n > 65535))
+ {
+ if (n > 65535)
+ ERR("dmabuf: %u format/modifier pairs exceeds the 16-bit index the "
+ "protocol allows; not advertising feedback", n);
+ while (eina_array_count(entries)) free(eina_array_pop(entries));
+ eina_array_free(entries);
+ return EINA_FALSE;
+ }
+
+ fd = memfd_create("e-dmabuf-format-table", MFD_CLOEXEC | MFD_ALLOW_SEALING);
+ if (fd < 0)
+ {
+ ERR("dmabuf: could not create the format table memfd: %m");
+ while (eina_array_count(entries)) free(eina_array_pop(entries));
+ eina_array_free(entries);
+ return EINA_FALSE;
+ }
+
+ dmabuf_table_size = (size_t)n * sizeof(Dmabuf_Table_Entry);
+ if (ftruncate(fd, dmabuf_table_size) < 0)
+ {
+ ERR("dmabuf: could not size the format table: %m");
+ close(fd);
+ while (eina_array_count(entries)) free(eina_array_pop(entries));
+ eina_array_free(entries);
+ dmabuf_table_size = 0;
+ return EINA_FALSE;
+ }
+
+ map = mmap(NULL, dmabuf_table_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
+ if (map == MAP_FAILED)
+ {
+ ERR("dmabuf: could not map the format table: %m");
+ close(fd);
+ while (eina_array_count(entries)) free(eina_array_pop(entries));
+ eina_array_free(entries);
+ dmabuf_table_size = 0;
+ return EINA_FALSE;
+ }
+
+ for (i = 0; i < (int)n; i++)
+ {
+ e = eina_array_data_get(entries, i);
+ ((Dmabuf_Table_Entry *)map)[i] = *e;
+ }
+ munmap(map, dmabuf_table_size);
+
+ while (eina_array_count(entries)) free(eina_array_pop(entries));
+ eina_array_free(entries);
+
+ /* Sealed against every kind of change, because clients get this fd and the
+ * protocol promises them the contents will not move under their feet. */
+ if (fcntl(fd, F_ADD_SEALS,
+ F_SEAL_SEAL | F_SEAL_SHRINK | F_SEAL_GROW | F_SEAL_WRITE) < 0)
+ {
+ ERR("dmabuf: could not seal the format table: %m");
+ close(fd);
+ dmabuf_table_size = 0;
+ return EINA_FALSE;
+ }
+
+ dmabuf_table_fd = fd;
+ dmabuf_table_entries = (uint16_t)n;
+ return EINA_TRUE;
+}
+
+/* Send one complete set of parameters. Order matters: everything, then
+ * tranches, then done - the client applies nothing until done arrives, which
+ * is what makes a change atomic. */
+static void
+_dmabuf_feedback_send(struct wl_resource *resource)
+{
+ struct wl_array device, indices;
+ uint64_t dev = dmabuf_main_device;
+ uint16_t *idx;
+ void *slot;
+ int i;
+
+ /* dev_t on the wire is an array of bytes, not an integer field: the
+ * protocol says so because dev_t's width is a platform matter. */
+ wl_array_init(&device);
+ slot = wl_array_add(&device, sizeof(dev));
+ if (!slot)
+ {
+ wl_array_release(&device);
+ return;
+ }
+ memcpy(slot, &dev, sizeof(dev));
+
+ zwp_linux_dmabuf_feedback_v1_send_format_table(resource, dmabuf_table_fd,
+ dmabuf_table_size);
+ zwp_linux_dmabuf_feedback_v1_send_main_device(resource, &device);
+
+ /* The one tranche: the main device, no scanout, every format in the table.
+ * "All of them" is not laziness - E composites everything through the GL
+ * engine, so anything it can import is equally good, and inventing an order
+ * of preference would be inventing information. */
+ wl_array_init(&indices);
+ for (i = 0; i < (int)dmabuf_table_entries; i++)
+ {
+ idx = wl_array_add(&indices, sizeof(uint16_t));
+ if (!idx)
+ {
+ /* Half a tranche is worse than none: the client would allocate
+ * against a list that silently omits formats E can import. Say
+ * nothing at all rather than something partly true. */
+ ERR("dmabuf: out of memory building the tranche; sending no feedback");
+ wl_array_release(&indices);
+ wl_array_release(&device);
+ return;
+ }
+ *idx = (uint16_t)i;
+ }
+
+ zwp_linux_dmabuf_feedback_v1_send_tranche_target_device(resource, &device);
+ zwp_linux_dmabuf_feedback_v1_send_tranche_flags(resource, 0);
+ zwp_linux_dmabuf_feedback_v1_send_tranche_formats(resource, &indices);
+ zwp_linux_dmabuf_feedback_v1_send_tranche_done(resource);
+
+ zwp_linux_dmabuf_feedback_v1_send_done(resource);
+
+ wl_array_release(&indices);
+ wl_array_release(&device);
+}
+
+static void
+_dmabuf_feedback_destroy(struct wl_client *client, struct wl_resource *resource)
+{
+ wl_resource_destroy(resource);
+}
+
+static const struct zwp_linux_dmabuf_feedback_v1_interface dmabuf_feedback_implementation =
+{
+ _dmabuf_feedback_destroy,
+};
+
+static void
+_dmabuf_feedback_create(struct wl_client *client, struct wl_resource *dmabuf_resource, uint32_t id)
+{
+ struct wl_resource *resource;
+
+ resource = wl_resource_create(client, &zwp_linux_dmabuf_feedback_v1_interface,
+ wl_resource_get_version(dmabuf_resource), id);
+ if (!resource)
+ {
+ wl_client_post_no_memory(client);
+ return;
+ }
+ wl_resource_set_implementation(resource, &dmabuf_feedback_implementation,
+ NULL, NULL);
+
+ _dmabuf_feedback_send(resource);
+}
+
+static void
+linux_dmabuf_get_default_feedback(struct wl_client *client, struct wl_resource *resource, uint32_t id)
+{
+ _dmabuf_feedback_create(client, resource, id);
+}
+
+/* Per-surface feedback exists so a compositor can tailor the answer to what it
+ * is doing with that particular surface - scanning it out, or compositing it
+ * on another GPU. E does neither, so the surface makes no difference and the
+ * answer is the default one. That is correct rather than incomplete: the
+ * protocol asks for the best available answer, not for a different one. */
+static void
+linux_dmabuf_get_surface_feedback(struct wl_client *client, struct wl_resource *resource, uint32_t id, struct wl_resource *surface EINA_UNUSED)
+{
+ _dmabuf_feedback_create(client, resource, id);
+}
+
static const struct zwp_linux_dmabuf_v1_interface linux_dmabuf_implementation =
{
linux_dmabuf_destroy,
- linux_dmabuf_create_params
+ linux_dmabuf_create_params,
+ linux_dmabuf_get_default_feedback,
+ linux_dmabuf_get_surface_feedback
};
static void
@@ -506,6 +802,15 @@ bind_linux_dmabuf(struct wl_client *client, void *data, uint32_t version, uint32
wl_resource_set_implementation(resource, &linux_dmabuf_implementation,
compositor, NULL);
+ /* From version 4 the format and modifier events are not deprecated in the
+ * usual sense of "still sent, please stop reading them" - the protocol says
+ * they must not be sent at all, and a client on 4 or later learns what it
+ * can allocate through feedback instead. Sending both would have a
+ * conforming client counting the same formats twice from two sources that
+ * are not required to agree. */
+ if (version >= ZWP_LINUX_DMABUF_V1_GET_DEFAULT_FEEDBACK_SINCE_VERSION)
+ return;
+
/*
* Use EGL_EXT_image_dma_buf_import_modifiers to query and advertise
* format/modifier codes.
@@ -561,7 +866,30 @@ bind_linux_dmabuf(struct wl_client *client, void *data, uint32_t version, uint32
EINTERN int
linux_dmabuf_setup(struct wl_display *display)
{
- if (!wl_global_create(display, &zwp_linux_dmabuf_v1_interface, 3,
+ int version = 3;
+
+ /* Version 4 is offered only if both halves of a feedback answer can be
+ * had: the device E imports on, and a format table to point at. Neither is
+ * something to guess.
+ *
+ * The device especially. A client takes main_device, resolves it to a real
+ * DRM node and allocates there - it does not merely compare the number - so
+ * naming the wrong node sends the client to the wrong GPU and reinstates
+ * the very copy through system memory that feedback exists to remove. On a
+ * single-GPU machine any plausible guess is right, which is exactly what
+ * makes guessing tempting and wrong: it would be correct everywhere it does
+ * not matter and wrong in the one case the protocol was written for.
+ *
+ * Staying at 3 is not a degraded mode. Version 3 is what E has always
+ * advertised, it is what Brave runs on today, and a client that gets it is
+ * no worse off than it was. */
+ if (e_comp_wl_query_dmabuf_device(&dmabuf_main_device) &&
+ dmabuf_main_device && _dmabuf_table_build())
+ version = 4;
+ else
+ dmabuf_main_device = 0;
+
+ if (!wl_global_create(display, &zwp_linux_dmabuf_v1_interface, version,
NULL, bind_linux_dmabuf))
return -1;
diff --git a/src/tests/wayland/e_wl_testkit.c b/src/tests/wayland/e_wl_testkit.c
index 20a1796cf..a8cd61531 100644
--- a/src/tests/wayland/e_wl_testkit.c
+++ b/src/tests/wayland/e_wl_testkit.c
@@ -35,6 +35,17 @@ struct _Tk
struct wl_test *tester;
uint32_t tester_version;
+ /* Every global the compositor advertised, so a test can bind one the kit
+ * does not know about. The kit cannot bind them all - a test that wants a
+ * protocol the kit has never heard of should not need the kit changed. */
+ struct
+ {
+ char iface[64];
+ uint32_t name;
+ uint32_t version;
+ } globals[TK_MAX_GLOBALS];
+ int global_count;
+
Tk_Client clients[TK_MAX_CLIENTS];
int client_count;
int done_count;
@@ -205,6 +216,15 @@ _global_add(void *data, struct wl_registry *reg, uint32_t id,
{
Tk *tk = data;
+ if (tk->global_count < TK_MAX_GLOBALS)
+ {
+ snprintf(tk->globals[tk->global_count].iface,
+ sizeof(tk->globals[tk->global_count].iface), "%s", iface);
+ tk->globals[tk->global_count].name = id;
+ tk->globals[tk->global_count].version = version;
+ tk->global_count++;
+ }
+
if (!strcmp(iface, "wl_compositor"))
tk->compositor = wl_registry_bind(reg, id, &wl_compositor_interface,
version < 4 ? version : 4);
@@ -385,6 +405,33 @@ tk_toplevel_new(Tk *tk, const char *app_id, const char *title, int w, int h)
return top;
}
+uint32_t
+tk_global_version(Tk *tk, const char *iface)
+{
+ int i;
+
+ for (i = 0; i < tk->global_count; i++)
+ if (!strcmp(tk->globals[i].iface, iface)) return tk->globals[i].version;
+ return 0;
+}
+
+void *
+tk_bind(Tk *tk, const struct wl_interface *iface, uint32_t version)
+{
+ int i;
+
+ for (i = 0; i < tk->global_count; i++)
+ {
+ if (strcmp(tk->globals[i].iface, iface->name)) continue;
+ /* Bind at exactly what was asked for. Clamping to what the compositor
+ * offers would turn "this test needs version 4" into a test that
+ * quietly measures version 3 - ask tk_global_version first and say so
+ * plainly instead. */
+ return wl_registry_bind(tk->registry, tk->globals[i].name, iface, version);
+ }
+ return NULL;
+}
+
void
tk_sync(Tk *tk)
{
@@ -396,6 +443,12 @@ tk_sync(Tk *tk)
}
}
+struct wl_surface *
+tk_toplevel_surface(Tk_Toplevel *top)
+{
+ return top->surface;
+}
+
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 3cf4689f9..a54fc4937 100644
--- a/src/tests/wayland/e_wl_testkit.h
+++ b/src/tests/wayland/e_wl_testkit.h
@@ -20,6 +20,7 @@
#include "xdg-shell-client-protocol.h"
#define TK_MAX_CLIENTS 64
+#define TK_MAX_GLOBALS 64
/* One window, as the compositor describes it. Mirrors wl_test.client_info. */
typedef struct
@@ -68,6 +69,10 @@ void tk_fail(Tk *tk, const char *fmt, ...) __attribute__((noreturn, format(print
Tk_Toplevel *tk_toplevel_new(Tk *tk, const char *app_id, const char *title,
int w, int h);
+/* The wl_surface behind a toplevel, for the protocols that take one. The kit
+ * owns it and destroys it with the connection. */
+struct wl_surface *tk_toplevel_surface(Tk_Toplevel *top);
+
/* The size the compositor last asked this toplevel to be, and the number of
* configures it has answered. What E asked for and what E then reports as the
* frame are different questions, and when they disagree only one of them is
@@ -81,6 +86,17 @@ 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);
+/* 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
+ * whatever it does offer. */
+uint32_t tk_global_version(Tk *tk, const char *iface);
+
+/* Bind a global the kit does not itself know about, at exactly this version.
+ * NULL if the compositor never advertised it. The kit binds what every test
+ * needs; this is for what one test needs. */
+void *tk_bind(Tk *tk, const struct wl_interface *iface, uint32_t version);
+
/* Round-trip through the compositor's own main loop, so effects E applies from
* a job or an idler have landed. Not wl_display.sync, which only proves the
* requests were read. */
diff --git a/src/tests/wayland/meson.build b/src/tests/wayland/meson.build
index 3acf7a841..d631a6517 100644
--- a/src/tests/wayland/meson.build
+++ b/src/tests/wayland/meson.build
@@ -39,6 +39,7 @@ foreach p: [
'@0@/staging/fractional-scale/fractional-scale-v1.xml'.format(dir_wayland_protocols),
'@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),
]
test_proto_src += gen_scanner_client.process(p)
test_proto_src += gen_scanner_impl.process(p)
@@ -79,6 +80,7 @@ wl_protocol_tests = [
['client-action', 'test_client_action.c'],
['client-state', 'test_client_state.c'],
['client-move', 'test_client_move.c'],
+ ['dmabuf-feedback', 'test_dmabuf_feedback.c'],
]
# Shared plumbing: registry binding, toplevel construction, enumeration and a
diff --git a/src/tests/wayland/test_dmabuf_feedback.c b/src/tests/wayland/test_dmabuf_feedback.c
new file mode 100644
index 000000000..c19e7177b
--- /dev/null
+++ b/src/tests/wayland/test_dmabuf_feedback.c
@@ -0,0 +1,350 @@
+/* Does zwp_linux_dmabuf_v1 version 4 say where to allocate, and does version 3
+ * still work?
+ *
+ * wlcs has no coverage for dmabuf at all, so this is the only evidence there
+ * will be, in either direction.
+ *
+ * Version 4 is not an addition to version 3, it is a replacement: the format
+ * and modifier events "must not be sent" from 4 on, and a client learns what
+ * it can allocate through a feedback object instead. That makes this a version
+ * gate, and a gate has two sides. Send the old events to a v4 client and a
+ * conforming one counts the same formats twice from two sources that need not
+ * agree; stop sending them to a v3 client and it has nothing at all. So the
+ * test binds the same global twice from one connection, at 3 and at 4, and
+ * asserts what each of them sees.
+ *
+ * The number that matters most is main_device. A client does not compare it,
+ * it resolves it to a DRM node and allocates there - so this checks that the
+ * node named actually exists, which is the difference between reading the
+ * device and reporting a plausible constant.
+ */
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/stat.h>
+#include <sys/sysmacros.h>
+#include <sys/mman.h>
+#include "e_wl_testkit.h"
+#include "linux-dmabuf-unstable-v1-client-protocol.h"
+
+#define PROG "test-dmabuf-feedback"
+
+/* 16 bytes: u32 format, u32 padding, u64 modifier. */
+typedef struct
+{
+ uint32_t format;
+ uint32_t padding;
+ uint64_t modifier;
+} Table_Entry;
+
+/* What one binding of the global saw. */
+typedef struct
+{
+ const char *name;
+ int formats; /* v3 format events */
+ int modifiers; /* v3 modifier events */
+ int done; /* feedback done events */
+ int main_devices;
+ int tranche_dones;
+ int tranche_targets;
+ int table_fds;
+ uint64_t device;
+ uint64_t tranche_device;
+ int table_fd;
+ uint32_t table_size;
+ int tranche_indices;
+ uint32_t tranche_flags;
+} Seen;
+
+static Seen v3 = { "v3", 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0 };
+static Seen v4 = { "v4", 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0 };
+
+/* ------------------------------------------------------- v3 dmabuf events */
+
+static void
+_dmabuf_format(void *d, struct zwp_linux_dmabuf_v1 *o, uint32_t format)
+{
+ Seen *s = d;
+ (void)o; (void)format;
+ s->formats++;
+}
+
+static void
+_dmabuf_modifier(void *d, struct zwp_linux_dmabuf_v1 *o, uint32_t format,
+ uint32_t hi, uint32_t lo)
+{
+ Seen *s = d;
+ (void)o; (void)format; (void)hi; (void)lo;
+ s->modifiers++;
+}
+
+static const struct zwp_linux_dmabuf_v1_listener _dmabuf_listener =
+{
+ _dmabuf_format, _dmabuf_modifier
+};
+
+/* --------------------------------------------------------- feedback events */
+
+static void
+_fb_done(void *d, struct zwp_linux_dmabuf_feedback_v1 *o)
+{
+ Seen *s = d;
+ (void)o;
+ s->done++;
+}
+
+static void
+_fb_format_table(void *d, struct zwp_linux_dmabuf_feedback_v1 *o, int32_t fd,
+ uint32_t size)
+{
+ Seen *s = d;
+ (void)o;
+ s->table_fds++;
+ s->table_fd = fd;
+ s->table_size = size;
+}
+
+static void
+_fb_main_device(void *d, struct zwp_linux_dmabuf_feedback_v1 *o,
+ struct wl_array *device)
+{
+ Seen *s = d;
+ (void)o;
+ s->main_devices++;
+ if (device->size == sizeof(uint64_t)) memcpy(&s->device, device->data, sizeof(uint64_t));
+}
+
+static void
+_fb_tranche_done(void *d, struct zwp_linux_dmabuf_feedback_v1 *o)
+{
+ Seen *s = d;
+ (void)o;
+ s->tranche_dones++;
+}
+
+static void
+_fb_tranche_target_device(void *d, struct zwp_linux_dmabuf_feedback_v1 *o,
+ struct wl_array *device)
+{
+ Seen *s = d;
+ (void)o;
+ s->tranche_targets++;
+ if (device->size == sizeof(uint64_t))
+ memcpy(&s->tranche_device, device->data, sizeof(uint64_t));
+}
+
+static void
+_fb_tranche_formats(void *d, struct zwp_linux_dmabuf_feedback_v1 *o,
+ struct wl_array *indices)
+{
+ Seen *s = d;
+ (void)o;
+ s->tranche_indices += (int)(indices->size / sizeof(uint16_t));
+}
+
+static void
+_fb_tranche_flags(void *d, struct zwp_linux_dmabuf_feedback_v1 *o, uint32_t flags)
+{
+ Seen *s = d;
+ (void)o;
+ s->tranche_flags = flags;
+}
+
+static const struct zwp_linux_dmabuf_feedback_v1_listener _fb_listener =
+{
+ _fb_done, _fb_format_table, _fb_main_device, _fb_tranche_done,
+ _fb_tranche_target_device, _fb_tranche_formats, _fb_tranche_flags
+};
+
+/* Does this dev_t name a node under /dev/dri? */
+static int
+device_exists(uint64_t device)
+{
+ char path[64];
+ struct stat st;
+ const char *dirs[] = { "/dev/dri", NULL };
+ int i;
+
+ (void)dirs;
+ /* Rather than scanning, ask the other way round: stat every plausible node
+ * and compare rdev. Cheap, and it does not depend on a naming convention. */
+ for (i = 0; i < 8; i++)
+ {
+ snprintf(path, sizeof(path), "/dev/dri/renderD%d", 128 + i);
+ if ((stat(path, &st) == 0) && ((uint64_t)st.st_rdev == device)) return 1;
+ snprintf(path, sizeof(path), "/dev/dri/card%d", i);
+ if ((stat(path, &st) == 0) && ((uint64_t)st.st_rdev == device)) return 1;
+ }
+ return 0;
+}
+
+int
+main(void)
+{
+ Tk *tk;
+ struct zwp_linux_dmabuf_v1 *d3, *d4;
+ struct zwp_linux_dmabuf_feedback_v1 *fb;
+ uint32_t advertised;
+ Table_Entry *table;
+
+ tk = tk_connect(PROG);
+
+ /* Both of the next two are skips rather than failures, and it is worth
+ * being clear why, because between them they mean this test does nothing
+ * at all under `meson test` as it is normally run.
+ *
+ * The default backend is the wl_buffer one, which has no GL: E never sets
+ * up dmabuf there, so the global is absent entirely - which is why it is
+ * not in globals.expected. Even with GL, E offers version 4 only when it
+ * can name its own DRM node, and a software canvas has none to name.
+ * Staying at 3 in that case is the behaviour this whole task was careful
+ * to preserve, so failing on it would be asserting the opposite of what
+ * was intended.
+ *
+ * To actually exercise this, run it on a GL canvas against a real GPU:
+ *
+ * E_TEST_BACKEND=x11 DISPLAY=:0 ./src/tests/wayland/run-nested.sh \
+ * build/src/tests/wayland/test_wl_dmabuf_feedback
+ *
+ * A skip here is honest, but a skip is not evidence. */
+ advertised = tk_global_version(tk, "zwp_linux_dmabuf_v1");
+ if (!advertised)
+ {
+ printf(PROG ": zwp_linux_dmabuf_v1 is not advertised - no GL on this "
+ "backend, so E never set dmabuf up. Needs a GL canvas; see the "
+ "comment in this file.\n");
+ printf(PROG ": skipped\n");
+ tk_disconnect(tk);
+ return 77;
+ }
+
+ if (advertised < 4)
+ {
+ printf(PROG ": dmabuf advertised at version %u, not 4. E offers 4 only "
+ "when it can name its own DRM device; on a software or GLX "
+ "canvas that is correct behaviour, not a regression.\n",
+ advertised);
+ printf(PROG ": skipped\n");
+ tk_disconnect(tk);
+ return 77;
+ }
+
+ /* --- version 3 still gets the old events, and no feedback ------------- */
+
+ d3 = tk_bind(tk, &zwp_linux_dmabuf_v1_interface, 3);
+ if (!d3) tk_fail(tk, "could not bind zwp_linux_dmabuf_v1 at 3");
+ zwp_linux_dmabuf_v1_add_listener(d3, &_dmabuf_listener, &v3);
+ tk_sync(tk);
+
+ if (!v3.modifiers)
+ tk_fail(tk, "a version 3 bind got no modifier events. Raising the global "
+ "to 4 must not take the old path away from clients that only "
+ "understand it");
+ printf(PROG ": v3 sees %d modifier events\n", v3.modifiers);
+
+ /* --- version 4 gets feedback, and none of the old events -------------- */
+
+ d4 = tk_bind(tk, &zwp_linux_dmabuf_v1_interface, 4);
+ if (!d4) tk_fail(tk, "could not bind zwp_linux_dmabuf_v1 at 4");
+ zwp_linux_dmabuf_v1_add_listener(d4, &_dmabuf_listener, &v4);
+ tk_sync(tk);
+
+ if (v4.formats || v4.modifiers)
+ tk_fail(tk, "a version 4 bind got %d format and %d modifier events. The "
+ "protocol says both must not be sent from 4 on, and a client "
+ "honouring feedback as well would count the same formats "
+ "twice", v4.formats, v4.modifiers);
+ printf(PROG ": v4 sees no format or modifier events\n");
+
+ fb = zwp_linux_dmabuf_v1_get_default_feedback(d4);
+ if (!fb) tk_fail(tk, "get_default_feedback returned nothing");
+ zwp_linux_dmabuf_feedback_v1_add_listener(fb, &_fb_listener, &v4);
+ tk_sync(tk);
+
+ if (!v4.done)
+ tk_fail(tk, "feedback never sent done. A client applies nothing until "
+ "done arrives, so feedback without it is feedback that never "
+ "took effect");
+
+ /* The device. */
+ if (v4.main_devices != 1)
+ tk_fail(tk, "expected exactly one main_device, got %d", v4.main_devices);
+ if (!v4.device)
+ tk_fail(tk, "main_device is 0, which names nothing");
+ if (!device_exists(v4.device))
+ tk_fail(tk, "main_device is %u:%u, which is not a node under /dev/dri. "
+ "A client resolves this number and allocates on whatever it "
+ "finds, so a plausible-looking wrong one is worse than none",
+ major((dev_t)v4.device), minor((dev_t)v4.device));
+ printf(PROG ": main_device is %u:%u and names a real node\n",
+ major((dev_t)v4.device), minor((dev_t)v4.device));
+
+ /* The tranche. */
+ if (v4.tranche_dones != 1)
+ tk_fail(tk, "expected exactly one tranche, got %d", v4.tranche_dones);
+ if (v4.tranche_targets != 1)
+ tk_fail(tk, "expected one tranche_target_device, got %d", v4.tranche_targets);
+ if (v4.tranche_device != v4.device)
+ tk_fail(tk, "the tranche targets %u:%u but the main device is %u:%u. The "
+ "protocol requires at least one tranche equal to the main "
+ "device",
+ major((dev_t)v4.tranche_device), minor((dev_t)v4.tranche_device),
+ major((dev_t)v4.device), minor((dev_t)v4.device));
+ if (!v4.tranche_indices)
+ tk_fail(tk, "the tranche lists no formats, so it offers the client "
+ "nothing to allocate");
+
+ /* The table, and that the tranche indexes into it. */
+ if (v4.table_fds != 1)
+ tk_fail(tk, "expected one format_table, got %d", v4.table_fds);
+ if (!v4.table_size || (v4.table_size % sizeof(Table_Entry)))
+ tk_fail(tk, "format table is %u bytes, not a multiple of %zu",
+ v4.table_size, sizeof(Table_Entry));
+ if (v4.tranche_indices > (int)(v4.table_size / sizeof(Table_Entry)))
+ tk_fail(tk, "the tranche names %d formats but the table only holds %zu",
+ v4.tranche_indices, v4.table_size / sizeof(Table_Entry));
+
+ /* Map it read-only, which is what the protocol tells clients to do, and
+ * confirm it is really readable that way - a table sent without the right
+ * seals would fail here rather than in some client months later. */
+ table = mmap(NULL, v4.table_size, PROT_READ, MAP_PRIVATE, v4.table_fd, 0);
+ if (table == MAP_FAILED)
+ tk_fail(tk, "could not map the format table read-only");
+ if (!table[0].format)
+ tk_fail(tk, "the first table entry has format 0");
+ if (table[0].padding)
+ tk_fail(tk, "the first table entry has padding %u, expected 0",
+ table[0].padding);
+ printf(PROG ": format table holds %zu entries, first is 0x%08x\n",
+ v4.table_size / sizeof(Table_Entry), table[0].format);
+ munmap(table, v4.table_size);
+ close(v4.table_fd);
+
+ /* Per-surface feedback exists and answers. E composites everything, so the
+ * answer is the same as the default one - what matters is that asking works
+ * at all, since a client that asks and hears nothing waits forever. */
+ {
+ Seen surf = { "v4-surface", 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0 };
+ Tk_Toplevel *top = tk_toplevel_new(tk, "dmabuf-feedback", "dmabuf", 200, 150);
+ struct zwp_linux_dmabuf_feedback_v1 *sfb;
+
+ (void)top;
+ sfb = zwp_linux_dmabuf_v1_get_surface_feedback(d4, tk_toplevel_surface(top));
+ if (!sfb) tk_fail(tk, "get_surface_feedback returned nothing");
+ zwp_linux_dmabuf_feedback_v1_add_listener(sfb, &_fb_listener, &surf);
+ tk_sync(tk);
+
+ if (!surf.done)
+ tk_fail(tk, "surface feedback never sent done");
+ if (surf.device != v4.device)
+ tk_fail(tk, "surface feedback names a different main device than the "
+ "default one");
+ if (surf.table_fd >= 0) close(surf.table_fd);
+ printf(PROG ": surface feedback answers with the same device\n");
+ }
+
+ printf(PROG ": ok\n");
+ tk_disconnect(tk);
+ return 0;
+}
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.