From a0aa1008156e17dfc5d3f47a05d0cebf62af9f1c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 29 Aug 2022 11:10:50 -0700 Subject: [PATCH] font: GroupCache is like Group, but with caching... --- src/font/Group.zig | 14 +-- src/font/GroupCache.zig | 194 ++++++++++++++++++++++++++++++++++++++++ src/font/main.zig | 1 + 3 files changed, 197 insertions(+), 12 deletions(-) create mode 100644 src/font/GroupCache.zig diff --git a/src/font/Group.zig b/src/font/Group.zig index 0b3351e3b..a952660bf 100644 --- a/src/font/Group.zig +++ b/src/font/Group.zig @@ -15,9 +15,8 @@ const Face = @import("main.zig").Face; const Library = @import("main.zig").Library; const Glyph = @import("main.zig").Glyph; const Style = @import("main.zig").Style; -const codepoint = @import("main.zig").codepoint; -const log = std.log.scoped(.font_fallback); +const log = std.log.scoped(.font_group); /// Packed array to map our styles to a set of faces. // Note: this is not the most efficient way to store these, but there is @@ -65,7 +64,7 @@ pub fn addFace(self: *Group, alloc: Allocator, style: Style, face: Face) !void { pub const FontIndex = packed struct { /// The number of bits we use for the index. const idx_bits = 8 - StyleArray.len; - const IndexInt = @Type(.{ .Int = .{ .signedness = .unsigned, .bits = idx_bits } }); + pub const IndexInt = @Type(.{ .Int = .{ .signedness = .unsigned, .bits = idx_bits } }); style: Style, idx: IndexInt, @@ -107,13 +106,6 @@ fn indexForCodepointExact(self: Group, style: Style, cp: u32) ?FontIndex { return null; } -/// Returns true if the glyph pointed to by the index requires color. -/// This is used to determine the proper atlas to pass in for rendering -/// the glyph. -pub fn indexRequiresColor(self: Group, index: FontIndex) bool { - return self.faces.get(index.style).items[@intCast(usize, index.idx)].hasColor(); -} - /// Return the Face represented by a given FontIndex. pub fn faceFromIndex(self: Group, index: FontIndex) Face { return self.faces.get(index.style).items[@intCast(usize, index.idx)]; @@ -165,7 +157,6 @@ test { const idx = group.indexForCodepoint(.regular, i).?; try testing.expectEqual(Style.regular, idx.style); try testing.expectEqual(@as(FontIndex.IndexInt, 0), idx.idx); - try testing.expect(!group.indexRequiresColor(idx)); // Render it const face = group.faceFromIndex(idx); @@ -183,6 +174,5 @@ test { const idx = group.indexForCodepoint(.regular, '🥸').?; try testing.expectEqual(Style.regular, idx.style); try testing.expectEqual(@as(FontIndex.IndexInt, 1), idx.idx); - try testing.expect(group.indexRequiresColor(idx)); } } diff --git a/src/font/GroupCache.zig b/src/font/GroupCache.zig new file mode 100644 index 000000000..5a06c91fa --- /dev/null +++ b/src/font/GroupCache.zig @@ -0,0 +1,194 @@ +//! 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 Library = @import("main.zig").Library; +const Glyph = @import("main.zig").Glyph; +const Style = @import("main.zig").Style; +const Group = @import("main.zig").Group; + +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, +}; + +const GlyphKey = struct { + index: Group.FontIndex, + glyph: u32, +}; + +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); +} + +/// 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(); +} + +/// Get the font index for a given codepoint. This is cached. +pub fn indexForCodepoint(self: *GroupCache, alloc: Allocator, style: Style, cp: u32) !?Group.FontIndex { + const key: CodepointKey = .{ .style = style, .codepoint = cp }; + 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(style, cp); + 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 = 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 group = try Group.init(alloc); + defer group.deinit(alloc); + try group.addFace(alloc, .regular, try Face.init(lib, testFont, .{ .points = 12 })); + + var cache = try init(alloc, group); + defer cache.deinit(alloc); + + // Visible ASCII. Do it twice to verify cache. + var i: u32 = 32; + while (i < 127) : (i += 1) { + const idx = (try cache.indexForCodepoint(alloc, .regular, i)).?; + try testing.expectEqual(Style.regular, idx.style); + try testing.expectEqual(@as(Group.FontIndex.IndexInt, 0), idx.idx); + + // Render + const face = 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, .regular, i)).?; + try testing.expectEqual(Style.regular, idx.style); + try testing.expectEqual(@as(Group.FontIndex.IndexInt, 0), idx.idx); + + // Render + const face = group.faceFromIndex(idx); + const glyph_index = face.glyphIndex(i).?; + _ = try cache.renderGlyph( + alloc, + idx, + glyph_index, + ); + } + } +} diff --git a/src/font/main.zig b/src/font/main.zig index ef62f132f..e4199cbf8 100644 --- a/src/font/main.zig +++ b/src/font/main.zig @@ -3,6 +3,7 @@ const std = @import("std"); pub const Face = @import("Face.zig"); pub const Family = @import("Family.zig"); pub const Group = @import("Group.zig"); +pub const GroupCache = @import("GroupCache.zig"); pub const Glyph = @import("Glyph.zig"); pub const FallbackSet = @import("FallbackSet.zig"); pub const Library = @import("Library.zig");