ascii-chat 0.8.38
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches
video/simd/common.c
Go to the documentation of this file.
1
7#include <ascii-chat/common.h>
8#include <ascii-chat/video/simd/common.h>
9#include <ascii-chat/uthash/uthash.h>
10#include <ascii-chat/video/palette.h>
11#include <ascii-chat/util/fnv1a.h>
12#include <ascii-chat/util/time.h>
13#include <ascii-chat/platform/init.h>
14#include <time.h>
15#include <math.h>
16#include <stdatomic.h>
17
18// Include SIMD architecture headers for cleanup functions
19// Note: Only ONE SIMD implementation is compiled based on highest available instruction set
20#if SIMD_SUPPORT_NEON
21#include <ascii-chat/video/simd/neon.h>
22#elif SIMD_SUPPORT_AVX2
23#include <ascii-chat/video/simd/avx2.h>
24#elif SIMD_SUPPORT_SSSE3
25#include <ascii-chat/video/simd/ssse3.h>
26#elif SIMD_SUPPORT_SSE2
27#include <ascii-chat/video/simd/sse2.h>
28#elif SIMD_SUPPORT_SVE
29#include <ascii-chat/video/simd/sve.h>
30#endif
31
32// Build 64-entry glyph LUT for vqtbl4q_u8 and other architecture's instrinsics (UTF-8 aware)
33void build_ramp64(uint8_t ramp64[RAMP64_SIZE], const char *ascii_chars) {
34 if (!ascii_chars) {
35 // Fallback to space character
36 for (int i = 0; i < RAMP64_SIZE; i++) {
37 ramp64[i] = ' ';
38 }
39 return;
40 }
41
42 // Use UTF-8 palette functions for proper character handling
43 utf8_palette_t *utf8_pal = utf8_palette_create(ascii_chars);
44 if (!utf8_pal) {
45 // Fallback to space character
46 for (int i = 0; i < RAMP64_SIZE; i++) {
47 ramp64[i] = ' ';
48 }
49 return;
50 }
51
52 size_t char_count = utf8_palette_get_char_count(utf8_pal);
53 if (char_count == 0) {
54 // No valid characters found, use space
55 for (int i = 0; i < RAMP64_SIZE; i++) {
56 ramp64[i] = ' ';
57 }
58 utf8_palette_destroy(utf8_pal);
59 return;
60 }
61
62 // Build the ramp64 lookup using UTF-8 character indices
63 for (int i = 0; i < RAMP64_SIZE; i++) {
64 // Map 0-63 to 0-(char_count-1) using proper character indexing
65 size_t char_idx = (i * (char_count - 1) + (RAMP64_SIZE - 1) / 2) / (RAMP64_SIZE - 1);
66 if (char_idx >= char_count) {
67 char_idx = char_count - 1;
68 }
69
70 // Get the first byte of the character at this character index
71 const utf8_char_info_t *char_info = utf8_palette_get_char(utf8_pal, char_idx);
72 if (char_info) {
73 ramp64[i] = (uint8_t)char_info->bytes[0];
74 } else {
75 ramp64[i] = ' '; // Fallback
76 }
77 }
78
79 utf8_palette_destroy(utf8_pal);
80}
81
82// Cache eviction helper functions
83double calculate_cache_eviction_score(uint64_t last_access_time, uint32_t access_count, uint64_t creation_time,
84 uint64_t current_time) {
85 // Protect against unsigned underflow if times are inconsistent (clock adjustments, etc.)
86 uint64_t age_ns = (current_time >= last_access_time) ? (current_time - last_access_time) : 0;
87 uint64_t total_age_ns = (current_time >= creation_time) ? (current_time - creation_time) : 0;
88
89 uint64_t age_seconds = age_ns / NS_PER_SEC_INT;
90 uint64_t total_age_seconds = total_age_ns / NS_PER_SEC_INT;
91
92 // Frequency factor: high-use palettes get protection (logarithmic scaling)
93 double frequency_factor = 1.0 + log10(1.0 + access_count);
94
95 // Aging factor: frequency bonus decays over time (5-minute half-life)
96 double aging_factor = exp(-(double)age_seconds / CACHE_FREQUENCY_DECAY_TIME);
97
98 // Recent access bonus: strong protection for recently used (1-minute protection)
99 double recency_bonus = exp(-(double)age_seconds / CACHE_RECENCY_SCALE);
100
101 // Cache lifetime penalty: prevent immortal caches (1-hour max lifetime)
102 double lifetime_penalty = total_age_seconds > CACHE_MAX_LIFETIME ? 0.5 : 1.0;
103
104 // Final score: higher = keep longer
105 return (frequency_factor * aging_factor + recency_bonus) * lifetime_penalty;
106}
107
108// Fast palette string hashing using shared FNV-1a hash function
109static inline uint32_t hash_palette_string(const char *palette) {
110 return fnv1a_hash_string(palette);
111}
112
113// Forward declarations for eviction functions (defined after globals)
114static bool try_insert_with_eviction_utf8(uint32_t hash, utf8_palette_cache_t *new_cache);
115
116// UTF-8 palette cache system with min-heap eviction
117static utf8_palette_cache_t *g_utf8_cache_table = NULL; // uthash uses structure pointer as head
118static rwlock_t g_utf8_cache_rwlock = {0};
119static _Atomic(bool) g_utf8_cache_initialized = false;
120
121// Min-heap for O(log n) intelligent eviction
122static utf8_palette_cache_t **g_utf8_heap = NULL; // Min-heap array
123static size_t g_utf8_heap_size = 0; // Current entries in heap
124static const size_t g_utf8_heap_capacity = 2048; // Max heap size (matching uthash capacity)
125
126// char_index_ramp_cache removed - data is already stored in utf8_palette_cache_t.char_index_ramp[64]
127
128// Initialize UTF-8 cache system with min-heap (thread-safe)
129static void init_utf8_cache_system(void) {
130 // Fast path: already initialized
131 if (atomic_load(&g_utf8_cache_initialized)) {
132 return;
133 }
134
135 // Slow path: need to initialize
136 // Use static_mutex_t to avoid bootstrap problem (can't protect init mutex with itself)
137 static static_mutex_t init_bootstrap_mutex = STATIC_MUTEX_INIT;
138
139 static_mutex_lock(&init_bootstrap_mutex);
140
141 // Double-check after acquiring lock (another thread may have initialized while we waited)
142 if (!atomic_load(&g_utf8_cache_initialized)) {
143 // Initialize the cache rwlock
144 rwlock_init(&g_utf8_cache_rwlock);
145
146 // Initialize uthash head to NULL (required)
147 g_utf8_cache_table = NULL;
148 g_utf8_heap = SAFE_MALLOC(g_utf8_heap_capacity * sizeof(utf8_palette_cache_t *), utf8_palette_cache_t **);
149 g_utf8_heap_size = 0;
150
151 // Mark as initialized
152 atomic_store(&g_utf8_cache_initialized, true);
153 }
154
155 static_mutex_unlock(&init_bootstrap_mutex);
156}
157
158// Min-heap management functions for UTF-8 cache
159static void utf8_heap_swap(size_t i, size_t j) {
160 utf8_palette_cache_t *temp = g_utf8_heap[i];
161 g_utf8_heap[i] = g_utf8_heap[j];
162 g_utf8_heap[j] = temp;
163
164 // Update heap indices in cache objects
165 g_utf8_heap[i]->heap_index = i;
166 g_utf8_heap[j]->heap_index = j;
167}
168
169static void utf8_heap_bubble_up(size_t index) {
170 while (index > 0) {
171 size_t parent = (index - 1) / 2;
172 if (g_utf8_heap[index]->cached_score >= g_utf8_heap[parent]->cached_score)
173 break;
174 utf8_heap_swap(index, parent);
175 index = parent;
176 }
177}
178
179static void utf8_heap_bubble_down(size_t index) {
180 while (true) {
181 size_t left = 2 * index + 1;
182 size_t right = 2 * index + 2;
183 size_t smallest = index;
184
185 if (left < g_utf8_heap_size && g_utf8_heap[left]->cached_score < g_utf8_heap[smallest]->cached_score) {
186 smallest = left;
187 }
188 if (right < g_utf8_heap_size && g_utf8_heap[right]->cached_score < g_utf8_heap[smallest]->cached_score) {
189 smallest = right;
190 }
191
192 if (smallest == index)
193 break;
194 utf8_heap_swap(index, smallest);
195 index = smallest;
196 }
197}
198
199static void utf8_heap_insert(utf8_palette_cache_t *cache, double score) {
200 if (g_utf8_heap_size >= g_utf8_heap_capacity) {
201 log_error("UTF8_HEAP: Heap capacity exceeded");
202 return;
203 }
204
205 cache->cached_score = score;
206 cache->heap_index = g_utf8_heap_size;
207 g_utf8_heap[g_utf8_heap_size] = cache;
208 g_utf8_heap_size++;
209
210 utf8_heap_bubble_up(cache->heap_index);
211}
212
213static utf8_palette_cache_t *utf8_heap_extract_min(void) {
214 if (g_utf8_heap_size == 0) {
215 return NULL;
216 }
217
218 utf8_palette_cache_t *min_cache = g_utf8_heap[0];
219
220 // Move last element to root and bubble down
221 g_utf8_heap_size--;
222 if (g_utf8_heap_size > 0) {
223 g_utf8_heap[0] = g_utf8_heap[g_utf8_heap_size];
224 g_utf8_heap[0]->heap_index = 0;
225 utf8_heap_bubble_down(0);
226 }
227
228 return min_cache;
229}
230
231static void utf8_heap_update_score(utf8_palette_cache_t *cache, double new_score) {
232 double old_score = cache->cached_score;
233 cache->cached_score = new_score;
234
235 if (new_score < old_score) {
236 utf8_heap_bubble_up(cache->heap_index);
237 } else {
238 utf8_heap_bubble_down(cache->heap_index);
239 }
240}
241
242// Min-heap functions now replace LRU list management
243
244// Thread-safe cache eviction implementations
245static bool try_insert_with_eviction_utf8(uint32_t hash, utf8_palette_cache_t *new_cache) {
246 // Already holding write lock
247 // Note: key should already be set by caller, but ensure it's set
248 new_cache->key = hash;
249
250 // Check if hash table is near capacity and evict proactively (80% threshold)
251 size_t entry_count = HASH_COUNT(g_utf8_cache_table);
252 if (entry_count >= g_utf8_heap_capacity) {
253 // Proactive eviction: free space before attempting insertion
254 utf8_palette_cache_t *victim_cache = utf8_heap_extract_min();
255 if (victim_cache) {
256 uint32_t victim_key = victim_cache->key;
257
258 // Log clean eviction
259 uint32_t victim_access_count = atomic_load(&victim_cache->access_count);
260 uint64_t current_time = time_get_ns();
261 uint64_t victim_age = (current_time - atomic_load(&victim_cache->last_access_time)) / NS_PER_SEC_INT;
262
263 log_debug("UTF8_CACHE_EVICTION: Proactive min-heap eviction hash=0x%x (age=%lus, count=%u)", victim_key,
264 victim_age, victim_access_count);
265
266 HASH_DEL(g_utf8_cache_table, victim_cache);
267 SAFE_FREE(victim_cache);
268 }
269 }
270
271 // Now attempt insertion (should succeed with freed space)
272 // Note: uthash doesn't have a failure path for insertion, so we handle eviction proactively
273 HASH_ADD_INT(g_utf8_cache_table, key, new_cache);
274
275 // Success: add to min-heap
276 uint64_t current_time = time_get_ns();
277 double initial_score = calculate_cache_eviction_score(current_time, 1, current_time, current_time);
278 utf8_heap_insert(new_cache, initial_score);
279 return true;
280}
281
282// char_ramp_cache functions removed - data already available in utf8_palette_cache_t
283
284// Get or create UTF-8 palette cache for a given palette
285utf8_palette_cache_t *get_utf8_palette_cache(const char *ascii_chars) {
286 if (!ascii_chars)
287 return NULL;
288
289 // Check for empty string
290 if (ascii_chars[0] == '\0')
291 return NULL;
292
293 // Create hash of palette for cache key
294 uint32_t palette_hash = hash_palette_string(ascii_chars);
295
296 // Ensure the cache system is initialized
297 init_utf8_cache_system();
298
299 // First try: read lock for cache lookup (allows multiple concurrent readers)
300 rwlock_rdlock(&g_utf8_cache_rwlock);
301
302 // Check if cache exists
303 utf8_palette_cache_t *cache = NULL;
304 HASH_FIND_INT(g_utf8_cache_table, &palette_hash, cache);
305 if (cache) {
306 // Cache hit: Update access tracking (atomics are thread-safe under rdlock)
307 uint64_t current_time = time_get_ns();
308 atomic_store(&cache->last_access_time, current_time);
309 uint32_t new_access_count = atomic_fetch_add(&cache->access_count, 1) + 1;
310
311 // Every 10th access: Update heap position (requires write lock)
312 if (new_access_count % 10 == 0) {
313 // Save the key before releasing read lock
314 uint32_t saved_key = cache->key;
315
316 // Release read lock and upgrade to write lock
317 rwlock_rdunlock(&g_utf8_cache_rwlock);
318 rwlock_wrlock(&g_utf8_cache_rwlock);
319
320 // Re-lookup cache entry after lock upgrade
321 // Another thread could have evicted this entry while we were upgrading locks
322 utf8_palette_cache_t *cache_relookup = NULL;
323 HASH_FIND_INT(g_utf8_cache_table, &saved_key, cache_relookup);
324 if (cache_relookup) {
325 // Cache entry still exists - safe to update heap position
326 uint64_t last_access = atomic_load(&cache_relookup->last_access_time);
327 uint32_t access_count = atomic_load(&cache_relookup->access_count);
328 double new_score =
329 calculate_cache_eviction_score(last_access, access_count, cache_relookup->creation_time, current_time);
330 utf8_heap_update_score(cache_relookup, new_score);
331 }
332 // If cache_relookup is NULL, entry was evicted - skip update (not an error)
333
334 rwlock_wrunlock(&g_utf8_cache_rwlock);
335 } else {
336 rwlock_rdunlock(&g_utf8_cache_rwlock);
337 }
338
339 return cache;
340 }
341
342 // Cache miss: Need to create new entry
343 // Release read lock and acquire write lock
344 rwlock_rdunlock(&g_utf8_cache_rwlock);
345 rwlock_wrlock(&g_utf8_cache_rwlock);
346
347 // Double-check: another thread might have created it while we upgraded locks
348 cache = NULL;
349 HASH_FIND_INT(g_utf8_cache_table, &palette_hash, cache);
350 if (cache) {
351 // Found it! Just update access tracking and return
352 uint64_t current_time = time_get_ns();
353 atomic_store(&cache->last_access_time, current_time);
354 atomic_fetch_add(&cache->access_count, 1);
355 rwlock_wrunlock(&g_utf8_cache_rwlock);
356 return cache;
357 }
358
359 // Create new cache entry (holding write lock)
360 cache = SAFE_MALLOC(sizeof(utf8_palette_cache_t), utf8_palette_cache_t *);
361 if (!cache) {
362 rwlock_wrunlock(&g_utf8_cache_rwlock);
363 return NULL;
364 }
365 memset(cache, 0, sizeof(utf8_palette_cache_t));
366
367 // Set the key for uthash
368 cache->key = palette_hash;
369
370 // Build both cache types
371 build_utf8_luminance_cache(ascii_chars, cache->cache);
372 build_utf8_ramp64_cache(ascii_chars, cache->cache64, cache->char_index_ramp);
373
374 // Store palette hash for validation
375 SAFE_STRNCPY(cache->palette_hash, ascii_chars, sizeof(cache->palette_hash));
376 cache->is_valid = true;
377
378 // Initialize eviction tracking
379 uint64_t current_time = time_get_ns();
380 atomic_store(&cache->last_access_time, current_time);
381 atomic_store(&cache->access_count, 1); // First access
382 cache->creation_time = current_time;
383
384 // Store in hash table with guaranteed eviction support
385 try_insert_with_eviction_utf8(palette_hash, cache);
386
387 log_debug("UTF8_CACHE: Created new cache for palette='%s' (hash=0x%x)", ascii_chars, palette_hash);
388
389 rwlock_wrunlock(&g_utf8_cache_rwlock);
390 return cache;
391}
392
393// Build 256-entry UTF-8 cache for direct luminance lookup (monochrome renderers)
394void build_utf8_luminance_cache(const char *ascii_chars, utf8_char_t cache[256]) {
395 if (!ascii_chars || !cache)
396 return;
397
398 // Parse characters
399 typedef struct {
400 const char *start;
401 int byte_len;
402 } char_info_t;
403
404 char_info_t char_infos[256];
405 int char_count = 0;
406 const char *p = ascii_chars;
407
408 while (*p && char_count < 255) {
409 char_infos[char_count].start = p;
410
411 if ((*p & 0xE0) == 0xC0) {
412 char_infos[char_count].byte_len = 2;
413 p += 2;
414 } else if ((*p & 0xF0) == 0xE0) {
415 char_infos[char_count].byte_len = 3;
416 p += 3;
417 } else if ((*p & 0xF8) == 0xF0) {
418 char_infos[char_count].byte_len = 4;
419 p += 4;
420 } else {
421 // ASCII characters (0x00-0x7F) and invalid sequences: treat as single byte
422 char_infos[char_count].byte_len = 1;
423 p++;
424 }
425 char_count++;
426 }
427
428 // Handle empty string case
429 if (char_count == 0)
430 return;
431
432 // Build 256-entry cache
433 for (int i = 0; i < 256; i++) {
434 int char_idx = char_count > 1 ? (i * (char_count - 1) + 127) / 255 : 0;
435 if (char_idx >= char_count)
436 char_idx = char_count - 1;
437
438 cache[i].byte_len = char_infos[char_idx].byte_len;
439 memcpy(cache[i].utf8_bytes, char_infos[char_idx].start, char_infos[char_idx].byte_len);
440 if (cache[i].byte_len < 4) {
441 cache[i].utf8_bytes[cache[i].byte_len] = '\0';
442 }
443 }
444}
445
446// Build 64-entry UTF-8 cache for SIMD color lookup
447void build_utf8_ramp64_cache(const char *ascii_chars, utf8_char_t cache64[64], uint8_t char_index_ramp[64]) {
448 if (!ascii_chars || !cache64 || !char_index_ramp)
449 return;
450
451 // Reuse the luminance cache building logic but for 64 entries
452 // (Same UTF-8 parsing as above, but map to 64 entries instead of 256)
453
454 // Parse characters (same as above)
455 typedef struct {
456 const char *start;
457 int byte_len;
458 } char_info_t;
459
460 char_info_t char_infos[256];
461 int char_count = 0;
462 const char *p = ascii_chars;
463
464 while (*p && char_count < 255) {
465 char_infos[char_count].start = p;
466
467 if ((*p & 0xE0) == 0xC0) {
468 char_infos[char_count].byte_len = 2;
469 p += 2;
470 } else if ((*p & 0xF0) == 0xE0) {
471 char_infos[char_count].byte_len = 3;
472 p += 3;
473 } else if ((*p & 0xF8) == 0xF0) {
474 char_infos[char_count].byte_len = 4;
475 p += 4;
476 } else {
477 // ASCII characters (0x00-0x7F) and invalid sequences: treat as single byte
478 char_infos[char_count].byte_len = 1;
479 p++;
480 }
481 char_count++;
482 }
483
484 // Handle empty string case
485 if (char_count == 0)
486 return;
487
488 // Build 64-entry cache and index ramp
489 for (int i = 0; i < 64; i++) {
490 int char_idx = char_count > 1 ? (i * (char_count - 1) + 31) / 63 : 0;
491 if (char_idx >= char_count)
492 char_idx = char_count - 1;
493
494 // Store character index for SIMD lookup
495 char_index_ramp[i] = (uint8_t)char_idx;
496
497 // Cache UTF-8 character
498 cache64[i].byte_len = char_infos[char_idx].byte_len;
499 memcpy(cache64[i].utf8_bytes, char_infos[char_idx].start, char_infos[char_idx].byte_len);
500 if (cache64[i].byte_len < 4) {
501 cache64[i].utf8_bytes[cache64[i].byte_len] = '\0';
502 }
503 }
504}
505
506// char_index_ramp_cache functions removed - data already available in utf8_palette_cache_t.char_index_ramp[64]
507
508// No callback needed - uthash iteration handles cleanup directly
509
510// Central cleanup function for all SIMD caches
512 log_dev("SIMD_CACHE: Starting cleanup of all SIMD caches");
513
514 // Only destroy caches if they were ever initialized
515 if (atomic_load(&g_utf8_cache_initialized)) {
516 // Destroy shared UTF-8 palette cache (write lock for cleanup)
517 rwlock_wrlock(&g_utf8_cache_rwlock);
518 if (g_utf8_cache_table) {
519 // Free all UTF-8 cache entries using HASH_ITER
520 utf8_palette_cache_t *cache, *tmp;
521 HASH_ITER(hh, g_utf8_cache_table, cache, tmp) {
522 HASH_DEL(g_utf8_cache_table, cache);
523 SAFE_FREE(cache);
524 }
525 g_utf8_cache_table = NULL;
526 log_debug("UTF8_CACHE: Destroyed shared UTF-8 palette cache");
527 }
528 // Clean up heap arrays
529 if (g_utf8_heap) {
530 SAFE_FREE(g_utf8_heap);
531 g_utf8_heap = NULL;
532 g_utf8_heap_size = 0;
533 }
534 // Reset initialization flag so system can be reinitialized
535 atomic_store(&g_utf8_cache_initialized, false);
536 rwlock_wrunlock(&g_utf8_cache_rwlock);
537 }
538
539 // Call architecture-specific cache cleanup functions
540 // Note: Only ONE SIMD implementation is compiled based on highest available instruction set
541 // Higher instruction sets (AVX2, SSSE3) handle cleanup for lower ones (SSE2)
542#if SIMD_SUPPORT_NEON
543 neon_caches_destroy();
544#elif SIMD_SUPPORT_AVX2
545 avx2_caches_destroy();
546#elif SIMD_SUPPORT_SSSE3
547 ssse3_caches_destroy();
548#elif SIMD_SUPPORT_SSE2
549 sse2_caches_destroy();
550#elif SIMD_SUPPORT_SVE
551 sve_caches_destroy();
552#endif
553
554 log_dev("SIMD_CACHE: All SIMD caches destroyed");
555}
556
557// Output buffer functions moved to lib/video/output_buffer.c
void utf8_palette_destroy(utf8_palette_t *palette)
Definition palette.c:485
size_t utf8_palette_get_char_count(const utf8_palette_t *palette)
Definition palette.c:502
const utf8_char_info_t * utf8_palette_get_char(const utf8_palette_t *palette, size_t index)
Definition palette.c:494
utf8_palette_t * utf8_palette_create(const char *palette_string)
Definition palette.c:363
int rwlock_init(rwlock_t *rwlock)
Definition threading.c:63
uint64_t time_get_ns(void)
Definition util/time.c:48
void build_ramp64(uint8_t ramp64[RAMP64_SIZE], const char *ascii_chars)
void build_utf8_luminance_cache(const char *ascii_chars, utf8_char_t cache[256])
double calculate_cache_eviction_score(uint64_t last_access_time, uint32_t access_count, uint64_t creation_time, uint64_t current_time)
void simd_caches_destroy_all(void)
void build_utf8_ramp64_cache(const char *ascii_chars, utf8_char_t cache64[64], uint8_t char_index_ramp[64])
utf8_palette_cache_t * get_utf8_palette_cache(const char *ascii_chars)