From 379f06b1e8a5dd33a779076d1b02f4abe75be512 Mon Sep 17 00:00:00 2001 From: Consolatis <35009135+Consolatis@users.noreply.github.com> Date: Sat, 4 Apr 2026 16:58:05 +0200 Subject: [PATCH 1/4] [move-to-master] common/macros: change WLR_VERSION macro to runtime evaluation --- include/common/macros.h | 8 ++++++-- include/labwc.h | 2 ++ src/main.c | 7 +++++++ 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/include/common/macros.h b/include/common/macros.h index 25226261..f3ceee82 100644 --- a/include/common/macros.h +++ b/include/common/macros.h @@ -62,7 +62,11 @@ #define BOUNDED_INT(a) ((a) < INT_MAX && (a) > INT_MIN) #endif -#define LAB_WLR_VERSION_AT_LEAST(major, minor, micro) \ - (WLR_VERSION_NUM >= (((major) << 16) | ((minor) << 8) | (micro))) +#define _LAB_CALC_WLR_VERSION_NUM(major, minor, micro) (((major) << 16) | ((minor) << 8) | (micro)) + +#define LAB_WLR_VERSION_AT_LEAST(major, minor, micro) ( \ + server.wlr_version >= _LAB_CALC_WLR_VERSION_NUM(major, minor, micro)) + +#define LAB_WLR_VERSION_LOWER(major, minor, micro) (!LAB_WLR_VERSION_AT_LEAST(major, minor, micro)) #endif /* LABWC_MACROS_H */ diff --git a/include/labwc.h b/include/labwc.h index 511fdd65..dd42f3b3 100644 --- a/include/labwc.h +++ b/include/labwc.h @@ -148,6 +148,8 @@ struct seat { }; struct server { + uint32_t wlr_version; + struct wl_display *wl_display; struct wl_event_loop *wl_event_loop; /* Can be used for timer events */ struct wlr_renderer *renderer; diff --git a/src/main.c b/src/main.c index 373f4480..f36be071 100644 --- a/src/main.c +++ b/src/main.c @@ -6,6 +6,7 @@ #include #include "common/fd-util.h" #include "common/font.h" +#include "common/macros.h" #include "common/spawn.h" #include "config/rcxml.h" #include "config/session.h" @@ -164,6 +165,12 @@ main(int argc, char *argv[]) char *primary_client = NULL; enum wlr_log_importance verbosity = WLR_ERROR; + server.wlr_version = _LAB_CALC_WLR_VERSION_NUM( + wlr_version_get_major(), + wlr_version_get_minor(), + wlr_version_get_micro() + ); + int c; while (1) { int index = 0; From facf3856cbe6b3a2cea2e5bd15df105792a1a048 Mon Sep 17 00:00:00 2001 From: Consolatis <35009135+Consolatis@users.noreply.github.com> Date: Sun, 6 Jul 2025 01:04:55 +0200 Subject: [PATCH 2/4] toplevel-capture: partial initial implementation Missing: - xwayland child windows - xwayland unmanaged windows, e.g. popups / menus / ... - xdg child window positioning - xdg subsurfaces (test-case: mate-terminal settings listboxes) - xdg popup positioning --- include/labwc.h | 7 +++++ include/view.h | 7 +++++ src/foreign-toplevel/ext-foreign.c | 3 ++ src/server.c | 49 ++++++++++++++++++++++++++++++ src/view.c | 7 +++++ src/xdg-popup.c | 2 ++ src/xdg.c | 24 +++++++++++++++ src/xwayland.c | 1 + 8 files changed, 100 insertions(+) diff --git a/include/labwc.h b/include/labwc.h index dd42f3b3..bdaa7d30 100644 --- a/include/labwc.h +++ b/include/labwc.h @@ -189,6 +189,13 @@ struct server { struct wlr_xdg_toplevel_icon_manager_v1 *xdg_toplevel_icon_manager; struct wl_listener xdg_toplevel_icon_set_icon; + struct { + struct wlr_ext_foreign_toplevel_image_capture_source_manager_v1 *manager; + struct { + struct wl_listener new_request; + } on; + } toplevel_capture; + /* front to back order */ struct wl_list views; uint64_t next_view_creation_id; diff --git a/include/view.h b/include/view.h index c2764f94..cc1731b5 100644 --- a/include/view.h +++ b/include/view.h @@ -177,6 +177,12 @@ struct view { char *title; char *app_id; /* WM_CLASS for xwayland windows */ + struct { + struct wlr_scene *scene; + struct wlr_ext_image_capture_source_v1 *source; + struct wl_listener on_capture_source_destroy; + } capture; + bool mapped; bool been_mapped; uint64_t creation_id; @@ -318,6 +324,7 @@ struct xdg_toplevel_view { /* Events unique to xdg-toplevel views */ struct wl_listener set_app_id; struct wl_listener request_show_window_menu; + struct wl_listener set_parent; struct wl_listener new_popup; }; diff --git a/src/foreign-toplevel/ext-foreign.c b/src/foreign-toplevel/ext-foreign.c index 78774433..1dfa1d96 100644 --- a/src/foreign-toplevel/ext-foreign.c +++ b/src/foreign-toplevel/ext-foreign.c @@ -75,6 +75,9 @@ ext_foreign_toplevel_init(struct ext_foreign_toplevel *ext_toplevel, return; } + /* In support for ext-toplevel-capture */ + ext_toplevel->handle->data = view; + /* Client side requests */ ext_toplevel->on.handle_destroy.notify = handle_handle_destroy; wl_signal_add(&ext_toplevel->handle->events.destroy, &ext_toplevel->on.handle_destroy); diff --git a/src/server.c b/src/server.c index fd48efed..8d881849 100644 --- a/src/server.c +++ b/src/server.c @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-2.0-only #define _POSIX_C_SOURCE 200809L #include "config.h" +#include #include #include #include @@ -428,6 +429,39 @@ handle_renderer_lost(struct wl_listener *listener, void *data) wlr_renderer_destroy(old_renderer); } +static void +handle_toplevel_capture_source_destroy(struct wl_listener *listener, void *data) +{ + struct view *view = wl_container_of(listener, view, capture.on_capture_source_destroy); + assert(view->capture.source); + view->capture.source = NULL; + wl_list_remove(&listener->link); + wl_list_init(&listener->link); +} + +static void +handle_toplevel_capture_request(struct wl_listener *listener, void *data) +{ + struct wlr_ext_foreign_toplevel_image_capture_source_manager_v1_request *request = data; + struct view *view = request->toplevel_handle->data; + assert(view); + wlr_log(WLR_DEBUG, "Capturing toplevel %s", view->app_id); + + if (!view->capture.source) { + view->capture.source = wlr_ext_image_capture_source_v1_create_with_scene_node( + &view->capture.scene->tree.node, server.wl_event_loop, + server.allocator, server.renderer); + assert(view->capture.source); + + view->capture.on_capture_source_destroy.notify = + handle_toplevel_capture_source_destroy; + wl_signal_add(&view->capture.source->events.destroy, + &view->capture.on_capture_source_destroy); + } + wlr_ext_foreign_toplevel_image_capture_source_manager_v1_request_accept( + request, view->capture.source); +} + void server_init(void) { @@ -666,6 +700,19 @@ server_init(void) wlr_screencopy_manager_v1_create(server.wl_display); wlr_ext_image_copy_capture_manager_v1_create(server.wl_display, 1); wlr_ext_output_image_capture_source_manager_v1_create(server.wl_display, 1); + + server.toplevel_capture.manager = + wlr_ext_foreign_toplevel_image_capture_source_manager_v1_create( + server.wl_display, 1); + if (server.toplevel_capture.manager) { + server.toplevel_capture.on.new_request.notify = handle_toplevel_capture_request; + wl_signal_add(&server.toplevel_capture.manager->events.new_request, + &server.toplevel_capture.on.new_request); + } else { + /* Allow safe removal on shutdown */ + wl_list_init(&server.toplevel_capture.on.new_request.link); + } + wlr_data_control_manager_v1_create(server.wl_display); wlr_ext_data_control_manager_v1_create(server.wl_display, LAB_EXT_DATA_CONTROL_VERSION); @@ -800,6 +847,8 @@ server_finish(void) server.drm_lease_request.notify = NULL; } + wl_list_remove(&server.toplevel_capture.on.new_request.link); + wlr_backend_destroy(server.backend); wlr_allocator_destroy(server.allocator); diff --git a/src/view.c b/src/view.c index 1d43fcfa..4321285d 100644 --- a/src/view.c +++ b/src/view.c @@ -2482,6 +2482,10 @@ view_init(struct view *view) view->title = xstrdup(""); view->app_id = xstrdup(""); + + view->capture.scene = wlr_scene_create(); + view->capture.scene->restack_xwayland_surfaces = false; + wl_list_init(&view->capture.on_capture_source_destroy.link); } void @@ -2503,6 +2507,9 @@ view_destroy(struct view *view) wl_list_remove(&view->request_fullscreen.link); wl_list_remove(&view->set_title.link); wl_list_remove(&view->destroy.link); + wl_list_remove(&view->capture.on_capture_source_destroy.link); + + wlr_scene_node_destroy(&view->capture.scene->tree.node); if (view->foreign_toplevel) { foreign_toplevel_destroy(view->foreign_toplevel); diff --git a/src/xdg-popup.c b/src/xdg-popup.c index 04f82f9e..6db5e501 100644 --- a/src/xdg-popup.c +++ b/src/xdg-popup.c @@ -166,4 +166,6 @@ xdg_popup_create(struct view *view, struct wlr_xdg_popup *wlr_popup) node_descriptor_create(wlr_popup->base->surface->data, LAB_NODE_XDG_POPUP, view, /*data*/ NULL); + + wlr_scene_xdg_surface_create(&view->capture.scene->tree, wlr_popup->base); } diff --git a/src/xdg.c b/src/xdg.c index 5092ab7a..d2749170 100644 --- a/src/xdg.c +++ b/src/xdg.c @@ -30,6 +30,8 @@ #define LAB_XDG_SHELL_VERSION 6 #define CONFIGURE_TIMEOUT_MS 100 +static struct view *xdg_toplevel_view_get_root(struct view *view); + static struct xdg_toplevel_view * xdg_toplevel_view_from_view(struct view *view) { @@ -463,6 +465,7 @@ handle_destroy(struct wl_listener *listener, void *data) /* Remove xdg-shell view specific listeners */ wl_list_remove(&xdg_toplevel_view->set_app_id.link); wl_list_remove(&xdg_toplevel_view->request_show_window_menu.link); + wl_list_remove(&xdg_toplevel_view->set_parent.link); wl_list_remove(&xdg_toplevel_view->new_popup.link); wl_list_remove(&view->commit.link); @@ -566,6 +569,23 @@ handle_request_show_window_menu(struct wl_listener *listener, void *data) menu_open_root(menu, cursor->x, cursor->y); } +static void +handle_set_parent(struct wl_listener *listener, void *data) +{ + struct xdg_toplevel_view *xdg_toplevel_view = wl_container_of( + listener, xdg_toplevel_view, set_parent); + struct view *view = &xdg_toplevel_view->base; + struct view *view_root = xdg_toplevel_view_get_root(view); + if (view_root == view) { + return; + } + struct wlr_scene_node *node, *tmp; + wl_list_for_each_safe(node, tmp, &view->capture.scene->tree.children, link) { + wlr_log(WLR_INFO, "moving capture scene node to view_root"); + wlr_scene_node_reparent(node, &view_root->capture.scene->tree); + } +} + static void handle_set_title(struct wl_listener *listener, void *data) { @@ -1024,6 +1044,9 @@ handle_new_xdg_toplevel(struct wl_listener *listener, void *data) mappable_connect(&view->mappable, xdg_surface->surface, handle_map, handle_unmap); + struct view *root_view = xdg_toplevel_view_get_root(view); + wlr_scene_xdg_surface_create(&root_view->capture.scene->tree, xdg_surface); + struct wlr_xdg_toplevel *toplevel = xdg_surface->toplevel; CONNECT_SIGNAL(toplevel, view, destroy); CONNECT_SIGNAL(toplevel, view, request_move); @@ -1037,6 +1060,7 @@ handle_new_xdg_toplevel(struct wl_listener *listener, void *data) /* Events specific to XDG toplevel views */ CONNECT_SIGNAL(toplevel, xdg_toplevel_view, set_app_id); CONNECT_SIGNAL(toplevel, xdg_toplevel_view, request_show_window_menu); + CONNECT_SIGNAL(toplevel, xdg_toplevel_view, set_parent); CONNECT_SIGNAL(xdg_surface, xdg_toplevel_view, new_popup); wl_list_insert(&server.views, &view->link); diff --git a/src/xwayland.c b/src/xwayland.c index 967364c5..5838d412 100644 --- a/src/xwayland.c +++ b/src/xwayland.c @@ -783,6 +783,7 @@ handle_map(struct wl_listener *listener, void *data) view->content_tree = wlr_scene_subsurface_tree_create( view->scene_tree, view->surface); die_if_null(view->content_tree); + wlr_scene_subsurface_tree_create(&view->capture.scene->tree, view->surface); } wlr_scene_node_set_enabled(&view->content_tree->node, !view->shaded); From d9649d630a4adc752ca534c25bb2a8c068fd316d Mon Sep 17 00:00:00 2001 From: Consolatis <35009135+Consolatis@users.noreply.github.com> Date: Sat, 4 Apr 2026 17:15:33 +0200 Subject: [PATCH 3/4] toplevel-capture: work around missing frame events due to wlroots bug In wlroots 0.20.0 there is an issue where a capture scene removes outputs from a wlr_surface which belong to another scene. This in turn causes `wlr_scene_output_send_frame_done()` to fail for our main scene after a toplevel has been captured once, for example via grim -T. To work around the issue we look for views with a capture session, iterate over all its wlr_scene_buffers and if they have the primary output set (e.g. they are not completely covered / hidden) we send the frame event manually. As wlroots turns multiple frame events without a new callback registered by the client into no-ops this also doesn't result in duplicated frame events seen by the client. Note that the other way around also causes an issue which this workaround does not fix. When capturing a toplevel and then changing its visibility state on screen and then covering it up completely / hiding it the capture will stall instead. --- src/output.c | 53 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/src/output.c b/src/output.c index aea35062..4042cc1c 100644 --- a/src/output.c +++ b/src/output.c @@ -51,6 +51,36 @@ #include #endif +/* + * Partial workaround for toplevel capture on wlroots 0.20.0 + * Note that even with this workaround the capture itself might still stall. + * + * See https://gitlab.freedesktop.org/wlroots/wlroots/-/merge_requests/5315 + * + * TODO: Remove once we start tracking wlroots 0.21.x + * or labwc depends on wlroots >= 0.20.1 + */ + +#include + +static void +workaround_frame_done_iter(struct wlr_scene_buffer *buffer, int sx, int sy, void *data) +{ + if (!buffer->primary_output) { + /* Catches hidden views, e.g. completely covered or disabled */ + return; + } + + struct wlr_scene_surface *scene_surface = wlr_scene_surface_try_from_buffer(buffer); + if (!scene_surface) { + return; + } + + wlr_surface_send_frame_done(scene_surface->surface, (struct timespec *)data); +} + +/* Workaround for toplevel capture end */ + bool output_get_tearing_allowance(struct output *output) { @@ -121,6 +151,29 @@ handle_output_frame(struct wl_listener *listener, void *data) struct timespec now = { 0 }; clock_gettime(CLOCK_MONOTONIC, &now); wlr_scene_output_send_frame_done(output->scene_output, &now); + + /* + * Workaround for toplevel capture on wlroots 0.20.0 + * + * TODO: Remove once we start tracking wlroots 0.21.x + * or labwc depends on wlroots >= 0.20.1 + */ + if (LAB_WLR_VERSION_LOWER(0, 20, 1)) { + struct view *view; + wl_list_for_each(view, &server.views, link) { + if (view->capture.source) { + if (!view_on_output(view, output)) { + continue; + } + /* + * view might still be covered / disabled, + * but the iterator takes care of that. + */ + wlr_scene_node_for_each_buffer(&view->content_tree->node, + workaround_frame_done_iter, &now); + } + } + } } static void From d333ed5c9f6b88fe67aeae17eaf367b41ba76a09 Mon Sep 17 00:00:00 2001 From: Consolatis <35009135+Consolatis@users.noreply.github.com> Date: Sat, 4 Apr 2026 17:36:38 +0200 Subject: [PATCH 4/4] toplevel-capture: work around wlroots memory leak --- src/server.c | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/server.c b/src/server.c index 8d881849..189f0d15 100644 --- a/src/server.c +++ b/src/server.c @@ -460,6 +460,16 @@ handle_toplevel_capture_request(struct wl_listener *listener, void *data) } wlr_ext_foreign_toplevel_image_capture_source_manager_v1_request_accept( request, view->capture.source); + + if (LAB_WLR_VERSION_LOWER(0, 20, 1)) { + /* + * Work around a memory leak in wlroots. + * See https://gitlab.freedesktop.org/wlroots/wlroots/-/merge_requests/5328 + * + * TODO: remove once we start tracking wlroots 0.21.x or depend on >= 0.20.1 + */ + free(request); + } } void