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:
parent
9ea77fde6e
commit
33a5f67c50
1 changed files with 61 additions and 161 deletions
222
kick.c
222
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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue