summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/gui/font.c59
-rw-r--r--src/gui/font.h16
-rw-r--r--src/gui/gui.zig93
3 files changed, 155 insertions, 13 deletions
diff --git a/src/gui/font.c b/src/gui/font.c
index 5cb98e1a..aebe26e6 100644
--- a/src/gui/font.c
+++ b/src/gui/font.c
@@ -6,14 +6,17 @@
#include "font.h"
#include <freetype-zig.h>
#include <freetype/ftbitmap.h>
+#include <freetype/ftmodapi.h>
#include <hb-ft.h>
#include <hb-ot.h>
#include <hb.h>
-#include <stdlib.h>
#include <string.h>
struct UIFont {
+ // FreeType allocates through ui_malloc and friends: the library holds a
+ // pointer to this for its lifetime, which the UIFont outlives.
+ struct FT_MemoryRec_ memory;
FT_Library library;
FT_Face face;
FT_F26Dot6 size;
@@ -60,20 +63,42 @@ static FT_Int32 load_flags(void) {
return FT_LOAD_DEFAULT | FT_LOAD_TARGET_LIGHT;
}
+static void *ft_alloc(FT_Memory memory, long size) {
+ (void)memory;
+ return ui_malloc((size_t)size);
+}
+
+static void ft_free(FT_Memory memory, void *block) {
+ (void)memory;
+ ui_free(block);
+}
+
+static void *ft_realloc(FT_Memory memory, long cur_size, long new_size,
+ void *block) {
+ (void)memory;
+ (void)cur_size;
+ return ui_realloc(block, (size_t)new_size);
+}
+
UIFont *ui_font_new(const uint8_t *data, int32_t len) {
if (!data || len <= 0)
return NULL;
- UIFont *f = (UIFont *)calloc(1, sizeof(UIFont));
+ UIFont *f = (UIFont *)ui_calloc(1, sizeof(UIFont));
if (!f)
return NULL;
- if (FT_Init_FreeType(&f->library) != 0) {
- free(f);
+ // FT_Init_FreeType is exactly FT_New_Library over FT_New_Memory's malloc
+ // manager, then the default modules and properties: the same, over ours.
+ f->memory = (struct FT_MemoryRec_){NULL, ft_alloc, ft_free, ft_realloc};
+ if (FT_New_Library(&f->memory, &f->library) != 0) {
+ ui_free(f);
return NULL;
}
+ FT_Add_Default_Modules(f->library);
+ FT_Set_Default_Properties(f->library);
if (FT_New_Memory_Face(f->library, data, (FT_Long)len, 0, &f->face) != 0) {
- FT_Done_FreeType(f->library);
- free(f);
+ FT_Done_Library(f->library);
+ ui_free(f);
return NULL;
}
// Some symbol fonts do not expose a Unicode charmap. FreeType already
@@ -136,10 +161,20 @@ void ui_font_free(UIFont *f) {
if (f->face)
FT_Done_Face(f->face);
if (f->library)
- FT_Done_FreeType(f->library);
- free(f->own);
- free(f->state);
- free(f);
+ FT_Done_Library(f->library);
+ ui_free(f->own);
+ ui_free(f->state);
+ ui_free(f);
+}
+
+void ui_font_prime(UIFont *f) {
+ if (!f)
+ return;
+ hb_font_destroy(hb_ft_font_create_referenced(f->face));
+ hb_buffer_t *buffer = hb_buffer_create();
+ hb_buffer_add_utf8(buffer, "a", 1, 0, 1);
+ hb_buffer_guess_segment_properties(buffer);
+ hb_buffer_destroy(buffer);
}
void ui_font_cell_metrics(UIFont *f, float px, int32_t *cell_w, int32_t *cell_h,
@@ -325,11 +360,11 @@ void ui_font_shape(UIFont *f, float px, int32_t cell_w, const uint32_t *cps,
if (!substitutable || cell_w <= 0 || !set_size(f, px))
return;
if (n > f->scratch) {
- uint32_t *own = (uint32_t *)realloc(f->own, (size_t)n * sizeof *own);
+ uint32_t *own = (uint32_t *)ui_realloc(f->own, (size_t)n * sizeof *own);
if (!own)
return;
f->own = own;
- uint8_t *state = (uint8_t *)realloc(f->state, (size_t)n);
+ uint8_t *state = (uint8_t *)ui_realloc(f->state, (size_t)n);
if (!state)
return;
f->state = state;
diff --git a/src/gui/font.h b/src/gui/font.h
index b38f822a..db964fb0 100644
--- a/src/gui/font.h
+++ b/src/gui/font.h
@@ -4,6 +4,7 @@
// for the face lifetime.
#ifndef UI_FONT_H
#define UI_FONT_H
+#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
@@ -12,6 +13,15 @@ extern "C" {
typedef struct UIFont UIFont;
+// Every allocation FreeType, HarfBuzz and this shim make: C's malloc, calloc,
+// realloc and free, implemented by gui.zig over a Zig allocator. HarfBuzz is
+// compiled to call them (hb_malloc_impl and friends); FreeType gets them as
+// each library's memory manager.
+void *ui_malloc(size_t size);
+void *ui_calloc(size_t count, size_t size);
+void *ui_realloc(void *block, size_t size);
+void ui_free(void *block);
+
// Parse the first face in an in-memory font. `data` must stay alive for the
// lifetime of the font. Returns NULL on failure.
UIFont *ui_font_new(const uint8_t *data, int32_t len);
@@ -76,6 +86,12 @@ int ui_font_raster_centered(UIFont *f, float px, uint32_t glyph, int bold,
void ui_font_free(UIFont *f);
+// HarfBuzz makes a few process-wide objects on first use and keeps them for
+// the life of the process (the default Unicode functions, the locale's
+// language, hb-ft's font functions). Making them here, with `f`'s face, lets
+// the caller hand them an allocator that does not count them as leaks.
+void ui_font_prime(UIFont *f);
+
#ifdef __cplusplus
}
#endif
diff --git a/src/gui/gui.zig b/src/gui/gui.zig
index 5bcb2867..a4cc7672 100644
--- a/src/gui/gui.zig
+++ b/src/gui/gui.zig
@@ -133,6 +133,85 @@ fn taglineRaster(font: *c.UIFont, body_px: f32, body_cell_w: u32, body_cell_h: u
.height = @intCast(band_h),
};
}
+/// What FreeType and HarfBuzz allocate from, through ui_malloc and friends
+/// below. runNative points it at the gui's gpa once HarfBuzz's process-wide
+/// objects exist (ui_font_prime), so a Debug build's leak report is about
+/// fonts; a test that counts what fonts leak points it at
+/// std.testing.allocator the same way. Only the render thread calls into
+/// fonts, and every allocator it is pointed at is thread-safe anyway.
+var font_allocator: std.mem.Allocator = std.heap.smp_allocator;
+
+/// C's free and realloc pass no size, so a block starts with its size and the
+/// allocator that made it: a block made before font_allocator was repointed
+/// goes back where it came from. 32 bytes keep the caller's pointer 16-byte
+/// (max_align_t) aligned.
+const FontBlock = struct { size: usize, allocator: std.mem.Allocator };
+const font_block_header = 32;
+comptime {
+ std.debug.assert(@sizeOf(FontBlock) <= font_block_header);
+}
+
+export fn ui_malloc(size: usize) ?*anyopaque {
+ const total = std.math.add(usize, font_block_header, size) catch return null;
+ const bytes = font_allocator.alignedAlloc(u8, .@"16", total) catch return null;
+ @as(*FontBlock, @ptrCast(bytes.ptr)).* = .{ .size = size, .allocator = font_allocator };
+ return bytes.ptr + font_block_header;
+}
+
+export fn ui_calloc(count: usize, size: usize) ?*anyopaque {
+ const total = std.math.mul(usize, count, size) catch return null;
+ const block: [*]u8 = @ptrCast(ui_malloc(total) orelse return null);
+ @memset(block[0..total], 0);
+ return block;
+}
+
+export fn ui_realloc(block: ?*anyopaque, size: usize) ?*anyopaque {
+ const old = block orelse return ui_malloc(size);
+ const base: [*]align(16) u8 = @alignCast(@as([*]u8, @ptrCast(old)) - font_block_header);
+ const header: *FontBlock = @ptrCast(base);
+ const total = std.math.add(usize, font_block_header, size) catch return null;
+ const bytes = header.allocator.realloc(base[0 .. font_block_header + header.size], total) catch return null;
+ @as(*FontBlock, @ptrCast(bytes.ptr)).size = size;
+ return bytes.ptr + font_block_header;
+}
+
+export fn ui_free(block: ?*anyopaque) void {
+ const old = block orelse return;
+ const base: [*]align(16) u8 = @alignCast(@as([*]u8, @ptrCast(old)) - font_block_header);
+ const header: *FontBlock = @ptrCast(base);
+ header.allocator.free(base[0 .. font_block_header + header.size]);
+}
+
+test "FreeType and HarfBuzz allocate from the gui allocator and give it all back" {
+ // HarfBuzz's process-wide objects come from the default heap, as in runNative.
+ const primer = c.ui_font_new(font_ttf.ptr, @intCast(font_ttf.len)).?;
+ c.ui_font_prime(primer);
+ c.ui_font_free(primer);
+ font_allocator = std.testing.allocator;
+ defer font_allocator = std.heap.smp_allocator;
+
+ const font = c.ui_font_new(font_ttf.ptr, @intCast(font_ttf.len)).?;
+ defer c.ui_font_free(font);
+ var mask: [64 * 64]u8 = undefined;
+ try std.testing.expectEqual(@as(c_int, 1), c.ui_font_raster(font, 27, c.ui_font_glyph(font, 'g'), 1, &mask, 64, 32, 48, 0, 32, 30));
+ try std.testing.expectEqual(@as(c_int, 0), c.ui_font_substitutes(font, c.ui_font_glyph(font, '-')));
+
+ // A font with ligatures runs the whole HarfBuzz pass.
+ const bytes = filesystem.readFile(std.testing.allocator, "assets/MapleMono-NF-Regular.ttf") catch return;
+ defer std.testing.allocator.free(bytes);
+ const maple = c.ui_font_new(bytes.ptr, @intCast(bytes.len)) orelse return error.FontInit;
+ defer c.ui_font_free(maple);
+ var cell_w: c_int = 0;
+ var cell_h: c_int = 0;
+ var ascent: c_int = 0;
+ c.ui_font_cell_metrics(maple, 27, &cell_w, &cell_h, &ascent);
+ const text = [_]u32{ 'a', '-', '>', 'b', '=', '=' };
+ var shaped: [text.len]c.UIShapedCell = undefined;
+ c.ui_font_shape(maple, 27, cell_w, &text, text.len, &shaped);
+ try std.testing.expectEqual(@as(u8, 2), shaped[1].span);
+ try std.testing.expectEqual(@as(u8, 2), shaped[4].span);
+}
+
const max_fallback_fonts = 1 + fonts.fallback_names.len;
const LoadedFallback = struct {
face: *c.UIFont,
@@ -2181,10 +2260,16 @@ fn runNative(init: std.process.Init, opts_in: pardes.Options, attach: ?[]const u
else
c.SDL_GetGPUSwapchainTextureFormat(device, window);
+ font_allocator = gpa;
const font = c.ui_font_new(font_ttf.ptr, @intCast(font_ttf.len)) orelse {
log.err("ui_font_new failed", .{});
return error.FontInit;
};
+ // HarfBuzz's process-wide objects are made once and never freed: made
+ // from the default heap, they stay out of a Debug build's leak report.
+ font_allocator = std.heap.smp_allocator;
+ c.ui_font_prime(font);
+ font_allocator = gpa;
const px: f32 = 27.0;
var cw: c_int = 10;
var chh: c_int = 20;
@@ -6106,8 +6191,12 @@ fn cellSlot(g: *Gui, line: []const pardes.Cell, at: usize, cursor: ?usize, role:
return words.slots.items[text_start + at - start];
}
-/// A Gui that can shape and rasterize `font` at `px`, and nothing else.
+/// A Gui that can shape and rasterize `font` at `px`, and nothing else. From
+/// here on the fonts allocate from std.testing.allocator (the caller puts
+/// font_allocator back), so the test counts what they leak.
fn textTestGui(font: *c.UIFont, px: f32) !Gui {
+ c.ui_font_prime(font);
+ font_allocator = std.testing.allocator;
var g: Gui = undefined;
g.font = font;
g.fallback_count = 0;
@@ -6141,6 +6230,7 @@ test "a ligature draws across its cells, one slice each, and comes apart under t
const font = c.ui_font_new(bytes.ptr, @intCast(bytes.len)) orelse return error.FontInit;
defer c.ui_font_free(font);
var g = try textTestGui(font, 27);
+ defer font_allocator = std.heap.smp_allocator;
defer std.testing.allocator.free(g.atlas_stage);
defer g.glyphs.deinit();
defer g.words.deinit(std.testing.allocator);
@@ -6199,6 +6289,7 @@ test "a font without ligatures draws every cell as its own glyph, exactly as bef
const font = c.ui_font_new(font_ttf.ptr, @intCast(font_ttf.len)).?;
defer c.ui_font_free(font);
var g = try textTestGui(font, 18);
+ defer font_allocator = std.heap.smp_allocator;
defer std.testing.allocator.free(g.atlas_stage);
defer g.glyphs.deinit();
defer g.words.deinit(std.testing.allocator);