Begin transition to dynamic glyph cache

This commit is contained in:
Mid 2025-11-24 11:30:14 +02:00
parent 6cbd201b63
commit b532ccd68f

317
src/glyphcache/glca.h Normal file
View File

@ -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<freetype/freetype.h>
#include<unistd.h>
#include<assert.h>
#include<stdint.h>
#include<stddef.h>
#include<stdbool.h>
#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