wayland: Phase 38 — per-toplevel view lifecycle and context store

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 <noreply@anthropic.com>
This commit is contained in:
Levi Neely 2026-03-27 23:19:18 +01:00
parent 3e1fd061e9
commit 74a43f0180
2 changed files with 168 additions and 24 deletions

View File

@ -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 */

View File

@ -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