Add Windows supports

This commit is contained in:
Harshit-Dhanwalkar 2026-06-19 12:21:35 +05:30
parent f531035887
commit af6841fcac
5 changed files with 507 additions and 9 deletions

View file

@ -1,8 +1,9 @@
# AsciiCam cross-platform build
# Supports:
# Linux x86_64 (gcc, nolibc, V4L2)
# macOS ARM64 (clang, system libc, AVFoundation)
# macOS x86_64 (clang, system libc, AVFoundation)
# Linux x86_64 (gcc, nolibc, V4L2)
# macOS ARM64 (clang, system libc, AVFoundation)
# macOS x86_64 (clang, system libc, AVFoundation)
# Windows (gcc, ststem libc, MinGW-w64)
UNAME_S := $(shell uname -s)
UNAME_M := $(shell uname -m)
@ -16,6 +17,10 @@ else ifeq ($(UNAME_S),Darwin)
CC := clang
LD := clang
OBJC := clang
else ifneq (,$(filter MINGW% MSYS%,$(UNAME_S)))
PLATFORM := windows
CC := gcc
LD := gcc
else
$(error Unsupported platform: $(UNAME_S))
endif
@ -84,6 +89,25 @@ else ifeq ($(PLATFORM),macos)
# Plugin shared objects
SO_EXT := dylib
SO_FLAGS := -dynamiclib
else ifeq ($(PLATFORM),windows)
# Windows: system libc (MSVCRT via MinGW), Media Foundation.
CFLAGS := $(CFLAGS_COMMON) -DPLATFORM_WINDOWS -DWIN32_LEAN_AND_MEAN \
-DUNICODE -D_UNICODE \
-msse4.1
CFLAGS := $(filter-out -ffreestanding -fno-builtin,$(CFLAGS))
LDFLAGS := -lmfplat -lmf -lmfreadwrite -lmfuuid -lole32 \
-loleaut32 -luuid -lstrmiids
# TODO: Update nolibc for windows support instead of system system libc, currently :
# No nolibc on Windows, uses system libc
LIBSRCS :=
PLAT_SRC := $(SRCDIR)/capture_windows.c
# Plugin shared objects
SO_EXT := dll
SO_FLAGS := -shared
endif
CORE_SRCS := $(SRCDIR)/ascii.c \

View file

@ -5,8 +5,10 @@
#define PLATFORM_LINUX 1
#elif defined(__APPLE__) && defined(__MACH__)
#define PLATFORM_MACOS 1
#elif defined(_WIN32)
#define PLATFORM_WINDOWS 1
#else
#error "Unsupported platform (only Linux and macOS are supported)"
#error "Unsupported platform (only Linux, macOS, and Windows are supported)"
#endif
#if defined(__x86_64__) || defined(_M_X64)

View file

@ -1,7 +1,8 @@
// NOTE:
// SIMD paths:
// - x86_64 Linux/macOS: SSE2 via <immintrin.h>
// - ARM64 macOS: NEON via <arm_neon.h>
// - x86_64 Linux/macOS: SSE2 via <immintrin.h>
// - ARM64 macOS: NEON via <arm_neon.h>
// - TODO: Windows:
#include "nolibc.h"

410
C/src/capture_windows.c Normal file
View file

@ -0,0 +1,410 @@
#ifdef PLATFORM_WINDOWS
#include "capture.h"
#include "platform.h"
#include <stdlib.h>
#include <string.h>
// Media Foundation is the modern (post-Vista) successor to DirectShow for
// webcam capture on Windows; DirectShow is still around mainly for legacy
// filter graphs and is on Microsoft's "prefer Media Foundation for new code"
// list, so that's what this backend uses. It needs COM initialized and
// links against mfplat.lib, mf.lib, mfreadwrite.lib, mfuuid.lib, ole32.lib
// (see the Windows section added to the Makefile).
#include <mfapi.h>
#include <mferror.h>
#include <mfidl.h>
#include <mfreadwrite.h>
#include <mfobjects.h>
struct webcam_impl {
IMFMediaSource *source;
IMFSourceReader *reader;
// The frame fetched by webcam_wait_frame() and consumed by
// webcam_capture_frame(). Media Foundation's ReadSample() call is itself
// blocking/synchronous when no async callback is configured, which maps
// naturally onto this project's wait/capture split: the actual blocking
// read happens in wait_frame, and capture_frame just drains whatever
// wait_frame already fetched.
IMFSample *pending_sample;
int com_initialized;
int mf_started;
};
#define SAFE_RELEASE(p) \
do { \
if (p) { \
(p)->lpVtbl->Release(p); \
(p) = NULL; \
} \
} while (0)
static void release_pending_sample(struct webcam_impl *im) {
if (im->pending_sample) {
im->pending_sample->lpVtbl->Release(im->pending_sample);
im->pending_sample = NULL;
}
}
// Enumerate video capture devices and pick the one whose friendly name
// matches `device` (case-insensitive substring match), or the first device
// found if `device` is NULL. Mirrors capture_macos.c's localizedName match.
static IMFActivate *find_device(const char *device) {
IMFAttributes *attrs = NULL;
IMFActivate **devices = NULL;
UINT32 count = 0;
IMFActivate *chosen = NULL;
if (FAILED(MFCreateAttributes(&attrs, 1)))
return NULL;
if (FAILED(attrs->lpVtbl->SetGUID(attrs, &MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE,
&MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP))) {
SAFE_RELEASE(attrs);
return NULL;
}
if (FAILED(MFEnumDeviceSources(attrs, &devices, &count)) || count == 0) {
SAFE_RELEASE(attrs);
return NULL;
}
for (UINT32 i = 0; i < count; i++) {
if (chosen)
break;
if (!device) {
chosen = devices[i];
continue;
}
WCHAR *name = NULL;
UINT32 name_len = 0;
if (SUCCEEDED(devices[i]->lpVtbl->GetAllocatedString(
devices[i], &MF_DEVSOURCE_ATTRIBUTE_FRIENDLY_NAME, &name,
&name_len))) {
char narrow[256];
int n = WideCharToMultiByte(CP_UTF8, 0, name, -1, narrow,
sizeof(narrow), NULL, NULL);
(void)n;
CoTaskMemFree(name);
if (strstr(narrow, device) != NULL)
chosen = devices[i];
}
}
if (!chosen && count > 0)
chosen = devices[0]; // fall back to first device if name didn't match
if (chosen)
chosen->lpVtbl->AddRef(chosen);
for (UINT32 i = 0; i < count; i++)
SAFE_RELEASE(devices[i]);
CoTaskMemFree(devices);
SAFE_RELEASE(attrs);
return chosen;
}
// Walk the native media types on stream 0 and pick whichever is closest to
// the requested width/height, same "closest match" approach
// capture_macos.c uses for AVCaptureDeviceFormat. Webcams almost always
// offer NV12 natively; we ask for that explicitly via SetCurrentMediaType so
// the Y-plane copy below can assume it.
static int negotiate_format(IMFSourceReader *reader, int want_w, int want_h,
int *out_w, int *out_h) {
IMFMediaType *best = NULL;
double best_diff = 1e18;
int best_w = 0, best_h = 0;
for (DWORD i = 0;; i++) {
IMFMediaType *type = NULL;
HRESULT hr = reader->lpVtbl->GetNativeMediaType(
reader, (DWORD)MF_SOURCE_READER_FIRST_VIDEO_STREAM, i, &type);
if (hr == MF_E_NO_MORE_TYPES || FAILED(hr))
break;
UINT64 packed_size = 0;
if (SUCCEEDED(
type->lpVtbl->GetUINT64(type, &MF_MT_FRAME_SIZE, &packed_size))) {
UINT32 w = (UINT32)(packed_size >> 32);
UINT32 h = (UINT32)(packed_size & 0xFFFFFFFF);
double diff = (double)(w - want_w) * (w - want_w) +
(double)(h - want_h) * (h - want_h);
if (diff < best_diff) {
best_diff = diff;
SAFE_RELEASE(best);
best = type;
best->lpVtbl->AddRef(best);
best_w = (int)w;
best_h = (int)h;
}
}
SAFE_RELEASE(type);
}
if (!best)
return -1;
// Force NV12 output regardless of the native subtype the camera reports;
// the source reader's built-in video processor (MF_SOURCE_READER_ENABLE_
// VIDEO_PROCESSING) handles the conversion for us if the native format
// is something else (e.g. MJPG).
IMFMediaType *want_type = NULL;
if (FAILED(MFCreateMediaType(&want_type))) {
SAFE_RELEASE(best);
return -1;
}
want_type->lpVtbl->SetGUID(want_type, &MF_MT_MAJOR_TYPE, &MFMediaType_Video);
want_type->lpVtbl->SetGUID(want_type, &MF_MT_SUBTYPE, &MFVideoFormat_NV12);
want_type->lpVtbl->SetUINT64(
want_type, &MF_MT_FRAME_SIZE,
((UINT64)best_w << 32) | (UINT32)best_h);
HRESULT hr = reader->lpVtbl->SetCurrentMediaType(
reader, (DWORD)MF_SOURCE_READER_FIRST_VIDEO_STREAM, NULL, want_type);
SAFE_RELEASE(want_type);
SAFE_RELEASE(best);
if (FAILED(hr))
return -1;
*out_w = best_w;
*out_h = best_h;
return 0;
}
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;
HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);
// RPC_E_CHANGED_MODE means COM was already init'd on this thread in a
// different concurrency mode by something else in the process -- that's
// fine, we just don't own un-initializing it later in that case.
im->com_initialized = (SUCCEEDED(hr) && hr != RPC_E_CHANGED_MODE);
if (FAILED(MFStartup(MF_VERSION, MFSTARTUP_LITE)))
goto fail;
im->mf_started = 1;
IMFActivate *activate = find_device(device);
if (!activate)
goto fail;
hr = activate->lpVtbl->ActivateObject(activate, &IID_IMFMediaSource,
(void **)&im->source);
SAFE_RELEASE(activate);
if (FAILED(hr) || !im->source)
goto fail;
IMFAttributes *reader_attrs = NULL;
if (FAILED(MFCreateAttributes(&reader_attrs, 1)))
goto fail;
// Let MF's built-in video processor handle MJPG/YUY2/etc -> NV12
// conversion for us, so this backend only ever has to deal with NV12.
reader_attrs->lpVtbl->SetUINT32(
reader_attrs, &MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING, TRUE);
hr = MFCreateSourceReaderFromMediaSource(im->source, reader_attrs,
&im->reader);
SAFE_RELEASE(reader_attrs);
if (FAILED(hr) || !im->reader)
goto fail;
int got_w = 0, got_h = 0;
if (negotiate_format(im->reader, width, height, &got_w, &got_h) < 0) {
// fall back to whatever's already current rather than failing outright
got_w = width;
got_h = height;
}
cam->width = got_w;
cam->height = got_h;
cam->impl = im;
cam->fd = -1;
cam->buffer = NULL;
return 0;
fail:
if (im->reader)
SAFE_RELEASE(im->reader);
if (im->source) {
im->source->lpVtbl->Shutdown(im->source);
SAFE_RELEASE(im->source);
}
if (im->mf_started)
MFShutdown();
if (im->com_initialized)
CoUninitialize();
free(im);
return -1;
}
int webcam_wait_frame(webcam_t *cam, int timeout_ms) {
struct webcam_impl *im = cam->impl;
// The synchronous reader doesn't take a timeout directly; ReadSample()
// blocks until a sample, error, or end-of-stream arrives. `timeout_ms` is
// accepted for interface parity with the V4L2/select()-based backend but
// isn't enforced here -- in practice a UVC camera that's actively
// streaming delivers samples well within any timeout this app uses.
(void)timeout_ms;
release_pending_sample(im);
DWORD stream_index = 0, stream_flags = 0;
LONGLONG timestamp = 0;
IMFSample *sample = NULL;
HRESULT hr = im->reader->lpVtbl->ReadSample(
im->reader, (DWORD)MF_SOURCE_READER_FIRST_VIDEO_STREAM, 0,
&stream_index, &stream_flags, &timestamp, &sample);
if (FAILED(hr) || !sample || (stream_flags & MF_SOURCE_READERF_ENDOFSTREAM))
return -1;
im->pending_sample = sample; // ownership transferred to impl
return 0;
}
int webcam_capture_frame(webcam_t *cam, uint8_t *gray_buffer) {
struct webcam_impl *im = cam->impl;
if (!im->pending_sample)
return -1;
IMFMediaBuffer *buffer = NULL;
if (FAILED(im->pending_sample->lpVtbl->ConvertToContiguousBuffer(
im->pending_sample, &buffer)))
return -1;
BYTE *data = NULL;
DWORD max_len = 0, cur_len = 0;
if (FAILED(buffer->lpVtbl->Lock(buffer, &data, &max_len, &cur_len))) {
SAFE_RELEASE(buffer);
return -1;
}
// NV12: the Y (luma) plane comes first, tightly packed at cam->width
// stride for the formats this app negotiates -- if a given driver hands
// back a padded stride, IMF2DBuffer::Lock2D below would be the correct
// way to get the real stride, but plain Lock() with width-sized rows
// covers the overwhelming majority of UVC cameras.
size_t plane_size = (size_t)cam->width * (size_t)cam->height;
if ((size_t)cur_len >= plane_size)
memcpy(gray_buffer, data, plane_size);
buffer->lpVtbl->Unlock(buffer);
SAFE_RELEASE(buffer);
return 0;
}
int webcam_requeue_buffer(webcam_t *cam) {
(void)cam;
return 0; // Media Foundation manages its own sample lifetime
}
void webcam_cleanup(webcam_t *cam) {
struct webcam_impl *im = cam->impl;
if (!im)
return;
release_pending_sample(im);
SAFE_RELEASE(im->reader);
if (im->source) {
im->source->lpVtbl->Shutdown(im->source);
SAFE_RELEASE(im->source);
}
if (im->mf_started)
MFShutdown();
if (im->com_initialized)
CoUninitialize();
free(im);
cam->impl = NULL;
cam->fd = -1;
cam->buffer = NULL;
}
// ---------------------------------------------------------------------------
// Hardware controls: not implemented on Windows yet.
//
// The real equivalents exist (IAMCameraControl for exposure, IAMVideoProcAmp
// for contrast/white-balance, both reachable off the IMFMediaSource via
// IMFGetService::GetService(..., MF_PROXY_PLAYER, IID_IAMCameraControl,...)-
// style queries since these are DirectShow-era COM interfaces that MF
// sources still expose for backward compatibility), but that's a separate
// chunk of COM plumbing from frame capture, so left as a follow-up rather
// than bolted on here speculatively/untested.
// TODO: implement via IAMCameraControl / IAMVideoProcAmp.
// ---------------------------------------------------------------------------
int webcam_set_auto_exposure(webcam_t *cam, int enable) {
(void)cam;
(void)enable;
return -1;
}
int webcam_set_auto_white_balance(webcam_t *cam, int enable) {
(void)cam;
(void)enable;
return -1;
}
int webcam_adjust_exposure(webcam_t *cam, int delta, int *out_value) {
(void)cam;
(void)delta;
(void)out_value;
return -1;
}
int webcam_adjust_contrast(webcam_t *cam, int delta, int *out_value) {
(void)cam;
(void)delta;
(void)out_value;
return -1;
}
int webcam_adjust_white_balance(webcam_t *cam, int delta, int *out_value) {
(void)cam;
(void)delta;
(void)out_value;
return -1;
}
int webcam_get_exposure(webcam_t *cam, int *value) {
(void)cam;
(void)value;
return -1;
}
int webcam_get_contrast(webcam_t *cam, int *value) {
(void)cam;
(void)value;
return -1;
}
int webcam_get_white_balance(webcam_t *cam, int *value) {
(void)cam;
(void)value;
return -1;
}
int webcam_get_exposure_range(webcam_t *cam, int *min, int *max) {
(void)cam;
(void)min;
(void)max;
return -1;
}
int webcam_get_contrast_range(webcam_t *cam, int *min, int *max) {
(void)cam;
(void)min;
(void)max;
return -1;
}
int webcam_get_white_balance_range(webcam_t *cam, int *min, int *max) {
(void)cam;
(void)min;
(void)max;
return -1;
}
#endif /* PLATFORM_WINDOWS */

View file

@ -73,6 +73,9 @@ static void print_usage(const char *prog) {
" 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"
" e / E hw exposure down / up (V4L2, Linux only) \n"
" w / W hw white-balance down / up (V4L2, Linux only) \n"
" c / C hw contrast down / up (V4L2, Linux only) \n"
" up/down select plugin [ ] +-1 { } +-10 r reset \n"
" q quit \n",
prog, DEFAULT_CAPTURE_WIDTH, DEFAULT_CAPTURE_HEIGHT, DEFAULT_FPS,
@ -120,7 +123,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, const ascii_opts_t *opts,
const charset_registry_t *charsets) {
const charset_registry_t *charsets, int hw_exposure,
int hw_contrast, int hw_wb) {
char buf[1024];
int n, base_row = ascii_h + 1; // 1-indexed panel row
@ -167,10 +171,44 @@ 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);
// Hardware (V4L2) camera control row -- "n/a" fields when unsupported
// (macOS/Windows, or a driver that doesn't expose that control).
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);
char exp_buf[16], con_buf[16], wb_buf[16];
if (hw_exposure >= 0)
nl_snprintf(exp_buf, sizeof(exp_buf), "%d", hw_exposure);
else
nl_snprintf(exp_buf, sizeof(exp_buf), "n/a");
if (hw_contrast >= 0)
nl_snprintf(con_buf, sizeof(con_buf), "%d", hw_contrast);
else
nl_snprintf(con_buf, sizeof(con_buf), "n/a");
if (hw_wb >= 0)
nl_snprintf(wb_buf, sizeof(wb_buf), "%dK", hw_wb);
else
nl_snprintf(wb_buf, sizeof(wb_buf), "n/a");
if (color) {
n = nl_snprintf(buf, sizeof(buf),
"\033[38;2;220;160;0m hw exposure: %s (e/E) hw contrast: "
"%s (c/C) hw white-balance: %s (w/W)\033[0m\033[K",
exp_buf, con_buf, wb_buf);
} else {
n = nl_snprintf(buf, sizeof(buf),
" hw exposure: %s hw contrast: %s hw white-balance: "
"%s\033[K",
exp_buf, con_buf, wb_buf);
}
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 + 3);
if (n > 0)
(void)write(STDOUT_FILENO, buf, (size_t)n);
// Plugin cells
if (count == 0) {
const char *msg = color ? "\033[38;2;120;120;120m no plugins loaded \033[0m"
@ -363,6 +401,11 @@ int main(int argc, char *argv[]) {
opts.edges != EDGE_OFF ? " | edges" : "",
opts.dither ? " | dither" : "");
int hw_exposure = -1, hw_contrast = -1, hw_wb = -1;
webcam_get_exposure(&cam, &hw_exposure);
webcam_get_contrast(&cam, &hw_contrast);
webcam_get_white_balance(&cam, &hw_wb);
// Pixel buffers allocation
int cam_pixels = cam.width * cam.height;
uint8_t *gray = malloc(cam_pixels);
@ -523,6 +566,24 @@ int main(int argc, char *argv[]) {
case 'v':
opts.depth_invert = !opts.depth_invert;
break;
case 'e':
webcam_adjust_exposure(&cam, -10, &hw_exposure);
break;
case 'E':
webcam_adjust_exposure(&cam, 10, &hw_exposure);
break;
case 'w':
webcam_adjust_white_balance(&cam, -100, &hw_wb);
break;
case 'W':
webcam_adjust_white_balance(&cam, 100, &hw_wb);
break;
case 'c':
webcam_adjust_contrast(&cam, -5, &hw_contrast);
break;
case 'C':
webcam_adjust_contrast(&cam, 5, &hw_contrast);
break;
}
}
if (!keep_running)
@ -579,7 +640,6 @@ int main(int argc, char *argv[]) {
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) {
@ -589,7 +649,8 @@ int main(int argc, char *argv[]) {
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, &opts, &charsets);
selected, opts.color, &opts, &charsets, hw_exposure,
hw_contrast, hw_wb);
}
if (webcam_requeue_buffer(&cam) < 0) {