summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBerke Güzel <wenekar1@gmail.com>2026-10-13 23:52:22 +0300
committerBerke Güzel <wenekar1@gmail.com>2026-10-13 23:52:22 +0300
commit563b23a33726289387193b85d90fb569631ee05f (patch)
treeddf438338034b5ab2690b531684906999e56df44
parent76073fe5fad8799e5850805f2f090af2cb62c741 (diff)
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 <noreply@anthropic.com>
-rw-r--r--docs/getting-started.md9
-rw-r--r--examples/game.c17
-rw-r--r--examples/notepad.c4
-rw-r--r--examples/simple_window.c2
-rw-r--r--examples/tablet.c2
-rw-r--r--include/yasl/yasl.h10
-rw-r--r--src/common/backend.h1
-rw-r--r--src/common/surface.c4
-rw-r--r--src/common/types.h3
-rw-r--r--src/common/vulkan.c1
-rw-r--r--src/platform/wayland/backend.c82
-rw-r--r--src/platform/wayland/buffer.c8
-rw-r--r--src/platform/wayland/internal.h15
-rw-r--r--src/platform/wayland/pointer.c2
-rw-r--r--src/platform/wayland/surface.c347
-rw-r--r--src/platform/x11/window.c1
-rw-r--r--tests/helpers/run_reconnect.py67
-rw-r--r--tests/meson.build5
-rw-r--r--tests/wayland/test_reconnect.c118
19 files changed, 526 insertions, 172 deletions
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 <errno.h>
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, &registry_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, &registry_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 <weston> <client>."""
+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 <yasl/yasl.h>
+#include <yasl/cpu.h>
+#include <yasl/gl.h>
+#if TEST_VULKAN
+#include <yasl/vulkan.h>
+#endif
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#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;
+}