ascii-chat 0.11.33
Video chat in your terminal
Loading...
Searching...
No Matches
media/render/terminal.c
Go to the documentation of this file.
1
18#include <ascii-chat/log/log.h>
20#include <ascii-chat/uthash.h>
21#include <vterm.h>
22#include <ft2build.h>
23#include FT_FREETYPE_H
24#include FT_GLYPH_H
25#include <string.h>
26#include <stddef.h>
27#include <stdint.h>
28#include <limits.h>
29
30// ============================================================================
31// Glyph Cache
32// ============================================================================
33
38typedef struct {
39 uint32_t codepoint; // Hash key (character code)
40 uint8_t *bitmap_buf; // Normalized 8-bit alpha bitmap
41 int width; // Bitmap dimensions (in pixels)
42 int rows; // Bitmap height (in rows)
43 int pitch; // Bytes per row
44 int bitmap_left; // Glyph positioning offsets
46 UT_hash_handle hh; // uthash handle
48
52static void glyph_cache_destroy(glyph_cache_entry_t **cache) {
53 glyph_cache_entry_t *curr, *tmp;
54 HASH_ITER(hh, *cache, curr, tmp) {
55 HASH_DEL(*cache, curr);
56 SAFE_FREE(curr->bitmap_buf);
57 SAFE_FREE(curr);
58 }
59}
60
62 VTerm *vt;
63 VTermScreen *vts;
64 int cols, rows;
65 FT_Library ft_lib;
66 FT_Face ft_face;
72 glyph_cache_entry_t *glyph_cache; // Per-renderer glyph cache (per-font)
73};
74
80static FT_Bitmap *glyph_cache_get(terminal_renderer_t *r, FT_Face face, uint32_t codepoint) {
81 glyph_cache_entry_t *entry = NULL;
82
83 // Lookup in renderer's per-font cache
84 HASH_FIND_INT(r->glyph_cache, &codepoint, entry);
85 if (entry) {
86 // Return cached bitmap
87 static FT_Bitmap cached_result;
88 cached_result.width = entry->width;
89 cached_result.rows = entry->rows;
90 cached_result.pitch = entry->pitch;
91 cached_result.buffer = entry->bitmap_buf;
92 return &cached_result;
93 }
94
95 // Not in cache — load and render glyph
96 FT_UInt gi = FT_Get_Char_Index(face, codepoint);
97 if (!gi || FT_Load_Glyph(face, gi, FT_LOAD_RENDER) != 0) {
98 return NULL; // Glyph not found or load failed
99 }
100
101 FT_Bitmap *src_bitmap = &face->glyph->bitmap;
102 if (!src_bitmap->buffer || src_bitmap->width == 0 || src_bitmap->rows == 0 || src_bitmap->width > INT_MAX ||
103 src_bitmap->rows > INT_MAX) {
104 return NULL;
105 }
106
107 // FreeType bitmaps may use packed mono/gray pixels and either pitch
108 // direction. Normalize them once so blit_glyph can safely read one alpha
109 // byte per pixel for every cached glyph.
110 int64_t signed_pitch = src_bitmap->pitch;
111 size_t source_pitch = (size_t)(signed_pitch < 0 ? -signed_pitch : signed_pitch);
112 size_t minimum_source_pitch = 0;
113 switch (src_bitmap->pixel_mode) {
114 case FT_PIXEL_MODE_MONO:
115 minimum_source_pitch = ((size_t)src_bitmap->width + 7) / 8;
116 break;
117 case FT_PIXEL_MODE_GRAY2:
118 minimum_source_pitch = ((size_t)src_bitmap->width + 3) / 4;
119 break;
120 case FT_PIXEL_MODE_GRAY4:
121 minimum_source_pitch = ((size_t)src_bitmap->width + 1) / 2;
122 break;
123 case FT_PIXEL_MODE_GRAY:
124 minimum_source_pitch = src_bitmap->width;
125 break;
126 case FT_PIXEL_MODE_BGRA:
127 minimum_source_pitch = (size_t)src_bitmap->width * 4;
128 break;
129 default:
130 return NULL;
131 }
132 if (source_pitch == 0 || source_pitch < minimum_source_pitch ||
133 src_bitmap->rows > SIZE_MAX / source_pitch ||
134 src_bitmap->rows > SIZE_MAX / (size_t)src_bitmap->width) {
135 return NULL;
136 }
137
138 size_t bitmap_size = (size_t)src_bitmap->width * src_bitmap->rows;
139
140 // Create and populate cache entry
142 if (!entry) {
143 return NULL;
144 }
145
146 entry->codepoint = codepoint;
147 entry->bitmap_buf = SAFE_MALLOC(bitmap_size, uint8_t *);
148 if (!entry->bitmap_buf) {
149 SAFE_FREE(entry);
150 return NULL;
151 }
152
153 for (unsigned row = 0; row < src_bitmap->rows; row++) {
154 unsigned source_row = signed_pitch < 0 ? src_bitmap->rows - row - 1 : row;
155 const uint8_t *source = src_bitmap->buffer + (size_t)source_row * source_pitch;
156 uint8_t *destination = entry->bitmap_buf + (size_t)row * src_bitmap->width;
157 for (unsigned col = 0; col < src_bitmap->width; col++) {
158 switch (src_bitmap->pixel_mode) {
159 case FT_PIXEL_MODE_MONO:
160 destination[col] = (source[col / 8] & (0x80U >> (col % 8))) ? 255 : 0;
161 break;
162 case FT_PIXEL_MODE_GRAY2:
163 destination[col] = (uint8_t)(((source[col / 4] >> (6 - (col % 4) * 2)) & 0x03U) * 85U);
164 break;
165 case FT_PIXEL_MODE_GRAY4:
166 destination[col] = (uint8_t)(((source[col / 2] >> (col % 2 == 0 ? 4 : 0)) & 0x0fU) * 17U);
167 break;
168 case FT_PIXEL_MODE_GRAY:
169 destination[col] = src_bitmap->num_grays > 1 && src_bitmap->num_grays != 256
170 ? (uint8_t)((source[col] * 255U) / (src_bitmap->num_grays - 1))
171 : source[col];
172 break;
173 case FT_PIXEL_MODE_BGRA:
174 destination[col] = source[col * 4 + 3];
175 break;
176 default:
177 break;
178 }
179 }
180 }
181 entry->width = src_bitmap->width;
182 entry->rows = src_bitmap->rows;
183 entry->pitch = (int)src_bitmap->width;
184 entry->bitmap_left = face->glyph->bitmap_left;
185 entry->bitmap_top = face->glyph->bitmap_top;
186
187 // Add to renderer's per-font hash table
188 HASH_ADD_INT(r->glyph_cache, codepoint, entry);
189
190 // Return the cached bitmap
191 static FT_Bitmap new_result;
192 new_result.width = entry->width;
193 new_result.rows = entry->rows;
194 new_result.pitch = entry->pitch;
195 new_result.buffer = entry->bitmap_buf;
196 return &new_result;
197}
198
199static int screen_damage(VTermRect r, void *u) {
200 (void)r;
201 (void)u;
202 return 1;
203}
204
205static VTermScreenCallbacks g_vterm_cbs = {.damage = screen_damage};
206
212static uint32_t matrix_char_map(uint32_t ascii_char) {
213 // Matrix font glyphs: U+E900 to U+E91A (27 glyphs total)
214 const uint32_t matrix_start = 0xE900;
215 const uint32_t matrix_count = 27;
216
217 // If it's already in the PUA range, return as-is
218 if (ascii_char >= 0xE900 && ascii_char <= 0xE91A) {
219 return ascii_char;
220 }
221
222 // Map printable ASCII (32-126) to matrix glyphs
223 if (ascii_char >= 32 && ascii_char <= 126) {
224 // Cycle through available glyphs
225 uint32_t offset = (ascii_char - 32) % matrix_count;
226 return matrix_start + offset;
227 }
228
229 // Return unmapped characters as-is (will have no glyph)
230 return ascii_char;
231}
232
236static void blit_glyph(terminal_renderer_t *r, FT_Bitmap *bm, int px, int py, uint8_t fr, uint8_t fg, uint8_t fb,
237 uint8_t br, uint8_t bg, uint8_t bb) {
238 for (unsigned row = 0; row < bm->rows; row++) {
239 int dy = py + (int)row;
240 if (dy < 0 || dy >= r->height_px)
241 continue;
242 for (unsigned col = 0; col < bm->width; col++) {
243 int dx = px + (int)col;
244 if (dx < 0 || dx >= r->width_px)
245 continue;
246 uint8_t a = bm->buffer[row * bm->pitch + col];
247
248 uint8_t *dst = r->framebuffer + dy * r->pitch + dx * 4;
249 uint8_t r_val = (uint8_t)((fr * a + br * (255 - a)) / 255);
250 uint8_t g_val = (uint8_t)((fg * a + bg * (255 - a)) / 255);
251 uint8_t b_val = (uint8_t)((fb * a + bb * (255 - a)) / 255);
252
253 dst[0] = r_val;
254 dst[1] = g_val;
255 dst[2] = b_val;
256 dst[3] = 255;
257 }
258 }
259}
260
263 r->cols = cfg->cols;
264 r->rows = cfg->rows;
265 r->theme = cfg->theme;
266
267 if (FT_Init_FreeType(&r->ft_lib)) {
268 SAFE_FREE(r);
269 return SET_ERRNO(ERROR_INIT, "FreeType init failed");
270 }
271
272 if (cfg->font_data && cfg->font_data_size > 0) {
273 if (FT_New_Memory_Face(r->ft_lib, cfg->font_data, (FT_Long)cfg->font_data_size, 0, &r->ft_face)) {
274 FT_Done_FreeType(r->ft_lib);
275 SAFE_FREE(r);
276 return SET_ERRNO(ERROR_INIT, "FreeType: cannot load bundled font");
277 }
278 } else {
279 if (FT_New_Face(r->ft_lib, cfg->font_spec, 0, &r->ft_face)) {
280 FT_Done_FreeType(r->ft_lib);
281 SAFE_FREE(r);
282 return SET_ERRNO(ERROR_NOT_FOUND, "FreeType: cannot load font '%s'", cfg->font_spec);
283 }
284 }
285
286 // Detect if this is the matrix font (needs character mapping to Private Use Area)
287 r->is_matrix_font = (strstr(cfg->font_spec, "matrix") != NULL || strstr(cfg->font_spec, "Matrix") != NULL) ||
288 (cfg->font_data != NULL && GET_OPTION(matrix_rain));
289 if (r->is_matrix_font) {
290 log_debug("term_renderer_create: Detected matrix font - will use Private Use Area character mapping");
291 }
292
293 // Handle scalable vs bitmap fonts differently
294 // Bitmap fonts (like matrix) don't respond to FT_Set_Char_Size
295 // Instead, use FT_Set_Pixel_Sizes to request a specific pixel size
296 if (r->ft_face->num_fixed_sizes > 0) {
297 // Bitmap font: use FT_Set_Pixel_Sizes to select appropriate size (12px is reasonable for ASCII)
298 int err = FT_Set_Pixel_Sizes(r->ft_face, 12, 12);
299 log_debug("term_renderer_create: [BITMAP] FT_Set_Pixel_Sizes(12, 12) returned %d", err);
300
301 // If that fails, try FT_Select_Size as fallback
302 if (err != 0) {
303 err = FT_Select_Size(r->ft_face, 0); // Use first available strike
304 log_debug("term_renderer_create: [BITMAP FALLBACK] FT_Select_Size(0) returned %d", err);
305 }
306 } else {
307 // Scalable font: use FT_Set_Char_Size
308 // FT_Set_Char_Size takes 1/64pt units and DPI — supports fractional point sizes.
309 // 96 DPI is the standard screen DPI used here; the 64 factor is the 26.6 fixed-point scale.
310 int err = FT_Set_Char_Size(r->ft_face, 0, (FT_F26Dot6)(cfg->font_size_pt * 64.0), 96, 96);
311 log_debug("term_renderer_create: [SCALABLE] FT_Set_Char_Size(size_pt=%.1f → %ld 1/64pt) returned %d",
312 cfg->font_size_pt, (long)(cfg->font_size_pt * 64.0), err);
313 }
314
315 // For matrix fonts, try PUA glyph first (U+E900) since ASCII chars might not have bitmap data
316 // For other fonts, start with 'M'
317 int load_err;
318 if (r->is_matrix_font) {
319 log_debug("Matrix font detected: trying PUA glyph U+E900 first");
320 load_err = FT_Load_Char(r->ft_face, 0xE900, FT_LOAD_RENDER);
321 log_debug("FT_Load_Char(U+E900) returned %d", load_err);
322
323 // If PUA glyph fails, try ASCII fallback
324 if (load_err != 0) {
325 log_debug("U+E900 failed, trying fallback character 'M'");
326 load_err = FT_Load_Char(r->ft_face, 'M', FT_LOAD_RENDER);
327 log_debug("FT_Load_Char('M') returned %d", load_err);
328 }
329 } else {
330 // For non-matrix fonts, standard approach
331 load_err = FT_Load_Char(r->ft_face, 'M', FT_LOAD_RENDER);
332 log_debug("DEBUG: FT_Load_Char('M', FT_LOAD_RENDER) returned %d", load_err);
333
334 // For bitmap fonts (like matrix), FT_Load_Char may fail if the character doesn't exist.
335 // Try a fallback character if 'M' fails.
336 if (load_err != 0 && r->ft_face->num_fixed_sizes > 0) {
337 log_debug("FT_Load_Char('M') failed for bitmap font, trying fallback character 'A'");
338 load_err = FT_Load_Char(r->ft_face, 'A', FT_LOAD_RENDER);
339 log_debug("FT_Load_Char('A') returned %d", load_err);
340
341 if (load_err != 0) {
342 // Last resort: try a PUA glyph for unknown bitmap fonts
343 log_debug("FT_Load_Char('A') also failed, trying PUA glyph U+E900");
344 load_err = FT_Load_Char(r->ft_face, 0xE900, FT_LOAD_RENDER);
345 log_debug("FT_Load_Char(U+E900) returned %d", load_err);
346 }
347 }
348 }
349
350 // For monospace ASCII grid: use advance.x (proper character spacing) and rendered height
351 FT_Pos advance_x_26_6 = r->ft_face->glyph->advance.x;
352 log_debug("DEBUG: advance.x (26.6pt)=%ld", advance_x_26_6);
353
354 // Calculate cell width from font metrics
355 r->cell_w = (int)(advance_x_26_6 >> 6);
356
357 // Safeguard: for bitmap fonts, cell_w might be wrong if the font metrics are off
358 // Ensure we have a reasonable minimum width
359 if (r->cell_w <= 0) {
360 log_warn("term_renderer_create: cell_w is %d (invalid), using default 10px", r->cell_w);
361 r->cell_w = 10;
362 }
363
364 // Only add spacing to fill entire frame if --stretch is specified
365 bool stretch_mode = GET_OPTION(stretch);
366 if (stretch_mode) {
367 r->cell_w++;
368 log_debug("ADVANCE_X: value=%ld (26.6pt) → cell_w=%d (advance + 1 for stretch), cols=%d → width_px=%d",
369 advance_x_26_6, r->cell_w, r->cols, r->cols * r->cell_w);
370 } else {
371 log_debug("ADVANCE_X: value=%ld (26.6pt) → cell_w=%d (preserve aspect ratio), cols=%d → width_px=%d",
372 advance_x_26_6, r->cell_w, r->cols, r->cols * r->cell_w);
373 }
374
375 // For both bitmap and scalable fonts, use the actual glyph bitmap height
376 // This ensures text doesn't overflow cells. Using size->metrics.height (line spacing)
377 // instead of bitmap.rows causes cells to be too large and text to overflow.
378 r->cell_h = r->ft_face->glyph->bitmap.rows;
379 int raw_bitmap_top = r->ft_face->glyph->bitmap_top;
380 log_debug("DEBUG: cell_h=%d (from bitmap.rows), raw_bitmap_top=%d", r->cell_h, raw_bitmap_top);
381
382 // Fallback: if cell_h is 0 (bitmap failed to load), use aspect ratio calculation
383 if (r->cell_h <= 0) {
384 log_warn("term_renderer_create: cell_h is 0 (FT_Load_Char failed or no bitmap), using aspect ratio fallback");
385 r->cell_h = r->cell_w * 2;
386 log_debug("FALLBACK: cell_h set to %d (2x width=%d)", r->cell_h, r->cell_w);
387 }
388
389 // Apply aspect ratio correction unless --stretch is specified
390 // Terminal characters are typically 2:1 (height:width) to appear normal
391 // BUT: only correct if the loaded glyph height is reasonably close (±30%)
392 // This prevents huge jumps from small glyphs (e.g., 8px → 32px)
393 if (!stretch_mode && r->cell_h > 0) {
394 int corrected_h = r->cell_w * 2;
395 int min_acceptable = (corrected_h * 70) / 100; // 70% of target
396 int max_acceptable = (corrected_h * 130) / 100; // 130% of target
397
398 if (r->cell_h < min_acceptable || r->cell_h > max_acceptable) {
399 // Loaded height is far from expected, apply correction
400 log_debug("ASPECT_RATIO: Correcting cell_h from %d to %d (out of range [%d,%d], 2x width=%d)", r->cell_h,
401 corrected_h, min_acceptable, max_acceptable, r->cell_w);
402 r->cell_h = corrected_h;
403 } else if (corrected_h != r->cell_h) {
404 // Loaded height is close to expected, fine-tune by small amount only
405 int delta = corrected_h - r->cell_h;
406 if (delta > 4 || delta < -4) {
407 // Only apply if difference is more than 4px
408 log_debug("ASPECT_RATIO: Fine-tuning cell_h from %d to %d (within acceptable range, 2x width=%d)", r->cell_h,
409 corrected_h, r->cell_w);
410 r->cell_h = corrected_h;
411 } else {
412 log_debug("ASPECT_RATIO: Keeping cell_h=%d (close to target %d, delta=%d)", r->cell_h, corrected_h, delta);
413 }
414 }
415 }
416
417 // Position baseline vertically within the cell.
418 // Baseline should be positioned at approximately 75% of cell height to allow proper
419 // glyph rendering with space both above and below. This prevents glyphs from being
420 // clipped at the cell boundaries.
421 //
422 // Baseline position: y_glyph = py + r->baseline - g->bitmap_top
423 // We want: 0 <= baseline - bitmap_top < cell_h (glyph fits within cell)
424 // Choose: baseline = (3 * cell_h) / 4 (positions at 75% height)
425 r->baseline = (3 * r->cell_h) / 4;
426 log_debug("DEBUG: baseline set to %d (75%% of cell_h=%d)", r->baseline, r->cell_h);
427
428 r->width_px = r->cols * r->cell_w;
429 r->height_px = r->rows * r->cell_h;
430 log_debug("DEBUG: calculated dimensions: width_px=%d (cols=%d * cell_w=%d), height_px=%d (rows=%d * cell_h=%d)",
431 r->width_px, r->cols, r->cell_w, r->height_px, r->rows, r->cell_h);
432 // Pitch in bytes: each pixel is 4 bytes (RGBA), already aligned to 4-byte boundary
433 int base_pitch = r->width_px * 4;
434 r->pitch = base_pitch;
435 log_debug("PITCH_CALC: width_px=%d, base_pitch=%d, final_pitch=%d (RGBA, no padding needed)", r->width_px, base_pitch,
436 r->pitch);
437 log_debug("DEBUG: Allocating framebuffer: %d bytes total (%d pitch * %d height)",
438 (int)((size_t)r->pitch * r->height_px), r->pitch, r->height_px);
439 r->framebuffer = SAFE_MALLOC((size_t)r->pitch * r->height_px, uint8_t *);
440
441 r->vt = vterm_new(r->rows, r->cols);
442 r->vts = vterm_obtain_screen(r->vt);
443
444 // Enable UTF-8 encoding so vterm correctly parses multi-byte characters
445 vterm_set_utf8(r->vt, 1);
446
447 // Set size BEFORE reset to ensure screen is allocated with correct dimensions
448 vterm_set_size(r->vt, r->rows, r->cols);
449 log_debug("DEBUG: vterm_set_size called with rows=%d cols=%d", r->rows, r->cols);
450
451 vterm_screen_set_callbacks(r->vts, &g_vterm_cbs, r);
452 // Reset MUST come before any input writing to initialize state correctly
453 vterm_screen_reset(r->vts, 1);
454
455 // Verify the size was set correctly after reset
456 int vt_rows, vt_cols;
457 vterm_get_size(r->vt, &vt_rows, &vt_cols);
458 log_debug("DEBUG: vterm_get_size returned rows=%d cols=%d (expected %d,%d)", vt_rows, vt_cols, r->rows, r->cols);
459
460 // CRITICAL: Set size AGAIN after reset, as reset may have affected it
461 vterm_set_size(r->vt, r->rows, r->cols);
462 log_debug("DEBUG: vterm_set_size called AGAIN after reset");
463
464 // Initialize per-renderer glyph cache (empty, will populate on demand)
465 r->glyph_cache = NULL;
466
467 *out = r;
468 return ASCIICHAT_OK;
469}
470
471asciichat_error_t term_renderer_feed(terminal_renderer_t *r, const char *ansi_frame, size_t len) {
472 if (!r || (!ansi_frame && len > 0)) {
473 return SET_ERRNO(ERROR_INVALID_PARAM, "term_renderer_feed: renderer or frame is invalid");
474 }
475
476 // libvterm natively implements CSI REP. Expanding those runs here turns a
477 // compact network frame into synchronous browser-main-thread work before
478 // every paint.
479 const char *frame = ansi_frame;
480 const size_t frame_len = len;
481
482 // Clear framebuffer to ensure no leftover pixels from previous frames
483 uint8_t def_fg_r, def_fg_g, def_fg_b;
484 uint8_t def_bg_r, def_bg_g, def_bg_b;
485 terminal_get_default_foreground_color(r->theme, &def_fg_r, &def_fg_g, &def_fg_b);
486 terminal_get_default_background_color(r->theme, &def_bg_r, &def_bg_g, &def_bg_b);
487 uint8_t def_bg = def_bg_r;
488 memset(r->framebuffer, def_bg, (size_t)r->pitch * r->height_px);
489
490 // Reset scrolling region and cursor position to prevent vertical shifts
491 static const char reset_sequence[] = "\033[2J\033[H\033[r\033[?7l";
492 vterm_input_write(r->vt, reset_sequence, sizeof(reset_sequence) - 1);
493
494 // Count newlines to determine CRLF conversion size
495 int newline_count = 0;
496 for (size_t i = 0; i < frame_len; i++) {
497 if (frame[i] == '\n')
498 newline_count++;
499 }
500
501 // Convert LF-only line endings to CRLF (vterm expects CRLF)
502 // This fixes cursor positioning on alternate rows
503 char *fixed_frame = SAFE_MALLOC(frame_len + (size_t)newline_count, char *);
504 if (!fixed_frame) {
505 log_error("failed to allocate memory for CRLF conversion");
506 return SET_ERRNO(ERROR_MEMORY, "CRLF conversion allocation failed");
507 }
508
509 size_t fixed_pos = 0;
510 int frame_row = 0;
511 for (size_t i = 0; i < frame_len; i++) {
512 // A video frame is a fixed grid, not a scrolling terminal transcript.
513 // A trailing newline on the bottom row must not scroll the whole image.
514 if (frame[i] == '\n' && frame_row >= r->rows - 1) {
515 continue;
516 }
517 fixed_frame[fixed_pos++] = frame[i];
518 if (frame[i] == '\n' && (i == 0 || frame[i - 1] != '\r')) {
519 // Insert carriage return before LF if not already preceded by CR
520 fixed_frame[fixed_pos - 1] = '\r';
521 fixed_frame[fixed_pos++] = '\n';
522 }
523 if (frame[i] == '\n') frame_row++;
524 }
525
526 vterm_input_write(r->vt, fixed_frame, fixed_pos);
527 SAFE_FREE(fixed_frame);
528
529 // Calculate centering offsets to position grid in center of canvas
530 int grid_width_px = r->cols * r->cell_w;
531 int grid_height_px = r->rows * r->cell_h;
532 int offset_x = (r->width_px - grid_width_px) / 2;
533 int offset_y = (r->height_px - grid_height_px) / 2;
534
535 static int frame_count = 0;
536 static char prev_row_sigs[52][5] = {0}; // Store signatures of previous frame's rows
537 frame_count++;
538
539 // Log if any row signature changed to detect vertical shifts
540 if (frame_count % 10 == 0) {
541 char curr_row_sigs[52][5];
542 for (int row = 0; row < r->rows && row < 52; row++) {
543 // Get first 4 chars as signature
544 for (int col = 0; col < 4 && col < r->cols; col++) {
545 VTermScreenCell cell;
546 vterm_screen_get_cell(r->vts, (VTermPos){row, col}, &cell);
547 curr_row_sigs[row][col] = (cell.chars[0] >= 32 && cell.chars[0] <= 126) ? cell.chars[0] : '.';
548 }
549 curr_row_sigs[row][4] = 0;
550 }
551 memcpy(prev_row_sigs, curr_row_sigs, sizeof(curr_row_sigs));
552 }
553
554 int glyph_rendered_count = 0;
555 int occupied_cell_count = 0;
556 int glyph_missing_count = 0;
557 // ANSI 16/256 colors are indexed. Converting them through libvterm for
558 // every cell is especially expensive in the single-threaded WASM renderer.
559 // A terminal palette has at most 256 entries and is stable for a frame.
560 uint32_t indexed_rgb[256] = {0};
561 bool indexed_rgb_valid[256] = {false};
562 for (int row = 0; row < r->rows; row++) {
563 for (int col = 0; col < r->cols; col++) {
564 VTermScreenCell cell;
565 vterm_screen_get_cell(r->vts, (VTermPos){row, col}, &cell);
566
567 uint8_t fr, fg, fb, br, bg, bb;
568 if (VTERM_COLOR_IS_RGB(&cell.fg)) {
569 fr = cell.fg.rgb.red;
570 fg = cell.fg.rgb.green;
571 fb = cell.fg.rgb.blue;
572 } else if (VTERM_COLOR_IS_INDEXED(&cell.fg)) {
573 const uint8_t index = cell.fg.indexed.idx;
574 if (!indexed_rgb_valid[index]) {
575 VTermColor foreground = cell.fg;
576 vterm_screen_convert_color_to_rgb(r->vts, &foreground);
577 indexed_rgb[index] = ((uint32_t)foreground.rgb.red << 16) |
578 ((uint32_t)foreground.rgb.green << 8) |
579 foreground.rgb.blue;
580 indexed_rgb_valid[index] = true;
581 }
582 fr = (uint8_t)(indexed_rgb[index] >> 16);
583 fg = (uint8_t)(indexed_rgb[index] >> 8);
584 fb = (uint8_t)indexed_rgb[index];
585 } else {
586 fr = def_fg_r;
587 fg = def_fg_g;
588 fb = def_fg_b;
589 }
590
591 if (VTERM_COLOR_IS_RGB(&cell.bg)) {
592 br = cell.bg.rgb.red;
593 bg = cell.bg.rgb.green;
594 bb = cell.bg.rgb.blue;
595 } else if (VTERM_COLOR_IS_INDEXED(&cell.bg)) {
596 const uint8_t index = cell.bg.indexed.idx;
597 if (!indexed_rgb_valid[index]) {
598 VTermColor background = cell.bg;
599 vterm_screen_convert_color_to_rgb(r->vts, &background);
600 indexed_rgb[index] = ((uint32_t)background.rgb.red << 16) |
601 ((uint32_t)background.rgb.green << 8) |
602 background.rgb.blue;
603 indexed_rgb_valid[index] = true;
604 }
605 br = (uint8_t)(indexed_rgb[index] >> 16);
606 bg = (uint8_t)(indexed_rgb[index] >> 8);
607 bb = (uint8_t)indexed_rgb[index];
608 } else {
609 br = def_bg_r;
610 bg = def_bg_g;
611 bb = def_bg_b;
612 }
613
614 int px = offset_x + col * r->cell_w, py = offset_y + row * r->cell_h;
615 for (int dy = 0; dy < r->cell_h; dy++) {
616 int y = py + dy;
617 if (y < 0 || y >= r->height_px)
618 continue;
619 uint8_t *line = r->framebuffer + y * r->pitch + px * 4;
620 for (int dx = 0; dx < r->cell_w; dx++) {
621 int x = px + dx;
622 if (x < 0 || x >= r->width_px)
623 continue;
624 line[dx * 4] = br;
625 line[dx * 4 + 1] = bg;
626 line[dx * 4 + 2] = bb;
627 line[dx * 4 + 3] = 255;
628 }
629 }
630
631 if (cell.chars[0] && cell.chars[0] != ' ') {
632 occupied_cell_count++;
633 // For matrix font, map ASCII characters to Private Use Area glyphs (U+E900-U+E91A)
634 uint32_t char_to_render = r->is_matrix_font ? matrix_char_map(cell.chars[0]) : cell.chars[0];
635
636 // Use per-renderer glyph cache to avoid rasterizing every glyph every frame
637 FT_Bitmap *cached_bitmap = glyph_cache_get(r, r->ft_face, char_to_render);
638 if (cached_bitmap && cached_bitmap->width > 0 && cached_bitmap->rows > 0) {
639 // Find cache entry to get positioning offsets
640 glyph_cache_entry_t *cache_entry = NULL;
641 HASH_FIND_INT(r->glyph_cache, &char_to_render, cache_entry);
642 if (cache_entry) {
643 blit_glyph(r, cached_bitmap, px + cache_entry->bitmap_left, py + r->baseline - cache_entry->bitmap_top, fr,
644 fg, fb, br, bg, bb);
645 glyph_rendered_count++;
646 }
647 } else {
648 glyph_missing_count++;
649 }
650 }
651 }
652 }
654 "term_renderer_feed: Rendered %d glyphs from %d occupied cells (%d missing) out of %d cells",
655 glyph_rendered_count, occupied_cell_count, glyph_missing_count, r->rows * r->cols);
656
657 return ASCIICHAT_OK;
658}
659
670 return r->pitch;
671}
672
674 if (!r)
675 return 0;
676 return r->cols;
677}
678
680 if (!r)
681 return 0;
682 return r->rows;
683}
684
686 if (!r)
687 return;
688 vterm_free(r->vt);
689 FT_Done_Face(r->ft_face);
690 FT_Done_FreeType(r->ft_lib);
692 glyph_cache_destroy(&r->glyph_cache);
693 SAFE_FREE(r);
694}
unsigned int uint32_t
Definition common.h:58
#define SAFE_FREE(ptr)
Definition common.h:376
#define SAFE_MALLOC(size, cast)
Definition common.h:264
#define SAFE_CALLOC(count, size, cast)
Definition common.h:274
unsigned char uint8_t
Definition common.h:56
#define SET_ERRNO(code, context_msg,...)
Set error code with custom context message and log it, returning the error code.
asciichat_error_t
Error and exit codes - unified status values (0-255)
Definition error_codes.h:49
@ ERROR_NOT_FOUND
@ ERROR_MEMORY
Definition error_codes.h:56
@ ASCIICHAT_OK
Definition error_codes.h:51
@ ERROR_INIT
Definition error_codes.h:61
@ ERROR_INVALID_PARAM
#define log_warn(...)
Log a WARN message.
Definition log/log.h:574
#define log_error(...)
Log an ERROR message.
Definition log/log.h:587
#define log_debug(...)
Log a DEBUG message.
Definition log/log.h:548
#define US_PER_SEC_INT
Definition time.h:161
#define GET_OPTION(field)
Safely get a specific option field (lock-free read)
⚙️ Unified options parsing system for ascii-chat with builder pattern and lock-free access
Application limits and constraints.
📝 Logging API with multiple log levels and terminal output control
#define log_debug_every(interval_us, fmt,...)
Rate-limited DEBUG logging.
Definition log/log.h:702
int term_renderer_get_rows(terminal_renderer_t *r)
int term_renderer_get_cols(terminal_renderer_t *r)
int term_renderer_width_px(terminal_renderer_t *r)
const uint8_t * term_renderer_pixels(terminal_renderer_t *r)
asciichat_error_t term_renderer_create(const term_renderer_config_t *cfg, terminal_renderer_t **out)
void term_renderer_destroy(terminal_renderer_t *r)
asciichat_error_t term_renderer_feed(terminal_renderer_t *r, const char *ansi_frame, size_t len)
int term_renderer_pitch(terminal_renderer_t *r)
int term_renderer_height_px(terminal_renderer_t *r)
Cross-platform memory allocation utilities.
void terminal_get_default_background_color(int theme, uint8_t *out_r, uint8_t *out_g, uint8_t *out_b)
Get theme-aware default background color for the renderer.
void terminal_get_default_foreground_color(int theme, uint8_t *out_r, uint8_t *out_g, uint8_t *out_b)
Get theme-aware default foreground color for the renderer.
Terminal-to-pixel renderer and render-file output (audio + video)
term_renderer_theme_t
Terminal rendering theme.
Definition renderer.h:26
int frame
Definition splash.c:99
UT_hash_handle hh
uint32_t codepoint
int bitmap_left
uint8_t * bitmap_buf
int bitmap_top
int rows
int width
int pitch
const uint8_t * font_data
Definition renderer.h:39
term_renderer_theme_t theme
Definition renderer.h:36
term_renderer_theme_t theme
glyph_cache_entry_t * glyph_cache
🖥️ Cross-platform terminal interface for ascii-chat