445311ee
feat(font): glyph atlas with row-based packing
a73x 2026-04-08 06:20
Commit message
src/font.zig
| Old | New | ||
|---|---|---|---|
| @@ -140,6 +140,93 @@ pub const Face = struct { | |||
| 140 | } | 140 | } |
| 141 | }; | 141 | }; |
| 142 | 142 | ||
| 143 | pub const GlyphUV = struct { | ||
| 144 | u0: f32, | ||
| 145 | v0: f32, | ||
| 146 | u1: f32, | ||
| 147 | v1: f32, | ||
| 148 | width: u32, | ||
| 149 | height: u32, | ||
| 150 | bearing_x: i32, | ||
| 151 | bearing_y: i32, | ||
| 152 | advance_x: i32, | ||
| 153 | }; | ||
| 154 | |||
| 155 | pub const Atlas = struct { | ||
| 156 | alloc: std.mem.Allocator, | ||
| 157 | width: u32, | ||
| 158 | height: u32, | ||
| 159 | pixels: []u8, // R8 | ||
| 160 | cursor_x: u32, | ||
| 161 | cursor_y: u32, | ||
| 162 | row_height: u32, | ||
| 163 | cache: std.AutoHashMap(u21, GlyphUV), | ||
| 164 | dirty: bool, | ||
| 165 | |||
| 166 | pub fn init(alloc: std.mem.Allocator, width: u32, height: u32) !Atlas { | ||
| 167 | const pixels = try alloc.alloc(u8, @as(usize, width) * @as(usize, height)); | ||
| 168 | @memset(pixels, 0); | ||
| 169 | return .{ | ||
| 170 | .alloc = alloc, | ||
| 171 | .width = width, | ||
| 172 | .height = height, | ||
| 173 | .pixels = pixels, | ||
| 174 | .cursor_x = 0, | ||
| 175 | .cursor_y = 0, | ||
| 176 | .row_height = 0, | ||
| 177 | .cache = std.AutoHashMap(u21, GlyphUV).init(alloc), | ||
| 178 | .dirty = true, | ||
| 179 | }; | ||
| 180 | } | ||
| 181 | |||
| 182 | pub fn deinit(self: *Atlas) void { | ||
| 183 | self.alloc.free(self.pixels); | ||
| 184 | self.cache.deinit(); | ||
| 185 | } | ||
| 186 | |||
| 187 | pub fn getOrInsert(self: *Atlas, face: *Face, codepoint: u21) !GlyphUV { | ||
| 188 | if (self.cache.get(codepoint)) |uv| return uv; | ||
| 189 | |||
| 190 | const glyph = try face.rasterize(codepoint); | ||
| 191 | defer face.freeGlyph(glyph); | ||
| 192 | |||
| 193 | if (self.cursor_x + glyph.width > self.width) { | ||
| 194 | self.cursor_x = 0; | ||
| 195 | self.cursor_y += self.row_height; | ||
| 196 | self.row_height = 0; | ||
| 197 | } | ||
| 198 | if (self.cursor_y + glyph.height > self.height) { | ||
| 199 | return error.AtlasFull; | ||
| 200 | } | ||
| 201 | |||
| 202 | var y: u32 = 0; | ||
| 203 | while (y < glyph.height) : (y += 1) { | ||
| 204 | const src = glyph.bitmap.ptr + @as(usize, y) * glyph.width; | ||
| 205 | const dst = self.pixels.ptr + (@as(usize, self.cursor_y + y) * self.width) + self.cursor_x; | ||
| 206 | @memcpy(dst[0..glyph.width], src[0..glyph.width]); | ||
| 207 | } | ||
| 208 | |||
| 209 | const uv = GlyphUV{ | ||
| 210 | .u0 = @as(f32, @floatFromInt(self.cursor_x)) / @as(f32, @floatFromInt(self.width)), | ||
| 211 | .v0 = @as(f32, @floatFromInt(self.cursor_y)) / @as(f32, @floatFromInt(self.height)), | ||
| 212 | .u1 = @as(f32, @floatFromInt(self.cursor_x + glyph.width)) / @as(f32, @floatFromInt(self.width)), | ||
| 213 | .v1 = @as(f32, @floatFromInt(self.cursor_y + glyph.height)) / @as(f32, @floatFromInt(self.height)), | ||
| 214 | .width = glyph.width, | ||
| 215 | .height = glyph.height, | ||
| 216 | .bearing_x = glyph.bearing_x, | ||
| 217 | .bearing_y = glyph.bearing_y, | ||
| 218 | .advance_x = glyph.advance_x, | ||
| 219 | }; | ||
| 220 | |||
| 221 | try self.cache.put(codepoint, uv); | ||
| 222 | self.cursor_x += glyph.width; | ||
| 223 | if (glyph.height > self.row_height) self.row_height = glyph.height; | ||
| 224 | self.dirty = true; | ||
| 225 | |||
| 226 | return uv; | ||
| 227 | } | ||
| 228 | }; | ||
| 229 | |||
| 143 | test "lookupMonospace returns a valid font path" { | 230 | test "lookupMonospace returns a valid font path" { |
| 144 | var lookup = try lookupMonospace(std.testing.allocator); | 231 | var lookup = try lookupMonospace(std.testing.allocator); |
| 145 | defer lookup.deinit(std.testing.allocator); | 232 | defer lookup.deinit(std.testing.allocator); |
| @@ -162,3 +249,24 @@ test "Face rasterizes glyph 'M'" { | |||
| 162 | try std.testing.expect(glyph.height > 0); | 249 | try std.testing.expect(glyph.height > 0); |
| 163 | try std.testing.expect(glyph.bitmap.len == @as(usize, glyph.width) * @as(usize, glyph.height)); | 250 | try std.testing.expect(glyph.bitmap.len == @as(usize, glyph.width) * @as(usize, glyph.height)); |
| 164 | } | 251 | } |
| 252 | |||
| 253 | test "Atlas packs multiple glyphs and returns UVs" { | ||
| 254 | var lookup = try lookupMonospace(std.testing.allocator); | ||
| 255 | defer lookup.deinit(std.testing.allocator); | ||
| 256 | |||
| 257 | var face = try Face.init(std.testing.allocator, lookup.path, lookup.index, 14); | ||
| 258 | defer face.deinit(); | ||
| 259 | |||
| 260 | var atlas = try Atlas.init(std.testing.allocator, 512, 512); | ||
| 261 | defer atlas.deinit(); | ||
| 262 | |||
| 263 | const uv_m = try atlas.getOrInsert(&face, 'M'); | ||
| 264 | const uv_a = try atlas.getOrInsert(&face, 'a'); | ||
| 265 | |||
| 266 | try std.testing.expect(uv_m.u0 >= 0.0 and uv_m.u1 <= 1.0); | ||
| 267 | try std.testing.expect(uv_a.u0 >= 0.0 and uv_a.u1 <= 1.0); | ||
| 268 | |||
| 269 | const uv_m2 = try atlas.getOrInsert(&face, 'M'); | ||
| 270 | try std.testing.expectEqual(uv_m.u0, uv_m2.u0); | ||
| 271 | try std.testing.expectEqual(uv_m.v0, uv_m2.v0); | ||
| 272 | } | ||