From e33aeea72372cafcea8ab36984a3e0116c40ab77 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 5 Apr 2022 17:57:09 -0700 Subject: [PATCH] starting FontAtlas --- build.zig | 1 + src/Atlas.zig | 9 ++- src/FontAtlas.zig | 185 ++++++++++++++++++++++++++++++++++++++++++++++ src/main.zig | 1 + 4 files changed, 193 insertions(+), 3 deletions(-) create mode 100644 src/FontAtlas.zig diff --git a/build.zig b/build.zig index 22b7f28f3..e0b514280 100644 --- a/build.zig +++ b/build.zig @@ -47,5 +47,6 @@ pub fn build(b: *std.build.Builder) !void { // Tests const test_step = b.step("test", "Run all tests"); const lib_tests = b.addTest("src/main.zig"); + ftlib.link(lib_tests); test_step.dependOn(&lib_tests.step); } diff --git a/src/Atlas.zig b/src/Atlas.zig index 9f8da6207..018e6b54b 100644 --- a/src/Atlas.zig +++ b/src/Atlas.zig @@ -26,9 +26,10 @@ data: []u8, /// Width and height of the atlas texture. The current implementation is /// always square so this is both the width and the height. -size: u32, +size: u32 = 0, -nodes: std.ArrayListUnmanaged(Node), +/// The nodes (rectangles) of available space. +nodes: std.ArrayListUnmanaged(Node) = .{}, const Node = struct { x: u32, @@ -41,6 +42,8 @@ pub const Error = error{ AtlasFull, }; +/// A region within the texture atlas. These can be acquired using the +/// "reserve" function. A region reservation is required to write data. pub const Region = struct { x: u32, y: u32, @@ -121,7 +124,7 @@ pub fn reserve(self: *Atlas, alloc: Allocator, width: u32, height: u32) !Region if (node.x < (prev.x + prev.width)) { const shrink = prev.x + prev.width - node.x; node.x += shrink; - node.width -= shrink; + node.width -|= shrink; if (node.width <= 0) { _ = self.nodes.orderedRemove(i); i -= 1; diff --git a/src/FontAtlas.zig b/src/FontAtlas.zig new file mode 100644 index 000000000..b3039e300 --- /dev/null +++ b/src/FontAtlas.zig @@ -0,0 +1,185 @@ +//! Implements font loading and rendering into a texture atlas, using +//! Atlas as the backing implementation. The FontAtlas represents a single +//! face with a single size. +const FontAtlas = @This(); + +const std = @import("std"); +const assert = std.debug.assert; +const testing = std.testing; +const Allocator = std.mem.Allocator; +const ftc = @import("freetype/c.zig"); +const Atlas = @import("Atlas.zig"); + +const ftok = ftc.FT_Err_Ok; +const log = std.log.scoped(.font_atlas); + +/// The texture atlas where all the font glyphs are rendered. +/// This is NOT owned by the FontAtlas, deinitialization must +/// be manually done. +atlas: Atlas, + +/// The glyphs that are loaded into the atlas, keyed by codepoint. +glyphs: std.AutoHashMapUnmanaged(u32, Glyph), + +/// The FreeType library +ft_library: ftc.FT_Library, + +/// Our font face. +ft_face: ftc.FT_Face = null, + +/// Information about a single glyph. +pub const Glyph = struct { + /// width of glyph in pixels + width: u32, + + /// height of glyph in pixels + height: u32, + + /// left bearing + offset_x: i32, + + /// top bearing + offset_y: i32, + + /// normalized x, y (s, t) coordinates + s0: f32, + t0: f32, + s1: f32, + t1: f32, + + /// horizontal position to increase drawing position for strings + advance_x: f32, +}; + +pub fn init(atlas: Atlas) !FontAtlas { + var res = FontAtlas{ + .atlas = atlas, + .ft_library = undefined, + .glyphs = .{}, + }; + + if (ftc.FT_Init_FreeType(&res.ft_library) != ftok) + return error.FreeTypeInitFailed; + + return res; +} + +pub fn deinit(self: *FontAtlas, alloc: Allocator) void { + self.glyphs.deinit(alloc); + + if (self.ft_face != null) { + if (ftc.FT_Done_Face(self.ft_face) != ftok) + log.err("failed to clean up font face", .{}); + } + + if (ftc.FT_Done_FreeType(self.ft_library) != ftok) + log.err("failed to clean up FreeType", .{}); + + self.* = undefined; +} + +/// Loads a font to use for the atlas. +/// +/// This can only be called if a font is not already loaded. +pub fn loadFaceFromMemory(self: *FontAtlas, source: [:0]const u8, size: u32) !void { + assert(self.ft_face == null); + + if (ftc.FT_New_Memory_Face( + self.ft_library, + source.ptr, + @intCast(c_long, source.len), + 0, + &self.ft_face, + ) != ftok) return error.FaceLoadFailed; + errdefer { + _ = ftc.FT_Done_Face(self.ft_face); + self.ft_face = null; + } + + if (ftc.FT_Select_Charmap(self.ft_face, ftc.FT_ENCODING_UNICODE) != ftok) + return error.FaceLoadFailed; + + if (ftc.FT_Set_Pixel_Sizes(self.ft_face, size, size) != ftok) + return error.FaceLoadFailed; +} + +/// Add a glyph to the font atlas. The codepoint can be either a u8 or +/// []const u8 depending on if you know it is ASCII or must be UTF-8 decoded. +pub fn addGlyph(self: *FontAtlas, alloc: Allocator, codepoint: anytype) !void { + assert(self.ft_face != null); + + // We need a UTF32 codepoint for freetype + const utf32 = switch (@TypeOf(codepoint)) { + comptime_int, u8 => @intCast(u32, codepoint), + []const u8 => @intCast(u32, try std.unicode.utfDecode(codepoint)), + else => @compileError("invalid codepoint type"), + }; + + const glyph_index = ftc.FT_Get_Char_Index(self.ft_face, utf32); + + // TODO: probably not an error because we want to add a box. + if (glyph_index == 0) return error.CodepointNotFound; + + if (ftc.FT_Load_Glyph( + self.ft_face, + glyph_index, + ftc.FT_LOAD_RENDER, + ) != ftok) return error.LoadGlyphFailed; + + const glyph = self.ft_face.*.glyph; + const bitmap = glyph.*.bitmap; + + const src_w = bitmap.width; + const src_h = bitmap.rows; + const tgt_w = src_w; + const tgt_h = src_h; + + const region = try self.atlas.reserve(alloc, tgt_w, tgt_h); + + // Build our buffer + const buffer = try alloc.alloc(u8, tgt_w * tgt_h); + defer alloc.free(buffer); + var dst_ptr = buffer; + var src_ptr = bitmap.buffer; + var i: usize = 0; + while (i < src_h) : (i += 1) { + std.mem.copy(u8, dst_ptr, src_ptr[0..bitmap.width]); + dst_ptr = dst_ptr[tgt_w..]; + src_ptr += @intCast(usize, bitmap.pitch); + } + + // Write the glyph information into the atlas + assert(region.width == tgt_w); + assert(region.height == tgt_h); + self.atlas.set(region, buffer); + + const gop = try self.glyphs.getOrPut(alloc, utf32); + gop.value_ptr.* = .{ + .width = tgt_w, + .height = tgt_h, + .offset_x = glyph.*.bitmap_left, + .offset_y = glyph.*.bitmap_top, + .s0 = @intToFloat(f32, region.x) / @intToFloat(f32, self.atlas.size), + .t0 = @intToFloat(f32, region.y) / @intToFloat(f32, self.atlas.size), + .s1 = @intToFloat(f32, region.x + tgt_w) / @intToFloat(f32, self.atlas.size), + .t1 = @intToFloat(f32, region.y + tgt_h) / @intToFloat(f32, self.atlas.size), + .advance_x = @intToFloat(f32, glyph.*.advance.x), + }; +} + +test { + const alloc = testing.allocator; + var font = try init(try Atlas.init(alloc, 512)); + defer font.deinit(alloc); + defer font.atlas.deinit(alloc); + + try font.loadFaceFromMemory(testFont, 48); + + // Generate all visible ASCII + var i: u8 = 32; + while (i < 127) : (i += 1) { + try font.addGlyph(alloc, i); + } +} + +const testFont = @embedFile("../fonts/Inconsolata-Regular.ttf"); diff --git a/src/main.zig b/src/main.zig index 1360b2c5f..e799c821b 100644 --- a/src/main.zig +++ b/src/main.zig @@ -20,4 +20,5 @@ pub fn main() !void { test { _ = @import("Atlas.zig"); + _ = @import("FontAtlas.zig"); }