From 74a43f0180f413ee9b2eeb9c8a6d85800c8f3725 Mon Sep 17 00:00:00 2001 From: Levi Neely Date: Fri, 27 Mar 2026 23:19:18 +0100 Subject: [PATCH] =?UTF-8?q?wayland:=20Phase=2038=20=E2=80=94=20per-topleve?= =?UTF-8?q?l=20view=20lifecycle=20and=20context=20store?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce struct wl_toplevel_view with map/unmap/destroy wl_signal listeners and a synthetic WNativeWindow ID (>= 0x10000000). Implement context_save/find/delete as a per-key linked list of (WNativeWindow, void*) entries — enables downstream policy code to look up views by handle. wl-smoke-test now verifies "toplevel mapped" appears in compositor stderr when weston-simple-shm creates its surface. Co-Authored-By: Claude Sonnet 4.6 --- src/backend/wayland/wl_backend.c | 183 +++++++++++++++++++++++++++---- tests/wl-smoke-test.sh | 9 +- 2 files changed, 168 insertions(+), 24 deletions(-) diff --git a/src/backend/wayland/wl_backend.c b/src/backend/wayland/wl_backend.c index 96cd0d7..48b3ebe 100644 --- a/src/backend/wayland/wl_backend.c +++ b/src/backend/wayland/wl_backend.c @@ -13,6 +13,8 @@ * Phase 36: wl_shm and wl_output advertised; XDG toplevel listener wired. * Phase 37: frame callbacks driven by output frame event; XDG configure * sent on new toplevel so clients complete their first frame. + * Phase 38: per-toplevel view struct with map/unmap/destroy lifecycle; + * synthetic WNativeWindow IDs; context_save/find/delete implemented. */ #ifdef HAVE_CONFIG_H @@ -46,6 +48,35 @@ #include "../backend.h" #include "wl_backend.h" +/* ------------------------------------------------------------------ */ +/* Phase 38: per-toplevel view and context store */ +/* ------------------------------------------------------------------ */ + +/* + * wl_toplevel_view -- one entry per XDG toplevel managed by the compositor. + * Holds the wlroots surface pointer, three lifecycle listeners, and a + * synthetic WNativeWindow ID used as the policy-layer window handle. + */ +struct wl_toplevel_view { + struct wlr_xdg_surface *xdg_surface; + struct wl_listener map_listener; + struct wl_listener unmap_listener; + struct wl_listener destroy_listener; + WNativeWindow id; /* synthetic handle, >= 0x10000000 */ + struct wl_list link; /* wl_state.toplevel_list */ +}; + +/* + * Context store: three per-key linked lists keyed by WNativeWindow. + * Backed by a flat list of (win, data) pairs — fine for the small + * number of windows a typical user has open. + */ +struct wl_ctx_entry { + WNativeWindow win; + void *data; + struct wl_list link; +}; + /* ------------------------------------------------------------------ */ /* Global compositor state */ /* ------------------------------------------------------------------ */ @@ -66,6 +97,10 @@ struct wl_compositor_state { int managed_surface_count; /* surfaces seen */ /* Phase 37: periodic timer that drives client frame callbacks */ struct wl_event_source *frame_timer; + /* Phase 38: per-toplevel view list and context store */ + struct wl_list toplevel_list; /* wl_toplevel_view.link */ + struct wl_list ctx_lists[3]; /* one per WContextKey */ + unsigned long next_synthetic_id; /* >= 0x10000000 */ char socket_name[64]; /* Phase 34: e.g. "wayland-0" */ int started; /* non-zero after successful display_open */ }; @@ -73,54 +108,113 @@ struct wl_compositor_state { static struct wl_compositor_state wl_state; /* ------------------------------------------------------------------ */ -/* Phase 36 / 37: XDG surface / toplevel management and frame loop */ +/* Phase 36 / 37 / 38: XDG surface lifecycle, frame loop, view mgmt */ /* ------------------------------------------------------------------ */ /* * Phase 37: Wayland event loop timer — drives client frame callbacks. - * For the headless backend we avoid the allocator/swapchain path entirely - * and call wlr_output_send_frame directly at ~60 fps. This causes - * wlroots to send wl_surface.frame callbacks to all waiting clients. */ static int frame_timer_cb(void *data) { (void)data; - if (wl_state.output) wlr_output_send_frame(wl_state.output); - - /* re-arm at ~60 fps */ if (wl_state.frame_timer) wl_event_source_timer_update(wl_state.frame_timer, 16); return 0; } +/* Helper: recover the view from one of its embedded listeners. */ +#define VIEW_FROM_LISTENER(ptr, field) \ + ((struct wl_toplevel_view *)((char *)(ptr) - offsetof(struct wl_toplevel_view, field))) + /* - * Phase 36: new XDG surface listener. - * Sends an initial configure to toplevels so clients can commit their - * first buffer (size 0,0 = "use whatever size you want"). + * Phase 38: toplevel map — surface is ready to be managed. + */ +static void +handle_toplevel_map(struct wl_listener *listener, void *data) +{ + struct wl_toplevel_view *view = VIEW_FROM_LISTENER(listener, map_listener); + (void)data; + + wwarning("wl_backend: toplevel mapped id=0x%lx app_id=%s title=%s", + view->id, + view->xdg_surface->toplevel->app_id ? view->xdg_surface->toplevel->app_id : "(none)", + view->xdg_surface->toplevel->title ? view->xdg_surface->toplevel->title : "(none)"); + + /* TODO Phase 39: call wManageWindow(scr, view->id) once context + * store can resolve the synthetic ID to this view. */ +} + +/* + * Phase 38: toplevel unmap — surface hidden (minimised or gone). + */ +static void +handle_toplevel_unmap(struct wl_listener *listener, void *data) +{ + struct wl_toplevel_view *view = VIEW_FROM_LISTENER(listener, unmap_listener); + (void)data; + + wwarning("wl_backend: toplevel unmapped id=0x%lx", view->id); + /* TODO Phase 39: call wUnmanageWindow */ +} + +/* + * Phase 38: toplevel destroy — free the view. + */ +static void +handle_toplevel_destroy(struct wl_listener *listener, void *data) +{ + struct wl_toplevel_view *view = VIEW_FROM_LISTENER(listener, destroy_listener); + (void)data; + + wl_list_remove(&view->map_listener.link); + wl_list_remove(&view->unmap_listener.link); + wl_list_remove(&view->destroy_listener.link); + wl_list_remove(&view->link); + wl_state.managed_surface_count--; + wfree(view); +} + +/* + * Phase 36 / 38: new XDG surface listener. + * Allocates a wl_toplevel_view, assigns a synthetic WNativeWindow ID, + * wires the three lifecycle listeners, and schedules the initial configure. */ static void handle_xdg_new_surface(struct wl_listener *listener, void *data) { - struct wlr_xdg_surface *xdg_surface = data; + struct wlr_xdg_surface *xdg_surface = data; + struct wl_toplevel_view *view; (void)listener; if (xdg_surface->role != WLR_XDG_SURFACE_ROLE_TOPLEVEL) return; /* ignore popups for now */ + view = wmalloc(sizeof(*view)); + memset(view, 0, sizeof(*view)); + view->xdg_surface = xdg_surface; + view->id = wl_state.next_synthetic_id++; + + view->map_listener.notify = handle_toplevel_map; + view->unmap_listener.notify = handle_toplevel_unmap; + view->destroy_listener.notify = handle_toplevel_destroy; + + wl_signal_add(&xdg_surface->events.map, &view->map_listener); + wl_signal_add(&xdg_surface->events.unmap, &view->unmap_listener); + wl_signal_add(&xdg_surface->events.destroy, &view->destroy_listener); + + wl_list_insert(&wl_state.toplevel_list, &view->link); wl_state.managed_surface_count++; - wwarning("wl_backend: new XDG toplevel #%d (app_id=%s title=%s)", - wl_state.managed_surface_count, - xdg_surface->toplevel->app_id ? xdg_surface->toplevel->app_id : "(none)", - xdg_surface->toplevel->title ? xdg_surface->toplevel->title : "(none)"); - /* Phase 37: send the initial configure so the client can commit. */ + wwarning("wl_backend: new XDG toplevel id=0x%lx app_id=%s title=%s", + view->id, + xdg_surface->toplevel->app_id ? xdg_surface->toplevel->app_id : "(none)", + xdg_surface->toplevel->title ? xdg_surface->toplevel->title : "(none)"); + + /* Phase 37: initial configure so the client can commit its first buffer. */ wlr_xdg_surface_schedule_configure(xdg_surface); - - /* TODO: create WWindow and call policy layer (wManageWindow equivalent) - * once the surface is committed (use map event on toplevel). */ } /* ------------------------------------------------------------------ */ @@ -209,6 +303,16 @@ wl_display_open(const char *name) if (wl_state.output) wlr_output_create_global(wl_state.output); + /* Phase 38: initialise per-toplevel tracking and context store. */ + wl_list_init(&wl_state.toplevel_list); + { + int ki; + for (ki = 0; ki < 3; ki++) + wl_list_init(&wl_state.ctx_lists[ki]); + } + wl_state.next_synthetic_id = 0x10000000UL; + wl_state.managed_surface_count = 0; + /* Phase 36: listen for new XDG toplevel surfaces. */ wl_state.xdg_new_surface_listener.notify = handle_xdg_new_surface; wl_signal_add(&wl_state.xdg_shell->events.new_surface, @@ -336,20 +440,53 @@ wl_frame_configure(WNativeWindow win, int x, int y, int width, int height) { (void)win; (void)x; (void)y; (void)width; (void)height; } /* ------------------------------------------------------------------ */ -/* Context (window → object lookup table) */ +/* Context (window → object lookup table) — Phase 38 */ /* ------------------------------------------------------------------ */ static void wl_context_save(WNativeWindow win, WContextKey key, void *data) -{ (void)win; (void)key; (void)data; } +{ + struct wl_list *list = &wl_state.ctx_lists[(int)key]; + struct wl_ctx_entry *e; + + /* update existing entry if present */ + wl_list_for_each(e, list, link) { + if (e->win == win) { e->data = data; return; } + } + /* new entry */ + e = wmalloc(sizeof(*e)); + e->win = win; + e->data = data; + wl_list_insert(list, &e->link); +} static void * wl_context_find(WNativeWindow win, WContextKey key) -{ (void)win; (void)key; return NULL; } +{ + struct wl_list *list = &wl_state.ctx_lists[(int)key]; + struct wl_ctx_entry *e; + + wl_list_for_each(e, list, link) { + if (e->win == win) return e->data; + } + return NULL; +} static int wl_context_delete(WNativeWindow win, WContextKey key) -{ (void)win; (void)key; return 0; } +{ + struct wl_list *list = &wl_state.ctx_lists[(int)key]; + struct wl_ctx_entry *e, *tmp; + + wl_list_for_each_safe(e, tmp, list, link) { + if (e->win == win) { + wl_list_remove(&e->link); + wfree(e); + return 1; + } + } + return 0; /* not found */ +} /* ------------------------------------------------------------------ */ /* Focus */ diff --git a/tests/wl-smoke-test.sh b/tests/wl-smoke-test.sh index 0291206..11c8f37 100755 --- a/tests/wl-smoke-test.sh +++ b/tests/wl-smoke-test.sh @@ -15,7 +15,9 @@ # 6. (Phase 36) wl_shm and wl_output advertised; a real SHM client # (weston-simple-shm) can connect without crashing the compositor # 7. (Phase 37) Frame callbacks fire; weston-simple-shm runs its render -# loop (stays alive for 1 s, compositor still alive afterwards) +# loop (stays alive for 3 s, compositor still alive afterwards) +# 8. (Phase 38) Per-toplevel view lifecycle: compositor logs "toplevel +# mapped" when weston-simple-shm creates its XDG toplevel surface # # This test grows as wl_backend vtable slots are implemented. # @@ -141,6 +143,11 @@ if command -v weston-simple-shm >/dev/null 2>&1; then kill -0 "$WM_PID" 2>/dev/null \ || die "compositor crashed after weston-simple-shm ran" echo " client check: weston-simple-shm ran 3 s; compositor survived" + + # -- 8. Phase 38: compositor must have logged "toplevel mapped" ------- + grep -q 'toplevel mapped' /tmp/wl-smoke-stderr \ + || die "compositor did not log 'toplevel mapped' for weston-simple-shm" + echo " lifecycle check: toplevel mapped event logged" else echo " client check: skipped (weston-simple-shm not installed)" fi