Force whole-output repaint when the standalone overlay shows or clears

The standalone overlay is rendered into the scene output buffer in a second
pass outside the scene graph, so wlroots' damage tracking never learns about
its pixels; a reused swapchain buffer therefore keeps the stale overlay baked
in after Alt is released.  A transparent full-output sentinel rect is
transiently raised to the top so the reorder traversal repopulates its
full-screen visible region, then its size is bumped to re-damage that region
through the public scene API, and the z-order is restored so it never
intercepts pointer hit-testing.  Both swapchain slots then repaint within the
next two frames and the overlay fully clears on release.

The set_enabled-based alternatives produce no output damage (wlroots only
damages the post-change visible region, which is empty for a disabled rect),
and the sentinel's visible region is otherwise emptied by the opaque client
occluding the shared update region before it is reached.
This commit is contained in:
Andrew Hamilton 2026-08-16 11:33:54 +10:00
parent 98879be1fc
commit c4a4b73393
2 changed files with 104 additions and 20 deletions

View file

@ -123,6 +123,9 @@ struct tabbed_server {
struct wlr_output *output;
struct wlr_scene_output *scene_output;
struct wlr_scene_rect *bg_rect;
/* Transparent full-output sentinel used to force whole-output damage on
overlay transitions; see standalone_overlay_damage(). */
struct wlr_scene_rect *overlay_damage_rect;
struct wlr_seat *seat;
struct wlr_xdg_shell *xdg_shell;
struct wlr_keyboard keyboard;
@ -1676,6 +1679,18 @@ server.output = output;
if (server.bg_rect)
wlr_scene_node_lower_to_bottom(&server.bg_rect->node);
}
/* Standalone-only damage sentinel for the overlay; see
standalone_overlay_damage(). A fully transparent rect, sized to the
output and left enabled (it renders nothing). It sits at the bottom of
the z-order and is transiently raised inside standalone_overlay_damage()
to re-populate its visible region and force whole-output damage. */
if (server.overlay_damage_rect) {
wlr_scene_rect_set_size(server.overlay_damage_rect, w, h);
} else if (server.standalone) {
server.overlay_damage_rect = wlr_scene_rect_create(
&server.scene->tree, w, h, (float[]){ 0.0f, 0.0f, 0.0f, 0.0f });
}
}
static void handle_toplevel_destroy(struct wl_listener *listener, void *data) {
@ -3278,6 +3293,38 @@ static void host_kbd_key(void *data, struct wl_keyboard *kbd, uint32_t serial,
}
}
/* Standalone overlay visibility. The overlay is drawn into the scene output
buffer outside the scene graph, so the scene's damage trackers never learn
about its pixels; a swapchain buffer reused after the overlay was drawn
keeps the stale overlay baked in unless we force a repaint on transition.
Bumping the sentinel rect's size routes through wlr_scene_rect_set_size ->
wlr_scene_node_update() with NULL damage, which re-damages the node's whole
populated "visible" region, so both swapchain slots repaint within the next
two frames: the overlay fully appears on press and fully clears on release. */
static void standalone_overlay_damage(void) {
if (!server.output || !server.overlay_damage_rect) return;
struct wlr_scene_rect *r = server.overlay_damage_rect;
/* The sentinel's "visible" region is only kept populated while it is the
topmost sibling (the scene's damage traversal walks children in reverse
z-order, and an opaque client surface above it would empty the shared
update region before the sentinel is visited). Raise it, so the reorder
traversal populates its full-screen visible region, then bump its size
to re-damage that whole region; restore the z-order immediately so the
always-enabled transparent rect never intercepts pointer hit-testing. */
wlr_scene_node_raise_to_top(&r->node);
wlr_scene_rect_set_size(r, r->width, r->height - 1);
wlr_scene_rect_set_size(r, r->width, r->height);
wlr_scene_node_lower_to_bottom(&r->node);
}
static void update_overlay_visible(bool alt) {
if (alt == host.overlay_visible) return;
host.overlay_visible = alt;
if (server.standalone)
standalone_overlay_damage();
needs_redraw = true;
}
/* Shared key handling for both the host keyboard and native input devices:
tab-switch bindings + overlay visibility, with the caller forwarding the
key to the seat afterwards. `mods_depressed` uses the wl_keyboard modifier
@ -3300,10 +3347,8 @@ static bool process_key_event(uint32_t time, uint32_t key, uint32_t state,
? xkb_state_mod_index_is_active(server.xkb_state,
server.xkb_alt_mod, XKB_STATE_MODS_EFFECTIVE)
: (mods_depressed & (1u << 3)) != 0;
if (alt != host.overlay_visible) {
host.overlay_visible = alt;
needs_redraw = true;
}
if (alt != host.overlay_visible)
update_overlay_visible(alt);
char symbuf[32];
xkb_keysym_get_name(sym, symbuf, sizeof(symbuf));
tabbed_log(TABBED_LOG_DEBUG,
@ -3404,10 +3449,8 @@ static void process_modifiers(uint32_t mods_depressed, uint32_t mods_latched,
? xkb_state_mod_index_is_active(server.xkb_state,
server.xkb_alt_mod, XKB_STATE_MODS_EFFECTIVE)
: (mods_depressed & (1u << 3)) != 0;
if (alt != host.overlay_visible) {
host.overlay_visible = alt;
needs_redraw = true;
}
if (alt != host.overlay_visible)
update_overlay_visible(alt);
if (host.repeat_key && !alt)
kbd_repeat_cancel();
}