Add hot-reloadable charsets, expand output buffer sizing and macOS build support

This commit is contained in:
Harshit-Dhanwalkar 2026-06-17 15:52:43 +05:30
parent b179ccb32c
commit 9963f01810
10 changed files with 897 additions and 163 deletions

View file

@ -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 \

View file

@ -5,24 +5,84 @@
#include <stdint.h>
#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

View file

@ -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 = \

View file

@ -3,18 +3,36 @@
// - x86_64 Linux/macOS: SSE2 via <immintrin.h>
// - ARM64 macOS: NEON via <arm_neon.h>
#include "ascii.h"
#include <stdint.h>
#include "nolibc.h"
#include "ascii.h"
#include "platform.h"
#include <stdint.h>
#include <sys/inotify.h>
// 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 <immintrin.h>
@ -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;

View file

@ -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 <linux/videodev2.h>
#include <stdint.h>
@ -126,4 +127,4 @@ void webcam_cleanup(webcam_t *cam) {
cam->impl = (webcam_impl_t *)0;
}
#endif /* PLATFORM_LINUX */
#endif

View file

@ -1,14 +1,14 @@
#ifdef PLATFORM_MACOS
#include "capture.h"
#include "platform.h"
#ifdef PLATFORM_MACOS
#include <pthread.h>
#include <stdlib.h>
#include <string.h>
#import <AVFoundation/AVFoundation.h>
#import <CoreMedia/CoreMedia.h>
#import <CoreVideo/CoreVideo.h>
#include <pthread.h>
#include <stdlib.h>
#include <string.h>
#define FRAME_BUFS 2

View file

@ -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 <pthread.h>
#include <stdint.h>
#include <time.h>
@ -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 <height> ASCII output rows (default: %d) \n"
" -s <chars> custom charset string (default: \"%s\") \n"
" -p <path> filter plugin .so path \n"
" -m <mode> render mode: braille|blocks|ascii|halfblock|dots\n"
" -k <dir> charset directory (hot-reloadable .txt ramps) \n"
"\n"
"Image adjustments:\n"
" -b <val> brightness offset -128..128 (default: 0)\n"
" -c <val> contrast in percent >0; 100=none (default: 100)\n"
" -i invert mapping \n"
" -e enable Sobel edge detection \n"
" -E <mode> 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;
}

View file

@ -1,10 +1,10 @@
#include "nolibc.h"
#include "plugins.h"
#include <dlfcn.h>
#include <sys/inotify.h>
#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;
}

View file

@ -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 <stdint.h>
#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;
}

View file

@ -1,9 +1,9 @@
#include "nolibc.h"
#include "timing.h"
#include <time.h>
#include "nolibc.h"
static long frame_duration_ns = 0; // nanoseconds per frame
void timing_init(int fps) { frame_duration_ns = 1000000000L / fps; }