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