refactor: paint held overlay at render time, drop poll machinery

Delete the content hash, painted-tracking, loop poll checks and
composite-next cover: the render paths already paint the overlay on
every rendered frame while held, so change detection was pure
overhead -- and polling from the frame handler wedged dispatch at
100% CPU.  Input bindings that mutate the visible menu (tab reorder,
member move, Alt+Shift+T) end in whole-damage plus redraw like any
other keybinding; everything else rides pre-existing commit, damage
and transition flows.  Keeps the persistent plane layer, probe with
sticky fallback, cursor parking during holds, and the per-frame
plane verdict log.
This commit is contained in:
Andrew Hamilton 2026-09-23 11:44:28 +10:00
parent 9ea77fde6e
commit 33a5f67c50

222
kick.c
View file

@ -403,11 +403,6 @@ struct kick_server {
} overlay_plane;
bool overlay_render_lock; /* attach-render lock held for the fallback */
bool overlay_cursor_lock; /* software-cursor lock held while up (DRM) */
/* One composite cover frame after a rejected plane frame (see
render_standalone): the next attempt retries the plane, so a
transient allocation shift costs one composited frame, not a stuck
menu. Cleared by the cover frame itself and on overlay hide. */
bool overlay_composite_next;
/* Standalone-only cursor: a real wlr_cursor so the pointer is visible
(hardware cursor plane on DRM, scene-composited on headless). */
struct wlr_cursor *cursor;
@ -1030,11 +1025,6 @@ static void apply_window_priorities(struct kick_window *active);
static void render_and_present(void);
static void overlay_sync_render_lock(void);
static void overlay_sync_cursor_lock(void);
/* Backstop for overlay staleness (see overlay_poll_check): hash what the
tab overlay would draw, and record what was last painted. */
static uint64_t overlay_content_hash(void);
static void overlay_note_painted(void);
static void overlay_poll_check(void);
static void layer_apply_layout_box(const struct wlr_box *full);
static void layer_apply_layout(void);
static void layer_raise_above_windows(void);
@ -2430,8 +2420,7 @@ static void handle_icon_set(struct wl_listener *listener, void *data) {
}
if (!ev->icon || wl_list_empty(&ev->icon->buffers)) {
/* Cleared (or empty): fall back to the app_id guesser on next
draw instead of keeping a stale texture. The per-frame poll
notices the icon change while the overlay is held. */
draw instead of keeping a stale texture. */
w->icon_resolved = false;
return;
}
@ -3101,14 +3090,8 @@ static void apply_pending_output_resize(struct wlr_output_state *state) {
static void handle_output_frame(struct wl_listener *listener, void *data) {
(void)listener; (void)data;
frame_pending = true;
/* NOTE: no overlay_poll_check() here by design. kick_request_redraw()
schedules through wlr_output_schedule_frame, which re-emits this very
frame event via an idle source: polling from inside the handler would
re-arm idle sources faster than wl_event_loop_dispatch_idle drains
them while any drift persists (the render gate that would clear it
never runs), wedging the loop at 100% CPU with dead stdin and
broken client sockets. The backstop lives in the main-loop bodies
instead, where each check is followed by the render gate. */
/* No scheduling from this handler: wlr_output_schedule_frame re-emits
this same event via idle, so scheduling here can wedge dispatch. */
}
/* How many standalone outputs have been seen since the last (re)connect.
@ -3136,7 +3119,6 @@ static void handle_output_destroy(struct wl_listener *listener, void *data) {
}
server.overlay_layer_state = (struct wlr_output_layer_state){0};
server.overlay_plane = OVERLAY_PLANE_UNKNOWN;
server.overlay_composite_next = false;
server.overlay_render_lock = false;
server.overlay_cursor_lock = false;
if (server.overlay_swapchain) {
@ -3744,10 +3726,9 @@ static void handle_toplevel_destroy(struct wl_listener *listener, void *data) {
w->toplevel = NULL;
w->tree = NULL;
/* The overlay's row for this tab disappears the moment the toplevel does,
shrinking the box mid-menu. The per-frame poll notices the smaller
box and repaints with whole-output damage, so no vacated ring stays
baked into a reused swapchain buffer. */
/* The overlay's row for this tab disappears the moment the toplevel
does, shrinking the box mid-menu; the destroy's own damage schedules
the repaint that shrinks it. */
window_detach_xdg_listeners(w);
window_destroy_common(w);
}
@ -4344,7 +4325,15 @@ static void reorder_window(struct kick_window *w, int dir) {
kick_log(KICK_LOG_INFO, "[member] moved app=%d slot %d -> %d\n",
tab_index(tab), from, to);
/* The held overlay picks the new order up via the per-frame poll. */
/* Input binding changed the visible order: repaint like any other
keybinding (whole damage keeps the composite coat clean). */
if (host.overlay_visible) {
if (server.standalone)
standalone_overlay_damage();
else if (server.scene_output)
wlr_damage_ring_add_whole(&server.scene_output->damage_ring);
kick_request_redraw();
}
}
/* Rotate the active tab's windows one step in `dir` (-1 = backward): the
@ -4979,7 +4968,7 @@ static void handle_xs_surface_map(struct wl_listener *listener, void *data) {
}
/* A newly mapped window of the shown tab joins outside the normal
commit-damage flow; force a full repaint, any backend. The held
overlay's new chip is picked up by the per-frame poll. */
overlay's new chip rides the same frame. */
if (server.active_tab == w->tab && server.scene_output)
wlr_damage_ring_add_whole(&server.scene_output->damage_ring);
}
@ -4995,8 +4984,7 @@ static void handle_xs_destroy(struct wl_listener *listener, void *data) {
w->tree = NULL;
}
/* Same overlay-shrink cleanup as handle_toplevel_destroy: the
per-frame poll notices and repaints. */
/* Same overlay-shrink cleanup as handle_toplevel_destroy. */
window_detach_x11_listeners(w);
window_destroy_common(w);
}
@ -5090,8 +5078,8 @@ static void handle_xs_request_close(struct wl_listener *listener, void *data) {
static void handle_xs_set_title(struct wl_listener *listener, void *data) {
(void)data;
struct kick_window *w = wl_container_of(listener, w, xs_set_title);
/* Picked up by the overlay row on its next draw (row_title cache),
which the per-frame poll schedules while the overlay is held. */
/* Picked up by the overlay row on its next draw (row_title cache);
the next repaint. */
(void)w;
if (w && w->ft_handle)
wlr_foreign_toplevel_handle_v1_set_title(w->ft_handle, foreign_str(window_title(w)));
@ -5107,8 +5095,8 @@ static void handle_xs_set_title(struct wl_listener *listener, void *data) {
static void handle_xs_set_class(struct wl_listener *listener, void *data) {
(void)data;
struct kick_window *w = wl_container_of(listener, w, xs_set_class);
/* Re-resolve the overlay icon against the new WM_CLASS; the held
overlay picks it up via the per-frame poll. */
/* Re-resolve the overlay icon against the new WM_CLASS; the
held menus repaint it on their next frame. */
w->icon_resolved = false;
if (w->ft_handle)
wlr_foreign_toplevel_handle_v1_set_app_id(w->ft_handle,
@ -6322,7 +6310,15 @@ static void reorder_active_tab(int dir) {
wl_list_insert(&tabs[to - 1]->link, &cur->link);
kick_log(KICK_LOG_INFO, "[reorder] app=%d dir=%d\n", to, dir);
/* The held overlay picks the new order up via the per-frame poll. */
/* Input binding changed the visible order: repaint like any other
keybinding (whole damage keeps the composite coat clean). */
if (host.overlay_visible) {
if (server.standalone)
standalone_overlay_damage();
else if (server.scene_output)
wlr_damage_ring_add_whole(&server.scene_output->damage_ring);
kick_request_redraw();
}
}
/* Fixed keybind repeat: first repeat after 600ms, then every 40ms (25Hz).
@ -8217,7 +8213,7 @@ static void tag_try_group(struct kick_window *w) {
end the session (the joined tab still lives). */
if (wl_list_empty(&old->windows))
drop_tab(old);
/* The held overlay picks the regroup up via the per-frame poll. */
/* The regroup paints on the next frame while held. */
return;
}
}
@ -8244,7 +8240,7 @@ static void tag_apply(struct wlr_xdg_toplevel *toplevel, const char *tag,
unmapped; a description-only update never regroups. */
if (tag && w->tag)
tag_try_group(w);
/* The held overlay picks the tag up via the per-frame poll. */
/* The new tag paints on the next frame while held. */
return;
}
/* Pre-claim: stash until claim_xdg_window picks it up. */
@ -9354,102 +9350,12 @@ static void host_kbd_key(void *data, struct wl_keyboard *kbd, uint32_t serial,
path removed from the primary -- so the scene's damage trackers never learn
about its old pixels; a swapchain slot reused after the overlay was drawn
keeps the stale overlay baked in unless we repaint the whole frame on every
transition. Use this on every overlay show/hide and cursor motion;
mid-hold content changes are picked up by the per-frame poll, which
damages through this same helper when its hash drifts. */
transition. Use this on every overlay show/hide and cursor motion. */
static void standalone_overlay_damage(void) {
if (server.scene_output)
wlr_damage_ring_add_whole(&server.scene_output->damage_ring);
}
/* Content hash of exactly what draw_tab_overlay_pass would paint: row
count, box geometry, per-row active flag, and per chip the title bytes,
focus flag and icon presence (plus the pending command line for black
rows). The confirm-quit modal is deliberately excluded -- it stays on
its explicit damage path. Used to notice overlay drift at frame
decision points no mutation site remembered to refresh from. */
static uint64_t overlay_hash_mix(uint64_t h, const void *p, size_t n) {
const unsigned char *b = p;
for (size_t i = 0; i < n; i++) {
h ^= b[i];
h *= 1099511628211ULL;
}
return h;
}
static uint64_t overlay_hash_str(uint64_t h, const char *s) {
if (!s) s = "";
return overlay_hash_mix(h, s, strlen(s) + 1);
}
static uint64_t overlay_content_hash(void) {
uint64_t h = 1469598103934665603ULL;
struct overlay_box box;
int n_rows = overlay_tab_box(&box);
h = overlay_hash_mix(h, &n_rows, sizeof(n_rows));
h = overlay_hash_mix(h, &box, sizeof(box));
struct kick_tab *tab;
wl_list_for_each(tab, &server.tabs, link) {
if (!tab_overlay_row(tab)) continue;
bool active = tab == server.active_tab;
h = overlay_hash_mix(h, &active, sizeof(active));
struct kick_window *wins[32];
int n = 0;
struct kick_window *w;
wl_list_for_each(w, &tab->windows, link) {
if (!w->mapped) continue;
if (n >= 32) break;
wins[n++] = w;
}
h = overlay_hash_mix(h, &n, sizeof(n));
if (n == 0) {
struct kick_window *first = tab_first_window(tab);
const char *ct = (first && first->cmd && first->cmd[0])
? first->cmd : "pending";
h = overlay_hash_str(h, ct);
}
for (int k = 0; k < n; k++) {
char tb[256];
const char *t = overlay_window_title(wins[k], tb, sizeof(tb));
h = overlay_hash_str(h, t);
h = overlay_hash_mix(h, &wins[k]->focused, sizeof(bool));
bool has_icon = wins[k]->icon_wlr != NULL;
h = overlay_hash_mix(h, &has_icon, sizeof(has_icon));
}
}
return h;
}
/* What the overlay showed the last time a frame painted it, and whether
any such frame has happened since the overlay was shown. */
static uint64_t overlay_painted_hash;
static bool overlay_painted_valid;
/* Record that the just-rendered frame painted the current overlay content.
Called from the render paths right after the overlay paint lands (or the
plane layer commits), never on paths that skipped the paint. */
static void overlay_note_painted(void) {
overlay_painted_hash = overlay_content_hash();
overlay_painted_valid = true;
}
/* Per-frame backstop: if the overlay is held but its content drifted since
the last painted frame (a mutation site that scheduled nothing), force a
repaint. Runs at frame decision points -- output frame events (display
rate on DRM) and main-loop iterations -- so it costs one cheap string
walk when clean and a frame only on drift. The modal stays out: with
confirm_quit_pending set only its explicit damage path paints. */
static void overlay_poll_check(void) {
if (!host.overlay_visible || confirm_quit_pending) return;
if (!overlay_painted_valid) return;
if (overlay_content_hash() == overlay_painted_hash) return;
if (server.standalone) {
standalone_overlay_damage();
} else if (server.scene_output) {
wlr_damage_ring_add_whole(&server.scene_output->damage_ring);
}
kick_request_redraw();
}
/* set_title / set_app_id for xdg toplevels: refresh the overlay and push the
new title/app_id onto the taskbar handles. The event's data is the
toplevel, from which we recover the owning window. */
@ -9457,7 +9363,7 @@ static void handle_window_meta(struct wl_listener *l, void *data) {
(void)l;
struct wlr_xdg_toplevel *toplevel = data;
struct kick_window *w = window_for_toplevel(toplevel);
/* The held overlay picks the new title up via the per-frame poll. */
/* The new title paints on the next frame while held. */
if (w) {
foreign_sync_title(w);
foreign_sync_app_id(w);
@ -9504,11 +9410,10 @@ static void update_overlay_visible(bool alt) {
host.overlay_visible = alt;
kick_log(KICK_LOG_DEBUG, "[render] overlay %s\n",
alt ? "show" : "hide");
/* A new hold paints fresh content on its first frame; until then the
poll backstop must not compare against a previous hold. A stale
composite cover flag dies here too so the hold starts on the plane. */
overlay_painted_valid = false;
server.overlay_composite_next = false;
/* A rejected plane goes back to UNKNOWN on the next hold so the fresh
allocation is probed again instead of staying composited forever. */
if (alt && server.overlay_plane == OVERLAY_PLANE_OFF)
server.overlay_plane = OVERLAY_PLANE_UNKNOWN;
/* Scanout lock ownership: when the overlay rides a hardware plane the lock
must stay off so direct scanout of the focused client keeps running
underneath; otherwise it must be held for the whole hold so the second
@ -12271,11 +12176,10 @@ static void render_standalone(void) {
struct overlay_box box;
bool have_box = false;
struct wlr_buffer *plane_buf = NULL;
/* A rejected plane frame sets composite_next for one composite cover
frame, then retries the plane: with a stable allocation rejects are
transient, so flap back instead of giving up. */
/* A rejected plane frame falls back to composite for the rest of the
hold; the next hold re-probes (see update_overlay_visible). */
bool plane_attempt = overlay_up && overlay_plane_capable() &&
server.overlay_layer && !server.overlay_composite_next;
server.overlay_layer;
if (kick_log_level() >= KICK_LOG_DEBUG && server.standalone)
kick_log(KICK_LOG_DEBUG, "[diag] frame start t=%.0f vis=%d plane=%d\n",
kick_monotonic_ms(), overlay_up, plane_attempt);
@ -12368,7 +12272,6 @@ static void render_standalone(void) {
if (pass) {
draw_tab_overlay_pass(pass, 0, 0);
wlr_render_pass_submit(pass);
overlay_note_painted();
}
}
@ -12415,11 +12318,10 @@ static void render_standalone(void) {
/* One-shot probe: the accepted flag is only populated by a real
commit -- libliftoff never writes it on a test-only path -- so this
is the authoritative "does the hardware have a plane for us"
answer, and fixed hardware makes it the session answer. */
answer. */
if (server.overlay_layer_state.accepted) {
kick_log(KICK_LOG_DEBUG, "[render] overlay plane available\n");
server.overlay_plane = OVERLAY_PLANE_USED;
overlay_note_painted();
} else {
kick_log(KICK_LOG_DEBUG,
"[render] overlay plane not available, compositing\n");
@ -12428,24 +12330,15 @@ static void render_standalone(void) {
the menu appear anyway by re-rendering it composited. */
kick_request_redraw();
}
} else if (plane_attempt) {
if (server.overlay_layer_state.accepted) {
overlay_note_painted();
} else {
/* Transiently rejected (allocation shifted under us): cover
this frame composited, then retry the plane next frame.
The lock must be held for the cover frame, and the next
attempt reuses the settled layer. */
kick_log(KICK_LOG_DEBUG,
"[render] overlay plane rejected, compositing once\n");
server.overlay_composite_next = true;
overlay_set_render_lock(true);
kick_request_redraw();
}
}
if (server.overlay_composite_next && composite_overlay) {
/* The cover frame above painted composited: retry the plane. */
server.overlay_composite_next = false;
} else if (plane_attempt && !server.overlay_layer_state.accepted) {
/* Rejected mid-hold (allocation shifted under us): composite for
the rest of the hold so the menu never drops a frame. The next
hold re-probes. */
kick_log(KICK_LOG_DEBUG,
"[render] overlay plane rejected, compositing\n");
server.overlay_plane = OVERLAY_PLANE_OFF;
overlay_set_render_lock(true);
kick_request_redraw();
}
frame_done:
@ -12943,7 +12836,16 @@ static int add_tab(const char *cmd) {
w->pending_claim = true;
w->origin = ORIGIN_ADD_TAB;
launch_client(w);
/* The held overlay picks the new row up via the per-frame poll. */
/* Input binding added a visible row: repaint like any other
keybinding. (The fork/exec itself produces no commit, damage or
signal, so without this nothing would schedule the frame.) */
if (host.overlay_visible) {
if (server.standalone)
standalone_overlay_damage();
else if (server.scene_output)
wlr_damage_ring_add_whole(&server.scene_output->damage_ring);
kick_request_redraw();
}
return tab_index(tab);
}
@ -14231,7 +14133,6 @@ int main(int argc, char *argv[]) {
} while (ret == -1 && errno == EINTR);
wl_display_flush_clients(server.wl_display);
sweep_dead_startup();
overlay_poll_check();
if (server.output && (wlr_output_is_headless(server.output)
|| !server.output->frame_pending)
&& (needs_redraw || server.output->needs_frame)) {
@ -14292,7 +14193,6 @@ int main(int argc, char *argv[]) {
}
wl_display_flush_clients(server.wl_display);
sweep_dead_startup();
overlay_poll_check();
wl_display_dispatch_pending(host.display);
if (host.use_libdecor)
libdecor_dispatch(host.libdecor, 0);