42 {
43
44 if (!display) {
45 SET_ERRNO(ERROR_INVALID_PARAM, "session_render_loop: display context is NULL");
46 return ERROR_INVALID_PARAM;
47 }
48
50 SET_ERRNO(ERROR_INVALID_PARAM, "session_render_loop: should_exit callback is NULL");
51 return ERROR_INVALID_PARAM;
52 }
53
54
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
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
77 uint64_t snapshot_start_time_ns = 0;
78 bool snapshot_done = false;
79 bool first_frame_rendered = false;
80 bool snapshot_mode = GET_OPTION(snapshot_mode);
81
82
83 bool initial_paused_frame_rendered = false;
84 bool was_paused = false;
85 bool is_paused = false;
86
87
88
89
90
91
92 bool keyboard_enabled = false;
94
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
102 keyboard_enabled = true;
103 }
104 }
105
106
107 bool is_synchronous = (capture != NULL);
108
109
110 uint64_t frame_count = 0;
111 uint64_t frame_start_ns = 0;
112 uint64_t frame_to_render_ns = 0;
113
114
115
117
118
120
122
123
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) {
132
133
134
137
138
139 if (!was_paused && is_paused) {
140 initial_paused_frame_rendered = true;
141 log_debug("Media paused, enabling keyboard polling");
142 }
143
144
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
152 if (is_paused && initial_paused_frame_rendered) {
153
154 uint64_t idle_sleep_ns = (uint64_t)(NS_PER_SEC_INT / GET_OPTION(fps));
155 platform_sleep_ns(idle_sleep_ns);
156
157
158
159 if (keyboard_handler) {
160 keyboard_key_t key = keyboard_read_nonblocking();
161 if (key != KEY_NONE) {
162
165 continue;
166 }
167
168 keyboard_handler(capture, key, user_data);
169 }
170 }
171 continue;
172 }
173
174
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
182 break;
183 }
184
185 log_debug_every(3 * US_PER_SEC_INT, "RENDER[%lu]: Starting frame read", frame_count);
187 log_debug_every(3 * US_PER_SEC_INT, "RENDER[%lu]: Frame read done, image=%p", frame_count, (void *)image);
189
190 if (!image) {
191
193 log_info("Media source reached end of file");
194 break;
195 }
196
197
199 break;
200 }
201
202 loop_retry_count++;
203
204
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
213 if (loop_retry_count > max_retries) {
214 max_retries = loop_retry_count;
215 }
216
218 continue;
219 }
220
221
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;
228 } while (true);
229
230
231
232 if (!image) {
233 continue;
234 }
235
236
238
239 frame_count++;
240
241
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
248
249 if (!is_paused && frame_count == 1 && GET_OPTION(pause) && source) {
251 is_paused = true;
252
253 log_debug("Paused media source after first frame");
254 }
255
256 } else {
257
258 sleep_cb(user_data);
259 image = capture_cb(user_data);
260
261 if (!image) {
262
263
264 continue;
265 }
266 }
267
268
269
273 uint64_t conversion_elapsed_ns = post_convert_ns - pre_convert_ns;
274
275 if (ascii_frame) {
276
277 bool is_paused_frame = initial_paused_frame_rendered && is_paused;
278
279
280 bool output_paused_frame = snapshot_mode && is_paused_frame;
281
282
283
284
285
286 bool should_write = true;
287 uint64_t pre_render_ns = 0, post_render_ns = 0;
288 if (should_write) {
289
290
292 START_TIMER("render_frame");
293
294
295
298 } else {
300 }
301
302 uint64_t render_elapsed_ns = STOP_TIMER("render_frame");
304
305
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
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
323
324
325
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];
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
339 continue;
340 }
341
342
343 keyboard_handler(capture, key, user_data);
344 }
345 }
346
347
348 if (snapshot_mode && !first_frame_rendered) {
350 first_frame_rendered = true;
351 log_debug("Snapshot mode: first frame rendered, timer started");
352 }
353
354
355 if (snapshot_mode && !snapshot_done && first_frame_rendered) {
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
363
364 if (elapsed_sec >= snapshot_delay) {
365
366
367
369 printf("\n");
370 }
371 log_info("Snapshot delay %.2f seconds elapsed, exiting", snapshot_delay);
372 snapshot_done = true;
373 }
374 }
375
376
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
385
387
388
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
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
407
408 if (snapshot_mode && snapshot_done) {
409 break;
410 }
411 }
412
413
414
415
416
417
418
419
420
421
422
423
424
425 if (is_synchronous && capture) {
427 if (target_fps > 0) {
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
436
437 if (frame_elapsed_ns < frame_target_ns) {
438 uint64_t sleep_ns = frame_target_ns - frame_elapsed_ns;
439
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
448
449 }
450
451
454 printf("\n");
455 }
456
457
458 if (keyboard_enabled) {
459 keyboard_destroy();
460 log_debug("Keyboard input disabled");
461 }
462
463 return ASCIICHAT_OK;
464}
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 media_source_is_paused(media_source_t *source)
void media_source_pause(media_source_t *source)
uint64_t time_get_ns(void)
int format_duration_ns(double nanoseconds, char *buffer, size_t buffer_size)
uint64_t time_elapsed_ns(uint64_t start_ns, uint64_t end_ns)