mirror of
https://gitea.wildfiregames.com/0ad/0ad
synced 2026-06-16 05:13:58 -07:00
Use freetype for font rendering
Replace premade bitmap fonts with FreeType2 and dynamic texture atlas. Switched from static, premade bitmap fonts to runtime-generated glyphs using FreeType2. Glyphs are rendered on demand into a dynamic texture atlas, improving scalability, font quality, and support for internationalization. Currently using RGBA format for compatibility across all render paths while initial support for R8_UNORM is being added. This prepares for future optimizations in VRAM and performance Includes groundwork for gamma-corrected blending and future swizzling support.
This commit is contained in:
parent
45340a2e32
commit
734386ce9f
17 changed files with 921 additions and 202 deletions
|
|
@ -620,3 +620,31 @@ far = 4096.0 ; Far plane distance
|
|||
fov = 45.0 ; Field of view (degrees), lower is narrow, higher is wide
|
||||
height.smoothness = 0.5
|
||||
height.min = 16
|
||||
|
||||
[fonts]
|
||||
default = "LinBiolinum_Rah.ttf" ; Default font to use for all text
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
; **************************************************************
|
||||
; Font format in configuration is
|
||||
; [fonts.<locale>.<fontalias>]
|
||||
; In GUI XML files, use the font alias.
|
||||
; Example for regular: font="<fontalias>-<fontsize>"
|
||||
; Example for bold: font="<fontalias>-bold-<fontsize>"
|
||||
; Example for italic: font="<fontalias>-italic-<fontsize>"
|
||||
; If you need troke font use the following format:
|
||||
; Example for stroke: font="....-stroke-<fontsize>"
|
||||
;
|
||||
; Support for locale fonts
|
||||
; [fonts.<locale>.<fontalias>]
|
||||
; Example: fonts.jp.sans = "font.ttf"
|
||||
; **************************************************************
|
||||
|
||||
[fonts.sans]
|
||||
regular = "LinBiolinum_Rah.ttf"
|
||||
bold = "LinBiolinum_RBah.ttf"
|
||||
italic = "LinBiolinum_RIah.ttf"
|
||||
|
||||
[fonts.mono]
|
||||
regular = "DejaVuSansMono.ttf"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ea56969bee3e6664a3bbf3f7c716ee8c9bb6a4adc0c48c3d39e5613248779bce
|
||||
size 322524
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f7140084369db686c71e522f0e8de148f0f3f429310376d5f52325a9f0955ba5
|
||||
size 657744
|
||||
99
binaries/data/mods/mod/fonts/DejaVu-LICENSE.txt
Normal file
99
binaries/data/mods/mod/fonts/DejaVu-LICENSE.txt
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
Fonts are (c) Bitstream (see below). DejaVu changes are in public domain.
|
||||
Glyphs imported from Arev fonts are (c) Tavmjong Bah (see below)
|
||||
|
||||
Bitstream Vera Fonts Copyright
|
||||
------------------------------
|
||||
|
||||
Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream Vera is
|
||||
a trademark of Bitstream, Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of the fonts accompanying this license ("Fonts") and associated
|
||||
documentation files (the "Font Software"), to reproduce and distribute the
|
||||
Font Software, including without limitation the rights to use, copy, merge,
|
||||
publish, distribute, and/or sell copies of the Font Software, and to permit
|
||||
persons to whom the Font Software is furnished to do so, subject to the
|
||||
following conditions:
|
||||
|
||||
The above copyright and trademark notices and this permission notice shall
|
||||
be included in all copies of one or more of the Font Software typefaces.
|
||||
|
||||
The Font Software may be modified, altered, or added to, and in particular
|
||||
the designs of glyphs or characters in the Fonts may be modified and
|
||||
additional glyphs or characters may be added to the Fonts, only if the fonts
|
||||
are renamed to names not containing either the words "Bitstream" or the word
|
||||
"Vera".
|
||||
|
||||
This License becomes null and void to the extent applicable to Fonts or Font
|
||||
Software that has been modified and is distributed under the "Bitstream
|
||||
Vera" names.
|
||||
|
||||
The Font Software may be sold as part of a larger software package but no
|
||||
copy of one or more of the Font Software typefaces may be sold by itself.
|
||||
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT,
|
||||
TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME
|
||||
FOUNDATION BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING
|
||||
ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES,
|
||||
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
|
||||
THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE
|
||||
FONT SOFTWARE.
|
||||
|
||||
Except as contained in this notice, the names of Gnome, the Gnome
|
||||
Foundation, and Bitstream Inc., shall not be used in advertising or
|
||||
otherwise to promote the sale, use or other dealings in this Font Software
|
||||
without prior written authorization from the Gnome Foundation or Bitstream
|
||||
Inc., respectively. For further information, contact: fonts at gnome dot
|
||||
org.
|
||||
|
||||
Arev Fonts Copyright
|
||||
------------------------------
|
||||
|
||||
Copyright (c) 2006 by Tavmjong Bah. All Rights Reserved.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the fonts accompanying this license ("Fonts") and
|
||||
associated documentation files (the "Font Software"), to reproduce
|
||||
and distribute the modifications to the Bitstream Vera Font Software,
|
||||
including without limitation the rights to use, copy, merge, publish,
|
||||
distribute, and/or sell copies of the Font Software, and to permit
|
||||
persons to whom the Font Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright and trademark notices and this permission notice
|
||||
shall be included in all copies of one or more of the Font Software
|
||||
typefaces.
|
||||
|
||||
The Font Software may be modified, altered, or added to, and in
|
||||
particular the designs of glyphs or characters in the Fonts may be
|
||||
modified and additional glyphs or characters may be added to the
|
||||
Fonts, only if the fonts are renamed to names not containing either
|
||||
the words "Tavmjong Bah" or the word "Arev".
|
||||
|
||||
This License becomes null and void to the extent applicable to Fonts
|
||||
or Font Software that has been modified and is distributed under the
|
||||
"Tavmjong Bah Arev" names.
|
||||
|
||||
The Font Software may be sold as part of a larger software package but
|
||||
no copy of one or more of the Font Software typefaces may be sold by
|
||||
itself.
|
||||
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL
|
||||
TAVMJONG BAH BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
|
||||
Except as contained in this notice, the name of Tavmjong Bah shall not
|
||||
be used in advertising or otherwise to promote the sale, use or other
|
||||
dealings in this Font Software without prior written authorization
|
||||
from Tavmjong Bah. For further information, contact: tavmjong @ free
|
||||
. fr.
|
||||
|
||||
$Id: LICENSE 2133 2007-11-28 02:46:28Z lechimp $
|
||||
3
binaries/data/mods/mod/fonts/DejaVuSansMono.ttf
Normal file
3
binaries/data/mods/mod/fonts/DejaVuSansMono.ttf
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ea56969bee3e6664a3bbf3f7c716ee8c9bb6a4adc0c48c3d39e5613248779bce
|
||||
size 322524
|
||||
12
binaries/data/mods/mod/fonts/LibBiolinum-LICENSE.txt
Normal file
12
binaries/data/mods/mod/fonts/LibBiolinum-LICENSE.txt
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
|
||||
Libertine and Biolinum
|
||||
License
|
||||
|
||||
Free UCS scalable fonts is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
|
||||
|
||||
The fonts are distributed in the hope that they will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
As a special exception, if you create a document which uses this font, and embed this font or unaltered portions of this font into the document, this font does not by itself cause the resulting document to be covered by the GNU General Public License. This exception does not however invalidate any other reasons why the document might be covered by the GNU General Public License. If you modify this font, you may extend this exception to your version of the font, but you are not obligated to do so. If you do not wish to do so, delete this exception statement from your version.
|
||||
|
||||
3
binaries/data/mods/mod/fonts/LinBiolinum_RBah.ttf
Normal file
3
binaries/data/mods/mod/fonts/LinBiolinum_RBah.ttf
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4e0c3301a562f9f5a75e5eb59e35a3505cd374184df939afc80778242b4fd2f5
|
||||
size 699528
|
||||
3
binaries/data/mods/mod/fonts/LinBiolinum_RIah.ttf
Normal file
3
binaries/data/mods/mod/fonts/LinBiolinum_RIah.ttf
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:cf25abdd264845316969e49d1631dd5c2014652844135c8c59e33202919940a4
|
||||
size 747032
|
||||
3
binaries/data/mods/mod/fonts/LinBiolinum_Rah.ttf
Normal file
3
binaries/data/mods/mod/fonts/LinBiolinum_Rah.ttf
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f7140084369db686c71e522f0e8de148f0f3f429310376d5f52325a9f0955ba5
|
||||
size 657744
|
||||
|
|
@ -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
|
||||
|
|
@ -19,68 +19,451 @@
|
|||
#include "Font.h"
|
||||
|
||||
#include "graphics/FontManager.h"
|
||||
#include "graphics/TextureManager.h"
|
||||
#include "ps/Filesystem.h"
|
||||
#include "ps/CLogger.h"
|
||||
#include "renderer/Renderer.h"
|
||||
#include "ps/Profile.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <map>
|
||||
#include <numeric>
|
||||
#include <string>
|
||||
|
||||
CFont::GlyphMap::GlyphMap()
|
||||
namespace
|
||||
{
|
||||
memset(m_Data, 0, sizeof(m_Data));
|
||||
struct FTGlyphDeleter {
|
||||
void operator()(FT_Glyph glyph) const
|
||||
{
|
||||
FT_Done_Glyph(glyph);
|
||||
}
|
||||
};
|
||||
|
||||
using UniqueFTGlyph = std::unique_ptr<std::remove_pointer_t<FT_Glyph>, FTGlyphDeleter>;
|
||||
|
||||
} // end namespace
|
||||
|
||||
const CFont::GlyphData* CFont::GlyphMap::get(u16 codepoint) const
|
||||
{
|
||||
if (!m_Data[codepoint >> 8])
|
||||
return nullptr;
|
||||
if (!(*m_Data[codepoint >> 8])[codepoint & 0xff].defined)
|
||||
return nullptr;
|
||||
return &(*m_Data[codepoint >> 8])[codepoint & 0xff];
|
||||
}
|
||||
|
||||
CFont::GlyphMap::~GlyphMap()
|
||||
void CFont::GlyphMap::set(u16 codepoint, const GlyphData& glyph)
|
||||
{
|
||||
for (size_t i = 0; i < ARRAY_SIZE(m_Data); i++)
|
||||
delete[] m_Data[i];
|
||||
if (!m_Data[codepoint >> 8])
|
||||
m_Data[codepoint >> 8] = std::make_unique<std::array<GlyphData, 256>>();
|
||||
(*m_Data[codepoint >> 8])[codepoint & 0xff] = glyph;
|
||||
(*m_Data[codepoint >> 8])[codepoint & 0xff].defined = 1;
|
||||
}
|
||||
|
||||
void CFont::GlyphMap::set(u16 i, const GlyphData& val)
|
||||
int CFont::GetCharacterWidth(wchar_t c)
|
||||
{
|
||||
if (!m_Data[i >> 8])
|
||||
m_Data[i >> 8] = new GlyphData[256]();
|
||||
m_Data[i >> 8][i & 0xff] = val;
|
||||
m_Data[i >> 8][i & 0xff].defined = 1;
|
||||
PROFILE2("GetCharacterWidth font texture generate");
|
||||
const CFont::GlyphData* g{GetGlyph(c)};
|
||||
|
||||
return g ? g->xadvance : 0;
|
||||
}
|
||||
|
||||
int CFont::GetCharacterWidth(wchar_t c) const
|
||||
{
|
||||
const GlyphData* g = m_Glyphs.get(c);
|
||||
|
||||
if (!g)
|
||||
g = m_Glyphs.get(0xFFFD); // Use the missing glyph symbol
|
||||
|
||||
if (!g)
|
||||
return 0;
|
||||
|
||||
return g->xadvance;
|
||||
}
|
||||
|
||||
void CFont::CalculateStringSize(const wchar_t* string, int& width, int& height) const
|
||||
void CFont::CalculateStringSize(const wchar_t* string, int& width, int& height)
|
||||
{
|
||||
PROFILE2("CalculateStringSize font texture generate");
|
||||
width = 0;
|
||||
height = m_Height;
|
||||
|
||||
// Compute the width as the width of the longest line.
|
||||
int lineWidth = 0;
|
||||
for (const wchar_t* c = string; *c != '\0'; c++)
|
||||
std::wistringstream stream{string};
|
||||
std::wstring line;
|
||||
while (std::getline(stream, line))
|
||||
{
|
||||
const GlyphData* g = m_Glyphs.get(*c);
|
||||
FT_UInt glyphIndexStorage{0};
|
||||
const int lineWidth{std::accumulate(line.begin(), line.end(), 0, [&](int sum, wchar_t c)
|
||||
{
|
||||
const CFont::GlyphData* g{GetGlyph(c)};
|
||||
|
||||
if (!g)
|
||||
g = m_Glyphs.get(0xFFFD); // Use the missing glyph symbol
|
||||
if (!g)
|
||||
return sum;
|
||||
|
||||
if (g)
|
||||
lineWidth += g->xadvance; // Add the character's advance distance
|
||||
if (!FT_HAS_KERNING(m_Font))
|
||||
return sum + g->xadvance;
|
||||
|
||||
if (*c == L'\n')
|
||||
const FT_UInt glyphIndex{FT_Get_Char_Index(m_Font.get(), c)};
|
||||
if (!glyphIndex)
|
||||
return sum + g->xadvance;
|
||||
|
||||
const FT_UInt prevGlyph{std::exchange(glyphIndexStorage, glyphIndex)};
|
||||
if (!prevGlyph)
|
||||
return sum + g->xadvance;
|
||||
|
||||
// Get the kerning value between the previous and current glyph.
|
||||
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<int>(kerning.x >> SUBPIXEL_SHIFT);
|
||||
})
|
||||
};
|
||||
|
||||
width = std::max(width, lineWidth);
|
||||
height += m_LineSpacing;
|
||||
}
|
||||
}
|
||||
|
||||
bool CFont::SetFontFromPath(const std::string& fontPath, const std::string& fontName, int size, int strokeWidth)
|
||||
{
|
||||
// TODO: expose the Stroke Width outside class.
|
||||
m_StrokeWidth = strokeWidth;
|
||||
m_FontSize = size;
|
||||
m_FontName = fontName;
|
||||
|
||||
FT_Face face;
|
||||
if (FT_Error error{FT_New_Face(m_FreeType, fontPath.c_str(), 0, &face)})
|
||||
{
|
||||
if (error == FT_Err_Unknown_File_Format)
|
||||
{
|
||||
height += m_LineSpacing;
|
||||
width = std::max(width, lineWidth);
|
||||
lineWidth = 0;
|
||||
LOGERROR("Font file format is not supported: %s", fontPath.c_str());
|
||||
return false;
|
||||
}
|
||||
LOGERROR("Failed to load font %s: %d", fontPath.c_str(), error);
|
||||
return false;
|
||||
}
|
||||
m_Font.reset(face);
|
||||
|
||||
// Set the font size.
|
||||
if (FT_Error error{FT_Set_Pixel_Sizes(m_Font.get(), 0, m_FontSize)})
|
||||
{
|
||||
LOGERROR("Failed to set font size %d: %d", size, error);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_StrokeWidth)
|
||||
{
|
||||
FT_Stroker stroker;
|
||||
if (FT_Error error{FT_Stroker_New(m_FreeType, &stroker)})
|
||||
{
|
||||
LOGERROR("Failed to create stroker: %d", error);
|
||||
return false;
|
||||
}
|
||||
m_Stroker.reset(stroker);
|
||||
FT_Stroker_Set(m_Stroker.get(), m_StrokeWidth * 64, FT_STROKER_LINECAP_ROUND, FT_STROKER_LINEJOIN_ROUND, 0);
|
||||
}
|
||||
|
||||
if (!ConstructTextureAtlas())
|
||||
{
|
||||
LOGERROR("Failed to create font texture atlas %s", fontName);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get the height of the font.
|
||||
m_Height = m_Font->size->metrics.height >> SUBPIXEL_SHIFT;
|
||||
|
||||
// Get the line spacing of the font.
|
||||
m_LineSpacing = (m_Font->size->metrics.ascender - m_Font->size->metrics.descender) >> SUBPIXEL_SHIFT;
|
||||
|
||||
m_IsLoading = true;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
Renderer::Backend::Sampler::Desc CFont::ChooseTextureFormatAndSampler()
|
||||
{
|
||||
Renderer::Backend::Sampler::Desc defaultSamplerDesc{
|
||||
Renderer::Backend::Sampler::MakeDefaultSampler(
|
||||
Renderer::Backend::Sampler::Filter::LINEAR,
|
||||
Renderer::Backend::Sampler::AddressMode::CLAMP_TO_EDGE)
|
||||
};
|
||||
|
||||
if (m_StrokeWidth > 0)
|
||||
return defaultSamplerDesc;
|
||||
|
||||
// TODO: Add Support for R8_UNORM.
|
||||
// for R8 we will use texture swizzling to convert to RGBA.
|
||||
// and sampler will be changed
|
||||
|
||||
// Legacy Format
|
||||
m_TextureFormat = Renderer::Backend::Format::A8_UNORM;
|
||||
m_TextureFormatStride = 1;
|
||||
m_HasRGB = false;
|
||||
|
||||
return defaultSamplerDesc;
|
||||
}
|
||||
|
||||
bool CFont::ConstructTextureAtlas()
|
||||
{
|
||||
Renderer::Backend::IDevice* backendDevice = g_Renderer.GetDeviceCommandContext()->GetDevice();
|
||||
|
||||
// Make backend texture ahead of time.
|
||||
// TODO: calculate based on device support.
|
||||
const int textureSize{1024};
|
||||
m_AtlasWidth = textureSize;
|
||||
m_AtlasHeight = textureSize;
|
||||
m_HasRGB = true;
|
||||
m_AtlasPadding = 4 + m_StrokeWidth * 2;
|
||||
m_AtlasX = m_AtlasY = 0;
|
||||
|
||||
m_Bounds.right = textureSize;
|
||||
m_Bounds.bottom = textureSize;
|
||||
|
||||
// TODO: preload from cache?.
|
||||
|
||||
const Renderer::Backend::Sampler::Desc defaultSamplerDesc{ChooseTextureFormatAndSampler()};
|
||||
|
||||
m_AtlasSize = m_AtlasWidth * m_AtlasHeight * m_TextureFormatStride;
|
||||
|
||||
m_Texture = g_Renderer.GetTextureManager().WrapBackendTexture(backendDevice->CreateTexture2D(
|
||||
("Font Texture " + m_FontName).c_str(),
|
||||
Renderer::Backend::ITexture::Usage::TRANSFER_DST |
|
||||
Renderer::Backend::ITexture::Usage::SAMPLED,
|
||||
m_TextureFormat,
|
||||
textureSize, textureSize, defaultSamplerDesc
|
||||
));
|
||||
|
||||
if (!m_Texture)
|
||||
{
|
||||
LOGERROR("Failed to create font texture %s", m_FontName);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Initialise texture with transparency, for the areas we don't
|
||||
// overwrite with uploading later.
|
||||
m_TexData = std::make_unique<u8[]>(m_AtlasSize);
|
||||
std::fill_n(m_TexData.get(), m_AtlasSize, 0x00);
|
||||
|
||||
g_Renderer.GetDeviceCommandContext()->UploadTexture(
|
||||
m_Texture->GetBackendTexture(), m_TextureFormat,
|
||||
m_TexData.get(), m_AtlasSize);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const CFont::GlyphData* CFont::GetGlyph(u16 codepoint)
|
||||
{
|
||||
const CFont::GlyphData* g{m_Glyphs.get(codepoint)};
|
||||
return (g && g->defined) ? g : ExtractAndGenerateGlyph(codepoint);
|
||||
}
|
||||
|
||||
const CFont::GlyphData* CFont::ExtractAndGenerateGlyph(u16 codepoint)
|
||||
{
|
||||
PROFILE2("Glyph font texture generate");
|
||||
|
||||
const FT_UInt glyphIndex{FT_Get_Char_Index(m_Font.get(), codepoint)};
|
||||
const FT_Int32 loadFlags{FT_LOAD_DEFAULT | (m_FontSize <= MINIMAL_FONT_SIZE_ANTIALIASING ? FT_LOAD_TARGET_MONO : 0)};
|
||||
|
||||
if (FT_Error error{FT_Load_Glyph(m_Font.get(), glyphIndex, loadFlags)})
|
||||
{
|
||||
LOGERROR("Failed to load glyph %u: %d", codepoint, error);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const FT_GlyphSlot slot{m_Font->glyph};
|
||||
FT_Glyph glyph;
|
||||
if (FT_Error error{FT_Get_Glyph(slot, &glyph)})
|
||||
{
|
||||
LOGERROR("Failed to get glyph %u: %d", codepoint, error);
|
||||
return nullptr;
|
||||
}
|
||||
UniqueFTGlyph glyphPtr(glyph);
|
||||
|
||||
const int baselineInAtlas{static_cast<int>(m_Font->size->metrics.ascender >> SUBPIXEL_SHIFT)};
|
||||
const int glyphW{static_cast<int>(slot->advance.x >> SUBPIXEL_SHIFT)};
|
||||
|
||||
if (m_AtlasX + glyphW + m_StrokeWidth + m_AtlasPadding > m_AtlasWidth)
|
||||
{
|
||||
m_AtlasX = 0;
|
||||
m_AtlasY += m_Height + m_AtlasPadding;
|
||||
}
|
||||
|
||||
if (m_AtlasY + m_Height + m_StrokeWidth + m_AtlasPadding > m_AtlasHeight)
|
||||
{
|
||||
LOGERROR("Font texture atlas is full, cannot load more glyphs");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
m_IsDirty = true;
|
||||
CFont::Offset offset{0,0};
|
||||
|
||||
const FT_Render_Mode renderMode{FT_RENDER_MODE_NORMAL};
|
||||
|
||||
if (m_StrokeWidth)
|
||||
{
|
||||
std::optional<CFont::Offset> offsetStroke{GenerateStrokeGlyphBitmap(glyph, codepoint, renderMode, baselineInAtlas)};
|
||||
if (!offsetStroke.has_value())
|
||||
{
|
||||
LOGERROR("Failed to generate stroke glyph %u", codepoint);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
offset = offsetStroke.value();
|
||||
}
|
||||
|
||||
std::optional<CFont::Offset> offsetGlyph{GenerateGlyphBitmap(glyph, codepoint, renderMode, offset, baselineInAtlas)};
|
||||
if (!offsetGlyph.has_value())
|
||||
{
|
||||
LOGERROR("Failed to generate glyph %u", codepoint);
|
||||
return nullptr;
|
||||
}
|
||||
offset = offsetGlyph.value();
|
||||
|
||||
CFont::GlyphData gd;
|
||||
gd.u0 = static_cast<float>(m_AtlasX) / m_AtlasWidth;
|
||||
gd.v0 = static_cast<float>(m_AtlasY) / m_AtlasHeight;
|
||||
gd.u1 = static_cast<float>(m_AtlasX - offset.x + glyphW + m_StrokeWidth * 2) / m_AtlasWidth;
|
||||
gd.v1 = static_cast<float>(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.xadvance = glyphW;
|
||||
gd.defined = 1;
|
||||
|
||||
m_Glyphs.set(codepoint, gd);
|
||||
|
||||
// Update positions for next glyph.
|
||||
m_AtlasX += glyphW + m_StrokeWidth + m_AtlasPadding;
|
||||
|
||||
return m_Glyphs.get(codepoint);
|
||||
}
|
||||
|
||||
std::optional<CFont::Offset> CFont::GenerateStrokeGlyphBitmap(const FT_Glyph& glyph, u16 codepoint, FT_Render_Mode renderMode, const int baselineInAtlas)
|
||||
{
|
||||
FT_Glyph strokedGlyph;
|
||||
if (FT_Error error{FT_Glyph_Copy(glyph, &strokedGlyph)})
|
||||
{
|
||||
LOGERROR("Failed to copy glyph %u: %d", codepoint, error);
|
||||
return std::nullopt;
|
||||
}
|
||||
UniqueFTGlyph strokeGlyphPtr(strokedGlyph);
|
||||
|
||||
if (FT_Error error{FT_Glyph_StrokeBorder(&strokedGlyph, m_Stroker.get(), 0, 0)})
|
||||
{
|
||||
LOGERROR("Failed to stroke glyph %u: %d", codepoint, error);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
if (FT_Error error{FT_Glyph_To_Bitmap(&strokedGlyph, renderMode, nullptr, 0)})
|
||||
{
|
||||
LOGERROR("Failed to render glyph %u: %d", codepoint, error);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
FT_BitmapGlyph bitmapGlyph{reinterpret_cast<FT_BitmapGlyph>(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};
|
||||
if (targetStrokeX < m_AtlasX)
|
||||
{
|
||||
offset.x = bitmapGlyph->left + m_StrokeWidth;
|
||||
targetStrokeX = m_AtlasX;
|
||||
}
|
||||
if (targetStrokeY < m_AtlasY)
|
||||
{
|
||||
offset.y = bitmapGlyph->top - baselineInAtlas - m_StrokeWidth;
|
||||
targetStrokeY = m_AtlasY;
|
||||
}
|
||||
BlendGlyphBitmapToTexture(bitmapStroke, targetStrokeX, targetStrokeY, 0, 0, 0);
|
||||
return offset;
|
||||
}
|
||||
|
||||
std::optional<CFont::Offset> CFont::GenerateGlyphBitmap(FT_Glyph& glyph, u16 codepoint, FT_Render_Mode renderMode, CFont::Offset offset, const int baselineInAtlas)
|
||||
{
|
||||
if (FT_Error error{FT_Glyph_To_Bitmap(&glyph, renderMode, nullptr, 0)})
|
||||
{
|
||||
LOGERROR("Failed to render glyph %u: %d", codepoint, error);
|
||||
return std::nullopt;
|
||||
}
|
||||
FT_BitmapGlyph bitmapGlyph{reinterpret_cast<FT_BitmapGlyph>(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};
|
||||
if (targetX < m_AtlasX)
|
||||
{
|
||||
newOffset.x = bitmapGlyph->left + m_StrokeWidth;
|
||||
targetX = m_AtlasX;
|
||||
}
|
||||
|
||||
if (targetY < m_AtlasY)
|
||||
{
|
||||
newOffset.y = bitmapGlyph->top - baselineInAtlas - m_StrokeWidth;
|
||||
targetY = m_AtlasY;
|
||||
}
|
||||
|
||||
BlendGlyphBitmapToTexture(bitmap, targetX, targetY, 255, 255, 255);
|
||||
return newOffset;
|
||||
}
|
||||
|
||||
void CFont::UploadTextureAtlasToGPU()
|
||||
{
|
||||
if (std::exchange(m_IsLoading, false))
|
||||
return;
|
||||
|
||||
if (!m_IsDirty)
|
||||
return;
|
||||
|
||||
Renderer::Backend::IDeviceCommandContext* deviceCommandContext = g_Renderer.GetDeviceCommandContext();
|
||||
PROFILE2("Loading font texture");
|
||||
GPU_SCOPED_LABEL(deviceCommandContext, "Loading font texture");
|
||||
|
||||
deviceCommandContext->UploadTextureRegion(
|
||||
m_Texture->GetBackendTexture(),
|
||||
m_TextureFormat,
|
||||
m_TexData.get(),
|
||||
m_AtlasSize,
|
||||
0, 0, m_AtlasWidth, m_AtlasHeight
|
||||
);
|
||||
|
||||
m_IsDirty = false;
|
||||
}
|
||||
|
||||
void CFont::BlendGlyphBitmapToTexture(const FT_Bitmap& bitmap, int targetX, int targetY, u8 r, u8 g, u8 b)
|
||||
{
|
||||
PROFILE2("BlendGlyphBitmapToTexture font texture generate");
|
||||
if (m_TextureFormat == Renderer::Backend::Format::R8G8B8A8_UNORM)
|
||||
BlendGlyphBitmapToTextureRGBA(bitmap, targetX, targetY, r, g, b);
|
||||
else
|
||||
BlendGlyphBitmapToTextureR8(bitmap, targetX, targetY);
|
||||
}
|
||||
|
||||
void CFont::BlendGlyphBitmapToTextureRGBA(const FT_Bitmap& bitmap, int targetX, int targetY, u8 r, u8 g, u8 b)
|
||||
{
|
||||
for (uint y{0}; y != bitmap.rows; ++y)
|
||||
{
|
||||
const u8* srcRow{bitmap.buffer + y * bitmap.pitch};
|
||||
u8* dstRow{m_TexData.get() + ((targetY + y) * m_AtlasWidth + targetX) * m_TextureFormatStride};
|
||||
|
||||
for (uint x{0}; x != bitmap.width; ++x)
|
||||
{
|
||||
const PS::span<u8> tempDstRow{dstRow + x * m_TextureFormatStride, 4};
|
||||
u8 alpha{srcRow[x]};
|
||||
|
||||
const float srcAlpha{m_StrokeWidth > 0 ? (*m_GammaCorrectionLUT)[alpha] : alpha/255.0f};
|
||||
const float dstAlpha{tempDstRow[3] / 255.0f};
|
||||
const float outAlpha{srcAlpha + dstAlpha * (1.0f - srcAlpha)};
|
||||
|
||||
if (outAlpha == 0.0f)
|
||||
continue;
|
||||
|
||||
tempDstRow[0] = static_cast<u8>(std::round(((r * srcAlpha + tempDstRow[0] * dstAlpha * (1.0f - srcAlpha)) / outAlpha)));
|
||||
tempDstRow[1] = static_cast<u8>(std::round(((g * srcAlpha + tempDstRow[1] * dstAlpha * (1.0f - srcAlpha)) / outAlpha)));
|
||||
tempDstRow[2] = static_cast<u8>(std::round(((b * srcAlpha + tempDstRow[2] * dstAlpha * (1.0f - srcAlpha)) / outAlpha)));
|
||||
tempDstRow[3] = static_cast<u8>(std::round(outAlpha * 255.0f));
|
||||
}
|
||||
}
|
||||
width = std::max(width, lineWidth);
|
||||
}
|
||||
|
||||
void CFont::BlendGlyphBitmapToTextureR8(const FT_Bitmap& bitmap, int targetX, int targetY)
|
||||
{
|
||||
for (uint y{0}; y != bitmap.rows; ++y)
|
||||
{
|
||||
const u8* srcRow{bitmap.buffer + y * bitmap.pitch};
|
||||
u8* dstRow{m_TexData.get() + ((targetY + y) * m_AtlasWidth + targetX)};
|
||||
|
||||
std::memcpy(dstRow, srcRow, bitmap.width);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -19,6 +19,17 @@
|
|||
#define INCLUDED_FONT
|
||||
|
||||
#include "graphics/Texture.h"
|
||||
#include "maths/Rect.h"
|
||||
#include "ps/CLogger.h"
|
||||
#include "renderer/Renderer.h"
|
||||
|
||||
#include <ft2build.h>
|
||||
#include FT_FREETYPE_H
|
||||
#include FT_STROKER_H
|
||||
#include FT_OUTLINE_H
|
||||
#include FT_GLYPH_H
|
||||
#include FT_BITMAP_H
|
||||
#include <optional>
|
||||
|
||||
/**
|
||||
* Storage for a bitmap font. Loaded by CFontManager.
|
||||
|
|
@ -26,12 +37,16 @@
|
|||
class CFont
|
||||
{
|
||||
public:
|
||||
CFont(FT_Library library, std::shared_ptr<std::array<float, 256>> gammaCorrection) : m_FreeType{library}, m_GammaCorrectionLUT{gammaCorrection} {};
|
||||
~CFont() = default;
|
||||
NONCOPYABLE(CFont);
|
||||
|
||||
struct GlyphData
|
||||
{
|
||||
float u0, v0, u1, v1;
|
||||
i16 x0, y0, x1, y1;
|
||||
i16 xadvance;
|
||||
u8 defined;
|
||||
u8 defined{0};
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -39,62 +54,128 @@ public:
|
|||
* This is stored as a sparse 2D array, exploiting the knowledge that a font
|
||||
* typically only supports a small number of 256-codepoint blocks, so most
|
||||
* elements of m_Data will be NULL.
|
||||
* This implementation expand the store the GlyphData for a given Unicode codepoint (0 ≤ cp ≤ 0x10FFFF)
|
||||
*/
|
||||
class GlyphMap
|
||||
{
|
||||
NONCOPYABLE(GlyphMap);
|
||||
public:
|
||||
GlyphMap();
|
||||
~GlyphMap();
|
||||
void set(u16 i, const GlyphData& val);
|
||||
GlyphMap() = default;
|
||||
~GlyphMap() = default;
|
||||
NONCOPYABLE(GlyphMap);
|
||||
|
||||
const GlyphData* get(u16 i) const
|
||||
{
|
||||
if (!m_Data[i >> 8])
|
||||
return NULL;
|
||||
if (!m_Data[i >> 8][i & 0xff].defined)
|
||||
return NULL;
|
||||
return &m_Data[i >> 8][i & 0xff];
|
||||
}
|
||||
/**
|
||||
* @brief Store the GlyphData for a given Unicode codepoint
|
||||
*
|
||||
* @param codepoint The unicode codepoint (0 ≤ cp ≤ 0x10FFFF)
|
||||
* @param glyph The glyphData data to store
|
||||
*/
|
||||
void set(u16 codepoint, const GlyphData& glyph);
|
||||
|
||||
const GlyphData* get(u16 codepoint) const;
|
||||
private:
|
||||
GlyphData* m_Data[256];
|
||||
std::unique_ptr<std::array<GlyphData, 256>> m_Data[256];
|
||||
};
|
||||
|
||||
bool HasRGB() const { return m_HasRGB; }
|
||||
int GetLineSpacing() const { return m_LineSpacing; }
|
||||
int GetHeight() const { return m_Height; }
|
||||
int GetCharacterWidth(wchar_t c) const;
|
||||
void CalculateStringSize(const wchar_t* string, int& w, int& h) const;
|
||||
int GetCharacterWidth(wchar_t c);
|
||||
int GetStrokeWidth() const { return m_StrokeWidth; }
|
||||
void CalculateStringSize(const wchar_t* string, int& w, int& h);
|
||||
void GetGlyphBounds(float& x0, float& y0, float& x1, float& y1) const
|
||||
{
|
||||
x0 = m_BoundsX0;
|
||||
y0 = m_BoundsY0;
|
||||
x1 = m_BoundsX1;
|
||||
y1 = m_BoundsY1;
|
||||
x0 = m_Bounds.left;
|
||||
y0 = m_Bounds.top;
|
||||
x1 = m_Bounds.right;
|
||||
y1 = m_Bounds.bottom;
|
||||
}
|
||||
const GlyphMap& GetGlyphs() const { return m_Glyphs; }
|
||||
CTexturePtr GetTexture() const { return m_Texture; }
|
||||
|
||||
CTexturePtr GetTexture() const { return m_Texture; }
|
||||
void UploadTextureAtlasToGPU();
|
||||
const GlyphData* GetGlyph(u16 i);
|
||||
private:
|
||||
static void ftFaceDeleter(FT_Face face) {
|
||||
FT_Done_Face(face);
|
||||
}
|
||||
static void ftStrokerDeleter(FT_Stroker stroker) {
|
||||
FT_Stroker_Done(stroker);
|
||||
}
|
||||
|
||||
struct Offset
|
||||
{
|
||||
int x{0};
|
||||
int y{0};
|
||||
};
|
||||
|
||||
friend class CFontManager;
|
||||
|
||||
CFont() = default;
|
||||
bool SetFontFromPath(const std::string& fontPath, const std::string& fontName, int size, int strokeWidth);
|
||||
|
||||
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<Offset> GenerateStrokeGlyphBitmap(const FT_Glyph& glyph, u16 codepoint, FT_Render_Mode renderMode, const int baselineInAtlas);
|
||||
std::optional<Offset> GenerateGlyphBitmap(FT_Glyph& glyph, u16 codepoint, FT_Render_Mode renderMode, Offset offset, const int baselineInAtlas);
|
||||
|
||||
const GlyphData* ExtractAndGenerateGlyph(u16 codepoint);
|
||||
bool ConstructTextureAtlas();
|
||||
Renderer::Backend::Sampler::Desc ChooseTextureFormatAndSampler();
|
||||
|
||||
CTexturePtr m_Texture;
|
||||
|
||||
bool m_HasRGB; // true if RGBA, false if ALPHA
|
||||
// True if RGBA, false if ALPHA.
|
||||
bool m_HasRGB{true};
|
||||
|
||||
GlyphMap m_Glyphs;
|
||||
|
||||
int m_LineSpacing;
|
||||
int m_Height; // height of a capital letter, roughly
|
||||
|
||||
// Height of a capital letter, roughly.
|
||||
int m_Height;
|
||||
|
||||
// Bounding box of all glyphs
|
||||
float m_BoundsX0;
|
||||
float m_BoundsY0;
|
||||
float m_BoundsX1;
|
||||
float m_BoundsY1;
|
||||
CRect m_Bounds;
|
||||
|
||||
// The position for the next glyph in the texture atlas.
|
||||
int m_AtlasX;
|
||||
int m_AtlasY;
|
||||
|
||||
// Size of the texture atlas.
|
||||
int m_AtlasWidth;
|
||||
int m_AtlasHeight;
|
||||
|
||||
int m_AtlasPadding;
|
||||
bool m_IsDirty{false};
|
||||
bool m_IsLoading{false};
|
||||
int m_StrokeWidth{0};
|
||||
|
||||
std::unique_ptr<u8[]> m_TexData;
|
||||
Renderer::Backend::Format m_TextureFormat{Renderer::Backend::Format::R8G8B8A8_UNORM};
|
||||
int m_TextureFormatStride{4};
|
||||
int m_AtlasSize{0};
|
||||
|
||||
int m_FontSize{0};
|
||||
std::string m_FontName;
|
||||
std::shared_ptr<std::array<float, 256>> m_GammaCorrectionLUT{nullptr};
|
||||
|
||||
FT_Library m_FreeType;
|
||||
std::unique_ptr<FT_FaceRec_, decltype(&ftFaceDeleter)> m_Font{nullptr, &ftFaceDeleter};
|
||||
std::unique_ptr<FT_StrokerRec_, decltype(&ftStrokerDeleter)> 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};
|
||||
};
|
||||
|
||||
#endif // INCLUDED_FONT
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -21,128 +21,186 @@
|
|||
|
||||
#include "graphics/Font.h"
|
||||
#include "graphics/TextureManager.h"
|
||||
#include "i18n/L10n.h"
|
||||
#include "ps/CLogger.h"
|
||||
#include "ps/ConfigDB.h"
|
||||
#include "ps/CStr.h"
|
||||
#include "ps/CStrInternStatic.h"
|
||||
#include "ps/Filesystem.h"
|
||||
#include "renderer/Renderer.h"
|
||||
|
||||
#include <string>
|
||||
#include <regex>
|
||||
#include <limits>
|
||||
|
||||
namespace {
|
||||
struct FontSpec {
|
||||
std::string type;
|
||||
bool bold{false};
|
||||
bool italic{false};
|
||||
bool stroke{false};
|
||||
int size{0};
|
||||
};
|
||||
|
||||
FontSpec ParseFontSpec(const std::string& spec)
|
||||
{
|
||||
// Regex breakdown:
|
||||
// ^([^\\-]+) → capture fontType (one or more non-'-')
|
||||
// (?:-(bold|italic))? → optional "-bold" or "-italic"
|
||||
// (?:-(stroke))? → optional "-stroke"
|
||||
// -([0-9]+)$ → "-" then fontSize digits at end
|
||||
// examples:
|
||||
// "Roboto-italic-stroke-24",
|
||||
// "OpenSans-bold-32",
|
||||
// "Arial-stroke-16",
|
||||
// "Lato-14"
|
||||
static const std::regex pattern{R"(^([^\-]+)(?:-(bold|italic))?(?:-(stroke))?-([0-9]+)$)",
|
||||
std::regex::icase};
|
||||
|
||||
std::smatch m;
|
||||
if (!std::regex_match(spec, m, pattern))
|
||||
{
|
||||
LOGERROR("Invalid font specification: %s", spec.c_str());
|
||||
return {};
|
||||
}
|
||||
|
||||
FontSpec fs;
|
||||
fs.type = m[1].str();
|
||||
|
||||
if (m[2].matched)
|
||||
{
|
||||
std::string style = m[2].str();
|
||||
if (strcasecmp(style.c_str(), "bold") == 0)
|
||||
fs.bold = true;
|
||||
else if (strcasecmp(style.c_str(), "italic") == 0)
|
||||
fs.italic = true;
|
||||
}
|
||||
|
||||
if (m[3].matched)
|
||||
fs.stroke = true;
|
||||
|
||||
fs.size = std::stoi(m[4].str());
|
||||
|
||||
return fs;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
CFontManager::CFontManager()
|
||||
{
|
||||
FT_Library lib;
|
||||
FT_Error error{FT_Init_FreeType(&lib)};
|
||||
if (error)
|
||||
throw std::runtime_error{"Failed to initialize FreeType " + std::to_string(error)};
|
||||
m_FreeType.reset(lib);
|
||||
|
||||
m_GammaCorrectionLUT = std::make_shared<std::array<float, 256>>();
|
||||
|
||||
std::generate(m_GammaCorrectionLUT->begin(), m_GammaCorrectionLUT->end(), [i = 0]() mutable {
|
||||
return std::pow((i++) / 255.0f, 1.0f / GAMMA_CORRECTION);
|
||||
});
|
||||
}
|
||||
|
||||
std::shared_ptr<CFont> CFontManager::LoadFont(CStrIntern fontName)
|
||||
{
|
||||
FontsMap::iterator it = m_Fonts.find(fontName);
|
||||
const std::string locale{g_L10n.GetCurrentLocale() != icu::Locale::getUS() ? g_L10n.GetCurrentLocaleString() : ""};
|
||||
CStrIntern localeFontName{locale + fontName.string()};
|
||||
|
||||
FontsMap::iterator it{m_Fonts.find(localeFontName)};
|
||||
if (it != m_Fonts.end())
|
||||
return it->second;
|
||||
|
||||
std::shared_ptr<CFont> font(new CFont());
|
||||
// TODO: use hooks or something to hotrealoding default font.
|
||||
const std::string defaultFont{g_ConfigDB.Get("fonts.default", std::string{})};
|
||||
|
||||
if (!ReadFont(font.get(), fontName))
|
||||
if (defaultFont.empty())
|
||||
{
|
||||
// Fall back to default font (unless this is the default font)
|
||||
if (fontName == str_sans_10)
|
||||
font.reset();
|
||||
else
|
||||
font = LoadFont(str_sans_10);
|
||||
LOGERROR("Default font not set in config");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
m_Fonts[fontName] = font;
|
||||
// FontName contain the format fontType(-fontBold|fontItalic)(-fontStroke)-fontSize.
|
||||
// We are going to split it to get the fontType and fontSize.
|
||||
FontSpec fontSpec{ParseFontSpec(fontName.string())};
|
||||
|
||||
if (fontSpec.type.empty())
|
||||
{
|
||||
LOGERROR("Failed to parse font specification: %s, using default font", fontName.string().c_str());
|
||||
fontSpec = ParseFontSpec(str_sans_10.string());
|
||||
}
|
||||
|
||||
// Check for font configuration or fallback.
|
||||
const std::string fontToSearch{[&]
|
||||
{
|
||||
std::vector<std::string> candidateFonts;
|
||||
// 3 types * 2 (bold, italic).
|
||||
candidateFonts.reserve(6);
|
||||
|
||||
// TODO: explicit Locale like RTL or Arabic fonts.
|
||||
// 1. Locale-specific fonts first
|
||||
if (!locale.empty())
|
||||
{
|
||||
candidateFonts.push_back(fmt::format("fonts.{}.{}.regular", locale, fontSpec.type));
|
||||
if (fontSpec.bold)
|
||||
candidateFonts.push_back(fmt::format("fonts.{}.{}.bold", locale, fontSpec.type));
|
||||
if (fontSpec.italic)
|
||||
candidateFonts.push_back(fmt::format("fonts.{}.{}.italic", locale, fontSpec.type));
|
||||
}
|
||||
|
||||
// 2. Then global fonts
|
||||
candidateFonts.push_back(fmt::format("fonts.{}.regular", fontSpec.type));
|
||||
if (fontSpec.bold)
|
||||
candidateFonts.push_back(fmt::format("fonts.{}.bold", fontSpec.type));
|
||||
if (fontSpec.italic)
|
||||
candidateFonts.push_back(fmt::format("fonts.{}.italic", fontSpec.type));
|
||||
|
||||
for (const std::string& key : candidateFonts)
|
||||
{
|
||||
std::string value = g_ConfigDB.Get(key, std::string{});
|
||||
if (!value.empty())
|
||||
return value;
|
||||
}
|
||||
|
||||
// Fallback to default.
|
||||
return defaultFont;
|
||||
}()
|
||||
};
|
||||
|
||||
std::shared_ptr<CFont> font{std::make_shared<CFont>(this->m_FreeType.get(), m_GammaCorrectionLUT)};
|
||||
|
||||
const VfsPath path(L"fonts/");
|
||||
const VfsPath fntName(fontToSearch);
|
||||
OsPath realPath;
|
||||
|
||||
if (!VfsFileExists(path / fntName))
|
||||
{
|
||||
LOGERROR("Font file %s not found", fontToSearch.c_str());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
g_VFS->GetOriginalPath(path / fntName, realPath);
|
||||
if (realPath.empty())
|
||||
{
|
||||
LOGERROR("Font file %s not found", fontToSearch.c_str());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// 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))
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
m_Fonts[localeFontName] = font;
|
||||
return font;
|
||||
}
|
||||
|
||||
bool CFontManager::ReadFont(CFont* font, CStrIntern fontName)
|
||||
void CFontManager::UploadTexturesAtlasToGPU()
|
||||
{
|
||||
const VfsPath path(L"fonts/");
|
||||
|
||||
// Read font definition file into a stringstream
|
||||
std::shared_ptr<u8> buffer;
|
||||
size_t size;
|
||||
const VfsPath fntName(fontName.string() + ".fnt");
|
||||
if (g_VFS->LoadFile(path / fntName, buffer, size) < 0)
|
||||
for (auto& [fontName, fontPtr] : m_Fonts)
|
||||
{
|
||||
LOGERROR("Failed to open font file %s", (path / fntName).string8());
|
||||
return false;
|
||||
}
|
||||
std::istringstream fontStream(
|
||||
std::string(reinterpret_cast<const char*>(buffer.get()), size));
|
||||
|
||||
int version;
|
||||
fontStream >> version;
|
||||
// Make sure this is from a recent version of the font builder.
|
||||
if (version != 101)
|
||||
{
|
||||
LOGERROR("Font %s has invalid version", fontName.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
int textureWidth, textureHeight;
|
||||
fontStream >> textureWidth >> textureHeight;
|
||||
|
||||
std::string format;
|
||||
fontStream >> format;
|
||||
if (format == "rgba")
|
||||
font->m_HasRGB = true;
|
||||
else if (format == "a")
|
||||
font->m_HasRGB = false;
|
||||
else
|
||||
{
|
||||
LOGWARNING("Invalid .fnt format string");
|
||||
return false;
|
||||
}
|
||||
|
||||
int mumberOfGlyphs;
|
||||
fontStream >> mumberOfGlyphs;
|
||||
|
||||
fontStream >> font->m_LineSpacing;
|
||||
fontStream >> font->m_Height;
|
||||
|
||||
font->m_BoundsX0 = std::numeric_limits<float>::max();
|
||||
font->m_BoundsY0 = std::numeric_limits<float>::max();
|
||||
font->m_BoundsX1 = -std::numeric_limits<float>::max();
|
||||
font->m_BoundsY1 = -std::numeric_limits<float>::max();
|
||||
|
||||
for (int i = 0; i < mumberOfGlyphs; ++i)
|
||||
{
|
||||
int codepoint, textureX, textureY, width, height, offsetX, offsetY, advance;
|
||||
fontStream >> codepoint
|
||||
>> textureX >> textureY >> width >> height
|
||||
>> offsetX >> offsetY >> advance;
|
||||
|
||||
if (codepoint < 0 || codepoint > 0xFFFF)
|
||||
{
|
||||
LOGWARNING("Font %s has invalid codepoint 0x%x", fontName.c_str(), codepoint);
|
||||
if (!fontPtr)
|
||||
continue;
|
||||
}
|
||||
|
||||
const float u = static_cast<float>(textureX) / textureWidth;
|
||||
const float v = static_cast<float>(textureY) / textureHeight;
|
||||
const float w = static_cast<float>(width) / textureWidth;
|
||||
const float h = static_cast<float>(height) / textureHeight;
|
||||
|
||||
CFont::GlyphData g =
|
||||
{
|
||||
u, -v, u + w, -v + h,
|
||||
static_cast<i16>(offsetX), static_cast<i16>(-offsetY),
|
||||
static_cast<i16>(offsetX + width), static_cast<i16>(-offsetY + height),
|
||||
static_cast<i16>(advance)
|
||||
};
|
||||
font->m_Glyphs.set(static_cast<u16>(codepoint), g);
|
||||
|
||||
font->m_BoundsX0 = std::min(font->m_BoundsX0, static_cast<float>(g.x0));
|
||||
font->m_BoundsY0 = std::min(font->m_BoundsY0, static_cast<float>(g.y0));
|
||||
font->m_BoundsX1 = std::max(font->m_BoundsX1, static_cast<float>(g.x1));
|
||||
font->m_BoundsY1 = std::max(font->m_BoundsY1, static_cast<float>(g.y1));
|
||||
fontPtr->UploadTextureAtlasToGPU();
|
||||
}
|
||||
|
||||
// Ensure the height has been found (which should always happen if the font includes an 'I').
|
||||
ENSURE(font->m_Height);
|
||||
|
||||
// Load glyph texture
|
||||
const VfsPath imageName(fontName.string() + ".png");
|
||||
CTextureProperties textureProps(path / imageName,
|
||||
font->m_HasRGB ? Renderer::Backend::Format::R8G8B8A8_UNORM : Renderer::Backend::Format::A8_UNORM);
|
||||
textureProps.SetIgnoreQuality(true);
|
||||
font->m_Texture = g_Renderer.GetTextureManager().CreateTexture(textureProps);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -20,6 +20,9 @@
|
|||
|
||||
#include "ps/CStrIntern.h"
|
||||
|
||||
#include <ft2build.h>
|
||||
#include FT_FREETYPE_H
|
||||
|
||||
#include <unordered_map>
|
||||
|
||||
class CFont;
|
||||
|
|
@ -30,13 +33,31 @@ class CFont;
|
|||
class CFontManager
|
||||
{
|
||||
public:
|
||||
CFontManager();
|
||||
~CFontManager() = default;
|
||||
NONCOPYABLE(CFontManager);
|
||||
|
||||
std::shared_ptr<CFont> LoadFont(CStrIntern fontName);
|
||||
void UploadTexturesAtlasToGPU();
|
||||
|
||||
private:
|
||||
bool ReadFont(CFont* font, CStrIntern fontName);
|
||||
static void ftLibraryDeleter(FT_Library library) {
|
||||
FT_Done_FreeType(library);
|
||||
}
|
||||
std::unique_ptr<FT_LibraryRec_, decltype(&ftLibraryDeleter)> m_FreeType{nullptr, &ftLibraryDeleter};
|
||||
|
||||
std::shared_ptr<std::array<float,256>> m_GammaCorrectionLUT;
|
||||
|
||||
using FontsMap = std::unordered_map<CStrIntern, std::shared_ptr<CFont>>;
|
||||
FontsMap m_Fonts;
|
||||
|
||||
/*
|
||||
* Most monitors today use 2.2 as the standard gamma.
|
||||
* MacOS may use 2.2 or 1.8 in some cases.
|
||||
* This method assumes your OS or GPU didn’t override the gamma ramp.
|
||||
* Unless we need super-accurate gamma (e.g., for print preview or color grading), this is usually acceptable.
|
||||
*/
|
||||
static constexpr float GAMMA_CORRECTION = 2.2f;
|
||||
};
|
||||
|
||||
#endif // INCLUDED_FONTMANAGER
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -230,8 +230,6 @@ void CTextRenderer::Render(
|
|||
{
|
||||
SBatch& batch = *it;
|
||||
|
||||
const CFont::GlyphMap& glyphs = batch.font->GetGlyphs();
|
||||
|
||||
if (lastTexture != batch.font->GetTexture().get())
|
||||
{
|
||||
lastTexture = batch.font->GetTexture().get();
|
||||
|
|
@ -285,11 +283,14 @@ void CTextRenderer::Render(
|
|||
i16 y = run.y;
|
||||
for (size_t i = 0; i < run.text->size(); ++i)
|
||||
{
|
||||
const CFont::GlyphData* g = glyphs.get((*run.text)[i]);
|
||||
const CFont::GlyphData* g = batch.font->GetGlyph((*run.text)[i]);
|
||||
|
||||
// Use the missing glyph symbol.
|
||||
if (!g)
|
||||
g = batch.font->GetGlyph(0xFFFD);
|
||||
|
||||
// Missing the missing glyph symbol - give up.
|
||||
if (!g)
|
||||
g = glyphs.get(0xFFFD); // Use the missing glyph symbol
|
||||
if (!g) // Missing the missing glyph symbol - give up
|
||||
continue;
|
||||
|
||||
uvs[idx*4].X = g->u1;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -22,6 +22,7 @@
|
|||
#include "gui/CGUI.h"
|
||||
#include "gui/CGUIText.h"
|
||||
#include "gui/SettingTypes/CGUIString.h"
|
||||
#include "i18n/L10n.h"
|
||||
#include "ps/CLogger.h"
|
||||
#include "ps/ConfigDB.h"
|
||||
#include "ps/Filesystem.h"
|
||||
|
|
@ -39,6 +40,7 @@ class TestCGUIText : public CxxTest::TestSuite
|
|||
std::optional<CXeromycesEngine> m_XeromycesEngine;
|
||||
CProfileViewer* m_Viewer = nullptr;
|
||||
CRenderer* m_Renderer = nullptr;
|
||||
std::unique_ptr<L10n> m_L10n;
|
||||
|
||||
public:
|
||||
void setUp()
|
||||
|
|
@ -56,14 +58,23 @@ public:
|
|||
// TODO: decouple this.
|
||||
CConfigDB::Initialise();
|
||||
CConfigDB::Instance()->SetValueString(CFG_SYSTEM, "rendererbackend", "dummy");
|
||||
|
||||
CConfigDB::Instance()->SetValueString(CFG_SYSTEM, "fonts.default", "LinBiolinum_Rah.ttf");
|
||||
CConfigDB::Instance()->SetValueString(CFG_SYSTEM, "fonts.sans.regular", "LinBiolinum_Rah.ttf");
|
||||
CConfigDB::Instance()->SetValueString(CFG_SYSTEM, "fonts.sans.bold", "LinBiolinum_RBah.ttf");
|
||||
CConfigDB::Instance()->SetValueString(CFG_SYSTEM, "fonts.sans.italic", "LinBiolinum_RIah.ttf");
|
||||
CConfigDB::Instance()->SetValueString(CFG_SYSTEM, "fonts.mono.regular", "DejaVuSansMono.ttf");
|
||||
|
||||
g_VideoMode.InitNonSDL();
|
||||
g_VideoMode.CreateBackendDevice(false);
|
||||
m_Viewer = new CProfileViewer;
|
||||
m_Renderer = new CRenderer(g_VideoMode.GetBackendDevice());
|
||||
m_L10n = std::make_unique<L10n>();
|
||||
}
|
||||
|
||||
void tearDown()
|
||||
{
|
||||
m_L10n.reset();
|
||||
delete m_Renderer;
|
||||
delete m_Viewer;
|
||||
g_VideoMode.Shutdown();
|
||||
|
|
@ -83,11 +94,11 @@ public:
|
|||
{
|
||||
CGUI gui{*g_ScriptContext};
|
||||
|
||||
const CStrW font = L"console";
|
||||
// Make sure this matches the value of the file.
|
||||
// TODO: load dynamically.
|
||||
const float lineHeight = 12.f;
|
||||
const float lineSpacing = 15.f;
|
||||
const CStrW font{L"mono-10"};
|
||||
const CFontMetrics fontMetrics{CStrIntern(font.ToUTF8())};
|
||||
|
||||
const float lineHeight{static_cast<float>(fontMetrics.GetHeight())};
|
||||
const float lineSpacing{static_cast<float>(fontMetrics.GetLineSpacing())};
|
||||
|
||||
CGUIString string;
|
||||
CGUIText text;
|
||||
|
|
@ -114,13 +125,13 @@ public:
|
|||
// We have 10 calls: the 9 words (wrap-around is split in two), the space after the newline.
|
||||
TS_ASSERT_EQUALS(text.GetTextCalls().size(), 10);
|
||||
TS_ASSERT_LESS_THAN(text.GetSize().Width, width);
|
||||
TS_ASSERT_EQUALS(text.GetSize().Height, padding * 2 + lineHeight + lineSpacing * 4);
|
||||
TS_ASSERT_EQUALS(text.GetSize().Height, padding * 2 + lineHeight + lineSpacing * 3);
|
||||
|
||||
align = EAlign::RIGHT;
|
||||
text = CGUIText(gui, string, font, width, padding, align, nullptr);
|
||||
TS_ASSERT_EQUALS(text.GetTextCalls().size(), 10);
|
||||
TS_ASSERT_EQUALS(text.GetSize().Width, renderedWidth); // Should be the same width as the left-case.
|
||||
TS_ASSERT_EQUALS(text.GetSize().Height, padding * 2 + lineHeight + lineSpacing * 4);
|
||||
TS_ASSERT_EQUALS(text.GetSize().Height, padding * 2 + lineHeight + lineSpacing * 3);
|
||||
|
||||
width = 400.f;
|
||||
padding = 3.0f;
|
||||
|
|
@ -194,7 +205,7 @@ public:
|
|||
const CStrW text = firstWord + L" " + secondWord;
|
||||
CGUIString string;
|
||||
string.SetValue(text);
|
||||
const CStrW font = L"console";
|
||||
const CStrW font{L"mono-10"};
|
||||
CFontMetrics fontMetrics{CStrIntern(font.ToUTF8())};
|
||||
|
||||
int firstWordWidth = 0, firstWordHeight = 0;
|
||||
|
|
@ -270,11 +281,10 @@ public:
|
|||
{
|
||||
CGUI gui{*g_ScriptContext};
|
||||
|
||||
const CStrW font = L"console";
|
||||
// Make sure this matches the value of the file.
|
||||
// TODO: load dynamically.
|
||||
const float lineHeight = 12.f;
|
||||
const float lineSpacing = 15.f;
|
||||
const CStrW font{L"mono-10"};
|
||||
const CFontMetrics fontMetrics{CStrIntern(font.ToUTF8())};
|
||||
const float lineHeight{static_cast<float>(fontMetrics.GetHeight())};
|
||||
const float lineSpacing{static_cast<float>(fontMetrics.GetLineSpacing())};
|
||||
|
||||
float renderedWidth = 0.f;
|
||||
const float width = 200.f;
|
||||
|
|
@ -318,31 +328,32 @@ public:
|
|||
|
||||
void test_regression_rP26522()
|
||||
{
|
||||
TS_ASSERT_OK(g_VFS->Mount(L"", DataDir() / "mods" / "mod" / "", VFS_MOUNT_MUST_EXIST));
|
||||
|
||||
CGUI gui{*g_ScriptContext};
|
||||
|
||||
const CStrW font = L"sans-bold-13";
|
||||
const CStrW font{L"sans-bold-13"};
|
||||
CFontMetrics fontMetrics{CStrIntern(font.ToUTF8())};
|
||||
const float lineHeight = fontMetrics.GetHeight();
|
||||
const float lineSpacing = fontMetrics.GetLineSpacing();
|
||||
|
||||
CGUIString string;
|
||||
CGUIText text;
|
||||
|
||||
// rP26522 introduced a bug that triggered in rare cases with word-wrapping.
|
||||
string.SetValue(L"90–120 min");
|
||||
text = CGUIText(gui, string, L"sans-bold-13", 53, 8.f, EAlign::LEFT, nullptr);
|
||||
text = CGUIText(gui, string, font, 53, 8.f, EAlign::LEFT, nullptr);
|
||||
|
||||
TS_ASSERT_EQUALS(text.GetTextCalls().size(), 2);
|
||||
TS_ASSERT_EQUALS(text.GetSize().Height, 14 + 9 + 8 * 2);
|
||||
TS_ASSERT_EQUALS(text.GetSize().Height, lineHeight + lineSpacing + 8 * 2);
|
||||
}
|
||||
|
||||
void test_multiple_blank_spaces()
|
||||
{
|
||||
CGUI gui{*g_ScriptContext};
|
||||
|
||||
const CStrW font = L"console";
|
||||
// Make sure this matches the value of the file.
|
||||
// TODO: load dynamically.
|
||||
const float lineHeight = 12.f;
|
||||
const float lineSpacing = 15.f;
|
||||
const CStrW font{L"mono-10"};
|
||||
const CFontMetrics fontMetrics{CStrIntern(font.ToUTF8())};
|
||||
const float lineHeight{static_cast<float>(fontMetrics.GetHeight())};
|
||||
const float lineSpacing{static_cast<float>(fontMetrics.GetLineSpacing())};
|
||||
|
||||
CGUIString string;
|
||||
CGUIText text;
|
||||
|
|
@ -357,7 +368,8 @@ public:
|
|||
// Blank spaces are treated as a word.
|
||||
TS_ASSERT_EQUALS(text.GetTextCalls().size(), 26);
|
||||
TS_ASSERT_EQUALS(text.GetSize().Height, lineHeight + lineSpacing * 4);
|
||||
TS_ASSERT_EQUALS(text.GetSize().Width, 89.f);
|
||||
// The longest line in text is " spaces " which amounts to 13 characters with a character width of 6.
|
||||
TS_ASSERT_EQUALS(text.GetSize().Width, 78.f);
|
||||
renderedWidth = text.GetSize().Width;
|
||||
|
||||
align = EAlign::RIGHT;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -24,6 +24,7 @@
|
|||
#include "gui/CGUIText.h"
|
||||
#include "gui/ObjectBases/IGUIObject.h"
|
||||
#include "gui/SettingTypes/CGUIString.h"
|
||||
#include "i18n/L10n.h"
|
||||
#include "ps/CLogger.h"
|
||||
#include "ps/ConfigDB.h"
|
||||
#include "ps/Filesystem.h"
|
||||
|
|
@ -41,6 +42,7 @@ class TestGUISetting : public CxxTest::TestSuite
|
|||
std::optional<CXeromycesEngine> m_XeromycesEngine;
|
||||
std::unique_ptr<CProfileViewer> m_Viewer;
|
||||
std::unique_ptr<CRenderer> m_Renderer;
|
||||
std::unique_ptr<L10n> m_L10n;
|
||||
|
||||
public:
|
||||
class TestGUIObject : public IGUIObject
|
||||
|
|
@ -70,10 +72,12 @@ public:
|
|||
g_VideoMode.CreateBackendDevice(false);
|
||||
m_Viewer = std::make_unique<CProfileViewer>();
|
||||
m_Renderer = std::make_unique<CRenderer>(g_VideoMode.GetBackendDevice());
|
||||
m_L10n = std::make_unique<L10n>();
|
||||
}
|
||||
|
||||
void tearDown()
|
||||
{
|
||||
m_L10n.reset();
|
||||
m_Renderer.reset();
|
||||
m_Viewer.reset();
|
||||
g_VideoMode.Shutdown();
|
||||
|
|
|
|||
|
|
@ -644,6 +644,8 @@ void CRenderer::RenderFrame2D(const bool renderGUI, const bool renderLogger)
|
|||
// Profile information
|
||||
g_ProfileViewer.RenderProfile(canvas);
|
||||
}
|
||||
|
||||
this->GetFontManager().UploadTexturesAtlasToGPU();
|
||||
}
|
||||
|
||||
void CRenderer::RenderScreenShot(const bool needsPresent)
|
||||
|
|
|
|||
Loading…
Reference in a new issue