Add hot loading plugins system

This commit is contained in:
Harshit-Dhanwalkar 2026-05-25 13:50:49 +05:30
parent 509554ba1d
commit 4c7d32663e
12 changed files with 475 additions and 82 deletions

View file

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

33
C/filters/edge_detect.c Normal file
View file

@ -0,0 +1,33 @@
#include "plugins.h"
#include <stdint.h>
#include <stdlib.h>
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; }

14
C/filters/invert.c Normal file
View file

@ -0,0 +1,14 @@
#include "plugins.h"
#include <stdint.h>
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; }

18
C/filters/threshold.c Normal file
View file

@ -0,0 +1,18 @@
#include "plugins.h"
#include <stdint.h>
#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; }

View file

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

25
C/include/plugins.h Normal file
View file

@ -0,0 +1,25 @@
#ifndef PLUGINS_H
#define PLUGINS_H
#include <stdint.h>
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

View file

@ -1,22 +1,23 @@
#ifndef THREAD_SHARING_H
#define THREAD_SHARING_H
#include "ascii.h"
#include <pthread.h>
#include <stdint.h>
#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

View file

@ -1,6 +1,6 @@
#include "ascii.h"
#include <math.h>
#include <immintrin.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
@ -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);
}

View file

@ -1,4 +1,6 @@
#include "capture.h"
#include "ascii.h"
#include <errno.h>
#include <fcntl.h>
#include <linux/videodev2.h>
@ -10,7 +12,6 @@
#include <sys/mman.h>
#include <sys/select.h>
#include <unistd.h>
#include <immintrin.h>
int webcam_init(webcam_t *cam, const char *device, int width, int height) {
// Open device (nonblocking 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;

View file

@ -2,14 +2,18 @@
#include "capture.h"
#include "thread_sharing.h"
#include "timing.h"
#include "plugins.h"
#include <dlfcn.h>
#include <getopt.h>
#include <pthread.h>
#include <signal.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/inotify.h>
#include <termios.h>
#include <time.h>
#include <unistd.h>
@ -37,40 +41,42 @@ static void print_usage(const char *prog) {
"Usage: %s [options]\n"
"\n"
"Capture options:\n"
" -d <device> video device (default: /dev/video0)\n"
" -w <width> capture width (default: %d)\n"
" -h <height> capture height (default: %d)\n"
" -f <fps> target framerate (default: %d)\n"
" -d <device> video device (default: /dev/video0)\n"
" -w <width> capture width (default: %d) \n"
" -h <height> capture height (default: %d) \n"
" -f <fps> target framerate (default: %d) \n"
"\n"
"Output options:\n"
" -W <width> ASCII output columns (default: %d)\n"
" -H <height> ASCII output rows (default: %d)\n"
" -s <chars> custom charset string (default: \"%s\")\n"
" -W <width> ASCII output columns (default: %d) \n"
" -H <height> ASCII output rows (default: %d) \n"
" -s <chars> custom charset string (default: \"%s\") \n"
" -p <path> filter plugin .so path \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 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);

171
C/src/plugins.c Normal file
View file

@ -0,0 +1,171 @@
#include "plugins.h"
#include <dlfcn.h>
#include <fcntl.h>
#include <libgen.h> // dirname()
#include <stdio.h>
#include <string.h>
#include <sys/inotify.h>
#include <time.h>
#include <unistd.h>
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;
}
}

View file

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