0ad/source/graphics/Font.cpp
trompetin17 c8c1522953
Fix memory leak from Font
FreeType’s FT_Glyph_To_Bitmap mutates the FT_Glyph by allocating a new
glyph object and overwriting the input pointer. This creates a risk of
memory leaks if the old glyph isn’t released or if ownership is unclear.

To avoid this, we now store the glyph in a UniqueFTGlyph and explicitly
pass it by reference to FT_Glyph_To_Bitmap. After the mutation,
ownership continues to reside in the smart pointer, and cleanup is
guaranteed even in error branches.

For temporary glyphs (e.g., strokes), we still manually call
FT_Done_Glyph, since they are not managed by UniqueFTGlyph. These
changes ensure all glyph memory is correctly released and reduce the
need for repetitive error-path cleanup code.
2025-05-22 11:35:12 -05:00

473 lines
14 KiB
C++

/* 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 <http://www.gnu.org/licenses/>.
*/
#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 <algorithm>
#include <map>
#include <numeric>
#include <string>
namespace
{
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];
}
void CFont::GlyphMap::set(u16 codepoint, const GlyphData& glyph)
{
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;
}
int 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, int& width, int& height)
{
PROFILE2("CalculateStringSize font texture generate");
width = 0;
height = m_Height;
// Compute the width as the width of the longest line.
std::wistringstream stream{string};
std::wstring line;
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 CFont::GlyphData* g{GetGlyph(c)};
if (!g)
return sum;
if (!FT_HAS_KERNING(m_Font))
return sum + g->xadvance;
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)
{
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);
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<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);
FT_Done_Glyph(strokedGlyph);
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);
FT_Done_Glyph(glyph);
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));
}
}
}
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);
}
}