#include "text_draw.h" #include "../lib/color.h" #include "../lib/math.h" #include "../lib/geometry.h" #include "../lib/text.h" #include "../lib/ds.h" #include "../platform.h" #include "stb_truetype.h" #include "../debug/logger.h" #include "../assets.h" #include "../enginestate.h" #include #include struct gui_glyph_info { utf8_codepoint codepoint; Rect box; // .position = offset from position for alignment, .size = size of the bitmap to draw v2u position; // Position of the top left border in the texture s32 advance; u32 next; // Anything >= container_capacity is to be considered NULL u32 previous; }; /* Glyph caching: * We store a small number of sizes and a fairly large number of characters for each size. * Each slot will have the same width and height, so that we can easily replace it without * re-packing everything. This will also make the code simpler. * We use 0xFFFFFFFF as a placeholder for empty slots. * */ struct gui_glyph_codepoint_map { utf8_codepoint codepoint; u32 index; }; struct gui_glyph_texture { f32 font_size; struct gui_glyph_codepoint_map *sorted_indices; gui_glyph_info *info; u32 oldest; u32 newest; u32 capacity; v2s glyph_max_size; r_texture *texture; }; struct gui_glyph_cache { gui_glyph_texture *glyphs; u32 capacity; u32 oldest; u32 max_glyphs_per_texture; }; static void gui_glyph_cache_init(); static void gui_glyph_cache_deinit(); static gui_glyph_texture gui_glyph_texture_create(f32 font_size, u32 capacity); static void gui_glyph_texture_destroy(gui_glyph_texture *glyphs); static gui_glyph_texture *gui_glyph_cache_texture_for_codepoints(f32 font_size, const utf8_codepoint *codepoints, u64 count); // Globals static u8 *Font_File; static stbtt_fontinfo Font_Info; static gui_glyph_cache Glyph_Cache; bool gui_text_draw_init() { // @Feature: user specified font // @Cleanup: one-line file read p_file f; p_file_init(&f, "assets/fonts/DejaVuSerif-Bold.ttf"); Buffer buf; buf.size = p_file_size(&f); buf.data = (u8*)p_alloc(buf.size + 1); buf.data[buf.size] = '\0'; p_file_read(&f, &buf, buf.size); p_file_deinit(&f); Font_File = buf.data; if(!stbtt_InitFont(&Font_Info, Font_File, 0)) { LOG(LOG_ERROR, "Cannot load font."); return false; } gui_glyph_cache_init(); return true; } void gui_text_draw_deinit() { gui_glyph_cache_deinit(); p_free(Font_File); } v2 gui_text_draw_size(const char *text, f32 font_size, v2 *cursor_position) { // UTF8 conversion u64 text_length = utf8_codepoint_count(text); utf8_codepoint *codepoints = (utf8_codepoint*) p_alloc(text_length * sizeof(utf8_codepoint)); { u64 bytes_read = 0; text_length = utf8_from_string(text, &bytes_read, codepoints, text_length); } // Compute size v2 result = gui_utf8_text_draw_size(codepoints, text_length, font_size, cursor_position); p_free(codepoints); return result; } void gui_text_draw(Rect r, const char *text, f32 font_size, v4 color) { // UTF8 conversion u64 text_length = utf8_codepoint_count(text); utf8_codepoint *codepoints = (utf8_codepoint*) p_alloc(text_length * sizeof(utf8_codepoint)); { u64 bytes_read = 0; text_length = utf8_from_string(text, &bytes_read, codepoints, text_length); } // Draw gui_utf8_text_draw(r, codepoints, text_length, font_size, color); p_free(codepoints); } v2 gui_utf8_text_draw_size(const utf8_codepoint *text, u64 length, f32 font_size, v2 *cursor_position) { f32 font_scale; s32 font_ascent; s32 font_descent; s32 font_line_gap; s32 font_baseline; { font_scale = stbtt_ScaleForPixelHeight(&Font_Info, font_size); stbtt_GetFontVMetrics(&Font_Info, &font_ascent, &font_descent, &font_line_gap); font_baseline = font_ascent; font_ascent *= font_scale; font_descent *= font_scale; font_line_gap *= font_scale; font_baseline *= font_scale; } // Compute size v2 size = {0, (f32)(font_ascent - font_descent)}; { v2 cursor = {0, (f32)(font_ascent - font_descent)}; for(u64 i = 0; i < length; i++) { s32 advance, lsb; stbtt_GetCodepointHMetrics(&Font_Info, text[i], &advance, &lsb); // Special characters if(text[i] == 10) // '\n' { advance = 0; cursor.x = 0; cursor.y += font_ascent - font_descent + font_line_gap; size.y = cursor.y; } // Normal characters cursor.x += floor(advance * font_scale); // Remember to consider kerning size.x = maximum(size.x, cursor.x); } if(cursor_position) *cursor_position = cursor; } return size; } void gui_utf8_text_draw(Rect r, const utf8_codepoint *text, u64 length, f32 font_size, v4 color) { f32 font_scale; s32 font_ascent; s32 font_descent; s32 font_line_gap; s32 font_baseline; { font_scale = stbtt_ScaleForPixelHeight(&Font_Info, font_size); stbtt_GetFontVMetrics(&Font_Info, &font_ascent, &font_descent, &font_line_gap); font_baseline = font_ascent; font_ascent *= font_scale; font_descent *= font_scale; font_line_gap *= font_scale; font_baseline *= font_scale; } // Compute glyphs gui_glyph_texture *glyphs = gui_glyph_cache_texture_for_codepoints(font_size, text, length); // Map text to quads v2 *vertices; v2 *uvs; u64 draw_count = 0; { vertices = (v2*) p_alloc(6 * length * sizeof(v2)); // 2 triangles, 3 vertices each = 6 vertices uvs = (v2*) p_alloc(6 * length * sizeof(v2)); v2 position = r.position; position.y += font_baseline; for(u64 i = 0; i < length; i++) { gui_glyph_codepoint_map *found = (gui_glyph_codepoint_map*) bsearch(&text[i], glyphs->sorted_indices, glyphs->capacity, sizeof(gui_glyph_codepoint_map), u32_cmp); if(found == NULL) { LOG(LOG_ERROR, "Cannot find codepoint 0x%X in glyph list.", text[i]); continue; } u64 glyph_i = glyphs->sorted_indices[found - glyphs->sorted_indices].index; gui_glyph_info *info = &glyphs->info[glyph_i]; // Special characters if(text[i] == 10) // '\n' { position.x = r.position.x; position.y += font_ascent - font_descent + font_line_gap; } // Normal characters // Map character to its vertices { Rect r; Rect r_uv; r.position = position + info->box.position; r.position.x = floor(r.position.x + 0.5); r.position.y = floor(r.position.y + 0.5); r.size = info->box.size; r_uv.position = V2(info->position) / V2(glyphs->texture->size); r_uv.size = info->box.size / V2(glyphs->texture->size); v2 a = r.position + v2{r.w, 0 }; v2 b = r.position + v2{0 , 0 }; v2 c = r.position + v2{0 , r.h}; v2 d = r.position + v2{r.w, r.h}; v2 a_uv = r_uv.position + v2{r_uv.w, 0 }; v2 b_uv = r_uv.position + v2{0 , 0 }; v2 c_uv = r_uv.position + v2{0 , r_uv.h}; v2 d_uv = r_uv.position + v2{r_uv.w, r_uv.h}; vertices[6*draw_count + 0] = a; vertices[6*draw_count + 1] = b; vertices[6*draw_count + 2] = c; vertices[6*draw_count + 3] = a; vertices[6*draw_count + 4] = c; vertices[6*draw_count + 5] = d; uvs[6*draw_count + 0] = a_uv; uvs[6*draw_count + 1] = b_uv; uvs[6*draw_count + 2] = c_uv; uvs[6*draw_count + 3] = a_uv; uvs[6*draw_count + 4] = c_uv; uvs[6*draw_count + 5] = d_uv; } position.x += info->advance; // Remember to consider kerning draw_count++; } } // Render quads r_2d_immediate_mesh(6*draw_count, vertices, color, uvs, glyphs->texture); p_free(vertices); p_free(uvs); } static void gui_glyph_cache_init() { Glyph_Cache.capacity = 4; Glyph_Cache.oldest = 0; Glyph_Cache.glyphs = (gui_glyph_texture*)p_alloc(sizeof(gui_glyph_texture) * Glyph_Cache.capacity); memset(Glyph_Cache.glyphs, 0, sizeof(gui_glyph_texture) * Glyph_Cache.capacity); Glyph_Cache.max_glyphs_per_texture = 256; // @Correctness: test with small values, to trigger the glyph replacement code } static void gui_glyph_cache_deinit() { for(u32 i = 0; i < Glyph_Cache.capacity; i++) gui_glyph_texture_destroy(&Glyph_Cache.glyphs[i]); p_free(Glyph_Cache.glyphs); Glyph_Cache.glyphs = NULL; Glyph_Cache.capacity = 0; Glyph_Cache.oldest = 0; } static gui_glyph_texture gui_glyph_texture_create(f32 font_size, u32 capacity) { // Init container for glyphs and info gui_glyph_texture glyphs; glyphs.font_size = font_size; glyphs.sorted_indices = (gui_glyph_codepoint_map*) p_alloc(sizeof(gui_glyph_codepoint_map) * capacity); glyphs.info = (gui_glyph_info*) p_alloc(sizeof(gui_glyph_info) * capacity); glyphs.oldest = 0; glyphs.newest = capacity - 1; glyphs.capacity = capacity; // Estimate max glyph size. // @Correctness: Text draw will fail if a bigger glyph is used. f32 font_scale = stbtt_ScaleForPixelHeight(&Font_Info, font_size); utf8_codepoint cp[] = {' ', 'M', 'j', '{', '=', 'w', 'W'}; glyphs.glyph_max_size = v2s{0, 0}; for(s32 i = 0; i < sizeof(cp)/sizeof(utf8_codepoint); i++) { v2s top_left, bottom_right; stbtt_GetCodepointBitmapBox(&Font_Info, cp[i], font_scale, font_scale, &top_left.x, &top_left.y, &bottom_right.x, &bottom_right.y); v2s size = bottom_right - top_left; glyphs.glyph_max_size.x = maximum(glyphs.glyph_max_size.x, size.x); glyphs.glyph_max_size.y = maximum(glyphs.glyph_max_size.y, size.y); } LOG(LOG_DEBUG, "Font size %f not in cache. Slot size (%d %d)", font_size, glyphs.glyph_max_size.x, glyphs.glyph_max_size.y); // Precompile some info data for(u32 i = 0; i < glyphs.capacity; i++) { glyphs.info[i] = gui_glyph_info{ .codepoint = 0xFFFFFFFF, .box = {0,0,0,0}, .position = v2u{glyphs.glyph_max_size.x * i, 0}, .advance = 0, .next = i + 1, // Last .next will be >= capacity (== capacity to be precise), so we will consider it to be NULL .previous = ((i == 0) ? glyphs.capacity : (i - 1)) }; glyphs.sorted_indices[i] = gui_glyph_codepoint_map{0xFFFFFFFF, i}; } // Initialize texture v2s texture_size = v2s{glyphs.glyph_max_size.x * glyphs.capacity, glyphs.glyph_max_size.y}; u8 *texture_data = (u8*)p_alloc(sizeof(u8) * texture_size.x * texture_size.y); memset(texture_data, 0, sizeof(u8) * texture_size.x * texture_size.y); glyphs.texture = assets_new_textures(&engine.am, 1); *glyphs.texture = r_texture_create(texture_data, texture_size, R_TEXTURE_ALPHA | R_TEXTURE_NO_MIPMAP); return glyphs; } static void gui_glyph_texture_destroy(gui_glyph_texture *glyphs) { if(glyphs->sorted_indices) p_free(glyphs->sorted_indices); if(glyphs->info) p_free(glyphs->info); if(glyphs->texture) r_texture_destroy(glyphs->texture); glyphs->sorted_indices = NULL; glyphs->info = NULL; glyphs->texture = NULL; glyphs->font_size = 0; glyphs->oldest = 0; glyphs->newest = 0; glyphs->capacity = 0; } static gui_glyph_texture *gui_glyph_cache_texture_for_codepoints(f32 font_size, const utf8_codepoint *codepoints, u64 count) { // Approximate font_size. We don't really want to build different bitmaps for size 12.000000 and 12.000001. // This will also prevent floating point rounding errors from rebuilding the cache. font_size = floor(font_size * 10) / 10; // Find cached texture for this size or build a new one gui_glyph_texture *glyphs = NULL; for(u32 i = 0; i < Glyph_Cache.capacity; i++) { //LOG(LOG_DEBUG, "Font size: %f - Cached: %f", font_size, Glyph_Cache.glyphs[i].font_size); if(abs(Glyph_Cache.glyphs[i].font_size - font_size) < 0.01) { glyphs = &Glyph_Cache.glyphs[i]; } } if(glyphs == NULL) { //LOG(LOG_DEBUG, "Size not matched"); glyphs = &Glyph_Cache.glyphs[Glyph_Cache.oldest]; Glyph_Cache.oldest = (Glyph_Cache.oldest + 1) % Glyph_Cache.capacity; gui_glyph_texture_destroy(glyphs); *glyphs = gui_glyph_texture_create(font_size, Glyph_Cache.max_glyphs_per_texture); } // Build list of unique codepoints (so that we don't render the same codepoint twice) utf8_codepoint *unique = (utf8_codepoint*) p_alloc(count * sizeof(utf8_codepoint)); memcpy(unique, codepoints, count * sizeof(utf8_codepoint)); u64 unique_count = make_unique(unique, count, sizeof(utf8_codepoint), u32_cmp); if(unique_count > glyphs->capacity) LOG(LOG_ERROR, "Unique codepoints count > cache capacity. Some codepoints will not be rendered."); // Find which codepoints are not already in the cache and need to be rendered utf8_codepoint to_render[unique_count]; u32 to_render_count = 0; for(u32 i = 0; i < unique_count; i++) { gui_glyph_codepoint_map *found = (gui_glyph_codepoint_map*) bsearch(&unique[i], glyphs->sorted_indices, glyphs->capacity, sizeof(gui_glyph_codepoint_map), u32_cmp); if(found == NULL) { // Not found -> add to the list of glyphs to render to_render[to_render_count] = unique[i]; to_render_count++; } else { // Found -> mark it as recent, so that is does not get deleted prematurely u32 index = glyphs->sorted_indices[found - glyphs->sorted_indices].index; if(index == glyphs->newest) { // Already the newest. Do nothing. } else if(index == glyphs->oldest) { // We have no previous to fix, only next u32 next = glyphs->info[index].next; glyphs->info[next].previous = glyphs->capacity; // Next is the new oldest -> no previous glyphs->oldest = next; // Set index as last element glyphs->info[index].next = glyphs->capacity; glyphs->info[index].previous = glyphs->newest; glyphs->info[glyphs->newest].next = index; glyphs->newest = index; } else { // We in between the list. We have both previous and next elements to fix. u32 previous = glyphs->info[index].previous; u32 next = glyphs->info[index].next; glyphs->info[previous].next = next; glyphs->info[next].previous = previous; // Set index as last element glyphs->info[index].next = glyphs->capacity; glyphs->info[index].previous = glyphs->newest; glyphs->info[glyphs->newest].next = index; glyphs->newest = index; } } } p_free(unique); // Get info for rendering f32 font_scale; s32 font_ascent; s32 font_descent; s32 font_line_gap; s32 font_baseline; { font_scale = stbtt_ScaleForPixelHeight(&Font_Info, font_size); stbtt_GetFontVMetrics(&Font_Info, &font_ascent, &font_descent, &font_line_gap); font_baseline = font_ascent; font_ascent *= font_scale; font_descent *= font_scale; font_line_gap *= font_scale; font_baseline *= font_scale; } // Render glyph in its appropriate place for(u32 i = 0; i < to_render_count; i++) { u32 index = glyphs->oldest; glyphs->oldest = glyphs->info[index].next; glyphs->info[glyphs->oldest].previous = glyphs->capacity; glyphs->info[index].next = glyphs->capacity; glyphs->info[index].previous = glyphs->newest; glyphs->info[index].codepoint = to_render[i]; glyphs->info[glyphs->newest].next = index; glyphs->newest = index; // Complete gui_glyph_info structure and render gui_glyph_info *info = &glyphs->info[index]; v2s top_left, bottom_right; stbtt_GetCodepointBitmapBox(&Font_Info, info->codepoint, font_scale, font_scale, &top_left.x, &top_left.y, &bottom_right.x, &bottom_right.y); s32 advance, lsb; stbtt_GetCodepointHMetrics(&Font_Info, info->codepoint, &advance, &lsb); v2s size = bottom_right - top_left; // Special codepoints if(info->codepoint == 10) // '\n' { size = {0, 0}; advance = 0; } info->box.position = V2(top_left); info->box.size = V2(size); //info->position = v2u{glyphs->glyph_max_size.x * index, 0}; // Commented because it's already pre-computed. info->advance = advance * font_scale; // @Correctness: needs to be premultiplied alpha stbtt_MakeCodepointBitmap(&Font_Info, glyphs->texture->data + info->position.x, info->box.size.x, info->box.size.y, glyphs->texture->size.x, font_scale, font_scale, info->codepoint); r_texture_update(glyphs->texture, glyphs->texture->data + info->position.x, V2S(info->box.size), V2S(info->position), glyphs->texture->size.x); } // Build sorted array with indices that point to the element u32 nonempty_count = glyphs->capacity; for(u32 i = 0; i < glyphs->capacity; i++) { glyphs->sorted_indices[i] = gui_glyph_codepoint_map{glyphs->info[i].codepoint, i}; if(glyphs->info[i].codepoint == 0xFFFFFFFF) { // When the cache is mostly empty, this makes the sorting way faster. nonempty_count = i; break; } } qsort(glyphs->sorted_indices, nonempty_count, sizeof(gui_glyph_codepoint_map), u32_cmp); return glyphs; }