Improve terminal rendering performance.
This commit is contained in:
+197
-4
@@ -81,6 +81,30 @@ layout: ?*pango.Layout = null,
|
|||||||
/// this tracks what was set on it, and a fresh layout has none of them.
|
/// this tracks what was set on it, and a fresh layout has none of them.
|
||||||
font_idx: ?usize = null,
|
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 geometry derived from the font metrics.
|
||||||
cell_w: f64 = 8,
|
cell_w: f64 = 8,
|
||||||
cell_h: f64 = 16,
|
cell_h: f64 = 16,
|
||||||
@@ -331,6 +355,9 @@ pub fn destroy(self: *Terminal) void {
|
|||||||
self.session.destroy();
|
self.session.destroy();
|
||||||
self.run_buf.deinit(self.alloc);
|
self.run_buf.deinit(self.alloc);
|
||||||
self.row_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();
|
if (self.layout) |layout| layout.unref();
|
||||||
for (self.fonts) |d| d.free();
|
for (self.fonts) |d| d.free();
|
||||||
self.font.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 look = looks[x];
|
||||||
|
const style = fontIndex(look.bold, look.italic);
|
||||||
|
const scaled = self.scaledFont(layout, style);
|
||||||
|
|
||||||
self.run_buf.clearRetainingCapacity();
|
self.run_buf.clearRetainingCapacity();
|
||||||
|
self.glyph_buf.clearRetainingCapacity();
|
||||||
self.run_blank = true;
|
self.run_blank = true;
|
||||||
|
self.run_fast = scaled != null;
|
||||||
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];
|
||||||
@@ -1093,23 +1124,35 @@ fn render(self: *Terminal, cr: *cairo.Context, _: c_int, _: c_int) !void {
|
|||||||
if (x != run_start and !look.sameRun(looks[x])) break;
|
if (x != run_start and !look.sameRun(looks[x])) break;
|
||||||
|
|
||||||
try self.appendCell(pin, cell);
|
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
|
// 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;
|
const inked = !self.run_blank or look.underline or look.strikethrough;
|
||||||
if (self.run_buf.items.len > 0 and inked) {
|
if (self.run_buf.items.len > 0 and inked) {
|
||||||
|
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(
|
try self.drawRun(
|
||||||
cr,
|
cr,
|
||||||
layout,
|
layout,
|
||||||
look,
|
look,
|
||||||
pad + @as(f64, @floatFromInt(run_start)) * self.cell_w,
|
origin_x,
|
||||||
row_top,
|
row_top,
|
||||||
x - run_start,
|
x - run_start,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
self.drawCursor(cr, layout, term, default_bg);
|
self.drawCursor(cr, layout, term, default_bg);
|
||||||
}
|
}
|
||||||
@@ -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 {
|
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;
|
||||||
@@ -1187,7 +1261,48 @@ fn drawRun(
|
|||||||
// across the run. Their width is the columns the run covers, not the
|
// 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
|
// 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.
|
// measure, and a run ending in a space would underline short of the cell.
|
||||||
if (look.underline or look.strikethrough) {
|
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;
|
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);
|
||||||
@@ -1198,7 +1313,6 @@ fn drawRun(
|
|||||||
cr.fill();
|
cr.fill();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// Point the layout at one of the prebuilt font descriptions, if it isn't
|
/// 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
|
/// already. Setting a description invalidates the layout, so runs that share
|
||||||
@@ -1209,6 +1323,85 @@ fn setFont(self: *Terminal, layout: *pango.Layout, idx: usize) void {
|
|||||||
self.font_idx = idx;
|
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(
|
fn drawCursor(
|
||||||
self: *Terminal,
|
self: *Terminal,
|
||||||
cr: *cairo.Context,
|
cr: *cairo.Context,
|
||||||
|
|||||||
Reference in New Issue
Block a user