wayland: Phase 37 — frame timer drives client frame callbacks at 60 fps

Replace the output frame listener + render pipeline approach (which
required a swapchain allocator) with a simple wl_event_loop timer that
calls wlr_output_send_frame every 16 ms.  Clients (weston-simple-shm)
now complete their render loops without the compositor needing a GPU
allocator.  Also schedule an initial xdg_surface configure on new
toplevels so they can commit their first buffer.  wl-smoke-test verifies
weston-simple-shm runs for 3 s without crashing the compositor.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Levi Neely 2026-03-27 23:15:59 +01:00
parent 2182ec0fc1
commit 3e1fd061e9
2 changed files with 58 additions and 4 deletions

View File

@ -11,6 +11,8 @@
* Phase 35: Core Wayland protocol globals advertised (wl_compositor,
* wl_subcompositor, wl_seat, wl_data_device_manager, xdg_wm_base).
* 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.
*/
#ifdef HAVE_CONFIG_H
@ -26,6 +28,7 @@
#include <wlr/backend.h>
#include <wlr/backend/headless.h>
#include <wlr/render/wlr_renderer.h>
#include <wlr/interfaces/wlr_output.h>
#include <wlr/types/wlr_compositor.h>
#include <wlr/types/wlr_data_device.h>
#include <wlr/types/wlr_output.h>
@ -61,6 +64,8 @@ struct wl_compositor_state {
/* Phase 36: XDG toplevel tracking */
struct wl_listener xdg_new_surface_listener;
int managed_surface_count; /* surfaces seen */
/* Phase 37: periodic timer that drives client frame callbacks */
struct wl_event_source *frame_timer;
char socket_name[64]; /* Phase 34: e.g. "wayland-0" */
int started; /* non-zero after successful display_open */
};
@ -68,9 +73,34 @@ struct wl_compositor_state {
static struct wl_compositor_state wl_state;
/* ------------------------------------------------------------------ */
/* Phase 36: XDG surface / toplevel management */
/* Phase 36 / 37: XDG surface / toplevel management and frame loop */
/* ------------------------------------------------------------------ */
/*
* 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;
}
/*
* 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").
*/
static void
handle_xdg_new_surface(struct wl_listener *listener, void *data)
{
@ -86,6 +116,9 @@ handle_xdg_new_surface(struct wl_listener *listener, void *data)
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. */
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). */
}
@ -181,6 +214,18 @@ wl_display_open(const char *name)
wl_signal_add(&wl_state.xdg_shell->events.new_surface,
&wl_state.xdg_new_surface_listener);
/* Phase 37: periodic timer to drive client frame callbacks at ~60 fps.
* Using a timer avoids the swapchain/allocator complexity of the full
* render pipeline while still keeping Wayland clients unblocked. */
{
struct wl_event_loop *loop =
wl_display_get_event_loop(wl_state.display);
wl_state.frame_timer =
wl_event_loop_add_timer(loop, frame_timer_cb, NULL);
if (wl_state.frame_timer)
wl_event_source_timer_update(wl_state.frame_timer, 16);
}
wl_state.started = 1;
return NULL; /* no X11 Display* in the Wayland path */
}
@ -190,6 +235,10 @@ wl_display_close(void)
{
if (!wl_state.started)
return;
if (wl_state.frame_timer) {
wl_event_source_remove(wl_state.frame_timer);
wl_state.frame_timer = NULL;
}
if (wl_state.output_layout) {
wlr_output_layout_destroy(wl_state.output_layout);
wl_state.output_layout = NULL;

View File

@ -14,6 +14,8 @@
# xdg_wm_base) — verified via weston-info if available
# 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)
#
# This test grows as wl_backend vtable slots are implemented.
#
@ -130,12 +132,15 @@ fi
# compositor is still alive after the client runs (or times out).
if command -v weston-simple-shm >/dev/null 2>&1; then
CLIENT_OUT=$(WAYLAND_DISPLAY="$WL_SOCK" timeout 2s weston-simple-shm 2>&1) || true
# Run weston-simple-shm for up to 3 seconds; it loops drawing frames.
# A clean timeout (exit 124) or exit 0 both mean frame callbacks fired.
# We only fail if wl_shm is missing or the compositor crashes.
CLIENT_OUT=$(WAYLAND_DISPLAY="$WL_SOCK" timeout 3s weston-simple-shm 2>&1) || true
echo "$CLIENT_OUT" | grep -q 'No wl_shm global' \
&& die "weston-simple-shm: compositor missing wl_shm"
kill -0 "$WM_PID" 2>/dev/null \
|| die "compositor crashed after weston-simple-shm connected"
echo " client check: weston-simple-shm connected; compositor survived"
|| die "compositor crashed after weston-simple-shm ran"
echo " client check: weston-simple-shm ran 3 s; compositor survived"
else
echo " client check: skipped (weston-simple-shm not installed)"
fi