/* 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
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it 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 0 A.D. If not, see .
*/
#include "precompiled.h"
#include "Font.h"
#include "graphics/TextureManager.h"
#include "ps/CLogger.h"
#include "ps/Filesystem.h"
#include "ps/Profile.h"
#include
#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
{
FT_Done_Glyph(glyph);
}
};
using UniqueFTGlyph = std::unique_ptr, 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];
}
void CFont::GlyphMap::set(u16 codepoint, const GlyphData& glyph)
{
if (!m_Data[codepoint >> 8])
m_Data[codepoint >> 8] = std::make_unique>();
(*m_Data[codepoint >> 8])[codepoint & 0xff] = glyph;
(*m_Data[codepoint >> 8])[codepoint & 0xff].defined = 1;
}
float CFont::GetHeight() const {
return m_Height / m_Scale;
}
float CFont::GetCapHeight()
{
const CFont::GlyphData* g{GetGlyph(L'I')};
return (g ? g->yadvance : 0) + std::abs(FPosF26Dot6ToFloat(m_Faces.front()->size->metrics.descender));
}
float CFont::GetCharacterWidth(wchar_t c)
{
PROFILE2("GetCharacterWidth font texture generate");
const CFont::GlyphData* g{GetGlyph(c)};
return g ? g->xadvance : 0;
}
void CFont::CalculateStringSize(const wchar_t* string, float& width, float& height)
{
PROFILE2("CalculateStringSize font texture generate");
width = 0;
height = 0;
// Compute the width as the width of the longest line.
std::wstring original{string};
std::wistringstream stream{string};
std::wstring line;
bool firstLine{true};
while (std::getline(stream, line))
{
FT_UInt glyphIndexStorage{0};
const float lineWidth{std::accumulate(line.begin(), line.end(), 0.0f, [&](float sum, wchar_t c)
{
const CFont::GlyphData* g{GetGlyph(c)};
if (!g)
return sum;
if (!FT_HAS_KERNING(g->face))
return sum + g->xadvance;
const FT_UInt glyphIndex{FT_Get_Char_Index(g->face, 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(g->face, prevGlyph, glyphIndex, FT_KERNING_DEFAULT, &kerning);
// Add the kerning distance.
return sum + g->xadvance + FPosF26Dot6ToFloat(kerning.x);
})
};
width = std::max(width, lineWidth);
if (!firstLine || !line.empty())
height += firstLine ? GetCapHeight() : GetHeight();
firstLine = false;
}
if (original.back() == L'\n')
height += GetHeight();
}
bool CFont::SetFontParams(const std::string& fontName, float size, float strokeWidth, float scale)
{
ENSURE(m_FontSize == 0 && size > 0);
// TODO: expose the Stroke Width outside class.
m_Scale = scale;
m_StrokeWidth = strokeWidth * scale;
m_FontSize = size * scale;
m_FontName = fontName;
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(), FloatToF26Dot6(m_StrokeWidth), FT_STROKER_LINECAP_ROUND, FT_STROKER_LINEJOIN_ROUND, 0);
}
if (!ConstructTextureAtlas())
{
LOGERROR("Failed to create font texture atlas %s", fontName);
return false;
}
return true;
}
bool CFont::AddFontFromPath(const OsPath& fontPath)
{
ENSURE(m_FontSize > 0);
if (!VfsFileExists(fontPath))
{
LOGERROR("Font file does not exist: %s", fontPath.string8());
return false;
}
std::shared_ptr fontData;
size_t fontDataSize;
if (g_VFS->LoadFile(fontPath, fontData, fontDataSize) != 0)
{
LOGERROR("Failed to load font file: %s", fontPath.string8());
return false;
}
FT_Face face;
if (FT_Error error{FT_New_Memory_Face(m_FreeType, fontData.get(), static_cast(fontDataSize), 0, &face)}; error == FT_Err_Unknown_File_Format)
{
LOGERROR("Font file format is not supported: %s", fontPath.string8());
return false;
}
else if (error)
{
LOGERROR("Failed to load font %s: %d", fontPath.string8(), error);
return false;
}
// Keep the font data alive.
m_FontsData.push_back(fontData);
// Set the font size.
if (FT_Error error{FT_Set_Char_Size(face, 0, FloatToF26Dot6(m_FontSize), 0 , 0)})
{
LOGERROR("Failed to set font size %d: %d", m_FontSize, error);
return false;
}
// Get the height of the font.
if(m_Faces.empty())
m_Height = FPosF26Dot6ToFloat(face->size->metrics.height);
// Add the fallback font to the list.
m_Faces.push_back({face, &ftFaceDeleter});
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;
}
m_IsLoadingTextureToGPU = true;
// Initialise texture with transparency, for the areas we don't
// overwrite with uploading later.
m_TexData = std::make_unique(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)
{
ENSURE(!m_Faces.empty());
PROFILE2("Glyph font texture generate");
const auto [faceToUse, glyphIndex]{[&]()->std::pair
{
FT_UInt index{0};
std::vector::iterator it{std::find_if(m_Faces.begin(), m_Faces.end(), [&](const UniqueFTFace& face)
{
index = FT_Get_Char_Index(face.get(), codepoint);
return index != 0;
}
)};
return {it != m_Faces.end() ? it->get() : m_Faces.front().get(), index};
}()
};
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(faceToUse, glyphIndex, loadFlags)})
{
LOGERROR("Failed to load glyph %u: %d", codepoint, error);
return nullptr;
}
const FT_GlyphSlot slot{faceToUse->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 float baselineInAtlas{FPosF26Dot6ToFloat(faceToUse->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 += std::ceil(m_Height + m_StrokeWidth + 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;
CVector2D offset{0,0};
const FT_Render_Mode renderMode{FT_RENDER_MODE_NORMAL};
if (m_StrokeWidth)
{
std::optional 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 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(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.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 / m_Scale;
gd.yadvance = FPosF26Dot6ToFloat(slot->metrics.height) / m_Scale;
gd.defined = 1;
gd.face = faceToUse;
m_Glyphs.set(codepoint, gd);
// Update positions for next glyph.
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 float baselineInAtlas)
{
FT_Glyph strokedGlyph;
if (FT_Error error{FT_Glyph_Copy(glyph, &strokedGlyph)})
{
LOGERROR("Failed to copy glyph %u: %d", codepoint, error);
FT_Done_Glyph(strokedGlyph);
return std::nullopt;
}
if (FT_Error error{FT_Glyph_StrokeBorder(&strokedGlyph, m_Stroker.get(), 0, 1)})
{
LOGERROR("Failed to stroke glyph %u: %d", codepoint, error);
FT_Done_Glyph(strokedGlyph);
return std::nullopt;
}
if (FT_Error error{FT_Glyph_To_Bitmap(&strokedGlyph, renderMode, nullptr, 1)})
{
LOGERROR("Failed to render glyph %u: %d", codepoint, error);
FT_Done_Glyph(strokedGlyph);
return std::nullopt;
}
FT_BitmapGlyph bitmapGlyph{reinterpret_cast(strokedGlyph)};
FT_Bitmap& bitmapStroke{bitmapGlyph->bitmap};
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;
targetStrokeX = m_AtlasX;
}
if (targetStrokeY < m_AtlasY)
{
offset.Y = bitmapGlyph->top - baselineInAtlas - m_StrokeWidth;
targetStrokeY = m_AtlasY;
}
BlendGlyphBitmapToTexture(bitmapStroke, targetStrokeX, targetStrokeY, 0, 0, 0);
FT_Done_Glyph(strokedGlyph);
return offset;
}
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)})
{
LOGERROR("Failed to render glyph %u: %d", codepoint, error);
return std::nullopt;
}
FT_BitmapGlyph bitmapGlyph{reinterpret_cast(glyph)};
FT_Bitmap& bitmap{bitmapGlyph->bitmap};
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;
targetX = m_AtlasX;
}
if (targetY < m_AtlasY)
{
newOffset.Y = bitmapGlyph->top - baselineInAtlas - m_StrokeWidth;
targetY = m_AtlasY;
}
BlendGlyphBitmapToTexture(bitmap, targetX, targetY, 255, 255, 255);
FT_Done_Glyph(glyph);
return newOffset;
}
void CFont::UploadTextureAtlasToGPU()
{
if (std::exchange(m_IsLoadingTextureToGPU, 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 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(std::round(((r * srcAlpha + tempDstRow[0] * dstAlpha * (1.0f - srcAlpha)) / outAlpha)));
tempDstRow[1] = static_cast(std::round(((g * srcAlpha + tempDstRow[1] * dstAlpha * (1.0f - srcAlpha)) / outAlpha)));
tempDstRow[2] = static_cast(std::round(((b * srcAlpha + tempDstRow[2] * dstAlpha * (1.0f - srcAlpha)) / outAlpha)));
tempDstRow[3] = static_cast(std::round(outAlpha * 255.0f));
}
}
}
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);
}
}