diff --git a/C/Makefile b/C/Makefile index f752387..18622b3 100644 --- a/C/Makefile +++ b/C/Makefile @@ -26,23 +26,21 @@ FILTERDIR := filters BUILDDIR := build OBJDIR := $(BUILDDIR)/obj -CFLAGS_COMMON := -Wall -Wextra -O2 -Iinclude -I$(LIBDIR) -fno-stack-protector -fno-builtin-memcpy -fno-builtin-memset -fno-builtin-strlen -CFLAGS_COMMON += +CFLAGS_COMMON := -Wall -Wextra -O2 -Iinclude -I$(LIBDIR) -fno-stack-protector -fno-builtin -ffreestanding ifeq ($(PLATFORM),linux) # Linux: nolibc, V4L2, SSE4.1 CFLAGS := $(CFLAGS_COMMON) -DPLATFORM_LINUX -D__LINUX_NOLIBC__ CFLAGS += -msse4.1 - LDFLAGS := -nostdlib + LDFLAGS := -nostdlib -nostdinc LDFLAGS += -Wl,--no-as-needed LDFLAGS += -Wl,-dynamic-linker,/lib64/ld-linux-x86-64.so.2 - LDFLAGS += $(shell $(CC) --print-file-name=crti.o) - LDFLAGS += $(shell $(CC) --print-file-name=crtn.o) LDFLAGS += -L/usr/lib/x86_64-linux-gnu # TEST: Explicit library paths # LDFLAGS += -Wl,-rpath-link=/usr/lib/x86_64-linux-gnu - LDFLAGS += -ldl -lpthread -lc - # LDFLAGS += -ldl -lpthread + LDFLAGS += -ldl -lpthread -lc # FIX: lc flag pulls standard libc,but removing them will -lpthread and -ldl will fail to link because they themselves depend on libc + # TODO: custom threading library using clone() + futex to eliminate -lpthread (pthread functions) dependency + # TODO: implement an ELF loader (or statically link plugins) to eliminate -ldl (dlopen/dlsym/dlclose are part of libdl.so also glibc) dependency LDFLAGS += -msse4.1 LIBSRCS := $(LIBDIR)/nl_alloc.c \ diff --git a/C/include/ascii.h b/C/include/ascii.h index 2281e91..9c14f49 100644 --- a/C/include/ascii.h +++ b/C/include/ascii.h @@ -5,24 +5,84 @@ #include #define ASCII_CHARS_DEFAULT " .:-=+*#%@" +#define MAX_CHARSETS 16 +#define CHARSET_NAME_LEN 32 +#define CHARSET_RAMP_LEN 128 + +// Render mode +typedef enum { + RENDER_BRAILLE = 0, + RENDER_BLOCKS, + RENDER_ASCII_RAMP, + RENDER_HALF_BLOCK, + RENDER_DOTS, + RENDER_MODE_COUNT +} render_mode_t; + +// Edge detection mode +typedef enum { + EDGE_OFF = 0, + EDGE_SOBEL, + EDGE_SOBEL_DIR, + EDGE_LAPLACIAN, + EDGE_MODE_COUNT +} edge_mode_t; + +typedef struct { + char name[CHARSET_NAME_LEN]; + char ramp[CHARSET_RAMP_LEN]; +} charset_entry_t; + +// Registry of loaded charsets, hot-reloadable from a directory of .txt files. +typedef struct { + charset_entry_t sets[MAX_CHARSETS]; + int count; + int active; + int inotify_fd; + int inotify_wd; + char dir_path[256]; +} charset_registry_t; + +int charset_registry_init(charset_registry_t *reg, const char *dir); +void charset_registry_scan( + charset_registry_t *reg); /* (re)loads all files in dir */ +void charset_registry_check_reload( + charset_registry_t *reg); /* inotify poll, call once per frame */ +void charset_registry_cleanup(charset_registry_t *reg); +const char *charset_registry_active_ramp(const charset_registry_t *reg); typedef struct { int brightness; /* additive offset: -128..128 */ int contrast; /* percent, 100 = no change */ int invert; /* flip brightness->charset mapping */ int color; /* ANSI truecolor output */ - int edges; /* Sobel edge detection */ + edge_mode_t edges; /* edge detection mode */ int dither; /* Floyd-Steinberg dithering */ - const char *charset; /* NULL = ASCII_CHARS_DEFAULT */ + int threshold_val; /* Binarization limit */ + const char *charset; /* NULL = ASCII_CHARS_DEFAULT; active ramp for + RENDER_ASCII_RAMP */ + render_mode_t render_mode; + + // TESTING: + /* Pseudo-3D "pop out" parallax effect. + * depth_pop: 0 = off, >0 = effect strength (1..100 suggested range) + * depth_invert: 0 = brighter is "closer" (pops toward viewer), 1 = darker is + * closer + */ + int depth_pop; + int depth_invert; } ascii_opts_t; // Convert YUYV raw data to grayscale -// void yuyv_to_gray(const uint8_t *yuyv, uint8_t *gray, int width, int height); -void yuyv_to_gray_simd(const uint8_t *yuyv, uint8_t *gray, int width, int height); +void yuyv_to_gray(const uint8_t *yuyv, uint8_t *gray, int width, int height); +void yuyv_to_gray_simd(const uint8_t *yuyv, uint8_t *gray, int width, + int height); void yuyv_to_rgb(const uint8_t *yuyv, uint8_t *rgb, int width, int height); // Output buffer sizing size_t ascii_out_size(int dst_w, int dst_h, int color); +size_t ascii_out_size_for_mode(int dst_w, int dst_h, int color, + render_mode_t mode); // grayscale to ASCII output grid int grayscale_to_ascii(const uint8_t *gray, const uint8_t *rgb, int src_w, @@ -32,4 +92,8 @@ int grayscale_to_ascii(const uint8_t *gray, const uint8_t *rgb, int src_w, // Overlay FPS box void overlay_fps_box(int dst_w, double fps, int color_enabled); +// Render mode name for UI display +const char *render_mode_name(render_mode_t m); +const char *edge_mode_name(edge_mode_t m); + #endif diff --git a/C/lib/nl_io.h b/C/lib/nl_io.h index 087bf97..0a0d56a 100644 --- a/C/lib/nl_io.h +++ b/C/lib/nl_io.h @@ -36,7 +36,7 @@ static inline int nl_ioctl(int fd, unsigned long req, void *arg) { #define stderr 2 #define fprintf(fd, fmt, ...) \ do { \ - char _fb[1024]; \ + char _fb[4096]; \ int _fn = nl_snprintf(_fb, sizeof(_fb), fmt, ##__VA_ARGS__); \ if (_fn > 0) { \ size_t _nw = \ diff --git a/C/src/ascii.c b/C/src/ascii.c index 1fbe3d8..d6a1ddf 100644 --- a/C/src/ascii.c +++ b/C/src/ascii.c @@ -3,18 +3,36 @@ // - x86_64 Linux/macOS: SSE2 via // - ARM64 macOS: NEON via -#include "ascii.h" -#include - #include "nolibc.h" + +#include "ascii.h" #include "platform.h" +#include +#include + // Helpers static inline uint8_t clamp_u8(int v) { return (v < 0) ? 0 : (v > 255) ? 255 : (uint8_t)v; } static inline int my_abs(int x) { return x < 0 ? -x : x; } +static inline double my_sqrt(double x) { + if (x <= 0.0) + return 0.0; + double guess = x; + // initial halve the exponent by repeated division + while (guess > 1.0) { + guess /= 2.0; + } + if (guess <= 0.0) + guess = 1.0; + for (int i = 0; i < 12; i++) { + guess = 0.5 * (guess + x / guess); + } + return guess; +} + // YUYV to grayscale #if defined(ARCH_X86_64) #include @@ -77,11 +95,40 @@ void yuyv_to_rgb(const uint8_t *yuyv, uint8_t *rgb, int width, int height) { } } -// Buffer sizing +// Buffer sizing for multi-byte UTF-8 Braille (3 bytes per block) + color codes size_t ascii_out_size(int dst_w, int dst_h, int color) { - if (color) - return 3 + (size_t)dst_h * ((size_t)dst_w * 21 + 6) + 1; - return 3 + (size_t)dst_h * ((size_t)dst_w + 1) + 1; + return ascii_out_size_for_mode(dst_w, dst_h, color, RENDER_BRAILLE); +} + +size_t ascii_out_size_for_mode(int dst_w, int dst_h, int color, + render_mode_t mode) { + int braille_term_w = dst_w / 2; + int braille_term_h = dst_h / 4; + + switch (mode) { + case RENDER_HALF_BLOCK: { + int tw = dst_w; + int th = dst_h / 2; + size_t per_cell = color ? 41 : 3; + return 3 + (size_t)th * ((size_t)tw * per_cell + 5) + 1; + } + case RENDER_BLOCKS: + case RENDER_ASCII_RAMP: + case RENDER_DOTS: { + int tw = braille_term_w; + int th = braille_term_h; + size_t per_cell = color ? 22 : 3; // worst case: 3-byte glyph + return 3 + (size_t)th * ((size_t)tw * per_cell + 5) + 1; + } + case RENDER_BRAILLE: + default: { + int tw = braille_term_w; + int th = braille_term_h; + if (color) + return 3 + (size_t)th * ((size_t)tw * 22 + 5) + 1; + return 3 + (size_t)th * ((size_t)tw * 3 + 1) + 1; + } + } } // Sobel edge detection @@ -103,44 +150,293 @@ static void sobel(const uint8_t *in, uint8_t *out, int w, int h) { } } +static void sobel_dir(const uint8_t *in, uint8_t *out_mag, uint8_t *out_dir, + int w, int h) { + static const int Gx[3][3] = {{-1, 0, 1}, {-2, 0, 2}, {-1, 0, 1}}; + static const int Gy[3][3] = {{-1, -2, -1}, {0, 0, 0}, {1, 2, 1}}; + for (int y = 1; y < h - 1; y++) { + for (int x = 1; x < w - 1; x++) { + int gx = 0, gy = 0; + for (int ky = -1; ky <= 1; ky++) + for (int kx = -1; kx <= 1; kx++) { + int p = in[(y + ky) * w + (x + kx)]; + gx += Gx[ky + 1][kx + 1] * p; + gy += Gy[ky + 1][kx + 1] * p; + } + int mag = my_abs(gx) + my_abs(gy); + out_mag[y * w + x] = (uint8_t)(mag > 255 ? 255 : mag); + + uint8_t dir; + int agx = my_abs(gx), agy = my_abs(gy); + if (agx == 0 && agy == 0) { + dir = 0; + } else if (agy * 5 < agx * 2) { + dir = 2; + } else if (agx * 5 < agy * 2) { + dir = 0; + } else { + dir = ((gx > 0) == (gy > 0)) ? 3 : 1; + } + out_dir[y * w + x] = dir; + } + } +} + +static void laplacian(const uint8_t *in, uint8_t *out, int w, int h) { + for (int y = 1; y < h - 1; y++) { + for (int x = 1; x < w - 1; x++) { + int center = in[y * w + x]; + int lap = 4 * center - in[(y - 1) * w + x] - in[(y + 1) * w + x] - + in[y * w + (x - 1)] - in[y * w + (x + 1)]; + out[y * w + x] = (uint8_t)(my_abs(lap) > 255 ? 255 : my_abs(lap)); + } + } +} + +static inline uint8_t get_braille_bitmask(int dx, int dy) { + static const uint8_t braille_dots[2][4] = { + {0x01, 0x02, 0x04, 0x40}, // Left layout column: dots 1, 2, 3, 7 + {0x08, 0x10, 0x20, 0x80} // Right layout column: dots 4, 5, 6, 8 + }; + return braille_dots[dx][dy]; +} + +// Charset registry: hot-reloadable character ramps from a directory. +static void _trim_line(char *s) { + // Cut at first newline/CR, then trim trailing whitespace. + for (char *p = s; *p; p++) { + if (*p == '\n' || *p == '\r') { + *p = '\0'; + break; + } + } + size_t len = nl_strlen(s); + while (len > 0 && (s[len - 1] == ' ' || s[len - 1] == '\t')) { + s[--len] = '\0'; + } +} + +static void _name_from_path(const char *path, char *out, size_t out_sz) { + char tmp[256]; + nl_strncpy_safe(tmp, path, sizeof(tmp)); + const char *base = basename(tmp); + nl_strncpy_safe(out, base, out_sz); + // strip extension + for (size_t i = 0; out[i]; i++) { + if (out[i] == '.') { + out[i] = '\0'; + break; + } + } +} + +int charset_registry_init(charset_registry_t *reg, const char *dir) { + nl_memset(reg, 0, sizeof(*reg)); + reg->inotify_fd = -1; + reg->inotify_wd = -1; + nl_strncpy_safe(reg->dir_path, dir, sizeof(reg->dir_path)); + + nl_strncpy_safe(reg->sets[0].name, "default", CHARSET_NAME_LEN); + nl_strncpy_safe(reg->sets[0].ramp, ASCII_CHARS_DEFAULT, CHARSET_RAMP_LEN); + reg->count = 1; + reg->active = 0; + + charset_registry_scan(reg); + + reg->inotify_fd = inotify_init1(IN_NONBLOCK); + if (reg->inotify_fd >= 0) { + reg->inotify_wd = + inotify_add_watch(reg->inotify_fd, dir, + IN_CLOSE_WRITE | IN_MOVED_TO | IN_CREATE | IN_DELETE); + } + return 0; +} + +// TEST: +void charset_registry_scan(charset_registry_t *reg) { + static const char *known[] = {"blocks", "ascii", "dots", "shades", + "minimal", "emoji", "lines", NULL}; + for (int i = 0; known[i] && reg->count < MAX_CHARSETS; i++) { + char path[256]; + nl_snprintf(path, sizeof(path), "%s/%s.txt", reg->dir_path, known[i]); + + int fd = open(path, O_RDONLY, 0); + if (fd < 0) + continue; + + char buf[CHARSET_RAMP_LEN]; + ssize_t n = read(fd, buf, sizeof(buf) - 1); + close(fd); + if (n <= 0) + continue; + buf[n] = '\0'; + _trim_line(buf); + if (buf[0] == '\0') + continue; + + // Skip if charset with name is already loaded + int dup = 0; + for (int j = 0; j < reg->count; j++) { + char nm[CHARSET_NAME_LEN]; + _name_from_path(path, nm, sizeof(nm)); + if (nl_strcmp(reg->sets[j].name, nm) == 0) { + nl_strncpy_safe(reg->sets[j].ramp, buf, CHARSET_RAMP_LEN); + dup = 1; + break; + } + } + if (dup) + continue; + + _name_from_path(path, reg->sets[reg->count].name, CHARSET_NAME_LEN); + nl_strncpy_safe(reg->sets[reg->count].ramp, buf, CHARSET_RAMP_LEN); + reg->count++; + } +} + +void charset_registry_check_reload(charset_registry_t *reg) { + if (reg->inotify_fd < 0) + return; + + char buf[4096] __attribute__((aligned(__alignof__(struct inotify_event)))); + ssize_t n = read(reg->inotify_fd, buf, sizeof(buf)); + if (n <= 0) + return; + + charset_registry_scan(reg); +} + +const char *charset_registry_active_ramp(const charset_registry_t *reg) { + if (reg->count == 0) + return ASCII_CHARS_DEFAULT; + int idx = reg->active; + if (idx < 0 || idx >= reg->count) + idx = 0; + return reg->sets[idx].ramp; +} + +void charset_registry_cleanup(charset_registry_t *reg) { + if (reg->inotify_fd >= 0) { + close(reg->inotify_fd); + reg->inotify_fd = -1; + } +} + // Grayscale to ASCII +// Block-shade ramp +static const char *BLOCK_SHADE_UTF8[] = {" ", "\xe2\x96\x91", "\xe2\x96\x92", + "\xe2\x96\x93", "\xe2\x96\x88"}; +#define BLOCK_SHADE_COUNT 5 + +// Dot ramp +static const char *DOT_UTF8[] = {" ", "\xc2\xb7", "\xe2\x80\xa2", + "\xe2\x97\x8f"}; +#define DOT_COUNT 4 + +// Half block glyph +static const char HALF_BLOCK_UTF8[] = "\xe2\x96\x80"; + +const char *render_mode_name(render_mode_t m) { + switch (m) { + case RENDER_BRAILLE: + return "braille"; + case RENDER_BLOCKS: + return "blocks"; + case RENDER_ASCII_RAMP: + return "ascii"; + case RENDER_HALF_BLOCK: + return "halfblock"; + case RENDER_DOTS: + return "dots"; + default: + return "?"; + } +} + +const char *edge_mode_name(edge_mode_t m) { + switch (m) { + case EDGE_OFF: + return "off"; + case EDGE_SOBEL: + return "sobel"; + case EDGE_SOBEL_DIR: + return "sobel-dir"; + case EDGE_LAPLACIAN: + return "laplacian"; + default: + return "?"; + } +} + +static int emit_glyph(char *out, size_t out_size, int out_idx, + const char *glyph, int do_color, int r, int g, int b) { + if (do_color) { + int written = snprintf(out + out_idx, out_size - (size_t)out_idx, + "\033[38;2;%d;%d;%dm%s", r, g, b, glyph); + if (written > 0 && (size_t)(out_idx + written) < out_size) + out_idx += written; + } else { + size_t glen = nl_strlen(glyph); + if ((size_t)(out_idx + (int)glen) < out_size) { + nl_memcpy(out + out_idx, glyph, glen); + out_idx += (int)glen; + } + } + return out_idx; +} + int grayscale_to_ascii(const uint8_t *gray, const uint8_t *rgb, int src_w, int src_h, int dst_w, int dst_h, char *out, size_t out_size, const ascii_opts_t *opts) { - const char *charset = - (opts && opts->charset) ? opts->charset : ASCII_CHARS_DEFAULT; - int nchars = (int)strlen(charset); + int safe_dst_w = dst_w - (dst_w % 2); + int safe_dst_h = dst_h - (dst_h % 4); + int brightness = opts ? opts->brightness : 0; int contrast = opts ? opts->contrast : 100; int invert = opts ? opts->invert : 0; int do_color = opts && opts->color && rgb; - int do_edges = opts ? opts->edges : 0; + edge_mode_t edge_mode = opts ? opts->edges : EDGE_OFF; int do_dither = opts ? opts->dither : 0; + int thresh_limit = opts ? opts->threshold_val : 35; + render_mode_t render_mode = opts ? opts->render_mode : RENDER_BRAILLE; + const char *ramp = (opts && opts->charset && opts->charset[0]) + ? opts->charset + : ASCII_CHARS_DEFAULT; + int ramp_len = (int)nl_strlen(ramp); + if (ramp_len < 1) { + ramp = ASCII_CHARS_DEFAULT; + ramp_len = (int)nl_strlen(ramp); + } - double bw = (double)src_w / dst_w; - double bh = (double)src_h / dst_h; + int depth_pop = opts ? opts->depth_pop : 0; + int depth_invert = opts ? opts->depth_invert : 0; - uint8_t *small_g = malloc((size_t)(dst_w * dst_h)); - uint8_t *small_rgb = do_color ? malloc((size_t)(dst_w * dst_h * 3)) : NULL; + double bw = (double)src_w / safe_dst_w; + double bh = (double)src_h / safe_dst_h; - if (!small_g || (do_color && !small_rgb)) { - free(small_g); - free(small_rgb); + uint8_t *subpixel_g = malloc((size_t)(safe_dst_w * safe_dst_h)); + uint8_t *subpixel_rgb = + do_color ? malloc((size_t)(safe_dst_w * safe_dst_h * 3)) : NULL; + + if (!subpixel_g || (do_color && !subpixel_rgb)) { + free(subpixel_g); + free(subpixel_rgb); return -1; } - for (int y = 0; y < dst_h; y++) { + // Downscale and interpolate the high-res frames to subpixel boundaries + for (int y = 0; y < safe_dst_h; y++) { int ys = (int)(y * bh), ye = (int)((y + 1) * bh); if (ye <= ys) ye = ys + 1; - for (int x = 0; x < dst_w; x++) { + for (int x = 0; x < safe_dst_w; x++) { int xs = (int)(x * bw), xe = (int)((x + 1) * bw); if (xe <= xs) xe = xs + 1; long tg = 0, tr = 0, tgv = 0, tb = 0; int count = 0; - for (int sy = ys; sy < ye && sy < src_h; sy++) + for (int sy = ys; sy < ye && sy < src_h; sy++) { for (int sx = xs; sx < xe && sx < src_w; sx++) { tg += gray[sy * src_w + sx]; if (do_color) { @@ -151,6 +447,7 @@ int grayscale_to_ascii(const uint8_t *gray, const uint8_t *rgb, int src_w, } count++; } + } if (!count) count = 1; @@ -158,10 +455,10 @@ int grayscale_to_ascii(const uint8_t *gray, const uint8_t *rgb, int src_w, if (contrast != 100) gv = 128 + (gv - 128) * contrast / 100; gv += brightness; - small_g[y * dst_w + x] = clamp_u8(gv); + subpixel_g[y * safe_dst_w + x] = clamp_u8(gv); if (do_color) { - uint8_t *op = small_rgb + (y * dst_w + x) * 3; + uint8_t *op = subpixel_rgb + (y * safe_dst_w + x) * 3; op[0] = clamp_u8((int)(tr / count)); op[1] = clamp_u8((int)(tgv / count)); op[2] = clamp_u8((int)(tb / count)); @@ -169,47 +466,120 @@ int grayscale_to_ascii(const uint8_t *gray, const uint8_t *rgb, int src_w, } } - if (do_edges) { - uint8_t *eb = calloc((size_t)(dst_w * dst_h), 1); + // TEST: + // Pseudo-3D "pop out" parallax effect. + if (depth_pop > 0) { + uint8_t *warped_g = malloc((size_t)(safe_dst_w * safe_dst_h)); + uint8_t *warped_rgb = + do_color ? malloc((size_t)(safe_dst_w * safe_dst_h * 3)) : NULL; + if (warped_g && (!do_color || warped_rgb)) { + double cx = safe_dst_w / 2.0; + double cy = safe_dst_h / 2.0; + double max_r = (cx > cy ? cx : cy); + double strength = (depth_pop / 100.0) * (max_r * 0.35); + + for (int y = 0; y < safe_dst_h; y++) { + for (int x = 0; x < safe_dst_w; x++) { + uint8_t luma = subpixel_g[y * safe_dst_w + x]; + double depth01 = depth_invert ? (255 - luma) / 255.0 : luma / 255.0; + + double dx = x - cx, dy = y - cy; + double r = (dx * dx + dy * dy); + double rlen = r > 0.0001 ? my_sqrt(r) : 0.0001; + double nx = dx / rlen, ny = dy / rlen; + + double rnorm = rlen / (max_r > 0.0001 ? max_r : 0.0001); + double disp = strength * depth01 * rnorm; + + int sx = (int)(x + nx * disp); + int sy = (int)(y + ny * disp); + if (sx < 0) + sx = 0; + if (sx >= safe_dst_w) + sx = safe_dst_w - 1; + if (sy < 0) + sy = 0; + if (sy >= safe_dst_h) + sy = safe_dst_h - 1; + + warped_g[y * safe_dst_w + x] = subpixel_g[sy * safe_dst_w + sx]; + if (do_color) { + const uint8_t *src_px = subpixel_rgb + (sy * safe_dst_w + sx) * 3; + uint8_t *dst_px = warped_rgb + (y * safe_dst_w + x) * 3; + dst_px[0] = src_px[0]; + dst_px[1] = src_px[1]; + dst_px[2] = src_px[2]; + } + } + } + nl_memcpy(subpixel_g, warped_g, (size_t)(safe_dst_w * safe_dst_h)); + if (do_color) + nl_memcpy(subpixel_rgb, warped_rgb, + (size_t)(safe_dst_w * safe_dst_h * 3)); + } + free(warped_g); + free(warped_rgb); + } + + // Edge detection dispatch + uint8_t *dir_buf = NULL; + if (edge_mode != EDGE_OFF) { + uint8_t *eb = calloc((size_t)(safe_dst_w * safe_dst_h), 1); if (eb) { - sobel(small_g, eb, dst_w, dst_h); - nl_memcpy(small_g, eb, (size_t)(dst_w * dst_h)); + switch (edge_mode) { + case EDGE_SOBEL: + sobel(subpixel_g, eb, safe_dst_w, safe_dst_h); + break; + case EDGE_SOBEL_DIR: + dir_buf = calloc((size_t)(safe_dst_w * safe_dst_h), 1); + if (dir_buf) + sobel_dir(subpixel_g, eb, dir_buf, safe_dst_w, safe_dst_h); + else + sobel(subpixel_g, eb, safe_dst_w, safe_dst_h); + break; + case EDGE_LAPLACIAN: + laplacian(subpixel_g, eb, safe_dst_w, safe_dst_h); + break; + default: + break; + } + nl_memcpy(subpixel_g, eb, (size_t)(safe_dst_w * safe_dst_h)); free(eb); } } + // Floyd-Steinberg dithering on the subpixel grayscale buffer if (do_dither) { - int16_t *eb = malloc((size_t)(dst_w * dst_h) * sizeof(int16_t)); - if (eb) { - for (int i = 0; i < dst_w * dst_h; i++) - eb[i] = (int16_t)small_g[i]; - for (int y = 0; y < dst_h; y++) { - for (int x = 0; x < dst_w; x++) { - int old_v = eb[y * dst_w + x]; - int qi = old_v * (nchars - 1) / 255; - if (qi < 0) - qi = 0; - if (qi >= nchars) - qi = nchars - 1; - int new_v = qi * 255 / (nchars - 1); - eb[y * dst_w + x] = (int16_t)new_v; - int err = old_v - new_v; -#define FS(DY, DX, N) \ - do { \ - int ny = y + (DY), nx = x + (DX); \ - if (nx >= 0 && nx < dst_w && ny >= 0 && ny < dst_h) \ - eb[ny * dst_w + nx] += (int16_t)(err * (N) / 16); \ - } while (0) - FS(0, 1, 7); - FS(1, -1, 3); - FS(1, 0, 5); - FS(1, 1, 1); -#undef FS + int16_t *err = calloc((size_t)(safe_dst_w * safe_dst_h), sizeof(int16_t)); + if (err) { + for (int i = 0; i < safe_dst_w * safe_dst_h; i++) + err[i] = (int16_t)subpixel_g[i]; + + for (int y = 0; y < safe_dst_h; y++) { + for (int x = 0; x < safe_dst_w; x++) { + int idx = y * safe_dst_w + x; + int16_t old = err[idx]; + int16_t nval = (old > (int16_t)thresh_limit) ? 255 : 0; + int16_t qerr = old - nval; + err[idx] = nval; + + if (x + 1 < safe_dst_w) + err[idx + 1] += (qerr * 7) >> 4; + if (y + 1 < safe_dst_h) { + if (x > 0) + err[idx + safe_dst_w - 1] += (qerr * 3) >> 4; + err[idx + safe_dst_w] += (qerr * 5) >> 4; + if (x + 1 < safe_dst_w) + err[idx + safe_dst_w + 1] += (qerr * 1) >> 4; + } } } - for (int i = 0; i < dst_w * dst_h; i++) - small_g[i] = clamp_u8(eb[i]); - free(eb); + + for (int i = 0; i < safe_dst_w * safe_dst_h; i++) { + int16_t v = err[i]; + subpixel_g[i] = (v < 0) ? 0 : (v > 255) ? 255 : (uint8_t)v; + } + free(err); } } @@ -220,33 +590,169 @@ int grayscale_to_ascii(const uint8_t *gray, const uint8_t *rgb, int src_w, out_idx = sizeof(HOME) - 1; } - for (int y = 0; y < dst_h; y++) { - for (int x = 0; x < dst_w; x++) { - int gv = small_g[y * dst_w + x]; - int idx = gv * (nchars - 1) / 255; - if (idx < 0) - idx = 0; - if (idx >= nchars) - idx = nchars - 1; - if (invert) - idx = nchars - 1 - idx; - char ch = charset[idx]; + if (render_mode == RENDER_HALF_BLOCK) { + int term_w = safe_dst_w; + int term_h = safe_dst_h / 2; + for (int ty = 0; ty < term_h; ty++) { + for (int tx = 0; tx < term_w; tx++) { + int top_idx = (ty * 2) * safe_dst_w + tx; + int bot_idx = (ty * 2 + 1) * safe_dst_w + tx; + uint8_t top_l = subpixel_g[top_idx]; + uint8_t bot_l = subpixel_g[bot_idx]; + if (do_color) { + const uint8_t *tp = subpixel_rgb + top_idx * 3; + const uint8_t *bp = subpixel_rgb + bot_idx * 3; + int written = + snprintf(out + out_idx, out_size - (size_t)out_idx, + "\033[38;2;%d;%d;%dm\033[48;2;%d;%d;%dm%s", tp[0], tp[1], + tp[2], bp[0], bp[1], bp[2], HALF_BLOCK_UTF8); + if (written > 0 && (size_t)(out_idx + written) < out_size) + out_idx += written; + } else { + int active = (top_l > thresh_limit) || (bot_l > thresh_limit); + if (invert) + active = !active; + const char *glyph = active ? "\xe2\x96\x88" : " "; + out_idx = emit_glyph(out, out_size, out_idx, glyph, 0, 0, 0, 0); + } + } if (do_color) { - const uint8_t *px = small_rgb + (y * dst_w + x) * 3; - int w = snprintf(out + out_idx, out_size - (size_t)out_idx, - "\033[38;2;%d;%d;%dm%c", px[0], px[1], px[2], ch); - if (w > 0 && (size_t)(out_idx + w) < out_size) - out_idx += w; + int written = + snprintf(out + out_idx, out_size - (size_t)out_idx, "\033[0m\n"); + if (written > 0 && (size_t)(out_idx + written) < out_size) + out_idx += written; } else { if ((size_t)(out_idx + 1) < out_size) - out[out_idx++] = ch; + out[out_idx++] = '\n'; } } + out[(size_t)out_idx < out_size ? (size_t)out_idx : out_size - 1] = '\0'; + free(subpixel_g); + free(subpixel_rgb); + free(dir_buf); + return out_idx; + } + + // BRAILLE / BLOCKS / ASCII_RAMP / DOTS + int term_w = safe_dst_w / 2; + int term_h = safe_dst_h / 4; + + for (int ty = 0; ty < term_h; ty++) { + for (int tx = 0; tx < term_w; tx++) { + uint8_t braille_offset = 0; + long sum_r = 0, sum_g = 0, sum_b = 0; + long sum_luma = 0; + int colored_subpixels = 0; + int active_count = 0; + uint8_t dom_dir = 0; + int dom_dir_votes[4] = {0, 0, 0, 0}; + + for (int dy = 0; dy < 4; dy++) { + for (int dx = 0; dx < 2; dx++) { + int sub_x = tx * 2 + dx; + int sub_y = ty * 4 + dy; + int pixel_idx = sub_y * safe_dst_w + sub_x; + + uint8_t luma = subpixel_g[pixel_idx]; + sum_luma += luma; + + int is_active = (luma > thresh_limit); + if (invert) + is_active = !is_active; + + if (is_active) { + braille_offset |= get_braille_bitmask(dx, dy); + active_count++; + if (dir_buf) + dom_dir_votes[dir_buf[pixel_idx]]++; + } + + if (do_color) { + const uint8_t *px = subpixel_rgb + pixel_idx * 3; + sum_r += px[0]; + sum_g += px[1]; + sum_b += px[2]; + colored_subpixels++; + } + } + } + + if (dir_buf) { + int best = 0; + for (int d = 1; d < 4; d++) + if (dom_dir_votes[d] > dom_dir_votes[best]) + best = d; + dom_dir = (uint8_t)best; + } + + int avg_r = 0, avg_g = 0, avg_b = 0; + if (do_color && colored_subpixels > 0) { + avg_r = (int)(sum_r / colored_subpixels); + avg_g = (int)(sum_g / colored_subpixels); + avg_b = (int)(sum_b / colored_subpixels); + } + int avg_luma = (int)(sum_luma / 8); + + switch (render_mode) { + case RENDER_BRAILLE: { + uint32_t unicode_val = 0x2800 + braille_offset; + char utf8_seq[4]; + utf8_seq[0] = (char)(0xE0 | ((unicode_val >> 12) & 0x0F)); + utf8_seq[1] = (char)(0x80 | ((unicode_val >> 6) & 0x3F)); + utf8_seq[2] = (char)(0x80 | (unicode_val & 0x3F)); + utf8_seq[3] = '\0'; + out_idx = emit_glyph(out, out_size, out_idx, utf8_seq, do_color, avg_r, + avg_g, avg_b); + break; + } + case RENDER_BLOCKS: { + int shade_idx = avg_luma * BLOCK_SHADE_COUNT / 256; + if (shade_idx >= BLOCK_SHADE_COUNT) + shade_idx = BLOCK_SHADE_COUNT - 1; + if (invert) + shade_idx = BLOCK_SHADE_COUNT - 1 - shade_idx; + out_idx = + emit_glyph(out, out_size, out_idx, BLOCK_SHADE_UTF8[shade_idx], + do_color, avg_r, avg_g, avg_b); + break; + } + case RENDER_DOTS: { + int dot_idx = avg_luma * DOT_COUNT / 256; + if (dot_idx >= DOT_COUNT) + dot_idx = DOT_COUNT - 1; + if (invert) + dot_idx = DOT_COUNT - 1 - dot_idx; + out_idx = emit_glyph(out, out_size, out_idx, DOT_UTF8[dot_idx], + do_color, avg_r, avg_g, avg_b); + break; + } + case RENDER_ASCII_RAMP: + default: { + char glyph[2] = {0, 0}; + if (dir_buf && active_count > 0) { + static const char dirs[4] = {'-', '/', '|', '\\'}; + glyph[0] = dirs[dom_dir]; + } else { + int ramp_idx = avg_luma * ramp_len / 256; + if (ramp_idx >= ramp_len) + ramp_idx = ramp_len - 1; + if (invert) + ramp_idx = ramp_len - 1 - ramp_idx; + glyph[0] = ramp[ramp_idx]; + } + out_idx = emit_glyph(out, out_size, out_idx, glyph, do_color, avg_r, + avg_g, avg_b); + break; + } + } + } + if (do_color) { - int w = snprintf(out + out_idx, out_size - (size_t)out_idx, "\033[0m\n"); - if (w > 0 && (size_t)(out_idx + w) < out_size) - out_idx += w; + int written = + snprintf(out + out_idx, out_size - (size_t)out_idx, "\033[0m\n"); + if (written > 0 && (size_t)(out_idx + written) < out_size) + out_idx += written; } else { if ((size_t)(out_idx + 1) < out_size) out[out_idx++] = '\n'; @@ -254,15 +760,18 @@ int grayscale_to_ascii(const uint8_t *gray, const uint8_t *rgb, int src_w, } out[(size_t)out_idx < out_size ? (size_t)out_idx : out_size - 1] = '\0'; - free(small_g); - free(small_rgb); + + free(subpixel_g); + free(subpixel_rgb); + free(dir_buf); return out_idx; } -// FPS overlay +// FPS Overlay Configuration void overlay_fps_box(int dst_w, double fps, int color_enabled) { char buf[80]; - int col = (dst_w - 13) / 2 + 1; + int term_w = dst_w / 2; + int col = (term_w - 13) / 2 + 1; if (col < 1) col = 1; diff --git a/C/src/capture_linux.c b/C/src/capture_linux.c index 738475a..e2de05a 100644 --- a/C/src/capture_linux.c +++ b/C/src/capture_linux.c @@ -1,10 +1,11 @@ +#ifdef PLATFORM_LINUX + +#include "nolibc.h" + #include "ascii.h" #include "capture.h" #include "platform.h" -#ifdef PLATFORM_LINUX - -#include "nolibc.h" #include #include @@ -126,4 +127,4 @@ void webcam_cleanup(webcam_t *cam) { cam->impl = (webcam_impl_t *)0; } -#endif /* PLATFORM_LINUX */ +#endif diff --git a/C/src/capture_macos.c b/C/src/capture_macos.c index 1e65356..8372cb9 100644 --- a/C/src/capture_macos.c +++ b/C/src/capture_macos.c @@ -1,14 +1,14 @@ +#ifdef PLATFORM_MACOS #include "capture.h" #include "platform.h" -#ifdef PLATFORM_MACOS +#include +#include +#include #import #import #import -#include -#include -#include #define FRAME_BUFS 2 diff --git a/C/src/main.c b/C/src/main.c index b8412c2..0d96f41 100644 --- a/C/src/main.c +++ b/C/src/main.c @@ -1,11 +1,11 @@ +#include "nolibc.h" + #include "ascii.h" #include "capture.h" #include "plugins.h" #include "thread_sharing.h" #include "timing.h" -#include "nolibc.h" - #include #include #include @@ -13,10 +13,11 @@ // Defaults #define DEFAULT_ASCII_WIDTH 80 #define DEFAULT_ASCII_HEIGHT 40 -#define DEFAULT_CAPTURE_WIDTH 160 -#define DEFAULT_CAPTURE_HEIGHT 120 +#define DEFAULT_CAPTURE_WIDTH 640 +#define DEFAULT_CAPTURE_HEIGHT 480 #define DEFAULT_FPS 20 #define MAX_PLUGINS 8 +#define DEFAULT_CHARSET_DIR "./charsets" // Signal handling volatile sig_atomic_t keep_running = 1; @@ -55,18 +56,55 @@ static void print_usage(const char *prog) { " -H ASCII output rows (default: %d) \n" " -s custom charset string (default: \"%s\") \n" " -p filter plugin .so path \n" + " -m render mode: braille|blocks|ascii|halfblock|dots\n" + " -k charset directory (hot-reloadable .txt ramps) \n" "\n" "Image adjustments:\n" " -b brightness offset -128..128 (default: 0)\n" " -c contrast in percent >0; 100=none (default: 100)\n" " -i invert mapping \n" - " -e enable Sobel edge detection \n" + " -E edge mode off|sobel|sobel-dir|laplacian\n" " -C ANSI truecolor output \n" - " -D Floyd-Steinberg dithering \n", + " -D Floyd-Steinberg dithering \n" + " -P <0-100> depth-pop 3D parallax strength (0=off) \n" + "\n" + "Live keybindings:\n" + " m / M cycle render mode forward / backward \n" + " x / X cycle edge detection mode forward / backward \n" + " n / N cycle loaded charset forward / backward \n" + " p / o increase / decrease depth-pop strength \n" + " up/down select plugin [ ] +-1 { } +-10 r reset \n" + " q quit \n", prog, DEFAULT_CAPTURE_WIDTH, DEFAULT_CAPTURE_HEIGHT, DEFAULT_FPS, DEFAULT_ASCII_WIDTH, DEFAULT_ASCII_HEIGHT, ASCII_CHARS_DEFAULT); } +static render_mode_t parse_render_mode(const char *s) { + if (nl_strcmp(s, "braille") == 0) + return RENDER_BRAILLE; + if (nl_strcmp(s, "blocks") == 0) + return RENDER_BLOCKS; + if (nl_strcmp(s, "ascii") == 0) + return RENDER_ASCII_RAMP; + if (nl_strcmp(s, "halfblock") == 0) + return RENDER_HALF_BLOCK; + if (nl_strcmp(s, "dots") == 0) + return RENDER_DOTS; + return RENDER_BRAILLE; +} + +static edge_mode_t parse_edge_mode(const char *s) { + if (nl_strcmp(s, "off") == 0) + return EDGE_OFF; + if (nl_strcmp(s, "sobel") == 0) + return EDGE_SOBEL; + if (nl_strcmp(s, "sobel-dir") == 0) + return EDGE_SOBEL_DIR; + if (nl_strcmp(s, "laplacian") == 0) + return EDGE_LAPLACIAN; + return EDGE_OFF; +} + // termios void term_raw_mode(void) { tcgetattr(STDIN_FILENO, &orig_terminal); // save stdin state @@ -81,7 +119,8 @@ void term_restore(void) { tcsetattr(STDIN_FILENO, TCSAFLUSH, &orig_terminal); } static void overlay_panel(int ascii_h, double fps, plugin_loader_t *plugins, int *plugin_params, int count, int selected, - int color) { + int color, const ascii_opts_t *opts, + const charset_registry_t *charsets) { char buf[1024]; int n, base_row = ascii_h + 1; // 1-indexed panel row @@ -103,7 +142,32 @@ static void overlay_panel(int ascii_h, double fps, plugin_loader_t *plugins, if (n > 0 && n < (int)sizeof(buf)) (void)write(STDOUT_FILENO, buf, (size_t)n); + // Mode/edge/charset/depth-pop status row + const char *cset_name = (charsets && charsets->count > 0 && + opts->render_mode == RENDER_ASCII_RAMP) + ? charsets->sets[charsets->active].name + : "-"; n = nl_snprintf(buf, sizeof(buf), "\033[%d;1H\033[K", base_row + 1); + if (n > 0) + (void)write(STDOUT_FILENO, buf, (size_t)n); + if (color) { + n = nl_snprintf(buf, sizeof(buf), + "\033[38;2;0;180;220m mode: %s (m/M) edges: %s (x/X) " + "charset: %s (n/N) depth-pop: %d%s (+/-, v)\033[0m\033[K", + render_mode_name(opts->render_mode), + edge_mode_name(opts->edges), cset_name, opts->depth_pop, + opts->depth_invert ? " [inv]" : ""); + } else { + n = nl_snprintf(buf, sizeof(buf), + " mode: %s edges: %s charset: %s depth-pop: %d%s\033[K", + render_mode_name(opts->render_mode), + edge_mode_name(opts->edges), cset_name, opts->depth_pop, + opts->depth_invert ? " [inv]" : ""); + } + if (n > 0 && n < (int)sizeof(buf)) + (void)write(STDOUT_FILENO, buf, (size_t)n); + + n = nl_snprintf(buf, sizeof(buf), "\033[%d;1H\033[K", base_row + 2); if (n > 0) (void)write(STDOUT_FILENO, buf, (size_t)n); @@ -163,15 +227,20 @@ int main(int argc, char *argv[]) { int cap_w = DEFAULT_CAPTURE_WIDTH; int cap_h = DEFAULT_CAPTURE_HEIGHT; int fps = DEFAULT_FPS; + const char *charset_dir = DEFAULT_CHARSET_DIR; ascii_opts_t opts = { .brightness = 0, .contrast = 100, .invert = 0, .color = 0, - .edges = 0, + .edges = EDGE_OFF, .dither = 0, + .threshold_val = 35, .charset = NULL, + .render_mode = RENDER_BRAILLE, + .depth_pop = 0, + .depth_invert = 0, }; // Plugins @@ -180,7 +249,7 @@ int main(int argc, char *argv[]) { // CLI parsing int opt; - while ((opt = nl_getopt(argc, argv, "d:W:H:w:h:f:b:c:iCDes:p:")) != -1) + while ((opt = nl_getopt(argc, argv, "d:W:H:w:h:f:b:c:iCDs:p:m:E:k:P:")) != -1) switch (opt) { case 'd': device = optarg; @@ -224,8 +293,21 @@ int main(int argc, char *argv[]) { case 'C': opts.color = 1; break; - case 'e': - opts.edges = 1; + case 'E': + opts.edges = parse_edge_mode(optarg); + break; + case 'm': + opts.render_mode = parse_render_mode(optarg); + break; + case 'k': + charset_dir = optarg; + break; + case 'P': + opts.depth_pop = my_atoi(optarg); + if (opts.depth_pop < 0) + opts.depth_pop = 0; + if (opts.depth_pop > 100) + opts.depth_pop = 100; break; case 'D': opts.dither = 1; @@ -275,10 +357,11 @@ int main(int argc, char *argv[]) { } fprintf(stderr, "Device: %s | capture %dx%d | ASCII %dx%d | %d fps | %d " - "plugin(s)%s%s%s%s\n", + "plugin(s) | mode: %s%s%s%s\n", device, cam.width, cam.height, ascii_w, ascii_h, fps, plugin_count, - opts.color ? " | color" : "", opts.edges ? " | edges" : "", - opts.dither ? " | dither" : "", opts.invert ? " | inverted" : ""); + render_mode_name(opts.render_mode), opts.color ? " | color" : "", + opts.edges != EDGE_OFF ? " | edges" : "", + opts.dither ? " | dither" : ""); // Pixel buffers allocation int cam_pixels = cam.width * cam.height; @@ -293,7 +376,13 @@ int main(int argc, char *argv[]) { } // Allocate output string buffer - size_t out_size = ascii_out_size(ascii_w, ascii_h, opts.color); + size_t out_size = 0; + for (render_mode_t rm = 0; rm < RENDER_MODE_COUNT; rm++) { + size_t s = + ascii_out_size_for_mode(ascii_w * 2, ascii_h * 4, opts.color, rm); + if (s > out_size) + out_size = s; + } char *out_buf = malloc(out_size); if (!out_buf) { @@ -304,6 +393,11 @@ int main(int argc, char *argv[]) { return 1; } + // Charset registry, hot-reloadable ramps from charset_dir + charset_registry_t charsets; + charset_registry_init(&charsets, charset_dir); + opts.charset = charset_registry_active_ramp(&charsets); + // // Thead sharing // shared_frame_t sf = {0}; // sf.buf[0] = malloc(cam_pixels); @@ -366,33 +460,68 @@ int main(int argc, char *argv[]) { continue; } - // adjust plugin param if selected - int *p = (plugin_count > 0) ? &plugin_params[selected] : NULL; + // Adjust plugin param if selected + int *pp = (plugin_count > 0) ? &plugin_params[selected] : NULL; switch (ch) { case 'q': case 'Q': keep_running = 0; break; case ']': - if (p && *p < 255) - (*p)++; + if (pp && *pp < 255) + (*pp)++; break; case '[': - if (p && *p > 0) - (*p)--; + if (pp && *pp > 0) + (*pp)--; break; case '}': - if (p) - *p = (*p + 10 > 255) ? 255 : *p + 10; + if (pp) + *pp = (*pp + 10 > 255) ? 255 : *pp + 10; break; case '{': - if (p) - *p = (*p - 10 < 0) ? 0 : *p - 10; + if (pp) + *pp = (*pp - 10 < 0) ? 0 : *pp - 10; break; case 'r': case 'R': - if (p) - *p = 128; + if (pp) + *pp = 128; + break; + case 'm': + opts.render_mode = (opts.render_mode + 1) % RENDER_MODE_COUNT; + break; + case 'M': + opts.render_mode = + (opts.render_mode - 1 + RENDER_MODE_COUNT) % RENDER_MODE_COUNT; + break; + case 'x': + opts.edges = (opts.edges + 1) % EDGE_MODE_COUNT; + break; + case 'X': + opts.edges = (opts.edges - 1 + EDGE_MODE_COUNT) % EDGE_MODE_COUNT; + break; + case 'n': + if (charsets.count > 0) { + charsets.active = (charsets.active + 1) % charsets.count; + opts.charset = charset_registry_active_ramp(&charsets); + } + break; + case 'N': + if (charsets.count > 0) { + charsets.active = + (charsets.active - 1 + charsets.count) % charsets.count; + opts.charset = charset_registry_active_ramp(&charsets); + } + break; + case '+': + opts.depth_pop = (opts.depth_pop + 5 > 100) ? 100 : opts.depth_pop + 5; + break; + case '-': + opts.depth_pop = (opts.depth_pop - 5 < 0) ? 0 : opts.depth_pop - 5; + break; + case 'v': + opts.depth_invert = !opts.depth_invert; break; } } @@ -403,6 +532,10 @@ int main(int argc, char *argv[]) { for (int i = 0; i < plugin_count; i++) plugin_check_reload(&plugins[i]); + // Hot-reload check for charset ramps + charset_registry_check_reload(&charsets); + opts.charset = charset_registry_active_ramp(&charsets); + // Frame capture if (webcam_wait_frame(&cam, 1000) < 0) continue; // timeout, retry @@ -419,18 +552,56 @@ int main(int argc, char *argv[]) { &plugin_params[i]); } - if (opts.color && rgb) + // NOTE: cam.buffer is the V4L2 mmap region (Linux only) + // On macOS, capture_macos.c delivers luma only; cam.buffer is NULL + // TODO: Add color support for macOS + // Color mode is therefore a Linux-only feature for now. + if (opts.color && rgb && cam.buffer && cam.buffer != MAP_FAILED) yuyv_to_rgb((const uint8_t *)cam.buffer, rgb, cam.width, cam.height); - int len = grayscale_to_ascii(gray, rgb, cam.width, cam.height, ascii_w, - ascii_h, out_buf, out_size, &opts); + // Calculate proper subpixel dimensions + int subpixel_w = ascii_w; + int subpixel_h = ascii_h; + + switch (opts.render_mode) { + case RENDER_BRAILLE: + subpixel_w = ascii_w * 2; + subpixel_h = ascii_h * 4; + break; + case RENDER_HALF_BLOCK: + subpixel_w = ascii_w * 1; + subpixel_h = ascii_h * 2; + break; + default: + // RENDER_BLOCKS, RENDER_ASCII_RAMP, RENDER_DOTS are 1x1 per cell + subpixel_w = ascii_w * 2; + subpixel_h = ascii_h * 4; + break; + } + + size_t out_size = ascii_out_size_for_mode(subpixel_w, subpixel_h, + opts.color, opts.render_mode); + char *out_buf = malloc(out_size); + if (!out_buf) { + perror("Failed to allocate text output buffer"); + break; + } + + // Process frame mapping using dynamically calculated bounds + int len = grayscale_to_ascii(gray, rgb, cam.width, cam.height, subpixel_w, + subpixel_h, out_buf, out_size, &opts); + if (len > (int)out_size) { + fprintf(stderr, "WARNING: len=%d > out_size=%zu\n", len, out_size); + } if (len > 0) { (void)write(STDOUT_FILENO, out_buf, (size_t)len); overlay_panel(ascii_h, current_fps, plugins, plugin_params, plugin_count, - selected, opts.color); + selected, opts.color, &opts, &charsets); } + free(out_buf); + if (webcam_requeue_buffer(&cam) < 0) { perror("requeue_buffer"); break; @@ -442,7 +613,7 @@ int main(int argc, char *argv[]) { // Cleanup term_restore(); // \033[2J = erase screen, \033[H = cursor home, \033[?25h = show cursor - static const char CLEANUP[] = "\033[2J\033[H\033[0m\033[?25h\n"; + static const char CLEANUP[] = "\033[2J\033[H\033[0m\033[?25h"; (void)write(STDOUT_FILENO, CLEANUP, sizeof(CLEANUP) - 1); fprintf(stderr, "Stopped.\n"); @@ -451,6 +622,7 @@ int main(int argc, char *argv[]) { free(out_buf); for (int i = 0; i < plugin_count; i++) plugin_cleanup(&plugins[i]); + charset_registry_cleanup(&charsets); webcam_cleanup(&cam); return 0; } diff --git a/C/src/plugins.c b/C/src/plugins.c index 6a143a0..fa92f30 100644 --- a/C/src/plugins.c +++ b/C/src/plugins.c @@ -1,10 +1,10 @@ +#include "nolibc.h" + #include "plugins.h" #include #include -#include "nolibc.h" - static int copy_file(const char *src, const char *dst) { int fd_src = open(src, O_RDONLY); if (fd_src < 0) { @@ -67,17 +67,9 @@ int plugin_load(plugin_loader_t *pl, const char *path) { return -1; } - void *sym = dlsym(pl->dl_handle, "plugin_get"); - if (!sym) { - fprintf(stderr, "[plugin] dlsym: %s\n", dlerror()); - dlclose(pl->dl_handle); - pl->dl_handle = NULL; - return -1; - } - filter_plugin_t *(*get_plugin)(void) = dlsym(pl->dl_handle, "plugin_get"); if (!get_plugin) { - fprintf(stderr, "[plugin] dlsym: %s\n", dlerror()); + fprintf(stderr, "[plugin] dlsym plugin_get: %s\n", dlerror()); dlclose(pl->dl_handle); pl->dl_handle = NULL; unlink(pl->tmp_path); @@ -88,9 +80,6 @@ int plugin_load(plugin_loader_t *pl, const char *path) { pl->plugin = get_plugin(); snprintf(pl->status_msg, sizeof(pl->status_msg), "loaded: %s", pl->plugin->name); - - // FIX: Temporary file leak in plugin_load() when dlsym fails - // unlink(pl->tmp_path) return 0; } diff --git a/C/src/thread_sharing.c b/C/src/thread_sharing.c index 38b261a..5a84894 100644 --- a/C/src/thread_sharing.c +++ b/C/src/thread_sharing.c @@ -1,7 +1,9 @@ /* -Still uses pthread functions, TODO: replace them with raw futex syscalls and clone() +Still uses pthread functions, TODO: replace them with raw futex syscalls and +clone() */ +#include "nolibc.h" #include "ascii.h" #include "capture.h" @@ -9,8 +11,6 @@ Still uses pthread functions, TODO: replace them with raw futex syscalls and clo #include -#include "nolibc.h" - // Producer thread for capturing frames void *capture_thread(void *arg) { shared_frame_t *sf = arg; @@ -53,11 +53,13 @@ void *capture_thread(void *arg) { void *render_thread(void *arg) { shared_frame_t *sf = arg; - // Allocate dynamic buffers - size_t out_size = ascii_out_size(sf->ascii_w, sf->ascii_h, sf->opts.color); - char *out_buf = malloc(out_size); - uint8_t *local_rgb = - sf->opts.color ? malloc(sf->width * sf->height * 3) : NULL; + int braille_w = sf->ascii_w * 2; + int braille_h = sf->ascii_h * 4; + size_t out_size = ascii_out_size(braille_w, braille_h, sf->opts.color); + char *out_buf = nl_malloc(out_size); + + // TODO: extend shared_frame_t to carry an rgb double-buffer alongside gray + uint8_t *local_rgb = NULL; if (!out_buf) { perror("render_thread malloc"); @@ -78,9 +80,9 @@ void *render_thread(void *arg) { pthread_mutex_unlock(&sf->lock); // Process frame outside locked state - int len = grayscale_to_ascii(sf->buf[read_idx], local_rgb, sf->width, - sf->height, sf->ascii_w, sf->ascii_h, out_buf, - out_size, &sf->opts); + int len = + grayscale_to_ascii(sf->buf[read_idx], local_rgb, sf->width, sf->height, + braille_w, braille_h, out_buf, out_size, &sf->opts); if (len > 0) { write(STDOUT_FILENO, "\033[H", 3); // cursor to top-left @@ -92,11 +94,10 @@ void *render_thread(void *arg) { pthread_mutex_unlock(&sf->lock); - // Cleanup memory free(out_buf); - if (local_rgb) { - free(local_rgb); - } + // local_rgb is NULL in this path (see comment above); free is a no-op but + // safe + free(local_rgb); return NULL; } diff --git a/C/src/timing.c b/C/src/timing.c index a8062e1..15e239c 100644 --- a/C/src/timing.c +++ b/C/src/timing.c @@ -1,9 +1,9 @@ +#include "nolibc.h" + #include "timing.h" #include -#include "nolibc.h" - static long frame_duration_ns = 0; // nanoseconds per frame void timing_init(int fps) { frame_duration_ns = 1000000000L / fps; }