Improve terminal rendering performance.
This commit is contained in:
+212
-19
@@ -81,6 +81,30 @@ layout: ?*pango.Layout = null,
|
||||
/// this tracks what was set on it, and a fresh layout has none of them.
|
||||
font_idx: ?usize = null,
|
||||
|
||||
/// The loaded font behind each entry of `fonts`, resolved on first use and
|
||||
/// held for as long as we use its scaled font: the scaled font belongs to it,
|
||||
/// not to us, and dropping this reference would take that with it.
|
||||
loaded: [4]?*pango.Font = @splat(null),
|
||||
|
||||
/// The Cairo scaled font behind each entry of `fonts`, resolved on first use.
|
||||
/// Drawing glyphs through Cairo directly skips Pango's itemization and
|
||||
/// shaping, which is almost the entire cost of a frame — see `drawGlyphRun`.
|
||||
scaled: [4]?*cairo.ScaledFont = @splat(null),
|
||||
|
||||
/// Codepoint to glyph index, per font style. A terminal draws the same few
|
||||
/// dozen characters over and over, so the map fills up almost immediately and
|
||||
/// then never misses. A zero index means the font has no glyph for it, which
|
||||
/// is the signal to fall back to Pango rather than draw a notdef box.
|
||||
glyphs: [4]std.AutoHashMapUnmanaged(u21, c_ulong) = @splat(.empty),
|
||||
|
||||
/// Scratch buffer for the glyphs of a single run, positioned on the grid.
|
||||
glyph_buf: std.ArrayListUnmanaged(cairo.Glyph) = .empty,
|
||||
|
||||
/// Whether every cell in the run so far can be drawn from `glyphs`. Cleared
|
||||
/// by anything the fast path can't place on the grid by itself: a character
|
||||
/// the font lacks, or a cell whose text is more than one codepoint.
|
||||
run_fast: bool = true,
|
||||
|
||||
/// Cell geometry derived from the font metrics.
|
||||
cell_w: f64 = 8,
|
||||
cell_h: f64 = 16,
|
||||
@@ -331,6 +355,9 @@ pub fn destroy(self: *Terminal) void {
|
||||
self.session.destroy();
|
||||
self.run_buf.deinit(self.alloc);
|
||||
self.row_buf.deinit(self.alloc);
|
||||
self.glyph_buf.deinit(self.alloc);
|
||||
for (&self.glyphs) |*m| m.deinit(self.alloc);
|
||||
for (self.loaded) |f| if (f) |font| font.as(gobject.Object).unref();
|
||||
if (self.layout) |layout| layout.unref();
|
||||
for (self.fonts) |d| d.free();
|
||||
self.font.free();
|
||||
@@ -1083,9 +1110,13 @@ fn render(self: *Terminal, cr: *cairo.Context, _: c_int, _: c_int) !void {
|
||||
}
|
||||
|
||||
const look = looks[x];
|
||||
const style = fontIndex(look.bold, look.italic);
|
||||
const scaled = self.scaledFont(layout, style);
|
||||
|
||||
self.run_buf.clearRetainingCapacity();
|
||||
self.glyph_buf.clearRetainingCapacity();
|
||||
self.run_blank = true;
|
||||
self.run_fast = scaled != null;
|
||||
const run_start = x;
|
||||
while (x < cells.len) : (x += 1) {
|
||||
const cell = &cells[x];
|
||||
@@ -1093,20 +1124,32 @@ fn render(self: *Terminal, cr: *cairo.Context, _: c_int, _: c_int) !void {
|
||||
if (x != run_start and !look.sameRun(looks[x])) break;
|
||||
|
||||
try self.appendCell(pin, cell);
|
||||
if (self.run_fast) try self.appendGlyph(
|
||||
scaled.?,
|
||||
style,
|
||||
cell,
|
||||
pad + @as(f64, @floatFromInt(x)) * self.cell_w,
|
||||
row_top + self.ascent,
|
||||
);
|
||||
}
|
||||
|
||||
// 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.
|
||||
// to draw it at all is a decoration 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,
|
||||
);
|
||||
const origin_x = pad + @as(f64, @floatFromInt(run_start)) * self.cell_w;
|
||||
if (self.run_fast) {
|
||||
self.drawGlyphRun(cr, scaled.?, look, origin_x, row_top, x - run_start);
|
||||
} else {
|
||||
try self.drawRun(
|
||||
cr,
|
||||
layout,
|
||||
look,
|
||||
origin_x,
|
||||
row_top,
|
||||
x - run_start,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1155,6 +1198,37 @@ fn appendCell(self: *Terminal, pin: vt.Pin, cell: *const vt.Cell) !void {
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a cell's glyph to the run being built, positioned on the grid rather
|
||||
/// than after whatever came before it. Clears `run_fast` if this cell is one
|
||||
/// the fast path can't speak for, which sends the whole run back to Pango.
|
||||
fn appendGlyph(
|
||||
self: *Terminal,
|
||||
scaled: *cairo.ScaledFont,
|
||||
style: usize,
|
||||
cell: *const vt.Cell,
|
||||
x: f64,
|
||||
baseline: f64,
|
||||
) !void {
|
||||
// Only a lone codepoint can be placed by index alone. A grapheme has
|
||||
// combining marks to position, and a background-only cell has no text.
|
||||
if (cell.content_tag != .codepoint) {
|
||||
if (cell.content_tag == .codepoint_grapheme) self.run_fast = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const cp = cell.content.codepoint.data;
|
||||
// Empty cells and spaces are holes in the run, not glyphs. Skipping them
|
||||
// is what lets a blank stretch inside a run cost nothing to draw.
|
||||
if (cp == 0 or cp == ' ') return;
|
||||
|
||||
const index = self.glyphIndex(scaled, style, cp) orelse {
|
||||
self.run_fast = false;
|
||||
return;
|
||||
};
|
||||
|
||||
try self.glyph_buf.append(self.alloc, .{ .index = index, .x = x, .y = baseline });
|
||||
}
|
||||
|
||||
fn appendCodepoint(self: *Terminal, cp: u21) !void {
|
||||
var buf: [4]u8 = undefined;
|
||||
const n = std.unicode.utf8Encode(cp, &buf) catch return;
|
||||
@@ -1187,16 +1261,56 @@ fn drawRun(
|
||||
// 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();
|
||||
}
|
||||
self.drawDecorations(cr, look, x, y, cols);
|
||||
}
|
||||
|
||||
/// Draw a run straight from cached glyph indices.
|
||||
///
|
||||
/// This is the whole point of the glyph cache: Pango re-itemizes and re-shapes
|
||||
/// every run on every frame, and for a monospace grid both answers are already
|
||||
/// known — one glyph per cell, at a column we can compute. Handing Cairo the
|
||||
/// glyphs directly skips all of it, and placing each glyph on its own column
|
||||
/// also stops advances accumulating rounding error across a long row.
|
||||
fn drawGlyphRun(
|
||||
self: *Terminal,
|
||||
cr: *cairo.Context,
|
||||
scaled: *cairo.ScaledFont,
|
||||
look: Appearance,
|
||||
x: f64,
|
||||
y: f64,
|
||||
cols: usize,
|
||||
) void {
|
||||
const r, const g, const b = look.fg.cairoRgb();
|
||||
cr.setSourceRgb(r, g, b);
|
||||
|
||||
if (self.glyph_buf.items.len > 0) {
|
||||
cr.setScaledFont(scaled);
|
||||
cr.showGlyphs(self.glyph_buf.items.ptr, @intCast(self.glyph_buf.items.len));
|
||||
}
|
||||
|
||||
self.drawDecorations(cr, look, x, y, cols);
|
||||
}
|
||||
|
||||
/// Underline and strikethrough, drawn across the columns the run covers.
|
||||
/// Shared by both draw paths so they can't disagree about where a line goes.
|
||||
fn drawDecorations(
|
||||
self: *Terminal,
|
||||
cr: *cairo.Context,
|
||||
look: Appearance,
|
||||
x: f64,
|
||||
y: f64,
|
||||
cols: usize,
|
||||
) void {
|
||||
if (!look.underline and !look.strikethrough) return;
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1209,6 +1323,85 @@ fn setFont(self: *Terminal, layout: *pango.Layout, idx: usize) void {
|
||||
self.font_idx = idx;
|
||||
}
|
||||
|
||||
/// The Cairo scaled font for a style, loaded on first use. Resolved through
|
||||
/// the layout's own context so it comes out with the same font options — and
|
||||
/// so the same hinting and antialiasing — as the Pango path it stands in for.
|
||||
fn scaledFont(self: *Terminal, layout: *pango.Layout, idx: usize) ?*cairo.ScaledFont {
|
||||
if (self.scaled[idx]) |s| return s;
|
||||
|
||||
const font = layout.getContext().loadFont(self.fonts[idx]) orelse return null;
|
||||
|
||||
// Sound because of where the context came from: the layout was built by
|
||||
// `pangocairo.createLayout`, so its font map is a PangoCairoFontMap and
|
||||
// every font it hands out implements PangoCairoFont. A GObject interface
|
||||
// pointer is the instance pointer, so this is the cast that check implies.
|
||||
const cairo_font: *pangocairo.Font = @ptrCast(font);
|
||||
const s = cairo_font.getScaledFont() orelse {
|
||||
font.as(gobject.Object).unref();
|
||||
return null;
|
||||
};
|
||||
|
||||
self.loaded[idx] = font;
|
||||
self.scaled[idx] = s;
|
||||
return s;
|
||||
}
|
||||
|
||||
/// The glyph index for a codepoint in a style, or null if the font has no
|
||||
/// glyph for it and Pango has to handle the run instead.
|
||||
fn glyphIndex(self: *Terminal, scaled: *cairo.ScaledFont, idx: usize, cp: u21) ?c_ulong {
|
||||
const gop = self.glyphs[idx].getOrPut(self.alloc, cp) catch return null;
|
||||
if (!gop.found_existing) gop.value_ptr.* = lookupGlyph(scaled, cp);
|
||||
|
||||
// Zero is .notdef in every font, so it is both "no glyph" and the cached
|
||||
// form of "don't ask again".
|
||||
return if (gop.value_ptr.* == 0) null else gop.value_ptr.*;
|
||||
}
|
||||
|
||||
/// Declared here rather than used from the Cairo bindings: `num_glyphs` and
|
||||
/// `num_clusters` are `int *` out-parameters in Cairo, and the generated
|
||||
/// binding has them by value, so calling it as bound would have Cairo write
|
||||
/// through whatever address the integer happened to be.
|
||||
extern fn cairo_scaled_font_text_to_glyphs(
|
||||
scaled_font: *cairo.ScaledFont,
|
||||
x: f64,
|
||||
y: f64,
|
||||
utf8: [*]const u8,
|
||||
utf8_len: c_int,
|
||||
glyphs: *?[*]cairo.Glyph,
|
||||
num_glyphs: *c_int,
|
||||
clusters: ?*?[*]cairo.TextCluster,
|
||||
num_clusters: ?*c_int,
|
||||
cluster_flags: ?*cairo.TextClusterFlags,
|
||||
) cairo.Status;
|
||||
|
||||
fn lookupGlyph(scaled: *cairo.ScaledFont, cp: u21) c_ulong {
|
||||
var buf: [4]u8 = undefined;
|
||||
const n = std.unicode.utf8Encode(cp, &buf) catch return 0;
|
||||
|
||||
var out: ?[*]cairo.Glyph = null;
|
||||
var count: c_int = 0;
|
||||
const status = cairo_scaled_font_text_to_glyphs(
|
||||
scaled,
|
||||
0,
|
||||
0,
|
||||
&buf,
|
||||
@intCast(n),
|
||||
&out,
|
||||
&count,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
);
|
||||
defer if (out) |g| cairo.Glyph.free(g);
|
||||
|
||||
if (status != .success) return 0;
|
||||
const glyphs = out orelse return 0;
|
||||
// More than one glyph means the character decomposed, which the grid
|
||||
// can't place; treat it as uncovered and let Pango draw the run.
|
||||
if (count != 1) return 0;
|
||||
return glyphs[0].index;
|
||||
}
|
||||
|
||||
fn drawCursor(
|
||||
self: *Terminal,
|
||||
cr: *cairo.Context,
|
||||
|
||||
Reference in New Issue
Block a user