MayaFlux 0.4.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
InkPress.cpp
Go to the documentation of this file.
1#include "InkPress.hpp"
2
3#include "TypeFaceFoundry.hpp"
4#include "TypeSetter.hpp"
5
9
10namespace MayaFlux::Portal::Text {
11
12namespace {
13
14 constexpr uint32_t k_grow_height_multiplier = 8;
15
16 /**
17 * @brief Pixel-raster result of a single composite pass.
18 *
19 * w is always the render bounds width (== the wrap boundary).
20 * h is the content height: the row count actually written.
21 * cursor_x is the pen x position after the last glyph, for cursor seeding.
22 * pixels is row-major RGBA8, stride == w * 4.
23 */
24 struct CompositeResult {
25 uint32_t h {};
26 uint32_t cursor_x {};
27 uint32_t cursor_y {};
28 std::vector<uint8_t> pixels;
29 };
30
31 /**
32 * @brief Resolve atlas pointer: use provided atlas or fall back to foundry default.
33 * @return Non-null pointer on success, nullptr if no atlas is available.
34 */
35 GlyphAtlas* resolve_atlas(GlyphAtlas* hint)
36 {
37 if (hint) {
38 return hint;
39 }
41 if (!def) {
43 "InkPress: no atlas available -- call set_default_font first");
44 }
45 return def;
46 }
47
48 /**
49 * @brief Lay out and rasterize text into a fresh pixel buffer.
50 *
51 * Always lays out from pen origin (0, 0). The pixel buffer width is
52 * exactly buf_w (== render bounds width). Content that wraps beyond
53 * buf_h is clipped by composite_into().
54 *
55 * @param text UTF-8 string.
56 * @param atlas Source atlas.
57 * @param color Glyph color.
58 * @param buf_w Buffer width and wrap boundary in pixels.
59 * @param buf_h Buffer height (allocated budget) in pixels.
60 * @return CompositeResult, or nullopt when no glyphs are produced.
61 */
62 std::optional<CompositeResult> composite(
63 std::string_view text,
64 GlyphAtlas& atlas,
65 glm::vec4 color,
66 uint32_t buf_w,
67 uint32_t buf_h,
68 float pen_y_start = 0.F)
69 {
70 const LayoutResult layout = lay_out(text, atlas, 0.F, pen_y_start, buf_w);
71 if (layout.quads.empty()) {
72 return std::nullopt;
73 }
74
75 const auto content_h = static_cast<uint32_t>(std::ceil(layout.final_pen_y))
76 + atlas.line_height();
77 const uint32_t dst_h = std::min(content_h, buf_h);
78
79 if (buf_w == 0 || dst_h == 0) {
80 return std::nullopt;
81 }
82
83 CompositeResult result;
84 result.h = content_h;
85 result.cursor_x = static_cast<uint32_t>(std::ceil(layout.final_pen_x));
86 result.cursor_y = static_cast<uint32_t>(std::ceil(layout.final_pen_y));
87 result.pixels.resize(static_cast<size_t>(buf_w) * dst_h * 4, 0);
88
89 rasterize_quads(layout.quads, atlas, color, result.pixels.data(), buf_w, dst_h);
90
91 return result;
92 }
93
94 /**
95 * @brief Allocate a TextBuffer from a CompositeResult.
96 *
97 * The GPU texture is always buf_w x budget_h. Content rows from result
98 * are copied in; remaining rows are zero (transparent).
99 *
100 * @param result Output of composite().
101 * @param buf_w Texture width == render bounds width.
102 * @param budget_h Allocated texture height (>= result.h).
103 * @param render_bounds Hard render bounds stored on the buffer.
104 * @param text Accumulated text string seeded on the buffer.
105 */
106 std::shared_ptr<Buffers::TextBuffer> make_buffer(
107 const CompositeResult& result,
108 uint32_t buf_w,
109 uint32_t budget_h,
110 glm::uvec2 render_bounds,
111 std::string_view text)
112 {
113 budget_h = std::max(budget_h, result.h);
114
115 const size_t budget_bytes = static_cast<size_t>(buf_w) * budget_h * 4;
116 std::vector<uint8_t> pixels(budget_bytes, 0);
117
118 const uint32_t copy_h = std::min(result.h,
119 static_cast<uint32_t>(result.pixels.size() / (buf_w * 4)));
120 for (uint32_t row = 0; row < copy_h; ++row) {
121 std::memcpy(
122 pixels.data() + static_cast<size_t>(row) * buf_w * 4,
123 result.pixels.data() + static_cast<size_t>(row) * buf_w * 4,
124 static_cast<size_t>(buf_w) * 4);
125 }
126
127 auto buf = std::make_shared<Buffers::TextBuffer>(
128 buf_w, budget_h,
130 pixels.data());
131
132 buf->set_budget(buf_w, budget_h);
133 buf->set_render_bounds(render_bounds.x, render_bounds.y);
134 buf->set_accumulated_text(text);
135 buf->get_cursor_x() = result.cursor_x;
136 buf->get_cursor_y() = result.cursor_y;
137
139 "InkPress: {}x{} content in {}x{} texture, render bounds {}x{}",
140 buf_w, result.h, buf_w, budget_h, render_bounds.x, render_bounds.y);
141
142 return buf;
143 }
144
145} // namespace
146
148 std::span<const GlyphQuad> quads,
149 GlyphAtlas& atlas,
150 glm::vec4 color,
151 uint8_t* dst,
152 uint32_t buf_w,
153 uint32_t buf_h)
154{
155 const uint8_t cr = static_cast<uint8_t>(std::clamp(color.r, 0.F, 1.F) * 255.F);
156 const uint8_t cg = static_cast<uint8_t>(std::clamp(color.g, 0.F, 1.F) * 255.F);
157 const uint8_t cb = static_cast<uint8_t>(std::clamp(color.b, 0.F, 1.F) * 255.F);
158 const uint8_t ca = static_cast<uint8_t>(std::clamp(color.a, 0.F, 1.F) * 255.F);
159
160 const Kakshya::TextureContainer& atlas_tex = atlas.texture();
161 const std::span<const uint8_t> atlas_pixels = atlas_tex.pixel_bytes(0);
162 const uint32_t atlas_size = atlas.atlas_size();
163
164 for (const auto& q : quads) {
165 const auto gx = static_cast<int32_t>(std::floor(q.x0));
166 const auto gy = static_cast<int32_t>(std::floor(q.y0));
167 const auto gw = static_cast<uint32_t>(std::ceil(q.x1 - q.x0));
168 const auto gh = static_cast<uint32_t>(std::ceil(q.y1 - q.y0));
169
170 const auto src_x = static_cast<uint32_t>(q.uv_x0 * static_cast<float>(atlas_size));
171 const auto src_y = static_cast<uint32_t>(q.uv_y0 * static_cast<float>(atlas_size));
172
173 for (uint32_t row = 0; row < gh; ++row) {
174 const int32_t dst_row = gy + static_cast<int32_t>(row);
175 if (dst_row < 0 || static_cast<uint32_t>(dst_row) >= buf_h) {
176 continue;
177 }
178
179 const uint8_t* src_row = atlas_pixels.data()
180 + static_cast<size_t>(src_y + row) * atlas_size + src_x;
181 uint8_t* dst_row_ptr = dst + static_cast<size_t>(dst_row) * buf_w * 4;
182
183 for (uint32_t col = 0; col < gw; ++col) {
184 const int32_t dst_col = gx + static_cast<int32_t>(col);
185 if (dst_col < 0 || static_cast<uint32_t>(dst_col) >= buf_w) {
186 continue;
187 }
188
189 const uint8_t coverage = src_row[col];
190 const auto alpha = static_cast<uint8_t>(
191 (static_cast<uint32_t>(coverage) * static_cast<uint32_t>(ca)) / 255U);
192
193 uint8_t* px = dst_row_ptr + static_cast<size_t>(dst_col) * 4;
194 px[0] = cr;
195 px[1] = cg;
196 px[2] = cb;
197 px[3] = alpha;
198 }
199 }
200 }
201}
202
204 const std::shared_ptr<Buffers::TextBuffer>& target,
205 std::span<const GlyphQuad> quads,
206 glm::vec4 color)
207{
208 if (!target) {
210 "ink_quads: target buffer is null");
211 return;
212 }
213
214 GlyphAtlas* atlas = resolve_atlas(nullptr);
215 if (!atlas) {
216 return;
217 }
218
219 const uint32_t buf_w = target->get_budget_width();
220 const uint32_t buf_h = target->get_budget_height();
221 const size_t buf_bytes = static_cast<size_t>(buf_w) * buf_h * 4;
222
223 thread_local std::vector<uint8_t> pixels;
224 pixels.assign(buf_bytes, 0);
225
226 rasterize_quads(quads, *atlas, color, pixels.data(), buf_w, buf_h);
227 target->set_pixel_data(pixels.data(), buf_bytes);
228}
229
230// =========================================================================
231// press
232// =========================================================================
233
234std::shared_ptr<Buffers::TextBuffer> press(
235 std::string_view text,
236 const PressParams& params)
237{
238 GlyphAtlas* atlas = resolve_atlas(params.atlas);
239 if (!atlas) {
240 return nullptr;
241 }
242
243 const uint32_t buf_w = params.render_bounds.x;
244
245 const auto result = composite(text, *atlas, params.color, buf_w, params.render_bounds.y,
246 static_cast<float>(atlas->line_height()));
247 if (!result) {
249 "press: no glyphs produced for '{}'", std::string(text));
250 return nullptr;
251 }
252
253 const uint32_t budget_h = params.budget_h > 0
254 ? std::max(params.budget_h, result->h)
255 : std::min(result->h * k_grow_height_multiplier, params.render_bounds.y);
256
257 return make_buffer(*result, buf_w, budget_h, params.render_bounds, text);
258}
259
260// =========================================================================
261// repress
262// =========================================================================
263
265 const std::shared_ptr<Buffers::TextBuffer>& target,
266 std::string_view text,
267 glm::vec4 color,
268 RedrawPolicy policy)
269{
270 if (!target) {
272 "repress: target buffer is null");
273 return false;
274 }
275
276 GlyphAtlas* atlas = resolve_atlas(nullptr);
277 if (!atlas) {
278 return false;
279 }
280
281 target->clear_accumulated_text();
282 target->reset_cursor();
283
284 const uint32_t buf_w = target->get_budget_width();
285 const uint32_t buf_h = target->get_budget_height();
286 const uint32_t bound_h = target->get_render_bounds_h();
287
288 const auto result = composite(text, *atlas, color, buf_w, bound_h,
289 static_cast<float>(atlas->line_height()));
290 if (!result) {
292 "repress: no glyphs produced for '{}'", std::string(text));
293 return false;
294 }
295
296 if (result->h > buf_h && policy == RedrawPolicy::Fit) {
297 const uint32_t new_h = std::min(result->h, bound_h);
298 target->resize_texture(buf_w, new_h);
299 target->set_budget(buf_w, new_h);
300 target->set_pixel_data(result->pixels.data(), result->pixels.size());
301 target->get_cursor_x() = result->cursor_x;
302 target->get_cursor_y() = result->cursor_y;
303 target->set_accumulated_text(text);
304
306 "repress(Fit): resized to {}x{}", buf_w, new_h);
307 return true;
308 }
309
310 const size_t buf_bytes = static_cast<size_t>(buf_w) * buf_h * 4;
311 std::vector<uint8_t> cleared(buf_bytes, 0);
312
313 const uint32_t copy_h = std::min(result->h, buf_h);
314 for (uint32_t row = 0; row < copy_h; ++row) {
315 std::memcpy(
316 cleared.data() + static_cast<size_t>(row) * buf_w * 4,
317 result->pixels.data() + static_cast<size_t>(row) * buf_w * 4,
318 static_cast<size_t>(buf_w) * 4);
319 }
320
321 target->set_pixel_data(cleared.data(), buf_bytes);
322 target->get_cursor_x() = result->cursor_x;
323 target->get_cursor_y() = result->cursor_y;
324 // target->get_cursor_y() = result->h;
325 target->set_accumulated_text(text);
326
328 "repress(Clip): '{}' -> {}x{} into {}x{} budget",
329 std::string(text), buf_w, result->h, buf_w, buf_h);
330
331 return true;
332}
333
334// =========================================================================
335// impress
336// =========================================================================
337
339 const std::shared_ptr<Buffers::TextBuffer>& target,
340 std::string_view text,
341 glm::vec4 color)
342{
343 if (!target) {
345 "impress: target buffer is null");
347 }
348
349 GlyphAtlas* atlas = resolve_atlas(nullptr);
350 if (!atlas) {
352 }
353
354 const uint32_t buf_w = target->get_budget_width();
355 const uint32_t buf_h = target->get_budget_height();
356 const uint32_t bound_h = target->get_render_bounds_h();
357 const auto pen_x = static_cast<float>(target->get_cursor_x());
358 const auto pen_y = static_cast<float>(target->get_cursor_y());
359
360 target->append_accumulated_text(text);
361
362 const LayoutResult layout = lay_out(text, *atlas, pen_x, pen_y, buf_w);
363
364 if (layout.quads.empty()) {
366 "impress: no glyphs produced for '{}'", std::string(text));
367 return ImpressResult::Ok;
368 }
369
370 const auto pen_y_ceil = static_cast<uint32_t>(std::ceil(layout.final_pen_y));
371 const uint32_t new_cursor_y = pen_y_ceil;
372 const uint32_t content_h = pen_y_ceil + atlas->line_height();
373
374 if (content_h > bound_h) {
376 }
377
378 if (content_h > buf_h) {
379 const uint32_t new_h = std::min(content_h * k_grow_height_multiplier, bound_h);
380 const std::string accumulated = target->get_accumulated_text();
381
382 const auto full = composite(accumulated, *atlas, color, buf_w, new_h);
383 if (!full) {
385 }
386
387 target->resize_texture(buf_w, new_h);
388 target->set_budget(buf_w, new_h);
389
390 const size_t buf_bytes = static_cast<size_t>(buf_w) * new_h * 4;
391 std::vector<uint8_t> pixels(buf_bytes, 0);
392 const uint32_t copy_h = std::min(full->h, new_h);
393 for (uint32_t row = 0; row < copy_h; ++row) {
394 std::memcpy(
395 pixels.data() + static_cast<size_t>(row) * buf_w * 4,
396 full->pixels.data() + static_cast<size_t>(row) * buf_w * 4,
397 static_cast<size_t>(buf_w) * 4);
398 }
399
400 target->set_pixel_data(pixels.data(), buf_bytes);
401 target->get_cursor_x() = full->cursor_x;
402 target->get_cursor_y() = full->h;
403 target->set_accumulated_text(accumulated);
404
406 "impress: vertical grow -> {}x{}", buf_w, new_h);
407
409 }
410
411 auto& pixel_data = target->get_pixel_data_mutable();
412 rasterize_quads(layout.quads, *atlas, color, pixel_data.data(), buf_w, buf_h);
413 target->mark_pixels_dirty();
414 target->get_cursor_x() = static_cast<uint32_t>(std::ceil(layout.final_pen_x));
415 target->get_cursor_y() = new_cursor_y;
416
418 "impress: '{}' at ({},{}) -> cursor ({},{})",
419 std::string(text),
420 static_cast<uint32_t>(pen_x), static_cast<uint32_t>(pen_y),
421 target->get_cursor_x(), target->get_cursor_y());
422
423 return ImpressResult::Ok;
424}
425
426} // namespace MayaFlux::Portal::Text
#define MF_ERROR(comp, ctx,...)
#define MF_WARN(comp, ctx,...)
#define MF_DEBUG(comp, ctx,...)
uint32_t h
Definition InkPress.cpp:25
uint32_t cursor_y
Definition InkPress.cpp:27
uint32_t cursor_x
Definition InkPress.cpp:26
double q
const std::vector< float > * pixels
std::optional< glm::vec3 > color
std::span< const uint8_t > pixel_bytes(uint32_t layer=0) const
Read-only byte-level view over the pixel buffer.
SignalSourceContainer wrapping GPU texture data as addressable pixel bytes.
uint32_t line_height() const
Line advance in pixels for this atlas's pixel_size.
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.
GlyphAtlas * get_default_glyph_atlas() const
Return the default GlyphAtlas, or nullptr if set_default_font() has not been called successfully.
@ API
API calls from external code.
@ Portal
High-level user-facing API layer.
ImpressResult impress(const std::shared_ptr< Buffers::TextBuffer > &target, std::string_view text, glm::vec4 color)
Append a UTF-8 string into an existing TextBuffer at the current cursor.
Definition InkPress.cpp:338
void rasterize_quads(std::span< const GlyphQuad > quads, GlyphAtlas &atlas, glm::vec4 color, uint8_t *dst, uint32_t buf_w, uint32_t buf_h)
Write glyph quads into a caller-provided RGBA8 pixel buffer.
Definition InkPress.cpp:147
RedrawPolicy
Policy controlling TextBuffer reuse behaviour in repress().
Definition InkPress.hpp:13
@ Fit
Replace content. Reallocate GPU texture if text exceeds existing budget.
ImpressResult
Result of an impress() call.
Definition InkPress.hpp:27
@ Overflow
Vertical budget exceeded. Texture reallocated. Previous content cleared.
@ Ok
Run composited at cursor. No GPU state change.
void ink_quads(const std::shared_ptr< Buffers::TextBuffer > &target, std::span< const GlyphQuad > quads, glm::vec4 color)
Rasterize a mutated quad span into an existing TextBuffer.
Definition InkPress.cpp:203
LayoutResult lay_out(std::string_view text, GlyphAtlas &atlas, float pen_x, float pen_y, uint32_t wrap_w)
Lay out a UTF-8 string into a sequence of screen-space quads.
Definition TypeSetter.cpp:9
bool repress(const std::shared_ptr< Buffers::TextBuffer > &target, std::string_view text, glm::vec4 color, RedrawPolicy policy)
Re-composite a UTF-8 string into an existing TextBuffer.
Definition InkPress.cpp:264
std::shared_ptr< Buffers::TextBuffer > press(std::string_view text, const PressParams &params)
Composite a UTF-8 string into a new TextBuffer.
Definition InkPress.cpp:234
std::vector< GlyphQuad > quads
Result of lay_out(), carrying the quads and the final pen position.
GlyphAtlas * atlas
Glyph atlas to use. Null selects the TypeFaceFoundry default at call time.
Definition InkPress.hpp:50
glm::vec4 color
RGBA color applied to all glyphs.
Definition InkPress.hpp:53
uint32_t budget_h
Initial vertical budget in pixels. Zero applies the grow heuristic.
Definition InkPress.hpp:60
glm::uvec2 render_bounds
Hard render bounds in pixels.
Definition InkPress.hpp:57
Construction parameters for press().
Definition InkPress.hpp:48