From 025e19d88e70191e974eb06991f5c8fae8a10d5f Mon Sep 17 00:00:00 2001 From: Takase <20792268+takase1121@users.noreply.github.com> Date: Sat, 15 Mar 2025 22:43:18 +0800 Subject: [PATCH] renderer: defer bitmap rendering when possible (#1856) ren_font_group_get_width() is getting called a lot while not 100% of the glyphs loaded from this function will be actually rendered. We can defer the bitmap rendering to ren_draw_text. (cherry picked from commit 1659f63dfc6fec7893673e7e95bbcd844ec0bda4) --- src/renderer.c | 259 ++++++++++++++++++++++++++----------------------- 1 file changed, 139 insertions(+), 120 deletions(-) diff --git a/src/renderer.c b/src/renderer.c index 02250b2..159e767 100644 --- a/src/renderer.c +++ b/src/renderer.c @@ -35,10 +35,10 @@ static void* _check_alloc(void *ptr, const char *const file, size_t ln) { /************************* Fonts *************************/ // approximate number of glyphs per atlas surface -#define GLYPHS_PER_ATLAS 256 +#define GLYPHS_PER_ATLAS 96 // some padding to add to atlas surface to store more glyphs -#define FONT_HEIGHT_OVERFLOW_PX 6 -#define FONT_WIDTH_OVERFLOW_PX 6 +#define FONT_HEIGHT_OVERFLOW_PX 0 +#define FONT_WIDTH_OVERFLOW_PX 9 // maximum unicode codepoint supported (https://stackoverflow.com/a/52203901) #define MAX_UNICODE 0x10FFFF @@ -55,20 +55,28 @@ static void* _check_alloc(void *ptr, const char *const file, size_t ln) { // number of subpixel bitmaps #define SUBPIXEL_BITMAPS_CACHED 3 +// the bitmap format of the glyph typedef enum { - EGlyphNone = 0, - EGlyphLoaded = (1 << 0L), - EGlyphBitmap = (1 << 1L), - // currently no-op because blits are always assumed to be dual source - EGlyphDualSource = (1 << 2L), + EGlyphFormatGrayscale, // 8bit graysclae + EGlyphFormatSubpixel, // 24bit subpixel + EGlyphFormatSize +} ERenGlyphFormat; + +// extra flags to store glyph info +typedef enum { + EGlyphNone = 0, // glyph is not loaded + EGlyphXAdvance = (1 << 0L), // xadvance is loaded + EGlyphBitmap = (1 << 1L) // bitmap is loaded } ERenGlyphFlags; // metrics for a loaded glyph typedef struct { - unsigned short atlas_idx, surface_idx; - unsigned int x1, y0, y1, flags; - int bitmap_left, bitmap_top; float xadvance; + unsigned short atlas_idx, surface_idx; + int bitmap_left, bitmap_top; + unsigned int x1, y0, y1; + unsigned short flags; + unsigned char format; } GlyphMetric; // maps codepoints -> glyph IDs @@ -86,9 +94,10 @@ typedef struct { typedef struct { // accessed with metrics[bitmap_idx][glyph_id / nrow][glyph_id - (row * ncol)] GlyphMetric *metrics[SUBPIXEL_BITMAPS_CACHED][GLYPHMAP_ROW]; - // accessed with atlas[bitmap_idx][atlas_idx].surfaces[surface_idx] - GlyphAtlas *atlas[SUBPIXEL_BITMAPS_CACHED]; - size_t natlas, bytesize; + // accessed by atlas[glyph_format][atlas_idx].surfaces[surface_idx] + GlyphAtlas *atlas[EGlyphFormatSize]; + size_t natlas[EGlyphFormatSize]; + size_t bytesize; } GlyphMap; typedef struct RenFont { @@ -99,7 +108,7 @@ typedef struct RenFont { int scale; #endif float size, space_advance; - unsigned short max_height, baseline, height, tab_size; + unsigned short baseline, height, tab_size; unsigned short underline_thickness; ERenFontAntialiasing antialiasing; ERenFontHinting hinting; @@ -193,30 +202,39 @@ static unsigned int font_get_glyph_id(RenFont *font, unsigned int codepoint) { #define FONT_IS_SUBPIXEL(F) ((F)->antialiasing == FONT_ANTIALIASING_SUBPIXEL) #define FONT_BITMAP_COUNT(F) ((F)->antialiasing == FONT_ANTIALIASING_SUBPIXEL ? SUBPIXEL_BITMAPS_CACHED : 1) +#define SLOT_BITMAP_TYPE(B) ((B).pixel_mode == FT_PIXEL_MODE_LCD ? EGlyphFormatSubpixel : EGlyphFormatGrayscale) + +static inline SDL_PixelFormatEnum glyphformat_to_pixelformat(ERenGlyphFormat format, int *depth) { + switch (format) { + case EGlyphFormatSubpixel: *depth = 24; return SDL_PIXELFORMAT_RGB24; + case EGlyphFormatGrayscale: *depth = 8; return SDL_PIXELFORMAT_INDEX8; + default: return SDL_PIXELFORMAT_UNKNOWN; + } +} static SDL_Surface *font_allocate_glyph_surface(RenFont *font, FT_GlyphSlot slot, int bitmap_idx, GlyphMetric *metric) { // get an atlas with the correct width + ERenGlyphFormat glyph_format = SLOT_BITMAP_TYPE(slot->bitmap); int atlas_idx = -1; - for (int i = 0; i < font->glyphs.natlas; i++) { - if (font->glyphs.atlas[bitmap_idx][i].width >= metric->x1) { + for (int i = 0; i < font->glyphs.natlas[glyph_format]; i++) { + if (font->glyphs.atlas[glyph_format][i].width >= metric->x1) { atlas_idx = i; break; } } if (atlas_idx < 0) { - // create a new atlas with the correct width, for each subpixel bitmap - for (int i = 0; i < FONT_BITMAP_COUNT(font); i++) { - font->glyphs.atlas[i] = check_alloc(realloc(font->glyphs.atlas[i], sizeof(GlyphAtlas) * (font->glyphs.natlas + 1))); - font->glyphs.atlas[i][font->glyphs.natlas] = (GlyphAtlas) { - .width = metric->x1 + FONT_WIDTH_OVERFLOW_PX, .nsurface = 0, - .surfaces = NULL, - }; - } + font->glyphs.atlas[glyph_format] = check_alloc( + realloc(font->glyphs.atlas[glyph_format], sizeof(GlyphAtlas) * (font->glyphs.natlas[glyph_format] + 1)) + ); + font->glyphs.atlas[glyph_format][font->glyphs.natlas[glyph_format]] = (GlyphAtlas) { + .width = metric->x1 + FONT_WIDTH_OVERFLOW_PX, .nsurface = 0, + .surfaces = NULL, + }; font->glyphs.bytesize += sizeof(GlyphAtlas); - atlas_idx = font->glyphs.natlas++; + atlas_idx = font->glyphs.natlas[glyph_format]++; } metric->atlas_idx = atlas_idx; - GlyphAtlas *atlas = &font->glyphs.atlas[bitmap_idx][atlas_idx]; + GlyphAtlas *atlas = &font->glyphs.atlas[glyph_format][atlas_idx]; // find the surface with the minimum height that can fit the glyph (limited to last 100 surfaces) int surface_idx = -1, max_surface_idx = (int) atlas->nsurface - 100, min_waste = INT_MAX; @@ -234,14 +252,12 @@ static SDL_Surface *font_allocate_glyph_surface(RenFont *font, FT_GlyphSlot slot int h = FONT_HEIGHT_OVERFLOW_PX + (double) font->face->size->metrics.height / 64.0f; if (h <= FONT_HEIGHT_OVERFLOW_PX) h += slot->bitmap.rows; if (h <= FONT_HEIGHT_OVERFLOW_PX) h += font->size; + int depth = 0; SDL_PixelFormatEnum format = glyphformat_to_pixelformat(glyph_format, &depth); atlas->surfaces = check_alloc(realloc(atlas->surfaces, sizeof(SDL_Surface *) * (atlas->nsurface + 1))); - atlas->surfaces[atlas->nsurface] = check_alloc(SDL_CreateRGBSurface( - 0, atlas->width, GLYPHS_PER_ATLAS * h, FONT_BITMAP_COUNT(font) * 8, - 0, 0, 0, 0 - )); + atlas->surfaces[atlas->nsurface] = check_alloc(SDL_CreateRGBSurfaceWithFormat(0, atlas->width, GLYPHS_PER_ATLAS * h, depth, format)); atlas->surfaces[atlas->nsurface]->userdata = NULL; surface_idx = atlas->nsurface++; - font->glyphs.bytesize += (sizeof(SDL_Surface *) + sizeof(SDL_Surface) + atlas->width * GLYPHS_PER_ATLAS * h * FONT_BITMAP_COUNT(font)); + font->glyphs.bytesize += (sizeof(SDL_Surface *) + sizeof(SDL_Surface) + atlas->width * GLYPHS_PER_ATLAS * h * glyph_format); } metric->surface_idx = surface_idx; if (atlas->surfaces[surface_idx]->userdata) { @@ -252,68 +268,80 @@ static SDL_Surface *font_allocate_glyph_surface(RenFont *font, FT_GlyphSlot slot return atlas->surfaces[surface_idx]; } -static void font_load_glyph(RenFont *font, unsigned int glyph_id) { - unsigned int render_option = font_set_render_options(font); +static GlyphMetric *font_load_glyph_metric(RenFont *font, unsigned int glyph_id, unsigned int bitmap_idx) { unsigned int load_option = font_set_load_options(font); - // load the font without hinting to fix an issue with monospaced fonts, - // because freetype doesn't report the correct LSB and RSB delta. Transformation & subpixel positioning don't affect - // the xadvance, so we can save some time by not doing this step multiple times - if (FT_Load_Glyph(font->face, glyph_id, (load_option | FT_LOAD_BITMAP_METRICS_ONLY | FT_LOAD_NO_HINTING) & ~FT_LOAD_FORCE_AUTOHINT) != 0) - return; - double unhinted_xadv = font->face->glyph->advance.x / 64.0f; - // render the glyph for all bitmap - int bitmaps = FONT_BITMAP_COUNT(font); int row = glyph_id / GLYPHMAP_COL, col = glyph_id - (row * GLYPHMAP_COL); - for (int bitmap_idx = 0; bitmap_idx < bitmaps; bitmap_idx++) { - FT_GlyphSlot slot = font->face->glyph; - if (FT_Load_Glyph(font->face, glyph_id, load_option | FT_LOAD_BITMAP_METRICS_ONLY) != 0 - || font_set_style(&slot->outline, bitmap_idx * (64 / SUBPIXEL_BITMAPS_CACHED), font->style) != 0 - || FT_Render_Glyph(slot, render_option) != 0) - return; + int bitmaps = FONT_BITMAP_COUNT(font); - // save the metrics - if (!font->glyphs.metrics[bitmap_idx][row]) { - font->glyphs.metrics[bitmap_idx][row] = check_alloc(calloc(sizeof(GlyphMetric), GLYPHMAP_COL)); - font->glyphs.bytesize += sizeof(GlyphMetric) * GLYPHMAP_COL; - } - GlyphMetric *metric = &font->glyphs.metrics[bitmap_idx][row][col]; - metric->flags = EGlyphLoaded; - metric->xadvance = unhinted_xadv; - - // if this bitmap is empty, or has a format we don't support, just store the xadvance - if (!slot->bitmap.width || !slot->bitmap.rows || !slot->bitmap.buffer || - (slot->bitmap.pixel_mode != FT_PIXEL_MODE_MONO - && slot->bitmap.pixel_mode != FT_PIXEL_MODE_GRAY - && slot->bitmap.pixel_mode != FT_PIXEL_MODE_LCD)) - continue; - - unsigned int glyph_width = slot->bitmap.width / bitmaps; - // FT_PIXEL_MODE_MONO uses 1 bit per pixel packed bitmap - if (slot->bitmap.pixel_mode == FT_PIXEL_MODE_MONO) glyph_width *= 8; - - metric->x1 = glyph_width; - metric->y1 = slot->bitmap.rows; - metric->bitmap_left = slot->bitmap_left; - metric->bitmap_top = slot->bitmap_top; - metric->flags |= (EGlyphBitmap | EGlyphDualSource); - - // find the best surface to copy the glyph over, and copy it - SDL_Surface *surface = font_allocate_glyph_surface(font, slot, bitmap_idx, metric); - uint8_t* pixels = surface->pixels; - for (unsigned int line = 0; line < slot->bitmap.rows; ++line) { - int target_offset = surface->pitch * (line + metric->y0); // x0 is always assumed to be 0 - int source_offset = line * slot->bitmap.pitch; - if (font->antialiasing == FONT_ANTIALIASING_NONE) { - for (unsigned int column = 0; column < slot->bitmap.width; ++column) { - int current_source_offset = source_offset + (column / 8); - int source_pixel = slot->bitmap.buffer[current_source_offset]; - pixels[++target_offset] = ((source_pixel >> (7 - (column % 8))) & 0x1) * 0xFF; - } - } else { - memcpy(&pixels[target_offset], &slot->bitmap.buffer[source_offset], slot->bitmap.width); + // we set all 3 subpixel bitmaps at once, so if either of them are missing we should load it with freetype + if (!font->glyphs.metrics[0][row] || !(font->glyphs.metrics[0][row][col].flags & EGlyphXAdvance)) { + // load the font without hinting to fix an issue with monospaced fonts, + // because freetype doesn't report the correct LSB and RSB delta. Transformation & subpixel positioning don't affect + // the xadvance, so we can save some time by not doing this step multiple times + if (FT_Load_Glyph(font->face, glyph_id, (load_option | FT_LOAD_BITMAP_METRICS_ONLY | FT_LOAD_NO_HINTING) & ~FT_LOAD_FORCE_AUTOHINT) != 0) + return NULL; + for (int i = 0; i < bitmaps; i++) { + // save the metrics for all subpixel indexes + if (!font->glyphs.metrics[i][row]) { + font->glyphs.metrics[i][row] = check_alloc(calloc(sizeof(GlyphMetric), GLYPHMAP_COL)); + font->glyphs.bytesize += sizeof(GlyphMetric) * GLYPHMAP_COL; } + GlyphMetric *metric = &font->glyphs.metrics[i][row][col]; + metric->flags |= EGlyphXAdvance; + metric->xadvance = font->face->glyph->advance.x / 64.0f; } } + return &font->glyphs.metrics[bitmap_idx][row][col]; +} + +static SDL_Surface *font_load_glyph_bitmap(RenFont *font, unsigned int glyph_id, unsigned int bitmap_idx) { + GlyphMetric *metric = font_load_glyph_metric(font, glyph_id, bitmap_idx); + if (!metric) return NULL; + if (metric->flags & EGlyphBitmap) return font->glyphs.atlas[metric->format][metric->atlas_idx].surfaces[metric->surface_idx]; + + // render the glyph for a bitmap_idx + unsigned int load_option = font_set_load_options(font), render_option = font_set_render_options(font); + FT_GlyphSlot slot = font->face->glyph; + if (FT_Load_Glyph(font->face, glyph_id, load_option | FT_LOAD_BITMAP_METRICS_ONLY) != 0 + || font_set_style(&slot->outline, bitmap_idx * (64 / SUBPIXEL_BITMAPS_CACHED), font->style) != 0 + || FT_Render_Glyph(slot, render_option) != 0) + return NULL; + + // if this bitmap is empty, or has a format we don't support, just store the xadvance + if (!slot->bitmap.width || !slot->bitmap.rows || !slot->bitmap.buffer || + (slot->bitmap.pixel_mode != FT_PIXEL_MODE_MONO + && slot->bitmap.pixel_mode != FT_PIXEL_MODE_GRAY + && slot->bitmap.pixel_mode != FT_PIXEL_MODE_LCD)) + return NULL; + + unsigned int glyph_width = slot->bitmap.width / FONT_BITMAP_COUNT(font); + // FT_PIXEL_MODE_MONO uses 1 bit per pixel packed bitmap + if (slot->bitmap.pixel_mode == FT_PIXEL_MODE_MONO) glyph_width *= 8; + + metric->x1 = glyph_width; + metric->y1 = slot->bitmap.rows; + metric->bitmap_left = slot->bitmap_left; + metric->bitmap_top = slot->bitmap_top; + metric->flags |= EGlyphBitmap; + metric->format = SLOT_BITMAP_TYPE(slot->bitmap); + + // find the best surface to copy the glyph over, and copy it + SDL_Surface *surface = font_allocate_glyph_surface(font, slot, bitmap_idx, metric); + uint8_t* pixels = surface->pixels; + for (unsigned int line = 0; line < slot->bitmap.rows; ++line) { + int target_offset = surface->pitch * (line + metric->y0); // x0 is always assumed to be 0 + int source_offset = line * slot->bitmap.pitch; + if (font->antialiasing == FONT_ANTIALIASING_NONE) { + for (unsigned int column = 0; column < slot->bitmap.width; ++column) { + int current_source_offset = source_offset + (column / 8); + int source_pixel = slot->bitmap.buffer[current_source_offset]; + pixels[++target_offset] = ((source_pixel >> (7 - (column % 8))) & 0x1) * 0xFF; + } + } else { + memcpy(&pixels[target_offset], &slot->bitmap.buffer[source_offset], slot->bitmap.width); + } + } + return surface; } // https://en.wikipedia.org/wiki/Whitespace_character @@ -324,11 +352,6 @@ static inline int is_whitespace(unsigned int codepoint) { return (codepoint >= 0x9 && codepoint <= 0xD) || (codepoint >= 0x2000 && codepoint <= 0x200A); } -static inline GlyphMetric *font_get_glyph(RenFont *font, unsigned int glyph_id, int subpixel_idx) { - int row = glyph_id / GLYPHMAP_COL, col = glyph_id - (row * GLYPHMAP_COL); - return font->glyphs.metrics[subpixel_idx][row] ? &font->glyphs.metrics[subpixel_idx][row][col] : NULL; -} - static RenFont *font_group_get_glyph(RenFont **fonts, unsigned int codepoint, int subpixel_idx, SDL_Surface **surface, GlyphMetric **metric) { if (subpixel_idx < 0) subpixel_idx += SUBPIXEL_BITMAPS_CACHED; RenFont *font = NULL; @@ -340,38 +363,36 @@ static RenFont *font_group_get_glyph(RenFont **fonts, unsigned int codepoint, in } // load the glyph if it is not loaded subpixel_idx = FONT_IS_SUBPIXEL(font) ? subpixel_idx : 0; - GlyphMetric *m = font_get_glyph(font, glyph_id, subpixel_idx); - if (!m || !(m->flags & EGlyphLoaded)) font_load_glyph(font, glyph_id); - // if the glyph ID (possibly 0) is not available and we are not trying to load whitespace, try to load U+25A1 (box character) - if ((!m || !(m->flags & EGlyphLoaded)) && codepoint != 0x25A1 && !is_whitespace(codepoint)) + GlyphMetric *m = font_load_glyph_metric(font, glyph_id, subpixel_idx); + // try the box drawing character (0x25A1) if the requested codepoint is not a whitespace, and we cannot load the .notdef glyph + if ((!m || !m->flags) && codepoint != 0x25A1 && !is_whitespace(codepoint)) return font_group_get_glyph(fonts, 0x25A1, subpixel_idx, surface, metric); - // fetch the glyph metrics again and save it - m = font_get_glyph(font, glyph_id, subpixel_idx); if (metric && m) *metric = m; - if (surface && m && m->flags & EGlyphBitmap) *surface = font->glyphs.atlas[subpixel_idx][m->atlas_idx].surfaces[m->surface_idx]; + if (surface && m) *surface = font_load_glyph_bitmap(font, glyph_id, subpixel_idx); return font; } static void font_clear_glyph_cache(RenFont* font) { - int bitmaps = FONT_BITMAP_COUNT(font); - for (int bitmap_idx = 0; bitmap_idx < bitmaps; bitmap_idx++) { - for (int atlas_idx = 0; atlas_idx < font->glyphs.natlas; atlas_idx++) { - GlyphAtlas *atlas = &font->glyphs.atlas[bitmap_idx][atlas_idx]; + for (int glyph_format_idx = 0; glyph_format_idx < EGlyphFormatSize; glyph_format_idx++) { + for (int atlas_idx = 0; atlas_idx < font->glyphs.natlas[glyph_format_idx]; atlas_idx++) { + GlyphAtlas *atlas = &font->glyphs.atlas[glyph_format_idx][atlas_idx]; for (int surface_idx = 0; surface_idx < atlas->nsurface; surface_idx++) { SDL_FreeSurface(atlas->surfaces[surface_idx]); } free(atlas->surfaces); } - free(font->glyphs.atlas[bitmap_idx]); - font->glyphs.atlas[bitmap_idx] = NULL; - // clear glyph metric + free(font->glyphs.atlas[glyph_format_idx]); + font->glyphs.atlas[glyph_format_idx] = NULL; + font->glyphs.natlas[glyph_format_idx] = 0; + } + // clear glyph metric + for (int subpixel_idx = 0; subpixel_idx < FONT_BITMAP_COUNT(font); subpixel_idx++) { for (int glyphmap_row = 0; glyphmap_row < GLYPHMAP_ROW; glyphmap_row++) { - free(font->glyphs.metrics[bitmap_idx][glyphmap_row]); - font->glyphs.metrics[bitmap_idx][glyphmap_row] = NULL; + free(font->glyphs.metrics[subpixel_idx][glyphmap_row]); + font->glyphs.metrics[subpixel_idx][glyphmap_row] = NULL; } } font->glyphs.bytesize = 0; - font->glyphs.natlas = 0; } // based on https://github.com/libsdl-org/SDL_ttf/blob/2a094959055fba09f7deed6e1ffeb986188982ae/SDL_ttf.c#L1735 @@ -410,7 +431,7 @@ static int font_set_face_metrics(RenFont *font, FT_Face face) { } else { font->height = (short) font->face->size->metrics.height / 64.0f; font->baseline = (short) font->face->size->metrics.ascender / 64.0f; - } + } if(!font->underline_thickness) font->underline_thickness = ceil((double) font->height / 14.0); @@ -426,7 +447,7 @@ RenFont* ren_font_load(RenWindow *window_renderer, const char* path, float size, file = SDL_RWFromFile(path, "rb"); if (!file) return NULL; - + int len = strlen(path); font = check_alloc(calloc(1, sizeof(RenFont) + len + 1)); strcpy(font->path, path); @@ -545,12 +566,11 @@ double ren_font_group_get_width(RenWindow *window_renderer, RenFont **fonts, con // this function can be used to debug font atlases, it is not public void ren_font_dump(RenFont *font) { char filename[1024]; - int bitmaps = FONT_BITMAP_COUNT(font); - for (int bitmap_idx = 0; bitmap_idx < bitmaps; bitmap_idx++) { - for (int atlas_idx = 0; atlas_idx < font->glyphs.natlas; atlas_idx++) { - GlyphAtlas *atlas = &font->glyphs.atlas[bitmap_idx][atlas_idx]; + for (int glyph_format_idx = 0; glyph_format_idx < EGlyphFormatSize; glyph_format_idx++) { + for (int atlas_idx = 0; atlas_idx < font->glyphs.natlas[glyph_format_idx]; atlas_idx++) { + GlyphAtlas *atlas = &font->glyphs.atlas[glyph_format_idx][atlas_idx]; for (int surface_idx = 0; surface_idx < atlas->nsurface; surface_idx++) { - snprintf(filename, 1024, "%s-%d-%d-%d.bmp", font->face->family_name, bitmap_idx, atlas_idx, surface_idx); + snprintf(filename, 1024, "%s-%d-%d-%d.bmp", font->face->family_name, glyph_format_idx, atlas_idx, surface_idx); SDL_SaveBMP(atlas->surfaces[surface_idx], filename); } } @@ -611,11 +631,10 @@ double ren_draw_text(RenSurface *rs, RenFont **fonts, const char *text, size_t l SDL_Color dst = { (destination_color & surface->format->Rmask) >> surface->format->Rshift, (destination_color & surface->format->Gmask) >> surface->format->Gshift, (destination_color & surface->format->Bmask) >> surface->format->Bshift, (destination_color & surface->format->Amask) >> surface->format->Ashift }; SDL_Color src; - if (font->antialiasing == FONT_ANTIALIASING_SUBPIXEL) { + if (metric->format == EGlyphFormatSubpixel) { src.r = *(source_pixel++); src.g = *(source_pixel++); - } - else { + } else { src.r = *(source_pixel); src.g = *(source_pixel); }