// Copyright (C) 2024 Jérôme "SirLynix" Leclercq (lynix680@gmail.com) // This file is part of the "Nazara Engine - Utility module" // For conditions of distribution and use, see copyright notice in Config.hpp #include #include #include #include #include #include #include #include namespace Nz { namespace { const UInt8 r_sansationRegular[] = { #include }; } bool FontParams::IsValid() const { return true; // Nothing to test } Font::Font() : m_glyphBorder(s_defaultGlyphBorder), m_minimumStepSize(s_defaultMinimumStepSize) { SetAtlas(s_defaultAtlas); } Font::~Font() { OnFontRelease(this); Destroy(); SetAtlas({}); // Reset our atlas } void Font::ClearGlyphCache() { if (m_atlas) { if (m_atlas.use_count() == 1) m_atlas->Clear(); // Will call OnAtlasCleared else { // At least one font is using this atlas, remove our glyphes for (auto&& [glyphKey, glyphMap] : m_glyphes) { NazaraUnused(glyphKey); for (auto&& [character, glyph] : glyphMap) { NazaraUnused(character); m_atlas->Free(&glyph.atlasRect, &glyph.layerIndex, 1); } } // Free all cached glyphes m_glyphes.clear(); OnFontGlyphCacheCleared(this); } } } void Font::ClearKerningCache() { m_kerningCache.clear(); OnFontKerningCacheCleared(this); } void Font::ClearSizeInfoCache() { m_sizeInfoCache.clear(); OnFontSizeInfoCacheCleared(this); } bool Font::Create(std::unique_ptr data) { NazaraAssert(data, "invalid font data"); Destroy(); m_data = std::move(data); return true; } void Font::Destroy() { if (m_data) { OnFontDestroy(this); ClearGlyphCache(); m_data.reset(); m_kerningCache.clear(); m_sizeInfoCache.clear(); } } bool Font::ExtractGlyph(unsigned int characterSize, char32_t character, TextStyleFlags style, float outlineThickness, FontGlyph* glyph) const { #if NAZARA_UTILITY_SAFE if (!IsValid()) { NazaraError("invalid font"); return false; } #endif return m_data->ExtractGlyph(characterSize, character, style, outlineThickness, glyph); } const std::shared_ptr& Font::GetAtlas() const { return m_atlas; } std::size_t Font::GetCachedGlyphCount(unsigned int characterSize, TextStyleFlags style, float outlineThickness) const { UInt64 key = ComputeKey(characterSize, style, outlineThickness); auto it = m_glyphes.find(key); if (it == m_glyphes.end()) return 0; return it->second.size(); } std::size_t Font::GetCachedGlyphCount() const { std::size_t count = 0; for (auto& pair : m_glyphes) count += pair.second.size(); return count; } std::string Font::GetFamilyName() const { #if NAZARA_UTILITY_SAFE if (!IsValid()) { NazaraError("invalid font"); return std::string("invalid font"); } #endif return m_data->GetFamilyName(); } int Font::GetKerning(unsigned int characterSize, char32_t first, char32_t second) const { #if NAZARA_UTILITY_SAFE if (!IsValid()) { NazaraError("invalid font"); return 0; } #endif // Use a cache as QueryKerning may be costly (may induce an internal size change) auto& map = m_kerningCache[characterSize]; UInt64 key = (static_cast(first) << 32) | second; auto it = map.find(key); if (it == map.end()) { int kerning = m_data->QueryKerning(characterSize, first, second); map.insert(std::make_pair(key, kerning)); return kerning; } else return it->second; } const Font::Glyph& Font::GetGlyph(unsigned int characterSize, TextStyleFlags style, float outlineThickness, char32_t character) const { UInt64 key = ComputeKey(characterSize, style, outlineThickness); return PrecacheGlyph(m_glyphes[key], characterSize, style, outlineThickness, character); } unsigned int Font::GetGlyphBorder() const { return m_glyphBorder; } unsigned int Font::GetMinimumStepSize() const { return m_minimumStepSize; } const Font::SizeInfo& Font::GetSizeInfo(unsigned int characterSize) const { #if NAZARA_UTILITY_SAFE if (!IsValid()) { NazaraError("invalid font"); static SizeInfo dummy; return dummy; } #endif auto it = m_sizeInfoCache.find(characterSize); if (it == m_sizeInfoCache.end()) { SizeInfo sizeInfo; sizeInfo.lineHeight = m_data->QueryLineHeight(characterSize); sizeInfo.underlinePosition = m_data->QueryUnderlinePosition(characterSize); sizeInfo.underlineThickness = m_data->QueryUnderlineThickness(characterSize); FontGlyph glyph; if (m_data->ExtractGlyph(characterSize, ' ', TextStyle_Regular, 0.f, &glyph)) sizeInfo.spaceAdvance = glyph.advance; else { NazaraWarning("Failed to extract space character from font, using half the character size"); sizeInfo.spaceAdvance = characterSize / 2; } it = m_sizeInfoCache.insert(std::make_pair(characterSize, sizeInfo)).first; } return it->second; } std::string Font::GetStyleName() const { #if NAZARA_UTILITY_SAFE if (!IsValid()) { NazaraError("invalid font"); return std::string("invalid font"); } #endif return m_data->GetStyleName(); } bool Font::IsValid() const { return m_data != nullptr; } bool Font::Precache(unsigned int characterSize, TextStyleFlags style, float outlineThickness, char32_t character) const { UInt64 key = ComputeKey(characterSize, style, outlineThickness); return PrecacheGlyph(m_glyphes[key], characterSize, style, outlineThickness, character).valid; } bool Font::Precache(unsigned int characterSize, TextStyleFlags style, float outlineThickness, std::string_view characterSet) const { NazaraAssert(!characterSet.empty(), "empty character set"); UInt64 key = ComputeKey(characterSize, style, outlineThickness); auto& glyphMap = m_glyphes[key]; IterateOnCodepoints(characterSet, [&](std::u32string_view characters) { for (char32_t character : characters) PrecacheGlyph(glyphMap, characterSize, style, outlineThickness, character); return true; }); return true; } void Font::SetAtlas(std::shared_ptr atlas) { if (m_atlas != atlas) { ClearGlyphCache(); m_atlas = std::move(atlas); if (m_atlas) { m_atlasClearedSlot.Connect(m_atlas->OnAtlasCleared, this, &Font::OnAtlasCleared); m_atlasLayerChangeSlot.Connect(m_atlas->OnAtlasLayerChange, this, &Font::OnAtlasLayerChange); } else { m_atlasClearedSlot.Disconnect(); m_atlasLayerChangeSlot.Disconnect(); } OnFontAtlasChanged(this); } } void Font::SetGlyphBorder(unsigned int borderSize) { if (m_glyphBorder != borderSize) { m_glyphBorder = borderSize; ClearGlyphCache(); } } void Font::SetMinimumStepSize(unsigned int minimumStepSize) { if (m_minimumStepSize != minimumStepSize) { NazaraAssert(minimumStepSize != 0, "Minimum step size cannot be zero"); m_minimumStepSize = minimumStepSize; ClearGlyphCache(); } } std::shared_ptr Font::GetDefaultAtlas() { return s_defaultAtlas; } const std::shared_ptr& Font::GetDefault() { // Nous n'initialisons la police par défaut qu'à la demande pour qu'elle prenne // les paramètres par défaut (qui peuvent avoir étés changés par l'utilisateur), // et pour ne pas consommer de la mémoire vive inutilement (si elle n'est jamais utilisée, elle n'est jamais ouverte). if (!s_defaultFont) { s_defaultFont = Font::OpenFromMemory(r_sansationRegular, sizeof(r_sansationRegular)); if (!s_defaultFont) NazaraError("failed to open default font"); } return s_defaultFont; } unsigned int Font::GetDefaultGlyphBorder() { return s_defaultGlyphBorder; } unsigned int Font::GetDefaultMinimumStepSize() { return s_defaultMinimumStepSize; } std::shared_ptr Font::OpenFromFile(const std::filesystem::path& filePath, const FontParams& params) { Utility* utility = Utility::Instance(); NazaraAssert(utility, "Utility module has not been initialized"); return utility->GetFontLoader().LoadFromFile(filePath, params); } std::shared_ptr Font::OpenFromMemory(const void* data, std::size_t size, const FontParams& params) { Utility* utility = Utility::Instance(); NazaraAssert(utility, "Utility module has not been initialized"); return utility->GetFontLoader().LoadFromMemory(data, size, params); } std::shared_ptr Font::OpenFromStream(Stream& stream, const FontParams& params) { Utility* utility = Utility::Instance(); NazaraAssert(utility, "Utility module has not been initialized"); return utility->GetFontLoader().LoadFromStream(stream, params); } void Font::SetDefaultAtlas(std::shared_ptr atlas) { s_defaultAtlas = std::move(atlas); } void Font::SetDefaultGlyphBorder(unsigned int borderSize) { s_defaultGlyphBorder = borderSize; } void Font::SetDefaultMinimumStepSize(unsigned int minimumStepSize) { NazaraAssert(minimumStepSize, "minimum step size cannot be zero as it implies a division by zero"); s_defaultMinimumStepSize = minimumStepSize; } UInt64 Font::ComputeKey(unsigned int characterSize, TextStyleFlags style, float outlineThickness) const { // Adjust size to step size UInt64 sizeStylePart = static_cast((characterSize/m_minimumStepSize)*m_minimumStepSize); sizeStylePart = std::min(sizeStylePart, Nz::IntegralPow(2, 30)); //< 2^30 should be more than enough as a max size sizeStylePart <<= 2; // Store bold and italic flags (other style are handled directly by a TextDrawer) if (style & TextStyle::Bold) sizeStylePart |= 1 << 0; if (style & TextStyle::Italic) sizeStylePart |= 1 << 1; return (sizeStylePart << 32) | reinterpret_cast(outlineThickness); } void Font::OnAtlasCleared(const AbstractAtlas* atlas) { NazaraUnused(atlas); #ifdef NAZARA_DEBUG // Est-ce qu'il s'agit bien de notre atlas ? if (m_atlas.get() != atlas) { NazaraInternalError("Notified by a non-listening-to resource"); return; } #endif // Notre atlas vient d'être vidé, détruisons le cache de glyphe m_glyphes.clear(); OnFontGlyphCacheCleared(this); } void Font::OnAtlasLayerChange(const AbstractAtlas* atlas, AbstractImage* oldLayer, AbstractImage* newLayer) { NazaraUnused(atlas); NazaraUnused(oldLayer); NazaraUnused(newLayer); #ifdef NAZARA_DEBUG // Est-ce qu'il s'agit bien de notre atlas ? if (m_atlas.get() != atlas) { NazaraInternalError("Notified by a non-listening-to resource"); return; } #endif // Pour faciliter le travail des ressources qui nous écoutent OnFontAtlasLayerChanged(this, oldLayer, newLayer); } const Font::Glyph& Font::PrecacheGlyph(GlyphMap& glyphMap, unsigned int characterSize, TextStyleFlags style, float outlineThickness, char32_t character) const { auto it = glyphMap.find(character); if (it != glyphMap.end()) return it->second; Glyph& glyph = glyphMap[character]; //< Insert a new glyph glyph.valid = false; #if NAZARA_UTILITY_SAFE if (!m_atlas) { NazaraError("font has no atlas"); return glyph; } #endif // Check if requested style is supported by our font (otherwise it will need to be simulated) glyph.fauxOutlineThickness = 0.f; glyph.requireFauxBold = false; glyph.requireFauxItalic = false; TextStyleFlags supportedStyle = style; if (style & TextStyle::Bold && !m_data->SupportsStyle(TextStyle::Bold)) { glyph.requireFauxBold = true; supportedStyle &= ~TextStyle::Bold; } if (style & TextStyle::Italic && !m_data->SupportsStyle(TextStyle::Italic)) { glyph.requireFauxItalic = true; supportedStyle &= ~TextStyle::Italic; } float supportedOutlineThickness = outlineThickness; if (outlineThickness > 0.f && !m_data->SupportsOutline(outlineThickness)) { glyph.fauxOutlineThickness = supportedOutlineThickness; supportedOutlineThickness = 0.f; } // Does font support requested style? if (style == supportedStyle && outlineThickness == supportedOutlineThickness) { FontGlyph fontGlyph; if (ExtractGlyph(characterSize, character, style, outlineThickness, &fontGlyph)) { if (fontGlyph.image.IsValid()) { glyph.atlasRect.width = fontGlyph.image.GetWidth(); glyph.atlasRect.height = fontGlyph.image.GetHeight(); } else { glyph.atlasRect.width = 0; glyph.atlasRect.height = 0; } // Insert rectangle (if not empty) into our atlas if (glyph.atlasRect.width > 0 && glyph.atlasRect.height > 0) { // Add a small border to prevent GPU to sample another glyph pixel glyph.atlasRect.width += m_glyphBorder * 2; glyph.atlasRect.height += m_glyphBorder * 2; if (!m_atlas->Insert(fontGlyph.image, &glyph.atlasRect, &glyph.flipped, &glyph.layerIndex)) { NazaraError("failed to insert glyph into atlas"); return glyph; } // Recenter and remove glyph border glyph.atlasRect.x += m_glyphBorder; glyph.atlasRect.y += m_glyphBorder; glyph.atlasRect.width -= m_glyphBorder * 2; glyph.atlasRect.height -= m_glyphBorder * 2; } glyph.aabb = fontGlyph.aabb; glyph.advance = fontGlyph.advance; glyph.valid = true; } else NazaraWarning("Failed to extract glyph \"" + FromUtf32String(std::u32string_view(&character, 1)) + "\""); } else { // Font doesn't support request style, precache the minimal supported version and copy its data UInt64 newKey = ComputeKey(characterSize, supportedStyle, supportedOutlineThickness); const Glyph& referenceGlyph = PrecacheGlyph(m_glyphes[newKey], characterSize, supportedStyle, supportedOutlineThickness, character); if (referenceGlyph.valid) { glyph.aabb = referenceGlyph.aabb; glyph.advance = referenceGlyph.advance; glyph.atlasRect = referenceGlyph.atlasRect; glyph.flipped = referenceGlyph.flipped; glyph.layerIndex = referenceGlyph.layerIndex; glyph.valid = true; } } return glyph; } bool Font::Initialize() { s_defaultAtlas = std::make_shared(); s_defaultGlyphBorder = 1; s_defaultMinimumStepSize = 1; return true; } void Font::Uninitialize() { s_defaultAtlas.reset(); s_defaultFont.reset(); } std::shared_ptr Font::s_defaultAtlas; std::shared_ptr Font::s_defaultFont; unsigned int Font::s_defaultGlyphBorder; unsigned int Font::s_defaultMinimumStepSize; }