diff --git a/cont/LuaUI/callins.lua b/cont/LuaUI/callins.lua index a7e5bb947b..944da0111d 100644 --- a/cont/LuaUI/callins.lua +++ b/cont/LuaUI/callins.lua @@ -94,6 +94,8 @@ CallInsList = { "DrawScreen", "DrawInMiniMap", + "FontsChanged", + "SunChanged", "Explosion", diff --git a/cont/LuaUI/widgets.lua b/cont/LuaUI/widgets.lua index ac2a3adabc..99604a9a62 100644 --- a/cont/LuaUI/widgets.lua +++ b/cont/LuaUI/widgets.lua @@ -169,6 +169,7 @@ local flexCallIns = { 'DrawScreenEffects', 'DrawScreenPost', 'DrawInMiniMap', + 'FontsChanged', 'SunChanged', 'RecvSkirmishAIMessage', } @@ -2167,6 +2168,18 @@ function widgetHandler:DownloadProgress(id, downloaded, total) end end + +-------------------------------------------------------------------------------- +-- +-- Font call-ins +-- + +function widgetHandler:FontsChaged() + for _,w in ripairs(self.FontsChangedList) do + w:FontsChanged() + end +end + -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- diff --git a/cont/base/springcontent/LuaGadgets/callins.lua b/cont/base/springcontent/LuaGadgets/callins.lua index e820e39f8f..ebd2093ff3 100644 --- a/cont/base/springcontent/LuaGadgets/callins.lua +++ b/cont/base/springcontent/LuaGadgets/callins.lua @@ -167,6 +167,8 @@ CALLIN_LIST = { "DrawProjectile", "DrawMaterial", + "FontsChanged", + "SunChanged", -- unsynced message callins diff --git a/cont/base/springcontent/LuaGadgets/gadgets.lua b/cont/base/springcontent/LuaGadgets/gadgets.lua index 2f72136cdb..3c59411c31 100644 --- a/cont/base/springcontent/LuaGadgets/gadgets.lua +++ b/cont/base/springcontent/LuaGadgets/gadgets.lua @@ -2198,6 +2198,15 @@ end -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- +function gadgetHandler:FontsChanged() + for _,g in r_ipairs(self.FontsChangedList) do + g:FontsChanged() + end +end + +-------------------------------------------------------------------------------- +-------------------------------------------------------------------------------- + gadgetHandler:Initialize() -------------------------------------------------------------------------------- diff --git a/rts/Lua/LuaFonts.cpp b/rts/Lua/LuaFonts.cpp index cfaf21c7bf..5e4881a17f 100644 --- a/rts/Lua/LuaFonts.cpp +++ b/rts/Lua/LuaFonts.cpp @@ -25,6 +25,8 @@ bool LuaFonts::PushEntries(lua_State* L) REGISTER_LUA_CFUNC(LoadFont); REGISTER_LUA_CFUNC(DeleteFont); + REGISTER_LUA_CFUNC(AddFallbackFont); + REGISTER_LUA_CFUNC(ClearFallbackFonts); return true; } @@ -205,6 +207,49 @@ int LuaFonts::DeleteFont(lua_State* L) return meta_gc(L); } +/*** Adds a fallback font for the font rendering engine. + * + * Fonts added first will have higher priority. + * When a glyph isn't found when rendering a font, the fallback fonts + * will be searched first, otherwise os fonts will be used. + * + * The application should listen for the unsynced 'FontsChanged' callin so + * modules can clear any already reserved display lists or other relevant + * caches. + * + * Note the callin won't be executed at the time of calling this method, + * but later, on the Update cycle (before other Update and Draw callins). + * + * @function gl.AddFallbackFont + * @string filePath VFS path to the file, for example "fonts/myfont.ttf". Uses VFS.RAW_FIRST access mode. + * @treturn bool success + */ +int LuaFonts::AddFallbackFont(lua_State* L) +{ + RECOIL_DETAILED_TRACY_ZONE; + + const auto font = luaL_checkstring(L, 1); + + const bool res = CFontTexture::AddFallbackFont(font); + lua_pushboolean(L, res); + return 1; +} + +/*** Clears all fallback fonts. + * + * See the note at 'AddFallbackFont' about the 'FontsChanged' callin, + * it also applies when calling this method. + * + * @function gl.ClearFallbackFonts + * @treturn nil + */ +int LuaFonts::ClearFallbackFonts(lua_State* L) +{ + RECOIL_DETAILED_TRACY_ZONE; + + CFontTexture::ClearFallbackFonts(); + return 0; +} /******************************************************************************/ /******************************************************************************/ diff --git a/rts/Lua/LuaFonts.h b/rts/Lua/LuaFonts.h index 3d5f0f912f..c423b12147 100644 --- a/rts/Lua/LuaFonts.h +++ b/rts/Lua/LuaFonts.h @@ -22,6 +22,8 @@ class LuaFonts { private: // call-outs static int LoadFont(lua_State* L); static int DeleteFont(lua_State* L); + static int AddFallbackFont(lua_State* L); + static int ClearFallbackFonts(lua_State* L); private: // userdata call-outs static int Print(lua_State* L); diff --git a/rts/Lua/LuaHandle.cpp b/rts/Lua/LuaHandle.cpp index 1925f142d0..f4e9c39ba8 100644 --- a/rts/Lua/LuaHandle.cpp +++ b/rts/Lua/LuaHandle.cpp @@ -2429,6 +2429,27 @@ void CLuaHandle::ViewResize() RunCallIn(L, cmdStr, 1, 0); } +/*** Called whenever fonts are updated. Signals the game display lists + * and other caches should be discarded. + * + * Gets called before other Update and Draw callins. + * + * @function FontsChanged + */ +void CLuaHandle::FontsChanged() +{ + RECOIL_DETAILED_TRACY_ZONE; + LUA_CALL_IN_CHECK(L); + luaL_checkstack(L, 2, __func__); + + static const LuaHashString cmdStr(__func__); + + if (!cmdStr.GetGlobalFunc(L)) + return; + + RunCallIn(L, cmdStr, 0, 0); +} + /*** * @function SunChanged */ diff --git a/rts/Lua/LuaHandle.h b/rts/Lua/LuaHandle.h index c9e86055f7..a1988ee322 100644 --- a/rts/Lua/LuaHandle.h +++ b/rts/Lua/LuaHandle.h @@ -237,6 +237,8 @@ class CLuaHandle : public CEventClient void ViewResize() override; + void FontsChanged() override; + void SunChanged() override; void DrawGenesis() override; diff --git a/rts/Rendering/Fonts/CFontTexture.cpp b/rts/Rendering/Fonts/CFontTexture.cpp index 6476f97a7c..fee305ffd7 100644 --- a/rts/Rendering/Fonts/CFontTexture.cpp +++ b/rts/Rendering/Fonts/CFontTexture.cpp @@ -23,6 +23,7 @@ #include "Rendering/Textures/Bitmap.h" #include "System/Config/ConfigHandler.h" #include "System/Exceptions.h" +#include "System/EventHandler.h" #include "System/Log/ILog.h" #include "System/FileSystem/FileHandler.h" #include "System/Threading/ThreadPool.h" @@ -88,6 +89,10 @@ class FtLibraryHandler { FtLibraryHandler() : config(nullptr) , lib(nullptr) + #ifdef USE_FONTCONFIG + , gameFontSet(nullptr) + , basePattern(nullptr) + #endif // USE_FONTCONFIG { const FT_Error error = FT_Init_FreeType(&lib); @@ -109,6 +114,12 @@ class FtLibraryHandler { return; FcConfigDestroy(config); + if (gameFontSet) { + FcFontSetDestroy(gameFontSet); + } + if (basePattern) { + FcPatternDestroy(basePattern); + } FcFini(); config = nullptr; #endif @@ -202,6 +213,9 @@ class FtLibraryHandler { } } + gameFontSet = FcFontSetCreate(); + basePattern = FcPatternCreate(); + // init app fonts dir res = FcConfigAppFontAddDir(config, reinterpret_cast("fonts")); if (!res) { @@ -257,9 +271,29 @@ class FtLibraryHandler { static inline bool CanUseFontConfig() { return GetFCConfig() != nullptr; } + #ifdef USE_FONTCONFIG + static FcFontSet *GetGameFontSet() { + return singleton->gameFontSet; + } + static FcPattern *GetBasePattern() { + return singleton->basePattern; + } + static void ClearGameFontSet() { + FcFontSetDestroy(singleton->gameFontSet); + singleton->gameFontSet = FcFontSetCreate(); + } + static void ClearBasePattern() { + FcPatternDestroy(singleton->basePattern); + singleton->basePattern = FcPatternCreate(); + } + #endif private: FcConfig* config; FT_Library lib; + #ifdef USE_FONTCONFIG + FcFontSet *gameFontSet; + FcPattern *basePattern; + #endif static inline std::unique_ptr singleton = nullptr; }; @@ -306,20 +340,12 @@ static inline uint64_t GetKerningHash(char32_t lchar, char32_t rchar) return (static_cast(lchar) << 32) | static_cast(rchar); // 64bit used } -static std::shared_ptr GetFontFace(const std::string& fontfile, const int size) +static std::shared_ptr LoadFontFace(const std::string& fontfile) { RECOIL_DETAILED_TRACY_ZONE; assert(CFontTexture::sync.GetThreadSafety() || Threading::IsMainThread()); auto lock = CFontTexture::sync.GetScopedLock(); - //TODO add support to load fonts by name (needs fontconfig) - - const auto fontKey = fontfile + IntToString(size); - const auto fontIt = fontFaceCache.find(fontKey); - - if (fontIt != fontFaceCache.end() && !fontIt->second.expired()) - return fontIt->second.lock(); - // get the file (no need to cache, takes too little time) std::string fontPath(fontfile); CFileHandler f(fontPath); @@ -359,17 +385,37 @@ static std::shared_ptr GetFontFace(const std::string& fontfile, const throw content_error(fmt::format("FT_New_Face failed: {}", GetFTError(error))); } + return std::make_shared(face.Release(), fontMem); +} + +static std::shared_ptr GetRenderFontFace(const std::string& fontfile, const int size) +{ + RECOIL_DETAILED_TRACY_ZONE; + assert(CFontTexture::sync.GetThreadSafety() || Threading::IsMainThread()); + auto lock = CFontTexture::sync.GetScopedLock(); + + //TODO add support to load fonts by name (needs fontconfig) + + FT_Error error; + + const auto fontKey = fontfile + IntToString(size); + const auto fontIt = fontFaceCache.find(fontKey); + + if (fontIt != fontFaceCache.end() && !fontIt->second.expired()) + return fontIt->second.lock(); + + std::shared_ptr facePtr = LoadFontFace(fontfile); + // set render size - if ((error = FT_Set_Pixel_Sizes(face, 0, size)) != 0) { + if ((error = FT_Set_Pixel_Sizes(facePtr->face, 0, size)) != 0) { throw content_error(fmt::format("FT_Set_Pixel_Sizes failed: {}", GetFTError(error))); } // select unicode charmap - if ((error = FT_Select_Charmap(face, FT_ENCODING_UNICODE)) != 0) { + if ((error = FT_Select_Charmap(facePtr->face, FT_ENCODING_UNICODE)) != 0) { throw content_error(fmt::format("FT_Select_Charmap failed: {}", GetFTError(error))); } - - return (fontFaceCache[fontKey] = std::make_shared(face.Release(), fontMem)).lock(); + return (fontFaceCache[fontKey] = facePtr).lock(); } #endif @@ -407,9 +453,9 @@ static std::shared_ptr GetFontForCharacters(const std::vector GetFontForCharacters(const std::vector ScopedFcFontSet; - if (fs == nullptr) - return nullptr; - if (res != FcResultMatch) - return nullptr; + int nFonts = 0; + bool loadMore = true; + FcResult res; - // iterate returned font list - for (int i = 0; i < fs->nfont; ++i) { - const FcPattern* font = fs->fonts[i]; + // first search game fonts + FcFontSet *sets[] = { FtLibraryHandler::GetGameFontSet() }; + ScopedFcFontSet fs(FcFontSetSort(FtLibraryHandler::GetFCConfig(), sets, 1, pattern, FcFalse, nullptr, &res), &FcFontSetDestroy); + + if (fs != nullptr && res == FcResultMatch) + nFonts = fs->nfont; + + // iterate returned font list, and perform system font search when in need of more fonts + int i = 0; + while (i < nFonts || loadMore) { + if (i == nFonts) { + // now search system fonts + fs = ScopedFcFontSet(FcFontSort(FtLibraryHandler::GetFCConfig(), pattern, FcFalse, nullptr, &res), &FcFontSetDestroy); + if (fs == nullptr || res != FcResultMatch) + return nullptr; + loadMore = false; + nFonts = fs->nfont; + i = 0; + } + const FcPattern* font = fs->fonts[i++]; FcChar8* cFilename = nullptr; FcResult r = FcPatternGetString(font, FC_FILE, 0, &cFilename); @@ -498,7 +556,7 @@ static std::shared_ptr GetFontForCharacters(const std::vectornfont; ++i) { + FcPattern* font = set->fonts[i]; + FcChar8 *file; + if (FcPatternGetString(font, FC_FILE, 0, &file) == FcResultMatch) { + if (fontfile.compare(reinterpret_cast(file)) == 0) { + return true; + } + } + } + + // Load font face + std::shared_ptr facePtr; + try { + facePtr = LoadFontFace(fontfile); + } catch (content_error& ex) { + LOG_L(L_ERROR, "[%s] \"%s\": %s", __func__, fontfile.c_str(), ex.what()); + return false; + } + + // Add fontconfig configuration + FT_Face face = *facePtr; + + // Store pattern + FcPattern* pattern = FcFreeTypeQueryFace(face, reinterpret_cast(fontfile.c_str()), 0, NULL); + if (!FcFontSetAdd(set, pattern)) + { + LOG_L(L_WARNING, "[%s] could not add pattern for %s", __func__, fontfile.c_str()); + return false; + } + // needed?: + //FcConfigSubstitute(FtLibraryHandler::GetFCConfig(), pattern, FcMatchScan); + + // Add to priority fonts pattern + FcChar8* family = nullptr; + if (FcPatternGetString( pattern, FC_FAMILY , 0, &family ) == FcResultMatch) { + FcPattern *basePattern = FtLibraryHandler::GetBasePattern(); + FcPatternAddString(basePattern, FC_FAMILY, family); + } else { + LOG_L(L_WARNING, "[%s] could not add priority for %s", __func__, fontfile.c_str()); + return false; + } + + needsClearGlyphs = true; + + return true; +#else + return false; +#endif +} + +/*** + * + * Clears fontconfig fallbacks + */ +void CFontTexture::ClearFallbackFonts() +{ +#if defined(USE_FONTCONFIG) && !defined(HEADLESS) + if (!FtLibraryHandler::CanUseFontConfig()) + return; + + FtLibraryHandler::ClearBasePattern(); + FtLibraryHandler::ClearGameFontSet(); + + needsClearGlyphs = true; +#endif +} + +/*** + * + * Clears all glyphs for all fonts + */ +void CFontTexture::ClearAllGlyphs() { +#ifndef HEADLESS + RECOIL_DETAILED_TRACY_ZONE; + + bool changed = false; + for (const auto& ft : allFonts) { + auto lf = ft.lock(); + changed |= lf->ClearGlyphs(); + } + if (changed) + eventHandler.FontsChanged(); + + needsClearGlyphs = false; +#endif +} + +/*** + * + * Clears all glyphs for a font + */ +bool CFontTexture::ClearGlyphs() { + RECOIL_DETAILED_TRACY_ZONE; + + bool changed = false; +#ifndef HEADLESS + + // Invalidate glyphs coming from other fonts, or those with the 'not found' glyph. + for (const auto& g : glyphs) { + if (g.second.face->face != shFace->face || g.second.index == 0) { + changed = true; + } + } + + // Always clear failed attempts in case we have any cache here. + failedAttemptsToReplace.clear(); + + if (changed) { + kerningPrecached = {}; + + // clear all glyps + glyphs.clear(); + + // clear atlases + ClearAtlases(32, 32); + + // preload standard glyphs + PreloadGlyphs(); + + // signal need to update texture + ++curTextureUpdate; + } +#endif + return changed; +} void CFontTexture::InitFonts() { @@ -666,6 +876,9 @@ void CFontTexture::Update() { static std::vector> fontsToUpdate; fontsToUpdate.clear(); + if (needsClearGlyphs) + ClearAllGlyphs(); + for (const auto& font : allFonts) { auto lf = font.lock(); if (lf->GlyphAtlasTextureNeedsUpdate() || lf->GlyphAtlasTextureNeedsUpload()) @@ -1043,6 +1256,28 @@ void CFontTexture::ReallocAtlases(bool pre) #endif } +void CFontTexture::ClearAtlases(const int width, const int height) +{ +#ifndef HEADLESS + // refresh the atlasAlloc to reset coordinates + atlasAlloc = CRowAtlasAlloc(); + atlasAlloc.SetMaxSize(globalRendering->maxTextureSize, globalRendering->maxTextureSize); + + // clear atlases + wantedTexWidth = width; + wantedTexHeight = height; + + atlasUpdate.Alloc(wantedTexWidth, wantedTexHeight); + atlasUpdateShadow.Alloc(1, 1); + atlasUpdateShadow = {}; + + if (!atlasGlyphs.empty()) + LOG_L(L_WARNING, "[FontTexture::%s] discarding %u glyph bitmaps", __func__, uint32_t(atlasGlyphs.size())); + + atlasGlyphs.clear(); +#endif +} + bool CFontTexture::GlyphAtlasTextureNeedsUpdate() const { RECOIL_DETAILED_TRACY_ZONE; diff --git a/rts/Rendering/Fonts/CFontTexture.h b/rts/Rendering/Fonts/CFontTexture.h index a0321e49c1..b830df1db3 100644 --- a/rts/Rendering/Fonts/CFontTexture.h +++ b/rts/Rendering/Fonts/CFontTexture.h @@ -112,6 +112,9 @@ class CFontTexture static void InitFonts(); static void KillFonts(); static void Update(); + static bool AddFallbackFont(const std::string& fontfile); + static void ClearFallbackFonts(); + static void ClearAllGlyphs(); inline static spring::WrappedSyncRecursiveMutex sync = {}; protected: @@ -142,14 +145,18 @@ class CFontTexture void UploadGlyphAtlasTexture(); void UploadGlyphAtlasTextureImpl(); private: + void ClearAtlases(const int width, const int height); void CreateTexture(const int width, const int height); void LoadGlyph(std::shared_ptr& f, char32_t ch, unsigned index); + bool ClearGlyphs(); + void PreloadGlyphs(); protected: float GetKerning(const GlyphInfo& lgl, const GlyphInfo& rgl); protected: static inline std::vector> allFonts = {}; static inline const GlyphInfo dummyGlyph = GlyphInfo(); + static inline bool needsClearGlyphs = false; std::array kerningPrecached = {}; // contains ASCII kerning diff --git a/rts/System/EventClient.h b/rts/System/EventClient.h index 79d978c01e..eb6d540e88 100644 --- a/rts/System/EventClient.h +++ b/rts/System/EventClient.h @@ -361,6 +361,8 @@ class CEventClient virtual void DrawShadowUnitsLua() {} virtual void DrawShadowFeaturesLua() {} + virtual void FontsChanged() {} + virtual void GameProgress(int gameFrame); virtual void DrawLoadScreen(); diff --git a/rts/System/EventHandler.cpp b/rts/System/EventHandler.cpp index a0aee9a820..62cdac82b3 100644 --- a/rts/System/EventHandler.cpp +++ b/rts/System/EventHandler.cpp @@ -996,3 +996,12 @@ void CEventHandler::DrawShadowFeaturesLua() } /******************************************************************************/ +/******************************************************************************/ + +void CEventHandler::FontsChanged() +{ + ZoneScoped; + ITERATE_EVENTCLIENTLIST_NA(FontsChanged); +} + +/******************************************************************************/ diff --git a/rts/System/EventHandler.h b/rts/System/EventHandler.h index 66d7126d7e..6a641f9a1f 100644 --- a/rts/System/EventHandler.h +++ b/rts/System/EventHandler.h @@ -242,6 +242,7 @@ class CEventHandler bool GroupChanged(int groupID); + void FontsChanged(); bool GameSetup(const std::string& state, bool& ready, const std::vector< std::pair >& playerStates); void DownloadQueued(int ID, const string& archiveName, const string& archiveType); diff --git a/rts/System/Events.def b/rts/System/Events.def index 0120eab369..28536101bf 100644 --- a/rts/System/Events.def +++ b/rts/System/Events.def @@ -158,6 +158,8 @@ SETUP_EVENT(DrawShadowUnitsLua, MANAGED_BIT | UNSYNCED_BIT) SETUP_EVENT(DrawShadowFeaturesLua, MANAGED_BIT | UNSYNCED_BIT) + SETUP_EVENT(FontsChanged, MANAGED_BIT | UNSYNCED_BIT) + SETUP_EVENT(RenderUnitPreCreated, MANAGED_BIT | UNSYNCED_BIT) SETUP_EVENT(RenderUnitCreated, MANAGED_BIT | UNSYNCED_BIT) SETUP_EVENT(RenderUnitDestroyed, MANAGED_BIT | UNSYNCED_BIT)