feat: keep hidden overlay-preview thumbnails live while held

Hidden tabs live in disabled scene trees, so
wlr_scene_output_send_frame_done() never reaches them and
frame-callback-throttled clients stop committing: only the shown
window's thumbnail tracked live while the rest sat at their last
frame.

Fan frame-done out to every hidden tab window once per output frame
while the overlay is held (overlay_send_hidden_frames after the scene
frame-done in both render_standalone and render_and_present). The
walk covers main plus subsurface trees for both xdg and X11 windows
via window_surface(), and skips the shown window the scene path
already handles.

Gated on --overlay-previews plus overlay_visible, so the steady state
with the menu hidden costs nothing and background clients are not
woken. The existing thumb_capture snapshot and draw-time tree
composite paths are unchanged; they simply receive the commits they
were missing.

Tests: new test_standalone_headless_overlay_previews_hidden_live uses
a hidden --pulse tab and asserts the red thumbnail count swings while
Alt is held. README overlay-previews paragraph updated from freeze to
live-while-held.

Verified: ./build.sh clean; overlay-previews suite 6 passed; full
suite 179 passed with -n4.
This commit is contained in:
Andrew Hamilton 2026-10-02 10:21:22 +10:00
parent b04f6a2826
commit 80e4e288ad
3 changed files with 71 additions and 4 deletions

View file

@ -266,10 +266,11 @@ past a quarter of the screen height. A header row at the top of the menu
names the selected window (icon plus ellipsized title) without affecting
the menu size. The
shown window keeps its thumbnail live -- it commits continuously, so the
preview tracks it. Hidden windows freeze with no frame callbacks while
another tab is shown, so their thumbnails sit at the last-shown frame
until they paint again on their own (a newly mapped window still appears
mid-hold). Both software (SHM) and GPU (DMA-BUF) clients are covered, and
preview tracks it. While the overlay is held, hidden windows are sent
frame callbacks too, so every thumbnail tracks live; with the overlay
hidden they freeze again (no background wakeups), sitting at the last
frame from the hold until they paint again on their own (a newly mapped
window still appears mid-hold). Both software (SHM) and GPU (DMA-BUF) clients are covered, and
a tab with no mapped window keeps its command-line text row. The plain
title list never wakes background clients this way.

36
kick.c
View file

@ -3086,6 +3086,40 @@ static void thumb_sub_wire_tree(struct kick_window *w) {
wlr_surface_for_each_surface(s, thumb_wire_iter, w);
}
/* Live thumbnails while the overlay is held: hidden windows sit in disabled
scene trees, so wlr_scene_output_send_frame_done() never reaches them and
frame-callback-throttled clients stop committing (only the shown window's
thumbnail tracks live). Fan frame-done out to every hidden tab window's
surface tree once per output frame while held, so each hidden client keeps
producing frames that thumb_capture() snapshots. Gated on
--overlay-previews + overlay_visible, so the steady state (menu hidden)
costs nothing. */
static void overlay_hidden_frame_iter(struct wlr_surface *surface, int sx,
int sy, void *data) {
(void)sx;
(void)sy;
struct timespec *now = data;
if (surface && now)
wlr_surface_send_frame_done(surface, now);
}
static void overlay_send_hidden_frames(struct timespec *now) {
if (!opt_overlay_previews || !host.overlay_visible || !now)
return;
struct kick_tab *t;
wl_list_for_each(t, &server.tabs, link) {
struct kick_window *w;
wl_list_for_each(w, &t->windows, link) {
if (!w->tree || w->tree->node.enabled)
continue; /* shown: scene path already sent frame-done */
struct wlr_surface *s = window_surface(w);
if (!s)
continue;
wlr_surface_for_each_surface(s, overlay_hidden_frame_iter, now);
}
}
}
static struct wlr_texture *overlay_thumb_tex(struct kick_window *w) {
if (!w || !server.renderer) return NULL;
if (w->thumb_src == THUMB_SRC_LIVE) {
@ -13127,6 +13161,7 @@ frame_done:
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
wlr_scene_output_send_frame_done(server.scene_output, &now);
overlay_send_hidden_frames(&now);
}
static void render_and_present(void) {
@ -13227,6 +13262,7 @@ frame_done:
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
wlr_scene_output_send_frame_done(server.scene_output, &now);
overlay_send_hidden_frames(&now);
wlr_output_state_finish(&state);
}

View file

@ -6855,3 +6855,33 @@ def test_standalone_headless_overlay_previews_live():
finally:
s.mods(0)
_assert_alive(s)
def test_standalone_headless_overlay_previews_hidden_live():
"""Hidden tabs keep live thumbnails while the overlay is held.
The hidden tab pulses red/black on frame callbacks, so it only
animates while the compositor fans frame-done out to hidden
windows. While Alt is held the red thumbnail pixel count must
swing between phases; without the fan-out it stays frozen."""
with _run_standalone(
{}, "--overlay-previews",
"%s --pulse 400 '#ff0000'" % WL,
"%s '#0000ff'" % WL, wait="any_frame") as s:
s.wait_surface_mapped(0)
s.wait_surface_mapped(1)
s.mods(ALT)
try:
reds = []
deadline = time.monotonic() + 12.0
while time.monotonic() < deadline:
red, _ = _dominant_counts(s, s.dump_host())
reds.append(red)
time.sleep(0.3)
if reds and max(reds) - min(reds) > 1500:
break
assert reds and max(reds) - min(reds) > 1500, \
f"hidden pulse thumbnail frozen while held: range={reds}"
finally:
s.mods(0)
_assert_alive(s)