ascii-chat 0.8.38
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches
render.c File Reference

Unified render loop implementation for all display modes. More...

Go to the source code of this file.

Functions

asciichat_error_t session_render_loop (session_capture_ctx_t *capture, session_display_ctx_t *display, session_should_exit_fn should_exit, session_capture_fn capture_cb, session_sleep_for_frame_fn sleep_cb, session_keyboard_handler_fn keyboard_handler, void *user_data)
 

Detailed Description

Unified render loop implementation for all display modes.

Provides a single, centralized render loop that supports both synchronous and event-driven modes. All display modes (mirror, client, discovery) use this loop.

Author
Zachary Fogg me@zf.nosp@m.o.gg
Date
January 2026

Definition in file lib/session/render.c.

Function Documentation

◆ session_render_loop()

asciichat_error_t session_render_loop ( session_capture_ctx_t *  capture,
session_display_ctx_t display,
session_should_exit_fn  should_exit,
session_capture_fn  capture_cb,
session_sleep_for_frame_fn  sleep_cb,
session_keyboard_handler_fn  keyboard_handler,
void *  user_data 
)

Definition at line 39 of file lib/session/render.c.

42 {
43 // Validate required parameters
44 if (!display) {
45 SET_ERRNO(ERROR_INVALID_PARAM, "session_render_loop: display context is NULL");
46 return ERROR_INVALID_PARAM;
47 }
48
49 if (!should_exit) {
50 SET_ERRNO(ERROR_INVALID_PARAM, "session_render_loop: should_exit callback is NULL");
51 return ERROR_INVALID_PARAM;
52 }
53
54 // Validate mode: either capture context OR custom callbacks, not both
55 if (!capture && !capture_cb) {
56 SET_ERRNO(ERROR_INVALID_PARAM, "session_render_loop: must provide either capture context or capture callback");
57 return ERROR_INVALID_PARAM;
58 }
59
60 if (capture && capture_cb) {
61 SET_ERRNO(ERROR_INVALID_PARAM, "session_render_loop: cannot provide both capture context and capture callback");
62 return ERROR_INVALID_PARAM;
63 }
64
65 // In event-driven mode, both capture_cb and sleep_cb must be provided together
66 if (capture_cb && !sleep_cb) {
67 SET_ERRNO(ERROR_INVALID_PARAM, "session_render_loop: capture_cb requires sleep_cb (must provide both or neither)");
68 return ERROR_INVALID_PARAM;
69 }
70
71 if (sleep_cb && !capture_cb) {
72 SET_ERRNO(ERROR_INVALID_PARAM, "session_render_loop: sleep_cb requires capture_cb (must provide both or neither)");
73 return ERROR_INVALID_PARAM;
74 }
75
76 // Snapshot mode state tracking
77 uint64_t snapshot_start_time_ns = 0; // Initialized when first frame is rendered
78 bool snapshot_done = false;
79 bool first_frame_rendered = false;
80 bool snapshot_mode = GET_OPTION(snapshot_mode);
81
82 // Pause mode state tracking
83 bool initial_paused_frame_rendered = false;
84 bool was_paused = false;
85 bool is_paused = false;
86
87 // Keyboard input initialization (if keyboard handler is provided)
88 // Disable keyboard in snapshot mode and non-interactive terminals
89 // Only enable keyboard if BOTH:
90 // 1. Terminal is interactive (stdin/stdout are TTYs)
91 // 2. NOT in snapshot mode
92 bool keyboard_enabled = false;
93 if (keyboard_handler && terminal_is_interactive() && !snapshot_mode) {
94 // Try to initialize keyboard if both stdin and stdout are TTYs in interactive mode
95 asciichat_error_t kb_result = keyboard_init();
96 if (kb_result == ASCIICHAT_OK) {
97 keyboard_enabled = true;
98 log_debug("Keyboard input enabled (TTY mode)");
99 } else {
100 log_debug("Failed to initialize keyboard input (%s) - will attempt fallback", asciichat_error_string(kb_result));
101 // Don't fail - continue with keyboard handler (will try to read anyway)
102 keyboard_enabled = true; // Allow trying to read keyboard even if init failed
103 }
104 }
105
106 // Determine mode: synchronous (capture provided) or event-driven (callbacks provided)
107 bool is_synchronous = (capture != NULL);
108
109 // Frame rate timing
110 uint64_t frame_count = 0;
111 uint64_t frame_start_ns = 0;
112 uint64_t frame_to_render_ns = 0;
113
114 // Disable console logging during rendering to prevent logs from corrupting frame display
115 // This must be done BEFORE the render loop to avoid repeated lock/unlock cycles that could cause deadlocks
117
118 // Main render loop - works for both synchronous and event-driven modes
119 while (!should_exit(user_data)) {
120 // Frame timing - measure total time to maintain target FPS
121 frame_start_ns = time_get_ns();
122
123 // Frame capture and timing - mode-dependent
124 image_t *image = NULL;
125 uint64_t capture_start_ns = 0;
126 uint64_t capture_end_ns = 0;
127 uint64_t pre_convert_ns = 0;
128 uint64_t post_convert_ns = 0;
129
130 if (is_synchronous) {
131 capture_start_ns = time_get_ns();
132 // SYNCHRONOUS MODE: Use session_capture context
133
134 // Check pause state and handle initial frame rendering
136 is_paused = source && media_source_is_paused(source);
137
138 // Detect pause transition - mark initial frame as rendered so polling starts
139 if (!was_paused && is_paused) {
140 initial_paused_frame_rendered = true;
141 log_debug("Media paused, enabling keyboard polling");
142 }
143
144 // Detect unpause transition to reset flag
145 if (was_paused && !is_paused) {
146 initial_paused_frame_rendered = false;
147 log_debug("Media unpaused, resuming frame capture");
148 }
149 was_paused = is_paused;
150
151 // If paused and already rendered initial frame, skip frame capture and poll for resume
152 if (is_paused && initial_paused_frame_rendered) {
153 // Sleep briefly to avoid busy-waiting while paused
154 uint64_t idle_sleep_ns = (uint64_t)(NS_PER_SEC_INT / GET_OPTION(fps)); // Frame period in nanoseconds
155 platform_sleep_ns(idle_sleep_ns);
156
157 // Keep polling keyboard to allow unpausing (even if keyboard wasn't formally initialized)
158 // keyboard_read_nonblocking() is safe to call even if keyboard_init() wasn't called - it just returns KEY_NONE
159 if (keyboard_handler) {
160 keyboard_key_t key = keyboard_read_nonblocking();
161 if (key != KEY_NONE) {
162 // Check if interactive grep should handle this key
165 continue; // Force immediate re-render
166 }
167
168 keyboard_handler(capture, key, user_data);
169 }
170 }
171 continue; // Skip frame capture and rendering, keep loop running
172 }
173
174 // Profile: frame capture with detailed retry tracking
175 static uint64_t max_retries = 0;
176 uint64_t loop_retry_count = 0;
177 uint64_t capture_elapsed_ns = 0;
178
179 do {
180 // Check for exit request before blocking on frame read
181 if (should_exit(user_data)) {
182 break;
183 }
184
185 log_debug_every(3 * US_PER_SEC_INT, "RENDER[%lu]: Starting frame read", frame_count);
186 image = session_capture_read_frame(capture);
187 log_debug_every(3 * US_PER_SEC_INT, "RENDER[%lu]: Frame read done, image=%p", frame_count, (void *)image);
188 capture_elapsed_ns = time_elapsed_ns(capture_start_ns, time_get_ns());
189
190 if (!image) {
191 // Check if we've reached end of file for media sources
192 if (session_capture_at_end(capture)) {
193 log_info("Media source reached end of file");
194 break; // Exit render loop - end of media
195 }
196
197 // Check for exit request during retry loop
198 if (should_exit(user_data)) {
199 break;
200 }
201
202 loop_retry_count++;
203
204 // Brief delay before retry on temporary frame unavailability
205 if (loop_retry_count <= 1 || frame_count % 100 == 0) {
206 if (loop_retry_count == 1 && frame_count > 0) {
207 log_debug_every(500 * US_PER_MS_INT, "FRAME_WAIT: retry at frame %lu (waited %.1f ms so far)",
208 frame_count, (double)capture_elapsed_ns / NS_PER_MS);
209 }
210 }
211
212 // Track max retry count for diagnostic logging
213 if (loop_retry_count > max_retries) {
214 max_retries = loop_retry_count;
215 }
216
217 platform_sleep_us(10 * US_PER_MS_INT); // 10ms
218 continue;
219 }
220
221 // Frame obtained successfully
222 if (loop_retry_count > 0) {
223 double wait_ms = (double)capture_elapsed_ns / NS_PER_MS;
224 log_debug_every(US_PER_SEC_INT, "FRAME_OBTAINED: after %lu retries, waited %.1f ms", loop_retry_count,
225 wait_ms);
226 }
227 break; // Exit retry loop
228 } while (true);
229
230 // If we still don't have an image after retries, skip this frame and continue
231 // (This happens during network latency or when prefetch thread is catching up)
232 if (!image) {
233 continue;
234 }
235
236 // Capture phase complete - record timestamp for phase breakdown
237 capture_end_ns = time_get_ns();
238
239 frame_count++;
240
241 // Log capture time every 30 frames
242 if (frame_count % 30 == 0) {
243 double capture_ms = (double)capture_elapsed_ns / NS_PER_MS;
244 log_dev_every(5 * US_PER_SEC_INT, "PROFILE[%lu]: CAPTURE=%.2f ms", frame_count, capture_ms);
245 }
246
247 // Pause after first frame if requested via --pause flag
248 // We read the frame first, then pause, so the initial frame is available for rendering
249 if (!is_paused && frame_count == 1 && GET_OPTION(pause) && source) {
250 media_source_pause(source);
251 is_paused = true;
252 // Note: initial_paused_frame_rendered will be set in next iteration when pause is detected above
253 log_debug("Paused media source after first frame");
254 }
255
256 } else {
257 // Both sleep_cb and capture_cb are guaranteed non-NULL by validation above
258 sleep_cb(user_data);
259 image = capture_cb(user_data);
260
261 if (!image) {
262 // No frame available - this is normal in async modes (network latency, etc.)
263 // Just continue to next iteration, don't exit
264 continue;
265 }
266 }
267
268 // Convert image to ASCII using display context
269 // Handles all palette, terminal caps, width, height, stretch settings
270 pre_convert_ns = time_get_ns();
271 char *ascii_frame = session_display_convert_to_ascii(display, image);
272 post_convert_ns = time_get_ns();
273 uint64_t conversion_elapsed_ns = post_convert_ns - pre_convert_ns;
274
275 if (ascii_frame) {
276 // Detect when we have a paused frame (first frame after pausing)
277 bool is_paused_frame = initial_paused_frame_rendered && is_paused;
278
279 // When paused with snapshot mode, output the initial frame immediately
280 bool output_paused_frame = snapshot_mode && is_paused_frame;
281
282 // Always attempt to render frames; the display context will handle filtering based on:
283 // - TTY mode: render all frames with cursor control (even in snapshot mode, for animation)
284 // - Piped mode: render all frames without cursor control (for continuous capture)
285 // - Snapshot mode on non-TTY: only the display context renders the final frame
286 bool should_write = true;
287 uint64_t pre_render_ns = 0, post_render_ns = 0; // Declare outside if block for timing
288 if (should_write) {
289 // is_final = true when: snapshot done, or paused frame (for both snapshot and pause modes)
290 // Profile: render frame
291 pre_render_ns = time_get_ns();
292 START_TIMER("render_frame");
293
294 // Check if help screen is active - if so, render help instead of frame
295 // Help screen is disabled in snapshot mode and non-interactive terminals (keyboard disabled)
296 if (display && session_display_is_help_active(display) && terminal_is_interactive() && !snapshot_mode) {
298 } else {
299 session_display_render_frame(display, ascii_frame);
300 }
301
302 uint64_t render_elapsed_ns = STOP_TIMER("render_frame");
303 post_render_ns = time_get_ns();
304
305 // Calculate total time from frame START (frame_start_ns) to render COMPLETE
306 frame_to_render_ns = time_elapsed_ns(frame_start_ns, post_render_ns);
307 if (frame_count % 30 == 0) {
308 double total_frame_time_ms = (double)frame_to_render_ns / NS_PER_MS;
309 log_dev("ACTUAL_TIME[%lu]: Total frame time from start to render complete: %.1f ms", frame_count,
310 total_frame_time_ms);
311 }
312
313 // Log render time every 30 frames
314 if (frame_count % 150 == 0) {
315 double conversion_ms = (double)conversion_elapsed_ns / NS_PER_MS;
316 double render_ms = (double)render_elapsed_ns / NS_PER_MS;
317 log_dev_every(5 * US_PER_SEC_INT, "PROFILE[%lu]: CONVERT=%.2f ms, RENDER=%.2f ms", frame_count, conversion_ms,
318 render_ms);
319 }
320 }
321
322 // Keyboard input polling (if enabled) - MUST come before snapshot exit check so help screen can be toggled
323 // Allow keyboard in snapshot mode too (for help screen toggle debugging)
324 // Only enable keyboard if BOTH stdin AND stdout are TTYs to avoid buffering issues
325 // when tcsetattr() modifies the tty line discipline
326 if (keyboard_enabled && keyboard_handler) {
327 START_TIMER("keyboard_read_%lu", (unsigned long)frame_count);
328 keyboard_key_t key = keyboard_read_nonblocking();
329 double keyboard_elapsed_ns = STOP_TIMER("keyboard_read_%lu", (unsigned long)frame_count);
330 if (keyboard_elapsed_ns >= 0.0) {
331 char _duration_str[32];
332 format_duration_ns(keyboard_elapsed_ns, _duration_str, sizeof(_duration_str));
333 log_dev("RENDER[%lu] Keyboard read complete (key=%d) in %s", (unsigned long)frame_count, key, _duration_str);
334 }
335 if (key != KEY_NONE) {
336 // Check if interactive grep should handle this key
339 continue; // Force immediate re-render
340 }
341
342 // Normal keyboard handler
343 keyboard_handler(capture, key, user_data);
344 }
345 }
346
347 // Snapshot mode timing: start timer right after rendering first frame
348 if (snapshot_mode && !first_frame_rendered) {
349 snapshot_start_time_ns = time_get_ns();
350 first_frame_rendered = true;
351 log_debug("Snapshot mode: first frame rendered, timer started");
352 }
353
354 // Snapshot mode: check if delay has elapsed after rendering a frame
355 if (snapshot_mode && !snapshot_done && first_frame_rendered) {
356 uint64_t current_time_ns = time_get_ns();
357 double elapsed_sec = time_ns_to_s(time_elapsed_ns(snapshot_start_time_ns, current_time_ns));
358 double snapshot_delay = GET_OPTION(snapshot_delay);
359
360 log_debug_every(US_PER_SEC_INT, "SNAPSHOT_DELAY_CHECK: elapsed=%.2f delay=%.2f", elapsed_sec, snapshot_delay);
361
362 // snapshot_delay=0 means exit immediately after rendering first frame
363 // snapshot_delay>0 means wait that many seconds after first frame
364 if (elapsed_sec >= snapshot_delay) {
365 // We don't end frames with newlines so the next log would print on the same line as the frame's
366 // last row without an \n here. We only need this \n in stdout in snapshot mode and when interactive,
367 // so piped snapshots don't have a weird newline in stdout that they don't need.
368 if (GET_OPTION(snapshot_mode) && terminal_is_interactive()) {
369 printf("\n");
370 }
371 log_info("Snapshot delay %.2f seconds elapsed, exiting", snapshot_delay);
372 snapshot_done = true;
373 }
374 }
375
376 // Exit conditions: snapshot mode exits after capturing the final frame or initial paused frame
377 if (snapshot_mode && (snapshot_done || output_paused_frame)) {
378 SAFE_FREE(ascii_frame);
379 break;
380 }
381
382 SAFE_FREE(ascii_frame);
383
384 // Measure frame completion right after rendering, BEFORE keyboard polling
385 // This gives us accurate timing for just the core frame operations
386 uint64_t frame_end_render_ns = time_get_ns();
387
388 // Calculate each phase duration
389 uint64_t prestart_ms =
390 (capture_start_ns > frame_start_ns) ? (capture_start_ns - frame_start_ns) / NS_PER_MS_INT : 0;
391 uint64_t capture_ms =
392 (capture_end_ns > capture_start_ns) ? (capture_end_ns - capture_start_ns) / NS_PER_MS_INT : 0;
393 uint64_t convert_ms = conversion_elapsed_ns / NS_PER_MS_INT;
394 uint64_t render_ms =
395 (post_render_ns > pre_render_ns && post_render_ns > 0) ? (post_render_ns - pre_render_ns) / NS_PER_MS_INT : 0;
396 uint64_t total_ms =
397 (frame_end_render_ns > frame_start_ns) ? (frame_end_render_ns - frame_start_ns) / NS_PER_MS_INT : 0;
398
399 // Log phase breakdown every 5 frames
400 if (frame_count % 5 == 0) {
401 log_dev(
402 "PHASE_BREAKDOWN[%lu]: prestart=%llu ms, capture=%llu ms, convert=%llu ms, render=%llu ms (total=%llu ms)",
403 frame_count, prestart_ms, capture_ms, convert_ms, render_ms, total_ms);
404 }
405 } else {
406 // Snapshot mode: even if frame conversion failed, check if we should exit
407 // This ensures snapshot_delay is honored even if display context isn't rendering
408 if (snapshot_mode && snapshot_done) {
409 break;
410 }
411 }
412
413 // Audio-Video Synchronization: Keep audio and video in sync by periodically adjusting audio to match video time
414 // DISABLED: Audio sync causes seeking every 1 second, which interrupts playback
415 // For file/media playback (mirror mode), audio and video decode independently
416 // and don't need synchronization - they play at their natural rates from the file.
417 // Audio sync is only needed for multi-client video conferencing (server mode).
418 //
419 // if (is_synchronous && capture && image && frame_count % 30 == 0) {
420 // media_source_sync_audio_to_video(source);
421 // }
422
423 // Frame rate limiting: Only sleep if we're ahead of schedule
424 // If decoder is slow and we're already behind, don't add extra sleep
425 if (is_synchronous && capture) {
426 uint32_t target_fps = session_capture_get_target_fps(capture);
427 if (target_fps > 0) {
428 uint64_t frame_end_ns = time_get_ns();
429 uint64_t frame_elapsed_ns = time_elapsed_ns(frame_start_ns, frame_end_ns);
430 uint64_t frame_target_ns = NS_PER_SEC_INT / target_fps;
431
432 log_dev("RENDER[%lu] TIMING_TOTAL: frame_time_ms=%.2f target_ms=%.2f", frame_count,
433 (double)frame_elapsed_ns / NS_PER_MS, (double)frame_target_ns / NS_PER_MS);
434
435 // Only sleep if we have time budget remaining
436 // If already behind, skip sleep to catch up
437 if (frame_elapsed_ns < frame_target_ns) {
438 uint64_t sleep_ns = frame_target_ns - frame_elapsed_ns;
439 // Sleep with 500us overhead reserved for recovery
440 if (sleep_ns > 500 * US_PER_MS_INT) {
441 platform_sleep_ns((sleep_ns - 500 * US_PER_MS_INT));
442 }
443 }
444 }
445 }
446
447 // Note: Images returned by media sources are cached/reused and should NOT be destroyed
448 // The image pointers are managed by the source and will be cleaned up on source shutdown
449 } // while (!should_exit(user_data)) {
450
451 // Re-enable console logging after rendering completes
453 if (!GET_OPTION(snapshot_mode) && terminal_is_interactive()) {
454 printf("\n");
455 }
456
457 // Keyboard input cleanup (if it was initialized)
458 if (keyboard_enabled) {
459 keyboard_destroy();
460 log_debug("Keyboard input disabled");
461 }
462
463 return ASCIICHAT_OK;
464}
bool should_exit(void)
Definition main.c:90
void session_display_render_help(session_display_ctx_t *ctx)
Render help screen centered on terminal.
asciichat_error_t interactive_grep_handle_key(keyboard_key_t key)
bool interactive_grep_should_handle(int key)
bool session_capture_at_end(session_capture_ctx_t *ctx)
image_t * session_capture_read_frame(session_capture_ctx_t *ctx)
uint32_t session_capture_get_target_fps(session_capture_ctx_t *ctx)
void * session_capture_get_media_source(session_capture_ctx_t *ctx)
void session_display_render_frame(session_display_ctx_t *ctx, const char *frame_data)
bool session_display_is_help_active(session_display_ctx_t *ctx)
Check if help screen is currently active (implemented in display.c for struct access)
char * session_display_convert_to_ascii(session_display_ctx_t *ctx, const image_t *image)
void log_set_terminal_output(bool enabled)
bool terminal_is_interactive(void)
void platform_sleep_us(unsigned int us)
bool media_source_is_paused(media_source_t *source)
Definition source.c:926
void media_source_pause(media_source_t *source)
Definition source.c:908
Media source for video and audio capture.
Definition source.c:31
uint64_t time_get_ns(void)
Definition util/time.c:48
int format_duration_ns(double nanoseconds, char *buffer, size_t buffer_size)
Definition util/time.c:275
uint64_t time_elapsed_ns(uint64_t start_ns, uint64_t end_ns)
Definition util/time.c:90

References format_duration_ns(), interactive_grep_handle_key(), interactive_grep_should_handle(), log_set_terminal_output(), media_source_is_paused(), media_source_pause(), platform_sleep_us(), session_capture_at_end(), session_capture_get_media_source(), session_capture_get_target_fps(), session_capture_read_frame(), session_display_convert_to_ascii(), session_display_is_help_active(), session_display_render_frame(), session_display_render_help(), should_exit(), terminal_is_interactive(), time_elapsed_ns(), and time_get_ns().