From 20c40d87e49ba34a3d61a89749b5a3169e3d398c Mon Sep 17 00:00:00 2001 From: OmniaX-Dev Date: Mon, 13 Apr 2026 06:48:19 +0200 Subject: [PATCH] Started work on Font Glyph Atlas --- .clangd | 3 +- CMakeLists.txt | 1 + other/build.nr | 2 +- src/ogfx/gui/widgets/CheckBox.cpp | 2 +- src/ogfx/render/BasicRenderer.cpp | 8 ++ src/ogfx/render/BasicRenderer.hpp | 9 +- src/ogfx/render/FontGlyphAtlas.cpp | 224 +++++++++++++++++++++++++++++ src/ogfx/render/FontGlyphAtlas.hpp | 84 +++++++++++ src/ostd/string/String.cpp | 47 ++++++ src/ostd/string/String.hpp | 5 + src/test/GuiTest.cpp | 19 +-- 11 files changed, 383 insertions(+), 21 deletions(-) create mode 100644 src/ogfx/render/FontGlyphAtlas.cpp create mode 100644 src/ogfx/render/FontGlyphAtlas.hpp diff --git a/.clangd b/.clangd index 122ab09..32169b9 100644 --- a/.clangd +++ b/.clangd @@ -1,2 +1,3 @@ CompileFlags: - CompilationDatabase: bin \ No newline at end of file + CompilationDatabase: bin + Add: ["--target=x86_64-w64-mingw32"] \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index aac59d2..40c633c 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -95,6 +95,7 @@ list(APPEND OGFX_SOURCE_FILES # render ${CMAKE_CURRENT_LIST_DIR}/src/ogfx/render/BasicRenderer.cpp ${CMAKE_CURRENT_LIST_DIR}/src/ogfx/render/PixelRenderer.cpp + ${CMAKE_CURRENT_LIST_DIR}/src/ogfx/render/FontGlyphAtlas.cpp # resources ${CMAKE_CURRENT_LIST_DIR}/src/ogfx/resources/Image.cpp diff --git a/other/build.nr b/other/build.nr index 8cb1ed8..94fd144 100644 --- a/other/build.nr +++ b/other/build.nr @@ -1 +1 @@ -2056 +2057 diff --git a/src/ogfx/gui/widgets/CheckBox.cpp b/src/ogfx/gui/widgets/CheckBox.cpp index e623b41..d5aedfa 100644 --- a/src/ogfx/gui/widgets/CheckBox.cpp +++ b/src/ogfx/gui/widgets/CheckBox.cpp @@ -67,7 +67,7 @@ namespace ogfx gfx.drawRoundRect({ getGlobalPosition(), getSize() }, m_borderColor, m_borderRadius, m_borderWidth); gfx.drawRoundRect({ getGlobalContentPosition(), m_checkSize }, getCheckBorderColor(), m_checkBorderRadius, m_checkBorderWidth); if (isChecked()) - gfx.fillRoundRect({ getGlobalContentPosition() + 5, m_checkSize - 10 }, getCheckBoxColor(), m_checkBorderRadius / 2); + gfx.fillRoundRect({ getGlobalContentPosition() + 5, m_checkSize - 10 }, getCheckBoxColor(), m_checkBorderRadius / 2.0f); gfx.drawString(getText(), getGlobalContentPosition() + ostd::Vec2 { m_checkSize.x + m_spacing, 0 }, getTextColor(), getFontSize()); } diff --git a/src/ogfx/render/BasicRenderer.cpp b/src/ogfx/render/BasicRenderer.cpp index 3a2da45..9833a3c 100644 --- a/src/ogfx/render/BasicRenderer.cpp +++ b/src/ogfx/render/BasicRenderer.cpp @@ -21,6 +21,7 @@ #include "BasicRenderer.hpp" #include "../gui/Window.hpp" #include "../resources/UbuntuMonoRegularTTF.hpp" +#include "../gui/Window.hpp" #define COLOR_CAST(ostd_color) std::bit_cast(ostd_color.getNormalizedColor()) #define QUAD_INDICES_ARR { 0, 1, 2, 2, 3, 0 } @@ -60,10 +61,17 @@ namespace ogfx m_window = &window; if (m_initialized) return set_error_state(tErrors::NoError); init_arrays(); + loadDefaultFont(20); + m_fontGlyphAtlas.init(*this); m_initialized = true; return set_error_state(tErrors::NoError); } + SDL_Renderer* BasicRenderer2D::getSDLRenderer(void) const + { + return m_window != nullptr ? m_window->getSDLRenderer() : nullptr; + } + void BasicRenderer2D::flushBatch(void) { if (!m_initialized || m_vertexCount == 0) diff --git a/src/ogfx/render/BasicRenderer.hpp b/src/ogfx/render/BasicRenderer.hpp index 3a541bc..610d726 100644 --- a/src/ogfx/render/BasicRenderer.hpp +++ b/src/ogfx/render/BasicRenderer.hpp @@ -20,10 +20,12 @@ #pragma once -#include #include #include +#include #include +#include +#include namespace ogfx { @@ -56,6 +58,7 @@ namespace ogfx BasicRenderer2D(void) = default; ~BasicRenderer2D(void); int32_t init(WindowCore& window); + SDL_Renderer* getSDLRenderer(void) const; void flushBatch(void); void endFrame(void); void pushClippingRect(const ostd::Rectangle& rect, bool additive = false); @@ -70,11 +73,12 @@ namespace ogfx inline uint32_t getDrawCallCount(void) { return m_drawCallCount; } inline bool hasOpenFont(void) { return m_fontOpen; } inline TTF_Font* getSDLFont(void) { return m_font; } - inline bool isValid(void) { return m_initialized && m_fontOpen && (m_font != nullptr || m_fontFromMemory); } + inline bool isValid(void) const { return m_initialized && m_fontOpen && (m_font != nullptr || m_fontFromMemory); } inline int32_t geterrorState(void) { return m_errorState; } inline int32_t getFontSize(void) { return m_fontSize; } inline WindowCore& getWindow(void) { return *m_window; } inline bool isInitialized(void) { return m_initialized; } + inline FontGlyphAtlas& getFontGlyphAtlas(void) { return m_fontGlyphAtlas; } void drawImage(const ogfx::Image& image, const ostd::Vec2& position, const ostd::Vec2& size = { 0, 0 }, const ostd::Rectangle& srcRect = { 0, 0, 0, 0 }); void drawAnimation(const Animation& anim, const ostd::Vec2& position, const ostd::Vec2& size = { 0, 0 }); @@ -138,6 +142,7 @@ namespace ogfx int32_t m_errorState { tErrors::NoError }; int32_t m_fontSize { DefaultFontSize }; bool m_fontFromMemory { false }; + FontGlyphAtlas m_fontGlyphAtlas; inline static constexpr int32_t DefaultFontSize { 16 }; }; diff --git a/src/ogfx/render/FontGlyphAtlas.cpp b/src/ogfx/render/FontGlyphAtlas.cpp new file mode 100644 index 0000000..83563a7 --- /dev/null +++ b/src/ogfx/render/FontGlyphAtlas.cpp @@ -0,0 +1,224 @@ +/* + OmniaFramework - A collection of useful functionality + Copyright (C) 2025 OmniaX-Dev + + This file is part of OmniaFramework. + + OmniaFramework 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 3 of the License, or + (at your option) any later version. + + OmniaFramework 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 OmniaFramework. If not, see . +*/ + +#include "FontGlyphAtlas.hpp" +#include "BasicRenderer.hpp" + +#define DEBUG(n) std::cout << (int)n << "\n"; + +namespace ogfx +{ + FontGlyphAtlas FontGlyphAtlas::init(BasicRenderer2D& renderer) + { + m_renderer = &renderer; + m_uvs.reserve(4096); + for (int32_t i = 0; i < FontGlyphAtlas::MaxAtlasCount; i++) + m_atlases[i] = nullptr; + m_currentAtlasCount = 0; + create_new_atlas(); + return *this; + } + + bool FontGlyphAtlas::rasterize_glyph(const ostd::String& glyphStr, TTF_Font* font, uint32_t fontSize) + { + DEBUG(1); + std::cout << (uint64_t)font << "\n"; + if (!font || m_currentAtlasCount <= 0) + return false; + + DEBUG(2); + // 1. Extract codepoint from the ostd::String + auto cps = glyphStr.getUTF8Codepoints(); + if (cps.size() != 1) + return false; // must be exactly one character + + DEBUG(3); + uint32_t codepoint = cps[0]; + + // 2. Convert the ostd::String to UTF-8 for SDL_ttf + std::string utf8 = glyphStr.cpp_str(); + + // 3. Rasterize glyph using SDL_ttf + SDL_Surface* surf = TTF_RenderText_Blended(font, utf8.c_str(), utf8.length(), SDL_Color{255, 255, 255, 255}); + + if (!surf) + return false; + + DEBUG(4); + int gw = surf->w; + int gh = surf->h; + + // 4. Get current atlas + SDL_Texture* atlas = m_atlases[m_currentAtlasCount - 1]; + + // 5. Shelf packing: move to next row if needed + if (m_penX + gw > int(AtlasTextureDimension)) + { + m_penX = 0; + m_penY += m_rowHeight; + m_rowHeight = 0; + } + + // 6. If still doesn't fit → create new atlas + if (m_penY + gh > int(AtlasTextureDimension)) + { + SDL_DestroySurface(surf); + + if (!create_new_atlas()) + return false; // no more atlases available + + atlas = m_atlases[m_currentAtlasCount - 1]; + m_penX = 0; + m_penY = 0; + m_rowHeight = 0; + } + + DEBUG(5); + // 7. Upload glyph bitmap into atlas + SDL_Rect dstRect { m_penX, m_penY, gw, gh }; + SDL_UpdateTexture(atlas, &dstRect, surf->pixels, surf->pitch); + SDL_DestroySurface(surf); + + // 8. Compute UVs + float u0 = float(m_penX) / float(AtlasTextureDimension); + float v0 = float(m_penY) / float(AtlasTextureDimension); + float u1 = float(m_penX + gw) / float(AtlasTextureDimension); + float v1 = float(m_penY + gh) / float(AtlasTextureDimension); + + GlyphUV uv; + uv.atlas = atlas; + uv.uvs[0] = { u0, v0 }; + uv.uvs[1] = { u1, v0 }; + uv.uvs[2] = { u1, v1 }; + uv.uvs[3] = { u0, v1 }; + + // 9. Store in hashmap + GlyphKey key { codepoint, uint64_t(font), fontSize }; + m_uvs[key] = uv; + + // 10. Advance shelf packing cursor + m_penX += gw; + m_rowHeight = std::max(m_rowHeight, gh); + return true; + } + + bool FontGlyphAtlas::create_new_atlas(void) + { + // 1. Check if we can create another atlas + if (m_currentAtlasCount >= int(MaxAtlasCount)) + return false; + + if (!m_renderer) + return false; + + SDL_Renderer* sdlRenderer = m_renderer->getSDLRenderer(); + if (!sdlRenderer) + return false; + + // 2. Create the texture + SDL_Texture* tex = SDL_CreateTexture( + sdlRenderer, + SDL_PIXELFORMAT_RGBA8888, + SDL_TEXTUREACCESS_STREAMING, + AtlasTextureDimension, + AtlasTextureDimension + ); + + if (!tex) + return false; + + // 3. Set blend mode (important for alpha glyphs) + SDL_SetTextureBlendMode(tex, SDL_BLENDMODE_BLEND); + + // 4. Clear the texture to transparent + { + void* pixels = nullptr; + int pitch = 0; + if (SDL_LockTexture(tex, nullptr, &pixels, &pitch) == 0) + { + memset(pixels, 0, pitch * AtlasTextureDimension); + SDL_UnlockTexture(tex); + } + } + + // 5. Store the atlas + m_atlases[m_currentAtlasCount] = tex; + m_currentAtlasCount++; + + // 6. Reset packing cursor for this new atlas + m_penX = 0; + m_penY = 0; + m_rowHeight = 0; + + return true; + } + + bool FontGlyphAtlas::save_atlas_png(int atlasIndex, const char* filename) + { + if (atlasIndex < 0 || atlasIndex >= m_currentAtlasCount) + return false; + + SDL_Texture* tex = m_atlases[atlasIndex]; + if (!tex) + return false; + + // --- 1. Query texture properties (SDL3 way) --- + SDL_PropertiesID props = SDL_GetTextureProperties(tex); + if (!props) + return false; + + int width = (int)SDL_GetNumberProperty(props, SDL_PROP_TEXTURE_WIDTH_NUMBER, 0); + int height = (int)SDL_GetNumberProperty(props, SDL_PROP_TEXTURE_HEIGHT_NUMBER, 0); + Uint32 format = (Uint32)SDL_GetNumberProperty(props, SDL_PROP_TEXTURE_FORMAT_NUMBER, SDL_PIXELFORMAT_RGBA32); + + if (width <= 0 || height <= 0) + return false; + + // --- 2. Create a surface to receive the pixels --- + SDL_Surface* surf = SDL_CreateSurface(width, height, SDL_PIXELFORMAT_RGBA32); + if (!surf) + return false; + + // --- 3. Lock texture and copy pixels --- + void* pixels = nullptr; + int pitch = 0; + + if (!SDL_LockTexture(tex, nullptr, &pixels, &pitch)) + { + SDL_DestroySurface(surf); + return false; + } + + // Copy row-by-row (pitch may differ) + uint8_t* dst = static_cast(surf->pixels); + uint8_t* src = static_cast(pixels); + + for (int y = 0; y < height; y++) + memcpy(dst + y * surf->pitch, src + y * pitch, width * 4); + + SDL_UnlockTexture(tex); + + // --- 4. Save PNG --- + bool ok = (IMG_SavePNG(surf, filename) == 0); + + SDL_DestroySurface(surf); + return ok; + } +} diff --git a/src/ogfx/render/FontGlyphAtlas.hpp b/src/ogfx/render/FontGlyphAtlas.hpp new file mode 100644 index 0000000..59460a5 --- /dev/null +++ b/src/ogfx/render/FontGlyphAtlas.hpp @@ -0,0 +1,84 @@ +/* + OmniaFramework - A collection of useful functionality + Copyright (C) 2025 OmniaX-Dev + + This file is part of OmniaFramework. + + OmniaFramework 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 3 of the License, or + (at your option) any later version. + + OmniaFramework 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 OmniaFramework. If not, see . +*/ + +#pragma once + +#include +#include +#include + +namespace ogfx +{ + class BasicRenderer2D; + class FontGlyphAtlas + { + public: struct GlyphUV + { + ostd::Vec2 uvs[4]; + SDL_Texture* atlas { nullptr }; + }; + struct GlyphKey + { + uint32_t codepoint; + uint64_t fontID; + uint32_t pixelSize; + + bool operator==(const GlyphKey& other) const noexcept + { + return codepoint == other.codepoint && + fontID == other.fontID && + pixelSize == other.pixelSize; + } + }; + struct GlyphKeyHasher + { + inline size_t operator()(const GlyphKey& k) const noexcept + { + uint64_t v = 0; + v |= uint64_t(k.codepoint); + v |= uint64_t(k.fontID) << 32; + v |= uint64_t(k.pixelSize) << 48; + return std::hash()(v); + } + }; + public: + inline FontGlyphAtlas(void) { } + inline FontGlyphAtlas(BasicRenderer2D& renderer) { init(renderer); } + FontGlyphAtlas init(BasicRenderer2D& renderer); + bool save_atlas_png(int atlasIndex, const char* filename); + + public: + bool rasterize_glyph(const ostd::String& glyphStr, TTF_Font* font, uint32_t fontSize); + bool create_new_atlas(void); + + public: + inline static constexpr uint32_t AtlasTextureDimension { 8192 }; + inline static constexpr uint32_t MaxAtlasCount { 16 }; + + private: + std::unordered_map m_uvs; + SDL_Texture* m_atlases[FontGlyphAtlas::MaxAtlasCount]; + int32_t m_currentAtlasCount { 0 }; + BasicRenderer2D* m_renderer { nullptr }; + int32_t m_penX { 0 }; + int32_t m_penY { 0 }; + int32_t m_rowHeight { 0 }; + }; +} diff --git a/src/ostd/string/String.cpp b/src/ostd/string/String.cpp index ac90b55..ab2bfd9 100644 --- a/src/ostd/string/String.cpp +++ b/src/ostd/string/String.cpp @@ -646,7 +646,54 @@ namespace ostd return str; } + std::vector String::decodeUTF8(const ostd::String& s) + { + std::vector out; + const char* p = s.m_data.data(); + const char* end = p + s.m_data.size(); + while (p < end) + out.push_back(utf8_next(p, end)); + + return out; + } + + + uint32_t String::utf8_next(const char*& p, const char* end) + { + uint8_t c = static_cast(*p++); + + // ASCII fast path + if (c < 0x80) + return c; + + // 2-byte sequence + if ((c >> 5) == 0x6) { + uint32_t cp = (c & 0x1F) << 6; + cp |= (static_cast(*p++) & 0x3F); + return cp; + } + + // 3-byte sequence + if ((c >> 4) == 0xE) { + uint32_t cp = (c & 0x0F) << 12; + cp |= (static_cast(*p++) & 0x3F) << 6; + cp |= (static_cast(*p++) & 0x3F); + return cp; + } + + // 4-byte sequence + if ((c >> 3) == 0x1E) { + uint32_t cp = (c & 0x07) << 18; + cp |= (static_cast(*p++) & 0x3F) << 12; + cp |= (static_cast(*p++) & 0x3F) << 6; + cp |= (static_cast(*p++) & 0x3F); + return cp; + } + + // Invalid byte → return replacement character + return 0xFFFD; + } String operator+(const cpp_string& str1, const String& str) { diff --git a/src/ostd/string/String.hpp b/src/ostd/string/String.hpp index 4739e9d..0f5c389 100644 --- a/src/ostd/string/String.hpp +++ b/src/ostd/string/String.hpp @@ -83,6 +83,7 @@ namespace ostd inline operator std::filesystem::path() const { return cpp_str(); } inline String& clr(void) { m_data = ""; return *this; } inline String& set(const cpp_string& str) { m_data = str; return *this; } + inline std::vector getUTF8Codepoints(void) const { return decodeUTF8(m_data); } inline auto begin(void) { return m_data.begin(); } inline auto end(void) { return m_data.end(); } @@ -174,9 +175,13 @@ namespace ostd static String getHexStr(uint64_t value, bool prefix = true, uint8_t nbytes = 1); static String getBinStr(uint64_t value, bool prefix = true, uint8_t nbytes = 1); static String duplicateChar(unsigned char c, uint16_t count); + static std::vector decodeUTF8(const ostd::String& s); friend std::ostream& operator<<(std::ostream& out, const String& val); + private: + static uint32_t utf8_next(const char*& p, const char* end); + private: ostd::cpp_string m_data; }; diff --git a/src/test/GuiTest.cpp b/src/test/GuiTest.cpp index 2f2dc5f..09ee6fb 100644 --- a/src/test/GuiTest.cpp +++ b/src/test/GuiTest.cpp @@ -30,20 +30,6 @@ class Window : public ogfx::gui::Window inline Window(void) { } inline void onInitialize(void) override { - test.loadFromFile("./test.png", m_gfx); - ogfx::Animation::AnimationData ad; - ad.backwards = false; - ad.columns = 9; - ad.rows = 4; - ad.frame_width = 256; - ad.frame_height = 256; - ad.length = 36; - ad.random = false; - ad.still = false; - ad.speed = 0; - m_anim.create(ad); - m_anim.setSpriteSheet(test); - m_panel1.setSize(300, 140); m_panel1.setPosition(350, 50); m_panel1.setMousePressedCallback([&](const ogfx::gui::Event& event) -> void { @@ -131,6 +117,9 @@ class Window : public ogfx::gui::Window setTheme(m_theme); + m_gfx.getFontGlyphAtlas().rasterize_glyph("A", m_gfx.getSDLFont(), 30); + m_gfx.getFontGlyphAtlas().save_atlas_png(0, "./atlas.png"); + // std::cout << m_theme.get("panel.titlebarType", "", {}, {}) << " \n"; } @@ -167,8 +156,6 @@ class Window : public ogfx::gui::Window ogfx::gui::widgets::CheckBox m_check1 { *this }; ostd::Stylesheet m_theme; ostd::Vec2 pos { 0, 0 }; - ogfx::Image test; - ogfx::Animation m_anim; }; int main(int argc, char** argv)