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

Go to the source code of this file.

Macros

#define SERVER_RENDER_THREAD_DISPLAY_NAME_BUFFER_SIZE   64
#define SERVER_RENDER_THREAD_NAME_BUFFER_SIZE   64

Functions

void * client_video_render_thread (void *arg)
 Interruptible sleep function with platform-specific optimizations.
void * client_audio_render_thread (void *arg)
 Main audio rendering thread function for individual clients.
int create_client_render_threads (server_context_t *server_ctx, client_info_t *client)
 Create and initialize per-client rendering threads.
asciichat_error_t stop_client_render_threads (client_info_t *client)
 Stop and cleanup per-client rendering threads.

Macro Definition Documentation

◆ SERVER_RENDER_THREAD_DISPLAY_NAME_BUFFER_SIZE

#define SERVER_RENDER_THREAD_DISPLAY_NAME_BUFFER_SIZE   64

Definition at line 3 of file src/server/render.c.

Referenced by client_audio_render_thread().

◆ SERVER_RENDER_THREAD_NAME_BUFFER_SIZE

#define SERVER_RENDER_THREAD_NAME_BUFFER_SIZE   64

Definition at line 4 of file src/server/render.c.

Referenced by create_client_render_threads().

Function Documentation

◆ client_audio_render_thread()

void * client_audio_render_thread ( void * arg)

Main audio rendering thread function for individual clients.

This is the audio processing thread that generates personalized audio mixes for a specific client at 100fps (10ms intervals, 480 samples @ 48kHz). Each client receives audio from all other clients while excluding their own audio to prevent echo and feedback.

THREAD EXECUTION FLOW:

  1. INITIALIZATION:
    • Validate client parameters and socket
    • Allocate local mixing buffer
    • Log thread startup for debugging
  2. MAIN PROCESSING LOOP:
    • Check thread running state (with mutex protection)
    • Take atomic snapshot of client state
    • Generate audio mix excluding this client's audio
    • Queue mixed audio for delivery to client
    • Sleep for precise timing (10ms intervals)
  3. CLEANUP AND EXIT:
    • Log thread termination
    • Return NULL to indicate clean exit

AUDIO MIXING STRATEGY:

ECHO PREVENTION:

  • Excludes client's own audio from their mix
  • Prevents audio feedback loops
  • Uses mixer_process_excluding_source() function
  • Maintains audio quality for other participants

PERFORMANCE CHARACTERISTICS:

TIMING PRECISION:

  • Target: 100fps (10ms intervals, 480 samples @ 48kHz)
  • Accumulates to OPUS_FRAME_SAMPLES samples (20ms) for Opus encoding
  • Fixed timing (no dynamic rate adjustment)
  • Low-latency audio delivery

BUFFER MANAGEMENT:

  • Uses fixed-size local buffer (AUDIO_FRAMES_PER_BUFFER)
  • No dynamic allocation in processing loop
  • Predictable memory usage
  • Cache-friendly processing pattern

THREAD SAFETY MECHANISMS:

STATE SYNCHRONIZATION:

  • Uses client->client_state_mutex for state access
  • Implements snapshot pattern for client ID and queues
  • Prevents race conditions during client disconnect
  • Safe concurrent access with other threads

MIXER COORDINATION:

  • Global audio mixer is internally thread-safe
  • Multiple audio threads can process concurrently
  • Lock-free audio buffer operations where possible

AUDIO PIPELINE INTEGRATION:

AUDIO MIXING:

  • Uses global g_audio_mixer for multi-client processing
  • Processes AUDIO_FRAMES_PER_BUFFER samples per iteration
  • Handles varying number of active audio sources

AUDIO DELIVERY:

  • Queues audio directly in client's audio packet queue
  • Higher priority than video packets in send thread
  • Real-time delivery requirements

ERROR HANDLING STRATEGY:

  • Invalid client parameters: Log error and exit immediately
  • Missing audio mixer: Continue with polling (mixer may initialize later)
  • Queue failures: Log debug info and continue (expected under load)
  • Client disconnect: Clean exit without error spam

PERFORMANCE OPTIMIZATIONS:

  • Local buffer allocation (no malloc in loop)
  • Minimal processing per iteration
  • Efficient mixer integration
  • Low-overhead packet queuing
Parameters
argPointer to client_info_t for the target client
Returns
NULL on thread completion (always clean exit)
Note
This function runs in its own dedicated thread
Audio processing has higher priority than video
Thread lifetime matches client connection lifetime
Warning
Invalid client parameter causes immediate thread termination
Thread must be joined properly to prevent resource leaks
See also
mixer_process_excluding_source() For Audio mixing implementation
AUDIO_FRAMES_PER_BUFFER For Owned Buffers size constant

Definition at line 561 of file src/server/render.c.

561 {
562 client_info_t *client = (client_info_t *)arg;
563
564 if (!client) {
565 log_error("Invalid client info in audio render thread");
566 return NULL;
567 }
568
569 // Take snapshot of client ID and display name at start to avoid race conditions
570 char thread_client_id[MAX_CLIENT_ID_LEN];
571 SAFE_STRNCPY(thread_client_id, client->client_id, sizeof(thread_client_id) - 1);
572 char thread_display_name[SERVER_RENDER_THREAD_DISPLAY_NAME_BUFFER_SIZE];
573 bool is_webrtc = acip_transport_get_type(client->transport) == ACIP_TRANSPORT_WEBRTC;
574 (void)is_webrtc; // May be unused in release builds
575
576 // LOCK OPTIMIZATION: Only need client_state_mutex, not global rwlock
577 // We already have a stable client pointer
578 mutex_lock(&client->client_state_mutex);
579 SAFE_STRNCPY(thread_display_name, client->display_name, sizeof(thread_display_name));
580 mutex_unlock(&client->client_state_mutex);
581
582#ifdef DEBUG_THREADS
583 log_debug("Audio render thread started for client %s (%s), webrtc=%d", thread_client_id, thread_display_name,
584 is_webrtc);
585#endif
586
587 // Mix buffer: up to OPUS_FRAME_SAMPLES samples for adaptive reading
588 // Normal: 480 samples = 10ms @ 48kHz
589 // Catchup: OPUS_FRAME_SAMPLES samples = 20ms when buffers are filling up
590 float mix_buffer[OPUS_FRAME_SAMPLES * AUDIO_CHANNELS];
591
592 // Opus frame accumulation buffer (OPUS_FRAME_SAMPLES samples = 20ms @ 48kHz)
593 // Opus requires minimum 480 samples, OPUS_FRAME_SAMPLES is optimal for 20ms frames
594 float opus_frame_buffer[(OPUS_FRAME_SAMPLES * AUDIO_CHANNELS)];
595 int opus_frame_accumulated = 0;
596
597 // Create an Opus encoder for the interleaved stereo stream.
598 opus_codec_t *opus_encoder =
600 if (!opus_encoder) {
601 log_error("Failed to create Opus encoder for audio render thread (client %s)", thread_client_id);
602 return NULL;
603 }
604
605 // FPS tracking for audio render thread
606 fps_t audio_fps_tracker = {0};
607 fps_init(&audio_fps_tracker, AUDIO_PACKET_FPS, "SERVER AUDIO");
608
609 // Per-thread counters (NOT static - each thread instance gets its own)
610 int server_audio_frame_count = 0;
611
612 bool should_continue = true;
613 uint64_t audio_deadline = time_get_ns();
614 while (should_continue && !atomic_load_bool(&g_should_exit) && !atomic_load_bool(&client->shutting_down)) {
615 ASCII_OTEL_SCOPE(SERVER_AUDIO);
616 log_debug_every(LOG_RATE_SLOW, "Audio render loop iteration for client %s", thread_client_id);
617
618 // Check for immediate shutdown
619 if (atomic_load_bool(&g_should_exit)) {
620 log_debug("Audio render thread stopping for client %s (g_should_exit)", thread_client_id);
621 break;
622 }
623
624 // Check thread state before acquiring any locks to prevent use-after-destroy.
625 // If we acquire locks after client is being destroyed, we'll crash with SIGSEGV
626 should_continue = (((int)atomic_load_bool(&client->audio_render_thread_running) != 0) &&
627 ((int)atomic_load_bool(&client->active) != 0) && !atomic_load_bool(&client->shutting_down));
628
629 if (!should_continue) {
630 log_debug("Audio render thread stopping for client %s (should_continue=false)", thread_client_id);
631 break;
632 }
633
634 if (!g_audio_mixer) {
635 log_dev_every(10 * NS_PER_MS_INT, "Audio render waiting for mixer (client %s)", thread_client_id);
636 // Check shutdown flag while waiting
637 if (atomic_load_bool(&g_should_exit))
638 break;
639 platform_sleep_ns(10 * NS_PER_MS_INT);
640 continue;
641 }
642
643 // There is no mixed audio to send when this is the only registered
644 // source. Avoid entering the mixer just to discover that exclusion; this
645 // is the common single-client server case.
647 audio_deadline += 20 * NS_PER_MS_INT;
648 uint64_t idle_now = time_get_ns();
649 if (audio_deadline > idle_now)
650 platform_sleep_ns(audio_deadline - idle_now);
651 else
652 audio_deadline = idle_now;
653 continue;
654 }
655
656 const char *client_id_snapshot = client->client_id;
657 if (!atomic_load_bool(&client->active))
658 break;
659
660 // Create mix excluding THIS client's audio using snapshot data
661
662 // Mix one complete Opus frame per iteration. This keeps the source lock at
663 // the packet cadence instead of reacquiring it twice for every 20 ms of
664 // audio, while preserving the exact frame boundary required by the codec.
665 int samples_to_read = OPUS_FRAME_SAMPLES * AUDIO_CHANNELS;
666
667 int samples_mixed = 0;
668 uint32_t client_id_hash = fnv1a_hash_string(client_id_snapshot);
669 uint64_t stats_mix_start = time_get_ns();
670 samples_mixed = mixer_process_excluding_source(g_audio_mixer, mix_buffer, samples_to_read, client_id_hash);
671 stats_duration_record(stats_runtime_scope(), STATS_DURATION_AUDIO_MIX, time_get_ns() - stats_mix_start);
672
673 log_debug_every(LOG_RATE_SLOW, "Audio render for client %s: samples_mixed=%d", client_id_snapshot, samples_mixed);
674
675 // Accumulate all samples (including 0 or partial) until we have a full Opus frame
676 // This maintains continuous stream without silence padding
677
678 int space_available = (OPUS_FRAME_SAMPLES * AUDIO_CHANNELS) - opus_frame_accumulated;
679 int samples_to_copy = (samples_mixed <= space_available) ? samples_mixed : space_available;
680
681 // Only copy if we have samples, otherwise just wait for next frame
682 if (samples_to_copy > 0) {
683 SAFE_MEMCPY(opus_frame_buffer + opus_frame_accumulated,
684 ((OPUS_FRAME_SAMPLES * AUDIO_CHANNELS) - opus_frame_accumulated) * sizeof(float), mix_buffer,
685 samples_to_copy * sizeof(float));
686 opus_frame_accumulated += samples_to_copy;
687 }
688
689 // Only encode and send when we have accumulated a full Opus frame
690 if (opus_frame_accumulated >= (OPUS_FRAME_SAMPLES * AUDIO_CHANNELS)) {
691 // Encode accumulated Opus frame (OPUS_FRAME_SAMPLES samples = 20ms @ 48kHz)
692 uint8_t opus_buffer[AUDIO_OPUS_SERVER_PACKET_CAPACITY]; // Max Opus frame size
693
694 int opus_size = opus_codec_encode(opus_encoder, opus_frame_buffer, OPUS_FRAME_SAMPLES * AUDIO_CHANNELS,
695 opus_buffer, sizeof(opus_buffer));
696
697 // DEBUG: Log mix buffer and encoding results to see audio levels being sent
698 {
699 float peak = 0.0f, rms = 0.0f;
700 for (int i = 0; i < (OPUS_FRAME_SAMPLES * AUDIO_CHANNELS); i++) {
701 float abs_val = fabsf(opus_frame_buffer[i]);
702 if (abs_val > peak)
703 peak = abs_val;
704 rms += opus_frame_buffer[i] * opus_frame_buffer[i];
705 }
706 rms = sqrtf(rms / (OPUS_FRAME_SAMPLES * AUDIO_CHANNELS));
707 // NOTE: server_audio_frame_count is now per-thread (not static), so each client thread has its own counter
708 server_audio_frame_count++;
709 if (server_audio_frame_count <= 5 || server_audio_frame_count % 20 == 0) {
710 // Log first 4 samples to verify they look like valid audio (not NaN/Inf/garbage)
712 "Server audio frame #%d for client %s: samples_mixed=%d, Peak=%.6f, RMS=%.6f, opus_size=%d, "
713 "first4=[%.4f,%.4f,%.4f,%.4f]",
714 server_audio_frame_count, client_id_snapshot, samples_mixed, peak, rms, opus_size,
715 opus_frame_buffer[0], opus_frame_buffer[1], opus_frame_buffer[2], opus_frame_buffer[3]);
716 }
717 }
718
719 // Always reset accumulation buffer after attempting to encode - we've consumed these samples
720 // If we don't reset, new audio samples would be dropped while stale data sits in the buffer
721 opus_frame_accumulated = 0;
722
723 if (opus_size <= 0) {
724 log_error_every(US_PER_SEC_INT, "Failed to encode audio to Opus for client %s: opus_size=%d",
725 client_id_snapshot, opus_size);
726 } else {
727 // Wrap Opus data in proper batch packet format for client parsing:
728 // Header: sample_rate (4B) + frame_duration (4B) + frame_count (4B)
729 // Frame sizes: frame_count * 2B (one uint16 per frame with Opus data size)
730 // Opus data: raw encoded audio
731
732 // Fixed values for Opus encoding (standard 48kHz, OPUS_FRAME_SAMPLES samples per frame = 20ms)
733 const uint32_t sample_rate = AUDIO_SAMPLE_RATE;
734 const uint32_t frame_duration =
735 CLIENT_AUDIO_PIPELINE_FRAME_MS; // milliseconds (OPUS_FRAME_SAMPLES samples / 48kHz = 20ms)
736 const uint32_t frame_count = 1; // one Opus frame per packet
737 const uint16_t frame_size = (uint16_t)opus_size;
738
739 // Calculate total packet size: 16-byte header + frame_sizes + opus data
740 const size_t header_size = 16; // 4 uint32s (sample_rate, frame_duration, frame_count, reserved)
741 const size_t frame_sizes_size = (size_t)frame_count * sizeof(uint16_t);
742 const size_t total_packet_size = header_size + frame_sizes_size + (size_t)opus_size;
743
744 // Allocate buffer for the complete packet
745 uint8_t *packet_buffer = SAFE_MALLOC(total_packet_size, uint8_t *);
746 if (!packet_buffer) {
747 log_error("Failed to allocate Opus batch packet buffer for client %s (size=%zu)", client_id_snapshot,
748 total_packet_size);
749 } else {
750 // Build header in network byte order
751 uint8_t *ptr = packet_buffer;
752
753 // Write sample_rate (4 bytes, network byte order)
754 uint32_t sr_net = htonl(sample_rate);
755 memcpy(ptr, &sr_net, 4);
756 ptr += 4;
757
758 // Write frame_duration (4 bytes, network byte order)
759 uint32_t fd_net = htonl(frame_duration);
760 memcpy(ptr, &fd_net, 4);
761 ptr += 4;
762
763 // Write frame_count (4 bytes, network byte order)
764 uint32_t fc_net = htonl(frame_count);
765 memcpy(ptr, &fc_net, 4);
766 ptr += 4;
767
768 // Write reserved field (4 bytes, all zeros)
769 uint32_t reserved = 0;
770 memcpy(ptr, &reserved, 4);
771 ptr += 4;
772
773 // Write frame sizes (2 bytes per frame, network byte order)
774 for (uint32_t i = 0; i < frame_count; i++) {
775 uint16_t fs_net = htons(frame_size);
776 memcpy(ptr, &fs_net, 2);
777 ptr += 2;
778 }
779
780 // Copy Opus data
781 memcpy(ptr, opus_buffer, (size_t)opus_size);
782
783 // Queue the properly formatted packet
784 asciichat_error_t result = server_send_audio(client, packet_buffer, total_packet_size);
785
786 if (result != ASCIICHAT_OK) {
787 log_debug_every(US_PER_SEC_INT, "Failed to queue Opus audio for client %s", client_id_snapshot);
788 } else {
789 // FPS tracking - audio packet successfully queued (handles lag detection and periodic reporting)
790 fps_frame_ns(&audio_fps_tracker, time_get_ns(), "audio packet queued");
791 }
792
793 // Free the allocated packet buffer after queuing (packet_queue_push_copy copies data)
794 SAFE_FREE(packet_buffer);
795 }
796 }
797 // NOTE: opus_frame_accumulated is already reset at line 928 after encode attempt
798 }
799
800 // Audio mixing rate limiting using adaptive sleep system.
801 // One complete Opus frame is mixed per 20 ms iteration.
802 // Use queue_depth=0 and target_depth=0 for constant-rate audio processing
803 audio_deadline += 20 * NS_PER_MS_INT;
804 uint64_t now = time_get_ns();
805 if (audio_deadline > now)
806 platform_sleep_ns(audio_deadline - now);
807 else if (now - audio_deadline > 100 * NS_PER_MS_INT)
808 audio_deadline = now;
809 }
810
811#ifdef DEBUG_THREADS
812 log_debug("Audio render thread stopped for client %s", thread_client_id);
813#endif
814
815 // Clean up Opus encoder
816 if (opus_encoder) {
817 opus_codec_destroy(opus_encoder);
818 }
819
820 // Clean up thread-local error context before exit
821
822 return NULL;
823}
#define OPUS_FRAME_SAMPLES
#define CLIENT_AUDIO_PIPELINE_FRAME_MS
#define AUDIO_SAMPLE_RATE
Definition audio/limits.h:4
#define AUDIO_CHANNELS
Definition audio/limits.h:5
#define AUDIO_OPUS_SERVER_PACKET_CAPACITY
atomic_t g_should_exit
Global application exit flag (shared across all modes).
Definition globals.c:34
int mixer_process_excluding_source(mixer_t *mixer, float *output, int num_samples, uint32_t exclude_client_id)
Process audio from all sources except one (for per-client output).
Definition mixer.c:689
void opus_codec_destroy(opus_codec_t *codec)
Destroy an Opus codec instance.
Definition opus.c:280
size_t opus_codec_encode(opus_codec_t *codec, const float *samples, int num_samples, uint8_t *out_data, size_t out_size)
Encode audio frame with Opus.
Definition opus.c:133
int mixer_source_count(const mixer_t *mixer)
Definition mixer.c:507
opus_codec_t * opus_codec_create_encoder_channels(opus_application_t application, int sample_rate, int bitrate, int channels)
Definition opus.c:39
@ OPUS_APPLICATION_AUDIO
General audio (optimized for music).
Definition opus.h:86
unsigned short uint16_t
Definition common.h:61
unsigned int uint32_t
Definition common.h:62
#define SAFE_STRNCPY(dst, src, size)
Definition common.h:423
#define SAFE_FREE(ptr)
Definition common.h:377
#define SAFE_MALLOC(size, cast)
Definition common.h:265
void fps_frame_ns(fps_t *tracker, uint64_t current_time_ns, const char *context)
Track a frame and detect lag conditions (nanosecond version - PRIMARY).
Definition fps.c:52
unsigned long long uint64_t
Definition common.h:63
unsigned char uint8_t
Definition common.h:60
#define SAFE_MEMCPY(dest, dest_size, src, count)
Definition common.h:483
void fps_init(fps_t *tracker, int expected_fps, const char *name)
Initialize FPS tracker.
Definition fps.c:32
asciichat_error_t
Error and exit codes - unified status values (0-255).
Definition error_codes.h:44
@ ASCIICHAT_OK
Definition error_codes.h:46
#define MAX_CLIENT_ID_LEN
Maximum client ID length - format: "noun.N (transport:port)" e.g., "mountain.0 (tcp:48036)".
#define LOG_RATE_SLOW
Log rate limit: 10 seconds (10,000,000 microseconds).
Definition log_rates.h:35
#define log_error(...)
Log an ERROR message.
Definition log/log.h:634
#define log_debug(...)
Log a DEBUG message.
Definition log/log.h:595
#define NS_PER_MS_INT
Definition time.h:168
#define US_PER_MS_INT
Definition time.h:172
#define US_PER_SEC_INT
Definition time.h:173
struct client_info client_info_t
@ ACIP_TRANSPORT_WEBRTC
WebRTC DataChannel (P2P).
void stats_duration_record(stats_scope_t *scope, stats_duration_id_t id, uint64_t elapsed_ns)
#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_dev_every(interval_us, fmt,...)
Rate-limited DEV logging.
Definition log/log.h:746
#define ASCII_OTEL_SCOPE(id)
Definition otel.h:72
stats_scope_t * stats_runtime_scope(void)
Definition runtime.c:80
mixer_t *volatile g_audio_mixer
Global audio mixer instance for multi-client audio processing.
int server_send_audio(const float *samples, int num_samples)
Send audio data to server.
#define SERVER_RENDER_THREAD_DISPLAY_NAME_BUFFER_SIZE
#define AUDIO_PACKET_FPS
char display_name[MAX_DISPLAY_NAME_LEN]
char client_id[MAX_CLIENT_ID_LEN]
Counters and timestamps used to calculate and report frame rate.
Definition fps.h:53
State for one Opus encoding or decoding direction.
Definition opus.h:105

References ACIP_TRANSPORT_WEBRTC, client_info::active, ASCII_OTEL_SCOPE, ASCIICHAT_OK, AUDIO_CHANNELS, AUDIO_OPUS_SERVER_PACKET_CAPACITY, AUDIO_PACKET_FPS, client_info::audio_render_thread_running, AUDIO_SAMPLE_RATE, CLIENT_AUDIO_PIPELINE_FRAME_MS, client_info::client_id, client_info::client_state_mutex, client_info::display_name, fps_frame_ns(), fps_init(), g_audio_mixer, g_should_exit, log_debug, log_debug_every, log_dev_every, log_error, log_error_every, LOG_RATE_SLOW, MAX_CLIENT_ID_LEN, mixer_process_excluding_source(), mixer_source_count(), NS_PER_MS_INT, OPUS_APPLICATION_AUDIO, opus_codec_create_encoder_channels(), opus_codec_destroy(), opus_codec_encode(), OPUS_FRAME_SAMPLES, SAFE_FREE, SAFE_MALLOC, SAFE_MEMCPY, SAFE_STRNCPY, SERVER_RENDER_THREAD_DISPLAY_NAME_BUFFER_SIZE, server_send_audio(), client_info::shutting_down, STATS_DURATION_AUDIO_MIX, stats_duration_record(), stats_runtime_scope(), client_info::transport, US_PER_MS_INT, and US_PER_SEC_INT.

Referenced by create_client_render_threads().

◆ client_video_render_thread()

void * client_video_render_thread ( void * arg)

Interruptible sleep function with platform-specific optimizations.

Provides a sleep mechanism that can be interrupted by the global shutdown signal, enabling responsive thread termination. The implementation varies by platform to optimize for responsiveness vs CPU usage.

PLATFORM-SPECIFIC BEHAVIOR:

WINDOWS IMPLEMENTATION:

  • Uses Sleep() API with millisecond precision
  • Simple approach due to Windows timer characteristics
  • Sleep(1) may sleep up to 15.6ms due to timer resolution
  • Checks shutdown flag before and after sleep

POSIX IMPLEMENTATION (Linux/macOS):

  • Uses condition variables for precise timing
  • Can be interrupted immediately by shutdown signal
  • Higher responsiveness for thread termination
  • Uses static mutex/condition variable for coordination

USAGE PATTERNS:

  • Rate limiting in render threads (maintain target FPS)
  • Backoff delays when queues are full
  • Polling intervals for state changes
  • Graceful busy-wait alternatives

PERFORMANCE CHARACTERISTICS:

  • Low CPU overhead (proper sleep, not busy wait)
  • Sub-second responsiveness to shutdown
  • Maintains timing precision for media processing
  • Scales well with many concurrent threads

ERROR HANDLING:

  • Early return if shutdown flag is set
  • Platform-specific error handling for sleep APIs
  • Logging for debugging (throttled to avoid spam)
Parameters
usecSleep duration in microseconds
Note
Function may return early if shutdown is requested
Minimum sleep time is platform-dependent (typically 1ms)
Static variables are used for call counting (debugging)
Warning
Not suitable for precise timing (use for rate limiting only)
See also
VIDEO_RENDER_FPS For Video to ASCII Conversion thread timing requirements

Main video rendering thread function for individual clients

This is the core video processing thread that generates personalized ASCII art frames for a specific client at 60fps. Each connected client gets their own dedicated video thread, providing linear performance scaling and personalized rendering based on terminal capabilities.

THREAD EXECUTION FLOW:

  1. INITIALIZATION:
    • Validate client parameters and socket
    • Initialize timing variables for rate limiting
    • Log thread startup for debugging
  2. MAIN PROCESSING LOOP:
    • Check thread running state (with mutex protection)
    • Rate limit to 60fps using high-resolution timing
    • Take atomic snapshot of client state
    • Generate personalized ASCII frame for this client
    • Queue frame for delivery to client
    • Update timing for next iteration
  3. CLEANUP AND EXIT:
    • Log thread termination
    • Return NULL to indicate clean exit

PERFORMANCE CHARACTERISTICS:

TIMING PRECISION:

  • Target: 60fps (16.67ms intervals)
  • Uses CLOCK_MONOTONIC for accurate timing
  • Only sleeps if ahead of schedule (no CPU waste)
  • Maintains consistent frame rate under varying load

CLIENT-SPECIFIC PROCESSING:

  • Generates frames customized for client's terminal
  • Respects color depth, palette, and size preferences
  • Adapts to client capability changes dynamically
  • Handles client disconnection gracefully

THREAD SAFETY MECHANISMS:

STATE SYNCHRONIZATION:

  • Uses client->client_state_mutex for all state access
  • Implements snapshot pattern (copy state, release lock)
  • Prevents race conditions with other threads
  • Safe concurrent access to client data

SHUTDOWN COORDINATION:

  • Monitors global g_should_exit flag
  • Checks per-client running flags with mutex protection
  • Responds to shutdown within one frame interval (16.67ms)

VIDEO PIPELINE INTEGRATION:

FRAME GENERATION:

FRAME DELIVERY:

  • Calls queue_ascii_frame_for_client() for packet queuing
  • Uses client's dedicated video packet queue
  • Send thread delivers frames asynchronously

ERROR HANDLING STRATEGY:

  • Invalid client parameters: Log error and exit immediately
  • Frame generation failures: Continue with debug logging
  • Queue overflow: Continue with debug logging (expected under load)
  • Client disconnect: Clean exit without error spam
  • Timing issues: Self-correcting (no accumulation)

LOGGING AND MONITORING:

  • Success logging: Throttled (every ~4 seconds)
  • Failure logging: Throttled (every ~10 seconds)
  • Performance metrics: Frame count and size tracking
  • Debug information: Timing and queue status
Parameters
argPointer to client_info_t for the target client
Returns
NULL on thread completion (always clean exit)
Note
This function runs in its own dedicated thread
Thread lifetime matches client connection lifetime
Memory allocated for frames is automatically freed
Warning
Invalid client parameter causes immediate thread termination
Thread must be joined properly to prevent resource leaks
See also
create_mixed_ascii_frame_for_client() For frame generation
queue_ascii_frame_for_client() For frame delivery
VIDEO_RENDER_FPS For timing constant definition

Definition at line 204 of file src/server/render.c.

204 {
205 acip_frame_workspace_t event_workspace = {0};
206 client_info_t *client = (client_info_t *)arg;
207 if (!client) {
208 log_error("NULL client pointer in video render thread");
209 return NULL;
210 }
211
212 // Take snapshot of client ID and socket at start to avoid race conditions
213 char thread_client_id[MAX_CLIENT_ID_LEN];
214 SAFE_STRNCPY(thread_client_id, client->client_id, sizeof(thread_client_id) - 1);
215 bool is_webrtc = acip_transport_get_type(client->transport) == ACIP_TRANSPORT_WEBRTC;
216 (void)is_webrtc; // May be unused in release builds
217
218 log_debug("Video render thread: client_id=%s, webrtc=%d", thread_client_id, is_webrtc);
219
220 log_debug("[VIDEO_RENDER_THREAD_START] ★★★ LOCK STATE at thread entry for client %s", thread_client_id);
221 // debug_sync_print_state(); // Disabled: causes AddressSanitizer stack-use-after-return crash
222
223 // Wait for client to send terminal capabilities before rendering
224 // Without capabilities, convert_composite_to_ascii() will fail
225 int timeout_ms = 5000;
226 int waited_ms = 0;
227 while (!client->has_terminal_caps && !atomic_load_bool(&g_should_exit) && !atomic_load_bool(&client->shutting_down)) {
228 if (waited_ms == 0) {
229 log_debug("Waiting for terminal capabilities from client %s...", thread_client_id);
230 }
231 APP_CALLBACK_VOID(platform_pump_events);
233 waited_ms += 10;
234 if (waited_ms >= timeout_ms) {
235 log_debug("Still waiting for terminal capabilities from client %s", thread_client_id);
236 waited_ms = 0;
237 }
238 }
239 if (client->has_terminal_caps) {
240 log_debug("Received terminal capabilities for client %s after %dms", thread_client_id, waited_ms);
241 }
242
243 // Get client's desired FPS from capabilities or use default
244 int client_fps = VIDEO_RENDER_FPS; // Default to 60 FPS
245 // Use snapshot pattern to avoid mutex in render thread
246 bool has_caps = client->has_terminal_caps;
247 int desired_fps = has_caps ? client->terminal_caps.desired_fps : 0;
248 if (has_caps && desired_fps > 0) {
249 client_fps = desired_fps;
250 log_debug("Client %s requested FPS: %d (has_caps=%d, desired_fps=%d)", thread_client_id, client_fps, has_caps,
251 desired_fps);
252 } else {
253 log_debug("Client %s using default FPS: %d (has_caps=%d, desired_fps=%d)", thread_client_id, client_fps, has_caps,
254 desired_fps);
255 }
256
257 int base_frame_interval_ms = 1000 / client_fps;
258 log_debug("Client %s render interval: %dms (%d FPS)", thread_client_id, base_frame_interval_ms, client_fps);
259
260 // FPS tracking for video render thread
261 fps_t video_fps_tracker = {0};
262 fps_init(&video_fps_tracker, client_fps, "SERVER VIDEO");
263
264 const uint64_t frame_interval_ns = NS_PER_SEC_INT / client_fps;
265 uint64_t next_frame_ns = time_get_ns() + frame_interval_ns;
266 bool last_has_sources = false;
267 uint32_t frame_gen_count = 0;
268 uint64_t frame_gen_start_time = 0;
269 uint64_t render_rate_window_start_ns = time_get_ns();
270 uint64_t rendered_frames_in_window = 0;
271 uint64_t missed_deadlines_in_window = 0;
272
273 log_debug("Video render loop STARTING for client %s", thread_client_id);
274
275 bool should_continue = true;
276 while (should_continue && !atomic_load_bool(&g_should_exit) && !atomic_load_bool(&client->shutting_down)) {
277 ASCII_OTEL_SCOPE(SERVER_VIDEO);
278 log_dev_every(10 * NS_PER_MS_INT, "Video render loop iteration for client %s", thread_client_id);
279
280 // Check for immediate shutdown
281 if (atomic_load_bool(&g_should_exit)) {
282 log_debug("Video render thread stopping for client %s (g_should_exit)", thread_client_id);
283 break;
284 }
285
286 bool video_running = atomic_load_bool(&client->video_render_thread_running);
287 bool active = atomic_load_bool(&client->active);
288 bool shutting_down = atomic_load_bool(&client->shutting_down);
289
290 should_continue = video_running && active && !shutting_down;
291
292 if (!should_continue) {
293 log_debug("Video render thread stopping for client %s (should_continue=false: video_running=%d, active=%d, "
294 "shutting_down=%d)",
295 thread_client_id, video_running, active, shutting_down);
296 break;
297 }
298
299 // Processing consumes part of the frame interval. Sleep only until the
300 // scheduled deadline, and skip missed deadlines without a catch-up burst.
301 uint64_t iter_start_ns = time_get_ns();
302 if (iter_start_ns < next_frame_ns) {
303 platform_sleep_ns(next_frame_ns - iter_start_ns);
304 }
305 uint64_t after_sleep_ns = time_get_ns();
306 uint64_t elapsed_intervals =
307 after_sleep_ns >= next_frame_ns ? (after_sleep_ns - next_frame_ns) / frame_interval_ns + 1 : 1;
308 if (elapsed_intervals > 1) {
309 missed_deadlines_in_window += elapsed_intervals - 1;
310 }
311 next_frame_ns += elapsed_intervals * frame_interval_ns;
312
313 // Capture timestamp for FPS tracking and frame timestamps
314 uint64_t current_time_ns = time_get_ns();
315
316 // Check thread state again before acquiring locks (client might have been destroyed during sleep).
317 should_continue = atomic_load_bool(&client->video_render_thread_running) && atomic_load_bool(&client->active) &&
318 !atomic_load_bool(&client->shutting_down);
319 if (!should_continue) {
320 break;
321 }
322
323 // Optimization: No mutex needed - all fields are atomic or stable.
324 // client_id: stable string pointer
325 // width/height: stable uint16_t values (set once, never modified)
326 // active: atomic_t - use atomic_load for thread safety
327 const char *client_id_snapshot = client->client_id; // Stable read
328 unsigned short width_snapshot = client->width; // Stable uint16_t
329 unsigned short height_snapshot = client->height; // Stable uint16_t
330 bool active_snapshot = atomic_load_bool(&client->active); // Atomic read
331
332 // Check if client is still active after getting snapshot
333 if (!active_snapshot) {
334 break;
335 }
336
337 // Phase 2 IMPLEMENTED: Generate frame specifically for THIS client using snapshot data
338 size_t frame_size = 0;
339
340 // Check if any clients are sending video
341 bool has_video_sources = any_clients_sending_video();
342
343 // DIAGNOSTIC: Track when video sources become available
344 if (has_video_sources != last_has_sources) {
345 log_debug("Client %s video sources: %s", thread_client_id, has_video_sources ? "AVAILABLE" : "UNAVAILABLE");
346 last_has_sources = has_video_sources;
347 }
348
350 "Video render iteration for client %s: has_video_sources=%d, width=%u, height=%u", thread_client_id,
351 has_video_sources, width_snapshot, height_snapshot);
352
353 // Use default dimensions if client dimensions not received
354 if (width_snapshot == 0 || height_snapshot == 0) {
355 log_dev_every(5 * NS_PER_MS_INT, "Using default dimensions for client %s (client reported: width=%u, height=%u)",
356 thread_client_id, width_snapshot, height_snapshot);
357 // Use 80x25 as standard terminal size fallback for snapshot/test clients
358 width_snapshot = 80;
359 height_snapshot = 25;
360 }
361
362 // Generate frames for all clients with valid dimensions, regardless of whether other clients are sending video.
363 // This ensures WebSocket snapshot clients (receive-only mode) receive frames even when no other clients are
364 // sending video.
365 int sources_count = 0; // Track number of video sources in this frame
366
367 // DIAGNOSTIC: Track every frame generation attempt
368 frame_gen_count++;
369 if (frame_gen_count == 1) {
370 frame_gen_start_time = current_time_ns;
371 }
372
373 // Log every 120 attempts (should be ~2 seconds at 60 Hz)
374 if (frame_gen_count % 120 == 0) {
375 uint64_t elapsed_ns = current_time_ns - frame_gen_start_time;
376 double gen_fps = (120.0 / (elapsed_ns / (double)NS_PER_SEC_INT));
377 log_dev("render loop: client=%s fps=%.1f", thread_client_id, gen_fps);
378 }
379
380 log_dev_every(5 * NS_PER_MS_INT, "About to call create_mixed_ascii_frame_for_client for client %s with dims %ux%u",
381 thread_client_id, width_snapshot, height_snapshot);
382
383 // debug_sync_print_state() is too expensive to call every frame (kills FPS)
384
385 uint64_t frame_create_start_ns = time_get_ns();
386 acip_render_settings_t frame_rendering = {0};
387 char *ascii_frame = create_mixed_ascii_frame_for_client(client_id_snapshot, width_snapshot, height_snapshot, false,
388 &frame_size, NULL, &sources_count, &frame_rendering);
389 uint64_t frame_create_end_ns = time_get_ns();
390 if (sources_count > 0) {
392 frame_create_end_ns - frame_create_start_ns);
395 }
396
397 if (frame_gen_count % 120 == 0) {
398 char sleep_str[TIME_DURATION_BUFFER_SIZE], create_str[TIME_DURATION_BUFFER_SIZE];
399 time_pretty((uint64_t)(after_sleep_ns - iter_start_ns), -1, sleep_str, sizeof(sleep_str));
400 time_pretty((uint64_t)(frame_create_end_ns - frame_create_start_ns), -1, create_str, sizeof(create_str));
401 log_dev("render timing: adaptive_sleep=%s frame_create=%s", sleep_str, create_str);
402 }
403
404 // Always send frames at configured FPS, even if no external video sources yet
405 // This ensures single-client mode (like testing) still gets continuous frames
406 // The frame data (animated background or test pattern) is still valid content
407
408 if (ascii_frame && frame_size > 0) {
409 server_send_video(client, &event_workspace, ascii_frame, frame_size, frame_rendering.width,
410 frame_rendering.height, sources_count, &frame_rendering);
411 fps_frame_ns(&video_fps_tracker, current_time_ns, "frame rendered");
412 rendered_frames_in_window++;
413 uint64_t render_rate_now_ns = time_get_ns();
414 uint64_t render_rate_elapsed_ns = render_rate_now_ns - render_rate_window_start_ns;
415 if (render_rate_elapsed_ns >= NS_PER_SEC_INT) {
416 double render_fps = (double)rendered_frames_in_window * NS_PER_SEC_INT / (double)render_rate_elapsed_ns;
417 if (render_fps < (double)client_fps * 0.9 || missed_deadlines_in_window > 0) {
418 log_warn("Server video render lagging client=%s fps=%.1f target=%d missed_deadlines=%llu", thread_client_id,
419 render_fps, client_fps, (unsigned long long)missed_deadlines_in_window);
420 } else {
421 log_debug("Server video render client=%s fps=%.1f target=%d missed_deadlines=0", thread_client_id, render_fps,
422 client_fps);
423 }
424 render_rate_window_start_ns = render_rate_now_ns;
425 rendered_frames_in_window = 0;
426 missed_deadlines_in_window = 0;
427 }
428 SAFE_FREE(ascii_frame);
429 }
430
431 uint64_t iter_end_ns = time_get_ns();
432 if (frame_gen_count % 120 == 0) {
433 char total_str[TIME_DURATION_BUFFER_SIZE];
434 time_pretty((uint64_t)(iter_end_ns - iter_start_ns), -1, total_str, sizeof(total_str));
435 log_warn_every(US_PER_SEC_INT, " TIMING: TOTAL_ITERATION=%s", total_str);
436 }
437 STOP_TIMER_AND_LOG(dev, 0, "render_iteration", "render_iteration completed (total)");
438 }
439
440#ifdef DEBUG_THREADS
441 log_debug("Video render thread stopped for client %s", thread_client_id);
442#endif
443 acip_frame_workspace_destroy(&event_workspace);
444
445 // Clean up thread-local error context before exit
446
447 return NULL;
448}
#define APP_CALLBACK_VOID(callback_name)
void server_send_video(client_info_t *client, acip_frame_workspace_t *workspace, const char *data, size_t length, uint32_t width, uint32_t height, int sources, const acip_render_settings_t *rendering)
#define log_warn(...)
Log a WARN message.
Definition log/log.h:621
#define log_dev(...)
Log a DEV message (most verbose, development only).
Definition log/log.h:581
#define STOP_TIMER_AND_LOG(log_level, threshold_ns, timer_name, msg_fmt,...)
Stop a timer and log the result with a custom message.
Definition time.h:626
#define NS_PER_SEC_INT
Definition time.h:169
int time_pretty(uint64_t nanoseconds, int decimals, char *buffer, size_t buffer_size)
Format nanoseconds as a compact duration with configurable sub-minute precision.
Definition time.c:374
#define TIME_DURATION_BUFFER_SIZE
Definition time.h:40
void platform_sleep_ms(unsigned int ms)
Sleep for a specified number of milliseconds.
void acip_frame_workspace_destroy(acip_frame_workspace_t *workspace)
void stats_counter_add(stats_scope_t *scope, stats_counter_id_t id, uint64_t amount)
#define log_warn_every(interval_us, fmt,...)
Rate-limited WARN logging.
Definition log/log.h:755
#define VIDEO_RENDER_FPS
bool any_clients_sending_video(void)
Queue ASCII frame for delivery to specific client.
char * create_mixed_ascii_frame_for_client(const char *target_client_id, unsigned short width, unsigned short height, bool wants_stretch, size_t *out_size, bool *out_grid_changed, int *out_sources_count, acip_render_settings_t *out_rendering)
Generate personalized ASCII frame for a specific client.
Send ASCII frame to client (server → client).
acip render settings keeps the values that move through the subsystem: caps, width,...
terminal_capabilities_t terminal_caps

References acip_frame_workspace_destroy(), ACIP_TRANSPORT_WEBRTC, client_info::active, any_clients_sending_video(), APP_CALLBACK_VOID, ASCII_OTEL_SCOPE, client_info::client_id, create_mixed_ascii_frame_for_client(), fps_frame_ns(), fps_init(), g_should_exit, client_info::has_terminal_caps, acip_render_settings_t::height, client_info::height, log_debug, log_debug_every, log_dev, log_dev_every, log_error, log_warn, log_warn_every, MAX_CLIENT_ID_LEN, NS_PER_MS_INT, NS_PER_SEC_INT, platform_sleep_ms(), SAFE_FREE, SAFE_STRNCPY, server_send_video(), client_info::shutting_down, stats_counter_add(), STATS_COUNTER_FRAMES_CONVERTED, STATS_COUNTER_FRAMES_DROPPED, STATS_DURATION_ASCII_CONVERT, stats_duration_record(), stats_runtime_scope(), STOP_TIMER_AND_LOG, client_info::terminal_caps, TIME_DURATION_BUFFER_SIZE, time_pretty(), client_info::transport, US_PER_SEC_INT, VIDEO_RENDER_FPS, client_info::video_render_thread_running, acip_render_settings_t::width, and client_info::width.

Referenced by create_client_render_threads().

◆ create_client_render_threads()

int create_client_render_threads ( server_context_t * server_ctx,
client_info_t * client )

Create and initialize per-client rendering threads.

Sets up the complete per-client threading infrastructure including both video and audio rendering threads plus all necessary synchronization primitives. This function is called once per client during connection establishment.

INITIALIZATION SEQUENCE:

  1. MUTEX INITIALIZATION:
    • client_state_mutex: Protects client state variables
  2. THREAD CREATION:
    • Create video rendering thread (60fps)
    • Create audio rendering thread (172fps)
    • Set running flags with proper synchronization
  3. ERROR HANDLING:
    • Complete cleanup if any step fails
    • Prevent partially initialized client state
    • Ensure no resource leaks on failure

THREAD SYNCHRONIZATION SETUP:

PER-CLIENT MUTEXES: Each client gets dedicated mutexes for fine-grained locking:

  • Prevents contention between clients
  • Allows concurrent processing of multiple clients
  • Enables lock-free operations within client context

THREAD RUNNING FLAGS:

  • Protected by client_state_mutex
  • Allow graceful thread termination
  • Checked frequently by render threads

ERROR RECOVERY:

PARTIAL FAILURE HANDLING: If thread creation fails partway through:

  • Stop and join any already-created threads
  • Destroy any initialized mutexes
  • Return error code to caller
  • Leave client in clean state for retry or cleanup

RESOURCE LEAK PREVENTION:

  • All allocations have corresponding cleanup
  • Thread handles are properly managed
  • Mutex destruction is deterministic

INTEGRATION POINTS:

Parameters
clientTarget client for thread creation
Returns
0 on success, -1 on failure
Note
Function performs complete initialization or complete cleanup
Mutexes must be destroyed by stop_client_render_threads()
Thread handles must be joined before client destruction
Warning
Partial initialization is not supported - complete success or failure
See also
stop_client_render_threads() For cleanup implementation

Definition at line 900 of file src/server/render.c.

900 {
901 log_debug("client_id=%s", client ? client->client_id : "NULL");
902
903 if (!server_ctx || !client) {
904 log_error("Cannot create render threads: NULL %s", !server_ctx ? "server_ctx" : "client");
905 return -1;
906 }
907
908#ifdef DEBUG_THREADS
909 log_debug("Creating render threads for client %s", client->client_id);
910#endif
911
912 // NOTE: Mutexes are already initialized in add_client() before any threads start
913 // This prevents race conditions where receive thread tries to use uninitialized mutexes
914
915 // Initialize render thread control flags
916 // IMPORTANT: Set to true BEFORE creating thread to avoid race condition
917 // where thread starts and immediately exits because flag is false
918 atomic_store_bool(&client->video_render_thread_running, true);
919 atomic_store_bool(&client->audio_render_thread_running, true);
920
921 // Create video rendering thread (stop_id=2, stop after receive thread)
923 safe_snprintf(thread_name, sizeof(thread_name), "video_render_%s", client->client_id);
924 asciichat_error_t video_result;
925
926 video_result = platform_thread_create(&client->video_render_thread, thread_name, client_video_render_thread, client);
927
928 if (video_result != ASCIICHAT_OK) {
929 // Reset flag since thread creation failed
930 atomic_store_bool(&client->video_render_thread_running, false);
931 atomic_store_bool(&client->audio_render_thread_running, false);
932 // Mutexes will be destroyed by remove_client() which called us
933 return -1;
934 }
935
936 // Create audio rendering thread (stop_id=2, same priority as video)
937 safe_snprintf(thread_name, sizeof(thread_name), "audio_render_%s", client->client_id);
938 asciichat_error_t audio_result;
939
940 audio_result = platform_thread_create(&client->audio_render_thread, thread_name, client_audio_render_thread, client);
941 if (audio_result != ASCIICHAT_OK) {
942 // Clean up video thread (atomic operation, no mutex needed)
943 atomic_store_bool(&client->video_render_thread_running, false);
944 // Reset audio flag since thread creation failed
945 atomic_store_bool(&client->audio_render_thread_running, false);
946 // stop_client_render_threads() will be called by remove_client()
947 // to clean up the video thread we just created
948 // Mutexes will be destroyed by remove_client() which called us
949 return -1;
950 }
951
952#ifdef DEBUG_THREADS
953 log_debug("Created render threads for client %s", client->client_id);
954#endif
955
956 return 0;
957}
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Platform-safe snprintf wrapper.
#define SERVER_RENDER_THREAD_NAME_BUFFER_SIZE
void * client_video_render_thread(void *arg)
Interruptible sleep function with platform-specific optimizations.
void * client_audio_render_thread(void *arg)
Main audio rendering thread function for individual clients.
platform_thread_t audio_render_thread
platform_thread_t video_render_thread

References ASCIICHAT_OK, client_info::audio_render_thread, client_info::audio_render_thread_running, client_audio_render_thread(), client_info::client_id, client_video_render_thread(), log_debug, log_error, safe_snprintf(), SERVER_RENDER_THREAD_NAME_BUFFER_SIZE, client_info::video_render_thread, and client_info::video_render_thread_running.

◆ stop_client_render_threads()

asciichat_error_t stop_client_render_threads ( client_info_t * client)

Stop and cleanup per-client rendering threads.

Performs graceful shutdown of both video and audio rendering threads for a specific client, including proper thread joining and resource cleanup. This function ensures deterministic cleanup without resource leaks.

SHUTDOWN SEQUENCE:

  1. SIGNAL SHUTDOWN:
    • Set thread running flags to false
    • Threads detect flag change and exit processing loops
    • Use mutex protection for atomic flag updates
  2. THREAD JOINING:
    • Wait for video render thread to complete
    • Wait for audio render thread to complete
    • Handle join failures appropriately
  3. RESOURCE CLEANUP:
    • Destroy per-client mutexes
    • Clear thread handles
    • Reset client thread state

GRACEFUL TERMINATION:

THREAD COORDINATION:

  • Threads monitor running flags in their main loops
  • Flags are checked frequently (every iteration)
  • Threads exit cleanly within one processing cycle
  • No forced thread termination (unsafe)

DETERMINISTIC CLEANUP:

  • thread_join() waits for complete thread exit
  • All thread resources are properly released
  • No zombie threads or resource leaks

ERROR HANDLING:

THREAD JOIN FAILURES:

  • Logged as errors but don't prevent cleanup
  • Continue with resource cleanup regardless
  • Platform-specific error reporting

NULL CLIENT HANDLING:

  • Safe to call with NULL client pointer
  • Error logged and function returns safely
  • No undefined behavior or crashes

RESOURCE MANAGEMENT:

MUTEX CLEANUP:

  • Destroys all per-client mutexes
  • Prevents resource leaks on client disconnect
  • Platform-independent destruction

THREAD HANDLE MANAGEMENT:

  • Clears thread handles after joining
  • Prevents accidental reuse of stale handles
  • Memory zeroing for safety

INTEGRATION REQUIREMENTS:

  • Called by remove_client() in client.c
  • Must be called before freeing client structure
  • Coordinates with other cleanup functions
Parameters
clientTarget client for thread cleanup
Note
Function is safe to call multiple times (idempotent)
All resources are cleaned up regardless of errors
Thread joining may take up to one processing cycle
Warning
Must be called before client structure deallocation
See also
create_client_render_threads() For thread creation
thread_join() For platform-specific joining

Definition at line 1040 of file src/server/render.c.

1040 {
1041 if (!client)
1042 return SET_ERRNO(ERROR_INVALID_PARAM, "Missing render client");
1043 atomic_store_bool(&client->video_render_thread_running, false);
1044 atomic_store_bool(&client->audio_render_thread_running, false);
1045 if (thread_is_initialized(&client->video_render_thread)) {
1046 asciichat_error_t result = thread_join(&client->video_render_thread, NULL);
1047 if (result != ASCIICHAT_OK)
1048 return result;
1049 thread_init(&client->video_render_thread);
1050 }
1051 if (thread_is_initialized(&client->audio_render_thread)) {
1052 asciichat_error_t result = thread_join(&client->audio_render_thread, NULL);
1053 if (result != ASCIICHAT_OK)
1054 return result;
1055 thread_init(&client->audio_render_thread);
1056 }
1057 return ASCIICHAT_OK;
1058}
#define SET_ERRNO(code,...)
@ ERROR_INVALID_PARAM

References ASCIICHAT_OK, client_info::audio_render_thread, client_info::audio_render_thread_running, ERROR_INVALID_PARAM, SET_ERRNO, client_info::video_render_thread, and client_info::video_render_thread_running.

Referenced by remove_client().