mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-08-02 14:57:31 +03:00
freetype: resize glyphs that are too tall prior to storing in texture
Most emoji fonts are massive glyphs (128x128, 256x256, etc.). This means the texture we need to store emoji is also massive. For a 128x128 emoji font (both Apple and Noto), we can only store 12 emoji before resizing prior to this commit. This commit now threads through a max height through to the font face and resizes the bitmap in memory before putting it in the atlas. This results in significant savings. The max height is the cell height. We allow the glyphs to be as wide as necessary due to double (and more) wide glyphs. For the unicode emoji test file, the atlas size before and after: Before: 262 MB After: 16 MB
This commit is contained in:
@ -11,6 +11,7 @@ const libxml2 = @import("vendor/zig-libxml2/libxml2.zig");
|
|||||||
const libuv = @import("pkg/libuv/build.zig");
|
const libuv = @import("pkg/libuv/build.zig");
|
||||||
const libpng = @import("pkg/libpng/build.zig");
|
const libpng = @import("pkg/libpng/build.zig");
|
||||||
const macos = @import("pkg/macos/build.zig");
|
const macos = @import("pkg/macos/build.zig");
|
||||||
|
const stb_image_resize = @import("pkg/stb_image_resize/build.zig");
|
||||||
const utf8proc = @import("pkg/utf8proc/build.zig");
|
const utf8proc = @import("pkg/utf8proc/build.zig");
|
||||||
const zlib = @import("pkg/zlib/build.zig");
|
const zlib = @import("pkg/zlib/build.zig");
|
||||||
const tracylib = @import("pkg/tracy/build.zig");
|
const tracylib = @import("pkg/tracy/build.zig");
|
||||||
@ -193,6 +194,7 @@ fn addDeps(
|
|||||||
step.addPackage(imgui.pkg);
|
step.addPackage(imgui.pkg);
|
||||||
step.addPackage(glfw.pkg);
|
step.addPackage(glfw.pkg);
|
||||||
step.addPackage(libuv.pkg);
|
step.addPackage(libuv.pkg);
|
||||||
|
step.addPackage(stb_image_resize.pkg);
|
||||||
step.addPackage(utf8proc.pkg);
|
step.addPackage(utf8proc.pkg);
|
||||||
|
|
||||||
// Mac Stuff
|
// Mac Stuff
|
||||||
@ -212,6 +214,9 @@ fn addDeps(
|
|||||||
system_sdk.include(b, tracy_step, .{});
|
system_sdk.include(b, tracy_step, .{});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// stb_image_resize
|
||||||
|
_ = try stb_image_resize.link(b, step, .{});
|
||||||
|
|
||||||
// utf8proc
|
// utf8proc
|
||||||
_ = try utf8proc.link(b, step);
|
_ = try utf8proc.link(b, step);
|
||||||
|
|
||||||
|
59
pkg/stb_image_resize/build.zig
Normal file
59
pkg/stb_image_resize/build.zig
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
/// Directories with our includes.
|
||||||
|
const root = thisDir();
|
||||||
|
pub const include_paths = [_][]const u8{
|
||||||
|
root,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const pkg = std.build.Pkg{
|
||||||
|
.name = "stb_image_resize",
|
||||||
|
.source = .{ .path = thisDir() ++ "/main.zig" },
|
||||||
|
};
|
||||||
|
|
||||||
|
fn thisDir() []const u8 {
|
||||||
|
return std.fs.path.dirname(@src().file) orelse ".";
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const Options = struct {};
|
||||||
|
|
||||||
|
pub fn link(
|
||||||
|
b: *std.build.Builder,
|
||||||
|
step: *std.build.LibExeObjStep,
|
||||||
|
opt: Options,
|
||||||
|
) !*std.build.LibExeObjStep {
|
||||||
|
const lib = try buildStbImageResize(b, step, opt);
|
||||||
|
step.linkLibrary(lib);
|
||||||
|
inline for (include_paths) |path| step.addIncludePath(path);
|
||||||
|
return lib;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn buildStbImageResize(
|
||||||
|
b: *std.build.Builder,
|
||||||
|
step: *std.build.LibExeObjStep,
|
||||||
|
opt: Options,
|
||||||
|
) !*std.build.LibExeObjStep {
|
||||||
|
_ = opt;
|
||||||
|
|
||||||
|
const lib = b.addStaticLibrary("stb_image_resize", null);
|
||||||
|
lib.setTarget(step.target);
|
||||||
|
lib.setBuildMode(step.build_mode);
|
||||||
|
|
||||||
|
// Include
|
||||||
|
inline for (include_paths) |path| lib.addIncludePath(path);
|
||||||
|
|
||||||
|
// Link
|
||||||
|
lib.linkLibC();
|
||||||
|
|
||||||
|
// Compile
|
||||||
|
var flags = std.ArrayList([]const u8).init(b.allocator);
|
||||||
|
defer flags.deinit();
|
||||||
|
try flags.appendSlice(&.{
|
||||||
|
//"-fno-sanitize=undefined",
|
||||||
|
});
|
||||||
|
|
||||||
|
// C files
|
||||||
|
lib.addCSourceFile(root ++ "/stb_image_resize.c", flags.items);
|
||||||
|
|
||||||
|
return lib;
|
||||||
|
}
|
7
pkg/stb_image_resize/main.zig
Normal file
7
pkg/stb_image_resize/main.zig
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
pub usingnamespace @cImport({
|
||||||
|
@cInclude("stb_image_resize.h");
|
||||||
|
});
|
||||||
|
|
||||||
|
test {
|
||||||
|
// Needed to not crash on test
|
||||||
|
}
|
2
pkg/stb_image_resize/stb_image_resize.c
Normal file
2
pkg/stb_image_resize/stb_image_resize.c
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
#define STB_IMAGE_RESIZE_IMPLEMENTATION
|
||||||
|
#include <stb_image_resize.h>
|
2634
pkg/stb_image_resize/stb_image_resize.h
Normal file
2634
pkg/stb_image_resize/stb_image_resize.h
Normal file
File diff suppressed because it is too large
Load Diff
@ -583,6 +583,7 @@ pub fn updateCell(
|
|||||||
self.alloc,
|
self.alloc,
|
||||||
shaper_run.font_index,
|
shaper_run.font_index,
|
||||||
shaper_cell.glyph_index,
|
shaper_cell.glyph_index,
|
||||||
|
@floatToInt(u16, @ceil(self.cell_size.height)),
|
||||||
);
|
);
|
||||||
|
|
||||||
// If we're rendering a color font, we use the color atlas
|
// If we're rendering a color font, we use the color atlas
|
||||||
|
@ -160,10 +160,11 @@ pub fn renderGlyph(
|
|||||||
atlas: *Atlas,
|
atlas: *Atlas,
|
||||||
index: FontIndex,
|
index: FontIndex,
|
||||||
glyph_index: u32,
|
glyph_index: u32,
|
||||||
|
max_height: ?u16,
|
||||||
) !Glyph {
|
) !Glyph {
|
||||||
const face = &self.faces.get(index.style).items[@intCast(usize, index.idx)];
|
const face = &self.faces.get(index.style).items[@intCast(usize, index.idx)];
|
||||||
try face.load(self.lib, self.size);
|
try face.load(self.lib, self.size);
|
||||||
return try face.face.?.renderGlyph(alloc, atlas, glyph_index);
|
return try face.face.?.renderGlyph(alloc, atlas, glyph_index, max_height);
|
||||||
}
|
}
|
||||||
|
|
||||||
test {
|
test {
|
||||||
@ -201,6 +202,7 @@ test {
|
|||||||
&atlas_greyscale,
|
&atlas_greyscale,
|
||||||
idx,
|
idx,
|
||||||
glyph_index,
|
glyph_index,
|
||||||
|
null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -260,6 +262,7 @@ test {
|
|||||||
&atlas_greyscale,
|
&atlas_greyscale,
|
||||||
idx,
|
idx,
|
||||||
glyph_index,
|
glyph_index,
|
||||||
|
null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -110,6 +110,7 @@ pub fn renderGlyph(
|
|||||||
alloc: Allocator,
|
alloc: Allocator,
|
||||||
index: Group.FontIndex,
|
index: Group.FontIndex,
|
||||||
glyph_index: u32,
|
glyph_index: u32,
|
||||||
|
max_height: ?u16,
|
||||||
) !Glyph {
|
) !Glyph {
|
||||||
const key: GlyphKey = .{ .index = index, .glyph = glyph_index };
|
const key: GlyphKey = .{ .index = index, .glyph = glyph_index };
|
||||||
const gop = try self.glyphs.getOrPut(alloc, key);
|
const gop = try self.glyphs.getOrPut(alloc, key);
|
||||||
@ -125,6 +126,7 @@ pub fn renderGlyph(
|
|||||||
atlas,
|
atlas,
|
||||||
index,
|
index,
|
||||||
glyph_index,
|
glyph_index,
|
||||||
|
max_height,
|
||||||
) catch |err| switch (err) {
|
) catch |err| switch (err) {
|
||||||
// If the atlas is full, we resize it
|
// If the atlas is full, we resize it
|
||||||
error.AtlasFull => blk: {
|
error.AtlasFull => blk: {
|
||||||
@ -134,6 +136,7 @@ pub fn renderGlyph(
|
|||||||
atlas,
|
atlas,
|
||||||
index,
|
index,
|
||||||
glyph_index,
|
glyph_index,
|
||||||
|
max_height,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -186,6 +189,7 @@ test {
|
|||||||
alloc,
|
alloc,
|
||||||
idx,
|
idx,
|
||||||
glyph_index,
|
glyph_index,
|
||||||
|
null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -207,6 +211,7 @@ test {
|
|||||||
alloc,
|
alloc,
|
||||||
idx,
|
idx,
|
||||||
glyph_index,
|
glyph_index,
|
||||||
|
null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -87,7 +87,15 @@ pub const Face = struct {
|
|||||||
|
|
||||||
/// Render a glyph using the glyph index. The rendered glyph is stored in the
|
/// Render a glyph using the glyph index. The rendered glyph is stored in the
|
||||||
/// given texture atlas.
|
/// given texture atlas.
|
||||||
pub fn renderGlyph(self: Face, alloc: Allocator, atlas: *Atlas, glyph_index: u32) !font.Glyph {
|
pub fn renderGlyph(
|
||||||
|
self: Face,
|
||||||
|
alloc: Allocator,
|
||||||
|
atlas: *Atlas,
|
||||||
|
glyph_index: u32,
|
||||||
|
max_height: ?u16,
|
||||||
|
) !font.Glyph {
|
||||||
|
_ = max_height;
|
||||||
|
|
||||||
var glyphs = [_]macos.graphics.Glyph{@intCast(macos.graphics.Glyph, glyph_index)};
|
var glyphs = [_]macos.graphics.Glyph{@intCast(macos.graphics.Glyph, glyph_index)};
|
||||||
|
|
||||||
// Get the bounding rect for this glyph to determine the width/height
|
// Get the bounding rect for this glyph to determine the width/height
|
||||||
@ -321,7 +329,7 @@ test {
|
|||||||
var i: u8 = 32;
|
var i: u8 = 32;
|
||||||
while (i < 127) : (i += 1) {
|
while (i < 127) : (i += 1) {
|
||||||
try testing.expect(face.glyphIndex(i) != null);
|
try testing.expect(face.glyphIndex(i) != null);
|
||||||
_ = try face.renderGlyph(alloc, &atlas, face.glyphIndex(i).?);
|
_ = try face.renderGlyph(alloc, &atlas, face.glyphIndex(i).?, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -365,6 +373,6 @@ test "in-memory" {
|
|||||||
var i: u8 = 32;
|
var i: u8 = 32;
|
||||||
while (i < 127) : (i += 1) {
|
while (i < 127) : (i += 1) {
|
||||||
try testing.expect(face.glyphIndex(i) != null);
|
try testing.expect(face.glyphIndex(i) != null);
|
||||||
_ = try face.renderGlyph(alloc, &atlas, face.glyphIndex(i).?);
|
_ = try face.renderGlyph(alloc, &atlas, face.glyphIndex(i).?, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ const std = @import("std");
|
|||||||
const builtin = @import("builtin");
|
const builtin = @import("builtin");
|
||||||
const freetype = @import("freetype");
|
const freetype = @import("freetype");
|
||||||
const harfbuzz = @import("harfbuzz");
|
const harfbuzz = @import("harfbuzz");
|
||||||
|
const resize = @import("stb_image_resize");
|
||||||
const assert = std.debug.assert;
|
const assert = std.debug.assert;
|
||||||
const testing = std.testing;
|
const testing = std.testing;
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
@ -118,7 +119,13 @@ pub const Face = struct {
|
|||||||
|
|
||||||
/// Render a glyph using the glyph index. The rendered glyph is stored in the
|
/// Render a glyph using the glyph index. The rendered glyph is stored in the
|
||||||
/// given texture atlas.
|
/// given texture atlas.
|
||||||
pub fn renderGlyph(self: Face, alloc: Allocator, atlas: *Atlas, glyph_index: u32) !Glyph {
|
pub fn renderGlyph(
|
||||||
|
self: Face,
|
||||||
|
alloc: Allocator,
|
||||||
|
atlas: *Atlas,
|
||||||
|
glyph_index: u32,
|
||||||
|
max_height: ?u16,
|
||||||
|
) !Glyph {
|
||||||
// If our glyph has color, we want to render the color
|
// If our glyph has color, we want to render the color
|
||||||
try self.face.loadGlyph(glyph_index, .{
|
try self.face.loadGlyph(glyph_index, .{
|
||||||
.render = true,
|
.render = true,
|
||||||
@ -168,11 +175,58 @@ pub const Face = struct {
|
|||||||
break :blk try func(alloc, bitmap_ft);
|
break :blk try func(alloc, bitmap_ft);
|
||||||
} else null;
|
} else null;
|
||||||
defer if (bitmap_converted) |bm| {
|
defer if (bitmap_converted) |bm| {
|
||||||
const len = bm.width * bm.rows * atlas.format.depth();
|
const len = @intCast(usize, bm.pitch) * @intCast(usize, bm.rows);
|
||||||
alloc.free(bm.buffer[0..len]);
|
alloc.free(bm.buffer[0..len]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const bitmap = bitmap_converted orelse bitmap_ft;
|
// Now we need to see if we need to resize this bitmap. This can happen
|
||||||
|
// in scenarios where we have fixed size glyphs. For example, emoji
|
||||||
|
// can be quite large (i.e. 128x128) when we have a cell width of 24!
|
||||||
|
// The issue with large bitmaps is they take a huge amount of space in
|
||||||
|
// the atlas and force resizes quite frequently. We pay some CPU cost
|
||||||
|
// up front to resize the glyph to avoid significant CPU cost to resize
|
||||||
|
// and copy the atlas.
|
||||||
|
const bitmap_resized: ?freetype.c.struct_FT_Bitmap_ = resized: {
|
||||||
|
const max = max_height orelse break :resized null;
|
||||||
|
const bm = bitmap_converted orelse bitmap_ft;
|
||||||
|
if (bm.rows <= max) break :resized null;
|
||||||
|
|
||||||
|
var result = bm;
|
||||||
|
result.rows = max;
|
||||||
|
result.width = (result.rows * bm.width) / bm.rows;
|
||||||
|
result.pitch = @intCast(c_int, result.width) * atlas.format.depth();
|
||||||
|
|
||||||
|
const buf = try alloc.alloc(
|
||||||
|
u8,
|
||||||
|
@intCast(usize, result.pitch) * @intCast(usize, result.rows),
|
||||||
|
);
|
||||||
|
result.buffer = buf.ptr;
|
||||||
|
errdefer alloc.free(buf);
|
||||||
|
|
||||||
|
if (resize.stbir_resize_uint8(
|
||||||
|
bm.buffer,
|
||||||
|
@intCast(c_int, bm.width),
|
||||||
|
@intCast(c_int, bm.rows),
|
||||||
|
bm.pitch,
|
||||||
|
result.buffer,
|
||||||
|
@intCast(c_int, result.width),
|
||||||
|
@intCast(c_int, result.rows),
|
||||||
|
result.pitch,
|
||||||
|
atlas.format.depth(),
|
||||||
|
) == 0) {
|
||||||
|
// This should never fail because this is a fairly straightforward
|
||||||
|
// in-memory operation...
|
||||||
|
return error.GlyphResizeFailed;
|
||||||
|
}
|
||||||
|
|
||||||
|
break :resized result;
|
||||||
|
};
|
||||||
|
defer if (bitmap_resized) |bm| {
|
||||||
|
const len = @intCast(usize, bm.pitch) * @intCast(usize, bm.rows);
|
||||||
|
alloc.free(bm.buffer[0..len]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const bitmap = bitmap_resized orelse (bitmap_converted orelse bitmap_ft);
|
||||||
const tgt_w = bitmap.width;
|
const tgt_w = bitmap.width;
|
||||||
const tgt_h = bitmap.rows;
|
const tgt_h = bitmap.rows;
|
||||||
|
|
||||||
@ -415,7 +469,7 @@ test {
|
|||||||
// Generate all visible ASCII
|
// Generate all visible ASCII
|
||||||
var i: u8 = 32;
|
var i: u8 = 32;
|
||||||
while (i < 127) : (i += 1) {
|
while (i < 127) : (i += 1) {
|
||||||
_ = try ft_font.renderGlyph(alloc, &atlas, ft_font.glyphIndex(i).?);
|
_ = try ft_font.renderGlyph(alloc, &atlas, ft_font.glyphIndex(i).?, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -434,7 +488,13 @@ test "color emoji" {
|
|||||||
|
|
||||||
try testing.expectEqual(Presentation.emoji, ft_font.presentation);
|
try testing.expectEqual(Presentation.emoji, ft_font.presentation);
|
||||||
|
|
||||||
_ = try ft_font.renderGlyph(alloc, &atlas, ft_font.glyphIndex('🥸').?);
|
_ = try ft_font.renderGlyph(alloc, &atlas, ft_font.glyphIndex('🥸').?, null);
|
||||||
|
|
||||||
|
// resize
|
||||||
|
{
|
||||||
|
const glyph = try ft_font.renderGlyph(alloc, &atlas, ft_font.glyphIndex('🥸').?, 24);
|
||||||
|
try testing.expectEqual(@as(u32, 24), glyph.height);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
test "mono to rgba" {
|
test "mono to rgba" {
|
||||||
@ -451,5 +511,5 @@ test "mono to rgba" {
|
|||||||
defer ft_font.deinit();
|
defer ft_font.deinit();
|
||||||
|
|
||||||
// glyph 3 is mono in Noto
|
// glyph 3 is mono in Noto
|
||||||
_ = try ft_font.renderGlyph(alloc, &atlas, 3);
|
_ = try ft_font.renderGlyph(alloc, &atlas, 3, null);
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user