ascii-chat
Binary v0.15.83-53-gb711d8720
Library lib/v0.3.2-701-gb711d8720
Video chat in your terminal
Loading...
Searching...
No Matches
pipeline.c
Go to the documentation of this file.
1#include <ascii-chat/otel.h>
3#include <ascii-chat/buffer.h>
9
12#include "pipeline.h"
13#include "capture.h"
14#include "display.h"
15#include "render.h"
17#include <libplatform/compiler.h>
18#include <libplatform/system.h>
19#include <libplatform/terminal.h>
21#include <libplatform/terminal.h>
22#include <libplatform/wakeup.h>
25#include <ascii-chat/log/log.h>
27#include <libplatform/atomic.h>
29#include <string.h>
30
31/* ============================================================================
32 * External Declarations
33 * ============================================================================ */
34
35// Flag set by display thread when first frame is rendered
37
38/* ============================================================================
39 * Frame Type & Queue
40 * ============================================================================ */
41
43
44typedef struct {
45 frame_slot_t *slot; // Exclusive until free_frame publishes the slot back to capture.
46 const uint8_t *pixels; // Immutable owning reference shared by display and encoding
47 int w, h;
49 gotel_context_t telemetry_context;
50 bool fresh;
51 uint64_t captured_ns; // wall-clock timestamp
65
68 _Atomic bool busy;
69 size_t capacity;
70};
71
72#define PIPELINE_FRAME_SLOTS 3
73
74// Thread-safe bounded queue (ring buffer)
75typedef struct frame_queue_s {
76 void **slots;
79 mutex_t mu;
80 cond_t not_empty;
81 cond_t not_full;
82 const char *name;
84
85static frame_queue_t *frame_queue_create(int capacity, const char *name) {
86 frame_queue_t *q = SAFE_CALLOC(1, sizeof(*q), frame_queue_t *);
87 q->slots = SAFE_CALLOC(capacity, sizeof(void *), void *);
88 q->capacity = capacity;
89 q->head = q->tail = q->count = 0;
90 q->name = name;
91 mutex_init(&q->mu, name);
92 cond_init(&q->not_empty, "queue_not_empty");
93 cond_init(&q->not_full, "queue_not_full");
94 return q;
95}
96
97static void frame_queue_destroy(frame_queue_t *q) {
98 if (!q)
99 return;
100 mutex_destroy(&q->mu);
101 cond_destroy(&q->not_empty);
102 cond_destroy(&q->not_full);
103 SAFE_FREE(q->slots);
104 SAFE_FREE(q);
105}
106
107// Push item, timeout_ns=0 means non-blocking (drops if full)
108static bool frame_queue_push(frame_queue_t *q, void *item, uint64_t timeout_ns) {
109 mutex_lock(&q->mu);
110 while (q->count >= q->capacity) {
111 if (timeout_ns == 0) {
112 // Non-blocking: drop the item
113 mutex_unlock(&q->mu);
114 return false;
115 }
116 if (cond_timedwait(&q->not_full, &q->mu, timeout_ns) != 0) {
117 mutex_unlock(&q->mu);
118 return false; // timeout
119 }
120 }
121 q->slots[q->tail] = item;
122 q->tail = (q->tail + 1) % q->capacity;
123 q->count++;
124 ascii_otel_metric(ASCII_OTEL_METRIC_ENCODE_DEPTH, q->count);
125 cond_signal(&q->not_empty);
126 mutex_unlock(&q->mu);
127 return true;
128}
129
130// Pop item, timeout_ns=0 means blocking forever
131static void *frame_queue_pop(frame_queue_t *q, uint64_t timeout_ns) {
132 mutex_lock(&q->mu);
133 while (q->count == 0) {
134 if (timeout_ns == 0) {
135 // Block forever
136 cond_wait(&q->not_empty, &q->mu);
137 } else {
138 if (cond_timedwait(&q->not_empty, &q->mu, timeout_ns) != 0) {
139 mutex_unlock(&q->mu);
140 return NULL; // timeout
141 }
142 }
143 }
144 void *item = q->slots[q->head];
145 q->head = (q->head + 1) % q->capacity;
146 q->count--;
147 ascii_otel_metric(!strcmp(q->name, "display") ? ASCII_OTEL_METRIC_DISPLAY_DEPTH : ASCII_OTEL_METRIC_ENCODE_DEPTH,
148 q->count);
149 pipeline_frame_t *telemetry_frame = item;
150 double age = (double)(time_get_ns() - telemetry_frame->captured_ns) / 1e9;
151 ascii_otel_metric(!strcmp(q->name, "display") ? ASCII_OTEL_METRIC_DISPLAY_WAIT : ASCII_OTEL_METRIC_ENCODE_WAIT, age);
152 ascii_otel_metric(ASCII_OTEL_METRIC_QUEUE_OLDEST, age);
153 cond_signal(&q->not_full);
154 mutex_unlock(&q->mu);
155 return item;
156}
157
158static int frame_queue_count(frame_queue_t *q) {
159 if (!q)
160 return 0;
161 mutex_lock(&q->mu);
162 int count = q->count;
163 mutex_unlock(&q->mu);
164 return count;
165}
166
167static void frame_queue_flush(frame_queue_t *q, void (*free_fn)(void *)) {
168 if (!q)
169 return;
170 mutex_lock(&q->mu);
171 while (q->count > 0) {
172 void *item = q->slots[q->head];
173 q->head = (q->head + 1) % q->capacity;
174 q->count--;
175 ascii_otel_metric(!strcmp(q->name, "display") ? ASCII_OTEL_METRIC_DISPLAY_DEPTH : ASCII_OTEL_METRIC_ENCODE_DEPTH,
176 q->count);
177 pipeline_frame_t *telemetry_frame = item;
178 double age = (double)(time_get_ns() - telemetry_frame->captured_ns) / 1e9;
179 ascii_otel_metric(!strcmp(q->name, "display") ? ASCII_OTEL_METRIC_DISPLAY_WAIT : ASCII_OTEL_METRIC_ENCODE_WAIT,
180 age);
181 ascii_otel_metric(ASCII_OTEL_METRIC_QUEUE_OLDEST, age);
182 if (item && free_fn)
183 free_fn(item);
184 }
185 mutex_unlock(&q->mu);
186}
187
188/* ============================================================================
189 * Frame Helpers
190 * ============================================================================ */
191
192static pipeline_frame_t *make_frame_copy(const image_t *src, frame_slot_t *slots) {
193 if (src->w <= 0 || src->h <= 0 || (size_t)src->w > SIZE_MAX / sizeof(rgb_pixel_t) / (size_t)src->h) {
194 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid pipeline frame dimensions");
195 return NULL;
196 }
197 size_t bytes = (size_t)src->w * (size_t)src->h * sizeof(rgb_pixel_t);
198 pipeline_frame_t *f = NULL;
199 if (slots) {
200 for (size_t i = 0; i < PIPELINE_FRAME_SLOTS; ++i) {
201 bool expected = false;
202 if (atomic_compare_exchange_strong_explicit(&slots[i].busy, &expected, true, memory_order_acquire,
203 memory_order_relaxed)) {
204 f = &slots[i].frame;
205 f->slot = &slots[i];
206 if (slots[i].capacity < bytes) {
208 f->pixels = NULL;
209 slots[i].capacity = 0;
210 }
211 break;
212 }
213 }
214 }
215 if (!f)
216 f = SAFE_CALLOC(1, sizeof(*f), pipeline_frame_t *);
217 if (!f->pixels) {
218 f->pixels = buffer_alloc(NULL, bytes);
219 if (!f->pixels) {
220 if (f->slot)
221 atomic_store_explicit(&f->slot->busy, false, memory_order_release);
222 else
223 SAFE_FREE(f);
224 return NULL;
225 }
226 if (f->slot)
227 f->slot->capacity = bytes;
228 }
229 f->w = src->w;
230 f->h = src->h;
231 f->captured_ns = time_get_ns();
232 memcpy((void *)f->pixels, src->pixels, bytes);
233 return f;
234}
235
236static void free_frame(pipeline_frame_t *f) {
237 if (!f)
238 return;
239 if (f->slot) {
240 atomic_store_explicit(&f->slot->busy, false, memory_order_release);
241 return;
242 }
244 SAFE_FREE(f);
245}
246
247static void frame_slots_destroy(frame_slot_t *slots) {
248 for (size_t i = 0; i < PIPELINE_FRAME_SLOTS; ++i) {
249 buffer_release(slots[i].frame.pixels);
250 slots[i].frame.pixels = NULL;
251 slots[i].capacity = 0;
252 }
253}
254
255// Generic wrapper for frame_queue_flush that matches the expected void (*)(void *) signature
256static void free_frame_generic(void *f) {
257 free_frame((pipeline_frame_t *)f);
258}
259
260// Validate frame dimensions to detect corruption
261static bool frame_is_valid(const pipeline_frame_t *f) {
262 if (!f) {
263 log_error("Frame pointer is NULL");
264 return false;
265 }
266 // Reasonable bounds: width/height should be between 1 and 10000
267 if (f->w <= 0 || f->w > 10000 || f->h <= 0 || f->h > 10000) {
268 log_error("Invalid frame dimensions detected: w=%d, h=%d (likely corrupted)", f->w, f->h);
269 return false;
270 }
271 if (!f->pixels) {
272 log_error("Frame pixels pointer is NULL");
273 return false;
274 }
275 return true;
276}
277
278/* ============================================================================
279 * Pipeline Context
280 * ============================================================================ */
281
288 platform_thread_t capture_tid;
289 platform_thread_t encode_tid;
290 platform_thread_t input_tid;
295
297 atomic_ptr_t display_pending;
300
303
304 atomic_t stop;
307 asciichat_error_context_t capture_failure; // Read after capture joins.
308 asciichat_error_t encode_error; // Encode-owned; read only after joining encode.
309 uint64_t frames_accepted; // Capture-owned; read only after joining capture.
310 bool has_render_file; // Track if render_file was set at creation time
311};
312
313/* ============================================================================
314 * Capture Thread
315 * ============================================================================ */
316
317static void *pipeline_capture_thread(void *arg) {
318 session_pipeline_t *pipeline = (session_pipeline_t *)arg;
319 log_debug("[PIPELINE_CAPTURE] Starting capture thread");
320
321 bool snapshot_mode = GET_OPTION(snapshot_mode);
322 uint64_t snapshot_frames_captured = 0;
323 uint64_t snapshot_target_frames = 1;
324 media_source_t *capture_source = session_capture_get_media_source(pipeline->capture);
325 media_source_type_t capture_source_type = media_source_get_type(capture_source);
326 bool snapshot_uses_frame_target =
327 (capture_source_type == MEDIA_SOURCE_FILE || capture_source_type == MEDIA_SOURCE_STDIN) &&
328 !media_source_uses_webcam(capture_source);
329 if (snapshot_mode) {
330 // Initialize duration estimate BEFORE encoding frames so PTS scaling works from frame 1
332 double snapshot_delay = GET_OPTION(snapshot_delay);
333 g_snapshot_actual_duration_ms = (uint64_t)(snapshot_delay * 1000.0);
334 if (snapshot_delay > 0.0 && snapshot_uses_frame_target) {
335 double target_frames = snapshot_delay * session_capture_get_target_fps(pipeline->capture);
336 snapshot_target_frames = (uint64_t)(target_frames + 0.999999);
337 }
338 log_debug(
339 "[PIPELINE_CAPTURE] Snapshot mode: initialized g_snapshot_actual_duration_ms=%llu ms (snapshot_delay=%.2f)",
340 (unsigned long long)g_snapshot_actual_duration_ms, snapshot_delay);
341 }
342
343 uint64_t previous_serial = 0;
344 while (!atomic_load_bool(&pipeline->stop)) {
345 ASCII_OTEL_SCOPE(PIPELINE_CAPTURE);
347 uint64_t acquisition_started_ns = time_get_ns();
349
350 if (!img) {
351 if (errno_peek_since(frame_scope, &pipeline->capture_failure))
352 break;
353 if (session_capture_at_end(pipeline->capture)) {
354 log_debug("[PIPELINE_CAPTURE] End of media reached");
355 break;
356 }
357 platform_sleep_ns(1 * NS_PER_MS_INT);
358 continue;
359 }
360
361 // Copy frame
362 pipeline_frame_t *frame = make_frame_copy(img, pipeline->has_render_file ? NULL : pipeline->frame_slots);
363 if (!frame) {
365 continue;
366 }
367 frame->acquisition_started_ns = acquisition_started_ns;
369
372 frame->fresh = frame->frame_serial != previous_serial;
373 previous_serial = frame->frame_serial;
374
375 // Record first frame timestamp for snapshot timer
376 uint64_t zero = 0;
377 atomic_cas_u64(&pipeline->first_frame_ns, &zero, frame->captured_ns);
378
379 int telemetry_screen = ui_controller_current_screen();
380 unsigned expected = telemetry_screen == UI_SCREEN_PROMPT || telemetry_screen == UI_SCREEN_LOGS ? 0 : 1;
381 ascii_otel_pipeline_progress(0, expected | (pipeline->has_render_file ? 2 : 0));
382 // Fan out: display queue (fast, can drop), encode queue (slow, keep all)
383 pipeline_frame_t *display_copy = frame;
384 if (pipeline->has_render_file) {
385 display_copy = SAFE_MALLOC(sizeof(*display_copy), pipeline_frame_t *);
386 memcpy(display_copy, frame, sizeof(*display_copy));
387 display_copy->pixels = buffer_share(frame->pixels);
388 }
389 pipeline_frame_t *retired = atomic_ptr_exchange(&pipeline->display_pending, display_copy);
390 ascii_otel_metric(ASCII_OTEL_METRIC_DISPLAY_DEPTH, 1);
392 if (retired)
393 ascii_otel_metric(ASCII_OTEL_METRIC_DROP_DISPLAY, 1);
394 if (retired) {
397 free_frame(retired);
398 }
400
401 if (pipeline->has_render_file) {
402 // Encode all captured frames, including frames before display rendering starts
403 // The encoder will use them with correct timestamps regardless of display timing
404 bool queued = frame_queue_push(pipeline->encode_queue, frame, 500 * NS_PER_MS_INT);
405 while (!queued && snapshot_uses_frame_target && !atomic_load_bool(&pipeline->stop))
406 queued = frame_queue_push(pipeline->encode_queue, frame, 100 * NS_PER_MS_INT);
407 if (!queued) {
408 ascii_otel_metric(ASCII_OTEL_METRIC_DROP_ENCODE, 1);
411 // Non-blocking: drop if queue full after 500ms
412 log_warn_every(US_PER_SEC_INT, "[PIPELINE_CAPTURE] Encode queue blocked, dropping frame");
413 free_frame(frame);
414 } else {
416 pipeline->frames_accepted++;
417 log_debug_every(60 * US_PER_SEC_INT, "[PIPELINE_CAPTURE] Enqueued frame to encode_queue");
418 }
419 }
420
421 // Publish the acquired image before pacing the next acquisition. Sleeping
422 // with an already captured frame adds a full interval to live latency.
424
425 // Check snapshot mode elapsed time AFTER queueing frame (ensure at least 1 frame is queued)
426 if (snapshot_mode) {
427 snapshot_frames_captured++;
428 uint64_t now_ns = time_get_ns();
429
430 // Set first capture timestamp on first frame (only once)
433 log_debug("[SNAPSHOT_CAPTURE] First frame captured at %llu ns", (unsigned long long)now_ns);
434 }
435
436 // Calculate elapsed time from first capture
437 double elapsed = (double)(now_ns - g_snapshot_first_capture_ns) / NS_PER_SEC_INT;
438 double snapshot_delay = GET_OPTION(snapshot_delay);
439 bool snapshot_complete =
440 snapshot_uses_frame_target ? snapshot_frames_captured >= snapshot_target_frames : elapsed >= snapshot_delay;
441
442 if (snapshot_complete) {
443 log_debug(
444 "[PIPELINE_CAPTURE] Snapshot video elapsed=%.3f reached delay=%.2f - stopping capture but waiting for "
445 "encode queue to drain",
446 elapsed, snapshot_delay);
447
448 // Update with actual measured duration so encoder can scale frames accurately
451 uint64_t actual_ms =
452 snapshot_uses_frame_target ? (uint64_t)(snapshot_delay * 1000.0) : (uint64_t)(elapsed * 1000.0);
454 uint64_t last_frame_elapsed_ns = now_ns - g_snapshot_first_capture_ns;
455 g_snapshot_last_capture_elapsed_ns = last_frame_elapsed_ns;
456 log_debug("[PIPELINE_CAPTURE] g_snapshot_actual_duration_ms=%llu, last_capture_elapsed_ns=%llu",
457 (unsigned long long)actual_ms, (unsigned long long)last_frame_elapsed_ns);
458
459 break;
460 }
461 }
462 }
463
464 atomic_store_bool(&pipeline->capture_finished, true);
466 log_debug("[PIPELINE_CAPTURE] Capture thread exiting");
467 return NULL;
468}
469
470/* ============================================================================
471 * Encode Thread
472 * ============================================================================ */
473
474static void *pipeline_encode_thread(void *arg) {
475 session_pipeline_t *pipeline = (session_pipeline_t *)arg;
476 log_debug("[PIPELINE_ENCODE] Starting encode thread");
477
478 uint64_t frames_processed = 0;
479 uint64_t recording_epoch = UINT64_MAX;
480 while (true) {
481 // Drain queued frames before exiting, even when capture was interrupted.
482 pipeline_frame_t *frame = (pipeline_frame_t *)frame_queue_pop(pipeline->encode_queue, 100 * NS_PER_MS_INT);
483
484 if (!frame) {
485 // An empty queue is final only after the producer has exited.
486 if (atomic_load_bool(&pipeline->capture_finished) && frame_queue_count(pipeline->encode_queue) == 0)
487 break; // No producer remains and the queue has drained.
488 log_debug_every(1 * US_PER_SEC_INT, "[PIPELINE_ENCODE] Waiting for frames (processed=%llu)",
489 (unsigned long long)frames_processed);
490 continue; // timeout, keep waiting
491 }
492
493 ASCII_OTEL_SCOPE_PARENT(PIPELINE_ENCODE, &frame->telemetry_context);
494 // Validate frame before processing (catch memory corruption)
495 if (!frame_is_valid(frame)) {
496 ascii_otel_metric(ASCII_OTEL_METRIC_DROP_INVALID, 1);
497 log_error("Skipping frame with corrupted dimensions in encode thread");
498 free_frame(frame);
499 continue;
500 }
501
502 if (frames_processed == 0 || frames_processed % 30 == 0) {
503 log_debug_every(US_PER_SEC_INT, "[PIPELINE_ENCODE] Processing frame %llu: %dx%d",
504 (unsigned long long)frames_processed, frame->w, frame->h);
505 }
506
507 if (recording_epoch != frame->source_epoch) {
509 recording_epoch = frame->source_epoch;
510 }
511
512 // Encode frame (convert_to_ascii is called internally by encode_frame)
513 // Avoid double-conversion that was causing state desynchronization between display/encode threads
514 image_t raw_image = {.w = frame->w, .h = frame->h, .pixels = (rgb_pixel_t *)frame->pixels};
515 pipeline->encode_error = session_display_encode_frame(pipeline->display, &raw_image, frame->captured_ns);
516 if (pipeline->encode_error == ASCIICHAT_OK) {
518 ascii_otel_metric(ASCII_OTEL_METRIC_ENCODE_LATENCY,
519 (double)(time_get_ns() - frame->acquisition_started_ns) / 1e9);
520 }
521 if (pipeline->encode_error != ASCIICHAT_OK) {
522 atomic_store_bool(&pipeline->stop, true);
524 free_frame(frame);
525 break;
526 }
527 frames_processed++;
528
529 free_frame(frame);
530 }
531
532 log_debug("[PIPELINE_ENCODE] Encode thread exiting (total frames encoded=%llu)",
533 (unsigned long long)frames_processed);
534 return NULL;
535}
536
537/* ============================================================================
538 * Public API
539 * ============================================================================ */
540
543 if (!capture || !display || !out) {
544 return SET_ERRNO(ERROR_INVALID_PARAM, "NULL argument");
545 }
546
548 for (size_t i = 0; i < PIPELINE_FRAME_SLOTS; ++i)
549 atomic_init(&p->frame_slots[i].busy, false);
550 p->capture = capture;
551 p->display = display;
553 SAFE_FREE(p);
554 return SET_ERRNO(ERROR_INIT, "failed to create display wakeup");
555 }
556 atomic_ptr_store(&p->display_pending, NULL);
557 NAMED_REGISTER_ATOMIC_PTR(&p->display_pending, "display.pending_frame", p);
558 p->encode_queue = frame_queue_create(256, "encode"); // Large buffer for slow encoding
559
560 // Check if display has render_file configured (will be checked in encode thread)
561 // We store a flag to know whether to enqueue frames for encoding
562 double render_fps = session_display_get_render_fps(display);
563 bool has_file = session_display_has_render_file(display);
565 // Synthetic/visualization sessions can switch sources too. Their recording
566 // clock must remain elapsed session time across gaps and source FPS changes.
569
570 // Playback FPS also has a default when no recording was requested. It must
571 // not enable the encoder's lossless queue for a live-only session.
572 p->has_render_file = has_file;
573 log_debug("[PIPELINE_CREATE] render_fps=%.1f, has_file=%d, has_render_file=%d", render_fps, has_file ? 1 : 0,
574 p->has_render_file ? 1 : 0);
575
576 atomic_store_bool(&p->stop, false);
577 atomic_store_bool(&p->capture_finished, false);
578 NAMED_REGISTER_ATOMIC(&p->capture_finished, "pipeline_capture_finished", p);
579 atomic_store_u64(&p->first_frame_ns, 0);
580
581 // Start capture thread
582 if (platform_thread_create(&p->capture_tid, "pipeline_capture", pipeline_capture_thread, p) != 0) {
583 free_frame(atomic_ptr_exchange(&p->display_pending, NULL));
586 frame_queue_destroy(p->encode_queue);
588 SAFE_FREE(p);
589 return SET_ERRNO(ERROR_INIT, "failed to start capture thread");
590 }
591
592 // Start encode thread only if render_file is active
593 if (p->has_render_file) {
594 if (platform_thread_create(&p->encode_tid, "pipeline_encode", pipeline_encode_thread, p) != 0) {
595 atomic_store_bool(&p->stop, true);
596 thread_join(&p->capture_tid, NULL);
597 free_frame(atomic_ptr_exchange(&p->display_pending, NULL));
598 frame_slots_destroy(p->frame_slots);
601 frame_queue_destroy(p->encode_queue);
603 SAFE_FREE(p);
604 return SET_ERRNO(ERROR_INIT, "failed to start encode thread");
605 }
606 }
607
608 *out = p;
609 return ASCIICHAT_OK;
610}
611
612// Input has its own bounded poll cadence; ASCII conversion and terminal output
613// must never determine when an interactive command is consumed.
614static void *pipeline_input_thread(void *data) {
615 session_pipeline_t *pipeline = data;
617 atomic_store(&pipeline->input_start_status, result == ASCIICHAT_OK ? 1 : -1);
618 if (result != ASCIICHAT_OK)
619 return NULL;
620 while (!atomic_load_bool(&pipeline->stop) && !pipeline->should_exit(pipeline->input_data)) {
622 if (key != KEY_NONE)
623 pipeline->keyboard_handler(pipeline->capture, (int)key, pipeline->input_data);
624 else
626 }
627 return NULL;
628}
629
631 session_keyboard_handler_fn keyboard_handler, void *user_data) {
632 if (!pipeline || !should_exit) {
633 return SET_ERRNO(ERROR_INVALID_PARAM, "NULL argument");
634 }
635
636 log_debug("[PIPELINE_MAIN] Starting main thread loop");
637 uint64_t display_epoch = UINT64_MAX;
638 if (keyboard_handler && !GET_OPTION(snapshot_mode) && !(pipeline->has_render_file && !terminal_is_interactive())) {
639 pipeline->keyboard_handler = keyboard_handler;
640 pipeline->should_exit = should_exit;
641 pipeline->input_data = user_data;
642 atomic_store(&pipeline->input_start_status, 0);
643 if (platform_thread_create(&pipeline->input_tid, "pipeline_input", pipeline_input_thread, pipeline) != ASCIICHAT_OK)
644 return SET_ERRNO(ERROR_THREAD, "Cannot start responsive session input");
645 while (atomic_load(&pipeline->input_start_status) == 0)
647 if (atomic_load(&pipeline->input_start_status) < 0) {
648 thread_join(&pipeline->input_tid, NULL);
649 return SET_ERRNO(ERROR_INIT, "Cannot initialize session keyboard input");
650 }
651 }
652
653 // Snapshot duration is owned by the capture thread. It records the first
654 // captured frame and marks capture_finished at its duration limit.
655 // The display thread waits for producer completion instead of using
656 // a second timer based on terminal rendering speed; large terminals can make
657 // ASCII conversion and render-file encoding substantially slower than capture.
658 while (!should_exit(user_data) && !atomic_load_bool(&pipeline->stop)) {
659 bool exit_check = should_exit(user_data);
660 log_debug_every(US_PER_SEC_INT, "[PIPELINE_DEBUG] should_exit=%d, stop=%d", exit_check,
661 atomic_load_bool(&pipeline->stop));
662
664 pipeline_frame_t *frame = atomic_ptr_exchange(&pipeline->display_pending, NULL);
665
666 if (!frame) {
667 if (atomic_load_bool(&pipeline->capture_finished) && !atomic_ptr_load(&pipeline->display_pending))
668 break;
669 continue;
670 }
671
672 ascii_otel_metric(ASCII_OTEL_METRIC_DISPLAY_DEPTH, 0);
673 double queue_age = (double)(time_get_ns() - frame->captured_ns) / 1e9;
674 ascii_otel_metric(ASCII_OTEL_METRIC_DISPLAY_WAIT, queue_age);
675 ascii_otel_metric(ASCII_OTEL_METRIC_QUEUE_OLDEST, queue_age);
676 ASCII_OTEL_SCOPE_PARENT(PIPELINE_DISPLAY, &frame->telemetry_context);
677 ascii_otel_scope_int(&otel_scope, "ascii_chat.queue.wait_ns", (int64_t)(time_get_ns() - frame->captured_ns));
678 // Validate frame structure exists before accessing fields
679 if (!frame_is_valid(frame)) {
680 ascii_otel_metric(ASCII_OTEL_METRIC_DROP_INVALID, 1);
681 log_error("Received invalid frame: corrupted or NULL");
682 free_frame(frame);
683 frame = NULL;
684 continue;
685 }
686
687 // Check if help screen is active - if so, don't render ASCII frames
689 // Snapshot mode has no interactive prompt/log overlay. Do not discard its
690 // only captured frame because startup UI state is still settling.
691 bool help_is_active = !GET_OPTION(snapshot_mode) && (screen == UI_SCREEN_PROMPT || screen == UI_SCREEN_LOGS);
692
693 if (!help_is_active) {
694 // Convert to ASCII
695 if (display_epoch != frame->source_epoch) {
697 display_epoch = frame->source_epoch;
698 }
699 image_t tmp = {.w = frame->w, .h = frame->h, .pixels = (rgb_pixel_t *)frame->pixels};
700 uint64_t conversion_started_ns = time_get_ns();
701 char *ascii = session_display_convert_to_ascii_reuse(pipeline->display, &tmp);
702 uint64_t converted_ns = time_get_ns();
703 double read_ms = (double)(frame->captured_ns - frame->acquisition_started_ns) / NS_PER_MS_INT;
704 double queue_ms = (double)(conversion_started_ns - frame->captured_ns) / NS_PER_MS_INT;
705 double convert_ms = (double)(converted_ns - conversion_started_ns) / NS_PER_MS_INT;
706 uint64_t epoch = frame->source_epoch;
707 bool fresh = frame->fresh;
708 uint64_t acquisition_started_ns = frame->acquisition_started_ns;
709 free_frame(frame);
710 frame = NULL;
711
712 if (ascii) {
713 if (epoch == session_capture_source_epoch(pipeline->capture)) {
714 ui_controller_frame_acquired_at(acquisition_started_ns);
715 session_display_write_ascii_frame(pipeline->display, ascii, fresh);
717 uint64_t published_ns = time_get_ns();
718 ascii_otel_metric(ASCII_OTEL_METRIC_FRAME_DISPLAYED, 1);
720 ascii_otel_metric(ASCII_OTEL_METRIC_FRAME_LATENCY, (double)(published_ns - acquisition_started_ns) / 1e9);
721 session_capture_complete_open(pipeline->capture, epoch);
723 "LIVE_FRAME: capture_to_publish_ms=%.2f fresh=%d read_ms=%.2f queue_ms=%.2f convert_ms=%.2f "
724 "publish_ms=%.2f",
725 (double)(published_ns - acquisition_started_ns) / NS_PER_MS_INT, fresh, read_ms, queue_ms,
726 convert_ms, (double)(published_ns - converted_ns) / NS_PER_MS_INT);
727 }
728 session_display_release_ascii(pipeline->display, ascii);
729 } else {
730 ascii_otel_metric(ASCII_OTEL_METRIC_DROP_CONVERT, 1);
731 log_error_every(US_PER_SEC_INT, "Failed to convert captured frame to ASCII; skipping display frame");
732 }
733 } else {
734 ascii_otel_metric(ASCII_OTEL_METRIC_DROP_SCREEN, 1);
735 free_frame(frame);
736 frame = NULL;
737 }
738 }
739
740 log_debug("[PIPELINE_MAIN] Main loop exiting, signaling threads to stop");
741 atomic_store_bool(&pipeline->stop, true);
743 if (thread_is_initialized(&pipeline->input_tid) && thread_join(&pipeline->input_tid, NULL) != ASCIICHAT_OK)
744 return SET_ERRNO(ERROR_THREAD, "Cannot join session input thread");
745
746 return ASCIICHAT_OK;
747}
748
750 if (!pipeline)
751 return ASCIICHAT_OK;
752
753 log_debug("[PIPELINE] Destroying pipeline, waiting for threads...");
754
755 // Signal threads to stop (may already be stopped)
756 atomic_store_bool(&pipeline->stop, true);
758
759 // Wake up any threads blocked on condition variable waits
760 // so they can check the stop flag and exit immediately
761 // instead of waiting for timeouts (e.g., 100ms frame_queue_pop timeout)
763 if (pipeline->encode_queue) {
764 mutex_lock(&pipeline->encode_queue->mu);
765 cond_broadcast(&pipeline->encode_queue->not_empty);
766 cond_broadcast(&pipeline->encode_queue->not_full);
767 mutex_unlock(&pipeline->encode_queue->mu);
768 }
769
770 if (thread_is_initialized(&pipeline->input_tid) && thread_join(&pipeline->input_tid, NULL) != ASCIICHAT_OK)
771 return SET_ERRNO(ERROR_THREAD, "Cannot join session input thread during shutdown");
772 session_display_begin_drain(pipeline->display, 0, false);
773
774 // A timeout must never let the owner free memory still referenced by a worker.
775 // Capture observes stop; encode drains its queue and exits once capture is done.
776 if (thread_is_initialized(&pipeline->capture_tid)) {
777 if (thread_join(&pipeline->capture_tid, NULL) != 0)
778 return SET_ERRNO(ERROR_THREAD, "Cannot join pipeline capture thread");
779 }
780 session_display_begin_drain(pipeline->display, pipeline->frames_accepted, true);
781 if (thread_is_initialized(&pipeline->encode_tid)) {
782 if (thread_join(&pipeline->encode_tid, NULL) != 0)
783 return SET_ERRNO(ERROR_THREAD, "Cannot join pipeline encode thread");
784 }
785
786 // Flush queues to free any remaining frames
787 free_frame(atomic_ptr_exchange(&pipeline->display_pending, NULL));
788 frame_queue_flush(pipeline->encode_queue, free_frame_generic);
789
790 frame_slots_destroy(pipeline->frame_slots);
793 frame_queue_destroy(pipeline->encode_queue);
796 : pipeline->encode_error;
797 if (pipeline->capture_failure.code != ASCIICHAT_OK)
798 LOG_ERRNO_IF_SET("Media capture failed");
799 SAFE_FREE(pipeline);
800
801 log_debug("[PIPELINE] Pipeline destroyed");
802 return err;
803}
bool errno_peek_since(asciichat_errno_scope_t scope, asciichat_error_context_t *out)
asciichat_errno_scope_t errno_checkpoint(void)
asciichat_error_t errno_import(const asciichat_error_context_t *cause)
#define SET_ERRNO(code,...)
#define LOG_ERRNO_IF_SET(message)
void * buffer_alloc(buffer_domain_t *domain, size_t size)
Definition buffer.c:274
const void * buffer_share(const void *data)
Definition buffer.c:304
#define buffer_release(data)
Definition buffer.h:91
uint64_t session_capture_frame_serial(session_capture_ctx_t *ctx)
void session_capture_complete_open(session_capture_ctx_t *ctx, uint64_t epoch)
uint64_t session_capture_source_epoch(session_capture_ctx_t *ctx)
📹 Unified media capture abstraction for session-based video sources
Named object registry for debugging — log identifiable resource names.
audio_visualization_source_t source
Definition fft.c:24
#define SAFE_FREE(ptr)
Definition common.h:377
#define SAFE_MALLOC(size, cast)
Definition common.h:265
#define SAFE_CALLOC(count, size, cast)
Definition common.h:275
unsigned long long uint64_t
Definition common.h:63
unsigned char uint8_t
Definition common.h:60
#define NAMED_REGISTER_ATOMIC(a, name, parent_ptr)
Register an atomic_t with automatic format specifier.
#define NAMED_REGISTER_ATOMIC_PTR(a, name, parent_ptr)
Register an _Atomic(void *) with automatic format specifier.
#define NAMED_UNREGISTER(ptr)
Unregister a pointer.
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_INIT
Definition error_codes.h:56
@ ERROR_INVALID_PARAM
@ ERROR_THREAD
#define log_error(...)
Log an ERROR message.
Definition log/log.h:634
#define log_debug(...)
Log a DEBUG message.
Definition log/log.h:595
media_source_type_t
Media source type enumeration.
Definition source.h:97
media_source_type_t media_source_get_type(media_source_t *source)
Get media source type.
Definition source.c:999
@ MEDIA_SOURCE_STDIN
Piped or redirected input.
Definition source.h:100
@ MEDIA_SOURCE_FILE
Media file (video/audio).
Definition source.h:99
#define NS_PER_SEC_INT
Definition time.h:169
#define NS_PER_MS_INT
Definition time.h:168
#define US_PER_SEC_INT
Definition time.h:173
#define GET_OPTION(field)
Safely get a specific option field (lock-free read).
Definition options.h:1336
asciichat_error_t keyboard_init(void)
Initialize keyboard input system.
void platform_sleep_ms(unsigned int ms)
Sleep for a specified number of milliseconds.
keyboard_key_t
Options for interactive line editing.
Definition keyboard.h:53
@ KEY_NONE
No key pressed or no input available.
Definition keyboard.h:54
bool(* session_should_exit_fn)(void *user_data)
Exit condition callback type.
void(* session_keyboard_handler_fn)(session_capture_ctx_t *capture, int key, void *user_data)
Keyboard input handler callback type.
struct session_display_ctx session_display_ctx_t
session display config is intentionally a small, concrete record. Fields such as snapshot_mode,...
void session_display_begin_drain(session_display_ctx_t *ctx, uint64_t total, bool total_known)
asciichat_error_t session_display_encode_frame(session_display_ctx_t *ctx, const image_t *image, uint64_t captured_ns)
Encode frame to render-file (FFmpeg only, no terminal output).
bool session_capture_at_end(session_capture_ctx_t *ctx)
Check if capture source has reached end of stream.
struct session_capture_ctx session_capture_ctx_t
session capture config keeps the values that move through the subsystem: type, path,...
image_t * session_capture_read_frame(session_capture_ctx_t *ctx)
Read the next video frame from the capture source.
bool session_display_has_render_file(session_display_ctx_t *ctx)
Check if display has render-file configured.
uint32_t session_capture_get_target_fps(session_capture_ctx_t *ctx)
Get the target FPS configured for this capture context.
void * session_capture_get_media_source(session_capture_ctx_t *ctx)
Get the underlying media source from capture context.
void session_capture_sleep_for_fps(session_capture_ctx_t *ctx)
Sleep to maintain target frame rate.
uint32_t session_display_get_render_fps(session_display_ctx_t *ctx)
Get the render FPS configured for file output.
🆘 Interactive keyboard help overlay for session keyboard shortcuts
void stats_counter_add(stats_scope_t *scope, stats_counter_id_t id, uint64_t amount)
📝 Logging API with multiple log levels and terminal output control
#define log_debug_every(interval_us, fmt,...)
Rate-limited DEBUG logging.
Definition log/log.h:749
#define log_error_every(interval_us, fmt,...)
Rate-limited ERROR logging.
Definition log/log.h:758
#define log_warn_every(interval_us, fmt,...)
Rate-limited WARN logging.
Definition log/log.h:755
bool should_exit(void)
Definition main.c:249
⚙️ Unified options parsing system for ascii-chat with builder pattern and lock-free access
gotel_context_t ascii_otel_context(void)
Definition otel.c:725
void ascii_otel_pipeline_progress(unsigned stage, unsigned expected)
Definition otel.c:270
void ascii_otel_scope_int(ascii_otel_scope_t *s, const char *key, int64_t value)
Definition otel.c:728
void ascii_otel_metric(ascii_otel_metric_t metric, double value)
Definition otel.c:161
#define ASCII_OTEL_SCOPE(id)
Definition otel.h:72
#define ASCII_OTEL_SCOPE_PARENT(id, parent)
Definition otel.h:69
asciichat_error_t session_pipeline_run_main(session_pipeline_t *pipeline, session_should_exit_fn should_exit, session_keyboard_handler_fn keyboard_handler, void *user_data)
Definition pipeline.c:630
struct frame_slot_s frame_slot_t
Definition pipeline.c:42
struct frame_queue_s frame_queue_t
#define PIPELINE_FRAME_SLOTS
Definition pipeline.c:72
asciichat_error_t session_pipeline_create(session_capture_ctx_t *capture, session_display_ctx_t *display, session_pipeline_t **out)
Definition pipeline.c:541
bool g_snapshot_first_frame_rendered
asciichat_error_t session_pipeline_destroy(session_pipeline_t *pipeline)
Definition pipeline.c:749
Three-thread render pipeline: capture → display/encode.
struct session_pipeline_s session_pipeline_t
Definition pipeline.h:22
static bool size_t gotel_query_t * out
Definition queries.h:8
stats_scope_t * stats_runtime_scope(void)
Definition runtime.c:80
bool media_source_uses_webcam(media_source_t *source)
Definition source.c:587
void session_display_release_ascii(session_display_ctx_t *ctx, char *ascii)
void session_display_reset_stabilizer(session_display_ctx_t *ctx, bool recording)
char * session_display_convert_to_ascii_reuse(session_display_ctx_t *ctx, const image_t *image)
void session_display_set_render_live_timing(session_display_ctx_t *ctx)
void session_display_write_ascii_frame(session_display_ctx_t *ctx, const char *ascii, bool fresh)
🖥️ Unified terminal display abstraction for session-based rendering
uint64_t g_snapshot_last_capture_elapsed_ns
uint64_t g_snapshot_first_capture_ns
uint64_t g_snapshot_actual_duration_ms
Centralized render loop abstraction for all display modes.
🔤 String Manipulation and Shell Escaping Utilities
When code needs to work with asciichat errno snapshot, it keeps total, copied here instead of scatter...
void ** slots
Definition pipeline.c:76
const char * name
Definition pipeline.c:82
cond_t not_empty
Definition pipeline.c:80
cond_t not_full
Definition pipeline.c:81
mutex_t mu
Definition pipeline.c:79
_Atomic bool busy
Definition pipeline.c:68
size_t capacity
Definition pipeline.c:69
pipeline_frame_t frame
Definition pipeline.c:67
SIMD-friendly storage for one RGB pixel. r, g, and b hold the color channels and padding keeps the la...
int w
Image width in pixels (must be > 0).
int h
Image height in pixels (must be > 0).
rgb_pixel_t * pixels
Pixel data array (width * height RGB pixels, row-major order).
Media source for video and audio capture.
Definition source.c:47
uint64_t acquisition_started_ns
Definition pipeline.c:52
uint64_t source_epoch
Definition pipeline.c:48
frame_slot_t * slot
Definition pipeline.c:45
uint64_t captured_ns
Definition pipeline.c:51
gotel_context_t telemetry_context
Definition pipeline.c:49
const uint8_t * pixels
Definition pipeline.c:46
uint64_t frame_serial
Definition pipeline.c:48
For session pipeline, this is the live record that ties capture_tid, encode_tid, input_tid,...
Definition pipeline.c:287
frame_slot_t frame_slots[3]
Definition pipeline.c:296
platform_thread_t encode_tid
Definition pipeline.c:289
atomic_t capture_finished
Definition pipeline.c:305
platform_wakeup_t * display_ready
Definition pipeline.c:298
platform_thread_t capture_tid
Definition pipeline.c:288
atomic_ptr_t display_pending
Definition pipeline.c:297
session_should_exit_fn should_exit
Definition pipeline.c:293
atomic_int input_start_status
Definition pipeline.c:291
asciichat_error_t encode_error
Definition pipeline.c:308
session_display_ctx_t * display
Definition pipeline.c:302
session_keyboard_handler_fn keyboard_handler
Definition pipeline.c:292
atomic_t first_frame_ns
Definition pipeline.c:306
frame_queue_t * encode_queue
Definition pipeline.c:299
session_capture_ctx_t * capture
Definition pipeline.c:301
uint64_t frames_accepted
Definition pipeline.c:309
platform_thread_t input_tid
Definition pipeline.c:290
asciichat_error_context_t capture_failure
Definition pipeline.c:307
⏱️ High-precision timing utilities using sokol_time.h and uthash
void ui_controller_frame_acquired_at(uint64_t started_ns)
int ui_controller_current_screen(void)
@ UI_SCREEN_PROMPT
@ UI_SCREEN_LOGS
@ UI_SCREEN_MEDIA
keyboard_key_t ui_input_read_key(ui_screen_t screen)
Definition tui/input.c:353
void platform_wakeup_signal(platform_wakeup_t *wake)
bool platform_wakeup_wait(platform_wakeup_t *wake, uint64_t timeout_ns)
asciichat_error_t platform_wakeup_create(platform_wakeup_t **out)
struct platform_wakeup platform_wakeup_t
Definition wakeup.h:9
void platform_wakeup_destroy(platform_wakeup_t *wake)