From 4c7d32663e7ab7f53688583be5e27d558f3b1b5a Mon Sep 17 00:00:00 2001 From: Harshit-Dhanwalkar Date: Mon, 25 May 2026 13:50:49 +0530 Subject: [PATCH] Add hot loading plugins system --- C/Makefile | 24 +++++- C/filters/edge_detect.c | 33 +++++++ C/filters/invert.c | 14 +++ C/filters/threshold.c | 18 ++++ C/include/ascii.h | 3 +- C/include/plugins.h | 25 ++++++ C/include/thread_sharing.h | 21 ++--- C/src/ascii.c | 110 +++++++++++++++++++----- C/src/capture.c | 26 ++---- C/src/main.c | 110 ++++++++++++++++++------ C/src/plugins.c | 171 +++++++++++++++++++++++++++++++++++++ README.md | 2 + 12 files changed, 475 insertions(+), 82 deletions(-) create mode 100644 C/filters/edge_detect.c create mode 100644 C/filters/invert.c create mode 100644 C/filters/threshold.c create mode 100644 C/include/plugins.h create mode 100644 C/src/plugins.c diff --git a/C/Makefile b/C/Makefile index f0bb4d3..b0e571c 100644 --- a/C/Makefile +++ b/C/Makefile @@ -2,7 +2,9 @@ CC = gcc CFLAGS = -Wall -Wextra -O2 -Iinclude LDFLAGS = -lm LDFLAGS += -lpthread # Multi-threaded capture-render, pthreads producer/consumer +LDFLAGS += -ldl # Dynamic loading symbols LDFLAGS += -msse4.1 # SIMD - SSE2 for the YUYV to gray conversion +# LDFLAGS += -fvisibility=hidden SRCDIR = src INCDIR = include @@ -12,11 +14,15 @@ OBJDIR = $(BUILDDIR)/obj SOURCES = $(wildcard $(SRCDIR)/*.c) OBJECTS = $(patsubst $(SRCDIR)/%.c,$(OBJDIR)/%.o,$(SOURCES)) TARGET = $(BUILDDIR)/webcam_ascii +PLUGIN_SRCS = $(wildcard filters/*.c) +PLUGIN_TARGETS = $(patsubst filters/%.c,$(BUILDDIR)/%.so,$(PLUGIN_SRCS)) -.PHONY: all clean -all: $(TARGET) +.PHONY: all clean plugins +all: $(TARGET) plugins + +# Main binary $(TARGET): $(OBJECTS) $(CC) $^ -o $@ $(LDFLAGS) @@ -26,5 +32,19 @@ $(OBJDIR)/%.o: $(SRCDIR)/%.c | $(OBJDIR) $(OBJDIR): mkdir -p $(OBJDIR) +# Plugins +plugins: $(PLUGIN_TARGET) + +$(BUILDDIR)/%.so: filters/%.c | $(BUILDDIR) + $(CC) $(CFLAGS) -fPIC -shared $< -o $@.tmp + mv $@.tmp $@ + +plugins: $(PLUGIN_TARGETS) + + +$(BUILDDIR): + mkdir -p $(BUILDDIR) + +# Cleanup clean: rm -rf $(BUILDDIR) diff --git a/C/filters/edge_detect.c b/C/filters/edge_detect.c new file mode 100644 index 0000000..3179120 --- /dev/null +++ b/C/filters/edge_detect.c @@ -0,0 +1,33 @@ +#include "plugins.h" +#include +#include + +static inline int clamp(int v) { return v < 0 ? 0 : v > 255 ? 255 : v; } + +static void do_edge_boost(uint8_t *gray, int w, int h, void *ctx) { + (void)ctx; + // Unsharp mask: sharpened = original + (original - blurred) * strength + uint8_t *tmp = malloc(w * h); + if (!tmp) + return; + + // 3x3 box blur + for (int y = 1; y < h - 1; y++) { + for (int x = 1; x < w - 1; x++) { + int sum = 0; + for (int dy = -1; dy <= 1; dy++) + for (int dx = -1; dx <= 1; dx++) + sum += gray[(y + dy) * w + (x + dx)]; + tmp[y * w + x] = (uint8_t)(sum / 9); + } + } + + // Sharpen: original + 0.5 * (original - blur) + for (int i = w + 1; i < w * (h - 1) - 1; i++) + gray[i] = (uint8_t)clamp(gray[i] + (gray[i] - tmp[i]) / 2); + + free(tmp); +} + +static filter_plugin_t self = {do_edge_boost, "edge_boost"}; +filter_plugin_t *plugin_get(void) { return &self; } diff --git a/C/filters/invert.c b/C/filters/invert.c new file mode 100644 index 0000000..3bc4b4e --- /dev/null +++ b/C/filters/invert.c @@ -0,0 +1,14 @@ +#include "plugins.h" +#include + +static void do_invert(uint8_t *gray, int w, int h, void *ctx) { + (void)ctx; + int total_pixels = w * h; + for (int i = 0; i < total_pixels; i++) { + gray[i] = 255 - gray[i]; // Flip brightness + } +} + +static filter_plugin_t self = {.process = do_invert, .name = "Invert"}; + +filter_plugin_t *plugin_get(void) { return &self; } diff --git a/C/filters/threshold.c b/C/filters/threshold.c new file mode 100644 index 0000000..6e7edf4 --- /dev/null +++ b/C/filters/threshold.c @@ -0,0 +1,18 @@ +#include "plugins.h" +#include + +#define DEFAULT_THRESH 128 + +static void thresh_process(uint8_t *gray, int w, int h, void *ctx) { + uint8_t thresh = (uint8_t)(ctx ? *(int *)ctx : DEFAULT_THRESH); + int total = w * h; + for (int i = 0; i < total; i++) + gray[i] = (gray[i] > thresh) ? 255 : 0; +} + +static filter_plugin_t self = { + .process = thresh_process, + .name = "threshold", +}; + +filter_plugin_t *plugin_get(void) { return &self; } diff --git a/C/include/ascii.h b/C/include/ascii.h index 1f88e5d..2281e91 100644 --- a/C/include/ascii.h +++ b/C/include/ascii.h @@ -17,7 +17,8 @@ typedef struct { } 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(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 diff --git a/C/include/plugins.h b/C/include/plugins.h new file mode 100644 index 0000000..bae4985 --- /dev/null +++ b/C/include/plugins.h @@ -0,0 +1,25 @@ +#ifndef PLUGINS_H +#define PLUGINS_H + +#include + +typedef struct { + void (*process)(uint8_t *gray, int w, int h, void *ctx); + const char *name; +} filter_plugin_t; + +typedef struct { + void *dl_handle; // handle from dlopen + filter_plugin_t *plugin; // resolved plugin vtable + char path[256]; // absolute path to .so + char tmp_path[280]; // temp copy path used for current dlopen // HACK: + int inotify_fd; // inotify instance fd + int inotify_wd; // watch descriptor +} plugin_loader_t; + +int plugin_load (plugin_loader_t *pl, const char *path); +void plugin_watch_init (plugin_loader_t *pl, const char *path); +void plugin_check_reload(plugin_loader_t *pl); +void plugin_cleanup (plugin_loader_t *pl); + +#endif diff --git a/C/include/thread_sharing.h b/C/include/thread_sharing.h index 9c223e0..4af4f83 100644 --- a/C/include/thread_sharing.h +++ b/C/include/thread_sharing.h @@ -1,22 +1,23 @@ #ifndef THREAD_SHARING_H +#define THREAD_SHARING_H +#include "ascii.h" #include #include -#include "ascii.h" typedef struct { - uint8_t *buf[2]; // Double buffer - int width, height; - int ascii_w, ascii_h; - int ready_idx; - int has_frame; + uint8_t *buf[2]; // Double buffer, one slot per thread + int width, height; // capture dimensions + int ascii_w, ascii_h; + int ready_idx; // which slot has the freshest frame + int has_frame; // non-zero once producer has written once pthread_mutex_t lock; - pthread_cond_t cond; - volatile int stop; - ascii_opts_t opts; + pthread_cond_t cond; + volatile int stop; + ascii_opts_t opts; } shared_frame_t; void *capture_thread(void *arg); -void *render_thread (void *arg); +void *render_thread(void *arg); #endif diff --git a/C/src/ascii.c b/C/src/ascii.c index 162f9c3..a335853 100644 --- a/C/src/ascii.c +++ b/C/src/ascii.c @@ -1,6 +1,6 @@ #include "ascii.h" -#include +#include #include #include #include @@ -13,10 +13,31 @@ static inline uint8_t clamp_u8(int v) { } // Image conversion -void yuyv_to_gray(const uint8_t *yuyv, uint8_t *gray, int width, int height) { - int n = width * height; - for (int i = 0; i < n; i++) - gray[i] = yuyv[i * 2]; // Y sample at even bytes in YUYV format +// void yuyv_to_gray(const uint8_t *yuyv, uint8_t *gray, int width, int height) { +// int n = width * height; +// for (int i = 0; i < n; i++) +// gray[i] = yuyv[i * 2]; // Y sample at even bytes in YUYV format +// } +void yuyv_to_gray_simd(const uint8_t *yuyv, uint8_t *gray, int width, int height) { + int total_pixels = width * height; + + // Mask to extract every even byte (Y samples) + __m128i mask = _mm_set1_epi16(0x00FF); + + int i = 0; + for (; i + 16 <= total_pixels; i += 16) { + // Load 32 bytes of YUYV (= 16 pixels) + __m128i lo = _mm_loadu_si128((__m128i *)(yuyv + i*2)); + __m128i hi = _mm_loadu_si128((__m128i *)(yuyv + i*2 + 16)); + // low byte of each 16-bit word (the Y sample) + lo = _mm_and_si128(lo, mask); + hi = _mm_and_si128(hi, mask); + // Pack 16-bit + __m128i result = _mm_packus_epi16(lo, hi); + _mm_storeu_si128((__m128i *)(gray + i), result); + } + for (; i < total_pixels; i++) + gray[i] = yuyv[i * 2]; } void yuyv_to_rgb(const uint8_t *yuyv, uint8_t *rgb, int width, int height) { @@ -38,6 +59,49 @@ void yuyv_to_rgb(const uint8_t *yuyv, uint8_t *rgb, int width, int height) { } } } +// void yuyv_to_rgb_simd(const uint8_t *yuyv, uint8_t *rgb, int width, int height) { +// int total_bytes = width * height * 2; +// int i = 0; +// +// // Vectors of constants for color weights shifted up by 8 bits +// __m128i r_v_weight = _mm_set1_epi16(409); +// __m128i g_u_weight = _mm_set1_epi16(-100); +// __m128i g_v_weight = _mm_set1_epi16(-208); +// __m128i b_u_weight = _mm_set1_epi16(516); +// __m128i const_128 = _mm_set1_epi16(128); +// +// for (; i + 8 <= total_bytes; i += 8) { +// // Load 8 bytes of YUYV = [Y0, U0, Y1, V0, Y2, U1, Y3, V1] +// // Cast directly into a 64-bit load to avoid polluting register spaces +// long long chunk = *(const long long *)(yuyv + i); +// __m128i raw = _mm_cvtsi64_si128(chunk); +// +// // Unpack bytes to 16-bit integers to hold intermediate precision math +// __m128i pixels = _mm_unpacklo_epi8(raw, _mm_setzero_si128()); +// +// // Extract channels across structural layouts +// int16_t y0 = _mm_extract_epi16(pixels, 0) - 16; +// int16_t u0 = _mm_extract_epi16(pixels, 1) - 128; +// int16_t y1 = _mm_extract_epi16(pixels, 2) - 16; +// int16_t v0 = _mm_extract_epi16(pixels, 3) - 128; +// +// int16_t y2 = _mm_extract_epi16(pixels, 4) - 16; +// int16_t u1 = _mm_extract_epi16(pixels, 5) - 128; +// int16_t y3 = _mm_extract_epi16(pixels, 6) - 16; +// int16_t v1 = _mm_extract_epi16(pixels, 7) - 128; +// +// // Perform fixed-point math approximations using scalar elements or smaller vector groupings +// // Example for first pixel pair layout: +// int32_t r0 = clamp_u8((298 * y0 + 409 * v0 + 128) >> 8); +// int32_t g0 = clamp_u8((298 * y0 - 100 * u0 - 208 * v0 + 128) >> 8); +// int32_t b0 = clamp_u8((298 * y0 + 516 * u0 + 128) >> 8); +// +// // Write sequentially to interleaved pointer layout +// int rgb_offset = (i / 4) * 6; +// rgb[rgb_offset + 0] = r0; rgb[rgb_offset + 1] = g0; rgb[rgb_offset + 2] = b0; +// // Continue similarly down line iterations... +// } +// } // Buffer sizing size_t ascii_out_size(int dst_w, int dst_h, int color) { @@ -89,23 +153,22 @@ static void sobel(const uint8_t *in, uint8_t *out, int w, int h) { 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 brightness = opts ? opts->brightness : 0; - int contrast = opts ? opts->contrast : 100; - int invert = opts ? opts->invert : 0; - int do_color = opts && opts->color && (rgb != NULL); - int do_edges = opts ? opts->edges : 0; - int do_dither = opts ? opts->dither : 0; + const char *charset = (opts && opts->charset) ? opts->charset : ASCII_CHARS_DEFAULT; + int nchars = (int)strlen(charset); + 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 != NULL); + int do_edges = opts ? opts->edges : 0; + int do_dither = opts ? opts->dither : 0; // Blocking width and height pixels in source pixels - double bw = (double)src_w / dst_w; - double bh = (double)src_h / dst_h; + double bw = (double)src_w / dst_w; + double bh = (double)src_h / dst_h; // Downsample to (dst_w x dst_h) - uint8_t *small_g = malloc(dst_w * dst_h); - uint8_t *small_rgb = do_color ? malloc(dst_w * dst_h * 3) : NULL; + uint8_t *small_g = malloc(dst_w * dst_h); + uint8_t *small_rgb = do_color ? malloc(dst_w * dst_h * 3) : NULL; if (!small_g || (do_color && !small_rgb)) { free(small_g); @@ -258,7 +321,7 @@ int grayscale_to_ascii(const uint8_t *gray, const uint8_t *rgb, int src_w, } } - out[(size_t)out_idx < out_size ? out_idx : out_size - 1] = '\0'; + out[(size_t)out_idx < out_size ? (size_t)out_idx : out_size - 1] = '\0'; free(small_g); free(small_rgb); @@ -275,12 +338,13 @@ void overlay_fps_box(int dst_w, double fps, int color_enabled) { int n; if (color_enabled) { n = snprintf(buf, sizeof(buf), - "\033[1;%dH\033[38;2;0;255;0m\033[48;2;30;30;30m" - "[ FPS: %4.1f ]\033[0m", + "\033[1;%dH\033[38;2;0;255;0m\033[48;2;30;30;30m[ FPS: %4.1f ]\033[0m", col, fps); } else { - n = snprintf(buf, sizeof(buf), "\033[1;%dH[ FPS: %4.1f ]", col, fps); + n = snprintf(buf, sizeof(buf), + "\033[1;%dH[ FPS: %4.1f ]", + col, fps); } if (n > 0 && n < (int)sizeof(buf)) - write(STDOUT_FILENO, buf, (size_t)n); + (void)write(STDOUT_FILENO, buf, (size_t)n); } diff --git a/C/src/capture.c b/C/src/capture.c index b8e1109..2d1c82e 100644 --- a/C/src/capture.c +++ b/C/src/capture.c @@ -1,4 +1,6 @@ #include "capture.h" +#include "ascii.h" + #include #include #include @@ -10,7 +12,6 @@ #include #include #include -#include int webcam_init(webcam_t *cam, const char *device, int width, int height) { // Open device (non‑blocking for select usage) @@ -98,24 +99,6 @@ int webcam_wait_frame(webcam_t *cam, int timeout_ms) { return 0; } -void yuyv_to_gray_simd(const uint8_t *yuyv, uint8_t *gray, int n) { - // Mask to extract every even byte (Y samples) - __m128i mask = _mm_set1_epi16(0x00FF); - int i = 0; - for (; i + 16 <= n; i += 16) { - // Load 32 bytes of YUYV (= 16 pixels) - __m128i lo = _mm_loadu_si128((__m128i *)(yuyv + i*2)); - __m128i hi = _mm_loadu_si128((__m128i *)(yuyv + i*2 + 16)); - // low byte of each 16-bit word (the Y sample) - lo = _mm_and_si128(lo, mask); - hi = _mm_and_si128(hi, mask); - // Pack 16-bit - __m128i result = _mm_packus_epi16(lo, hi); - _mm_storeu_si128((__m128i *)(gray + i), result); - } - for (; i < n; i++) gray[i] = yuyv[i * 2]; // scalar tail -} - int webcam_capture_frame(webcam_t *cam, uint8_t *gray_buffer) { // Dequeue buffer struct v4l2_buffer buf = cam->buf_info; @@ -126,8 +109,9 @@ int webcam_capture_frame(webcam_t *cam, uint8_t *gray_buffer) { // for (int i = 0, j = 0; i < cam->width * cam->height * 2; i += 2, j++) { // gray_buffer[j] = yuyv[i]; // } - int total_pixels = cam->width * cam->height; - yuyv_to_gray_simd((uint8_t *)cam->buffer, gray_buffer, total_pixels); + int w = cam->width; + int h = cam->height; + yuyv_to_gray_simd((uint8_t *)cam->buffer, gray_buffer, w, h); // Store updated buffer info for requeue cam->buf_info = buf; diff --git a/C/src/main.c b/C/src/main.c index ab1ee67..d9e48d4 100644 --- a/C/src/main.c +++ b/C/src/main.c @@ -2,14 +2,18 @@ #include "capture.h" #include "thread_sharing.h" #include "timing.h" +#include "plugins.h" +#include #include #include #include #include #include #include +#include #include +#include #include #include #include @@ -37,40 +41,42 @@ static void print_usage(const char *prog) { "Usage: %s [options]\n" "\n" "Capture options:\n" - " -d video device (default: /dev/video0)\n" - " -w capture width (default: %d)\n" - " -h capture height (default: %d)\n" - " -f target framerate (default: %d)\n" + " -d video device (default: /dev/video0)\n" + " -w capture width (default: %d) \n" + " -h capture height (default: %d) \n" + " -f target framerate (default: %d) \n" "\n" "Output options:\n" - " -W ASCII output columns (default: %d)\n" - " -H ASCII output rows (default: %d)\n" - " -s custom charset string (default: \"%s\")\n" + " -W ASCII output columns (default: %d) \n" + " -H ASCII output rows (default: %d) \n" + " -s custom charset string (default: \"%s\") \n" + " -p filter plugin .so path \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 brightness->charset mapping\n" - " -e enable Sobel edge detection\n" - " -C colour output (ANSI truecolor)\n" - " -D Floyd-Steinberg dithering\n", - prog, DEFAULT_CAPTURE_WIDTH, DEFAULT_CAPTURE_HEIGHT, DEFAULT_FPS, - DEFAULT_ASCII_WIDTH, DEFAULT_ASCII_HEIGHT, ASCII_CHARS_DEFAULT); // FIX: undeclared identifier ASCII_CHARS_DEFAULT + " -i invert mapping \n" + " -e enable Sobel edge detection \n" + " -C ANSI truecolor output \n" + " -D Floyd-Steinberg dithering \n", + prog, + DEFAULT_CAPTURE_WIDTH, DEFAULT_CAPTURE_HEIGHT, DEFAULT_FPS, + DEFAULT_ASCII_WIDTH, DEFAULT_ASCII_HEIGHT, ASCII_CHARS_DEFAULT); } // termios void term_raw_mode(void) { - tcgetattr(STDOUT_FILENO, &orig_terminal); // save stdin state + tcgetattr(STDIN_FILENO, &orig_terminal); // save stdin state struct termios raw = orig_terminal; - raw.c_lflag &= ~(ICANON | ECHO); // no line buffering or no echo - raw.c_cc[VMIN] = 0; // non-blocking read - raw.c_cc[VTIME] = 0; + raw.c_lflag &= ~(ICANON | ECHO); // no line buffering or no echo + raw.c_cc[VMIN] = 0; // non-blocking read + raw.c_cc[VTIME] = 0; tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw); } void term_restore(void) { tcsetattr(STDIN_FILENO, TCSAFLUSH, &orig_terminal); } -fps_counter_t fps_calc = {0}; // Global FPS counter // FIX: +fps_counter_t fps_calc = {0}; // Main int main(int argc, char *argv[]) { @@ -85,7 +91,7 @@ int main(int argc, char *argv[]) { int cap_h = DEFAULT_CAPTURE_HEIGHT; int fps = DEFAULT_FPS; - ascii_opts_t opts = { // FIX: undcleared identifier 'ascii_opts_t' + ascii_opts_t opts = { .brightness = 0, .contrast = 100, .invert = 0, @@ -95,9 +101,14 @@ int main(int argc, char *argv[]) { .charset = NULL, }; + // Plugins + // plugin_loader_t pl; + // memset(&pl, 0, sizeof(plugin_loader_t)); + const char *plugin_path = NULL; + // CLI parsing int opt; - while ((opt = getopt(argc, argv, "ed:W:H:w:h:f:b:c:iCDs:")) != -1) { + while ((opt = getopt(argc, argv, "d:W:H:w:h:f:b:c:iCDs:p:")) != -1) { switch (opt) { case 'd': device = optarg; @@ -150,6 +161,9 @@ int main(int argc, char *argv[]) { case 's': opts.charset = optarg; break; + case 'p': + plugin_path = optarg; + break; default: print_usage(argv[0]); return 1; @@ -158,6 +172,26 @@ int main(int argc, char *argv[]) { timing_init(fps); + // Initialize plugin system + plugin_loader_t pl; + memset(&pl, 0, sizeof(pl)); + pl.inotify_fd = -1; + + if (plugin_path) { + plugin_load(&pl, plugin_path); + plugin_watch_init(&pl, plugin_path); + } + + // void plugin_check_reload(plugin_loader_t *pl) { + // char buf[sizeof(struct inotify_event) + 256]; + // if (read(pl->inotify_fd, buf, sizeof(buf)) > 0) { + // usleep(100000); + // if (plugin_load(pl, pl->path) == 0) { + // fprintf(stderr, "Successfully hot-swapped filter plugin: [%s]\n", pl->plugin->name); + // } + // } + // }; + // Open webcam webcam_t cam = {.fd = -1, .buffer = MAP_FAILED}; if (webcam_init(&cam, device, cap_w, cap_h) < 0) { @@ -169,7 +203,8 @@ int main(int argc, char *argv[]) { opts.color ? " | color" : "", opts.edges ? " | edges" : "", opts.dither ? " | dither" : "", - opts.invert ? " | inverted": ""); + opts.invert ? " | inverted": "", + pl.plugin ? " | plugin" : ""); // Allocate pixel buffers int cam_pixels = cam.width * cam.height; @@ -185,7 +220,7 @@ int main(int argc, char *argv[]) { // Allocate output string buffer size_t out_size = ascii_out_size(ascii_w, ascii_h, opts.color); - char *out_buf = malloc(out_size); + char *out_buf = malloc(out_size); if (!out_buf) { perror("malloc out_buf"); free(gray); @@ -194,8 +229,27 @@ int main(int argc, char *argv[]) { return 1; } + // // Thead sharing + // shared_frame_t sf = {0}; + // sf.buf[0] = malloc(cam_pixels); + // sf.buf[1] = malloc(cam_pixels); + // sf.width = cam.width; sf.height = cam.height; + // sf.ascii_w = ascii_w; sf.ascii_h = ascii_h; + // sf.opts = opts; + // pthread_mutex_init(&sf.lock, NULL); + // pthread_cond_init(&sf.cond, NULL); + // + // pthread_t tid_cap, tid_render; + // pthread_create(&tid_cap, NULL, capture_thread, &sf); + // pthread_create(&tid_render, NULL, render_thread, &sf); + // + // sf.stop = 1; + // pthread_cond_broadcast(&sf.cond); + // pthread_join(tid_cap, NULL); + // pthread_join(tid_render, NULL); + // Initial full clear - write(STDOUT_FILENO, "\033[2J\033[H\033[?25l", 13); + (void)write(STDOUT_FILENO, "\033[2J\033[H\033[?25l", 13); term_raw_mode(); // Main loop @@ -224,6 +278,9 @@ int main(int argc, char *argv[]) { } } + if (plugin_path) + plugin_check_reload(&pl); + if (webcam_wait_frame(&cam, 1000) < 0) continue; // timeout, retry @@ -232,6 +289,9 @@ int main(int argc, char *argv[]) { break; } + if (pl.plugin) + pl.plugin->process(gray, cam.width, cam.height, NULL); + if (opts.color && rgb) yuyv_to_rgb((const uint8_t *)cam.buffer, rgb, cam.width, cam.height); @@ -239,7 +299,7 @@ int main(int argc, char *argv[]) { ascii_h, out_buf, out_size, &opts); if (len > 0) { - write(STDOUT_FILENO, out_buf, (size_t)len); + (void)write(STDOUT_FILENO, out_buf, (size_t)len); overlay_fps_box(ascii_w, current_fps, opts.color); } @@ -253,7 +313,7 @@ int main(int argc, char *argv[]) { // Cleanup term_restore(); - write(STDOUT_FILENO, "\033[0m\033[?25h\n", 11); + (void)write(STDOUT_FILENO, "\033[0m\033[?25h\n", 11); fprintf(stderr, "Stopped.\n"); free(gray); diff --git a/C/src/plugins.c b/C/src/plugins.c new file mode 100644 index 0000000..ce3374b --- /dev/null +++ b/C/src/plugins.c @@ -0,0 +1,171 @@ +#include "plugins.h" + +#include +#include +#include // dirname() +#include +#include +#include +#include +#include + +static int copy_file(const char *src, const char *dst) { + int fd_src = open(src, O_RDONLY); + if (fd_src < 0) { + perror("[plugin] open src"); + return -1; + } + + int fd_dst = open(dst, O_WRONLY | O_CREAT | O_TRUNC, 0755); + if (fd_dst < 0) { + perror("[plugin] open dst"); + close(fd_src); + return -1; + } + + char buf[65536]; + ssize_t n; + while ((n = read(fd_src, buf, sizeof(buf))) > 0) { + if (write(fd_dst, buf, (size_t)n) != n) { + perror("[plugin] write"); + close(fd_src); + close(fd_dst); + unlink(dst); + return -1; + } + } + + close(fd_src); + close(fd_dst); + return 0; +} + +int plugin_load(plugin_loader_t *pl, const char *path) { + // Close old handle + if (pl->dl_handle) { + dlclose(pl->dl_handle); + pl->dl_handle = NULL; + pl->plugin = NULL; + } + + // Delete the previous temp copy + if (pl->tmp_path[0] != '\0') { + unlink(pl->tmp_path); + pl->tmp_path[0] = '\0'; + } + + snprintf(pl->tmp_path, sizeof(pl->tmp_path), "%s.%ld.tmp", path, + (long)time(NULL)); + + if (copy_file(path, pl->tmp_path) < 0) { + fprintf(stderr, "[plugin] could not copy %s -> %s\n", path, pl->tmp_path); + pl->tmp_path[0] = '\0'; + return -1; + } + + pl->dl_handle = dlopen(pl->tmp_path, RTLD_NOW | RTLD_LOCAL); + if (!pl->dl_handle) { + fprintf(stderr, "[plugin] dlopen: %s\n", dlerror()); + unlink(pl->tmp_path); + pl->tmp_path[0] = '\0'; + 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()); + dlclose(pl->dl_handle); + pl->dl_handle = NULL; + unlink(pl->tmp_path); + pl->tmp_path[0] = '\0'; + return -1; + } + + pl->plugin = get_plugin(); + fprintf(stderr, "[plugin] loaded: %s\n", pl->plugin->name); + return 0; +} + +void plugin_watch_init(plugin_loader_t *pl, const char *path) { + strncpy(pl->path, path, sizeof(pl->path) - 1); + + char dir_copy[256]; + strncpy(dir_copy, path, sizeof(dir_copy) - 1); + const char *dir = dirname(dir_copy); + + pl->inotify_fd = inotify_init1(IN_NONBLOCK); + if (pl->inotify_fd < 0) { + perror("[plugin] inotify_init1"); + return; + } + + pl->inotify_wd = + inotify_add_watch(pl->inotify_fd, dir, IN_CLOSE_WRITE | IN_MOVED_TO); + + if (pl->inotify_wd < 0) + perror("[plugin] inotify_add_watch"); +} + +void plugin_check_reload(plugin_loader_t *pl) { + if (pl->inotify_fd < 0) + return; + + // Read all available events from the non-blocking queue + char buf[4096] __attribute__((aligned(__alignof__(struct inotify_event)))); + // char buf[sizeof(struct inotify_event) + 256]; + ssize_t n = read(pl->inotify_fd, buf, sizeof(buf)); + if (n <= 0) + return; // No new filesystem modifications detected + + char path_copy[256]; + strncpy(path_copy, pl->path, sizeof(path_copy) - 1); + const char *soname = basename(path_copy); + + int relevant = 0; + const char *p = buf; + while (p < buf + n) { + const struct inotify_event *ev = (const struct inotify_event *)p; + if (ev->len > 0 && strcmp(ev->name, soname) == 0) { + relevant = 1; + break; + } + p += sizeof(*ev) + ev->len; + } + + if (!relevant) + return; + + usleep(150000); // 150ms delay for linker + + if (plugin_load(pl, pl->path) == 0) { + fprintf(stderr, "\n[plugin] Hot-swapped: %s\n", pl->plugin->name); + } else { + fprintf(stderr, "\n[plugin] Hot-swap failed. Retaining active filter.\n"); + } +} + +void plugin_cleanup(plugin_loader_t *pl) { + if (pl->dl_handle) { + dlclose(pl->dl_handle); + pl->dl_handle = NULL; + } + + // Remove tmp copy + if (pl->tmp_path[0] != '\0') { + unlink(pl->tmp_path); + pl->tmp_path[0] = '\0'; + } + + if (pl->inotify_fd >= 0) { + close(pl->inotify_fd); + pl->inotify_fd = -1; + } +} diff --git a/README.md b/README.md index 491a0f2..de75535 100644 --- a/README.md +++ b/README.md @@ -25,5 +25,7 @@ cd build/ - [ ] Add feature to record and save it in popular video formats like `.mp4`, `.mov` and `.gif`. - [x] Dithering effect. - [x] Sobel edge detection (kernel convolution). Algorithm reference: https://homepages.inf.ed.ac.uk/rbf/HIPR2/sobel.htm +- [x] SIMD for YUYV to grayscale coversion. +- [x] Hot-reloading plugin system - [ ] Migrate from C to Cpp.