wings(wayland-client): wl_shm buffers + xdg_toplevel surfaces

Tasks 3 + 4.

wclient_wl.c grows a memfd+mmap-backed wl_shm buffer allocator that
wraps the mapped region as a pixman_image_t.  Painting into the pixman
image writes directly into the buffer the compositor will read on
commit, so no copy is needed.  wl_buffer.release marks the buffer as
released for future reuse (currently we just realloc on resize, so the
flag is informational).

Each top-level W_View now acquires a WLClientToplevel with a wl_surface,
xdg_surface, and xdg_toplevel.  The first xdg_surface.configure event
acks and attaches the buffer; subsequent configures with non-zero
dimensions realloc the backing.  xdg_toplevel.close sets a want_close
flag (closure event synthesis is in the next pass).

wview_wl.c now calls three function-pointer hooks for top-level views:

  W_WLClientToplevelRealizeFn(view, w, h)  -> pixman_image_t *
  W_WLClientToplevelResizeFn(view, w, h)
  W_WLClientToplevelDestroyFn(view)

When the hooks are NULL (compositor-internal mode or X11), wview_wl.c
falls back to plain pixman_image_create_bits — preserving every
existing behaviour.  When wclient_wl.c installs them, the same paints
end up on a wl_shm buffer attached to a real Wayland surface.

W_ViewCommitBackingFn and W_ViewDestroyBackingFn are also installed by
W_WLClientInit so wview_wl.c's existing redisplay/destroy plumbing
drives wl_surface_attach / damage / commit / surface destruction.

No input handling yet; that's task 5.
This commit is contained in:
Window Maker 2026-05-24 10:25:29 +02:00
parent 12ae1e48fb
commit 97b2d03a34
3 changed files with 455 additions and 10 deletions

View File

@ -509,6 +509,15 @@ void W_WL_SetLoopDispatch(struct W_Screen *scr,
Bool W_WLClientInit(struct W_Screen *scrPtr, const char *display);
void W_WLClientShutdown(struct W_Screen *scrPtr);
/* Hooks set by wclient_wl.c (Wayland-client mode) so that wview_wl.c
* routes top-level view backing creation/resize/destroy through the
* libwayland-client surface management. When NULL (compositor mode
* or X11), the default pixman allocation path runs. */
struct W_View;
extern void *(*W_WLClientToplevelRealizeFn)(struct W_View *view, int width, int height);
extern void (*W_WLClientToplevelResizeFn)(struct W_View *view, int width, int height);
extern void (*W_WLClientToplevelDestroyFn)(struct W_View *view);
/* ---[ wfont.c ]--------------------------------------------------------- */

View File

@ -34,6 +34,8 @@
#include <xkbcommon/xkbcommon.h>
#include <pixman.h>
#include <errno.h>
#include <fcntl.h>
#include <poll.h>
@ -67,6 +69,409 @@ typedef struct WLClientCtx {
struct W_Screen *scr;
} WLClientCtx;
/* Storage for the active context — for now we only support one screen. */
static WLClientCtx *g_wlclient_ctx = NULL;
/* Forward decl — defined in wview_wl.c */
extern void W_SetViewBacking(Window id, void *img);
/* ------------------------------------------------------------------ */
/* wl_shm-backed buffer allocator */
/* */
/* A WLClientShmBuffer pairs a wl_buffer with the pixman_image_t that */
/* shares the same memfd-mapped region. Painting into the pixman */
/* image touches the same memory the compositor will read on commit, */
/* so no copy is needed. wl_buffer.release marks the buffer as */
/* available for reuse — the surface code may then double-buffer if */
/* it wants, or re-attach the same buffer. */
/* ------------------------------------------------------------------ */
typedef struct WLClientShmBuffer {
struct wl_buffer *wl_buf;
pixman_image_t *image;
void *map;
size_t map_size;
int width;
int height;
int stride; /* bytes per row */
int busy; /* 1 = still owned by compositor */
} WLClientShmBuffer;
static void
shm_buffer_release(void *data, struct wl_buffer *wl_buf)
{
(void)wl_buf;
WLClientShmBuffer *b = data;
if (b) b->busy = 0;
}
static const struct wl_buffer_listener shm_buffer_listener = {
.release = shm_buffer_release,
};
/*
* Create an anonymous, sealed memfd of the given size. We use memfd_create
* with MFD_CLOEXEC because shm_open + unlink is slower and the resulting
* file lingers in /dev/shm. This requires Linux >= 3.17, kernel default
* everywhere we care about.
*/
static int
shm_create_memfd(size_t size)
{
int fd = memfd_create("wings-wl-shm", MFD_CLOEXEC);
if (fd < 0) return -1;
if (ftruncate(fd, size) < 0) {
int saved = errno;
close(fd);
errno = saved;
return -1;
}
return fd;
}
static WLClientShmBuffer *
wlclient_create_shm_buffer(WLClientCtx *ctx, int width, int height)
{
if (!ctx || !ctx->shm || width <= 0 || height <= 0) return NULL;
int stride = width * 4; /* ARGB32, 4 bytes/pixel */
size_t size = (size_t)stride * (size_t)height;
int fd = shm_create_memfd(size);
if (fd < 0) {
wwarning(_("WINGs: memfd_create failed: %s"), strerror(errno));
return NULL;
}
void *map = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (map == MAP_FAILED) {
wwarning(_("WINGs: mmap of shm pool failed: %s"), strerror(errno));
close(fd);
return NULL;
}
struct wl_shm_pool *pool = wl_shm_create_pool(ctx->shm, fd, size);
if (!pool) {
munmap(map, size);
close(fd);
return NULL;
}
/* WL_SHM_FORMAT_ARGB8888 matches PIXMAN_a8r8g8b8 byte-for-byte on
* little-endian. WINGs is little-endian-only in practice. */
struct wl_buffer *wl_buf = wl_shm_pool_create_buffer(pool, 0,
width, height, stride, WL_SHM_FORMAT_ARGB8888);
/* Pool can be destroyed immediately after creating the buffer; the
* compositor keeps the fd mapping alive on its side. */
wl_shm_pool_destroy(pool);
/* The fd is duplicated by the compositor; we close ours. */
close(fd);
if (!wl_buf) {
munmap(map, size);
return NULL;
}
WLClientShmBuffer *b = wmalloc(sizeof(*b));
memset(b, 0, sizeof(*b));
b->wl_buf = wl_buf;
b->map = map;
b->map_size = size;
b->width = width;
b->height = height;
b->stride = stride;
b->image = pixman_image_create_bits(PIXMAN_a8r8g8b8, width, height,
(uint32_t *)map, stride);
if (!b->image) {
wl_buffer_destroy(wl_buf);
munmap(map, size);
wfree(b);
return NULL;
}
wl_buffer_add_listener(wl_buf, &shm_buffer_listener, b);
return b;
}
static void
wlclient_destroy_shm_buffer(WLClientShmBuffer *b)
{
if (!b) return;
if (b->image) pixman_image_unref(b->image);
if (b->wl_buf) wl_buffer_destroy(b->wl_buf);
if (b->map && b->map != MAP_FAILED) munmap(b->map, b->map_size);
wfree(b);
}
/* ------------------------------------------------------------------ */
/* Per-toplevel state */
/* */
/* One WLClientToplevel per top-level W_View. Holds the wl_surface, */
/* xdg_surface, xdg_toplevel, and a single shm-backed buffer that */
/* doubles as the view's pixman backing image. */
/* ------------------------------------------------------------------ */
typedef struct WLClientToplevel {
WLClientCtx *ctx;
struct W_View *view;
struct wl_surface *surface;
struct xdg_surface *xdg_surface;
struct xdg_toplevel *xdg_toplevel;
WLClientShmBuffer *buf; /* current backing */
int pending_w;
int pending_h;
int configured; /* received first xdg_surface.configure */
int mapped; /* surface has at least one buffer */
int want_close;
} WLClientToplevel;
#define WLC_TOPTABLE_SIZE 128
static struct {
Window id;
WLClientToplevel *top;
} wlc_toptable[WLC_TOPTABLE_SIZE];
static void
wlc_top_put(Window id, WLClientToplevel *top)
{
unsigned idx = ((unsigned)id) % WLC_TOPTABLE_SIZE;
for (int i = 0; i < WLC_TOPTABLE_SIZE; i++) {
unsigned slot = (idx + i) % WLC_TOPTABLE_SIZE;
if (wlc_toptable[slot].id == 0 || wlc_toptable[slot].id == id) {
wlc_toptable[slot].id = id;
wlc_toptable[slot].top = top;
return;
}
}
}
static WLClientToplevel *
wlc_top_find(Window id)
{
unsigned idx = ((unsigned)id) % WLC_TOPTABLE_SIZE;
for (int i = 0; i < WLC_TOPTABLE_SIZE; i++) {
unsigned slot = (idx + i) % WLC_TOPTABLE_SIZE;
if (wlc_toptable[slot].id == id) return wlc_toptable[slot].top;
if (wlc_toptable[slot].id == 0) return NULL;
}
return NULL;
}
static void
wlc_top_remove(Window id)
{
unsigned idx = ((unsigned)id) % WLC_TOPTABLE_SIZE;
for (int i = 0; i < WLC_TOPTABLE_SIZE; i++) {
unsigned slot = (idx + i) % WLC_TOPTABLE_SIZE;
if (wlc_toptable[slot].id == id) {
wlc_toptable[slot].id = 0;
wlc_toptable[slot].top = NULL;
return;
}
if (wlc_toptable[slot].id == 0) return;
}
}
/* Forward declarations */
static void wl_client_toplevel_commit_locked(WLClientToplevel *top);
/* ------------------------------------------------------------------ */
/* xdg_surface listener */
/* ------------------------------------------------------------------ */
static void
xdg_surface_handle_configure(void *data, struct xdg_surface *xs, uint32_t serial)
{
WLClientToplevel *top = data;
xdg_surface_ack_configure(xs, serial);
top->configured = 1;
/* If the compositor configured a new size, resize our backing.
* (Pending size of 0 means "you decide" — keep current.) */
if (top->pending_w > 0 && top->pending_h > 0 &&
(top->pending_w != top->buf->width ||
top->pending_h != top->buf->height)) {
WLClientShmBuffer *nb =
wlclient_create_shm_buffer(top->ctx, top->pending_w, top->pending_h);
if (nb) {
wlclient_destroy_shm_buffer(top->buf);
top->buf = nb;
/* Tell WINGs the backing pointer changed so subsequent
* draws hit the new buffer. */
W_SetViewBacking(top->view->window, nb->image);
top->view->size.width = top->pending_w;
top->view->size.height = top->pending_h;
}
}
/* On the very first configure, attach the buffer and commit so
* the surface becomes mapped. */
wl_client_toplevel_commit_locked(top);
}
static const struct xdg_surface_listener xdg_surface_listener = {
.configure = xdg_surface_handle_configure,
};
/* ------------------------------------------------------------------ */
/* xdg_toplevel listener */
/* ------------------------------------------------------------------ */
static void
xdg_toplevel_handle_configure(void *data, struct xdg_toplevel *xt,
int32_t width, int32_t height,
struct wl_array *states)
{
(void)xt; (void)states;
WLClientToplevel *top = data;
/* Width/height of 0 means the compositor leaves sizing to us. */
top->pending_w = width > 0 ? width : top->buf ? top->buf->width : top->view->size.width;
top->pending_h = height > 0 ? height : top->buf ? top->buf->height : top->view->size.height;
}
static void
xdg_toplevel_handle_close(void *data, struct xdg_toplevel *xt)
{
(void)xt;
WLClientToplevel *top = data;
top->want_close = 1;
/* TODO: synthesize a WMEvent close so the WINGs app can shut down
* gracefully. For now we just exit on next event-loop turn. */
}
static void
xdg_toplevel_handle_configure_bounds(void *data, struct xdg_toplevel *xt,
int32_t width, int32_t height)
{
(void)data; (void)xt; (void)width; (void)height;
}
static void
xdg_toplevel_handle_wm_capabilities(void *data, struct xdg_toplevel *xt,
struct wl_array *capabilities)
{
(void)data; (void)xt; (void)capabilities;
}
static const struct xdg_toplevel_listener xdg_toplevel_listener = {
.configure = xdg_toplevel_handle_configure,
.close = xdg_toplevel_handle_close,
.configure_bounds = xdg_toplevel_handle_configure_bounds,
.wm_capabilities = xdg_toplevel_handle_wm_capabilities,
};
/* ------------------------------------------------------------------ */
/* Commit / realize / resize / destroy */
/* ------------------------------------------------------------------ */
static void
wl_client_toplevel_commit_locked(WLClientToplevel *top)
{
if (!top || !top->configured || !top->buf || !top->buf->wl_buf)
return;
wl_surface_attach(top->surface, top->buf->wl_buf, 0, 0);
wl_surface_damage_buffer(top->surface, 0, 0,
top->buf->width, top->buf->height);
wl_surface_commit(top->surface);
top->buf->busy = 1;
top->mapped = 1;
}
static void
wl_client_view_commit(unsigned long id)
{
WLClientToplevel *top = wlc_top_find((Window)id);
if (top) wl_client_toplevel_commit_locked(top);
}
static void
wl_client_view_destroy_backing(unsigned long id)
{
WLClientToplevel *top = wlc_top_find((Window)id);
if (!top) return;
if (top->xdg_toplevel) xdg_toplevel_destroy(top->xdg_toplevel);
if (top->xdg_surface) xdg_surface_destroy(top->xdg_surface);
if (top->surface) wl_surface_destroy(top->surface);
if (top->buf) wlclient_destroy_shm_buffer(top->buf);
wlc_top_remove((Window)id);
wfree(top);
}
static void *
wl_client_toplevel_realize(struct W_View *view, int width, int height)
{
WLClientCtx *ctx = g_wlclient_ctx;
if (!ctx || !ctx->compositor || !ctx->wm_base) return NULL;
WLClientToplevel *top = wmalloc(sizeof(*top));
memset(top, 0, sizeof(*top));
top->ctx = ctx;
top->view = view;
top->pending_w = width;
top->pending_h = height;
top->buf = wlclient_create_shm_buffer(ctx, width, height);
if (!top->buf) goto fail;
top->surface = wl_compositor_create_surface(ctx->compositor);
if (!top->surface) goto fail;
top->xdg_surface = xdg_wm_base_get_xdg_surface(ctx->wm_base, top->surface);
if (!top->xdg_surface) goto fail;
xdg_surface_add_listener(top->xdg_surface, &xdg_surface_listener, top);
top->xdg_toplevel = xdg_surface_get_toplevel(top->xdg_surface);
if (!top->xdg_toplevel) goto fail;
xdg_toplevel_add_listener(top->xdg_toplevel, &xdg_toplevel_listener, top);
/* Default app_id; the application can override via xdg_toplevel_set_app_id.
* WPrefs and friends ought to call WMSetApplicationIconImage / set_title;
* we'll plumb that next. */
xdg_toplevel_set_app_id(top->xdg_toplevel, "WINGs");
xdg_toplevel_set_title(top->xdg_toplevel, "WINGs Application");
/* Initial commit with no buffer so the compositor sends the first
* configure. We attach a real buffer in the configure handler. */
wl_surface_commit(top->surface);
/* view->window may not be assigned yet at this point — wl_realize sets
* it AFTER allocating the backing. We register the toplevel under
* view->window here using a deferred-association: the caller sets the
* window field first. Actually wview_wl.c assigns view->window BEFORE
* calling W_WLClientToplevelRealizeFn, so we can use it now. */
wlc_top_put((Window)view->window, top);
return top->buf->image;
fail:
if (top->xdg_toplevel) xdg_toplevel_destroy(top->xdg_toplevel);
if (top->xdg_surface) xdg_surface_destroy(top->xdg_surface);
if (top->surface) wl_surface_destroy(top->surface);
if (top->buf) wlclient_destroy_shm_buffer(top->buf);
wfree(top);
return NULL;
}
static void
wl_client_toplevel_resize(struct W_View *view, int width, int height)
{
WLClientToplevel *top = wlc_top_find((Window)view->window);
if (!top) return;
if (top->buf && top->buf->width == width && top->buf->height == height)
return;
WLClientShmBuffer *nb = wlclient_create_shm_buffer(top->ctx, width, height);
if (!nb) return;
if (top->buf) wlclient_destroy_shm_buffer(top->buf);
top->buf = nb;
W_SetViewBacking(view->window, nb->image);
}
static void
wl_client_toplevel_destroy(struct W_View *view)
{
wl_client_view_destroy_backing((unsigned long)view->window);
}
/* ------------------------------------------------------------------ */
/* xdg_wm_base ping */
/* ------------------------------------------------------------------ */
@ -171,9 +576,6 @@ wlclient_dispatch(int timeout_ms, void *userdata)
/* Public API */
/* ------------------------------------------------------------------ */
/* Storage for the active context — for now we only support one screen. */
static WLClientCtx *g_wlclient_ctx = NULL;
Bool
W_WLClientInit(struct W_Screen *scrPtr, const char *display)
{
@ -212,6 +614,19 @@ W_WLClientInit(struct W_Screen *scrPtr, const char *display)
wlclient_dispatch,
ctx);
/* Wire surface lifecycle into wview_wl.c: top-level views will
* acquire wl_surface + xdg_toplevel on realize, attach + commit on
* paint, and tear down on destroy. */
W_WLClientToplevelRealizeFn = wl_client_toplevel_realize;
W_WLClientToplevelResizeFn = wl_client_toplevel_resize;
W_WLClientToplevelDestroyFn = wl_client_toplevel_destroy;
{
extern void (*W_ViewCommitBackingFn)(unsigned long id);
extern void (*W_ViewDestroyBackingFn)(unsigned long id);
W_ViewCommitBackingFn = wl_client_view_commit;
W_ViewDestroyBackingFn = wl_client_view_destroy_backing;
}
g_wlclient_ctx = ctx;
return True;

View File

@ -152,6 +152,14 @@ static void wl_create_root(void *vctx, W_View *view)
view->size.height = 1080;
}
/* Optional hook set by wclient_wl.c (Wayland-client mode) so that
* top-level views allocate their backing as wl_shm-mapped pixman images
* paired with a wl_surface + xdg_toplevel. When NULL (compositor mode
* or X11), wl_realize falls back to a free-floating pixman image. */
void *(*W_WLClientToplevelRealizeFn)(W_View *view, int width, int height) = NULL;
void (*W_WLClientToplevelResizeFn)(W_View *view, int width, int height) = NULL;
void (*W_WLClientToplevelDestroyFn)(W_View *view) = NULL;
static void wl_realize(void *vctx, W_View *view)
{
(void)vctx;
@ -163,7 +171,11 @@ static void wl_realize(void *vctx, W_View *view)
/* Allocate a backing pixman image for this view */
int w = view->size.width > 0 ? view->size.width : 1;
int h = view->size.height > 0 ? view->size.height : 1;
pixman_image_t *img = pixman_image_create_bits(PIXMAN_a8r8g8b8, w, h, NULL, 0);
pixman_image_t *img = NULL;
if (view->flags.topLevel && W_WLClientToplevelRealizeFn)
img = (pixman_image_t *)W_WLClientToplevelRealizeFn(view, w, h);
if (!img)
img = pixman_image_create_bits(PIXMAN_a8r8g8b8, w, h, NULL, 0);
W_SetViewBacking(view->window, img);
}
@ -310,14 +322,21 @@ static void wl_resize(void *vctx, W_View *view, unsigned int w, unsigned int h)
view->size.width = w;
view->size.height = h;
/* Reallocate backing pixman buffer */
pixman_image_t *old = (pixman_image_t *)W_GetViewBacking(view->window);
if (old)
pixman_image_unref(old);
int bw = w > 0 ? (int)w : 1;
int bh = h > 0 ? (int)h : 1;
pixman_image_t *img = pixman_image_create_bits(PIXMAN_a8r8g8b8, bw, bh, NULL, 0);
W_SetViewBacking(view->window, img);
if (view->flags.topLevel && W_WLClientToplevelResizeFn) {
/* Wayland-client mode: backend swaps to a freshly allocated
* shm-backed pixman image and updates the surface. */
W_WLClientToplevelResizeFn(view, bw, bh);
} else {
/* Default path: free old image, allocate a new one. */
pixman_image_t *old = (pixman_image_t *)W_GetViewBacking(view->window);
if (old)
pixman_image_unref(old);
pixman_image_t *img = pixman_image_create_bits(PIXMAN_a8r8g8b8, bw, bh, NULL, 0);
W_SetViewBacking(view->window, img);
}
if (view->flags.mapped)
wl_redisplay(vctx, view);
@ -358,6 +377,8 @@ static void wl_destroy(void *vctx, W_View *view)
{
(void)vctx;
W_WL_InvalidateView(view->screen, view);
if (view->flags.topLevel && W_WLClientToplevelDestroyFn)
W_WLClientToplevelDestroyFn(view);
if (view->flags.topLevel && W_ViewDestroyBackingFn)
W_ViewDestroyBackingFn((unsigned long)view->window);
}