Merge pull request #333 from mitchellh/face-rework

font: Improve quirks mode performance, DeferredFace, Group refactor

This was motivated by #331. I realized with #331 that this added a lot of overhead on every render frame, because for each text run being rendered, we were (1) reloading the font name (unknown performance cost on macOS due to closed source, looks like [it ain't exactly free in Freetype](https://sourcegraph.com/github.com/freetype/freetype@4d8db130ea4342317581bab65fc96365ce806b77/-/blob/src/base/ftsnames.c?L44)) (2) doing multiple string compares (3) multiple function call overhead. And realistically, quirks mode will be _rare_, so paying a very active cost for a rare thing is 💩 .

In investigating that I finally shaved some yaks I've been wanting to shave on this path... the end result is that overall we should be doing [very] slightly less work no matter what and using slightly less memory. And on the renderer path, we're doing _way_ less work. 

- All loaded font `Face` structures must also check and cache their quirks mode settings. This makes #331 a single boolean check per text run which is effectively free compared to the actual shaping task. The cost is one additional byte on the face structure (well, whatever the alignment is).
- `DeferredFace` now only represents an _unloaded_ face (that can be loaded multiple times). This lowers memory usage significantly (`Face` was large) per deferred face.
- `DeferredFace.name()` now takes a buffer for output instead of just failing on no static space, weird decision. No performance impact but makes this function more robust.
- `Group.addFace` now takes a tagged union representing either a deferred or loaded face, this gets rid of all the weird `DeferredFace.initLoaded` wrappers that were unnecessary and dumb. No real performance impact.
- When a font face is loaded in `Group` (lots of things trigger this), the DeferredFace it came from is now deinitialized immediately. This should save on memory significantly because we free all the font discovery information. We can do this because a deferred face is only ever loaded _once_ when its part of a `Group`, and the group owns the deferred face.
- Auto-italicize moves into `Group` and is no longer duplicated between Deferred and normal Face. No performance change, less code.
This commit is contained in:
Mitchell Hashimoto
2023-08-25 15:13:01 -07:00
committed by GitHub
8 changed files with 170 additions and 238 deletions

View File

@ -222,6 +222,9 @@ pub fn init(
};
group.discover = disco;
// A buffer we use to store the font names for logging.
var name_buf: [256]u8 = undefined;
if (config.@"font-family") |family| {
var disco_it = try disco.discover(.{
.family = family,
@ -229,8 +232,8 @@ pub fn init(
});
defer disco_it.deinit();
if (try disco_it.next()) |face| {
log.info("font regular: {s}", .{try face.name()});
try group.addFace(alloc, .regular, face);
log.info("font regular: {s}", .{try face.name(&name_buf)});
try group.addFace(.regular, .{ .deferred = face });
} else log.warn("font-family not found: {s}", .{family});
}
if (config.@"font-family-bold") |family| {
@ -241,8 +244,8 @@ pub fn init(
});
defer disco_it.deinit();
if (try disco_it.next()) |face| {
log.info("font bold: {s}", .{try face.name()});
try group.addFace(alloc, .bold, face);
log.info("font bold: {s}", .{try face.name(&name_buf)});
try group.addFace(.bold, .{ .deferred = face });
} else log.warn("font-family-bold not found: {s}", .{family});
}
if (config.@"font-family-italic") |family| {
@ -253,8 +256,8 @@ pub fn init(
});
defer disco_it.deinit();
if (try disco_it.next()) |face| {
log.info("font italic: {s}", .{try face.name()});
try group.addFace(alloc, .italic, face);
log.info("font italic: {s}", .{try face.name(&name_buf)});
try group.addFace(.italic, .{ .deferred = face });
} else log.warn("font-family-italic not found: {s}", .{family});
}
if (config.@"font-family-bold-italic") |family| {
@ -266,56 +269,35 @@ pub fn init(
});
defer disco_it.deinit();
if (try disco_it.next()) |face| {
log.info("font bold+italic: {s}", .{try face.name()});
try group.addFace(alloc, .bold_italic, face);
log.info("font bold+italic: {s}", .{try face.name(&name_buf)});
try group.addFace(.bold_italic, .{ .deferred = face });
} else log.warn("font-family-bold-italic not found: {s}", .{family});
}
}
// Our built-in font will be used as a backup
try group.addFace(
alloc,
.regular,
font.DeferredFace.initLoaded(try font.Face.init(font_lib, face_ttf, font_size)),
.{ .loaded = try font.Face.init(font_lib, face_ttf, font_size) },
);
try group.addFace(
alloc,
.bold,
font.DeferredFace.initLoaded(try font.Face.init(font_lib, face_bold_ttf, font_size)),
.{ .loaded = try font.Face.init(font_lib, face_bold_ttf, font_size) },
);
// If we support auto-italicization and we don't have an italic face,
// then we can try to auto-italicize our regular face.
if (comptime font.DeferredFace.canItalicize()) {
if (group.getFace(.italic) == null) {
if (group.getFace(.regular)) |regular| {
if (try regular.italicize()) |face| {
log.info("font auto-italicized: {s}", .{try face.name()});
try group.addFace(alloc, .italic, face);
}
}
}
} else {
// We don't support auto-italics. If we don't have an italic font
// face let the user know so they aren't surprised (if they look
// at logs).
if (group.getFace(.italic) == null) {
log.warn("no italic font face available, italics will not render", .{});
}
}
// Auto-italicize if we have to.
try group.italicize();
// Emoji fallback. We don't include this on Mac since Mac is expected
// to always have the Apple Emoji available.
if (builtin.os.tag != .macos or font.Discover == void) {
try group.addFace(
alloc,
.regular,
font.DeferredFace.initLoaded(try font.Face.init(font_lib, face_emoji_ttf, font_size)),
.{ .loaded = try font.Face.init(font_lib, face_emoji_ttf, font_size) },
);
try group.addFace(
alloc,
.regular,
font.DeferredFace.initLoaded(try font.Face.init(font_lib, face_emoji_text_ttf, font_size)),
.{ .loaded = try font.Face.init(font_lib, face_emoji_text_ttf, font_size) },
);
}
@ -328,8 +310,9 @@ pub fn init(
});
defer disco_it.deinit();
if (try disco_it.next()) |face| {
log.info("font emoji: {s}", .{try face.name()});
try group.addFace(alloc, .regular, face);
var name_buf: [256]u8 = undefined;
log.info("font emoji: {s}", .{try face.name(&name_buf)});
try group.addFace(.regular, .{ .deferred = face });
}
}
}

View File

@ -30,9 +30,6 @@ const FaceState = switch (options.backend) {
.web_canvas => WebCanvas,
};
/// The loaded face (once loaded).
face: ?Face = null,
/// Fontconfig
fc: if (options.backend == .fontconfig_freetype) ?Fontconfig else void =
if (options.backend == .fontconfig_freetype) null else {},
@ -72,13 +69,6 @@ pub const CoreText = struct {
self.font.release();
self.* = undefined;
}
/// Auto-italicize the font by applying a skew.
pub fn italicize(self: *const CoreText) !CoreText {
const ct_font = try self.font.copyWithAttributes(0.0, &Face.italic_skew, null);
errdefer ct_font.release();
return .{ .font = ct_font };
}
};
/// WebCanvas specific data. This is only present when building with canvas.
@ -98,14 +88,7 @@ pub const WebCanvas = struct {
}
};
/// Initialize a deferred face that is already pre-loaded. The deferred face
/// takes ownership over the loaded face, deinit will deinit the loaded face.
pub fn initLoaded(face: Face) DeferredFace {
return .{ .face = face };
}
pub fn deinit(self: *DeferredFace) void {
if (self.face) |*face| face.deinit();
switch (options.backend) {
.fontconfig_freetype => if (self.fc) |*fc| fc.deinit(),
.coretext, .coretext_freetype => if (self.ct) |*ct| ct.deinit(),
@ -115,14 +98,9 @@ pub fn deinit(self: *DeferredFace) void {
self.* = undefined;
}
/// Returns true if the face has been loaded.
pub inline fn loaded(self: DeferredFace) bool {
return self.face != null;
}
/// Returns the name of this face. The memory is always owned by the
/// face so it doesn't have to be freed.
pub fn name(self: DeferredFace) ![:0]const u8 {
pub fn name(self: DeferredFace, buf: []u8) ![]const u8 {
switch (options.backend) {
.freetype => {},
@ -136,22 +114,15 @@ pub fn name(self: DeferredFace) ![:0]const u8 {
// this to be returned efficiently." In this case, we need
// to allocate. But we can't return an allocated string because
// we don't have an allocator. Let's use the stack and log it.
var buf: [1024]u8 = undefined;
const buf_name = display_name.cstring(&buf, .utf8) orelse
"<not enough internal storage space>";
log.info(
"CoreText font required too much space to copy, value = {s}",
.{buf_name},
);
break :unsupported "<CoreText internal storage limited, see logs>";
break :unsupported display_name.cstring(buf, .utf8) orelse
return error.OutOfMemory;
};
},
.web_canvas => if (self.wc) |wc| return wc.font_str,
}
return "TODO: built-in font names";
return "";
}
/// Load the deferred font face. This does nothing if the face is loaded.
@ -159,69 +130,48 @@ pub fn load(
self: *DeferredFace,
lib: Library,
size: font.face.DesiredSize,
) !void {
// No-op if we already loaded
if (self.face != null) return;
switch (options.backend) {
.fontconfig_freetype => {
try self.loadFontconfig(lib, size);
return;
},
.coretext => {
try self.loadCoreText(lib, size);
return;
},
.coretext_freetype => {
try self.loadCoreTextFreetype(lib, size);
return;
},
.web_canvas => {
try self.loadWebCanvas(size);
return;
},
) !Face {
return switch (options.backend) {
.fontconfig_freetype => try self.loadFontconfig(lib, size),
.coretext => try self.loadCoreText(lib, size),
.coretext_freetype => try self.loadCoreTextFreetype(lib, size),
.web_canvas => try self.loadWebCanvas(size),
// Unreachable because we must be already loaded or have the
// proper configuration for one of the other deferred mechanisms.
.freetype => unreachable,
}
};
}
fn loadFontconfig(
self: *DeferredFace,
lib: Library,
size: font.face.DesiredSize,
) !void {
assert(self.face == null);
) !Face {
const fc = self.fc.?;
// Filename and index for our face so we can load it
const filename = (try fc.pattern.get(.file, 0)).string;
const face_index = (try fc.pattern.get(.index, 0)).integer;
self.face = try Face.initFile(lib, filename, face_index, size);
return try Face.initFile(lib, filename, face_index, size);
}
fn loadCoreText(
self: *DeferredFace,
lib: Library,
size: font.face.DesiredSize,
) !void {
) !Face {
_ = lib;
assert(self.face == null);
const ct = self.ct.?;
self.face = try Face.initFontCopy(ct.font, size);
return try Face.initFontCopy(ct.font, size);
}
fn loadCoreTextFreetype(
self: *DeferredFace,
lib: Library,
size: font.face.DesiredSize,
) !void {
assert(self.face == null);
) !Face {
const ct = self.ct.?;
// Get the URL for the font so we can get the filepath
@ -253,16 +203,15 @@ fn loadCoreTextFreetype(
// TODO: face index 0 is not correct long term and we should switch
// to using CoreText for rendering, too.
//std.log.warn("path={s}", .{path_slice});
self.face = try Face.initFile(lib, buf[0..path_slice.len :0], 0, size);
return try Face.initFile(lib, buf[0..path_slice.len :0], 0, size);
}
fn loadWebCanvas(
self: *DeferredFace,
size: font.face.DesiredSize,
) !void {
assert(self.face == null);
) !Face {
const wc = self.wc.?;
self.face = try Face.initNamed(wc.alloc, wc.font_str, size, wc.presentation);
return try Face.initNamed(wc.alloc, wc.font_str, size, wc.presentation);
}
/// Returns true if this face can satisfy the given codepoint and
@ -273,12 +222,6 @@ fn loadWebCanvas(
/// discovery mechanism (i.e. fontconfig). If no discovery is used,
/// the face is always expected to be loaded.
pub fn hasCodepoint(self: DeferredFace, cp: u32, p: ?Presentation) bool {
// If we have the face, use the face.
if (self.face) |face| {
if (p) |desired| if (face.presentation != desired) return false;
return face.glyphIndex(cp) != null;
}
switch (options.backend) {
.fontconfig_freetype => {
// If we are using fontconfig, use the fontconfig metadata to
@ -351,40 +294,6 @@ pub fn hasCodepoint(self: DeferredFace, cp: u32, p: ?Presentation) bool {
unreachable;
}
/// Returns true if our deferred font implementation supports auto-itacilization.
pub fn canItalicize() bool {
return @hasDecl(FaceState, "italicize") and @hasDecl(Face, "italicize");
}
/// Returns a new deferred face with the italicized version of this face
/// by applying a skew. This is NOT TRUE italics. You should use the discovery
/// mechanism to try to find an italic font. This is a fallback for when
/// that fails.
pub fn italicize(self: *const DeferredFace) !?DeferredFace {
if (comptime !canItalicize()) return null;
var result: DeferredFace = .{};
if (self.face) |face| {
result.face = try face.italicize();
}
switch (options.backend) {
.freetype => {},
.fontconfig_freetype => if (self.fc) |*fc| {
result.fc = try fc.italicize();
},
.coretext, .coretext_freetype => if (self.ct) |*ct| {
result.ct = try ct.italicize();
},
.web_canvas => if (self.wc) |*wc| {
result.wc = try wc.italicize();
},
}
return result;
}
/// The wasm-compatible API.
pub const Wasm = struct {
const wasm = @import("../os/wasm.zig");
@ -429,30 +338,8 @@ pub const Wasm = struct {
return;
};
}
/// Caller should not free this, the face is owned by the deferred face.
export fn deferred_face_face(self: *DeferredFace) ?*Face {
assert(self.loaded());
return &self.face.?;
}
};
test "preloaded" {
const testing = std.testing;
const testFont = @import("test.zig").fontRegular;
var lib = try Library.init();
defer lib.deinit();
var face = try Face.init(lib, testFont, .{ .points = 12 });
errdefer face.deinit();
var def = initLoaded(face);
defer def.deinit();
try testing.expect(def.hasCodepoint(' ', null));
}
test "fontconfig" {
if (options.backend != .fontconfig_freetype) return error.SkipZigTest;
@ -471,16 +358,15 @@ test "fontconfig" {
break :def (try it.next()).?;
};
defer def.deinit();
try testing.expect(!def.loaded());
// Verify we can get the name
const n = try def.name();
var buf: [1024]u8 = undefined;
const n = try def.name(&buf);
try testing.expect(n.len > 0);
// Load it and verify it works
try def.load(lib, .{ .points = 12 });
try testing.expect(def.hasCodepoint(' ', null));
try testing.expect(def.face.?.glyphIndex(' ') != null);
const face = try def.load(lib, .{ .points = 12 });
try testing.expect(face.glyphIndex(' ') != null);
}
test "coretext" {
@ -501,15 +387,14 @@ test "coretext" {
break :def (try it.next()).?;
};
defer def.deinit();
try testing.expect(!def.loaded());
try testing.expect(def.hasCodepoint(' ', null));
// Verify we can get the name
const n = try def.name();
var buf: [1024]u8 = undefined;
const n = try def.name(&buf);
try testing.expect(n.len > 0);
// Load it and verify it works
try def.load(lib, .{ .points = 12 });
try testing.expect(def.hasCodepoint(' ', null));
try testing.expect(def.face.?.glyphIndex(' ') != null);
const face = try def.load(lib, .{ .points = 12 });
try testing.expect(face.glyphIndex(' ') != null);
}

View File

@ -22,6 +22,7 @@ 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);
@ -30,8 +31,7 @@ const log = std.log.scoped(.font_group);
// 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(DeferredFace));
const StyleArray = std.EnumArray(Style, std.ArrayListUnmanaged(GroupFace));
/// The allocator for this group
alloc: Allocator,
@ -55,6 +55,19 @@ discover: ?*font.Discover = null,
/// terminal rendering will look wrong.
sprite: ?font.sprite.Face = null,
/// A face in a group can be deferred or loaded.
pub const GroupFace = union(enum) {
deferred: DeferredFace, // Not loaded
loaded: Face, // Loaded
pub fn deinit(self: *GroupFace) void {
switch (self.*) {
.deferred => |*v| v.deinit(),
.loaded => |*v| v.deinit(),
}
}
};
pub fn init(
alloc: Allocator,
lib: Library,
@ -86,22 +99,55 @@ pub fn deinit(self: *Group) void {
///
/// The group takes ownership of the face. The face will be deallocated when
/// the group is deallocated.
pub fn addFace(self: *Group, alloc: Allocator, style: Style, face: DeferredFace) !void {
pub fn addFace(self: *Group, style: Style, face: GroupFace) !void {
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;
try list.append(alloc, face);
try list.append(self.alloc, face);
}
/// Get the face for the given style. This will always return the first
/// face (if it exists). The returned pointer is only valid as long as
/// the faces do not change.
pub fn getFace(self: *Group, style: Style) ?*DeferredFace {
const list = self.faces.getPtr(style);
if (list.items.len == 0) return null;
return &list.items[0];
/// Returns true if we have a face for the given style, though the face may
/// not be loaded yet.
pub fn hasFaceForStyle(self: Group, style: Style) bool {
const list = self.faces.get(style);
return list.items.len > 0;
}
/// 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;
// The font must be loaded.
break :regular try self.faceFromIndex(.{
.style = .regular,
.idx = 0,
});
};
// Try to italicize it.
const face = try regular.italicize();
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.
@ -113,10 +159,10 @@ pub fn setSize(self: *Group, size: font.face.DesiredSize) !void {
// Resize all our faces that are loaded
var it = self.faces.iterator();
while (it.next()) |entry| {
for (entry.value.items) |*deferred| {
if (!deferred.loaded()) continue;
try deferred.face.?.setSize(size);
}
for (entry.value.items) |*elem| switch (elem.*) {
.deferred => continue,
.loaded => |*f| try f.setSize(size),
};
}
// Set our size for future loads
@ -213,11 +259,12 @@ pub fn indexForCodepoint(
defer disco_it.deinit();
if (disco_it.next() catch break :discover) |face| {
var buf: [256]u8 = undefined;
log.info("found codepoint 0x{x} in fallback face={s}", .{
cp,
face.name() catch "<error>",
face.name(&buf) catch "<error>",
});
self.addFace(self.alloc, style, face) catch break :discover;
self.addFace(style, .{ .deferred = face }) catch break :discover;
if (self.indexForCodepointExact(cp, style, p)) |value| return value;
}
}
@ -231,8 +278,16 @@ pub fn indexForCodepoint(
}
fn indexForCodepointExact(self: Group, cp: u32, style: Style, p: ?Presentation) ?FontIndex {
for (self.faces.get(style).items, 0..) |deferred, i| {
if (deferred.hasCodepoint(cp, p)) {
for (self.faces.get(style).items, 0..) |elem, i| {
const has_cp = switch (elem) {
.deferred => |v| v.hasCodepoint(cp, p),
.loaded => |face| loaded: {
if (p) |desired| if (face.presentation != desired) break :loaded false;
break :loaded face.glyphIndex(cp) != null;
},
};
if (has_cp) {
return FontIndex{
.style = style,
.idx = @intCast(i),
@ -246,7 +301,7 @@ fn indexForCodepointExact(self: Group, cp: u32, style: Style, p: ?Presentation)
/// 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 {
pub fn presentationFromIndex(self: *Group, index: FontIndex) !font.Presentation {
if (index.special()) |sp| switch (sp) {
.sprite => return .text,
};
@ -256,12 +311,22 @@ pub fn presentationFromIndex(self: Group, index: FontIndex) !font.Presentation {
}
/// Return the Face represented by a given FontIndex. Note that special
/// fonts (i.e. box glyphs) do not have a face.
pub fn faceFromIndex(self: Group, index: FontIndex) !*Face {
/// 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 deferred = &self.faces.get(index.style).items[@intCast(index.idx)];
try deferred.load(self.lib, self.size);
return &deferred.face.?;
const list = self.faces.getPtr(index.style);
const item = &list.items[@intCast(index.idx)];
return switch (item.*) {
.deferred => |*d| deferred: {
const face = try d.load(self.lib, self.size);
d.deinit();
item.* = .{ .loaded = face };
break :deferred &item.loaded;
},
.loaded => |*f| f,
};
}
/// Render a glyph by glyph index into the given font atlas and return
@ -276,7 +341,7 @@ pub fn faceFromIndex(self: Group, index: FontIndex) !*Face {
/// doing text shaping, the text shaping library (i.e. HarfBuzz) will automatically
/// determine glyph indexes for a text run.
pub fn renderGlyph(
self: Group,
self: *Group,
alloc: Allocator,
atlas: *font.Atlas,
index: FontIndex,
@ -292,9 +357,8 @@ pub fn renderGlyph(
),
};
const face = &self.faces.get(index.style).items[@intCast(index.idx)];
try face.load(self.lib, self.size);
const glyph = try face.face.?.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;
}
@ -349,7 +413,7 @@ pub const Wasm = struct {
}
export fn group_add_face(self: *Group, style: u16, face: *font.DeferredFace) void {
return self.addFace(alloc, @enumFromInt(style), face.*) catch |err| {
return self.addFace(@enumFromInt(style), face.*) catch |err| {
log.warn("error adding face to group err={}", .{err});
return;
};
@ -422,13 +486,13 @@ test {
var group = try init(alloc, lib, .{ .points = 12 });
defer group.deinit();
try group.addFace(alloc, .regular, DeferredFace.initLoaded(try Face.init(lib, testFont, .{ .points = 12 })));
try group.addFace(.regular, .{ .loaded = try Face.init(lib, testFont, .{ .points = 12 }) });
if (font.options.backend != .coretext) {
// Coretext doesn't support Noto's format
try group.addFace(alloc, .regular, DeferredFace.initLoaded(try Face.init(lib, testEmoji, .{ .points = 12 })));
try group.addFace(.regular, .{ .loaded = try Face.init(lib, testEmoji, .{ .points = 12 }) });
}
try group.addFace(alloc, .regular, DeferredFace.initLoaded(try Face.init(lib, testEmojiText, .{ .points = 12 })));
try group.addFace(.regular, .{ .loaded = try Face.init(lib, testEmojiText, .{ .points = 12 }) });
// Should find all visible ASCII
var i: u32 = 32;
@ -521,7 +585,7 @@ test "resize" {
var group = try init(alloc, lib, .{ .points = 12, .xdpi = 96, .ydpi = 96 });
defer group.deinit();
try group.addFace(alloc, .regular, DeferredFace.initLoaded(try Face.init(lib, testFont, .{ .points = 12, .xdpi = 96, .ydpi = 96 })));
try group.addFace(.regular, .{ .loaded = try Face.init(lib, testFont, .{ .points = 12, .xdpi = 96, .ydpi = 96 }) });
// Load a letter
{
@ -574,7 +638,7 @@ test "discover monospace with fontconfig and freetype" {
defer lib.deinit();
var group = try init(alloc, lib, .{ .points = 12 });
defer group.deinit();
try group.addFace(alloc, .regular, (try it.next()).?);
try group.addFace(.regular, .{ .deferred = (try it.next()).? });
// Should find all visible ASCII
var atlas_greyscale = try font.Atlas.init(alloc, 512, .greyscale);
@ -612,7 +676,7 @@ test "faceFromIndex returns pointer" {
var group = try init(alloc, lib, .{ .points = 12, .xdpi = 96, .ydpi = 96 });
defer group.deinit();
try group.addFace(alloc, .regular, DeferredFace.initLoaded(try Face.init(lib, testFont, .{ .points = 12, .xdpi = 96, .ydpi = 96 })));
try group.addFace(.regular, .{ .loaded = try Face.init(lib, testFont, .{ .points = 12, .xdpi = 96, .ydpi = 96 }) });
{
const idx = group.indexForCodepoint('A', .regular, null).?;

View File

@ -183,11 +183,10 @@ test {
// Setup group
try cache.group.addFace(
alloc,
.regular,
DeferredFace.initLoaded(try Face.init(lib, testFont, .{ .points = 12 })),
.{ .loaded = try Face.init(lib, testFont, .{ .points = 12 }) },
);
const group = cache.group;
var group = cache.group;
// Visible ASCII. Do it twice to verify cache.
var i: u32 = 32;
@ -340,9 +339,8 @@ test "resize" {
// Setup group
try cache.group.addFace(
alloc,
.regular,
DeferredFace.initLoaded(try Face.init(lib, testFont, .{ .points = 12, .xdpi = 96, .ydpi = 96 })),
.{ .loaded = try Face.init(lib, testFont, .{ .points = 12, .xdpi = 96, .ydpi = 96 }) },
);
// Load a letter

View File

@ -217,7 +217,6 @@ pub const Fontconfig = struct {
defer self.i += 1;
return DeferredFace{
.face = null,
.fc = .{
.pattern = font_pattern,
.charset = (try font_pattern.get(.charset, 0)).char_set,
@ -301,7 +300,6 @@ pub const CoreText = struct {
defer self.i += 1;
return DeferredFace{
.face = null,
.ct = .{ .font = font },
};
}
@ -311,14 +309,9 @@ pub const CoreText = struct {
test "fontconfig" {
if (options.backend != .fontconfig_freetype) return error.SkipZigTest;
const testing = std.testing;
var fc = Fontconfig.init();
var it = try fc.discover(.{ .family = "monospace", .size = 12 });
defer it.deinit();
while (try it.next()) |face| {
try testing.expect(!face.loaded());
}
}
test "fontconfig codepoint" {
@ -333,7 +326,6 @@ test "fontconfig codepoint" {
// The first result should have the codepoint. Later ones may not
// because fontconfig returns all fonts sorted.
const face = (try it.next()).?;
try testing.expect(!face.loaded());
try testing.expect(face.hasCodepoint('A', null));
// Should have other codepoints too
@ -351,9 +343,8 @@ test "coretext" {
var it = try ct.discover(.{ .family = "Monaco", .size = 12 });
defer it.deinit();
var count: usize = 0;
while (try it.next()) |face| {
while (try it.next()) |_| {
count += 1;
try testing.expect(!face.loaded());
}
try testing.expect(count > 0);
}
@ -372,7 +363,6 @@ test "coretext codepoint" {
// The first result should have the codepoint. Later ones may not
// because fontconfig returns all fonts sorted.
const face = (try it.next()).?;
try testing.expect(!face.loaded());
try testing.expect(face.hasCodepoint('A', null));
// Should have other codepoints too

View File

@ -4,6 +4,7 @@ const Allocator = std.mem.Allocator;
const macos = @import("macos");
const harfbuzz = @import("harfbuzz");
const font = @import("../main.zig");
const quirks = @import("../../quirks.zig");
const log = std.log.scoped(.font_face);
@ -20,6 +21,9 @@ pub const Face = struct {
/// Metrics for this font face. These are useful for renderers.
metrics: font.face.Metrics,
/// Set quirks.disableDefaultFontFeatures
quirks_disable_default_font_features: bool = false,
/// The matrix applied to a regular font to auto-italicize it.
pub const italic_skew = macos.graphics.AffineTransform{
.a = 1,
@ -65,12 +69,14 @@ pub const Face = struct {
const traits = ct_font.getSymbolicTraits();
return Face{
var result: Face = .{
.font = ct_font,
.hb_font = hb_font,
.presentation = if (traits.color_glyphs) .emoji else .text,
.metrics = try calcMetrics(ct_font),
};
result.quirks_disable_default_font_features = quirks.disableDefaultFontFeatures(&result);
return result;
}
pub fn deinit(self: *Face) void {

View File

@ -18,6 +18,7 @@ const Library = font.Library;
const Presentation = font.Presentation;
const convert = @import("freetype_convert.zig");
const fastmem = @import("../../fastmem.zig");
const quirks = @import("../../quirks.zig");
const log = std.log.scoped(.font_face);
@ -35,6 +36,9 @@ pub const Face = struct {
/// Metrics for this font face. These are useful for renderers.
metrics: font.face.Metrics,
/// Set quirks.disableDefaultFontFeatures
quirks_disable_default_font_features: bool = false,
/// Initialize a new font face with the given source in-memory.
pub fn initFile(lib: Library, path: [:0]const u8, index: i32, size: font.face.DesiredSize) !Face {
const face = try lib.lib.initFace(path, index);
@ -56,12 +60,14 @@ pub const Face = struct {
const hb_font = try harfbuzz.freetype.createFont(face.handle);
errdefer hb_font.destroy();
return Face{
var result: Face = .{
.face = face,
.hb_font = hb_font,
.presentation = if (face.hasColor()) .emoji else .text,
.metrics = calcMetrics(face),
};
result.quirks_disable_default_font_features = quirks.disableDefaultFontFeatures(&result);
return result;
}
pub fn deinit(self: *Face) void {

View File

@ -12,7 +12,6 @@ const Library = font.Library;
const Style = font.Style;
const Presentation = font.Presentation;
const terminal = @import("../../terminal/main.zig");
const quirks = @import("../../quirks.zig");
const log = std.log.scoped(.font_shaper);
@ -108,7 +107,7 @@ pub const Shaper = struct {
// fonts, the codepoint == glyph_index so we don't need to run any shaping.
if (run.font_index.special() == null) {
const face = try run.group.group.faceFromIndex(run.font_index);
const i = if (!quirks.disableDefaultFontFeatures(face)) 0 else i: {
const i = if (!face.quirks_disable_default_font_features) 0 else i: {
// If we are disabling default font features we just offset
// our features by the hardcoded items because always
// add those at the beginning.
@ -795,12 +794,13 @@ fn testShaper(alloc: Allocator) !TestShaper {
errdefer cache_ptr.*.deinit(alloc);
// Setup group
try cache_ptr.group.addFace(alloc, .regular, DeferredFace.initLoaded(try Face.init(lib, testFont, .{ .points = 12 })));
try cache_ptr.group.addFace(.regular, .{ .loaded = try Face.init(lib, testFont, .{ .points = 12 }) });
if (font.options.backend != .coretext) {
// Coretext doesn't support Noto's format
try cache_ptr.group.addFace(alloc, .regular, DeferredFace.initLoaded(try Face.init(lib, testEmoji, .{ .points = 12 })));
try cache_ptr.group.addFace(.regular, .{ .loaded = try Face.init(lib, testEmoji, .{ .points = 12 }) });
}
try cache_ptr.group.addFace(alloc, .regular, DeferredFace.initLoaded(try Face.init(lib, testEmojiText, .{ .points = 12 })));
try cache_ptr.group.addFace(.regular, .{ .loaded = try Face.init(lib, testEmojiText, .{ .points = 12 }) });
var cell_buf = try alloc.alloc(font.shape.Cell, 80);
errdefer alloc.free(cell_buf);