From 06df9b78679e42aff28aaa359ae1b587dd66337e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 6 Apr 2024 20:10:57 -0700 Subject: [PATCH] font: remove old files --- src/font/Group.zig | 1179 ------------------------------------ src/font/GroupCache.zig | 381 ------------ src/font/GroupCacheSet.zig | 518 ---------------- src/font/main.zig | 5 - src/renderer/Metal.zig | 4 +- src/renderer/OpenGL.zig | 4 +- 6 files changed, 4 insertions(+), 2087 deletions(-) delete mode 100644 src/font/Group.zig delete mode 100644 src/font/GroupCache.zig delete mode 100644 src/font/GroupCacheSet.zig diff --git a/src/font/Group.zig b/src/font/Group.zig deleted file mode 100644 index 4637be3d9..000000000 --- a/src/font/Group.zig +++ /dev/null @@ -1,1179 +0,0 @@ -//! A font group is a a set of multiple font faces of potentially different -//! styles that are used together to find glyphs. They usually share sizing -//! properties so that they can be used interchangeably with each other in cases -//! a codepoint doesn't map cleanly. For example, if a user requests a bold -//! char and it doesn't exist we can fallback to a regular non-bold char so -//! we show SOMETHING. -//! -//! Note this is made specifically for terminals so it has some features -//! that aren't generally helpful, such as detecting and drawing the terminal -//! box glyphs and requiring cell sizes for such glyphs. -const Group = @This(); - -const std = @import("std"); -const assert = std.debug.assert; -const Allocator = std.mem.Allocator; -const ziglyph = @import("ziglyph"); - -const font = @import("main.zig"); -const DeferredFace = @import("main.zig").DeferredFace; -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 Presentation = @import("main.zig").Presentation; -const options = @import("main.zig").options; -const quirks = @import("../quirks.zig"); - -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 -// usually only one font group for the entire process so this isn't the -// most important memory efficiency we can look for. This is totally opaque -// to the user so we can change this later. -const StyleArray = std.EnumArray(Style, std.ArrayListUnmanaged(GroupFace)); - -/// Packed array of booleans to indicate if a style is enabled or not. -pub const StyleStatus = std.EnumArray(Style, bool); - -/// Map of descriptors to faces. This is used with manual codepoint maps -/// to ensure that we don't load the same font multiple times. -/// -/// Note that the current implementation will load the same font multiple -/// times if the font used for a codepoint map is identical to a font used -/// for a regular style. That's just an inefficient choice made now because -/// the implementation is simpler and codepoint maps matching a regular -/// font is a rare case. -const DescriptorCache = std.HashMapUnmanaged( - font.discovery.Descriptor, - ?FontIndex, - struct { - const KeyType = font.discovery.Descriptor; - - pub fn hash(ctx: @This(), k: KeyType) u64 { - _ = ctx; - return k.hashcode(); - } - - pub fn eql(ctx: @This(), a: KeyType, b: KeyType) bool { - return ctx.hash(a) == ctx.hash(b); - } - }, - std.hash_map.default_max_load_percentage, -); - -/// The requested presentation for a codepoint. -const PresentationMode = union(enum) { - /// The codepoint has an explicit presentation that is required, - /// i.e. VS15/V16. - explicit: Presentation, - - /// The codepoint has no explicit presentation and we should use - /// the presentation from the UCd. - default: Presentation, - - /// The codepoint can be any presentation. - any: void, -}; - -/// The allocator for this group -alloc: Allocator, - -/// The library being used for all the faces. -lib: Library, - -/// The desired font size. All fonts in a group must share the same size. -size: font.face.DesiredSize, - -/// Metric modifiers to apply to loaded fonts. The Group takes ownership -/// over the memory and will use the associated allocator to free it. -metric_modifiers: ?font.face.Metrics.ModifierSet = null, - -/// The available faces we have. This shouldn't be modified manually. -/// Instead, use the functions available on Group. -faces: StyleArray, - -/// The set of statuses and whether they're enabled or not. This defaults -/// to true. This can be changed at runtime with no ill effect. If you -/// change this at runtime and are using a GroupCache, the GroupCache -/// must be reset. -styles: StyleStatus = StyleStatus.initFill(true), - -/// If discovery is available, we'll look up fonts where we can't find -/// the codepoint. This can be set after initialization. -discover: ?*font.Discover = null, - -/// A map of codepoints to font requests for codepoint-level overrides. -/// The memory associated with the map is owned by the caller and is not -/// modified or freed by Group. -codepoint_map: ?font.CodepointMap = null, - -/// The descriptor cache is used to cache the descriptor to font face -/// mapping for codepoint maps. -descriptor_cache: DescriptorCache = .{}, - -/// Set this to a non-null value to enable sprite glyph drawing. If this -/// isn't enabled we'll just fall through to trying to use regular fonts -/// to render sprite glyphs. But more than likely, if this isn't set then -/// terminal rendering will look wrong. -sprite: ?font.sprite.Face = null, - -/// A face in a group can be deferred or loaded. A deferred face -/// is not yet fully loaded and only represents the font descriptor -/// and usually uses less resources. A loaded face is fully parsed, -/// ready to rasterize, and usually uses more resources than a -/// deferred version. -/// -/// A face can also be a "fallback" variant that is still either -/// deferred or loaded. Today, there is only one different between -/// fallback and non-fallback (or "explicit") faces: the handling -/// of emoji presentation. -/// -/// For explicit faces, when an explicit emoji presentation is -/// not requested, we will use any glyph for that codepoint found -/// even if the font presentation does not match the UCD -/// (Unicode Character Database) value. When an explicit presentation -/// is requested (via either VS15/V16), that is always honored. -/// The reason we do this is because we assume that if a user -/// explicitly chosen a font face (hence it is "explicit" and -/// not "fallback"), they want to use any glyphs possible within that -/// font face. Fallback fonts on the other hand are picked as a -/// last resort, so we should prefer exactness if possible. -pub const GroupFace = union(enum) { - deferred: DeferredFace, // Not loaded - loaded: Face, // Loaded, explicit use - - // The same as deferred/loaded but fallback font semantics (see large - // comment above GroupFace). - fallback_deferred: DeferredFace, - fallback_loaded: Face, - - pub fn deinit(self: *GroupFace) void { - switch (self.*) { - inline .deferred, - .loaded, - .fallback_deferred, - .fallback_loaded, - => |*v| v.deinit(), - } - } - - /// True if this face satisfies the given codepoint and presentation. - fn hasCodepoint(self: GroupFace, cp: u32, p_mode: PresentationMode) bool { - return switch (self) { - // Non-fallback fonts require explicit presentation matching but - // otherwise don't care about presentation - .deferred => |v| switch (p_mode) { - .explicit => |p| v.hasCodepoint(cp, p), - .default, .any => v.hasCodepoint(cp, null), - }, - - .loaded => |face| switch (p_mode) { - .explicit => |p| face.presentation == p and face.glyphIndex(cp) != null, - .default, .any => face.glyphIndex(cp) != null, - }, - - // Fallback fonts require exact presentation matching. - .fallback_deferred => |v| switch (p_mode) { - .explicit, .default => |p| v.hasCodepoint(cp, p), - .any => v.hasCodepoint(cp, null), - }, - - .fallback_loaded => |face| switch (p_mode) { - .explicit, - .default, - => |p| face.presentation == p and face.glyphIndex(cp) != null, - .any => face.glyphIndex(cp) != null, - }, - }; - } -}; - -/// Initializes an empty group. This is not useful until faces are added -/// and finalizeInit is called. -pub fn init( - alloc: Allocator, - lib: Library, - size: font.face.DesiredSize, -) !Group { - var result = Group{ .alloc = alloc, .lib = lib, .size = size, .faces = undefined }; - - // Initialize all our styles to initially sized lists. - var i: usize = 0; - while (i < StyleArray.len) : (i += 1) { - result.faces.values[i] = .{}; - try result.faces.values[i].ensureTotalCapacityPrecise(alloc, 2); - } - - return result; -} - -pub fn deinit(self: *Group) void { - { - var it = self.faces.iterator(); - while (it.next()) |entry| { - for (entry.value.items) |*item| item.deinit(); - entry.value.deinit(self.alloc); - } - } - - if (self.metric_modifiers) |*v| v.deinit(self.alloc); - - self.descriptor_cache.deinit(self.alloc); -} - -/// Returns the options for initializing a face based on the options associated -/// with this font group. -pub fn faceOptions(self: *const Group) font.face.Options { - return .{ - .size = self.size, - .metric_modifiers = if (self.metric_modifiers) |*v| v else null, - }; -} - -/// Add a face to the list for the given style. This face will be added as -/// next in priority if others exist already, i.e. it'll be the _last_ to be -/// searched for a glyph in that list. -/// -/// The group takes ownership of the face. The face will be deallocated when -/// the group is deallocated. -pub fn addFace(self: *Group, style: Style, face: GroupFace) !FontIndex { - const list = self.faces.getPtr(style); - - // We have some special indexes so we must never pass those. - if (list.items.len >= FontIndex.Special.start - 1) return error.GroupFull; - - const idx = list.items.len; - try list.append(self.alloc, face); - return .{ .style = style, .idx = @intCast(idx) }; -} - -/// This will automatically create an italicized font from the regular -/// font face if we don't have any italicized fonts. -pub fn italicize(self: *Group) !void { - // If we have an italic font, do nothing. - const italic_list = self.faces.getPtr(.italic); - if (italic_list.items.len > 0) return; - - // Not all font backends support auto-italicization. - if (comptime !@hasDecl(Face, "italicize")) { - log.warn("no italic font face available, italics will not render", .{}); - return; - } - - // Our regular font. If we have no regular font we also do nothing. - const regular = regular: { - const list = self.faces.get(.regular); - if (list.items.len == 0) return; - - // Find our first font that is text. - for (0..list.items.len) |i| { - const face = try self.faceFromIndex(.{ - .style = .regular, - .idx = @intCast(i), - }); - if (face.presentation == .text) break :regular face; - } - - return; - }; - - // Try to italicize it. - const face = try regular.italicize(self.faceOptions()); - try italic_list.append(self.alloc, .{ .loaded = face }); - - var buf: [128]u8 = undefined; - if (face.name(&buf)) |name| { - log.info("font auto-italicized: {s}", .{name}); - } else |_| {} -} - -/// Resize the fonts to the desired size. -pub fn setSize(self: *Group, size: font.face.DesiredSize) !void { - // Note: there are some issues here with partial failure. We don't - // currently handle it in any meaningful way if one face can resize - // but another can't. - - // Set our size for future loads - self.size = size; - - // Resize all our faces that are loaded - var it = self.faces.iterator(); - while (it.next()) |entry| { - for (entry.value.items) |*elem| switch (elem.*) { - .deferred, .fallback_deferred => continue, - .loaded, .fallback_loaded => |*f| try f.setSize(self.faceOptions()), - }; - } -} - -/// This represents a specific font in the group. -pub const FontIndex = packed struct(FontIndex.Backing) { - const Backing = u16; - const backing_bits = @typeInfo(Backing).Int.bits; - - /// The number of bits we use for the index. - const idx_bits = backing_bits - @typeInfo(@typeInfo(Style).Enum.tag_type).Int.bits; - pub const IndexInt = @Type(.{ .Int = .{ .signedness = .unsigned, .bits = idx_bits } }); - - /// The special-case fonts that we support. - pub const Special = enum(IndexInt) { - // We start all special fonts at this index so they can be detected. - pub const start = std.math.maxInt(IndexInt); - - /// Sprite drawing, this is rendered JIT using 2D graphics APIs. - sprite = start, - }; - - style: Style = .regular, - idx: IndexInt = 0, - - /// Initialize a special font index. - pub fn initSpecial(v: Special) FontIndex { - return .{ .style = .regular, .idx = @intFromEnum(v) }; - } - - /// Convert to int - pub fn int(self: FontIndex) Backing { - return @bitCast(self); - } - - /// Returns true if this is a "special" index which doesn't map to - /// a real font face. We can still render it but there is no face for - /// this font. - pub fn special(self: FontIndex) ?Special { - if (self.idx < Special.start) return null; - return @enumFromInt(self.idx); - } - - test { - // We never want to take up more than a byte since font indexes are - // everywhere so if we increase the size of this we'll dramatically - // increase our memory usage. - try std.testing.expectEqual(@sizeOf(Backing), @sizeOf(FontIndex)); - - // Just so we're aware when this changes. The current maximum number - // of fonts for a style is 13 bits or 8192 fonts. - try std.testing.expectEqual(13, idx_bits); - } -}; - -/// Looks up the font that should be used for a specific codepoint. -/// The font index is valid as long as font faces aren't removed. This -/// isn't cached; it is expected that downstream users handle caching if -/// that is important. -/// -/// Optionally, a presentation format can be specified. This presentation -/// format will be preferred but if it can't be found in this format, -/// any format will be accepted. If presentation is null, the UCD -/// (Unicode Character Database) will be used to determine the default -/// presentation for the codepoint. -/// a code point. -/// -/// This logic is relatively complex so the exact algorithm is documented -/// here. If this gets out of sync with the code, ask questions. -/// -/// 1. If a font style is requested that is disabled (i.e. bold), -/// we start over with the regular font style. The regular font style -/// cannot be disabled, but it can be replaced with a stylized font -/// face. -/// -/// 2. If there is a codepoint override for the codepoint, we satisfy -/// that requirement if we can, no matter what style or presentation. -/// -/// 3. If this is a sprite codepoint (such as an underline), then the -/// sprite font always is the result. -/// -/// 4. If the exact style and presentation request can be satisfied by -/// one of our loaded fonts, we return that value. We search loaded -/// fonts in the order they're added to the group, so the caller must -/// set the priority order. -/// -/// 5. If the style isn't regular, we restart this process at this point -/// but with the regular style. This lets us fall back to regular with -/// our loaded fonts before trying a fallback. We'd rather show a regular -/// version of a codepoint from a loaded font than find a new font in -/// the correct style because styles in other fonts often change -/// metrics like glyph widths. -/// -/// 6. If the style is regular, and font discovery is enabled, we look -/// for a fallback font to satisfy our request. -/// -/// 7. Finally, as a last resort, we fall back to restarting this whole -/// process with a regular font face satisfying ANY presentation for -/// the codepoint. If this fails, we return null. -/// -pub fn indexForCodepoint( - self: *Group, - cp: u32, - style: Style, - p: ?Presentation, -) ?FontIndex { - // If we've disabled a font style, then fall back to regular. - if (style != .regular and !self.styles.get(style)) { - return self.indexForCodepoint(cp, .regular, p); - } - - // Codepoint overrides. - if (self.indexForCodepointOverride(cp)) |idx_| { - if (idx_) |idx| return idx; - } else |err| { - log.warn("codepoint override failed codepoint={} err={}", .{ cp, err }); - } - - // If we have sprite drawing enabled, check if our sprite face can - // handle this. - if (self.sprite) |sprite| { - if (sprite.hasCodepoint(cp, p)) { - return FontIndex.initSpecial(.sprite); - } - } - - // Build our presentation mode. If we don't have an explicit presentation - // given then we use the UCD (Unicode Character Database) to determine - // the default presentation. Note there is some inefficiency here because - // we'll do this muliple times if we recurse, but this is a cached function - // call higher up (GroupCache) so this should be rare. - const p_mode: PresentationMode = if (p) |v| .{ .explicit = v } else .{ - .default = if (ziglyph.emoji.isEmojiPresentation(@intCast(cp))) - .emoji - else - .text, - }; - - // If we can find the exact value, then return that. - if (self.indexForCodepointExact(cp, style, p_mode)) |value| return value; - - // If we're not a regular font style, try looking for a regular font - // that will satisfy this request. Blindly looking for unmatched styled - // fonts to satisfy one codepoint results in some ugly rendering. - if (style != .regular) { - if (self.indexForCodepoint(cp, .regular, p)) |value| return value; - } - - // If we are regular, try looking for a fallback using discovery. - if (style == .regular and font.Discover != void) { - log.debug("searching for a fallback font for cp={x}", .{cp}); - if (self.discover) |disco| discover: { - var disco_it = disco.discover(self.alloc, .{ - .codepoint = cp, - .size = self.size.points, - .bold = style == .bold or style == .bold_italic, - .italic = style == .italic or style == .bold_italic, - .monospace = false, - }) catch break :discover; - defer disco_it.deinit(); - - while (true) { - var deferred_face = (disco_it.next() catch |err| { - log.warn("fallback search failed with error err={}", .{err}); - break; - }) orelse break; - - // Discovery is supposed to only return faces that have our - // codepoint but we can't search presentation in discovery so - // we have to check it here. - const face: GroupFace = .{ .fallback_deferred = deferred_face }; - if (!face.hasCodepoint(cp, p_mode)) { - deferred_face.deinit(); - continue; - } - - var buf: [256]u8 = undefined; - log.info("found codepoint 0x{x} in fallback face={s}", .{ - cp, - deferred_face.name(&buf) catch "", - }); - return self.addFace(style, face) catch { - deferred_face.deinit(); - break :discover; - }; - } - - log.debug("no fallback face found for cp={x}", .{cp}); - } - } - - // If this is already regular, we're done falling back. - if (style == .regular and p == null) return null; - - // For non-regular fonts, we fall back to regular with any presentation - return self.indexForCodepointExact(cp, .regular, .{ .any = {} }); -} - -fn indexForCodepointExact( - self: Group, - cp: u32, - style: Style, - p_mode: PresentationMode, -) ?FontIndex { - for (self.faces.get(style).items, 0..) |elem, i| { - if (elem.hasCodepoint(cp, p_mode)) { - return FontIndex{ - .style = style, - .idx = @intCast(i), - }; - } - } - - // Not found - return null; -} - -/// Checks if the codepoint is in the map of codepoint overrides, -/// finds the override font, and returns it. -fn indexForCodepointOverride(self: *Group, cp: u32) !?FontIndex { - if (comptime font.Discover == void) return null; - const map = self.codepoint_map orelse return null; - - // If we have a codepoint too large or isn't in the map, then we - // don't have an override. - const cp_u21 = std.math.cast(u21, cp) orelse return null; - const desc = map.get(cp_u21) orelse return null; - - // Fast path: the descriptor is already loaded. - const idx_: ?FontIndex = self.descriptor_cache.get(desc) orelse idx: { - // Slow path: we have to find this descriptor and load the font - const discover = self.discover orelse return null; - var disco_it = try discover.discover(self.alloc, desc); - defer disco_it.deinit(); - - const face = (try disco_it.next()) orelse { - log.warn( - "font lookup for codepoint map failed codepoint={} err=FontNotFound", - .{cp}, - ); - - // Add null to the cache so we don't do a lookup again later. - try self.descriptor_cache.put(self.alloc, desc, null); - return null; - }; - - // Add the font to our list of fonts so we can get an index for it, - // and ensure the index is stored in the descriptor cache for next time. - const idx = try self.addFace(.regular, .{ .deferred = face }); - try self.descriptor_cache.put(self.alloc, desc, idx); - - break :idx idx; - }; - - // The descriptor cache will populate null if the descriptor is not found - // to avoid expensive discoveries later. - const idx = idx_ orelse return null; - - // We need to verify that this index has the codepoint we want. - if (self.hasCodepoint(idx, cp, null)) { - log.debug("codepoint override based on config codepoint={} family={s}", .{ - cp, - desc.family orelse "", - }); - - return idx; - } - - return null; -} - -/// Check if a specific font index has a specific codepoint. This does not -/// necessarily force the font to load. The presentation value "p" will -/// verify the Emoji representation matches if it is non-null. If "p" is -/// null then any presentation will be accepted. -pub fn hasCodepoint(self: *Group, index: FontIndex, cp: u32, p: ?Presentation) bool { - const list = self.faces.getPtr(index.style); - if (index.idx >= list.items.len) return false; - return list.items[index.idx].hasCodepoint( - cp, - if (p) |v| .{ .explicit = v } else .{ .any = {} }, - ); -} - -/// Returns the presentation for a specific font index. This is useful for -/// determining what atlas is needed. -pub fn presentationFromIndex(self: *Group, index: FontIndex) !font.Presentation { - if (index.special()) |sp| switch (sp) { - .sprite => return .text, - }; - - const face = try self.faceFromIndex(index); - return face.presentation; -} - -/// Return the Face represented by a given FontIndex. Note that special -/// fonts (i.e. box glyphs) do not have a face. The returned face pointer is -/// only valid until the set of faces change. -pub fn faceFromIndex(self: *Group, index: FontIndex) !*Face { - if (index.special() != null) return error.SpecialHasNoFace; - const list = self.faces.getPtr(index.style); - const item = &list.items[index.idx]; - return switch (item.*) { - inline .deferred, .fallback_deferred => |*d, tag| deferred: { - const face = try d.load(self.lib, self.faceOptions()); - d.deinit(); - item.* = switch (tag) { - .deferred => .{ .loaded = face }, - .fallback_deferred => .{ .fallback_loaded = face }, - else => unreachable, - }; - - break :deferred switch (tag) { - .deferred => &item.loaded, - .fallback_deferred => &item.fallback_loaded, - else => unreachable, - }; - }, - - .loaded, .fallback_loaded => |*f| f, - }; -} - -/// Render a glyph by glyph index into the given font atlas and return -/// metadata about it. -/// -/// This performs no caching, it is up to the caller to cache calls to this -/// if they want. This will also not resize the atlas if it is full. -/// -/// IMPORTANT: this renders by /glyph index/ and not by /codepoint/. The caller -/// is expected to translate codepoints to glyph indexes in some way. The most -/// trivial way to do this is to get the Face and call glyphIndex. If you're -/// doing text shaping, the text shaping library (i.e. HarfBuzz) will automatically -/// determine glyph indexes for a text run. -pub fn renderGlyph( - self: *Group, - alloc: Allocator, - atlas: *font.Atlas, - index: FontIndex, - glyph_index: u32, - opts: font.face.RenderOptions, -) !Glyph { - // Special-case fonts are rendered directly. - if (index.special()) |sp| switch (sp) { - .sprite => return try self.sprite.?.renderGlyph( - alloc, - atlas, - glyph_index, - opts, - ), - }; - - const face = try self.faceFromIndex(index); - const glyph = try face.renderGlyph(alloc, atlas, glyph_index, opts); - // log.warn("GLYPH={}", .{glyph}); - return glyph; -} - -/// The wasm-compatible API. -pub const Wasm = struct { - const wasm = @import("../os/wasm.zig"); - const alloc = wasm.alloc; - - export fn group_new(pts: u16) ?*Group { - return group_new_(pts) catch null; - } - - fn group_new_(pts: u16) !*Group { - var group = try Group.init(alloc, .{}, .{ .points = pts }); - errdefer group.deinit(); - - const result = try alloc.create(Group); - errdefer alloc.destroy(result); - result.* = group; - return result; - } - - export fn group_free(ptr: ?*Group) void { - if (ptr) |v| { - v.deinit(); - alloc.destroy(v); - } - } - - export fn group_init_sprite_face(self: *Group) void { - return group_init_sprite_face_(self) catch |err| { - log.warn("error initializing sprite face err={}", .{err}); - return; - }; - } - - fn group_init_sprite_face_(self: *Group) !void { - const metrics = metrics: { - const index = self.indexForCodepoint('M', .regular, .text).?; - const face = try self.faceFromIndex(index); - break :metrics face.metrics; - }; - - // Set details for our sprite font - self.sprite = font.sprite.Face{ - .width = metrics.cell_width, - .height = metrics.cell_height, - .thickness = 2, - .underline_position = metrics.underline_position, - }; - } - - export fn group_add_face(self: *Group, style: u16, face: *font.DeferredFace) void { - return self.addFace(@enumFromInt(style), face.*) catch |err| { - log.warn("error adding face to group err={}", .{err}); - return; - }; - } - - export fn group_set_size(self: *Group, size: u16) void { - return self.setSize(.{ .points = size }) catch |err| { - log.warn("error setting group size err={}", .{err}); - return; - }; - } - - /// Presentation is negative for doesn't matter. - export fn group_index_for_codepoint(self: *Group, cp: u32, style: u16, p: i16) i16 { - const presentation: ?Presentation = if (p < 0) null else @enumFromInt(p); - const idx = self.indexForCodepoint( - cp, - @enumFromInt(style), - presentation, - ) orelse return -1; - return @intCast(@as(u8, @bitCast(idx))); - } - - export fn group_render_glyph( - self: *Group, - atlas: *font.Atlas, - idx: i16, - cp: u32, - max_height: u16, - ) ?*Glyph { - return group_render_glyph_(self, atlas, idx, cp, max_height) catch |err| { - log.warn("error rendering group glyph err={}", .{err}); - return null; - }; - } - - fn group_render_glyph_( - self: *Group, - atlas: *font.Atlas, - idx_: i16, - cp: u32, - max_height_: u16, - ) !*Glyph { - const idx = @as(FontIndex, @bitCast(@as(u8, @intCast(idx_)))); - const max_height = if (max_height_ <= 0) null else max_height_; - const glyph = try self.renderGlyph(alloc, atlas, idx, cp, .{ - .max_height = max_height, - }); - - const result = try alloc.create(Glyph); - errdefer alloc.destroy(result); - result.* = glyph; - return result; - } -}; - -// X -test { - const testing = std.testing; - const alloc = testing.allocator; - const testFont = @import("test.zig").fontRegular; - const testEmoji = @import("test.zig").fontEmoji; - const testEmojiText = @import("test.zig").fontEmojiText; - - var atlas_greyscale = try font.Atlas.init(alloc, 512, .greyscale); - defer atlas_greyscale.deinit(alloc); - - var lib = try Library.init(); - defer lib.deinit(); - - var group = try init(alloc, lib, .{ .points = 12 }); - defer group.deinit(); - - _ = try group.addFace( - .regular, - .{ .loaded = try Face.init(lib, testFont, .{ .size = .{ .points = 12 } }) }, - ); - - if (font.options.backend != .coretext) { - // Coretext doesn't support Noto's format - _ = try group.addFace( - .regular, - .{ .loaded = try Face.init(lib, testEmoji, .{ .size = .{ .points = 12 } }) }, - ); - } - _ = try group.addFace( - .regular, - .{ .loaded = try Face.init(lib, testEmojiText, .{ .size = .{ .points = 12 } }) }, - ); - - // Should find all visible ASCII - var i: u32 = 32; - while (i < 127) : (i += 1) { - const idx = group.indexForCodepoint(i, .regular, null).?; - try testing.expectEqual(Style.regular, idx.style); - try testing.expectEqual(@as(FontIndex.IndexInt, 0), idx.idx); - - // Render it - const face = try group.faceFromIndex(idx); - const glyph_index = face.glyphIndex(i).?; - _ = try group.renderGlyph( - alloc, - &atlas_greyscale, - idx, - glyph_index, - .{}, - ); - } - - // Try emoji - { - const idx = group.indexForCodepoint('🥸', .regular, null).?; - try testing.expectEqual(Style.regular, idx.style); - try testing.expectEqual(@as(FontIndex.IndexInt, 1), idx.idx); - } - - // Try text emoji - { - const idx = group.indexForCodepoint(0x270C, .regular, .text).?; - try testing.expectEqual(Style.regular, idx.style); - const text_idx = if (font.options.backend == .coretext) 1 else 2; - try testing.expectEqual(@as(FontIndex.IndexInt, text_idx), idx.idx); - } - { - const idx = group.indexForCodepoint(0x270C, .regular, .emoji).?; - try testing.expectEqual(Style.regular, idx.style); - try testing.expectEqual(@as(FontIndex.IndexInt, 1), idx.idx); - } - - // Box glyph should be null since we didn't set a box font - { - try testing.expect(group.indexForCodepoint(0x1FB00, .regular, null) == null); - } -} - -// X -test "disabled font style" { - const testing = std.testing; - const alloc = testing.allocator; - const testFont = @import("test.zig").fontRegular; - - var atlas_greyscale = try font.Atlas.init(alloc, 512, .greyscale); - defer atlas_greyscale.deinit(alloc); - - var lib = try Library.init(); - defer lib.deinit(); - - var group = try init(alloc, lib, .{ .points = 12 }); - defer group.deinit(); - - // Disable bold - group.styles.set(.bold, false); - - // Same font but we can test the style in the index - const opts: font.face.Options = .{ .size = .{ .points = 12 } }; - _ = try group.addFace(.regular, .{ .loaded = try Face.init(lib, testFont, opts) }); - _ = try group.addFace(.bold, .{ .loaded = try Face.init(lib, testFont, opts) }); - _ = try group.addFace(.italic, .{ .loaded = try Face.init(lib, testFont, opts) }); - - // Regular should work fine - { - const idx = group.indexForCodepoint('A', .regular, null).?; - try testing.expectEqual(Style.regular, idx.style); - try testing.expectEqual(@as(FontIndex.IndexInt, 0), idx.idx); - } - - // Bold should go to regular - { - const idx = group.indexForCodepoint('A', .bold, null).?; - try testing.expectEqual(Style.regular, idx.style); - try testing.expectEqual(@as(FontIndex.IndexInt, 0), idx.idx); - } - - // Italic should still work - { - const idx = group.indexForCodepoint('A', .italic, null).?; - try testing.expectEqual(Style.italic, idx.style); - try testing.expectEqual(@as(FontIndex.IndexInt, 0), idx.idx); - } -} - -// X -test "face count limit" { - const testing = std.testing; - const alloc = testing.allocator; - const testFont = @import("test.zig").fontRegular; - - var atlas_greyscale = try font.Atlas.init(alloc, 512, .greyscale); - defer atlas_greyscale.deinit(alloc); - - var lib = try Library.init(); - defer lib.deinit(); - - const opts: font.face.Options = .{ .size = .{ .points = 12 } }; - var group = try init(alloc, lib, opts.size); - defer group.deinit(); - - for (0..FontIndex.Special.start - 1) |_| { - _ = try group.addFace(.regular, .{ .loaded = try Face.init(lib, testFont, opts) }); - } - - try testing.expectError(error.GroupFull, group.addFace( - .regular, - .{ .loaded = try Face.init(lib, testFont, opts) }, - )); -} - -// X -test "box glyph" { - const testing = std.testing; - const alloc = testing.allocator; - - var atlas_greyscale = try font.Atlas.init(alloc, 512, .greyscale); - defer atlas_greyscale.deinit(alloc); - - var lib = try Library.init(); - defer lib.deinit(); - - var group = try init(alloc, lib, .{ .points = 12 }); - defer group.deinit(); - - // Set box font - group.sprite = font.sprite.Face{ .width = 18, .height = 36, .thickness = 2 }; - - // Should find a box glyph - const idx = group.indexForCodepoint(0x2500, .regular, null).?; - try testing.expectEqual(Style.regular, idx.style); - try testing.expectEqual(@intFromEnum(FontIndex.Special.sprite), idx.idx); - - // Should render it - const glyph = try group.renderGlyph( - alloc, - &atlas_greyscale, - idx, - 0x2500, - .{}, - ); - try testing.expectEqual(@as(u32, 36), glyph.height); -} - -test "resize" { - const testing = std.testing; - const alloc = testing.allocator; - const testFont = @import("test.zig").fontRegular; - - var atlas_greyscale = try font.Atlas.init(alloc, 512, .greyscale); - defer atlas_greyscale.deinit(alloc); - - var lib = try Library.init(); - defer lib.deinit(); - - var group = try init(alloc, lib, .{ .points = 12, .xdpi = 96, .ydpi = 96 }); - defer group.deinit(); - - _ = try group.addFace(.regular, .{ .loaded = try Face.init( - lib, - testFont, - .{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } }, - ) }); - - // Load a letter - { - const idx = group.indexForCodepoint('A', .regular, null).?; - const face = try group.faceFromIndex(idx); - const glyph_index = face.glyphIndex('A').?; - const glyph = try group.renderGlyph( - alloc, - &atlas_greyscale, - idx, - glyph_index, - .{}, - ); - - try testing.expectEqual(@as(u32, 11), glyph.height); - } - - // Resize - try group.setSize(.{ .points = 24, .xdpi = 96, .ydpi = 96 }); - { - const idx = group.indexForCodepoint('A', .regular, null).?; - const face = try group.faceFromIndex(idx); - const glyph_index = face.glyphIndex('A').?; - const glyph = try group.renderGlyph( - alloc, - &atlas_greyscale, - idx, - glyph_index, - .{}, - ); - - try testing.expectEqual(@as(u32, 21), glyph.height); - } -} - -test "discover monospace with fontconfig and freetype" { - if (options.backend != .fontconfig_freetype) return error.SkipZigTest; - - const testing = std.testing; - const alloc = testing.allocator; - const Discover = @import("main.zig").Discover; - - // Search for fonts - var fc = Discover.init(); - var it = try fc.discover(alloc, .{ .family = "monospace", .size = 12 }); - defer it.deinit(); - - // Initialize the group with the deferred face - var lib = try Library.init(); - defer lib.deinit(); - var group = try init(alloc, lib, .{ .points = 12 }); - defer group.deinit(); - _ = try group.addFace(.regular, .{ .deferred = (try it.next()).? }); - - // Should find all visible ASCII - var atlas_greyscale = try font.Atlas.init(alloc, 512, .greyscale); - defer atlas_greyscale.deinit(alloc); - var i: u32 = 32; - while (i < 127) : (i += 1) { - const idx = group.indexForCodepoint(i, .regular, null).?; - try testing.expectEqual(Style.regular, idx.style); - try testing.expectEqual(@as(FontIndex.IndexInt, 0), idx.idx); - - // Render it - const face = try group.faceFromIndex(idx); - const glyph_index = face.glyphIndex(i).?; - _ = try group.renderGlyph( - alloc, - &atlas_greyscale, - idx, - glyph_index, - .{}, - ); - } -} - -test "faceFromIndex returns pointer" { - const testing = std.testing; - const alloc = testing.allocator; - const testFont = @import("test.zig").fontRegular; - - var atlas_greyscale = try font.Atlas.init(alloc, 512, .greyscale); - defer atlas_greyscale.deinit(alloc); - - var lib = try Library.init(); - defer lib.deinit(); - - var group = try init(alloc, lib, .{ .points = 12, .xdpi = 96, .ydpi = 96 }); - defer group.deinit(); - - _ = try group.addFace(.regular, .{ .loaded = try Face.init( - lib, - testFont, - .{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } }, - ) }); - - { - const idx = group.indexForCodepoint('A', .regular, null).?; - const face1 = try group.faceFromIndex(idx); - const face2 = try group.faceFromIndex(idx); - try testing.expectEqual(@intFromPtr(face1), @intFromPtr(face2)); - } -} - -test "indexFromCodepoint: prefer emoji in non-fallback font" { - // CoreText can't load Noto - if (font.options.backend == .coretext) return error.SkipZigTest; - - const testing = std.testing; - const alloc = testing.allocator; - const testCozette = @import("test.zig").fontCozette; - 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 group = try init(alloc, lib, .{ .points = 12 }); - defer group.deinit(); - - _ = try group.addFace( - .regular, - .{ .loaded = try Face.init( - lib, - testCozette, - .{ .size = .{ .points = 12 } }, - ) }, - ); - _ = try group.addFace( - .regular, - .{ .fallback_loaded = try Face.init( - lib, - testEmoji, - .{ .size = .{ .points = 12 } }, - ) }, - ); - - // The "alien" emoji is default emoji presentation but present - // in Cozette as text. Since Cozette isn't a fallback, we shoulod - // load it from there. - { - const idx = group.indexForCodepoint(0x1F47D, .regular, null).?; - try testing.expectEqual(Style.regular, idx.style); - try testing.expectEqual(@as(FontIndex.IndexInt, 0), idx.idx); - } - - // If we specifically request emoji, we should find it in the fallback. - { - const idx = group.indexForCodepoint(0x1F47D, .regular, .emoji).?; - try testing.expectEqual(Style.regular, idx.style); - try testing.expectEqual(@as(FontIndex.IndexInt, 1), idx.idx); - } -} - -test "indexFromCodepoint: prefer emoji with correct presentation" { - // CoreText can't load Noto - if (font.options.backend == .coretext) return error.SkipZigTest; - - const testing = std.testing; - const alloc = testing.allocator; - const testCozette = @import("test.zig").fontCozette; - 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 group = try init(alloc, lib, .{ .points = 12 }); - defer group.deinit(); - - _ = try group.addFace( - .regular, - .{ .loaded = try Face.init( - lib, - testEmoji, - .{ .size = .{ .points = 12 } }, - ) }, - ); - _ = try group.addFace( - .regular, - .{ .loaded = try Face.init( - lib, - testCozette, - .{ .size = .{ .points = 12 } }, - ) }, - ); - - // Check that we check the default presentation - { - const idx = group.indexForCodepoint(0x1F47D, .regular, null).?; - try testing.expectEqual(Style.regular, idx.style); - try testing.expectEqual(@as(FontIndex.IndexInt, 0), idx.idx); - } - - // If we specifically request text - { - const idx = group.indexForCodepoint(0x1F47D, .regular, .text).?; - try testing.expectEqual(Style.regular, idx.style); - try testing.expectEqual(@as(FontIndex.IndexInt, 1), idx.idx); - } -} diff --git a/src/font/GroupCache.zig b/src/font/GroupCache.zig deleted file mode 100644 index 99e5548bf..000000000 --- a/src/font/GroupCache.zig +++ /dev/null @@ -1,381 +0,0 @@ -//! 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, - opts: font.face.RenderOptions, -}; - -/// 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, .opts = opts }; - 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, .{ .size = .{ .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); - - const 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, - }); - - const 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, - .{ .size = .{ .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); - } -} diff --git a/src/font/GroupCacheSet.zig b/src/font/GroupCacheSet.zig deleted file mode 100644 index 818e13e91..000000000 --- a/src/font/GroupCacheSet.zig +++ /dev/null @@ -1,518 +0,0 @@ -//! This structure contains a set of GroupCache instances keyed by -//! unique font configuration. -//! -//! Most terminals (surfaces) will share the same font configuration. -//! This structure allows expensive font information such as -//! the font atlas, glyph cache, font faces, etc. to be shared. -//! -//! The Ghostty renderers which use this information run on their -//! own threads so this structure is thread-safe. It expects that -//! the case where all glyphs are cached is the common case and -//! optimizes for that. When a glyph is not cached, all renderers -//! that share the same font configuration will be blocked until -//! the glyph is cached. -const GroupCacheSet = @This(); - -const std = @import("std"); -const builtin = @import("builtin"); -const assert = std.debug.assert; -const Allocator = std.mem.Allocator; -const ArenaAllocator = std.heap.ArenaAllocator; -const fontpkg = @import("main.zig"); -const Collection = fontpkg.Collection; -const Discover = fontpkg.Discover; -const Style = fontpkg.Style; -const Library = fontpkg.Library; -const Metrics = fontpkg.face.Metrics; -const CodepointMap = fontpkg.CodepointMap; -const DesiredSize = fontpkg.face.DesiredSize; -const Face = fontpkg.Face; -const Group = fontpkg.Group; -const GroupCache = fontpkg.GroupCache; -const discovery = @import("discovery.zig"); -const configpkg = @import("../config.zig"); -const Config = configpkg.Config; - -const log = std.log.scoped(.font_group_cache_set); - -/// The allocator to use for all heap allocations. -alloc: Allocator, - -/// The map of font configurations to GroupCache instances. -map: Map = .{}, - -/// The font library that is used for all font groups. -font_lib: Library, - -/// Font discovery mechanism. -font_discover: ?Discover = null, - -/// Initialize a new GroupCacheSet. -pub fn init(alloc: Allocator) !GroupCacheSet { - var font_lib = try Library.init(); - errdefer font_lib.deinit(); - - return .{ - .alloc = alloc, - .map = .{}, - .font_lib = font_lib, - }; -} - -pub fn deinit(self: *GroupCacheSet) void { - var it = self.map.iterator(); - while (it.next()) |entry| { - entry.key_ptr.deinit(); - const ref = entry.value_ptr.*; - ref.cache.deinit(self.alloc); - self.alloc.destroy(ref.cache); - } - self.map.deinit(self.alloc); - - if (comptime Discover != void) { - if (self.font_discover) |*v| v.deinit(); - } - - self.font_lib.deinit(); -} - -/// Initialize a GroupCache for the given font configuration. If the -/// GroupCache is not present it will be initialized with a ref count of -/// 1. If it is present, the ref count will be incremented. -/// -/// This is NOT thread-safe. -pub fn groupRef( - self: *GroupCacheSet, - config: *const Config, - font_size: DesiredSize, -) !struct { Key, *GroupCache } { - var key = try Key.init(self.alloc, config); - errdefer key.deinit(); - - const gop = try self.map.getOrPut(self.alloc, key); - if (gop.found_existing) { - log.debug("found cached GroupCache for font config", .{}); - - // We can deinit the key because we found a cached value. - key.deinit(); - - // Increment our ref count and return the cache - gop.value_ptr.ref += 1; - return .{ gop.key_ptr.*, gop.value_ptr.cache }; - } - errdefer self.map.removeByPtr(gop.key_ptr); - - log.debug("initializing new GroupCache for font config", .{}); - - // A new font config, initialize the cache. - const cache = try self.alloc.create(GroupCache); - errdefer self.alloc.destroy(cache); - gop.value_ptr.* = .{ - .cache = cache, - .ref = 1, - }; - - // Build our font face collection that we'll use to initialize - // the Group. - - cache.* = try GroupCache.init(self.alloc, group: { - var group = try Group.init( - self.alloc, - self.font_lib, - font_size, - try self.collection(&key, font_size), - ); - errdefer group.deinit(); - group.metric_modifiers = key.metric_modifiers; - group.codepoint_map = key.codepoint_map; - group.discover = try self.discover(); - - // Set our styles - group.styles.set(.bold, config.@"font-style-bold" != .false); - group.styles.set(.italic, config.@"font-style-italic" != .false); - group.styles.set(.bold_italic, config.@"font-style-bold-italic" != .false); - - // Auto-italicize if we have to. - try group.italicize(); - - log.info("font loading complete, any non-logged faces are using the built-in font", .{}); - break :group group; - }); - errdefer cache.deinit(self.alloc); - - return .{ gop.key_ptr.*, gop.value_ptr.cache }; -} - -/// Builds the Collection for the given configuration key and -/// initial font size. -fn collection( - self: *GroupCacheSet, - key: *const Key, - size: DesiredSize, -) !Collection { - var c = try Collection.init(self.alloc); - errdefer c.deinit(self.alloc); - - const opts: fontpkg.face.Options = .{ - .size = size, - .metric_modifiers = &key.metric_modifiers, - }; - - // Search for fonts - if (Discover != void) discover: { - const disco = try self.discover() orelse { - log.warn( - "font discovery not available, cannot search for fonts", - .{}, - ); - break :discover; - }; - - // A buffer we use to store the font names for logging. - var name_buf: [256]u8 = undefined; - - inline for (@typeInfo(Style).Enum.fields) |field| { - const style = @field(Style, field.name); - for (key.descriptorsForStyle(style)) |desc| { - var disco_it = try disco.discover(self.alloc, desc); - defer disco_it.deinit(); - if (try disco_it.next()) |face| { - log.info("font {s}: {s}", .{ - field.name, - try face.name(&name_buf), - }); - - _ = try c.add( - self.alloc, - style, - .{ .deferred = face }, - ); - } else log.warn("font-family {s} not found: {s}", .{ - field.name, - desc.family.?, - }); - } - } - } - - // Our built-in font will be used as a backup - _ = try c.add( - self.alloc, - .regular, - .{ .fallback_loaded = try Face.init( - self.font_lib, - face_ttf, - opts, - ) }, - ); - _ = try c.add( - self.alloc, - .bold, - .{ .fallback_loaded = try Face.init( - self.font_lib, - face_bold_ttf, - opts, - ) }, - ); - - // On macOS, always search for and add the Apple Emoji font - // as our preferred emoji font for fallback. We do this in case - // people add other emoji fonts to their system, we always want to - // prefer the official one. Users can override this by explicitly - // specifying a font-family for emoji. - if (comptime builtin.target.isDarwin()) apple_emoji: { - const disco = try self.discover() orelse break :apple_emoji; - var disco_it = try disco.discover(self.alloc, .{ - .family = "Apple Color Emoji", - }); - defer disco_it.deinit(); - if (try disco_it.next()) |face| { - _ = try c.add( - self.alloc, - .regular, - .{ .fallback_deferred = face }, - ); - } - } - - // Emoji fallback. We don't include this on Mac since Mac is expected - // to always have the Apple Emoji available on the system. - if (comptime !builtin.target.isDarwin() or Discover == void) { - _ = try c.add( - self.alloc, - .regular, - .{ .fallback_loaded = try Face.init( - self.font_lib, - face_emoji_ttf, - opts, - ) }, - ); - _ = try c.add( - self.alloc, - .regular, - .{ .fallback_loaded = try Face.init( - self.font_lib, - face_emoji_text_ttf, - opts, - ) }, - ); - } - - return c; -} - -/// Decrement the ref count for the given key. If the ref count is zero, -/// the GroupCache will be deinitialized and removed from the map.j:w -pub fn groupDeref(self: *GroupCacheSet, key: Key) void { - const entry = self.map.getEntry(key) orelse return; - assert(entry.value_ptr.ref >= 1); - - // If we have more than one reference, decrement and return. - if (entry.value_ptr.ref > 1) { - entry.value_ptr.ref -= 1; - return; - } - - // We are at a zero ref count so deinit the group and remove. - entry.key_ptr.deinit(); - entry.value_ptr.cache.deinit(self.alloc); - self.alloc.destroy(entry.value_ptr.cache); - self.map.removeByPtr(entry.key_ptr); -} - -/// Map of font configurations to GroupCache instances. The GroupCache -/// instances are pointers that are heap allocated so that they're -/// stable pointers across hash map resizes. -pub const Map = std.HashMapUnmanaged( - Key, - RefGroupCache, - struct { - const KeyType = Key; - - pub fn hash(ctx: @This(), k: KeyType) u64 { - _ = ctx; - return k.hashcode(); - } - - pub fn eql(ctx: @This(), a: KeyType, b: KeyType) bool { - return ctx.hash(a) == ctx.hash(b); - } - }, - std.hash_map.default_max_load_percentage, -); - -/// Initialize once and return the font discovery mechanism. This remains -/// initialized throughout the lifetime of the application because some -/// font discovery mechanisms (i.e. fontconfig) are unsafe to reinit. -fn discover(self: *GroupCacheSet) !?*Discover { - // If we're built without a font discovery mechanism, return null - if (comptime Discover == void) return null; - - // If we initialized, use it - if (self.font_discover) |*v| return v; - - self.font_discover = Discover.init(); - return &self.font_discover.?; -} - -/// Ref-counted GroupCache. -const RefGroupCache = struct { - cache: *GroupCache, - ref: u32 = 0, -}; - -/// The key used to uniquely identify a font configuration. -pub const Key = struct { - arena: ArenaAllocator, - - /// The descriptors used for all the fonts added to the - /// initial group, including all styles. This is hashed - /// in order so the order matters. All users of the struct - /// should ensure that the order is consistent. - descriptors: []const discovery.Descriptor, - - /// These are the offsets into the descriptors array for - /// each style. For example, bold is from - /// offsets[@intFromEnum(.bold) - 1] to - /// offsets[@intFromEnum(.bold)]. - style_offsets: StyleOffsets = .{0} ** style_offsets_len, - - /// The codepoint map configuration. - codepoint_map: CodepointMap, - - /// The metric modifier set configuration. - metric_modifiers: Metrics.ModifierSet, - - const style_offsets_len = std.enums.directEnumArrayLen(Style, 0); - const StyleOffsets = [style_offsets_len]usize; - - comptime { - // We assume this throughout this structure. If this changes - // we may need to change this structure. - assert(@intFromEnum(Style.regular) == 0); - assert(@intFromEnum(Style.bold) == 1); - assert(@intFromEnum(Style.italic) == 2); - assert(@intFromEnum(Style.bold_italic) == 3); - } - - pub fn init( - alloc_gpa: Allocator, - config: *const Config, - ) !Key { - var arena = ArenaAllocator.init(alloc_gpa); - errdefer arena.deinit(); - const alloc = arena.allocator(); - - var descriptors = std.ArrayList(discovery.Descriptor).init(alloc); - defer descriptors.deinit(); - for (config.@"font-family".list.items) |family| { - try descriptors.append(.{ - .family = family, - .style = config.@"font-style".nameValue(), - .size = config.@"font-size", - .variations = config.@"font-variation".list.items, - }); - } - - // In all the styled cases below, we prefer to specify an exact - // style via the `font-style` configuration. If a style is not - // specified, we use the discovery mechanism to search for a - // style category such as bold, italic, etc. We can't specify both - // because the latter will restrict the search to only that. If - // a user says `font-style = italic` for the bold face for example, - // no results would be found if we restrict to ALSO searching for - // italic. - for (config.@"font-family-bold".list.items) |family| { - const style = config.@"font-style-bold".nameValue(); - try descriptors.append(.{ - .family = family, - .style = style, - .size = config.@"font-size", - .bold = style == null, - .variations = config.@"font-variation".list.items, - }); - } - for (config.@"font-family-italic".list.items) |family| { - const style = config.@"font-style-italic".nameValue(); - try descriptors.append(.{ - .family = family, - .style = style, - .size = config.@"font-size", - .italic = style == null, - .variations = config.@"font-variation".list.items, - }); - } - for (config.@"font-family-bold-italic".list.items) |family| { - const style = config.@"font-style-bold-italic".nameValue(); - try descriptors.append(.{ - .family = family, - .style = style, - .size = config.@"font-size", - .bold = style == null, - .italic = style == null, - .variations = config.@"font-variation".list.items, - }); - } - - // Setup the codepoint map - const codepoint_map: CodepointMap = map: { - const map = config.@"font-codepoint-map"; - if (map.map.list.len == 0) break :map .{}; - const clone = try config.@"font-codepoint-map".clone(alloc); - break :map clone.map; - }; - - // Metric modifiers - const metric_modifiers: Metrics.ModifierSet = set: { - var set: Metrics.ModifierSet = .{}; - if (config.@"adjust-cell-width") |m| try set.put(alloc, .cell_width, m); - if (config.@"adjust-cell-height") |m| try set.put(alloc, .cell_height, m); - if (config.@"adjust-font-baseline") |m| try set.put(alloc, .cell_baseline, m); - if (config.@"adjust-underline-position") |m| try set.put(alloc, .underline_position, m); - if (config.@"adjust-underline-thickness") |m| try set.put(alloc, .underline_thickness, m); - if (config.@"adjust-strikethrough-position") |m| try set.put(alloc, .strikethrough_position, m); - if (config.@"adjust-strikethrough-thickness") |m| try set.put(alloc, .strikethrough_thickness, m); - break :set set; - }; - - return .{ - .arena = arena, - .descriptors = try descriptors.toOwnedSlice(), - .style_offsets = .{ - config.@"font-family".list.items.len, - config.@"font-family-bold".list.items.len, - config.@"font-family-italic".list.items.len, - config.@"font-family-bold-italic".list.items.len, - }, - .codepoint_map = codepoint_map, - .metric_modifiers = metric_modifiers, - }; - } - - pub fn deinit(self: *Key) void { - self.arena.deinit(); - } - - /// Get the descriptors for the given font style that can be - /// used with discovery. - pub fn descriptorsForStyle( - self: Key, - style: Style, - ) []const discovery.Descriptor { - const idx = @intFromEnum(style); - const start: usize = if (idx == 0) 0 else self.style_offsets[idx - 1]; - const end = self.style_offsets[idx]; - return self.descriptors[start..end]; - } - - /// Hash the key with the given hasher. - pub fn hash(self: Key, hasher: anytype) void { - const autoHash = std.hash.autoHash; - autoHash(hasher, self.descriptors.len); - for (self.descriptors) |d| d.hash(hasher); - autoHash(hasher, self.codepoint_map); - autoHash(hasher, self.metric_modifiers.count()); - if (self.metric_modifiers.count() > 0) { - inline for (@typeInfo(Metrics.Key).Enum.fields) |field| { - const key = @field(Metrics.Key, field.name); - if (self.metric_modifiers.get(key)) |value| { - autoHash(hasher, key); - value.hash(hasher); - } - } - } - } - - /// Returns a hash code that can be used to uniquely identify this - /// action. - pub fn hashcode(self: Key) u64 { - var hasher = std.hash.Wyhash.init(0); - self.hash(&hasher); - return hasher.final(); - } -}; - -const face_ttf = @embedFile("res/JetBrainsMono-Regular.ttf"); -const face_bold_ttf = @embedFile("res/JetBrainsMono-Bold.ttf"); -const face_emoji_ttf = @embedFile("res/NotoColorEmoji.ttf"); -const face_emoji_text_ttf = @embedFile("res/NotoEmoji-Regular.ttf"); - -test "Key" { - const testing = std.testing; - const alloc = testing.allocator; - var cfg = try Config.default(alloc); - defer cfg.deinit(); - - var k = try Key.init(alloc, &cfg); - defer k.deinit(); - - try testing.expect(k.hashcode() > 0); -} - -test "basics" { - const testing = std.testing; - const alloc = testing.allocator; - - var set = try GroupCacheSet.init(alloc); - defer set.deinit(); -} diff --git a/src/font/main.zig b/src/font/main.zig index 0a0d376e9..dffdac5c0 100644 --- a/src/font/main.zig +++ b/src/font/main.zig @@ -10,9 +10,6 @@ pub const CodepointResolver = @import("CodepointResolver.zig"); pub const Collection = @import("Collection.zig"); pub const DeferredFace = @import("DeferredFace.zig"); pub const Face = face.Face; -pub const Group = @import("Group.zig"); -pub const GroupCache = @import("GroupCache.zig"); -pub const GroupCacheSet = @import("GroupCacheSet.zig"); pub const Glyph = @import("Glyph.zig"); pub const Metrics = face.Metrics; pub const shape = @import("shape.zig"); @@ -30,8 +27,6 @@ pub usingnamespace @import("library.zig"); pub usingnamespace if (builtin.target.isWasm()) struct { pub usingnamespace Atlas.Wasm; pub usingnamespace DeferredFace.Wasm; - pub usingnamespace Group.Wasm; - pub usingnamespace GroupCache.Wasm; pub usingnamespace face.web_canvas.Wasm; pub usingnamespace shape.web_canvas.Wasm; } else struct {}; diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index a5e87b59b..f525f986a 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -160,7 +160,7 @@ pub const DerivedConfig = struct { font_thicken: bool, font_features: std.ArrayListUnmanaged([:0]const u8), - font_styles: font.Group.StyleStatus, + font_styles: font.CodepointResolver.StyleStatus, cursor_color: ?terminal.color.RGB, cursor_opacity: f64, cursor_text: ?terminal.color.RGB, @@ -189,7 +189,7 @@ pub const DerivedConfig = struct { const font_features = try config.@"font-feature".list.clone(alloc); // Get our font styles - var font_styles = font.Group.StyleStatus.initFill(true); + var font_styles = font.CodepointResolver.StyleStatus.initFill(true); font_styles.set(.bold, config.@"font-style-bold" != .false); font_styles.set(.italic, config.@"font-style-italic" != .false); font_styles.set(.bold_italic, config.@"font-style-bold-italic" != .false); diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index d6669ccfa..2056a236d 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -243,7 +243,7 @@ pub const DerivedConfig = struct { font_thicken: bool, font_features: std.ArrayListUnmanaged([:0]const u8), - font_styles: font.Group.StyleStatus, + font_styles: font.CodepointResolver.StyleStatus, cursor_color: ?terminal.color.RGB, cursor_text: ?terminal.color.RGB, cursor_opacity: f64, @@ -272,7 +272,7 @@ pub const DerivedConfig = struct { const font_features = try config.@"font-feature".list.clone(alloc); // Get our font styles - var font_styles = font.Group.StyleStatus.initFill(true); + var font_styles = font.CodepointResolver.StyleStatus.initFill(true); font_styles.set(.bold, config.@"font-style-bold" != .false); font_styles.set(.italic, config.@"font-style-italic" != .false); font_styles.set(.bold_italic, config.@"font-style-bold-italic" != .false);