From 610ac9adb3ea929ac5e95ed0dba7e79a975bc623 Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Mon, 10 Oct 2022 20:32:35 +0200 Subject: [PATCH] Implemented dynamic texture allocation to the font handling. --- es-core/src/resources/Font.cpp | 138 +++++++++++++-------------------- es-core/src/resources/Font.h | 6 +- 2 files changed, 55 insertions(+), 89 deletions(-) diff --git a/es-core/src/resources/Font.cpp b/es-core/src/resources/Font.cpp index c89f4dafc..a4de6521f 100644 --- a/es-core/src/resources/Font.cpp +++ b/es-core/src/resources/Font.cpp @@ -22,12 +22,12 @@ Font::Font(int size, const std::string& path) , mFontSize(size) , mMaxGlyphHeight {0} { - if (mFontSize < 9) { - mFontSize = 9; + if (mFontSize < 3) { + mFontSize = 3; LOG(LogWarning) << "Requested font size too small, changing to minimum supported size"; } - else if (mFontSize > Renderer::getScreenHeight()) { - mFontSize = static_cast(Renderer::getScreenHeight()); + else if (mFontSize > Renderer::getScreenHeight() * 1.5f) { + mFontSize = static_cast(Renderer::getScreenHeight() * 1.5f); LOG(LogWarning) << "Requested font size too large, changing to maximum supported size"; } @@ -205,12 +205,12 @@ TextCache* Font::buildTextCache(const std::string& text, cache->vertexLists.resize(vertMap.size()); cache->metrics = {sizeText(text, lineSpacing)}; - unsigned int i {0}; + size_t i {0}; for (auto it = vertMap.cbegin(); it != vertMap.cend(); ++it) { TextCache::VertexList& vertList = cache->vertexLists.at(i); - vertList.textureIdPtr = &it->first->textureId; vertList.verts = it->second; + ++i; } clearFaceCache(); @@ -402,23 +402,24 @@ float Font::getLetterHeight() std::shared_ptr Font::getFromTheme(const ThemeData::ThemeElement* elem, unsigned int properties, - const std::shared_ptr& orig) + const std::shared_ptr& orig, + const float maxHeight) { using namespace ThemeFlags; if (!(properties & FONT_PATH) && !(properties & FONT_SIZE)) return orig; - std::shared_ptr font; int size {static_cast(orig ? orig->mFontSize : FONT_SIZE_MEDIUM)}; std::string path {orig ? orig->mPath : getDefaultPath()}; - float sh {static_cast(Renderer::getScreenHeight())}; + float screenHeight {static_cast(Renderer::getScreenHeight())}; - // Make sure the size is not unreasonably large (which may be caused by a mistake in the - // theme configuration). if (properties & FONT_SIZE && elem->has("fontSize")) - size = glm::clamp(static_cast(sh * elem->get("fontSize")), 0, - static_cast(Renderer::getInstance()->getScreenHeight())); + size = static_cast(glm::clamp(screenHeight * elem->get("fontSize"), + screenHeight * 0.001f, screenHeight * 1.5f)); + + if (maxHeight != 0.0f && static_cast(size) > maxHeight) + size = static_cast(maxHeight); if (properties & FONT_PATH && elem->has("fontPath")) path = elem->get("fontPath"); @@ -438,7 +439,7 @@ size_t Font::getMemUsage() const { size_t memUsage {0}; for (auto it = mTextures.cbegin(); it != mTextures.cend(); ++it) - memUsage += it->textureSize.x * it->textureSize.y * 4; + memUsage += (*it)->textureSize.x * (*it)->textureSize.y * 4; for (auto it = mFaceCache.cbegin(); it != mFaceCache.cend(); ++it) memUsage += it->second->data.length; @@ -495,36 +496,12 @@ std::vector Font::getFallbackFontPaths() Font::FontTexture::FontTexture(const int mFontSize) { textureId = 0; - - // This is a hack to add some extra texture size when running at very low resolutions. If not - // doing this, the use of fallback fonts (such as Japanese characters) could result in the - // texture not fitting the glyphs. - int extraTextureSize {0}; - const float screenSizeModifier { - std::min(Renderer::getScreenWidthModifier(), Renderer::getScreenHeightModifier())}; - - if (screenSizeModifier < 0.2f) - extraTextureSize += 6; - if (screenSizeModifier < 0.45f) - extraTextureSize += 4; - - // It's not entirely clear if the 20 and 22 constants are correct, but they seem to provide - // a texture buffer large enough to hold the fonts. This logic is obviously a hack though - // and needs to be properly reviewed and improved. - textureSize = - glm::ivec2 {mFontSize * (20 + extraTextureSize), mFontSize * (22 + extraTextureSize / 2)}; - - // Make sure the size is not unreasonably large (which may be caused by a mistake in the - // theme configuration). - if (textureSize.x > static_cast(Renderer::getScreenWidth()) * 10) - textureSize.x = - glm::clamp(textureSize.x, 0, static_cast(Renderer::getScreenWidth()) * 10); - if (textureSize.y > static_cast(Renderer::getScreenHeight()) * 10) - textureSize.y = - glm::clamp(textureSize.y, 0, static_cast(Renderer::getScreenHeight()) * 10); - - writePos = glm::ivec2 {0, 0}; rowHeight = 0; + writePos = glm::ivec2 {0, 0}; + + // Set the texture to a reasonable size, if we run out of space for adding glyphs then + // more textures will be created dynamically. + textureSize = glm::ivec2 {mFontSize * 6, mFontSize * 6}; } Font::FontTexture::~FontTexture() @@ -540,16 +517,14 @@ bool Font::FontTexture::findEmpty(const glm::ivec2& size, glm::ivec2& cursor_out if (writePos.x + size.x >= textureSize.x && writePos.y + rowHeight + size.y + 1 < textureSize.y) { - // Row full, but it should fit on the next row so move the cursor there. + // Row is full, but the glyph should fit on the next row so move the cursor there. // Leave 1px of space between glyphs. writePos = glm::ivec2 {0, writePos.y + rowHeight + 1}; rowHeight = 0; } - if (writePos.x + size.x >= textureSize.x || writePos.y + size.y >= textureSize.y) { - // Nope, still won't fit. - return false; - } + if (writePos.x + size.x >= textureSize.x || writePos.y + size.y >= textureSize.y) + return false; // No it still won't fit. cursor_out = writePos; // Leave 1px of space between glyphs. @@ -608,7 +583,7 @@ void Font::rebuildTextures() { // Recreate OpenGL textures. for (auto it = mTextures.begin(); it != mTextures.end(); ++it) - it->initTexture(); + (*it)->initTexture(); // Re-upload the texture data. for (auto it = mGlyphMap.cbegin(); it != mGlyphMap.cend(); ++it) { @@ -635,7 +610,7 @@ void Font::rebuildTextures() void Font::unloadTextures() { for (auto it = mTextures.begin(); it != mTextures.end(); ++it) - it->deinitTexture(); + (*it)->deinitTexture(); } void Font::getTextureForNewGlyph(const glm::ivec2& glyphSize, @@ -643,29 +618,19 @@ void Font::getTextureForNewGlyph(const glm::ivec2& glyphSize, glm::ivec2& cursor_out) { if (mTextures.size()) { - // Check if the most recent texture has space. - tex_out = &mTextures.back(); + // Check if the most recent texture has space available for the glyph. + tex_out = mTextures.back().get(); // Will this one work? if (tex_out->findEmpty(glyphSize, cursor_out)) return; // Yes. } - // This should never happen, assuming the texture size is large enough to fit the font, - // as set in the FontTexture constructor. In the unlikely situation that it still happens, - // setting the texture to nullptr makes sure the application doesn't crash and that the - // user is clearly notified of the problem by the fact that the glyph/character will be - // completely missing. - if (mGlyphMap.size() > 0) { - tex_out = nullptr; - return; - } - - mTextures.push_back(FontTexture(mFontSize)); - tex_out = &mTextures.back(); + mTextures.emplace_back(std::make_unique(mFontSize)); + tex_out = mTextures.back().get(); tex_out->initTexture(); - bool ok = tex_out->findEmpty(glyphSize, cursor_out); + bool ok {tex_out->findEmpty(glyphSize, cursor_out)}; if (!ok) { LOG(LogError) << "Glyph too big to fit on a new texture (glyph size > " << tex_out->textureSize.x << ", " << tex_out->textureSize.y << ")"; @@ -677,15 +642,11 @@ FT_Face Font::getFaceForChar(unsigned int id) { static const std::vector fallbackFonts {getFallbackFontPaths()}; - // Look through our current font + fallback fonts to see if any have the - // glyph we're looking for. + // Look for the glyph in our current font and then in the fallback fonts if needed. for (unsigned int i = 0; i < fallbackFonts.size() + 1; ++i) { auto fit = mFaceCache.find(i); - // Doesn't exist yet. if (fit == mFaceCache.cend()) { - // i == 0 -> mPath - // Otherwise, take from fallbackFonts. const std::string& path {i == 0 ? mPath : fallbackFonts.at(i - 1)}; ResourceData data {ResourceManager::getInstance().getFileData(path)}; mFaceCache[i] = @@ -697,18 +658,18 @@ FT_Face Font::getFaceForChar(unsigned int id) return fit->second->face; } - // Nothing has a valid glyph - return the "real" face so we get a "missing" character. + // Couldn't find a valid glyph, return the "real" face so we get a "missing" character. return mFaceCache.cbegin()->second->face; } Font::Glyph* Font::getGlyph(const unsigned int id) { - // Is it already loaded? + // Check if the glyph has already been loaded. auto it = mGlyphMap.find(id); if (it != mGlyphMap.cend()) return &it->second; - // Nope, need to make a glyph. + // We need to create a new entry. FT_Face face {getFaceForChar(id)}; if (!face) { LOG(LogError) << "Couldn't find appropriate font face for character " << id << " for font " @@ -716,22 +677,29 @@ Font::Glyph* Font::getGlyph(const unsigned int id) return nullptr; } - FT_GlyphSlot g {face->glyph}; + const FT_GlyphSlot glyphSlot {face->glyph}; - if (FT_Load_Char(face, id, FT_LOAD_RENDER)) { + // TODO: Evaluate/test hinting when HarfBuzz has been added. + // If the font does not contain hinting information then force the use of the automatic + // hinter that is built into FreeType. + // const bool hasHinting {static_cast(glyphSlot->face->face_flags & FT_FACE_FLAG_HINTER)}; + const bool hasHinting {true}; + + if (FT_Load_Char(face, id, + (hasHinting ? + FT_LOAD_RENDER : + FT_LOAD_RENDER | FT_LOAD_FORCE_AUTOHINT | FT_LOAD_TARGET_LIGHT))) { LOG(LogError) << "Couldn't find glyph for character " << id << " for font " << mPath << ", size " << mFontSize; return nullptr; } - glm::ivec2 glyphSize {g->bitmap.width, g->bitmap.rows}; - FontTexture* tex {nullptr}; glm::ivec2 cursor {0, 0}; + const glm::ivec2 glyphSize {glyphSlot->bitmap.width, glyphSlot->bitmap.rows}; getTextureForNewGlyph(glyphSize, tex, cursor); - // getTextureForNewGlyph can fail if the glyph is bigger than the max texture - // size (absurdly large font size). + // This should (hopefully) never occur as size constraints are enforced earlier on. if (tex == nullptr) { LOG(LogError) << "Couldn't create glyph for character " << id << " for font " << mPath << ", size " << mFontSize << " (no suitable texture found)"; @@ -747,20 +715,18 @@ Font::Glyph* Font::getGlyph(const unsigned int id) glyph.texSize = glm::vec2 {glyphSize.x / static_cast(tex->textureSize.x), glyphSize.y / static_cast(tex->textureSize.y)}; - glyph.advance = glm::vec2 {static_cast(g->metrics.horiAdvance) / 64.0f, - static_cast(g->metrics.vertAdvance) / 64.0f}; - glyph.bearing = glm::vec2 {static_cast(g->metrics.horiBearingX) / 64.0f, - static_cast(g->metrics.horiBearingY) / 64.0f}; + glyph.advance = glm::vec2 {static_cast(glyphSlot->metrics.horiAdvance) / 64.0f, + static_cast(glyphSlot->metrics.vertAdvance) / 64.0f}; + glyph.bearing = glm::vec2 {static_cast(glyphSlot->metrics.horiBearingX) / 64.0f, + static_cast(glyphSlot->metrics.horiBearingY) / 64.0f}; // Upload glyph bitmap to texture. mRenderer->updateTexture(tex->textureId, Renderer::TextureType::RED, cursor.x, cursor.y, - glyphSize.x, glyphSize.y, g->bitmap.buffer); + glyphSize.x, glyphSize.y, glyphSlot->bitmap.buffer); - // Update max glyph height. if (glyphSize.y > mMaxGlyphHeight) mMaxGlyphHeight = glyphSize.y; - // Done. return &glyph; } diff --git a/es-core/src/resources/Font.h b/es-core/src/resources/Font.h index f2f4ea09d..df2b5d506 100644 --- a/es-core/src/resources/Font.h +++ b/es-core/src/resources/Font.h @@ -92,7 +92,8 @@ public: static std::shared_ptr getFromTheme(const ThemeData::ThemeElement* elem, unsigned int properties, - const std::shared_ptr& orig); + const std::shared_ptr& orig, + const float maxHeight = 0.0f); // Returns an approximation of VRAM used by this font's texture (in bytes). size_t getMemUsage() const; @@ -159,11 +160,10 @@ private: static inline FT_Library sLibrary {nullptr}; static inline std::map, std::weak_ptr> sFontMap; - static inline std::vector mFallbackFonts; Renderer* mRenderer; + std::vector> mTextures; std::map> mFaceCache; - std::vector mTextures; std::map mGlyphMap; const std::string mPath;