From 1ebc8129ecee3338ee8f2b63b56dc1d0af904036 Mon Sep 17 00:00:00 2001 From: taya Date: Sat, 25 Apr 2026 10:59:52 +0200 Subject: [PATCH] fix: keep XWayland frame callbacks alive on hidden workspaces Instead of disabling the scene node (which kills frame callbacks and stalls games at 0 FPS), move XWayland clients far offscreen while keeping the node enabled. Gated by xwayland_render_unfocused config option (default on). Fixes #867. --- src/animation/client.h | 43 +++++++++++++++++++++++++++++++++++---- src/animation/tag.h | 21 +++++++++++++++---- src/config/parse_config.h | 5 +++++ src/mango.c | 2 ++ 4 files changed, 63 insertions(+), 8 deletions(-) diff --git a/src/animation/client.h b/src/animation/client.h index e94f872a..c53ede39 100644 --- a/src/animation/client.h +++ b/src/animation/client.h @@ -8,6 +8,29 @@ void set_rect_size(struct wlr_scene_rect *rect, int32_t width, int32_t height) { wlr_scene_rect_set_size(rect, GEZERO(width), GEZERO(height)); } +/* Keep node enabled offscreen so XWayland frame callbacks keep flowing, + * preventing games from stalling at 0 FPS when on a hidden workspace. */ +#define XWAYLAND_OFFSCREEN_OFFSET (-100000) + +void xwayland_hide_offscreen(Client *c) { + if (!client_is_x11(c)) + return; + c->is_xwayland_hidden = true; + wlr_scene_node_set_enabled(&c->border->node, false); + wlr_scene_node_set_enabled(&c->shadow->node, false); + wlr_scene_node_set_position(&c->scene->node, XWAYLAND_OFFSCREEN_OFFSET, + XWAYLAND_OFFSCREEN_OFFSET); +} + +void xwayland_show_from_offscreen(Client *c) { + if (!client_is_x11(c) || !c->is_xwayland_hidden) + return; + c->is_xwayland_hidden = false; + wlr_scene_node_set_enabled(&c->border->node, true); + wlr_scene_node_set_enabled(&c->shadow->node, true); + wlr_scene_node_set_position(&c->scene->node, c->geom.x, c->geom.y); +} + enum corner_location set_client_corner_location(Client *c) { enum corner_location current_corner_location = CORNER_LOCATION_ALL; struct wlr_box target_geom = @@ -501,10 +524,18 @@ struct ivec2 clip_to_hide(Client *c, struct wlr_box *clip_box) { if ((clip_box->width + bw <= 0 || clip_box->height + bw <= 0) && (ISSCROLLTILED(c) || c->animation.tagouting || c->animation.tagining)) { c->is_clip_to_hide = true; - wlr_scene_node_set_enabled(&c->scene->node, false); + if (client_is_x11(c) && config.xwayland_render_unfocused) { + xwayland_hide_offscreen(c); + } else { + wlr_scene_node_set_enabled(&c->scene->node, false); + } } else if (c->is_clip_to_hide && VISIBLEON(c, c->mon)) { c->is_clip_to_hide = false; - wlr_scene_node_set_enabled(&c->scene->node, true); + if (client_is_x11(c) && config.xwayland_render_unfocused) { + xwayland_show_from_offscreen(c); + } else { + wlr_scene_node_set_enabled(&c->scene->node, true); + } } return offset; @@ -737,8 +768,12 @@ void client_animation_next_tick(Client *c) { if (c->animation.tagouting) { c->animation.tagouting = false; - wlr_scene_node_set_enabled(&c->scene->node, false); - client_set_suspended(c, true); + if (client_is_x11(c) && config.xwayland_render_unfocused) { + xwayland_hide_offscreen(c); + } else { + wlr_scene_node_set_enabled(&c->scene->node, false); + client_set_suspended(c, true); + } c->animation.tagouted = true; c->animation.current = c->geom; } diff --git a/src/animation/tag.h b/src/animation/tag.h index 18eef56a..311c32e4 100644 --- a/src/animation/tag.h +++ b/src/animation/tag.h @@ -33,8 +33,15 @@ void set_arrange_visible(Monitor *m, Client *c, bool want_animation) { if (!c->is_clip_to_hide || !ISTILED(c) || !is_scroller_layout(c->mon)) { c->is_clip_to_hide = false; - wlr_scene_node_set_enabled(&c->scene->node, true); - wlr_scene_node_set_enabled(&c->scene_surface->node, true); + /* For XWayland clients that were hidden offscreen, just restore + * border/shadow — the node stays enabled and position is fixed + * by resize() below. For non-XWayland, re-enable normally. */ + if (c->is_xwayland_hidden && config.xwayland_render_unfocused) { + xwayland_show_from_offscreen(c); + } else { + wlr_scene_node_set_enabled(&c->scene->node, true); + wlr_scene_node_set_enabled(&c->scene_surface->node, true); + } } client_set_suspended(c, false); @@ -89,7 +96,13 @@ void set_arrange_hidden(Monitor *m, Client *c, bool want_animation) { c->animation.tagining = false; set_tagout_animation(m, c); } else { - wlr_scene_node_set_enabled(&c->scene->node, false); - client_set_suspended(c, true); + /* For XWayland clients, hide offscreen instead of disabling the + * scene node to keep frame callbacks flowing. */ + if (client_is_x11(c) && config.xwayland_render_unfocused) { + xwayland_hide_offscreen(c); + } else { + wlr_scene_node_set_enabled(&c->scene->node, false); + client_set_suspended(c, true); + } } } diff --git a/src/config/parse_config.h b/src/config/parse_config.h index e02b5017..f38017ae 100644 --- a/src/config/parse_config.h +++ b/src/config/parse_config.h @@ -358,6 +358,7 @@ typedef struct { int32_t single_scratchpad; int32_t xwayland_persistence; + int32_t xwayland_render_unfocused; int32_t syncobj_enable; float drag_tile_refresh_interval; float drag_floating_refresh_interval; @@ -1414,6 +1415,8 @@ bool parse_option(Config *config, char *key, char *value) { config->single_scratchpad = atoi(value); } else if (strcmp(key, "xwayland_persistence") == 0) { config->xwayland_persistence = atoi(value); + } else if (strcmp(key, "xwayland_render_unfocused") == 0) { + config->xwayland_render_unfocused = atoi(value); } else if (strcmp(key, "syncobj_enable") == 0) { config->syncobj_enable = atoi(value); } else if (strcmp(key, "drag_tile_refresh_interval") == 0) { @@ -3157,6 +3160,7 @@ void override_config(void) { config.overviewgappi = CLAMP_INT(config.overviewgappi, 0, 1000); config.overviewgappo = CLAMP_INT(config.overviewgappo, 0, 1000); config.xwayland_persistence = CLAMP_INT(config.xwayland_persistence, 0, 1); + config.xwayland_render_unfocused = CLAMP_INT(config.xwayland_render_unfocused, 0, 1); config.syncobj_enable = CLAMP_INT(config.syncobj_enable, 0, 1); config.drag_tile_refresh_interval = CLAMP_FLOAT(config.drag_tile_refresh_interval, 1.0f, 16.0f); @@ -3314,6 +3318,7 @@ void set_value_default() { config.view_current_to_back = 0; config.single_scratchpad = 1; config.xwayland_persistence = 1; + config.xwayland_render_unfocused = 1; config.syncobj_enable = 0; config.drag_tile_refresh_interval = 8.0f; config.drag_floating_refresh_interval = 8.0f; diff --git a/src/mango.c b/src/mango.c index 85fc00ac..0074ce05 100644 --- a/src/mango.c +++ b/src/mango.c @@ -396,6 +396,7 @@ struct Client { pid_t pid; Client *swallowing, *swallowedby; bool is_clip_to_hide; + bool is_xwayland_hidden; bool drag_to_tile; bool scratchpad_switching_mon; bool fake_no_border; @@ -4070,6 +4071,7 @@ void init_client_properties(Client *c) { c->is_scratchpad_show = 0; c->need_float_size_reduce = 0; c->is_clip_to_hide = 0; + c->is_xwayland_hidden = 0; c->is_restoring_from_ov = 0; c->isurgent = 0; c->need_output_flush = 0;