diff options
Diffstat (limited to 'src/gui')
| -rw-r--r-- | src/gui/font.c | 59 | ||||
| -rw-r--r-- | src/gui/font.h | 16 | ||||
| -rw-r--r-- | src/gui/gui.zig | 93 |
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); |
