From 796ffa5b2d09dd32565b0acbebd9a8af07579bf6 Mon Sep 17 00:00:00 2001 From: Rokas Puzonas Date: Mon, 14 Sep 2026 22:46:47 +0300 Subject: [PATCH] remove fonts from graphics state --- src/app.zig | 18 +- src/platform/font.zig | 739 ++++++++++++++++++++++++++++++++ src/platform/graphics.zig | 856 ++++---------------------------------- src/platform/slot_map.zig | 32 +- 4 files changed, 856 insertions(+), 789 deletions(-) create mode 100644 src/platform/font.zig diff --git a/src/app.zig b/src/app.zig index 640f8a4..d21427c 100644 --- a/src/app.zig +++ b/src/app.zig @@ -97,7 +97,7 @@ const Entity = struct { const Id = SlotMap.Id; }; -roboto_font: Gfx.Font.Id, +roboto_font: Gfx.Font, tilemap: Tilemap, entities: Entity.SlotMap, @@ -161,8 +161,10 @@ const Tilesheet = struct { }; pub fn init(self: *App, plt: Platform.Init) !?u8 { - const robot_font = Gfx.initFont(); - Gfx.setFont(robot_font, plt.assets.readFile("roboto-font/Roboto-Regular.ttf")); + const roboto_font = try Gfx.Font.init(plt.gpa, .{ + .spritesheet = Gfx.initSpritesheet(.{ .format = .r8 }), + .ttf_data = plt.assets.readFile("roboto-font/Roboto-Regular.ttf") + }); const tilesheet_png = try STBImage.load(plt.assets.readFile("sokoban/sokoban_tilesheet.png")); const tilesheet = try Tilesheet.init( @@ -206,7 +208,7 @@ pub fn init(self: *App, plt: Platform.Init) !?u8 { self.* = App{ .player_sprites = player_sprites, - .roboto_font = robot_font, + .roboto_font = roboto_font, .animation_timer = 0, .tilesheet = tilesheet, .footstep_buffer = footstep_sound, @@ -394,9 +396,17 @@ pub fn frame(self: *App, plt: Platform.Frame) !void { } } + // self.roboto_font.drawText(.{ + // .text = try std.fmt.allocPrint(plt.frame, "Hello {:.0}!", .{plt.time()}), + // .pos = .init(0, 0), + // .height = 32, + // }); } pub fn deinit(self: *App, plt: Platform.Deinit) void { + self.roboto_font.deinit(plt.gpa); + Gfx.deinitSpritesheet(self.roboto_font.spritesheet); + self.tilemap.deinit(plt.gpa); self.entities.slots.deinit(plt.gpa); } diff --git a/src/platform/font.zig b/src/platform/font.zig new file mode 100644 index 0000000..ef1a478 --- /dev/null +++ b/src/platform/font.zig @@ -0,0 +1,739 @@ +const std = @import("std"); +const log = std.log.scoped(.font); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; +const Io = std.Io; + +const Math = @import("math"); +const Vec2 = Math.Vec2; +const Rect = Math.Rect; + +const Gfx = @import("./graphics.zig"); +const SlotMapType = @import("slot_map.zig").SlotMapType; +const STBTrueType = @import("stb_truetype"); + +const Font = @This(); + +spritesheet: Gfx.Spritesheet.Id, +stb: STBTrueType.Font, + +cache: GlyphCache, + +const Options = struct { + spritesheet: Gfx.Spritesheet.Id, + ttf_data: [:0]const u8, + glyph_slots: u32 = 256 +}; + +pub fn init(gpa: Allocator, opts: Options) !Font { + const offset = STBTrueType.Font.getFontOffsetForIndex(opts.ttf_data, 0) + orelse return error.MissingFont0; + + const stb_font = try STBTrueType.Font.init(opts.ttf_data, offset); + + var cache = try GlyphCache.init(gpa, opts.glyph_slots); + errdefer cache.deinit(gpa); + + return Font{ + .spritesheet = opts.spritesheet, + .stb = stb_font, + .cache = cache, + }; +} + +pub fn deinit(self: *Font, gpa: Allocator) void { + self.cache.deinit(gpa); +} + +pub fn getGlyphIndex(font: *Font, codepoint: u21) ?Glyph.Index { + if (font.stb.findGlyphIndex(codepoint)) |index| { + return index; + } else { + // Fallback + return font.getGlyphIndex('?'); + } +} + +pub fn getGlyph(font: *Font, index: Glyph.Index, scale_x: f32, scale_y: f32) ?*Font.Glyph { + const glyph_key = Font.Glyph.Key{ + .index = index, + .scale_x = .fromF32(scale_x), + .scale_y = .fromF32(scale_y) + }; + + var iog_result = font.cache.insertOrGet(glyph_key); + if (iog_result == null and font.cache.lru_tail != GlyphSlot.nil_id) { + const id = font.cache.lru_tail; + if (font.cache.get(id).last_used_frame != Gfx.getFrameIndex()) { + font.cache.remove(id); + iog_result = font.cache.insertOrGet(glyph_key); + log.debug("Evicted least recently used glyph", .{}); + } + } + + if (iog_result) |result| { + const glyph_id = result.id; + const glyph = font.cache.get(glyph_id); + + if (!result.found_existing) { + glyph.box = font.stb.getGlyphBitmapBox( + glyph_key.index, + glyph_key.scale_x.toF32(), + glyph_key.scale_y.toF32() + ); + + const box_width: u32 = @intCast(glyph.box.x1 - glyph.box.x0); + const box_height: u32 = @intCast(glyph.box.y1 - glyph.box.y0); + if (box_width > 0 and box_height > 0) { + const sprite = Gfx.initSprite(.{ .spritesheet = font.spritesheet, .padding = 1 }); + + assert(box_width < Font.Glyph.max_width); + assert(box_height < Font.Glyph.max_height); + + var bitmap_buffer: [Font.Glyph.max_width * Font.Glyph.max_height]u8 = undefined; + const bitmap = Gfx.ImageData{ + .pixels = .{ .r8 = (&bitmap_buffer).ptr }, + .width = box_width, + .height = box_height + }; + font.stb.makeGlyphBitmap( + bitmap.pixels.?.r8, + box_width, box_height, + box_width, + glyph_key.scale_x.toF32(), + glyph_key.scale_y.toF32(), + glyph_key.index + ); + Gfx.setSprite(sprite, .{ .image = bitmap }); + + glyph.sprite = sprite; + } + } + + glyph.last_used_frame = Gfx.getFrameIndex(); + return glyph; + } else { + log.warn("Failed to render glyph, because cache is full", .{}); + return null; + } +} + +pub fn getAscent(font: *Font, height: f32) f32 { + const vmetrics = font.stb.getFontVMetrics(); + const scale = font.stb.scaleForPixelHeight(height); + return @as(f32, @floatFromInt(vmetrics.ascent)) * scale; +} + +pub fn measureText(font: *Font, height: f32, text: []const u8) Rect { + var bounds: Rect = .init(0, 0, 0, 0); + var first_glyph = false; + + var layout = TextRunLayout.init(font, height); + var iter = std.unicode.Utf8Iterator{ + .bytes = text, + .i = 0 + }; + while (iter.nextCodepoint()) |codepoint| { + const glyph_index = font.getGlyphIndex(codepoint) orelse continue; + const glyph_layout = layout.next(glyph_index) orelse continue; + + if (first_glyph) { + bounds = glyph_layout.rect; + first_glyph = false; + } else { + bounds = bounds.expand(glyph_layout.rect); + } + } + + return bounds; +} + +pub const DrawTextOptions = struct { + text: []const u8, + pos: Vec2, + height: f32, + color: Gfx.Color = .white, + alignment: Vec2 = .init(0, 0) +}; + +pub fn drawText(font: *Font, opts: DrawTextOptions) void { + var layout = Font.TextRunLayout.init(font, opts.height); + + var pos = opts.pos; + pos.y += font.getAscent(opts.height); + + if (!opts.alignment.eql(.zero)) { + const bounds = font.measureText(opts.height, opts.text); + pos = pos.sub(bounds.size.multiply(opts.alignment)); + } + + var iter = std.unicode.Utf8Iterator{ + .bytes = opts.text, + .i = 0 + }; + while (iter.nextCodepoint()) |codepoint| { + const glyph_index = font.getGlyphIndex(codepoint) orelse continue; + const glyph_layout = layout.next(glyph_index) orelse continue; + if (glyph_layout.sprite == Gfx.Sprite.Id.nil) { + continue; + } + + var glyph_pos = glyph_layout.rect.pos.add(pos); + + // Round to nearest pixel to avoid artifacts + glyph_pos.x = @round(glyph_pos.x); + glyph_pos.y = @round(glyph_pos.y); + + Gfx.drawSprite( + glyph_layout.sprite, + glyph_pos, + glyph_layout.rect.size, + opts.color + ); + } +} + +const GlyphCache = struct { + glyphs: []GlyphSlot, + glyphs_hashmap: []GlyphSlot.Id, + glyphs_used: std.bit_set.DynamicBitSetUnmanaged, + lru_head: GlyphSlot.Id, + lru_tail: GlyphSlot.Id, + free_head: GlyphSlot.Id, + + fn init(gpa: Allocator, capacity: u32) !GlyphCache { + assert(capacity > 0); + + const glyphs = try gpa.alloc(GlyphSlot, capacity); + errdefer gpa.free(glyphs); + + const glyphs_hashmap = try gpa.alloc(GlyphSlot.Id, 2*capacity); + errdefer gpa.free(glyphs); + + var glyphs_used = try std.bit_set.DynamicBitSetUnmanaged.initEmpty(gpa, capacity); + errdefer glyphs_used.deinit(gpa); + + var self: GlyphCache = .{ + .glyphs = glyphs, + .glyphs_hashmap = glyphs_hashmap, + .glyphs_used = glyphs_used, + .lru_head = GlyphSlot.nil_id, + .lru_tail = GlyphSlot.nil_id, + .free_head = undefined + }; + @memset(self.glyphs_hashmap, GlyphSlot.nil_id); + @memset(self.glyphs, .{ + .glyph = undefined, + .lru_prev = GlyphSlot.nil_id, + .lru_next = GlyphSlot.nil_id, + .next_free = GlyphSlot.nil_id, + }); + + assert(self.glyphs_hashmap.len > self.glyphs.len); + + self.free_head = 1; + for (1.., self.glyphs[1..(self.glyphs.len-1)]) |i, *glyph| { + glyph.next_free = @intCast(i + 1); + } + + return self; + } + + fn deinit(self: *GlyphCache, gpa: Allocator) void { + for (self.glyphs_hashmap) |glyph_id| { + if (glyph_id == GlyphSlot.nil_id) { + continue; + } + const slot = self.glyphs[glyph_id]; + slot.glyph.deinit(); + } + + gpa.free(self.glyphs); + gpa.free(self.glyphs_hashmap); + self.glyphs_used.deinit(gpa); + + self.* = undefined; + } + + fn get(self: *GlyphCache, id: GlyphSlot.Id) *Glyph { + return &self.glyphs[id].glyph; + } + + fn appendToLRUHead(self: *GlyphCache, id: GlyphSlot.Id) void { + if (self.lru_head != GlyphSlot.nil_id) { + self.glyphs[self.lru_head].lru_prev = id; + self.glyphs[id].lru_next = self.lru_head; + } else { + assert(self.lru_tail == GlyphSlot.nil_id); + self.lru_tail = id; + } + + self.glyphs[id].lru_prev = GlyphSlot.nil_id; + self.lru_head = id; + } + + fn removeFromLRU(self: *GlyphCache, id: GlyphSlot.Id) void { + assert(id != GlyphSlot.nil_id); + + const lru_prev = self.glyphs[id].lru_prev; + const lru_next = self.glyphs[id].lru_next; + + if (lru_prev != GlyphSlot.nil_id) { + self.glyphs[lru_prev].lru_next = lru_next; + } + + if (lru_next != GlyphSlot.nil_id) { + self.glyphs[lru_next].lru_prev = lru_prev; + } + + if (id == self.lru_head) { + self.lru_head = lru_next; + } + if (id == self.lru_tail) { + self.lru_tail = lru_prev; + } + } + + fn popFreeId(self: *GlyphCache) ?GlyphSlot.Id { + if (self.free_head != GlyphSlot.nil_id) { + const result = self.free_head; + self.free_head = self.glyphs[result].next_free; + return result; + } + + return null; + } + + fn pushFreeId(self: *GlyphCache, id: GlyphSlot.Id) void { + assert(id != GlyphSlot.nil_id); + self.glyphs[id].next_free = self.free_head; + self.free_head = id; + } + + pub const InsertOrGetResult = struct { + id: GlyphSlot.Id, + found_existing: bool, + }; + + fn insertOrGet(self: *GlyphCache, key: Glyph.Key) ?InsertOrGetResult { + for (0..self.glyphs_hashmap.len) |offset| { + const i = (key.toInt() + offset) % self.glyphs_hashmap.len; + const hashmap_id = self.glyphs_hashmap[i]; + if (hashmap_id == GlyphSlot.nil_id) { + const id = self.popFreeId() orelse return null; + self.glyphs_hashmap[i] = id; + + self.glyphs[id].glyph = .init(key); + self.glyphs[id].lru_next = GlyphSlot.nil_id; + self.glyphs[id].lru_prev = GlyphSlot.nil_id; + self.appendToLRUHead(id); + return InsertOrGetResult{ + .id = id, + .found_existing = false + }; + } else { + const glyph = &self.glyphs[hashmap_id].glyph; + if (glyph.key == key) { + // An entry for this key already exists + self.removeFromLRU(hashmap_id); + self.appendToLRUHead(hashmap_id); + return InsertOrGetResult{ + .id = hashmap_id, + .found_existing = true + }; + } + } + } + + return null; + } + + fn removeFromHashmap(self: *GlyphCache, id: GlyphSlot.Id) void { + var bucket = self.glyphs[id].glyph.key.toInt() % self.glyphs_hashmap.len; + for (0..self.glyphs_hashmap.len) |_| { + assert(self.glyphs_hashmap[bucket] != GlyphSlot.nil_id); + if (self.glyphs_hashmap[bucket] == id) { + break; + } + + bucket = (bucket + 1) % self.glyphs_hashmap.len; + } + + assert(self.glyphs_hashmap[bucket] == id); + + var empty = bucket; + var current = (bucket + 1) % self.glyphs_hashmap.len; + + while (self.glyphs_hashmap[current] != GlyphSlot.nil_id) { + const current_glyph_id = self.glyphs_hashmap[current]; + const home = self.glyphs[current_glyph_id].glyph.key.toInt() % self.glyphs_hashmap.len; + + if (self.hashmapDistance(home, current) > self.hashmapDistance(home, empty)) { + self.glyphs_hashmap[empty] = self.glyphs_hashmap[current]; + empty = current; + } + + current = (current + 1) % self.glyphs_hashmap.len; + } + + assert(self.glyphs_hashmap[empty] != GlyphSlot.nil_id); + self.glyphs_hashmap[empty] = GlyphSlot.nil_id; + } + + fn hashmapDistance(self: GlyphCache, from: usize, to: usize) usize { + if (to >= from) { + return to - from; + } else { + return self.glyphs_hashmap.len - from + to; + } + } + + fn remove(self: *GlyphCache, id: GlyphSlot.Id) void { + assert(id != GlyphSlot.nil_id); + self.pushFreeId(id); + self.removeFromHashmap(id); + self.removeFromLRU(id); + + const glyph = self.glyphs[id].glyph; + glyph.deinit(); + } + + fn getLRUItems(self: *GlyphCache, gpa: Allocator) ![]GlyphSlot.Id { + var lru: std.ArrayList(GlyphSlot.Id) = .empty; + defer lru.deinit(gpa); + + var lru_iter = self.lru_head; + while (lru_iter != GlyphSlot.nil_id) { + try lru.append(gpa, lru_iter); + lru_iter = self.glyphs[lru_iter].lru_next; + } + + return try lru.toOwnedSlice(gpa); + } + + test { + const gpa = std.testing.allocator; + var cache = try GlyphCache.init(gpa, 256); + defer cache.deinit(gpa); + + const key = Glyph.Key{ .index = 10, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }; + const id1 = cache.insert(key).?; + const id2 = cache.insert(key).?; + try std.testing.expectEqual(id1, id2); + } + + test { + const gpa = std.testing.allocator; + var cache = try GlyphCache.init(gpa, 256); + defer cache.deinit(gpa); + + const key = Glyph.Key{ .index = 10, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }; + const id1 = cache.insert(key).?; + cache.remove(id1); + const id2 = cache.insert(key).?; + try std.testing.expectEqual(id1, id2); + } + + test { + const gpa = std.testing.allocator; + var cache = try GlyphCache.init(gpa, 256); + defer cache.deinit(gpa); + + const id1 = cache.insert(Glyph.Key{ .index = 10, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?; + const id2 = cache.insert(Glyph.Key{ .index = 11, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?; + try std.testing.expect(id1 != id2); + } + + test { + const gpa = std.testing.allocator; + const limit = 256; + var cache = try GlyphCache.init(gpa, limit); + defer cache.deinit(gpa); + + for (0..limit-1) |i| { + const id = cache.insert(Glyph.Key{ .index = @intCast(i), .scale_x = .fromF32(1), .scale_y = .fromF32(1) }); + try std.testing.expect(id != null); + } + const id = cache.insert(Glyph.Key{ .index = limit, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }); + try std.testing.expect(id == null); + } + + test { + const gpa = std.testing.allocator; + const limit = 256; + var cache = try GlyphCache.init(gpa, limit); + defer cache.deinit(gpa); + + for (0..10) |_| { + var ids: std.ArrayList(GlyphSlot.Id) = .empty; + defer ids.deinit(gpa); + + for (0..(limit-1)) |i| { + const id = cache.insert(Glyph.Key{ .index = @intCast(i), .scale_x = .fromF32(1), .scale_y = .fromF32(1) }); + try std.testing.expect(id != null); + try ids.append(gpa, id.?); + } + assert(cache.free_head == GlyphSlot.nil_id); + + const lru = try cache.getLRUItems(gpa); + defer gpa.free(lru); + + const expected_lru = try gpa.dupe(GlyphSlot.Id, ids.items); + defer gpa.free(expected_lru); + std.mem.reverse(GlyphSlot.Id, expected_lru); + + try std.testing.expectEqualSlices(GlyphSlot.Id, expected_lru, lru); + + for (ids.items) |id| { + cache.remove(id); + } + assert(cache.lru_head == GlyphSlot.nil_id); + assert(cache.lru_tail == GlyphSlot.nil_id); + } + } + + test { + const gpa = std.testing.allocator; + var cache = try GlyphCache.init(gpa, 256); + defer cache.deinit(gpa); + + const id1 = cache.insert(.{ .index = 0, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?; + const id2 = cache.insert(.{ .index = 1, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?; + const id3 = cache.insert(.{ .index = 2, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?; + + const lru = try cache.getLRUItems(gpa); + defer gpa.free(lru); + + try std.testing.expectEqualSlices(GlyphSlot.Id, &.{ id3, id2, id1 }, lru); + } + + test { + const gpa = std.testing.allocator; + var cache = try GlyphCache.init(gpa, 256); + defer cache.deinit(gpa); + + const id1 = cache.insert(.{ .index = 0, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?; + const id2 = cache.insert(.{ .index = 1, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?; + const id3 = cache.insert(.{ .index = 2, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?; + + const lru = try cache.getLRUItems(gpa); + defer gpa.free(lru); + + try std.testing.expectEqualSlices(GlyphSlot.Id, &.{ id3, id2, id1 }, lru); + } + + test { + const gpa = std.testing.allocator; + var cache = try GlyphCache.init(gpa, 256); + defer cache.deinit(gpa); + + const id = cache.insert(.{ .index = 0, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?; + try std.testing.expect(cache.lru_head != GlyphSlot.nil_id); + try std.testing.expect(cache.lru_tail != GlyphSlot.nil_id); + cache.remove(id); + try std.testing.expect(cache.lru_head == GlyphSlot.nil_id); + try std.testing.expect(cache.lru_tail == GlyphSlot.nil_id); + } + + test { + const gpa = std.testing.allocator; + var cache = try GlyphCache.init(gpa, 256); + defer cache.deinit(gpa); + + const key1 = Glyph.Key{ .index = 0, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }; + const id1 = cache.insert(key1).?; + const key2 = Glyph.Key{ .index = 1, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }; + const id2 = cache.insert(key2).?; + const key3 = Glyph.Key{ .index = 2, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }; + const id3 = cache.insert(key3).?; + + _ = cache.lookup(key2); + + { + const lru = try cache.getLRUItems(gpa); + defer gpa.free(lru); + try std.testing.expectEqualSlices(GlyphSlot.Id, &.{ id2, id3, id1 }, lru); + } + + _ = cache.lookup(key1); + + { + const lru = try cache.getLRUItems(gpa); + defer gpa.free(lru); + try std.testing.expectEqualSlices(GlyphSlot.Id, &.{ id1, id2, id3 }, lru); + } + } + + test { + const gpa = std.testing.allocator; + var cache = try GlyphCache.init(gpa, 256); + defer cache.deinit(gpa); + + const id1 = cache.insert(.{ .index = 0, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?; + const id2 = cache.insert(.{ .index = 1, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?; + const id3 = cache.insert(.{ .index = 2, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?; + + { + const lru = try cache.getLRUItems(gpa); + defer gpa.free(lru); + try std.testing.expectEqualSlices(GlyphSlot.Id, &.{ id3, id2, id1 }, lru); + } + + _ = cache.remove(id2); + + { + const lru = try cache.getLRUItems(gpa); + defer gpa.free(lru); + try std.testing.expectEqualSlices(GlyphSlot.Id, &.{ id3, id1 }, lru); + } + + _ = cache.remove(id3); + + { + const lru = try cache.getLRUItems(gpa); + defer gpa.free(lru); + try std.testing.expectEqualSlices(GlyphSlot.Id, &.{ id1 }, lru); + } + + _ = cache.remove(id1); + + { + const lru = try cache.getLRUItems(gpa); + defer gpa.free(lru); + try std.testing.expectEqualSlices(GlyphSlot.Id, &.{ }, lru); + } + } +}; + +const GlyphSlot = struct { + glyph: Glyph, + + lru_next: GlyphSlot.Id, + lru_prev: GlyphSlot.Id, + + next_free: GlyphSlot.Id, + + const Id = u16; + const nil_id: GlyphSlot.Id = 0; + + comptime { + assert(@bitSizeOf(Id) % 8 == 0); + } +}; + +const Glyph = struct { + key: Key, + + box: STBTrueType.Box, + sprite: Gfx.Sprite.Id, + last_used_frame: u32, + + const max_width: u32 = 512; + const max_height: u32 = 512; + + const Index = u32; + + const Key = packed struct(u64) { + index: Index, + scale_x: q4d12, + scale_y: q4d12, + + pub fn toInt(self: Key) u64 { + return @bitCast(self); + } + + // 4 integer bits, 12 fractional bits + const q4d12 = enum(u16) { + _, + + pub fn toF32(integer: q4d12) f32 { + return @as(f32, @floatFromInt(@intFromEnum(integer))) / (1 << 12); + } + + pub fn fromF32(float: f32) q4d12 { + return @enumFromInt(@as(u16, @intFromFloat(float * (1 << 12)))); + } + + pub fn format(self: q4d12, writer: *Io.Writer) Io.Writer.Error!void { + try writer.print("q12d4{{ {} }}", .{self.toF32()}); + } + }; + + test { + try std.testing.expectEqual(q4d12.fromF32(10.0625).toF32(), 10.0625); + } + }; + + fn init(key: Key) Glyph { + return Glyph{ + .key = key, + .box = .{ + .x0 = 0, + .y0 = 0, + .x1 = 0, + .y1 = 0, + }, + .sprite = .nil, + .last_used_frame = 0 + }; + } + + fn deinit(self: Glyph) void { + Gfx.deinitSprite(self.sprite); + } +}; + +pub const TextRunLayout = struct { + font: *Font, + scale_x: f32, + scale_y: f32, + + prev_glyph: ?Font.Glyph.Index, + pen_pos: Vec2, + + const GlyphRect = struct { + rect: Rect, + sprite: Gfx.Sprite.Id, + }; + + pub fn init(font: *Font, height: f32) TextRunLayout { + const scale = font.stb.scaleForPixelHeight(height); + + return TextRunLayout{ + .font = font, + .scale_x = scale, + .scale_y = scale, + .prev_glyph = null, + .pen_pos = .init(0, 0) + }; + } + + pub fn next(self: *TextRunLayout, glyph_index: Font.Glyph.Index) ?GlyphRect { + const glyph = self.font.getGlyph(glyph_index, self.scale_x, self.scale_y) orelse return null; + + if (self.prev_glyph) |prev_glyph| { + const kerning = self.font.stb.getGlyphKernAdvance(prev_glyph, glyph_index); + self.pen_pos.x += @as(f32, @floatFromInt(kerning)) * self.scale_x; + } + + const hmetrics = self.font.stb.getGlyphHMetrics(glyph_index); + const glyph_size = Vec2.initFromInt(i32, glyph.box.x1 - glyph.box.x0, glyph.box.y1 - glyph.box.y0); + + var draw_pos = self.pen_pos; + draw_pos.x -= @floatFromInt(glyph.box.x0); + draw_pos.x += @as(f32, @floatFromInt(hmetrics.left_side_bearing)) * self.scale_x; + draw_pos.y += @floatFromInt(glyph.box.y0); + const draw_rect = Rect{ + .pos = draw_pos, + .size = glyph_size + }; + + self.pen_pos.x += @as(f32, @floatFromInt(hmetrics.advance_width)) * self.scale_x; + self.prev_glyph = glyph_index; + + return GlyphRect{ + .rect = draw_rect, + .sprite = glyph.sprite + }; + } +}; diff --git a/src/platform/graphics.zig b/src/platform/graphics.zig index a468ca7..8bb68c8 100644 --- a/src/platform/graphics.zig +++ b/src/platform/graphics.zig @@ -17,14 +17,25 @@ const Rect = Math.Rect; const STBRectPack = @import("stb_rect_pack"); const STBTrueType = @import("stb_truetype"); const ImGUI = @import("imgui.zig"); -const Color = @import("./color.zig"); +pub const Color = @import("./color.zig"); const shd = @import("shader"); const SlotMapType = @import("slot_map.zig").SlotMapType; +pub const Font = @import("./font.zig"); + +const Frame = struct { + index: u32, + quads: std.ArrayList(VertexQuad), + transforms: std.ArrayList(TransformFrame), + + clear_color: Color = .black, + pass_started: bool = false +}; + const State = struct { gpa: std.mem.Allocator, - frame_index: u64, + frame: Frame, shader: sg.Shader, pipeline: sg.Pipeline, @@ -33,19 +44,13 @@ const State = struct { default_sprite: Sprite.Id, error_sprite: Sprite.Id, default_spritesheet: Spritesheet.Id, - default_font_spritesheet: Spritesheet.Id, linear_sampler: sg.Sampler, nearest_sampler: sg.Sampler, default_sampler: Sampler, quads_buffer: [8192]VertexQuad, - quads: std.ArrayList(VertexQuad), - - clear_color: Color, - transforms_buffer: [32]TransformFrame, - transforms: std.ArrayList(TransformFrame) = .empty, textures_buffer: [Texture.max_textures]Texture.SlotMap.Slot, textures: Texture.SlotMap, @@ -55,9 +60,6 @@ const State = struct { sprites_buffer: [Sprite.max_sprites]Sprite.SlotMap.Slot, sprites: Sprite.SlotMap, - - fonts_buffer: [Font.max_fonts]Font.SlotMap.Slot, - fonts: Font.SlotMap, }; var g_state: State = undefined; @@ -148,25 +150,19 @@ pub fn init(gpa: std.mem.Allocator, logger: sg.Logger) !void { self.* = State{ .gpa = gpa, - .frame_index = 0, .shader = shader, .pipeline = pipeline, .bindings = bindings, .quads_buffer = undefined, - .quads = .initBuffer(&self.quads_buffer), - .clear_color = .black, + .transforms_buffer = undefined, .default_sprite = undefined, .error_sprite = undefined, .default_spritesheet = undefined, - .default_font_spritesheet = undefined, .linear_sampler = linear_sampler, .nearest_sampler = nearest_sampler, .default_sampler = .nearest, - .transforms_buffer = undefined, - .transforms = .initBuffer(&self.transforms_buffer), - .textures_buffer = undefined, .textures = .initBuffer(&self.textures_buffer), @@ -176,12 +172,15 @@ pub fn init(gpa: std.mem.Allocator, logger: sg.Logger) !void { .spritesheets_buffer = undefined, .spritesheets = .initBuffer(&self.spritesheets_buffer), - .fonts_buffer = undefined, - .fonts = .initBuffer(&self.fonts_buffer), + .frame = .{ + .index = 0, + .quads = .initBuffer(&self.quads_buffer), + .transforms = .initBuffer(&self.transforms_buffer), + .clear_color = .black + } }; self.default_spritesheet = initSpritesheet(.{}); - self.default_font_spritesheet = initSpritesheet(.{ .format = .r8 }); { const width = 8; @@ -279,11 +278,11 @@ fn toSokolColor(color: Color) sokol.gfx.Color { fn getTransformPtr() ?*TransformFrame { const self = &g_state; - if (self.transforms.items.len == 0) { + if (self.frame.transforms.items.len == 0) { return null; } - return &self.transforms.items[self.transforms.items.len-1]; + return &self.frame.transforms.items[self.frame.transforms.items.len-1]; } pub fn transformTranslate(offset: Vec2) void { @@ -303,14 +302,14 @@ pub fn transformPush() void { const top_frame = getTransformPtr() orelse return; - self.transforms.appendBounded(top_frame.*) catch { + self.frame.transforms.appendBounded(top_frame.*) catch { log.warn("Transform stack limit reached!", .{}); }; } pub fn transformPop() void { const self = &g_state; - _ = self.transforms.pop(); + _ = self.frame.transforms.pop(); } pub fn getTransform() TransformFrame { @@ -327,21 +326,17 @@ pub fn getTransform() TransformFrame { pub fn beginFrame() void { const self = &g_state; - self.transforms.clearRetainingCapacity(); - self.transforms.appendAssumeCapacity(TransformFrame{ - .offset = .init(0, 0), - .scale = .init(1, 1), - }); - - var pass_action: sg.PassAction = .{}; - pass_action.colors[0] = .{ - .load_action = .CLEAR, - .clear_value = toSokolColor(self.clear_color) + self.frame.quads.clearRetainingCapacity(); + self.frame.transforms.clearRetainingCapacity(); + self.frame = Frame{ + .index = self.frame.index, + .quads = self.frame.quads, + .transforms = self.frame.transforms }; - sg.beginPass(.{ - .action = pass_action, - .swapchain = sglue.swapchain() + self.frame.transforms.appendAssumeCapacity(TransformFrame{ + .offset = .init(0, 0), + .scale = .init(1, 1), }); self.bindings.views[shd.VIEW_tex].id = sg.invalid_id; @@ -415,21 +410,38 @@ pub fn showDebug() void { } } -// TODO: This will always have a 1-frame delay. -// fix this. pub fn setClearColor(color: Color) void { const self = &g_state; - self.clear_color = color; + self.frame.clear_color = color; +} + +pub fn getFrameIndex() u32 { + const self = &g_state; + return self.frame.index; } pub fn flush() void { const self = &g_state; - if (self.quads.items.len == 0) { + if (self.frame.quads.items.len == 0) { return; } - defer self.quads.clearRetainingCapacity(); + defer self.frame.quads.clearRetainingCapacity(); + + if (!self.frame.pass_started) { + var pass_action: sg.PassAction = .{}; + pass_action.colors[0] = .{ + .load_action = .CLEAR, + .clear_value = toSokolColor(self.frame.clear_color) + }; + + sg.beginPass(.{ + .action = pass_action, + .swapchain = sglue.swapchain() + }); + self.frame.pass_started = true; + } var spritesheet_iter = self.spritesheets.iterator(); while (spritesheet_iter.next()) |spritesheet_id| { @@ -447,7 +459,7 @@ pub fn flush() void { const vertex_buffer = self.bindings.vertex_buffers[0]; - const data = sg.asRange(self.quads.items); + const data = sg.asRange(self.frame.quads.items); if (sg.queryBufferWillOverflow(vertex_buffer, data.size)) { log.warn("Vertex buffer overflow", .{}); return; @@ -477,8 +489,8 @@ pub fn flush() void { sg.applyBindings(self.bindings); sg.applyUniforms(shd.UB_vs_params, sg.asRange(&vs_params)); sg.applyUniforms(shd.UB_fs_params, sg.asRange(&fs_params)); - assert(self.quads.items.len > 0); - sg.draw(0, @intCast(self.quads.items.len * 6), 1); + assert(self.frame.quads.items.len > 0); + sg.draw(0, @intCast(self.frame.quads.items.len * 6), 1); } pub fn endFrame() void { @@ -486,21 +498,23 @@ pub fn endFrame() void { flush(); - sg.endPass(); - sg.commit(); + if (self.frame.pass_started) { + sg.endPass(); + sg.commit(); + } var spritesheet_iter = self.spritesheets.iterator(); while (spritesheet_iter.next()) |spritesheet_id| { repackSpritesheetIfNeeded(spritesheet_id); } - if (self.transforms.items.len > 1) { + if (self.frame.transforms.items.len > 1) { log.warn("Too many calls to transformPush()", .{}); - } else if (self.transforms.items.len == 0) { + } else if (self.frame.transforms.items.len == 0) { log.warn("Too many calls to transformPop()", .{}); } - self.frame_index += 1; + self.frame.index += 1; } pub fn drawRectangle(pos: Vec2, size: Vec2, color: Color) void { @@ -616,7 +630,7 @@ fn isViewBound(view: sg.View) bool { pub fn deinitTexture(id: Texture.Id) void { const self = &g_state; - if (id == .nil) { + if (id == Texture.Id.nil) { return; } @@ -681,11 +695,11 @@ pub fn setTexture(id: Texture.Id, texture_data: ImageData) void { image_desc.usage.immutable == new_image_desc.usage.immutable and image_desc.usage.dynamic_update ) { - log.debug("[{}] Update texture '{f}'", .{self.frame_index, id}); - if (texture.updated_at == null or texture.updated_at.? != self.frame_index) { + log.debug("[{}] Update texture '{f}'", .{self.frame.index, id}); + if (texture.updated_at == null or texture.updated_at.? != self.frame.index) { sg.updateImage(texture.image, image_data); texture_updated = true; - texture.updated_at = self.frame_index; + texture.updated_at = self.frame.index; } else { log.warn("Attempt to update the same texture multiple times per frame, texture: {f}", .{id}); } @@ -693,7 +707,7 @@ pub fn setTexture(id: Texture.Id, texture_data: ImageData) void { } if (!texture_updated) { - log.debug("[{}] Create image-view, texture={f}", .{self.frame_index, id}); + log.debug("[{}] Create image-view, texture={f}", .{self.frame.index, id}); const new_image = sg.makeImage(new_image_desc); if (sg.queryImageState(new_image) != .VALID) { log.warn("makeImage() failed", .{}); @@ -719,7 +733,7 @@ pub fn setTexture(id: Texture.Id, texture_data: ImageData) void { if (texture.dynamic) { log.debug("update texture {f}", .{id}); sg.updateImage(new_image, image_data); - texture.updated_at = self.frame_index; + texture.updated_at = self.frame.index; } } } @@ -810,7 +824,7 @@ pub fn draw(opts: DrawOptions) void { bound_sampler.* = sampler; } - if (self.quads.items.len == self.quads.capacity) { + if (self.frame.quads.items.len == self.frame.quads.capacity) { flush(); } @@ -820,7 +834,7 @@ pub fn draw(opts: DrawOptions) void { } } - self.quads.appendAssumeCapacity(quad); + self.frame.quads.appendAssumeCapacity(quad); } const SpriteOptions = struct { @@ -848,6 +862,10 @@ pub fn initSprite(opts: SpriteOptions) Sprite.Id { pub fn deinitSprite(id: Sprite.Id) void { const self = &g_state; + if (id == Sprite.Id.nil) { + return; + } + if (self.sprites.exists(id)) { const sprite = self.sprites.getAssumeExists(id); if (sprite.data) |data| { @@ -943,7 +961,7 @@ fn repackSpritesheet(id: Spritesheet.Id) void { return; }; - log.debug("[{}] Repack spritesheet: {f}", .{self.frame_index, id}); + log.debug("[{}] Repack spritesheet: {f}", .{self.frame.index, id}); // TODO: Add a smarter startegy for picking the initial texture size. // One idea is to calculate the sum area of all sprites and pick a atlas size based on that. @@ -1022,7 +1040,7 @@ fn rebuildSpritesheetTextureIfNeeded(id: Spritesheet.Id) void { return; } - log.debug("[{}] Rebuild spritesheet texture: {f}", .{self.frame_index, id}); + log.debug("[{}] Rebuild spritesheet texture: {f}", .{self.frame.index, id}); const image_data = ImageData.init( self.gpa, @@ -1184,275 +1202,19 @@ pub fn initSpritesheet(opts: SpriteSheetOptions) Spritesheet.Id { pub fn deinitSpritesheet(id: Spritesheet.Id) void { const self = &g_state; - if (self.spritesheets.get(id)) { - const spritesheet = self.spritesheets.getAssumeExists(id); + if (self.spritesheets.get(id)) |spritesheet| { deinitTexture(spritesheet.texture); self.spritesheets.removeAssumeExists(id); } } -pub fn initFont() Font.Id { - const self = &g_state; - const id = self.fonts.insertUndefined() catch { - log.warn("Failed to create font, limit reached! limit: {}", .{self.fonts.slots.capacity}); - return .nil; - }; - - const font = self.fonts.getAssumeExists(id); - font.* = .{ - .spritesheet = self.default_font_spritesheet, - .stb = null, - .cache = .init(), - }; - - return id; -} - -pub fn deinitFont(id: Font.Id) void { - const self = &g_state; - if (self.fonts.get(id)) |font| { - font.cache.deinit(); - self.fonts.removeAssumeExists(id); - } -} - -pub fn setFont(id: Font.Id, ttf_data: [:0]const u8) void { - const self = &g_state; - const font = self.fonts.get(id) orelse { - log.warn("Attempt to set font that doesn't exist: {}", .{id}); - return; - }; - - const offset = STBTrueType.Font.getFontOffsetForIndex(ttf_data, 0) orelse { - log.err("Failed to get offset for font at index 0", .{}); - return; - }; - - const stb_font = STBTrueType.Font.init(ttf_data, offset) catch { - log.err("Failed to initialize font info", .{}); - return; - }; - - // TODO: deinit previous glyphs - - font.stb = stb_font; - - font.cache.deinit(); - font.cache = .init(); -} - -pub fn getGlyphIndex(id: Font.Id, codepoint: u21) ?GlyphIndex { - const self = &g_state; - const font = self.fonts.get(id) orelse return null; - const stb_font = font.stb orelse return null; - - if (stb_font.findGlyphIndex(codepoint)) |index| { - return index; - } else { - // Fallback - return getGlyphIndex(id, '?'); - } -} - -pub fn getFontPixelScale(id: Font.Id, height: f32) f32 { - const self = &g_state; - const font = self.fonts.get(id) orelse return 0; - const stb_font = font.stb orelse return 0; - - return stb_font.scaleForPixelHeight(height); -} - -pub fn getFontAscent(id: Font.Id, height: f32) f32 { - const self = &g_state; - const font = self.fonts.get(id) orelse return 0; - const stb_font = font.stb orelse return 0; - - const vmetrics = stb_font.getFontVMetrics(); - const scale = stb_font.scaleForPixelHeight(height); - return @as(f32, @floatFromInt(vmetrics.ascent)) * scale; -} - -pub fn getGlyph(id: Font.Id, index: GlyphIndex, scale_x: f32, scale_y: f32) ?*Font.Glyph { - const self = &g_state; - const font = self.fonts.get(id) orelse return null; - const stb_font = font.stb orelse return null; - - // TODO: Allow specifying a different scale for x and y - const glyph_key = Font.Glyph.Key{ - .index = index, - .scale_x = scale_x, - .scale_y = scale_y - }; - - var glyph: *Font.Glyph = undefined; - if (font.cache.lookup(glyph_key)) |glyph_id| { - return font.cache.get(glyph_id); - } else { - const glyph_id = font.cache.insert(glyph_key) orelse { - @panic("TODO: render glyph lru_tail"); - }; - glyph = font.cache.get(glyph_id); - glyph.box = stb_font.getGlyphBitmapBox(glyph_key.index, glyph_key.scale_x, glyph_key.scale_y); - glyph.sprite = null; - - const box_width: u32 = @intCast(glyph.box.x1 - glyph.box.x0); - const box_height: u32 = @intCast(glyph.box.y1 - glyph.box.y0); - if (box_width > 0 and box_height > 0) { - const sprite = initSprite(.{ .spritesheet = font.spritesheet, .padding = 1 }); - - assert(box_width < Font.Glyph.max_width); - assert(box_height < Font.Glyph.max_height); - - var bitmap_buffer: [Font.Glyph.max_width * Font.Glyph.max_height]u8 = undefined; - const bitmap = ImageData{ - .pixels = .{ .r8 = (&bitmap_buffer).ptr }, - .width = box_width, - .height = box_height - }; - stb_font.makeGlyphBitmap( - bitmap.pixels.?.r8, - box_width, box_height, - box_width, - glyph_key.scale_x, glyph_key.scale_y, - glyph_key.index - ); - setSprite(sprite, .{ .image = bitmap }); - - assert(glyph.sprite == null); - glyph.sprite = sprite; - } - } - glyph.used_this_frame = true; - - return glyph; -} - -pub const TextRunLayout = struct { - font: Font.Id, - scale_x: f32, - scale_y: f32, - - prev_glyph: ?GlyphIndex, - pen_pos: Vec2, - - const GlyphRect = struct { - rect: Rect, - sprite: ?Sprite.Id, - }; - - pub fn init(font: Font.Id, height: f32) TextRunLayout { - const scale = getFontPixelScale(font, height); - - return TextRunLayout{ - .font = font, - .scale_x = scale, - .scale_y = scale, - .prev_glyph = null, - .pen_pos = .init(0, 0) - }; - } - - pub fn next(self: *TextRunLayout, glyph_index: GlyphIndex) ?GlyphRect { - const glyph = getGlyph(self.font, glyph_index, self.scale_x, self.scale_y) orelse return null; - - const font = g_state.fonts.get(self.font) orelse return null; - const stb_font = font.stb orelse return null; - - if (self.prev_glyph) |prev_glyph| { - const kerning = stb_font.getGlyphKernAdvance(prev_glyph, glyph_index); - self.pen_pos.x += @as(f32, @floatFromInt(kerning)) * self.scale_x; - } - - const hmetrics = stb_font.getGlyphHMetrics(glyph_index); - const glyph_size = Vec2.initFromInt(i32, glyph.box.x1 - glyph.box.x0, glyph.box.y1 - glyph.box.y0); - - var draw_pos = self.pen_pos; - draw_pos.x -= @floatFromInt(glyph.box.x0); - draw_pos.x += @as(f32, @floatFromInt(hmetrics.left_side_bearing)) * self.scale_x; - draw_pos.y += @floatFromInt(glyph.box.y0); - const draw_rect = Rect{ - .pos = draw_pos, - .size = glyph_size - }; - - self.pen_pos.x += @as(f32, @floatFromInt(hmetrics.advance_width)) * self.scale_x; - self.prev_glyph = glyph_index; - - return GlyphRect{ - .rect = draw_rect, - .sprite = glyph.sprite - }; - } -}; - -pub fn measureText(id: Font.Id, height: f32, text: []const u8) Rect { - var bounds: Rect = .init(0, 0, 0, 0); - var first_glyph = false; - - var layout = TextRunLayout.init(id, height); - var iter = std.unicode.Utf8Iterator{ - .bytes = text, - .i = 0 - }; - while (iter.nextCodepoint()) |codepoint| { - const glyph_index = getGlyphIndex(id, codepoint) orelse continue; - const glyph_layout = layout.next(glyph_index) orelse continue; - - if (first_glyph) { - bounds = glyph_layout.rect; - first_glyph = false; - } else { - bounds = bounds.expand(glyph_layout.rect); - } - } - - return bounds; -} - -pub const DrawTextOptions = struct { - text: []const u8, - pos: Vec2, - height: f32, - color: Color = .white, - alignment: Vec2 = .init(0, 0) -}; - -pub fn drawText(id: Font.Id, opts: DrawTextOptions) void { - var layout = TextRunLayout.init(id, opts.height); - - var pos = opts.pos; - pos.y += getFontAscent(id, opts.height); - - if (!opts.alignment.eql(.zero)) { - const bounds = measureText(id, opts.height, opts.text); - pos = pos.sub(bounds.size.multiply(opts.alignment)); - } - - var iter = std.unicode.Utf8Iterator{ - .bytes = opts.text, - .i = 0 - }; - while (iter.nextCodepoint()) |codepoint| { - const glyph_index = getGlyphIndex(id, codepoint) orelse continue; - const glyph_layout = layout.next(glyph_index) orelse continue; - const glyph_sprite = glyph_layout.sprite orelse continue; - - drawSprite( - glyph_sprite, - glyph_layout.rect.pos.add(pos), - glyph_layout.rect.size, - opts.color - ); - } -} - pub const TextBuilder = struct { gpa: std.mem.Allocator, glyphs: std.ArrayList(Glyph), color: Color, baseline: f32, - layout: ?TextRunLayout, + layout: ?Font.TextRunLayout, bounds: Rect, @@ -1473,8 +1235,8 @@ pub const TextBuilder = struct { }; } - pub fn font(self: *TextBuilder, id: Font.Id, height: f32) void { - var new_layout = TextRunLayout.init(id, height); + pub fn font(self: *TextBuilder, new_font: *Font, height: f32) void { + var new_layout = Font.TextRunLayout.init(new_font, height); if (self.layout) |layout| { if (new_layout.font == layout.font and new_layout.scale_x == layout.scale_x and @@ -1484,7 +1246,7 @@ pub const TextBuilder = struct { } } - const font_baseline = getFontAscent(id, height); + const font_baseline = new_font.getAscent(height); if (font_baseline > self.baseline) { // Fixup previously drawn glyph to match the new larger baseline const offset_y = font_baseline - self.baseline; @@ -1511,7 +1273,7 @@ pub const TextBuilder = struct { } const layout = &self.layout.?; - const glyph_index = getGlyphIndex(layout.font, codepoint) orelse return; + const glyph_index = layout.font.getGlyphIndex(codepoint) orelse return; const glyph_layout = layout.next(glyph_index) orelse return; var rect = glyph_layout.rect; @@ -1760,454 +1522,6 @@ pub const Spritesheet = struct { pub const Id = SlotMap.Id; }; -pub const Font = struct { - spritesheet: Spritesheet.Id, - stb: ?STBTrueType.Font, - - cache: GlyphCache, - - const max_fonts = 16; - const SlotMap = SlotMapType(std.math.IntFittingRange(0, max_fonts-1), u8, Font); - pub const Id = SlotMap.Id; - - const GlyphCache = struct { - glyphs: [GlyphSlot.limit]GlyphSlot, - glyphs_hashmap: [GlyphSlot.limit*2]GlyphSlot.Id, - lru_head: GlyphSlot.Id, - lru_tail: GlyphSlot.Id, - free_head: GlyphSlot.Id, - - fn init() GlyphCache { - var self: GlyphCache = .{ - .glyphs = undefined, - .glyphs_hashmap = undefined, - .lru_head = GlyphSlot.nil_id, - .lru_tail = GlyphSlot.nil_id, - .free_head = undefined - }; - @memset(&self.glyphs_hashmap, GlyphSlot.nil_id); - @memset(&self.glyphs, .{ - .glyph = undefined, - .lru_prev = GlyphSlot.nil_id, - .lru_next = GlyphSlot.nil_id, - .next_free = GlyphSlot.nil_id, - }); - - assert(self.glyphs_hashmap.len > self.glyphs.len); - - self.free_head = 1; - for (1.., self.glyphs[1..(self.glyphs.len-1)]) |i, *glyph| { - glyph.next_free = @intCast(i + 1); - } - - return self; - } - - fn deinit(self: *GlyphCache) void { - for (self.glyphs_hashmap) |glyph_id| { - if (glyph_id == GlyphSlot.nil_id) { - continue; - } - const slot = self.glyphs[glyph_id]; - slot.glyph.deinit(); - } - self.* = undefined; - } - - fn get(self: *GlyphCache, id: GlyphSlot.Id) *Glyph { - return &self.glyphs[id].glyph; - } - - fn lookup(self: *GlyphCache, key: Glyph.Key) ?GlyphSlot.Id { - const home_bucket = key.hash() % self.glyphs_hashmap.len; - for (0..self.glyphs_hashmap.len) |offset| { - const i = (home_bucket + offset) % self.glyphs_hashmap.len; - const id = self.glyphs_hashmap[i]; - if (id == GlyphSlot.nil_id) { - continue; - } - if (self.glyphs[id].glyph.key == key) { - self.removeFromLRU(id); - self.appendToLRUHead(id); - return id; - } - } - - return null; - } - - fn appendToLRUHead(self: *GlyphCache, id: GlyphSlot.Id) void { - if (self.lru_head != GlyphSlot.nil_id) { - self.glyphs[self.lru_head].lru_prev = id; - self.glyphs[id].lru_next = self.lru_head; - } else { - assert(self.lru_tail == GlyphSlot.nil_id); - self.lru_tail = id; - } - - self.glyphs[id].lru_prev = GlyphSlot.nil_id; - self.lru_head = id; - } - - fn removeFromLRU(self: *GlyphCache, id: GlyphSlot.Id) void { - assert(id != GlyphSlot.nil_id); - - const lru_prev = self.glyphs[id].lru_prev; - const lru_next = self.glyphs[id].lru_next; - - if (lru_prev != GlyphSlot.nil_id) { - self.glyphs[lru_prev].lru_next = lru_next; - } - - if (lru_next != GlyphSlot.nil_id) { - self.glyphs[lru_next].lru_prev = lru_prev; - } - - if (id == self.lru_head) { - self.lru_head = lru_next; - } - if (id == self.lru_tail) { - self.lru_tail = lru_prev; - } - } - - fn popFreeId(self: *GlyphCache) ?GlyphSlot.Id { - if (self.free_head != GlyphSlot.nil_id) { - const result = self.free_head; - self.free_head = self.glyphs[result].next_free; - return result; - } - return null; - } - - fn pushFreeId(self: *GlyphCache, id: GlyphSlot.Id) void { - assert(id != GlyphSlot.nil_id); - self.glyphs[id].next_free = self.free_head; - self.free_head = id; - } - - fn insert(self: *GlyphCache, key: Glyph.Key) ?GlyphSlot.Id { - const key_hash = key.hash(); - - for (0..self.glyphs_hashmap.len) |offset| { - const i = (key_hash + offset) % self.glyphs_hashmap.len; - const hashmap_id = self.glyphs_hashmap[i]; - if (hashmap_id == GlyphSlot.nil_id) { - const id = self.popFreeId() orelse return null; - self.glyphs_hashmap[i] = id; - - self.glyphs[id].glyph = .init(key); - self.glyphs[id].lru_next = GlyphSlot.nil_id; - self.glyphs[id].lru_prev = GlyphSlot.nil_id; - self.appendToLRUHead(id); - return id; - } else { - const glyph = &self.glyphs[hashmap_id].glyph; - if (glyph.key == key) { - // An entry for this key already exists - self.removeFromLRU(hashmap_id); - self.appendToLRUHead(hashmap_id); - return hashmap_id; - } - } - } - - // This can't be reached because the hasmap is guarenteed to have enough slots to store all of the glyph ids - unreachable; - } - - fn removeFromHashmap(self: *GlyphCache, id: GlyphSlot.Id) void { - var bucket = self.glyphs[id].glyph.key.hash() % self.glyphs_hashmap.len; - for (0..self.glyphs_hashmap.len) |_| { - assert(self.glyphs_hashmap[bucket] != GlyphSlot.nil_id); - if (self.glyphs_hashmap[bucket] == id) { - break; - } - - bucket = (bucket + 1) % self.glyphs_hashmap.len; - } - - assert(self.glyphs_hashmap[bucket] == id); - - var empty = bucket; - var current = (bucket + 1) % self.glyphs_hashmap.len; - - while (self.glyphs_hashmap[current] != GlyphSlot.nil_id) { - const current_glyph_id = self.glyphs_hashmap[current]; - const home = self.glyphs[current_glyph_id].glyph.key.hash() % self.glyphs_hashmap.len; - - if (self.hashmapDistance(home, current) > self.hashmapDistance(home, empty)) { - self.glyphs_hashmap[empty] = self.glyphs_hashmap[current]; - empty = current; - } - - current = (current + 1) % self.glyphs_hashmap.len; - } - - assert(self.glyphs_hashmap[empty] != GlyphSlot.nil_id); - self.glyphs_hashmap[empty] = GlyphSlot.nil_id; - } - - fn hashmapDistance(self: GlyphCache, from: usize, to: usize) usize { - if (to >= from) { - return to - from; - } else { - return self.glyphs_hashmap.len - from + to; - } - } - - fn remove(self: *GlyphCache, id: GlyphSlot.Id) void { - assert(id != GlyphSlot.nil_id); - self.pushFreeId(id); - self.removeFromHashmap(id); - self.removeFromLRU(id); - - const glyph = self.glyphs[id].glyph; - glyph.deinit(); - } - - fn getLRUItems(self: *GlyphCache, gpa: Allocator) ![]GlyphSlot.Id { - var lru: std.ArrayList(GlyphSlot.Id) = .empty; - defer lru.deinit(gpa); - - var lru_iter = self.lru_head; - while (lru_iter != GlyphSlot.nil_id) { - try lru.append(gpa, lru_iter); - lru_iter = self.glyphs[lru_iter].lru_next; - } - - return try lru.toOwnedSlice(gpa); - } - - test { - var cache = GlyphCache.init(); - const key = Glyph.Key{ .index = 10, .scale_x = 1, .scale_y = 1 }; - const id1 = cache.insert(key).?; - const id2 = cache.insert(key).?; - try std.testing.expectEqual(id1, id2); - } - - test { - var cache = GlyphCache.init(); - const key = Glyph.Key{ .index = 10, .scale_x = 1, .scale_y = 1 }; - const id1 = cache.insert(key).?; - cache.remove(id1); - const id2 = cache.insert(key).?; - try std.testing.expectEqual(id1, id2); - } - - test { - var cache = GlyphCache.init(); - const id1 = cache.insert(Glyph.Key{ .index = 10, .scale_x = 1, .scale_y = 1 }).?; - const id2 = cache.insert(Glyph.Key{ .index = 11, .scale_x = 1, .scale_y = 1 }).?; - try std.testing.expect(id1 != id2); - } - - test { - var cache = GlyphCache.init(); - for (0..(GlyphSlot.limit-1)) |i| { - const id = cache.insert(Glyph.Key{ .index = @intCast(i), .scale_x = 1, .scale_y = 1 }); - try std.testing.expect(id != null); - } - const id = cache.insert(Glyph.Key{ .index = GlyphSlot.limit, .scale_x = 1, .scale_y = 1 }); - try std.testing.expect(id == null); - } - - test { - const gpa = std.testing.allocator; - var cache = GlyphCache.init(); - for (0..10) |_| { - var ids: std.ArrayList(GlyphSlot.Id) = .empty; - defer ids.deinit(gpa); - - for (0..(GlyphSlot.limit-1)) |i| { - const id = cache.insert(Glyph.Key{ .index = @intCast(i), .scale_x = 1, .scale_y = 1 }); - try std.testing.expect(id != null); - try ids.append(gpa, id.?); - } - assert(cache.free_head == GlyphSlot.nil_id); - - const lru = try cache.getLRUItems(gpa); - defer gpa.free(lru); - - const expected_lru = try gpa.dupe(GlyphSlot.Id, ids.items); - defer gpa.free(expected_lru); - std.mem.reverse(GlyphSlot.Id, expected_lru); - - try std.testing.expectEqualSlices(GlyphSlot.Id, expected_lru, lru); - - for (ids.items) |id| { - cache.remove(id); - } - assert(cache.lru_head == GlyphSlot.nil_id); - assert(cache.lru_tail == GlyphSlot.nil_id); - } - } - - test { - var cache = GlyphCache.init(); - const id1 = cache.insert(.{ .index = 0, .scale_x = 1, .scale_y = 1 }).?; - const id2 = cache.insert(.{ .index = 1, .scale_x = 1, .scale_y = 1 }).?; - const id3 = cache.insert(.{ .index = 2, .scale_x = 1, .scale_y = 1 }).?; - - const gpa = std.testing.allocator; - const lru = try cache.getLRUItems(gpa); - defer gpa.free(lru); - - try std.testing.expectEqualSlices(GlyphSlot.Id, &.{ id3, id2, id1 }, lru); - } - - test { - var cache = GlyphCache.init(); - const id1 = cache.insert(.{ .index = 0, .scale_x = 1, .scale_y = 1 }).?; - const id2 = cache.insert(.{ .index = 1, .scale_x = 1, .scale_y = 1 }).?; - const id3 = cache.insert(.{ .index = 2, .scale_x = 1, .scale_y = 1 }).?; - - const gpa = std.testing.allocator; - const lru = try cache.getLRUItems(gpa); - defer gpa.free(lru); - - try std.testing.expectEqualSlices(GlyphSlot.Id, &.{ id3, id2, id1 }, lru); - } - - test { - var cache = GlyphCache.init(); - const id = cache.insert(.{ .index = 0, .scale_x = 1, .scale_y = 1 }).?; - try std.testing.expect(cache.lru_head != GlyphSlot.nil_id); - try std.testing.expect(cache.lru_tail != GlyphSlot.nil_id); - cache.remove(id); - try std.testing.expect(cache.lru_head == GlyphSlot.nil_id); - try std.testing.expect(cache.lru_tail == GlyphSlot.nil_id); - } - - test { - const gpa = std.testing.allocator; - var cache = GlyphCache.init(); - const key1 = Glyph.Key{ .index = 0, .scale_x = 1, .scale_y = 1 }; - const id1 = cache.insert(key1).?; - const key2 = Glyph.Key{ .index = 1, .scale_x = 1, .scale_y = 1 }; - const id2 = cache.insert(key2).?; - const key3 = Glyph.Key{ .index = 2, .scale_x = 1, .scale_y = 1 }; - const id3 = cache.insert(key3).?; - - _ = cache.lookup(key2); - - { - const lru = try cache.getLRUItems(gpa); - defer gpa.free(lru); - try std.testing.expectEqualSlices(GlyphSlot.Id, &.{ id2, id3, id1 }, lru); - } - - _ = cache.lookup(key1); - - { - const lru = try cache.getLRUItems(gpa); - defer gpa.free(lru); - try std.testing.expectEqualSlices(GlyphSlot.Id, &.{ id1, id2, id3 }, lru); - } - } - - test { - const gpa = std.testing.allocator; - var cache = GlyphCache.init(); - const id1 = cache.insert(.{ .index = 0, .scale_x = 1, .scale_y = 1 }).?; - const id2 = cache.insert(.{ .index = 1, .scale_x = 1, .scale_y = 1 }).?; - const id3 = cache.insert(.{ .index = 2, .scale_x = 1, .scale_y = 1 }).?; - - { - const lru = try cache.getLRUItems(gpa); - defer gpa.free(lru); - try std.testing.expectEqualSlices(GlyphSlot.Id, &.{ id3, id2, id1 }, lru); - } - - _ = cache.remove(id2); - - { - const lru = try cache.getLRUItems(gpa); - defer gpa.free(lru); - try std.testing.expectEqualSlices(GlyphSlot.Id, &.{ id3, id1 }, lru); - } - - _ = cache.remove(id3); - - { - const lru = try cache.getLRUItems(gpa); - defer gpa.free(lru); - try std.testing.expectEqualSlices(GlyphSlot.Id, &.{ id1 }, lru); - } - - _ = cache.remove(id1); - - { - const lru = try cache.getLRUItems(gpa); - defer gpa.free(lru); - try std.testing.expectEqualSlices(GlyphSlot.Id, &.{ }, lru); - } - } - }; - - const GlyphSlot = struct { - glyph: Glyph, - - lru_next: GlyphSlot.Id, - lru_prev: GlyphSlot.Id, - - next_free: GlyphSlot.Id, - - const limit = 256; - const Id = std.math.IntFittingRange(0, limit-1); - const nil_id: GlyphSlot.Id = 0; - }; - - const Glyph = struct { - key: Key, - - box: STBTrueType.Box, - sprite: ?Sprite.Id, - used_this_frame: bool, - - const max_width: u32 = 512; - const max_height: u32 = 512; - - const Index = u32; - - const Key = packed struct { - // TODO: Quantize `scale_x` and `scale_y`. - index: u32, - scale_x: f32, - scale_y: f32, - - pub fn hash(self: Key) u64 { - var hasher = std.hash.Wyhash.init(0); - hasher.update(std.mem.asBytes(&self)); - return hasher.final(); - } - }; - - fn init(key: Key) Glyph { - return Glyph{ - .key = key, - .box = .{ - .x0 = 0, - .y0 = 0, - .x1 = 0, - .y1 = 0, - }, - .sprite = .nil, - .used_this_frame = false - }; - } - - fn deinit(self: Glyph) void { - if (self.sprite) |sprite| { - deinitSprite(sprite); - } - } - }; -}; - -const GlyphIndex = Font.Glyph.Index; - pub const Vertex = extern struct { position: Vec2, texcoord: Vec2, diff --git a/src/platform/slot_map.zig b/src/platform/slot_map.zig index 1b3c36e..6f7865f 100644 --- a/src/platform/slot_map.zig +++ b/src/platform/slot_map.zig @@ -87,7 +87,11 @@ pub fn SlotMapType(Index: type, Generation: type, Value: type) type { }; pub fn clearRetainingCapacity(self: *Self) void { - self.* = .initBuffer(self.slots.items); + var slots = self.slots; + slots.clearRetainingCapacity(); + + self.* = .empty; + self.slots = slots; } fn insertHole(self: *Self, index: Index) void { @@ -245,16 +249,16 @@ test "insert & remove" { const expect = std.testing.expect; const gpa = std.testing.allocator; - var map: TestMap = .empty; + var map: TestMap = try .initCapacity(gpa, 32); defer map.deinit(gpa); - const id1 = try map.insertUndefined(gpa); + const id1 = try map.insertUndefined(); try expect(map.exists(id1)); try expect(map.remove(id1)); try expect(!map.exists(id1)); try expect(!map.remove(id1)); - const id2 = try map.insertUndefined(gpa); + const id2 = try map.insertUndefined(); try expect(map.exists(id2)); try expect(!map.exists(id1)); } @@ -263,18 +267,18 @@ test "generation wrap around" { const expectEqual = std.testing.expectEqual; const gpa = std.testing.allocator; - var map: TestMap = .empty; + var map: TestMap = try .initCapacity(gpa, 32); defer map.deinit(gpa); // Grow array list so that at least 1 slot exists - const id1 = try map.insertUndefined(gpa); + const id1 = try map.insertUndefined(); map.removeAssumeExists(id1); // Artificially increase generation count map.slots.items[id1.index].generation = std.math.maxInt(@FieldType(TestMap.Id, "generation")); // Check if generation wraps around - const id2 = try map.insertUndefined(gpa); + const id2 = try map.insertUndefined(); map.removeAssumeExists(id2); try expectEqual(id1.index, id2.index); try expectEqual(0, map.slots.items[id1.index].generation); @@ -284,13 +288,13 @@ test "iterator" { const expectEqual = std.testing.expectEqual; const gpa = std.testing.allocator; - var map: TestMap = .empty; + var map: TestMap = try .initCapacity(gpa, 32); defer map.deinit(gpa); // Create array which has a hole - const id1 = try map.insertUndefined(gpa); - const id2 = try map.insertUndefined(gpa); - const id3 = try map.insertUndefined(gpa); + const id1 = try map.insertUndefined(); + const id2 = try map.insertUndefined(); + const id3 = try map.insertUndefined(); map.removeAssumeExists(id2); @@ -305,14 +309,14 @@ test "clear retaining capacity" { const expectEqual = std.testing.expectEqual; const gpa = std.testing.allocator; - var map: TestMap = .empty; + var map: TestMap = try .initCapacity(gpa, 32); defer map.deinit(gpa); - const id1 = try map.insertUndefined(gpa); + const id1 = try map.insertUndefined(); try expect(map.exists(id1)); map.clearRetainingCapacity(); - const id2 = try map.insertUndefined(gpa); + const id2 = try map.insertUndefined(); try expect(map.exists(id2)); try expectEqual(id1, id2);