ascii-chat
Binary v0.15.83-53-gb711d8720
Library lib/v0.3.2-701-gb711d8720
Video chat in your terminal
Loading...
Searching...
No Matches
lib/video/ascii/render.c
Go to the documentation of this file.
1
4
5/* Storage capacities in elements; text buffers include the terminating NUL. */
6#define VIDEO_ASCII_RENDER_SEQUENCE_BUFFER_SIZE 16
7#define ASCII_TEMPORAL_MOTION_LIMIT 8
8#define ASCII_TEMPORAL_MAX_INTERVAL_NS 250000000ULL
9#define ASCII_TEMPORAL_COLOR_TIME_NS 50000000ULL
10
11#include <ascii-chat/otel.h>
12#include <ascii-chat/common.h>
14#include <libplatform/util/string.h>
20#include <string.h>
21#include <limits.h>
23
24static _Thread_local outbuf_t *g_output_target;
25#define ASCII_RENDER_WORKSPACE_POOL_SIZE 4
26static _Thread_local uint32_t g_scratch_workspace[ASCII_RENDER_WORKSPACE_POOL_SIZE]
28static _Thread_local bool g_scratch_workspace_busy[ASCII_RENDER_WORKSPACE_POOL_SIZE];
29
30static uint32_t *acquire_scratch(uint32_t *inline_scratch, size_t bytes) {
32 return inline_scratch;
33 if (bytes > sizeof(g_scratch_workspace[0]))
34 return NULL;
35 for (size_t i = 0; i < ASCII_RENDER_WORKSPACE_POOL_SIZE; ++i) {
36 if (!g_scratch_workspace_busy[i]) {
37 g_scratch_workspace_busy[i] = true;
38 return g_scratch_workspace[i];
39 }
40 }
41 return NULL;
42}
43
44static void release_scratch(uint32_t *scratch) {
45 for (size_t i = 0; i < ASCII_RENDER_WORKSPACE_POOL_SIZE; ++i) {
46 if (scratch == g_scratch_workspace[i]) {
47 g_scratch_workspace_busy[i] = false;
48 return;
49 }
50 }
51}
52static _Thread_local ascii_stabilizer_t *g_stabilizer_target;
53
55 ascii_stabilizer_t *previous = g_stabilizer_target;
56 g_stabilizer_target = state;
57 return previous;
58}
59
61 if (!state)
62 return;
63 SAFE_FREE(state->cells);
64 memset(state, 0, sizeof(*state));
65}
66
68 uint64_t configuration) {
69 if (!state || !width || !height || width > SIZE_MAX / height ||
70 width * height > SIZE_MAX / sizeof(ascii_temporal_cell_t))
71 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid stabilization viewport");
72 if (state->cells && state->width == width && state->height == height && state->configuration == configuration)
73 return ASCIICHAT_OK;
75 if (!cells)
76 return SET_ERRNO(ERROR_MEMORY, "Cannot allocate stabilization history");
77 SAFE_FREE(state->cells);
78 state->cells = cells;
79 state->width = width;
80 state->height = height;
81 state->configuration = configuration;
82 state->last_timestamp_ns = 0;
83 state->elapsed_ns = UINT64_MAX;
84 return ASCIICHAT_OK;
85}
86
87static uint32_t temporal_blend_color(uint32_t previous, uint32_t current, unsigned weight) {
88 uint32_t result = 0;
89 for (unsigned shift = 0; shift <= 16; shift += 8) {
90 unsigned old_channel = (previous >> shift) & 255;
91 unsigned new_channel = (current >> shift) & 255;
92 result |= (uint32_t)((old_channel * (256 - weight) + new_channel * weight + 128) / 256) << shift;
93 }
94 return result;
95}
96
98 uint64_t candidate_error, uint64_t previous_error, uint32_t color, uint64_t elapsed_ns,
99 uint32_t *stabilized_color) {
100 unsigned motion = 0;
101 if (cell->valid) {
102 for (size_t i = 0; i < 16; ++i)
103 motion += (unsigned)abs((int)samples[i] - (int)cell->samples[i]);
104 motion /= 16;
105 }
106 uint32_t result = candidate;
107 uint32_t output_color = color;
108 if (cell->valid && motion <= ASCII_TEMPORAL_MOTION_LIMIT && elapsed_ns <= ASCII_TEMPORAL_MAX_INTERVAL_NS) {
109 uint64_t margin = candidate_error / 8 + 128;
110 if (previous_error != UINT64_MAX &&
111 (previous_error <= candidate_error || previous_error - candidate_error <= margin))
112 result = cell->glyph;
113 // Weight depends on elapsed time, avoiding a different response at each FPS.
114 unsigned weight = (unsigned)(elapsed_ns * 256 / (elapsed_ns + ASCII_TEMPORAL_COLOR_TIME_NS));
115 output_color = temporal_blend_color(cell->color, color, weight);
116 }
117 memcpy(cell->samples, samples, sizeof(cell->samples));
118 cell->glyph = result;
119 cell->color = output_color;
120 cell->valid = true;
121 if (stabilized_color)
122 *stabilized_color = output_color;
123 return result;
124}
125
127 outbuf_t *previous = g_output_target;
128 g_output_target = target;
129 return previous;
130}
131
132static void prepare_output(ascii_frame_t *frame, size_t capacity, outbuf_t *target) {
133 frame->output_target = target;
134 if (target) {
135 if (target->cap < capacity) {
136 target->buf = SAFE_REALLOC(target->buf, capacity, char *);
137 target->cap = capacity;
138 }
139 frame->output = *target;
140 frame->output.len = 0;
141 } else {
142 frame->output.cap = capacity;
143 frame->output.buf = SAFE_MALLOC(capacity, char *);
144 }
145}
146
147static void emit_color(outbuf_t *out, ascii_color_t mode, bool background, int value) {
148 if (mode == ASCII_COLOR_TRUE) {
149 if (background)
150 emit_set_truecolor_bg(out, (uint8_t)(value >> 16), (uint8_t)(value >> 8), (uint8_t)value);
151 else
152 emit_set_truecolor_fg(out, (uint8_t)(value >> 16), (uint8_t)(value >> 8), (uint8_t)value);
153 } else if (mode == ASCII_COLOR_256) {
154 if (background)
156 else
158 } else if (mode == ASCII_COLOR_16) {
160 char *end = background ? append_16color_bg(sequence, (uint8_t)value) : append_16color_fg(sequence, (uint8_t)value);
161 ob_write(out, sequence, (size_t)(end - sequence));
162 }
163}
164
165static size_t glyph_bytes(uint32_t key) {
166 uint8_t first;
167 memcpy(&first, &key, 1);
168 return first < 128 ? 1 : (first & 0xe0) == 0xc0 ? 2 : (first & 0xf0) == 0xe0 ? 3 : 4;
169}
170
171static void emit_prepared_row(outbuf_t *out, ascii_color_t mode, const simd_cells_t *ops, const uint32_t *glyph,
172 const uint32_t *fg, const uint32_t *bg, size_t width) {
173 if (mode == ASCII_COLOR_NONE && ops->emit_mono) {
174 ops->emit_mono(out, glyph, width);
175 return;
176 }
177 uint32_t current_fg = UINT32_MAX, current_bg = UINT32_MAX;
178 for (size_t x = 0; x < width;) {
179 size_t run = ops->run(glyph + x, fg + x, bg + x, width - x);
180 if (fg[x] == UINT32_MAX && bg[x] == UINT32_MAX && (current_fg != UINT32_MAX || current_bg != UINT32_MAX)) {
182 current_fg = current_bg = UINT32_MAX;
183 }
184 if (fg[x] != current_fg) {
185 emit_color(out, mode, false, (int)fg[x]);
186 current_fg = fg[x];
187 }
188 if (bg[x] != current_bg) {
189 emit_color(out, mode, true, (int)bg[x]);
190 current_bg = bg[x];
191 }
192 emit_glyph_run(out, &glyph[x], glyph_bytes(glyph[x]), (uint32_t)run);
193 x += run;
194 }
195}
196
197char *ascii_render(const image_t *image, const char *palette, ascii_color_t mode, bool background, bool dither,
198 const simd_cells_t *ops) {
199 if (!ops || mode < ASCII_COLOR_NONE || mode > ASCII_COLOR_TRUE) {
200 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid rendering backend or color mode");
201 return NULL;
202 }
203 if (!dither) {
204#if SIMD_SUPPORT_SSE2
205 if (ops == &simd_cells_sse2)
206 return render_ascii_cells_sse2(image, palette, mode, background);
207#endif
208#if SIMD_SUPPORT_SSSE3
209 if (ops == &simd_cells_ssse3)
210 return render_ascii_cells_ssse3(image, palette, mode, background);
211#endif
212#if SIMD_SUPPORT_AVX2
213 if (ops == &simd_cells_avx2)
214 return render_ascii_cells_avx2(image, palette, mode, background);
215#endif
216#if SIMD_SUPPORT_NEON
217 if (ops == &simd_cells_neon)
218 return render_ascii_cells_neon(image, palette, mode, background);
219#endif
220#if SIMD_SUPPORT_SVE
221 if (ops == &simd_cells_sve)
222 return render_ascii_cells_sve(image, palette, mode, background);
223#endif
224 }
225 ascii_frame_t frame;
226 if (ascii_frame_init(&frame, image, palette) != ASCIICHAT_OK)
227 return NULL;
228 rgb_error_t *errors = NULL;
229 if (mode == ASCII_COLOR_16 && dither)
230 errors = SAFE_CALLOC((size_t)image->w * (size_t)image->h, sizeof(rgb_error_t), rgb_error_t *);
231 frame.dithered = errors != NULL;
232 for (int y = 0; y < image->h; ++y) {
233 const uint8_t *source = (const uint8_t *)(image->pixels + (size_t)y * frame.width);
234 ops->luminance(source, frame.luma, frame.width);
235 ops->pack(source, frame.packed, frame.width);
236 ops->lookup(frame.luma, frame.table, frame.glyph, frame.width);
237 if (mode == ASCII_COLOR_16 || mode == ASCII_COLOR_256) {
238 if (errors) {
239 // Floyd-Steinberg error propagation is ordered; the other stages remain SIMD.
240 for (int x = 0; x < image->w; ++x) {
241 uint32_t pixel = frame.packed[x];
242 frame.index[x] = rgb_to_16color_dithered((int)(pixel >> 16), (int)((pixel >> 8) & 255), (int)(pixel & 255), x,
243 y, image->w, image->h, errors);
244 }
245 } else
246 ops->quantize(frame.packed, frame.index, frame.width, mode == ASCII_COLOR_16 ? 16 : 256);
247 }
248 ascii_frame_emit_row(&frame, mode, background, y + 1 < image->h, ops);
249 }
250 SAFE_FREE(errors);
251 return ascii_frame_finish(&frame);
252}
253
254char *ascii_render_halfblocks(const uint8_t *rgb, int width, int height, int stride, ascii_color_t mode,
255 const simd_cells_t *ops) {
256#if SIMD_SUPPORT_SSE2
257 if (ops == &simd_cells_sse2)
258 return render_halfblocks_sse2(rgb, width, height, stride, mode);
259#endif
260#if SIMD_SUPPORT_SSSE3
261 if (ops == &simd_cells_ssse3)
262 return render_halfblocks_ssse3(rgb, width, height, stride, mode);
263#endif
264#if SIMD_SUPPORT_AVX2
265 if (ops == &simd_cells_avx2)
266 return render_halfblocks_avx2(rgb, width, height, stride, mode);
267#endif
268#if SIMD_SUPPORT_NEON
269 if (ops == &simd_cells_neon)
270 return render_halfblocks_neon(rgb, width, height, stride, mode);
271#endif
272#if SIMD_SUPPORT_SVE
273 if (ops == &simd_cells_sve)
274 return render_halfblocks_sve(rgb, width, height, stride, mode);
275#endif
276
277 if (width <= 0 || height <= 0)
278 return platform_strdup("");
279 if (!ops) {
280 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid half-block backend");
281 return NULL;
282 }
283 ascii_frame_t frame;
284 if (ascii_half_frame_init(&frame, rgb, width, height, &stride, mode) != ASCIICHAT_OK)
285 return NULL;
286 for (size_t y = 0; y < (size_t)height; y += 2) {
287 const uint8_t *top = rgb + y * (size_t)stride;
288 const uint8_t *bottom = y + 1 < (size_t)height ? top + stride : top;
289 ops->pack(top, frame.packed, frame.width);
290 ops->pack(bottom, frame.index, frame.width);
291 if (mode == ASCII_COLOR_TRUE) {
292 memcpy(frame.fg, frame.packed, frame.width * sizeof(uint32_t));
293 memcpy(frame.bg, frame.index, frame.width * sizeof(uint32_t));
294 } else if (mode != ASCII_COLOR_NONE) {
295 ops->quantize(frame.packed, frame.fg, frame.width, mode == ASCII_COLOR_16 ? 16 : 256);
296 ops->quantize(frame.index, frame.bg, frame.width, mode == ASCII_COLOR_16 ? 16 : 256);
297 }
298 ops->halfblock(frame.packed, frame.index, frame.glyph, frame.fg, frame.bg, frame.width, (unsigned)mode,
299 frame.table);
300 ascii_frame_emit_halfrow(&frame, mode, y + 2 < (size_t)height, ops);
301 }
302 return ascii_frame_finish(&frame);
303}
304
305asciichat_error_t ascii_frame_init(ascii_frame_t *frame, const image_t *image, const char *palette) {
306 ASCII_OTEL_SCOPE(ASCII_FRAME_INIT);
307 outbuf_t *target = g_output_target;
308 memset(frame, 0, sizeof(*frame));
309 if (!image || !image->pixels || image->w <= 0 || image->h <= 0 || !palette || !palette[0])
310 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid image or palette");
311 size_t bytes, pixels, capacity, rows;
312 if (checked_size_mul((size_t)image->w, (size_t)image->h, &pixels) != ASCIICHAT_OK ||
313 checked_size_mul(pixels, 48u, &capacity) != ASCIICHAT_OK ||
314 checked_size_mul((size_t)image->h, 5u, &rows) != ASCIICHAT_OK ||
315 checked_size_add(capacity, rows, &capacity) != ASCIICHAT_OK ||
316 checked_size_add(capacity, 1u, &capacity) != ASCIICHAT_OK ||
317 checked_size_mul((size_t)image->w, 6 * sizeof(uint32_t), &bytes) != ASCIICHAT_OK)
318 return ERROR_INVALID_PARAM;
320 if (!cache)
321 return ERROR_INVALID_PARAM;
322 prepare_output(frame, capacity, target);
323 frame->width = (size_t)image->w;
324 if (g_stabilizer_target && g_stabilizer_target->cells && g_stabilizer_target->width == (size_t)image->w &&
325 g_stabilizer_target->height == (size_t)image->h)
326 frame->stabilizer = g_stabilizer_target;
327 for (size_t i = 0; i < 64; ++i)
328 memcpy(frame->table + i, cache->cache64[i].utf8_bytes, cache->cache64[i].byte_len);
329 frame->scratch = acquire_scratch(frame->inline_scratch, bytes);
330 if (!frame->scratch)
331 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate ASCII scratch workspace");
332 frame->packed = frame->scratch;
333 frame->index = frame->packed + frame->width;
334 frame->glyph = frame->index + frame->width;
335 frame->fg = frame->glyph + frame->width;
336 frame->bg = frame->fg + frame->width;
337 frame->luma = (uint8_t *)(frame->bg + frame->width);
338 return ASCIICHAT_OK;
339}
340void ascii_frame_emit_row(ascii_frame_t *frame, ascii_color_t mode, bool background, bool more_rows,
341 const simd_cells_t *ops) {
342 if (frame->stabilizer && !frame->temporal_preselected) {
343 const uint64_t elapsed = frame->stabilizer->elapsed_ns;
344 const bool recent = elapsed <= ASCII_TEMPORAL_MAX_INTERVAL_NS;
345 const unsigned weight = recent ? (unsigned)(elapsed * 256 / (elapsed + ASCII_TEMPORAL_COLOR_TIME_NS)) : 256;
346 for (size_t x = 0; x < frame->width; ++x) {
347 ascii_temporal_cell_t *cell = &frame->stabilizer->cells[frame->row * frame->width + x];
348 uint8_t luma = frame->luma[x];
349 uint32_t glyph = frame->glyph[x];
350 uint32_t color = frame->packed[x];
351 if (cell->valid && recent && abs((int)luma - (int)cell->samples[0]) <= ASCII_TEMPORAL_MOTION_LIMIT) {
352 if (cell->glyph != glyph) {
353 // Only nearby brightness bins can win the hysteresis test. Searching
354 // outward avoids scanning the entire 64-entry palette for every cell.
355 unsigned bin = luma >> 2;
356 int best_delta = (int)luma - ((int)bin * 4 + 2);
357 uint64_t best_error = (uint64_t)(best_delta * best_delta);
358 uint64_t margin = best_error / 8 + 128;
359 for (unsigned distance = 1; distance < 64; ++distance) {
360 bool found = false;
361 for (int direction = -1; direction <= 1; direction += 2) {
362 int index = (int)bin + direction * (int)distance;
363 if (index < 0 || index >= 64 || frame->table[index] != cell->glyph)
364 continue;
365 int delta = (int)luma - (index * 4 + 2);
366 uint64_t error = (uint64_t)(delta * delta);
367 if (error <= best_error || error - best_error <= margin)
368 glyph = cell->glyph;
369 found = true;
370 }
371 // Bins farther than this cannot fall within the error margin.
372 int nearest_delta = (int)distance * 4 - 3;
373 if (found || (uint64_t)(nearest_delta * nearest_delta) > best_error + margin)
374 break;
375 }
376 }
377 color = temporal_blend_color(cell->color, color, weight);
378 }
379 cell->samples[0] = luma;
380 cell->glyph = frame->glyph[x] = glyph;
381 cell->color = frame->packed[x] = color;
382 cell->valid = true;
383 if (background)
384 frame->luma[x] =
385 (uint8_t)((77u * (color >> 16) + 150u * ((color >> 8) & 255) + 29u * (color & 255) + 128u) >> 8);
386 }
387 if (!frame->dithered && (mode == ASCII_COLOR_16 || mode == ASCII_COLOR_256))
388 ops->quantize(frame->packed, frame->index, frame->width, mode == ASCII_COLOR_16 ? 16 : 256);
389 }
390 for (size_t x = 0; x < frame->width; ++x) {
391 frame->fg[x] = frame->bg[x] = UINT32_MAX;
392 if (mode == ASCII_COLOR_NONE)
393 continue;
394 uint32_t value = mode == ASCII_COLOR_TRUE ? frame->packed[x] : frame->index[x];
395 uint32_t contrast = frame->luma[x] < 128 ? (mode == ASCII_COLOR_TRUE ? 0xffffffu : 15u) : 0;
396 if (mode == ASCII_COLOR_16) {
397 uint8_t r, g, b;
398 get_16color_rgb((uint8_t)value, &r, &g, &b);
399 contrast = (77 * r + 150 * g + 29 * b) / 256 < 127 ? 15u : 0;
400 }
401 frame->fg[x] = background ? contrast : value;
402 frame->bg[x] = background ? value : UINT32_MAX;
403 }
404 emit_prepared_row(&frame->output, mode, ops, frame->glyph, frame->fg, frame->bg, frame->width);
405 emit_row_end(&frame->output, mode != ASCII_COLOR_NONE, more_rows);
406 ++frame->row;
407}
409 if (frame->scratch != frame->inline_scratch)
410 release_scratch(frame->scratch);
411 ob_term(&frame->output);
412 if (frame->output_target)
413 *frame->output_target = frame->output;
414 return frame->output.buf;
415}
416
418 ascii_color_t mode) {
419 ASCII_OTEL_SCOPE(ASCII_HALF_FRAME_INIT);
420 outbuf_t *target = g_output_target;
421 memset(frame, 0, sizeof(*frame));
422 size_t row_bytes, extent, bytes, capacity;
423 if (!rgb || width <= 0 || height <= 0 || mode < ASCII_COLOR_NONE || mode > ASCII_COLOR_TRUE ||
424 checked_size_mul((size_t)width, 3, &row_bytes) != ASCIICHAT_OK || row_bytes > INT_MAX)
425 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid half-block input");
426 if (*stride <= 0)
427 *stride = (int)row_bytes;
428 if ((size_t)*stride < row_bytes || checked_size_mul((size_t)*stride, (size_t)height, &extent) != ASCIICHAT_OK ||
429 checked_size_mul((size_t)width, 6 * sizeof(uint32_t), &bytes) != ASCIICHAT_OK ||
430 checked_size_mul((size_t)width, ((size_t)height + 1) / 2, &capacity) != ASCIICHAT_OK ||
431 checked_size_mul(capacity, 48u, &capacity) != ASCIICHAT_OK ||
432 checked_size_add(capacity, 1u, &capacity) != ASCIICHAT_OK)
433 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid half-block stride or size");
434 prepare_output(frame, capacity, target);
435 frame->width = (size_t)width;
436 frame->source_height = (size_t)height;
437 if (g_stabilizer_target && g_stabilizer_target->cells && g_stabilizer_target->width == (size_t)width &&
438 g_stabilizer_target->height == (size_t)height)
439 frame->stabilizer = g_stabilizer_target;
440 frame->scratch = acquire_scratch(frame->inline_scratch, bytes);
441 if (!frame->scratch)
442 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate ASCII scratch workspace");
443 frame->packed = frame->scratch;
444 frame->index = frame->packed + width;
445 frame->glyph = frame->index + width;
446 frame->fg = frame->glyph + width;
447 frame->bg = frame->fg + width;
448 const char blocks[5][4] = {"\xe2\x96\x91", "\xe2\x96\x92", "\xe2\x96\x93", "\xe2\x96\x88", "\xe2\x96\x80"};
449 for (size_t i = 0; i < 5; ++i)
450 memcpy(frame->table + i, blocks[i], 3);
451 return ASCIICHAT_OK;
452}
453void ascii_frame_emit_halfrow(ascii_frame_t *frame, ascii_color_t mode, bool more_rows, const simd_cells_t *ops) {
454 if (frame->stabilizer) {
455 size_t top_row = frame->row * 2;
456 bool has_bottom = top_row + 1 < frame->source_height;
457 for (size_t x = 0; x < frame->width; ++x) {
458 uint32_t *pixels[] = {&frame->packed[x], &frame->index[x]};
459 for (size_t half = 0; half < (has_bottom ? 2u : 1u); ++half) {
460 uint32_t color = *pixels[half];
461 uint8_t luma = (uint8_t)((76u * (color >> 16) + 150u * ((color >> 8) & 255) + 29u * (color & 255)) >> 8);
462 uint8_t samples[16];
463 memset(samples, luma, sizeof(samples));
464 ascii_temporal_cell_t *cell = &frame->stabilizer->cells[(top_row + half) * frame->width + x];
465 ascii_stabilizer_choose(cell, samples, 0, 0, UINT64_MAX, color, frame->stabilizer->elapsed_ns, pixels[half]);
466 }
467 if (!has_bottom)
468 frame->index[x] = frame->packed[x];
469 }
470 if (mode == ASCII_COLOR_TRUE) {
471 memcpy(frame->fg, frame->packed, frame->width * sizeof(uint32_t));
472 memcpy(frame->bg, frame->index, frame->width * sizeof(uint32_t));
473 } else if (mode != ASCII_COLOR_NONE) {
474 ops->quantize(frame->packed, frame->fg, frame->width, mode == ASCII_COLOR_16 ? 16 : 256);
475 ops->quantize(frame->index, frame->bg, frame->width, mode == ASCII_COLOR_16 ? 16 : 256);
476 }
477 ops->halfblock(frame->packed, frame->index, frame->glyph, frame->fg, frame->bg, frame->width, (unsigned)mode,
478 frame->table);
479 }
480 emit_prepared_row(&frame->output, mode, ops, frame->glyph, frame->fg, frame->bg, frame->width);
481 emit_row_end(&frame->output, mode != ASCII_COLOR_NONE, more_rows);
482 ++frame->row;
483}
484
485/* Compare a small spatial image patch against glyph coverage masks.
486 * The source is fitted at 8x8 samples per output cell, then averaged to 4x4.
487 * ASCII coverage comes from DejaVu Sans Mono; real terminal fonts may differ. */
488#include "glyph_masks.h"
489
490#define GLYPH_ASCII_COUNT 95
491#define GLYPH_BLOCK_COUNT 20
492#define GLYPH_COUNT (GLYPH_ASCII_COUNT + GLYPH_BLOCK_COUNT)
493
494static int glyph_palette_index(const uint8_t *p, size_t *length) {
495 if (*p >= 32 && *p <= 126) {
496 *length = 1;
497 return (int)*p - 32;
498 }
499 if (*p == 0xe2 && p[1] == 0x96 && p[2] >= 0x80 && p[2] <= 0x93) {
500 *length = 3;
501 return GLYPH_ASCII_COUNT + (int)(p[2] - 0x80);
502 }
503 return -1;
504}
505
506// Block elements have font-independent geometry. Coverage is the area of
507// each quarter-cell covered by the block, including eighth-cell boundaries.
508static void glyph_block_mask(int block, uint8_t mask[16]) {
509 for (int y = 0; y < 4; ++y) {
510 for (int x = 0; x < 4; ++x) {
511 int coverage = 0;
512 for (int sy = 0; sy < 2; ++sy) {
513 for (int sx = 0; sx < 2; ++sx) {
514 int px = 2 * x + sx, py = 2 * y + sy;
515 bool set = block == 0 ? py < 4
516 : block <= 7 ? py >= 8 - block
517 : block == 8 ? true
518 : block <= 15 ? px < 16 - block
519 : block == 16 ? px >= 4
520 : false;
521 coverage += set ? 1 : 0;
522 }
523 }
524 mask[y * 4 + x] = block >= 17 ? (uint8_t)((block - 16) * 255 / 4) : (uint8_t)(coverage * 255 / 4);
525 }
526 }
527}
528
529static uint32_t glyph_key(int index) {
531 return (uint32_t)(index + 32);
532 uint32_t cp = 0x2580u + (uint32_t)(index - GLYPH_ASCII_COUNT);
533 const uint8_t bytes[4] = {(uint8_t)(0xe0u | (cp >> 12)), (uint8_t)(0x80u | ((cp >> 6) & 63)),
534 (uint8_t)(0x80u | (cp & 63)), 0};
535 uint32_t key = 0;
536 memcpy(&key, bytes, sizeof(key));
537 return key;
538}
539
540bool ascii_glyph_palette_supported(const char *palette) {
541 if (!palette || !*palette)
542 return false;
543 for (const uint8_t *p = (const uint8_t *)palette; *p;) {
544 size_t length;
545 if (glyph_palette_index(p, &length) < 0)
546 return false;
547 p += length;
548 }
549 return true;
550}
551
552typedef struct {
553 int16_t mask[16];
554 int16_t centered[16];
563
564// Preserve hue while keeping visible ink against the dark canvas. Color is
565// weighted by glyph coverage, so a small feature is not colored by its corner.
566static uint32_t glyph_foreground(const glyph_candidate_t *glyph, const int16_t samples[3][16], const int sums[3]) {
567 unsigned channels[3], peak = 0;
568 for (int c = 0; c < 3; ++c) {
569 int weighted = 0;
570 for (int i = 0; i < 16; ++i)
571 weighted += samples[c][i] * glyph->mask[i];
572 channels[c] = glyph->sum ? (unsigned)((weighted + glyph->sum / 2) / glyph->sum) : (unsigned)((sums[c] + 8) / 16);
573 if (channels[c] > peak)
574 peak = channels[c];
575 }
576 if (peak) {
577 unsigned target = 128 + peak / 2;
578 for (int c = 0; c < 3; ++c)
579 channels[c] = (channels[c] * target + peak / 2) / peak;
580 }
581 return (channels[0] << 16) | (channels[1] << 8) | channels[2];
582}
583
584char *ascii_render_glyph_match(const image_t *image, ascii_color_t mode, const simd_cells_t *ops, const char *palette) {
585 return ascii_render_glyph_match_mode(image, mode, ops, palette, false);
586}
587
589 const char *palette, bool background) {
590 if (!image || !image->pixels || !ops || image->w <= 0 || image->h <= 0 || image->w % 8 || image->h % 8 ||
591 mode < ASCII_COLOR_NONE || mode > ASCII_COLOR_TRUE) {
592 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid glyph matching input");
593 return NULL;
594 }
595 bool allowed[GLYPH_COUNT] = {false};
596 int candidates[GLYPH_COUNT];
597 // Restrict the atlas to unique palette entries before scoring any cells.
598 size_t candidate_count = 0;
599 int (*shape_correlation)(const int16_t[16], const int16_t[16]) =
601 if (palette) {
602 if (!ascii_glyph_palette_supported(palette)) {
603 SET_ERRNO(ERROR_INVALID_PARAM, "Glyph palette contains an unsupported character");
604 return NULL;
605 }
606 for (const uint8_t *p = (const uint8_t *)palette; *p;) {
607 size_t length;
608 int index = glyph_palette_index(p, &length);
609 if (!allowed[index]) {
610 allowed[index] = true;
611 candidates[candidate_count++] = index;
612 }
613 p += length;
614 }
615 } else {
616 for (int i = 0; i < GLYPH_ASCII_COUNT; ++i) {
617 allowed[i] = true;
618 candidates[candidate_count++] = i;
619 }
620 }
621 // Prepare coverage and its sufficient statistics once per frame.
623 // Precompute each mask's brightness and centered shape for correlation.
624 for (size_t c = 0; c < candidate_count; ++c) {
625 int g = candidates[c];
626 glyph_candidate_t *glyph = &glyphs[c];
627 glyph->key = glyph_key(g);
628 uint8_t block_mask[16];
629 const uint8_t *mask = g < GLYPH_ASCII_COUNT ? glyph_masks[g] : block_mask;
630 if (g >= GLYPH_ASCII_COUNT)
631 glyph_block_mask(g - GLYPH_ASCII_COUNT, block_mask);
632 glyph->sum = glyph->energy = 0;
633 for (int i = 0; i < 16; ++i) {
634 glyph->mask[i] = mask[i];
635 glyph->sum += mask[i];
636 }
637 int mean = glyph->sum / 16;
638 glyph->tone = glyph->sum * 255 / (g < GLYPH_ASCII_COUNT ? GLYPH_MASK_MAX_SUM : 16 * 255);
639 for (int i = 0; i < 16; ++i) {
640 glyph->centered[i] = (int16_t)((int)mask[i] - mean);
641 glyph->energy += glyph->centered[i] * glyph->centered[i];
642 }
643 }
644 image_t cells = *image;
645 cells.w /= 8;
646 cells.h /= 8;
647 ascii_frame_t frame;
648 if (ascii_frame_init(&frame, &cells, " .") != ASCIICHAT_OK)
649 return NULL;
650 frame.temporal_preselected = true;
651 for (int y = 0; y < cells.h; ++y) {
652 for (int x = 0; x < cells.w; ++x) {
653 int16_t samples[3][16] = {{0}};
654 int sums[3] = {0};
655 uint8_t patch[16];
656 int sample_mean = 0;
657 for (int sy = 0; sy < 4; ++sy) {
658 const rgb_pixel_t *top = image->pixels + (size_t)(y * 8 + sy * 2) * (size_t)image->w + (size_t)(x * 8);
659 const rgb_pixel_t *bottom = top + image->w;
660 for (int sx = 0; sx < 4; ++sx) {
661 const rgb_pixel_t a = top[sx * 2], b = top[sx * 2 + 1];
662 const rgb_pixel_t c = bottom[sx * 2], d = bottom[sx * 2 + 1];
663 int i = sy * 4 + sx;
664 int red = (a.r + b.r + c.r + d.r) / 4;
665 int green = (a.g + b.g + c.g + d.g) / 4;
666 int blue = (a.b + b.b + c.b + d.b) / 4;
667 patch[i] = (uint8_t)((77 * red + 150 * green + 29 * blue) >> 8);
668 samples[0][i] = (int16_t)red;
669 samples[1][i] = (int16_t)green;
670 samples[2][i] = (int16_t)blue;
671 sample_mean += patch[i];
672 for (int channel = 0; channel < 3; ++channel)
673 sums[channel] += samples[channel][i];
674 }
675 }
676 // Compare local shape independently of the patch's average brightness.
677 sample_mean /= 16;
678 int16_t centered_samples[16];
679 int sample_energy = 0;
680 for (int i = 0; i < 16; ++i) {
681 int centered = (int)patch[i] - sample_mean;
682 centered_samples[i] = (int16_t)centered;
683 sample_energy += centered * centered;
684 }
685 size_t best = 0, previous_index = 0;
686 uint64_t best_error = UINT64_MAX;
687 uint64_t previous_error = UINT64_MAX;
688 ascii_temporal_cell_t *previous = frame.stabilizer ? &frame.stabilizer->cells[(size_t)y * frame.width + x] : NULL;
689 for (size_t c = 0; c < candidate_count; ++c) {
690 bool is_previous = previous && previous->valid && previous->glyph == glyphs[c].key;
691 int tone_delta = sample_mean - glyphs[c].tone;
692 int tone_error = 16 * tone_delta * tone_delta;
693 // Shape error cannot lower the score below the brightness error.
694 if (!is_previous && (uint64_t)tone_error > best_error)
695 continue;
696 int dot = shape_correlation(centered_samples, glyphs[c].centered);
697 uint64_t error = (uint64_t)(tone_error + sample_energy + glyphs[c].energy - 2 * dot);
698 if (is_previous) {
699 previous_error = error;
700 previous_index = c;
701 }
702 if (error < best_error) {
703 best_error = error;
704 best = c;
705 }
706 }
707 uint32_t rgb = glyph_foreground(&glyphs[best], samples, sums);
708 uint32_t bg_rgb =
709 ((uint32_t)(sums[0] / 128) << 16) | ((uint32_t)(sums[1] / 128) << 8) | (uint32_t)(sums[2] / 128);
710 uint32_t selected = glyphs[best].key;
711 if (previous) {
712 ascii_temporal_cell_t old = *previous;
713 unsigned motion = 0;
714 unsigned color_motion = 0;
715 for (int i = 0; i < 16; ++i)
716 motion += (unsigned)abs((int)patch[i] - (int)old.samples[i]);
717 if (old.valid && mode != ASCII_COLOR_NONE) {
718 for (int channel = 0; channel < 3; ++channel)
719 for (int i = 0; i < 16; ++i)
720 color_motion += (unsigned)abs(samples[channel][i] - (int)old.rgb_samples[channel][i]);
721 color_motion /= 48;
722 }
723 uint64_t elapsed = frame.stabilizer->elapsed_ns;
724 uint64_t effective_elapsed = color_motion > ASCII_TEMPORAL_MOTION_LIMIT ? UINT64_MAX : elapsed;
725 selected = ascii_stabilizer_choose(previous, patch, selected, best_error, previous_error, rgb,
726 effective_elapsed, &rgb);
727 if (selected != glyphs[best].key) {
728 *previous = old;
729 selected = ascii_stabilizer_choose(previous, patch, glyphs[best].key, best_error, previous_error,
730 glyph_foreground(&glyphs[previous_index], samples, sums),
731 effective_elapsed, &rgb);
732 }
733 if (old.valid && motion / 16 <= ASCII_TEMPORAL_MOTION_LIMIT && color_motion <= ASCII_TEMPORAL_MOTION_LIMIT &&
735 unsigned weight = (unsigned)(elapsed * 256 / (elapsed + ASCII_TEMPORAL_COLOR_TIME_NS));
736 bg_rgb = temporal_blend_color(old.background, bg_rgb, weight);
737 }
738 for (int channel = 0; channel < 3; ++channel)
739 for (int i = 0; i < 16; ++i)
740 previous->rgb_samples[channel][i] = (uint8_t)samples[channel][i];
741 previous->background = bg_rgb;
742 }
743 frame.glyph[x] = selected;
744 frame.packed[x] = rgb;
745 frame.index[x] = bg_rgb;
746 frame.luma[x] = (uint8_t)((77 * ((rgb >> 16) & 255) + 150 * ((rgb >> 8) & 255) + 29 * (rgb & 255)) >> 8);
747 }
748 if (background && mode != ASCII_COLOR_NONE) {
749 if (mode == ASCII_COLOR_TRUE) {
750 memcpy(frame.fg, frame.packed, frame.width * sizeof(uint32_t));
751 memcpy(frame.bg, frame.index, frame.width * sizeof(uint32_t));
752 } else {
753 ops->quantize(frame.packed, frame.fg, frame.width, mode == ASCII_COLOR_16 ? 16 : 256);
754 ops->quantize(frame.index, frame.bg, frame.width, mode == ASCII_COLOR_16 ? 16 : 256);
755 }
756 emit_prepared_row(&frame.output, mode, ops, frame.glyph, frame.fg, frame.bg, frame.width);
757 emit_row_end(&frame.output, true, y + 1 < cells.h);
758 ++frame.row;
759 } else {
760 if (mode == ASCII_COLOR_16 || mode == ASCII_COLOR_256)
761 ops->quantize(frame.packed, frame.index, frame.width, mode == ASCII_COLOR_16 ? 16 : 256);
762 ascii_frame_emit_row(&frame, mode, false, y + 1 < cells.h, ops);
763 }
764 }
765 return ascii_frame_finish(&frame);
766}
ANSI escape sequence utilities and fast color code generation.
#define SET_ERRNO(code,...)
int glyph_shape_correlation_scalar(const int16_t samples[16], const int16_t mask[16])
Definition cells.c:4
const simd_cells_t simd_cells_sve
const simd_cells_t simd_cells_sse2
const simd_cells_t simd_cells_avx2
const simd_cells_t simd_cells_ssse3
const simd_cells_t simd_cells_neon
⚙️ Common definitions, error codes, macros, and types shared throughout the application
char cells[(512 *256 *2)]
Definition fft.c:21
audio_visualization_source_t source
Definition fft.c:24
unsigned int height
Definition fft.c:23
unsigned int width
Definition fft.c:23
#define GLYPH_MASK_MAX_SUM
char * append_16color_fg(char *dst, uint8_t color_index)
Definition ansi.c:322
char * append_16color_bg(char *dst, uint8_t color_index)
Definition ansi.c:338
uint8_t rgb_to_16color_dithered(int r, int g, int b, int x, int y, int width, int height, rgb_error_t *error_buffer)
Definition ansi.c:428
void get_16color_rgb(uint8_t color_index, uint8_t *r, uint8_t *g, uint8_t *b)
Definition ansi.c:397
#define SAFE_REALLOC(ptr, size, cast)
Definition common.h:285
unsigned int uint32_t
Definition common.h:62
#define SAFE_FREE(ptr)
Definition common.h:377
#define SAFE_MALLOC(size, cast)
Definition common.h:265
#define SAFE_CALLOC(count, size, cast)
Definition common.h:275
unsigned long long uint64_t
Definition common.h:63
unsigned char uint8_t
Definition common.h:60
asciichat_error_t
Error and exit codes - unified status values (0-255).
Definition error_codes.h:44
@ ERROR_MEMORY
Definition error_codes.h:51
@ ASCIICHAT_OK
Definition error_codes.h:46
@ ERROR_INVALID_PARAM
void emit_set_256_color_bg(outbuf_t *ob, uint8_t color_idx)
Emit 256-color background ANSI sequence.
uint8_t utf8_bytes[4]
utf8_palette_cache_t * get_utf8_palette_cache(const char *ascii_chars)
Get UTF-8 palette cache for a character set.
void emit_set_256_color_fg(outbuf_t *ob, uint8_t color_idx)
Emit 256-color foreground ANSI sequence.
void ob_term(outbuf_t *ob)
Append null terminator to buffer.
void emit_set_truecolor_fg(outbuf_t *out, uint8_t r, uint8_t g, uint8_t b)
Emit truecolor foreground ANSI sequence.
#define IMAGE_MAX_WIDTH
An owned RGB image with dimensions and row-major pixel storage.
struct utf8_palette_cache utf8_palette_cache_t
void emit_reset(outbuf_t *out)
Emit ANSI reset sequence.
utf8_char_t cache64[64]
void emit_set_truecolor_bg(outbuf_t *out, uint8_t r, uint8_t g, uint8_t b)
Emit truecolor background ANSI sequence.
void ob_write(outbuf_t *ob, const void *s, size_t n)
Append a string to buffer.
char * render_halfblocks_sse2(const uint8_t *, int, int, int, ascii_color_t)
char * render_halfblocks_ssse3(const uint8_t *, int, int, int, ascii_color_t)
char * render_halfblocks_avx2(const uint8_t *, int, int, int, ascii_color_t)
char * render_ascii_cells_sve(const image_t *, const char *, ascii_color_t, bool)
char * render_ascii_cells_sse2(const image_t *, const char *, ascii_color_t, bool)
char * render_halfblocks_sve(const uint8_t *, int, int, int, ascii_color_t)
#define ASCII_INLINE_SCRATCH_COLUMNS
char * render_halfblocks_neon(const uint8_t *, int, int, int, ascii_color_t)
char * render_ascii_cells_ssse3(const image_t *, const char *, ascii_color_t, bool)
char * render_ascii_cells_neon(const image_t *, const char *, ascii_color_t, bool)
char * render_ascii_cells_avx2(const image_t *, const char *, ascii_color_t, bool)
char * ascii_frame_finish(ascii_frame_t *frame)
#define ASCII_RENDER_WORKSPACE_POOL_SIZE
#define VIDEO_ASCII_RENDER_SEQUENCE_BUFFER_SIZE
asciichat_error_t ascii_stabilizer_prepare(ascii_stabilizer_t *state, size_t width, size_t height, uint64_t configuration)
char * ascii_render_halfblocks(const uint8_t *rgb, int width, int height, int stride, ascii_color_t mode, const simd_cells_t *ops)
void ascii_frame_emit_row(ascii_frame_t *frame, ascii_color_t mode, bool background, bool more_rows, const simd_cells_t *ops)
#define ASCII_TEMPORAL_COLOR_TIME_NS
asciichat_error_t ascii_half_frame_init(ascii_frame_t *frame, const uint8_t *rgb, int width, int height, int *stride, ascii_color_t mode)
char * ascii_render_glyph_match(const image_t *image, ascii_color_t mode, const simd_cells_t *ops, const char *palette)
uint32_t ascii_stabilizer_choose(ascii_temporal_cell_t *cell, const uint8_t samples[16], uint32_t candidate, uint64_t candidate_error, uint64_t previous_error, uint32_t color, uint64_t elapsed_ns, uint32_t *stabilized_color)
#define GLYPH_COUNT
ascii_stabilizer_t * ascii_stabilizer_set_target(ascii_stabilizer_t *state)
asciichat_error_t ascii_frame_init(ascii_frame_t *frame, const image_t *image, const char *palette)
#define GLYPH_ASCII_COUNT
char * ascii_render(const image_t *image, const char *palette, ascii_color_t mode, bool background, bool dither, const simd_cells_t *ops)
#define ASCII_TEMPORAL_MAX_INTERVAL_NS
bool ascii_glyph_palette_supported(const char *palette)
outbuf_t * ascii_render_set_output_target(outbuf_t *target)
#define ASCII_TEMPORAL_MOTION_LIMIT
void ascii_frame_emit_halfrow(ascii_frame_t *frame, ascii_color_t mode, bool more_rows, const simd_cells_t *ops)
void ascii_stabilizer_reset(ascii_stabilizer_t *state)
char * ascii_render_glyph_match_mode(const image_t *image, ascii_color_t mode, const simd_cells_t *ops, const char *palette, bool background)
#define ASCII_OTEL_SCOPE(id)
Definition otel.h:72
void emit_glyph_run(outbuf_t *out, const void *glyph, size_t bytes, uint32_t count)
void emit_row_end(outbuf_t *out, bool colored, bool newline)
✅ Safe Integer Arithmetic and Overflow Detection
static bool size_t index
Definition queries.h:8
static bool size_t gotel_query_t * out
Definition queries.h:8
return true
Definition queries.h:13
🔤 String Manipulation and Shell Escaping Utilities
When code needs to work with ascii frame, it keeps output, output_target, width, source_height here i...
For ascii stabilizer, this is the live record that ties cells, width, height, configuration together....
SIMD-friendly storage for one RGB pixel. r, g, and b hold the color channels and padding keeps the la...
int w
Image width in pixels (must be > 0).
int h
Image height in pixels (must be > 0).
rgb_pixel_t * pixels
Pixel data array (width * height RGB pixels, row-major order).
Dynamic output buffer (auto-expanding).
size_t cap
Buffer capacity in bytes (maximum length before reallocation).
size_t len
Current length in bytes (excluding null terminator).
char * buf
Buffer pointer (allocated, owned by caller, must be freed).
uint8_t b
Blue color component (0-255).
uint8_t g
Green color component (0-255).
uint8_t r
Red color component (0-255).
The useful part of simd cells is in luminance: together they describe what is pending,...
Definition cells.h:10
void(* pack)(const uint8_t *rgb, uint32_t *out, size_t count)
Definition cells.h:12
size_t(* run)(const uint32_t *glyph, const uint32_t *fg, const uint32_t *bg, size_t count)
Definition cells.h:17
void(* emit_mono)(outbuf_t *out, const uint32_t *glyph, size_t count)
Definition cells.h:18
void(* halfblock)(const uint32_t *top, const uint32_t *bottom, uint32_t *glyph, uint32_t *fg, uint32_t *bg, size_t count, unsigned mode, const uint32_t keys[5])
Definition cells.h:15
void(* quantize)(const uint32_t *rgb, uint32_t *out, size_t count, unsigned colors)
Definition cells.h:14
simd_luminance_fn luminance
Definition cells.h:11
void(* lookup)(const uint8_t *luma, const uint32_t table[64], uint32_t *out, size_t count)
Definition cells.h:13
int(* glyph_shape_correlation)(const int16_t samples[16], const int16_t mask[16])
Definition cells.h:20
SIMD-optimized ASCII conversion interface.