From 14cc7dcc2e4dc960f5bab0144539021fa0dac8e9 Mon Sep 17 00:00:00 2001 From: Harshit-Dhanwalkar Date: Wed, 10 Jun 2026 20:30:10 +0530 Subject: [PATCH] Add macos support --- C/Makefile | 189 ++++++++++++----- C/include/capture.h | 16 +- C/include/platform.h | 20 ++ C/lib/nl_io.h | 35 +++- C/lib/nl_printf.c | 7 +- C/lib/nl_printf.h | 17 +- C/src/ascii.c | 202 ++++++++---------- C/src/{capture.c => capture_linux.c} | 72 +++---- C/src/capture_macos.c | 297 +++++++++++++++++++++++++++ C/src/main.c | 11 +- 10 files changed, 616 insertions(+), 250 deletions(-) create mode 100644 C/include/platform.h rename C/src/{capture.c => capture_linux.c} (64%) create mode 100644 C/src/capture_macos.c diff --git a/C/Makefile b/C/Makefile index 83a90f7..f752387 100644 --- a/C/Makefile +++ b/C/Makefile @@ -1,81 +1,156 @@ -CC = gcc -CFLAGS = -Wall -Wextra -O2 -Iinclude -Ilib +# AsciiCam cross-platform build +# Supports: +# Linux x86_64 (gcc, nolibc, V4L2) +# macOS ARM64 (clang, system libc, AVFoundation) +# macOS x86_64 (clang, system libc, AVFoundation) UNAME_S := $(shell uname -s) +UNAME_M := $(shell uname -m) + ifeq ($(UNAME_S),Linux) - CFLAGS += -fno-stack-protector -D__LINUX_NOLIBC__ - # LDFLAGS = -lm - LDFLAGS += -nostdlib # drops crt startup files and default libs - # LDFLAGS += -nodefaultlibs # drops default libs but keeps crt startup files - LDFLAGS += -Wl,--no-as-needed - LDLIBS := -ldl -lpthread -lc - # LDFLAGS += -Wl,-rpath,/lib/x86_64-linux-gnu - # LDFLAGS += /lib/x86_64-linux-gnu/libdl.so.2 - LDFLAGS += -Wl,-dynamic-linker,/lib64/ld-linux-x86-64.so.2 - LDFLAGS += /usr/lib/x86_64-linux-gnu/crti.o - LDFLAGS += /usr/lib/x86_64-linux-gnu/crtn.o - LDFLAGS += -ldl # Dynamic loading symbols - LDFLAGS += -lpthread # Multi-threaded capture-render, pthreads producer/consumer - LDFLAGS += -lc - LDFLAGS += -msse4.1 # SIMD - SSE2 for the YUYV to gray conversion - # LDFLAGS += -static - LIBSRCS = $(wildcard lib/*.c) + PLATFORM := linux + CC := gcc + LD := gcc else ifeq ($(UNAME_S),Darwin) - CFLAGS += - LDFLAGS += - LDLIBS := - LIBSRCS = + PLATFORM := macos + CC := clang + LD := clang + OBJC := clang +else + $(error Unsupported platform: $(UNAME_S)) endif -SRCDIR = src -INCDIR = include -LIBDIR = lib -BUILDDIR = build -OBJDIR = $(BUILDDIR)/obj +SRCDIR := src +LIBDIR := lib +FILTERDIR := filters +BUILDDIR := build +OBJDIR := $(BUILDDIR)/obj -LIBSRCS = $(wildcard $(LIBDIR)/*.c) -SOURCES = $(wildcard $(SRCDIR)/*.c) $(LIBSRCS) -OBJECTS = $(patsubst $(SRCDIR)/%.c,$(OBJDIR)/%.o,$(SOURCES)) -TARGET = $(BUILDDIR)/webcam_ascii -PLUGIN_SRCS = $(wildcard filters/*.c) -PLUGIN_C_SRCS = $(filter-out filters/%.h, $(PLUGIN_SRCS)) -PLUGIN_TARGETS = $(patsubst filters/%.c,$(BUILDDIR)/%.so,$(PLUGIN_C_SRCS)) +CFLAGS_COMMON := -Wall -Wextra -O2 -Iinclude -I$(LIBDIR) -fno-stack-protector -fno-builtin-memcpy -fno-builtin-memset -fno-builtin-strlen +CFLAGS_COMMON += -# Security Testing Targets -TEST_TARGET = $(BUILDDIR)/security_tests -TEST_SRC = ../tests/security_tests.c -FILTER_SRCS = filters/edge_detect.c filters/invert.c filters/threshold.c +ifeq ($(PLATFORM),linux) + # Linux: nolibc, V4L2, SSE4.1 + CFLAGS := $(CFLAGS_COMMON) -DPLATFORM_LINUX -D__LINUX_NOLIBC__ + CFLAGS += -msse4.1 -.PHONY: all clean plugins test + LDFLAGS := -nostdlib + 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 += -msse4.1 + + LIBSRCS := $(LIBDIR)/nl_alloc.c \ + $(LIBDIR)/nl_errno.c \ + $(LIBDIR)/nl_getopt.c \ + $(LIBDIR)/nl_printf.c \ + $(LIBDIR)/nl_start.c + + PLAT_SRC := $(SRCDIR)/capture_linux.c + + # Plugin shared objects + SO_EXT := so + SO_FLAGS := -fPIC -shared +else ifeq ($(PLATFORM),macos) + # macOS: system libc + AVFoundation, supports ARM64 + x86_64 + CFLAGS := $(CFLAGS_COMMON) -DPLATFORM_MACOS + OBJCFLAGS:= $(CFLAGS) -x objective-c \ + -fobjc-arc \ + -fmodules + + ifeq ($(UNAME_M),arm64) + CFLAGS += -arch arm64 + LDFLAGS := -arch arm64 + else + CFLAGS += -arch x86_64 -msse4.1 + LDFLAGS := -arch x86_64 + endif + + LDFLAGS += -framework AVFoundation \ + -framework CoreMedia \ + -framework CoreVideo \ + -framework Foundation \ + -lpthread + + # TODO: Update nolibc for macos support instead of system system libc, currently : + # No nolibc on macOS, uses system libc + LIBSRCS := + + PLAT_SRC := $(SRCDIR)/capture_macos.m + + # Plugin shared objects + SO_EXT := dylib + SO_FLAGS := -dynamiclib +endif + +CORE_SRCS := $(SRCDIR)/ascii.c \ + $(SRCDIR)/main.c \ + $(SRCDIR)/plugins.c \ + $(SRCDIR)/thread_sharing.c \ + $(SRCDIR)/timing.c + +ALL_C_SRCS := $(CORE_SRCS) $(PLAT_SRC) $(LIBSRCS) + +OBJECTS := $(patsubst $(SRCDIR)/%.c, $(OBJDIR)/%.o, $(CORE_SRCS)) +OBJECTS += $(patsubst $(SRCDIR)/%.c, $(OBJDIR)/%.o, $(filter $(SRCDIR)/%.c, $(PLAT_SRC))) +OBJECTS += $(patsubst $(SRCDIR)/%.m, $(OBJDIR)/%.o, $(filter $(SRCDIR)/%.m, $(PLAT_SRC))) +OBJECTS += $(patsubst $(LIBDIR)/%.c, $(OBJDIR)/lib_%.o, $(LIBSRCS)) + +TARGET := $(BUILDDIR)/webcam_ascii + +# Plugins +PLUGIN_SRCS := $(wildcard $(FILTERDIR)/*.c) +PLUGIN_TARGETS := $(patsubst $(FILTERDIR)/%.c, $(BUILDDIR)/%.$(SO_EXT), $(PLUGIN_SRCS)) + +.PHONY: all clean plugins info all: $(TARGET) plugins -# Main binary -$(TARGET): $(OBJECTS) - $(CC) $^ -o $@ $(LDFLAGS) +$(TARGET): $(OBJECTS) | $(BUILDDIR) + $(LD) $^ -o $@ $(LDFLAGS) + @echo " LD $@ ($(PLATFORM)/$(UNAME_M))" +# Compile .c $(OBJDIR)/%.o: $(SRCDIR)/%.c | $(OBJDIR) $(CC) $(CFLAGS) -c $< -o $@ +# Compile .m (Objective-C for macOS capture) +$(OBJDIR)/%.o: $(SRCDIR)/%.m | $(OBJDIR) + $(OBJC) $(OBJCFLAGS) -c $< -o $@ + +# Compile nolibc (Linux only) +$(OBJDIR)/lib_%.o: $(LIBDIR)/%.c | $(OBJDIR) + $(CC) $(CFLAGS) -c $< -o $@ + +# Plugins +plugins: $(PLUGIN_TARGETS) + +$(BUILDDIR)/%.$(SO_EXT): $(FILTERDIR)/%.c | $(BUILDDIR) +ifeq ($(PLATFORM),linux) + $(CC) $(CFLAGS) $(SO_FLAGS) $< -o $@.tmp && mv $@.tmp $@ +else + $(CC) $(CFLAGS) $(SO_FLAGS) $< -o $@ \ + -undefined dynamic_lookup +endif + $(OBJDIR): mkdir -p $(OBJDIR) -# Plugins compilation -plugins: $(PLUGIN_TARGETS) - -$(BUILDDIR)/%.so: filters/%.c | $(BUILDDIR) - $(CC) $(CFLAGS) -fPIC -shared $< -o $@.tmp - mv $@.tmp $@ - -# Automated Unit Tests Execution -test: $(BUILDDIR) - $(CC) -I.. $(CFLAGS) -DTESTING $(TEST_SRC) $(FILTER_SRCS) -o $(TEST_TARGET) -lcheck -lsubunit -lm -lrt -lpthread - @echo " RUNNING ALL PLUGINS INVARIANT TEST " - ./$(TEST_TARGET) - $(BUILDDIR): mkdir -p $(BUILDDIR) -# Cleanup clean: rm -rf $(BUILDDIR) + +info: + @echo "Platform : $(PLATFORM)" + @echo "Arch : $(UNAME_M)" + @echo "CC : $(CC)" + @echo "CFLAGS : $(CFLAGS)" + @echo "LDFLAGS : $(LDFLAGS)" + @echo "Objects : $(OBJECTS)" diff --git a/C/include/capture.h b/C/include/capture.h index f948b33..4de3775 100644 --- a/C/include/capture.h +++ b/C/include/capture.h @@ -1,18 +1,22 @@ #ifndef CAPTURE_H #define CAPTURE_H +#include #include -#include + +typedef struct webcam_impl webcam_impl_t; typedef struct { - int fd; - int width; - int height; - void *buffer; - struct v4l2_buffer buf_info; + int fd; /* Linux: V4L2 fd. macOS: -1 (unused externally) */ + int width; + int height; + void *buffer; + webcam_impl_t *impl; } webcam_t; // Initialize webcam +// On Linux: device = "/dev/video0" +// On macOS: device = NULL (uses system default camera) or a device name string int webcam_init(webcam_t *cam, const char *device, int width, int height); // Wait for frame to be ready diff --git a/C/include/platform.h b/C/include/platform.h new file mode 100644 index 0000000..11f9bd5 --- /dev/null +++ b/C/include/platform.h @@ -0,0 +1,20 @@ +#ifndef PLATFORM_H +#define PLATFORM_H + +#if defined(__linux__) +#define PLATFORM_LINUX 1 +#elif defined(__APPLE__) && defined(__MACH__) +#define PLATFORM_MACOS 1 +#else +#error "Unsupported platform (only Linux and macOS are supported)" +#endif + +#if defined(__x86_64__) || defined(_M_X64) +#define ARCH_X86_64 1 +#elif defined(__aarch64__) || defined(_M_ARM64) +#define ARCH_ARM64 1 +#else +#error "Unsupported architecture (only x86_64 and ARM64 are supported)" +#endif + +#endif diff --git a/C/lib/nl_io.h b/C/lib/nl_io.h index bc5ac7f..087bf97 100644 --- a/C/lib/nl_io.h +++ b/C/lib/nl_io.h @@ -6,11 +6,15 @@ inotify */ +#include #include #ifdef __LINUX_NOLIBC__ +#ifndef MAP_FAILED +#define MAP_FAILED ((void *)-1) +#endif #include "nl_syscall.h" -/* Basic I/O */ +// Basic I/O static inline ssize_t nl_write(int fd, const void *buf, size_t n) { return (ssize_t)__sc3(SYS_write, fd, (long)buf, (long)n); } @@ -28,7 +32,20 @@ static inline int nl_ioctl(int fd, unsigned long req, void *arg) { return (int)__sc3(SYS_ioctl, fd, (long)req, (long)arg); } -/* mmap / munmap */ +// fprintf / stderr +#define stderr 2 +#define fprintf(fd, fmt, ...) \ + do { \ + char _fb[1024]; \ + int _fn = nl_snprintf(_fb, sizeof(_fb), fmt, ##__VA_ARGS__); \ + if (_fn > 0) { \ + size_t _nw = \ + (_fn < (int)sizeof(_fb) - 1) ? (size_t)_fn : sizeof(_fb) - 1; \ + nl_write((int)(long)(fd), _fb, _nw); \ + } \ + } while (0) + +// mmap / munmap static inline void *nl_mmap(void *addr, size_t len, int prot, int flags, int fd, long off) { return (void *)__sc6(SYS_mmap, (long)addr, (long)len, prot, flags, fd, off); @@ -37,7 +54,7 @@ static inline int nl_munmap(void *addr, size_t len) { return (int)__sc2(SYS_munmap, (long)addr, (long)len); } -/* select */ +// select struct nl_timeval { long tv_sec; long tv_usec; @@ -55,7 +72,7 @@ static inline int nl_select(int nfds, nl_fd_set *r, nl_fd_set *w, nl_fd_set *e, return (int)__sc6(SYS_select, nfds, (long)r, (long)w, (long)e, (long)tv, 0); } -/* clock / sleep */ +// clock / sleep static inline int nl_clock_gettime(clockid_t id, struct timespec *ts) { return (int)__sc2(SYS_clock_gettime, id, (long)ts); } @@ -78,7 +95,7 @@ static inline void nl_exit(int code) { __builtin_unreachable(); } -/* inotify */ +// inotify static inline int nl_inotify_init1(int flags) { return (int)__sc1(SYS_inotify_init1, flags); } @@ -87,12 +104,16 @@ static inline int nl_inotify_add_watch(int fd, const char *path, return (int)__sc3(SYS_inotify_add_watch, fd, (long)path, mask); } -/* termios via ioctl */ +// termios via ioctl #include #define NL_TCGETS 0x5401 #define NL_TCSETS 0x5402 #define NL_TCSETSF 0x5404 +#define TCSANOW 0 +#define TCSADRAIN 1 +#define TCSAFLUSH 2 + static inline int nl_tcgetattr(int fd, struct termios *t) { return nl_ioctl(fd, NL_TCGETS, t); } @@ -101,7 +122,7 @@ static inline int nl_tcsetattr(int fd, int action, const struct termios *t) { return nl_ioctl(fd, req, (void *)t); } -/* Macro redirects */ +// Macro redirects #define write(fd, buf, n) nl_write(fd, buf, n) #define read(fd, buf, n) nl_read(fd, buf, n) #define _open2(p, f) nl_open(p, f, 0) diff --git a/C/lib/nl_printf.c b/C/lib/nl_printf.c index 114fe31..a97aa9d 100644 --- a/C/lib/nl_printf.c +++ b/C/lib/nl_printf.c @@ -1,5 +1,4 @@ -#include "nl_printf.h" -// #include "nolibc.h" +#include "nolibc.h" #include #include @@ -163,3 +162,7 @@ int nl_snprintf(char *buf, size_t size, const char *fmt, ...) { va_end(ap); return r; } + +void nl_eprint(const char *msg) { + nl_write(2, msg, nl_strlen(msg)); +} diff --git a/C/lib/nl_printf.h b/C/lib/nl_printf.h index 48fe0e9..1bd2df4 100644 --- a/C/lib/nl_printf.h +++ b/C/lib/nl_printf.h @@ -1,17 +1,13 @@ #ifndef NL_PRINTF_H #define NL_PRINTF_H -#include "nl_io.h" #include "nl_string.h" #include #include int nl_vsnprintf(char *buf, size_t size, const char *fmt, va_list ap); int nl_snprintf(char *buf, size_t size, const char *fmt, ...); - -static inline void nl_eprint(const char *msg) { - nl_write(2, msg, nl_strlen(msg)); -} +void nl_eprint(const char *msg); static inline int nl_fmt_fps(char *buf, size_t sz, double fps) { int whole = (int)fps; @@ -25,15 +21,4 @@ static inline int nl_fmt_fps(char *buf, size_t sz, double fps) { #define snprintf nl_snprintf -#define stderr 2 -#define fprintf(fd, fmt, ...) \ - do { \ - char _fb[1024]; \ - int _fn = nl_snprintf(_fb, sizeof(_fb), fmt, ##__VA_ARGS__); \ - if (_fn > 0) { \ - size_t _nwrite = \ - (_fn < (int)sizeof(_fb) - 1) ? (size_t)_fn : sizeof(_fb) - 1; \ - nl_write((int)(long)(fd), _fb, _nwrite); \ - } \ - } while (0) #endif diff --git a/C/src/ascii.c b/C/src/ascii.c index 9824697..1fbe3d8 100644 --- a/C/src/ascii.c +++ b/C/src/ascii.c @@ -1,8 +1,13 @@ -#include "ascii.h" +// NOTE: +// SIMD paths: +// - x86_64 Linux/macOS: SSE2 via +// - ARM64 macOS: NEON via +#include "ascii.h" #include #include "nolibc.h" +#include "platform.h" // Helpers static inline uint8_t clamp_u8(int v) { @@ -10,101 +15,95 @@ static inline uint8_t clamp_u8(int v) { } static inline int my_abs(int x) { return x < 0 ? -x : x; } -// Image conversion -#ifdef __x86_64__ +// YUYV to grayscale +#if defined(ARCH_X86_64) #include - -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]; -} -#else void yuyv_to_gray_simd(const uint8_t *yuyv, uint8_t *gray, int width, int height) { int total = width * height; - for (int i = 0; i < total; i++) { - gray[i] = yuyv[i * 2]; + __m128i mask = _mm_set1_epi16(0x00FF); + int i = 0; + for (; i + 16 <= total; i += 16) { + __m128i lo = _mm_loadu_si128((__m128i *)(yuyv + i * 2)); + __m128i hi = _mm_loadu_si128((__m128i *)(yuyv + i * 2 + 16)); + lo = _mm_and_si128(lo, mask); + hi = _mm_and_si128(hi, mask); + _mm_storeu_si128((__m128i *)(gray + i), _mm_packus_epi16(lo, hi)); } + for (; i < total; i++) + gray[i] = yuyv[i * 2]; +} + +#elif defined(ARCH_ARM64) +#include +void yuyv_to_gray_simd(const uint8_t *yuyv, uint8_t *gray, int width, + int height) { + int total = width * height; + // NEON: process 8 YUYV pairs (= 16 px) per iteration + int i = 0; + for (; i + 16 <= total; i += 16) { + // Load 32 bytes: [Y0 U0 Y1 V0 Y2 U1 Y3 V1 ...] + uint8x16x2_t yuv = vld2q_u8(yuyv + i * 2); + // yuv.val[0] = all Y bytes (even bytes = luma) + vst1q_u8(gray + i, yuv.val[0]); + } + for (; i < total; i++) + gray[i] = yuyv[i * 2]; +} + +#else +// fallback +void yuyv_to_gray_simd(const uint8_t *yuyv, uint8_t *gray, int width, + int height) { + int total = width * height; + for (int i = 0; i < total; i++) + gray[i] = yuyv[i * 2]; } #endif void yuyv_to_rgb(const uint8_t *yuyv, uint8_t *rgb, int width, int height) { int pairs = (width * height) / 2; for (int i = 0; i < pairs; i++) { - int y0 = yuyv[i * 4 + 0]; - int u = yuyv[i * 4 + 1]; - int y1 = yuyv[i * 4 + 2]; - int v = yuyv[i * 4 + 3]; - int d = u - 128; - int e = v - 128; - + int y0 = yuyv[i * 4 + 0], u = yuyv[i * 4 + 1]; + int y1 = yuyv[i * 4 + 2], v = yuyv[i * 4 + 3]; + int d = u - 128, e = v - 128; for (int p = 0; p < 2; p++) { int c = ((p == 0) ? y0 : y1) - 16; uint8_t *px = rgb + (i * 2 + p) * 3; - px[0] = clamp_u8((298 * c + 409 * e + 128) >> 8); // R - px[1] = clamp_u8((298 * c - 100 * d - 208 * e + 128) >> 8); // G - px[2] = clamp_u8((298 * c + 516 * d + 128) >> 8); // B + px[0] = clamp_u8((298 * c + 409 * e + 128) >> 8); + px[1] = clamp_u8((298 * c - 100 * d - 208 * e + 128) >> 8); + px[2] = clamp_u8((298 * c + 516 * d + 128) >> 8); } } } // Buffer sizing size_t ascii_out_size(int dst_w, int dst_h, int color) { - /* prefix: 3 bytes + 1 Char */ - if (color) { - /* Per cell: "\033[38;2;255;255;255m" (20) + char (1) = 21 - * Per row end: "\033[0m\n" (6) */ + if (color) return 3 + (size_t)dst_h * ((size_t)dst_w * 21 + 6) + 1; - } else { - /* Per cell: 1 byte; per row: + newline */ - return 3 + (size_t)dst_h * ((size_t)dst_w + 1) + 1; - } + return 3 + (size_t)dst_h * ((size_t)dst_w + 1) + 1; } -// Sobel edge detection (kernel convolution) +// Sobel edge detection static void sobel(const uint8_t *in, uint8_t *out, 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 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; } - } - - // TEST: test both L1 and L2 normalisations - int mag = my_abs(gx) + my_abs(gy); // L1 normalisation - // int mag = sqrt(gx * gx + gy * gy); // L2 normalisation - + int mag = my_abs(gx) + my_abs(gy); out[y * w + x] = (uint8_t)(mag > 255 ? 255 : mag); } } } -// Grayscale to ascii +// Grayscale to ASCII 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) { @@ -114,17 +113,15 @@ int grayscale_to_ascii(const uint8_t *gray, const uint8_t *rgb, int src_w, 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_color = opts && opts->color && rgb; 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; - // 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((size_t)(dst_w * dst_h)); + uint8_t *small_rgb = do_color ? malloc((size_t)(dst_w * dst_h * 3)) : NULL; if (!small_g || (do_color && !small_rgb)) { free(small_g); @@ -133,21 +130,17 @@ int grayscale_to_ascii(const uint8_t *gray, const uint8_t *rgb, int src_w, } for (int y = 0; y < dst_h; y++) { - int ys = (int)(y * bh); - int ye = (int)((y + 1) * bh); + int ys = (int)(y * bh), ye = (int)((y + 1) * bh); if (ye <= ys) ye = ys + 1; - for (int x = 0; x < dst_w; x++) { - int xs = (int)(x * bw); - int xe = (int)((x + 1) * bw); + 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) { @@ -158,11 +151,9 @@ int grayscale_to_ascii(const uint8_t *gray, const uint8_t *rgb, int src_w, } count++; } - } - if (count == 0) + if (!count) count = 1; - // Brightness and contrast int gv = (int)(tg / count); if (contrast != 100) gv = 128 + (gv - 128) * contrast / 100; @@ -170,75 +161,62 @@ int grayscale_to_ascii(const uint8_t *gray, const uint8_t *rgb, int src_w, small_g[y * dst_w + x] = clamp_u8(gv); if (do_color) { - uint8_t *out_px = small_rgb + (y * dst_w + x) * 3; - out_px[0] = clamp_u8((int)(tr / count)); - out_px[1] = clamp_u8((int)(tgv / count)); - out_px[2] = clamp_u8((int)(tb / count)); + uint8_t *op = small_rgb + (y * 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)); } } } - // Sobel edge detection if (do_edges) { - // Temporary buffer for detected edges results - uint8_t *edge_buf = calloc(dst_w * dst_h, sizeof(uint8_t)); - if (edge_buf) { - sobel(small_g, edge_buf, dst_w, dst_h); - // overwite grayscale image with edge map - memcpy(small_g, edge_buf, dst_w * dst_h); - free(edge_buf); + uint8_t *eb = calloc((size_t)(dst_w * dst_h), 1); + if (eb) { + sobel(small_g, eb, dst_w, dst_h); + nl_memcpy(small_g, eb, (size_t)(dst_w * dst_h)); + free(eb); } } - // Floyd-Steinberg dithering if (do_dither) { - int16_t *eb = malloc(dst_w * dst_h * sizeof(int16_t)); + 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]; - - // Quantise to nearest charset level 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_ADD(DY, DX, NUM) \ +#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 * (NUM) / 16); \ + eb[ny * dst_w + nx] += (int16_t)(err * (N) / 16); \ } while (0) - FS_ADD(0, 1, 7); - FS_ADD(1, -1, 3); - FS_ADD(1, 0, 5); - FS_ADD(1, 1, 1); -#undef FS_ADD + FS(0, 1, 7); + FS(1, -1, 3); + FS(1, 0, 5); + FS(1, 1, 1); +#undef FS } } - for (int i = 0; i < dst_w * dst_h; i++) small_g[i] = clamp_u8(eb[i]); - free(eb); } - /* HACK: If malloc fails silently fall back to no dithering */ } - // Render into caller's buffer int out_idx = 0; - static const char HOME[] = "\033[H"; // Cursor repositions without erasing + static const char HOME[] = "\033[H"; if (out_size > sizeof(HOME)) { - memcpy(out, HOME, sizeof(HOME) - 1); + nl_memcpy(out, HOME, sizeof(HOME) - 1); out_idx = sizeof(HOME) - 1; } @@ -252,7 +230,6 @@ int grayscale_to_ascii(const uint8_t *gray, const uint8_t *rgb, int src_w, idx = nchars - 1; if (invert) idx = nchars - 1 - idx; - char ch = charset[idx]; if (do_color) { @@ -266,7 +243,6 @@ int grayscale_to_ascii(const uint8_t *gray, const uint8_t *rgb, int src_w, out[out_idx++] = ch; } } - 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) @@ -278,7 +254,6 @@ 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); return out_idx; @@ -291,19 +266,18 @@ void overlay_fps_box(int dst_w, double fps, int color_enabled) { if (col < 1) col = 1; + char fpsbuf[10]; + nl_fmt_fps(fpsbuf, sizeof(fpsbuf), fps); + int n; - if (color_enabled) { - char fpsbuf[10]; - nl_fmt_fps(fpsbuf, sizeof(fpsbuf), fps); + if (color_enabled) n = snprintf( buf, sizeof(buf), "\033[1;%dH\033[38;2;0;255;0m\033[48;2;30;30;30m[ FPS: %s ]\033[0m", col, fpsbuf); - } else { - char fpsbuf[10]; - nl_fmt_fps(fpsbuf, sizeof(fpsbuf), fps); + else n = snprintf(buf, sizeof(buf), "\033[1;%dH[ FPS: %s ]", col, fpsbuf); - } + if (n > 0 && n < (int)sizeof(buf)) (void)write(STDOUT_FILENO, buf, (size_t)n); } diff --git a/C/src/capture.c b/C/src/capture_linux.c similarity index 64% rename from C/src/capture.c rename to C/src/capture_linux.c index fc742cf..738475a 100644 --- a/C/src/capture.c +++ b/C/src/capture_linux.c @@ -1,22 +1,34 @@ -#include "capture.h" #include "ascii.h" +#include "capture.h" +#include "platform.h" +#ifdef PLATFORM_LINUX + +#include "nolibc.h" #include #include -#include "nolibc.h" +typedef struct webcam_impl webcam_impl_t; + +struct webcam_impl { + struct v4l2_buffer buf_info; +}; + +static webcam_impl_t _impl_storage; int webcam_init(webcam_t *cam, const char *device, int width, int height) { - // Open device (non‑blocking for select usage) - cam->fd = open(device, O_RDWR | O_NONBLOCK, 0); + cam->impl = &_impl_storage; + cam->buffer = MAP_FAILED; + + // Open device non-blocking (for select) + cam->fd = open(device ? device : "/dev/video0", O_RDWR | O_NONBLOCK, 0); if (cam->fd < 0) return -1; - // Set format struct v4l2_format fmt = {0}; fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; - fmt.fmt.pix.width = width; - fmt.fmt.pix.height = height; + fmt.fmt.pix.width = (unsigned)width; + fmt.fmt.pix.height = (unsigned)height; fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV; fmt.fmt.pix.field = V4L2_FIELD_NONE; @@ -25,11 +37,9 @@ int webcam_init(webcam_t *cam, const char *device, int width, int height) { return -1; } - // Read back actual resolution - cam->width = fmt.fmt.pix.width; - cam->height = fmt.fmt.pix.height; + cam->width = (int)fmt.fmt.pix.width; + cam->height = (int)fmt.fmt.pix.height; - // Request one mmap buffer struct v4l2_requestbuffers req = {0}; req.count = 1; req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; @@ -40,7 +50,6 @@ int webcam_init(webcam_t *cam, const char *device, int width, int height) { return -1; } - // Query buffer info struct v4l2_buffer buf = {0}; buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; buf.memory = V4L2_MEMORY_MMAP; @@ -51,25 +60,21 @@ int webcam_init(webcam_t *cam, const char *device, int width, int height) { return -1; } - // mmap cam->buffer = mmap(NULL, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED, - cam->fd, buf.m.offset); + cam->fd, (long)buf.m.offset); if (cam->buffer == MAP_FAILED) { close(cam->fd); return -1; } - // Store buffer info for later munmap and requeue - cam->buf_info = buf; + cam->impl->buf_info = buf; - // Queue the buffer if (ioctl(cam->fd, VIDIOC_QBUF, &buf) < 0) { munmap(cam->buffer, buf.length); close(cam->fd); return -1; } - // Start streaming enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE; if (ioctl(cam->fd, VIDIOC_STREAMON, &type) < 0) { munmap(cam->buffer, buf.length); @@ -89,47 +94,36 @@ int webcam_wait_frame(webcam_t *cam, int timeout_ms) { tv.tv_usec = (timeout_ms % 1000) * 1000; int ret = nl_select(cam->fd + 1, &fds, (nl_fd_set *)0, (nl_fd_set *)0, &tv); - if (ret <= 0) - return -1; // timeout or error - return 0; + return (ret <= 0) ? -1 : 0; } int webcam_capture_frame(webcam_t *cam, uint8_t *gray_buffer) { - // Dequeue buffer - struct v4l2_buffer buf = cam->buf_info; + struct v4l2_buffer buf = cam->impl->buf_info; if (ioctl(cam->fd, VIDIOC_DQBUF, &buf) < 0) return -1; - // Convert YUYV -> grayscale (Y component) - // uint8_t *yuyv = (uint8_t *)cam->buffer; - // for (int i = 0, j = 0; i < cam->width * cam->height * 2; i += 2, j++) { - // gray_buffer[j] = yuyv[i]; - // } - int w = cam->width; - int h = cam->height; - yuyv_to_gray_simd((uint8_t *)cam->buffer, gray_buffer, w, h); + yuyv_to_gray_simd((uint8_t *)cam->buffer, gray_buffer, cam->width, + cam->height); - // Store updated buffer info for requeue - cam->buf_info = buf; + cam->impl->buf_info = buf; return 0; } int webcam_requeue_buffer(webcam_t *cam) { - if (ioctl(cam->fd, VIDIOC_QBUF, &cam->buf_info) < 0) - return -1; - return 0; + return (ioctl(cam->fd, VIDIOC_QBUF, &cam->impl->buf_info) < 0) ? -1 : 0; } void webcam_cleanup(webcam_t *cam) { if (cam->fd >= 0) { - // Stop streaming enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE; ioctl(cam->fd, VIDIOC_STREAMOFF, &type); - // Unmap and close if (cam->buffer != MAP_FAILED) - munmap(cam->buffer, cam->buf_info.length); + munmap(cam->buffer, cam->impl->buf_info.length); close(cam->fd); } cam->fd = -1; cam->buffer = MAP_FAILED; + cam->impl = (webcam_impl_t *)0; } + +#endif /* PLATFORM_LINUX */ diff --git a/C/src/capture_macos.c b/C/src/capture_macos.c new file mode 100644 index 0000000..1e65356 --- /dev/null +++ b/C/src/capture_macos.c @@ -0,0 +1,297 @@ +#include "capture.h" +#include "platform.h" + +#ifdef PLATFORM_MACOS + +#import +#import +#import +#include +#include +#include + +#define FRAME_BUFS 2 + +struct webcam_impl { + // AVFoundation objects + AVCaptureSession *session; + AVCaptureDeviceInput *input; + AVCaptureVideoDataOutput *output; + id delegate; + dispatch_queue_t queue; + + // Frame ring buffer + uint8_t *gray_buf[FRAME_BUFS]; + int buf_w, buf_h; + int write_idx; + int ready_idx; + int has_frame; + + pthread_mutex_t lock; + pthread_cond_t cond; + int stopped; +}; + +@interface FrameDelegate + : NSObject +@property(assign) struct webcam_impl *impl; +@end + +@implementation FrameDelegate + +- (void)captureOutput:(AVCaptureOutput *)output + didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer + fromConnection:(AVCaptureConnection *)connection { + + struct webcam_impl *im = self.impl; + if (!im || im->stopped) + return; + + CVPixelBufferRef pixbuf = CMSampleBufferGetImageBuffer(sampleBuffer); + if (!pixbuf) + return; + + CVPixelBufferLockBaseAddress(pixbuf, kCVPixelBufferLock_ReadOnly); + + // Request NV12 (YUV 4:2:0 biplanar). + // Plane 0 is pure luma (Y) + size_t width = CVPixelBufferGetWidth(pixbuf); + size_t height = CVPixelBufferGetHeight(pixbuf); + + uint8_t *y_plane = (uint8_t *)CVPixelBufferGetBaseAddressOfPlane(pixbuf, 0); + size_t y_stride = CVPixelBufferGetBytesPerRowOfPlane(pixbuf, 0); + + pthread_mutex_lock(&im->lock); + + int wi = im->write_idx; + uint8_t *dst = im->gray_buf[wi]; + + // Copy luma plane row-by-row + for (size_t row = 0; row < height; row++) { + memcpy(dst + row * width, y_plane + row * y_stride, width); + } + + im->ready_idx = wi; + im->has_frame = 1; + im->write_idx = wi ^ 1; // swap to other slot + pthread_cond_signal(&im->cond); + pthread_mutex_unlock(&im->lock); + + CVPixelBufferUnlockBaseAddress(pixbuf, kCVPixelBufferLock_ReadOnly); +} + +@end + +int webcam_init(webcam_t *cam, const char *device, int width, int height) { + struct webcam_impl *im = calloc(1, sizeof(struct webcam_impl)); + if (!im) + return -1; + + pthread_mutex_init(&im->lock, NULL); + pthread_cond_init(&im->cond, NULL); + + // Allocate two grayscale frame buffers + im->buf_w = width; + im->buf_h = height; + for (int i = 0; i < FRAME_BUFS; i++) { + im->gray_buf[i] = calloc((size_t)(width * height), 1); + if (!im->gray_buf[i]) + goto fail; + } + + // Find the camera device + AVCaptureDevice *dev = nil; + if (device) { + // Match by localizedName + NSString *devName = [NSString stringWithUTF8String:device]; + NSArray *devices = + [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo]; + for (AVCaptureDevice *d in devices) { + if ([d.localizedName isEqualToString:devName]) { + dev = d; + break; + } + } + } + if (!dev) { + // Fall back to system default + if (@available(macOS 10.15, *)) { + AVCaptureDeviceDiscoverySession *ds = [AVCaptureDeviceDiscoverySession + discoverySessionWithDeviceTypes:@[ + AVCaptureDeviceTypeBuiltInWideAngleCamera + ] + mediaType:AVMediaTypeVideo + position:AVCaptureDevicePositionUnspecified]; + dev = ds.devices.firstObject; + } else { + dev = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo]; + } + } + if (!dev) { + goto fail; + } + + // Configure format + AVCaptureDeviceFormat *best_fmt = nil; + float best_diff = 1e9f; + for (AVCaptureDeviceFormat *fmt in dev.formats) { + CMFormatDescriptionRef desc = fmt.formatDescription; + CMVideoDimensions dim = CMVideoFormatDescriptionGetDimensions(desc); + float diff = (float)((dim.width - width) * (dim.width - width) + + (dim.height - height) * (dim.height - height)); + if (diff < best_diff) { + best_diff = diff; + best_fmt = fmt; + } + } + if (best_fmt) { + CMVideoDimensions dim = + CMVideoFormatDescriptionGetDimensions(best_fmt.formatDescription); + cam->width = dim.width; + cam->height = dim.height; + im->buf_w = cam->width; + im->buf_h = cam->height; + if (cam->width != width || cam->height != height) { + for (int i = 0; i < FRAME_BUFS; i++) { + free(im->gray_buf[i]); + im->gray_buf[i] = calloc((size_t)(cam->width * cam->height), 1); + if (!im->gray_buf[i]) + goto fail; + } + } + if ([dev lockForConfiguration:nil]) { + dev.activeFormat = best_fmt; + [dev unlockForConfiguration]; + } + } else { + cam->width = width; + cam->height = height; + } + + im->session = [[AVCaptureSession alloc] init]; + [im->session beginConfiguration]; + im->session.sessionPreset = AVCaptureSessionPresetInputPriority; + + NSError *err = nil; + im->input = [AVCaptureDeviceInput deviceInputWithDevice:dev error:&err]; + if (!im->input || err) + goto fail_session; + + if (![im->session canAddInput:im->input]) + goto fail_session; + [im->session addInput:im->input]; + + im->output = [[AVCaptureVideoDataOutput alloc] init]; + im->output.videoSettings = @{ + (NSString *)kCVPixelBufferPixelFormatTypeKey : + @(kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange) + }; + im->output.alwaysDiscardsLateVideoFrames = YES; + + im->queue = dispatch_queue_create("asciicam.capture", DISPATCH_QUEUE_SERIAL); + + FrameDelegate *delegate = [[FrameDelegate alloc] init]; + delegate.impl = im; + im->delegate = delegate; + + [im->output setSampleBufferDelegate:delegate queue:im->queue]; + + if (![im->session canAddOutput:im->output]) + goto fail_session; + [im->session addOutput:im->output]; + + [im->session commitConfiguration]; + [im->session startRunning]; + + cam->impl = im; + cam->fd = -1; + cam->buffer = NULL; + return 0; + +fail_session: + [im->session commitConfiguration]; +fail: + for (int i = 0; i < FRAME_BUFS; i++) + free(im->gray_buf[i]); + pthread_mutex_destroy(&im->lock); + pthread_cond_destroy(&im->cond); + free(im); + return -1; +} + +int webcam_wait_frame(webcam_t *cam, int timeout_ms) { + struct webcam_impl *im = cam->impl; + struct timespec ts; + clock_gettime(CLOCK_REALTIME, &ts); + ts.tv_sec += timeout_ms / 1000; + ts.tv_nsec += (timeout_ms % 1000) * 1000000L; + if (ts.tv_nsec >= 1000000000L) { + ts.tv_sec++; + ts.tv_nsec -= 1000000000L; + } + + pthread_mutex_lock(&im->lock); + while (!im->has_frame && !im->stopped) { + if (pthread_cond_timedwait(&im->cond, &im->lock, &ts) != 0) { + pthread_mutex_unlock(&im->lock); + return -1; // timeout + } + } + int ok = im->has_frame; + pthread_mutex_unlock(&im->lock); + return ok ? 0 : -1; +} + +int webcam_capture_frame(webcam_t *cam, uint8_t *gray_buffer) { + struct webcam_impl *im = cam->impl; + + pthread_mutex_lock(&im->lock); + if (!im->has_frame) { + pthread_mutex_unlock(&im->lock); + return -1; + } + + int ri = im->ready_idx; + im->has_frame = 0; + pthread_mutex_unlock(&im->lock); + + // Copy the luma buffer out + memcpy(gray_buffer, im->gray_buf[ri], (size_t)(im->buf_w * im->buf_h)); + return 0; +} + +int webcam_requeue_buffer(webcam_t *cam) { + (void)cam; + return 0; // AVFoundation manages buffers +} + +void webcam_cleanup(webcam_t *cam) { + struct webcam_impl *im = cam->impl; + if (!im) + return; + + pthread_mutex_lock(&im->lock); + im->stopped = 1; + pthread_cond_broadcast(&im->cond); + pthread_mutex_unlock(&im->lock); + + if (im->session) { + [im->session stopRunning]; + im->session = nil; + im->input = nil; + im->output = nil; + im->delegate = nil; + } + + for (int i = 0; i < FRAME_BUFS; i++) + free(im->gray_buf[i]); + pthread_mutex_destroy(&im->lock); + pthread_cond_destroy(&im->cond); + free(im); + + cam->impl = NULL; + cam->fd = -1; + cam->buffer = NULL; +} + +#endif /* PLATFORM_MACOS */ diff --git a/C/src/main.c b/C/src/main.c index e63c3db..b8412c2 100644 --- a/C/src/main.c +++ b/C/src/main.c @@ -4,19 +4,12 @@ #include "thread_sharing.h" #include "timing.h" +#include "nolibc.h" + #include #include #include -#include "nolibc.h" - -typedef int sig_atomic_t; - -#define MAP_FAILED ((void *)-1) -#define PROT_READ 1 -#define PROT_WRITE 2 -#define MAP_SHARED 1 - // Defaults #define DEFAULT_ASCII_WIDTH 80 #define DEFAULT_ASCII_HEIGHT 40