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
8
10
12
13namespace MayaFlux::Portal::Text {
14
15namespace {
16
17 constexpr uint32_t k_grow_height_multiplier = 8;
18
19 /**
20 * @brief Pixel-raster result of a single composite pass.
21 *
22 * w is always the render bounds width (== the wrap boundary).
23 * h is the content height: the row count actually written.
24 * cursor_x is the pen x position after the last glyph, for cursor seeding.
25 * pixels is row-major RGBA8, stride == w * 4.
26 */
27 struct CompositeResult {
28 uint32_t h {};
29 uint32_t cursor_x {};
30 uint32_t cursor_y {};
31 std::vector<uint8_t> pixels;
32 };
33
34 /**
35 * @brief Resolve atlas pointer: use provided atlas or fall back to foundry default.
36 * @return Non-null pointer on success, nullptr if no atlas is available.
37 */
38 GlyphAtlas* resolve_atlas(GlyphAtlas* hint)
39 {
40 if (hint) {
41 return hint;
42 }
44 if (!def) {
46 "InkPress: no atlas available -- call set_default_font first");
47 }
48 return def;
49 }
50
51 /**
52 * @brief Lay out and rasterize text into a fresh pixel buffer.
53 *
54 * Always lays out from pen origin (0, 0). The pixel buffer width is
55 * exactly buf_w (== render bounds width). Content that wraps beyond
56 * buf_h is clipped by composite_into().
57 *
58 * @param text UTF-8 string.
59 * @param atlas Source atlas.
60 * @param color Glyph color.
61 * @param buf_w Buffer width and wrap boundary in pixels.
62 * @param buf_h Buffer height (allocated budget) in pixels.
63 * @return CompositeResult, or nullopt when no glyphs are produced.
64 */
65 std::optional<CompositeResult> composite(
66 std::string_view text,
67 GlyphAtlas& atlas,
68 glm::vec4 color,
69 uint32_t buf_w,
70 uint32_t buf_h,
71 float pen_y_start = 0.F)
72 {
73 const LayoutResult layout = lay_out(text, atlas, 0.F, pen_y_start, buf_w);
74 if (layout.quads.empty()) {
75 return std::nullopt;
76 }
77
78 const auto content_h = static_cast<uint32_t>(std::ceil(layout.final_pen_y))
79 + atlas.line_height();
80 const uint32_t dst_h = std::min(content_h, buf_h);
81
82 if (buf_w == 0 || dst_h == 0) {
83 return std::nullopt;
84 }
85
86 CompositeResult result;
87 result.h = content_h;
88 result.cursor_x = static_cast<uint32_t>(std::ceil(layout.final_pen_x));
89 result.cursor_y = static_cast<uint32_t>(std::ceil(layout.final_pen_y));
90 result.pixels.resize(static_cast<size_t>(buf_w) * dst_h * 4, 0);
91
92 rasterize_quads(layout.quads, atlas, color, result.pixels.data(), buf_w, dst_h);
93
94 return result;
95 }
96
97 /**
98 * @brief Allocate a TextBuffer from a CompositeResult.
99 *
100 * The GPU texture is always buf_w x budget_h. Content rows from result
101 * are copied in; remaining rows are zero (transparent).
102 *
103 * @param result Output of composite().
104 * @param buf_w Texture width == render bounds width.
105 * @param budget_h Allocated texture height (>= result.h).
106 * @param render_bounds Hard render bounds stored on the buffer.
107 * @param text Accumulated text string seeded on the buffer.
108 */
109 std::shared_ptr<Buffers::TextBuffer> make_buffer(
110 const CompositeResult& result,
111 uint32_t buf_w,
112 uint32_t budget_h,
113 glm::uvec2 render_bounds,
114 std::string_view text)
115 {
116 budget_h = std::max(budget_h, result.h);
117
118 const size_t budget_bytes = static_cast<size_t>(buf_w) * budget_h * 4;
119 std::vector<uint8_t> pixels(budget_bytes, 0);
120
121 const uint32_t copy_h = std::min(result.h,
122 static_cast<uint32_t>(result.pixels.size() / (static_cast<size_t>(buf_w) * 4)));
123 for (uint32_t row = 0; row < copy_h; ++row) {
124 std::memcpy(
125 pixels.data() + static_cast<size_t>(row) * buf_w * 4,
126 result.pixels.data() + static_cast<size_t>(row) * buf_w * 4,
127 static_cast<size_t>(buf_w) * 4);
128 }
129
130 auto buf = std::make_shared<Buffers::TextBuffer>(
131 buf_w, budget_h,
133 pixels.data());
134
135 buf->set_budget(buf_w, budget_h);
136 buf->set_render_bounds(render_bounds.x, render_bounds.y);
137 buf->set_accumulated_text(text);
138 buf->get_cursor_x() = result.cursor_x;
139 buf->get_cursor_y() = result.cursor_y;
140
142 "InkPress: {}x{} content in {}x{} texture, render bounds {}x{}",
143 buf_w, result.h, buf_w, budget_h, render_bounds.x, render_bounds.y);
144
145 return buf;
146 }
147
148} // namespace
149
151 std::span<const GlyphQuad> quads,
152 GlyphAtlas& atlas,
153 glm::vec4 color,
154 uint8_t* dst,
155 uint32_t buf_w,
156 uint32_t buf_h)
157{
158 const uint8_t cr = static_cast<uint8_t>(std::clamp(color.r, 0.F, 1.F) * 255.F);
159 const uint8_t cg = static_cast<uint8_t>(std::clamp(color.g, 0.F, 1.F) * 255.F);
160 const uint8_t cb = static_cast<uint8_t>(std::clamp(color.b, 0.F, 1.F) * 255.F);
161 const uint8_t ca = static_cast<uint8_t>(std::clamp(color.a, 0.F, 1.F) * 255.F);
162
163 const Kakshya::TextureContainer& atlas_tex = atlas.texture();
164 const std::span<const uint8_t> atlas_pixels = atlas_tex.pixel_bytes(0);
165 const uint32_t atlas_size = atlas.atlas_size();
166
167 for (const auto& q : quads) {
168 const auto gx = static_cast<int32_t>(std::floor(q.x0));
169 const auto gy = static_cast<int32_t>(std::floor(q.y0));
170 const auto gw = static_cast<uint32_t>(std::ceil(q.x1 - q.x0));
171 const auto gh = static_cast<uint32_t>(std::ceil(q.y1 - q.y0));
172
173 const auto src_x = static_cast<uint32_t>(q.uv_x0 * static_cast<float>(atlas_size));
174 const auto src_y = static_cast<uint32_t>(q.uv_y0 * static_cast<float>(atlas_size));
175
176 for (uint32_t row = 0; row < gh; ++row) {
177 const int32_t dst_row = gy + static_cast<int32_t>(row);
178 if (dst_row < 0 || static_cast<uint32_t>(dst_row) >= buf_h) {
179 continue;
180 }
181
182 const uint8_t* src_row = atlas_pixels.data()
183 + static_cast<size_t>(src_y + row) * atlas_size + src_x;
184 uint8_t* dst_row_ptr = dst + static_cast<size_t>(dst_row) * buf_w * 4;
185
186 for (uint32_t col = 0; col < gw; ++col) {
187 const int32_t dst_col = gx + static_cast<int32_t>(col);
188 if (dst_col < 0 || static_cast<uint32_t>(dst_col) >= buf_w) {
189 continue;
190 }
191
192 const uint8_t coverage = src_row[col];
193 const auto alpha = static_cast<uint8_t>(
194 (static_cast<uint32_t>(coverage) * static_cast<uint32_t>(ca)) / 255U);
195
196 uint8_t* px = dst_row_ptr + static_cast<size_t>(dst_col) * 4;
197 px[0] = cr;
198 px[1] = cg;
199 px[2] = cb;
200 px[3] = alpha;
201 }
202 }
203 }
204}
205
207 const std::shared_ptr<Buffers::TextBuffer>& target,
208 std::span<const GlyphQuad> quads,
209 glm::vec4 color)
210{
211 if (!target) {
213 "ink_quads: target buffer is null");
214 return;
215 }
216
217 GlyphAtlas* atlas = resolve_atlas(nullptr);
218 if (!atlas) {
219 return;
220 }
221
222 const uint32_t buf_w = target->get_budget_width();
223 const uint32_t buf_h = target->get_budget_height();
224 const size_t buf_bytes = static_cast<size_t>(buf_w) * buf_h * 4;
225
226 thread_local std::vector<uint8_t> pixels;
227 pixels.assign(buf_bytes, 0);
228
229 rasterize_quads(quads, *atlas, color, pixels.data(), buf_w, buf_h);
230 target->set_pixel_data(pixels.data(), buf_bytes);
231}
232
233// =========================================================================
234// press
235// =========================================================================
236
237std::shared_ptr<Buffers::TextBuffer> press(
238 std::string_view text,
239 const PressParams& params)
240{
241 GlyphAtlas* atlas = resolve_atlas(params.atlas);
242 if (!atlas) {
243 return nullptr;
244 }
245
246 const uint32_t buf_w = params.render_bounds.x;
247
248 const auto result = composite(text, *atlas, params.color, buf_w, params.render_bounds.y,
249 static_cast<float>(atlas->line_height()));
250 if (!result) {
252 "press: no glyphs produced for '{}'", std::string(text));
253 return nullptr;
254 }
255
256 const uint32_t budget_h = params.budget_h > 0
257 ? std::max(params.budget_h, result->h)
258 : std::min(result->h * k_grow_height_multiplier, params.render_bounds.y);
259
260 return make_buffer(*result, buf_w, budget_h, params.render_bounds, text);
261}
262
263std::shared_ptr<Core::VKImage> press(
264 std::string_view text,
265 glm::uvec2 render_bounds,
266 const PressParams& params)
267{
268 GlyphAtlas* atlas = resolve_atlas(params.atlas);
269 if (!atlas)
270 return nullptr;
271
272 const uint32_t buf_w = render_bounds.x;
273
274 const uint32_t composite_h = std::max(render_bounds.y,
275 static_cast<uint32_t>(atlas->line_height()));
276
277 const auto result = composite(text, *atlas, params.color, buf_w, composite_h,
278 static_cast<float>(atlas->ascender()));
279
280 if (!result) {
282 "press(AsTexture): no glyphs produced for '{}'", std::string(text));
283 return nullptr;
284 }
285
286 const uint32_t budget_h = composite_h;
287
288 const size_t budget_bytes = static_cast<size_t>(buf_w) * budget_h * 4;
289 std::vector<uint8_t> pixels(budget_bytes, 0);
290
291 const auto copy_h = static_cast<uint32_t>(
292 result->pixels.size() / (static_cast<size_t>(buf_w) * 4));
293
294 for (uint32_t row = 0; row < copy_h; ++row) {
295 std::memcpy(
296 pixels.data() + static_cast<size_t>(row) * buf_w * 4,
297 result->pixels.data() + static_cast<size_t>(row) * buf_w * 4,
298 static_cast<size_t>(buf_w) * 4);
299 }
300
302 auto image = loom.create_2d(buf_w, budget_h, Portal::Graphics::ImageFormat::RGBA8,
303 pixels.data(), 1);
304
306 "press(AsTexture): {}x{} content in {}x{} texture",
307 buf_w, result->h, buf_w, budget_h);
308
309 return image;
310}
311
312// =========================================================================
313// repress
314// =========================================================================
315
317 const std::shared_ptr<Buffers::TextBuffer>& target,
318 std::string_view text,
319 glm::vec4 color,
320 RedrawPolicy policy)
321{
322 if (!target) {
324 "repress: target buffer is null");
325 return false;
326 }
327
328 GlyphAtlas* atlas = resolve_atlas(nullptr);
329 if (!atlas) {
330 return false;
331 }
332
333 target->clear_accumulated_text();
334 target->reset_cursor();
335
336 const uint32_t buf_w = target->get_budget_width();
337 const uint32_t buf_h = target->get_budget_height();
338 const uint32_t bound_h = target->get_render_bounds_h();
339
340 const auto result = composite(text, *atlas, color, buf_w, bound_h,
341 static_cast<float>(atlas->line_height()));
342 if (!result) {
344 "repress: no glyphs produced for '{}'", std::string(text));
345 return false;
346 }
347
348 if (result->h > buf_h && policy == RedrawPolicy::Fit) {
349 const uint32_t new_h = std::min(result->h, bound_h);
350 target->resize_texture(buf_w, new_h);
351 target->set_budget(buf_w, new_h);
352 target->set_pixel_data(result->pixels.data(), result->pixels.size());
353 target->get_cursor_x() = result->cursor_x;
354 target->get_cursor_y() = result->cursor_y;
355 target->set_accumulated_text(text);
356
358 "repress(Fit): resized to {}x{}", buf_w, new_h);
359 return true;
360 }
361
362 const size_t buf_bytes = static_cast<size_t>(buf_w) * buf_h * 4;
363 std::vector<uint8_t> cleared(buf_bytes, 0);
364
365 const uint32_t copy_h = std::min(result->h, buf_h);
366 for (uint32_t row = 0; row < copy_h; ++row) {
367 std::memcpy(
368 cleared.data() + static_cast<size_t>(row) * buf_w * 4,
369 result->pixels.data() + static_cast<size_t>(row) * buf_w * 4,
370 static_cast<size_t>(buf_w) * 4);
371 }
372
373 target->set_pixel_data(cleared.data(), buf_bytes);
374 target->get_cursor_x() = result->cursor_x;
375 target->get_cursor_y() = result->cursor_y;
376 // target->get_cursor_y() = result->h;
377 target->set_accumulated_text(text);
378
380 "repress(Clip): '{}' -> {}x{} into {}x{} budget",
381 std::string(text), buf_w, result->h, buf_w, buf_h);
382
383 return true;
384}
385
387 std::shared_ptr<Core::VKImage>& target,
388 std::string_view text,
389 const PressParams& params,
390 const std::shared_ptr<Buffers::VKBuffer>& staging)
391{
392 if (!target) {
394 "repress(VKImage): target is null");
395 return false;
396 }
397
398 GlyphAtlas* atlas = resolve_atlas(params.atlas);
399 if (!atlas)
400 return false;
401
402 const uint32_t buf_w = target->get_width();
403 const uint32_t buf_h = target->get_height();
404
405 const uint32_t composite_h = std::max(buf_h,
406 static_cast<uint32_t>(atlas->line_height()));
407
408 const auto result = composite(text, *atlas, params.color, buf_w, composite_h,
409 static_cast<float>(atlas->ascender()));
410 if (!result) {
412 "repress(VKImage): no glyphs produced for '{}'", std::string(text));
413 return false;
414 }
415
417
418 const uint32_t new_h = composite_h;
419 const bool needs_realloc = composite_h != buf_h || buf_w != target->get_width();
420
421 const size_t buf_bytes = static_cast<size_t>(buf_w) * new_h * 4;
422 std::vector<uint8_t> pixels(buf_bytes, 0);
423
424 const uint32_t copy_h = std::min(result->h, new_h);
425 for (uint32_t row = 0; row < copy_h; ++row) {
426 std::memcpy(
427 pixels.data() + static_cast<size_t>(row) * buf_w * 4,
428 result->pixels.data() + static_cast<size_t>(row) * buf_w * 4,
429 static_cast<size_t>(buf_w) * 4);
430 }
431
432 if (needs_realloc) {
433 target = loom.create_2d(buf_w, new_h, Portal::Graphics::ImageFormat::RGBA8,
434 nullptr, 1);
435 if (!target)
436 return false;
437 if (staging) {
438 loom.upload_data(target, pixels.data(), buf_bytes, staging, true);
439 } else {
440 loom.upload_data(target, pixels.data(), buf_bytes);
441 }
442
444 "repress(VKImage): reallocated {}x{}", buf_w, new_h);
445 return true;
446 }
447
448 if (staging) {
449 loom.upload_data(target, pixels.data(), buf_bytes, staging);
450 } else {
451 loom.upload_data(target, pixels.data(), buf_bytes);
452 }
453
455 "repress(VKImage): updated {}x{} in-place", buf_w, buf_h);
456
457 return true;
458}
459
460// =========================================================================
461// impress
462// =========================================================================
463
465 const std::shared_ptr<Buffers::TextBuffer>& target,
466 std::string_view text,
467 glm::vec4 color)
468{
469 if (!target) {
471 "impress: target buffer is null");
473 }
474
475 GlyphAtlas* atlas = resolve_atlas(nullptr);
476 if (!atlas) {
478 }
479
480 const uint32_t buf_w = target->get_budget_width();
481 const uint32_t buf_h = target->get_budget_height();
482 const uint32_t bound_h = target->get_render_bounds_h();
483 const auto pen_x = static_cast<float>(target->get_cursor_x());
484 const auto pen_y = static_cast<float>(target->get_cursor_y());
485
486 target->append_accumulated_text(text);
487
488 const LayoutResult layout = lay_out(text, *atlas, pen_x, pen_y, buf_w);
489
490 if (layout.quads.empty()) {
492 "impress: no glyphs produced for '{}'", std::string(text));
493 return ImpressResult::Ok;
494 }
495
496 const auto pen_y_ceil = static_cast<uint32_t>(std::ceil(layout.final_pen_y));
497 const uint32_t new_cursor_y = pen_y_ceil;
498 const uint32_t content_h = pen_y_ceil + atlas->line_height();
499
500 if (content_h > bound_h) {
502 }
503
504 if (content_h > buf_h) {
505 const uint32_t new_h = std::min(content_h * k_grow_height_multiplier, bound_h);
506 const std::string accumulated = target->get_accumulated_text();
507
508 const auto full = composite(accumulated, *atlas, color, buf_w, new_h);
509 if (!full) {
511 }
512
513 target->resize_texture(buf_w, new_h);
514 target->set_budget(buf_w, new_h);
515
516 const size_t buf_bytes = static_cast<size_t>(buf_w) * new_h * 4;
517 std::vector<uint8_t> pixels(buf_bytes, 0);
518 const uint32_t copy_h = std::min(full->h, new_h);
519 for (uint32_t row = 0; row < copy_h; ++row) {
520 std::memcpy(
521 pixels.data() + static_cast<size_t>(row) * buf_w * 4,
522 full->pixels.data() + static_cast<size_t>(row) * buf_w * 4,
523 static_cast<size_t>(buf_w) * 4);
524 }
525
526 target->set_pixel_data(pixels.data(), buf_bytes);
527 target->get_cursor_x() = full->cursor_x;
528 target->get_cursor_y() = full->h;
529 target->set_accumulated_text(accumulated);
530
532 "impress: vertical grow -> {}x{}", buf_w, new_h);
533
535 }
536
537 auto& pixel_data = target->get_pixel_data_mutable();
538 rasterize_quads(layout.quads, *atlas, color, pixel_data.data(), buf_w, buf_h);
539 target->mark_pixels_dirty();
540 target->get_cursor_x() = static_cast<uint32_t>(std::ceil(layout.final_pen_x));
541 target->get_cursor_y() = new_cursor_y;
542
544 "impress: '{}' at ({},{}) -> cursor ({},{})",
545 std::string(text),
546 static_cast<uint32_t>(pen_x), static_cast<uint32_t>(pen_y),
547 target->get_cursor_x(), target->get_cursor_y());
548
549 return ImpressResult::Ok;
550}
551
552} // namespace MayaFlux::Portal::Text
#define MF_ERROR(comp, ctx,...)
#define MF_WARN(comp, ctx,...)
#define MF_DEBUG(comp, ctx,...)
IO::ImageData image
Definition Decoder.cpp:57
const std::vector< float > * pixels
Definition Decoder.cpp:58
uint32_t h
Definition InkPress.cpp:28
uint32_t cursor_y
Definition InkPress.cpp:30
uint32_t cursor_x
Definition InkPress.cpp:29
double q
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 ascender() const
Ascender 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.
MAYAFLUX_API TextureLoom & get_texture_manager()
Get the global texture manager instance.
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:464
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:150
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:206
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:316
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:237
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