summaryrefslogtreecommitdiff
path: root/src/gui/gui.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-24 12:22:51 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:14 -0300
commit39cdba439c1ae47dbc7c3243a9273aa011c6affa (patch)
treec9408bae952e350892f073c8f9bd3661f5e90943 /src/gui/gui.zig
parente5f764bd201df6554272427fc8cedffece18066f (diff)
downloadpardes-39cdba439c1ae47dbc7c3243a9273aa011c6affa.tar.gz
pardes-39cdba439c1ae47dbc7c3243a9273aa011c6affa.zip
Allocate FreeType's and HarfBuzz's memory from the gui's Zig allocator
FreeType and HarfBuzz allocated with libc malloc, out of sight of the Zig allocator the rest of the gui uses, so its Debug leak report and a test's std.testing.allocator never saw them. Now every allocation the font code makes goes through ui_malloc, ui_calloc, ui_realloc and ui_free, exported from gui.zig over `font_allocator`: - HarfBuzz is compiled with hb_malloc_impl and friends pointing at them. - Each UIFont gives its FreeType library its own memory manager (FT_New_Library with an FT_MemoryRec_ over the same functions, then FT_Add_Default_Modules and FT_Set_Default_Properties: exactly what FT_Init_FreeType does over its malloc manager), torn down with FT_Done_Library. - The shim's own UIFont and scratch arrays use them too. C's free and realloc pass no size, so a block starts with a 32-byte header (the caller's pointer stays 16-byte, max_align_t, aligned) holding its size and the allocator that made it, so a block goes back where it came from even after font_allocator is repointed. runNative points font_allocator at the gui's gpa. HarfBuzz makes a few process-wide objects on first use and never frees them (the default Unicode functions, the locale's language, hb-ft's font functions); ui_font_prime makes them from the default heap before that, so a Debug build's leak report stays about fonts. Tests do the same: the shaping tests, and a new test that creates, rasterizes and shapes fonts under std.testing.allocator, report any leak. Only the render thread calls into fonts, and every allocator used is thread-safe. It costs nothing measurable: against the previous change, 10 interleaved rounds with an A/A pair show every scenario as fast or faster with Adwaita Mono and Maple Mono alike, and first paint over 30 launches each matches main. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Diffstat (limited to 'src/gui/gui.zig')
-rw-r--r--src/gui/gui.zig93
1 files changed, 92 insertions, 1 deletions
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);