From 64cd4598f295467bfad81ecad899ab83d461a882 Mon Sep 17 00:00:00 2001 From: OmniaX-Dev Date: Mon, 13 Apr 2026 15:11:25 +0200 Subject: [PATCH] Implemented Font GLyph Atlas overflow --- .clangd | 3 +- extra/testTheme.txt | 2 +- other/build.nr | 2 +- src/ogfx/gui/WindowOutputHandler.cpp | 2 +- src/ogfx/render/BasicRenderer.cpp | 2 + src/ogfx/render/BasicRenderer.hpp | 4 +- src/ogfx/render/FontGlyphAtlas.cpp | 148 ++++++++++++++++----------- src/ogfx/render/FontGlyphAtlas.hpp | 18 ++-- src/ostd/utils/Defines.hpp | 3 + src/test/GuiTest.cpp | 121 ++++++++++++++++++++-- 10 files changed, 221 insertions(+), 84 deletions(-) diff --git a/.clangd b/.clangd index 32169b9..122ab09 100644 --- a/.clangd +++ b/.clangd @@ -1,3 +1,2 @@ CompileFlags: - CompilationDatabase: bin - Add: ["--target=x86_64-w64-mingw32"] \ No newline at end of file + CompilationDatabase: bin \ No newline at end of file diff --git a/extra/testTheme.txt b/extra/testTheme.txt index 26422b3..28d7ae9 100644 --- a/extra/testTheme.txt +++ b/extra/testTheme.txt @@ -5,7 +5,7 @@ $accentColor = Color(rgba(255, 0, 255, 255)) const $color2 = Color(#0000FFFF) % ------------------- -window.backgroundColor = Color(rgba(120, 120, 120, 255)) +window.backgroundColor = Color(rgba(0, 0, 0, 255)) $accentColor = Color(rgb(255, 0, 0)) diff --git a/other/build.nr b/other/build.nr index 94fd144..b742f79 100644 --- a/other/build.nr +++ b/other/build.nr @@ -1 +1 @@ -2057 +2058 diff --git a/src/ogfx/gui/WindowOutputHandler.cpp b/src/ogfx/gui/WindowOutputHandler.cpp index ab41f29..694772e 100644 --- a/src/ogfx/gui/WindowOutputHandler.cpp +++ b/src/ogfx/gui/WindowOutputHandler.cpp @@ -35,7 +35,7 @@ namespace ogfx { if (m_window != nullptr) return; m_window = &window; - m_renderer.init(window); + // m_renderer.init(window); //TODO: Fix } void GraphicsWindowOutputHandler::setMonospaceFont(const String& filePath) diff --git a/src/ogfx/render/BasicRenderer.cpp b/src/ogfx/render/BasicRenderer.cpp index 9833a3c..c6925ec 100644 --- a/src/ogfx/render/BasicRenderer.cpp +++ b/src/ogfx/render/BasicRenderer.cpp @@ -61,7 +61,9 @@ namespace ogfx m_window = &window; if (m_initialized) return set_error_state(tErrors::NoError); init_arrays(); + m_initialized = true; loadDefaultFont(20); + m_initialized = false; m_fontGlyphAtlas.init(*this); m_initialized = true; return set_error_state(tErrors::NoError); diff --git a/src/ogfx/render/BasicRenderer.hpp b/src/ogfx/render/BasicRenderer.hpp index 610d726..0f49c1b 100644 --- a/src/ogfx/render/BasicRenderer.hpp +++ b/src/ogfx/render/BasicRenderer.hpp @@ -55,7 +55,7 @@ namespace ogfx BasicRenderer2D& m_parent; }; public: - BasicRenderer2D(void) = default; + inline BasicRenderer2D(void) { } ~BasicRenderer2D(void); int32_t init(WindowCore& window); SDL_Renderer* getSDLRenderer(void) const; @@ -106,7 +106,7 @@ namespace ogfx void outlinedCircle(const ostd::Rectangle& rect, const ostd::Color& fillColor, const ostd::Color& outlineColor, int32_t outlineThickness = 1); void outlinedEllipse(const ostd::Rectangle& rect, const ostd::Color& fillColor, const ostd::Color& outlineColor, int32_t outlineThickness = 1); - private: + public: void init_arrays(void); void generate_half_circle(const ostd::Vec2& center, const ostd::Vec2& dir, float radius, int segments, const ostd::Color& color); void generate_quarter_circle(const ostd::Vec2& center, float radius, float thickness, float startAngle, const ostd::Color& color, int segments); diff --git a/src/ogfx/render/FontGlyphAtlas.cpp b/src/ogfx/render/FontGlyphAtlas.cpp index 83563a7..f97c324 100644 --- a/src/ogfx/render/FontGlyphAtlas.cpp +++ b/src/ogfx/render/FontGlyphAtlas.cpp @@ -20,8 +20,9 @@ #include "FontGlyphAtlas.hpp" #include "BasicRenderer.hpp" - -#define DEBUG(n) std::cout << (int)n << "\n"; +#include +#include +#include namespace ogfx { @@ -38,20 +39,20 @@ namespace ogfx 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]; + GlyphKey key { codepoint, uint64_t(font), fontSize }; + if (auto it = m_uvs.find(key) != m_uvs.end()) + return true; + // 2. Convert the ostd::String to UTF-8 for SDL_ttf std::string utf8 = glyphStr.cpp_str(); @@ -61,13 +62,19 @@ namespace ogfx if (!surf) return false; - DEBUG(4); + // SDL_Surface* converted = SDL_ConvertSurface(surf, SDL_PIXELFORMAT_RGBA32); + // SDL_DestroySurface(surf); + // surf = converted; + int gw = surf->w; int gh = surf->h; // 4. Get current atlas SDL_Texture* atlas = m_atlases[m_currentAtlasCount - 1]; + // SDL_Log("atlas format = %s", SDL_GetPixelFormatName(atlas->format)); + // SDL_Log("surf format = %s", SDL_GetPixelFormatName(surf->format)); + // 5. Shelf packing: move to next row if needed if (m_penX + gw > int(AtlasTextureDimension)) { @@ -90,10 +97,47 @@ namespace ogfx m_rowHeight = 0; } - DEBUG(5); + uint8_t* pixels = (uint8_t*)surf->pixels; + int pitch = surf->pitch; + + // for (int y = 0; y < surf->h; y++) + // { + // uint32_t* row = (uint32_t*)(pixels + y * pitch); + + // for (int x = 0; x < surf->w; x++) + // { + // uint32_t px = row[x]; + + // uint8_t a = (px >> 24) & 0xFF; // ABGR → A is highest byte + // uint8_t r = (px >> 16) & 0xFF; + // uint8_t g = (px >> 8) & 0xFF; + // uint8_t b = (px) & 0xFF; + + // std::cout << (int)r << "," << (int)g << "," << (int)b << "," << (int)a << " "; + + // row[x] = (a << 24) | (r << 16) | (g << 8) | b; + // } + // } + // 7. Upload glyph bitmap into atlas SDL_Rect dstRect { m_penX, m_penY, gw, gh }; - SDL_UpdateTexture(atlas, &dstRect, surf->pixels, surf->pitch); + + void* lockedPixels = nullptr; + int lockedPitch = 0; + if (SDL_LockTexture(atlas, &dstRect, &lockedPixels, &lockedPitch)) + { + uint8_t* src = (uint8_t*)surf->pixels; + uint8_t* dst = (uint8_t*)lockedPixels; + for (int y = 0; y < gh; y++) + { + memcpy(dst, src, gw * 4); // 4 bytes per pixel (ARGB8888) + src += surf->pitch; + dst += lockedPitch; + } + SDL_UnlockTexture(atlas); + } + + // SDL_UpdateTexture(atlas, &dstRect, surf->pixels, surf->pitch); SDL_DestroySurface(surf); // 8. Compute UVs @@ -108,9 +152,9 @@ namespace ogfx uv.uvs[1] = { u1, v0 }; uv.uvs[2] = { u1, v1 }; uv.uvs[3] = { u0, v1 }; + uv.size = { static_cast(gw), static_cast(gh) }; // 9. Store in hashmap - GlyphKey key { codepoint, uint64_t(font), fontSize }; m_uvs[key] = uv; // 10. Advance shelf packing cursor @@ -133,19 +177,14 @@ namespace ogfx return false; // 2. Create the texture - SDL_Texture* tex = SDL_CreateTexture( - sdlRenderer, - SDL_PIXELFORMAT_RGBA8888, - SDL_TEXTUREACCESS_STREAMING, - AtlasTextureDimension, - AtlasTextureDimension - ); + SDL_Texture* tex = SDL_CreateTexture(sdlRenderer, SDL_PIXELFORMAT_RGBA32, SDL_TEXTUREACCESS_STREAMING, AtlasTextureDimension, AtlasTextureDimension); if (!tex) return false; // 3. Set blend mode (important for alpha glyphs) SDL_SetTextureBlendMode(tex, SDL_BLENDMODE_BLEND); + SDL_SetTextureScaleMode(tex, SDL_SCALEMODE_LINEAR); // 4. Clear the texture to transparent { @@ -153,7 +192,7 @@ namespace ogfx int pitch = 0; if (SDL_LockTexture(tex, nullptr, &pixels, &pitch) == 0) { - memset(pixels, 0, pitch * AtlasTextureDimension); + memset(pixels, 0x00, pitch * AtlasTextureDimension); SDL_UnlockTexture(tex); } } @@ -170,55 +209,40 @@ namespace ogfx return true; } - bool FontGlyphAtlas::save_atlas_png(int atlasIndex, const char* filename) + void FontGlyphAtlas::save_atlas_to_png(SDL_Renderer* renderer, SDL_Texture* atlas, const char* filename) { - if (atlasIndex < 0 || atlasIndex >= m_currentAtlasCount) - return false; + // 1. Get texture dimensions and format + float w, h; + SDL_GetTextureSize(atlas, &w, &h); + int iw = (int)w, ih = (int)h; - SDL_Texture* tex = m_atlases[atlasIndex]; - if (!tex) - return false; + // 2. Create a target surface + SDL_Surface* surf = SDL_CreateSurface(iw, ih, SDL_PIXELFORMAT_ARGB8888); + if (!surf) return; - // --- 1. Query texture properties (SDL3 way) --- - SDL_PropertiesID props = SDL_GetTextureProperties(tex); - if (!props) - return false; + // 3. Read pixels back from the texture + SDL_Rect rect { 0, 0, iw, ih }; - 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); + // For STREAMING textures, lock/read directly: + void* pixels = nullptr; + int pitch = 0; + if (SDL_LockTexture(atlas, nullptr, &pixels, &pitch)) + { + uint8_t* src = (uint8_t*)pixels; + uint8_t* dst = (uint8_t*)surf->pixels; + for (int y = 0; y < ih; y++) + { + memcpy(dst, src, iw * 4); + src += pitch; + dst += surf->pitch; + } + SDL_UnlockTexture(atlas); + } - if (width <= 0 || height <= 0) - return false; + // 4. Save + IMG_SavePNG(surf, filename); // needs SDL3_image + // or: SDL_SaveBMP(surf, "atlas_debug.bmp"); - // --- 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; + SDL_DestroySurface(surf); } } diff --git a/src/ogfx/render/FontGlyphAtlas.hpp b/src/ogfx/render/FontGlyphAtlas.hpp index 59460a5..748cdfa 100644 --- a/src/ogfx/render/FontGlyphAtlas.hpp +++ b/src/ogfx/render/FontGlyphAtlas.hpp @@ -33,6 +33,7 @@ namespace ogfx { ostd::Vec2 uvs[4]; SDL_Texture* atlas { nullptr }; + ostd::Vec2 size; }; struct GlyphKey { @@ -49,30 +50,29 @@ namespace ogfx }; struct GlyphKeyHasher { - inline size_t operator()(const GlyphKey& k) const noexcept + 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); - } + size_t h = std::hash()(k.codepoint); + h ^= std::hash()(k.fontID) + 0x9e3779b9 + (h << 6) + (h >> 2); + h ^= std::hash()(k.pixelSize) + 0x9e3779b9 + (h << 6) + (h >> 2); + return h; + } }; 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); + void save_atlas_to_png(SDL_Renderer* renderer, SDL_Texture* atlas, const char* filename); public: inline static constexpr uint32_t AtlasTextureDimension { 8192 }; inline static constexpr uint32_t MaxAtlasCount { 16 }; - private: + public: std::unordered_map m_uvs; SDL_Texture* m_atlases[FontGlyphAtlas::MaxAtlasCount]; int32_t m_currentAtlasCount { 0 }; diff --git a/src/ostd/utils/Defines.hpp b/src/ostd/utils/Defines.hpp index 9c23ac2..120c73e 100755 --- a/src/ostd/utils/Defines.hpp +++ b/src/ostd/utils/Defines.hpp @@ -50,7 +50,10 @@ #endif #ifndef OX_DEBUG_BUILD + #define _DEBUG(n) #ifndef OX_RELEASE_BUILD #define OX_RELEASE_BUILD #endif +#else + #define _DEBUG(n) std::cout << (int)n << "\n"; #endif diff --git a/src/test/GuiTest.cpp b/src/test/GuiTest.cpp index 09ee6fb..38f940b 100644 --- a/src/test/GuiTest.cpp +++ b/src/test/GuiTest.cpp @@ -19,11 +19,51 @@ */ #include "gui/Events.hpp" +#include #include #include ostd::ConsoleOutputHandler out; +void drawTexturedQuad(SDL_Renderer* renderer, + SDL_Texture* tex, + const ostd::Rectangle& rect, + const ostd::Vec2 uvs[4]) +{ + SDL_Vertex verts[4]; + + // Expand rectangle into quad corners + const float x = rect.x; + const float y = rect.y; + const float w = rect.w; + const float h = rect.h; + + const ostd::Vec2 quad[4] = { + { x, y }, + { x + w, y }, + { x + w, y + h }, + { x, y + h } + }; + + for (int i = 0; i < 4; i++) + { + verts[i].position.x = quad[i].x; + verts[i].position.y = quad[i].y; + + verts[i].tex_coord.x = uvs[i].x; + verts[i].tex_coord.y = uvs[i].y; + + auto crim = ostd::Colors::Crimson.getNormalizedColor(); + verts[i].color = SDL_FColor{crim.r, crim.g, crim.b, crim.a}; + } + + // Two triangles: (0,1,2) and (2,3,0) + const int indices[6] = { 0, 1, 2, 2, 3, 0 }; + + SDL_RenderGeometry(renderer, tex, verts, 4, indices, 6); +} + + class Window : public ogfx::gui::Window { public: @@ -65,14 +105,14 @@ class Window : public ogfx::gui::Window m_label1.setMouseScrolledCallback([&](const ogfx::gui::Event& event) -> void { std::cout << "SCROLL!\n"; }); - addWidget(m_label1); + // addWidget(m_label1); m_panel2.addChild(m_panel1); - addWidget(m_panel2); + // addWidget(m_panel2); m_check1.setPosition(30, 30); m_check1.setText("Check this out!"); - addWidget(m_check1); + // addWidget(m_check1); m_label2.setPosition(0, 0); m_label2.setText("Ciccia Bella!"); @@ -117,10 +157,55 @@ 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"); + int32_t count = 0; + for (char c = 'A'; c <= 'Z'; c++) + { + for (int32_t i = 0; i < 200; i++) + { + count++; + uint32_t _fs = ostd::Random::getui32(8, 200); + m_gfx.setFontSize(_fs); + m_gfx.getFontGlyphAtlas().rasterize_glyph(ostd::String("").addChar(c), m_gfx.getSDLFont(), _fs); + } + } + for (char c = '0'; c <= '9'; c++) + { + for (int32_t i = 0; i < 200; i++) + { + count++; + uint32_t _fs = ostd::Random::getui32(8, 200); + m_gfx.setFontSize(_fs); + m_gfx.getFontGlyphAtlas().rasterize_glyph(ostd::String("").addChar(c), m_gfx.getSDLFont(), _fs); + } + } + for (char c = 'a'; c <= 'z'; c++) + { + count++; + for (int32_t i = 0; i < 200; i++) + { + uint32_t _fs = ostd::Random::getui32(8, 200); + m_gfx.setFontSize(_fs); + m_gfx.getFontGlyphAtlas().rasterize_glyph(ostd::String("").addChar(c), m_gfx.getSDLFont(), _fs); + } + } + for (char c = '0'; c <= '9'; c++) + { + for (int32_t i = 0; i < 200; i++) + { + count++; + uint32_t _fs = ostd::Random::getui32(8, 200); + m_gfx.setFontSize(_fs); + m_gfx.getFontGlyphAtlas().rasterize_glyph(ostd::String("").addChar(c), m_gfx.getSDLFont(), _fs); + } + } - // std::cout << m_theme.get("panel.titlebarType", "", {}, {}) << " \n"; + for (int32_t i = 0; i < m_gfx.getFontGlyphAtlas().m_currentAtlasCount; i++) + { + ostd::String file("./atlas"); + file.add(i).add(".png"); + m_gfx.getFontGlyphAtlas().save_atlas_to_png(m_gfx.getSDLRenderer(), m_gfx.getFontGlyphAtlas().m_atlases[i], file); + } + std::cout << "Atlas cound: " << (int)count << "\n"; } inline void onSignal(ostd::Signal& signal) override @@ -145,6 +230,29 @@ class Window : public ogfx::gui::Window // gfx.drawRect({ 50, 50, 200, 120 }, ostd::Colors::Aquamarine, 1); // std::cout << (int)(gfx.getDrawCallCount() + 1) << "\n"; + + gfx.endFrame(); + + auto l_renderGlyph = [&](char c, float x, float y, float scale = 1) -> ostd::Vec2 { + auto& tex = m_gfx.getFontGlyphAtlas().m_atlases[0]; + auto glyph = m_gfx.getFontGlyphAtlas().m_uvs[ { static_cast(c), (uint64_t)gfx.getSDLFont(), fs }]; + drawTexturedQuad(getSDLRenderer(), tex, { x, y, glyph.size.x * scale, glyph.size.y * scale }, glyph.uvs); + return glyph.size; + }; + auto l_renderString = [&](const ostd::String& str, const ostd::Vec2& pos, float scale = 1.0f) -> void { + float x = pos.x; + float y = pos.y; + for (auto& c : str) + { + auto size = l_renderGlyph(c, x, y, scale); + x += (size.x * scale); + } + }; + + l_renderString("Hello", { 10, 140 }); + + gfx.renderText("Hello", 10, 100, ostd::Colors::Crimson, 30); + } private: @@ -156,6 +264,7 @@ class Window : public ogfx::gui::Window ogfx::gui::widgets::CheckBox m_check1 { *this }; ostd::Stylesheet m_theme; ostd::Vec2 pos { 0, 0 }; + uint32_t fs = 30; }; int main(int argc, char** argv)