From 4d8940812ae2e6323cbb214af0c5f1edf4e8accf Mon Sep 17 00:00:00 2001 From: Slendi Date: Sun, 5 Oct 2025 07:27:12 +0300 Subject: [PATCH] dsfs Signed-off-by: Slendi --- CMakeLists.txt | 1 + src/TextRenderer.cpp | 476 ++++++++++++++++++++++++++++++++++++++++++- src/TextRenderer.hpp | 66 +++++- src/Theme.hpp | 23 ++- src/Tick.cpp | 7 + 5 files changed, 553 insertions(+), 20 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index cf39324..c65e4e2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -154,6 +154,7 @@ target_link_libraries(waylight PRIVATE raylib msdfgen::msdfgen-core + msdfgen::msdfgen-ext m dl diff --git a/src/TextRenderer.cpp b/src/TextRenderer.cpp index edc1871..7eb446e 100644 --- a/src/TextRenderer.cpp +++ b/src/TextRenderer.cpp @@ -1,16 +1,236 @@ #include "TextRenderer.hpp" +#include +#include #include +#include +#include #include +#include #include #include #include #include #include +#include +#include #include #include +#include + +#undef BLACK +#undef WHITE +#undef RED +#undef GREEN +#undef BLUE +#undef YELLOW +#undef MAGENTA + +#include +#include FT_FREETYPE_H +#include FT_GLYPH_H +#include +#include + +#include +#include + +namespace { + +constexpr int kAtlasDimension = 1024; +constexpr int kAtlasPadding = 2; +constexpr float kDefaultPxRange = 4.0f; +constexpr float kDefaultEmScale = 48.0f; + +constexpr float hb_to_em(hb_position_t value, unsigned upem) +{ + return static_cast(value) + / (64.0f * static_cast(upem ? upem : 1)); +} + +auto ft_library() -> FT_Library +{ + static FT_Library library = nullptr; + static std::once_flag once; + std::call_once(once, [] { + if (FT_Init_FreeType(&library) != 0) + library = nullptr; + else + std::atexit([] { + if (library) + FT_Done_FreeType(library); + }); + }); + return library; +} + +} // namespace + +auto TextRenderer::flush_font(FontRuntime &rt, FontData &fd) -> void +{ + rt.glyph_cache.clear(); + fd.glyphs.clear(); + rt.pen_x = kAtlasPadding; + rt.pen_y = kAtlasPadding; + rt.row_height = 0; + if (fd.atlas_img.data) + ImageClearBackground(&fd.atlas_img, BLANK); + if (fd.atlas.id != 0 && fd.atlas_img.data) + UpdateTexture(fd.atlas, fd.atlas_img.data); +} + +auto TextRenderer::allocate_region( + FontRuntime &rt, FontData &fd, int width, int height) + -> std::optional> +{ + (void)fd; + int padded_w = width + kAtlasPadding; + if (padded_w > rt.atlas_width + || height + kAtlasPadding > rt.atlas_height) + return std::nullopt; + if (rt.pen_x + padded_w > rt.atlas_width) { + rt.pen_x = kAtlasPadding; + rt.pen_y += rt.row_height; + rt.row_height = 0; + } + if (rt.pen_y + height + kAtlasPadding > rt.atlas_height) + return std::nullopt; + int x = rt.pen_x; + int y = rt.pen_y; + rt.pen_x += padded_w; + rt.row_height = std::max(rt.row_height, height + kAtlasPadding); + return std::pair { x, y }; +} + +auto TextRenderer::upload_region(FontData &fd, int dst_x, int dst_y, int width, + int height, std::vector const &buffer) -> void +{ + Rectangle rec { static_cast(dst_x), static_cast(dst_y), + static_cast(width), static_cast(height) }; + if (fd.atlas.id != 0) + UpdateTextureRec(fd.atlas, rec, buffer.data()); + if (!fd.atlas_img.data) + return; + auto *pixels = static_cast(fd.atlas_img.data); + for (int row = 0; row < height; ++row) { + auto *dst = pixels + (dst_y + row) * fd.atlas_img.width + dst_x; + std::memcpy(dst, buffer.data() + row * width, sizeof(Color) * width); + } +} + +auto TextRenderer::generate_glyph(FontRuntime &rt, FontData &fd, + u32 glyph_index) -> std::optional +{ + auto const gen_start = std::chrono::steady_clock::now(); + msdfgen::Shape shape; + double advance_em = 0.0; + msdfgen::GlyphIndex const index(glyph_index); + if (!rt.msdf_font + || !msdfgen::loadGlyph(shape, rt.msdf_font, index, + msdfgen::FONT_SCALING_EM_NORMALIZED, &advance_em)) + return std::nullopt; + shape.normalize(); + msdfgen::edgeColoringSimple(shape, 3.0); + auto bounds = shape.getBounds(); + float const width_em = static_cast(bounds.r - bounds.l); + float const height_em = static_cast(bounds.t - bounds.b); + double const scale = rt.em_scale; + int bmp_w = std::max(1, static_cast(std::ceil( + width_em * scale + 2.0 * rt.px_range))); + int bmp_h = std::max(1, static_cast(std::ceil( + height_em * scale + 2.0 * rt.px_range))); + + if (bmp_w + kAtlasPadding > rt.atlas_width + || bmp_h + kAtlasPadding > rt.atlas_height) { + TraceLog(LOG_WARNING, + "Glyph %u bitmap %dx%d exceeds atlas %dx%d", glyph_index, bmp_w, + bmp_h, rt.atlas_width, rt.atlas_height); + GlyphCacheEntry too_large {}; + too_large.width = 0; + too_large.height = 0; + return too_large; + } + + auto place = allocate_region(rt, fd, bmp_w, bmp_h); + if (!place) { + TraceLog(LOG_INFO, "Atlas full, flushing before glyph %u", glyph_index); + flush_font(rt, fd); + place = allocate_region(rt, fd, bmp_w, bmp_h); + if (!place) + return std::nullopt; + } + + msdfgen::Bitmap msdf_bitmap(bmp_w, bmp_h); + msdfgen::Vector2 scale_vec(scale, scale); + msdfgen::Vector2 translate( + -bounds.l * scale + rt.px_range, -bounds.b * scale + rt.px_range); + msdfgen::generateMSDF(msdf_bitmap, shape, rt.px_range, scale_vec, translate); + + std::vector buffer(static_cast(bmp_w) * bmp_h); + for (int y = 0; y < bmp_h; ++y) { + for (int x = 0; x < bmp_w; ++x) { + float const *px = msdf_bitmap(x, y); + auto const clamp = [](float v) { + return static_cast( + std::lround(std::clamp(v, 0.0f, 1.0f) * 255.0f)); + }; + buffer[static_cast(y) * bmp_w + x] + = Color { clamp(px[0]), clamp(px[1]), clamp(px[2]), 255 }; + } + } + + upload_region(fd, place->first, place->second, bmp_w, bmp_h, buffer); + + GlyphCacheEntry entry; + entry.atlas_x = place->first; + entry.atlas_y = place->second; + entry.width = bmp_w; + entry.height = bmp_h; + + entry.glyph.advance = static_cast(advance_em); + entry.glyph.plane_bounds.left = static_cast(bounds.l); + entry.glyph.plane_bounds.right = static_cast(bounds.r); + entry.glyph.plane_bounds.top = static_cast(bounds.t); + entry.glyph.plane_bounds.bottom = static_cast(bounds.b); + entry.glyph.glyph_bounds.left = static_cast(entry.atlas_x); + entry.glyph.glyph_bounds.top = static_cast(entry.atlas_y); + entry.glyph.glyph_bounds.right + = static_cast(entry.atlas_x + entry.width); + entry.glyph.glyph_bounds.bottom + = static_cast(entry.atlas_y + entry.height); + + auto const gen_end = std::chrono::steady_clock::now(); + auto const gen_ms + = std::chrono::duration(gen_end - gen_start).count(); + if (gen_ms > 2.0) + TraceLog(LOG_INFO, + "Generated glyph %u in %.2f ms (%dx%d texels)", glyph_index, gen_ms, + entry.width, entry.height); + return entry; +} + +auto TextRenderer::ensure_glyph(FontRuntime &rt, FontData &fd, u32 glyph_index, + bool mark_usage) -> GlyphCacheEntry * +{ + auto it = rt.glyph_cache.find(glyph_index); + if (it != rt.glyph_cache.end()) { + if (mark_usage) + it->second.stamp = rt.frame_stamp; + return &it->second; + } + auto entry = generate_glyph(rt, fd, glyph_index); + if (!entry) + return nullptr; + auto [inserted_it, ok] + = rt.glyph_cache.emplace(glyph_index, std::move(*entry)); + if (!ok) + return nullptr; + inserted_it->second.stamp = mark_usage ? rt.frame_stamp : inserted_it->second.stamp; + fd.glyphs[glyph_index] = inserted_it->second.glyph; + return &inserted_it->second; +} TextRenderer::TextRenderer() { @@ -20,37 +240,275 @@ TextRenderer::TextRenderer() }; m_msdf_shader = LoadShaderFromMemory(nullptr, msdf_fs_data); assert(IsShaderValid(m_msdf_shader)); + m_px_range_uniform = GetShaderLocation(m_msdf_shader, "pxRange"); } -TextRenderer::~TextRenderer() { UnloadShader(m_msdf_shader); } +TextRenderer::~TextRenderer() +{ + for (usize i = 0; i < m_font_runtime.size(); ++i) { + if (m_font_runtime[i]) { + FontHandle handle; + handle.id = i; + unload_font(handle); + } + } + UnloadShader(m_msdf_shader); +} auto TextRenderer::measure_text(FontHandle const font, std::string_view const text, int const size) -> Vector2 { - // FIXME: Implement. - return {}; + usize const font_id = font(); + if (font_id >= m_font_runtime.size() || !m_font_runtime[font_id] + || !m_font_runtime[font_id]->hb_font) + return Vector2 { 0.0f, 0.0f }; + + auto &rt = *m_font_runtime[font_id]; + auto &fd = m_font_data[font_id]; + + hb_buffer_t *buffer = hb_buffer_create(); + hb_buffer_add_utf8(buffer, text.data(), static_cast(text.size()), 0, + static_cast(text.size())); + hb_buffer_guess_segment_properties(buffer); + hb_shape(rt.hb_font, buffer, nullptr, 0); + + unsigned length = hb_buffer_get_length(buffer); + auto *infos = hb_buffer_get_glyph_infos(buffer, nullptr); + auto *positions = hb_buffer_get_glyph_positions(buffer, nullptr); + float advance_em = 0.0f; + float min_x_em = 0.0f; + float max_x_em = 0.0f; + bool first = true; + + for (unsigned i = 0; i < length; ++i) { + u32 glyph_index = infos[i].codepoint; + if (glyph_index == 0) + continue; + auto *entry = ensure_glyph(rt, fd, glyph_index, false); + if (!entry || entry->width == 0 || entry->height == 0) + continue; + float const x_offset_em = hb_to_em(positions[i].x_offset, rt.units_per_em); + float const left = advance_em + x_offset_em + entry->glyph.plane_bounds.left; + float const right = advance_em + x_offset_em + entry->glyph.plane_bounds.right; + if (first) { + min_x_em = left; + max_x_em = right; + first = false; + } else { + min_x_em = std::min(min_x_em, left); + max_x_em = std::max(max_x_em, right); + } + advance_em += hb_to_em(positions[i].x_advance, rt.units_per_em); + } + + hb_buffer_destroy(buffer); + + if (first) + return Vector2 { 0.0f, (rt.ascent - rt.descent) * static_cast(size) }; + + float width_em = std::max(max_x_em, advance_em) - min_x_em; + float height_em = rt.ascent - rt.descent; + return Vector2 { width_em * static_cast(size), + height_em * static_cast(size) }; } auto TextRenderer::draw_text(FontHandle const font, std::string_view const text, Vector2 const pos, int const size, Color const color) -> void { + auto const draw_start = std::chrono::steady_clock::now(); int const pos_x = pos.x; int const pos_y = pos.y; // Don't use pos from here on out! - // FIXME: Implement. - (void)pos_x, (void)pos_y; + usize const font_id = font(); + if (font_id >= m_font_runtime.size() || !m_font_runtime[font_id] + || !m_font_runtime[font_id]->hb_font) + return; + auto &rt = *m_font_runtime[font_id]; + auto &fd = m_font_data[font_id]; + + hb_buffer_t *buffer = hb_buffer_create(); + hb_buffer_add_utf8(buffer, text.data(), static_cast(text.size()), 0, + static_cast(text.size())); + hb_buffer_guess_segment_properties(buffer); + hb_shape(rt.hb_font, buffer, nullptr, 0); + + unsigned length = hb_buffer_get_length(buffer); + auto *infos = hb_buffer_get_glyph_infos(buffer, nullptr); + auto *positions = hb_buffer_get_glyph_positions(buffer, nullptr); + float const size_f = static_cast(size); + float pen_x_em = 0.0f; + float pen_y_em = 0.0f; + rt.frame_stamp++; + + // BeginShaderMode(m_msdf_shader); + // if (m_px_range_uniform >= 0) { + // float shader_px_range = rt.px_range; + // SetShaderValue( + // m_msdf_shader, m_px_range_uniform, &shader_px_range, + // SHADER_UNIFORM_FLOAT); + // } + + for (unsigned i = 0; i < length; ++i) { + u32 glyph_index = infos[i].codepoint; + if (glyph_index == 0) + continue; + auto *entry = ensure_glyph(rt, fd, glyph_index, true); + if (!entry) + continue; + if (entry->width == 0 || entry->height == 0) + continue; + float const advance_em = hb_to_em(positions[i].x_advance, rt.units_per_em); + float const x_offset_em = hb_to_em(positions[i].x_offset, rt.units_per_em); + float const y_offset_em = hb_to_em(positions[i].y_offset, rt.units_per_em); + float const x_base_em = pen_x_em + x_offset_em; + float const y_base_em = pen_y_em + y_offset_em; + float const scale_px = size_f / static_cast(rt.em_scale); + float const margin_px = static_cast(rt.px_range) * scale_px; + float const dest_x = pos_x + + (x_base_em + entry->glyph.plane_bounds.left) * size_f - margin_px; + float const dest_y = pos_y + - (y_base_em + entry->glyph.plane_bounds.top) * size_f - margin_px; + float const dest_w = static_cast(entry->width) * scale_px; + float const dest_h = static_cast(entry->height) * scale_px; + + Rectangle source { + entry->glyph.glyph_bounds.left, + entry->glyph.glyph_bounds.top, + static_cast(entry->width), + static_cast(entry->height), + }; + Rectangle dest { dest_x, dest_y, dest_w, dest_h }; + DrawTexturePro(fd.atlas, source, dest, Vector2 { 0.0f, 0.0f }, 0.0f, + color); + + pen_x_em += advance_em; + pen_y_em += hb_to_em(positions[i].y_advance, rt.units_per_em); + } + + // EndShaderMode(); + hb_buffer_destroy(buffer); + + auto const draw_end = std::chrono::steady_clock::now(); + auto const draw_ms + = std::chrono::duration(draw_end - draw_start).count(); + if (draw_ms > 5.0) + TraceLog(LOG_INFO, "draw_text took %.2f ms for %zu glyphs", draw_ms, + static_cast(length)); } auto TextRenderer::load_font(std::filesystem::path const &path) -> std::optional { - // FIXME: Implement. - return std::nullopt; + FT_Library const ft = ft_library(); + if (!ft) + return std::nullopt; + + FT_Face face = nullptr; + if (FT_New_Face(ft, path.string().c_str(), 0, &face) != 0) + return std::nullopt; + FT_Select_Charmap(face, FT_ENCODING_UNICODE); + + auto runtime = std::make_unique(); + runtime->face = face; + runtime->atlas_width = kAtlasDimension; + runtime->atlas_height = kAtlasDimension; + runtime->pen_x = kAtlasPadding; + runtime->pen_y = kAtlasPadding; + runtime->row_height = 0; + runtime->px_range = kDefaultPxRange; + runtime->em_scale = kDefaultEmScale; + runtime->frame_stamp = 0; + runtime->units_per_em + = static_cast(face->units_per_EM ? face->units_per_EM : 2048); + runtime->ascent = static_cast(face->ascender) + / (64.0f * static_cast(runtime->units_per_em)); + runtime->descent = static_cast(face->descender) + / (64.0f * static_cast(runtime->units_per_em)); + float line_height = static_cast(face->height) + / (64.0f * static_cast(runtime->units_per_em)); + float adv_height = runtime->ascent - runtime->descent; + runtime->line_gap = std::max(0.0f, line_height - adv_height); + + runtime->hb_face = hb_ft_face_create_referenced(face); + runtime->hb_font = hb_ft_font_create_referenced(face); + if (!runtime->hb_font) { + if (runtime->hb_face) + hb_face_destroy(runtime->hb_face); + FT_Done_Face(face); + return std::nullopt; + } + hb_font_set_scale(runtime->hb_font, static_cast(runtime->units_per_em) << 6, + static_cast(runtime->units_per_em) << 6); + hb_ft_font_set_funcs(runtime->hb_font); + + runtime->msdf_font = msdfgen::adoptFreetypeFont(face); + if (!runtime->msdf_font) { + hb_font_destroy(runtime->hb_font); + hb_face_destroy(runtime->hb_face); + FT_Done_Face(face); + return std::nullopt; + } + + FontData font_data {}; + font_data.font_path = path; + font_data.atlas_img + = GenImageColor(runtime->atlas_width, runtime->atlas_height, BLANK); + if (!font_data.atlas_img.data) { + msdfgen::destroyFont(runtime->msdf_font); + hb_font_destroy(runtime->hb_font); + hb_face_destroy(runtime->hb_face); + FT_Done_Face(face); + return std::nullopt; + } + font_data.atlas = LoadTextureFromImage(font_data.atlas_img); + if (font_data.atlas.id == 0) { + UnloadImage(font_data.atlas_img); + msdfgen::destroyFont(runtime->msdf_font); + hb_font_destroy(runtime->hb_font); + hb_face_destroy(runtime->hb_face); + FT_Done_Face(face); + return std::nullopt; + } + SetTextureFilter(font_data.atlas, TEXTURE_FILTER_BILINEAR); + SetTextureWrap(font_data.atlas, TEXTURE_WRAP_CLAMP); + flush_font(*runtime, font_data); + + m_font_data.emplace_back(std::move(font_data)); + m_font_runtime.emplace_back(std::move(runtime)); + + FontHandle handle; + handle.id = m_font_data.size() - 1; + return handle; } -auto TextRenderer::unload_font(FontHandle const font) +auto TextRenderer::unload_font(FontHandle const font) -> void { - // FIXME: Implement. + usize const font_id = font(); + if (font_id >= m_font_runtime.size()) + return; + + if (m_font_runtime[font_id]) { + auto &rt = *m_font_runtime[font_id]; + rt.glyph_cache.clear(); + if (rt.msdf_font) + msdfgen::destroyFont(rt.msdf_font); + if (rt.hb_font) + hb_font_destroy(rt.hb_font); + if (rt.hb_face) + hb_face_destroy(rt.hb_face); + if (rt.face) + FT_Done_Face(rt.face); + } + m_font_runtime[font_id].reset(); + + if (font_id < m_font_data.size()) { + auto &fd = m_font_data[font_id]; + if (fd.atlas.id != 0) + UnloadTexture(fd.atlas); + if (fd.atlas_img.data) + UnloadImage(fd.atlas_img); + fd.glyphs.clear(); + } } auto find_font_path(std::string_view path) diff --git a/src/TextRenderer.hpp b/src/TextRenderer.hpp index 17e3a12..1867140 100644 --- a/src/TextRenderer.hpp +++ b/src/TextRenderer.hpp @@ -1,23 +1,42 @@ #pragma once #include +#include +#include #include +#include #include #include #include "common.hpp" +struct hb_face_t; +struct hb_font_t; +struct FT_FaceRec_; +using FT_Face = FT_FaceRec_*; + +namespace msdfgen { +class FontHandle; +} + struct FontHandle { auto operator()() const -> auto const & { return id; } private: + friend struct TextRenderer; usize id; }; +struct FontRuntime; + struct TextRenderer { TextRenderer(); // Requires raylib to be initialized! ~TextRenderer(); + TextRenderer(TextRenderer const &) = delete; + auto operator=(TextRenderer const &) -> TextRenderer & = delete; + TextRenderer(TextRenderer &&) = default; + auto operator=(TextRenderer &&) -> TextRenderer & = default; auto measure_text(FontHandle const font, std::string_view const text, int const size = 16) -> Vector2; @@ -27,7 +46,7 @@ struct TextRenderer { auto load_font(std::filesystem::path const &path) -> std::optional; - auto unload_font(FontHandle const font); + auto unload_font(FontHandle const font) -> void; private: struct FontData { @@ -48,8 +67,53 @@ private: }; Shader m_msdf_shader; + int m_px_range_uniform { -1 }; std::vector m_font_data; + struct GlyphCacheEntry { + FontData::Glyph glyph; + int atlas_x {}; + int atlas_y {}; + int width {}; + int height {}; + int stamp {}; + }; + + struct FontRuntime { + FT_Face face {}; + hb_face_t *hb_face {}; + hb_font_t *hb_font {}; + msdfgen::FontHandle *msdf_font {}; + + int atlas_width {}; + int atlas_height {}; + int pen_x {}; + int pen_y {}; + int row_height {}; + float px_range {}; + float em_scale {}; + int frame_stamp {}; + + unsigned units_per_em {}; + float ascent {}; + float descent {}; + float line_gap {}; + + std::unordered_map glyph_cache; + }; + + std::vector> m_font_runtime; + + static auto flush_font(FontRuntime &rt, FontData &fd) -> void; + static auto allocate_region( + FontRuntime &rt, FontData &fd, int width, int height) + -> std::optional>; + static auto upload_region(FontData &fd, int dst_x, int dst_y, int width, + int height, std::vector const &buffer) -> void; + static auto generate_glyph(FontRuntime &rt, FontData &fd, u32 glyph_index) + -> std::optional; + static auto ensure_glyph(FontRuntime &rt, FontData &fd, u32 glyph_index, + bool mark_usage) -> GlyphCacheEntry *; }; auto find_font_path(std::string_view path = "sans-serif:style=Regular") diff --git a/src/Theme.hpp b/src/Theme.hpp index 17695d1..10f26a0 100644 --- a/src/Theme.hpp +++ b/src/Theme.hpp @@ -5,6 +5,7 @@ #include "enum_array.hpp" struct ColorScheme { + Color foreground; struct { Color background; } window; @@ -21,16 +22,18 @@ constexpr auto make_default_themes() -> enum_array const { enum_array array; array[Theme::Light] = { - .window = - { - .background = {255, 255, 255, 100}, - }, - }; + .foreground = { 0, 0, 0, 255 }, + .window = + { + .background = { 255, 255, 255, 100 }, + }, + }; array[Theme::Dark] = { - .window = - { - .background = {0, 0, 0, 100}, - }, - }; + .foreground = { 255, 255, 255, 255 }, + .window = + { + .background = { 0, 0, 0, 100 }, + }, + }; return array; } diff --git a/src/Tick.cpp b/src/Tick.cpp index 15453b3..7994941 100644 --- a/src/Tick.cpp +++ b/src/Tick.cpp @@ -33,6 +33,13 @@ auto App::tick() -> void ClearBackground(BLANK); DrawFPS(10, 10); + if (m_tr) { + Color const fg = theme().foreground; + Vector2 const pos { 40.0f, 60.0f }; + auto text = std::string_view("Hello from Waylight"); + auto size = 48; + m_tr->draw_text(m_font, text, pos, size, fg); + } EndDrawing();