#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); double const inv_scale = 1.0 / scale; msdfgen::Vector2 translate(-bounds.l + rt.px_range * inv_scale, -bounds.b + rt.px_range * inv_scale); 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) { int const dst_y = bmp_h - 1 - y; for (int x = 0; x < bmp_w; ++x) { float const *px = msdf_bitmap(x, y); auto const clamp = [](float v) { printf("%.2f ", v); return static_cast( std::lround(std::clamp(v, 0.0f, 1.0f) * 255.0f)); }; buffer[static_cast(dst_y) * bmp_w + x] = Color { clamp(px[0]), clamp(px[1]), clamp(px[2]), 255 }; printf("\n"); } } 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() { static char const msdf_fs_data[] { #embed "msdf.fs" , 0 }; m_msdf_shader = LoadShaderFromMemory(nullptr, msdf_fs_data); assert(IsShaderValid(m_msdf_shader)); m_px_range_uniform = GetShaderLocation(m_msdf_shader, "pxRange"); } 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 { 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! 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 { 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) -> void { 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) -> std::optional { static std::once_flag fc_once; std::call_once(fc_once, []() { if (FcInit()) std::atexit([] { FcFini(); }); }); static std::mutex m; static std::unordered_map> cache; std::string const key(path); { std::scoped_lock lock(m); if (auto it = cache.find(key); it != cache.end()) return it->second; } FcPattern *pattern = FcNameParse(reinterpret_cast(key.c_str())); if (!pattern) { std::scoped_lock lock(m); return cache[key] = std::nullopt; } FcConfigSubstitute(nullptr, pattern, FcMatchPattern); FcDefaultSubstitute(pattern); FcResult result; FcPattern *font = FcFontMatch(nullptr, pattern, &result); std::optional final_path; if (font) { FcChar8 *file; if (FcPatternGetString(font, FC_FILE, 0, &file) == FcResultMatch) final_path = reinterpret_cast(file); FcPatternDestroy(font); } FcPatternDestroy(pattern); { std::scoped_lock lock(m); cache[key] = final_path; } return final_path; }