From b532ccd68f2b31bc476606bf2fffdab438b8a696 Mon Sep 17 00:00:00 2001 From: Mid <> Date: Mon, 24 Nov 2025 11:30:14 +0200 Subject: [PATCH] Begin transition to dynamic glyph cache --- src/glyphcache/glca.h | 317 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 317 insertions(+) create mode 100644 src/glyphcache/glca.h diff --git a/src/glyphcache/glca.h b/src/glyphcache/glca.h new file mode 100644 index 0000000..5591f33 --- /dev/null +++ b/src/glyphcache/glca.h @@ -0,0 +1,317 @@ +/* + * This is a mostly standalone file that implements an SDF glyph cache. + * It can support any graphics backend as long as it can modify and load subtextures. + * + * It is SDF-only since this way it is less critical to support different font sizes. + * + * Example initialization: + * FT_Library ftlib; + * FT_Init_FreeType(&ftlib); + * + * FT_Property_Set(ftlib, "sdf", "spread", &(int) {24}); + * + * FT_Face face; + * assert(FT_New_Face(ftlib, "my font file.ttf", 0, &face) == 0); + * FT_Select_Charmap(face, FT_ENCODING_UNICODE); + * FT_Set_Pixel_Sizes(face, 64, 0); + * + * void *userdata = NULL; + * + * GlyphCache gc = {}; + * glca_init(&gc, face, 2048, 2048, userdata, my_set_image, my_get_image, my_fill_custom_data); + * + * my_fill_custom_data can be used in case you need glyph metrics. + * If GLCA_CUSTOM_GLYPH_DATA is not defined, my_fill_custom_data is unused. + * + * Example usage: + * // Should be called regularly (recommended once per frame) + * glca_set_time(&gc, get_time_now_somehow()); + * ... + * // When you need to render text: + * GlyphCacheGlyph *glyph = glca_request(&gc, 'Ь'); + * if(!glyph) { + * // Skip + * } + * + * Safety padding is added (in height) to each new row. It is by default + * zero but it is a good idea to experiment with other values. + * + * WARNING: GlyphCache relies on a few heuristics that assume the following: + * 1. Glyphs are roughly equal in size (true for text) + * 2. The atlas is reasonably larger in area than a single glyph (at least ~100x) + * If either does not hold, GlyphCache will perform AWFULLY. + */ + +#ifndef GLCA_H +#define GLCA_H + +#include +#include +#include +#include +#include +#include + +#ifndef GLCA_CUSTOM_GLYPH_DATA +typedef struct GlyphCacheGlyphData {} GlyphCacheGlyphData; +#endif + +typedef struct GlyphCacheGlyph { + uint32_t codepoint; + + uint16_t x; + uint16_t y; + uint16_t w; + uint16_t h; + + GlyphCacheGlyphData data; + + uint64_t last_use; +} GlyphCacheGlyph; + +typedef struct GlyphCacheRow { + size_t y; + size_t width; + size_t height; + + size_t entry_count; + uint32_t *entries; +} GlyphCacheRow; + +struct GlyphCache; +typedef void(*GlyphCacheSetImage)(struct GlyphCache *gc, int x, int y, int w, int h, const void *buf); +typedef void(*GlyphCacheGetImage)(struct GlyphCache *gc, int x, int y, int w, int h, void *buf); +typedef void(*GlyphCacheFillCustomData)(struct GlyphCache *gc, FT_GlyphSlot, GlyphCacheGlyph*); + +typedef struct GlyphCache { + FT_Face face; + uint64_t current_time; + int safety_padding; + + size_t total_width; + size_t total_height; + + size_t row_count; + GlyphCacheRow *rows; + + size_t item_count; + GlyphCacheGlyph *items; + + void *userdata; + GlyphCacheSetImage set_image; + GlyphCacheGetImage get_image; + GlyphCacheFillCustomData fill_custom_data; +} GlyphCache; + +int glca_init(GlyphCache*, FT_Face, size_t w, size_t h, void *userdata, GlyphCacheSetImage, GlyphCacheGetImage, GlyphCacheFillCustomData); + +GlyphCacheGlyph *glca_get_noupdate(GlyphCache *gc, uint32_t codepoint); +GlyphCacheGlyph *glca_get(GlyphCache *gc, uint32_t codepoint); +GlyphCacheGlyph *glca_request(GlyphCache *gc, uint32_t codepoint); +void glca_try_evict(GlyphCache *gc); + +void glca_set_safety_padding(GlyphCache *gc, int safety_padding); + +void glca_set_time(GlyphCache *gc, uint64_t time); + +#endif + +#ifdef GLCA_IMPLEMENTATION +int glca_init(GlyphCache *gc, FT_Face face, size_t total_width, size_t total_height, void *userdata, GlyphCacheSetImage set_image, GlyphCacheGetImage get_image, GlyphCacheFillCustomData fill_custom_data) { + memset(gc, 0, sizeof(*gc)); + gc->face = face; + gc->total_width = total_width; + gc->total_height = total_height; + gc->userdata = userdata; + gc->set_image = set_image; + gc->get_image = get_image; + gc->fill_custom_data = fill_custom_data; + return 0; +} + +// Returns row number for new glyph. If == -1, impossible. +static int choose_row(GlyphCache *gc, size_t w, size_t h) { + size_t y = 0; + + if(gc->row_count == 0) { + goto make_new; + } + + bool found = false; + int bestRow = -1; + int bestRowHDiff = 10000000; + for(int row = 0; row < gc->row_count; row++) { + y += gc->rows[row].height; + + if(gc->total_width - gc->rows[row].width < w) { + // No width remains in row. + continue; + } + + if(gc->rows[row].height < h) { + // Row too short. + continue; + } + + int diff = gc->rows[row].height - h; + + if(bestRowHDiff > diff) { + bestRowHDiff = diff; + bestRow = row; + found = true; + } + } + + if(found) { + return bestRow; + } + +make_new: + if(y + h > gc->total_height) { + return -1; + } + + h += gc->safety_padding; + if(y + h > gc->total_height) { + h = gc->total_height - y; + } + + int row = gc->row_count; + gc->rows = realloc(gc->rows, sizeof(*gc->rows) * (++gc->row_count)); + memset(&gc->rows[row], 0, sizeof(*gc->rows)); + gc->rows[row].y = y; + gc->rows[row].height = h; + + return row; +} + +static int comparator(const void *A, const void *B) { + uint32_t cpa = *(uint32_t*) A; + uint32_t cpb = *(uint32_t*) B; + + if(cpa == cpb) { + return 0; + } + + return cpa > cpb ? 1 : -1; +} +GlyphCacheGlyph *glca_get_noupdate(GlyphCache *gc, uint32_t codepoint) { + return bsearch(&codepoint, gc->items, gc->item_count, sizeof(*gc->items), comparator); +} +GlyphCacheGlyph *glca_get(GlyphCache *gc, uint32_t codepoint) { + GlyphCacheGlyph *glyph = glca_get_noupdate(gc, codepoint); + if(glyph) { + glyph->last_use = gc->current_time; + } + return glyph; +} + +void glca_try_evict(GlyphCache *gc) { + uint64_t min_time = gc->current_time, max_time = 0; + for(size_t i = 0; i < gc->item_count; i++) { + if(min_time > gc->items[i].last_use) { + min_time = gc->items[i].last_use; + } + if(max_time < gc->items[i].last_use) { + max_time = gc->items[i].last_use; + } + } + + uint64_t threshold = max_time / 2 + min_time / 2; + + for(size_t r = 0; r < gc->row_count; r++) { + GlyphCacheRow *gcr = &gc->rows[r]; + + size_t x_shift = 0; + for(size_t i = 0; i < gcr->entry_count;) { + GlyphCacheGlyph *glyph = glca_get_noupdate(gc, gcr->entries[i]); + + if(x_shift) { + void *buf = malloc(glyph->w * glyph->h); + gc->get_image(gc, glyph->x, glyph->y, glyph->w, glyph->h, buf); + glyph->x -= x_shift; + gc->set_image(gc, glyph->x, glyph->y, glyph->w, glyph->h, buf); + free(buf); + } + + if(glyph->last_use >= threshold) { + // Do not evict. + i++; + continue; + } + + x_shift += glyph->w; + + memmove(gcr->entries + i, gcr->entries + i + 1, sizeof(*gcr->entries) * (gcr->entry_count - i - 1)); + gcr->width -= glyph->w; + gcr->entry_count--; + + size_t glyph_index = glyph - gc->items; + memmove(glyph, glyph + 1, sizeof(*glyph) * (gc->item_count - glyph_index - 1)); + gc->item_count--; + glyph = NULL; + } + } +} +GlyphCacheGlyph *glca_request(GlyphCache *gc, uint32_t codepoint) { + GlyphCacheGlyph *glyph = glca_get(gc, codepoint); + if(glyph) { + return glyph; + } + + assert(FT_Load_Char(gc->face, codepoint, 0) == 0); + assert(FT_Render_Glyph(gc->face->glyph, FT_RENDER_MODE_NORMAL) == 0); + assert(FT_Render_Glyph(gc->face->glyph, FT_RENDER_MODE_SDF) == 0); + + size_t w = gc->face->glyph->bitmap.width, h = gc->face->glyph->bitmap.rows; + + int row = choose_row(gc, w, h); + if(row == -1) { + glca_try_evict(gc); + row = choose_row(gc, w, h); + if(row == -1) { + return NULL; + } + } + + GlyphCacheRow *gcr = &gc->rows[row]; + + size_t x = gcr->width; + size_t y = gcr->y; + gcr->width += w; + gcr->entries = realloc(gcr->entries, sizeof(*gcr->entries) * (gcr->entry_count + 1)); + gcr->entries[gcr->entry_count++] = codepoint; + + gc->items = realloc(gc->items, sizeof(*gc->items) * (gc->item_count + 1)); + gc->items[gc->item_count++] = (GlyphCacheGlyph) { + .codepoint = codepoint, + .x = x, + .y = y, + .w = w, + .h = h, + }; +#ifdef GLCA_CUSTOM_GLYPH_DATA + if(gc->fill_custom_data) { + gc->fill_custom_data(gc, gc->face->glyph, &gc->items[gc->item_count - 1]); + } +#endif + qsort(gc->items, gc->item_count, sizeof(*gc->items), comparator); + + uint8_t *buf = malloc(w * h); + for(int y = 0; y < h; y++) { + memcpy(buf + y * w, gc->face->glyph->bitmap.buffer + y * gc->face->glyph->bitmap.pitch, w); + } + gc->set_image(gc, x, y, w, h, buf); + free(buf); + + return glca_get(gc, codepoint); +} + +void glca_set_safety_padding(GlyphCache *gc, int safety_padding) { + gc->safety_padding = safety_padding; +} + +void glca_set_time(GlyphCache *gc, uint64_t time) { + gc->current_time = time; +} +#endif