feat(renderer): msdf text renderer

This commit is contained in:
Lemmy
2026-04-02 21:41:07 -04:00
parent 97abde75ef
commit 64c709561b
10 changed files with 454 additions and 337 deletions
+3
View File
@@ -0,0 +1,3 @@
[submodule "third_party/msdfgen"]
path = third_party/msdfgen
url = https://github.com/Chlumsky/msdfgen.git
+14 -2
View File
@@ -18,6 +18,16 @@ pkg_check_modules(GLES2 REQUIRED glesv2)
pkg_check_modules(WAYLAND_EGL REQUIRED wayland-egl)
pkg_check_modules(FREETYPE2 REQUIRED freetype2)
pkg_check_modules(HARFBUZZ REQUIRED harfbuzz)
# --- msdfgen (vendored) ---
set(MSDFGEN_CORE_ONLY OFF CACHE BOOL "" FORCE)
set(MSDFGEN_BUILD_STANDALONE OFF CACHE BOOL "" FORCE)
set(MSDFGEN_USE_VCPKG OFF CACHE BOOL "" FORCE)
set(MSDFGEN_USE_SKIA OFF CACHE BOOL "" FORCE)
set(MSDFGEN_DISABLE_SVG ON CACHE BOOL "" FORCE)
set(MSDFGEN_DISABLE_PNG ON CACHE BOOL "" FORCE)
set(MSDFGEN_INSTALL OFF CACHE BOOL "" FORCE)
add_subdirectory(third_party/msdfgen)
pkg_get_variable(WAYLAND_PROTOCOLS_PKGDATADIR wayland-protocols pkgdatadir)
file(REAL_PATH "${WAYLAND_PROTOCOLS_PKGDATADIR}" WAYLAND_PROTOCOLS_PKGDATADIR)
@@ -126,8 +136,8 @@ add_executable(noctalia
src/render/LinearGradientProgram.cpp
src/render/RoundedRectProgram.cpp
src/render/ShaderProgram.cpp
src/render/TextProgram.cpp
src/render/TextRenderer.cpp
src/render/MsdfTextProgram.cpp
src/render/MsdfTextRenderer.cpp
src/shell/BarShell.cpp
src/wayland/LayerSurface.cpp
src/wayland/WaylandConnection.cpp
@@ -151,6 +161,8 @@ target_include_directories(noctalia PRIVATE
)
target_link_libraries(noctalia PRIVATE
SDBusCpp::sdbus-c++
msdfgen::msdfgen-core
msdfgen::msdfgen-ext
${WAYLAND_CLIENT_LIBRARIES}
${EGL_LIBRARIES}
${GLES2_LIBRARIES}
+3 -1
View File
@@ -131,7 +131,8 @@ void GlRenderer::render(std::uint32_t width, std::uint32_t height) {
glClear(GL_COLOR_BUFFER_BIT);
constexpr auto kLabel = "Noctalia";
const auto labelMetrics = m_textRenderer.measure(kLabel);
constexpr float kFontSize = 14.0f;
const auto labelMetrics = m_textRenderer.measure(kLabel, kFontSize);
const float labelX = (static_cast<float>(width) - labelMetrics.width) * 0.5f;
const float labelHeight = labelMetrics.bottom - labelMetrics.top;
const float labelBaseline =
@@ -176,6 +177,7 @@ void GlRenderer::render(std::uint32_t width, std::uint32_t height) {
labelX,
labelBaseline,
kLabel,
kFontSize,
kRosePinePalette.text);
if (eglSwapBuffers(m_eglDisplay, m_eglSurface) != EGL_TRUE) {
+2 -2
View File
@@ -3,7 +3,7 @@
#include "render/LinearGradientProgram.hpp"
#include "render/RoundedRectProgram.hpp"
#include "render/Renderer.hpp"
#include "render/TextRenderer.hpp"
#include "render/MsdfTextRenderer.hpp"
#include <EGL/egl.h>
@@ -32,7 +32,7 @@ private:
EGLSurface m_eglSurface = EGL_NO_SURFACE;
LinearGradientProgram m_linearGradientProgram;
RoundedRectProgram m_roundedRectProgram;
TextRenderer m_textRenderer;
MsdfTextRenderer m_textRenderer;
std::uint32_t m_surfaceWidth = 0;
std::uint32_t m_surfaceHeight = 0;
};
@@ -1,4 +1,4 @@
#include "render/TextProgram.hpp"
#include "render/MsdfTextProgram.hpp"
#include <array>
#include <stdexcept>
@@ -31,18 +31,25 @@ precision highp float;
uniform sampler2D u_texture;
uniform vec4 u_color;
uniform float u_px_range;
varying vec2 v_texcoord;
float median(float r, float g, float b) {
return max(min(r, g), min(max(r, g), b));
}
void main() {
float alpha = texture2D(u_texture, v_texcoord).a;
alpha = pow(clamp(alpha, 0.0, 1.0), 0.9);
gl_FragColor = vec4(u_color.rgb, u_color.a * alpha);
vec3 msd = texture2D(u_texture, v_texcoord).rgb;
float sd = median(msd.r, msd.g, msd.b);
float screenPxDist = u_px_range * (sd - 0.5);
float opacity = clamp(screenPxDist + 0.5, 0.0, 1.0);
gl_FragColor = vec4(u_color.rgb, u_color.a * opacity);
}
)";
} // namespace
void TextProgram::ensureInitialized() {
void MsdfTextProgram::ensureInitialized() {
if (m_program.isValid()) {
return;
}
@@ -52,6 +59,7 @@ void TextProgram::ensureInitialized() {
m_texCoordLocation = glGetAttribLocation(m_program.id(), "a_texcoord");
m_surfaceSizeLocation = glGetUniformLocation(m_program.id(), "u_surface_size");
m_rectLocation = glGetUniformLocation(m_program.id(), "u_rect");
m_pxRangeLocation = glGetUniformLocation(m_program.id(), "u_px_range");
m_colorLocation = glGetUniformLocation(m_program.id(), "u_color");
m_samplerLocation = glGetUniformLocation(m_program.id(), "u_texture");
@@ -59,34 +67,37 @@ void TextProgram::ensureInitialized() {
m_texCoordLocation < 0 ||
m_surfaceSizeLocation < 0 ||
m_rectLocation < 0 ||
m_pxRangeLocation < 0 ||
m_colorLocation < 0 ||
m_samplerLocation < 0) {
throw std::runtime_error("failed to query text shader locations");
throw std::runtime_error("failed to query MSDF text shader locations");
}
}
void TextProgram::destroy() {
void MsdfTextProgram::destroy() {
m_program.destroy();
m_positionLocation = -1;
m_texCoordLocation = -1;
m_surfaceSizeLocation = -1;
m_rectLocation = -1;
m_pxRangeLocation = -1;
m_colorLocation = -1;
m_samplerLocation = -1;
}
void TextProgram::draw(GLuint texture,
float surfaceWidth,
float surfaceHeight,
float x,
float y,
float width,
float height,
float u0,
float v0,
float u1,
float v1,
const Color& color) const {
void MsdfTextProgram::draw(GLuint texture,
float surfaceWidth,
float surfaceHeight,
float x,
float y,
float width,
float height,
float u0,
float v0,
float u1,
float v1,
float pxRange,
const Color& color) const {
if (!m_program.isValid() || texture == 0 || width <= 0.0f || height <= 0.0f) {
return;
}
@@ -112,15 +123,18 @@ void TextProgram::draw(GLuint texture,
glUseProgram(m_program.id());
glUniform2f(m_surfaceSizeLocation, surfaceWidth, surfaceHeight);
glUniform4f(m_rectLocation, x, y, width, height);
glUniform1f(m_pxRangeLocation, pxRange);
glUniform4f(m_colorLocation, color.r, color.g, color.b, color.a);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture);
glUniform1i(m_samplerLocation, 0);
glVertexAttribPointer(m_positionLocation, 2, GL_FLOAT, GL_FALSE, 0, positions.data());
glVertexAttribPointer(m_texCoordLocation, 2, GL_FLOAT, GL_FALSE, 0, texcoords.data());
glEnableVertexAttribArray(m_positionLocation);
glEnableVertexAttribArray(m_texCoordLocation);
const auto posAttr = static_cast<GLuint>(m_positionLocation);
const auto texAttr = static_cast<GLuint>(m_texCoordLocation);
glVertexAttribPointer(posAttr, 2, GL_FLOAT, GL_FALSE, 0, positions.data());
glVertexAttribPointer(texAttr, 2, GL_FLOAT, GL_FALSE, 0, texcoords.data());
glEnableVertexAttribArray(posAttr);
glEnableVertexAttribArray(texAttr);
glDrawArrays(GL_TRIANGLES, 0, 6);
glDisableVertexAttribArray(m_positionLocation);
glDisableVertexAttribArray(m_texCoordLocation);
glDisableVertexAttribArray(posAttr);
glDisableVertexAttribArray(texAttr);
}
@@ -5,13 +5,13 @@
#include <GLES2/gl2.h>
class TextProgram {
class MsdfTextProgram {
public:
TextProgram() = default;
~TextProgram() = default;
MsdfTextProgram() = default;
~MsdfTextProgram() = default;
TextProgram(const TextProgram&) = delete;
TextProgram& operator=(const TextProgram&) = delete;
MsdfTextProgram(const MsdfTextProgram&) = delete;
MsdfTextProgram& operator=(const MsdfTextProgram&) = delete;
void ensureInitialized();
void destroy();
@@ -27,6 +27,7 @@ public:
float v0,
float u1,
float v1,
float pxRange,
const Color& color) const;
private:
@@ -35,6 +36,7 @@ private:
GLint m_texCoordLocation = -1;
GLint m_surfaceSizeLocation = -1;
GLint m_rectLocation = -1;
GLint m_pxRangeLocation = -1;
GLint m_colorLocation = -1;
GLint m_samplerLocation = -1;
};
+365
View File
@@ -0,0 +1,365 @@
#include "render/MsdfTextRenderer.hpp"
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wshadow"
#include <msdfgen.h>
#include <msdfgen-ext.h>
#pragma GCC diagnostic pop
#include <algorithm>
#include <cmath>
#include <cstring>
#include <stdexcept>
#include <vector>
namespace {
constexpr auto kDefaultFontPath = "/usr/share/fonts/google-roboto/Roboto-Medium.ttf";
constexpr float kAtlasEmSize = 48.0f;
constexpr double kDistanceRange = 4.0;
constexpr int kGlyphPadding = 2;
} // namespace
MsdfTextRenderer::MsdfTextRenderer() = default;
MsdfTextRenderer::~MsdfTextRenderer() {
cleanup();
}
void MsdfTextRenderer::initialize() {
if (m_face != nullptr) {
return;
}
if (FT_Init_FreeType(&m_library) != 0) {
throw std::runtime_error("FT_Init_FreeType failed");
}
if (FT_New_Face(m_library, kDefaultFontPath, 0, &m_face) != 0) {
throw std::runtime_error("FT_New_Face failed");
}
if (FT_Set_Pixel_Sizes(m_face, 0, static_cast<FT_UInt>(kAtlasEmSize)) != 0) {
throw std::runtime_error("FT_Set_Pixel_Sizes failed");
}
m_hbFont = hb_ft_font_create_referenced(m_face);
if (m_hbFont == nullptr) {
throw std::runtime_error("hb_ft_font_create_referenced failed");
}
m_currentShapingSize = kAtlasEmSize;
m_fontHandle = msdfgen::adoptFreetypeFont(m_face);
if (m_fontHandle == nullptr) {
throw std::runtime_error("msdfgen::adoptFreetypeFont failed");
}
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
ensureAtlasInitialized();
m_program.ensureInitialized();
}
MsdfTextRenderer::TextMetrics MsdfTextRenderer::measure(std::string_view text, float fontSize) {
if (text.empty()) {
return {};
}
setShapingSize(fontSize);
hb_buffer_t* buffer = hb_buffer_create();
if (buffer == nullptr) {
throw std::runtime_error("hb_buffer_create failed");
}
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(m_hbFont, buffer, nullptr, 0);
unsigned int glyphCount = 0;
hb_glyph_position_t* glyphPositions = hb_buffer_get_glyph_positions(buffer, &glyphCount);
hb_glyph_info_t* glyphInfos = hb_buffer_get_glyph_infos(buffer, &glyphCount);
const float scale = fontSize / kAtlasEmSize;
float width = 0.0f;
float penX = 0.0f;
float minTop = 0.0f;
float maxBottom = 0.0f;
bool hasBounds = false;
for (unsigned int i = 0; i < glyphCount; ++i) {
const auto glyphIndex = glyphInfos[i].codepoint;
Glyph& glyph = loadGlyph(glyphIndex);
const float xOffset = static_cast<float>(glyphPositions[i].x_offset) / 64.0f;
const float yOffset = static_cast<float>(glyphPositions[i].y_offset) / 64.0f;
const float glyphLeft = penX + xOffset + glyph.bearingX * scale;
const float glyphTop = -yOffset - glyph.bearingY * scale;
const float glyphBottom = glyphTop + glyph.atlasHeight * scale;
const float glyphRight = glyphLeft + glyph.atlasWidth * scale;
if (glyph.atlasWidth > 0.0f && glyph.atlasHeight > 0.0f) {
if (!hasBounds) {
minTop = glyphTop;
maxBottom = glyphBottom;
width = glyphRight;
hasBounds = true;
} else {
minTop = std::min(minTop, glyphTop);
maxBottom = std::max(maxBottom, glyphBottom);
width = std::max(width, glyphRight);
}
}
penX += static_cast<float>(glyphPositions[i].x_advance) / 64.0f;
}
width = std::max(width, penX);
hb_buffer_destroy(buffer);
return TextMetrics{
.width = width,
.top = minTop,
.bottom = maxBottom,
};
}
void MsdfTextRenderer::draw(float surfaceWidth,
float surfaceHeight,
float x,
float baselineY,
std::string_view text,
float fontSize,
const Color& color) {
if (text.empty()) {
return;
}
setShapingSize(fontSize);
hb_buffer_t* buffer = hb_buffer_create();
if (buffer == nullptr) {
throw std::runtime_error("hb_buffer_create failed");
}
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(m_hbFont, buffer, nullptr, 0);
unsigned int glyphCount = 0;
hb_glyph_info_t* glyphInfos = hb_buffer_get_glyph_infos(buffer, &glyphCount);
hb_glyph_position_t* glyphPositions = hb_buffer_get_glyph_positions(buffer, &glyphCount);
const float scale = fontSize / kAtlasEmSize;
const float pxRange = std::max(static_cast<float>(kDistanceRange) * scale, 1.0f);
float penX = x;
float penY = std::round(baselineY);
for (unsigned int i = 0; i < glyphCount; ++i) {
const auto glyphIndex = glyphInfos[i].codepoint;
Glyph& glyph = loadGlyph(glyphIndex);
if (glyph.atlasWidth > 0.0f && glyph.atlasHeight > 0.0f) {
const float xOffset = static_cast<float>(glyphPositions[i].x_offset) / 64.0f;
const float yOffset = static_cast<float>(glyphPositions[i].y_offset) / 64.0f;
const float glyphX = penX + xOffset + glyph.bearingX * scale;
const float glyphY = penY - yOffset - glyph.bearingY * scale;
const float glyphW = glyph.atlasWidth * scale;
const float glyphH = glyph.atlasHeight * scale;
m_program.draw(
m_atlasTexture,
surfaceWidth,
surfaceHeight,
glyphX,
glyphY,
glyphW,
glyphH,
glyph.u0,
glyph.v0,
glyph.u1,
glyph.v1,
pxRange,
color);
}
penX += static_cast<float>(glyphPositions[i].x_advance) / 64.0f;
penY -= static_cast<float>(glyphPositions[i].y_advance) / 64.0f;
}
hb_buffer_destroy(buffer);
}
void MsdfTextRenderer::cleanup() {
if (m_atlasTexture != 0) {
glDeleteTextures(1, &m_atlasTexture);
m_atlasTexture = 0;
}
m_glyphs.clear();
m_atlasCursorX = 1;
m_atlasCursorY = 1;
m_atlasRowHeight = 0;
m_program.destroy();
if (m_fontHandle != nullptr) {
msdfgen::destroyFont(m_fontHandle);
m_fontHandle = nullptr;
}
if (m_hbFont != nullptr) {
hb_font_destroy(m_hbFont);
m_hbFont = nullptr;
}
if (m_face != nullptr) {
FT_Done_Face(m_face);
m_face = nullptr;
}
if (m_library != nullptr) {
FT_Done_FreeType(m_library);
m_library = nullptr;
}
}
void MsdfTextRenderer::ensureAtlasInitialized() {
if (m_atlasTexture != 0) {
return;
}
glGenTextures(1, &m_atlasTexture);
glBindTexture(GL_TEXTURE_2D, m_atlasTexture);
glTexImage2D(
GL_TEXTURE_2D,
0,
GL_RGB,
m_atlasWidth,
m_atlasHeight,
0,
GL_RGB,
GL_UNSIGNED_BYTE,
nullptr);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
}
void MsdfTextRenderer::setShapingSize(float fontSize) {
if (std::abs(fontSize - m_currentShapingSize) < 0.01f) {
return;
}
if (FT_Set_Pixel_Sizes(m_face, 0, static_cast<FT_UInt>(fontSize)) != 0) {
throw std::runtime_error("FT_Set_Pixel_Sizes failed");
}
if (m_hbFont != nullptr) {
hb_font_destroy(m_hbFont);
}
m_hbFont = hb_ft_font_create_referenced(m_face);
if (m_hbFont == nullptr) {
throw std::runtime_error("hb_ft_font_create_referenced failed");
}
m_currentShapingSize = fontSize;
}
MsdfTextRenderer::Glyph& MsdfTextRenderer::loadGlyph(std::uint32_t glyphIndex) {
if (auto it = m_glyphs.find(glyphIndex); it != m_glyphs.end()) {
return it->second;
}
msdfgen::Shape shape;
double advance = 0.0;
if (!msdfgen::loadGlyph(shape, m_fontHandle, msdfgen::GlyphIndex(glyphIndex),
msdfgen::FONT_SCALING_EM_NORMALIZED, &advance)) {
auto [it, _] = m_glyphs.emplace(glyphIndex, Glyph{});
return it->second;
}
if (shape.contours.empty()) {
auto [it, _] = m_glyphs.emplace(glyphIndex, Glyph{});
return it->second;
}
shape.normalize();
msdfgen::Shape::Bounds bounds = shape.getBounds();
const auto emSize = static_cast<double>(kAtlasEmSize);
const double pxLeft = bounds.l * emSize - kDistanceRange;
const double pxBottom = bounds.b * emSize - kDistanceRange;
const double pxRight = bounds.r * emSize + kDistanceRange;
const double pxTop = bounds.t * emSize + kDistanceRange;
const int glyphW = std::max(1, static_cast<int>(std::ceil(pxRight - pxLeft)));
const int glyphH = std::max(1, static_cast<int>(std::ceil(pxTop - pxBottom)));
msdfgen::edgeColoringSimple(shape, 3.0);
msdfgen::Bitmap<float, 3> msdf(glyphW, glyphH);
const msdfgen::Projection projection(
msdfgen::Vector2(emSize, emSize),
msdfgen::Vector2(-pxLeft / emSize, -pxBottom / emSize));
const msdfgen::Range range(kDistanceRange / emSize);
msdfgen::generateMSDF(msdf, shape, projection, range);
const auto pixelCount = static_cast<std::size_t>(glyphW * glyphH * 3);
std::vector<unsigned char> pixels(pixelCount);
for (int y = 0; y < glyphH; ++y) {
const int srcY = glyphH - 1 - y;
for (int x = 0; x < glyphW; ++x) {
const auto dstIdx = static_cast<std::size_t>((y * glyphW + x) * 3);
const float* pixel = msdf(x, srcY);
pixels[dstIdx + 0] = msdfgen::pixelFloatToByte(pixel[0]);
pixels[dstIdx + 1] = msdfgen::pixelFloatToByte(pixel[1]);
pixels[dstIdx + 2] = msdfgen::pixelFloatToByte(pixel[2]);
}
}
const int paddedW = glyphW + kGlyphPadding * 2;
const int paddedH = glyphH + kGlyphPadding * 2;
if (m_atlasCursorX + paddedW > m_atlasWidth) {
m_atlasCursorX = 1;
m_atlasCursorY += m_atlasRowHeight + 1;
m_atlasRowHeight = 0;
}
if (m_atlasCursorY + paddedH > m_atlasHeight) {
throw std::runtime_error("MSDF text atlas is full");
}
const int destX = m_atlasCursorX + kGlyphPadding;
const int destY = m_atlasCursorY + kGlyphPadding;
glBindTexture(GL_TEXTURE_2D, m_atlasTexture);
glTexSubImage2D(
GL_TEXTURE_2D,
0,
destX,
destY,
glyphW,
glyphH,
GL_RGB,
GL_UNSIGNED_BYTE,
pixels.data());
Glyph glyph{
.atlasWidth = static_cast<float>(glyphW),
.atlasHeight = static_cast<float>(glyphH),
.bearingX = static_cast<float>(pxLeft),
.bearingY = static_cast<float>(pxTop),
.u0 = static_cast<float>(destX) / static_cast<float>(m_atlasWidth),
.v0 = static_cast<float>(destY) / static_cast<float>(m_atlasHeight),
.u1 = static_cast<float>(destX + glyphW) / static_cast<float>(m_atlasWidth),
.v1 = static_cast<float>(destY + glyphH) / static_cast<float>(m_atlasHeight),
};
m_atlasCursorX += paddedW + 1;
m_atlasRowHeight = std::max(m_atlasRowHeight, paddedH);
auto [it, _] = m_glyphs.emplace(glyphIndex, glyph);
return it->second;
}
@@ -1,7 +1,7 @@
#pragma once
#include "render/Color.hpp"
#include "render/TextProgram.hpp"
#include "render/MsdfTextProgram.hpp"
#include <ft2build.h>
#include FT_FREETYPE_H
@@ -11,7 +11,11 @@
#include <string_view>
#include <unordered_map>
class TextRenderer {
namespace msdfgen {
class FontHandle;
}
class MsdfTextRenderer {
public:
struct TextMetrics {
float width = 0.0f;
@@ -19,26 +23,27 @@ public:
float bottom = 0.0f;
};
TextRenderer();
~TextRenderer();
MsdfTextRenderer();
~MsdfTextRenderer();
TextRenderer(const TextRenderer&) = delete;
TextRenderer& operator=(const TextRenderer&) = delete;
MsdfTextRenderer(const MsdfTextRenderer&) = delete;
MsdfTextRenderer& operator=(const MsdfTextRenderer&) = delete;
void initialize();
[[nodiscard]] TextMetrics measure(std::string_view text);
[[nodiscard]] TextMetrics measure(std::string_view text, float fontSize);
void draw(float surfaceWidth,
float surfaceHeight,
float x,
float baselineY,
std::string_view text,
float fontSize,
const Color& color);
void cleanup();
private:
struct Glyph {
float width = 0.0f;
float height = 0.0f;
float atlasWidth = 0.0f;
float atlasHeight = 0.0f;
float bearingX = 0.0f;
float bearingY = 0.0f;
float u0 = 0.0f;
@@ -49,16 +54,19 @@ private:
Glyph& loadGlyph(std::uint32_t glyphIndex);
void ensureAtlasInitialized();
void setShapingSize(float fontSize);
FT_Library m_library = nullptr;
FT_Face m_face = nullptr;
hb_font_t* m_hbFont = nullptr;
TextProgram m_program;
msdfgen::FontHandle* m_fontHandle = nullptr;
MsdfTextProgram m_program;
GLuint m_atlasTexture = 0;
int m_atlasWidth = 1024;
int m_atlasHeight = 1024;
int m_atlasWidth = 2048;
int m_atlasHeight = 2048;
int m_atlasCursorX = 1;
int m_atlasCursorY = 1;
int m_atlasRowHeight = 0;
float m_currentShapingSize = 0.0f;
std::unordered_map<std::uint32_t, Glyph> m_glyphs;
};
-290
View File
@@ -1,290 +0,0 @@
#include "render/TextRenderer.hpp"
#include <algorithm>
#include <array>
#include <cstring>
#include <cmath>
#include <stdexcept>
#include <string>
#include <vector>
namespace {
constexpr auto kDefaultFontPath = "/usr/share/fonts/google-roboto/Roboto-Medium.ttf";
constexpr unsigned int kFontPixelSize = 14;
constexpr float kRasterScale = 1.0f;
constexpr int kGlyphPadding = 1;
} // namespace
TextRenderer::TextRenderer() = default;
TextRenderer::~TextRenderer() {
cleanup();
}
void TextRenderer::initialize() {
if (m_face != nullptr) {
return;
}
if (FT_Init_FreeType(&m_library) != 0) {
throw std::runtime_error("FT_Init_FreeType failed");
}
if (FT_New_Face(m_library, kDefaultFontPath, 0, &m_face) != 0) {
throw std::runtime_error("FT_New_Face failed");
}
if (FT_Set_Pixel_Sizes(m_face, 0, static_cast<FT_UInt>(kFontPixelSize)) != 0) {
throw std::runtime_error("FT_Set_Pixel_Sizes failed");
}
m_hbFont = hb_ft_font_create_referenced(m_face);
if (m_hbFont == nullptr) {
throw std::runtime_error("hb_ft_font_create_referenced failed");
}
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
ensureAtlasInitialized();
m_program.ensureInitialized();
}
TextRenderer::TextMetrics TextRenderer::measure(std::string_view text) {
if (text.empty()) {
return {};
}
hb_buffer_t* buffer = hb_buffer_create();
if (buffer == nullptr) {
throw std::runtime_error("hb_buffer_create failed");
}
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(m_hbFont, buffer, nullptr, 0);
unsigned int glyphCount = 0;
hb_glyph_position_t* glyphPositions = hb_buffer_get_glyph_positions(buffer, &glyphCount);
hb_glyph_info_t* glyphInfos = hb_buffer_get_glyph_infos(buffer, &glyphCount);
float width = 0.0f;
float penX = 0.0f;
float minTop = 0.0f;
float maxBottom = 0.0f;
bool hasBounds = false;
for (unsigned int i = 0; i < glyphCount; ++i) {
const auto glyphIndex = glyphInfos[i].codepoint;
Glyph& glyph = loadGlyph(glyphIndex);
const float xOffset = static_cast<float>(glyphPositions[i].x_offset) / 64.0f / kRasterScale;
const float yOffset = static_cast<float>(glyphPositions[i].y_offset) / 64.0f / kRasterScale;
const float glyphLeft = penX + xOffset + glyph.bearingX;
const float glyphTop = -yOffset - glyph.bearingY;
const float glyphBottom = glyphTop + glyph.height;
const float glyphRight = glyphLeft + glyph.width;
if (glyph.width > 0.0f && glyph.height > 0.0f) {
if (!hasBounds) {
minTop = glyphTop;
maxBottom = glyphBottom;
width = glyphRight;
hasBounds = true;
} else {
minTop = std::min(minTop, glyphTop);
maxBottom = std::max(maxBottom, glyphBottom);
width = std::max(width, glyphRight);
}
}
penX += static_cast<float>(glyphPositions[i].x_advance) / 64.0f / kRasterScale;
}
width = std::max(width, penX);
hb_buffer_destroy(buffer);
return TextMetrics{
.width = width,
.top = minTop,
.bottom = maxBottom,
};
}
void TextRenderer::draw(float surfaceWidth,
float surfaceHeight,
float x,
float baselineY,
std::string_view text,
const Color& color) {
if (text.empty()) {
return;
}
hb_buffer_t* buffer = hb_buffer_create();
if (buffer == nullptr) {
throw std::runtime_error("hb_buffer_create failed");
}
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(m_hbFont, buffer, nullptr, 0);
unsigned int glyphCount = 0;
hb_glyph_info_t* glyphInfos = hb_buffer_get_glyph_infos(buffer, &glyphCount);
hb_glyph_position_t* glyphPositions = hb_buffer_get_glyph_positions(buffer, &glyphCount);
float penX = x;
float penY = std::round(baselineY);
for (unsigned int i = 0; i < glyphCount; ++i) {
const auto glyphIndex = glyphInfos[i].codepoint;
Glyph& glyph = loadGlyph(glyphIndex);
const float xOffset = static_cast<float>(glyphPositions[i].x_offset) / 64.0f / kRasterScale;
const float yOffset = static_cast<float>(glyphPositions[i].y_offset) / 64.0f / kRasterScale;
const float glyphX = std::round(penX + xOffset + glyph.bearingX);
const float glyphY = std::round(penY - yOffset - glyph.bearingY);
m_program.draw(
m_atlasTexture,
surfaceWidth,
surfaceHeight,
glyphX,
glyphY,
glyph.width,
glyph.height,
glyph.u0,
glyph.v0,
glyph.u1,
glyph.v1,
color);
penX += static_cast<float>(glyphPositions[i].x_advance) / 64.0f / kRasterScale;
penY -= static_cast<float>(glyphPositions[i].y_advance) / 64.0f / kRasterScale;
}
hb_buffer_destroy(buffer);
}
void TextRenderer::cleanup() {
if (m_atlasTexture != 0) {
glDeleteTextures(1, &m_atlasTexture);
m_atlasTexture = 0;
}
m_glyphs.clear();
m_atlasCursorX = 1;
m_atlasCursorY = 1;
m_atlasRowHeight = 0;
m_program.destroy();
if (m_hbFont != nullptr) {
hb_font_destroy(m_hbFont);
m_hbFont = nullptr;
}
if (m_face != nullptr) {
FT_Done_Face(m_face);
m_face = nullptr;
}
if (m_library != nullptr) {
FT_Done_FreeType(m_library);
m_library = nullptr;
}
}
void TextRenderer::ensureAtlasInitialized() {
if (m_atlasTexture != 0) {
return;
}
glGenTextures(1, &m_atlasTexture);
glBindTexture(GL_TEXTURE_2D, m_atlasTexture);
glTexImage2D(
GL_TEXTURE_2D,
0,
GL_ALPHA,
m_atlasWidth,
m_atlasHeight,
0,
GL_ALPHA,
GL_UNSIGNED_BYTE,
nullptr);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
}
TextRenderer::Glyph& TextRenderer::loadGlyph(std::uint32_t glyphIndex) {
if (auto it = m_glyphs.find(glyphIndex); it != m_glyphs.end()) {
return it->second;
}
if (FT_Load_Glyph(m_face, glyphIndex, FT_LOAD_DEFAULT | FT_LOAD_TARGET_LIGHT) != 0) {
throw std::runtime_error("FT_Load_Glyph failed");
}
if (FT_Render_Glyph(m_face->glyph, FT_RENDER_MODE_NORMAL) != 0) {
throw std::runtime_error("FT_Render_Glyph failed");
}
const FT_GlyphSlot slot = m_face->glyph;
const int glyphPixelWidth = static_cast<int>(slot->bitmap.width);
const int paddedWidth = glyphPixelWidth + (kGlyphPadding * 2);
const int paddedHeight = static_cast<int>(slot->bitmap.rows) + (kGlyphPadding * 2);
if (m_atlasCursorX + paddedWidth > m_atlasWidth) {
m_atlasCursorX = 1;
m_atlasCursorY += m_atlasRowHeight + 1;
m_atlasRowHeight = 0;
}
if (m_atlasCursorY + paddedHeight > m_atlasHeight) {
throw std::runtime_error("text atlas is full");
}
std::vector<unsigned char> paddedBitmap(static_cast<std::size_t>(paddedWidth * paddedHeight), 0);
for (unsigned int row = 0; row < slot->bitmap.rows; ++row) {
const auto* src = slot->bitmap.buffer + row * slot->bitmap.pitch;
auto* dst = paddedBitmap.data()
+ ((static_cast<std::size_t>(row) + static_cast<std::size_t>(kGlyphPadding))
* static_cast<std::size_t>(paddedWidth))
+ static_cast<std::size_t>(kGlyphPadding);
std::memcpy(dst, src, static_cast<std::size_t>(slot->bitmap.width));
}
glBindTexture(GL_TEXTURE_2D, m_atlasTexture);
glTexSubImage2D(
GL_TEXTURE_2D,
0,
m_atlasCursorX,
m_atlasCursorY,
paddedWidth,
paddedHeight,
GL_ALPHA,
GL_UNSIGNED_BYTE,
paddedBitmap.data());
Glyph glyph{
.width = static_cast<float>(glyphPixelWidth) / kRasterScale,
.height = static_cast<float>(slot->bitmap.rows) / kRasterScale,
.bearingX = static_cast<float>(slot->bitmap_left) / kRasterScale,
.bearingY = static_cast<float>(slot->bitmap_top) / kRasterScale,
.u0 = (static_cast<float>(m_atlasCursorX + kGlyphPadding) + 0.5f) / static_cast<float>(m_atlasWidth),
.v0 = (static_cast<float>(m_atlasCursorY + kGlyphPadding) + 0.5f) / static_cast<float>(m_atlasHeight),
.u1 = (static_cast<float>(m_atlasCursorX + kGlyphPadding + glyphPixelWidth) - 0.5f)
/ static_cast<float>(m_atlasWidth),
.v1 = (static_cast<float>(m_atlasCursorY + kGlyphPadding + slot->bitmap.rows) - 0.5f)
/ static_cast<float>(m_atlasHeight),
};
m_atlasCursorX += paddedWidth + 1;
m_atlasRowHeight = std::max(m_atlasRowHeight, paddedHeight);
auto [it, _] = m_glyphs.emplace(glyphIndex, glyph);
return it->second;
}
Vendored Submodule
+1
Submodule third_party/msdfgen added at 85e8b3d47b