From 563b23a33726289387193b85d90fb569631ee05f Mon Sep 17 00:00:00 2001 From: Berke Güzel Date: Tue, 13 Oct 2026 23:52:22 +0300 Subject: Reconnect to a restarted Wayland compositor Windows now outlive the compositor. On connection loss YASL sends DISPLAY_LOST instead of closing every window, retries the connection on a loop timer, rebuilds each toplevel with its title, size, state and GL context, and sends DISPLAY_RESTORED. CPU windows keep their pixels; GPU renderers drop Vulkan surfaces and GL objects on loss and rebuild them on restore. - Free the dead connection's objects on the next wait, after the application has released GPU objects built on them. - Report a lost connection as an event rather than a failed wait. - Refuse window and Vulkan surface creation while offline with the new YASL_ERROR_DISCONNECTED. - Close popups, cancel drags and fail pending clipboard requests on loss. - Print only protocol errors, not ordinary disconnects. - Examples survive a restart; game.c rebinds its GL context. - Add an end-to-end test that kills Weston under CPU, GL and Vulkan windows and requires them to recover. Co-Authored-By: Claude Opus 5.5 --- docs/getting-started.md | 9 +- examples/game.c | 17 +- examples/notepad.c | 4 - examples/simple_window.c | 2 +- examples/tablet.c | 2 +- include/yasl/yasl.h | 10 +- src/common/backend.h | 1 + src/common/surface.c | 4 +- src/common/types.h | 3 +- src/common/vulkan.c | 1 + src/platform/wayland/backend.c | 82 ++++++---- src/platform/wayland/buffer.c | 8 +- src/platform/wayland/internal.h | 15 ++ src/platform/wayland/pointer.c | 2 +- src/platform/wayland/surface.c | 347 ++++++++++++++++++++++++++-------------- src/platform/x11/window.c | 1 + tests/helpers/run_reconnect.py | 67 ++++++++ tests/meson.build | 5 + tests/wayland/test_reconnect.c | 118 ++++++++++++++ 19 files changed, 526 insertions(+), 172 deletions(-) create mode 100644 tests/helpers/run_reconnect.py create mode 100644 tests/wayland/test_reconnect.c diff --git a/docs/getting-started.md b/docs/getting-started.md index bb7f095..b82dae5 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -48,8 +48,13 @@ there is no public display/context object. Closing the final window does not implicitly terminate the application or reset configuration. `yasl_shutdown` releases shared resources and restores defaults. -[hello.c](../examples/hello.c) draws into a CPU surface's pixel buffer on every REFRESH. [game.c](../examples/game.c) creates a GL context, binds it, -loads procedures, draws, and swaps. [tablet.c](../examples/tablet.c) retains a canvas +If the Wayland compositor restarts, windows stay open: YASL sends DISPLAY_LOST, +reconnects, and sends DISPLAY_RESTORED. CPU windows need nothing; GPU renderers drop +their Vulkan surfaces and GL objects on loss and rebuild them on restore. + +[hello.c](../examples/hello.c) draws into a CPU surface's pixel buffer on every REFRESH. +[game.c](../examples/game.c) creates a GL context, binds it, loads procedures, draws, +swaps, and rebinds the context after a compositor restart. [tablet.c](../examples/tablet.c) retains a canvas and draws pressure-sensitive pen/eraser strokes. [notepad.c](../examples/notepad.c) uses a worker queue, wakeups, clipboard, IME, and its own blink deadline. diff --git a/examples/game.c b/examples/game.c index 79beb7c..0be4461 100644 --- a/examples/game.c +++ b/examples/game.c @@ -29,15 +29,20 @@ int main(int argc, char **argv) } yasl_gl_swap_interval(s, 1); int result = 0, frames = 0; + bool online = true; for (bool running = true; running;) { + /* Drain events before each frame; while the display is gone, block until it returns. */ yasl_event e; - int n; - while ((n = yasl_wait_event(&e, 0)) > 0) { - if (e.type == YASL_EVENT_CLOSE || e.type == YASL_EVENT_QUIT || - (e.type == YASL_EVENT_KEY && e.key.key == YASL_KEY_ESCAPE)) running = false; - if (e.type == YASL_EVENT_DISPLAY_LOST) { result = 1; running = false; } - } + int n = yasl_wait_event(&e, online ? 0 : -1); if (n < 0) { result = 1; break; } + if (e.type == YASL_EVENT_CLOSE || e.type == YASL_EVENT_QUIT || + (e.type == YASL_EVENT_KEY && e.key.key == YASL_KEY_ESCAPE)) running = false; + if (e.type == YASL_EVENT_DISPLAY_LOST) online = false; + if (e.type == YASL_EVENT_DISPLAY_RESTORED) { + /* The context is new: bind it again and restore GL state here. */ + online = yasl_gl_make_current(s) && yasl_gl_swap_interval(s, 1); + } + if (n > 0 || !online || !running) continue; int w, h; yasl_surface_get_framebuffer_size(s, &w, &h); viewport(0, 0, w, h); diff --git a/examples/notepad.c b/examples/notepad.c index 92b9d44..9134653 100644 --- a/examples/notepad.c +++ b/examples/notepad.c @@ -1464,10 +1464,6 @@ static void on_event(struct editor *ed, const yasl_event *ev) ed->running = false; return; case YASL_EVENT_QUIT: ed->running = false; return; - case YASL_EVENT_DISPLAY_LOST: - ed->failed = true; - ed->running = false; - return; case YASL_EVENT_WINDOW: ed->surface_scale = ev->window.scale; diff --git a/examples/simple_window.c b/examples/simple_window.c index 114bfbf..cb12db1 100644 --- a/examples/simple_window.c +++ b/examples/simple_window.c @@ -14,7 +14,7 @@ int main(int argc, char **argv) yasl_event e; for (;;) { int n = yasl_wait_event(&e, smoke ? 20 : -1); - if (n < 0 || e.type == YASL_EVENT_DISPLAY_LOST) break; + if (n < 0) break; if (e.type == YASL_EVENT_CLOSE || e.type == YASL_EVENT_QUIT || (e.type == YASL_EVENT_KEY && e.key.key == YASL_KEY_ESCAPE)) { result = 0; break; } if (e.type == YASL_EVENT_POINTER) { diff --git a/examples/tablet.c b/examples/tablet.c index dad4b30..176a82b 100644 --- a/examples/tablet.c +++ b/examples/tablet.c @@ -55,7 +55,7 @@ int main(int argc, char **argv) yasl_event e; for (;;) { int n = yasl_wait_event(&e, smoke ? 20 : -1); - if (n < 0 || e.type == YASL_EVENT_DISPLAY_LOST) break; + if (n < 0) break; if (smoke && frames && n == 0) { result = 0; break; } bool escape = e.type == YASL_EVENT_KEY && e.key.action == YASL_KEY_PRESS && e.key.key == YASL_KEY_ESCAPE; if (e.type == YASL_EVENT_CLOSE || e.type == YASL_EVENT_QUIT || escape) { result = 0; break; } diff --git a/include/yasl/yasl.h b/include/yasl/yasl.h index 350339b..38c5b88 100644 --- a/include/yasl/yasl.h +++ b/include/yasl/yasl.h @@ -20,7 +20,7 @@ typedef struct yasl_event yasl_event; enum yasl_backend { YASL_BACKEND_AUTO, YASL_BACKEND_WAYLAND, YASL_BACKEND_X11, YASL_BACKEND_COCOA }; enum yasl_surface_flags { YASL_SURFACE_CPU=1u, YASL_SURFACE_HIDDEN=2u, YASL_SURFACE_UNDECORATED=4u, YASL_SURFACE_FIXED_SIZE=8u }; enum yasl_error { YASL_ERROR_NONE, YASL_ERROR_INVALID, YASL_ERROR_UNSUPPORTED, YASL_ERROR_NO_MEMORY, - YASL_ERROR_NOT_INITIALIZED, YASL_ERROR_BUSY, YASL_ERROR_WRONG_THREAD, YASL_ERROR_CONTEXT_EXPIRED, YASL_ERROR_IO }; + YASL_ERROR_NOT_INITIALIZED, YASL_ERROR_BUSY, YASL_ERROR_WRONG_THREAD, YASL_ERROR_CONTEXT_EXPIRED, YASL_ERROR_IO, YASL_ERROR_DISCONNECTED }; enum yasl_selection { YASL_CLIPBOARD, YASL_PRIMARY }; enum yasl_key_action { YASL_KEY_PRESS, YASL_KEY_RELEASE, YASL_KEY_REPEAT }; enum yasl_mod { YASL_MOD_SHIFT=1u, YASL_MOD_ALT=2u, YASL_MOD_CTRL=4u, YASL_MOD_SUPER=8u, YASL_MOD_CAPS=16u }; @@ -52,7 +52,7 @@ enum yasl_resize_edge { YASL_EDGE_TOP=1, YASL_EDGE_BOTTOM=2, YASL_EDGE_LEFT=4, Y enum yasl_event_type { YASL_EVENT_NONE, YASL_EVENT_CLOSE, YASL_EVENT_QUIT, YASL_EVENT_REOPEN, YASL_EVENT_WINDOW, YASL_EVENT_REFRESH, YASL_EVENT_KEY, YASL_EVENT_TEXT, YASL_EVENT_POINTER, YASL_EVENT_SCROLL, YASL_EVENT_PEN, YASL_EVENT_INPUT_RESET, YASL_EVENT_COMPOSITION, YASL_EVENT_TEXT_EDIT, YASL_EVENT_CLIPBOARD, YASL_EVENT_DROP, - YASL_EVENT_DRAG, YASL_EVENT_DISPLAY_LOST }; + YASL_EVENT_DRAG, YASL_EVENT_DISPLAY_LOST, YASL_EVENT_DISPLAY_RESTORED }; enum yasl_drop_action { YASL_DROP_OVER, YASL_DROP_LEAVE, YASL_DROP_DATA }; enum yasl_drag_action { YASL_DRAG_OVER, YASL_DRAG_LEAVE, YASL_DRAG_DROP }; /** Read the union member named by type. Strings stay valid until the next wait. @@ -60,7 +60,11 @@ enum yasl_drag_action { YASL_DRAG_OVER, YASL_DRAG_LEAVE, YASL_DRAG_DROP }; * WINDOW: size, scale or state changed. REFRESH: draw now; sent once the window * has a size, after every resize and when the system needs the contents again. * INPUT_RESET: forget held keys, buttons and strokes. DROP: external data over - * the window, delivered as DATA. DRAG: another YASL surface dragged over. */ + * the window, delivered as DATA. DRAG: another YASL surface dragged over. + * DISPLAY_LOST: the display server went away. Destroy Vulkan surfaces and GL + * objects now; windows stay valid. Wayland reconnects, then sends DISPLAY_RESTORED: + * recreate them (GL contexts are new and must be bound again). Elsewhere loss is + * final and later waits fail. */ struct yasl_event { enum yasl_event_type type; yasl_surface *surface; diff --git a/src/common/backend.h b/src/common/backend.h index e3dc6de..bf6815a 100644 --- a/src/common/backend.h +++ b/src/common/backend.h @@ -16,6 +16,7 @@ struct yasl_private_event_state; struct yasl_private_display { const struct yasl_private_backend_ops *ops; void *native_handle; + bool offline; /* Connection lost; native handles are unusable until it is restored. */ struct yasl_private_display_cbs callbacks; struct yasl_private_loop *loop; /* Backend's copy of yasl_private_display_spec.app_id, or NULL; for desktop services. */ diff --git a/src/common/surface.c b/src/common/surface.c index 897b95a..9b4a8ad 100644 --- a/src/common/surface.c +++ b/src/common/surface.c @@ -32,13 +32,14 @@ bool yasl_public_fail(int error) else if (error==ENOMEM) code=YASL_ERROR_NO_MEMORY; else if (error==EBUSY) code=YASL_ERROR_BUSY; else if (error==EPERM) code=YASL_ERROR_WRONG_THREAD; + else if (error==ENOTCONN) code=YASL_ERROR_DISCONNECTED; yasl_public_set_error(code,error); errno=error; return false; } enum yasl_error yasl_get_error(const char **description) { static const char *const messages[]={"No error","Invalid argument","Unsupported operation", "Out of memory","Library not initialized","Operation is busy","Wrong thread", - "Input context expired","Native operation failed"}; + "Input context expired","Native operation failed","Display disconnected"}; if(description) *description=messages[last_error]; return last_error; } @@ -254,6 +255,7 @@ static bool convert_event(const struct yasl_private_event *p,yasl_event *e) case YASL_PRIVATE_EVENT_DISPLAY_QUIT:e->type=YASL_EVENT_QUIT;break; case YASL_PRIVATE_EVENT_DISPLAY_REOPEN:e->type=YASL_EVENT_REOPEN;break; case YASL_PRIVATE_EVENT_DISPLAY_LOST:e->type=YASL_EVENT_DISPLAY_LOST;break; + case YASL_PRIVATE_EVENT_DISPLAY_RESTORED:e->type=YASL_EVENT_DISPLAY_RESTORED;break; case YASL_PRIVATE_EVENT_REFRESH: if(!p->surface) return false; if(!refresh_ready(p->surface)) { p->surface->refresh_owed=true; return false; } diff --git a/src/common/types.h b/src/common/types.h index 91f8a47..40c502f 100644 --- a/src/common/types.h +++ b/src/common/types.h @@ -355,7 +355,8 @@ enum yasl_private_event_type { YASL_PRIVATE_EVENT_DISPLAY_LOST, YASL_PRIVATE_EVENT_PEN, YASL_PRIVATE_EVENT_TEXT_EDIT, - YASL_PRIVATE_EVENT_REFRESH, /**< No payload; display connection failed. */ + YASL_PRIVATE_EVENT_REFRESH, /**< No payload; surface contents are needed. */ + YASL_PRIVATE_EVENT_DISPLAY_RESTORED, /**< No payload; display reconnected after DISPLAY_LOST. */ }; /** Physical keyboard transition with optional layout labels. diff --git a/src/common/vulkan.c b/src/common/vulkan.c index 1bca415..62bc3a0 100644 --- a/src/common/vulkan.c +++ b/src/common/vulkan.c @@ -55,6 +55,7 @@ VkResult yasl_vulkan_create_surface(yasl_surface *s,VkInstance instance,const Vk { if(out)*out=VK_NULL_HANDLE; if(!yasl_public_surface(s,false)||!instance||!out||!get_proc){yasl_public_fail(EINVAL);return VK_ERROR_INITIALIZATION_FAILED;} + if(s->display->offline){yasl_public_fail(ENOTCONN);return VK_ERROR_SURFACE_LOST_KHR;} if(s->presenter!=YASL_PRIVATE_PRESENTER_NONE){yasl_public_fail(EBUSY);return VK_ERROR_NATIVE_WINDOW_IN_USE_KHR;} VkResult result=VK_ERROR_EXTENSION_NOT_PRESENT; switch(s->display->ops->type) { diff --git a/src/platform/wayland/backend.c b/src/platform/wayland/backend.c index 02aae59..b3337cc 100644 --- a/src/platform/wayland/backend.c +++ b/src/platform/wayland/backend.c @@ -125,42 +125,36 @@ static bool egl_configure(struct wayland_display *d, const struct yasl_private_g return eglBindAPI(api) == EGL_TRUE; } -static struct yasl_surface *impl_surface_create_gl(struct yasl_private_display *display, - const struct yasl_private_window_spec *spec, const int *attributes, struct yasl_surface *share) -{ - struct wayland_display *d = (struct wayland_display *)display; - struct yasl_private_gl_options o; - if (!spec || !yasl_private_gl_parse_options(attributes, &o) || !d || !d->egl_initialized) - return NULL; - struct yasl_private_window_spec gl_spec = *spec; - gl_spec.external_gpu = true; - gl_spec.flags &= ~YASL_PRIVATE_SURFACE_CPU; - struct wayland_surface *w = wayland_surface_create_toplevel(d, &gl_spec, NULL); - if (!w) return NULL; - if (!egl_configure(d, &o, &w->egl_config)) goto fail; - EGLContext share_context = share && share->display == display - ? ((struct wayland_surface *)share)->egl_context : EGL_NO_CONTEXT; - if (share && share_context == EGL_NO_CONTEXT) goto fail; +bool surface_build_gl(struct wayland_surface *w) +{ + struct wayland_display *d = w->conn; + const struct yasl_private_gl_options *o = &w->gl_options; + if (!egl_configure(d, o, &w->egl_config)) return false; + EGLContext share_context = EGL_NO_CONTEXT; + if (w->gl_share) { + if (!w->gl_share->gl_built && !surface_build_gl(w->gl_share)) return false; + share_context = w->gl_share->egl_context; + } w->egl_window = wl_egl_window_create(w->surface, w->width, w->height); - if (!w->egl_window) goto fail; + if (!w->egl_window) return false; EGLint context_attrs[12]; int n = 0; - if (o.api == YASL_PRIVATE_GL_OPENGL_ES) { + if (o->api == YASL_PRIVATE_GL_OPENGL_ES) { context_attrs[n++] = EGL_CONTEXT_CLIENT_VERSION; - context_attrs[n++] = o.major; + context_attrs[n++] = o->major; } else { context_attrs[n++] = EGL_CONTEXT_MAJOR_VERSION_KHR; - context_attrs[n++] = o.major; + context_attrs[n++] = o->major; context_attrs[n++] = EGL_CONTEXT_MINOR_VERSION_KHR; - context_attrs[n++] = o.minor; - if (o.profile != YASL_PRIVATE_GL_ANY) { + context_attrs[n++] = o->minor; + if (o->profile != YASL_PRIVATE_GL_ANY) { context_attrs[n++] = EGL_CONTEXT_OPENGL_PROFILE_MASK_KHR; - context_attrs[n++] = o.profile == YASL_PRIVATE_GL_CORE + context_attrs[n++] = o->profile == YASL_PRIVATE_GL_CORE ? EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT_KHR : EGL_CONTEXT_OPENGL_COMPATIBILITY_PROFILE_BIT_KHR; } - EGLint flags = (o.debug ? EGL_CONTEXT_OPENGL_DEBUG_BIT_KHR : 0) | - (o.forward_compat ? EGL_CONTEXT_OPENGL_FORWARD_COMPATIBLE_BIT_KHR : 0); + EGLint flags = (o->debug ? EGL_CONTEXT_OPENGL_DEBUG_BIT_KHR : 0) | + (o->forward_compat ? EGL_CONTEXT_OPENGL_FORWARD_COMPATIBLE_BIT_KHR : 0); if (flags) { context_attrs[n++] = EGL_CONTEXT_FLAGS_KHR; context_attrs[n++] = flags; @@ -168,21 +162,42 @@ static struct yasl_surface *impl_surface_create_gl(struct yasl_private_display * } context_attrs[n] = EGL_NONE; w->egl_context = eglCreateContext(d->egl_display, w->egl_config, share_context, context_attrs); - if (w->egl_context == EGL_NO_CONTEXT) goto fail; + if (w->egl_context == EGL_NO_CONTEXT) { w->egl_context = 0; return false; } EGLint surface_attrs[] = { #ifdef EGL_GL_COLORSPACE_KHR - EGL_GL_COLORSPACE_KHR, o.srgb ? EGL_GL_COLORSPACE_SRGB_KHR : EGL_GL_COLORSPACE_LINEAR_KHR, + EGL_GL_COLORSPACE_KHR, o->srgb ? EGL_GL_COLORSPACE_SRGB_KHR : EGL_GL_COLORSPACE_LINEAR_KHR, #endif EGL_NONE, }; w->egl_surface = eglCreateWindowSurface(d->egl_display, w->egl_config, (EGLNativeWindowType)w->egl_window, surface_attrs); - if (w->egl_surface == EGL_NO_SURFACE) { w->egl_surface = 0; goto fail; } + if (w->egl_surface == EGL_NO_SURFACE) { w->egl_surface = 0; return false; } + w->gl_built = true; + return true; +} + +static struct yasl_surface *impl_surface_create_gl(struct yasl_private_display *display, + const struct yasl_private_window_spec *spec, const int *attributes, struct yasl_surface *share) +{ + struct wayland_display *d = (struct wayland_display *)display; + struct yasl_private_gl_options o; + if (!spec || !yasl_private_gl_parse_options(attributes, &o) || !d || !d->egl_initialized) + return NULL; + if (share && (share->display != display || !((struct wayland_surface *)share)->egl_context)) + return NULL; + struct yasl_private_window_spec gl_spec = *spec; + gl_spec.external_gpu = true; + gl_spec.flags &= ~YASL_PRIVATE_SURFACE_CPU; + struct wayland_surface *w = wayland_surface_create_toplevel(d, &gl_spec, NULL); + if (!w) return NULL; + w->gl_options = o; + w->gl_share = (struct wayland_surface *)share; + if (!surface_build_gl(w)) { + wayland_surface_destroy(w); + return NULL; + } w->base.presenter = YASL_PRIVATE_PRESENTER_GL; return &w->base; -fail: - wayland_surface_destroy(w); - return NULL; } static bool impl_gl_make_current(struct yasl_private_display *display, struct yasl_surface *surface) @@ -250,7 +265,7 @@ static struct input_queue *impl_surface_input_queue(struct yasl_surface *s) static bool impl_surface_set_size(struct yasl_surface *s, struct yasl_private_vector2i size) { struct wayland_surface *w = (struct wayland_surface *)s; - if (!w || w->closed || w->destroyed || !w->toplevel || + if (!w || w->closed || w->destroyed || (!w->toplevel && !w->conn->lost) || size.x <= 0 || size.y <= 0) { errno = EINVAL; return false; @@ -258,7 +273,8 @@ static bool impl_surface_set_size(struct yasl_surface *s, struct yasl_private_ve /* Floating windows choose their own size; constrained states keep it for later. */ w->floating_width = size.x; w->floating_height = size.y; - if (w->base.state & (YASL_PRIVATE_STATE_MAXIMIZED | YASL_PRIVATE_STATE_FULLSCREEN | YASL_PRIVATE_STATE_TILED)) + if (w->conn->lost || + (w->base.state & (YASL_PRIVATE_STATE_MAXIMIZED | YASL_PRIVATE_STATE_FULLSCREEN | YASL_PRIVATE_STATE_TILED))) return true; w->req_width = w->req_height = 0; w->size_pending = true; diff --git a/src/platform/wayland/buffer.c b/src/platform/wayland/buffer.c index bbc055d..029b684 100644 --- a/src/platform/wayland/buffer.c +++ b/src/platform/wayland/buffer.c @@ -211,6 +211,7 @@ static const struct wp_fractional_scale_v1_listener frac_scale_listener = { void surface_follow_outputs(struct wayland_surface *w) { + if (!w->surface) return; /* Not rebuilt yet after a reconnect. */ uint32_t refresh = 0; int32_t max_scale = 0; bool need_scale = !w->frac_scale && @@ -563,11 +564,12 @@ int surface_init_scaling(struct wayland_surface *w) void wayland_surface_set_title(struct wayland_surface *w, const char *title) { - if (!w || !w->toplevel || !title) return; + if (!w || !title) return; char *copy = yasl_private_utf8_sanitize_dup(title); if (!copy) return; - xdg_toplevel_set_title(w->toplevel, copy); - free(copy); + free(w->title); + w->title = copy; + if (w->toplevel) xdg_toplevel_set_title(w->toplevel, copy); } struct yasl_private_timing wayland_surface_timing(const struct wayland_surface *w) diff --git a/src/platform/wayland/internal.h b/src/platform/wayland/internal.h index a479e2b..92108c1 100644 --- a/src/platform/wayland/internal.h +++ b/src/platform/wayland/internal.h @@ -251,6 +251,10 @@ struct wayland_display { struct conn_drag drag; struct wayland_surface *surfaces; struct wayland_surface *dead; + /* While lost: retries a connection; stale marks objects of the dead one. */ + yasl_private_timer_id reconnect_timer; + uint64_t reconnect_delay_ns; + bool stale; /* 4-byte integers */ int cursor_size; @@ -287,7 +291,12 @@ struct wayland_surface { EGLSurface egl_surface; EGLContext egl_context; EGLConfig egl_config; + struct yasl_private_gl_options gl_options; + struct wayland_surface *gl_share; + bool gl_built; #endif + char *title; + bool tearing; struct xdg_surface *xdg_surface; struct xdg_toplevel *toplevel; struct xdg_popup *popup; @@ -393,6 +402,12 @@ void surface_draw_frame(struct wayland_surface *w); void wayland_surface_arm_frame(struct wayland_surface *w); void surface_fail(struct wayland_surface *w); void display_leave(struct wayland_display *c); +/* Native objects of a toplevel: built at creation and again after a reconnect. */ +int surface_build_toplevel(struct wayland_surface *w, bool fullscreen); +void surface_release_native(struct wayland_surface *w); +#if YASL_HAS_EGL +bool surface_build_gl(struct wayland_surface *w); +#endif static inline struct wayland_surface *surface_root(struct wayland_surface *w) { diff --git a/src/platform/wayland/pointer.c b/src/platform/wayland/pointer.c index 5f486ae..0cd8c5b 100644 --- a/src/platform/wayland/pointer.c +++ b/src/platform/wayland/pointer.c @@ -542,7 +542,7 @@ static const struct zwp_locked_pointer_v1_listener locked_pointer_listener = { static void surface_lock_pointer(struct wayland_surface *w, struct seat *s) { struct wayland_display *c = w->conn; - if (!w->relative || w->pointer_lock || w->destroyed || !s->pointer) + if (!w->relative || w->pointer_lock || w->destroyed || !w->surface || !s->pointer) return; w->pointer_lock = zwp_pointer_constraints_v1_lock_pointer(c->pointer_constraints, w->surface, s->pointer, NULL, ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_PERSISTENT); diff --git a/src/platform/wayland/surface.c b/src/platform/wayland/surface.c index 31096f4..e04a594 100644 --- a/src/platform/wayland/surface.c +++ b/src/platform/wayland/surface.c @@ -3,6 +3,7 @@ #include static void display_free(struct wayland_display *c); +static uint64_t reconnect_tick(void *ud, yasl_private_timer_id id, uint64_t interval_ns); static void display_cancel_read(struct wayland_display *c) { @@ -12,19 +13,16 @@ static void display_cancel_read(struct wayland_display *c) c->reading = false; } +/* A compositor going away is expected; a protocol error is a YASL bug worth reporting. */ static void report_display_error(struct wayland_display *c) { - int err = wl_display_get_error(c->wl); - if (!err) + if (wl_display_get_error(c->wl) != EPROTO) return; uint32_t object_id = 0; const struct wl_interface *iface = NULL; uint32_t protocol_err = wl_display_get_protocol_error(c->wl, &iface, &object_id); - if (protocol_err) - fprintf(stderr, "[yasl] protocol error %u on %s (object %u)\n", - protocol_err, iface ? iface->name : "?", object_id); - else - fprintf(stderr, "[yasl] display error %d\n", err); + fprintf(stderr, "[yasl] protocol error %u on %s (object %u)\n", + protocol_err, iface ? iface->name : "?", object_id); } void display_leave(struct wayland_display *c) @@ -39,20 +37,32 @@ void display_leave(struct wayland_display *c) } } +/* Windows outlive the connection. Its objects stay until the next dispatch, after + * the application has seen DISPLAY_LOST and released GPU objects built on them. */ static void display_lose(struct wayland_display *c) { if (c->lost) return; c->lost = true; + c->stale = true; + c->base.offline = true; report_display_error(c); display_cancel_read(c); + if (c->loop && c->watch > 0) + yasl_private_loop_del_fd(c->loop, c->watch); + c->watch = 0; c->depth++; + conn_drag_end(c); for (struct wayland_surface *w = c->surfaces, *next; w; w = next) { next = w->conn_next; - surface_fail(w); + if (w->parent) + surface_fail(w); } display_leave(c); yasl_private_display_notify_event(&c->base, YASL_PRIVATE_EVENT_DISPLAY_LOST); + c->reconnect_delay_ns = YASL_PRIVATE_MS_TO_NS(100); + if (c->loop && !c->reconnect_timer) + c->reconnect_timer = yasl_private_loop_add_timer(c->loop, c->reconnect_delay_ns, reconnect_tick, c); } void surface_fail(struct wayland_surface *w) @@ -131,21 +141,28 @@ static inline bool display_has_immediate_work(const struct wayland_display *c) int wayland_display_dispatch_timeout(struct wayland_display *c, int timeout_ms) { - if (!c || c->lost) + if (!c) return -1; c->depth++; + if (c->lost) { + /* Reconnect attempts run as a loop timer. */ + int n = c->loop ? yasl_private_loop_step(c->loop, timeout_ms) : 0; + display_leave(c); + return n; + } + /* Losing the connection is reported as DISPLAY_LOST, not as a failed wait. */ if (wl_display_dispatch_pending(c->wl) < 0) { display_lose(c); display_leave(c); - return -1; + return 0; } display_arm(c); if (c->lost) { display_leave(c); - return -1; + return 0; } if (timeout_ms != 0 && display_has_immediate_work(c)) { @@ -161,10 +178,10 @@ int wayland_display_dispatch_timeout(struct wayland_display *c, int timeout_ms) display_cancel_read(c); } - if (wl_display_dispatch_pending(c->wl) < 0) { + if (c->wl && wl_display_dispatch_pending(c->wl) < 0) { display_lose(c); display_leave(c); - return -1; + return 0; } display_leave(c); @@ -187,40 +204,126 @@ void wayland_display_flush(struct wayland_display *c) display_leave(c); } -static void display_free(struct wayland_display *c) +#define RELEASE(field, destroy) do { if (c->field) { destroy(c->field); c->field = NULL; } } while (0) + +static void connection_release(struct wayland_display *c) { + for (struct wayland_surface *w = c->surfaces; w; w = w->conn_next) + surface_release_native(w); conn_drag_end(c); if (c->loop && c->watch > 0) yasl_private_loop_del_fd(c->loop, c->watch); + c->watch = 0; if (c->wl) display_cancel_read(c); while (c->seats) seat_destroy(c->seats); - if (c->toplevel_drag_mgr) xdg_toplevel_drag_manager_v1_destroy(c->toplevel_drag_mgr); - if (c->primary_selection_mgr) zwp_primary_selection_device_manager_v1_destroy(c->primary_selection_mgr); - if (c->data_manager) wl_data_device_manager_destroy(c->data_manager); - if (c->cursor_shape_mgr) wp_cursor_shape_manager_v1_destroy(c->cursor_shape_mgr); - if (c->relative_pointer_mgr) zwp_relative_pointer_manager_v1_destroy(c->relative_pointer_mgr); - if (c->pointer_constraints) zwp_pointer_constraints_v1_destroy(c->pointer_constraints); - if (c->system_bell) xdg_system_bell_v1_destroy(c->system_bell); - if (c->cursor_theme) wl_cursor_theme_destroy(c->cursor_theme); - if (c->tablet_mgr) zwp_tablet_manager_v2_destroy(c->tablet_mgr); - if (c->text_input_mgr) zwp_text_input_manager_v3_destroy(c->text_input_mgr); - if (c->tearing_mgr) wp_tearing_control_manager_v1_destroy(c->tearing_mgr); - if (c->fifo_mgr) wp_fifo_manager_v1_destroy(c->fifo_mgr); - if (c->commit_timing_mgr) wp_commit_timing_manager_v1_destroy(c->commit_timing_mgr); - if (c->presentation) wp_presentation_destroy(c->presentation); - if (c->xkb_ctx) xkb_context_unref(c->xkb_ctx); - if (c->scale_mgr) wp_fractional_scale_manager_v1_destroy(c->scale_mgr); - if (c->viewporter) wp_viewporter_destroy(c->viewporter); - if (c->decor_mgr) zxdg_decoration_manager_v1_destroy(c->decor_mgr); + RELEASE(toplevel_drag_mgr, xdg_toplevel_drag_manager_v1_destroy); + RELEASE(primary_selection_mgr, zwp_primary_selection_device_manager_v1_destroy); + RELEASE(data_manager, wl_data_device_manager_destroy); + RELEASE(cursor_shape_mgr, wp_cursor_shape_manager_v1_destroy); + RELEASE(relative_pointer_mgr, zwp_relative_pointer_manager_v1_destroy); + RELEASE(pointer_constraints, zwp_pointer_constraints_v1_destroy); + RELEASE(system_bell, xdg_system_bell_v1_destroy); + RELEASE(cursor_theme, wl_cursor_theme_destroy); + RELEASE(tablet_mgr, zwp_tablet_manager_v2_destroy); + RELEASE(text_input_mgr, zwp_text_input_manager_v3_destroy); + RELEASE(tearing_mgr, wp_tearing_control_manager_v1_destroy); + RELEASE(fifo_mgr, wp_fifo_manager_v1_destroy); + RELEASE(commit_timing_mgr, wp_commit_timing_manager_v1_destroy); + RELEASE(presentation, wp_presentation_destroy); + RELEASE(scale_mgr, wp_fractional_scale_manager_v1_destroy); + RELEASE(viewporter, wp_viewporter_destroy); + RELEASE(decor_mgr, zxdg_decoration_manager_v1_destroy); conn_destroy_outputs(c); - if (c->wm_base) xdg_wm_base_destroy(c->wm_base); - if (c->shm) wl_shm_destroy(c->shm); - if (c->compositor) wl_compositor_destroy(c->compositor); - if (c->registry) wl_registry_destroy(c->registry); + RELEASE(wm_base, xdg_wm_base_destroy); + RELEASE(shm, wl_shm_destroy); + RELEASE(compositor, wl_compositor_destroy); + RELEASE(registry, wl_registry_destroy); #if YASL_HAS_EGL if (c->egl_initialized) eglTerminate(c->egl_display); + c->egl_initialized = false; + c->egl_display = EGL_NO_DISPLAY; + c->egl_api = 0; #endif - if (c->wl) wl_display_disconnect(c->wl); + RELEASE(wl, wl_display_disconnect); + c->base.native_handle = NULL; + c->cursor_theme_scale = 0; + c->pres_clock_known = false; + c->active_seat = NULL; +} + +static bool connection_open(struct wayland_display *c) +{ + c->wl = wl_display_connect(NULL); + if (!c->wl) return false; + c->base.native_handle = c->wl; +#if YASL_HAS_EGL + c->egl_display = eglGetDisplay((EGLNativeDisplayType)c->wl); + if (c->egl_display != EGL_NO_DISPLAY) { + EGLint major = 0, minor = 0; + c->egl_initialized = eglInitialize(c->egl_display, &major, &minor) == EGL_TRUE; + } +#endif + c->registry = wl_display_get_registry(c->wl); + if (!c->registry) return false; + wl_registry_add_listener(c->registry, ®istry_listener, c); + if (wl_display_roundtrip(c->wl) < 0 || !c->compositor || !c->shm || !c->wm_base) + return false; + surface_init_cursor(c); + if (c->loop) { + c->watch = yasl_private_loop_add_fd(c->loop, wl_display_get_fd(c->wl), POLLIN, display_ready, c); + if (c->watch <= 0) { c->watch = 0; return false; } + } + return display_input_init(c) == 0; +} + +/* Recreates every window on a new connection; one that cannot be rebuilt is closed. */ +static void display_rebuild(struct wayland_display *c) +{ + for (struct wayland_surface *w = c->surfaces; w && !c->lost; w = w->conn_next) { + if (w->parent || w->closed || w->destroyed) continue; + w->req_width = w->req_height = 0; + if (surface_build_toplevel(w, w->base.state & YASL_PRIVATE_STATE_FULLSCREEN) < 0 && !c->lost) + surface_fail(w); + } +#if YASL_HAS_EGL + for (struct wayland_surface *w = c->surfaces; w && !c->lost; w = w->conn_next) + if (w->base.presenter == YASL_PRIVATE_PRESENTER_GL && !w->closed && !w->gl_built && !surface_build_gl(w)) + surface_fail(w); +#endif + for (struct wayland_surface *w = c->surfaces; w && !c->lost; w = w->conn_next) + if (!w->closed) yasl_private_surface_notify_refresh(&w->base); +} + +static uint64_t reconnect_tick(void *ud, yasl_private_timer_id id, uint64_t interval_ns) +{ + (void)id; (void)interval_ns; + struct wayland_display *c = ud; + c->depth++; + if (c->stale) { + connection_release(c); + c->stale = false; + } + if (!connection_open(c)) { + connection_release(c); + display_leave(c); + uint64_t limit = YASL_PRIVATE_MS_TO_NS(2000); + c->reconnect_delay_ns = c->reconnect_delay_ns * 2 < limit ? c->reconnect_delay_ns * 2 : limit; + return c->reconnect_delay_ns; + } + c->reconnect_timer = 0; + c->lost = false; + c->base.offline = false; + yasl_private_display_notify_event(&c->base, YASL_PRIVATE_EVENT_DISPLAY_RESTORED); + display_rebuild(c); + display_leave(c); + return 0; +} + +static void display_free(struct wayland_display *c) +{ + connection_release(c); + if (c->loop && c->reconnect_timer) yasl_private_loop_del_timer(c->loop, c->reconnect_timer); + if (c->xkb_ctx) xkb_context_unref(c->xkb_ctx); if (c->owns_loop && c->loop) { yasl_private_loop_destroy(c->loop); c->loop = NULL; @@ -251,29 +354,7 @@ struct wayland_display *wayland_display_open(struct yasl_private_loop *loop, con c->base.app_id = c->app_id; } c->xkb_ctx = xkb_context_new(XKB_CONTEXT_NO_FLAGS); - c->wl = c->xkb_ctx ? wl_display_connect(NULL) : NULL; - if (!c->wl) goto err; - c->base.native_handle = c->wl; -#if YASL_HAS_EGL - c->egl_display = eglGetDisplay((EGLNativeDisplayType)c->wl); - if (c->egl_display != EGL_NO_DISPLAY) { - EGLint major = 0, minor = 0; - c->egl_initialized = eglInitialize(c->egl_display, &major, &minor) == EGL_TRUE; - } -#endif - - c->registry = wl_display_get_registry(c->wl); - if (!c->registry) goto err; - wl_registry_add_listener(c->registry, ®istry_listener, c); - if (wl_display_roundtrip(c->wl) < 0 || !c->compositor || !c->shm || !c->wm_base) - goto err; - - surface_init_cursor(c); - if (c->loop) { - c->watch = yasl_private_loop_add_fd(c->loop, wl_display_get_fd(c->wl), POLLIN, display_ready, c); - if (c->watch <= 0) { c->watch = 0; goto err; } - } - if (display_input_init(c) < 0) + if (!c->xkb_ctx || !connection_open(c)) goto err; return c; @@ -343,11 +424,91 @@ static bool spec_valid(const struct yasl_private_window_spec *spec) : buffer_size(spec->size.x, spec->size.y, &bytes); } +int surface_build_toplevel(struct wayland_surface *w, bool fullscreen) +{ + struct wayland_display *c = w->conn; + w->surface = wl_compositor_create_surface(c->compositor); + if (!w->surface) return -1; + w->base.native_handle = w->surface; + wl_surface_set_user_data(w->surface, w); + if (surface_init_scaling(w) < 0) return -1; + if (surface_init_shell(w, w->title) < 0) return -1; + if (w->base.creation_flags & YASL_PRIVATE_SURFACE_FIXED_SIZE) { + xdg_toplevel_set_min_size(w->toplevel, w->logical_width, w->logical_height); + xdg_toplevel_set_max_size(w->toplevel, w->logical_width, w->logical_height); + } + if (fullscreen) + xdg_toplevel_set_fullscreen(w->toplevel, NULL); + else if (w->base.state & YASL_PRIVATE_STATE_MAXIMIZED) + xdg_toplevel_set_maximized(w->toplevel); + if (w->tearing && c->tearing_mgr) { + w->tearing_control = wp_tearing_control_manager_v1_get_tearing_control( + c->tearing_mgr, w->surface); + if (w->tearing_control) + wp_tearing_control_v1_set_presentation_hint(w->tearing_control, + WP_TEARING_CONTROL_V1_PRESENTATION_HINT_ASYNC); + } + w->size_pending = true; + wl_surface_commit(w->surface); + if (wl_display_roundtrip(c->wl) < 0) { + display_lose(c); + return -1; + } + if (apply_pending_geometry(w) < 0 || w->closed) return -1; + return 0; +} + +void surface_release_native(struct wayland_surface *w) +{ + surface_unlock_pointer(w); + w->ime_seat = NULL; +#define DROP(field, destroy) do { if (w->field) { destroy(w->field); w->field = NULL; } } while (0) + DROP(tearing_control, wp_tearing_control_v1_destroy); +#if YASL_HAS_EGL + struct wayland_display *c = w->conn; + if (w->egl_context) { + if (eglGetCurrentContext() == w->egl_context) + eglMakeCurrent(c->egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); + eglDestroyContext(c->egl_display, w->egl_context); + w->egl_context = 0; + } + if (w->egl_surface) eglDestroySurface(c->egl_display, w->egl_surface); + w->egl_surface = 0; + DROP(egl_window, wl_egl_window_destroy); + w->gl_built = false; +#endif + DROP(fifo, wp_fifo_v1_destroy); + DROP(commit_timer, wp_commit_timer_v1_destroy); + surface_feedback_destroy(w); + DROP(frame_cb, wl_callback_destroy); + for (int i = 0; i < NBUF; i++) buffer_destroy(&w->buffers[i]); + DROP(frac_scale, wp_fractional_scale_v1_destroy); + DROP(viewport, wp_viewport_destroy); + DROP(toplevel_decor, zxdg_toplevel_decoration_v1_destroy); + DROP(toplevel, xdg_toplevel_destroy); + DROP(popup, xdg_popup_destroy); + DROP(xdg_surface, xdg_surface_destroy); + if (w->surface) { + wl_surface_set_user_data(w->surface, NULL); + wl_surface_destroy(w->surface); + w->surface = NULL; + } +#undef DROP + w->base.native_handle = NULL; + /* A Vulkan surface built on the old wl_surface is gone; allow a new one. */ + if (w->base.presenter == YASL_PRIVATE_PRESENTER_VULKAN) + w->base.presenter = YASL_PRIVATE_PRESENTER_NONE; + w->configured = w->has_pending_serial = w->has_shown = false; + w->frame_active = w->want_draw = false; + w->outputs = 0; +} + struct wayland_surface *wayland_surface_create_toplevel(struct wayland_display *c, const struct yasl_private_window_spec *spec, void *graphics_device) { (void)graphics_device; - if (!c || c->lost || !spec_valid(spec)) + if (c && c->lost) { errno = ENOTCONN; return NULL; } + if (!c || !spec_valid(spec)) return NULL; int width = spec->size.x, height = spec->size.y; display_cancel_read(c); @@ -360,6 +521,7 @@ struct wayland_surface *wayland_surface_create_toplevel(struct wayland_display * w->base.presenter = (spec->external_gpu && !(spec->flags & YASL_PRIVATE_SURFACE_CPU)) ? YASL_PRIVATE_PRESENTER_NONE : YASL_PRIVATE_PRESENTER_CPU; wayland_surface_set_callbacks(w, NULL, NULL); + wayland_surface_set_title(w, spec->title ? spec->title : "yasl_private_app"); w->cursor = YASL_PRIVATE_CURSOR_TEXT; w->req_width = width; w->req_height = height; @@ -368,40 +530,13 @@ struct wayland_surface *wayland_surface_create_toplevel(struct wayland_display * w->scale_120 = YASL_PRIVATE_SCALE_UNIT; w->pending_scale_120 = YASL_PRIVATE_SCALE_UNIT; w->scale_factor = 1.0; - w->size_pending = true; + w->tearing = spec->tearing; + w->opaque = spec->fullscreen; w->clock.refresh_ns = c->outputs && c->outputs->refresh_ns ? c->outputs->refresh_ns : YASL_PRIVATE_REFRESH_DEFAULT_NS; - - w->surface = wl_compositor_create_surface(c->compositor); - if (!w->surface) goto err; - w->base.native_handle = w->surface; w->base.size = (struct yasl_private_vector2i){width, height}; w->base.scale_120 = YASL_PRIVATE_SCALE_UNIT; - wl_surface_set_user_data(w->surface, w); - if (surface_init_scaling(w) < 0) goto err; - if (surface_init_shell(w, spec->title) < 0) goto err; - if (spec->flags & YASL_PRIVATE_SURFACE_FIXED_SIZE) { - xdg_toplevel_set_min_size(w->toplevel, width, height); - xdg_toplevel_set_max_size(w->toplevel, width, height); - } - w->opaque = spec->fullscreen; - if (spec->fullscreen) - xdg_toplevel_set_fullscreen(w->toplevel, NULL); - if (spec->tearing && c->tearing_mgr) { - w->tearing_control = wp_tearing_control_manager_v1_get_tearing_control( - c->tearing_mgr, w->surface); - if (w->tearing_control) - wp_tearing_control_v1_set_presentation_hint(w->tearing_control, - WP_TEARING_CONTROL_V1_PRESENTATION_HINT_ASYNC); - } - - wl_surface_commit(w->surface); - if (wl_display_roundtrip(c->wl) < 0) { - display_lose(c); - goto err; - } - if (apply_pending_geometry(w) < 0) goto err; - if (w->closed) goto err; + if (surface_build_toplevel(w, spec->fullscreen) < 0) goto err; display_leave(c); return w; @@ -426,37 +561,17 @@ void wayland_surface_destroy(struct wayland_surface *w) if (!w || w->destroyed) return; for (struct wayland_surface *o = w->conn ? w->conn->surfaces : NULL; o; o = o->conn_next) assert(o->parent != w && "destroy a popup before its parent"); - surface_unlock_pointer(w); + surface_release_native(w); input_queue_clear(&w->input); free(w->ime_preedit); free(w->ime_commit); free(w->ime_shown); - if (w->tearing_control) wp_tearing_control_v1_destroy(w->tearing_control); + free(w->title); + struct wayland_display *c = w->conn; #if YASL_HAS_EGL - if (w->egl_context) { - if (eglGetCurrentContext() == w->egl_context) - eglMakeCurrent(w->conn->egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); - eglDestroyContext(w->conn->egl_display, w->egl_context); - } - if (w->egl_surface) eglDestroySurface(w->conn->egl_display, w->egl_surface); - if (w->egl_window) wl_egl_window_destroy(w->egl_window); + for (struct wayland_surface *o = c ? c->surfaces : NULL; o; o = o->conn_next) + if (o->gl_share == w) o->gl_share = NULL; #endif - if (w->fifo) wp_fifo_v1_destroy(w->fifo); - if (w->commit_timer) wp_commit_timer_v1_destroy(w->commit_timer); - surface_feedback_destroy(w); - if (w->frame_cb) wl_callback_destroy(w->frame_cb); - for (int i = 0; i < NBUF; i++) buffer_destroy(&w->buffers[i]); - if (w->frac_scale) wp_fractional_scale_v1_destroy(w->frac_scale); - if (w->viewport) wp_viewport_destroy(w->viewport); - if (w->toplevel_decor) zxdg_toplevel_decoration_v1_destroy(w->toplevel_decor); - if (w->toplevel) xdg_toplevel_destroy(w->toplevel); - if (w->popup) xdg_popup_destroy(w->popup); - if (w->xdg_surface) xdg_surface_destroy(w->xdg_surface); - if (w->surface) { - wl_surface_set_user_data(w->surface, NULL); - wl_surface_destroy(w->surface); - } - struct wayland_display *c = w->conn; conn_unlink(w); static const struct yasl_private_window_cbs no_callbacks = {0}; *w = (struct wayland_surface){ diff --git a/src/platform/x11/window.c b/src/platform/x11/window.c index 8129dd5..ef13afe 100644 --- a/src/platform/x11/window.c +++ b/src/platform/x11/window.c @@ -2391,6 +2391,7 @@ static int ready(int fd, short events, void *ud) struct x11_display *d = (struct x11_display *)base; if ((events & (POLLERR | POLLHUP)) && !d->lost) { d->lost = true; + base->offline = true; yasl_private_display_notify_event(base, YASL_PRIVATE_EVENT_DISPLAY_LOST); return 0; } diff --git a/tests/helpers/run_reconnect.py b/tests/helpers/run_reconnect.py new file mode 100644 index 0000000..e86e994 --- /dev/null +++ b/tests/helpers/run_reconnect.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 +"""Kill Weston under a running client, start a new one on the same socket, and +require the client to reconnect: run_reconnect.py .""" +import os +from pathlib import Path +import signal +import subprocess +import sys +import tempfile +import time + + +def start_weston(weston, root, env, log): + process = subprocess.Popen([weston, '--backend=headless', '--renderer=gl', '--socket=yasl-test', + '--idle-time=0', '--no-config', '--width=1024', '--height=768'], + env=env, stdout=log, stderr=log) + deadline = time.monotonic() + 12 + while not (root/'yasl-test').is_socket(): + if process.poll() is not None or time.monotonic() > deadline: + raise RuntimeError('Weston did not start') + time.sleep(0.02) + return process + + +def expect(client, word): + line = client.stdout.readline().strip() + if line != word: + raise RuntimeError(f'client printed {line!r}, expected {word!r}') + + +def main(): + weston, binary = sys.argv[1:] + signal.alarm(50) + with tempfile.TemporaryDirectory(prefix='yasl-reconnect-') as directory: + root = Path(directory) + env = os.environ.copy() + env.pop('WAYLAND_SOCKET', None) + env.update(XDG_RUNTIME_DIR=directory, WAYLAND_DISPLAY='yasl-test', LIBGL_ALWAYS_SOFTWARE='1') + lavapipe = next(Path('/usr/share/vulkan/icd.d').glob('*lvp*.json'), None) + if lavapipe: + env['VK_DRIVER_FILES'] = str(lavapipe) + with (root/'weston.log').open('w+') as log: + servers = [start_weston(weston, root, env, log)] + client = subprocess.Popen([binary], env=env, stdout=subprocess.PIPE, text=True) + try: + expect(client, 'ready') + servers[0].send_signal(signal.SIGKILL) + servers[0].wait() + expect(client, 'lost') + time.sleep(0.5) + servers.append(start_weston(weston, root, env, log)) + expect(client, 'restored') + if client.wait(timeout=15) != 0: + raise RuntimeError(f'client exited with {client.returncode}') + except BaseException: + client.kill() + log.flush(); log.seek(0); print(log.read(), file=sys.stderr) + raise + finally: + for server in servers: + if server.poll() is None: + server.terminate() + server.wait(timeout=5) + + +if __name__ == '__main__': + main() diff --git a/tests/meson.build b/tests/meson.build index 4cbf082..d61d094 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -93,6 +93,11 @@ if get_option('integration_tests') xwayland = find_program('Xwayland') test('surface-backends', python, args: [files('helpers/run_surfaces.py'), weston, xwayland, integration, hello_example, tablet_example, gl_integration, vulkan_integration, notepad_example], timeout: 120) + if has_wayland and has_egl + reconnect = executable('yasl-reconnect-test', 'wayland/test_reconnect.c', dependencies: yasl_dep, + c_args: ['-DTEST_VULKAN=' + (has_vulkan ? '1' : '0')]) + test('reconnect-wayland', python, args: [files('helpers/run_reconnect.py'), weston, reconnect], timeout: 60) + endif elif has_cocoa test('surface-cocoa', integration, args: ['cocoa'], timeout: 30) foreach unit : ['native', 'ownership', 'keyboard', 'tablet', 'cursor'] diff --git a/tests/wayland/test_reconnect.c b/tests/wayland/test_reconnect.c new file mode 100644 index 0000000..0c42043 --- /dev/null +++ b/tests/wayland/test_reconnect.c @@ -0,0 +1,118 @@ +/* Compositor restart: run by helpers/run_reconnect.py, which kills the compositor + * after "ready" and starts a new one after "lost". Windows must survive, keep + * their pixels, and accept new GL and Vulkan objects after DISPLAY_RESTORED. */ +#include +#include +#include +#if TEST_VULKAN +#include +#endif +#include +#include +#include + +#define CHECK(cond, what) do { if (!(cond)) { const char *why; yasl_get_error(&why); \ + fprintf(stderr, "reconnect: %s failed at line %d (%s)\n", what, __LINE__, why); exit(1); } } while (0) + +typedef void (*clear_color_fn)(float, float, float, float); +typedef void (*clear_fn)(unsigned); + +static yasl_surface *cpu, *gl; + +/* Waits up to timeout_ms for an event of the given type, failing on any CLOSE. */ +static yasl_event expect(enum yasl_event_type type, yasl_surface *surface, int timeout_ms) +{ + yasl_event e; + for (int waited = 0; waited < timeout_ms; waited += 50) { + CHECK(yasl_wait_event(&e, 50) >= 0, "wait"); + CHECK(e.type != YASL_EVENT_CLOSE, "keeping windows open"); + if (e.type == type && (!surface || e.surface == surface)) return e; + } + fprintf(stderr, "reconnect: timed out waiting for event %d\n", type); + exit(1); +} + +static void draw_gl(float red) +{ + CHECK(yasl_gl_make_current(gl), "binding GL"); + clear_color_fn clear_color = (clear_color_fn)yasl_gl_get_proc_address("glClearColor"); + clear_fn clear = (clear_fn)yasl_gl_get_proc_address("glClear"); + CHECK(clear_color && clear, "loading GL procedures"); + clear_color(red, 0.2f, 0.3f, 1); + clear(0x00004000u); + CHECK(yasl_gl_swap_buffers(gl), "swapping GL buffers"); +} + +#if TEST_VULKAN +static VkInstance instance; +static yasl_surface *vk; + +static VkSurfaceKHR vulkan_surface(void) +{ + VkSurfaceKHR out = VK_NULL_HANDLE; + CHECK(yasl_vulkan_create_surface(vk, instance, NULL, &out) == VK_SUCCESS, "creating Vulkan surface"); + return out; +} +#endif + +int main(void) +{ + CHECK(yasl_set_backend(YASL_BACKEND_WAYLAND), "selecting Wayland"); + cpu = yasl_surface_create("reconnect CPU", 160, 120, YASL_SURFACE_CPU); + CHECK(cpu, "creating CPU window"); + gl = yasl_gl_surface_create("reconnect GL", 160, 120, 0, NULL, NULL); + CHECK(gl, "creating GL window"); + + expect(YASL_EVENT_REFRESH, cpu, 5000); + int w, h; + uint32_t *pixels = yasl_cpu_pixels(cpu, &w, &h); + CHECK(pixels, "getting pixels"); + for (size_t i = 0; i < (size_t)w * h; i++) pixels[i] = 0xff336699u; + CHECK(yasl_cpu_present(cpu), "presenting"); + draw_gl(0.8f); + +#if TEST_VULKAN + CHECK(yasl_vulkan_init(vkGetInstanceProcAddr), "loading Vulkan"); + vk = yasl_surface_create("reconnect Vulkan", 160, 120, 0); + CHECK(vk, "creating Vulkan window"); + uint32_t count = 0; + const char *const *extensions = yasl_vulkan_instance_extensions(vk, &count); + VkInstanceCreateInfo info = {.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO, + .enabledExtensionCount = count, .ppEnabledExtensionNames = extensions}; + CHECK(vkCreateInstance(&info, NULL, &instance) == VK_SUCCESS, "creating Vulkan instance"); + VkSurfaceKHR surface = vulkan_surface(); +#endif + + puts("ready"); + fflush(stdout); + + expect(YASL_EVENT_DISPLAY_LOST, NULL, 10000); +#if TEST_VULKAN + vkDestroySurfaceKHR(instance, surface, NULL); +#endif + CHECK(!yasl_surface_create("offline", 64, 64, YASL_SURFACE_CPU), "refusing windows while offline"); + CHECK(yasl_get_error(NULL) == YASL_ERROR_DISCONNECTED, "reporting the disconnect"); + puts("lost"); + fflush(stdout); + + expect(YASL_EVENT_DISPLAY_RESTORED, NULL, 15000); + expect(YASL_EVENT_REFRESH, cpu, 5000); + int rw, rh; + pixels = yasl_cpu_pixels(cpu, &rw, &rh); + CHECK(pixels && rw == w && rh == h, "keeping the window size"); + CHECK(pixels[0] == 0xff336699u && pixels[(size_t)w * h - 1] == 0xff336699u, "keeping the pixels"); + CHECK(yasl_cpu_present(cpu), "presenting after reconnect"); + draw_gl(0.1f); +#if TEST_VULKAN + surface = vulkan_surface(); + vkDestroySurfaceKHR(instance, surface, NULL); + vkDestroyInstance(instance, NULL); +#endif + + /* A second window proves the new connection accepts creation. */ + CHECK(yasl_surface_create("after reconnect", 64, 64, YASL_SURFACE_CPU), "creating a window after reconnect"); + yasl_gl_make_current(NULL); + yasl_shutdown(); + puts("restored"); + return 0; +} -- cgit v1.2.3