Some perf optimizations for blank screens.
This commit is contained in:
+118
-21
@@ -65,6 +65,22 @@ session: *Session,
|
|||||||
area: *gtk.DrawingArea,
|
area: *gtk.DrawingArea,
|
||||||
font: *pango.FontDescription,
|
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 geometry derived from the font metrics.
|
||||||
cell_w: f64 = 8,
|
cell_w: f64 = 8,
|
||||||
cell_h: f64 = 16,
|
cell_h: f64 = 16,
|
||||||
@@ -83,6 +99,18 @@ gesture: vt.SelectionGesture = .init,
|
|||||||
/// Scratch buffer for building the UTF-8 of a single text run.
|
/// Scratch buffer for building the UTF-8 of a single text run.
|
||||||
run_buf: std.ArrayListUnmanaged(u8) = .empty,
|
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.
|
/// 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_title: *const fn (ctx: ?*anyopaque, title: []const u8) void,
|
||||||
on_exit: *const fn (ctx: ?*anyopaque) void,
|
on_exit: *const fn (ctx: ?*anyopaque) void,
|
||||||
@@ -173,12 +201,24 @@ pub fn create(
|
|||||||
|
|
||||||
const area = gtk.DrawingArea.new();
|
const area = gtk.DrawingArea.new();
|
||||||
const font = pango.FontDescription.fromString(font_spec);
|
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.* = .{
|
self.* = .{
|
||||||
.alloc = alloc,
|
.alloc = alloc,
|
||||||
.session = undefined,
|
.session = undefined,
|
||||||
.area = area,
|
.area = area,
|
||||||
.font = font,
|
.font = font,
|
||||||
|
.fonts = fonts,
|
||||||
.on_title = cbs.on_title,
|
.on_title = cbs.on_title,
|
||||||
.on_exit = cbs.on_exit,
|
.on_exit = cbs.on_exit,
|
||||||
.on_focus = cbs.on_focus,
|
.on_focus = cbs.on_focus,
|
||||||
@@ -290,6 +330,9 @@ pub fn destroy(self: *Terminal) void {
|
|||||||
self.gesture.deinit(&self.session.term);
|
self.gesture.deinit(&self.session.term);
|
||||||
self.session.destroy();
|
self.session.destroy();
|
||||||
self.run_buf.deinit(self.alloc);
|
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.font.free();
|
||||||
self.alloc.destroy(self);
|
self.alloc.destroy(self);
|
||||||
}
|
}
|
||||||
@@ -857,6 +900,15 @@ fn onKeyPressed(
|
|||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Rendering
|
// 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.
|
/// A cell's appearance after resolving palette indices and SGR attributes.
|
||||||
const Appearance = struct {
|
const Appearance = struct {
|
||||||
fg: theme.Rgb,
|
fg: theme.Rgb,
|
||||||
@@ -956,9 +1008,18 @@ fn render(self: *Terminal, cr: *cairo.Context, _: c_int, _: c_int) !void {
|
|||||||
else
|
else
|
||||||
theme.fg();
|
theme.fg();
|
||||||
|
|
||||||
const layout = pangocairo.createLayout(cr);
|
// The layout outlives the frame, but it is bound to the Cairo context it
|
||||||
defer layout.unref();
|
// was made from, so a returning one has to be re-pointed at this frame's.
|
||||||
layout.setFontDescription(self.font);
|
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 bounds: ?SelectionBounds = bounds: {
|
||||||
const sel = screen.selection orelse break :bounds null;
|
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);
|
break :span b.span(p.screen.y, cells.len);
|
||||||
} else null;
|
} 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
|
// Pass 1: backgrounds. Drawn as one rect per run so that a wide
|
||||||
// block of color doesn't turn into hundreds of tiny fills.
|
// block of color doesn't turn into hundreds of tiny fills.
|
||||||
var x: usize = 0;
|
var x: usize = 0;
|
||||||
while (x < cells.len) {
|
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;
|
var end = x + 1;
|
||||||
while (end < cells.len) : (end += 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, looks[end].bg)) break;
|
||||||
if (!std.meta.eql(start_bg, next)) break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (start_bg) |color| {
|
if (start_bg) |color| {
|
||||||
@@ -1018,27 +1082,30 @@ fn render(self: *Terminal, cr: *cairo.Context, _: c_int, _: c_int) !void {
|
|||||||
continue;
|
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_buf.clearRetainingCapacity();
|
||||||
|
self.run_blank = true;
|
||||||
const run_start = x;
|
const run_start = x;
|
||||||
while (x < cells.len) : (x += 1) {
|
while (x < cells.len) : (x += 1) {
|
||||||
const cell = &cells[x];
|
const cell = &cells[x];
|
||||||
if (cell.wide == .spacer_tail) continue;
|
if (cell.wide == .spacer_tail) continue;
|
||||||
|
if (x != run_start and !look.sameRun(looks[x])) break;
|
||||||
const cell_look = styled(pin, cell, term, default_fg, default_bg, inSpan(span, x));
|
|
||||||
if (x != run_start and !look.sameRun(cell_look)) break;
|
|
||||||
|
|
||||||
try self.appendCell(pin, cell);
|
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(
|
try self.drawRun(
|
||||||
cr,
|
cr,
|
||||||
layout,
|
layout,
|
||||||
look,
|
look,
|
||||||
pad + @as(f64, @floatFromInt(run_start)) * self.cell_w,
|
pad + @as(f64, @floatFromInt(run_start)) * self.cell_w,
|
||||||
row_top,
|
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);
|
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.
|
/// Append a cell's text to the current run.
|
||||||
fn appendCell(self: *Terminal, pin: vt.Pin, cell: *const vt.Cell) !void {
|
fn appendCell(self: *Terminal, pin: vt.Pin, cell: *const vt.Cell) !void {
|
||||||
switch (cell.content_tag) {
|
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 {
|
fn appendCodepoint(self: *Terminal, cp: u21) !void {
|
||||||
var buf: [4]u8 = undefined;
|
var buf: [4]u8 = undefined;
|
||||||
const n = std.unicode.utf8Encode(cp, &buf) catch return;
|
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]);
|
try self.run_buf.appendSlice(self.alloc, buf[0..n]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1080,10 +1169,9 @@ fn drawRun(
|
|||||||
look: Appearance,
|
look: Appearance,
|
||||||
x: f64,
|
x: f64,
|
||||||
y: f64,
|
y: f64,
|
||||||
|
cols: usize,
|
||||||
) !void {
|
) !void {
|
||||||
self.font.setWeight(if (look.bold) .bold else .normal);
|
self.setFont(layout, fontIndex(look.bold, look.italic));
|
||||||
self.font.setStyle(if (look.italic) .italic else .normal);
|
|
||||||
layout.setFontDescription(self.font);
|
|
||||||
|
|
||||||
// Pango wants a NUL-terminated pointer even though we pass the length.
|
// Pango wants a NUL-terminated pointer even though we pass the length.
|
||||||
try self.run_buf.append(self.alloc, 0);
|
try self.run_buf.append(self.alloc, 0);
|
||||||
@@ -1096,8 +1184,11 @@ fn drawRun(
|
|||||||
pangocairo.showLayout(cr, layout);
|
pangocairo.showLayout(cr, layout);
|
||||||
|
|
||||||
// Pango has no notion of our grid, so decorations are drawn by hand
|
// Pango has no notion of our grid, so decorations are drawn by hand
|
||||||
// across the exact width of the run.
|
// across the run. Their width is the columns the run covers, not the
|
||||||
const run_w = runWidth(layout);
|
// 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) {
|
if (look.underline) {
|
||||||
cr.rectangle(x, y + self.ascent + 2, run_w, 1);
|
cr.rectangle(x, y + self.ascent + 2, run_w, 1);
|
||||||
cr.fill();
|
cr.fill();
|
||||||
@@ -1107,12 +1198,15 @@ fn drawRun(
|
|||||||
cr.fill();
|
cr.fill();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn runWidth(layout: *pango.Layout) f64 {
|
/// Point the layout at one of the prebuilt font descriptions, if it isn't
|
||||||
var w: c_int = 0;
|
/// already. Setting a description invalidates the layout, so runs that share
|
||||||
var h: c_int = 0;
|
/// a style shouldn't keep re-setting the same one.
|
||||||
layout.getPixelSize(&w, &h);
|
fn setFont(self: *Terminal, layout: *pango.Layout, idx: usize) void {
|
||||||
return @floatFromInt(w);
|
if (self.font_idx == idx) return;
|
||||||
|
layout.setFontDescription(self.fonts[idx]);
|
||||||
|
self.font_idx = idx;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn drawCursor(
|
fn drawCursor(
|
||||||
@@ -1154,6 +1248,9 @@ fn drawCursor(
|
|||||||
|
|
||||||
var buf: [5]u8 = @splat(0);
|
var buf: [5]u8 = @splat(0);
|
||||||
const n = std.unicode.utf8Encode(cp, buf[0..4]) catch return;
|
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));
|
layout.setText(buf[0..n :0].ptr, @intCast(n));
|
||||||
|
|
||||||
const tr, const tg, const tb = default_bg.cairoRgb();
|
const tr, const tg, const tb = default_bg.cairoRgb();
|
||||||
|
|||||||
Reference in New Issue
Block a user