perf(typography): use font blobs in place and share identical faces
Typography copied every font blob into a fresh heap buffer for each face it added (IM_ALLOC + memcpy, FontDataOwnedByAtlas = true): the Ubuntu weight, the CJK subset merge, the emoji merge, and the icon and pickaxe fonts. The blobs are INCBIN symbols in read-only memory that live for the whole process, so the copies (16 faces, each with its own CJK copy and, for 8 of them, an emoji copy) only cost memory. Styles with the same weight and size also built separate, identical ImFonts. Now: - every font is added straight from the embedded data with FontDataOwnedByAtlas = false. ImGui 1.92 frees only data it owns, so the blobs survive atlas clears and DPI or skin reloads; - load() walks a style table and shares the ImFont between styles with the same (weight, scaled size, emoji) key. Emoji is part of the key, so no style gains or loses the emoji fallback. With the current ui.toml sizes H3/H4, H6/ButtonLg and Body1/Overline share a face: 13 ImFonts instead of 16 (12 text faces plus Mono), CJK merged 13 times instead of 16 and emoji 7 times instead of 8. Measured on the Lite build, RssAnon 15 s after start, median of 3 runs: - color emoji: 108.6 -> 78.8 MiB (-29.8 MiB); - monochrome emoji: 102.8 -> 78.6 MiB (-24.2 MiB). RssFile is also 2.4-5.1 MiB lower, because the blobs are no longer read end to end for copying. Full screenshot sweeps (610 screens per run; color and mono emoji at 1280x720 in English and Japanese, color at 2560x1440) match the previous build within run-to-run noise. The only content differences are live values such as the mining tab's RAM readout (233 MB before, 201 MB after). Also fixed: when AddFontFromMemoryTTF failed, ImGui had already freed the owned copy and the caller freed it again, a latent double free; those IM_FREE calls are gone. The emoji-merge failure log now prints the blob size instead of passing the float font size to %u. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -151,64 +151,40 @@ bool Typography::load(ImGuiIO& io, float dpiScale)
|
||||
// For ImGui, we need to load fonts at specific pixel sizes.
|
||||
// Font sizes come from Layout:: accessors (backed by UISchema JSON)
|
||||
|
||||
// Load fonts in order of TypeStyle enum
|
||||
|
||||
// H1: Light
|
||||
fonts_[0] = loadFont(io, kWeightLight, Layout::kFontH1() * scale, "H1");
|
||||
|
||||
// H2: Light
|
||||
fonts_[1] = loadFont(io, kWeightLight, Layout::kFontH2() * scale, "H2");
|
||||
|
||||
// H3: Regular
|
||||
fonts_[2] = loadFont(io, kWeightRegular, Layout::kFontH3() * scale, "H3");
|
||||
|
||||
// H4: Regular
|
||||
fonts_[3] = loadFont(io, kWeightRegular, Layout::kFontH4() * scale, "H4");
|
||||
|
||||
// H5: Regular
|
||||
fonts_[4] = loadFont(io, kWeightRegular, Layout::kFontH5() * scale, "H5");
|
||||
|
||||
// H6: Medium
|
||||
fonts_[5] = loadFont(io, kWeightMedium, Layout::kFontH6() * scale, "H6");
|
||||
|
||||
// Subtitle1: Regular
|
||||
fonts_[6] = loadFont(io, kWeightRegular, Layout::kFontSubtitle1() * scale, "Subtitle1");
|
||||
|
||||
// Subtitle2: Medium
|
||||
fonts_[7] = loadFont(io, kWeightMedium, Layout::kFontSubtitle2() * scale, "Subtitle2");
|
||||
|
||||
// Body1: Regular (shares with Subtitle1 if same size)
|
||||
if (Layout::kFontBody1() == Layout::kFontSubtitle1()) {
|
||||
fonts_[8] = fonts_[6]; // Reuse Subtitle1 font
|
||||
} else {
|
||||
fonts_[8] = loadFont(io, kWeightRegular, Layout::kFontBody1() * scale, "Body1");
|
||||
// Style table (order == TypeStyle enum). `emoji` = the styles that have always received the emoji merge.
|
||||
struct StyleDef { int weight; float px; const char* name; bool emoji; };
|
||||
const StyleDef defs[kNumStyles] = {
|
||||
{ kWeightLight, Layout::kFontH1(), "H1", false },
|
||||
{ kWeightLight, Layout::kFontH2(), "H2", false },
|
||||
{ kWeightRegular, Layout::kFontH3(), "H3", false },
|
||||
{ kWeightRegular, Layout::kFontH4(), "H4", false },
|
||||
{ kWeightRegular, Layout::kFontH5(), "H5", false },
|
||||
{ kWeightMedium, Layout::kFontH6(), "H6", false },
|
||||
{ kWeightRegular, Layout::kFontSubtitle1(), "Subtitle1", true },
|
||||
{ kWeightMedium, Layout::kFontSubtitle2(), "Subtitle2", true },
|
||||
{ kWeightRegular, Layout::kFontBody1(), "Body1", true },
|
||||
{ kWeightRegular, Layout::kFontBody2(), "Body2", true },
|
||||
{ kWeightMedium, Layout::kFontButton(), "Button", true },
|
||||
{ kWeightRegular, Layout::kFontCaption(), "Caption", true },
|
||||
{ kWeightRegular, Layout::kFontOverline(), "Overline", true },
|
||||
{ kWeightMedium, Layout::kFontButtonSm(), "ButtonSm", true },
|
||||
{ kWeightMedium, Layout::kFontButtonLg(), "ButtonLg", false },
|
||||
};
|
||||
static_assert(sizeof(fonts_) / sizeof(fonts_[0]) == kNumStyles, "style table must cover every TypeStyle");
|
||||
// Styles with the same (weight, scaled size, emoji) would build byte-identical ImFonts: load once and share
|
||||
// the pointer. emoji is part of the key so no style gains or loses the emoji fallback (identical rendering).
|
||||
// loadFont() adds the base face and its merges back-to-back (MergeMode attaches to Fonts.back()).
|
||||
for (int i = 0; i < kNumStyles; ++i) {
|
||||
const float px = defs[i].px * scale;
|
||||
int first = -1;
|
||||
for (int j = 0; j < i; ++j)
|
||||
if (defs[j].weight == defs[i].weight && defs[j].px * scale == px && defs[j].emoji == defs[i].emoji) { first = j; break; }
|
||||
fonts_[i] = (first >= 0) ? fonts_[first] : loadFont(io, defs[i].weight, px, defs[i].name, defs[i].emoji);
|
||||
}
|
||||
|
||||
// Body2: Regular
|
||||
fonts_[9] = loadFont(io, kWeightRegular, Layout::kFontBody2() * scale, "Body2");
|
||||
|
||||
// Button: Medium (shares with Subtitle2 if same size)
|
||||
if (Layout::kFontButton() == Layout::kFontSubtitle2()) {
|
||||
fonts_[10] = fonts_[7]; // Reuse Subtitle2 font
|
||||
} else {
|
||||
fonts_[10] = loadFont(io, kWeightMedium, Layout::kFontButton() * scale, "Button");
|
||||
}
|
||||
|
||||
// Caption: Regular
|
||||
fonts_[11] = loadFont(io, kWeightRegular, Layout::kFontCaption() * scale, "Caption");
|
||||
|
||||
// Overline: Regular
|
||||
fonts_[12] = loadFont(io, kWeightRegular, Layout::kFontOverline() * scale, "Overline");
|
||||
|
||||
// ButtonSm: Medium
|
||||
fonts_[13] = loadFont(io, kWeightMedium, Layout::kFontButtonSm() * scale, "ButtonSm");
|
||||
|
||||
// ButtonLg: Medium
|
||||
fonts_[14] = loadFont(io, kWeightMedium, Layout::kFontButtonLg() * scale, "ButtonLg");
|
||||
|
||||
// Monospace (Ubuntu Mono) at caption size — used by the console so terminal text +
|
||||
// pretty-printed JSON columns align. Same size as Caption to keep line heights matched.
|
||||
mono_ = loadFont(io, kWeightMono, Layout::kFontCaption() * scale, "Mono");
|
||||
mono_ = loadFont(io, kWeightMono, Layout::kFontCaption() * scale, "Mono", false);
|
||||
|
||||
// --- Icon fonts at multiple sizes ---
|
||||
// These are standalone fonts (not merged) so we can PushFont(iconXxx) for icon rendering.
|
||||
@@ -247,7 +223,7 @@ bool Typography::load(ImGuiIO& io, float dpiScale)
|
||||
return allLoaded;
|
||||
}
|
||||
|
||||
ImFont* Typography::loadFont(ImGuiIO& io, int weight, float size, const char* name)
|
||||
ImFont* Typography::loadFont(ImGuiIO& io, int weight, float size, const char* name, bool wantEmoji)
|
||||
{
|
||||
// Select font data based on weight
|
||||
const unsigned char* fontData = nullptr;
|
||||
@@ -273,12 +249,10 @@ ImFont* Typography::loadFont(ImGuiIO& io, int weight, float size, const char* na
|
||||
break;
|
||||
}
|
||||
|
||||
// ImGui needs to own a copy of the font data
|
||||
void* fontDataCopy = IM_ALLOC(fontDataLen);
|
||||
memcpy(fontDataCopy, fontData, fontDataLen);
|
||||
|
||||
// Zero-copy: the INCBIN blob is immutable .rodata that outlives the atlas (ImGui 1.92 requires the data to
|
||||
// persist for the atlas lifetime); FontDataOwnedByAtlas=false means ImGui never frees or writes it.
|
||||
ImFontConfig cfg;
|
||||
cfg.FontDataOwnedByAtlas = true; // ImGui will free this
|
||||
cfg.FontDataOwnedByAtlas = false;
|
||||
cfg.OversampleH = 2;
|
||||
cfg.OversampleV = 1;
|
||||
cfg.PixelSnapH = true;
|
||||
@@ -306,18 +280,15 @@ ImFont* Typography::loadFont(ImGuiIO& io, int weight, float size, const char* na
|
||||
weight == kWeightMono ? "Mono" : "Regular";
|
||||
snprintf(cfg.Name, sizeof(cfg.Name), "Ubuntu %s %.0fpx", weightName, size);
|
||||
|
||||
ImFont* font = io.Fonts->AddFontFromMemoryTTF(fontDataCopy, fontDataLen, size, &cfg);
|
||||
ImFont* font = io.Fonts->AddFontFromMemoryTTF(const_cast<unsigned char*>(fontData), static_cast<int>(fontDataLen), size, &cfg);
|
||||
|
||||
if (font) {
|
||||
DEBUG_LOGF("Typography: Loaded %s (%.0fpx) as '%s'\n", name, size, cfg.Name);
|
||||
|
||||
// Merge CJK fallback glyphs (Chinese/Japanese/Korean) from subset font
|
||||
if (g_noto_cjk_subset_size > 0) {
|
||||
void* cjkCopy = IM_ALLOC(g_noto_cjk_subset_size);
|
||||
memcpy(cjkCopy, g_noto_cjk_subset_data, g_noto_cjk_subset_size);
|
||||
|
||||
ImFontConfig cjkCfg;
|
||||
cjkCfg.FontDataOwnedByAtlas = true;
|
||||
cjkCfg.FontDataOwnedByAtlas = false; // zero-copy: static INCBIN blob
|
||||
cjkCfg.MergeMode = true; // merge into the font we just loaded
|
||||
cjkCfg.OversampleH = 1;
|
||||
cjkCfg.OversampleV = 1;
|
||||
@@ -338,7 +309,8 @@ ImFont* Typography::loadFont(ImGuiIO& io, int weight, float size, const char* na
|
||||
cjkCfg.GlyphRanges = cjkRanges;
|
||||
snprintf(cjkCfg.Name, sizeof(cjkCfg.Name), "NotoSansCJK %.0fpx (merge)", size);
|
||||
|
||||
ImFont* mergeResult = io.Fonts->AddFontFromMemoryTTF(cjkCopy, g_noto_cjk_subset_size, size, &cjkCfg);
|
||||
ImFont* mergeResult = io.Fonts->AddFontFromMemoryTTF(const_cast<unsigned char*>(g_noto_cjk_subset_data),
|
||||
static_cast<int>(g_noto_cjk_subset_size), size, &cjkCfg);
|
||||
if (mergeResult) {
|
||||
DEBUG_LOGF("Typography: Merged CJK (%u bytes) into %s OK\n",
|
||||
g_noto_cjk_subset_size, name);
|
||||
@@ -348,15 +320,9 @@ ImFont* Typography::loadFont(ImGuiIO& io, int weight, float size, const char* na
|
||||
}
|
||||
}
|
||||
|
||||
// Merge monochrome emoji for chat + user text (Q12). Only into the small text fonts — baking
|
||||
// ~1400 emoji at heading sizes would bloat the atlas for glyphs no heading needs. ImGui does no
|
||||
// shaping, so single-codepoint emoji render but ZWJ sequences / flags won't compose. The
|
||||
// >0xFFFF ranges require IMGUI_USE_WCHAR32 (imconfig.h).
|
||||
static const char* const kEmojiFonts[] = {
|
||||
"Body1", "Body2", "Subtitle1", "Subtitle2", "Caption", "Overline", "Button", "ButtonSm"
|
||||
};
|
||||
bool wantEmoji = false;
|
||||
for (const char* n : kEmojiFonts) if (strcmp(name, n) == 0) { wantEmoji = true; break; }
|
||||
// Emoji (monochrome subset, or COLR color via FreeType) is merged only into the small text styles; load()
|
||||
// passes `wantEmoji` from its style table (dedup keys on it). ImGui does no shaping (no ZWJ/flags); the
|
||||
// >0xFFFF ranges need IMGUI_USE_WCHAR32.
|
||||
|
||||
// Emoji blob: the COLR/CPAL color font (FreeType-rendered) when color emoji is enabled and this
|
||||
// is a FreeType build, else the monochrome subset (default / non-FreeType path).
|
||||
@@ -371,11 +337,8 @@ ImFont* Typography::loadFont(ImGuiIO& io, int weight, float size, const char* na
|
||||
}
|
||||
#endif
|
||||
if (wantEmoji && emojiSize > 0) {
|
||||
void* emojiCopy = IM_ALLOC(emojiSize);
|
||||
memcpy(emojiCopy, emojiData, emojiSize);
|
||||
|
||||
ImFontConfig emojiCfg;
|
||||
emojiCfg.FontDataOwnedByAtlas = true;
|
||||
emojiCfg.FontDataOwnedByAtlas = false; // zero-copy: static INCBIN blob
|
||||
emojiCfg.MergeMode = true; // merge into the text font just loaded
|
||||
emojiCfg.OversampleH = 1;
|
||||
emojiCfg.OversampleV = 1;
|
||||
@@ -396,17 +359,17 @@ ImFont* Typography::loadFont(ImGuiIO& io, int weight, float size, const char* na
|
||||
snprintf(emojiCfg.Name, sizeof(emojiCfg.Name), "%s %.0fpx (merge)",
|
||||
colorGlyphs ? "Twemoji" : "NotoEmoji", size);
|
||||
|
||||
ImFont* emojiMerge = io.Fonts->AddFontFromMemoryTTF(emojiCopy, emojiSize, size, &emojiCfg);
|
||||
ImFont* emojiMerge = io.Fonts->AddFontFromMemoryTTF(const_cast<unsigned char*>(emojiData),
|
||||
static_cast<int>(emojiSize), size, &emojiCfg);
|
||||
if (emojiMerge) {
|
||||
DEBUG_LOGF("Typography: Merged %s emoji (%u bytes) into %s OK\n",
|
||||
colorGlyphs ? "color" : "mono", emojiSize, name);
|
||||
} else {
|
||||
DEBUG_LOGF("Typography: WARNING — emoji merge FAILED for %s (size=%u)\n", name, size);
|
||||
DEBUG_LOGF("Typography: WARNING — emoji merge FAILED for %s (size=%u)\n", name, emojiSize);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
DEBUG_LOGF("Typography: Failed to load %s\n", name);
|
||||
IM_FREE(fontDataCopy);
|
||||
}
|
||||
|
||||
return font;
|
||||
@@ -414,12 +377,8 @@ ImFont* Typography::loadFont(ImGuiIO& io, int weight, float size, const char* na
|
||||
|
||||
ImFont* Typography::loadIconFont(ImGuiIO& io, float size, const char* name)
|
||||
{
|
||||
// ImGui needs to own a copy of the font data
|
||||
void* fontDataCopy = IM_ALLOC(g_material_icons_size);
|
||||
memcpy(fontDataCopy, g_material_icons_data, g_material_icons_size);
|
||||
|
||||
ImFontConfig cfg;
|
||||
cfg.FontDataOwnedByAtlas = true;
|
||||
cfg.FontDataOwnedByAtlas = false; // zero-copy: static INCBIN blob
|
||||
cfg.OversampleH = 2;
|
||||
cfg.OversampleV = 1;
|
||||
cfg.PixelSnapH = true;
|
||||
@@ -431,12 +390,11 @@ ImFont* Typography::loadIconFont(ImGuiIO& io, float size, const char* name)
|
||||
|
||||
snprintf(cfg.Name, sizeof(cfg.Name), "MDIcons %.0fpx", size);
|
||||
|
||||
ImFont* font = io.Fonts->AddFontFromMemoryTTF(fontDataCopy, g_material_icons_size, size, &cfg);
|
||||
ImFont* font = io.Fonts->AddFontFromMemoryTTF(const_cast<unsigned char*>(g_material_icons_data), static_cast<int>(g_material_icons_size), size, &cfg);
|
||||
if (font) {
|
||||
DEBUG_LOGF("Typography: Loaded icon font %s (%.0fpx)\n", name, size);
|
||||
} else {
|
||||
DEBUG_LOGF("Typography: Failed to load icon font %s\n", name);
|
||||
IM_FREE(fontDataCopy);
|
||||
}
|
||||
return font;
|
||||
}
|
||||
@@ -448,11 +406,8 @@ ImFont* Typography::loadPickaxeFont(ImGuiIO& io, float size, const char* name)
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void* fontDataCopy = IM_ALLOC(g_mdi_pickaxe_subset_size);
|
||||
memcpy(fontDataCopy, g_mdi_pickaxe_subset_data, g_mdi_pickaxe_subset_size);
|
||||
|
||||
ImFontConfig cfg;
|
||||
cfg.FontDataOwnedByAtlas = true;
|
||||
cfg.FontDataOwnedByAtlas = false; // zero-copy: static INCBIN blob
|
||||
cfg.OversampleH = 2;
|
||||
cfg.OversampleV = 1;
|
||||
cfg.PixelSnapH = true;
|
||||
@@ -468,12 +423,11 @@ ImFont* Typography::loadPickaxeFont(ImGuiIO& io, float size, const char* name)
|
||||
|
||||
snprintf(cfg.Name, sizeof(cfg.Name), "MDIPickaxe %.0fpx", size);
|
||||
|
||||
ImFont* font = io.Fonts->AddFontFromMemoryTTF(fontDataCopy, g_mdi_pickaxe_subset_size, size, &cfg);
|
||||
ImFont* font = io.Fonts->AddFontFromMemoryTTF(const_cast<unsigned char*>(g_mdi_pickaxe_subset_data), static_cast<int>(g_mdi_pickaxe_subset_size), size, &cfg);
|
||||
if (font) {
|
||||
DEBUG_LOGF("Typography: Loaded pickaxe font %s (%.0fpx)\n", name, size);
|
||||
} else {
|
||||
DEBUG_LOGF("Typography: Failed to load pickaxe font %s\n", name);
|
||||
IM_FREE(fontDataCopy);
|
||||
}
|
||||
return font;
|
||||
}
|
||||
|
||||
@@ -259,7 +259,7 @@ private:
|
||||
Typography& operator=(const Typography&) = delete;
|
||||
|
||||
// Load fonts at a specific size with a specific weight
|
||||
ImFont* loadFont(ImGuiIO& io, int weight, float size, const char* name);
|
||||
ImFont* loadFont(ImGuiIO& io, int weight, float size, const char* name, bool wantEmoji);
|
||||
|
||||
// Font weight constants
|
||||
static constexpr int kWeightLight = 300;
|
||||
|
||||
Reference in New Issue
Block a user