ascii-chat 0.11.33
Video chat in your terminal
Loading...
Searching...
No Matches
stream.c
Go to the documentation of this file.
1
131#include <ascii-chat/atomic.h>
133#include <stdio.h>
134#include <string.h>
135#include <time.h>
136#include <math.h>
137#include <float.h>
138
139#include "main.h"
140#include "stream.h"
141#include "client.h"
142#include <ascii-chat/common.h>
154#include <ascii-chat/util/time.h>
156
166static atomic_t g_previous_active_video_count = {0};
167
206typedef struct {
210 char client_id[MAX_CLIENT_ID_LEN];
214
222static int collect_video_sources(image_source_t *sources, int max_sources) {
223 int source_count = 0;
224
225 // Check for shutdown before acquiring locks to prevent lock corruption
227 return 0;
228 }
229
230 // Keep client slots and their incoming frame buffers alive until every
231 // active source has been copied out of its double buffer. remove_client()
232 // frees those buffers under the manager write lock after stopping only that
233 // client's own workers; the other clients' render workers also read them.
234 bool manager_lock_held = g_client_manager_rwlock_initialized;
235 if (manager_lock_held) {
237 }
238
239 // Snapshot fields while holding the manager read lock so removal cannot
240 // invalidate a frame-buffer pointer between this scan and its copy.
241 typedef struct {
242 char client_id[MAX_CLIENT_ID_LEN];
243 bool is_active;
244 bool is_sending_video;
245 video_frame_buffer_t *video_buffer;
246 } client_snapshot_t;
247
248 client_snapshot_t client_snapshots[MAX_CLIENTS];
249 int snapshot_count = 0;
250
251 // The manager read lock protects slot identity and buffer lifetime; stream
252 // flags are atomic because receive callbacks update them independently.
253 for (int i = 0; i < MAX_CLIENTS; i++) {
255
256 if (client->client_id[0] == '\0') {
257 continue; // Skip uninitialized clients
258 }
259
260 // Snapshot all needed client state (all atomic reads or stable pointers)
261 SAFE_STRNCPY(client_snapshots[snapshot_count].client_id, client->client_id,
262 sizeof(client_snapshots[snapshot_count].client_id) - 1);
263 client_snapshots[snapshot_count].is_active = atomic_load_bool(&client->active);
264 client_snapshots[snapshot_count].is_sending_video = atomic_load_bool(&client->is_sending_video);
265 client_snapshots[snapshot_count].video_buffer = client->incoming_video_buffer; // Stable pointer
266 snapshot_count++;
267 }
268
269 // Process frames (expensive operations)
270 log_dev_every(5 * NS_PER_MS_INT, "collect_video_sources: Processing %d snapshots", snapshot_count);
271 for (int i = 0; i < snapshot_count && source_count < max_sources; i++) {
272 client_snapshot_t *snap = &client_snapshots[i];
273
274 if (!snap->is_active) {
275 log_dev_every(5 * NS_PER_MS_INT, "collect_video_sources: Skipping inactive client %s", snap->client_id);
276 continue;
277 }
278
279 log_dev_every(5 * NS_PER_MS_INT, "collect_video_sources: Client %s: is_sending_video=%d", snap->client_id,
280 snap->is_sending_video);
281
282 SAFE_STRNCPY(sources[source_count].client_id, snap->client_id, sizeof(sources[source_count].client_id) - 1);
283 sources[source_count].image = NULL; // Will be set if video is available
284 sources[source_count].has_video = false;
285
286 // Declare these outside the if block so they're accessible later
287 multi_source_frame_t current_frame = {0};
288 bool got_new_frame = false;
289
290 // Always try to get the last available video frame for consistent ASCII generation.
291 // Keep the swap mutex held until the front-buffer bytes have been copied:
292 // after the next commit, this frame becomes the writer's back buffer and
293 // can be overwritten while the compositor is still reading it.
294 if (snap->is_sending_video && snap->video_buffer) {
295 video_frame_buffer_t *video_buffer = snap->video_buffer;
296 mutex_lock(&video_buffer->swap_mutex);
297 const video_frame_t *frame = video_buffer->front_buffer;
298 if (!frame || !frame->data || frame->size < sizeof(uint32_t) * 2 ||
299 frame->size > video_buffer->allocated_buffer_size) {
300 mutex_unlock(&video_buffer->swap_mutex);
301 continue;
302 }
303
304 size_t frame_size_val = frame->size;
305
306 if (frame_size_val >= (sizeof(uint32_t) * 2 + 3)) {
307 // PARSE AND VALIDATE DIMENSIONS BEFORE COPYING
308 // Don't trust frame->size - calculate correct size from dimensions
310 uint32_t peek_height = NET_TO_HOST_U32(read_u32_unaligned(frame->data + sizeof(uint32_t)));
311
312 // Reject obviously corrupted dimensions
313 if (peek_width == 0 || peek_height == 0 || peek_width > 4096 || peek_height > 2160) {
314 log_debug("Per-client %s: rejected dimensions %ux%u as corrupted", snap->client_id, peek_width, peek_height);
315 mutex_unlock(&video_buffer->swap_mutex);
316 continue;
317 }
318
319 // Validate dimensions
320 if (image_validate_dimensions((size_t)peek_width, (size_t)peek_height) != ASCIICHAT_OK) {
321 mutex_unlock(&video_buffer->swap_mutex);
322 continue;
323 }
324
325 // Calculate CORRECT frame size based on dimensions (don't trust frame->size)
326 size_t correct_frame_size = sizeof(uint32_t) * 2;
327 {
328 size_t rgb_size = 0;
329 if (image_calc_rgb_size((size_t)peek_width, (size_t)peek_height, &rgb_size) != ASCIICHAT_OK) {
330 log_debug("Per-client: rgb_size calc failed for %ux%u", peek_width, peek_height);
331 mutex_unlock(&video_buffer->swap_mutex);
332 continue;
333 }
334 correct_frame_size += rgb_size;
335 }
336
337 log_debug_every(NS_PER_MS_INT, "Per-client: frame dimensions=%ux%u, frame_size=%zu, correct_size=%zu",
338 peek_width, peek_height, frame_size_val, correct_frame_size);
339
340 // Verify frame is at least large enough for the correct size
341 if (frame_size_val < correct_frame_size) {
342 log_debug("Per-client: frame too small: got %zu, need %zu", frame_size_val, correct_frame_size);
343 mutex_unlock(&video_buffer->swap_mutex);
344 continue;
345 }
346
347 // We have frame data - copy ONLY the correct amount based on dimensions
348 // Use SAFE_MALLOC (not buffer pool - image_new_from_pool uses pool and causes overlap)
349 current_frame.data = SAFE_MALLOC(correct_frame_size, void *);
350
351 if (current_frame.data) {
352 memcpy(current_frame.data, frame->data, correct_frame_size);
353 current_frame.size = correct_frame_size;
354 SAFE_STRNCPY(current_frame.source_client_id, snap->client_id, sizeof(current_frame.source_client_id) - 1);
355 current_frame.timestamp = (uint32_t)(frame->capture_timestamp_ns / NS_PER_SEC_INT);
356 got_new_frame = true;
357 }
358 mutex_unlock(&video_buffer->swap_mutex);
359 } else {
360 mutex_unlock(&video_buffer->swap_mutex);
361 }
362 }
363
364 multi_source_frame_t *frame_to_use = got_new_frame ? &current_frame : NULL;
365
366 if (frame_to_use && frame_to_use->data && frame_to_use->size > sizeof(uint32_t) * 2) {
367 // Parse the image data
368 // Format: [width:4][height:4][rgb_data:w*h*3]
369 // Use unaligned read helpers - frame data may not be aligned
370 uint32_t img_width = NET_TO_HOST_U32(read_u32_unaligned(frame_to_use->data));
371 uint32_t img_height = NET_TO_HOST_U32(read_u32_unaligned(frame_to_use->data + sizeof(uint32_t)));
372
373 // Debug logging to understand the data
374 if (img_width == 0xBEBEBEBE || img_height == 0xBEBEBEBE) {
375 SET_ERRNO(ERROR_INVALID_STATE, "Uninitialized frame dimensions from client %s: 0x%08x x 0x%08x",
376 snap->client_id, img_width, img_height);
377 }
378
379 // Validate dimensions using image utility function
380 if (image_validate_dimensions((size_t)img_width, (size_t)img_height) != ASCIICHAT_OK) {
382 "Per-client: Invalid image dimensions from client %s: %ux%u (data may be corrupted)", snap->client_id,
383 img_width, img_height);
384 if (got_new_frame) {
385 SAFE_FREE(current_frame.data);
386 }
387 source_count++;
388 continue;
389 }
390
391 // Calculate expected frame size with overflow checking
392 size_t expected_size = sizeof(uint32_t) * 2;
393 {
394 size_t rgb_size = 0;
395 if (image_calc_rgb_size((size_t)img_width, (size_t)img_height, &rgb_size) != ASCIICHAT_OK) {
396 SET_ERRNO(ERROR_INVALID_STATE, "Per-client: RGB size calculation failed for client %s: %ux%u",
397 snap->client_id, img_width, img_height);
398 if (got_new_frame) {
399 SAFE_FREE(current_frame.data);
400 }
401 source_count++;
402 continue;
403 }
404 expected_size += rgb_size;
405 }
406 if (frame_to_use->size != expected_size) {
408 "Per-client: Frame size mismatch from client %s: got %zu, expected %zu for %ux%u image",
409 snap->client_id, frame_to_use->size, expected_size, img_width, img_height);
410 if (got_new_frame) {
411 SAFE_FREE(current_frame.data);
412 }
413 source_count++;
414 continue;
415 }
416
417 // Extract pixel data pointer
418 rgb_pixel_t *pixels = (rgb_pixel_t *)(frame_to_use->data + (sizeof(uint32_t) * 2));
419
420 // Create image from buffer pool for consistent video pipeline management
421 image_t *img = image_new_from_pool(img_width, img_height);
422 if (!img) {
423 log_error("Per-client: image_new_from_pool failed for %ux%u", img_width, img_height);
424 if (got_new_frame) {
425 SAFE_FREE(current_frame.data);
426 }
427 continue;
428 }
429 memcpy(img->pixels, pixels, (size_t)img_width * (size_t)img_height * sizeof(rgb_pixel_t));
430 sources[source_count].image = img;
431 sources[source_count].has_video = true;
432
433 // Free temporary frame buffer - image has its own pixel data now
434 if (got_new_frame) {
435 SAFE_FREE(current_frame.data);
436 }
437 } else {
438 // frame_to_use check failed - clean up allocated frame data
439 if (got_new_frame && current_frame.data) {
440 SAFE_FREE(current_frame.data);
441 }
442 }
443
444 // Increment source count for this active client (with or without video)
445 source_count++;
446 }
447
448 if (manager_lock_held) {
450 }
451 return source_count;
452}
453
464static image_t *create_single_source_composite(image_source_t *sources, int source_count,
465 const char *target_client_id __attribute__((unused)),
466 unsigned short width __attribute__((unused)),
467 unsigned short height __attribute__((unused))) {
468 // Find the single source with video
469 image_t *single_source = NULL;
470 for (int i = 0; i < source_count; i++) {
471 if (sources[i].has_video && sources[i].image) {
472 single_source = sources[i].image;
473 break;
474 }
475 }
476
477 if (!single_source) {
478 SET_ERRNO(ERROR_INVALID_STATE, "Logic error: sources_with_video=1 but no source found");
479 return NULL;
480 }
481
482 // For single source, don't pre-fit the image. Let ascii_convert_with_capabilities handle
483 // all aspect ratio fitting with proper CHAR_ASPECT correction. This avoids double-correction
484 // that was happening when the image was pre-fitted in pixel space and then aspect_ratio()
485 // was called again with CHAR_ASPECT=2.0.
486 // Just return the source image as-is; ascii_convert_with_capabilities will fit it properly.
487 return single_source;
488}
489
511static void calculate_optimal_grid_layout(image_source_t *sources, int source_count, int sources_with_video,
512 int terminal_width, int terminal_height, int *out_cols, int *out_rows) {
513 // Special cases
514 if (sources_with_video == 0) {
515 *out_cols = 0;
516 *out_rows = 0;
517 return;
518 }
519
520 if (sources_with_video == 1) {
521 *out_cols = 1;
522 *out_rows = 1;
523 return;
524 }
525
526 // ASCII character aspect ratio: characters are ~2x taller than wide
527 // So we need to adjust terminal dimensions to visual space
528 const float CHAR_ASPECT = 2.0f; // Character height / width ratio
529
530 // Calculate average aspect ratio of all video sources
531 float avg_aspect = 0.0f;
532 int aspect_count = 0;
533 for (int i = 0; i < source_count; i++) {
534 if (sources[i].has_video && sources[i].image) {
535 float aspect = (float)sources[i].image->w / (float)sources[i].image->h;
536 avg_aspect += aspect;
537 aspect_count++;
538 }
539 }
540 if (aspect_count > 0) {
541 avg_aspect /= aspect_count;
542 } else {
543 avg_aspect = 1.6f; // Default aspect ratio
544 }
545
546 // Try all reasonable grid configurations
547 int best_cols = 1;
548 int best_rows = sources_with_video;
549 float best_utilization = 0.0f;
550
551 // Try grid configurations from 1x1 up to reasonable limits
552 for (int cols = 1; cols <= sources_with_video; cols++) {
553 int rows = (sources_with_video + cols - 1) / cols; // Ceiling division
554
555 // Skip configurations with too many empty cells (more than one row worth)
556 int total_cells = cols * rows;
557 int empty_cells = total_cells - sources_with_video;
558 if (empty_cells > cols) {
559 continue;
560 }
561
562 // Calculate cell dimensions for this configuration
563 int cell_width = terminal_width / cols;
564 int cell_height = terminal_height / rows;
565
566 // Skip if cells would be too small
567 if (cell_width < 20 || cell_height < 10) {
568 continue;
569 }
570
571 // Calculate total area utilized by all videos in this configuration
572 // For each video, calculate how much space it would use in a cell
573 // IMPORTANT: Account for character aspect ratio (chars are 2x taller than wide)
574 float total_area_used = 0.0f;
575 int cell_area = cell_width * cell_height;
576
577 for (int i = 0; i < sources_with_video; i++) {
578 // Use average aspect ratio for calculation
579 float video_aspect = avg_aspect;
580
581 // Calculate VISUAL cell aspect ratio (accounting for character shape)
582 // A cell that is cell_width chars wide and cell_height chars tall
583 // has visual aspect = cell_width / (cell_height * CHAR_ASPECT)
584 float cell_visual_aspect = (float)cell_width / ((float)cell_height * CHAR_ASPECT);
585
586 // Calculate fitted dimensions while preserving aspect ratio
587 int fitted_width, fitted_height;
588
589 if (video_aspect > cell_visual_aspect) {
590 // Video is wider than cell - fit to width
591 fitted_width = cell_width;
592 // Visual height needed: cell_width / video_aspect
593 // Character height: visual_height / CHAR_ASPECT
594 fitted_height = (int)((cell_width / video_aspect) / CHAR_ASPECT);
595 } else {
596 // Video is taller than cell - fit to height
597 fitted_height = cell_height;
598 // Visual width needed: cell_height * CHAR_ASPECT * video_aspect
599 fitted_width = (int)(cell_height * CHAR_ASPECT * video_aspect);
600 }
601
602 // Clamp to cell bounds
603 if (fitted_width > cell_width) {
604 fitted_width = cell_width;
605 }
606 if (fitted_height > cell_height) {
607 fitted_height = cell_height;
608 }
609
610 // Add area used by this video
611 total_area_used += fitted_width * fitted_height;
612 }
613
614 // Calculate utilization percentage
615 float total_available_area = (float)(cell_area * sources_with_video);
616 float utilization = total_area_used / total_available_area;
617
618 float test_cell_visual_aspect = (float)cell_width / ((float)cell_height * CHAR_ASPECT);
619 log_dev_every(LOG_RATE_NORMAL, " Testing %dx%d: cell=%dx%d (visual aspect %.2f), utilization=%.1f%%", cols, rows,
620 cell_width, cell_height, test_cell_visual_aspect, utilization * 100.0f);
621
622 // Prefer configurations with better utilization
623 if (utilization > best_utilization) {
624 best_utilization = utilization;
625 best_cols = cols;
626 best_rows = rows;
627 }
628 }
629
630 *out_cols = best_cols;
631 *out_rows = best_rows;
632
633 float terminal_visual_aspect = (float)terminal_width / ((float)terminal_height * CHAR_ASPECT);
635 "Grid layout: %d clients -> %dx%d grid (%.1f%% utilization) | terminal=%dx%d (char aspect %.2f, VISUAL "
636 "aspect %.2f), video aspect: %.2f",
637 sources_with_video, best_cols, best_rows, best_utilization * 100.0f, terminal_width, terminal_height,
638 (float)terminal_width / (float)terminal_height, terminal_visual_aspect, avg_aspect);
639}
640
652static image_t *create_multi_source_composite(image_source_t *sources, int source_count, int sources_with_video,
653 const char *target_client_id, unsigned short width,
654 unsigned short height) {
655 (void)target_client_id; // Unused - composite is same for all clients now
656
657 // Calculate optimal grid layout using space-maximizing algorithm
658 int grid_cols, grid_rows;
659 calculate_optimal_grid_layout(sources, source_count, sources_with_video, width, height, &grid_cols, &grid_rows);
660
661 // Calculate composite dimensions in PIXELS for half-block mode
662 // Terminal dimensions are in CHARACTERS, need to convert to pixels:
663 // - Width: 1 char = 1 horizontal pixel
664 // - Height: 1 char = 2 vertical pixels (half-block = 2 pixels per char)
665 const int PIXELS_PER_CHAR_HEIGHT = 2;
666 int composite_width_px = width; // chars = pixels horizontally
667 int composite_height_px = height * PIXELS_PER_CHAR_HEIGHT; // chars * 2 = pixels vertically
668
669 // Create composite with final dimensions - no recreation needed
670 image_t *composite = image_new_from_pool(composite_width_px, composite_height_px);
671 image_clear(composite);
672
673 // Place each source in the grid
674 int video_source_index = 0; // Track only sources with video
675 for (int i = 0; i < source_count && video_source_index < 9; i++) { // Max 9 sources in 3x3 grid
676 if (!sources[i].image)
677 continue;
678
679 int row = video_source_index / grid_cols;
680 int col = video_source_index % grid_cols;
681 video_source_index++;
682
683 // Use actual composite dimensions for cell calculations
684 // Composite is now in PIXELS (already converted from terminal characters)
685 int cell_width_px = composite->w / grid_cols;
686 int cell_height_px = composite->h / grid_rows;
687
688 // Calculate aspect ratios
689 float src_aspect = (float)sources[i].image->w / (float)sources[i].image->h;
690 float cell_visual_aspect = (float)cell_width_px / (float)cell_height_px;
691
692 int target_width_px, target_height_px;
693
694 // CONTAIN strategy: Fill one dimension completely, let other scale down (never overflow)
695 // Compare aspects to decide which dimension to fill
696 if (src_aspect > cell_visual_aspect) {
697 // Video is WIDER than cell → fill WIDTH (height will be smaller)
698 target_width_px = cell_width_px;
699 target_height_px = (int)((cell_width_px / src_aspect) + 0.5f);
700 } else {
701 // Video is TALLER than cell → fill HEIGHT (width will be smaller)
702 target_height_px = cell_height_px;
703 target_width_px = (int)((cell_height_px * src_aspect) + 0.5f);
704 }
705
706 log_dev_every(LOG_RATE_NORMAL, "Cell %d: %dx%d px, video %.1f, cell %.2f → target %dx%d px (fill %s)",
707 video_source_index - 1, cell_width_px, cell_height_px, src_aspect, cell_visual_aspect,
708 target_width_px, target_height_px, (src_aspect > cell_visual_aspect) ? "WIDTH" : "HEIGHT");
709
710 // Create resized image with standard allocation
711 image_t *resized = image_new_from_pool(target_width_px, target_height_px);
712 image_resize(sources[i].image, resized);
713
714 // Calculate cell position in pixels (after any composite recreation)
715 int cell_x_offset_px = col * cell_width_px;
716 int cell_y_offset_px = row * cell_height_px;
717
718 // Grid centering strategy:
719 // - Multi-client: Apply padding ONLY to edge cells to center the grid as a whole
720 // (left column gets left padding, right column gets right padding)
721 // (top row gets top padding, bottom row gets bottom padding)
722 // This keeps cells edge-to-edge while centering the entire grid
723 // - Single client: Center the image within the cell
724 int x_padding_px, y_padding_px;
725
726 // Center images within their cells for all layouts
727 // This prevents gaps/stripes between clients
728 x_padding_px = (cell_width_px - target_width_px) / 2;
729 y_padding_px = (cell_height_px - target_height_px) / 2;
730
731 // Define cell boundaries for clipping (prevents bleeding into adjacent cells)
732 int cell_x_min = cell_x_offset_px;
733 int cell_x_max = cell_x_offset_px + cell_width_px - 1;
734 int cell_y_min = cell_y_offset_px;
735 int cell_y_max = cell_y_offset_px + cell_height_px - 1;
736
737 // Copy resized image to composite with cell boundary clipping
738 // This allows images to fill cells completely while preventing overlap
739
740 for (int y = 0; y < resized->h; y++) {
741 for (int x = 0; x < resized->w; x++) {
742 // Calculate destination position
743 int dst_x = cell_x_offset_px + x_padding_px + x;
744 int dst_y = cell_y_offset_px + y_padding_px + y;
745
746 // Clip to cell boundaries (prevents bleeding into adjacent cells)
747 if (dst_x < cell_x_min || dst_x > cell_x_max || dst_y < cell_y_min || dst_y > cell_y_max) {
748 continue; // Skip pixels outside cell boundaries
749 }
750
751 // Additional composite boundary check
752 if (dst_x < 0 || dst_x >= composite->w || dst_y < 0 || dst_y >= composite->h) {
753 continue; // Skip pixels outside composite
754 }
755
756 // Copy pixel
757 int src_idx = (y * resized->w) + x;
758 int dst_idx = (dst_y * composite->w) + dst_x;
759 composite->pixels[dst_idx] = resized->pixels[src_idx];
760 }
761 }
762
763 image_destroy_to_pool(resized);
764 }
765
766 return composite;
767}
768
778static char *convert_composite_to_ascii(image_t *composite, const char *target_client_id, unsigned short width,
779 unsigned short height) {
780 // LOCK OPTIMIZATION: Don't call find_client_by_id() - it would acquire rwlock unnecessarily
781 // Instead, the render thread already has snapshot of client state, so we just need palette data
782 // which is stable after initialization
783
784 // We need to find the client to access palette data, but we can do this without locking
785 // since palette is initialized once and never changes
786 client_info_t *render_client = NULL;
787
788 // Find client without locking - client_id is stable once set
789 for (int i = 0; i < MAX_CLIENTS; i++) {
791 if (strcmp(client->client_id, target_client_id) == 0) {
792 render_client = client;
793 break;
794 }
795 }
796
797 if (!render_client) {
798 SET_ERRNO(ERROR_INVALID_STATE, "Per-client %s: Target client not found", target_client_id);
799 return NULL;
800 }
801
802 // Snapshot terminal capabilities WITHOUT holding rwlock
803 // Terminal caps are set once during handshake and never change, so this is safe
804 bool has_terminal_caps_snapshot = render_client->has_terminal_caps;
805 if (!has_terminal_caps_snapshot) {
806 SET_ERRNO(ERROR_INVALID_STATE, "Per-client %s: Terminal capabilities not received", target_client_id);
807 return NULL;
808 }
809
810 terminal_capabilities_t caps_snapshot = render_client->terminal_caps;
811
812 if (!render_client->client_palette_initialized) {
813 SET_ERRNO(ERROR_TERMINAL, "Client %s palette not initialized - cannot render frame", target_client_id);
814 return NULL;
815 }
816
817 // Render with client's custom palette using enhanced capabilities
818 // Palette data is stable after initialization, so no locking needed
819 const int h = caps_snapshot.render_mode == RENDER_MODE_HALF_BLOCK ? height * 2 : height;
820
821 // DEBUG: Log dimensions being used for ASCII conversion
822 log_dev_every(LOG_RATE_SLOW, "convert_composite_to_ascii: composite=%dx%d, terminal=%dx%d, h=%d (mode=%d)",
823 composite->w, composite->h, width, height, h, caps_snapshot.render_mode);
824
825 // Pass full terminal dimensions so ascii_convert_with_capabilities can fit the image correctly
826 // with proper CHAR_ASPECT correction. The composite may have been pre-fitted in pixel space,
827 // but ascii_convert will handle terminal character aspect ratio properly when aspect_ratio=true.
828 uint64_t convert_start_ns = time_get_ns();
829 char *ascii_frame = ascii_convert_with_capabilities(composite, width, h, &caps_snapshot, true, false,
830 render_client->client_palette_chars);
831 uint64_t convert_end_ns = time_get_ns();
832 uint64_t convert_duration_ns = convert_end_ns - convert_start_ns;
833
834 if (convert_duration_ns > 5 * NS_PER_MS_INT) { // Log if > 5ms
835 char duration_str[32];
836 time_pretty((uint64_t)((double)convert_duration_ns), -1, duration_str, sizeof(duration_str));
837 log_warn_every(US_PER_SEC_INT, "SLOW_ASCII_CONVERT: Client %s took %s to convert %dx%d image to ASCII",
838 target_client_id, duration_str, composite->w, composite->h);
839 }
840
841 const bool matrix_rain = (caps_snapshot.capabilities & TERM_CAP_MATRIX_RAIN) != 0;
842 if (matrix_rain && ascii_frame) {
843 if (!render_client->digital_rain || render_client->digital_rain->num_columns != width ||
844 render_client->digital_rain->num_rows != h) {
845 if (render_client->digital_rain) {
846 digital_rain_destroy(render_client->digital_rain);
847 }
848 render_client->digital_rain = digital_rain_init(width, h);
849 render_client->digital_rain_last_update_time = (double)time_get_ns() / (double)NS_PER_SEC_INT;
850 }
851
852 if (render_client->digital_rain) {
853 const double now = (double)time_get_ns() / (double)NS_PER_SEC_INT;
854 const float elapsed = (float)(now - render_client->digital_rain_last_update_time);
855 render_client->digital_rain_last_update_time = now;
856 digital_rain_set_color_from_filter(render_client->digital_rain, caps_snapshot.color_filter);
857 char *rain_frame = digital_rain_apply(render_client->digital_rain, ascii_frame, elapsed);
858 if (rain_frame) {
859 SAFE_FREE(ascii_frame);
860 ascii_frame = rain_frame;
861 }
862 }
863 } else if (render_client->digital_rain) {
864 digital_rain_destroy(render_client->digital_rain);
865 render_client->digital_rain = NULL;
866 }
867
868 return ascii_frame;
869}
870
871/* ============================================================================
872 * Per-Client Video Mixing and Frame Generation
873 * ============================================================================
874 */
875
971// Compute hash of all active video sources for cache invalidation
972// Uses hardware-accelerated CRC32 for ultra-fast hashing
973char *create_mixed_ascii_frame_for_client(const char *target_client_id, unsigned short width, unsigned short height,
974 bool wants_stretch, size_t *out_size, bool *out_grid_changed,
975 int *out_sources_count) {
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}
1206
1207/* ============================================================================
1208 * Frame Queuing and Delivery Functions
1209 * ============================================================================
1210 */
1211
1270// REMOVED: queue_ascii_frame_for_client - video now uses double buffer directly in client->outgoing_video_buffer
1271
1315int queue_audio_for_client(client_info_t *client, const void *audio_data, size_t data_size) {
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}
1322
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}
#define CHAR_ASPECT
📐 Aspect Ratio Calculation Functions
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
bool atomic_load_bool(atomic_t *a)
Atomically load a boolean value.
Definition atomic.c:169
uint64_t atomic_load_u64(atomic_t *a)
Atomically load a uint64_t value.
Definition atomic.c:233
⚛️ Atomic operations abstraction layer with debug tracking
🗃️ Lock-Free Unified Memory Buffer Pool with Lazy Allocation
⚙️ Common definitions, error codes, macros, and types shared throughout the application
Named object registry for debugging — log identifiable resource names.
Matrix-style digital rain effect for ASCII frames.
🔄 Network byte order conversion helpers
#define NET_TO_HOST_U32(val)
Definition endian.h:81
atomic_t g_should_exit
Global application exit flag (shared across all modes)
Definition globals.c:34
unsigned int uint32_t
Definition common.h:58
#define SAFE_STRNCPY(dst, src, size)
Definition common.h:414
#define SAFE_FREE(ptr)
Definition common.h:376
#define SAFE_MALLOC(size, cast)
Definition common.h:264
#define read_u32_unaligned
Definition common.h:485
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
@ ASCIICHAT_OK
Definition error_codes.h:51
@ ERROR_INVALID_PARAM
@ ERROR_TERMINAL
Definition error_codes.h:69
#define MAX_CLIENTS
Maximum possible clients (static array size) - actual runtime limit set by –max-clients (1-32)
Definition limits.h:26
#define MAX_CLIENT_ID_LEN
Maximum client ID length - format: "noun.N (transport:port)" e.g., "mountain.0 (tcp:48036)".
Definition limits.h:23
#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_RATE_NORMAL
Log rate limit: 3 seconds (3,000,000 microseconds)
Definition log_rates.h:29
#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 log_debug(...)
Log a DEBUG message.
Definition log/log.h:548
#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
#define NS_PER_SEC_INT
Definition time.h:157
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
#define US_PER_SEC_INT
Definition time.h:161
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
#define mutex_lock(mutex)
Lock a mutex (with debug tracking in debug builds)
#define rwlock_rdlock(lock)
Acquire a read lock (with debug tracking in debug builds)
Definition rwlock.h:294
#define mutex_unlock(mutex)
Unlock a mutex (with debug tracking in debug builds)
#define rwlock_rdunlock(lock)
Release a read lock (with debug tracking in debug builds)
Definition rwlock.h:331
image_t * image_new_copy(const image_t *source)
Create a copy of an existing image.
char * ascii_convert_with_capabilities(image_t *original, const ssize_t width, const ssize_t height, const terminal_capabilities_t *caps, const bool use_aspect_ratio, const bool stretch, const char *palette_chars)
Convert image to ASCII art with terminal capability awareness.
Definition ascii.c:233
void digital_rain_set_color_from_filter(digital_rain_t *rain, color_filter_t filter)
Set digital rain color from color filter.
void image_destroy_to_pool(image_t *image)
Destroy an image allocated from buffer pool.
void image_clear(image_t *p)
Clear image (set all pixels to black)
void digital_rain_destroy(digital_rain_t *rain)
Destroy digital rain effect context.
char * digital_rain_apply(digital_rain_t *rain, const char *frame, float delta_time)
Apply digital rain effect to ASCII frame.
void image_resize(const image_t *s, image_t *d)
Resize image using nearest-neighbor interpolation.
image_t * image_new_from_pool(size_t width, size_t height)
Create a new image from buffer pool.
digital_rain_t * digital_rain_init(int num_columns, int num_rows)
Initialize digital rain effect context.
void image_destroy(image_t *p)
Destroy an image allocated with image_new()
@ IMAGE_ALLOC_POOL
Pixels allocated with buffer_pool_alloc()
#define log_debug_every(interval_us, fmt,...)
Rate-limited DEBUG logging.
Definition log/log.h:702
#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
🔢 Mathematical Utility Functions
Lock-Free Ring Buffer and Frame Buffer Management.
ascii-chat Server Mode Entry Point Header
int frame
Definition splash.c:99
rwlock_t g_client_manager_rwlock
Reader-writer lock protecting the global client manager.
bool g_client_manager_rwlock_initialized
client_manager_t g_client_manager
Global client manager singleton - central coordination point.
Client-side H.265 media capture and encoding pipeline.
bool any_clients_sending_video(void)
Check if any connected clients are currently sending video.
Definition stream.c:1335
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.
Definition stream.c:973
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.
Definition stream.c:1315
Multi-client video mixing and ASCII frame generation.
Network quality metrics for a single participant.
Definition metrics.h:33
Atomic value wrapper for integral/boolean types.
Definition atomic.h:76
Per-client state structure for server-side client management.
terminal_capabilities_t terminal_caps
video_frame_buffer_t * incoming_video_buffer
char client_id[MAX_CLIENT_ID_LEN]
client_info_t clients[MAX_CLIENTS]
Array of client_info_t structures (backing storage)
int num_columns
Number of columns in grid.
int num_rows
Number of rows in grid.
Image source structure for multi-client video mixing.
Definition stream.c:206
bool has_video
Whether this client has active video stream.
Definition stream.c:212
image_t * image
Pointer to client's current video frame (owned by buffer system)
Definition stream.c:208
Image structure.
int w
Image width in pixels (must be > 0)
uint8_t alloc_method
Allocation method (image_alloc_method_t) for correct deallocation.
int h
Image height in pixels (must be > 0)
rgb_pixel_t * pixels
Pixel data array (width * height RGB pixels, row-major order)
Multi-source frame structure for multi-user support.
Definition ringbuffer.h:400
char * data
Pointer to frame data (not owned by this struct)
Definition ringbuffer.h:412
uint32_t timestamp
Timestamp when frame was captured.
Definition ringbuffer.h:408
char source_client_id[64]
Client ID that sent this frame.
Definition ringbuffer.h:404
size_t size
Actual size of frame data in bytes.
Definition ringbuffer.h:410
RGB pixel structure.
Complete terminal capabilities structure.
Definition terminal.h:709
render_mode_t render_mode
Preferred rendering mode (render_mode_t)
Definition terminal.h:721
uint32_t capabilities
Capability flags bitmask (terminal_capability_flags_t)
Definition terminal.h:713
color_filter_t color_filter
Monochromatic color filter enum value (color_filter_t)
Definition terminal.h:735
Video frame buffer manager.
mutex_t swap_mutex
Brief mutex for pointer swap only.
size_t allocated_buffer_size
Size of allocated data buffers (for cleanup)
video_frame_t * front_buffer
Currently displayed frame (reader owns)
Video frame structure.
@ TERM_CAP_MATRIX_RAIN
Client requests Matrix digital-rain post-processing.
Definition terminal.h:651
@ RENDER_MODE_HALF_BLOCK
Unicode half-block characters (mixed foreground/background)
Definition terminal.h:668
⏱️ High-precision timing utilities using sokol_time.h and uthash
asciichat_error_t image_calc_rgb_size(size_t width, size_t height, size_t *out_size)
Calculate total RGB buffer size from dimensions.
Definition util/image.c:54
🖼️ Safe overflow-checked buffer size calculations for images and video frames
asciichat_error_t image_validate_dimensions(size_t width, size_t height)
Validate image dimensions (non-zero, within limits)
Definition video.c:12