ascii-chat 0.11.33
Video chat in your terminal
Loading...
Searching...
No Matches
stream.h File Reference

Multi-client video mixing and ASCII frame generation. More...

Go to the source code of this file.

Functions

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)
 Generate personalized ASCII frame for a specific client.
 
bool any_clients_sending_video (void)
 Check if any connected clients are currently sending video.
 
int queue_audio_for_client (client_info_t *client, const void *audio_data, size_t data_size)
 Queue ASCII frame for delivery to specific client.
 

Variables

atomic_t g_should_exit
 Global application exit flag (shared across all modes)
 

Detailed Description

Multi-client video mixing and ASCII frame generation.

Definition in file stream.h.

Function Documentation

◆ any_clients_sending_video()

bool any_clients_sending_video ( void  )

Check if any connected clients are currently sending video.

This function scans all active clients to determine if at least one is sending video frames. Used by render threads to avoid generating frames when no video sources are available (e.g., during webcam warmup).

LOCK OPTIMIZATION: Uses atomic reads only, no rwlock acquisition This is safe because client_id, active, and is_sending_video are all atomics

Returns
true if at least one client has is_sending_video flag set, false otherwise

Definition at line 1335 of file stream.c.

1335 {
1336 // LOCK OPTIMIZATION: Don't acquire rwlock - all fields we access are atomic
1337 // client_id, active, is_sending_video are all atomic variables
1338
1339 // Iterate through all client slots
1340 for (int i = 0; i < MAX_CLIENTS; i++) {
1342
1343 // Skip uninitialized clients (atomic read)
1344 if (client->client_id[0] == '\0') {
1345 continue;
1346 }
1347
1348 // Check if client is active and sending video (both atomic reads)
1349 bool is_active = atomic_load_bool(&client->active);
1350 bool is_sending = atomic_load_bool(&client->is_sending_video);
1351
1352 if (is_active && is_sending) {
1353 return true;
1354 }
1355 }
1356
1357 return false;
1358}
bool atomic_load_bool(atomic_t *a)
Atomically load a boolean value.
Definition atomic.c:169
#define MAX_CLIENTS
Maximum possible clients (static array size) - actual runtime limit set by –max-clients (1-32)
Definition limits.h:26
client_manager_t g_client_manager
Global client manager singleton - central coordination point.
Per-client state structure for server-side client management.
char client_id[MAX_CLIENT_ID_LEN]
client_info_t clients[MAX_CLIENTS]
Array of client_info_t structures (backing storage)

References client_info::active, atomic_load_bool(), client_info::client_id, client_manager_t::clients, g_client_manager, client_info::is_sending_video, and MAX_CLIENTS.

Referenced by client_video_render_thread().

◆ create_mixed_ascii_frame_for_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 
)

Generate personalized ASCII frame for a specific client.

This is the core video mixing function that creates customized ASCII art frames for individual clients. It collects video from all active clients, creates an appropriate grid layout, and converts to ASCII using the target client's terminal capabilities.

ALGORITHM OVERVIEW:

  1. FRAME COLLECTION PHASE:
    • Scan all active clients for available video frames
    • Use double-buffer system to get latest frames
    • Implement aggressive frame dropping under load (maintains real-time)
    • Always use the most current frame for professional-grade quality
  2. LAYOUT CALCULATION PHASE:
    • Determine grid dimensions based on active client count
    • Calculate cell sizes with aspect ratio preservation
    • Handle special cases: single client, multiple clients
  3. COMPOSITE GENERATION PHASE:
    • Create composite image with appropriate dimensions
    • Place each client's video in grid cell with proper scaling
    • Support both normal and half-block rendering modes
  4. ASCII CONVERSION PHASE:
    • Convert composite to ASCII using client capabilities
    • Apply client-specific palette and color settings
    • Generate ANSI escape sequences if supported

PERFORMANCE OPTIMIZATIONS:

FRAME BUFFER MANAGEMENT:

  • Aggressive frame dropping: always read latest available
  • Double-buffer system: ensures smooth frame delivery
  • Buffer pool allocation: reduces malloc/free overhead
  • Zero-copy operations where possible

CONCURRENCY OPTIMIZATIONS:

  • Reader locks on client manager (allows parallel execution)
  • Double-buffer atomic operations (lock-free access)
  • Atomic snapshots of client state
  • Thread-safe buffer operations

CLIENT CAPABILITY INTEGRATION:

TERMINAL AWARENESS:

  • Respects client's color depth capabilities
  • Uses client's preferred ASCII palette
  • Handles UTF-8 vs ASCII-only terminals
  • Supports half-block mode for 2x vertical resolution

RENDERING MODES SUPPORTED:

  • RENDER_MODE_FOREGROUND: Traditional ASCII art
  • RENDER_MODE_BACKGROUND: Inverted colors for better contrast
  • RENDER_MODE_HALF_BLOCK: Unicode half-blocks for 2x resolution

ERROR HANDLING STRATEGY:

  • Invalid dimensions: Logged and frame generation aborted
  • Memory allocation failures: Graceful degradation, cleanup resources
  • Client disconnections: Skip client in current frame, continue
  • Buffer overruns: Frame dropping maintains real-time performance
  • Capability missing: Wait for capabilities before generating frames

MEMORY MANAGEMENT:

  • All image operations use buffer pool allocation
  • Frame data is properly freed after use
  • Cached frames use deep copies (prevent corruption)
  • Automatic cleanup on function exit
Parameters
target_client_idClient who will receive this customized frame
widthTerminal width in characters for this client
heightTerminal height in characters for this client
wants_stretchUnused parameter (aspect ratio always preserved)
out_sizePointer to store resulting ASCII frame size
Returns
Allocated ASCII frame string (caller must free), or NULL on error
Note
This function is called at 60fps per client by render threads
Generated frame is customized for target client's capabilities
Memory allocated from buffer pool - caller must free properly
Warning
Function performs extensive validation - invalid input returns NULL
Client capabilities must be received before frame generation
See also
ascii_convert_with_capabilities() For ASCII conversion implementation
framebuffer_read_multi_frame() For frame buffer operations
calculate_fit_dimensions_pixel() For aspect ratio calculations

Definition at line 973 of file stream.c.

975 {
976 (void)wants_stretch; // Unused - we always handle aspect ratio ourselves
977
978 // Register the stream counter once even when per-client render workers enter
979 // this function concurrently.
980 static _Atomic bool stream_atomics_registered = false;
981 if (!atomic_exchange_explicit(&stream_atomics_registered, true, memory_order_acq_rel)) {
982 NAMED_REGISTER_ATOMIC(&g_previous_active_video_count, "server_video_source_count_for_layout_detection", NULL);
983 }
984
985 uint64_t frame_gen_start_ns = time_get_ns();
986
987 // Initialize output parameters
988 if (out_grid_changed) {
989 *out_grid_changed = false;
990 }
991 if (out_sources_count) {
992 *out_sources_count = 0;
993 }
994
995 if (!out_size || width == 0 || height == 0) {
997 "Invalid parameters for create_mixed_ascii_frame_for_client: width=%u, height=%u, out_size=%p", width,
998 height, out_size);
999 return NULL;
1000 }
1001
1002 // Collect all active clients and their image sources
1003 image_source_t sources[MAX_CLIENTS];
1004 uint64_t collect_start_ns = time_get_ns();
1005 int source_count = collect_video_sources(sources, MAX_CLIENTS);
1006 uint64_t collect_end_ns = time_get_ns();
1007
1008 // Count sources that actually have video data
1009 int sources_with_video = 0;
1010 for (int i = 0; i < source_count; i++) {
1011 if (sources[i].has_video && sources[i].image) {
1012 sources_with_video++;
1013 }
1014 }
1015
1016 log_dev_every(333 * NS_PER_MS_INT, "FRAME_GEN_START: target_client=%s sources=%d collect=%.1fms", target_client_id,
1017 sources_with_video, (collect_end_ns - collect_start_ns) / NS_PER_MS);
1018
1019 // Return the source count for debugging/tracking
1020 if (out_sources_count) {
1021 *out_sources_count = sources_with_video;
1022 }
1023
1024 // GRID LAYOUT CHANGE DETECTION:
1025 // Check if the number of active video sources has changed
1026 // NOTE: We only UPDATE the count and SIGNAL the change via out parameter
1027 // Broadcasting CLEAR_CONSOLE must happen AFTER the new frames are written to buffers
1028 // to prevent race condition where CLEAR arrives before new frame is ready
1029 uint64_t previous_count = atomic_load_u64(&g_previous_active_video_count);
1030 if (sources_with_video != (int)previous_count) {
1031 // Use compare-and-swap to ensure only ONE thread detects the change
1032 if (atomic_cas_u64(&g_previous_active_video_count, &previous_count, (uint64_t)sources_with_video)) {
1035 "Grid layout changing: %d -> %d active video sources - caller will broadcast clear AFTER buffering frame",
1036 previous_count, sources_with_video);
1037 if (out_grid_changed) {
1038 *out_grid_changed = true; // Signal to caller
1039 }
1040 }
1041 }
1042
1043 // No active video sources - don't generate placeholder frames
1044 image_t *composite = NULL;
1045
1046 if (sources_with_video == 0) {
1047 *out_size = 0;
1048 // No active video sources for client - this isn't an error.
1049 // Return NULL to indicate no frame should be sent
1050 return NULL;
1051 }
1052
1053 START_TIMER("composite_create");
1054 if (sources_with_video == 1) {
1055 // Single source handling - create composite and convert to ASCII
1056 // Note: create_single_source_composite returns a reference to sources[i].image
1057 // which could be modified by other threads. Make a copy to prevent concurrent
1058 // modification during ascii_convert_with_capabilities.
1059 image_t *single_source = create_single_source_composite(sources, source_count, target_client_id, width, height);
1060 if (single_source) {
1061 composite = image_new_copy(single_source);
1062 if (!composite) {
1063 SET_ERRNO(ERROR_MEMORY, "Failed to copy single source composite");
1064 *out_size = 0;
1065 return NULL;
1066 }
1067 }
1068 } else {
1069 // Multiple sources - create grid layout
1070 composite =
1071 create_multi_source_composite(sources, source_count, sources_with_video, target_client_id, width, height);
1072 }
1073 STOP_TIMER_AND_LOG(dev, 500000, "composite_create", "Composite creation (single/multi)");
1074
1075 char *out = NULL;
1076
1077 if (!composite) {
1078 SET_ERRNO(ERROR_INVALID_STATE, "Per-client %s: Failed to create composite image", target_client_id);
1079 *out_size = 0;
1080 return NULL;
1081 }
1082
1083 // Convert composite to ASCII using client capabilities
1084 // Pass terminal dimensions so the frame can be padded to full width
1085 START_TIMER("ascii_convert");
1086 char *ascii_frame = convert_composite_to_ascii(composite, target_client_id, width, height);
1087 STOP_TIMER_AND_LOG(dev, 500000, "ascii_convert", "ASCII conversion (composite to ASCII)");
1088
1089 if (ascii_frame) {
1090 // The frame should have been null-terminated by the padding functions.
1091 // Use strlen() which is optimized and reliable
1092 size_t ascii_len = strlen(ascii_frame);
1093
1094 // Safety check: don't accept unreasonably large frames (10MB limit)
1095 if (ascii_len > 10 * 1024 * 1024) {
1096 log_error("Frame size exceeds 10MB safety limit (possible buffer overflow)");
1097 SET_ERRNO(ERROR_INVALID_PARAM, "Frame size exceeds 10MB");
1098 SAFE_FREE(ascii_frame);
1099 image_destroy(composite);
1100 *out_size = 0;
1101 return NULL;
1102 }
1103
1104 // Ensure frame ends with a reset sequence to avoid garbage at terminal
1105 // This prevents color codes from leaking into uninitialized memory
1106 const char reset_seq[] = "\033[0m";
1107 const size_t reset_len = 4;
1108
1109 if (ascii_len >= reset_len) {
1110 // Check if frame already ends with reset
1111 const char *frame_end = ascii_frame + ascii_len - reset_len;
1112 if (strncmp(frame_end, reset_seq, reset_len) == 0) {
1113 // Frame properly ends with reset, use full length
1114 *out_size = ascii_len;
1115 } else {
1116 // Frame doesn't end with reset - this is the REAL bug!
1117 // Truncate to the last occurrence of reset sequence
1118 const char *last_reset = NULL;
1119 for (const char *p = ascii_frame + ascii_len - reset_len; p >= ascii_frame; p--) {
1120 if (strncmp(p, reset_seq, reset_len) == 0) {
1121 last_reset = p;
1122 break;
1123 }
1124 }
1125
1126 if (last_reset) {
1127 // Include the reset sequence and truncate after it
1128 *out_size = (size_t)(last_reset - ascii_frame) + reset_len;
1129 ascii_frame[*out_size] = '\0'; // Ensure null termination
1130 log_warn("Frame was missing reset at end (had garbage), truncated from %zu to %zu bytes", ascii_len,
1131 *out_size);
1132 } else {
1133 // No reset found, use full length as fallback
1134 *out_size = ascii_len;
1135 log_dev_every(LOG_RATE_SLOW, "Frame has no reset sequences, sending full %zu bytes", ascii_len);
1136 }
1137 }
1138 } else {
1139 // Frame too short to have a reset, use as-is
1140 *out_size = ascii_len;
1141 }
1142
1143 log_dev_every(LOG_RATE_SLOW, "create_mixed_ascii_frame_for_client: Final frame size=%zu bytes for client %s",
1144 *out_size, target_client_id);
1145
1146 // Debug: Log the last 50 bytes of the frame to see what's really there
1147 if (*out_size >= 50) {
1148 char hex_buf[300] = {0};
1149 size_t hex_len = 0;
1150 const uint8_t *last_bytes = (const uint8_t *)ascii_frame + (*out_size - 50);
1151 for (int i = 0; i < 50 && hex_len < sizeof(hex_buf) - 5; i++) {
1152 hex_len += snprintf(hex_buf + hex_len, sizeof(hex_buf) - hex_len, "%02X ", last_bytes[i]);
1153 }
1154 log_dev_every(4500 * US_PER_MS_INT, "FRAME_LAST_50_BYTES (hex): %s", hex_buf);
1155
1156 // Also log as ASCII for readability
1157 char ascii_buf[100] = {0};
1158 for (int i = 0; i < 50 && i < (int)sizeof(ascii_buf) - 1; i++) {
1159 if (last_bytes[i] >= 32 && last_bytes[i] < 127) {
1160 ascii_buf[i] = (char)last_bytes[i];
1161 } else if (last_bytes[i] == '\n') {
1162 ascii_buf[i] = 'N';
1163 } else if (last_bytes[i] == '\0') {
1164 ascii_buf[i] = '0';
1165 } else {
1166 ascii_buf[i] = '.';
1167 }
1168 }
1169 log_dev_every(4500 * US_PER_MS_INT, "FRAME_LAST_50_ASCII: %s", ascii_buf);
1170 }
1171
1172 out = ascii_frame;
1173 } else {
1174 SET_ERRNO(ERROR_TERMINAL, "Per-client %s: Failed to convert image to ASCII", target_client_id);
1175 *out_size = 0;
1176 }
1177
1178 START_TIMER("image_cleanup");
1179 if (composite) {
1180 // For single source, composite is a malloc-allocated copy, not from pool
1181 // Check alloc method to determine correct destroy function
1182 if (composite->alloc_method == IMAGE_ALLOC_POOL) {
1183 image_destroy_to_pool(composite);
1184 } else {
1185 image_destroy(composite);
1186 }
1187 }
1188 for (int i = 0; i < source_count; i++) {
1189 if (sources[i].image) {
1190 image_destroy_to_pool(sources[i].image);
1191 }
1192 }
1193 STOP_TIMER_AND_LOG(dev, 500000, "image_cleanup", "Image cleanup (composite + sources)");
1194
1195 uint64_t frame_gen_end_ns = time_get_ns();
1196 uint64_t frame_gen_duration_ns = frame_gen_end_ns - frame_gen_start_ns;
1197 if (frame_gen_duration_ns > 10 * NS_PER_MS_INT) { // Log if > 10ms
1198 char duration_str[32];
1199 time_pretty((uint64_t)((double)frame_gen_duration_ns), -1, duration_str, sizeof(duration_str));
1200 log_warn_every(LOG_RATE_DEFAULT, "SLOW_FRAME_GENERATION: Client %s full frame gen took %s", target_client_id,
1201 duration_str);
1202 }
1203
1204 return out;
1205}
bool atomic_cas_u64(atomic_t *a, uint64_t *expected, uint64_t new_value)
Atomically compare-and-swap a uint64_t.
Definition atomic.c:264
uint64_t atomic_load_u64(atomic_t *a)
Atomically load a uint64_t value.
Definition atomic.c:233
#define SAFE_FREE(ptr)
Definition common.h:376
unsigned long long uint64_t
Definition common.h:59
unsigned char uint8_t
Definition common.h:56
#define NAMED_REGISTER_ATOMIC(a, name, parent_ptr)
Register an atomic_t with automatic format specifier.
#define SET_ERRNO(code, context_msg,...)
Set error code with custom context message and log it, returning the error code.
@ ERROR_INVALID_STATE
@ ERROR_MEMORY
Definition error_codes.h:56
@ ERROR_INVALID_PARAM
@ ERROR_TERMINAL
Definition error_codes.h:69
#define LOG_RATE_SLOW
Log rate limit: 10 seconds (10,000,000 microseconds)
Definition log_rates.h:35
#define LOG_RATE_DEFAULT
Log rate limit: 5 seconds (5,000,000 microseconds) - default for audio/video packets.
Definition log_rates.h:32
#define log_warn(...)
Log a WARN message.
Definition log/log.h:574
#define log_error(...)
Log an ERROR message.
Definition log/log.h:587
#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:612
uint64_t time_get_ns(void)
Get current monotonic time in nanoseconds.
Definition util/time.c:108
int time_pretty(uint64_t nanoseconds, int decimals, char *buffer, size_t buffer_size)
Format nanoseconds as pretty duration with spaces and configurable precision.
Definition util/time.c:424
#define START_TIMER(name_fmt,...)
Start a timer with formatted name.
Definition time.h:333
#define NS_PER_MS_INT
Definition time.h:156
#define NS_PER_MS
Definition time.h:147
#define US_PER_MS_INT
Definition time.h:160
image_t * image_new_copy(const image_t *source)
Create a copy of an existing image.
void image_destroy_to_pool(image_t *image)
Destroy an image allocated from buffer pool.
void image_destroy(image_t *p)
Destroy an image allocated with image_new()
@ IMAGE_ALLOC_POOL
Pixels allocated with buffer_pool_alloc()
#define log_warn_every(interval_us, fmt,...)
Rate-limited WARN logging.
Definition log/log.h:708
#define log_dev_every(interval_us, fmt,...)
Rate-limited DEV logging.
Definition log/log.h:699
Image source structure for multi-client video mixing.
Definition stream.c:206
Image structure.
uint8_t alloc_method
Allocation method (image_alloc_method_t) for correct deallocation.

References image_t::alloc_method, atomic_cas_u64(), atomic_load_u64(), ERROR_INVALID_PARAM, ERROR_INVALID_STATE, ERROR_MEMORY, ERROR_TERMINAL, IMAGE_ALLOC_POOL, image_destroy(), image_destroy_to_pool(), image_new_copy(), log_dev_every, log_error, LOG_RATE_DEFAULT, LOG_RATE_SLOW, log_warn, log_warn_every, MAX_CLIENTS, NAMED_REGISTER_ATOMIC, NS_PER_MS, NS_PER_MS_INT, SAFE_FREE, SET_ERRNO, START_TIMER, STOP_TIMER_AND_LOG, time_get_ns(), time_pretty(), and US_PER_MS_INT.

Referenced by client_video_render_thread().

◆ queue_audio_for_client()

int queue_audio_for_client ( client_info_t *  client,
const void *  audio_data,
size_t  data_size 
)

Queue ASCII frame for delivery to specific client.

Packages a generated ASCII frame into a protocol packet and queues it for asynchronous delivery to the target client. This function handles all protocol details including headers, checksums, and metadata.

PACKET STRUCTURE CREATED:

  • ascii_frame_packet_t header containing:
    • width, height: Client terminal dimensions
    • original_size: Uncompressed frame size
    • compressed_size: Compressed size (currently 0)
    • checksum: CRC32 of frame data for integrity
    • flags: Capability flags (color support, etc.)
  • Frame data: Complete ASCII art string with ANSI codes

PROTOCOL INTEGRATION:

  • Converts dimensions to network byte order
  • Generates CRC32 checksum for error detection
  • Sets appropriate capability flags based on client
  • Uses PACKET_TYPE_ASCII_FRAME packet type

DELIVERY MECHANISM:

  • Queues packet in client's video packet queue
  • Send thread delivers packet asynchronously
  • Queue overflow handling prevents memory exhaustion
  • Error conditions are logged but don't affect other clients

MEMORY MANAGEMENT:

  • Creates temporary packet buffer for header+data combination
  • packet_queue_enqueue() copies data (safe to free temp buffer)
  • No memory leaks on success or failure paths
  • Buffer pool allocation not used here (temporary buffer)

ERROR HANDLING:

  • Invalid parameters: Return -1, no side effects
  • Memory allocation failure: Logged, return -1
  • Queue overflow: Logged as debug (expected under load)
  • Client shutdown: Graceful failure without error spam
Parameters
clientTarget client for frame delivery
ascii_frameGenerated ASCII art string (null-terminated)
frame_sizeSize of ASCII frame in bytes
Returns
0 on successful queuing, -1 on error
Note
Frame delivery happens asynchronously via send thread
Queue overflow drops frames to maintain real-time performance
Checksums enable client-side integrity verification
See also
packet_queue_enqueue() For underlying queue implementation
asciichat_crc32() For checksum generation

Queue audio data for delivery to specific client

Queues mixed audio data for delivery to a specific client. This is a simple wrapper around the packet queue system for audio delivery.

AUDIO PIPELINE INTEGRATION:

  • Called by audio mixing threads after combining multiple client streams
  • Audio data is already in final format (float samples, mixed)
  • No additional processing required at this stage

PACKET DETAILS:

  • Uses PACKET_TYPE_AUDIO_BATCH packet type
  • Audio data is raw float samples bundled together
  • Batch format reduces packet overhead ~32x
  • Sample rate and format determined by audio system

DELIVERY CHARACTERISTICS:

  • Higher priority than video packets (lower latency)
  • Uses client's audio packet queue
  • Send thread prioritizes audio over video
  • Queue overflow drops oldest audio to maintain real-time

ERROR HANDLING:

  • Invalid parameters: Return -1 immediately
  • Queue overflow: Handled by packet queue (drops old data)
  • Client shutdown: Graceful failure
Parameters
clientTarget client for audio delivery
audio_dataMixed audio samples (float format)
data_sizeSize of audio data in bytes
Returns
0 on successful queuing, -1 on error
Note
Audio has priority over video in send thread
Real-time audio requires queue overflow handling
See also
packet_queue_enqueue() For underlying queue implementation
mixer.c For Audio mixing implementation

Definition at line 1315 of file stream.c.

1315 {
1316 if (!client || !client->audio_queue || !audio_data || data_size == 0) {
1317 return -1;
1318 }
1319
1320 return packet_queue_enqueue(client->audio_queue, PACKET_TYPE_AUDIO_BATCH, audio_data, data_size, 0, true);
1321}
int packet_queue_enqueue(packet_queue_t *queue, packet_type_t type, const void *data, size_t data_len, uint32_t client_id, bool copy_data)
Enqueue a packet into the queue.
Definition queue.c:209
@ PACKET_TYPE_AUDIO_BATCH
Batched audio packets for efficiency.
Definition packet.h:372

References client_info::audio_queue, packet_queue_enqueue(), and PACKET_TYPE_AUDIO_BATCH.

Variable Documentation

◆ g_should_exit

atomic_t g_should_exit
extern

Global application exit flag (shared across all modes)

Global shutdown flag referenced by lib/logging.c and lib/debug/lock.c In production builds, this is defined in main.c and set during shutdown. For tests, we stub it out as false (tests don't perform shutdown).

Definition at line 34 of file globals.c.

34{0};

Referenced by client_audio_render_thread(), client_dispatch_thread(), client_receive_thread(), client_send_thread_func(), client_video_render_thread(), handle_image_frame_h265_packet(), handle_image_frame_packet(), server_main(), should_exit(), signal_exit(), stats_logger_thread(), and stop_client_render_threads().