Begin transition to dynamic glyph cache
This commit is contained in:
parent
6cbd201b63
commit
b532ccd68f
317
src/glyphcache/glca.h
Normal file
317
src/glyphcache/glca.h
Normal 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
|
||||
Loading…
Reference in New Issue
Block a user