mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 16:56:09 +03:00
avoid large shaper buffer allocation on every frame
This commit is contained in:
28
src/Grid.zig
28
src/Grid.zig
@ -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 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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{
|
||||||
|
Reference in New Issue
Block a user