avoid large shaper buffer allocation on every frame

This commit is contained in:
Mitchell Hashimoto
2022-09-07 20:27:36 -07:00
parent 98dff5a163
commit 97aef76501
2 changed files with 44 additions and 33 deletions

View File

@ -43,9 +43,10 @@ vbo: gl.Buffer,
texture: gl.Texture, texture: gl.Texture,
texture_color: gl.Texture, texture_color: gl.Texture,
/// The font atlas. /// The font structures.
font_lib: font.Library, font_lib: font.Library,
font_group: font.GroupCache, font_group: font.GroupCache,
font_shaper: font.Shaper,
/// Whether the cursor is visible or not. This is used to control cursor /// Whether the cursor is visible or not. This is used to control cursor
/// blinking. /// blinking.
@ -176,6 +177,12 @@ pub fn init(
}); });
errdefer font_group.deinit(alloc); errdefer font_group.deinit(alloc);
// Create the initial font shaper
var shape_buf = try alloc.alloc(font.Shaper.Cell, 1);
errdefer alloc.free(shape_buf);
var shaper = try font.Shaper.init(shape_buf);
errdefer shaper.deinit();
// Load all visible ASCII characters and build our cell width based on // Load all visible ASCII characters and build our cell width based on
// the widest character that we see. // the widest character that we see.
const metrics = try font_group.metrics(alloc); const metrics = try font_group.metrics(alloc);
@ -306,6 +313,7 @@ pub fn init(
.texture_color = tex_color, .texture_color = tex_color,
.font_lib = font_lib, .font_lib = font_lib,
.font_group = font_group, .font_group = font_group,
.font_shaper = shaper,
.cursor_visible = true, .cursor_visible = true,
.cursor_style = .box, .cursor_style = .box,
.background = .{ .r = 0, .g = 0, .b = 0 }, .background = .{ .r = 0, .g = 0, .b = 0 },
@ -314,6 +322,8 @@ pub fn init(
} }
pub fn deinit(self: *Grid) void { pub fn deinit(self: *Grid) void {
self.font_shaper.deinit();
self.alloc.free(self.font_shaper.cell_buf);
self.font_group.deinit(self.alloc); self.font_group.deinit(self.alloc);
self.font_lib.deinit(); self.font_lib.deinit();
@ -353,12 +363,6 @@ pub fn rebuildCells(self: *Grid, term: *Terminal) !void {
// We've written no data to the GPU, refresh it all // We've written no data to the GPU, refresh it all
self.gl_cells_written = 0; self.gl_cells_written = 0;
// Create a text shaper we'll use for the screen
var shape_buf = try self.alloc.alloc(font.Shaper.Cell, term.screen.cols * 2);
defer self.alloc.free(shape_buf);
var shaper = try font.Shaper.init(&self.font_group, shape_buf);
defer shaper.deinit();
// Build each cell // Build each cell
var rowIter = term.screen.rowIterator(.viewport); var rowIter = term.screen.rowIterator(.viewport);
var y: usize = 0; var y: usize = 0;
@ -366,9 +370,9 @@ pub fn rebuildCells(self: *Grid, term: *Terminal) !void {
defer y += 1; defer y += 1;
// Split our row into runs and shape each one. // Split our row into runs and shape each one.
var iter = shaper.runIterator(row); var iter = self.font_shaper.runIterator(&self.font_group, row);
while (try iter.next(self.alloc)) |run| { while (try iter.next(self.alloc)) |run| {
for (try shaper.shape(run)) |shaper_cell| { for (try self.font_shaper.shape(run)) |shaper_cell| {
assert(try self.updateCell( assert(try self.updateCell(
term, term,
row.getCell(shaper_cell.x), row.getCell(shaper_cell.x),
@ -621,6 +625,12 @@ pub fn setScreenSize(self: *Grid, dim: ScreenSize) !void {
// Recalculate the rows/columns. // Recalculate the rows/columns.
self.size.update(dim, self.cell_size); self.size.update(dim, self.cell_size);
// Update our shaper
var shape_buf = try self.alloc.alloc(font.Shaper.Cell, self.size.columns * 2);
errdefer self.alloc.free(shape_buf);
self.alloc.free(self.font_shaper.cell_buf);
self.font_shaper.cell_buf = shape_buf;
log.debug("screen size screen={} grid={}, cell={}", .{ dim, self.size, self.cell_size }); log.debug("screen size screen={} grid={}, cell={}", .{ dim, self.size, self.cell_size });
} }

View File

@ -17,9 +17,6 @@ const terminal = @import("../terminal/main.zig");
const log = std.log.scoped(.font_shaper); const log = std.log.scoped(.font_shaper);
/// The font group to use under the covers
group: *GroupCache,
/// The buffer used for text shaping. We reuse it across multiple shaping /// The buffer used for text shaping. We reuse it across multiple shaping
/// calls to prevent allocations. /// calls to prevent allocations.
hb_buf: harfbuzz.Buffer, hb_buf: harfbuzz.Buffer,
@ -29,9 +26,8 @@ cell_buf: []Cell,
/// The cell_buf argument is the buffer to use for storing shaped results. /// The cell_buf argument is the buffer to use for storing shaped results.
/// This should be at least the number of columns in the terminal. /// This should be at least the number of columns in the terminal.
pub fn init(group: *GroupCache, cell_buf: []Cell) !Shaper { pub fn init(cell_buf: []Cell) !Shaper {
return Shaper{ return Shaper{
.group = group,
.hb_buf = try harfbuzz.Buffer.create(), .hb_buf = try harfbuzz.Buffer.create(),
.cell_buf = cell_buf, .cell_buf = cell_buf,
}; };
@ -44,8 +40,8 @@ pub fn deinit(self: *Shaper) void {
/// Returns an iterator that returns one text run at a time for the /// Returns an iterator that returns one text run at a time for the
/// given terminal row. Note that text runs are are only valid one at a time /// given terminal row. Note that text runs are are only valid one at a time
/// for a Shaper struct since they share state. /// for a Shaper struct since they share state.
pub fn runIterator(self: *Shaper, row: terminal.Screen.Row) RunIterator { pub fn runIterator(self: *Shaper, group: *GroupCache, row: terminal.Screen.Row) RunIterator {
return .{ .shaper = self, .row = row }; return .{ .shaper = self, .group = group, .row = row };
} }
/// Shape the given text run. The text run must be the immediately previous /// Shape the given text run. The text run must be the immediately previous
@ -65,7 +61,7 @@ pub fn shape(self: *Shaper, run: TextRun) ![]Cell {
harfbuzz.Feature.fromString("liga").?, harfbuzz.Feature.fromString("liga").?,
}; };
const face = self.group.group.faceFromIndex(run.font_index); const face = run.group.group.faceFromIndex(run.font_index);
harfbuzz.shape(face.hb_font, self.hb_buf, hb_feats); harfbuzz.shape(face.hb_font, self.hb_buf, hb_feats);
// If our buffer is empty, we short-circuit the rest of the work // If our buffer is empty, we short-circuit the rest of the work
@ -114,12 +110,16 @@ pub const TextRun = struct {
/// The total number of cells produced by this run. /// The total number of cells produced by this run.
cells: u16, cells: u16,
/// The font group that built this run.
group: *GroupCache,
/// The font index to use for the glyphs of this run. /// The font index to use for the glyphs of this run.
font_index: Group.FontIndex, font_index: Group.FontIndex,
}; };
pub const RunIterator = struct { pub const RunIterator = struct {
shaper: *Shaper, shaper: *Shaper,
group: *GroupCache,
row: terminal.Screen.Row, row: terminal.Screen.Row,
i: usize = 0, i: usize = 0,
@ -174,18 +174,18 @@ pub const RunIterator = struct {
// Determine the font for this cell. We'll use fallbacks // Determine the font for this cell. We'll use fallbacks
// manually here to try replacement chars and then a space // manually here to try replacement chars and then a space
// for unknown glyphs. // for unknown glyphs.
const font_idx_opt = (try self.shaper.group.indexForCodepoint( const font_idx_opt = (try self.group.indexForCodepoint(
alloc, alloc,
if (cell.empty()) ' ' else cell.char, if (cell.empty()) ' ' else cell.char,
style, style,
presentation, presentation,
)) orelse (try self.shaper.group.indexForCodepoint( )) orelse (try self.group.indexForCodepoint(
alloc, alloc,
0xFFFD, 0xFFFD,
style, style,
.text, .text,
)) orelse )) orelse
try self.shaper.group.indexForCodepoint(alloc, ' ', style, .text); try self.group.indexForCodepoint(alloc, ' ', style, .text);
const font_idx = font_idx_opt.?; const font_idx = font_idx_opt.?;
//log.warn("char={x} idx={}", .{ cell.char, font_idx }); //log.warn("char={x} idx={}", .{ cell.char, font_idx });
if (j == self.i) current_font = font_idx; if (j == self.i) current_font = font_idx;
@ -216,6 +216,7 @@ pub const RunIterator = struct {
return TextRun{ return TextRun{
.offset = @intCast(u16, self.i), .offset = @intCast(u16, self.i),
.cells = @intCast(u16, j - self.i), .cells = @intCast(u16, j - self.i),
.group = self.group,
.font_index = current_font, .font_index = current_font,
}; };
} }
@ -236,7 +237,7 @@ test "run iterator" {
// Get our run iterator // Get our run iterator
var shaper = testdata.shaper; var shaper = testdata.shaper;
var it = shaper.runIterator(screen.getRow(.{ .screen = 0 })); var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }));
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |_| count += 1; while (try it.next(alloc)) |_| count += 1;
try testing.expectEqual(@as(usize, 1), count); try testing.expectEqual(@as(usize, 1), count);
@ -249,7 +250,7 @@ test "run iterator" {
try screen.testWriteString("ABCD EFG"); try screen.testWriteString("ABCD EFG");
var shaper = testdata.shaper; var shaper = testdata.shaper;
var it = shaper.runIterator(screen.getRow(.{ .screen = 0 })); var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }));
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |_| count += 1; while (try it.next(alloc)) |_| count += 1;
try testing.expectEqual(@as(usize, 1), count); try testing.expectEqual(@as(usize, 1), count);
@ -263,7 +264,7 @@ test "run iterator" {
// Get our run iterator // Get our run iterator
var shaper = testdata.shaper; var shaper = testdata.shaper;
var it = shaper.runIterator(screen.getRow(.{ .screen = 0 })); var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }));
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |_| { while (try it.next(alloc)) |_| {
count += 1; count += 1;
@ -295,7 +296,7 @@ test "shape" {
// Get our run iterator // Get our run iterator
var shaper = testdata.shaper; var shaper = testdata.shaper;
var it = shaper.runIterator(screen.getRow(.{ .screen = 0 })); var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }));
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
count += 1; count += 1;
@ -318,7 +319,7 @@ test "shape inconsolata ligs" {
try screen.testWriteString(">="); try screen.testWriteString(">=");
var shaper = testdata.shaper; var shaper = testdata.shaper;
var it = shaper.runIterator(screen.getRow(.{ .screen = 0 })); var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }));
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
count += 1; count += 1;
@ -335,7 +336,7 @@ test "shape inconsolata ligs" {
try screen.testWriteString("==="); try screen.testWriteString("===");
var shaper = testdata.shaper; var shaper = testdata.shaper;
var it = shaper.runIterator(screen.getRow(.{ .screen = 0 })); var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }));
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
count += 1; count += 1;
@ -360,7 +361,7 @@ test "shape emoji width" {
try screen.testWriteString("👍"); try screen.testWriteString("👍");
var shaper = testdata.shaper; var shaper = testdata.shaper;
var it = shaper.runIterator(screen.getRow(.{ .screen = 0 })); var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }));
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
count += 1; count += 1;
@ -394,7 +395,7 @@ test "shape emoji width long" {
// Get our run iterator // Get our run iterator
var shaper = testdata.shaper; var shaper = testdata.shaper;
var it = shaper.runIterator(screen.getRow(.{ .screen = 0 })); var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }));
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
count += 1; count += 1;
@ -425,7 +426,7 @@ test "shape variation selector VS15" {
// Get our run iterator // Get our run iterator
var shaper = testdata.shaper; var shaper = testdata.shaper;
var it = shaper.runIterator(screen.getRow(.{ .screen = 0 })); var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }));
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
count += 1; count += 1;
@ -456,7 +457,7 @@ test "shape variation selector VS16" {
// Get our run iterator // Get our run iterator
var shaper = testdata.shaper; var shaper = testdata.shaper;
var it = shaper.runIterator(screen.getRow(.{ .screen = 0 })); var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }));
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
count += 1; count += 1;
@ -484,7 +485,7 @@ test "shape with empty cells in between" {
// Get our run iterator // Get our run iterator
var shaper = testdata.shaper; var shaper = testdata.shaper;
var it = shaper.runIterator(screen.getRow(.{ .screen = 0 })); var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }));
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
count += 1; count += 1;
@ -516,7 +517,7 @@ test "shape Chinese characters" {
// Get our run iterator // Get our run iterator
var shaper = testdata.shaper; var shaper = testdata.shaper;
var it = shaper.runIterator(screen.getRow(.{ .screen = 0 })); var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }));
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
count += 1; count += 1;
@ -569,7 +570,7 @@ fn testShaper(alloc: Allocator) !TestShaper {
var cell_buf = try alloc.alloc(Cell, 80); var cell_buf = try alloc.alloc(Cell, 80);
errdefer alloc.free(cell_buf); errdefer alloc.free(cell_buf);
var shaper = try init(cache_ptr, cell_buf); var shaper = try init(cell_buf);
errdefer shaper.deinit(); errdefer shaper.deinit();
return TestShaper{ return TestShaper{