a73x

445311ee

feat(font): glyph atlas with row-based packing

a73x   2026-04-08 06:20

Commit message
feat(font): glyph atlas with row-based packing

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 }