Files
waylight/src/TextRenderer.cpp
Slendi 4cbdc572a9 sadf
Signed-off-by: Slendi <slendi@socopon.com>
2025-10-05 08:43:32 +03:00

575 lines
17 KiB
C++

#include "TextRenderer.hpp"
#include <algorithm>
#include <array>
#include <cassert>
#include <chrono>
#include <cmath>
#include <cstdlib>
#include <cstring>
#include <mutex>
#include <optional>
#include <string>
#include <string_view>
#include <unordered_map>
#include <utility>
#include <vector>
#include <fontconfig/fontconfig.h>
#include <raylib.h>
#include <rlgl.h>
#undef BLACK
#undef WHITE
#undef RED
#undef GREEN
#undef BLUE
#undef YELLOW
#undef MAGENTA
#include <ft2build.h>
#include FT_FREETYPE_H
#include FT_GLYPH_H
#include <hb-ft.h>
#include <hb.h>
#include <ext/import-font.h>
#include <msdfgen.h>
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<float>(value)
/ (64.0f * static_cast<float>(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<std::pair<int, int>>
{
(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<Color> const &buffer) -> void
{
Rectangle rec { static_cast<float>(dst_x), static_cast<float>(dst_y),
static_cast<float>(width), static_cast<float>(height) };
if (fd.atlas.id != 0)
UpdateTextureRec(fd.atlas, rec, buffer.data());
if (!fd.atlas_img.data)
return;
auto *pixels = static_cast<Color *>(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<GlyphCacheEntry>
{
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<float>(bounds.r - bounds.l);
float const height_em = static_cast<float>(bounds.t - bounds.b);
double const scale = rt.em_scale;
int bmp_w = std::max(
1, static_cast<int>(std::ceil(width_em * scale + 2.0 * rt.px_range)));
int bmp_h = std::max(
1, static_cast<int>(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<float, 3> 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<Color> buffer(static_cast<size_t>(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<unsigned char>(
std::lround(std::clamp(v, 0.0f, 1.0f) * 255.0f));
};
buffer[static_cast<size_t>(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<float>(advance_em);
entry.glyph.plane_bounds.left = static_cast<float>(bounds.l);
entry.glyph.plane_bounds.right = static_cast<float>(bounds.r);
entry.glyph.plane_bounds.top = static_cast<float>(bounds.t);
entry.glyph.plane_bounds.bottom = static_cast<float>(bounds.b);
entry.glyph.glyph_bounds.left = static_cast<float>(entry.atlas_x);
entry.glyph.glyph_bounds.top = static_cast<float>(entry.atlas_y);
entry.glyph.glyph_bounds.right
= static_cast<float>(entry.atlas_x + entry.width);
entry.glyph.glyph_bounds.bottom
= static_cast<float>(entry.atlas_y + entry.height);
auto const gen_end = std::chrono::steady_clock::now();
auto const gen_ms
= std::chrono::duration<double, std::milli>(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<int>(text.size()), 0,
static_cast<int>(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<float>(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<float>(size),
height_em * static_cast<float>(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<int>(text.size()), 0,
static_cast<int>(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<float>(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<float>(rt.em_scale);
float const margin_px = static_cast<float>(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<float>(entry->width) * scale_px;
float const dest_h = static_cast<float>(entry->height) * scale_px;
Rectangle source {
entry->glyph.glyph_bounds.left,
entry->glyph.glyph_bounds.top,
static_cast<float>(entry->width),
static_cast<float>(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<double, std::milli>(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<size_t>(length));
}
auto TextRenderer::load_font(std::filesystem::path const &path)
-> std::optional<FontHandle>
{
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<FontRuntime>();
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<unsigned>(face->units_per_EM ? face->units_per_EM : 2048);
runtime->ascent = static_cast<float>(face->ascender)
/ (64.0f * static_cast<float>(runtime->units_per_em));
runtime->descent = static_cast<float>(face->descender)
/ (64.0f * static_cast<float>(runtime->units_per_em));
float line_height = static_cast<float>(face->height)
/ (64.0f * static_cast<float>(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<int>(runtime->units_per_em) << 6,
static_cast<int>(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<std::filesystem::path>
{
static std::once_flag fc_once;
std::call_once(fc_once, []() {
if (FcInit())
std::atexit([] { FcFini(); });
});
static std::mutex m;
static std::unordered_map<std::string, std::optional<std::string>> 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<FcChar8 const *>(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<std::string> final_path;
if (font) {
FcChar8 *file;
if (FcPatternGetString(font, FC_FILE, 0, &file) == FcResultMatch)
final_path = reinterpret_cast<char *>(file);
FcPatternDestroy(font);
}
FcPatternDestroy(pattern);
{
std::scoped_lock lock(m);
cache[key] = final_path;
}
return final_path;
}