Some perf optimizations for blank screens.
This commit is contained in:
+125
-28
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user