131#include <stdatomic.h>
165static atomic_int g_previous_active_video_count = 0;
178 if (frame && frame->
data) {
245static int collect_video_sources(
image_source_t *sources,
int max_sources) {
246 int source_count = 0;
261 bool is_sending_video;
266 int snapshot_count = 0;
272 if (atomic_load(&client->
client_id) == 0) {
278 client_snapshots[snapshot_count].is_active = atomic_load(&client->
active);
279 client_snapshots[snapshot_count].is_sending_video = atomic_load(&client->
is_sending_video);
285 for (
int i = 0; i < snapshot_count && source_count < max_sources; i++) {
286 client_snapshot_t *snap = &client_snapshots[i];
288 if (!snap->is_active) {
292 sources[source_count].
client_id = snap->client_id;
293 sources[source_count].
image = NULL;
298 bool got_new_frame =
false;
302 if (snap->is_sending_video && snap->video_buffer) {
311 void *frame_data_ptr = frame->
data;
313 size_t frame_size_val = frame->
size;
315 if (frame_data_ptr && frame_size_val > 0) {
321 if (!current_frame.
data) {
326 if (current_frame.
data) {
331 got_new_frame =
true;
340 if (frame_to_use && frame_to_use->
data && frame_to_use->
size >
sizeof(
uint32_t) * 2) {
348 if (img_width == 0xBEBEBEBE || img_height == 0xBEBEBEBE) {
352 " %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X", bytes[0],
353 bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7], bytes[8], bytes[9], bytes[10],
354 bytes[11], bytes[12], bytes[13], bytes[14], bytes[15]);
360 "Per-client: Invalid image dimensions from client %u: %ux%u (data may be corrupted)", snap->client_id,
361 img_width, img_height);
364 cleanup_current_frame_data(¤t_frame);
371 size_t expected_size =
sizeof(
uint32_t) * 2;
376 snap->client_id, img_width, img_height);
378 cleanup_current_frame_data(¤t_frame);
383 expected_size += rgb_size;
385 if (frame_to_use->
size != expected_size) {
387 "Per-client: Frame size mismatch from client %u: got %zu, expected %zu for %ux%u image",
388 snap->client_id, frame_to_use->
size, expected_size, img_width, img_height);
391 cleanup_current_frame_data(¤t_frame);
398 rgb_pixel_t *pixels = (rgb_pixel_t *)(frame_to_use->
data + (
sizeof(
uint32_t) * 2));
402 memcpy(img->
pixels, pixels, (
size_t)img_width * (
size_t)img_height *
sizeof(rgb_pixel_t));
403 sources[source_count].
image = img;
409 if (got_new_frame && current_frame.
data) {
410 cleanup_current_frame_data(¤t_frame);
431 unsigned short width,
unsigned short height) {
434 for (
int i = 0; i < source_count; i++) {
435 if (sources[i].has_video && sources[i].image) {
436 single_source = sources[i].
image;
441 if (!single_source) {
451 if (atomic_load(&client->
client_id) == target_client_id) {
452 target_client = client;
459 int composite_width_px, composite_height_px;
461 if (use_half_block) {
463 composite_width_px = width;
464 composite_height_px = height * 2;
468 &composite_height_px);
475 if (use_half_block) {
477 float src_aspect = (float)single_source->
w / (
float)single_source->
h;
478 float target_aspect = (float)composite_width_px / (
float)composite_height_px;
480 int fitted_width, fitted_height;
481 if (src_aspect > target_aspect) {
483 fitted_width = composite_width_px;
484 fitted_height = (int)(composite_width_px / src_aspect);
487 fitted_height = composite_height_px;
488 fitted_width = (int)(composite_height_px * src_aspect);
492 int x_offset = (composite_width_px - fitted_width) / 2;
493 int y_offset = (composite_height_px - fitted_height) / 2;
500 for (
int y = 0; y < fitted_height; y++) {
501 for (
int x = 0; x < fitted_width; x++) {
502 int src_idx = (y * fitted_width) + x;
503 int dst_x = x_offset + x;
504 int dst_y = y_offset + y;
505 int dst_idx = (dst_y * composite->
w) + dst_x;
507 if (dst_x >= 0 && dst_x < composite->w && dst_y >= 0 && dst_y < composite->h) {
543static void calculate_optimal_grid_layout(
image_source_t *sources,
int source_count,
int sources_with_video,
544 int terminal_width,
int terminal_height,
int *out_cols,
int *out_rows) {
546 if (sources_with_video == 0) {
552 if (sources_with_video == 1) {
563 float avg_aspect = 0.0f;
564 int aspect_count = 0;
565 for (
int i = 0; i < source_count; i++) {
566 if (sources[i].has_video && sources[i].image) {
567 float aspect = (float)sources[i].image->w / (
float)sources[i].
image->
h;
568 avg_aspect += aspect;
572 if (aspect_count > 0) {
573 avg_aspect /= aspect_count;
580 int best_rows = sources_with_video;
581 float best_utilization = 0.0f;
584 for (
int cols = 1; cols <= sources_with_video; cols++) {
585 int rows = (sources_with_video + cols - 1) / cols;
588 int total_cells = cols * rows;
589 int empty_cells = total_cells - sources_with_video;
590 if (empty_cells > cols) {
595 int cell_width = terminal_width / cols;
596 int cell_height = terminal_height / rows;
599 if (cell_width < 20 || cell_height < 10) {
606 float total_area_used = 0.0f;
607 int cell_area = cell_width * cell_height;
609 for (
int i = 0; i < sources_with_video; i++) {
611 float video_aspect = avg_aspect;
616 float cell_visual_aspect = (float)cell_width / ((
float)cell_height *
CHAR_ASPECT);
619 int fitted_width, fitted_height;
621 if (video_aspect > cell_visual_aspect) {
623 fitted_width = cell_width;
626 fitted_height = (int)((cell_width / video_aspect) /
CHAR_ASPECT);
629 fitted_height = cell_height;
631 fitted_width = (int)(cell_height *
CHAR_ASPECT * video_aspect);
635 if (fitted_width > cell_width) {
636 fitted_width = cell_width;
638 if (fitted_height > cell_height) {
639 fitted_height = cell_height;
643 total_area_used += fitted_width * fitted_height;
647 float total_available_area = (float)(cell_area * sources_with_video);
648 float utilization = total_area_used / total_available_area;
650 float test_cell_visual_aspect = (float)cell_width / ((
float)cell_height *
CHAR_ASPECT);
652 cell_width, cell_height, test_cell_visual_aspect, utilization * 100.0f);
655 if (utilization > best_utilization) {
656 best_utilization = utilization;
662 *out_cols = best_cols;
663 *out_rows = best_rows;
665 float terminal_visual_aspect = (float)terminal_width / ((
float)terminal_height *
CHAR_ASPECT);
666 log_info(
"Grid layout: %d clients -> %dx%d grid (%.1f%% utilization) | terminal=%dx%d (char aspect %.2f, VISUAL "
667 "aspect %.2f), video aspect: %.2f",
668 sources_with_video, best_cols, best_rows, best_utilization * 100.0f, terminal_width, terminal_height,
669 (
float)terminal_width / (
float)terminal_height, terminal_visual_aspect, avg_aspect);
683static image_t *create_multi_source_composite(
image_source_t *sources,
int source_count,
int sources_with_video,
684 uint32_t target_client_id,
unsigned short width,
unsigned short height) {
685 (void)target_client_id;
688 int grid_cols, grid_rows;
689 calculate_optimal_grid_layout(sources, source_count, sources_with_video, width, height, &grid_cols, &grid_rows);
695 const int PIXELS_PER_CHAR_HEIGHT = 2;
696 int composite_width_px = width;
697 int composite_height_px = height * PIXELS_PER_CHAR_HEIGHT;
704 int video_source_index = 0;
705 for (
int i = 0; i < source_count && video_source_index < 9; i++) {
706 if (!sources[i].image)
709 int row = video_source_index / grid_cols;
710 int col = video_source_index % grid_cols;
711 video_source_index++;
715 int cell_width_px = composite->
w / grid_cols;
716 int cell_height_px = composite->
h / grid_rows;
719 float src_aspect = (float)sources[i].image->w / (
float)sources[i].
image->
h;
720 float cell_visual_aspect = (float)cell_width_px / (
float)cell_height_px;
722 int target_width_px, target_height_px;
726 if (src_aspect > cell_visual_aspect) {
728 target_width_px = cell_width_px;
729 target_height_px = (int)((cell_width_px / src_aspect) + 0.5f);
732 target_height_px = cell_height_px;
733 target_width_px = (int)((cell_height_px * src_aspect) + 0.5f);
736 log_info(
"Cell %d: %dx%d px, video %.2f, cell %.2f → target %dx%d px (fill %s)", video_source_index - 1,
737 cell_width_px, cell_height_px, src_aspect, cell_visual_aspect, target_width_px, target_height_px,
738 (src_aspect > cell_visual_aspect) ?
"WIDTH" :
"HEIGHT");
745 int cell_x_offset_px = col * cell_width_px;
746 int cell_y_offset_px = row * cell_height_px;
754 int x_padding_px, y_padding_px;
758 x_padding_px = (cell_width_px - target_width_px) / 2;
759 y_padding_px = (cell_height_px - target_height_px) / 2;
762 int cell_x_min = cell_x_offset_px;
763 int cell_x_max = cell_x_offset_px + cell_width_px - 1;
764 int cell_y_min = cell_y_offset_px;
765 int cell_y_max = cell_y_offset_px + cell_height_px - 1;
770 for (
int y = 0; y < resized->
h; y++) {
771 for (
int x = 0; x < resized->
w; x++) {
773 int dst_x = cell_x_offset_px + x_padding_px + x;
774 int dst_y = cell_y_offset_px + y_padding_px + y;
777 if (dst_x < cell_x_min || dst_x > cell_x_max || dst_y < cell_y_min || dst_y > cell_y_max) {
782 if (dst_x < 0 || dst_x >= composite->
w || dst_y < 0 || dst_y >= composite->
h) {
787 int src_idx = (y * resized->
w) + x;
788 int dst_idx = (dst_y * composite->
w) + dst_x;
808static char *convert_composite_to_ascii(
image_t *composite,
uint32_t target_client_id,
unsigned short width,
809 unsigned short height) {
821 if (atomic_load(&client->
client_id) == target_client_id) {
822 render_client = client;
827 if (!render_client) {
835 if (!has_terminal_caps_snapshot) {
960 bool wants_stretch,
size_t *out_size,
bool *out_grid_changed,
961 int *out_sources_count) {
965 if (out_grid_changed) {
966 *out_grid_changed =
false;
968 if (out_sources_count) {
969 *out_sources_count = 0;
972 if (!out_size || width == 0 || height == 0) {
974 "Invalid parameters for create_mixed_ascii_frame_for_client: width=%u, height=%u, out_size=%p", width,
981 int source_count = collect_video_sources(sources,
MAX_CLIENTS);
984 int sources_with_video = 0;
985 for (
int i = 0; i < source_count; i++) {
986 if (sources[i].has_video && sources[i].image) {
987 sources_with_video++;
992 if (out_sources_count) {
993 *out_sources_count = sources_with_video;
1001 int previous_count = atomic_load(&g_previous_active_video_count);
1002 if (sources_with_video != previous_count) {
1004 if (atomic_compare_exchange_strong(&g_previous_active_video_count, &previous_count, sources_with_video)) {
1006 "Grid layout changing: %d -> %d active video sources - caller will broadcast clear AFTER buffering frame",
1007 previous_count, sources_with_video);
1008 if (out_grid_changed) {
1009 *out_grid_changed =
true;
1017 if (sources_with_video == 0) {
1024 if (sources_with_video == 1) {
1026 composite = create_single_source_composite(sources, source_count, target_client_id, width, height);
1030 create_multi_source_composite(sources, source_count, sources_with_video, target_client_id, width, height);
1042 char *ascii_frame = convert_composite_to_ascii(composite, target_client_id, width, height);
1045 *out_size = strlen(ascii_frame);
1055 for (
int i = 0; i < source_count; i++) {
1056 if (sources[i].image) {
1173 if (!client || !client->
audio_queue || !audio_data || data_size == 0) {
1201 if (atomic_load(&client->
client_id) == 0) {
1206 bool is_active = atomic_load(&client->
active);
1209 if (is_active && is_sending) {
🖼️ ASCII Art Conversion and Output Interface
📐 Aspect Ratio Calculation Functions
🗃️ Lock-Free Unified Memory Buffer Pool with Lazy Allocation
🔄 Network byte order conversion helpers
#define NET_TO_HOST_U32(val)
buffer_pool_t * buffer_pool_get_global(void)
void buffer_pool_free(buffer_pool_t *pool, void *data, size_t size)
Free a buffer back to the pool (lock-free)
void * buffer_pool_alloc(buffer_pool_t *pool, size_t size)
Allocate a buffer from the pool (lock-free fast path)
#define SAFE_MALLOC_ALIGNED(size, alignment, cast)
#define read_u32_unaligned
#define SET_ERRNO(code, context_msg,...)
Set error code with custom context message and log it.
#define MAX_CLIENTS
Maximum possible clients (static array size) - actual runtime limit set by –max-clients (1-32)
#define LOG_RATE_NORMAL
Log rate limit: 3 seconds (3,000,000 microseconds)
#define log_debug_every(interval_us, fmt,...)
Rate-limited DEBUG logging.
#define log_info(...)
Log an INFO message.
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.
@ PACKET_TYPE_AUDIO
Single audio packet (legacy)
void calculate_fit_dimensions_pixel(int img_width, int img_height, int max_width, int max_height, int *out_width, int *out_height)
Calculate fit dimensions for pixel-based images.
const video_frame_t * video_frame_get_latest(video_frame_buffer_t *vfb)
Reader API: Get latest frame if available.
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 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.
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, const char luminance_palette[256])
Convert image to ASCII art with terminal capability awareness.
🔢 Mathematical Utility Functions
📬 Thread-safe packet queue system for per-client send threads
Lock-Free Ring Buffer and Frame Buffer Management.
atomic_bool g_server_should_exit
Global shutdown flag from main.c.
Per-client state management and lifecycle orchestration.
bool any_clients_sending_video(void)
Check if any connected clients are currently sending video.
rwlock_t g_client_manager_rwlock
Reader-writer lock protecting the global client manager.
client_manager_t g_client_manager
Global client manager singleton - central coordination point.
char * create_mixed_ascii_frame_for_client(uint32_t 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.
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.
Multi-client video mixing and ASCII frame generation.
Unified buffer pool with lock-free fast path.
Per-client state structure for server-side client management.
terminal_capabilities_t terminal_caps
char client_palette_chars[256]
video_frame_buffer_t * incoming_video_buffer
bool client_palette_initialized
packet_queue_t * audio_queue
char client_luminance_palette[256]
atomic_bool is_sending_video
Global client manager structure for server-side client coordination.
client_info_t clients[MAX_CLIENTS]
Array of client_info_t structures (backing storage)
Image source structure for multi-client video mixing.
uint32_t client_id
Unique client identifier for this source.
bool has_video
Whether this client has active video stream.
image_t * image
Pointer to client's current video frame (owned by buffer system)
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)
Multi-source frame structure for multi-user support.
char * data
Pointer to frame data (not owned by this struct)
uint32_t timestamp
Timestamp when frame was captured.
uint32_t source_client_id
Client ID that sent this frame.
size_t size
Actual size of frame data in bytes.
Complete terminal capabilities structure.
render_mode_t render_mode
Preferred rendering mode (render_mode_t)
Video frame buffer manager.
uint64_t capture_timestamp_us
Timestamp when frame was captured (microseconds)
size_t size
Size of frame data in bytes.
void * data
Frame data pointer (points to pre-allocated buffer)
⏱️ High-precision timing utilities using sokol_time.h and uthash
asciichat_error_t image_validate_dimensions(size_t width, size_t height)
Validate image dimensions (non-zero, within limits)
asciichat_error_t image_calc_rgb_size(size_t width, size_t height, size_t *out_size)
Calculate total RGB buffer size from dimensions.
🖼️ Safe overflow-checked buffer size calculations for images and video frames
Image Data Structures and Operations.