ascii-chat
Binary v0.15.83-53-gb711d8720
Library lib/v0.3.2-701-gb711d8720
Video chat in your terminal
Loading...
Searching...
No Matches
src/server/stream.c
Go to the documentation of this file.
1
130
131/* Storage capacities in elements; text buffers include the terminating NUL. */
132#define SERVER_STREAM_HEX_BUFFER_SIZE 300
133#define SERVER_STREAM_ASCII_BUFFER_SIZE 100
134
135#include <libplatform/atomic.h>
137#include <stdio.h>
138#include <string.h>
139#include <time.h>
140#include <math.h>
141#include <float.h>
142
143#include "main.h"
144#include "stream.h"
145#include "client.h"
146#include <ascii-chat/common.h>
148#include <ascii-chat/buffer.h>
154#include <ascii-chat/util/utf8.h>
159#include <ascii-chat/util/time.h>
161
171static atomic_t g_previous_active_video_count = {0};
172
222
230static int collect_video_sources(image_source_t *sources, int max_sources) {
231 int source_count = 0;
232
233 // Check for shutdown before acquiring locks to prevent lock corruption
234 if (atomic_load_bool(&g_should_exit)) {
235 return 0;
236 }
237
238 // Keep client slots and their incoming frame buffers alive until every
239 // active source has been copied out of its double buffer. remove_client()
240 // frees those buffers under the manager write lock after stopping only that
241 // client's own workers; the other clients' render workers also read them.
242 bool manager_lock_held = g_client_manager_rwlock_initialized;
243 if (manager_lock_held) {
245 }
246
247 // Snapshot fields while holding the manager read lock so removal cannot
248 // invalidate a frame-buffer pointer between this scan and its copy.
252 typedef struct {
253 char client_id[MAX_CLIENT_ID_LEN];
254 bool is_active;
255 bool is_sending_video;
256 video_frame_buffer_t *video_buffer;
257 } client_snapshot_t;
258
259 client_snapshot_t client_snapshots[MAX_CLIENTS];
260 int snapshot_count = 0;
261
262 // The manager read lock protects slot identity and buffer lifetime; stream
263 // flags are atomic because receive callbacks update them independently.
264 for (int i = 0; i < MAX_CLIENTS; i++) {
265 client_info_t *client = &g_client_manager.clients[i];
266
267 if (client->client_id[0] == '\0') {
268 continue; // Skip uninitialized clients
269 }
270
271 // Snapshot all needed client state (all atomic reads or stable pointers)
272 SAFE_STRNCPY(client_snapshots[snapshot_count].client_id, client->client_id,
273 sizeof(client_snapshots[snapshot_count].client_id) - 1);
274 client_snapshots[snapshot_count].is_active = atomic_load_bool(&client->active);
275 client_snapshots[snapshot_count].is_sending_video = atomic_load_bool(&client->is_sending_video);
276 client_snapshots[snapshot_count].video_buffer = client->incoming_video_buffer; // Stable pointer
277 snapshot_count++;
278 }
279
280 // Process frames (expensive operations)
281 log_dev_every(5 * NS_PER_MS_INT, "Processing %d snapshots", snapshot_count);
282 for (int i = 0; i < snapshot_count && source_count < max_sources; i++) {
283 client_snapshot_t *snap = &client_snapshots[i];
284
285 if (!snap->is_active) {
286 log_dev_every(5 * NS_PER_MS_INT, "Skipping inactive client %s", snap->client_id);
287 continue;
288 }
289
290 log_dev_every(5 * NS_PER_MS_INT, "Client %s: is_sending_video=%d", snap->client_id, snap->is_sending_video);
291
292 SAFE_STRNCPY(sources[source_count].client_id, snap->client_id, sizeof(sources[source_count].client_id) - 1);
293 sources[source_count].image = NULL; // Will be set if video is available
294 sources[source_count].has_video = false;
295
296 // Declare these outside the if block so they're accessible later
297 multi_source_frame_t current_frame = {0};
298 bool got_new_frame = false;
299
300 // Always try to get the last available video frame for consistent ASCII generation.
301 // Keep the swap mutex held until the front-buffer bytes have been copied:
302 // after the next commit, this frame becomes the writer's back buffer and
303 // can be overwritten while the compositor is still reading it.
304 if (snap->is_sending_video && snap->video_buffer) {
305 video_frame_buffer_t *video_buffer = snap->video_buffer;
306 mutex_lock(&video_buffer->swap_mutex);
307 const video_frame_t *frame = video_buffer->front_buffer;
308 if (!frame || !frame->data || frame->size < sizeof(uint32_t) * 2 ||
309 frame->size > video_buffer->allocated_buffer_size) {
310 mutex_unlock(&video_buffer->swap_mutex);
311 continue;
312 }
313
314 size_t frame_size_val = frame->size;
315
316 if (frame_size_val >= (sizeof(uint32_t) * 2 + 3)) {
317 // PARSE AND VALIDATE DIMENSIONS BEFORE COPYING
318 // Don't trust frame->size - calculate correct size from dimensions
319 uint32_t peek_width = NET_TO_HOST_U32(read_u32_unaligned(frame->data));
320 uint32_t peek_height = NET_TO_HOST_U32(read_u32_unaligned(frame->data + sizeof(uint32_t)));
321
322 // Reject obviously corrupted dimensions
323 if (peek_width == 0 || peek_height == 0 || peek_width > 4096 || peek_height > 2160) {
324 log_debug("Per-client %s: rejected dimensions %ux%u as corrupted", snap->client_id, peek_width, peek_height);
325 mutex_unlock(&video_buffer->swap_mutex);
326 continue;
327 }
328
329 // Validate dimensions
330 if (image_validate_dimensions((size_t)peek_width, (size_t)peek_height) != ASCIICHAT_OK) {
331 mutex_unlock(&video_buffer->swap_mutex);
332 continue;
333 }
334
335 // Calculate CORRECT frame size based on dimensions (don't trust frame->size)
336 size_t correct_frame_size = sizeof(uint32_t) * 2;
337 {
338 size_t rgb_size = 0;
339 if (image_calc_rgb_size((size_t)peek_width, (size_t)peek_height, &rgb_size) != ASCIICHAT_OK) {
340 log_debug("Per-client: rgb_size calc failed for %ux%u", peek_width, peek_height);
341 mutex_unlock(&video_buffer->swap_mutex);
342 continue;
343 }
344 correct_frame_size += rgb_size;
345 }
346
347 log_debug_every(NS_PER_MS_INT, "Per-client: frame dimensions=%ux%u, frame_size=%zu, correct_size=%zu",
348 peek_width, peek_height, frame_size_val, correct_frame_size);
349
350 // Verify frame is at least large enough for the correct size
351 if (frame_size_val < correct_frame_size) {
352 log_debug("Per-client: frame too small: got %zu, need %zu", frame_size_val, correct_frame_size);
353 mutex_unlock(&video_buffer->swap_mutex);
354 continue;
355 }
356
357 // We have frame data - copy ONLY the correct amount based on dimensions
358 current_frame.data = buffer_alloc(NULL, correct_frame_size);
359
360 if (current_frame.data) {
361 memcpy(current_frame.data, frame->data, correct_frame_size);
362 current_frame.size = correct_frame_size;
363 SAFE_STRNCPY(current_frame.source_client_id, snap->client_id, sizeof(current_frame.source_client_id) - 1);
364 current_frame.timestamp = (uint32_t)(frame->capture_timestamp_ns / NS_PER_SEC_INT);
365 got_new_frame = true;
366 // Keep the frame for reuse, but mark it consumed before the next swap.
367 atomic_store_bool(&video_buffer->new_frame_available, false);
368 }
369 mutex_unlock(&video_buffer->swap_mutex);
370 } else {
371 mutex_unlock(&video_buffer->swap_mutex);
372 }
373 }
374
375 multi_source_frame_t *frame_to_use = got_new_frame ? &current_frame : NULL;
376
377 if (frame_to_use && frame_to_use->data && frame_to_use->size > sizeof(uint32_t) * 2) {
378 // Parse the image data
379 // Format: [width:4][height:4][rgb_data:w*h*3]
380 // Use unaligned read helpers - frame data may not be aligned
381 uint32_t img_width = NET_TO_HOST_U32(read_u32_unaligned(frame_to_use->data));
382 uint32_t img_height = NET_TO_HOST_U32(read_u32_unaligned(frame_to_use->data + sizeof(uint32_t)));
383
384 // Debug logging to understand the data
385 if (img_width == 0xBEBEBEBE || img_height == 0xBEBEBEBE) {
386 SET_ERRNO(ERROR_INVALID_STATE, "Uninitialized frame dimensions from client %s: 0x%08x x 0x%08x",
387 snap->client_id, img_width, img_height);
388 }
389
390 // Validate dimensions using image utility function
391 if (image_validate_dimensions((size_t)img_width, (size_t)img_height) != ASCIICHAT_OK) {
393 "Per-client: Invalid image dimensions from client %s: %ux%u (data may be corrupted)", snap->client_id,
394 img_width, img_height);
395 if (got_new_frame) {
396 buffer_release(current_frame.data);
397 }
398 source_count++;
399 continue;
400 }
401
402 // Calculate expected frame size with overflow checking
403 size_t expected_size = sizeof(uint32_t) * 2;
404 {
405 size_t rgb_size = 0;
406 if (image_calc_rgb_size((size_t)img_width, (size_t)img_height, &rgb_size) != ASCIICHAT_OK) {
407 SET_ERRNO(ERROR_INVALID_STATE, "Per-client: RGB size calculation failed for client %s: %ux%u",
408 snap->client_id, img_width, img_height);
409 if (got_new_frame) {
410 buffer_release(current_frame.data);
411 }
412 source_count++;
413 continue;
414 }
415 expected_size += rgb_size;
416 }
417 if (frame_to_use->size != expected_size) {
419 "Per-client: Frame size mismatch from client %s: got %zu, expected %zu for %ux%u image",
420 snap->client_id, frame_to_use->size, expected_size, img_width, img_height);
421 if (got_new_frame) {
422 buffer_release(current_frame.data);
423 }
424 source_count++;
425 continue;
426 }
427
428 // Extract pixel data pointer
429 rgb_pixel_t *pixels = (rgb_pixel_t *)(frame_to_use->data + (sizeof(uint32_t) * 2));
430
431 // Create image from buffer domain for consistent video pipeline management
432 image_t *img = image_new(img_width, img_height);
433 if (!img) {
434 log_error("Per-client: image_new failed for %ux%u", img_width, img_height);
435 if (got_new_frame) {
436 buffer_release(current_frame.data);
437 }
438 continue;
439 }
440 memcpy(img->pixels, pixels, (size_t)img_width * (size_t)img_height * sizeof(rgb_pixel_t));
441 sources[source_count].image = img;
442 sources[source_count].has_video = true;
443
444 // Free temporary frame buffer - image has its own pixel data now
445 if (got_new_frame) {
446 buffer_release(current_frame.data);
447 }
448 } else {
449 // frame_to_use check failed - clean up allocated frame data
450 if (got_new_frame && current_frame.data) {
451 buffer_release(current_frame.data);
452 }
453 }
454
455 // Increment source count for this active client (with or without video)
456 source_count++;
457 }
458
459 if (manager_lock_held) {
461 }
462 return source_count;
463}
464
475static image_t *create_single_source_composite(image_source_t *sources, int source_count,
476 const char *target_client_id __attribute__((unused)),
477 unsigned short width __attribute__((unused)),
478 unsigned short height __attribute__((unused))) {
479 // Find the single source with video
480 image_t *single_source = NULL;
481 for (int i = 0; i < source_count; i++) {
482 if (sources[i].has_video && sources[i].image) {
483 single_source = sources[i].image;
484 break;
485 }
486 }
487
488 if (!single_source) {
489 SET_ERRNO(ERROR_INVALID_STATE, "Logic error: sources_with_video=1 but no source found");
490 return NULL;
491 }
492
493 // For single source, don't pre-fit the image. Let ascii_convert_with_capabilities handle
494 // all aspect ratio fitting with proper CHAR_ASPECT correction. This avoids double-correction
495 // that was happening when the image was pre-fitted in pixel space and then aspect_ratio()
496 // was called again with CHAR_ASPECT=2.0.
497 // Just return the source image as-is; ascii_convert_with_capabilities will fit it properly.
498 return single_source;
499}
500
522static void calculate_optimal_grid_layout(image_source_t *sources, int source_count, int sources_with_video,
523 int terminal_width, int terminal_height, int *out_cols, int *out_rows) {
524 // Special cases
525 if (sources_with_video == 0) {
526 *out_cols = 0;
527 *out_rows = 0;
528 return;
529 }
530
531 if (sources_with_video == 1) {
532 *out_cols = 1;
533 *out_rows = 1;
534 return;
535 }
536
537 // ASCII character aspect ratio: characters are ~2x taller than wide
538 // So we need to adjust terminal dimensions to visual space
539 const float CHAR_ASPECT = 2.0f; // Character height / width ratio
540
541 // Calculate average aspect ratio of all video sources
542 float avg_aspect = 0.0f;
543 int aspect_count = 0;
544 for (int i = 0; i < source_count; i++) {
545 if (sources[i].has_video && sources[i].image) {
546 float aspect = (float)sources[i].image->w / (float)sources[i].image->h;
547 avg_aspect += aspect;
548 aspect_count++;
549 }
550 }
551 if (aspect_count > 0) {
552 avg_aspect /= aspect_count;
553 } else {
554 avg_aspect = 1.6f; // Default aspect ratio
555 }
556
557 // Try all reasonable grid configurations
558 int best_cols = 1;
559 int best_rows = sources_with_video;
560 float best_utilization = 0.0f;
561
562 // Try grid configurations from 1x1 up to reasonable limits
563 for (int cols = 1; cols <= sources_with_video; cols++) {
564 int rows = (sources_with_video + cols - 1) / cols; // Ceiling division
565
566 // Skip configurations with too many empty cells (more than one row worth)
567 int total_cells = cols * rows;
568 int empty_cells = total_cells - sources_with_video;
569 if (empty_cells > cols) {
570 continue;
571 }
572
573 // Calculate cell dimensions for this configuration
574 int cell_width = terminal_width / cols;
575 int cell_height = terminal_height / rows;
576
577 // Skip if cells would be too small
578 if (cell_width < 20 || cell_height < 10) {
579 continue;
580 }
581
582 // Calculate total area utilized by all videos in this configuration
583 // For each video, calculate how much space it would use in a cell
584 // IMPORTANT: Account for character aspect ratio (chars are 2x taller than wide)
585 float total_area_used = 0.0f;
586 int cell_area = cell_width * cell_height;
587
588 for (int i = 0; i < sources_with_video; i++) {
589 // Use average aspect ratio for calculation
590 float video_aspect = avg_aspect;
591
592 // Calculate VISUAL cell aspect ratio (accounting for character shape)
593 // A cell that is cell_width chars wide and cell_height chars tall
594 // has visual aspect = cell_width / (cell_height * CHAR_ASPECT)
595 float cell_visual_aspect = (float)cell_width / ((float)cell_height * CHAR_ASPECT);
596
597 // Calculate fitted dimensions while preserving aspect ratio
598 int fitted_width, fitted_height;
599
600 if (video_aspect > cell_visual_aspect) {
601 // Video is wider than cell - fit to width
602 fitted_width = cell_width;
603 // Visual height needed: cell_width / video_aspect
604 // Character height: visual_height / CHAR_ASPECT
605 fitted_height = (int)((cell_width / video_aspect) / CHAR_ASPECT);
606 } else {
607 // Video is taller than cell - fit to height
608 fitted_height = cell_height;
609 // Visual width needed: cell_height * CHAR_ASPECT * video_aspect
610 fitted_width = (int)(cell_height * CHAR_ASPECT * video_aspect);
611 }
612
613 // Clamp to cell bounds
614 if (fitted_width > cell_width) {
615 fitted_width = cell_width;
616 }
617 if (fitted_height > cell_height) {
618 fitted_height = cell_height;
619 }
620
621 // Add area used by this video
622 total_area_used += fitted_width * fitted_height;
623 }
624
625 // Calculate utilization percentage
626 float total_available_area = (float)(cell_area * sources_with_video);
627 float utilization = total_area_used / total_available_area;
628
629 float test_cell_visual_aspect = (float)cell_width / ((float)cell_height * CHAR_ASPECT);
630 log_dev_every(LOG_RATE_NORMAL, " Testing %dx%d: cell=%dx%d (visual aspect %.2f), utilization=%.1f%%", cols, rows,
631 cell_width, cell_height, test_cell_visual_aspect, utilization * 100.0f);
632
633 // Prefer configurations with better utilization
634 if (utilization > best_utilization) {
635 best_utilization = utilization;
636 best_cols = cols;
637 best_rows = rows;
638 }
639 }
640
641 *out_cols = best_cols;
642 *out_rows = best_rows;
643
644 float terminal_visual_aspect = (float)terminal_width / ((float)terminal_height * CHAR_ASPECT);
646 "Grid layout: %d clients -> %dx%d grid (%.1f%% utilization) | terminal=%dx%d (char aspect %.2f, VISUAL "
647 "aspect %.2f), video aspect: %.2f",
648 sources_with_video, best_cols, best_rows, best_utilization * 100.0f, terminal_width, terminal_height,
649 (float)terminal_width / (float)terminal_height, terminal_visual_aspect, avg_aspect);
650}
651
663static image_t *create_multi_source_composite(image_source_t *sources, int source_count, int sources_with_video,
664 const char *target_client_id, unsigned short width,
665 unsigned short height) {
666 (void)target_client_id; // Unused - composite is same for all clients now
667
668 // Calculate optimal grid layout using space-maximizing algorithm
669 int grid_cols, grid_rows;
670 calculate_optimal_grid_layout(sources, source_count, sources_with_video, width, height, &grid_cols, &grid_rows);
671
672 // Calculate composite dimensions in PIXELS for half-block mode
673 // Terminal dimensions are in CHARACTERS, need to convert to pixels:
674 // - Width: 1 char = 1 horizontal pixel
675 // - Height: 1 char = 2 vertical pixels (half-block = 2 pixels per char)
676 const int PIXELS_PER_CHAR_HEIGHT = 2;
677 int composite_width_px = width; // chars = pixels horizontally
678 int composite_height_px = height * PIXELS_PER_CHAR_HEIGHT; // chars * 2 = pixels vertically
679
680 // Create composite with final dimensions - no recreation needed
681 image_t *composite = image_new(composite_width_px, composite_height_px);
682 image_clear(composite);
683
684 // Place each source in the grid
685 int video_source_index = 0; // Track only sources with video
686 for (int i = 0; i < source_count && video_source_index < 9; i++) { // Max 9 sources in 3x3 grid
687 if (!sources[i].image)
688 continue;
689
690 int row = video_source_index / grid_cols;
691 int col = video_source_index % grid_cols;
692 video_source_index++;
693
694 // Use actual composite dimensions for cell calculations
695 // Composite is now in PIXELS (already converted from terminal characters)
696 int cell_width_px = composite->w / grid_cols;
697 int cell_height_px = composite->h / grid_rows;
698
699 // Calculate aspect ratios
700 float src_aspect = (float)sources[i].image->w / (float)sources[i].image->h;
701 float cell_visual_aspect = (float)cell_width_px / (float)cell_height_px;
702
703 int target_width_px, target_height_px;
704
705 // CONTAIN strategy: Fill one dimension completely, let other scale down (never overflow)
706 // Compare aspects to decide which dimension to fill
707 if (src_aspect > cell_visual_aspect) {
708 // Video is WIDER than cell → fill WIDTH (height will be smaller)
709 target_width_px = cell_width_px;
710 target_height_px = (int)((cell_width_px / src_aspect) + 0.5f);
711 } else {
712 // Video is TALLER than cell → fill HEIGHT (width will be smaller)
713 target_height_px = cell_height_px;
714 target_width_px = (int)((cell_height_px * src_aspect) + 0.5f);
715 }
716
717 log_dev_every(LOG_RATE_NORMAL, "Cell %d: %dx%d px, video %.1f, cell %.2f → target %dx%d px (fill %s)",
718 video_source_index - 1, cell_width_px, cell_height_px, src_aspect, cell_visual_aspect,
719 target_width_px, target_height_px, (src_aspect > cell_visual_aspect) ? "WIDTH" : "HEIGHT");
720
721 // Create resized image with standard allocation
722 image_t *resized = image_new(target_width_px, target_height_px);
723 image_resize(sources[i].image, resized);
724
725 // Calculate cell position in pixels (after any composite recreation)
726 int cell_x_offset_px = col * cell_width_px;
727 int cell_y_offset_px = row * cell_height_px;
728
729 // Grid centering strategy:
730 // - Multi-client: Apply padding ONLY to edge cells to center the grid as a whole
731 // (left column gets left padding, right column gets right padding)
732 // (top row gets top padding, bottom row gets bottom padding)
733 // This keeps cells edge-to-edge while centering the entire grid
734 // - Single client: Center the image within the cell
735 int x_padding_px, y_padding_px;
736
737 // Center images within their cells for all layouts
738 // This prevents gaps/stripes between clients
739 x_padding_px = (cell_width_px - target_width_px) / 2;
740 y_padding_px = (cell_height_px - target_height_px) / 2;
741
742 // Define cell boundaries for clipping (prevents bleeding into adjacent cells)
743 int cell_x_min = cell_x_offset_px;
744 int cell_x_max = cell_x_offset_px + cell_width_px - 1;
745 int cell_y_min = cell_y_offset_px;
746 int cell_y_max = cell_y_offset_px + cell_height_px - 1;
747
748 // Copy resized image to composite with cell boundary clipping
749 // This allows images to fill cells completely while preventing overlap
750
751 for (int y = 0; y < resized->h; y++) {
752 for (int x = 0; x < resized->w; x++) {
753 // Calculate destination position
754 int dst_x = cell_x_offset_px + x_padding_px + x;
755 int dst_y = cell_y_offset_px + y_padding_px + y;
756
757 // Clip to cell boundaries (prevents bleeding into adjacent cells)
758 if (dst_x < cell_x_min || dst_x > cell_x_max || dst_y < cell_y_min || dst_y > cell_y_max) {
759 continue; // Skip pixels outside cell boundaries
760 }
761
762 // Additional composite boundary check
763 if (dst_x < 0 || dst_x >= composite->w || dst_y < 0 || dst_y >= composite->h) {
764 continue; // Skip pixels outside composite
765 }
766
767 // Copy pixel
768 int src_idx = (y * resized->w) + x;
769 int dst_idx = (dst_y * composite->w) + dst_x;
770 composite->pixels[dst_idx] = resized->pixels[src_idx];
771 }
772 }
773
774 image_destroy(resized);
775 }
776
777 return composite;
778}
779
789static void invalidate_changed_composite_owners(ascii_stabilizer_t *history, image_source_t *sources, int source_count,
790 int sources_with_video, int width, int height) {
791 if (!history->cells || history->width != (size_t)width || sources_with_video <= 0 ||
792 (history->height != (size_t)height && history->height != (size_t)height * 2))
793 return;
794 size_t history_scale = history->height / (size_t)height;
795 int cols = 1, rows = 1;
796 if (sources_with_video > 1)
797 calculate_optimal_grid_layout(sources, source_count, sources_with_video, width, height, &cols, &rows);
798 uint64_t owners[9] = {0};
799 int count = 0;
800 for (int i = 0; i < source_count && count < 9; ++i) {
801 if (!sources[i].has_video || !sources[i].image)
802 continue;
803 uint64_t hash = 1469598103934665603ULL;
804 for (const unsigned char *p = (const unsigned char *)sources[i].client_id; *p; ++p) {
805 hash ^= *p;
806 hash *= 1099511628211ULL;
807 }
808 owners[count++] = hash ? hash : 1;
809 }
810 int cell_width = width / cols, cell_height = (height * 2) / rows;
811 if (!cell_width || !cell_height)
812 return;
813 for (size_t y = 0; y < history->height; ++y) {
814 for (int x = 0; x < width; ++x) {
815 int col = x / cell_width, row = (int)((y * 2 / history_scale) / (size_t)cell_height);
816 int index = row * cols + col;
817 uint64_t owner = col < cols && row < rows && index < count ? owners[index] : 0;
818 ascii_temporal_cell_t *cell = &history->cells[y * (size_t)width + (size_t)x];
819 if (cell->source_owner != owner)
820 cell->valid = false;
821 cell->source_owner = owner;
822 }
823 }
824}
825
826static char *convert_composite_to_ascii(image_t *composite, const char *target_client_id, unsigned short width,
827 unsigned short height, acip_render_settings_t *out_rendering,
828 image_source_t *sources, int source_count, int sources_with_video) {
829 // LOCK OPTIMIZATION: Don't call find_client_by_id() - it would acquire rwlock unnecessarily
830 // Instead, the render thread already has snapshot of client state, so we just need palette data
831 // which is stable after initialization
832
833 // We need to find the client to access palette data, but we can do this without locking
834 // since palette is initialized once and never changes
835 client_info_t *render_client = NULL;
836
837 // Find client without locking - client_id is stable once set
838 for (int i = 0; i < MAX_CLIENTS; i++) {
839 client_info_t *client = &g_client_manager.clients[i];
840 if (strcmp(client->client_id, target_client_id) == 0) {
841 render_client = client;
842 break;
843 }
844 }
845
846 if (!render_client) {
847 SET_ERRNO(ERROR_INVALID_STATE, "Per-client %s: Target client not found", target_client_id);
848 return NULL;
849 }
850
851 mutex_lock(&render_client->client_state_mutex);
852 bool ready = render_client->has_terminal_caps && render_client->client_palette_initialized;
853 acip_render_settings_t rendering = render_client->render_settings;
854 mutex_unlock(&render_client->client_state_mutex);
855 if (!ready) {
856 SET_ERRNO(ERROR_TERMINAL, "Client rendering is not configured");
857 return NULL;
858 }
859 if (out_rendering)
860 *out_rendering = rendering;
861 terminal_capabilities_t caps_snapshot = rendering.caps;
862 width = rendering.width;
863 height = rendering.height;
864 if (caps_snapshot.stabilize)
865 invalidate_changed_composite_owners(&render_client->render_stabilizer, sources, source_count, sources_with_video,
866 width, height);
867 uint64_t convert_start_ns = time_get_ns();
868 char *ascii_frame = ascii_convert_with_buffers_stabilized(
869 composite, width, height, &caps_snapshot, true, false, rendering.palette, false, NULL, NULL,
870 caps_snapshot.stabilize ? &render_client->render_stabilizer : NULL);
871 if (!caps_snapshot.stabilize)
873 uint64_t convert_end_ns = time_get_ns();
874 uint64_t convert_duration_ns = convert_end_ns - convert_start_ns;
875
876 if (convert_duration_ns > 5 * NS_PER_MS_INT) { // Log if > 5ms
877 char duration_str[TIME_DURATION_BUFFER_SIZE];
878 time_pretty((uint64_t)((double)convert_duration_ns), -1, duration_str, sizeof(duration_str));
879 log_warn_every(US_PER_SEC_INT, "SLOW_ASCII_CONVERT: Client %s took %s to convert %dx%d image to ASCII",
880 target_client_id, duration_str, composite->w, composite->h);
881 }
882
883 const bool matrix_rain = (caps_snapshot.capabilities & TERM_CAP_MATRIX_RAIN) != 0;
884 if (matrix_rain && ascii_frame) {
885 if (!render_client->digital_rain || render_client->digital_rain->num_columns != width ||
886 render_client->digital_rain->num_rows != height) {
887 if (render_client->digital_rain) {
888 digital_rain_destroy(render_client->digital_rain);
889 }
890 render_client->digital_rain = digital_rain_init(width, height);
891 render_client->digital_rain_last_update_time = (double)time_get_ns() / (double)NS_PER_SEC_INT;
892 }
893
894 if (render_client->digital_rain) {
895 const double now = (double)time_get_ns() / (double)NS_PER_SEC_INT;
896 const float elapsed = (float)(now - render_client->digital_rain_last_update_time);
897 render_client->digital_rain_last_update_time = now;
898 digital_rain_set_color_from_filter(render_client->digital_rain, caps_snapshot.color_filter);
899 char *rain_frame = digital_rain_apply(render_client->digital_rain, ascii_frame, elapsed);
900 if (rain_frame) {
901 SAFE_FREE(ascii_frame);
902 ascii_frame = rain_frame;
903 }
904 }
905 } else if (render_client->digital_rain) {
906 digital_rain_destroy(render_client->digital_rain);
907 render_client->digital_rain = NULL;
908 }
909
910 if (ascii_frame && (size_t)utf8_display_width(rendering.palette) != utf8_char_count(rendering.palette)) {
911 // Normalize display-cell geometry (wide palettes and REP included) with the
912 // same row composer as discovery/browser hosts.
913 ascii_frame_source_t source = {.frame_data = ascii_frame, .frame_size = strlen(ascii_frame)};
914 size_t size;
915 char *rectangular = ascii_create_grid(&source, 1, width, height, &size);
916 SAFE_FREE(ascii_frame);
917 ascii_frame = rectangular;
918 }
919 return ascii_frame;
920}
921
922/* ============================================================================
923 * Per-Client Video Mixing and Frame Generation
924 * ============================================================================
925 */
926
1022// Compute hash of all active video sources for cache invalidation
1023// Uses hardware-accelerated CRC32 for ultra-fast hashing
1024char *create_mixed_ascii_frame_for_client(const char *target_client_id, unsigned short width, unsigned short height,
1025 bool wants_stretch, size_t *out_size, bool *out_grid_changed,
1026 int *out_sources_count, acip_render_settings_t *out_rendering) {
1027 (void)wants_stretch; // Unused - we always handle aspect ratio ourselves
1028
1029 // Register the stream counter once even when per-client render workers enter
1030 // this function concurrently.
1031 static _Atomic bool stream_atomics_registered = false;
1032 if (!atomic_exchange_explicit(&stream_atomics_registered, true, memory_order_acq_rel)) {
1033 NAMED_REGISTER_ATOMIC(&g_previous_active_video_count, "server_video_source_count_for_layout_detection", NULL);
1034 }
1035
1036 uint64_t frame_gen_start_ns = time_get_ns();
1037
1038 // Initialize output parameters
1039 if (out_grid_changed) {
1040 *out_grid_changed = false;
1041 }
1042 if (out_sources_count) {
1043 *out_sources_count = 0;
1044 }
1045
1046 if (!out_size || width == 0 || height == 0) {
1047 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: width=%u, height=%u, out_size=%p", width, height, out_size);
1048 return NULL;
1049 }
1050
1051 // Collect all active clients and their image sources
1052 image_source_t sources[MAX_CLIENTS];
1053 uint64_t collect_start_ns = time_get_ns();
1054 int source_count = collect_video_sources(sources, MAX_CLIENTS);
1055 uint64_t collect_end_ns = time_get_ns();
1056
1057 // Count sources that actually have video data
1058 int sources_with_video = 0;
1059 for (int i = 0; i < source_count; i++) {
1060 if (sources[i].has_video && sources[i].image) {
1061 sources_with_video++;
1062 }
1063 }
1064
1065 log_dev_every(333 * NS_PER_MS_INT, "FRAME_GEN_START: target_client=%s sources=%d collect=%.1fms", target_client_id,
1066 sources_with_video, (collect_end_ns - collect_start_ns) / NS_PER_MS);
1067
1068 // Return the source count for debugging/tracking
1069 if (out_sources_count) {
1070 *out_sources_count = sources_with_video;
1071 }
1072
1073 // GRID LAYOUT CHANGE DETECTION:
1074 // Check if the number of active video sources has changed
1075 // NOTE: We only UPDATE the count and SIGNAL the change via out parameter
1076 // Broadcasting CLEAR_CONSOLE must happen AFTER the new frames are written to buffers
1077 // to prevent race condition where CLEAR arrives before new frame is ready
1078 uint64_t previous_count = atomic_load_u64(&g_previous_active_video_count);
1079 if (sources_with_video != (int)previous_count) {
1080 // Use compare-and-swap to ensure only ONE thread detects the change
1081 if (atomic_cas_u64(&g_previous_active_video_count, &previous_count, (uint64_t)sources_with_video)) {
1084 "Grid layout changing: %d -> %d active video sources - caller will broadcast clear AFTER buffering frame",
1085 previous_count, sources_with_video);
1086 if (out_grid_changed) {
1087 *out_grid_changed = true; // Signal to caller
1088 }
1089 }
1090 }
1091
1092 // No active video sources - don't generate placeholder frames
1093 image_t *composite = NULL;
1094
1095 if (sources_with_video == 0) {
1096 *out_size = 0;
1097 // No active video sources for client - this isn't an error.
1098 // Return NULL to indicate no frame should be sent
1099 return NULL;
1100 }
1101
1102 if (sources_with_video == 1) {
1103 // Single source handling - create composite and convert to ASCII
1104 // Note: create_single_source_composite returns a reference to sources[i].image
1105 // which could be modified by other threads. Make a copy to prevent concurrent
1106 // modification during ascii_convert_with_capabilities.
1107 image_t *single_source = create_single_source_composite(sources, source_count, target_client_id, width, height);
1108 if (single_source) {
1109 composite = image_new_copy(single_source);
1110 if (!composite) {
1111 SET_ERRNO(ERROR_MEMORY, "Failed to copy single source composite");
1112 *out_size = 0;
1113 return NULL;
1114 }
1115 }
1116 } else {
1117 // Multiple sources - create grid layout
1118 composite =
1119 create_multi_source_composite(sources, source_count, sources_with_video, target_client_id, width, height);
1120 }
1121
1122 char *out = NULL;
1123
1124 if (!composite) {
1125 SET_ERRNO(ERROR_INVALID_STATE, "Per-client %s: Failed to create composite image", target_client_id);
1126 *out_size = 0;
1127 return NULL;
1128 }
1129
1130 // Convert composite to ASCII using client capabilities
1131 // Pass terminal dimensions so the frame can be padded to full width
1132 char *ascii_frame = convert_composite_to_ascii(composite, target_client_id, width, height, out_rendering, sources,
1133 source_count, sources_with_video);
1134
1135 if (ascii_frame) {
1136 // The frame should have been null-terminated by the padding functions.
1137 // Use strlen() which is optimized and reliable
1138 size_t ascii_len = strlen(ascii_frame);
1139
1140 // Safety check: don't accept unreasonably large frames (10MB limit)
1141 if (ascii_len > 10 * 1024 * 1024) {
1142 log_error("Frame size exceeds 10MB safety limit (possible buffer overflow)");
1143 SET_ERRNO(ERROR_INVALID_PARAM, "Frame size exceeds 10MB");
1144 SAFE_FREE(ascii_frame);
1145 image_destroy(composite);
1146 *out_size = 0;
1147 return NULL;
1148 }
1149
1150 *out_size = ascii_len; // Renderer owns complete rows; never truncate to a color reset.
1151
1152 log_dev_every(LOG_RATE_SLOW, "Final frame size=%zu bytes for client %s", *out_size, target_client_id);
1153
1154 // Debug: Log the last 50 bytes of the frame to see what's really there
1155 if (*out_size >= 50) {
1156 char hex_buf[SERVER_STREAM_HEX_BUFFER_SIZE] = {0};
1157 size_t hex_len = 0;
1158 const uint8_t *last_bytes = (const uint8_t *)ascii_frame + (*out_size - 50);
1159 for (int i = 0; i < 50 && hex_len < sizeof(hex_buf) - 5; i++) {
1160 hex_len += snprintf(hex_buf + hex_len, sizeof(hex_buf) - hex_len, "%02X ", last_bytes[i]);
1161 }
1162 log_dev_every(4500 * US_PER_MS_INT, "FRAME_LAST_50_BYTES (hex): %s", hex_buf);
1163
1164 // Also log as ASCII for readability
1165 char ascii_buf[SERVER_STREAM_ASCII_BUFFER_SIZE] = {0};
1166 for (int i = 0; i < 50 && i < (int)sizeof(ascii_buf) - 1; i++) {
1167 if (last_bytes[i] >= 32 && last_bytes[i] < 127) {
1168 ascii_buf[i] = (char)last_bytes[i];
1169 } else if (last_bytes[i] == '\n') {
1170 ascii_buf[i] = 'N';
1171 } else if (last_bytes[i] == '\0') {
1172 ascii_buf[i] = '0';
1173 } else {
1174 ascii_buf[i] = '.';
1175 }
1176 }
1177 log_dev_every(4500 * US_PER_MS_INT, "FRAME_LAST_50_ASCII: %s", ascii_buf);
1178 }
1179
1180 out = ascii_frame;
1181 } else {
1182 SET_ERRNO(ERROR_TERMINAL, "Per-client %s: Failed to convert image to ASCII", target_client_id);
1183 *out_size = 0;
1184 }
1185
1186 if (composite) {
1187 image_destroy(composite);
1188 }
1189 for (int i = 0; i < source_count; i++) {
1190 if (sources[i].image) {
1191 image_destroy(sources[i].image);
1192 }
1193 }
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[TIME_DURATION_BUFFER_SIZE];
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
1284 // LOCK OPTIMIZATION: Don't acquire rwlock - all fields we access are atomic
1285 // client_id, active, is_sending_video are all atomic variables
1286
1287 // Iterate through all client slots
1288 for (int i = 0; i < MAX_CLIENTS; i++) {
1289 client_info_t *client = &g_client_manager.clients[i];
1290
1291 // Skip uninitialized clients (atomic read)
1292 if (client->client_id[0] == '\0') {
1293 continue;
1294 }
1295
1296 // Check if client is active and sending video (both atomic reads)
1297 bool is_active = atomic_load_bool(&client->active);
1298 bool is_sending = atomic_load_bool(&client->is_sending_video);
1299
1300 if (is_active && is_sending) {
1301 return true;
1302 }
1303 }
1304
1305 return false;
1306}
char * ascii_convert_with_buffers_stabilized(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, const bool contain, image_t **workspace, outbuf_t *output, ascii_stabilizer_t *stabilizer)
Definition ascii.c:293
#define SET_ERRNO(code,...)
#define CHAR_ASPECT
📐 Aspect Ratio Calculation Functions
void * buffer_alloc(buffer_domain_t *domain, size_t size)
Definition buffer.c:274
#define buffer_release(data)
Definition buffer.h:91
⚙️ 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
audio_visualization_source_t source
Definition fft.c:24
unsigned int height
Definition fft.c:23
unsigned int width
Definition fft.c:23
atomic_t g_should_exit
Global application exit flag (shared across all modes).
Definition globals.c:34
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 read_u32_unaligned
Definition common.h:500
unsigned long long uint64_t
Definition common.h:63
unsigned char uint8_t
Definition common.h:60
#define NAMED_REGISTER_ATOMIC(a, name, parent_ptr)
Register an atomic_t with automatic format specifier.
@ ERROR_INVALID_STATE
@ ERROR_MEMORY
Definition error_codes.h:51
@ ASCIICHAT_OK
Definition error_codes.h:46
@ ERROR_INVALID_PARAM
@ ERROR_TERMINAL
Definition error_codes.h:64
#define MAX_CLIENTS
Maximum possible clients (static array size) - actual runtime limit set by –max-clients (1-128).
#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_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_error(...)
Log an ERROR message.
Definition log/log.h:634
#define log_debug(...)
Log a DEBUG message.
Definition log/log.h:595
int32_t vec_t __attribute__((vector_size(4 *4)))
SESSION_CREATED (PACKET_TYPE_ACIP_SESSION_CREATED) - Session created response.
Definition vector.h:42
#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
#define NS_PER_MS_INT
Definition time.h:168
#define NS_PER_MS
Definition time.h:159
#define US_PER_MS_INT
Definition time.h:172
#define US_PER_SEC_INT
Definition time.h:173
#define rwlock_rdlock(lock)
Acquire a read lock (with debug tracking in debug builds).
Definition rwlock.h:299
#define rwlock_rdunlock(lock)
Release a read lock (with debug tracking in debug builds).
Definition rwlock.h:338
int utf8_display_width(const char *str)
Calculate terminal display width of a UTF-8 string.
Definition utf8.c:98
size_t utf8_char_count(const char *str)
Count UTF-8 characters (not bytes).
Definition utf8.c:108
struct client_info client_info_t
image_t * image_new_copy(const image_t *source)
Create a copy of an existing image.
void digital_rain_set_color_from_filter(digital_rain_t *rain, color_filter_t filter)
Set digital rain color from color filter.
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.
digital_rain_t * digital_rain_init(int num_columns, int num_rows)
Initialize digital rain effect context.
char * ascii_create_grid(ascii_frame_source_t *sources, int source_count, int width, int height, size_t *out_size)
Create a grid layout from multiple ASCII frames.
Definition ascii.c:742
void image_destroy(image_t *image)
Destroy an image allocated with image_new().
image_t * image_new(size_t width, size_t height)
Create a new image with standard allocation.
void ascii_stabilizer_reset(ascii_stabilizer_t *state)
#define log_debug_every(interval_us, fmt,...)
Rate-limited DEBUG logging.
Definition log/log.h:749
#define log_warn_every(interval_us, fmt,...)
Rate-limited WARN logging.
Definition log/log.h:755
#define log_dev_every(interval_us, fmt,...)
Rate-limited DEV logging.
Definition log/log.h:746
🔢 Mathematical Utility Functions
static bool size_t index
Definition queries.h:8
static bool size_t gotel_query_t * out
Definition queries.h:8
Lock-Free Ring Buffer and Frame Buffer Management.
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
Hash table entry for client packet dispatch.
Per-client state management and lifecycle orchestration.
bool any_clients_sending_video(void)
Queue ASCII frame for delivery to specific client.
#define SERVER_STREAM_HEX_BUFFER_SIZE
#define SERVER_STREAM_ASCII_BUFFER_SIZE
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.
Multi-client video mixing and ASCII frame generation.
🔤 String Manipulation and Shell Escaping Utilities
acip render settings keeps the values that move through the subsystem: caps, width,...
Frame source structure for grid layout.
Definition ascii.h:352
For ascii stabilizer, this is the live record that ties cells, width, height, configuration together....
acip_render_settings_t render_settings
video_frame_buffer_t * incoming_video_buffer
ascii_stabilizer_t render_stabilizer
char client_id[MAX_CLIENT_ID_LEN]
int num_rows
Number of rows in grid.
int num_columns
Number of columns in grid.
One client's video input as seen by the compositor.
bool has_video
Whether this client has active video stream.
image_t * image
Pointer to client's current video frame (owned by buffer system).
char client_id[MAX_CLIENT_ID_LEN]
Unique client identifier for this source.
SIMD-friendly storage for one RGB pixel. r, g, and b hold the color channels and padding keeps the la...
int w
Image width in pixels (must be > 0).
int h
Image height in pixels (must be > 0).
rgb_pixel_t * pixels
Pixel data array (width * height RGB pixels, row-major order).
One variable-length payload in the video ring buffer.
Definition ringbuffer.h:412
char * data
Owning frame payload; release with buffer_release() after reading.
Definition ringbuffer.h:424
uint32_t timestamp
Timestamp when frame was captured.
Definition ringbuffer.h:420
size_t size
Actual size of frame data in bytes.
Definition ringbuffer.h:422
char source_client_id[MAX_CLIENT_ID_LEN]
Client ID that sent this frame.
Definition ringbuffer.h:416
One captured, decoded, or rendered video frame and its timing metadata.
mutex_t swap_mutex
Brief mutex for pointer swap only.
atomic_t new_frame_available
Atomic flag: true when new frame available.
size_t allocated_buffer_size
Size of allocated data buffers (for cleanup).
video_frame_t * front_buffer
Currently displayed frame (reader owns).
When code needs to work with simple frame swap, it keeps current_frame, frame_a, frame_b,...
size_t size
Size of frame data in bytes.
uint64_t capture_timestamp_ns
Timestamp when frame was captured (nanoseconds).
void * data
Frame data pointer (points to pre-allocated buffer).
⏱️ High-precision timing utilities using sokol_time.h and uthash
🔤 UTF-8 Encoding and Decoding Utilities
asciichat_error_t image_validate_dimensions(size_t width, size_t height)
Validate image dimensions (non-zero, within limits).
Definition util/image.c:100
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