From c31258b0f582e6dbcfd1a6d54bbc9ece49774f16 Mon Sep 17 00:00:00 2001 From: Gleb Mazovetskiy Date: Sat, 20 Sep 2025 09:42:08 +0100 Subject: [PATCH] text_render: Support per-glyph overrides Previously, an override font had to contain all the 256 glyphs in the row that it was overriding. Experiments have shown that this would cause huge file increase in CJK scripts (+200MiB or so). Instead, allows the override font to only override some glyphs in a row. For all glyphs (sprites) that have width 0, we now fall back to the base font. --- Source/engine/render/text_render.cpp | 103 ++++++++++++++++++--------- 1 file changed, 70 insertions(+), 33 deletions(-) diff --git a/Source/engine/render/text_render.cpp b/Source/engine/render/text_render.cpp index 002f313e9..c2abebd31 100644 --- a/Source/engine/render/text_render.cpp +++ b/Source/engine/render/text_render.cpp @@ -45,7 +45,39 @@ namespace { constexpr char32_t ZWSP = U'\u200B'; // Zero-width space -ankerl::unordered_dense::map Fonts; +struct OwnedFontStack { + OptionalOwnedClxSpriteList baseFont; + OptionalOwnedClxSpriteList overrideFont; +}; + +struct FontStack { + OptionalClxSpriteList baseFont; + OptionalClxSpriteList overrideFont; + + FontStack() = default; + + explicit FontStack(const OwnedFontStack &owned) + { + if (owned.baseFont.has_value()) baseFont.emplace(*owned.baseFont); + if (owned.overrideFont.has_value()) overrideFont.emplace(*owned.overrideFont); + } + + [[nodiscard]] bool has_value() const // NOLINT(readability-identifier-naming) + { + return baseFont.has_value() || overrideFont.has_value(); + } + + [[nodiscard]] ClxSprite glyph(size_t i) const + { + if (overrideFont.has_value()) { + ClxSprite overrideGlyph = (*overrideFont)[i]; + if (overrideGlyph.width() != 0) return overrideGlyph; + } + return (*baseFont)[i]; + } +}; + +ankerl::unordered_dense::map Fonts; std::array FontSizes = { 12, 24, 30, 42, 46, 22 }; constexpr std::array LineHeights = { 12, 26, 38, 42, 50, 22 }; @@ -153,7 +185,7 @@ uint32_t GetFontId(GameFontTables size, uint16_t row) return (size << 16) | row; } -OptionalClxSpriteList LoadFont(GameFontTables size, text_color color, uint16_t row) +FontStack LoadFont(GameFontTables size, text_color color, uint16_t row) { if (ColorTranslations[color] != nullptr && !ColorTranslationsData[color]) { ColorTranslationsData[color].emplace(); @@ -163,48 +195,52 @@ OptionalClxSpriteList LoadFont(GameFontTables size, text_color color, uint16_t r const uint32_t fontId = GetFontId(size, row); auto hotFont = Fonts.find(fontId); if (hotFont != Fonts.end()) { - return OptionalClxSpriteList(*hotFont->second); + return FontStack(hotFont->second); } - OptionalOwnedClxSpriteList &font = Fonts[fontId]; + OwnedFontStack &font = Fonts[fontId]; char path[32]; - // Try loading the language-specific variant first: - const std::string_view language_code = GetLanguageCode(); - const std::string_view language_tag = language_code.substr(0, 2); - if (language_tag == "zh" || language_tag == "ja" || language_tag == "ko" - || (language_tag == "tr" && row == 0)) { - GetFontPath(language_code, size, row, ".clx", &path[0]); - font = LoadOptionalClx(path); - } - if (!font) { - // Fall back to the base variant: - GetFontPath(size, row, ".clx", &path[0]); - font = LoadOptionalClx(path); + // Load language-specific glyphs: + const std::string_view languageCode = GetLanguageCode(); + const std::string_view lang = languageCode.substr(0, 2); + if (lang == "zh" || lang == "ja" || lang == "ko" + || (lang == "tr" && row == 0)) { + GetFontPath(languageCode, size, row, ".clx", &path[0]); + font.overrideFont = LoadOptionalClx(path); } + // Load the base glyphs: + GetFontPath(size, row, ".clx", &path[0]); + font.baseFont = LoadOptionalClx(path); + #ifndef UNPACKED_MPQS - if (!font) { + if (!font.baseFont.has_value()) { // Could be an old devilutionx.mpq or fonts.mpq with PCX instead of CLX. // // We'll show an error elsewhere (in `CheckArchivesUpToDate`) and we need to load // the font files to display it. char pcxPath[32]; GetFontPath(size, row, "", &pcxPath[0]); - font = LoadPcxSpriteList(pcxPath, /*numFramesOrFrameHeight=*/256, /*transparentColor=*/1); + font.baseFont = LoadPcxSpriteList(pcxPath, /*numFramesOrFrameHeight=*/256, /*transparentColor=*/1); } #endif - if (!font) { + if (!font.baseFont.has_value()) { LogError("Error loading font: {}", path); } - return OptionalClxSpriteList(*font); + return FontStack(font); } class CurrentFont { public: - OptionalClxSpriteList sprite; + FontStack fontStack; + + [[nodiscard]] ClxSprite glyph(size_t i) const + { + return fontStack.glyph(i); + } bool load(GameFontTables size, text_color color, char32_t next) { @@ -213,11 +249,11 @@ public: return true; } - sprite = LoadFont(size, color, unicodeRow); + fontStack = LoadFont(size, color, unicodeRow); hasAttemptedLoad_ = true; currentUnicodeRow_ = unicodeRow; - return sprite; + return fontStack.has_value(); } void clear() @@ -424,9 +460,10 @@ uint32_t DoDrawString(const Surface &out, std::string_view text, Rectangle rect, Point position = characterPosition; MaybeWrap(position, 2, rightMargin, position.x, opts.lineHeight); if (GetAnimationFrame(2, 500) != 0) { - OptionalClxSpriteList baseFont = LoadFont(size, color, 0); - if (baseFont) - DrawFont(out, position, (*baseFont)['|'], color, outline); + FontStack baseFont = LoadFont(size, color, 0); + if (baseFont.has_value()) { + DrawFont(out, position, baseFont.glyph('|'), color, outline); + } } if (opts.renderedCursorPositionOut != nullptr) { *opts.renderedCursorPositionOut = position; @@ -448,7 +485,7 @@ uint32_t DoDrawString(const Surface &out, std::string_view text, Rectangle rect, } const uint8_t frame = next & 0xFF; - const uint16_t width = (*currentFont.sprite)[frame].width(); + const uint16_t width = currentFont.glyph(frame).width(); if (next == U'\n' || characterPosition.x + width > rightMargin) { if (next == '\n') maybeDrawCursor(); @@ -473,7 +510,7 @@ uint32_t DoDrawString(const Surface &out, std::string_view text, Rectangle rect, continue; } - const ClxSprite glyph = (*currentFont.sprite)[frame]; + const ClxSprite glyph = currentFont.glyph(frame); const auto byteIndex = static_cast(text.size() - remaining.size()); // Draw highlight @@ -528,7 +565,7 @@ int GetLineWidth(std::string_view text, GameFontTables size, int spacing, int *c } const uint8_t frame = next & 0xFF; - lineWidth += (*currentFont.sprite)[frame].width() + spacing; + lineWidth += currentFont.glyph(frame).width() + spacing; ++codepoints; } if (charactersInLine != nullptr) @@ -599,7 +636,7 @@ int GetLineWidth(std::string_view fmt, DrawStringFormatArg *args, std::size_t ar } const uint8_t frame = next & 0xFF; - lineWidth += (*currentFont.sprite)[frame].width() + spacing; + lineWidth += currentFont.glyph(frame).width() + spacing; ++codepoints; } if (charactersInLine != nullptr) @@ -660,7 +697,7 @@ std::string WordWrapString(std::string_view text, unsigned width, GameFontTables } } - lineWidth += (*currentFont.sprite)[frame].width() + spacing; + lineWidth += currentFont.glyph(frame).width() + spacing; } if (IsBreakableWhitespace(codepoint)) { @@ -840,7 +877,7 @@ void DrawStringWithColors(const Surface &out, std::string_view fmt, DrawStringFo } const uint8_t frame = next & 0xFF; - const uint16_t width = (*currentFont.sprite)[frame].width(); + const uint16_t width = currentFont.glyph(frame).width(); if (next == U'\n' || characterPosition.x + width > rightMargin) { const int nextLineY = characterPosition.y + opts.lineHeight; if (nextLineY >= bottomMargin) @@ -871,7 +908,7 @@ void DrawStringWithColors(const Surface &out, std::string_view fmt, DrawStringFo continue; } - DrawFont(clippedOut, characterPosition, (*currentFont.sprite)[frame], curColor, outlined); + DrawFont(clippedOut, characterPosition, currentFont.glyph(frame), curColor, outlined); characterPosition.x += width + opts.spacing; }