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

Three-thread render pipeline implementation. More...

Go to the source code of this file.

Data Structures

struct  pipeline_frame_t
struct  frame_slot_s
struct  frame_queue_s
struct  session_pipeline_s
 For session pipeline, this is the live record that ties capture_tid, encode_tid, input_tid, input_start_status together. The owning code creates it, updates it while work is active, and releases its handles and pointed-to storage before freeing the record. More...

Macros

#define PIPELINE_FRAME_SLOTS   3

Typedefs

typedef struct frame_slot_s frame_slot_t
typedef struct frame_queue_s frame_queue_t

Functions

asciichat_error_t session_pipeline_create (session_capture_ctx_t *capture, session_display_ctx_t *display, session_pipeline_t **out)
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)
asciichat_error_t session_pipeline_destroy (session_pipeline_t *pipeline)

Variables

bool g_snapshot_first_frame_rendered

Detailed Description

Three-thread render pipeline implementation.

Definition in file pipeline.c.

Macro Definition Documentation

◆ PIPELINE_FRAME_SLOTS

#define PIPELINE_FRAME_SLOTS   3

Definition at line 72 of file pipeline.c.

Referenced by session_pipeline_create().

Typedef Documentation

◆ frame_queue_t

typedef struct frame_queue_s frame_queue_t

◆ frame_slot_t

typedef struct frame_slot_s frame_slot_t

Definition at line 42 of file pipeline.c.

Function Documentation

◆ session_pipeline_create()

asciichat_error_t session_pipeline_create ( session_capture_ctx_t * capture,
session_display_ctx_t * display,
session_pipeline_t ** out )

Create and start the capture thread and (if –render-file) encode thread.

Parameters
captureCapture context (must have real media_source_t for synchronous mode)
displayDisplay context (may have render_file set for –render-file mode)
outPointer to receive allocated pipeline context
Returns
ASCIICHAT_OK on success, error code otherwise
Note
The pipeline does NOT own capture or display — caller must keep them alive until session_pipeline_destroy() returns.

Definition at line 541 of file pipeline.c.

542 {
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}
#define SET_ERRNO(code,...)
audio_visualization_source_t source
Definition fft.c:24
#define SAFE_FREE(ptr)
Definition common.h:377
#define SAFE_CALLOC(count, size, cast)
Definition common.h:275
#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_OK
Definition error_codes.h:46
@ ERROR_INIT
Definition error_codes.h:56
@ ERROR_INVALID_PARAM
#define log_debug(...)
Log a DEBUG message.
Definition log/log.h:595
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
bool session_display_has_render_file(session_display_ctx_t *ctx)
Check if display has render-file configured.
void * session_capture_get_media_source(session_capture_ctx_t *ctx)
Get the underlying media source from capture context.
uint32_t session_display_get_render_fps(session_display_ctx_t *ctx)
Get the render FPS configured for file output.
#define PIPELINE_FRAME_SLOTS
Definition pipeline.c:72
struct session_pipeline_s session_pipeline_t
Definition pipeline.h:22
static bool size_t gotel_query_t * out
Definition queries.h:8
void session_display_set_render_live_timing(session_display_ctx_t *ctx)
_Atomic bool busy
Definition pipeline.c:68
Media source for video and audio capture.
Definition source.c:47
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_display_ctx_t * display
Definition pipeline.c:302
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
asciichat_error_t platform_wakeup_create(platform_wakeup_t **out)
void platform_wakeup_destroy(platform_wakeup_t *wake)

References ASCIICHAT_OK, frame_slot_s::busy, session_pipeline_s::capture, session_pipeline_s::capture_finished, session_pipeline_s::capture_tid, session_pipeline_s::display, session_pipeline_s::display_pending, session_pipeline_s::display_ready, session_pipeline_s::encode_queue, session_pipeline_s::encode_tid, ERROR_INIT, ERROR_INVALID_PARAM, session_pipeline_s::first_frame_ns, session_pipeline_s::frame_slots, session_pipeline_s::has_render_file, log_debug, media_source_get_type(), MEDIA_SOURCE_STDIN, NAMED_REGISTER_ATOMIC, NAMED_REGISTER_ATOMIC_PTR, NAMED_UNREGISTER, out, PIPELINE_FRAME_SLOTS, platform_wakeup_create(), platform_wakeup_destroy(), SAFE_CALLOC, SAFE_FREE, session_capture_get_media_source(), session_display_get_render_fps(), session_display_has_render_file(), session_display_set_render_live_timing(), SET_ERRNO, source, and session_pipeline_s::stop.

Referenced by session_render_loop().

◆ session_pipeline_destroy()

asciichat_error_t session_pipeline_destroy ( session_pipeline_t * pipeline)

Stop threads and free pipeline.

Waits for capture and encode threads to exit cleanly.

Parameters
pipelinePipeline context (safe to call with NULL)
Returns
ASCIICHAT_OK

Definition at line 749 of file pipeline.c.

749 {
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}
asciichat_error_t errno_import(const asciichat_error_context_t *cause)
#define LOG_ERRNO_IF_SET(message)
asciichat_error_t
Error and exit codes - unified status values (0-255).
Definition error_codes.h:44
@ ERROR_THREAD
void session_display_begin_drain(session_display_ctx_t *ctx, uint64_t total, bool total_known)
void ascii_otel_pipeline_progress(unsigned stage, unsigned expected)
Definition otel.c:270
cond_t not_empty
Definition pipeline.c:80
cond_t not_full
Definition pipeline.c:81
mutex_t mu
Definition pipeline.c:79
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
void platform_wakeup_signal(platform_wakeup_t *wake)

References ascii_otel_pipeline_progress(), ASCIICHAT_OK, session_pipeline_s::capture_failure, session_pipeline_s::capture_finished, session_pipeline_s::capture_tid, asciichat_error_context_t::code, session_pipeline_s::display, session_pipeline_s::display_pending, session_pipeline_s::display_ready, session_pipeline_s::encode_error, session_pipeline_s::encode_queue, session_pipeline_s::encode_tid, errno_import(), ERROR_THREAD, session_pipeline_s::frame_slots, session_pipeline_s::frames_accepted, session_pipeline_s::input_tid, log_debug, LOG_ERRNO_IF_SET, frame_queue_s::mu, NAMED_UNREGISTER, frame_queue_s::not_empty, frame_queue_s::not_full, platform_wakeup_destroy(), platform_wakeup_signal(), SAFE_FREE, session_display_begin_drain(), SET_ERRNO, and session_pipeline_s::stop.

Referenced by session_render_loop().

◆ session_pipeline_run_main()

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 )

Main thread loop: drives terminal output and keyboard handling.

Pops frames from display queue, converts to ASCII, writes to terminal. Handles snapshot delay timer and exit conditions.

Parameters
pipelinePipeline context created by session_pipeline_create()
should_exitCallback to check if rendering should stop
keyboard_handlerOptional keyboard handler callback
user_dataOpaque data passed to callbacks
Returns
ASCIICHAT_OK on success
Note
This function blocks until should_exit() returns true or capture ends. Signals capture/encode threads to stop before returning.

Definition at line 630 of file pipeline.c.

631 {
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}
void session_capture_complete_open(session_capture_ctx_t *ctx, uint64_t epoch)
uint64_t session_capture_source_epoch(session_capture_ctx_t *ctx)
unsigned long long uint64_t
Definition common.h:63
#define log_error(...)
Log an ERROR message.
Definition log/log.h:634
#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
void platform_sleep_ms(unsigned int ms)
Sleep for a specified number of milliseconds.
#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
bool should_exit(void)
Definition main.c:249
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_PARENT(id, parent)
Definition otel.h:69
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_write_ascii_frame(session_display_ctx_t *ctx, const char *ascii, bool fresh)
SIMD-friendly storage for one RGB pixel. r, g, and b hold the color channels and padding keeps the la...
uint64_t acquisition_started_ns
Definition pipeline.c:52
uint64_t source_epoch
Definition pipeline.c:48
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
session_should_exit_fn should_exit
Definition pipeline.c:293
atomic_int input_start_status
Definition pipeline.c:291
session_keyboard_handler_fn keyboard_handler
Definition pipeline.c:292
void ui_controller_frame_acquired_at(uint64_t started_ns)
int ui_controller_current_screen(void)
@ UI_SCREEN_PROMPT
@ UI_SCREEN_LOGS
bool platform_wakeup_wait(platform_wakeup_t *wake, uint64_t timeout_ns)

References pipeline_frame_t::acquisition_started_ns, ascii_otel_metric(), ascii_otel_pipeline_progress(), ascii_otel_scope_int(), ASCII_OTEL_SCOPE_PARENT, ASCIICHAT_OK, session_pipeline_s::capture, session_pipeline_s::capture_finished, pipeline_frame_t::captured_ns, session_pipeline_s::display, session_pipeline_s::display_pending, session_pipeline_s::display_ready, ERROR_INIT, ERROR_INVALID_PARAM, ERROR_THREAD, pipeline_frame_t::fresh, GET_OPTION, pipeline_frame_t::h, session_pipeline_s::has_render_file, session_pipeline_s::input_data, session_pipeline_s::input_start_status, session_pipeline_s::input_tid, session_pipeline_s::keyboard_handler, log_debug, log_debug_every, log_error, log_error_every, NS_PER_MS_INT, pipeline_frame_t::pixels, platform_sleep_ms(), platform_wakeup_wait(), session_capture_complete_open(), session_capture_source_epoch(), session_display_convert_to_ascii_reuse(), session_display_release_ascii(), session_display_reset_stabilizer(), session_display_write_ascii_frame(), SET_ERRNO, session_pipeline_s::should_exit, should_exit(), pipeline_frame_t::source_epoch, session_pipeline_s::stop, pipeline_frame_t::telemetry_context, ui_controller_current_screen(), ui_controller_frame_acquired_at(), UI_SCREEN_LOGS, UI_SCREEN_PROMPT, US_PER_SEC_INT, and pipeline_frame_t::w.

Referenced by session_render_loop().

Variable Documentation

◆ g_snapshot_first_frame_rendered

bool g_snapshot_first_frame_rendered
extern