From 33a5f67c5097325f3441e282cd0721c7ff6dd501 Mon Sep 17 00:00:00 2001 From: Andrew Hamilton Date: Wed, 23 Sep 2026 11:44:28 +1000 Subject: [PATCH] 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. --- kick.c | 222 ++++++++++++++++----------------------------------------- 1 file changed, 61 insertions(+), 161 deletions(-) diff --git a/kick.c b/kick.c index a8e93b6..df6b72d 100644 --- a/kick.c +++ b/kick.c @@ -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);