From 1fff449547a45b5c562b52aa62a4bc70c5dea556 Mon Sep 17 00:00:00 2001 From: Asmir A Date: Sat, 29 Aug 2026 15:27:31 +0200 Subject: [PATCH] optimize title and menubar redraw --- src/Output.zig | 55 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/Window.zig | 46 ++++++++++++++++++++++++++++------------- 2 files changed, 87 insertions(+), 14 deletions(-) diff --git a/src/Output.zig b/src/Output.zig index 82380f4..316545c 100644 --- a/src/Output.zig +++ b/src/Output.zig @@ -208,10 +208,22 @@ pub const TabBar = struct { rects: std.ArrayList(Box) = .empty, windows: std.ArrayList(*Window) = .empty, + /// The labels painted into the strip that is on screen, in tab order and + /// NUL separated, so a redraw can be skipped when the titles it would + /// paint are the ones already there. + label_cache: std.ArrayList(u8) = .empty, + /// Scratch holding the labels a pending redraw would paint. Swapped into + /// `label_cache` once they have actually been painted. + label_next: std.ArrayList(u8) = .empty, + /// The window highlighted in the strip that is on screen. + drawn_focused: ?*Window = null, + pub fn deinit(self: *TabBar) void { const gpa = self.wm.gpa; self.rects.deinit(gpa); self.windows.deinit(gpa); + self.label_cache.deinit(gpa); + self.label_next.deinit(gpa); if (self.pool) |*p| p.deinit(); if (self.node) |n| n.destroy(); if (self.shell) |s| s.destroy(); @@ -251,6 +263,24 @@ pub const TabBar = struct { } const gpa = self.wm.gpa; + + // Every retitle that lands on a tab drives a render sequence, and + // terminals retitle themselves constantly. Redrawing to produce a + // byte-identical strip would rasterise every glyph again and cost the + // compositor a full-width damage and a fresh texture upload, so + // compare against what is on screen first and leave the surface be + // when nothing visible moved. + self.label_next.clearRetainingCapacity(); + const labels_recorded = self.recordLabels(windows); + if (labels_recorded and self.mapped and + self.drawn_focused == focused and + std.meta.eql(self.box, box) and + std.mem.eql(*Window, self.windows.items, windows) and + std.mem.eql(u8, self.label_cache.items, self.label_next.items)) + { + return; + } + const pool = &self.pool.?; const buffer = pool.acquire(box.width, box.height) catch |err| { @@ -311,6 +341,29 @@ pub const TabBar = struct { self.node.?.setPosition(box.x, box.y); self.box = box; self.mapped = true; + + if (labels_recorded) { + std.mem.swap(std.ArrayList(u8), &self.label_cache, &self.label_next); + } else { + // Nothing trustworthy to compare against, so make sure the next + // call redraws rather than skipping on a stale cache. + self.label_cache.clearRetainingCapacity(); + } + self.drawn_focused = focused; + } + + /// Fill `label_next` with the labels a redraw would paint. Returns false + /// if they could not be recorded, in which case the caller redraws and + /// invalidates the cache rather than trusting it. + fn recordLabels(self: *TabBar, windows: []const *Window) bool { + const gpa = self.wm.gpa; + for (windows) |win| { + self.label_next.appendSlice(gpa, label(win)) catch return false; + // A separator, so that neighbouring labels cannot run together and + // compare equal to a different split of the same text. + self.label_next.append(gpa, 0) catch return false; + } + return true; } /// Unmap the bar. Must be called during a render sequence if it was @@ -324,6 +377,8 @@ pub const TabBar = struct { self.mapped = false; self.rects.clearRetainingCapacity(); self.windows.clearRetainingCapacity(); + self.label_cache.clearRetainingCapacity(); + self.drawn_focused = null; } /// What to write on a window's tab: its title, or its app id while it has diff --git a/src/Window.zig b/src/Window.zig index 7b06f3f..2a12de1 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -183,13 +183,29 @@ fn onTabBar(self: *Window) bool { return out.state().layout == .tabbed; } -fn setString(self: *Window, field: *?[]u8, value: ?[*:0]const u8) void { +/// Replace an owned string field, returning whether the value actually +/// changed. Clients re-send strings that have not moved — a terminal animating +/// a spinner in its title re-sets the same text several times a second — and +/// the caller can use the answer to skip the IPC broadcast and the tab bar +/// redraw those no-op updates would otherwise trigger. +fn setString(self: *Window, field: *?[]u8, value: ?[*:0]const u8) bool { const gpa = self.wm.gpa; + const new: ?[]const u8 = if (value) |v| std.mem.span(v) else null; + + const same = if (field.*) |old| + if (new) |n| std.mem.eql(u8, old, n) else false + else + new == null; + if (same) return false; + if (field.*) |old| gpa.free(old); field.* = null; - if (value) |v| { - field.* = gpa.dupe(u8, std.mem.span(v)) catch null; + if (new) |n| { + // A failed dupe leaves the field null, which compares unequal to the + // next event carrying the same text, so the copy is retried then. + field.* = gpa.dupe(u8, n) catch null; } + return true; } fn onEvent(_: *river.WindowV1, event: river.WindowV1.Event, self: *Window) void { @@ -227,23 +243,25 @@ fn onEvent(_: *river.WindowV1, event: river.WindowV1.Event, self: *Window) void }, .app_id => |ev| { - self.setString(&self.app_id, ev.app_id); - self.applyRules(); - self.wm.ipcDirty(); + if (self.setString(&self.app_id, ev.app_id)) { + self.applyRules(); + self.wm.ipcDirty(); + } }, .title => |ev| { - self.setString(&self.title, ev.title); - self.applyRules(); - self.wm.ipcDirty(); - // The tab bar has this title painted into it, so a window that is - // wearing a tab needs the strip redrawn. Terminals retitle - // themselves constantly, so ask only when it will show. - if (self.onTabBar()) self.wm.needsManage(); + if (self.setString(&self.title, ev.title)) { + self.applyRules(); + self.wm.ipcDirty(); + // The tab bar has this title painted into it, so a window that + // is wearing a tab needs the strip redrawn. Terminals retitle + // themselves constantly, so ask only when it will show. + if (self.onTabBar()) self.wm.needsManage(); + } }, .identifier => |ev| { - self.setString(&self.identifier, ev.identifier); + _ = self.setString(&self.identifier, ev.identifier); }, .parent => |ev| {