ghostty/src/font/GroupCache.zig
2022-09-29 10:30:45 -07:00

271 lines
8.6 KiB
Zig

//! A glyph cache sits on top of a Group and caches the results from it.
const GroupCache = @This();
const std = @import("std");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const Atlas = @import("../Atlas.zig");
const Face = @import("main.zig").Face;
const DeferredFace = @import("main.zig").DeferredFace;
const Library = @import("main.zig").Library;
const Glyph = @import("main.zig").Glyph;
const Style = @import("main.zig").Style;
const Group = @import("main.zig").Group;
const Metrics = @import("main.zig").Metrics;
const Presentation = @import("main.zig").Presentation;
const log = std.log.scoped(.font_groupcache);
/// Cache for codepoints to font indexes in a group.
codepoints: std.AutoHashMapUnmanaged(CodepointKey, ?Group.FontIndex) = .{},
/// Cache for glyph renders.
glyphs: std.AutoHashMapUnmanaged(GlyphKey, Glyph) = .{},
/// The underlying font group. Users are expected to use this directly
/// to setup the group or make changes. Beware some changes require a reset
/// (see reset).
group: Group,
/// The texture atlas to store renders in. The GroupCache has to store these
/// because the cached Glyph result is dependent on the Atlas.
atlas_greyscale: Atlas,
atlas_color: Atlas,
const CodepointKey = struct {
style: Style,
codepoint: u32,
presentation: ?Presentation,
};
const GlyphKey = struct {
index: Group.FontIndex,
glyph: u32,
};
/// The GroupCache takes ownership of Group and will free it.
pub fn init(alloc: Allocator, group: Group) !GroupCache {
var atlas_greyscale = try Atlas.init(alloc, 512, .greyscale);
errdefer atlas_greyscale.deinit(alloc);
var atlas_color = try Atlas.init(alloc, 512, .rgba);
errdefer atlas_color.deinit(alloc);
var result: GroupCache = .{
.group = group,
.atlas_greyscale = atlas_greyscale,
.atlas_color = atlas_color,
};
// We set an initial capacity that can fit a good number of characters.
// This number was picked empirically based on my own terminal usage.
try result.codepoints.ensureTotalCapacity(alloc, 128);
try result.glyphs.ensureTotalCapacity(alloc, 128);
return result;
}
pub fn deinit(self: *GroupCache, alloc: Allocator) void {
self.codepoints.deinit(alloc);
self.glyphs.deinit(alloc);
self.atlas_greyscale.deinit(alloc);
self.atlas_color.deinit(alloc);
self.group.deinit(alloc);
}
/// Reset the cache. This should be called:
///
/// - If an Atlas was reset
/// - If a font group font size was changed
/// - If a font group font set was changed
///
pub fn reset(self: *GroupCache) void {
self.codepoints.clearRetainingCapacity();
self.glyphs.clearRetainingCapacity();
}
/// Calculate the metrics for this group. This also warms the cache
/// since this preloads all the ASCII characters.
pub fn metrics(self: *GroupCache, alloc: Allocator) !Metrics {
// Load all visible ASCII characters and build our cell width based on
// the widest character that we see.
const cell_width: f32 = cell_width: {
var cell_width: f32 = 0;
var i: u32 = 32;
while (i <= 126) : (i += 1) {
const index = (try self.indexForCodepoint(alloc, i, .regular, .text)).?;
const face = try self.group.faceFromIndex(index);
const glyph_index = face.glyphIndex(i).?;
const glyph = try self.renderGlyph(alloc, index, glyph_index);
if (glyph.advance_x > cell_width) {
cell_width = @ceil(glyph.advance_x);
}
}
break :cell_width cell_width;
};
// The cell height is the vertical height required to render underscore
// '_' which should live at the bottom of a cell.
const cell_height: f32 = cell_height: {
// Get the '_' char for height
const index = (try self.indexForCodepoint(alloc, '_', .regular, .text)).?;
const face = try self.group.faceFromIndex(index);
const glyph_index = face.glyphIndex('_').?;
const glyph = try self.renderGlyph(alloc, index, glyph_index);
// This is the height reported by the font face
const face_height: i32 = face.unitsToPxY(face.face.handle.*.height);
// Determine the height of the underscore char
var res: i32 = face.unitsToPxY(face.face.handle.*.ascender);
res -= glyph.offset_y;
res += @intCast(i32, glyph.height);
// We take whatever is larger to account for some fonts that
// put the underscore outside f the rectangle.
if (res < face_height) res = face_height;
break :cell_height @intToFloat(f32, res);
};
const cell_baseline = cell_baseline: {
const face = self.group.faces.get(.regular).items[0].face.?;
break :cell_baseline cell_height - @intToFloat(
f32,
face.unitsToPxY(face.face.handle.*.ascender),
);
};
return Metrics{
.cell_width = cell_width,
.cell_height = cell_height,
.cell_baseline = cell_baseline,
};
}
/// Get the font index for a given codepoint. This is cached.
pub fn indexForCodepoint(
self: *GroupCache,
alloc: Allocator,
cp: u32,
style: Style,
p: ?Presentation,
) !?Group.FontIndex {
const key: CodepointKey = .{ .style = style, .codepoint = cp, .presentation = p };
const gop = try self.codepoints.getOrPut(alloc, key);
// If it is in the cache, use it.
if (gop.found_existing) return gop.value_ptr.*;
// Load a value and cache it. This even caches negative matches.
const value = self.group.indexForCodepoint(cp, style, p);
gop.value_ptr.* = value;
return value;
}
/// Render a glyph. This automatically determines the correct texture
/// atlas to use and caches the result.
pub fn renderGlyph(
self: *GroupCache,
alloc: Allocator,
index: Group.FontIndex,
glyph_index: u32,
) !Glyph {
const key: GlyphKey = .{ .index = index, .glyph = glyph_index };
const gop = try self.glyphs.getOrPut(alloc, key);
// If it is in the cache, use it.
if (gop.found_existing) return gop.value_ptr.*;
// Uncached, render it
const face = try self.group.faceFromIndex(index);
const atlas: *Atlas = if (face.hasColor()) &self.atlas_color else &self.atlas_greyscale;
const glyph = self.group.renderGlyph(
alloc,
atlas,
index,
glyph_index,
) catch |err| switch (err) {
// If the atlas is full, we resize it
error.AtlasFull => blk: {
try atlas.grow(alloc, atlas.size * 2);
break :blk try self.group.renderGlyph(
alloc,
atlas,
index,
glyph_index,
);
},
else => return err,
};
// Cache and return
gop.value_ptr.* = glyph;
return glyph;
}
test {
const testing = std.testing;
const alloc = testing.allocator;
const testFont = @import("test.zig").fontRegular;
// const testEmoji = @import("test.zig").fontEmoji;
var atlas_greyscale = try Atlas.init(alloc, 512, .greyscale);
defer atlas_greyscale.deinit(alloc);
var lib = try Library.init();
defer lib.deinit();
var cache = try init(alloc, try Group.init(alloc, lib));
defer cache.deinit(alloc);
// Setup group
try cache.group.addFace(
alloc,
.regular,
DeferredFace.initLoaded(try Face.init(lib, testFont, .{ .points = 12 })),
);
const group = cache.group;
// Visible ASCII. Do it twice to verify cache.
var i: u32 = 32;
while (i < 127) : (i += 1) {
const idx = (try cache.indexForCodepoint(alloc, i, .regular, null)).?;
try testing.expectEqual(Style.regular, idx.style);
try testing.expectEqual(@as(Group.FontIndex.IndexInt, 0), idx.idx);
// Render
const face = try cache.group.faceFromIndex(idx);
const glyph_index = face.glyphIndex(i).?;
_ = try cache.renderGlyph(
alloc,
idx,
glyph_index,
);
}
// Do it again, but reset the group so that we know for sure its not hitting it
{
cache.group = undefined;
defer cache.group = group;
i = 32;
while (i < 127) : (i += 1) {
const idx = (try cache.indexForCodepoint(alloc, i, .regular, null)).?;
try testing.expectEqual(Style.regular, idx.style);
try testing.expectEqual(@as(Group.FontIndex.IndexInt, 0), idx.idx);
// Render
const face = try group.faceFromIndex(idx);
const glyph_index = face.glyphIndex(i).?;
_ = try cache.renderGlyph(
alloc,
idx,
glyph_index,
);
}
}
}