mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-20 02:36:22 +03:00
377 lines
11 KiB
Zig
377 lines
11 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 font = @import("main.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 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: font.Atlas,
|
|
atlas_color: font.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 font.Atlas.init(alloc, 512, .greyscale);
|
|
errdefer atlas_greyscale.deinit(alloc);
|
|
var atlas_color = try font.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();
|
|
}
|
|
|
|
/// 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();
|
|
}
|
|
|
|
/// Resize the fonts in the group. This will clear the cache.
|
|
pub fn setSize(self: *GroupCache, size: font.face.DesiredSize) !void {
|
|
try self.group.setSize(size);
|
|
|
|
// Reset our internal state
|
|
self.reset();
|
|
|
|
// Clear our atlases
|
|
self.atlas_greyscale.clear();
|
|
self.atlas_color.clear();
|
|
}
|
|
|
|
/// 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,
|
|
opts: font.face.RenderOptions,
|
|
) !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 atlas: *font.Atlas = switch (try self.group.presentationFromIndex(index)) {
|
|
.text => &self.atlas_greyscale,
|
|
.emoji => &self.atlas_color,
|
|
};
|
|
const glyph = self.group.renderGlyph(
|
|
alloc,
|
|
atlas,
|
|
index,
|
|
glyph_index,
|
|
opts,
|
|
) 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,
|
|
opts,
|
|
);
|
|
},
|
|
|
|
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 font.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,
|
|
.{ .points = 12 },
|
|
));
|
|
defer cache.deinit(alloc);
|
|
|
|
// Setup group
|
|
_ = try cache.group.addFace(
|
|
.regular,
|
|
.{ .loaded = try Face.init(lib, testFont, .{ .points = 12 }) },
|
|
);
|
|
var 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,
|
|
.{},
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The wasm-compatible API.
|
|
pub const Wasm = struct {
|
|
const wasm = @import("../os/wasm.zig");
|
|
const alloc = wasm.alloc;
|
|
|
|
export fn group_cache_new(group: *Group) ?*GroupCache {
|
|
return group_cache_new_(group) catch null;
|
|
}
|
|
|
|
fn group_cache_new_(group: *Group) !*GroupCache {
|
|
var gc = try GroupCache.init(alloc, group.*);
|
|
errdefer gc.deinit(alloc);
|
|
|
|
var result = try alloc.create(GroupCache);
|
|
errdefer alloc.destroy(result);
|
|
result.* = gc;
|
|
return result;
|
|
}
|
|
|
|
export fn group_cache_free(ptr: ?*GroupCache) void {
|
|
if (ptr) |v| {
|
|
v.deinit(alloc);
|
|
alloc.destroy(v);
|
|
}
|
|
}
|
|
|
|
export fn group_cache_set_size(self: *GroupCache, size: u16) void {
|
|
return self.setSize(.{ .points = size }) catch |err| {
|
|
log.warn("error setting group cache size err={}", .{err});
|
|
return;
|
|
};
|
|
}
|
|
|
|
/// Presentation is negative for doesn't matter.
|
|
export fn group_cache_index_for_codepoint(self: *GroupCache, cp: u32, style: u16, p: i16) i16 {
|
|
const presentation: ?Presentation = if (p < 0) null else @enumFromInt(p);
|
|
if (self.indexForCodepoint(
|
|
alloc,
|
|
cp,
|
|
@enumFromInt(style),
|
|
presentation,
|
|
)) |idx| {
|
|
return @intCast(@as(u8, @bitCast(idx orelse return -1)));
|
|
} else |err| {
|
|
log.warn("error getting index for codepoint from group cache size err={}", .{err});
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
export fn group_cache_render_glyph(
|
|
self: *GroupCache,
|
|
idx: i16,
|
|
cp: u32,
|
|
max_height: u16,
|
|
) ?*Glyph {
|
|
return group_cache_render_glyph_(self, idx, cp, max_height) catch |err| {
|
|
log.warn("error rendering group cache glyph err={}", .{err});
|
|
return null;
|
|
};
|
|
}
|
|
|
|
fn group_cache_render_glyph_(
|
|
self: *GroupCache,
|
|
idx_: i16,
|
|
cp: u32,
|
|
max_height_: u16,
|
|
) !*Glyph {
|
|
const idx = @as(Group.FontIndex, @bitCast(@as(u8, @intCast(idx_))));
|
|
const max_height = if (max_height_ <= 0) null else max_height_;
|
|
const glyph = try self.renderGlyph(alloc, idx, cp, .{
|
|
.max_height = max_height,
|
|
});
|
|
|
|
var result = try alloc.create(Glyph);
|
|
errdefer alloc.destroy(result);
|
|
result.* = glyph;
|
|
return result;
|
|
}
|
|
|
|
export fn group_cache_atlas_greyscale(self: *GroupCache) *font.Atlas {
|
|
return &self.atlas_greyscale;
|
|
}
|
|
|
|
export fn group_cache_atlas_color(self: *GroupCache) *font.Atlas {
|
|
return &self.atlas_color;
|
|
}
|
|
};
|
|
|
|
test "resize" {
|
|
const testing = std.testing;
|
|
const alloc = testing.allocator;
|
|
const testFont = @import("test.zig").fontRegular;
|
|
// const testEmoji = @import("test.zig").fontEmoji;
|
|
|
|
var atlas_greyscale = try font.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,
|
|
.{ .points = 12 },
|
|
));
|
|
defer cache.deinit(alloc);
|
|
|
|
// Setup group
|
|
_ = try cache.group.addFace(
|
|
.regular,
|
|
.{ .loaded = try Face.init(lib, testFont, .{ .points = 12, .xdpi = 96, .ydpi = 96 }) },
|
|
);
|
|
|
|
// Load a letter
|
|
{
|
|
const idx = (try cache.indexForCodepoint(alloc, 'A', .regular, null)).?;
|
|
const face = try cache.group.faceFromIndex(idx);
|
|
const glyph_index = face.glyphIndex('A').?;
|
|
const glyph = try cache.renderGlyph(
|
|
alloc,
|
|
idx,
|
|
glyph_index,
|
|
.{},
|
|
);
|
|
|
|
try testing.expectEqual(@as(u32, 11), glyph.height);
|
|
}
|
|
|
|
// Resize
|
|
try cache.setSize(.{ .points = 24, .xdpi = 96, .ydpi = 96 });
|
|
{
|
|
const idx = (try cache.indexForCodepoint(alloc, 'A', .regular, null)).?;
|
|
const face = try cache.group.faceFromIndex(idx);
|
|
const glyph_index = face.glyphIndex('A').?;
|
|
const glyph = try cache.renderGlyph(
|
|
alloc,
|
|
idx,
|
|
glyph_index,
|
|
.{},
|
|
);
|
|
|
|
try testing.expectEqual(@as(u32, 21), glyph.height);
|
|
}
|
|
}
|