From 97b2d03a345f17450b84500d48f168117be134b5 Mon Sep 17 00:00:00 2001 From: Window Maker Date: Sun, 24 May 2026 10:25:29 +0200 Subject: [PATCH] wings(wayland-client): wl_shm buffers + xdg_toplevel surfaces MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- WINGs/WINGs/WINGsP.h.in | 9 + WINGs/wclient_wl.c | 421 +++++++++++++++++++++++++++++++++++++++- WINGs/wview_wl.c | 35 +++- 3 files changed, 455 insertions(+), 10 deletions(-) diff --git a/WINGs/WINGs/WINGsP.h.in b/WINGs/WINGs/WINGsP.h.in index 8ce9f8c..12f6149 100644 --- a/WINGs/WINGs/WINGsP.h.in +++ b/WINGs/WINGs/WINGsP.h.in @@ -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 ]--------------------------------------------------------- */ diff --git a/WINGs/wclient_wl.c b/WINGs/wclient_wl.c index 8f8749a..8f5a7ae 100644 --- a/WINGs/wclient_wl.c +++ b/WINGs/wclient_wl.c @@ -34,6 +34,8 @@ #include +#include + #include #include #include @@ -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; diff --git a/WINGs/wview_wl.c b/WINGs/wview_wl.c index ec6cd13..8e1aa32 100644 --- a/WINGs/wview_wl.c +++ b/WINGs/wview_wl.c @@ -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); }