mirror of
https://gitea.wildfiregames.com/0ad/0ad
synced 2026-06-16 05:13:58 -07:00
Reduces string allocations in GUI and maths.
This commit is contained in:
parent
4bf107352f
commit
bedb6129f2
16 changed files with 315 additions and 51 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)};
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
105
source/ps/strings/StringBuilder.h
Normal file
105
source/ps/strings/StringBuilder.h
Normal 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
|
||||
66
source/ps/strings/tests/test_StringBuilder.h
Normal file
66
source/ps/strings/tests/test_StringBuilder.h
Normal 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");
|
||||
}
|
||||
};
|
||||
|
|
@ -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");
|
||||
|
|
|
|||
Loading…
Reference in a new issue