summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndrey Lihatskiy <118752495+marchcat@users.noreply.github.com>2026-07-24 12:26:16 +0300
committerGitHub <noreply@github.com>2026-07-24 12:26:16 +0300
commite32d763ccbe1b02c7d74a5b63712ec946a24b42e (patch)
treef09286325dfcda29510115ff6d8c2fdb71e7ff5c
parenta6a2f98070cd55fcaadafc237bd0ffba9d466655 (diff)
parentcd7295e09b354a99c29f2bb80da7c0a77ccb0457 (diff)
Merge pull request #5993 from nibbbl/cjk-lazy-font-fallback
Fix missing CJK/Indic text on Linux with lazy font fallback
-rw-r--r--indra/llrender/llfontfreetype.cpp47
-rw-r--r--indra/llrender/llfontfreetype.h12
-rw-r--r--indra/llwindow/llwindow.cpp14
-rw-r--r--indra/llwindow/llwindow.h10
-rw-r--r--indra/llwindow/llwindowmacosx.cpp6
-rw-r--r--indra/llwindow/llwindowmacosx.h1
-rw-r--r--indra/llwindow/llwindowsdl.cpp120
-rw-r--r--indra/llwindow/llwindowsdl.h1
-rw-r--r--indra/llwindow/llwindowwin32.cpp6
-rw-r--r--indra/llwindow/llwindowwin32.h1
10 files changed, 135 insertions, 83 deletions
diff --git a/indra/llrender/llfontfreetype.cpp b/indra/llrender/llfontfreetype.cpp
index 075c496ec5..cc4d6ad5ef 100644
--- a/indra/llrender/llfontfreetype.cpp
+++ b/indra/llrender/llfontfreetype.cpp
@@ -52,6 +52,7 @@
//#include "imdebug.h"
#include "llfontbitmapcache.h"
#include "llgl.h"
+#include "llwindow.h"
#define ENABLE_OT_SVG_SUPPORT
@@ -188,7 +189,7 @@ bool LLFontFreetype::loadFace(const std::string& filename, F32 point_size, F32 v
return false;
openArgs.flags = FT_OPEN_MEMORY;
- int error = FT_Open_Face( gFTLibrary, &openArgs, 0, &mFTFace );
+ int error = FT_Open_Face( gFTLibrary, &openArgs, face_n, &mFTFace );
if (error)
return false;
@@ -197,6 +198,9 @@ bool LLFontFreetype::loadFace(const std::string& filename, F32 point_size, F32 v
mHinting = hinting;
mFontFlags = flags;
mWeight = weight;
+ mFaceIndex = face_n;
+ mVertDPI = vert_dpi;
+ mHorzDPI = horz_dpi;
bool variable_font = false;
if (weight >= 0)
@@ -314,7 +318,7 @@ S32 LLFontFreetype::getNumFaces(const std::string& filename)
}
void LLFontFreetype::addFallbackFont(const LLPointer<LLFontFreetype>& fallback_font,
- const char_functor_t& functor)
+ const char_functor_t& functor) const
{
mFallbackFonts.emplace_back(fallback_font, functor);
}
@@ -419,6 +423,18 @@ bool LLFontFreetype::hasGlyph(llwchar wch) const
return(mCharGlyphInfoMap.find(wch) != mCharGlyphInfoMap.end());
}
+bool LLFontFreetype::hasFallbackPath(const std::string& path) const
+{
+ for (const fallback_font_t& pair : mFallbackFonts)
+ {
+ if (pair.first->getName() == path)
+ {
+ return true;
+ }
+ }
+ return false;
+}
+
LLFontGlyphInfo* LLFontFreetype::addGlyph(llwchar wch, EFontGlyphType glyph_type) const
{
if (!mFTFace)
@@ -501,6 +517,31 @@ LLFontGlyphInfo* LLFontFreetype::addGlyph(llwchar wch, EFontGlyphType glyph_type
glyph_type);
}
}
+
+ // Nothing above covers this char: ask the OS for a font that does,
+ // load it and attach it.
+ if (mAttemptedFallbackChars.insert(wch).second)
+ {
+ LLFontFallbackMatch match = LLWindow::findFallbackFontForChar(wch);
+ if (!match.mPath.empty() && !hasFallbackPath(match.mPath))
+ {
+ LLPointer<LLFontFreetype> fallback = new LLFontFreetype;
+ if (fallback->loadFace(match.mPath, mPointSize, mVertDPI, mHorzDPI,
+ /*weight*/ -1, /*is_fallback*/ true,
+ match.mFaceIndex, mHinting, mFontFlags))
+ {
+ glyph_index = FT_Get_Char_Index(fallback->mFTFace, wch);
+ if (glyph_index)
+ {
+ LL_DEBUGS("Font") << "Lazy OS fallback for U+" << std::hex << (U32)wch << std::dec
+ << ": " << match.mPath << " (face " << match.mFaceIndex << ")" << LL_ENDL;
+ addFallbackFont(fallback, nullptr);
+ return addGlyphFromFont(fallback, wch, glyph_index, glyph_type);
+ }
+ // Matched font doesn't actually cover wch: discard it.
+ }
+ }
+ }
}
auto range_it = mCharGlyphInfoMap.equal_range(wch);
@@ -728,7 +769,7 @@ void LLFontFreetype::renderGlyph(EFontGlyphType bitmap_type, U32 glyph_index, ll
void LLFontFreetype::reset(F32 vert_dpi, F32 horz_dpi)
{
resetBitmapCache();
- loadFace(mName, mPointSize, vert_dpi ,horz_dpi, mWeight, mIsFallback, 0, mHinting, mFontFlags);
+ loadFace(mName, mPointSize, vert_dpi ,horz_dpi, mWeight, mIsFallback, mFaceIndex, mHinting, mFontFlags);
if (!mIsFallback)
{
// This is the head of the list - need to rebuild ourself and all fallbacks.
diff --git a/indra/llrender/llfontfreetype.h b/indra/llrender/llfontfreetype.h
index d2164e8fa2..12a4052cae 100644
--- a/indra/llrender/llfontfreetype.h
+++ b/indra/llrender/llfontfreetype.h
@@ -34,6 +34,7 @@
#include "llfontbitmapcache.h"
#include <unordered_map>
+#include <unordered_set>
// Hack. FT_Face is just a typedef for a pointer to a struct,
// but there's no simple forward declarations file for FreeType,
@@ -108,7 +109,7 @@ public:
S32 getNumFaces(const std::string& filename);
typedef std::function<bool(llwchar)> char_functor_t;
- void addFallbackFont(const LLPointer<LLFontFreetype>& fallback_font, const char_functor_t& functor = nullptr);
+ void addFallbackFont(const LLPointer<LLFontFreetype>& fallback_font, const char_functor_t& functor = nullptr) const;
// Global font metrics - in units of pixels
F32 getLineHeight() const;
@@ -167,6 +168,7 @@ private:
bool setSubImageBGRA(U32 x, U32 y, U32 bitmap_num, U16 width, U16 height, const U8* data, U32 stride) const;
bool setVariationAxis(const std::string& axis_tag, F32 value);
bool hasGlyph(llwchar wch) const; // Has a glyph for this character
+ bool hasFallbackPath(const std::string& path) const; // Is a fallback font with this file path already attached?
LLFontGlyphInfo* addGlyph(llwchar wch, EFontGlyphType glyph_type) const; // Add a new character to the font if necessary
LLFontGlyphInfo* addGlyphFromFont(
const LLFontFreetype *fontp,
@@ -191,9 +193,15 @@ private:
EFontHinting mHinting;
S32 mFontFlags;
S32 mWeight = -1;
+ S32 mFaceIndex = 0; // Face index within the (possibly collection) font file
+ F32 mVertDPI = 0.f; // Kept so lazily-discovered fallback faces can be
+ F32 mHorzDPI = 0.f; // opened at this font's size (see addGlyph)
typedef std::pair<LLPointer<LLFontFreetype>, char_functor_t> fallback_font_t;
typedef std::vector<fallback_font_t> fallback_font_vector_t;
- fallback_font_vector_t mFallbackFonts; // A list of fallback fonts to look for glyphs in (for Unicode chars)
+ // mutable: fallback fonts are also discovered lazily in addGlyph (const)
+ mutable fallback_font_vector_t mFallbackFonts; // A list of fallback fonts to look for glyphs in (for Unicode chars)
+ // Codepoints we've already asked the OS about, so we only query once each
+ mutable std::unordered_set<llwchar> mAttemptedFallbackChars;
// *NOTE: the same glyph can be present with multiple representations (but the pointer is always unique)
typedef std::unordered_multimap<llwchar, LLFontGlyphInfo*> char_glyph_info_map_t;
diff --git a/indra/llwindow/llwindow.cpp b/indra/llwindow/llwindow.cpp
index 2313aeda50..b6177abfc7 100644
--- a/indra/llwindow/llwindow.cpp
+++ b/indra/llwindow/llwindow.cpp
@@ -268,6 +268,20 @@ std::vector<std::string> LLWindow::getDynamicFallbackFontList()
}
// static
+LLFontFallbackMatch LLWindow::findFallbackFontForChar(llwchar wch)
+{
+#if LL_SDL_WINDOW && !LL_MESA_HEADLESS
+ return LLWindowSDL::findFallbackFontForChar(wch);
+#elif LL_WINDOWS
+ return LLWindowWin32::findFallbackFontForChar(wch);
+#elif LL_DARWIN
+ return LLWindowMacOSX::findFallbackFontForChar(wch);
+#else
+ return LLFontFallbackMatch();
+#endif
+}
+
+// static
std::vector<std::string> LLWindow::getDisplaysResolutionList()
{
#if LL_SDL_WINDOW && !LL_MESA_HEADLESS
diff --git a/indra/llwindow/llwindow.h b/indra/llwindow/llwindow.h
index e9dfa3aba4..327323b018 100644
--- a/indra/llwindow/llwindow.h
+++ b/indra/llwindow/llwindow.h
@@ -38,6 +38,13 @@ class LLSplashScreen;
class LLPreeditor;
class LLWindowCallbacks;
+// Result of an OS font-fallback query; empty mPath means no font was found.
+struct LLFontFallbackMatch
+{
+ std::string mPath;
+ S32 mFaceIndex = 0;
+};
+
// Refer to llwindow_test in test/common/llwindow for usage example
class LLWindow : public LLInstanceTracker<LLWindow>
@@ -183,6 +190,9 @@ public:
static std::vector<std::string> getDynamicFallbackFontList();
+ // Ask the OS for a font file covering the given codepoint (lazy fallback).
+ static LLFontFallbackMatch findFallbackFontForChar(llwchar wch);
+
// Provide native key event data
virtual LLSD getNativeKeyData() { return LLSD::emptyMap(); }
diff --git a/indra/llwindow/llwindowmacosx.cpp b/indra/llwindow/llwindowmacosx.cpp
index f8920318d3..b5dc841c45 100644
--- a/indra/llwindow/llwindowmacosx.cpp
+++ b/indra/llwindow/llwindowmacosx.cpp
@@ -2633,6 +2633,12 @@ std::vector<std::string> LLWindowMacOSX::getDynamicFallbackFontList()
return std::vector<std::string>();
}
+LLFontFallbackMatch LLWindowMacOSX::findFallbackFontForChar(llwchar wch)
+{
+ // Not implemented on macOS; would use CoreText (CTFontCreateForString).
+ return LLFontFallbackMatch();
+}
+
// static
MASK LLWindowMacOSX::modifiersToMask(S16 modifiers)
{
diff --git a/indra/llwindow/llwindowmacosx.h b/indra/llwindow/llwindowmacosx.h
index dc8b7504c9..9534eadf8e 100644
--- a/indra/llwindow/llwindowmacosx.h
+++ b/indra/llwindow/llwindowmacosx.h
@@ -117,6 +117,7 @@ public:
static std::vector<std::string> getDisplaysResolutionList();
static std::vector<std::string> getDynamicFallbackFontList();
+ static LLFontFallbackMatch findFallbackFontForChar(llwchar wch);
// Provide native key event data
LLSD getNativeKeyData() override;
diff --git a/indra/llwindow/llwindowsdl.cpp b/indra/llwindow/llwindowsdl.cpp
index 2d8a74c782..e3ae9493a6 100644
--- a/indra/llwindow/llwindowsdl.cpp
+++ b/indra/llwindow/llwindowsdl.cpp
@@ -1879,100 +1879,64 @@ void LLWindowSDL::bringToFront()
//static
std::vector<std::string> LLWindowSDL::getDynamicFallbackFontList()
{
- std::vector<std::string> rtns;
-#if LL_LINUX
- // Use libfontconfig to find us a nice ordered list of fallback fonts
- // specific to this system.
- std::string final_fallback("/usr/share/fonts/truetype/kochi/kochi-gothic.ttf");
- const int max_font_count_cutoff = 40; // fonts are expensive in the current system, don't enumerate an arbitrary number of them
- // Our 'ideal' font properties which define the sorting results.
- // slant=0 means Roman, index=0 means the first face in a font file
- // (the one we actually use), weight=80 means medium weight,
- // spacing=0 means proportional spacing.
- std::string sort_order("slant=0:index=0:weight=80:spacing=0");
- // elide_unicode_coverage removes fonts from the list whose unicode
- // range is covered by fonts earlier in the list. This usually
- // removes ~90% of the fonts as redundant (which is great because
- // the font list can be huge), but might unnecessarily reduce the
- // renderable range if for some reason our FreeType actually fails
- // to use some of the fonts we want it to.
- const bool elide_unicode_coverage = true;
-
- FcFontSet *fs = nullptr;
- FcPattern *sortpat = nullptr;
-
- LL_INFOS() << "Getting system font list from FontConfig..." << LL_ENDL;
+ // Lazy-loaded fonts, seeded with fonts.xml
+ return std::vector<std::string>();
+}
- // If the user has a system-wide language preference, then favor
- // fonts from that language group. This doesn't affect the types
- // of languages that can be displayed, but ensures that their
- // preferred language is rendered from a single consistent font where
- // possible.
- FL_Locale *locale = nullptr;
- FL_Success success = FL_FindLocale(&locale, FL_MESSAGES);
- if (success != 0)
+LLFontFallbackMatch LLWindowSDL::findFallbackFontForChar(llwchar wch)
+{
+ LLFontFallbackMatch result;
+#if LL_LINUX
+ // FcInit() is idempotent, but this runs per missing codepoint, so only
+ // attempt initialization once.
+ static bool fc_ready = FcInit();
+ if (!fc_ready)
{
- if (success >= 2 && locale->lang) // confident!
- {
- LL_INFOS("AppInit") << "Language " << locale->lang << LL_ENDL;
- LL_INFOS("AppInit") << "Location " << locale->country << LL_ENDL;
- LL_INFOS("AppInit") << "Variant " << locale->variant << LL_ENDL;
-
- LL_INFOS() << "Preferring fonts of language: "
- << locale->lang
- << LL_ENDL;
- sort_order = "lang=" + std::string(locale->lang) + ":"
- + sort_order;
- }
+ LL_WARNS_ONCE() << "FontConfig failed to initialize." << LL_ENDL;
+ return result;
}
- FL_FreeLocale(&locale);
- if (!FcInit())
+ // Ask FontConfig for the best font covering this codepoint.
+ FcCharSet* charset = FcCharSetCreate();
+ FcPattern* pat = FcPatternCreate();
+ if (!charset || !pat)
{
- LL_WARNS() << "FontConfig failed to initialize." << LL_ENDL;
- rtns.push_back(final_fallback);
- return rtns;
+ if (charset) FcCharSetDestroy(charset);
+ if (pat) FcPatternDestroy(pat);
+ return result;
}
- sortpat = FcNameParse((FcChar8*) sort_order.c_str());
- if (sortpat)
- {
- // Sort the list of system fonts from most-to-least-desirable.
- FcResult result;
- fs = FcFontSort(nullptr, sortpat, elide_unicode_coverage, nullptr, &result);
- FcPatternDestroy(sortpat);
- }
+ FcCharSetAddChar(charset, (FcChar32)wch);
+
+ FcPatternAddCharSet(pat, FC_CHARSET, charset);
+ FcPatternAddBool(pat, FC_SCALABLE, FcTrue);
+
+ FcConfigSubstitute(nullptr, pat, FcMatchPattern);
+ FcDefaultSubstitute(pat);
- int found_font_count = 0;
- if (fs)
+ FcResult fc_result;
+ FcPattern* match = FcFontMatch(nullptr, pat, &fc_result);
+ if (match)
{
- // Get the full pathnames to the fonts, where available,
- // which is what we really want.
- found_font_count = fs->nfont;
- for (int i=0; i<fs->nfont; ++i)
+ FcChar8* filename = nullptr;
+ if (FcResultMatch == FcPatternGetString(match, FC_FILE, 0, &filename) && filename)
{
- FcChar8 *filename;
- if (FcResultMatch == FcPatternGetString(fs->fonts[i], FC_FILE, 0, &filename) && filename)
+ result.mPath = (const char*)filename;
+
+ // .ttc/.otc collections carry several faces; get the right one.
+ int index = 0;
+ if (FcResultMatch == FcPatternGetInteger(match, FC_INDEX, 0, &index))
{
- rtns.push_back(std::string((const char*)filename));
- if (rtns.size() >= max_font_count_cutoff)
- break; // hit limit
+ result.mFaceIndex = index;
}
}
- FcFontSetDestroy (fs);
+ FcPatternDestroy(match);
}
- LL_DEBUGS() << "Using font list: " << LL_ENDL;
- for (auto it = rtns.begin(); it != rtns.end(); ++it)
- {
- LL_DEBUGS() << " file: " << *it << LL_ENDL;
- }
-
- LL_INFOS() << "Using " << rtns.size() << "/" << found_font_count << " system fonts." << LL_ENDL;
-
- rtns.push_back(final_fallback);
+ FcPatternDestroy(pat);
+ FcCharSetDestroy(charset);
#endif
- return rtns;
+ return result;
}
void LLWindowSDL::setLanguageTextInput(const LLCoordGL& position)
diff --git a/indra/llwindow/llwindowsdl.h b/indra/llwindow/llwindowsdl.h
index 4d4a7a5f65..7156ad66b7 100644
--- a/indra/llwindow/llwindowsdl.h
+++ b/indra/llwindow/llwindowsdl.h
@@ -157,6 +157,7 @@ public:
void setTitle(const std::string title) override;
static std::vector<std::string> getDynamicFallbackFontList();
+ static LLFontFallbackMatch findFallbackFontForChar(llwchar wch);
void *createSharedContext() override;
void makeContextCurrent(void *context) override;
diff --git a/indra/llwindow/llwindowwin32.cpp b/indra/llwindow/llwindowwin32.cpp
index 07caeff11d..6ac17f8779 100644
--- a/indra/llwindow/llwindowwin32.cpp
+++ b/indra/llwindow/llwindowwin32.cpp
@@ -5023,6 +5023,12 @@ std::vector<std::string> LLWindowWin32::getDynamicFallbackFontList()
// Fonts previously in getFontListSans() have moved to fonts.xml.
return std::vector<std::string>();
}
+
+LLFontFallbackMatch LLWindowWin32::findFallbackFontForChar(llwchar wch)
+{
+ // Not implemented on Windows; would use DirectWrite (IDWriteFontFallback::MapCharacters).
+ return LLFontFallbackMatch();
+}
#endif // LL_WINDOWS
inline LLWindowWin32::LLWindowWin32Thread::LLWindowWin32Thread()
diff --git a/indra/llwindow/llwindowwin32.h b/indra/llwindow/llwindowwin32.h
index 6e7176c5f1..c0a1816cf3 100644
--- a/indra/llwindow/llwindowwin32.h
+++ b/indra/llwindow/llwindowwin32.h
@@ -127,6 +127,7 @@ public:
static PROC WINAPI getProcAddress(const char* func);
static std::vector<std::string> getDisplaysResolutionList();
static std::vector<std::string> getDynamicFallbackFontList();
+ static LLFontFallbackMatch findFallbackFontForChar(llwchar wch);
static void setDPIAwareness();
void* getDirectInput8() override;