From bdd94bd2640294dab43facee29ea8d84d95e9dde Mon Sep 17 00:00:00 2001 From: trompetin17 Date: Fri, 16 May 2025 18:02:40 -0500 Subject: [PATCH] Add font engine aware of Gui.scale This commit enhances text rendering by leveraging FreeType and a dynamic font atlas. Previously, GUI scaling relied on bitmap fonts, which led to blurry and distorted text when scaling up. Now, we scale the font size directly using FreeType, resulting in much sharper and more readable text at any GUI scale. Key improvements: - Replaced bitmap font scaling with true font size scaling via FreeType. - Reduced glyph cache dependency on fixed positions, relying on shader scaling instead. - Switched to float-based glyph positioning for more accurate rendering and scaling. These changes ensure clean and consistent text appearance across different GUI scales. --- source/graphics/Font.cpp | 106 ++++++++++++++--------- source/graphics/Font.h | 50 ++++------- source/graphics/FontManager.cpp | 5 +- source/graphics/FontMetrics.cpp | 10 +-- source/graphics/FontMetrics.h | 10 +-- source/graphics/TextRenderer.cpp | 8 +- source/gui/CGUISprite.cpp | 18 +++- source/gui/CGUISprite.h | 11 ++- source/gui/CGUIText.cpp | 6 +- source/gui/GUIRenderer.cpp | 8 +- source/gui/GUIRenderer.h | 4 +- source/gui/ObjectBases/IGUITextOwner.cpp | 4 +- source/gui/SettingTypes/CGUIString.cpp | 10 +-- source/gui/tests/test_CGUIText.h | 18 ++-- source/ps/ProfileViewer.cpp | 6 +- source/ps/scripting/JSInterface_Main.cpp | 4 +- 16 files changed, 155 insertions(+), 123 deletions(-) diff --git a/source/graphics/Font.cpp b/source/graphics/Font.cpp index 535a20c4dd..42ade45ec4 100644 --- a/source/graphics/Font.cpp +++ b/source/graphics/Font.cpp @@ -18,19 +18,32 @@ #include "precompiled.h" #include "Font.h" -#include "graphics/FontManager.h" #include "graphics/TextureManager.h" -#include "ps/Filesystem.h" #include "ps/CLogger.h" #include "ps/Profile.h" #include -#include +#include #include #include namespace { +/** +* FreeType represents most of its size and position values in 26.6 fixed-point format — that is, +* 26 bits for the integer part and 6 bits for the fractional part. +* FreeType's metrics such as: ascender, descender, height, advance, etc. are measured in 1/64th of a pixel. +*/ +inline FT_F26Dot6 FloatToF26Dot6(float value) +{ + return static_cast(std::lround(value * 64.0f)); +} + +inline float FPosF26Dot6ToFloat(FT_Pos value) +{ + return static_cast(value) / 64.0f; +} + struct FTGlyphDeleter { void operator()(FT_Glyph glyph) const { @@ -59,7 +72,15 @@ void CFont::GlyphMap::set(u16 codepoint, const GlyphData& glyph) (*m_Data[codepoint >> 8])[codepoint & 0xff].defined = 1; } -int CFont::GetCharacterWidth(wchar_t c) +float CFont::GetLineSpacing() const { + return m_LineSpacing / m_Scale; +} + +float CFont::GetHeight() const { + return m_Height / m_Scale; +} + +float CFont::GetCharacterWidth(wchar_t c) { PROFILE2("GetCharacterWidth font texture generate"); const CFont::GlyphData* g{GetGlyph(c)}; @@ -67,11 +88,11 @@ int CFont::GetCharacterWidth(wchar_t c) return g ? g->xadvance : 0; } -void CFont::CalculateStringSize(const wchar_t* string, int& width, int& height) +void CFont::CalculateStringSize(const wchar_t* string, float& width, float& height) { PROFILE2("CalculateStringSize font texture generate"); width = 0; - height = m_Height; + height = GetHeight(); // Compute the width as the width of the longest line. std::wistringstream stream{string}; @@ -79,7 +100,7 @@ void CFont::CalculateStringSize(const wchar_t* string, int& width, int& height) while (std::getline(stream, line)) { FT_UInt glyphIndexStorage{0}; - const int lineWidth{std::accumulate(line.begin(), line.end(), 0, [&](int sum, wchar_t c) + const float lineWidth{std::accumulate(line.begin(), line.end(), 0.0f, [&](float sum, wchar_t c) { const CFont::GlyphData* g{GetGlyph(c)}; @@ -101,20 +122,21 @@ void CFont::CalculateStringSize(const wchar_t* string, int& width, int& height) FT_Vector kerning; FT_Get_Kerning(m_Font.get(), prevGlyph, glyphIndex, FT_KERNING_DEFAULT, &kerning); // Add the kerning distance. - return sum + g->xadvance + static_cast(kerning.x >> SUBPIXEL_SHIFT); + return sum + g->xadvance + FPosF26Dot6ToFloat(kerning.x); }) }; width = std::max(width, lineWidth); - height += m_LineSpacing; + height += GetLineSpacing(); } } -bool CFont::SetFontFromPath(const std::string& fontPath, const std::string& fontName, int size, int strokeWidth) +bool CFont::SetFontFromPath(const std::string& fontPath, const std::string& fontName, float size, float strokeWidth, float scale) { // TODO: expose the Stroke Width outside class. - m_StrokeWidth = strokeWidth; - m_FontSize = size; + m_Scale = scale; + m_StrokeWidth = strokeWidth * scale; + m_FontSize = size * scale; m_FontName = fontName; FT_Face face; @@ -131,7 +153,7 @@ bool CFont::SetFontFromPath(const std::string& fontPath, const std::string& font m_Font.reset(face); // Set the font size. - if (FT_Error error{FT_Set_Pixel_Sizes(m_Font.get(), 0, m_FontSize)}) + if (FT_Error error{FT_Set_Char_Size(m_Font.get(), 0, FloatToF26Dot6(m_FontSize), 0 , 0)}) { LOGERROR("Failed to set font size %d: %d", size, error); return false; @@ -146,7 +168,7 @@ bool CFont::SetFontFromPath(const std::string& fontPath, const std::string& font return false; } m_Stroker.reset(stroker); - FT_Stroker_Set(m_Stroker.get(), m_StrokeWidth * 64, FT_STROKER_LINECAP_ROUND, FT_STROKER_LINEJOIN_ROUND, 0); + FT_Stroker_Set(m_Stroker.get(), FloatToF26Dot6(m_StrokeWidth), FT_STROKER_LINECAP_ROUND, FT_STROKER_LINEJOIN_ROUND, 0); } if (!ConstructTextureAtlas()) @@ -156,10 +178,10 @@ bool CFont::SetFontFromPath(const std::string& fontPath, const std::string& font } // Get the height of the font. - m_Height = m_Font->size->metrics.height >> SUBPIXEL_SHIFT; + m_Height = FPosF26Dot6ToFloat(m_Font->size->metrics.height); // Get the line spacing of the font. - m_LineSpacing = (m_Font->size->metrics.ascender - m_Font->size->metrics.descender) >> SUBPIXEL_SHIFT; + m_LineSpacing = FPosF26Dot6ToFloat(m_Font->size->metrics.ascender - m_Font->size->metrics.descender); m_IsLoading = true; @@ -265,13 +287,13 @@ const CFont::GlyphData* CFont::ExtractAndGenerateGlyph(u16 codepoint) } UniqueFTGlyph glyphPtr(glyph); - const int baselineInAtlas{static_cast(m_Font->size->metrics.ascender >> SUBPIXEL_SHIFT)}; - const int glyphW{static_cast(slot->advance.x >> SUBPIXEL_SHIFT)}; + const float baselineInAtlas{FPosF26Dot6ToFloat(m_Font->size->metrics.ascender)}; + const float glyphW{FPosF26Dot6ToFloat(slot->advance.x)}; if (m_AtlasX + glyphW + m_StrokeWidth + m_AtlasPadding > m_AtlasWidth) { m_AtlasX = 0; - m_AtlasY += m_Height + m_AtlasPadding; + m_AtlasY += std::ceil(m_Height + m_StrokeWidth + m_AtlasPadding); } if (m_AtlasY + m_Height + m_StrokeWidth + m_AtlasPadding > m_AtlasHeight) @@ -281,13 +303,13 @@ const CFont::GlyphData* CFont::ExtractAndGenerateGlyph(u16 codepoint) } m_IsDirty = true; - CFont::Offset offset{0,0}; + CVector2D offset{0,0}; const FT_Render_Mode renderMode{FT_RENDER_MODE_NORMAL}; if (m_StrokeWidth) { - std::optional offsetStroke{GenerateStrokeGlyphBitmap(glyph, codepoint, renderMode, baselineInAtlas)}; + std::optional offsetStroke{GenerateStrokeGlyphBitmap(glyph, codepoint, renderMode, baselineInAtlas)}; if (!offsetStroke.has_value()) { LOGERROR("Failed to generate stroke glyph %u", codepoint); @@ -297,7 +319,7 @@ const CFont::GlyphData* CFont::ExtractAndGenerateGlyph(u16 codepoint) offset = offsetStroke.value(); } - std::optional offsetGlyph{GenerateGlyphBitmap(glyph, codepoint, renderMode, offset, baselineInAtlas)}; + std::optional offsetGlyph{GenerateGlyphBitmap(glyph, codepoint, renderMode, offset, baselineInAtlas)}; if (!offsetGlyph.has_value()) { LOGERROR("Failed to generate glyph %u", codepoint); @@ -308,26 +330,26 @@ const CFont::GlyphData* CFont::ExtractAndGenerateGlyph(u16 codepoint) CFont::GlyphData gd; gd.u0 = static_cast(m_AtlasX) / m_AtlasWidth; gd.v0 = static_cast(m_AtlasY) / m_AtlasHeight; - gd.u1 = static_cast(m_AtlasX - offset.x + glyphW + m_StrokeWidth * 2) / m_AtlasWidth; - gd.v1 = static_cast(m_AtlasY + offset.y + m_Height + m_StrokeWidth * 2) / m_AtlasHeight; + gd.u1 = static_cast(m_AtlasX - offset.X + glyphW + m_StrokeWidth * 2) / m_AtlasWidth; + gd.v1 = static_cast(m_AtlasY + offset.Y + m_Height + m_StrokeWidth * 2) / m_AtlasHeight; - gd.x0 = offset.x - m_StrokeWidth; - gd.y0 = - (m_Height + offset.y + m_StrokeWidth); - gd.x1 = glyphW + m_StrokeWidth; - gd.y1 = m_StrokeWidth; + gd.x0 = (offset.X - m_StrokeWidth) / m_Scale; + gd.y0 = (-(m_Height + offset.Y + m_StrokeWidth)) / m_Scale; + gd.x1 = (glyphW + m_StrokeWidth) / m_Scale; + gd.y1 = m_StrokeWidth / m_Scale; - gd.xadvance = glyphW; + gd.xadvance = glyphW / m_Scale; gd.defined = 1; m_Glyphs.set(codepoint, gd); // Update positions for next glyph. - m_AtlasX += glyphW + m_StrokeWidth + m_AtlasPadding; + m_AtlasX += std::ceil(glyphW + m_StrokeWidth + m_AtlasPadding); return m_Glyphs.get(codepoint); } -std::optional CFont::GenerateStrokeGlyphBitmap(const FT_Glyph& glyph, u16 codepoint, FT_Render_Mode renderMode, const int baselineInAtlas) +std::optional CFont::GenerateStrokeGlyphBitmap(const FT_Glyph& glyph, u16 codepoint, FT_Render_Mode renderMode, const float baselineInAtlas) { FT_Glyph strokedGlyph; if (FT_Error error{FT_Glyph_Copy(glyph, &strokedGlyph)}) @@ -354,17 +376,17 @@ std::optional CFont::GenerateStrokeGlyphBitmap(const FT_Glyph& gl FT_BitmapGlyph bitmapGlyph{reinterpret_cast(strokedGlyph)}; FT_Bitmap& bitmapStroke{bitmapGlyph->bitmap}; - CFont::Offset offset{0,0}; - int targetStrokeY{m_AtlasY + m_StrokeWidth + baselineInAtlas - bitmapGlyph->top}; - int targetStrokeX{m_AtlasX + m_StrokeWidth + bitmapGlyph->left}; + CVector2D offset{0.0f, 0.0f}; + int targetStrokeY{static_cast(std::ceil(m_AtlasY + m_StrokeWidth + baselineInAtlas - bitmapGlyph->top))}; + int targetStrokeX{static_cast(std::ceil(m_AtlasX + m_StrokeWidth + bitmapGlyph->left))}; if (targetStrokeX < m_AtlasX) { - offset.x = bitmapGlyph->left + m_StrokeWidth; + offset.X = bitmapGlyph->left + m_StrokeWidth; targetStrokeX = m_AtlasX; } if (targetStrokeY < m_AtlasY) { - offset.y = bitmapGlyph->top - baselineInAtlas - m_StrokeWidth; + offset.Y = bitmapGlyph->top - baselineInAtlas - m_StrokeWidth; targetStrokeY = m_AtlasY; } BlendGlyphBitmapToTexture(bitmapStroke, targetStrokeX, targetStrokeY, 0, 0, 0); @@ -372,7 +394,7 @@ std::optional CFont::GenerateStrokeGlyphBitmap(const FT_Glyph& gl return offset; } -std::optional CFont::GenerateGlyphBitmap(FT_Glyph& glyph, u16 codepoint, FT_Render_Mode renderMode, CFont::Offset offset, const int baselineInAtlas) +std::optional CFont::GenerateGlyphBitmap(FT_Glyph& glyph, u16 codepoint, FT_Render_Mode renderMode, CVector2D offset, const float baselineInAtlas) { if (FT_Error error{FT_Glyph_To_Bitmap(&glyph, renderMode, nullptr, 0)}) { @@ -382,18 +404,18 @@ std::optional CFont::GenerateGlyphBitmap(FT_Glyph& glyph, u16 cod FT_BitmapGlyph bitmapGlyph{reinterpret_cast(glyph)}; FT_Bitmap& bitmap{bitmapGlyph->bitmap}; - int targetY{m_AtlasY + offset.y + m_StrokeWidth + baselineInAtlas - bitmapGlyph->top}; - int targetX{m_AtlasX - offset.x + m_StrokeWidth + bitmapGlyph->left}; - CFont::Offset newOffset{0,0}; + int targetY{static_cast(std::ceil(m_AtlasY + offset.Y + m_StrokeWidth + baselineInAtlas - bitmapGlyph->top))}; + int targetX{static_cast(std::ceil(m_AtlasX - offset.X + m_StrokeWidth + bitmapGlyph->left))}; + CVector2D newOffset{0.0f, 0.0f}; if (targetX < m_AtlasX) { - newOffset.x = bitmapGlyph->left + m_StrokeWidth; + newOffset.X = bitmapGlyph->left + m_StrokeWidth; targetX = m_AtlasX; } if (targetY < m_AtlasY) { - newOffset.y = bitmapGlyph->top - baselineInAtlas - m_StrokeWidth; + newOffset.Y = bitmapGlyph->top - baselineInAtlas - m_StrokeWidth; targetY = m_AtlasY; } diff --git a/source/graphics/Font.h b/source/graphics/Font.h index a21462d6fc..65cd5861c1 100644 --- a/source/graphics/Font.h +++ b/source/graphics/Font.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2025 Wildfire Games. + /* Copyright (C) 2025 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -20,15 +20,13 @@ #include "graphics/Texture.h" #include "maths/Rect.h" -#include "ps/CLogger.h" #include "renderer/Renderer.h" #include #include FT_FREETYPE_H #include FT_STROKER_H -#include FT_OUTLINE_H #include FT_GLYPH_H -#include FT_BITMAP_H +#include #include /** @@ -44,8 +42,8 @@ public: struct GlyphData { float u0, v0, u1, v1; - i16 x0, y0, x1, y1; - i16 xadvance; + float x0, y0, x1, y1; + float xadvance; u8 defined{0}; }; @@ -77,11 +75,11 @@ public: }; bool HasRGB() const { return m_HasRGB; } - int GetLineSpacing() const { return m_LineSpacing; } - int GetHeight() const { return m_Height; } - int GetCharacterWidth(wchar_t c); - int GetStrokeWidth() const { return m_StrokeWidth; } - void CalculateStringSize(const wchar_t* string, int& w, int& h); + float GetLineSpacing() const; + float GetHeight() const; + float GetCharacterWidth(wchar_t c); + float GetStrokeWidth() const { return m_StrokeWidth; } + void CalculateStringSize(const wchar_t* string, float& w, float& h); void GetGlyphBounds(float& x0, float& y0, float& x1, float& y1) const { x0 = m_Bounds.left; @@ -104,22 +102,16 @@ private: using UniqueFTFace = std::unique_ptr, decltype(&ftFaceDeleter)>; using UniqueFTStroker = std::unique_ptr, decltype(&ftStrokerDeleter)>; - struct Offset - { - int x{0}; - int y{0}; - }; - friend class CFontManager; - bool SetFontFromPath(const std::string& fontPath, const std::string& fontName, int size, int strokeWidth); + bool SetFontFromPath(const std::string& fontPath, const std::string& fontName, float size, float strokeWidth, float scale); void BlendGlyphBitmapToTexture(const FT_Bitmap& bitmap, int targetX, int targetY, u8 r, u8 g, u8 b); void BlendGlyphBitmapToTextureRGBA(const FT_Bitmap& bitmap, int targetX, int targetY, u8 r, u8 g, u8 b); void BlendGlyphBitmapToTextureR8(const FT_Bitmap& bitmap, int targetX, int targetY); - std::optional GenerateStrokeGlyphBitmap(const FT_Glyph& glyph, u16 codepoint, FT_Render_Mode renderMode, const int baselineInAtlas); - std::optional GenerateGlyphBitmap(FT_Glyph& glyph, u16 codepoint, FT_Render_Mode renderMode, Offset offset, const int baselineInAtlas); + std::optional GenerateStrokeGlyphBitmap(const FT_Glyph& glyph, u16 codepoint, FT_Render_Mode renderMode, const float baselineInAtlas); + std::optional GenerateGlyphBitmap(FT_Glyph& glyph, u16 codepoint, FT_Render_Mode renderMode, CVector2D offset, const float baselineInAtlas); const GlyphData* ExtractAndGenerateGlyph(u16 codepoint); bool ConstructTextureAtlas(); @@ -132,10 +124,10 @@ private: GlyphMap m_Glyphs; - int m_LineSpacing; + float m_LineSpacing; // Height of a capital letter, roughly. - int m_Height; + float m_Height; // Bounding box of all glyphs CRect m_Bounds; @@ -151,14 +143,15 @@ private: int m_AtlasPadding; bool m_IsDirty{false}; bool m_IsLoading{false}; - int m_StrokeWidth{0}; + float m_StrokeWidth{0.0f}; + float m_Scale{1.0f}; std::unique_ptr m_TexData; Renderer::Backend::Format m_TextureFormat{Renderer::Backend::Format::R8G8B8A8_UNORM}; int m_TextureFormatStride{4}; int m_AtlasSize{0}; - int m_FontSize{0}; + float m_FontSize{0.0f}; std::string m_FontName; std::shared_ptr> m_GammaCorrectionLUT{nullptr}; @@ -166,19 +159,12 @@ private: UniqueFTFace m_Font{nullptr, &ftFaceDeleter}; UniqueFTStroker m_Stroker{nullptr, &ftStrokerDeleter}; - /** - * FreeType represents most of its size and position values in 26.6 fixed-point format — that is, - * 26 bits for the integer part and 6 bits for the fractional part. - * FreeType's metrics such as: ascender, descender, height, advance, etc. are measured in 1/64th of a pixel. - */ - static constexpr int SUBPIXEL_SHIFT{6}; - /** * Some fonts are not rendered well at small sizes, so we set a minimum size. * Because we are using a bitmap blending mode, when a font is using small size, * We need to use a different render mode, one with less antialiasing. */ - static constexpr int MINIMAL_FONT_SIZE_ANTIALIASING{12}; + static constexpr float MINIMAL_FONT_SIZE_ANTIALIASING{12.0f}; }; #endif // INCLUDED_FONT diff --git a/source/graphics/FontManager.cpp b/source/graphics/FontManager.cpp index d8356b1419..6a13399050 100644 --- a/source/graphics/FontManager.cpp +++ b/source/graphics/FontManager.cpp @@ -103,7 +103,8 @@ CFontManager::CFontManager() std::shared_ptr CFontManager::LoadFont(CStrIntern fontName) { const std::string locale{g_L10n.GetCurrentLocale() != icu::Locale::getUS() ? g_L10n.GetCurrentLocaleString() : ""}; - CStrIntern localeFontName{locale + fontName.string()}; + const float guiScale{g_ConfigDB.Get("gui.scale", 1.0f)}; + CStrIntern localeFontName{fmt::format("{}{}-{}", locale ,fontName.string(), guiScale)}; FontsMap::iterator it{m_Fonts.find(localeFontName)}; if (it != m_Fonts.end()) @@ -185,7 +186,7 @@ std::shared_ptr CFontManager::LoadFont(CStrIntern fontName) } // TODO: For now set strokeWith = 1, later we can expose it to the GUI. - if (!font.get()->SetFontFromPath(realPath.string8(), localeFontName.string(), fontSpec.size, fontSpec.stroke ? 1 : 0)) + if (!font->SetFontFromPath(realPath.string8(), localeFontName.string(), fontSpec.size, fontSpec.stroke ? 1.0f : 0.0f, guiScale)) { return nullptr; } diff --git a/source/graphics/FontMetrics.cpp b/source/graphics/FontMetrics.cpp index 1026b90efb..8a722e5477 100644 --- a/source/graphics/FontMetrics.cpp +++ b/source/graphics/FontMetrics.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2013 Wildfire Games. +/* Copyright (C) 2025 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -29,7 +29,7 @@ CFontMetrics::CFontMetrics(CStrIntern font) m_Font = g_Renderer.GetFontManager().LoadFont(font); } -int CFontMetrics::GetLineSpacing() const +float CFontMetrics::GetLineSpacing() const { // Return some arbitrary default if the font failed to load, so that the // user of CFontMetrics doesn't have to care about failures @@ -38,21 +38,21 @@ int CFontMetrics::GetLineSpacing() const return m_Font->GetLineSpacing(); } -int CFontMetrics::GetHeight() const +float CFontMetrics::GetHeight() const { if (!m_Font) return 6; return m_Font->GetHeight(); } -int CFontMetrics::GetCharacterWidth(wchar_t c) const +float CFontMetrics::GetCharacterWidth(wchar_t c) const { if (!m_Font) return 6; return m_Font->GetCharacterWidth(c); } -void CFontMetrics::CalculateStringSize(const wchar_t* string, int& w, int& h) const +void CFontMetrics::CalculateStringSize(const wchar_t* string, float& w, float& h) const { if (!m_Font) w = h = 0; diff --git a/source/graphics/FontMetrics.h b/source/graphics/FontMetrics.h index d84bf7f39e..7bb63fe0b7 100644 --- a/source/graphics/FontMetrics.h +++ b/source/graphics/FontMetrics.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2021 Wildfire Games. +/* Copyright (C) 2025 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -31,10 +31,10 @@ class CFontMetrics public: CFontMetrics(CStrIntern font); - int GetLineSpacing() const; - int GetHeight() const; - int GetCharacterWidth(wchar_t c) const; - void CalculateStringSize(const wchar_t* string, int& w, int& h) const; + float GetLineSpacing() const; + float GetHeight() const; + float GetCharacterWidth(wchar_t c) const; + void CalculateStringSize(const wchar_t* string, float& w, float& h) const; private: std::shared_ptr m_Font; diff --git a/source/graphics/TextRenderer.cpp b/source/graphics/TextRenderer.cpp index 0fc94c4280..0eb333db83 100644 --- a/source/graphics/TextRenderer.cpp +++ b/source/graphics/TextRenderer.cpp @@ -116,9 +116,9 @@ void CTextRenderer::PutAdvance(const wchar_t* buf) { Put(0.0f, 0.0f, buf); - int w, h; + float w, h; m_Font->CalculateStringSize(buf, w, h); - Translate((float)w, 0.0f); + Translate(w, 0.0f); } void CTextRenderer::Put(float x, float y, const wchar_t* buf) @@ -279,8 +279,8 @@ void CTextRenderer::Render( for (std::list::iterator runit = batch.runs.begin(); runit != batch.runs.end(); ++runit) { SBatchRun& run = *runit; - i16 x = run.x; - i16 y = run.y; + float x{run.x}; + float y{run.y}; for (size_t i = 0; i < run.text->size(); ++i) { const CFont::GlyphData* g = batch.font->GetGlyph((*run.text)[i]); diff --git a/source/gui/CGUISprite.cpp b/source/gui/CGUISprite.cpp index 268b573422..0b326c7889 100644 --- a/source/gui/CGUISprite.cpp +++ b/source/gui/CGUISprite.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2021 Wildfire Games. +/* Copyright (C) 2025 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -30,7 +30,7 @@ void CGUISpriteInstance::Draw(CGUI& pGUI, CCanvas2D& canvas, const CRect& Size, { if (m_CachedSize != Size) { - GUIRenderer::UpdateDrawCallCache(pGUI, m_DrawCallCache, m_SpriteName, Size, Sprites); + GUIRenderer::UpdateDrawCallCache(pGUI, m_DrawCallCache, m_SpriteName, Size, m_RoundCoordinates, Sprites); m_CachedSize = Size; } GUIRenderer::Draw(m_DrawCallCache, canvas); @@ -45,7 +45,12 @@ CGUISpriteInstance::CGUISpriteInstance() } CGUISpriteInstance::CGUISpriteInstance(const CStr& SpriteName) - : m_SpriteName(SpriteName) + : m_SpriteName(SpriteName), m_RoundCoordinates(true) +{ +} + +CGUISpriteInstance::CGUISpriteInstance(const CStr& SpriteName, const bool RoundCoordinates) + : m_SpriteName(SpriteName), m_RoundCoordinates(RoundCoordinates) { } @@ -55,3 +60,10 @@ void CGUISpriteInstance::SetName(const CStr& SpriteName) m_CachedSize = CRect(); m_DrawCallCache.clear(); } + +void CGUISpriteInstance::SetRoundCoordinates(const bool RoundCoordinates) +{ + m_RoundCoordinates = RoundCoordinates; + m_CachedSize = CRect(); + m_DrawCallCache.clear(); +} diff --git a/source/gui/CGUISprite.h b/source/gui/CGUISprite.h index 0cf8aae4c6..6b551df478 100644 --- a/source/gui/CGUISprite.h +++ b/source/gui/CGUISprite.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2022 Wildfire Games. +/* Copyright (C) 2025 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -138,6 +138,7 @@ public: CGUISpriteInstance(); CGUISpriteInstance(const CStr& SpriteName); + CGUISpriteInstance(const CStr& SpriteName, const bool RoundCoordinates); void Draw(CGUI& pGUI, CCanvas2D& canvas, const CRect& Size, std::map>& Sprites) const; @@ -157,9 +158,17 @@ public: */ void SetName(const CStr& SpriteName); + /** + * Sets the coordinates to be rounded to integer pixels. + * This is useful for pixel art, icons in text, to avoid blurry filtering. + * Use as rarely as possible, because it clears the draw cache. + */ + void SetRoundCoordinates(const bool RoundCoordinates); private: CStr m_SpriteName; + bool m_RoundCoordinates; + // Stored drawing calls, for more efficient rendering mutable GUIRenderer::DrawCalls m_DrawCallCache; // Relevant details of previously rendered sprite; the cache is invalidated diff --git a/source/gui/CGUIText.cpp b/source/gui/CGUIText.cpp index 820a3aea90..777bfbbf1c 100644 --- a/source/gui/CGUIText.cpp +++ b/source/gui/CGUIText.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2023 Wildfire Games. +/* Copyright (C) 2025 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -52,7 +52,7 @@ void SGenerateTextImage::SetupSpriteCall( spriteCall.m_Area.right = width - bufferZone; } - spriteCall.m_Sprite = textureName; + spriteCall.m_Sprite = CGUISpriteInstance{textureName, false}; m_YFrom = spriteCall.m_Area.top - bufferZone; m_YTo = spriteCall.m_Area.bottom + bufferZone; @@ -469,7 +469,7 @@ void CGUIText::Draw(CGUI& pGUI, CCanvas2D& canvas, const CGUIColor& DefaultColor textRenderer.SetCurrentColor(tc.m_UseCustomColor ? tc.m_Color : DefaultColor); textRenderer.SetCurrentFont(tc.m_Font); - textRenderer.Put(floorf(pos.X + tc.m_Pos.X), floorf(pos.Y + tc.m_Pos.Y), &tc.m_String); + textRenderer.Put(pos.X + tc.m_Pos.X, pos.Y + tc.m_Pos.Y, &tc.m_String); } canvas.DrawText(textRenderer); diff --git a/source/gui/GUIRenderer.cpp b/source/gui/GUIRenderer.cpp index 2531f01d60..2d96dead39 100644 --- a/source/gui/GUIRenderer.cpp +++ b/source/gui/GUIRenderer.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2022 Wildfire Games. +/* Copyright (C) 2025 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -56,7 +56,7 @@ DrawCalls& DrawCalls::operator=(const DrawCalls&) } -void GUIRenderer::UpdateDrawCallCache(const CGUI& pGUI, DrawCalls& Calls, const CStr& SpriteName, const CRect& Size, std::map>& Sprites) +void GUIRenderer::UpdateDrawCallCache(const CGUI& pGUI, DrawCalls& Calls, const CStr& SpriteName, const CRect& Size, const bool RoundCoordinates, std::map>& Sprites) { // This is called only when something has changed (like the size of the // sprite), so it doesn't need to be particularly efficient. @@ -166,7 +166,7 @@ void GUIRenderer::UpdateDrawCallCache(const CGUI& pGUI, DrawCalls& Calls, const LOGERROR("Trying to use a sprite that doesn't exist (\"%s\").", SpriteName.c_str()); return; } - + it = Sprites.emplace(SpriteName, std::move(sprite)).first; } @@ -188,7 +188,7 @@ void GUIRenderer::UpdateDrawCallCache(const CGUI& pGUI, DrawCalls& Calls, const } Call.m_Vertices = ObjectSize; - if ((*cit)->m_RoundCoordinates) + if ((*cit)->m_RoundCoordinates && RoundCoordinates) { // Round the vertex coordinates to integers, to avoid ugly filtering artifacts Call.m_Vertices.left = (int)(Call.m_Vertices.left + 0.5f); diff --git a/source/gui/GUIRenderer.h b/source/gui/GUIRenderer.h index dc68e41617..fa26b9f12e 100644 --- a/source/gui/GUIRenderer.h +++ b/source/gui/GUIRenderer.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2022 Wildfire Games. +/* Copyright (C) 2025 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -64,7 +64,7 @@ namespace GUIRenderer DrawCalls& operator=(const DrawCalls&); }; - void UpdateDrawCallCache(const CGUI& pGUI, DrawCalls& Calls, const CStr8& SpriteName, const CRect& Size, std::map>& Sprites); + void UpdateDrawCallCache(const CGUI& pGUI, DrawCalls& Calls, const CStr8& SpriteName, const CRect& Size, const bool RoundCoordinates, std::map>& Sprites); void Draw(DrawCalls& Calls, CCanvas2D& canvas); } diff --git a/source/gui/ObjectBases/IGUITextOwner.cpp b/source/gui/ObjectBases/IGUITextOwner.cpp index 34f0bff3c9..5355ce736b 100644 --- a/source/gui/ObjectBases/IGUITextOwner.cpp +++ b/source/gui/ObjectBases/IGUITextOwner.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2021 Wildfire Games. +/* Copyright (C) 2025 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -113,7 +113,7 @@ void IGUITextOwner::CalculateTextPosition(CRect& ObjSize, CVector2D& TextPos, CG break; case EVAlign::CENTER: // Round to integer pixel values, else the fonts look awful - TextPos.Y = floorf(ObjSize.CenterPoint().Y - Text.GetSize().Height / 2.f); + TextPos.Y = ObjSize.CenterPoint().Y - Text.GetSize().Height / 2.f; break; case EVAlign::BOTTOM: TextPos.Y = ObjSize.bottom - Text.GetSize().Height; diff --git a/source/gui/SettingTypes/CGUIString.cpp b/source/gui/SettingTypes/CGUIString.cpp index 4b04dc6031..b6a9646737 100644 --- a/source/gui/SettingTypes/CGUIString.cpp +++ b/source/gui/SettingTypes/CGUIString.cpp @@ -157,7 +157,7 @@ void CGUIString::GenerateTextCall(const CGUI& pGUI, SFeedback& Feedback, CStrInt TextCall.m_Tooltip = tagAttrib.value; } - SpriteCall.m_Sprite = icon.m_SpriteName; + SpriteCall.m_Sprite = CGUISpriteInstance{icon.m_SpriteName,false}; // Add sprite call Feedback.m_SpriteCalls.push_back(std::move(SpriteCall)); @@ -209,17 +209,17 @@ void CGUIString::GenerateTextCall(const CGUI& pGUI, SFeedback& Feedback, CStrInt // Calculate the size of the font. CSize2D size; - int cx, cy; + float cx, cy; CFontMetrics font (TextCall.m_Font); font.CalculateStringSize(TextCall.m_String.c_str(), cx, cy); - size.Width = static_cast(cx); + size.Width = cx; // For anything other than the first line, the line spacing // needs to be considered rather than just the height of the text. if (FirstLine) - size.Height = static_cast(font.GetHeight()); + size.Height = font.GetHeight(); else - size.Height = static_cast(font.GetLineSpacing()); + size.Height = font.GetLineSpacing(); // Append width, and make maximum height the height. Feedback.m_Size.Width += size.Width; diff --git a/source/gui/tests/test_CGUIText.h b/source/gui/tests/test_CGUIText.h index 731afb913d..f1b8ac0839 100644 --- a/source/gui/tests/test_CGUIText.h +++ b/source/gui/tests/test_CGUIText.h @@ -97,8 +97,8 @@ public: const CStrW font{L"mono-10"}; const CFontMetrics fontMetrics{CStrIntern(font.ToUTF8())}; - const float lineHeight{static_cast(fontMetrics.GetHeight())}; - const float lineSpacing{static_cast(fontMetrics.GetLineSpacing())}; + const float lineHeight{fontMetrics.GetHeight()}; + const float lineSpacing{fontMetrics.GetLineSpacing()}; CGUIString string; CGUIText text; @@ -208,10 +208,12 @@ public: const CStrW font{L"mono-10"}; CFontMetrics fontMetrics{CStrIntern(font.ToUTF8())}; - int firstWordWidth = 0, firstWordHeight = 0; + float firstWordWidth{0}; + float firstWordHeight{0}; fontMetrics.CalculateStringSize(firstWord.c_str(), firstWordWidth, firstWordHeight); TS_ASSERT(firstWordWidth > 0); - int secondWordWidth = 0, secondWordHeight = 0; + float secondWordWidth{0}; + float secondWordHeight{0}; fontMetrics.CalculateStringSize(secondWord.c_str(), secondWordWidth, secondWordHeight); TS_ASSERT(secondWordWidth > 0); TS_ASSERT(firstWordWidth < secondWordWidth); @@ -283,8 +285,8 @@ public: const CStrW font{L"mono-10"}; const CFontMetrics fontMetrics{CStrIntern(font.ToUTF8())}; - const float lineHeight{static_cast(fontMetrics.GetHeight())}; - const float lineSpacing{static_cast(fontMetrics.GetLineSpacing())}; + const float lineHeight{fontMetrics.GetHeight()}; + const float lineSpacing{fontMetrics.GetLineSpacing()}; float renderedWidth = 0.f; const float width = 200.f; @@ -352,8 +354,8 @@ public: const CStrW font{L"mono-10"}; const CFontMetrics fontMetrics{CStrIntern(font.ToUTF8())}; - const float lineHeight{static_cast(fontMetrics.GetHeight())}; - const float lineSpacing{static_cast(fontMetrics.GetLineSpacing())}; + const float lineHeight{fontMetrics.GetHeight()}; + const float lineSpacing{fontMetrics.GetLineSpacing()}; CGUIString string; CGUIText text; diff --git a/source/ps/ProfileViewer.cpp b/source/ps/ProfileViewer.cpp index c942110055..8d91731b61 100644 --- a/source/ps/ProfileViewer.cpp +++ b/source/ps/ProfileViewer.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2024 Wildfire Games. +/* Copyright (C) 2025 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -195,7 +195,7 @@ void CProfileViewer::RenderProfile(CCanvas2D& canvas) for (size_t col = 0; col < columns.size(); ++col) { CStrW text = columns[col].title.FromUTF8(); - int w, h; + float w, h; font.CalculateStringSize(text.c_str(), w, h); float x = colX; @@ -228,7 +228,7 @@ void CProfileViewer::RenderProfile(CCanvas2D& canvas) for (size_t col = 0; col < columns.size(); ++col) { CStrW text = table->GetCellText(row, col).FromUTF8(); - int w, h; + float w, h; font.CalculateStringSize(text.c_str(), w, h); float x = rowColX; diff --git a/source/ps/scripting/JSInterface_Main.cpp b/source/ps/scripting/JSInterface_Main.cpp index 13e6f85915..a06575f14f 100644 --- a/source/ps/scripting/JSInterface_Main.cpp +++ b/source/ps/scripting/JSInterface_Main.cpp @@ -87,8 +87,8 @@ int GetFps() CSize2D GetTextSize(const std::string& fontName, const std::wstring& text) { - int width = 0; - int height = 0; + float width = 0; + float height = 0; CStrIntern _fontName(fontName); CFontMetrics fontMetrics(_fontName); fontMetrics.CalculateStringSize(text.c_str(), width, height);