Some perf optimizations for blank screens.

This commit is contained in:
Greyson Parrelli
2026-08-14 22:18:40 -04:00
parent b50e2f4108
commit 9df3d35422
+125 -28
View File
@@ -65,6 +65,22 @@ session: *Session,
area: *gtk.DrawingArea,
font: *pango.FontDescription,
/// The font description for each combination of bold and italic, indexed by
/// `fontIndex`. Pango re-itemizes a layout whenever its description changes,
/// so the four we can actually ask for are built once and swapped between
/// rather than mutating one in place per run.
fonts: [4]*pango.FontDescription,
/// The Pango layout runs are drawn through, kept across frames. Building one
/// per frame throws away Pango's internal state along with it; a live one only
/// has to be re-pointed at the current Cairo context.
layout: ?*pango.Layout = null,
/// Which of `fonts` the layout currently holds, so an unchanged style between
/// runs doesn't re-set it. Reset per frame — the layout outlives the frame but
/// this tracks what was set on it, and a fresh layout has none of them.
font_idx: ?usize = null,
/// Cell geometry derived from the font metrics.
cell_w: f64 = 8,
cell_h: f64 = 16,
@@ -83,6 +99,18 @@ gesture: vt.SelectionGesture = .init,
/// Scratch buffer for building the UTF-8 of a single text run.
run_buf: std.ArrayListUnmanaged(u8) = .empty,
/// Whether the run built so far is blank — nothing but spaces and empty
/// cells. A blank run puts no ink on the surface, so it can skip Pango
/// entirely unless it carries a decoration, and most of a terminal row is
/// blank.
run_blank: bool = true,
/// One row's cells resolved to their final appearance, rebuilt per row and
/// read by both render passes. Resolving a style means a lookup in the page's
/// style table, and doing it per pass per cell meant paying for the same
/// answer about three times over.
row_buf: std.ArrayListUnmanaged(Appearance) = .empty,
/// Called when the session's title changes, so the owner can retitle the tab.
on_title: *const fn (ctx: ?*anyopaque, title: []const u8) void,
on_exit: *const fn (ctx: ?*anyopaque) void,
@@ -173,12 +201,24 @@ pub fn create(
const area = gtk.DrawingArea.new();
const font = pango.FontDescription.fromString(font_spec);
errdefer font.free();
var fonts: [4]*pango.FontDescription = undefined;
var built: usize = 0;
errdefer for (fonts[0..built]) |d| d.free();
while (built < fonts.len) : (built += 1) {
const d = font.copy() orelse return error.OutOfMemory;
d.setWeight(if (built & bold_bit != 0) .bold else .normal);
d.setStyle(if (built & italic_bit != 0) .italic else .normal);
fonts[built] = d;
}
self.* = .{
.alloc = alloc,
.session = undefined,
.area = area,
.font = font,
.fonts = fonts,
.on_title = cbs.on_title,
.on_exit = cbs.on_exit,
.on_focus = cbs.on_focus,
@@ -290,6 +330,9 @@ pub fn destroy(self: *Terminal) void {
self.gesture.deinit(&self.session.term);
self.session.destroy();
self.run_buf.deinit(self.alloc);
self.row_buf.deinit(self.alloc);
if (self.layout) |layout| layout.unref();
for (self.fonts) |d| d.free();
self.font.free();
self.alloc.destroy(self);
}
@@ -857,6 +900,15 @@ fn onKeyPressed(
// -------------------------------------------------------------------------
// Rendering
/// Bit positions into `fonts`, which is indexed by the two font attributes a
/// run can vary by.
const bold_bit: usize = 1;
const italic_bit: usize = 2;
fn fontIndex(bold: bool, italic: bool) usize {
return (if (bold) bold_bit else 0) | (if (italic) italic_bit else 0);
}
/// A cell's appearance after resolving palette indices and SGR attributes.
const Appearance = struct {
fg: theme.Rgb,
@@ -956,9 +1008,18 @@ fn render(self: *Terminal, cr: *cairo.Context, _: c_int, _: c_int) !void {
else
theme.fg();
const layout = pangocairo.createLayout(cr);
defer layout.unref();
layout.setFontDescription(self.font);
// The layout outlives the frame, but it is bound to the Cairo context it
// was made from, so a returning one has to be re-pointed at this frame's.
const layout = self.layout orelse layout: {
const l = pangocairo.createLayout(cr);
self.layout = l;
break :layout l;
};
pangocairo.updateLayout(cr, layout);
// Nothing is set on the layout yet this frame, and `updateLayout` may have
// invalidated what was.
self.font_idx = null;
const bounds: ?SelectionBounds = bounds: {
const sel = screen.selection orelse break :bounds null;
@@ -984,15 +1045,18 @@ fn render(self: *Terminal, cr: *cairo.Context, _: c_int, _: c_int) !void {
break :span b.span(p.screen.y, cells.len);
} else null;
// Resolve the row once. Both passes below want the same answer for
// every cell, and working it out is the expensive half of drawing.
const looks = try self.rowAppearance(pin, cells, term, default_fg, default_bg, span);
// Pass 1: backgrounds. Drawn as one rect per run so that a wide
// block of color doesn't turn into hundreds of tiny fills.
var x: usize = 0;
while (x < cells.len) {
const start_bg = styled(pin, &cells[x], term, default_fg, default_bg, inSpan(span, x)).bg;
const start_bg = looks[x].bg;
var end = x + 1;
while (end < cells.len) : (end += 1) {
const next = styled(pin, &cells[end], term, default_fg, default_bg, inSpan(span, end)).bg;
if (!std.meta.eql(start_bg, next)) break;
if (!std.meta.eql(start_bg, looks[end].bg)) break;
}
if (start_bg) |color| {
@@ -1018,27 +1082,30 @@ fn render(self: *Terminal, cr: *cairo.Context, _: c_int, _: c_int) !void {
continue;
}
const look = styled(pin, &cells[x], term, default_fg, default_bg, inSpan(span, x));
const look = looks[x];
self.run_buf.clearRetainingCapacity();
self.run_blank = true;
const run_start = x;
while (x < cells.len) : (x += 1) {
const cell = &cells[x];
if (cell.wide == .spacer_tail) continue;
const cell_look = styled(pin, cell, term, default_fg, default_bg, inSpan(span, x));
if (x != run_start and !look.sameRun(cell_look)) break;
if (x != run_start and !look.sameRun(looks[x])) break;
try self.appendCell(pin, cell);
}
if (self.run_buf.items.len > 0) {
// A run of nothing but blanks puts no ink down, so the only reason
// to hand it to Pango is a decoration drawn across it.
const inked = !self.run_blank or look.underline or look.strikethrough;
if (self.run_buf.items.len > 0 and inked) {
try self.drawRun(
cr,
layout,
look,
pad + @as(f64, @floatFromInt(run_start)) * self.cell_w,
row_top,
x - run_start,
);
}
}
@@ -1047,6 +1114,27 @@ fn render(self: *Terminal, cr: *cairo.Context, _: c_int, _: c_int) !void {
self.drawCursor(cr, layout, term, default_bg);
}
/// Resolve every cell in a row to its final appearance, into the reusable row
/// buffer. Returned as a slice of that buffer, valid until the next row.
fn rowAppearance(
self: *Terminal,
pin: vt.Pin,
cells: []const vt.Cell,
term: *vt.Terminal,
default_fg: theme.Rgb,
default_bg: theme.Rgb,
span: ?[2]usize,
) ![]const Appearance {
self.row_buf.clearRetainingCapacity();
try self.row_buf.ensureTotalCapacity(self.alloc, cells.len);
for (cells, 0..) |*cell, x| {
self.row_buf.appendAssumeCapacity(
styled(pin, cell, term, default_fg, default_bg, inSpan(span, x)),
);
}
return self.row_buf.items;
}
/// Append a cell's text to the current run.
fn appendCell(self: *Terminal, pin: vt.Pin, cell: *const vt.Cell) !void {
switch (cell.content_tag) {
@@ -1070,6 +1158,7 @@ fn appendCell(self: *Terminal, pin: vt.Pin, cell: *const vt.Cell) !void {
fn appendCodepoint(self: *Terminal, cp: u21) !void {
var buf: [4]u8 = undefined;
const n = std.unicode.utf8Encode(cp, &buf) catch return;
if (cp != ' ') self.run_blank = false;
try self.run_buf.appendSlice(self.alloc, buf[0..n]);
}
@@ -1080,10 +1169,9 @@ fn drawRun(
look: Appearance,
x: f64,
y: f64,
cols: usize,
) !void {
self.font.setWeight(if (look.bold) .bold else .normal);
self.font.setStyle(if (look.italic) .italic else .normal);
layout.setFontDescription(self.font);
self.setFont(layout, fontIndex(look.bold, look.italic));
// Pango wants a NUL-terminated pointer even though we pass the length.
try self.run_buf.append(self.alloc, 0);
@@ -1096,23 +1184,29 @@ fn drawRun(
pangocairo.showLayout(cr, layout);
// Pango has no notion of our grid, so decorations are drawn by hand
// across the exact width of the run.
const run_w = runWidth(layout);
if (look.underline) {
cr.rectangle(x, y + self.ascent + 2, run_w, 1);
cr.fill();
}
if (look.strikethrough) {
cr.rectangle(x, y + self.ascent * 0.6, run_w, 1);
cr.fill();
// across the run. Their width is the columns the run covers, not the
// width Pango laid the text out to: asking Pango means forcing it to
// measure, and a run ending in a space would underline short of the cell.
if (look.underline or look.strikethrough) {
const run_w = @as(f64, @floatFromInt(cols)) * self.cell_w;
if (look.underline) {
cr.rectangle(x, y + self.ascent + 2, run_w, 1);
cr.fill();
}
if (look.strikethrough) {
cr.rectangle(x, y + self.ascent * 0.6, run_w, 1);
cr.fill();
}
}
}
fn runWidth(layout: *pango.Layout) f64 {
var w: c_int = 0;
var h: c_int = 0;
layout.getPixelSize(&w, &h);
return @floatFromInt(w);
/// Point the layout at one of the prebuilt font descriptions, if it isn't
/// already. Setting a description invalidates the layout, so runs that share
/// a style shouldn't keep re-setting the same one.
fn setFont(self: *Terminal, layout: *pango.Layout, idx: usize) void {
if (self.font_idx == idx) return;
layout.setFontDescription(self.fonts[idx]);
self.font_idx = idx;
}
fn drawCursor(
@@ -1154,6 +1248,9 @@ fn drawCursor(
var buf: [5]u8 = @splat(0);
const n = std.unicode.utf8Encode(cp, buf[0..4]) catch return;
// Explicitly regular: the layout still holds whichever font the
// last run left on it, which has nothing to do with this cell.
self.setFont(layout, fontIndex(false, false));
layout.setText(buf[0..n :0].ptr, @intCast(n));
const tr, const tg, const tb = default_bg.cairoRgb();