From bedb6129f28da31c92a1ed8575db82e02f9696dc Mon Sep 17 00:00:00 2001 From: Vladislav Belov Date: Sat, 11 Oct 2025 19:08:02 +0200 Subject: [PATCH] Reduces string allocations in GUI and maths. --- source/graphics/Canvas2D.cpp | 13 ++- source/graphics/Font.cpp | 9 +- source/graphics/Font.h | 5 +- source/graphics/FontManager.cpp | 43 +++++--- source/graphics/FontManager.h | 10 +- source/graphics/FontMetrics.h | 4 +- source/graphics/ShaderDefines.cpp | 14 ++- source/graphics/TextRenderer.cpp | 12 +-- source/graphics/TextRenderer.h | 7 +- source/maths/Fixed.cpp | 19 ++-- source/ps/CStrIntern.cpp | 5 + source/ps/CStrIntern.h | 1 + source/ps/ConfigDB.cpp | 13 ++- source/ps/strings/StringBuilder.h | 105 +++++++++++++++++++ source/ps/strings/tests/test_StringBuilder.h | 66 ++++++++++++ source/ps/tests/test_ConfigDB.h | 40 +++++++ 16 files changed, 315 insertions(+), 51 deletions(-) create mode 100644 source/ps/strings/StringBuilder.h create mode 100644 source/ps/strings/tests/test_StringBuilder.h diff --git a/source/graphics/Canvas2D.cpp b/source/graphics/Canvas2D.cpp index 5b7015d442..954176e7e1 100644 --- a/source/graphics/Canvas2D.cpp +++ b/source/graphics/Canvas2D.cpp @@ -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 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 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) diff --git a/source/graphics/Font.cpp b/source/graphics/Font.cpp index d00af9f7c5..b5c3f98b61 100644 --- a/source/graphics/Font.cpp +++ b/source/graphics/Font.cpp @@ -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)}; diff --git a/source/graphics/Font.h b/source/graphics/Font.h index 9b992822da..0ab702d8e1 100644 --- a/source/graphics/Font.h +++ b/source/graphics/Font.h @@ -31,6 +31,7 @@ #include FT_GLYPH_H #include FT_IMAGE_H #include FT_STROKER_H +#include #include #include #include @@ -49,6 +50,7 @@ class CFont public: CFont(FT_Library library, const std::array& 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& m_GammaCorrectionLUT; + std::reference_wrapper> m_GammaCorrectionLUT; FT_Library m_FreeType; std::vector> m_FontsData; diff --git a/source/graphics/FontManager.cpp b/source/graphics/FontManager.cpp index 1d1b84177a..d6c10f8c8a 100644 --- a/source/graphics/FontManager.cpp +++ b/source/graphics/FontManager.cpp @@ -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 @@ -101,6 +102,11 @@ FontSpec ParseFontSpec(const std::string& spec) } // namespace CFontManager::CFontManager() + : m_GUIScaleHook{std::make_unique(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 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 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 CFontManager::LoadFont(CStrIntern fontName, CStrIntern lo }() }; - std::shared_ptr font{std::make_shared(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 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 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); } diff --git a/source/graphics/FontManager.h b/source/graphics/FontManager.h index c068739311..12f90a7ab6 100644 --- a/source/graphics/FontManager.h +++ b/source/graphics/FontManager.h @@ -27,6 +27,7 @@ #include #include +class CConfigDBHook; class CFont; struct FT_LibraryRec_; @@ -39,10 +40,10 @@ class CFontManager { public: CFontManager(); - ~CFontManager() = default; + ~CFontManager(); NONCOPYABLE(CFontManager); - std::shared_ptr 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> m_GammaCorrectionLUT; - using FontsMap = std::unordered_map>; + using FontsMap = std::unordered_map; FontsMap m_Fonts; + float m_GUIScale{1.0f}; + std::unique_ptr m_GUIScaleHook; + /* * Most monitors today use 2.2 as the standard gamma. * MacOS may use 2.2 or 1.8 in some cases. diff --git a/source/graphics/FontMetrics.h b/source/graphics/FontMetrics.h index 999ed97f4d..fb4586502e 100644 --- a/source/graphics/FontMetrics.h +++ b/source/graphics/FontMetrics.h @@ -18,8 +18,6 @@ #ifndef INCLUDED_FONTMETRICS #define INCLUDED_FONTMETRICS -#include - class CFont; class CStrIntern; @@ -40,7 +38,7 @@ public: void CalculateStringSize(const wchar_t* string, float& w, float& h) const; private: - std::shared_ptr m_Font; + CFont* m_Font; }; #endif // INCLUDED_FONTMETRICS diff --git a/source/graphics/ShaderDefines.cpp b/source/graphics/ShaderDefines.cpp index 7fb54d2ce2..20fc45a3f9 100644 --- a/source/graphics/ShaderDefines.cpp +++ b/source/graphics/ShaderDefines.cpp @@ -29,9 +29,15 @@ #include #include -#include #include #include +#include + +#if defined(__cpp_lib_to_chars) +#include +#else +#include +#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; } } diff --git a/source/graphics/TextRenderer.cpp b/source/graphics/TextRenderer.cpp index 2467cc7c72..6d623f2248 100644 --- a/source/graphics/TextRenderer.cpp +++ b/source/graphics/TextRenderer.cpp @@ -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 indices; std::vector positions; std::vector 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()); diff --git a/source/graphics/TextRenderer.h b/source/graphics/TextRenderer.h index 7db102e7d8..e933967be7 100644 --- a/source/graphics/TextRenderer.h +++ b/source/graphics/TextRenderer.h @@ -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 font; + CFont* font; std::list runs; }; @@ -168,7 +169,7 @@ private: CColor m_Color; CStrIntern m_FontName; - std::shared_ptr m_Font; + CFont* m_Font{}; bool m_Dirty = true; diff --git a/source/maths/Fixed.cpp b/source/maths/Fixed.cpp index eafbc77170..a6babec23d 100644 --- a/source/maths/Fixed.cpp +++ b/source/maths/Fixed.cpp @@ -20,8 +20,8 @@ #include "Fixed.h" #include "ps/CStr.h" +#include "ps/strings/StringBuilder.h" -#include #include 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 diff --git a/source/ps/CStrIntern.cpp b/source/ps/CStrIntern.cpp index 0b1351e22a..9b20e64bb2 100644 --- a/source/ps/CStrIntern.cpp +++ b/source/ps/CStrIntern.cpp @@ -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; diff --git a/source/ps/CStrIntern.h b/source/ps/CStrIntern.h index 4a4eef673a..ef345ec41b 100644 --- a/source/ps/CStrIntern.h +++ b/source/ps/CStrIntern.h @@ -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. diff --git a/source/ps/ConfigDB.cpp b/source/ps/ConfigDB.cpp index 3137605b0b..95ac817358 100644 --- a/source/ps/ConfigDB.cpp +++ b/source/ps/ConfigDB.cpp @@ -36,8 +36,14 @@ #include #include #include -#include #include +#include + +#if defined(__cpp_lib_to_chars) +#include +#else +#include +#endif namespace { @@ -65,8 +71,13 @@ const std::unordered_set g_UnloggedEntries = { template 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) diff --git a/source/ps/strings/StringBuilder.h b/source/ps/strings/StringBuilder.h new file mode 100644 index 0000000000..624f2ddc57 --- /dev/null +++ b/source/ps/strings/StringBuilder.h @@ -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 . + */ + +#ifndef INCLUDED_PS_STRINGBUILDER +#define INCLUDED_PS_STRINGBUILDER + +#include +#include +#include +#include +#include +#include +#include + +#if defined(__cpp_lib_to_chars) +#include +#else +#include +#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 buffer) noexcept + : m_Buffer{buffer}, m_BufferBegin{buffer.data()} + { + ENSURE(!m_Buffer.empty()); + } + + template + typename std::enable_if>::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(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 m_Buffer; + char* m_BufferBegin; +}; + +} // namespace PS + +#endif // INCLUDED_PS_STRINGBUILDER diff --git a/source/ps/strings/tests/test_StringBuilder.h b/source/ps/strings/tests/test_StringBuilder.h new file mode 100644 index 0000000000..2f447791c6 --- /dev/null +++ b/source/ps/strings/tests/test_StringBuilder.h @@ -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 . + */ + +#include "lib/self_test.h" + +#include "ps/strings/StringBuilder.h" + +#include +#include + +class TestStringBuilder : public CxxTest::TestSuite +{ +public: + template + 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(0, "0"); + Check(std::numeric_limits::min(), "-2147483648"); + Check(std::numeric_limits::max(), "2147483647"); + + Check(std::numeric_limits::min(), "0"); + Check(std::numeric_limits::max(), "18446744073709551615"); + + Check(0.0f, "0"); + Check(1.0f, "1"); + Check(-1.0f, "-1"); + Check(0.5f, "0.5"); + Check(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"); + } +}; diff --git a/source/ps/tests/test_ConfigDB.h b/source/ps/tests/test_ConfigDB.h index 651b951acf..7ef198f4ea 100644 --- a/source/ps/tests/test_ConfigDB.h +++ b/source/ps/tests/test_ConfigDB.h @@ -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");