ascii-chat 0.11.33
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_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)
 
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)
 
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 54 of file renderer.h.

◆ terminal_renderer_t

Definition at line 17 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 26 of file renderer.h.

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

Function Documentation

◆ 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 33 of file renderer.c.

34 {
35 log_info("[RENDER_FILE_CREATE] CALLED with output_path=%s, cols=%d, rows=%d, fps=%d, theme=%d, out=%p",
36 output_path ? output_path : "(null)", cols, rows, fps, theme, (void *)out);
37 render_file_ctx_t *ctx = SAFE_CALLOC(1, sizeof(*ctx), render_file_ctx_t *);
38
39 // Resolve font spec → platform-appropriate file path, family name, or bundled data.
40 char font_spec[512] = {0};
41 bool font_is_path = false;
42 const uint8_t *font_data = NULL;
43 size_t font_data_size = 0;
44 const char *raw_font = GET_OPTION(render_font);
45
46 // Log the font option value from GET_OPTION (for WASM font debugging)
47 log_info("[WASM_FONT_GETTER] GET_OPTION(render_font) returned: '%s' (empty=%d), GET_OPTION(matrix_rain)=%d",
48 raw_font ? raw_font : "(null)", (!raw_font || raw_font[0] == '\0') ? 1 : 0, GET_OPTION(matrix_rain));
49
50 // Auto-select matrix font when --matrix flag is set, unless user explicitly overrides with --render-font
51 log_debug("raw_font=%s, matrix_rain=%d", raw_font, GET_OPTION(matrix_rain));
52 if ((!raw_font || raw_font[0] == '\0') && GET_OPTION(matrix_rain)) {
53 raw_font = "matrix";
54 log_debug("render_file_create: [MATRIX] Using matrix font (enabled by --matrix flag)");
55 }
56
58 platform_font_resolve(raw_font, font_spec, sizeof(font_spec), &font_is_path, &font_data, &font_data_size);
59 if (fe != ASCIICHAT_OK) {
60 log_warn("renderer: font resolution failed for '%s' — using bundled default",
61 raw_font ? raw_font : "(explicit system)");
62 fe = platform_font_resolve("default", font_spec, sizeof(font_spec), &font_is_path, &font_data, &font_data_size);
63 if (fe != ASCIICHAT_OK) {
64 SAFE_FREE(ctx);
65 return fe;
66 }
67 }
68
69 log_debug("render_file_create: Font resolved: font_spec='%s', font_is_path=%d, font_data=%p (size=%zu)", font_spec,
70 font_is_path, (void *)font_data, font_data_size);
71
72 // Use reasonable default font size for render-file quality
73 double font_size_pt = GET_OPTION(render_font_size);
74 term_renderer_theme_t resolved_theme = (term_renderer_theme_t)theme;
75 if (resolved_theme == TERM_RENDERER_THEME_AUTO) {
77 log_debug("render_file_create: auto theme resolved to %s",
78 resolved_theme == TERM_RENDERER_THEME_DARK ? "dark" : "light");
79 }
80
81 term_renderer_config_t tr_cfg = {
82 .cols = cols,
83 .rows = rows,
84 .font_size_pt = font_size_pt,
85 .theme = resolved_theme,
86 .font_is_path = font_is_path,
87 .font_data = font_data,
88 .font_data_size = font_data_size,
89 };
90 SAFE_STRNCPY(tr_cfg.font_spec, font_spec, sizeof(tr_cfg.font_spec));
91
93 if (err != ASCIICHAT_OK) {
94 log_error("render_file_create: term_renderer_create failed: %s", asciichat_error_string(err));
95 SAFE_FREE(ctx);
96 return err;
97 }
98 int actual_width_px = term_renderer_width_px(ctx->renderer);
99 int actual_height_px = term_renderer_height_px(ctx->renderer);
100 log_info("render_file_create: GRID DIMENSIONS: %ux%u cells -> PIXEL DIMENSIONS: %dx%d px", cols, rows,
101 actual_width_px, actual_height_px);
102 log_debug("render_file_create: term_renderer created (%dx%d cells, %dx%d px)", cols, rows, actual_width_px,
103 actual_height_px);
104
105 err = ffmpeg_encoder_create(output_path, actual_width_px, actual_height_px, fps, &ctx->encoder);
106 if (err != ASCIICHAT_OK) {
107 log_error("render_file_create: ffmpeg_encoder_create failed: %s", asciichat_error_string(err));
109 SAFE_FREE(ctx);
110 return err;
111 }
112 log_debug("render_file_create: ffmpeg_encoder created successfully");
113
114 // Initialize audio fields
115 ctx->audio_sample_rate = 48000; // Audio pipeline is 48kHz
116 ctx->fps = fps;
117 // Allocate large buffer for snapshot mode (5 seconds at 48kHz with ~14 FPS = 3,428 samples per frame)
118 // Use 8192 to comfortably handle snapshot reads without excessive overhead
119 ctx->audio_buf_size = 8192; // Temporary buffer for reading audio (in floats)
120 ctx->audio_read_buf = SAFE_MALLOC(ctx->audio_buf_size * sizeof(float), float *);
121
123 if (err != ASCIICHAT_OK) {
127 SAFE_FREE(ctx);
128 return err;
129 }
130 log_info("renderer: initialized encoder for %s", output_path);
131
132 // Register render file context for debugging
133 NAMED_REGISTER_CONTEXT(ctx, "render_file_ctx", output_path, NULL);
134
135 *out = ctx;
136 return ASCIICHAT_OK;
137}
asciichat_error_t ffmpeg_encoder_destroy(ffmpeg_encoder_t *enc)
asciichat_error_t ffmpeg_encoder_create(const char *output_path, int width_px, int height_px, int fps, ffmpeg_encoder_t **out)
#define SAFE_STRNCPY(dst, src, size)
Definition common.h:414
#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 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:49
@ ASCIICHAT_OK
Definition error_codes.h:51
#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_info(...)
Log an INFO message.
Definition log/log.h:561
#define log_debug(...)
Log a DEBUG message.
Definition log/log.h:548
#define GET_OPTION(field)
Safely get a specific option field (lock-free read)
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)
asciichat_error_t platform_font_resolve(const char *spec, char *out, size_t out_size, bool *out_is_path, const uint8_t **out_font_data, size_t *out_font_data_size)
bool terminal_has_dark_background(void)
Detect if terminal theme is dark.
asciichat_error_t audio_recording_create(audio_recording_t **out)
Definition recording.c:22
terminal_renderer_t * renderer
Definition renderer.c:18
ffmpeg_encoder_t * encoder
Definition renderer.c:19
uint32_t audio_sample_rate
Definition renderer.c:27
audio_recording_t * recording
Definition renderer.c:20
float * audio_read_buf
Definition renderer.c:29

References ASCIICHAT_OK, render_file_ctx_s::audio_buf_size, render_file_ctx_s::audio_read_buf, audio_recording_create(), render_file_ctx_s::audio_sample_rate, term_renderer_config_t::cols, render_file_ctx_s::encoder, ffmpeg_encoder_create(), ffmpeg_encoder_destroy(), term_renderer_config_t::font_spec, render_file_ctx_s::fps, GET_OPTION, log_debug, log_error, log_info, log_warn, NAMED_REGISTER_CONTEXT, platform_font_resolve(), render_file_ctx_s::recording, render_file_ctx_s::renderer, SAFE_CALLOC, SAFE_FREE, SAFE_MALLOC, SAFE_STRNCPY, 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(), and terminal_has_dark_background().

Referenced by session_display_init_render_file().

◆ render_file_destroy()

asciichat_error_t render_file_destroy ( render_file_ctx_t *  ctx)

Definition at line 247 of file renderer.c.

247 {
248 if (!ctx)
249 return ASCIICHAT_OK;
250 uint64_t target = ctx->audio_target;
252 while (ctx->audio_samples_written < target) {
253 int count = (int)((target - ctx->audio_samples_written) > (uint64_t)ctx->audio_buf_size
255 : target - ctx->audio_samples_written);
257 err = ffmpeg_encoder_write_audio(ctx->encoder, ctx->audio_read_buf, count);
258 if (err != ASCIICHAT_OK)
259 break;
260 ctx->audio_samples_written += count;
261 }
264 if (err == ASCIICHAT_OK)
265 err = close_err;
268 SAFE_FREE(ctx);
269 return err;
270}
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:59
void audio_recording_read(audio_recording_t *recording, float *samples, size_t count)
Definition recording.c:87
void audio_recording_destroy(audio_recording_t *recording)
Definition recording.c:39
uint64_t audio_samples_written
Definition renderer.c:22
uint64_t audio_target
Definition renderer.c:25

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::encoder, ffmpeg_encoder_destroy(), ffmpeg_encoder_write_audio(), render_file_ctx_s::recording, render_file_ctx_s::renderer, SAFE_FREE, and term_renderer_destroy().

Referenced by session_display_destroy().

◆ render_file_set_live_timing()

void render_file_set_live_timing ( render_file_ctx_t *  ctx)

Definition at line 139 of file renderer.c.

139 {
140 if (ctx) {
141 ctx->live_timing = true;
143 }
144}
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 241 of file renderer.c.

241 {
242 if (!ctx || !ctx->encoder)
243 return;
244 ffmpeg_encoder_set_snapshot_actual_duration(ctx->encoder, actual_duration_sec);
245}
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 146 of file renderer.c.

146 {
147
148 log_info("render_file_write_frame: CALLED - ctx=%p, captured_ns=%llu", (void *)ctx, (unsigned long long)captured_ns);
149
150 if (!ctx) {
151 log_warn("render_file_write_frame: ctx is NULL");
152 return ASCIICHAT_OK;
153 }
154
155 if (!ansi_frame) {
156 log_warn("render_file_write_frame: ansi_frame is NULL");
157 return ASCIICHAT_OK;
158 }
159
160 size_t frame_len = strlen(ansi_frame);
161 uint64_t frame_index = ctx->frames_written;
162 if (ctx->live_timing && ctx->frames_written > 0) {
163 uint64_t elapsed = captured_ns > ctx->first_frame_ns ? captured_ns - ctx->first_frame_ns : 0;
164 frame_index = elapsed / 1000000000ULL * ctx->fps + elapsed % 1000000000ULL * ctx->fps / 1000000000ULL;
165 if (frame_index <= ctx->last_frame_index)
166 return ASCIICHAT_OK;
167 }
168 log_info("render_file_write_frame: processing frame (len=%zu)", frame_len);
169 if (frame_len > 0) {
170 log_info(" first 100 chars: %.100s", ansi_frame);
171 }
172
173 asciichat_error_t err = term_renderer_feed(ctx->renderer, ansi_frame, frame_len);
174 if (err != ASCIICHAT_OK) {
175 log_warn("render_file_write_frame: term_renderer_feed failed: %s", asciichat_error_string(err));
176 return err;
177 }
178
179 const uint8_t *pixels = term_renderer_pixels(ctx->renderer);
180 int pitch = term_renderer_pitch(ctx->renderer);
181 int width_px = term_renderer_width_px(ctx->renderer);
182 int height_px = term_renderer_height_px(ctx->renderer);
183
184 log_info("render_file_write_frame: pixels=%p pitch=%d dims=%dx%d", (void *)pixels, pitch, width_px, height_px);
185
186 if (pixels) {
187 uint8_t sample_r = pixels[0], sample_g = pixels[1], sample_b = pixels[2], sample_a = pixels[3];
188 size_t mid_offset = (size_t)(height_px / 2) * (size_t)pitch + (size_t)(width_px / 2) * 4;
189 uint8_t sample_r_mid = pixels[mid_offset], sample_g_mid = pixels[mid_offset + 1],
190 sample_b_mid = pixels[mid_offset + 2], sample_a_mid = pixels[mid_offset + 3];
191
192 log_info(" pixel[0,0]: RGBA(%u,%u,%u,%u), pixel[%d,%d]: RGBA(%u,%u,%u,%u)", sample_r, sample_g, sample_b, sample_a,
193 width_px / 2, height_px / 2, sample_r_mid, sample_g_mid, sample_b_mid, sample_a_mid);
194 }
195
196 // term_renderer_pixels() returns a pointer to the renderer's internal buffer which gets
197 // overwritten on the next frame. We must copy the data before passing to the encoder.
198 uint8_t *pixels_copy = NULL;
199 if (pixels) {
200 size_t pixel_buffer_size = (size_t)pitch * (size_t)height_px;
201 pixels_copy = SAFE_MALLOC(pixel_buffer_size, uint8_t *);
202 memcpy(pixels_copy, pixels, pixel_buffer_size);
203 log_info("render_file_write_frame: copied %zu bytes of pixel data (was at %p, now at %p)", pixel_buffer_size,
204 (void *)pixels, (void *)pixels_copy);
205 }
206
207 err = ffmpeg_encoder_write_frame(ctx->encoder, pixels_copy, pitch, captured_ns);
208 if (err != ASCIICHAT_OK) {
209 log_warn("render_file_write_frame: ffmpeg_encoder_write_frame failed: %s", asciichat_error_string(err));
210 }
211
212 // Free the pixel copy (ffmpeg_encoder_write_frame reads and converts it immediately)
213 SAFE_FREE(pixels_copy);
214
215 if (err == ASCIICHAT_OK) {
216 if (ctx->frames_written == 0) {
217 ctx->first_frame_ns = captured_ns;
218 audio_recording_start(ctx->recording, captured_ns);
219 }
220 ctx->frames_written++;
221 ctx->last_frame_index = frame_index;
222 uint64_t target = (ctx->live_timing ? frame_index + 1 : ctx->frames_written) * ctx->audio_sample_rate / ctx->fps;
223 ctx->audio_target = target;
224 /* Keep 100 ms pending so audio callbacks can deliver samples before encoding. */
225 uint64_t ready = target > 4800 ? target - 4800 : 0;
226 while (ctx->audio_samples_written < ready) {
227 int count = (int)((ready - ctx->audio_samples_written) > (uint64_t)ctx->audio_buf_size
228 ? (uint64_t)ctx->audio_buf_size
229 : ready - ctx->audio_samples_written);
230 audio_recording_read(ctx->recording, ctx->audio_read_buf, count);
231 err = ffmpeg_encoder_write_audio(ctx->encoder, ctx->audio_read_buf, count);
232 if (err != ASCIICHAT_OK)
233 break;
234 ctx->audio_samples_written += count;
235 }
236 }
237
238 return err;
239}
asciichat_error_t ffmpeg_encoder_write_frame(ffmpeg_encoder_t *enc, const uint8_t *rgb, int pitch, uint64_t captured_ns)
const uint8_t * term_renderer_pixels(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)
void audio_recording_start(audio_recording_t *recording, uint64_t epoch_ns)
Definition recording.c:50

References ASCIICHAT_OK, render_file_ctx_s::audio_buf_size, render_file_ctx_s::audio_read_buf, audio_recording_read(), audio_recording_start(), render_file_ctx_s::audio_sample_rate, render_file_ctx_s::audio_samples_written, render_file_ctx_s::audio_target, render_file_ctx_s::encoder, ffmpeg_encoder_write_audio(), ffmpeg_encoder_write_frame(), render_file_ctx_s::first_frame_ns, render_file_ctx_s::fps, render_file_ctx_s::frames_written, render_file_ctx_s::last_frame_index, render_file_ctx_s::live_timing, log_info, log_warn, render_file_ctx_s::recording, render_file_ctx_s::renderer, SAFE_FREE, SAFE_MALLOC, term_renderer_feed(), term_renderer_height_px(), term_renderer_pitch(), term_renderer_pixels(), and term_renderer_width_px().

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 261 of file media/render/terminal.c.

261 {
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}
#define SET_ERRNO(code, context_msg,...)
Set error code with custom context message and log it, returning the error code.
@ ERROR_NOT_FOUND
@ ERROR_INIT
Definition error_codes.h:61
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

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_NOT_FOUND, 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, 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().

◆ term_renderer_destroy()

void term_renderer_destroy ( terminal_renderer_t *  r)

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

685 {
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}

References terminal_renderer_s::framebuffer, terminal_renderer_s::ft_face, terminal_renderer_s::ft_lib, terminal_renderer_s::glyph_cache, SAFE_FREE, and terminal_renderer_s::vt.

Referenced by render_file_create(), and render_file_destroy().

◆ term_renderer_feed()

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

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

471 {
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}
unsigned int uint32_t
Definition common.h:58
@ ERROR_MEMORY
Definition error_codes.h:56
@ ERROR_INVALID_PARAM
#define US_PER_SEC_INT
Definition time.h:161
#define log_debug_every(interval_us, fmt,...)
Rate-limited DEBUG logging.
Definition log/log.h:702
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.
int frame
Definition splash.c:99
int bitmap_left
int bitmap_top

References ASCIICHAT_OK, terminal_renderer_s::baseline, glyph_cache_entry_t::bitmap_left, glyph_cache_entry_t::bitmap_top, terminal_renderer_s::cell_h, terminal_renderer_s::cell_w, terminal_renderer_s::cols, ERROR_INVALID_PARAM, ERROR_MEMORY, frame, terminal_renderer_s::framebuffer, terminal_renderer_s::ft_face, terminal_renderer_s::glyph_cache, terminal_renderer_s::height_px, terminal_renderer_s::is_matrix_font, log_debug_every, log_error, terminal_renderer_s::pitch, terminal_renderer_s::rows, SAFE_FREE, SAFE_MALLOC, SET_ERRNO, terminal_get_default_background_color(), terminal_get_default_foreground_color(), terminal_renderer_s::theme, US_PER_SEC_INT, terminal_renderer_s::vt, terminal_renderer_s::vts, and terminal_renderer_s::width_px.

Referenced by render_file_write_frame().

◆ term_renderer_height_px()

int term_renderer_height_px ( terminal_renderer_t *  r)

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

666 {
667 return r->height_px;
668}

References terminal_renderer_s::height_px.

Referenced by render_file_create(), and render_file_write_frame().

◆ term_renderer_pitch()

int term_renderer_pitch ( terminal_renderer_t *  r)

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

669 {
670 return r->pitch;
671}

References terminal_renderer_s::pitch.

Referenced by render_file_write_frame().

◆ term_renderer_pixels()

const uint8_t * term_renderer_pixels ( terminal_renderer_t *  r)

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

660 {
661 return r->framebuffer;
662}

References terminal_renderer_s::framebuffer.

Referenced by render_file_write_frame().

◆ term_renderer_width_px()

int term_renderer_width_px ( terminal_renderer_t *  r)

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

663 {
664 return r->width_px;
665}

References terminal_renderer_s::width_px.

Referenced by render_file_create(), and render_file_write_frame().