MayaFlux 1.0.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
GlyphAtlas.hpp
Go to the documentation of this file.
1#pragma once
2
3#include "FontFace.hpp"
4
6
7#include <ft2build.h>
8#include FT_FREETYPE_H
9
10namespace MayaFlux::Portal::Text {
11
12/**
13 * @struct GlyphMetrics
14 * @brief Per-glyph layout and UV data produced by GlyphAtlas.
15 *
16 * UV coordinates are in normalised [0, 1] atlas space.
17 * Bearing and size are in pixels at the atlas's declared pixel_size.
18 *
19 * face and glyph_index identify which FontFace actually produced this
20 * glyph (the primary face, or a fallback on a primary miss) and its glyph
21 * index within that specific face. Glyph indices are only meaningful
22 * relative to the face that produced them: a consumer doing anything
23 * face-sensitive (kerning) must compare face identity, not just index.
24 *
25 * When HarfBuzz is introduced it will supply glyph_index and position
26 * offsets directly; this struct remains the downstream currency.
27 */
29 float uv_x0 { 0.F }; ///< Left UV edge in atlas texture.
30 float uv_y0 { 0.F }; ///< Top UV edge in atlas texture.
31 float uv_x1 { 0.F }; ///< Right UV edge in atlas texture.
32 float uv_y1 { 0.F }; ///< Bottom UV edge in atlas texture.
33
34 int32_t bearing_x { 0 }; ///< Horizontal bearing in pixels (from FT_GlyphSlot).
35 int32_t bearing_y { 0 }; ///< Vertical bearing in pixels (from FT_GlyphSlot).
36 uint32_t width { 0 }; ///< Glyph bitmap width in pixels.
37 uint32_t height { 0 }; ///< Glyph bitmap height in pixels.
38 int32_t advance_x { 0 }; ///< Horizontal advance in pixels (26.6 fixed-point >> 6).
39
40 const FontFace* face { nullptr }; ///< Face that produced this glyph (primary or fallback).
41 FT_UInt glyph_index { 0 }; ///< Glyph index within `face` (from FT_Get_Char_Index).
42};
43
44/**
45 * @class GlyphAtlas
46 * @brief Rasterizes and packs glyphs from a FontFace into a TextureContainer.
47 *
48 * One atlas corresponds to one (FontFace, pixel_size) pair. The atlas
49 * texture is R8 (single-channel coverage); colour is applied in the shader.
50 *
51 * Glyphs are rasterized on first request via get_or_rasterize() and packed
52 * into the atlas using a simple shelf packing algorithm. On shelf overflow
53 * the atlas texture doubles in size and every previously cached glyph is
54 * re-rasterized into the new texture from its owning face; rasterization
55 * never permanently refuses for lack of room.
56 *
57 * The primary face is fixed at construction. A codepoint the primary face
58 * lacks is looked up in the fallback chain (add_fallback()), in order, and
59 * the first face reporting a glyph produces it; a codepoint absent from
60 * every face fails as before. The atlas's cache is keyed on (face,
61 * glyph_index) rather than glyph_index alone, since glyph indices are only
62 * unique within the face that assigned them: the same numeric index from
63 * two different faces names two unrelated glyphs.
64 *
65 * Callers obtain glyph indices via FT_Get_Char_Index on the FontFace. This
66 * keeps the path open for HarfBuzz, which outputs glyph indices directly
67 * (against the primary face; HarfBuzz does not participate in fallback).
68 *
69 * Thread safety: not thread-safe. Rasterization must occur on the thread
70 * that owns the FontFace.
71 */
72class MAYAFLUX_API GlyphAtlas {
73public:
74 /**
75 * @brief Construct an atlas for a specific face and pixel size.
76 * @param face Loaded FontFace. Must outlive this atlas.
77 * @param pixel_size Glyph height in pixels (width is derived by FreeType).
78 * @param atlas_size Width and height of the atlas texture in pixels.
79 * Must be a power of two. Default 512 is sufficient
80 * for ASCII + extended Latin at sizes up to ~48px.
81 */
82 explicit GlyphAtlas(
83 FontFace& face,
84 uint32_t pixel_size,
85 uint32_t atlas_size = 512);
86
87 ~GlyphAtlas() = default;
88
89 GlyphAtlas(const GlyphAtlas&) = delete;
90 GlyphAtlas& operator=(const GlyphAtlas&) = delete;
93
94 /**
95 * @brief Return metrics for a glyph, rasterizing it into the atlas if needed.
96 * @param glyph_index FT_UInt glyph index (from FT_Get_Char_Index).
97 * @return Pointer to cached GlyphMetrics, or nullptr if rasterization fails.
98 */
99 const GlyphMetrics* get_or_rasterize(FT_UInt glyph_index);
100
101 /**
102 * @brief Access the backing FontFace. Callers may query FT_Face properties
103 * or call FT_Get_Char_Index directly for HarfBuzz integration.
104 */
105 [[nodiscard]] FontFace& get_face() { return m_face; }
106 [[nodiscard]] const FontFace& get_face() const { return m_face; }
107
108 /**
109 * @brief Convenience: look up by Unicode codepoint.
110 *
111 * Tries the primary face first, then each fallback face in registration
112 * order, returning the first hit. Prefer the glyph_index overload when
113 * integrating with HarfBuzz output (primary face only, no fallback).
114 *
115 * @param codepoint Unicode codepoint (e.g. U+0041 for 'A').
116 * @return Pointer to cached GlyphMetrics, or nullptr if no face (primary
117 * or fallback) has this codepoint.
118 */
119 const GlyphMetrics* get_or_rasterize(FT_ULong codepoint);
120
121 /**
122 * @brief Register a fallback face, tried in order after the primary on a miss.
123 *
124 * @param face Loaded FontFace. Must outlive this atlas. Sized to this
125 * atlas's pixel_size lazily, on first use, same as the
126 * primary face.
127 */
128 void add_fallback(FontFace& face);
129
130 /**
131 * @brief The atlas texture as a TextureContainer (R8, atlas_size x atlas_size).
132 *
133 * The container is valid after construction. Its pixel data is updated
134 * in-place as new glyphs are rasterized; callers must re-upload to GPU
135 * after any rasterization call that returns non-null.
136 */
137 [[nodiscard]] const Kakshya::TextureContainer& texture() const { return *m_texture; }
138 [[nodiscard]] Kakshya::TextureContainer& texture() { return *m_texture; }
139
140 /**
141 * @brief Returns true if at least one glyph was rasterized since the last
142 * call to clear_dirty(). Use to decide whether to re-upload to GPU.
143 */
144 [[nodiscard]] bool is_dirty() const { return m_dirty; }
145
146 /**
147 * @brief Clear the dirty flag after re-uploading the atlas to GPU.
148 */
149 void clear_dirty() { m_dirty = false; }
150
151 /**
152 * @brief Pixel size passed at construction.
153 */
154 [[nodiscard]] uint32_t pixel_size() const { return m_pixel_size; }
155
156 /**
157 * @brief Atlas texture dimension (width == height == atlas_size).
158 */
159 [[nodiscard]] uint32_t atlas_size() const { return m_atlas_size; }
160
161 /**
162 * @brief Line advance in pixels for this atlas's pixel_size.
163 *
164 * Derived from FT_Face metrics: (ascender - descender) in 26.6 fixed-point,
165 * shifted right by 6. Returns pixel_size as a safe fallback if the face has
166 * not yet been sized (i.e. before the first get_or_rasterize() call).
167 */
168 [[nodiscard]] uint32_t line_height() const;
169
170 /// @brief Ascender in pixels for this atlas's pixel_size.
171 [[nodiscard]] uint32_t ascender() const;
172
173private:
174 const GlyphMetrics* get_or_rasterize_from(FontFace& face, FT_UInt glyph_index);
175 bool rasterize(FontFace& face, FT_UInt glyph_index);
176
177 /// @brief Double the atlas texture and re-rasterize every cached glyph into it.
178 void grow();
179
181 std::vector<FontFace*> m_fallbacks;
182 uint32_t m_pixel_size;
183 uint32_t m_atlas_size;
184
185 std::unique_ptr<Kakshya::TextureContainer> m_texture;
186
187 /// @brief Per-face glyph cache. Outer key is the producing FontFace,
188 /// since glyph indices are only unique within one face.
189 std::unordered_map<FontFace*, std::unordered_map<FT_UInt, GlyphMetrics>> m_cache;
190
191 uint32_t m_cursor_x { 0 };
192 uint32_t m_cursor_y { 0 };
193 uint32_t m_shelf_height { 0 };
194
195 bool m_dirty { false };
196};
197
198} // namespace MayaFlux::Portal::Text
SignalSourceContainer wrapping GPU texture data as addressable pixel bytes.
Owns a single FT_Face loaded from a file path.
Definition FontFace.hpp:24
FontFace & get_face()
Access the backing FontFace.
std::unordered_map< FontFace *, std::unordered_map< FT_UInt, GlyphMetrics > > m_cache
Per-face glyph cache.
GlyphAtlas(const GlyphAtlas &)=delete
GlyphAtlas(GlyphAtlas &&)=delete
GlyphAtlas & operator=(const GlyphAtlas &)=delete
std::unique_ptr< Kakshya::TextureContainer > m_texture
bool is_dirty() const
Returns true if at least one glyph was rasterized since the last call to clear_dirty().
void clear_dirty()
Clear the dirty flag after re-uploading the atlas to GPU.
GlyphAtlas & operator=(GlyphAtlas &&)=delete
std::vector< FontFace * > m_fallbacks
Kakshya::TextureContainer & texture()
uint32_t pixel_size() const
Pixel size passed at construction.
const FontFace & get_face() const
uint32_t atlas_size() const
Atlas texture dimension (width == height == atlas_size).
const Kakshya::TextureContainer & texture() const
The atlas texture as a TextureContainer (R8, atlas_size x atlas_size).
Rasterizes and packs glyphs from a FontFace into a TextureContainer.
FT_UInt glyph_index
Glyph index within face (from FT_Get_Char_Index).
int32_t bearing_x
Horizontal bearing in pixels (from FT_GlyphSlot).
float uv_x0
Left UV edge in atlas texture.
float uv_x1
Right UV edge in atlas texture.
const FontFace * face
Face that produced this glyph (primary or fallback).
uint32_t height
Glyph bitmap height in pixels.
int32_t advance_x
Horizontal advance in pixels (26.6 fixed-point >> 6).
uint32_t width
Glyph bitmap width in pixels.
float uv_y0
Top UV edge in atlas texture.
int32_t bearing_y
Vertical bearing in pixels (from FT_GlyphSlot).
float uv_y1
Bottom UV edge in atlas texture.
Per-glyph layout and UV data produced by GlyphAtlas.