From fd75bdccb033e74afeba44f500edc8fcef7eb023 Mon Sep 17 00:00:00 2001 From: ocornut Date: Mon, 30 Jun 2025 21:16:20 +0200 Subject: [PATCH] Fonts: for large size fonts, layout/size calculation only load glyphs metrics. Actual glyphs are renderer+packed when used by drawing functions. (#8758, #8465) (Breaking) breaks signature of ImFontLoader::FontBakedLoadGlyph, sorry. --- docs/CHANGELOG.txt | 2 + imgui_draw.cpp | 90 ++++++++++++++++++++++++++------ imgui_internal.h | 6 ++- misc/freetype/imgui_freetype.cpp | 18 +++++-- 4 files changed, 93 insertions(+), 23 deletions(-) diff --git a/docs/CHANGELOG.txt b/docs/CHANGELOG.txt index 99246de8f..625a5b927 100644 --- a/docs/CHANGELOG.txt +++ b/docs/CHANGELOG.txt @@ -45,6 +45,8 @@ Other changes: - Fonts: added ImFontAtlas::SetFontLoader() to dynamically change font loader at runtime without using internal API. (#8752, #8465) +- Fonts: for large size fonts, layout/size calculation only load glyphs metrics. + Actual glyphs are renderer+packed when used by drawing functions. (#8758, #8465) - Fonts: set a maximum font size of 512.0f at ImGui:: API level to reduce edge cases (e.g. out of memory errors). ImDrawList:: API doesn't have the constraint. (#8758) diff --git a/imgui_draw.cpp b/imgui_draw.cpp index 704cc6d2f..374088573 100644 --- a/imgui_draw.cpp +++ b/imgui_draw.cpp @@ -2560,6 +2560,7 @@ void ImTextureData::DestroyPixels() //----------------------------------------------------------------------------- // - ImFontBaked_BuildGrowIndex() // - ImFontBaked_BuildLoadGlyph() +// - ImFontBaked_BuildLoadGlyphAdvanceX() // - ImFontAtlasDebugLogTextureRequests() //----------------------------------------------------------------------------- // - ImFontAtlasGetFontLoaderForStbTruetype() @@ -4409,7 +4410,7 @@ static void ImFontAtlas_FontHookRemapCodepoint(ImFontAtlas* atlas, ImFont* font, *c = (ImWchar)font->RemapPairs.GetInt((ImGuiID)*c, (int)*c); } -static ImFontGlyph* ImFontBaked_BuildLoadGlyph(ImFontBaked* baked, ImWchar codepoint) +static ImFontGlyph* ImFontBaked_BuildLoadGlyph(ImFontBaked* baked, ImWchar codepoint, float* only_load_advance_x) { ImFont* font = baked->ContainerFont; ImFontAtlas* atlas = font->ContainerAtlas; @@ -4442,13 +4443,25 @@ static ImFontGlyph* ImFontBaked_BuildLoadGlyph(ImFontBaked* baked, ImWchar codep const ImFontLoader* loader = src->FontLoader ? src->FontLoader : atlas->FontLoader; if (!src->GlyphExcludeRanges || ImFontAtlasBuildAcceptCodepointForSource(src, codepoint)) { - ImFontGlyph glyph_buf; - if (loader->FontBakedLoadGlyph(atlas, src, baked, loader_user_data_p, codepoint, &glyph_buf)) + if (only_load_advance_x == NULL) { - // FIXME: Add hooks for e.g. #7962 - glyph_buf.Codepoint = src_codepoint; - glyph_buf.SourceIdx = src_n; - return ImFontAtlasBakedAddFontGlyph(atlas, baked, src, &glyph_buf); + ImFontGlyph glyph_buf; + if (loader->FontBakedLoadGlyph(atlas, src, baked, loader_user_data_p, codepoint, &glyph_buf, NULL)) + { + // FIXME: Add hooks for e.g. #7962 + glyph_buf.Codepoint = src_codepoint; + glyph_buf.SourceIdx = src_n; + return ImFontAtlasBakedAddFontGlyph(atlas, baked, src, &glyph_buf); + } + } + else + { + // Special mode but only loading glyphs metrics. Will rasterize and pack later. + if (loader->FontBakedLoadGlyph(atlas, src, baked, loader_user_data_p, codepoint, NULL, only_load_advance_x)) + { + ImFontAtlasBakedAddFontGlyphAdvancedX(atlas, baked, src, codepoint, *only_load_advance_x); + return NULL; + } } } loader_user_data_p += loader->FontBakedSrcLoaderDataSize; @@ -4468,12 +4481,27 @@ static ImFontGlyph* ImFontBaked_BuildLoadGlyph(ImFontBaked* baked, ImWchar codep return NULL; } +static float ImFontBaked_BuildLoadGlyphAdvanceX(ImFontBaked* baked, ImWchar codepoint) +{ + if (baked->Size >= IMGUI_FONT_SIZE_THRESHOLD_FOR_LOADADVANCEXONLYMODE) + { + // First load AdvanceX value used by CalcTextSize() API then load the rest when loaded by drawing API. + float only_advance_x = 0.0f; + ImFontGlyph* glyph = ImFontBaked_BuildLoadGlyph(baked, (ImWchar)codepoint, &only_advance_x); + return glyph ? glyph->AdvanceX : only_advance_x; + } + else + { + ImFontGlyph* glyph = ImFontBaked_BuildLoadGlyph(baked, (ImWchar)codepoint, NULL); + return glyph ? glyph->AdvanceX : baked->FallbackAdvanceX; + } +} + // The point of this indirection is to not be inlined in debug mode in order to not bloat inner loop.b IM_MSVC_RUNTIME_CHECKS_OFF static float BuildLoadGlyphGetAdvanceOrFallback(ImFontBaked* baked, unsigned int codepoint) { - ImFontGlyph* glyph = ImFontBaked_BuildLoadGlyph(baked, (ImWchar)codepoint); - return glyph ? glyph->AdvanceX : baked->FallbackAdvanceX; + return ImFontBaked_BuildLoadGlyphAdvanceX(baked, (ImWchar)codepoint); } IM_MSVC_RUNTIME_CHECKS_RESTORE @@ -4594,7 +4622,7 @@ static bool ImGui_ImplStbTrueType_FontBakedInit(ImFontAtlas* atlas, ImFontConfig return true; } -static bool ImGui_ImplStbTrueType_FontBakedLoadGlyph(ImFontAtlas* atlas, ImFontConfig* src, ImFontBaked* baked, void*, ImWchar codepoint, ImFontGlyph* out_glyph) +static bool ImGui_ImplStbTrueType_FontBakedLoadGlyph(ImFontAtlas* atlas, ImFontConfig* src, ImFontBaked* baked, void*, ImWchar codepoint, ImFontGlyph* out_glyph, float* out_advance_x) { // Search for first font which has the glyph ImGui_ImplStbTrueType_FontSrcData* bd_font_data = (ImGui_ImplStbTrueType_FontSrcData*)src->FontLoaderData; @@ -4616,7 +4644,14 @@ static bool ImGui_ImplStbTrueType_FontBakedLoadGlyph(ImFontAtlas* atlas, ImFontC int advance, lsb; stbtt_GetGlyphBitmapBoxSubpixel(&bd_font_data->FontInfo, glyph_index, scale_for_raster_x, scale_for_raster_y, 0, 0, &x0, &y0, &x1, &y1); stbtt_GetGlyphHMetrics(&bd_font_data->FontInfo, glyph_index, &advance, &lsb); - const bool is_visible = (x0 != x1 && y0 != y1); + + // Load metrics only mode + if (out_advance_x != NULL) + { + IM_ASSERT(out_glyph == NULL); + *out_advance_x = advance * scale_for_layout; + return true; + } // Prepare glyph out_glyph->Codepoint = codepoint; @@ -4624,6 +4659,7 @@ static bool ImGui_ImplStbTrueType_FontBakedLoadGlyph(ImFontAtlas* atlas, ImFontC // Pack and retrieve position inside texture atlas // (generally based on stbtt_PackFontRangesRenderIntoRects) + const bool is_visible = (x0 != x1 && y0 != y1); if (is_visible) { const int w = (x1 - x0 + oversample_h - 1); @@ -5124,6 +5160,29 @@ ImFontGlyph* ImFontAtlasBakedAddFontGlyph(ImFontAtlas* atlas, ImFontBaked* baked return glyph; } +// FIXME: Code is duplicated with code above. +void ImFontAtlasBakedAddFontGlyphAdvancedX(ImFontAtlas* atlas, ImFontBaked* baked, ImFontConfig* src, ImWchar codepoint, float advance_x) +{ + IM_UNUSED(atlas); + if (src != NULL) + { + // Clamp & recenter if needed + const float ref_size = baked->ContainerFont->Sources[0]->SizePixels; + const float offsets_scale = (ref_size != 0.0f) ? (baked->Size / ref_size) : 1.0f; + advance_x = ImClamp(advance_x, src->GlyphMinAdvanceX * offsets_scale, src->GlyphMaxAdvanceX * offsets_scale); + + // Snap to pixel + if (src->PixelSnapH) + advance_x = IM_ROUND(advance_x); + + // Bake spacing + advance_x += src->GlyphExtraAdvanceX; + } + + ImFontBaked_BuildGrowIndex(baked, codepoint + 1); + baked->IndexAdvanceX[codepoint] = advance_x; +} + // Copy to texture, post-process and queue update for backend void ImFontAtlasBakedSetFontGlyphBitmap(ImFontAtlas* atlas, ImFontBaked* baked, ImFontConfig* src, ImFontGlyph* glyph, ImTextureRect* r, const unsigned char* src_pixels, ImTextureFormat src_fmt, int src_pitch) { @@ -5151,7 +5210,7 @@ ImFontGlyph* ImFontBaked::FindGlyph(ImWchar c) if (i != IM_FONTGLYPH_INDEX_UNUSED) return &Glyphs.Data[i]; } - ImFontGlyph* glyph = ImFontBaked_BuildLoadGlyph(this, c); + ImFontGlyph* glyph = ImFontBaked_BuildLoadGlyph(this, c, NULL); return glyph ? glyph : &Glyphs.Data[FallbackGlyphIndex]; } @@ -5167,7 +5226,7 @@ ImFontGlyph* ImFontBaked::FindGlyphNoFallback(ImWchar c) return &Glyphs.Data[i]; } LockLoadingFallback = true; // This is actually a rare call, not done in hot-loop, so we prioritize not adding extra cruft to ImFontBaked_BuildLoadGlyph() call sites. - ImFontGlyph* glyph = ImFontBaked_BuildLoadGlyph(this, c); + ImFontGlyph* glyph = ImFontBaked_BuildLoadGlyph(this, c, NULL); LockLoadingFallback = false; return glyph; } @@ -5210,10 +5269,7 @@ float ImFontBaked::GetCharAdvance(ImWchar c) if (x >= 0.0f) return x; } - - // Same as BuildLoadGlyphGetAdvanceOrFallback(): - const ImFontGlyph* glyph = ImFontBaked_BuildLoadGlyph(this, c); - return glyph ? glyph->AdvanceX : FallbackAdvanceX; + return ImFontBaked_BuildLoadGlyphAdvanceX(this, c); } IM_MSVC_RUNTIME_CHECKS_RESTORE diff --git a/imgui_internal.h b/imgui_internal.h index 7ac0c2079..12fdac568 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -3691,7 +3691,7 @@ struct ImFontLoader bool (*FontSrcContainsGlyph)(ImFontAtlas* atlas, ImFontConfig* src, ImWchar codepoint); bool (*FontBakedInit)(ImFontAtlas* atlas, ImFontConfig* src, ImFontBaked* baked, void* loader_data_for_baked_src); void (*FontBakedDestroy)(ImFontAtlas* atlas, ImFontConfig* src, ImFontBaked* baked, void* loader_data_for_baked_src); - bool (*FontBakedLoadGlyph)(ImFontAtlas* atlas, ImFontConfig* src, ImFontBaked* baked, void* loader_data_for_baked_src, ImWchar codepoint, ImFontGlyph* out_glyph); + bool (*FontBakedLoadGlyph)(ImFontAtlas* atlas, ImFontConfig* src, ImFontBaked* baked, void* loader_data_for_baked_src, ImWchar codepoint, ImFontGlyph* out_glyph, float* out_advance_x); // Size of backend data, Per Baked * Per Source. Buffers are managed by core to avoid excessive allocations. // FIXME: At this point the two other types of buffers may be managed by core to be consistent? @@ -3711,7 +3711,8 @@ typedef ImFontLoader ImFontBuilderIO; // [renamed/changed in 1.92] The types are // [SECTION] ImFontAtlas internal API //----------------------------------------------------------------------------- -#define IMGUI_FONT_SIZE_MAX (512.0f) +#define IMGUI_FONT_SIZE_MAX (512.0f) +#define IMGUI_FONT_SIZE_THRESHOLD_FOR_LOADADVANCEXONLYMODE (128.0f) // Helpers: ImTextureRef ==/!= operators provided as convenience // (note that _TexID and _TexData are never set simultaneously) @@ -3829,6 +3830,7 @@ IMGUI_API ImFontBaked* ImFontAtlasBakedGetClosestMatch(ImFontAtlas* atlas, IMGUI_API ImFontBaked* ImFontAtlasBakedAdd(ImFontAtlas* atlas, ImFont* font, float font_size, float font_rasterizer_density, ImGuiID baked_id); IMGUI_API void ImFontAtlasBakedDiscard(ImFontAtlas* atlas, ImFont* font, ImFontBaked* baked); IMGUI_API ImFontGlyph* ImFontAtlasBakedAddFontGlyph(ImFontAtlas* atlas, ImFontBaked* baked, ImFontConfig* src, const ImFontGlyph* in_glyph); +IMGUI_API void ImFontAtlasBakedAddFontGlyphAdvancedX(ImFontAtlas* atlas, ImFontBaked* baked, ImFontConfig* src, ImWchar codepoint, float advance_x); IMGUI_API void ImFontAtlasBakedDiscardFontGlyph(ImFontAtlas* atlas, ImFont* font, ImFontBaked* baked, ImFontGlyph* glyph); IMGUI_API void ImFontAtlasBakedSetFontGlyphBitmap(ImFontAtlas* atlas, ImFontBaked* baked, ImFontConfig* src, ImFontGlyph* glyph, ImTextureRect* r, const unsigned char* src_pixels, ImTextureFormat src_fmt, int src_pitch); diff --git a/misc/freetype/imgui_freetype.cpp b/misc/freetype/imgui_freetype.cpp index e5f2818db..33f259365 100644 --- a/misc/freetype/imgui_freetype.cpp +++ b/misc/freetype/imgui_freetype.cpp @@ -475,7 +475,7 @@ void ImGui_ImplFreeType_FontBakedDestroy(ImFontAtlas* atlas, ImFontConfig* src, bd_baked_data->~ImGui_ImplFreeType_FontSrcBakedData(); // ~IM_PLACEMENT_DELETE() } -bool ImGui_ImplFreeType_FontBakedLoadGlyph(ImFontAtlas* atlas, ImFontConfig* src, ImFontBaked* baked, void* loader_data_for_baked_src, ImWchar codepoint, ImFontGlyph* out_glyph) +bool ImGui_ImplFreeType_FontBakedLoadGlyph(ImFontAtlas* atlas, ImFontConfig* src, ImFontBaked* baked, void* loader_data_for_baked_src, ImWchar codepoint, ImFontGlyph* out_glyph, float* out_advance_x) { ImGui_ImplFreeType_FontSrcData* bd_font_data = (ImGui_ImplFreeType_FontSrcData*)src->FontLoaderData; uint32_t glyph_index = FT_Get_Char_Index(bd_font_data->FtFace, codepoint); @@ -494,9 +494,20 @@ bool ImGui_ImplFreeType_FontBakedLoadGlyph(ImFontAtlas* atlas, ImFontConfig* src if (metrics == nullptr) return false; - // Render glyph into a bitmap (currently held by FreeType) FT_Face face = bd_font_data->FtFace; FT_GlyphSlot slot = face->glyph; + const float rasterizer_density = src->RasterizerDensity * baked->RasterizerDensity; + + // Load metrics only mode + const float advance_x = (slot->advance.x / FT_SCALEFACTOR) / rasterizer_density; + if (out_advance_x != NULL) + { + IM_ASSERT(out_glyph == NULL); + *out_advance_x = advance_x; + return true; + } + + // Render glyph into a bitmap (currently held by FreeType) FT_Render_Mode render_mode = (bd_font_data->UserFlags & ImGuiFreeTypeLoaderFlags_Monochrome) ? FT_RENDER_MODE_MONO : FT_RENDER_MODE_NORMAL; FT_Error error = FT_Render_Glyph(slot, render_mode); const FT_Bitmap* ft_bitmap = &slot->bitmap; @@ -506,11 +517,10 @@ bool ImGui_ImplFreeType_FontBakedLoadGlyph(ImFontAtlas* atlas, ImFontConfig* src const int w = (int)ft_bitmap->width; const int h = (int)ft_bitmap->rows; const bool is_visible = (w != 0 && h != 0); - const float rasterizer_density = src->RasterizerDensity * baked->RasterizerDensity; // Prepare glyph out_glyph->Codepoint = codepoint; - out_glyph->AdvanceX = (slot->advance.x / FT_SCALEFACTOR) / rasterizer_density; + out_glyph->AdvanceX = advance_x; // Pack and retrieve position inside texture atlas if (is_visible)