Reduces string allocations in GUI and maths.

This commit is contained in:
Vladislav Belov 2025-10-11 19:08:02 +02:00
parent 4bf107352f
commit bedb6129f2
No known key found for this signature in database
GPG key ID: 353545E45DB9CCB3
16 changed files with 315 additions and 51 deletions

View file

@ -31,6 +31,7 @@
#include "maths/Matrix3D.h"
#include "maths/Rect.h"
#include "maths/Vector2D.h"
#include "ps/ConfigDB.h"
#include "ps/CStrIntern.h"
#include "ps/CStrInternStatic.h"
#include "ps/containers/StaticVector.h"
@ -98,7 +99,8 @@ public:
const uint32_t widthInPixels, const uint32_t heightInPixels, const float scale,
Renderer::Backend::IDeviceCommandContext* deviceCommandContext)
: WidthInPixels(widthInPixels), HeightInPixels(heightInPixels),
Scale(scale), DeviceCommandContext(deviceCommandContext)
Scale(scale), DeviceCommandContext(deviceCommandContext),
DebugFontBox(g_ConfigDB.Get("fonts.debugbox", false))
{
constexpr std::array<Renderer::Backend::SVertexAttributeFormat, 2> attributes{{
{Renderer::Backend::VertexAttributeStream::POSITION,
@ -109,6 +111,9 @@ public:
Renderer::Backend::VertexAttributeRate::PER_VERTEX, 1}
}};
m_VertexInputLayout = g_Renderer.GetVertexInputLayout(attributes);
const std::string debugFontBoxColor{g_ConfigDB.Get("fonts.debugboxcolor", std::string{"128 0 128"})};
DebugBoxColor.ParseString(debugFontBoxColor.c_str());
}
void BindTechIfNeeded()
@ -205,6 +210,9 @@ public:
SBindingSlots BindingSlots;
PS::StaticVector<CRect, 4> Scissors;
bool DebugFontBox;
CColor DebugBoxColor;
};
CCanvas2D::CCanvas2D(
@ -478,7 +486,8 @@ void CCanvas2D::DrawText(CTextRenderer& textRenderer)
m->BindingSlots.grayscaleFactor, 0.0f);
textRenderer.Render(
m->DeviceCommandContext, m->Tech->GetShader(), m->TransformScale, m->Translation);
m->DeviceCommandContext, m->Tech->GetShader(), m->TransformScale, m->Translation,
m->DebugFontBox, m->DebugBoxColor);
}
void CCanvas2D::PushScissor(const CRect& scissor)

View file

@ -25,6 +25,7 @@
#include "ps/CLogger.h"
#include "ps/Filesystem.h"
#include "ps/Profiler2.h"
#include "ps/strings/StringBuilder.h"
#include "renderer/Renderer.h"
#include "renderer/backend/IDevice.h"
#include "renderer/backend/IDeviceCommandContext.h"
@ -284,8 +285,12 @@ bool CFont::ConstructAtlasTexture()
m_AtlasSize = m_AtlasWidth * m_AtlasHeight * m_TextureFormatStride;
char buffer[128];
PS::StringBuilder fontTextureNameBuilder{{std::begin(buffer), std::end(buffer)}};
fontTextureNameBuilder.Append("Font Texture ");
fontTextureNameBuilder.Append(m_FontName);
m_Texture = g_Renderer.GetTextureManager().WrapBackendTexture(backendDevice->CreateTexture2D(
("Font Texture " + m_FontName).c_str(),
fontTextureNameBuilder.Str().data(),
Renderer::Backend::ITexture::Usage::TRANSFER_DST |
Renderer::Backend::ITexture::Usage::SAMPLED,
m_TextureFormat,
@ -537,7 +542,7 @@ void CFont::BlendGlyphBitmapToTextureRGBA(const FT_Bitmap& bitmap, int targetX,
u8* tempDstRow{dstRow + x * m_TextureFormatStride};
u8 alpha{srcRow[x]};
const float srcAlpha{m_StrokeWidth > 0 ? m_GammaCorrectionLUT[alpha] : alpha/255.0f};
const float srcAlpha{m_StrokeWidth > 0 ? m_GammaCorrectionLUT.get()[alpha] : alpha / 255.0f};
const float dstAlpha{tempDstRow[3] / 255.0f};
const float outAlpha{srcAlpha + dstAlpha * (1.0f - srcAlpha)};

View file

@ -31,6 +31,7 @@
#include FT_GLYPH_H
#include FT_IMAGE_H
#include FT_STROKER_H
#include <functional>
#include <memory>
#include <optional>
#include <string>
@ -49,6 +50,7 @@ class CFont
public:
CFont(FT_Library library, const std::array<float, 256>& gammaCorrection) : m_FreeType{library}, m_GammaCorrectionLUT{gammaCorrection} {}
~CFont() = default;
MOVABLE(CFont);
NONCOPYABLE(CFont);
struct GlyphData
@ -73,6 +75,7 @@ public:
public:
GlyphMap() = default;
~GlyphMap() = default;
MOVABLE(GlyphMap);
NONCOPYABLE(GlyphMap);
/**
@ -181,7 +184,7 @@ private:
float m_FontSize{0.0f};
std::string m_FontName;
const std::array<float, 256>& m_GammaCorrectionLUT;
std::reference_wrapper<const std::array<float, 256>> m_GammaCorrectionLUT;
FT_Library m_FreeType;
std::vector<std::shared_ptr<u8>> m_FontsData;

View file

@ -29,6 +29,7 @@
#include "ps/ConfigDB.h"
#include "ps/Filesystem.h"
#include "ps/Profiler2.h"
#include "ps/strings/StringBuilder.h"
#include "renderer/backend/IDeviceCommandContext.h"
#include <algorithm>
@ -101,6 +102,11 @@ FontSpec ParseFontSpec(const std::string& spec)
} // namespace
CFontManager::CFontManager()
: m_GUIScaleHook{std::make_unique<CConfigDBHook>(g_ConfigDB.RegisterHookAndCall(
"gui.scale", [this]()
{
m_GUIScale = g_ConfigDB.Get("gui.scale", 1.0f);
}))}
{
FT_Library lib;
FT_Error error{FT_Init_FreeType(&lib)};
@ -115,7 +121,9 @@ CFontManager::CFontManager()
});
}
std::shared_ptr<CFont> CFontManager::LoadFont(CStrIntern fontName, CStrIntern locale)
CFontManager::~CFontManager() = default;
CFont* CFontManager::LoadFont(CStrIntern fontName, CStrIntern locale)
{
const std::string localeToUse{[&]
{
@ -129,12 +137,19 @@ std::shared_ptr<CFont> CFontManager::LoadFont(CStrIntern fontName, CStrIntern lo
return g_L10n.GetCurrentLocaleString();
} ()
};
const float guiScale{g_ConfigDB.Get("gui.scale", 1.0f)};
CStrIntern localeFontName{fmt::format("{}{}-{}", localeToUse ,fontName.string(), guiScale)};
// fmt::format_to_n is expensive for frequent LoadFont calls, parsing the
// format string takes a noticeable amount of time.
char buffer[128];
PS::StringBuilder fontNameBuilder{{std::begin(buffer), std::end(buffer)}};
fontNameBuilder.Append(localeToUse);
fontNameBuilder.Append(fontName.string());
fontNameBuilder.Append('-');
fontNameBuilder.Append(m_GUIScale);
CStrIntern localeFontName{fontNameBuilder.Str()};
FontsMap::iterator it{m_Fonts.find(localeFontName)};
if (it != m_Fonts.end())
return it->second;
return &it->second;
// TODO: use hooks or something to hotrealoding default font.
const std::string defaultFont{g_ConfigDB.Get("fonts.default", std::string{})};
@ -194,9 +209,9 @@ std::shared_ptr<CFont> CFontManager::LoadFont(CStrIntern fontName, CStrIntern lo
}()
};
std::shared_ptr<CFont> font{std::make_shared<CFont>(this->m_FreeType.get(), *m_GammaCorrectionLUT)};
CFont font{this->m_FreeType.get(), *m_GammaCorrectionLUT};
if (!font->SetFontParams(localeFontName.string(), fontSpec.size, fontSpec.stroke ? 1.0f : 0.0f, guiScale))
if (!font.SetFontParams(localeFontName.string(), fontSpec.size, fontSpec.stroke ? 1.0f : 0.0f, m_GUIScale))
{
LOGERROR("Failed to set font params for %s", localeFontName.string().c_str());
return nullptr;
@ -214,7 +229,7 @@ std::shared_ptr<CFont> CFontManager::LoadFont(CStrIntern fontName, CStrIntern lo
return nullptr;
}
if (!font->AddFontFromPath(fntPath))
if (!font.AddFontFromPath(fntPath))
{
LOGERROR("Failed to load font %s", fntPath.string8());
return nullptr;
@ -226,10 +241,9 @@ std::shared_ptr<CFont> CFontManager::LoadFont(CStrIntern fontName, CStrIntern lo
// Common characters are: Latin, numbers, punctuation.
std::string_view glypshSet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789.,;:!?\"'()[]{}<>-+=_@#$%^&*`~\\|/";
for (const char c : glypshSet)
font->GetGlyph(c);
font.GetGlyph(c);
m_Fonts[localeFontName] = font;
return font;
return &m_Fonts.insert_or_assign(localeFontName, std::move(font)).first->second;
}
void CFontManager::UploadAtlasTexturesToGPU(Renderer::Backend::IDeviceCommandContext* deviceCommandContext)
@ -237,11 +251,6 @@ void CFontManager::UploadAtlasTexturesToGPU(Renderer::Backend::IDeviceCommandCon
PROFILE2("Loading font textures");
GPU_SCOPED_LABEL(deviceCommandContext, "Loading font textures");
for (auto& [fontName, fontPtr] : m_Fonts)
{
if (!fontPtr)
continue;
fontPtr->UploadAtlasTextureToGPU(deviceCommandContext);
}
for (auto& [fontName, font] : m_Fonts)
font.UploadAtlasTextureToGPU(deviceCommandContext);
}

View file

@ -27,6 +27,7 @@
#include <memory>
#include <unordered_map>
class CConfigDBHook;
class CFont;
struct FT_LibraryRec_;
@ -39,10 +40,10 @@ class CFontManager
{
public:
CFontManager();
~CFontManager() = default;
~CFontManager();
NONCOPYABLE(CFontManager);
std::shared_ptr<CFont> LoadFont(CStrIntern fontName, CStrIntern locale);
CFont* LoadFont(CStrIntern fontName, CStrIntern locale);
void UploadAtlasTexturesToGPU(
Renderer::Backend::IDeviceCommandContext* deviceCommandContext);
@ -56,9 +57,12 @@ private:
std::unique_ptr<std::array<float, 256>> m_GammaCorrectionLUT;
using FontsMap = std::unordered_map<CStrIntern, std::shared_ptr<CFont>>;
using FontsMap = std::unordered_map<CStrIntern, CFont>;
FontsMap m_Fonts;
float m_GUIScale{1.0f};
std::unique_ptr<CConfigDBHook> m_GUIScaleHook;
/*
* Most monitors today use 2.2 as the standard gamma.
* MacOS may use 2.2 or 1.8 in some cases.

View file

@ -18,8 +18,6 @@
#ifndef INCLUDED_FONTMETRICS
#define INCLUDED_FONTMETRICS
#include <memory>
class CFont;
class CStrIntern;
@ -40,7 +38,7 @@ public:
void CalculateStringSize(const wchar_t* string, float& w, float& h) const;
private:
std::shared_ptr<CFont> m_Font;
CFont* m_Font;
};
#endif // INCLUDED_FONTMETRICS

View file

@ -29,9 +29,15 @@
#include <algorithm>
#include <iterator>
#include <sstream>
#include <string>
#include <string_view>
#include <version>
#if defined(__cpp_lib_to_chars)
#include <charconv>
#else
#include <sstream>
#endif
namespace std
{
@ -185,9 +191,13 @@ int CShaderDefines::GetInt(const char* name) const
{
if (item.first == nameIntern)
{
int ret;
int ret{};
#if defined(__cpp_lib_to_chars)
std::from_chars(item.second.c_str(), item.second.c_str() + item.second.string().size(), ret);
#else
std::stringstream str(item.second.c_str());
str >> ret;
#endif
return ret;
}
}

View file

@ -179,7 +179,7 @@ void CTextRenderer::PutString(float x, float y, const std::wstring* buf, bool ow
batch.translate = m_Translate;
batch.color = m_Color;
batch.font = m_Font;
m_Batches.push_back(batch);
m_Batches.emplace_back(batch);
m_Dirty = false;
}
@ -187,7 +187,7 @@ void CTextRenderer::PutString(float x, float y, const std::wstring* buf, bool ow
SBatchRun run;
run.x = x;
run.y = y;
m_Batches.back().runs.push_back(run);
m_Batches.back().runs.emplace_back(run);
m_Batches.back().runs.back().text = buf;
m_Batches.back().runs.back().owned = owned;
m_Batches.back().chars += buf->size();
@ -207,17 +207,13 @@ struct SBatchCompare
void CTextRenderer::Render(
Renderer::Backend::IDeviceCommandContext* deviceCommandContext,
Renderer::Backend::IShaderProgram* shader,
const CVector2D& transformScale, const CVector2D& translation)
const CVector2D& transformScale, const CVector2D& translation,
const bool debugFontBox, const CColor& debugBoxColor)
{
std::vector<u16> indices;
std::vector<CVector2D> positions;
std::vector<CVector2D> uvs;
const bool debugFontBox{g_ConfigDB.Get("fonts.debugbox", false)};
const std::string debugFontBoxColor{g_ConfigDB.Get("fonts.debugboxcolor", std::string{"128 0 128"})};
CColor debugBoxColor;
debugBoxColor.ParseString(debugFontBoxColor.c_str());
// Try to merge non-consecutive batches that share the same font/color/translate:
// sort the batch list by font, then merge the runs of adjacent compatible batches
m_Batches.sort(SBatchCompare());

View file

@ -108,7 +108,8 @@ public:
void Render(
Renderer::Backend::IDeviceCommandContext* deviceCommandContext,
Renderer::Backend::IShaderProgram* shader,
const CVector2D& transformScale, const CVector2D& translation);
const CVector2D& transformScale, const CVector2D& translation,
const bool debugFontBox, const CColor& debugBoxColor);
private:
friend struct SBatchCompare;
@ -157,7 +158,7 @@ private:
size_t chars; // sum of runs[i].text->size()
CVector2D translate;
CColor color;
std::shared_ptr<CFont> font;
CFont* font;
std::list<SBatchRun> runs;
};
@ -168,7 +169,7 @@ private:
CColor m_Color;
CStrIntern m_FontName;
std::shared_ptr<CFont> m_Font;
CFont* m_Font{};
bool m_Dirty = true;

View file

@ -20,8 +20,8 @@
#include "Fixed.h"
#include "ps/CStr.h"
#include "ps/strings/StringBuilder.h"
#include <sstream>
#include <string>
template<>
@ -96,18 +96,19 @@ CFixed_15_16 CFixed_15_16::FromString(const CStrW& s)
template<>
CStr8 CFixed_15_16::ToString() const
{
std::stringstream r;
char buffer[16];
PS::StringBuilder builder({std::begin(buffer), std::end(buffer)});
u32 posvalue = abs(value);
if (value < 0)
r << "-";
builder.Append('-');
r << (posvalue >> fract_bits);
builder.Append(posvalue >> fract_bits);
u16 fraction = posvalue & ((1 << fract_bits) - 1);
if (fraction)
{
r << ".";
builder.Append('.');
u32 frac = 0;
u32 div = 1;
@ -125,23 +126,23 @@ CStr8 CFixed_15_16::ToString() const
// If this gives the exact target, then add the digit and stop
if (((u64)frac << 16) / div == fraction)
{
r << digit;
builder.Append(digit);
break;
}
// If the next higher digit gives the exact target, then add that digit and stop
if (digit <= 8 && (((u64)frac+1) << 16) / div == fraction)
{
r << digit+1;
builder.Append(digit+1);
break;
}
// Otherwise add the digit and continue
r << digit;
builder.Append(digit);
}
}
return r.str();
return CStr(builder.Str());
}
// Based on http://www.dspguru.com/dsp/tricks/fixed-point-atan2-with-self-normalization

View file

@ -130,6 +130,11 @@ CStrIntern::CStrIntern(const std::string& str)
m = GetString(str.c_str(), str.length());
}
CStrIntern::CStrIntern(const std::string_view str)
{
m = GetString(str.data(), str.length());
}
u32 CStrIntern::GetHash() const
{
return m->hash;

View file

@ -46,6 +46,7 @@ public:
CStrIntern();
explicit CStrIntern(const char* str);
explicit CStrIntern(const std::string& str);
explicit CStrIntern(const std::string_view str);
/**
* Returns cached FNV1-A hash of the string.

View file

@ -36,8 +36,14 @@
#include <boost/algorithm/string/predicate.hpp>
#include <cstddef>
#include <mutex>
#include <sstream>
#include <unordered_set>
#include <version>
#if defined(__cpp_lib_to_chars)
#include <charconv>
#else
#include <sstream>
#endif
namespace
{
@ -65,8 +71,13 @@ const std::unordered_set<std::string> g_UnloggedEntries = {
template<typename T> void Get(const CStr& value, T& ret)
{
#if defined(__cpp_lib_to_chars)
std::from_chars(value.data(), value.data() + value.size(), ret);
#else
// TODO: switch to std::from_chars after minimal libcxx supports it.
std::stringstream ss(value);
ss >> ret;
#endif
}
template<> void Get<>(const CStr& value, bool& ret)

View file

@ -0,0 +1,105 @@
/* 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/>.
*/
#ifndef INCLUDED_PS_STRINGBUILDER
#define INCLUDED_PS_STRINGBUILDER
#include <charconv>
#include <cstdint>
#include <span>
#include <string>
#include <string_view>
#include <type_traits>
#include <version>
#if defined(__cpp_lib_to_chars)
#include <charconv>
#else
#include <fmt/core.h>
#endif
namespace PS
{
/**
* A helper class to build string in-place without additional cost of
* allocations, locales and format parsing. Prefer to use the class with stack
* allocated buffers over standard tools in high-load cases.
* TODO: in case of overflow it might use a heap allocated storage.
*/
class StringBuilder
{
public:
StringBuilder(std::span<char> buffer) noexcept
: m_Buffer{buffer}, m_BufferBegin{buffer.data()}
{
ENSURE(!m_Buffer.empty());
}
template<typename T>
typename std::enable_if<std::is_arithmetic_v<T>>::type Append(const T value) noexcept
{
ENSURE(m_Buffer.size() > 0);
#if defined(__cpp_lib_to_chars)
const std::to_chars_result result{std::to_chars(m_Buffer.data(), m_Buffer.data() + m_Buffer.size() - 1, value)};
ENSURE(result.ec == std::errc());
m_Buffer = m_Buffer.subspan(result.ptr - m_Buffer.data());
// to_chars doesn't write terminating null.
m_Buffer.front() = 0;
#else
// TODO: switch to std::to_chars after minimal libcxx supports it.
const fmt::format_to_n_result result{
fmt::format_to_n(m_Buffer.data(), m_Buffer.size() - 1, "{}", value)};
ENSURE(m_Buffer.data() != result.out);
m_Buffer = m_Buffer.subspan(result.size);
// format_to_n doesn't write terminating null.
m_Buffer.front() = 0;
#endif
}
void Append(const char ch)
{
ENSURE(m_Buffer.size() > 1);
m_Buffer.front() = ch;
m_Buffer = m_Buffer.subspan(1);
m_Buffer.front() = 0;
}
void Append(const std::string_view str) noexcept
{
ENSURE(m_Buffer.size() >= static_cast<size_t>(str.size()) + 1);
std::copy(str.begin(), str.end(), m_Buffer.begin());
m_Buffer = m_Buffer.subspan(str.size());
m_Buffer.front() = 0;
}
/**
* Returns a string_view to a null-terminated string.
*/
std::string_view Str() noexcept
{
return {m_BufferBegin, m_Buffer.data()};
}
private:
std::span<char> m_Buffer;
char* m_BufferBegin;
};
} // namespace PS
#endif // INCLUDED_PS_STRINGBUILDER

View file

@ -0,0 +1,66 @@
/* 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 "lib/self_test.h"
#include "ps/strings/StringBuilder.h"
#include <array>
#include <limits>
class TestStringBuilder : public CxxTest::TestSuite
{
public:
template<typename T>
void Check(const T value, const std::string_view str)
{
char buffer[64] = {};
PS::StringBuilder builder{{std::begin(buffer), std::end(buffer)}};
builder.Append(value);
TS_ASSERT_EQUALS(builder.Str(), str);
}
void test_append()
{
Check<int>(0, "0");
Check<int>(std::numeric_limits<int>::min(), "-2147483648");
Check<int>(std::numeric_limits<int>::max(), "2147483647");
Check<uint64_t>(std::numeric_limits<uint64_t>::min(), "0");
Check<uint64_t>(std::numeric_limits<uint64_t>::max(), "18446744073709551615");
Check<float>(0.0f, "0");
Check<float>(1.0f, "1");
Check<float>(-1.0f, "-1");
Check<float>(0.5f, "0.5");
Check<float>(1e-3f, "0.001");
}
void test_overflow()
{
char buffer[8] = {};
buffer[3] = 1;
buffer[4] = 2;
PS::StringBuilder builder{{std::begin(buffer), std::begin(buffer) + 4}};
builder.Append("abc");
TS_ASSERT_EQUALS(buffer[0], 'a');
TS_ASSERT_EQUALS(buffer[3], 0);
TS_ASSERT_EQUALS(buffer[4], 2);
TS_ASSERT_EQUALS(builder.Str(), "abc");
}
};

View file

@ -70,6 +70,46 @@ public:
}
}
void CheckFloat(const std::string& value, const float expectedValue)
{
configDB->SetValueString(CFG_SYSTEM, "test_setting", value);
configDB->WriteFile(CFG_SYSTEM);
configDB->Reload(CFG_SYSTEM);
{
std::string res;
configDB->GetValue(CFG_SYSTEM, "test_setting", res);
TS_ASSERT_EQUALS(res, value);
}
{
float res;
configDB->GetValue(CFG_SYSTEM, "test_setting", res);
TS_ASSERT_EQUALS(res, expectedValue);
}
}
void test_setting_float()
{
configDB->SetConfigFile(CFG_SYSTEM, "config/file.cfg");
configDB->WriteFile(CFG_SYSTEM);
configDB->Reload(CFG_SYSTEM);
const char* oldLocale{setlocale(LC_NUMERIC, "")};
for (const char* locale : {oldLocale, "fr_FR.UTF-8", "de_DE.UTF-8", "ja_JP.UTF-8"})
{
setlocale(LC_NUMERIC, locale);
CheckFloat("1", 1.0f);
CheckFloat("1.0", 1.0f);
CheckFloat("1e-3", 1e-3f);
CheckFloat("-1e-3", -1e-3f);
CheckFloat("1234.567", 1234.567f);
CheckFloat("-1234.567", -1234.567f);
CheckFloat("1.0suffix", 1.0f);
}
setlocale(LC_NUMERIC, oldLocale);
}
void test_setting_empty()
{
configDB->SetConfigFile(CFG_SYSTEM, "config/file.cfg");