ascii-chat
Binary v0.15.83-53-gb711d8720
Library lib/v0.3.2-701-gb711d8720
Video chat in your terminal
Loading...
Searching...
No Matches
renderer.h File Reference

Terminal-to-pixel renderer and render-file output (audio + video). More...

Go to the source code of this file.

Data Structures

struct  term_renderer_config_t

Typedefs

typedef struct terminal_renderer_s terminal_renderer_t
typedef struct render_file_ctx_s render_file_ctx_t

Enumerations

enum  term_renderer_theme_t { TERM_RENDERER_THEME_DARK = 0 , TERM_RENDERER_THEME_LIGHT = 1 , TERM_RENDERER_THEME_AUTO = 2 }
 Terminal rendering theme. More...

Functions

asciichat_error_t term_renderer_create (const term_renderer_config_t *cfg, terminal_renderer_t **out)
asciichat_error_t term_renderer_set_zalgo (terminal_renderer_t *r, int level)
asciichat_error_t term_renderer_feed (terminal_renderer_t *r, const char *ansi_frame, size_t len)
const uint8_t * term_renderer_pixels (terminal_renderer_t *r)
int term_renderer_width_px (terminal_renderer_t *r)
int term_renderer_height_px (terminal_renderer_t *r)
int term_renderer_pitch (terminal_renderer_t *r)
int term_renderer_get_cols (terminal_renderer_t *r)
int term_renderer_get_rows (terminal_renderer_t *r)
void term_renderer_destroy (terminal_renderer_t *r)
asciichat_error_t render_file_create (const char *output_path, int cols, int rows, int fps, int theme, render_file_ctx_t **out)
asciichat_error_t render_file_write_frame (render_file_ctx_t *ctx, const char *ansi_frame, uint64_t captured_ns)
void render_file_set_live_timing (render_file_ctx_t *ctx)
void render_file_set_snapshot_actual_duration (render_file_ctx_t *ctx, double actual_duration_sec)
void render_file_begin_drain (render_file_ctx_t *ctx, uint64_t total, bool total_known)
asciichat_error_t render_file_destroy (render_file_ctx_t *ctx)

Detailed Description

Terminal-to-pixel renderer and render-file output (audio + video).

Supports theme-aware rendering that adapts colors based on the terminal's background theme (dark or light) for optimal readability.

Definition in file renderer.h.

Typedef Documentation

◆ render_file_ctx_t

Definition at line 63 of file renderer.h.

◆ terminal_renderer_t

Definition at line 18 of file renderer.h.

Enumeration Type Documentation

◆ term_renderer_theme_t

Terminal rendering theme.

Determines color palette for pixel-based ANSI text rendering. When TERM_RENDERER_THEME_AUTO is used, theme is detected from terminal_has_dark_background() to adapt colors to user's theme.

Enumerator
TERM_RENDERER_THEME_DARK 

Dark theme: use light colors for dark background

TERM_RENDERER_THEME_LIGHT 

Light theme: use dark colors for light background

TERM_RENDERER_THEME_AUTO 

Auto-detect theme from terminal (default)

Definition at line 27 of file renderer.h.

27 {
term_renderer_theme_t
Terminal rendering theme.
Definition renderer.h:27
@ TERM_RENDERER_THEME_LIGHT
Definition renderer.h:29
@ TERM_RENDERER_THEME_DARK
Definition renderer.h:28
@ TERM_RENDERER_THEME_AUTO
Definition renderer.h:30

Function Documentation

◆ render_file_begin_drain()

void render_file_begin_drain ( render_file_ctx_t * ctx,
uint64_t total,
bool total_known )

Definition at line 364 of file renderer.c.

364 {
365 if (ctx)
366 render_progress_begin(ctx->progress, total, total_known);
367}
void render_progress_begin(render_progress_t *progress, uint64_t total, bool total_known)
render_progress_t * progress
Definition renderer.c:33

References render_file_ctx_s::progress, and render_progress_begin().

Referenced by session_display_begin_drain().

◆ render_file_create()

asciichat_error_t render_file_create ( const char * output_path,
int cols,
int rows,
int fps,
int theme,
render_file_ctx_t ** out )

Definition at line 53 of file renderer.c.

54 {
55 if (!output_path || !out || cols <= 0 || rows <= 0)
56 return SET_ERRNO(ERROR_INVALID_PARAM, "Missing render output, grid, or context");
57 *out = NULL;
58 log_info("output_path=%s, cols=%d, rows=%d, fps=%d, theme=%d, out=%p", output_path ? output_path : "(null)", cols,
59 rows, fps, theme, (void *)out);
60 render_file_ctx_t *ctx = SAFE_CALLOC(1, sizeof(*ctx), render_file_ctx_t *);
61 if (strcmp(output_path, "/dev/null") == 0) {
62 ctx->discard = true;
63 ctx->error = ASCIICHAT_OK;
64 *out = ctx;
65 return ASCIICHAT_OK;
66 }
67 ctx->kitty_only = strcmp(output_path, "--kitty") == 0;
68 ctx->kitty = ctx->kitty_only || GET_OPTION(render_kitty);
69 if (ctx->kitty && !GET_OPTION(snapshot_mode)) {
70 SAFE_FREE(ctx);
71 return SET_ERRNO(ERROR_INVALID_PARAM, "Kitty graphics output requires --snapshot");
72 }
73 if (ctx->kitty && (strcmp(output_path, "-") == 0 || strcmp(output_path, "pipe:") == 0)) {
74 SAFE_FREE(ctx);
75 return SET_ERRNO(ERROR_INVALID_PARAM, "Kitty graphics and binary media cannot share stdout");
76 }
77 const char *term = SAFE_GETENV("TERM");
78 if (ctx->kitty && terminal_is_stdout_tty() &&
79 (!term || strcmp(term, "dumb") == 0 || strcmp(term, "linux") == 0 || strcmp(term, "vt100") == 0)) {
80 SAFE_FREE(ctx);
82 "Kitty graphics require a capable terminal; redirect stdout to export the protocol");
83 }
84
85 // Resolve font spec → platform-appropriate file path, family name, or bundled data.
86 char font_spec[PLATFORM_MAX_PATH_LENGTH] = {0};
87 bool font_is_path = false;
88 const uint8_t *font_data = NULL;
89 size_t font_data_size = 0;
90 const char *raw_font = GET_OPTION(render_font);
91
92 // Log the font option value from GET_OPTION (for WASM font debugging)
93 log_info("[WASM_FONT_GETTER] GET_OPTION(render_font) returned: '%s' (empty=%d), GET_OPTION(matrix_rain)=%d",
94 raw_font ? raw_font : "(null)", (!raw_font || raw_font[0] == '\0') ? 1 : 0, GET_OPTION(matrix_rain));
95
96 // Auto-select matrix font when --matrix flag is set, unless user explicitly overrides with --render-font
97 log_debug("raw_font=%s, matrix_rain=%d", raw_font, GET_OPTION(matrix_rain));
98 if ((!raw_font || raw_font[0] == '\0') && GET_OPTION(matrix_rain)) {
99 raw_font = "matrix";
100 log_debug("[MATRIX] Using matrix font (enabled by --matrix flag)");
101 }
102
103 platform_font_register("matrix", g_font_matrix_resurrected, g_font_matrix_resurrected_size);
104 platform_font_register("default", g_font_default, g_font_default_size);
105 platform_font_register("DejaVu Sans Mono", g_font_default, g_font_default_size);
107 platform_font_resolve(raw_font, font_spec, sizeof(font_spec), &font_is_path, &font_data, &font_data_size);
108 if (fe != ASCIICHAT_OK) {
109 if (raw_font && raw_font[0]) {
110 SAFE_FREE(ctx);
111 return fe;
112 }
113 log_warn("font resolution failed for '%s' — using bundled default", raw_font ? raw_font : "(explicit system)");
114 fe = platform_font_resolve("default", font_spec, sizeof(font_spec), &font_is_path, &font_data, &font_data_size);
115 if (fe != ASCIICHAT_OK) {
116 SAFE_FREE(ctx);
117 return fe;
118 }
119 }
120
121 log_debug("Font resolved: font_spec='%s', font_is_path=%d, font_data=%p (size=%zu)", font_spec, font_is_path,
122 (void *)font_data, font_data_size);
123
124 // A zero size selects a compact default for video; explicit font sizes keep
125 // their exact dimensions. Images and Kitty snapshots retain the 12 pt default.
126 double font_size_pt = GET_OPTION(render_font_size);
127 const char *extension = strrchr(output_path, '.');
128 bool is_image =
129 extension && (!strcmp(extension, ".png") || !strcmp(extension, ".jpg") || !strcmp(extension, ".jpeg") ||
130 !strcmp(extension, ".bmp") || !strcmp(extension, ".webp"));
131 bool automatic_video_size = font_size_pt == 0.0 && !is_image && !ctx->kitty;
132 if (font_size_pt == 0.0) {
133 font_size_pt = RENDER_AUTO_FONT_SIZE_PT;
134 if (automatic_video_size) {
135 double width_scale = (double)RENDER_AUTO_VIDEO_MAX_DIMENSION / ((double)cols * 10.0);
136 double height_scale = (double)RENDER_AUTO_VIDEO_MAX_DIMENSION / ((double)rows * 20.0);
137 double scale = width_scale < height_scale ? width_scale : height_scale;
138 if (scale < 1.0)
139 font_size_pt *= scale;
140 if (font_size_pt < RENDER_AUTO_MIN_FONT_SIZE_PT)
141 font_size_pt = RENDER_AUTO_MIN_FONT_SIZE_PT;
142 }
143 }
144 term_renderer_theme_t resolved_theme = (term_renderer_theme_t)theme;
145 if (resolved_theme == TERM_RENDERER_THEME_AUTO) {
146 resolved_theme =
148 log_debug("auto theme resolved to %s", resolved_theme == TERM_RENDERER_THEME_DARK ? "dark" : "light");
149 }
150
151 term_renderer_config_t tr_cfg = {
152 .cols = cols,
153 .rows = rows,
154 .font_size_pt = font_size_pt,
155 .theme = resolved_theme,
156 .font_is_path = font_is_path,
157 .font_data = font_data,
158 .font_data_size = font_data_size,
159 };
160 SAFE_STRNCPY(tr_cfg.font_spec, font_spec, sizeof(tr_cfg.font_spec));
161
162 asciichat_error_t err = term_renderer_create(&tr_cfg, &ctx->renderer);
163 if (err != ASCIICHAT_OK) {
164 log_error("term_renderer_create failed: %s", asciichat_error_format(NULL, err));
165 SAFE_FREE(ctx);
166 return err;
167 }
168 int actual_width_px = term_renderer_width_px(ctx->renderer);
169 int actual_height_px = term_renderer_height_px(ctx->renderer);
170 // Hinting and font metrics vary. Recalculate once if the initial estimate
171 // exceeds the target, retaining the original renderer if a bitmap font
172 // cannot be scaled down.
173 if (automatic_video_size &&
174 (actual_width_px > RENDER_AUTO_VIDEO_MAX_DIMENSION || actual_height_px > RENDER_AUTO_VIDEO_MAX_DIMENSION) &&
175 font_size_pt > RENDER_AUTO_MIN_FONT_SIZE_PT) {
176 double width_scale = (double)RENDER_AUTO_VIDEO_MAX_DIMENSION / actual_width_px;
177 double height_scale = (double)RENDER_AUTO_VIDEO_MAX_DIMENSION / actual_height_px;
178 double scale = width_scale < height_scale ? width_scale : height_scale;
179 double smaller = font_size_pt * scale * 0.98;
180 if (smaller < RENDER_AUTO_MIN_FONT_SIZE_PT)
182 tr_cfg.font_size_pt = smaller;
183 terminal_renderer_t *candidate = NULL;
184 err = term_renderer_create(&tr_cfg, &candidate);
185 if (err == ASCIICHAT_OK) {
186 int candidate_width = term_renderer_width_px(candidate);
187 int candidate_height = term_renderer_height_px(candidate);
188 if (candidate_width <= actual_width_px && candidate_height <= actual_height_px &&
189 (candidate_width < actual_width_px || candidate_height < actual_height_px)) {
191 ctx->renderer = candidate;
192 actual_width_px = candidate_width;
193 actual_height_px = candidate_height;
194 } else {
195 term_renderer_destroy(candidate);
196 }
197 } else {
199 SAFE_FREE(ctx);
200 return err;
201 }
202 }
203 log_info("GRID DIMENSIONS: %ux%u cells -> PIXEL DIMENSIONS: %dx%d px", cols, rows, actual_width_px, actual_height_px);
204 log_debug("term_renderer created (%dx%d cells, %dx%d px)", cols, rows, actual_width_px, actual_height_px);
205
206 if (ctx->kitty_only) {
207 ctx->fps = fps;
208 ctx->progress = render_progress_create(true, "terminal");
209 NAMED_REGISTER_CONTEXT(ctx, "render_file_ctx", output_path, NULL);
211 *out = ctx;
212 return ASCIICHAT_OK;
213 }
214 int output_width_px = actual_width_px;
215 int output_height_px = actual_height_px;
216 if (automatic_video_size &&
217 (output_width_px > RENDER_AUTO_VIDEO_MAX_DIMENSION || output_height_px > RENDER_AUTO_VIDEO_MAX_DIMENSION)) {
218 double scale_x = (double)RENDER_AUTO_VIDEO_MAX_DIMENSION / output_width_px;
219 double scale_y = (double)RENDER_AUTO_VIDEO_MAX_DIMENSION / output_height_px;
220 double scale = scale_x < scale_y ? scale_x : scale_y;
221 output_width_px = ((int)(output_width_px * scale)) & ~1;
222 output_height_px = ((int)(output_height_px * scale)) & ~1;
223 if (output_width_px < 2)
224 output_width_px = 2;
225 if (output_height_px < 2)
226 output_height_px = 2;
227 }
228 err = ffmpeg_encoder_create_scaled(output_path, actual_width_px, actual_height_px, output_width_px, output_height_px,
229 fps, &ctx->encoder);
230 if (err != ASCIICHAT_OK) {
231 log_error("ffmpeg_encoder_create failed: %s", asciichat_error_format(NULL, err));
233 SAFE_FREE(ctx);
234 return err;
235 }
236 log_debug("ffmpeg_encoder created successfully");
237
238 // Initialize audio fields
239 ctx->audio_sample_rate = 48000; // Audio pipeline is 48kHz
240 ctx->fps = fps;
241 // Allocate large buffer for snapshot mode (5 seconds at 48kHz with ~14 FPS = 3,428 samples per frame)
242 // Use 8192 to comfortably handle snapshot reads without excessive overhead
243 ctx->audio_buf_size = 8192; // Temporary buffer for reading audio (in floats)
244 ctx->audio_read_buf = SAFE_MALLOC(ctx->audio_buf_size * sizeof(float), float *);
245
247 if (err != ASCIICHAT_OK) {
251 SAFE_FREE(ctx);
252 return err;
253 }
254 log_info("initialized encoder for %s", output_path);
255
256 // Register render file context for debugging
257 NAMED_REGISTER_CONTEXT(ctx, "render_file_ctx", output_path, NULL);
258
259 bool binary_stdout = strcmp(output_path, "-") == 0 || strcmp(output_path, "pipe:") == 0;
260 ctx->progress = render_progress_create(!binary_stdout, output_path);
262 *out = ctx;
263 return ASCIICHAT_OK;
264}
const char * asciichat_error_format(const char *message, asciichat_error_t code)
#define SET_ERRNO(code,...)
terminal_background_t colorscheme_current_background(void)
@ TERM_BACKGROUND_DARK
Definition colorscheme.h:67
asciichat_error_t ffmpeg_encoder_create_scaled(const char *output_path, int source_width, int source_height, int width_px, int height_px, int fps, ffmpeg_encoder_t **out)
asciichat_error_t ffmpeg_encoder_destroy(ffmpeg_encoder_t *enc)
#define SAFE_STRNCPY(dst, src, size)
Definition common.h:423
#define SAFE_FREE(ptr)
Definition common.h:377
#define SAFE_MALLOC(size, cast)
Definition common.h:265
#define SAFE_GETENV(name)
Definition common.h:449
#define SAFE_CALLOC(count, size, cast)
Definition common.h:275
#define PLATFORM_MAX_PATH_LENGTH
Definition common.h:95
unsigned char uint8_t
Definition common.h:60
#define NAMED_REGISTER_CONTEXT(context, context_type, name, parent_ptr)
Register a generic context with automatic format specifier.
asciichat_error_t
Error and exit codes - unified status values (0-255).
Definition error_codes.h:44
@ ASCIICHAT_OK
Definition error_codes.h:46
@ ERROR_INVALID_PARAM
@ ERROR_NOT_SUPPORTED
Definition error_codes.h:69
#define log_warn(...)
Log a WARN message.
Definition log/log.h:621
#define log_error(...)
Log an ERROR message.
Definition log/log.h:634
#define log_info(...)
Log an INFO message.
Definition log/log.h:608
#define log_debug(...)
Log a DEBUG message.
Definition log/log.h:595
#define GET_OPTION(field)
Safely get a specific option field (lock-free read).
Definition options.h:1336
const size_t g_font_matrix_resurrected_size
const unsigned char g_font_matrix_resurrected[]
const size_t g_font_default_size
const unsigned char g_font_default[]
int term_renderer_width_px(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)
int term_renderer_height_px(terminal_renderer_t *r)
static bool size_t gotel_query_t * out
Definition queries.h:8
asciichat_error_t audio_recording_create_channels(audio_recording_t **out, unsigned channels)
Definition recording.c:34
render_progress_t * render_progress_create(bool enabled, const char *destination)
#define RENDER_AUTO_VIDEO_MAX_DIMENSION
Definition renderer.c:24
#define RENDER_AUTO_FONT_SIZE_PT
Definition renderer.c:23
#define RENDER_AUTO_MIN_FONT_SIZE_PT
Definition renderer.c:25
struct terminal_renderer_s terminal_renderer_t
Definition renderer.h:18
struct render_file_ctx_s render_file_ctx_t
Definition renderer.h:63
asciichat_error_t error
Definition renderer.c:34
terminal_renderer_t * renderer
Definition renderer.c:35
ffmpeg_encoder_t * encoder
Definition renderer.c:36
uint32_t audio_sample_rate
Definition renderer.c:47
audio_recording_t * recording
Definition renderer.c:37
float * audio_read_buf
Definition renderer.c:49
char font_spec[PLATFORM_MAX_PATH_LENGTH]
Definition renderer.h:38
void ui_controller_title_render(ui_title_render_t phase)
@ UI_TITLE_RENDERING

References asciichat_error_format(), ASCIICHAT_OK, render_file_ctx_s::audio_buf_size, render_file_ctx_s::audio_read_buf, audio_recording_create_channels(), render_file_ctx_s::audio_sample_rate, colorscheme_current_background(), render_file_ctx_s::discard, render_file_ctx_s::encoder, render_file_ctx_s::error, ERROR_INVALID_PARAM, ERROR_NOT_SUPPORTED, ffmpeg_encoder_create_scaled(), ffmpeg_encoder_destroy(), term_renderer_config_t::font_size_pt, term_renderer_config_t::font_spec, render_file_ctx_s::fps, g_font_default, g_font_default_size, g_font_matrix_resurrected, g_font_matrix_resurrected_size, GET_OPTION, render_file_ctx_s::kitty, render_file_ctx_s::kitty_only, log_debug, log_error, log_info, log_warn, NAMED_REGISTER_CONTEXT, out, PLATFORM_MAX_PATH_LENGTH, render_file_ctx_s::progress, render_file_ctx_s::recording, RENDER_AUTO_FONT_SIZE_PT, RENDER_AUTO_MIN_FONT_SIZE_PT, RENDER_AUTO_VIDEO_MAX_DIMENSION, render_progress_create(), render_file_ctx_s::renderer, SAFE_CALLOC, SAFE_FREE, SAFE_GETENV, SAFE_MALLOC, SAFE_STRNCPY, SET_ERRNO, TERM_BACKGROUND_DARK, term_renderer_create(), term_renderer_destroy(), term_renderer_height_px(), TERM_RENDERER_THEME_AUTO, TERM_RENDERER_THEME_DARK, TERM_RENDERER_THEME_LIGHT, term_renderer_width_px(), ui_controller_title_render(), and UI_TITLE_RENDERING.

Referenced by session_display_init_render_file().

◆ render_file_destroy()

asciichat_error_t render_file_destroy ( render_file_ctx_t * ctx)

Definition at line 375 of file renderer.c.

375 {
376 if (!ctx)
377 return ASCIICHAT_OK;
378 if (ctx->discard) {
379 SAFE_FREE(ctx);
380 return ASCIICHAT_OK;
381 }
382 NAMED_UNREGISTER(ctx);
385 uint64_t target = ctx->audio_target;
387 while (ctx->audio_samples_written < target) {
388 int count = (int)((target - ctx->audio_samples_written) > (uint64_t)ctx->audio_buf_size
390 : target - ctx->audio_samples_written);
392 err = ffmpeg_encoder_write_audio(ctx->encoder, ctx->audio_read_buf, count);
393 if (err != ASCIICHAT_OK)
394 break;
395 ctx->audio_samples_written += count;
396 }
399 if (err == ASCIICHAT_OK)
400 err = close_err;
401 if (ctx->error != ASCIICHAT_OK)
402 err = ctx->error;
404 if (err == ASCIICHAT_OK && ctx->kitty && ctx->frames_written == 0)
405 err = SET_ERRNO(ERROR_MEDIA_INIT, "No snapshot was rendered for Kitty output");
406 if (err == ASCIICHAT_OK && ctx->kitty && ctx->frames_written > 0)
412 SAFE_FREE(ctx);
413 return err;
414}
asciichat_error_t ffmpeg_encoder_write_audio(ffmpeg_encoder_t *enc, const float *samples, int num_samples)
unsigned long long uint64_t
Definition common.h:63
#define NAMED_UNREGISTER(ptr)
Unregister a pointer.
@ ERROR_MEDIA_INIT
Definition error_codes.h:65
asciichat_error_t kitty_image_write(FILE *stream, const uint8_t *rgba, int width, int height, int pitch)
const uint8_t * term_renderer_pixels(terminal_renderer_t *r)
int term_renderer_pitch(terminal_renderer_t *r)
void audio_recording_read(audio_recording_t *recording, float *samples, size_t count)
Definition recording.c:127
void audio_recording_destroy(audio_recording_t *recording)
Definition recording.c:54
void render_progress_finalize(render_progress_t *progress)
void render_progress_destroy(render_progress_t *progress)
uint64_t audio_samples_written
Definition renderer.c:39
uint64_t frames_written
Definition renderer.c:38
uint64_t audio_target
Definition renderer.c:42
@ UI_TITLE_FINALIZING
@ UI_TITLE_FAILED
@ UI_TITLE_IDLE

References ASCIICHAT_OK, render_file_ctx_s::audio_buf_size, render_file_ctx_s::audio_read_buf, audio_recording_destroy(), audio_recording_read(), render_file_ctx_s::audio_samples_written, render_file_ctx_s::audio_target, render_file_ctx_s::discard, render_file_ctx_s::encoder, render_file_ctx_s::error, ERROR_MEDIA_INIT, ffmpeg_encoder_destroy(), ffmpeg_encoder_write_audio(), render_file_ctx_s::frames_written, render_file_ctx_s::kitty, kitty_image_write(), NAMED_UNREGISTER, render_file_ctx_s::progress, render_file_ctx_s::recording, render_progress_destroy(), render_progress_finalize(), render_file_ctx_s::renderer, SAFE_FREE, SET_ERRNO, term_renderer_destroy(), term_renderer_height_px(), term_renderer_pitch(), term_renderer_pixels(), term_renderer_width_px(), ui_controller_title_render(), UI_TITLE_FAILED, UI_TITLE_FINALIZING, and UI_TITLE_IDLE.

Referenced by session_display_finish_render_file().

◆ render_file_set_live_timing()

void render_file_set_live_timing ( render_file_ctx_t * ctx)

Definition at line 266 of file renderer.c.

266 {
267 if (ctx) {
268 ctx->live_timing = true;
269 if (ctx->encoder)
271 }
272}
void ffmpeg_encoder_set_live_timing(ffmpeg_encoder_t *enc)

References render_file_ctx_s::encoder, ffmpeg_encoder_set_live_timing(), and render_file_ctx_s::live_timing.

Referenced by session_display_render_frame(), and session_display_set_render_live_timing().

◆ render_file_set_snapshot_actual_duration()

void render_file_set_snapshot_actual_duration ( render_file_ctx_t * ctx,
double actual_duration_sec )

Definition at line 369 of file renderer.c.

369 {
370 if (!ctx || !ctx->encoder)
371 return;
372 ffmpeg_encoder_set_snapshot_actual_duration(ctx->encoder, actual_duration_sec);
373}
void ffmpeg_encoder_set_snapshot_actual_duration(ffmpeg_encoder_t *enc, double actual_duration_sec)

References render_file_ctx_s::encoder, and ffmpeg_encoder_set_snapshot_actual_duration().

Referenced by session_display_set_snapshot_actual_duration().

◆ render_file_write_frame()

asciichat_error_t render_file_write_frame ( render_file_ctx_t * ctx,
const char * ansi_frame,
uint64_t captured_ns )

Definition at line 348 of file renderer.c.

348 {
349 if (!ctx || !frame)
350 return SET_ERRNO(ERROR_INVALID_PARAM, "Missing render context or frame");
351 if (ctx->error != ASCIICHAT_OK)
352 return ctx->error;
353 if (ctx->discard)
354 return ASCIICHAT_OK;
355 asciichat_error_t err = render_file_write_frame_internal(ctx, frame, captured_ns);
356 if (err != ASCIICHAT_OK)
358 render_progress_frame(ctx->progress, frame, err == ASCIICHAT_OK);
359 if (err != ASCIICHAT_OK)
360 ctx->error = err;
361 return err;
362}
void render_progress_frame(render_progress_t *progress, const char *frame, bool succeeded)

References ASCIICHAT_OK, render_file_ctx_s::discard, render_file_ctx_s::error, ERROR_INVALID_PARAM, render_file_ctx_s::progress, render_progress_frame(), SET_ERRNO, ui_controller_title_render(), and UI_TITLE_FAILED.

Referenced by session_display_encode_frame(), and session_display_render_frame().

◆ term_renderer_create()

asciichat_error_t term_renderer_create ( const term_renderer_config_t * cfg,
terminal_renderer_t ** out )

Definition at line 314 of file media/render/terminal.c.

314 {
315 if (!cfg || !out || cfg->cols <= 0 || cfg->rows <= 0)
316 return SET_ERRNO(ERROR_INVALID_PARAM, "Missing font renderer configuration or output");
317 *out = NULL;
318 if (cfg->font_data_size > LONG_MAX || (cfg->font_data_size && !cfg->font_data))
319 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid font data or size");
321 r->cols = cfg->cols;
322 r->rows = cfg->rows;
323 r->theme = cfg->theme;
324
325 if (FT_Init_FreeType(&r->ft_lib)) {
326 SAFE_FREE(r);
327 return SET_ERRNO(ERROR_INIT, "FreeType init failed");
328 }
329
330 FT_Error face_error;
331 if (cfg->font_data && cfg->font_data_size > 0) {
332 face_error = FT_New_Memory_Face(r->ft_lib, cfg->font_data, (FT_Long)cfg->font_data_size, 0, &r->ft_face);
333 } else {
334 face_error = FT_New_Face(r->ft_lib, cfg->font_spec, 0, &r->ft_face);
335 }
336 if (face_error) {
337 FT_Done_FreeType(r->ft_lib);
338 SAFE_FREE(r);
339 if (face_error == FT_Err_Cannot_Open_Resource)
340 return SET_ERRNO(ERROR_NOT_FOUND, "Cannot open font '%s' (FreeType error %d)", cfg->font_spec, face_error);
341 // Older FreeType versions recognize an HVF SFNT wrapper, then reject its
342 // missing TrueType outline tables instead of reporting an unknown format.
343 if (face_error == FT_Err_Unknown_File_Format || face_error == FT_Err_Unimplemented_Feature ||
344 face_error == FT_Err_Locations_Missing)
346 "Unsupported font format: '%s' (FreeType error %d). HVF fonts require HVF-enabled FreeType "
347 "2.15+ on macOS 15.4+ or iOS 18.4+",
348 cfg->font_spec, face_error);
349 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid font '%s' (FreeType error %d)", cfg->font_spec, face_error);
350 }
351
352 // Some older FreeType drivers can open an SFNT container without supporting
353 // its outlines. Check the parsed table directory before accepting that face.
354 FT_ULong hvf_size = 0;
355 bool is_hvf = FT_IS_SFNT(r->ft_face) &&
356 FT_Load_Sfnt_Table(r->ft_face, FT_MAKE_TAG('h', 'v', 'g', 'l'), 0, NULL, &hvf_size) == 0;
357 const char *font_format = FT_Get_Font_Format(r->ft_face);
358 if (is_hvf) {
359 bool supported = false;
360#ifdef __APPLE__
361 if (__builtin_available(macOS 15.4, iOS 18.4, *))
362 supported = font_format && strcmp(font_format, "HVF") == 0;
363#endif
364 if (!supported) {
365 FT_Done_Face(r->ft_face);
366 FT_Done_FreeType(r->ft_lib);
367 SAFE_FREE(r);
369 "Unsupported HVF font '%s': requires HVF-enabled FreeType 2.15+ on macOS 15.4+ or iOS 18.4+",
370 cfg->font_spec);
371 }
372 }
373 log_debug("Loaded font '%s' using FreeType format %s", cfg->font_spec, font_format ? font_format : "unknown");
374
375 // Detect if this is the matrix font (needs character mapping to Private Use Area)
376 r->is_matrix_font = (strstr(cfg->font_spec, "matrix") != NULL || strstr(cfg->font_spec, "Matrix") != NULL) ||
377 (cfg->font_data != NULL && GET_OPTION(matrix_rain));
378 if (r->is_matrix_font) {
379 log_debug("Detected matrix font - will use Private Use Area character mapping");
380 }
381
382 // Handle scalable vs bitmap fonts differently
383 // Bitmap fonts (like matrix) don't respond to FT_Set_Char_Size
384 // Instead, use FT_Set_Pixel_Sizes to request a specific pixel size
385 if (r->ft_face->num_fixed_sizes > 0) {
386 // Bitmap font: use FT_Set_Pixel_Sizes to select appropriate size (12px is reasonable for ASCII)
387 int err = FT_Set_Pixel_Sizes(r->ft_face, 12, 12);
388 log_debug("[BITMAP] FT_Set_Pixel_Sizes(12, 12) returned %d", err);
389
390 // If that fails, try FT_Select_Size as fallback
391 if (err != 0) {
392 err = FT_Select_Size(r->ft_face, 0); // Use first available strike
393 log_debug("[BITMAP FALLBACK] FT_Select_Size(0) returned %d", err);
394 }
395 if (err != 0) {
396 FT_Done_Face(r->ft_face);
397 FT_Done_FreeType(r->ft_lib);
398 SAFE_FREE(r);
399 return SET_ERRNO(ERROR_NOT_SUPPORTED, "Font '%s' has no usable bitmap size (FreeType error %d)", cfg->font_spec,
400 err);
401 }
402 } else {
403 // Scalable font: use FT_Set_Char_Size
404 // FT_Set_Char_Size takes 1/64pt units and DPI — supports fractional point sizes.
405 // 96 DPI is the standard screen DPI used here; the 64 factor is the 26.6 fixed-point scale.
406 int err = FT_Set_Char_Size(r->ft_face, 0, (FT_F26Dot6)(cfg->font_size_pt * 64.0), 96, 96);
407 log_debug("[SCALABLE] FT_Set_Char_Size(size_pt=%.1f → %ld 1/64pt) returned %d", cfg->font_size_pt,
408 (long)(cfg->font_size_pt * 64.0), err);
409 if (err != 0) {
410 FT_Done_Face(r->ft_face);
411 FT_Done_FreeType(r->ft_lib);
412 SAFE_FREE(r);
413 return SET_ERRNO(ERROR_NOT_SUPPORTED, "Font '%s' cannot be sized (FreeType error %d)", cfg->font_spec, err);
414 }
415 }
416
417 // For matrix fonts, try PUA glyph first (U+E900) since ASCII chars might not have bitmap data
418 // For other fonts, start with 'M'
419 int load_err;
420 if (r->is_matrix_font) {
421 log_debug("Matrix font detected: trying PUA glyph U+E900 first");
422 load_err = FT_Load_Char(r->ft_face, 0xE900, FT_LOAD_RENDER);
423 log_debug("FT_Load_Char(U+E900) returned %d", load_err);
424
425 // If PUA glyph fails, try ASCII fallback
426 if (load_err != 0) {
427 log_debug("U+E900 failed, trying fallback character 'M'");
428 load_err = FT_Load_Char(r->ft_face, 'M', FT_LOAD_RENDER);
429 log_debug("FT_Load_Char('M') returned %d", load_err);
430 }
431 } else {
432 // For non-matrix fonts, standard approach
433 load_err = FT_Load_Char(r->ft_face, 'M', FT_LOAD_RENDER);
434 log_debug("FT_Load_Char('M', FT_LOAD_RENDER) returned %d", load_err);
435
436 // For bitmap fonts (like matrix), FT_Load_Char may fail if the character doesn't exist.
437 // Try a fallback character if 'M' fails.
438 if (load_err != 0 && r->ft_face->num_fixed_sizes > 0) {
439 log_debug("FT_Load_Char('M') failed for bitmap font, trying fallback character 'A'");
440 load_err = FT_Load_Char(r->ft_face, 'A', FT_LOAD_RENDER);
441 log_debug("FT_Load_Char('A') returned %d", load_err);
442
443 if (load_err != 0) {
444 // Last resort: try a PUA glyph for unknown bitmap fonts
445 log_debug("FT_Load_Char('A') also failed, trying PUA glyph U+E900");
446 load_err = FT_Load_Char(r->ft_face, 0xE900, FT_LOAD_RENDER);
447 log_debug("FT_Load_Char(U+E900) returned %d", load_err);
448 }
449 }
450 }
451
452 if (load_err != 0) {
453 FT_Done_Face(r->ft_face);
454 FT_Done_FreeType(r->ft_lib);
455 SAFE_FREE(r);
456 return SET_ERRNO(ERROR_NOT_SUPPORTED, "Font '%s' cannot render glyphs (FreeType error %d)", cfg->font_spec,
457 load_err);
458 }
459
460 // For monospace ASCII grid: use advance.x (proper character spacing) and rendered height
461 FT_Pos advance_x_26_6 = r->ft_face->glyph->advance.x;
462 log_debug("advance.x (26.6pt)=%ld", advance_x_26_6);
463
464 // Calculate cell width from font metrics
465 r->cell_w = (int)(advance_x_26_6 >> 6);
466
467 // Safeguard: for bitmap fonts, cell_w might be wrong if the font metrics are off
468 // Ensure we have a reasonable minimum width
469 if (r->cell_w <= 0) {
470 log_warn("cell_w is %d (invalid), using default 10px", r->cell_w);
471 r->cell_w = 10;
472 }
473
474 // Only add spacing to fill entire frame if --stretch is specified
475 bool stretch_mode = GET_OPTION(stretch);
476 if (stretch_mode) {
477 r->cell_w++;
478 log_debug("ADVANCE_X: value=%ld (26.6pt) → cell_w=%d (advance + 1 for stretch), cols=%d → width_px=%d",
479 advance_x_26_6, r->cell_w, r->cols, r->cols * r->cell_w);
480 } else {
481 log_debug("ADVANCE_X: value=%ld (26.6pt) → cell_w=%d (preserve aspect ratio), cols=%d → width_px=%d",
482 advance_x_26_6, r->cell_w, r->cols, r->cols * r->cell_w);
483 }
484
485 // For both bitmap and scalable fonts, use the actual glyph bitmap height
486 // This ensures text doesn't overflow cells. Using size->metrics.height (line spacing)
487 // instead of bitmap.rows causes cells to be too large and text to overflow.
488 r->cell_h = r->ft_face->glyph->bitmap.rows;
489 int raw_bitmap_top = r->ft_face->glyph->bitmap_top;
490 log_debug("cell_h=%d (from bitmap.rows), raw_bitmap_top=%d", r->cell_h, raw_bitmap_top);
491
492 // Fallback: if cell_h is 0 (bitmap failed to load), use aspect ratio calculation
493 if (r->cell_h <= 0) {
494 log_warn("cell_h is 0 (FT_Load_Char failed or no bitmap), using aspect ratio fallback");
495 r->cell_h = r->cell_w * 2;
496 log_debug("FALLBACK: cell_h set to %d (2x width=%d)", r->cell_h, r->cell_w);
497 }
498
499 // Apply aspect ratio correction unless --stretch is specified
500 // Terminal characters are typically 2:1 (height:width) to appear normal
501 // BUT: only correct if the loaded glyph height is reasonably close (±30%)
502 // This prevents huge jumps from small glyphs (e.g., 8px → 32px)
503 if (!stretch_mode && r->cell_h > 0) {
504 int corrected_h = r->cell_w * 2;
505 int min_acceptable = (corrected_h * 70) / 100; // 70% of target
506 int max_acceptable = (corrected_h * 130) / 100; // 130% of target
507
508 if (r->cell_h < min_acceptable || r->cell_h > max_acceptable) {
509 // Loaded height is far from expected, apply correction
510 log_debug("ASPECT_RATIO: Correcting cell_h from %d to %d (out of range [%d,%d], 2x width=%d)", r->cell_h,
511 corrected_h, min_acceptable, max_acceptable, r->cell_w);
512 r->cell_h = corrected_h;
513 } else if (corrected_h != r->cell_h) {
514 // Loaded height is close to expected, fine-tune by small amount only
515 int delta = corrected_h - r->cell_h;
516 if (delta > 4 || delta < -4) {
517 // Only apply if difference is more than 4px
518 log_debug("ASPECT_RATIO: Fine-tuning cell_h from %d to %d (within acceptable range, 2x width=%d)", r->cell_h,
519 corrected_h, r->cell_w);
520 r->cell_h = corrected_h;
521 } else {
522 log_debug("ASPECT_RATIO: Keeping cell_h=%d (close to target %d, delta=%d)", r->cell_h, corrected_h, delta);
523 }
524 }
525 }
526
527 // Position baseline vertically within the cell.
528 // Baseline should be positioned at approximately 75% of cell height to allow proper
529 // glyph rendering with space both above and below. This prevents glyphs from being
530 // clipped at the cell boundaries.
531 //
532 // Baseline position: y_glyph = py + r->baseline - g->bitmap_top
533 // We want: 0 <= baseline - bitmap_top < cell_h (glyph fits within cell)
534 // Choose: baseline = (3 * cell_h) / 4 (positions at 75% height)
535 r->baseline = (3 * r->cell_h) / 4;
536 log_debug("baseline set to %d (75%% of cell_h=%d)", r->baseline, r->cell_h);
537
538 r->width_px = r->cols * r->cell_w;
539 r->height_px = r->rows * r->cell_h;
540 log_debug("calculated dimensions: width_px=%d (cols=%d * cell_w=%d), height_px=%d (rows=%d * cell_h=%d)", r->width_px,
541 r->cols, r->cell_w, r->height_px, r->rows, r->cell_h);
542 // Pitch in bytes: each pixel is 4 bytes (RGBA), already aligned to 4-byte boundary
543 int base_pitch = r->width_px * 4;
544 r->pitch = base_pitch;
545 log_debug("PITCH_CALC: width_px=%d, base_pitch=%d, final_pitch=%d (RGBA, no padding needed)", r->width_px, base_pitch,
546 r->pitch);
547 log_debug("Allocating framebuffer: %d bytes total (%d pitch * %d height)", (int)((size_t)r->pitch * r->height_px),
548 r->pitch, r->height_px);
549 r->framebuffer = SAFE_MALLOC((size_t)r->pitch * r->height_px, uint8_t *);
550
551 r->vt = vterm_new(r->rows, r->cols);
552 r->vts = vterm_obtain_screen(r->vt);
553
554 // Enable UTF-8 encoding so vterm correctly parses multi-byte characters
555 vterm_set_utf8(r->vt, 1);
556
557 // Set size BEFORE reset to ensure screen is allocated with correct dimensions
558 vterm_set_size(r->vt, r->rows, r->cols);
559 log_debug("vterm_set_size called with rows=%d cols=%d", r->rows, r->cols);
560
561 vterm_screen_set_callbacks(r->vts, &g_vterm_cbs, r);
562 // Reset MUST come before any input writing to initialize state correctly
563 vterm_screen_reset(r->vts, 1);
564
565 // Verify the size was set correctly after reset
566 int vt_rows, vt_cols;
567 vterm_get_size(r->vt, &vt_rows, &vt_cols);
568 log_debug("vterm_get_size returned rows=%d cols=%d (expected %d,%d)", vt_rows, vt_cols, r->rows, r->cols);
569
570 // CRITICAL: Set size AGAIN after reset, as reset may have affected it
571 vterm_set_size(r->vt, r->rows, r->cols);
572 log_debug("vterm_set_size called AGAIN after reset");
573
574 // Initialize per-renderer glyph cache (empty, will populate on demand)
575 r->glyph_cache = NULL;
576
577 *out = r;
578 return ASCIICHAT_OK;
579}
@ ERROR_NOT_FOUND
@ ERROR_INIT
Definition error_codes.h:56
const uint8_t * font_data
Definition renderer.h:40
term_renderer_theme_t theme
Definition renderer.h:37
term_renderer_theme_t theme
glyph_cache_entry_t * glyph_cache

References ASCIICHAT_OK, terminal_renderer_s::baseline, terminal_renderer_s::cell_h, terminal_renderer_s::cell_w, term_renderer_config_t::cols, terminal_renderer_s::cols, ERROR_INIT, ERROR_INVALID_PARAM, ERROR_NOT_FOUND, ERROR_NOT_SUPPORTED, term_renderer_config_t::font_data, term_renderer_config_t::font_data_size, term_renderer_config_t::font_size_pt, term_renderer_config_t::font_spec, terminal_renderer_s::framebuffer, terminal_renderer_s::ft_face, terminal_renderer_s::ft_lib, GET_OPTION, terminal_renderer_s::glyph_cache, terminal_renderer_s::height_px, terminal_renderer_s::is_matrix_font, log_debug, log_warn, out, terminal_renderer_s::pitch, term_renderer_config_t::rows, terminal_renderer_s::rows, SAFE_CALLOC, SAFE_FREE, SAFE_MALLOC, SET_ERRNO, term_renderer_config_t::theme, terminal_renderer_s::theme, terminal_renderer_s::vt, terminal_renderer_s::vts, and terminal_renderer_s::width_px.

Referenced by render_file_create(), and TerminalRendererBinding::TerminalRendererBinding().

◆ term_renderer_destroy()

void term_renderer_destroy ( terminal_renderer_t * r)

Definition at line 947 of file media/render/terminal.c.

947 {
948 if (!r)
949 return;
950 vterm_free(r->vt);
951 if (r->zalgo_face)
952 FT_Done_Face(r->zalgo_face);
953 glyph_cache_destroy(&r->zalgo_cache);
954 FT_Done_Face(r->ft_face);
955 FT_Done_FreeType(r->ft_lib);
957 glyph_cache_destroy(&r->glyph_cache);
958 for (size_t i = 0; i < CELL_CACHE_SIZE; i++)
959 SAFE_FREE(r->cells[i].pixels);
960 SAFE_FREE(r);
961}
#define CELL_CACHE_SIZE
rendered_cell_t cells[512]
glyph_cache_entry_t * zalgo_cache

References CELL_CACHE_SIZE, terminal_renderer_s::cells, terminal_renderer_s::framebuffer, terminal_renderer_s::ft_face, terminal_renderer_s::ft_lib, terminal_renderer_s::glyph_cache, rendered_cell_t::pixels, SAFE_FREE, terminal_renderer_s::vt, terminal_renderer_s::zalgo_cache, and terminal_renderer_s::zalgo_face.

Referenced by render_file_create(), render_file_destroy(), and TerminalRendererBinding::~TerminalRendererBinding().

◆ term_renderer_feed()

asciichat_error_t term_renderer_feed ( terminal_renderer_t * r,
const char * ansi_frame,
size_t len )

Definition at line 653 of file media/render/terminal.c.

653 {
654 if (!r || (!ansi_frame && len > 0)) {
655 return SET_ERRNO(ERROR_INVALID_PARAM, "renderer or frame is invalid");
656 }
657
658 uint64_t stage_start_ns = time_get_ns();
659 // libvterm natively implements CSI REP. Expanding those runs here turns a
660 // compact network frame into synchronous browser-main-thread work before
661 // every paint.
662 const char *frame = ansi_frame;
663 const size_t frame_len = len;
664
665 // Clear framebuffer to ensure no leftover pixels from previous frames
666 uint8_t def_fg_r, def_fg_g, def_fg_b;
667 uint8_t def_bg_r, def_bg_g, def_bg_b;
668 terminal_get_default_foreground_color(r->theme, &def_fg_r, &def_fg_g, &def_fg_b);
669 terminal_get_default_background_color(r->theme, &def_bg_r, &def_bg_g, &def_bg_b);
670 uint8_t def_bg = def_bg_r;
671 memset(r->framebuffer, def_bg, (size_t)r->pitch * r->height_px);
672
673 // Reset scrolling region and cursor position to prevent vertical shifts
674 static const char reset_sequence[] = "\033[2J\033[H\033[r\033[?7l";
675 vterm_input_write(r->vt, reset_sequence, sizeof(reset_sequence) - 1);
676
677 // Count newlines to determine CRLF conversion size
678 int newline_count = 0;
679 for (size_t i = 0; i < frame_len; i++) {
680 if (frame[i] == '\n')
681 newline_count++;
682 }
683
684 // Convert LF-only line endings to CRLF (vterm expects CRLF)
685 // This fixes cursor positioning on alternate rows
686 char *fixed_frame = SAFE_MALLOC(frame_len + (size_t)newline_count, char *);
687 if (!fixed_frame) {
688 log_error("failed to allocate memory for CRLF conversion");
689 return SET_ERRNO(ERROR_MEMORY, "CRLF conversion allocation failed");
690 }
691
692 size_t fixed_pos = 0;
693 int frame_row = 0;
694 for (size_t i = 0; i < frame_len; i++) {
695 // A video frame is a fixed grid, not a scrolling terminal transcript.
696 // A trailing newline on the bottom row must not scroll the whole image.
697 if (frame[i] == '\n' && frame_row >= r->rows - 1) {
698 continue;
699 }
700 fixed_frame[fixed_pos++] = frame[i];
701 if (frame[i] == '\n' && (i == 0 || frame[i - 1] != '\r')) {
702 // Insert carriage return before LF if not already preceded by CR
703 fixed_frame[fixed_pos - 1] = '\r';
704 fixed_frame[fixed_pos++] = '\n';
705 }
706 if (frame[i] == '\n')
707 frame_row++;
708 }
709
710 vterm_input_write(r->vt, fixed_frame, fixed_pos);
711 uint64_t parsed_ns = time_get_ns();
712 SAFE_FREE(fixed_frame);
713
714 // Calculate centering offsets to position grid in center of canvas
715 int grid_width_px = r->cols * r->cell_w;
716 int grid_height_px = r->rows * r->cell_h;
717 int offset_x = (r->width_px - grid_width_px) / 2;
718 int offset_y = (r->height_px - grid_height_px) / 2;
719
720 static int frame_count = 0;
722 0}; // Store signatures of previous frame's rows
723 frame_count++;
724
725 // Log if any row signature changed to detect vertical shifts
726 if (frame_count % 10 == 0) {
728 for (int row = 0; row < r->rows && row < RENDER_DEBUG_SIGNATURE_ROWS; row++) {
729 // Get first 4 chars as signature
730 for (int col = 0; col < RENDER_DEBUG_SIGNATURE_COLUMNS && col < r->cols; col++) {
731 VTermScreenCell cell;
732 vterm_screen_get_cell(r->vts, (VTermPos){row, col}, &cell);
733 curr_row_sigs[row][col] = (cell.chars[0] >= 32 && cell.chars[0] <= 126) ? cell.chars[0] : '.';
734 }
735 curr_row_sigs[row][RENDER_DEBUG_SIGNATURE_COLUMNS] = 0;
736 }
737 memcpy(prev_row_sigs, curr_row_sigs, sizeof(curr_row_sigs));
738 }
739
740 int glyph_rendered_count = 0;
741 int occupied_cell_count = 0;
742 // ANSI 16/256 colors are indexed. Converting them through libvterm for
743 // every cell is especially expensive in the single-threaded WASM renderer.
744 // A terminal palette has at most 256 entries and is stable for a frame.
745 uint32_t indexed_rgb[ASCII_LUMINANCE_LEVELS] = {0};
746 bool indexed_rgb_valid[ASCII_LUMINANCE_LEVELS] = {false};
747 for (int row = 0; row < r->rows; row++) {
748 for (int col = 0; col < r->cols; col++) {
749 VTermScreenCell cell;
750 vterm_screen_get_cell(r->vts, (VTermPos){row, col}, &cell);
751
752 uint8_t fr, fg, fb, br, bg, bb;
753 if (VTERM_COLOR_IS_RGB(&cell.fg)) {
754 fr = cell.fg.rgb.red;
755 fg = cell.fg.rgb.green;
756 fb = cell.fg.rgb.blue;
757 } else if (VTERM_COLOR_IS_INDEXED(&cell.fg)) {
758 const uint8_t index = cell.fg.indexed.idx;
759 if (!indexed_rgb_valid[index]) {
760 VTermColor foreground = cell.fg;
761 vterm_screen_convert_color_to_rgb(r->vts, &foreground);
762 indexed_rgb[index] =
763 ((uint32_t)foreground.rgb.red << 16) | ((uint32_t)foreground.rgb.green << 8) | foreground.rgb.blue;
764 indexed_rgb_valid[index] = true;
765 }
766 fr = (uint8_t)(indexed_rgb[index] >> 16);
767 fg = (uint8_t)(indexed_rgb[index] >> 8);
768 fb = (uint8_t)indexed_rgb[index];
769 } else {
770 fr = def_fg_r;
771 fg = def_fg_g;
772 fb = def_fg_b;
773 }
774
775 if (VTERM_COLOR_IS_RGB(&cell.bg)) {
776 br = cell.bg.rgb.red;
777 bg = cell.bg.rgb.green;
778 bb = cell.bg.rgb.blue;
779 } else if (VTERM_COLOR_IS_INDEXED(&cell.bg)) {
780 const uint8_t index = cell.bg.indexed.idx;
781 if (!indexed_rgb_valid[index]) {
782 VTermColor background = cell.bg;
783 vterm_screen_convert_color_to_rgb(r->vts, &background);
784 indexed_rgb[index] =
785 ((uint32_t)background.rgb.red << 16) | ((uint32_t)background.rgb.green << 8) | background.rgb.blue;
786 indexed_rgb_valid[index] = true;
787 }
788 br = (uint8_t)(indexed_rgb[index] >> 16);
789 bg = (uint8_t)(indexed_rgb[index] >> 8);
790 bb = (uint8_t)indexed_rgb[index];
791 } else {
792 br = def_bg_r;
793 bg = def_bg_g;
794 bb = def_bg_b;
795 }
796
797 int px = offset_x + col * r->cell_w, py = offset_y + row * r->cell_h;
798 uint32_t foreground = ((uint32_t)fr << 16) | ((uint32_t)fg << 8) | fb;
799 uint32_t background = ((uint32_t)br << 16) | ((uint32_t)bg << 8) | bb;
800 // Retain the low 32 bits without overflowing the intermediate products.
801 uint32_t cell_hash = (uint32_t)((uint64_t)cell.chars[0] * UINT64_C(2654435761) ^
802 (uint64_t)foreground * UINT64_C(2246822519) ^ background);
803 cell_hash ^= cell_hash >> 16;
804 rendered_cell_t *cached_cell = &r->cells[cell_hash % CELL_CACHE_SIZE];
805 bool contained = px >= 0 && py >= 0 && px + r->cell_w <= r->width_px && py + r->cell_h <= r->height_px;
806 if (contained && cached_cell->valid && cached_cell->character == cell.chars[0] &&
807 cached_cell->foreground == foreground && cached_cell->background == background) {
808 for (int dy = 0; dy < r->cell_h; dy++) {
809 memcpy(r->framebuffer + (py + dy) * r->pitch + px * 4, cached_cell->pixels + dy * r->cell_w * 4,
810 (size_t)r->cell_w * 4);
811 }
812 if (cell.chars[0] && cell.chars[0] != ' ') {
813 occupied_cell_count++;
814 glyph_rendered_count++;
815 }
816 continue;
817 }
818 // Block elements represent cell geometry, independent of font bearings and line spacing.
819 bool block = cell.chars[0] == 0x2580 || cell.chars[0] == 0x2584 || cell.chars[0] == 0x2588;
820 for (int dy = 0; dy < r->cell_h; dy++) {
821 bool block_foreground =
822 block && (cell.chars[0] == 0x2588 || (cell.chars[0] == 0x2580 ? dy < r->cell_h / 2 : dy >= r->cell_h / 2));
823 int y = py + dy;
824 if (y < 0 || y >= r->height_px)
825 continue;
826 uint8_t *line = r->framebuffer + y * r->pitch + px * 4;
827 for (int dx = 0; dx < r->cell_w; dx++) {
828 int x = px + dx;
829 if (x < 0 || x >= r->width_px)
830 continue;
831 line[dx * 4] = block_foreground ? fr : br;
832 line[dx * 4 + 1] = block_foreground ? fg : bg;
833 line[dx * 4 + 2] = block_foreground ? fb : bb;
834 line[dx * 4 + 3] = 255;
835 }
836 }
837
838 if (block) {
839 occupied_cell_count++;
840 glyph_rendered_count++;
841 } else if (cell.chars[0] && cell.chars[0] != ' ') {
842 occupied_cell_count++;
843 // For matrix font, map ASCII characters to Private Use Area glyphs (U+E900-U+E91A)
844 uint32_t char_to_render = r->is_matrix_font ? matrix_char_map(cell.chars[0]) : cell.chars[0];
845
846 // Use per-renderer glyph cache to avoid rasterizing every glyph every frame
847 FT_Bitmap *cached_bitmap = glyph_cache_get(r, r->ft_face, char_to_render);
848 if (cached_bitmap && cached_bitmap->width > 0 && cached_bitmap->rows > 0) {
849 // Find cache entry to get positioning offsets
850 glyph_cache_entry_t *cache_entry = NULL;
851 HASH_FIND_INT(r->glyph_cache, &char_to_render, cache_entry);
852 if (cache_entry) {
853 // Overhanging glyphs must retain the normal cross-cell drawing order.
854 contained = contained && cache_entry->bitmap_left >= 0 &&
855 cache_entry->bitmap_left + cache_entry->width <= r->cell_w &&
856 r->baseline - cache_entry->bitmap_top >= 0 &&
857 r->baseline - cache_entry->bitmap_top + cache_entry->rows <= r->cell_h;
858 blit_glyph(r, cached_bitmap, px + cache_entry->bitmap_left, py + r->baseline - cache_entry->bitmap_top, fr,
859 fg, fb, br, bg, bb);
860 glyph_rendered_count++;
861 }
862 } else if (cell.chars[0] >= 0x2800 && cell.chars[0] <= 0x28ff) {
863 // The bundled font lacks Braille. Draw its eight-dot pattern in the
864 // cell when the selected font cannot supply the requested glyph.
865 static const unsigned dot_bits[4][2] = {{0, 3}, {1, 4}, {2, 5}, {6, 7}};
866 unsigned pattern = cell.chars[0] - 0x2800;
867 int dot_w = r->cell_w / 4 > 0 ? r->cell_w / 4 : 1;
868 int dot_h = r->cell_h / 8 > 0 ? r->cell_h / 8 : 1;
869 for (int row = 0; row < 4; row++) {
870 for (int col = 0; col < 2; col++) {
871 if (!(pattern & (1U << dot_bits[row][col])))
872 continue;
873 int left = px + (2 * col + 1) * r->cell_w / 4 - dot_w / 2;
874 int top = py + (2 * row + 1) * r->cell_h / 8 - dot_h / 2;
875 for (int dy = 0; dy < dot_h; dy++) {
876 for (int dx = 0; dx < dot_w; dx++) {
877 int x = left + dx, y = top + dy;
878 if (x < px || x >= px + r->cell_w || y < py || y >= py + r->cell_h || x < 0 || x >= r->width_px ||
879 y < 0 || y >= r->height_px)
880 continue;
881 uint8_t *pixel = r->framebuffer + y * r->pitch + x * 4;
882 pixel[0] = fr;
883 pixel[1] = fg;
884 pixel[2] = fb;
885 pixel[3] = 255;
886 }
887 }
888 }
889 }
890 glyph_rendered_count++;
891 } else {
892 return SET_ERRNO(ERROR_NOT_SUPPORTED, "Font cannot render Unicode character U+%04X", cell.chars[0]);
893 }
894 }
895 if (contained) {
896 if (!cached_cell->pixels)
897 cached_cell->pixels = SAFE_MALLOC((size_t)r->cell_w * r->cell_h * 4, uint8_t *);
898 if (cached_cell->pixels) {
899 for (int dy = 0; dy < r->cell_h; dy++) {
900 memcpy(cached_cell->pixels + dy * r->cell_w * 4, r->framebuffer + (py + dy) * r->pitch + px * 4,
901 (size_t)r->cell_w * 4);
902 }
903 cached_cell->character = cell.chars[0];
904 cached_cell->foreground = foreground;
905 cached_cell->background = background;
906 cached_cell->valid = true;
907 }
908 }
909 }
910 }
911 render_zalgo(r, offset_x, offset_y);
912 uint64_t rendered_ns = time_get_ns();
913 log_debug_every(US_PER_SEC_INT, "RECORD_RASTER: input_bytes=%zu parse_ms=%.2f paint_ms=%.2f cells=%dx%d", len,
914 (double)(parsed_ns - stage_start_ns) / NS_PER_MS_INT,
915 (double)(rendered_ns - parsed_ns) / NS_PER_MS_INT, r->cols, r->rows);
916 log_debug_every(5 * US_PER_SEC_INT, "Rendered %d glyphs from %d occupied cells out of %d cells", glyph_rendered_count,
917 occupied_cell_count, r->rows * r->cols);
918
919 return ASCIICHAT_OK;
920}
unsigned int uint32_t
Definition common.h:62
@ ERROR_MEMORY
Definition error_codes.h:51
#define NS_PER_MS_INT
Definition time.h:168
#define US_PER_SEC_INT
Definition time.h:173
#define ASCII_LUMINANCE_LEVELS
Number of luminance levels supported (256).
Definition ascii.h:440
#define log_debug_every(interval_us, fmt,...)
Rate-limited DEBUG logging.
Definition log/log.h:749
#define RENDER_DEBUG_SIGNATURE_ROWS
#define RENDER_DEBUG_SIGNATURE_COLUMNS
#define RENDER_DEBUG_SIGNATURE_BUFFER_SIZE
static bool size_t index
Definition queries.h:8
Owns the temporary storage used by glyph cache entry. The data pointers and size or cursor fields des...
int bitmap_left
int bitmap_top
int rows
int width

References ASCII_LUMINANCE_LEVELS, ASCIICHAT_OK, rendered_cell_t::background, terminal_renderer_s::baseline, glyph_cache_entry_t::bitmap_left, glyph_cache_entry_t::bitmap_top, CELL_CACHE_SIZE, terminal_renderer_s::cell_h, terminal_renderer_s::cell_w, terminal_renderer_s::cells, rendered_cell_t::character, terminal_renderer_s::cols, ERROR_INVALID_PARAM, ERROR_MEMORY, ERROR_NOT_SUPPORTED, rendered_cell_t::foreground, terminal_renderer_s::framebuffer, terminal_renderer_s::ft_face, terminal_renderer_s::glyph_cache, terminal_renderer_s::height_px, index, terminal_renderer_s::is_matrix_font, log_debug_every, log_error, NS_PER_MS_INT, terminal_renderer_s::pitch, rendered_cell_t::pixels, RENDER_DEBUG_SIGNATURE_BUFFER_SIZE, RENDER_DEBUG_SIGNATURE_COLUMNS, RENDER_DEBUG_SIGNATURE_ROWS, glyph_cache_entry_t::rows, terminal_renderer_s::rows, SAFE_FREE, SAFE_MALLOC, SET_ERRNO, terminal_renderer_s::theme, US_PER_SEC_INT, rendered_cell_t::valid, terminal_renderer_s::vt, terminal_renderer_s::vts, glyph_cache_entry_t::width, and terminal_renderer_s::width_px.

Referenced by TerminalRendererBinding::feed().

◆ term_renderer_get_cols()

int term_renderer_get_cols ( terminal_renderer_t * r)

Definition at line 935 of file media/render/terminal.c.

935 {
936 if (!r)
937 return 0;
938 return r->cols;
939}

References terminal_renderer_s::cols.

Referenced by TerminalRendererBinding::cols().

◆ term_renderer_get_rows()

int term_renderer_get_rows ( terminal_renderer_t * r)

Definition at line 941 of file media/render/terminal.c.

941 {
942 if (!r)
943 return 0;
944 return r->rows;
945}

References terminal_renderer_s::rows.

Referenced by TerminalRendererBinding::rows().

◆ term_renderer_height_px()

int term_renderer_height_px ( terminal_renderer_t * r)

Definition at line 928 of file media/render/terminal.c.

928 {
929 return r->height_px;
930}

References terminal_renderer_s::height_px.

Referenced by TerminalRendererBinding::height(), render_file_create(), and render_file_destroy().

◆ term_renderer_pitch()

int term_renderer_pitch ( terminal_renderer_t * r)

Definition at line 931 of file media/render/terminal.c.

931 {
932 return r->pitch;
933}

References terminal_renderer_s::pitch.

Referenced by TerminalRendererBinding::copyPixels(), and render_file_destroy().

◆ term_renderer_pixels()

const uint8_t * term_renderer_pixels ( terminal_renderer_t * r)

Definition at line 922 of file media/render/terminal.c.

922 {
923 return r->framebuffer;
924}

References terminal_renderer_s::framebuffer.

Referenced by TerminalRendererBinding::copyPixels(), and render_file_destroy().

◆ term_renderer_set_zalgo()

asciichat_error_t term_renderer_set_zalgo ( terminal_renderer_t * r,
int level )

Enable raster Zalgo (1-10), or disable with zero.

Definition at line 581 of file media/render/terminal.c.

581 {
582 if (!r || level < 0 || level > 10)
583 return SET_ERRNO(ERROR_INVALID_PARAM, "Zalgo raster level must be 0-10");
584 if (level && !r->zalgo_face) {
585 if (FT_New_Memory_Face(r->ft_lib, g_font_default, (FT_Long)g_font_default_size, 0, &r->zalgo_face))
586 return SET_ERRNO(ERROR_INIT, "Cannot load Zalgo combining font");
587 if (FT_Set_Pixel_Sizes(r->zalgo_face, 0, (FT_UInt)(r->cell_w * 2))) {
588 FT_Done_Face(r->zalgo_face);
589 r->zalgo_face = NULL;
590 return SET_ERRNO(ERROR_INIT, "Cannot size Zalgo combining font");
591 }
592 }
593 r->zalgo_level = level;
594 return ASCIICHAT_OK;
595}

References ASCIICHAT_OK, terminal_renderer_s::cell_w, ERROR_INIT, ERROR_INVALID_PARAM, terminal_renderer_s::ft_lib, g_font_default, g_font_default_size, SET_ERRNO, terminal_renderer_s::zalgo_face, and terminal_renderer_s::zalgo_level.

Referenced by TerminalRendererBinding::setZalgo().

◆ term_renderer_width_px()

int term_renderer_width_px ( terminal_renderer_t * r)

Definition at line 925 of file media/render/terminal.c.

925 {
926 return r->width_px;
927}

References terminal_renderer_s::width_px.

Referenced by render_file_create(), render_file_destroy(), and TerminalRendererBinding::width().