From 8a3a09c5201d52cd323ba7ab10a2a6db72534da4 Mon Sep 17 00:00:00 2001 From: ApfelTeeSaft <91074565+ApfelTeeSaft@users.noreply.github.com> Date: Tue, 28 Oct 2025 19:17:52 +0100 Subject: [PATCH] Release --- .gitignore | 35 + 3ds-app/Makefile | 260 +++ 3ds-app/source/camera_stream.c | 369 ++++ 3ds-app/source/camera_stream.h | 69 + 3ds-app/source/main.c | 560 ++++++ 3ds-app/source/third_party/stb_image_write.h | 1724 ++++++++++++++++++ README.md | 508 ++++++ docs/design.md | 479 +++++ docs/troubleshooting.md | 733 ++++++++ examples/sample-config.txt | 11 + pc-server/requirements.txt | 4 + pc-server/run_server.ps1 | 55 + pc-server/run_server.sh | 16 + pc-server/server.py | 599 ++++++ scripts/build-3ds.sh | 66 + scripts/create-v4l2.sh | 82 + scripts/install-devkitpro.sh | 55 + scripts/pc-install.sh | 69 + scripts/windows/helpers.psm1 | 104 ++ scripts/windows/pc-install.ps1 | 160 ++ 20 files changed, 5958 insertions(+) create mode 100644 .gitignore create mode 100644 3ds-app/Makefile create mode 100644 3ds-app/source/camera_stream.c create mode 100644 3ds-app/source/camera_stream.h create mode 100644 3ds-app/source/main.c create mode 100644 3ds-app/source/third_party/stb_image_write.h create mode 100644 README.md create mode 100644 docs/design.md create mode 100644 docs/troubleshooting.md create mode 100644 examples/sample-config.txt create mode 100644 pc-server/requirements.txt create mode 100644 pc-server/run_server.ps1 create mode 100644 pc-server/run_server.sh create mode 100644 pc-server/server.py create mode 100644 scripts/build-3ds.sh create mode 100644 scripts/create-v4l2.sh create mode 100644 scripts/install-devkitpro.sh create mode 100644 scripts/pc-install.sh create mode 100644 scripts/windows/helpers.psm1 create mode 100644 scripts/windows/pc-install.ps1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..30f0573 --- /dev/null +++ b/.gitignore @@ -0,0 +1,35 @@ +# Build artifacts +dist/ +*.3dsx +*.elf +*.smdh +*.cia + +# 3DS build +3ds-app/build/ +*.o +*.d + +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +.venv/ +venv/ +env/ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Config (user-specific) +config.local.json diff --git a/3ds-app/Makefile b/3ds-app/Makefile new file mode 100644 index 0000000..7d79269 --- /dev/null +++ b/3ds-app/Makefile @@ -0,0 +1,260 @@ +#--------------------------------------------------------------------------------- +.SUFFIXES: +#--------------------------------------------------------------------------------- + +ifeq ($(strip $(DEVKITARM)),) +$(error "Please set DEVKITARM in your environment. export DEVKITARM=devkitARM") +endif + +TOPDIR ?= $(CURDIR) +include $(DEVKITARM)/3ds_rules + +#--------------------------------------------------------------------------------- +# TARGET is the name of the output +# BUILD is the directory where object files & intermediate files will be placed +# SOURCES is a list of directories containing source code +# DATA is a list of directories containing data files +# INCLUDES is a list of directories containing header files +# GRAPHICS is a list of directories containing graphics files +# GFXBUILD is the directory where converted graphics files will be placed +# If set to $(BUILD), it will statically link in the converted +# files as if they were data files. +# +# NO_SMDH: if set to anything, no SMDH file is generated. +# ROMFS is the directory which contains the RomFS, relative to the Makefile (Optional) +# APP_TITLE is the name of the app stored in the SMDH file (Optional) +# APP_DESCRIPTION is the description of the app stored in the SMDH file (Optional) +# APP_AUTHOR is the author of the app stored in the SMDH file (Optional) +# ICON is the filename of the icon (.png), relative to the project folder. +# If not set, it attempts to use one of the following (in this order): +# - .png +# - icon.png +# - /default_icon.png +#--------------------------------------------------------------------------------- +TARGET := 3ds-webcam-bridge +BUILD := build +SOURCES := source +DATA := data +INCLUDES := source source/third_party +GRAPHICS := gfx +#ROMFS := romfs + +APP_TITLE := 3DS Webcam Bridge +APP_DESCRIPTION := Stream 3DS cameras to PC as virtual webcam +APP_AUTHOR := CTR-WebCam Contributors + +#--------------------------------------------------------------------------------- +# options for code generation +#--------------------------------------------------------------------------------- +ARCH := -march=armv6k -mtune=mpcore -mfloat-abi=hard -mtp=soft + +CFLAGS := -g -Wall -O2 -mword-relocations \ + -ffunction-sections \ + $(ARCH) + +CFLAGS += $(INCLUDE) -D__3DS__ + +CXXFLAGS := $(CFLAGS) -fno-rtti -fno-exceptions -std=gnu++11 + +ASFLAGS := -g $(ARCH) +LDFLAGS = -specs=3dsx.specs -g $(ARCH) -Wl,-Map,$(notdir $*.map) + +LIBS := -lctru -lm + +#--------------------------------------------------------------------------------- +# list of directories containing libraries, this must be the top level containing +# include and lib +#--------------------------------------------------------------------------------- +LIBDIRS := $(CTRULIB) + + +#--------------------------------------------------------------------------------- +# no real need to edit anything past this point unless you need to add additional +# rules for different file extensions +#--------------------------------------------------------------------------------- +ifneq ($(BUILD),$(notdir $(CURDIR))) +#--------------------------------------------------------------------------------- + +export OUTPUT := $(CURDIR)/../dist/$(TARGET) +export TOPDIR := $(CURDIR) + +export VPATH := $(foreach dir,$(SOURCES),$(CURDIR)/$(dir)) \ + $(foreach dir,$(GRAPHICS),$(CURDIR)/$(dir)) \ + $(foreach dir,$(DATA),$(CURDIR)/$(dir)) + +export DEPSDIR := $(CURDIR)/$(BUILD) + +CFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.c))) +CPPFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.cpp))) +SFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.s))) +PICAFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.v.pica))) +SHLISTFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.shlist))) +GFXFILES := $(foreach dir,$(GRAPHICS),$(notdir $(wildcard $(dir)/*.t3s))) +BINFILES := $(foreach dir,$(DATA),$(notdir $(wildcard $(dir)/*.*))) + +#--------------------------------------------------------------------------------- +# use CXX for linking C++ projects, CC for standard C +#--------------------------------------------------------------------------------- +ifeq ($(strip $(CPPFILES)),) +#--------------------------------------------------------------------------------- + export LD := $(CC) +#--------------------------------------------------------------------------------- +else +#--------------------------------------------------------------------------------- + export LD := $(CXX) +#--------------------------------------------------------------------------------- +endif +#--------------------------------------------------------------------------------- + +#--------------------------------------------------------------------------------- +ifeq ($(GFXBUILD),$(BUILD)) +#--------------------------------------------------------------------------------- +export T3XFILES := $(GFXFILES:.t3s=.t3x) +#--------------------------------------------------------------------------------- +else +#--------------------------------------------------------------------------------- +export ROMFS_T3XFILES := $(patsubst %.t3s, $(GFXBUILD)/%.t3x, $(GFXFILES)) +export T3XHFILES := $(patsubst %.t3s, $(BUILD)/%.h, $(GFXFILES)) +#--------------------------------------------------------------------------------- +endif +#--------------------------------------------------------------------------------- + +export OFILES_SOURCES := $(CPPFILES:.cpp=.o) $(CFILES:.c=.o) $(SFILES:.s=.o) + +export OFILES_BIN := $(addsuffix .o,$(BINFILES)) \ + $(PICAFILES:.v.pica=.shbin.o) $(SHLISTFILES:.shlist=.shbin.o) \ + $(addsuffix .o,$(T3XFILES)) + +export OFILES := $(OFILES_BIN) $(OFILES_SOURCES) + +export HFILES := $(PICAFILES:.v.pica=_shbin.h) $(SHLISTFILES:.shlist=_shbin.h) \ + $(addsuffix .h,$(subst .,_,$(BINFILES))) \ + $(GFXFILES:.t3s=.h) + +export INCLUDE := $(foreach dir,$(INCLUDES),-I$(CURDIR)/$(dir)) \ + $(foreach dir,$(LIBDIRS),-I$(dir)/include) \ + -I$(CURDIR)/$(BUILD) + +export LIBPATHS := $(foreach dir,$(LIBDIRS),-L$(dir)/lib) + +export _3DSXDEPS := $(if $(NO_SMDH),,$(OUTPUT).smdh) + +ifeq ($(strip $(ICON)),) + icons := $(wildcard *.png) + ifneq (,$(findstring $(TARGET).png,$(icons))) + export APP_ICON := $(TOPDIR)/$(TARGET).png + else + ifneq (,$(findstring icon.png,$(icons))) + export APP_ICON := $(TOPDIR)/icon.png + endif + endif +else + export APP_ICON := $(TOPDIR)/$(ICON) +endif + +ifeq ($(strip $(NO_SMDH)),) + export _3DSXFLAGS += --smdh=$(CURDIR)/../dist/$(TARGET).smdh +endif + +ifneq ($(ROMFS),) + export _3DSXFLAGS += --romfs=$(CURDIR)/$(ROMFS) +endif + +.PHONY: all clean + +#--------------------------------------------------------------------------------- +all: $(BUILD) $(GFXBUILD) $(DEPSDIR) $(ROMFS_T3XFILES) $(T3XHFILES) + @mkdir -p $(CURDIR)/../dist + @$(MAKE) --no-print-directory -C $(BUILD) -f $(CURDIR)/Makefile + +$(BUILD): + @mkdir -p $@ + +ifneq ($(GFXBUILD),$(BUILD)) +$(GFXBUILD): + @mkdir -p $@ +endif + +ifneq ($(DEPSDIR),$(BUILD)) +$(DEPSDIR): + @mkdir -p $@ +endif + +#--------------------------------------------------------------------------------- +clean: + @echo clean ... + @rm -fr $(BUILD) $(GFXBUILD) $(TARGET).3dsx $(OUTPUT).smdh $(TARGET).elf + @rm -fr $(CURDIR)/../dist/$(TARGET).* + + +#--------------------------------------------------------------------------------- +$(GFXBUILD)/%.t3x $(BUILD)/%.h : %.t3s +#--------------------------------------------------------------------------------- + @echo $(notdir $<) + @tex3ds -i $< -H $(BUILD)/$*.h -d $(DEPSDIR)/$*.d -o $(GFXBUILD)/$*.t3x + +#--------------------------------------------------------------------------------- +else + +#--------------------------------------------------------------------------------- +# main targets +#--------------------------------------------------------------------------------- +$(OUTPUT).3dsx : $(OUTPUT).elf $(_3DSXDEPS) + +$(OFILES_SOURCES) : $(HFILES) + +$(OUTPUT).elf : $(OFILES) + +#--------------------------------------------------------------------------------- +# you need a rule like this for each extension you use as binary data +#--------------------------------------------------------------------------------- +%.bin.o %_bin.h : %.bin +#--------------------------------------------------------------------------------- + @echo $(notdir $<) + @$(bin2o) + +#--------------------------------------------------------------------------------- +.PRECIOUS : %.t3x +#--------------------------------------------------------------------------------- +%.t3x.o %_t3x.h : %.t3x +#--------------------------------------------------------------------------------- + @echo $(notdir $<) + @$(bin2o) + +#--------------------------------------------------------------------------------- +# rules for assembling GPU shaders +#--------------------------------------------------------------------------------- +define shader-as + $(eval CURBIN := $*.shbin) + $(eval DEPSFILE := $(DEPSDIR)/$*.shbin.d) + echo "$(CURBIN).o: $< $1" > $(DEPSFILE) + echo "extern const u8" `(echo $(CURBIN) | sed -e 's/^\([0-9]\)/_\1/' | tr . _)`"_end[];" > `(echo $(CURBIN) | tr . _)`.h + echo "extern const u8" `(echo $(CURBIN) | sed -e 's/^\([0-9]\)/_\1/' | tr . _)`"[];" >> `(echo $(CURBIN) | tr . _)`.h + echo "extern const u32" `(echo $(CURBIN) | sed -e 's/^\([0-9]\)/_\1/' | tr . _)`_size";" >> `(echo $(CURBIN) | tr . _)`.h + picasso -o $(CURBIN) $1 + bin2s $(CURBIN) | $(AS) -o $*.shbin.o +endef + +%.shbin.o %_shbin.h : %.v.pica %.g.pica + @echo $(notdir $^) + @$(call shader-as,$^) + +%.shbin.o %_shbin.h : %.v.pica + @echo $(notdir $<) + @$(call shader-as,$<) + +%.shbin.o %_shbin.h : %.shlist + @echo $(notdir $<) + @$(call shader-as,$(foreach file,$(shell cat $<),$(dir $<)$(file))) + +#--------------------------------------------------------------------------------- +%.t3x %.h : %.t3s +#--------------------------------------------------------------------------------- + @echo $(notdir $<) + @tex3ds -i $< -H $*.h -d $*.d -o $*.t3x + +-include $(DEPSDIR)/*.d + +#--------------------------------------------------------------------------------------- +endif +#--------------------------------------------------------------------------------------- diff --git a/3ds-app/source/camera_stream.c b/3ds-app/source/camera_stream.c new file mode 100644 index 0000000..85d6c95 --- /dev/null +++ b/3ds-app/source/camera_stream.c @@ -0,0 +1,369 @@ +#include "camera_stream.h" +#include +#include +#include + +#define STB_IMAGE_WRITE_IMPLEMENTATION +#include "third_party/stb_image_write.h" + +// Global buffers +static uint16_t* cam_buffer_left = NULL; +static uint16_t* cam_buffer_right = NULL; +static uint8_t* rgb_buffer = NULL; + +// Camera handles +static u32 cam_left = 0; +static u32 cam_right = 0; +static bool cameras_active = false; + +// STB callback for JPEG encoding +static void stb_write_callback(void* context, void* data, int size) { + JpegBuffer* buf = (JpegBuffer*)context; + + // Resize if needed + if (buf->size + size > buf->capacity) { + size_t new_capacity = buf->capacity * 2; + if (new_capacity < buf->size + size) { + new_capacity = buf->size + size; + } + uint8_t* new_data = realloc(buf->data, new_capacity); + if (!new_data) return; + buf->data = new_data; + buf->capacity = new_capacity; + } + + memcpy(buf->data + buf->size, data, size); + buf->size += size; +} + +JpegBuffer* jpeg_buffer_create(size_t initial_capacity) { + JpegBuffer* buf = malloc(sizeof(JpegBuffer)); + if (!buf) return NULL; + + buf->data = malloc(initial_capacity); + if (!buf->data) { + free(buf); + return NULL; + } + + buf->size = 0; + buf->capacity = initial_capacity; + return buf; +} + +void jpeg_buffer_free(JpegBuffer* buf) { + if (buf) { + if (buf->data) free(buf->data); + free(buf); + } +} + +Result camera_init(void) { + Result ret = camInit(); + if (R_FAILED(ret)) return ret; + + // Allocate buffers + cam_buffer_left = memalign(0x80, CAM_BUFFER_SIZE); + cam_buffer_right = memalign(0x80, CAM_BUFFER_SIZE); + rgb_buffer = malloc(RGB_BUFFER_SIZE); + + if (!cam_buffer_left || !cam_buffer_right || !rgb_buffer) { + camera_shutdown(); + return -1; + } + + return 0; +} + +void camera_shutdown(void) { + if (cameras_active) { + camera_stop(); + } + + if (cam_buffer_left) { + free(cam_buffer_left); + cam_buffer_left = NULL; + } + if (cam_buffer_right) { + free(cam_buffer_right); + cam_buffer_right = NULL; + } + if (rgb_buffer) { + free(rgb_buffer); + rgb_buffer = NULL; + } + + camExit(); +} + +Result camera_start(CameraMode mode) { + Result ret; + + if (cameras_active) { + camera_stop(); + } + + // Select camera(s) + u32 select; + switch (mode) { + case CAM_MODE_INNER: + select = SELECT_IN1; + break; + case CAM_MODE_OUTER_MONO: + case CAM_MODE_OUTER_STEREO: + select = SELECT_OUT1 | SELECT_OUT2; + break; + default: + return -1; + } + + // Set camera parameters + ret = CAMU_SetSize(select, SIZE_CTR_TOP_LCD, CONTEXT_A); + if (R_FAILED(ret)) return ret; + + ret = CAMU_SetOutputFormat(select, OUTPUT_YUV_422, CONTEXT_A); + if (R_FAILED(ret)) return ret; + + ret = CAMU_SetFrameRate(select, FRAME_RATE_15); + if (R_FAILED(ret)) return ret; + + ret = CAMU_SetNoiseFilter(select, true); + if (R_FAILED(ret)) return ret; + + ret = CAMU_SetAutoExposure(select, true); + if (R_FAILED(ret)) return ret; + + ret = CAMU_SetAutoWhiteBalance(select, true); + if (R_FAILED(ret)) return ret; + + ret = CAMU_SetTrimming(select, false); + if (R_FAILED(ret)) return ret; + + // Activate cameras + ret = CAMU_Activate(select); + if (R_FAILED(ret)) return ret; + + cameras_active = true; + return 0; +} + +void camera_stop(void) { + if (cameras_active) { + CAMU_Activate(SELECT_NONE); + cameras_active = false; + } +} + +void yuv422_to_rgb24(const uint16_t* yuv, uint8_t* rgb, int width, int height) { + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x += 2) { + int idx = y * width + x; + uint16_t px1 = yuv[idx]; + uint16_t px2 = yuv[idx + 1]; + + // YUV422: [Y0 U Y1 V] packed in 16-bit values + int y0 = (px1 >> 8) & 0xFF; + int u = px1 & 0xFF; + int y1 = (px2 >> 8) & 0xFF; + int v = px2 & 0xFF; + + // YUV to RGB conversion + int c0 = y0 - 16; + int c1 = y1 - 16; + int d = u - 128; + int e = v - 128; + + // Pixel 0 + int r0 = (298 * c0 + 409 * e + 128) >> 8; + int g0 = (298 * c0 - 100 * d - 208 * e + 128) >> 8; + int b0 = (298 * c0 + 516 * d + 128) >> 8; + + // Pixel 1 + int r1 = (298 * c1 + 409 * e + 128) >> 8; + int g1 = (298 * c1 - 100 * d - 208 * e + 128) >> 8; + int b1 = (298 * c1 + 516 * d + 128) >> 8; + + // Clamp + r0 = r0 < 0 ? 0 : (r0 > 255 ? 255 : r0); + g0 = g0 < 0 ? 0 : (g0 > 255 ? 255 : g0); + b0 = b0 < 0 ? 0 : (b0 > 255 ? 255 : b0); + r1 = r1 < 0 ? 0 : (r1 > 255 ? 255 : r1); + g1 = g1 < 0 ? 0 : (g1 > 255 ? 255 : g1); + b1 = b1 < 0 ? 0 : (b1 > 255 ? 255 : b1); + + // Write RGB + int out_idx = (y * width + x) * 3; + rgb[out_idx + 0] = r0; + rgb[out_idx + 1] = g0; + rgb[out_idx + 2] = b0; + rgb[out_idx + 3] = r1; + rgb[out_idx + 4] = g1; + rgb[out_idx + 5] = b1; + } + } +} + +Result camera_capture_frame(CameraMode mode, JpegBuffer* left, JpegBuffer* right) { + Result ret; + u32 transfer_size = 0; + + // Reset JPEG buffer sizes + if (left) left->size = 0; + if (right) right->size = 0; + + switch (mode) { + case CAM_MODE_INNER: + case CAM_MODE_OUTER_MONO: { + // Capture left/main camera only + u32 cam_select = (mode == CAM_MODE_INNER) ? SELECT_IN1 : SELECT_OUT1; + + ret = CAMU_GetMaxBytes(&transfer_size, CAM_WIDTH, CAM_HEIGHT); + if (R_FAILED(ret)) return ret; + + ret = CAMU_SetTransferBytes(cam_select, transfer_size, CAM_WIDTH, CAM_HEIGHT); + if (R_FAILED(ret)) return ret; + + ret = CAMU_ClearBuffer(PORT_CAM1); + if (R_FAILED(ret)) return ret; + + ret = CAMU_StartCapture(cam_select); + if (R_FAILED(ret)) return ret; + + ret = CAMU_SetReceiving(&cam_left, cam_buffer_left, cam_select, transfer_size, 0); + if (R_FAILED(ret)) return ret; + + // Wait for transfer + svcWaitSynchronization(cam_left, U64_MAX); + svcCloseHandle(cam_left); + + ret = CAMU_StopCapture(cam_select); + if (R_FAILED(ret)) return ret; + + // Convert to RGB + yuv422_to_rgb24(cam_buffer_left, rgb_buffer, CAM_WIDTH, CAM_HEIGHT); + + // Encode JPEG + if (left) { + stbi_write_jpg_to_func(stb_write_callback, left, CAM_WIDTH, CAM_HEIGHT, 3, + rgb_buffer, JPEG_QUALITY); + } + break; + } + + case CAM_MODE_OUTER_STEREO: { + // Capture both outer cameras + ret = CAMU_GetMaxBytes(&transfer_size, CAM_WIDTH, CAM_HEIGHT); + if (R_FAILED(ret)) return ret; + + // Left camera (OUT1) + ret = CAMU_SetTransferBytes(SELECT_OUT1, transfer_size, CAM_WIDTH, CAM_HEIGHT); + if (R_FAILED(ret)) return ret; + + ret = CAMU_ClearBuffer(PORT_CAM1); + if (R_FAILED(ret)) return ret; + + ret = CAMU_StartCapture(SELECT_OUT1); + if (R_FAILED(ret)) return ret; + + ret = CAMU_SetReceiving(&cam_left, cam_buffer_left, SELECT_OUT1, transfer_size, 0); + if (R_FAILED(ret)) return ret; + + // Right camera (OUT2) + ret = CAMU_SetTransferBytes(SELECT_OUT2, transfer_size, CAM_WIDTH, CAM_HEIGHT); + if (R_FAILED(ret)) return ret; + + ret = CAMU_ClearBuffer(PORT_CAM2); + if (R_FAILED(ret)) return ret; + + ret = CAMU_StartCapture(SELECT_OUT2); + if (R_FAILED(ret)) return ret; + + ret = CAMU_SetReceiving(&cam_right, cam_buffer_right, SELECT_OUT2, transfer_size, 0); + if (R_FAILED(ret)) return ret; + + // Wait for both transfers + svcWaitSynchronization(cam_left, U64_MAX); + svcWaitSynchronization(cam_right, U64_MAX); + svcCloseHandle(cam_left); + svcCloseHandle(cam_right); + + ret = CAMU_StopCapture(SELECT_OUT1 | SELECT_OUT2); + if (R_FAILED(ret)) return ret; + + // Convert left to RGB + yuv422_to_rgb24(cam_buffer_left, rgb_buffer, CAM_WIDTH, CAM_HEIGHT); + if (left) { + stbi_write_jpg_to_func(stb_write_callback, left, CAM_WIDTH, CAM_HEIGHT, 3, + rgb_buffer, JPEG_QUALITY); + } + + // Convert right to RGB + yuv422_to_rgb24(cam_buffer_right, rgb_buffer, CAM_WIDTH, CAM_HEIGHT); + if (right) { + stbi_write_jpg_to_func(stb_write_callback, right, CAM_WIDTH, CAM_HEIGHT, 3, + rgb_buffer, JPEG_QUALITY); + } + break; + } + } + + return 0; +} + +int sendall(int sock, const void* buf, size_t len) { + const uint8_t* ptr = buf; + size_t sent = 0; + + while (sent < len) { + int n = send(sock, ptr + sent, len - sent, 0); + if (n < 0) return -1; + if (n == 0) return -2; // Connection closed + sent += n; + } + + return 0; +} + +Result send_frame_mono(int sock, JpegBuffer* jpeg) { + uint8_t header[6]; + + // Header: [flags][version][length] + header[0] = 0x00; // No stereo flag + header[1] = PROTO_VERSION; + uint32_t len = htonl(jpeg->size); + memcpy(&header[2], &len, 4); + + if (sendall(sock, header, 6) < 0) return -1; + if (sendall(sock, jpeg->data, jpeg->size) < 0) return -1; + + return 0; +} + +Result send_frame_stereo(int sock, JpegBuffer* left, JpegBuffer* right, + uint16_t wL, uint16_t hL, uint16_t wR, uint16_t hR) { + uint8_t header[18]; + + // Header: [flags][version][wL][hL][wR][hR][left_len][right_len] + header[0] = PROTO_FLAG_STEREO; + header[1] = PROTO_VERSION; + + uint16_t wL_be = htons(wL); + uint16_t hL_be = htons(hL); + uint16_t wR_be = htons(wR); + uint16_t hR_be = htons(hR); + memcpy(&header[2], &wL_be, 2); + memcpy(&header[4], &hL_be, 2); + memcpy(&header[6], &wR_be, 2); + memcpy(&header[8], &hR_be, 2); + + uint32_t left_len = htonl(left->size); + uint32_t right_len = htonl(right->size); + memcpy(&header[10], &left_len, 4); + memcpy(&header[14], &right_len, 4); + + if (sendall(sock, header, 18) < 0) return -1; + if (sendall(sock, left->data, left->size) < 0) return -1; + if (sendall(sock, right->data, right->size) < 0) return -1; + + return 0; +} diff --git a/3ds-app/source/camera_stream.h b/3ds-app/source/camera_stream.h new file mode 100644 index 0000000..c2e0aaa --- /dev/null +++ b/3ds-app/source/camera_stream.h @@ -0,0 +1,69 @@ +#ifndef CAMERA_STREAM_H +#define CAMERA_STREAM_H + +#include <3ds.h> +#include +#include + +// Camera configuration +#define CAM_WIDTH 400 +#define CAM_HEIGHT 240 +#define CAM_BUFFER_SIZE (CAM_WIDTH * CAM_HEIGHT * 2) +#define RGB_BUFFER_SIZE (CAM_WIDTH * CAM_HEIGHT * 3) + +// JPEG quality (0-100) +#define JPEG_QUALITY 65 + +// Protocol constants +#define PROTO_VERSION 0x01 +#define PROTO_FLAG_STEREO 0x01 + +// Camera mode +typedef enum { + CAM_MODE_INNER, + CAM_MODE_OUTER_MONO, + CAM_MODE_OUTER_STEREO +} CameraMode; + +// JPEG encoded buffer +typedef struct { + uint8_t* data; + size_t size; + size_t capacity; +} JpegBuffer; + +// Initialize camera subsystem +Result camera_init(void); + +// Shutdown camera subsystem +void camera_shutdown(void); + +// Start camera capture +Result camera_start(CameraMode mode); + +// Stop camera capture +void camera_stop(void); + +// Capture and encode frame(s) +Result camera_capture_frame(CameraMode mode, JpegBuffer* left, JpegBuffer* right); + +// Send mono frame over socket +Result send_frame_mono(int sock, JpegBuffer* jpeg); + +// Send stereo frame over socket +Result send_frame_stereo(int sock, JpegBuffer* left, JpegBuffer* right, + uint16_t wL, uint16_t hL, uint16_t wR, uint16_t hR); + +// Helper: convert YUV422 to RGB24 +void yuv422_to_rgb24(const uint16_t* yuv, uint8_t* rgb, int width, int height); + +// Helper: send all data +int sendall(int sock, const void* buf, size_t len); + +// Helper: allocate JPEG buffer +JpegBuffer* jpeg_buffer_create(size_t initial_capacity); + +// Helper: free JPEG buffer +void jpeg_buffer_free(JpegBuffer* buf); + +#endif // CAMERA_STREAM_H diff --git a/3ds-app/source/main.c b/3ds-app/source/main.c new file mode 100644 index 0000000..33e5055 --- /dev/null +++ b/3ds-app/source/main.c @@ -0,0 +1,560 @@ +#include <3ds.h> +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "camera_stream.h" + +#define SOC_BUFFER_SIZE 0x100000 +#define SERVER_PORT 9000 +#define RECONNECT_DELAY_MS 3000 +#define CONFIG_PATH "/3ds/3ds-webcam-bridge/config.txt" + +// Application state +typedef struct { + char server_ip[32]; + int server_port; + CameraMode camera_mode; + bool stereo_enabled; + bool streaming; + bool should_exit; + + // Stats + u64 frames_sent; + u64 bytes_sent; + float fps; + bool connected; + + // Threading + Thread stream_thread; + LightLock state_lock; +} AppState; + +static AppState g_state; +static u32* soc_buffer = NULL; +static PrintConsole topScreen, bottomScreen; + +// Forward declarations +static void stream_thread_func(void* arg); +static void draw_ui(void); +static void handle_input(void); +static void load_config(void); +static void save_config(void); +static bool input_ip_address(void); +static bool input_port(void); + +int main(int argc, char** argv) { + Result ret; + + // Initialize services + gfxInitDefault(); + consoleInit(GFX_TOP, &topScreen); + consoleInit(GFX_BOTTOM, &bottomScreen); + + // Initialize SOC (sockets) + soc_buffer = memalign(0x1000, SOC_BUFFER_SIZE); + if (!soc_buffer) { + printf("Failed to allocate SOC buffer\n"); + goto cleanup; + } + + ret = socInit(soc_buffer, SOC_BUFFER_SIZE); + if (R_FAILED(ret)) { + printf("socInit failed: 0x%08lX\n", ret); + goto cleanup; + } + + // Initialize camera + ret = camera_init(); + if (R_FAILED(ret)) { + printf("camera_init failed: 0x%08lX\n", ret); + goto cleanup; + } + + // Initialize application state + memset(&g_state, 0, sizeof(AppState)); + strcpy(g_state.server_ip, "192.168.1.100"); // Default IP + g_state.server_port = SERVER_PORT; + g_state.camera_mode = CAM_MODE_OUTER_STEREO; + g_state.stereo_enabled = true; + g_state.streaming = false; + g_state.should_exit = false; + LightLock_Init(&g_state.state_lock); + + // Load config from SD card + load_config(); + + printf("3DS Webcam Bridge v1.0\n"); + printf("======================\n\n"); + printf("Press START to exit\n"); + printf("Press A to toggle streaming\n"); + printf("Press SELECT to change camera\n"); + printf("Press Y to edit IP/Port\n"); + printf("Press X to toggle 3D\n\n"); + + // Main loop + while (aptMainLoop() && !g_state.should_exit) { + handle_input(); + draw_ui(); + + gspWaitForVBlank(); + } + + // Cleanup + if (g_state.streaming) { + g_state.should_exit = true; + threadJoin(g_state.stream_thread, U64_MAX); + threadFree(g_state.stream_thread); + } + +cleanup: + camera_shutdown(); + socExit(); + + if (soc_buffer) { + free(soc_buffer); + } + + gfxExit(); + return 0; +} + +static void handle_input(void) { + hidScanInput(); + u32 kDown = hidKeysDown(); + + // Exit + if (kDown & KEY_START) { + g_state.should_exit = true; + return; + } + + // Toggle streaming + if (kDown & KEY_A) { + LightLock_Lock(&g_state.state_lock); + if (g_state.streaming) { + g_state.should_exit = true; + LightLock_Unlock(&g_state.state_lock); + threadJoin(g_state.stream_thread, U64_MAX); + threadFree(g_state.stream_thread); + LightLock_Lock(&g_state.state_lock); + g_state.streaming = false; + g_state.should_exit = false; + g_state.connected = false; + } else { + // Determine camera mode + if (g_state.camera_mode == CAM_MODE_INNER) { + // Inner camera - always mono + g_state.camera_mode = CAM_MODE_INNER; + } else { + // Outer cameras - check stereo flag + if (g_state.stereo_enabled) { + g_state.camera_mode = CAM_MODE_OUTER_STEREO; + } else { + g_state.camera_mode = CAM_MODE_OUTER_MONO; + } + } + + g_state.streaming = true; + g_state.frames_sent = 0; + g_state.bytes_sent = 0; + + s32 prio = 0; + svcGetThreadPriority(&prio, CUR_THREAD_HANDLE); + g_state.stream_thread = threadCreate(stream_thread_func, NULL, 8*1024, prio-1, -2, false); + } + LightLock_Unlock(&g_state.state_lock); + } + + // Change camera mode + if (kDown & KEY_SELECT) { + LightLock_Lock(&g_state.state_lock); + if (!g_state.streaming) { + if (g_state.camera_mode == CAM_MODE_INNER || + g_state.camera_mode == CAM_MODE_OUTER_MONO || + g_state.camera_mode == CAM_MODE_OUTER_STEREO) { + // Toggle between inner and outer + if (g_state.camera_mode == CAM_MODE_INNER) { + g_state.camera_mode = g_state.stereo_enabled ? + CAM_MODE_OUTER_STEREO : CAM_MODE_OUTER_MONO; + } else { + g_state.camera_mode = CAM_MODE_INNER; + g_state.stereo_enabled = false; // Inner doesn't support stereo + } + } + } + LightLock_Unlock(&g_state.state_lock); + } + + // Toggle stereo (only for outer cameras) + if (kDown & KEY_X) { + LightLock_Lock(&g_state.state_lock); + if (!g_state.streaming && + (g_state.camera_mode == CAM_MODE_OUTER_MONO || + g_state.camera_mode == CAM_MODE_OUTER_STEREO)) { + g_state.stereo_enabled = !g_state.stereo_enabled; + } + LightLock_Unlock(&g_state.state_lock); + } + + // Edit IP/Port (only when not streaming) + if (kDown & KEY_Y) { + LightLock_Lock(&g_state.state_lock); + if (!g_state.streaming) { + LightLock_Unlock(&g_state.state_lock); + + // Show menu on bottom screen + consoleSelect(&bottomScreen); + printf("\x1b[2J"); + printf("\x1b[0;0H"); + printf("=== Configuration ===\n\n"); + printf("A: Edit IP Address\n"); + printf("B: Edit Port\n"); + printf("X: Cancel\n\n"); + printf("Current:\n"); + printf("IP: %s\n", g_state.server_ip); + printf("Port: %d\n", g_state.server_port); + gfxFlushBuffers(); + gfxSwapBuffers(); + + // Wait for choice + while (aptMainLoop()) { + hidScanInput(); + u32 kPressed = hidKeysDown(); + + if (kPressed & KEY_A) { + if (input_ip_address()) { + save_config(); + } + break; + } else if (kPressed & KEY_B) { + if (input_port()) { + save_config(); + } + break; + } else if (kPressed & KEY_X) { + break; + } + gspWaitForVBlank(); + } + } else { + LightLock_Unlock(&g_state.state_lock); + } + } +} + +static void draw_ui(void) { + LightLock_Lock(&g_state.state_lock); + + // Top screen - Status + consoleSelect(&topScreen); + printf("\x1b[2J"); // Clear screen + printf("\x1b[0;0H"); // Home cursor + + printf("=== 3DS Webcam Bridge ===\n\n"); + + printf("Server: %s:%d\n", g_state.server_ip, g_state.server_port); + printf("Status: %s\n", g_state.streaming ? + (g_state.connected ? "STREAMING" : "CONNECTING...") : "STOPPED"); + + const char* cam_str = "Unknown"; + switch (g_state.camera_mode) { + case CAM_MODE_INNER: + cam_str = "Inner (Mono)"; + break; + case CAM_MODE_OUTER_MONO: + cam_str = "Outer (Mono)"; + break; + case CAM_MODE_OUTER_STEREO: + cam_str = "Outer (Stereo)"; + break; + } + printf("Camera: %s\n", cam_str); + printf("3D Mode: %s\n", g_state.stereo_enabled ? "Enabled" : "Disabled"); + + printf("\n--- Statistics ---\n"); + printf("Frames: %llu\n", g_state.frames_sent); + printf("Data: %.2f MB\n", g_state.bytes_sent / (1024.0 * 1024.0)); + printf("FPS: %.1f\n", g_state.fps); + + printf("\n--- Controls ---\n"); + printf("A: Start/Stop\n"); + printf("SELECT: Change Camera\n"); + printf("X: Toggle 3D%s\n", + (g_state.camera_mode == CAM_MODE_INNER) ? " (Outer only!)" : ""); + printf("Y: Edit IP/Port\n"); + printf("START: Exit\n"); + + // Bottom screen - Settings + consoleSelect(&bottomScreen); + printf("\x1b[2J"); + printf("\x1b[0;0H"); + + printf("=== Settings ===\n\n"); + + printf("Network Configuration:\n"); + printf("IP: %s\n", g_state.server_ip); + printf("Port: %d\n", g_state.server_port); + printf("-> Press Y to edit\n"); + + printf("\nCamera Selection:\n"); + printf("%s Inner (single lens)\n", + (g_state.camera_mode == CAM_MODE_INNER) ? ">" : " "); + printf("%s Outer (dual lens)\n", + (g_state.camera_mode != CAM_MODE_INNER) ? ">" : " "); + + printf("\n3D Stereo Mode:\n"); + if (g_state.camera_mode == CAM_MODE_INNER) { + printf(" (Requires outer cameras)\n"); + } else { + printf("[%c] Enable 3D (stereo)\n", g_state.stereo_enabled ? 'X' : ' '); + printf("Press X to toggle\n"); + } + + printf("\nResolution: %dx%d\n", CAM_WIDTH, CAM_HEIGHT); + printf("JPEG Quality: %d\n", JPEG_QUALITY); + + LightLock_Unlock(&g_state.state_lock); + gfxFlushBuffers(); + gfxSwapBuffers(); +} + +static void stream_thread_func(void* arg) { + (void)arg; + + int sock = -1; + JpegBuffer* jpeg_left = NULL; + JpegBuffer* jpeg_right = NULL; + u64 last_time = 0; + u32 frame_count = 0; + + jpeg_left = jpeg_buffer_create(CAM_WIDTH * CAM_HEIGHT); + jpeg_right = jpeg_buffer_create(CAM_WIDTH * CAM_HEIGHT); + + if (!jpeg_left || !jpeg_right) { + goto thread_cleanup; + } + + while (!g_state.should_exit) { + // Connect to server + sock = socket(AF_INET, SOCK_STREAM, 0); + if (sock < 0) { + svcSleepThread(RECONNECT_DELAY_MS * 1000000ULL); + continue; + } + + // Set socket options + int flag = 1; + setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, &flag, sizeof(flag)); + + struct sockaddr_in server_addr; + memset(&server_addr, 0, sizeof(server_addr)); + server_addr.sin_family = AF_INET; + server_addr.sin_port = htons(g_state.server_port); + inet_aton(g_state.server_ip, &server_addr.sin_addr); + + if (connect(sock, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) { + close(sock); + sock = -1; + svcSleepThread(RECONNECT_DELAY_MS * 1000000ULL); + continue; + } + + // Connected + LightLock_Lock(&g_state.state_lock); + g_state.connected = true; + CameraMode mode = g_state.camera_mode; + LightLock_Unlock(&g_state.state_lock); + + // Start camera + if (R_FAILED(camera_start(mode))) { + close(sock); + sock = -1; + continue; + } + + // Streaming loop + last_time = svcGetSystemTick(); + frame_count = 0; + + while (!g_state.should_exit) { + // Capture frame + Result ret = camera_capture_frame(mode, jpeg_left, + (mode == CAM_MODE_OUTER_STEREO) ? jpeg_right : NULL); + + if (R_FAILED(ret)) { + break; + } + + // Send frame + if (mode == CAM_MODE_OUTER_STEREO) { + ret = send_frame_stereo(sock, jpeg_left, jpeg_right, + CAM_WIDTH, CAM_HEIGHT, CAM_WIDTH, CAM_HEIGHT); + } else { + ret = send_frame_mono(sock, jpeg_left); + } + + if (R_FAILED(ret)) { + break; // Connection lost + } + + // Update stats + LightLock_Lock(&g_state.state_lock); + g_state.frames_sent++; + g_state.bytes_sent += jpeg_left->size; + if (mode == CAM_MODE_OUTER_STEREO) { + g_state.bytes_sent += jpeg_right->size; + } + + frame_count++; + u64 current_time = svcGetSystemTick(); + u64 elapsed_ms = (current_time - last_time) * 1000 / CPU_TICKS_PER_MSEC; + if (elapsed_ms >= 1000) { + g_state.fps = (float)frame_count * 1000.0f / elapsed_ms; + frame_count = 0; + last_time = current_time; + } + LightLock_Unlock(&g_state.state_lock); + + // Frame rate limiting (target ~15 FPS) + svcSleepThread(33 * 1000000ULL); // ~30ms + } + + // Cleanup connection + camera_stop(); + close(sock); + sock = -1; + + LightLock_Lock(&g_state.state_lock); + g_state.connected = false; + LightLock_Unlock(&g_state.state_lock); + + if (g_state.should_exit) break; + + // Wait before reconnecting + svcSleepThread(RECONNECT_DELAY_MS * 1000000ULL); + } + +thread_cleanup: + if (sock >= 0) { + close(sock); + } + if (jpeg_left) { + jpeg_buffer_free(jpeg_left); + } + if (jpeg_right) { + jpeg_buffer_free(jpeg_right); + } +} + +// Configuration file loading +static void load_config(void) { + FILE* f = fopen(CONFIG_PATH, "r"); + if (!f) { + return; // Use defaults if file doesn't exist + } + + char line[128]; + while (fgets(line, sizeof(line), f)) { + // Remove newline + line[strcspn(line, "\r\n")] = 0; + + // Parse key=value + char* equals = strchr(line, '='); + if (!equals) continue; + + *equals = '\0'; + char* key = line; + char* value = equals + 1; + + if (strcmp(key, "ip") == 0) { + strncpy(g_state.server_ip, value, sizeof(g_state.server_ip) - 1); + g_state.server_ip[sizeof(g_state.server_ip) - 1] = '\0'; + } else if (strcmp(key, "port") == 0) { + int port = atoi(value); + if (port > 0 && port < 65536) { + g_state.server_port = port; + } + } + } + + fclose(f); +} + +// Configuration file saving +static void save_config(void) { + // Create directory if it doesn't exist + mkdir("/3ds", 0777); + mkdir("/3ds/3ds-webcam-bridge", 0777); + + FILE* f = fopen(CONFIG_PATH, "w"); + if (!f) { + return; + } + + fprintf(f, "ip=%s\n", g_state.server_ip); + fprintf(f, "port=%d\n", g_state.server_port); + + fclose(f); +} + +// Input IP address using software keyboard +static bool input_ip_address(void) { + SwkbdState swkbd; + char mybuf[32]; + SwkbdButton button = SWKBD_BUTTON_NONE; + + swkbdInit(&swkbd, SWKBD_TYPE_NORMAL, 2, -1); + swkbdSetHintText(&swkbd, "Enter IP address"); + swkbdSetInitialText(&swkbd, g_state.server_ip); + swkbdSetFeatures(&swkbd, SWKBD_MULTILINE); + + button = swkbdInputText(&swkbd, mybuf, sizeof(mybuf)); + + if (button == SWKBD_BUTTON_CONFIRM) { + // Basic validation + if (strlen(mybuf) > 0) { + strncpy(g_state.server_ip, mybuf, sizeof(g_state.server_ip) - 1); + g_state.server_ip[sizeof(g_state.server_ip) - 1] = '\0'; + return true; + } + } + + return false; +} + +// Input port using software keyboard +static bool input_port(void) { + SwkbdState swkbd; + char mybuf[16]; + char portstr[16]; + SwkbdButton button = SWKBD_BUTTON_NONE; + + snprintf(portstr, sizeof(portstr), "%d", g_state.server_port); + + swkbdInit(&swkbd, SWKBD_TYPE_NUMPAD, 2, -1); + swkbdSetHintText(&swkbd, "Enter port (1-65535)"); + swkbdSetInitialText(&swkbd, portstr); + swkbdSetFeatures(&swkbd, SWKBD_FIXED_WIDTH); + + button = swkbdInputText(&swkbd, mybuf, sizeof(mybuf)); + + if (button == SWKBD_BUTTON_CONFIRM) { + int port = atoi(mybuf); + if (port > 0 && port < 65536) { + g_state.server_port = port; + return true; + } + } + + return false; +} diff --git a/3ds-app/source/third_party/stb_image_write.h b/3ds-app/source/third_party/stb_image_write.h new file mode 100644 index 0000000..e4b32ed --- /dev/null +++ b/3ds-app/source/third_party/stb_image_write.h @@ -0,0 +1,1724 @@ +/* stb_image_write - v1.16 - public domain - http://nothings.org/stb + writes out PNG/BMP/TGA/JPEG/HDR images to C stdio - Sean Barrett 2010-2015 + no warranty implied; use at your own risk + + Before #including, + + #define STB_IMAGE_WRITE_IMPLEMENTATION + + in the file that you want to have the implementation. + + Will probably not work correctly with strict-aliasing optimizations. + +ABOUT: + + This header file is a library for writing images to C stdio or a callback. + + The PNG output is not optimal; it is 20-50% larger than the file + written by a decent optimizing implementation; though providing a custom + zlib compress function (see STBIW_ZLIB_COMPRESS) can mitigate that. + This library is designed for source code compactness and simplicity, + not optimal image file size or run-time performance. + +BUILDING: + + You can #define STBIW_ASSERT(x) before the #include to avoid using assert.h. + You can #define STBIW_MALLOC(), STBIW_REALLOC(), and STBIW_FREE() to replace + malloc,realloc,free. + You can #define STBIW_MEMMOVE() to replace memmove() + You can #define STBIW_ZLIB_COMPRESS to use a custom zlib-style compress function + for PNG compression (instead of the builtin one), it must have the following signature: + unsigned char * my_compress(unsigned char *data, int data_len, int *out_len, int quality); + The returned data will be freed with STBIW_FREE() (free() by default), + so it must be heap allocated with STBIW_MALLOC() (malloc() by default), + +UNICODE: + + If compiling for Windows and you wish to use Unicode filenames, compile + with + #define STBIW_WINDOWS_UTF8 + and pass utf8-encoded filenames. Call stbiw_convert_wchar_to_utf8 to convert + Windows wchar_t filenames to utf8. + +USAGE: + + There are five functions, one for each image file format: + + int stbi_write_png(char const *filename, int w, int h, int comp, const void *data, int stride_in_bytes); + int stbi_write_bmp(char const *filename, int w, int h, int comp, const void *data); + int stbi_write_tga(char const *filename, int w, int h, int comp, const void *data); + int stbi_write_jpg(char const *filename, int w, int h, int comp, const void *data, int quality); + int stbi_write_hdr(char const *filename, int w, int h, int comp, const float *data); + + void stbi_flip_vertically_on_write(int flag); // flag is non-zero to flip data vertically + + There are also five equivalent functions that use an arbitrary write function. You are + expected to open/close your file-equivalent before and after calling these: + + int stbi_write_png_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const void *data, int stride_in_bytes); + int stbi_write_bmp_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const void *data); + int stbi_write_tga_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const void *data); + int stbi_write_hdr_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const float *data); + int stbi_write_jpg_to_func(stbi_write_func *func, void *context, int x, int y, int comp, const void *data, int quality); + + where the callback is: + void stbi_write_func(void *context, void *data, int size); + + You can configure it with these global variables: + int stbi_write_tga_with_rle; // defaults to true; set to 0 to disable RLE + int stbi_write_png_compression_level; // defaults to 8; set to higher for more compression + int stbi_write_force_png_filter; // defaults to -1; set to 0..5 to force a filter mode + + + You can define STBI_WRITE_NO_STDIO to disable the file variant of these + functions, so the library will not use stdio.h at all. However, this will + also disable HDR writing, because it requires stdio for formatted output. + + Each function returns 0 on failure and non-0 on success. + + The functions create an image file defined by the parameters. The image + is a rectangle of pixels stored from left-to-right, top-to-bottom. + Each pixel contains 'comp' channels of data stored interleaved with 8-bits + per channel, in the following order: 1=Y, 2=YA, 3=RGB, 4=RGBA. (Y is + monochrome color.) The rectangle is 'w' pixels wide and 'h' pixels tall. + The *data pointer points to the first byte of the top-left-most pixel. + For PNG, "stride_in_bytes" is the distance in bytes from the first byte of + a row of pixels to the first byte of the next row of pixels. + + PNG creates output files with the same number of components as the input. + The BMP format expands Y to RGB in the file format and does not + output alpha. + + PNG supports writing rectangles of data even when the bytes storing rows of + data are not consecutive in memory (e.g. sub-rectangles of a larger image), + by supplying the stride between the beginning of adjacent rows. The other + formats do not. (Thus you cannot write a native-format BMP through the BMP + writer, both because it is in BGR order and because it may have padding + at the end of the line.) + + PNG allows you to set the deflate compression level by setting the global + variable 'stbi_write_png_compression_level' (it defaults to 8). + + HDR expects linear float data. Since the format is always 32-bit rgb(e) + data, alpha (if provided) is discarded, and for monochrome data it is + replicated across all three channels. + + TGA supports RLE or non-RLE compressed data. To use non-RLE-compressed + data, set the global variable 'stbi_write_tga_with_rle' to 0. + + JPEG does ignore alpha channels in input data; quality is between 1 and 100. + Higher quality looks better but results in a bigger image. + JPEG baseline (no JPEG progressive). + +CREDITS: + + + Sean Barrett - PNG/BMP/TGA + Baldur Karlsson - HDR + Jean-Sebastien Guay - TGA monochrome + Tim Kelsey - misc enhancements + Alan Hickman - TGA RLE + Emmanuel Julien - initial file IO callback implementation + Jon Olick - original jo_jpeg.cpp code + Daniel Gibson - integrate JPEG, allow external zlib + Aarni Koskela - allow choosing PNG filter + + bugfixes: + github:Chribba + Guillaume Chereau + github:jry2 + github:romigrou + Sergio Gonzalez + Jonas Karlsson + Filip Wasil + Thatcher Ulrich + github:poppolopoppo + Patrick Boettcher + github:xeekworx + Cap Petschulat + Simon Rodriguez + Ivan Tikhonov + github:ignotion + Adam Schackart + Andrew Kensler + +LICENSE + + See end of file for license information. + +*/ + +#ifndef INCLUDE_STB_IMAGE_WRITE_H +#define INCLUDE_STB_IMAGE_WRITE_H + +#include + +// if STB_IMAGE_WRITE_STATIC causes problems, try defining STBIWDEF to 'inline' or 'static inline' +#ifndef STBIWDEF +#ifdef STB_IMAGE_WRITE_STATIC +#define STBIWDEF static +#else +#ifdef __cplusplus +#define STBIWDEF extern "C" +#else +#define STBIWDEF extern +#endif +#endif +#endif + +#ifndef STB_IMAGE_WRITE_STATIC // C++ forbids static forward declarations +STBIWDEF int stbi_write_tga_with_rle; +STBIWDEF int stbi_write_png_compression_level; +STBIWDEF int stbi_write_force_png_filter; +#endif + +#ifndef STBI_WRITE_NO_STDIO +STBIWDEF int stbi_write_png(char const *filename, int w, int h, int comp, const void *data, int stride_in_bytes); +STBIWDEF int stbi_write_bmp(char const *filename, int w, int h, int comp, const void *data); +STBIWDEF int stbi_write_tga(char const *filename, int w, int h, int comp, const void *data); +STBIWDEF int stbi_write_hdr(char const *filename, int w, int h, int comp, const float *data); +STBIWDEF int stbi_write_jpg(char const *filename, int x, int y, int comp, const void *data, int quality); + +#ifdef STBIW_WINDOWS_UTF8 +STBIWDEF int stbiw_convert_wchar_to_utf8(char *buffer, size_t bufferlen, const wchar_t* input); +#endif +#endif + +typedef void stbi_write_func(void *context, void *data, int size); + +STBIWDEF int stbi_write_png_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const void *data, int stride_in_bytes); +STBIWDEF int stbi_write_bmp_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const void *data); +STBIWDEF int stbi_write_tga_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const void *data); +STBIWDEF int stbi_write_hdr_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const float *data); +STBIWDEF int stbi_write_jpg_to_func(stbi_write_func *func, void *context, int x, int y, int comp, const void *data, int quality); + +STBIWDEF void stbi_flip_vertically_on_write(int flip_boolean); + +#endif//INCLUDE_STB_IMAGE_WRITE_H + +#ifdef STB_IMAGE_WRITE_IMPLEMENTATION + +#ifdef _WIN32 + #ifndef _CRT_SECURE_NO_WARNINGS + #define _CRT_SECURE_NO_WARNINGS + #endif + #ifndef _CRT_NONSTDC_NO_DEPRECATE + #define _CRT_NONSTDC_NO_DEPRECATE + #endif +#endif + +#ifndef STBI_WRITE_NO_STDIO +#include +#endif // STBI_WRITE_NO_STDIO + +#include +#include +#include +#include + +#if defined(STBIW_MALLOC) && defined(STBIW_FREE) && (defined(STBIW_REALLOC) || defined(STBIW_REALLOC_SIZED)) +// ok +#elif !defined(STBIW_MALLOC) && !defined(STBIW_FREE) && !defined(STBIW_REALLOC) && !defined(STBIW_REALLOC_SIZED) +// ok +#else +#error "Must define all or none of STBIW_MALLOC, STBIW_FREE, and STBIW_REALLOC (or STBIW_REALLOC_SIZED)." +#endif + +#ifndef STBIW_MALLOC +#define STBIW_MALLOC(sz) malloc(sz) +#define STBIW_REALLOC(p,newsz) realloc(p,newsz) +#define STBIW_FREE(p) free(p) +#endif + +#ifndef STBIW_REALLOC_SIZED +#define STBIW_REALLOC_SIZED(p,oldsz,newsz) STBIW_REALLOC(p,newsz) +#endif + + +#ifndef STBIW_MEMMOVE +#define STBIW_MEMMOVE(a,b,sz) memmove(a,b,sz) +#endif + + +#ifndef STBIW_ASSERT +#include +#define STBIW_ASSERT(x) assert(x) +#endif + +#define STBIW_UCHAR(x) (unsigned char) ((x) & 0xff) + +#ifdef STB_IMAGE_WRITE_STATIC +static int stbi_write_png_compression_level = 8; +static int stbi_write_tga_with_rle = 1; +static int stbi_write_force_png_filter = -1; +#else +int stbi_write_png_compression_level = 8; +int stbi_write_tga_with_rle = 1; +int stbi_write_force_png_filter = -1; +#endif + +static int stbi__flip_vertically_on_write = 0; + +STBIWDEF void stbi_flip_vertically_on_write(int flag) +{ + stbi__flip_vertically_on_write = flag; +} + +typedef struct +{ + stbi_write_func *func; + void *context; + unsigned char buffer[64]; + int buf_used; +} stbi__write_context; + +// initialize a callback-based context +static void stbi__start_write_callbacks(stbi__write_context *s, stbi_write_func *c, void *context) +{ + s->func = c; + s->context = context; +} + +#ifndef STBI_WRITE_NO_STDIO + +static void stbi__stdio_write(void *context, void *data, int size) +{ + fwrite(data,1,size,(FILE*) context); +} + +#if defined(_WIN32) && defined(STBIW_WINDOWS_UTF8) +#ifdef __cplusplus +#define STBIW_EXTERN extern "C" +#else +#define STBIW_EXTERN extern +#endif +STBIW_EXTERN __declspec(dllimport) int __stdcall MultiByteToWideChar(unsigned int cp, unsigned long flags, const char *str, int cbmb, wchar_t *widestr, int cchwide); +STBIW_EXTERN __declspec(dllimport) int __stdcall WideCharToMultiByte(unsigned int cp, unsigned long flags, const wchar_t *widestr, int cchwide, char *str, int cbmb, const char *defchar, int *used_default); + +STBIWDEF int stbiw_convert_wchar_to_utf8(char *buffer, size_t bufferlen, const wchar_t* input) +{ + return WideCharToMultiByte(65001 /* UTF8 */, 0, input, -1, buffer, (int) bufferlen, NULL, NULL); +} +#endif + +static FILE *stbiw__fopen(char const *filename, char const *mode) +{ + FILE *f; +#if defined(_WIN32) && defined(STBIW_WINDOWS_UTF8) + wchar_t wMode[64]; + wchar_t wFilename[1024]; + if (0 == MultiByteToWideChar(65001 /* UTF8 */, 0, filename, -1, wFilename, sizeof(wFilename)/sizeof(*wFilename))) + return 0; + + if (0 == MultiByteToWideChar(65001 /* UTF8 */, 0, mode, -1, wMode, sizeof(wMode)/sizeof(*wMode))) + return 0; + +#if defined(_MSC_VER) && _MSC_VER >= 1400 + if (0 != _wfopen_s(&f, wFilename, wMode)) + f = 0; +#else + f = _wfopen(wFilename, wMode); +#endif + +#elif defined(_MSC_VER) && _MSC_VER >= 1400 + if (0 != fopen_s(&f, filename, mode)) + f=0; +#else + f = fopen(filename, mode); +#endif + return f; +} + +static int stbi__start_write_file(stbi__write_context *s, const char *filename) +{ + FILE *f = stbiw__fopen(filename, "wb"); + stbi__start_write_callbacks(s, stbi__stdio_write, (void *) f); + return f != NULL; +} + +static void stbi__end_write_file(stbi__write_context *s) +{ + fclose((FILE *)s->context); +} + +#endif // !STBI_WRITE_NO_STDIO + +typedef unsigned int stbiw_uint32; +typedef int stb_image_write_test[sizeof(stbiw_uint32)==4 ? 1 : -1]; + +static void stbiw__writefv(stbi__write_context *s, const char *fmt, va_list v) +{ + while (*fmt) { + switch (*fmt++) { + case ' ': break; + case '1': { unsigned char x = STBIW_UCHAR(va_arg(v, int)); + s->func(s->context,&x,1); + break; } + case '2': { int x = va_arg(v,int); + unsigned char b[2]; + b[0] = STBIW_UCHAR(x); + b[1] = STBIW_UCHAR(x>>8); + s->func(s->context,b,2); + break; } + case '4': { stbiw_uint32 x = va_arg(v,int); + unsigned char b[4]; + b[0]=STBIW_UCHAR(x); + b[1]=STBIW_UCHAR(x>>8); + b[2]=STBIW_UCHAR(x>>16); + b[3]=STBIW_UCHAR(x>>24); + s->func(s->context,b,4); + break; } + default: + STBIW_ASSERT(0); + return; + } + } +} + +static void stbiw__writef(stbi__write_context *s, const char *fmt, ...) +{ + va_list v; + va_start(v, fmt); + stbiw__writefv(s, fmt, v); + va_end(v); +} + +static void stbiw__write_flush(stbi__write_context *s) +{ + if (s->buf_used) { + s->func(s->context, &s->buffer, s->buf_used); + s->buf_used = 0; + } +} + +static void stbiw__putc(stbi__write_context *s, unsigned char c) +{ + s->func(s->context, &c, 1); +} + +static void stbiw__write1(stbi__write_context *s, unsigned char a) +{ + if ((size_t)s->buf_used + 1 > sizeof(s->buffer)) + stbiw__write_flush(s); + s->buffer[s->buf_used++] = a; +} + +static void stbiw__write3(stbi__write_context *s, unsigned char a, unsigned char b, unsigned char c) +{ + int n; + if ((size_t)s->buf_used + 3 > sizeof(s->buffer)) + stbiw__write_flush(s); + n = s->buf_used; + s->buf_used = n+3; + s->buffer[n+0] = a; + s->buffer[n+1] = b; + s->buffer[n+2] = c; +} + +static void stbiw__write_pixel(stbi__write_context *s, int rgb_dir, int comp, int write_alpha, int expand_mono, unsigned char *d) +{ + unsigned char bg[3] = { 255, 0, 255}, px[3]; + int k; + + if (write_alpha < 0) + stbiw__write1(s, d[comp - 1]); + + switch (comp) { + case 2: // 2 pixels = mono + alpha, alpha is written separately, so same as 1-channel case + case 1: + if (expand_mono) + stbiw__write3(s, d[0], d[0], d[0]); // monochrome bmp + else + stbiw__write1(s, d[0]); // monochrome TGA + break; + case 4: + if (!write_alpha) { + // composite against pink background + for (k = 0; k < 3; ++k) + px[k] = bg[k] + ((d[k] - bg[k]) * d[3]) / 255; + stbiw__write3(s, px[1 - rgb_dir], px[1], px[1 + rgb_dir]); + break; + } + /* FALLTHROUGH */ + case 3: + stbiw__write3(s, d[1 - rgb_dir], d[1], d[1 + rgb_dir]); + break; + } + if (write_alpha > 0) + stbiw__write1(s, d[comp - 1]); +} + +static void stbiw__write_pixels(stbi__write_context *s, int rgb_dir, int vdir, int x, int y, int comp, void *data, int write_alpha, int scanline_pad, int expand_mono) +{ + stbiw_uint32 zero = 0; + int i,j, j_end; + + if (y <= 0) + return; + + if (stbi__flip_vertically_on_write) + vdir *= -1; + + if (vdir < 0) { + j_end = -1; j = y-1; + } else { + j_end = y; j = 0; + } + + for (; j != j_end; j += vdir) { + for (i=0; i < x; ++i) { + unsigned char *d = (unsigned char *) data + (j*x+i)*comp; + stbiw__write_pixel(s, rgb_dir, comp, write_alpha, expand_mono, d); + } + stbiw__write_flush(s); + s->func(s->context, &zero, scanline_pad); + } +} + +static int stbiw__outfile(stbi__write_context *s, int rgb_dir, int vdir, int x, int y, int comp, int expand_mono, void *data, int alpha, int pad, const char *fmt, ...) +{ + if (y < 0 || x < 0) { + return 0; + } else { + va_list v; + va_start(v, fmt); + stbiw__writefv(s, fmt, v); + va_end(v); + stbiw__write_pixels(s,rgb_dir,vdir,x,y,comp,data,alpha,pad, expand_mono); + return 1; + } +} + +static int stbi_write_bmp_core(stbi__write_context *s, int x, int y, int comp, const void *data) +{ + if (comp != 4) { + // write RGB bitmap + int pad = (-x*3) & 3; + return stbiw__outfile(s,-1,-1,x,y,comp,1,(void *) data,0,pad, + "11 4 22 4" "4 44 22 444444", + 'B', 'M', 14+40+(x*3+pad)*y, 0,0, 14+40, // file header + 40, x,y, 1,24, 0,0,0,0,0,0); // bitmap header + } else { + // RGBA bitmaps need a v4 header + // use BI_BITFIELDS mode with 32bpp and alpha mask + // (straight BI_RGB with alpha mask doesn't work in most readers) + return stbiw__outfile(s,-1,-1,x,y,comp,1,(void *)data,1,0, + "11 4 22 4" "4 44 22 444444 4444 4 444 444 444 444", + 'B', 'M', 14+108+x*y*4, 0, 0, 14+108, // file header + 108, x,y, 1,32, 3,0,0,0,0,0, 0xff0000,0xff00,0xff,0xff000000u, 0, 0,0,0, 0,0,0, 0,0,0, 0,0,0); // bitmap V4 header + } +} + +STBIWDEF int stbi_write_bmp_to_func(stbi_write_func *func, void *context, int x, int y, int comp, const void *data) +{ + stbi__write_context s = { 0 }; + stbi__start_write_callbacks(&s, func, context); + return stbi_write_bmp_core(&s, x, y, comp, data); +} + +#ifndef STBI_WRITE_NO_STDIO +STBIWDEF int stbi_write_bmp(char const *filename, int x, int y, int comp, const void *data) +{ + stbi__write_context s = { 0 }; + if (stbi__start_write_file(&s,filename)) { + int r = stbi_write_bmp_core(&s, x, y, comp, data); + stbi__end_write_file(&s); + return r; + } else + return 0; +} +#endif //!STBI_WRITE_NO_STDIO + +static int stbi_write_tga_core(stbi__write_context *s, int x, int y, int comp, void *data) +{ + int has_alpha = (comp == 2 || comp == 4); + int colorbytes = has_alpha ? comp-1 : comp; + int format = colorbytes < 2 ? 3 : 2; // 3 color channels (RGB/RGBA) = 2, 1 color channel (Y/YA) = 3 + + if (y < 0 || x < 0) + return 0; + + if (!stbi_write_tga_with_rle) { + return stbiw__outfile(s, -1, -1, x, y, comp, 0, (void *) data, has_alpha, 0, + "111 221 2222 11", 0, 0, format, 0, 0, 0, 0, 0, x, y, (colorbytes + has_alpha) * 8, has_alpha * 8); + } else { + int i,j,k; + int jend, jdir; + + stbiw__writef(s, "111 221 2222 11", 0,0,format+8, 0,0,0, 0,0,x,y, (colorbytes + has_alpha) * 8, has_alpha * 8); + + if (stbi__flip_vertically_on_write) { + j = 0; + jend = y; + jdir = 1; + } else { + j = y-1; + jend = -1; + jdir = -1; + } + for (; j != jend; j += jdir) { + unsigned char *row = (unsigned char *) data + j * x * comp; + int len; + + for (i = 0; i < x; i += len) { + unsigned char *begin = row + i * comp; + int diff = 1; + len = 1; + + if (i < x - 1) { + ++len; + diff = memcmp(begin, row + (i + 1) * comp, comp); + if (diff) { + const unsigned char *prev = begin; + for (k = i + 2; k < x && len < 128; ++k) { + if (memcmp(prev, row + k * comp, comp)) { + prev += comp; + ++len; + } else { + --len; + break; + } + } + } else { + for (k = i + 2; k < x && len < 128; ++k) { + if (!memcmp(begin, row + k * comp, comp)) { + ++len; + } else { + break; + } + } + } + } + + if (diff) { + unsigned char header = STBIW_UCHAR(len - 1); + stbiw__write1(s, header); + for (k = 0; k < len; ++k) { + stbiw__write_pixel(s, -1, comp, has_alpha, 0, begin + k * comp); + } + } else { + unsigned char header = STBIW_UCHAR(len - 129); + stbiw__write1(s, header); + stbiw__write_pixel(s, -1, comp, has_alpha, 0, begin); + } + } + } + stbiw__write_flush(s); + } + return 1; +} + +STBIWDEF int stbi_write_tga_to_func(stbi_write_func *func, void *context, int x, int y, int comp, const void *data) +{ + stbi__write_context s = { 0 }; + stbi__start_write_callbacks(&s, func, context); + return stbi_write_tga_core(&s, x, y, comp, (void *) data); +} + +#ifndef STBI_WRITE_NO_STDIO +STBIWDEF int stbi_write_tga(char const *filename, int x, int y, int comp, const void *data) +{ + stbi__write_context s = { 0 }; + if (stbi__start_write_file(&s,filename)) { + int r = stbi_write_tga_core(&s, x, y, comp, (void *) data); + stbi__end_write_file(&s); + return r; + } else + return 0; +} +#endif + +// ************************************************************************************************* +// Radiance RGBE HDR writer +// by Baldur Karlsson + +#define stbiw__max(a, b) ((a) > (b) ? (a) : (b)) + +#ifndef STBI_WRITE_NO_STDIO + +static void stbiw__linear_to_rgbe(unsigned char *rgbe, float *linear) +{ + int exponent; + float maxcomp = stbiw__max(linear[0], stbiw__max(linear[1], linear[2])); + + if (maxcomp < 1e-32f) { + rgbe[0] = rgbe[1] = rgbe[2] = rgbe[3] = 0; + } else { + float normalize = (float) frexp(maxcomp, &exponent) * 256.0f/maxcomp; + + rgbe[0] = (unsigned char)(linear[0] * normalize); + rgbe[1] = (unsigned char)(linear[1] * normalize); + rgbe[2] = (unsigned char)(linear[2] * normalize); + rgbe[3] = (unsigned char)(exponent + 128); + } +} + +static void stbiw__write_run_data(stbi__write_context *s, int length, unsigned char databyte) +{ + unsigned char lengthbyte = STBIW_UCHAR(length+128); + STBIW_ASSERT(length+128 <= 255); + s->func(s->context, &lengthbyte, 1); + s->func(s->context, &databyte, 1); +} + +static void stbiw__write_dump_data(stbi__write_context *s, int length, unsigned char *data) +{ + unsigned char lengthbyte = STBIW_UCHAR(length); + STBIW_ASSERT(length <= 128); // inconsistent with spec but consistent with official code + s->func(s->context, &lengthbyte, 1); + s->func(s->context, data, length); +} + +static void stbiw__write_hdr_scanline(stbi__write_context *s, int width, int ncomp, unsigned char *scratch, float *scanline) +{ + unsigned char scanlineheader[4] = { 2, 2, 0, 0 }; + unsigned char rgbe[4]; + float linear[3]; + int x; + + scanlineheader[2] = (width&0xff00)>>8; + scanlineheader[3] = (width&0x00ff); + + /* skip RLE for images too small or large */ + if (width < 8 || width >= 32768) { + for (x=0; x < width; x++) { + switch (ncomp) { + case 4: /* fallthrough */ + case 3: linear[2] = scanline[x*ncomp + 2]; + linear[1] = scanline[x*ncomp + 1]; + linear[0] = scanline[x*ncomp + 0]; + break; + default: + linear[0] = linear[1] = linear[2] = scanline[x*ncomp + 0]; + break; + } + stbiw__linear_to_rgbe(rgbe, linear); + s->func(s->context, rgbe, 4); + } + } else { + int c,r; + /* encode into scratch buffer */ + for (x=0; x < width; x++) { + switch(ncomp) { + case 4: /* fallthrough */ + case 3: linear[2] = scanline[x*ncomp + 2]; + linear[1] = scanline[x*ncomp + 1]; + linear[0] = scanline[x*ncomp + 0]; + break; + default: + linear[0] = linear[1] = linear[2] = scanline[x*ncomp + 0]; + break; + } + stbiw__linear_to_rgbe(rgbe, linear); + scratch[x + width*0] = rgbe[0]; + scratch[x + width*1] = rgbe[1]; + scratch[x + width*2] = rgbe[2]; + scratch[x + width*3] = rgbe[3]; + } + + s->func(s->context, scanlineheader, 4); + + /* RLE each component separately */ + for (c=0; c < 4; c++) { + unsigned char *comp = &scratch[width*c]; + + x = 0; + while (x < width) { + // find first run + r = x; + while (r+2 < width) { + if (comp[r] == comp[r+1] && comp[r] == comp[r+2]) + break; + ++r; + } + if (r+2 >= width) + r = width; + // dump up to first run + while (x < r) { + int len = r-x; + if (len > 128) len = 128; + stbiw__write_dump_data(s, len, &comp[x]); + x += len; + } + // if there's a run, output it + if (r+2 < width) { // same test as what we break out of in search loop, so only true if we break'd + // find next byte after run + while (r < width && comp[r] == comp[x]) + ++r; + // output run up to r + while (x < r) { + int len = r-x; + if (len > 127) len = 127; + stbiw__write_run_data(s, len, comp[x]); + x += len; + } + } + } + } + } +} + +static int stbi_write_hdr_core(stbi__write_context *s, int x, int y, int comp, float *data) +{ + if (y <= 0 || x <= 0 || data == NULL) + return 0; + else { + // Each component is stored separately. Allocate scratch space for full output scanline. + unsigned char *scratch = (unsigned char *) STBIW_MALLOC(x*4); + int i, len; + char buffer[128]; + char header[] = "#?RADIANCE\n# Written by stb_image_write.h\nFORMAT=32-bit_rle_rgbe\n"; + s->func(s->context, header, sizeof(header)-1); + +#ifdef __STDC_LIB_EXT1__ + len = sprintf_s(buffer, sizeof(buffer), "EXPOSURE= 1.0000000000000\n\n-Y %d +X %d\n", y, x); +#else + len = sprintf(buffer, "EXPOSURE= 1.0000000000000\n\n-Y %d +X %d\n", y, x); +#endif + s->func(s->context, buffer, len); + + for(i=0; i < y; i++) + stbiw__write_hdr_scanline(s, x, comp, scratch, data + comp*x*(stbi__flip_vertically_on_write ? y-1-i : i)); + STBIW_FREE(scratch); + return 1; + } +} + +STBIWDEF int stbi_write_hdr_to_func(stbi_write_func *func, void *context, int x, int y, int comp, const float *data) +{ + stbi__write_context s = { 0 }; + stbi__start_write_callbacks(&s, func, context); + return stbi_write_hdr_core(&s, x, y, comp, (float *) data); +} + +STBIWDEF int stbi_write_hdr(char const *filename, int x, int y, int comp, const float *data) +{ + stbi__write_context s = { 0 }; + if (stbi__start_write_file(&s,filename)) { + int r = stbi_write_hdr_core(&s, x, y, comp, (float *) data); + stbi__end_write_file(&s); + return r; + } else + return 0; +} +#endif // STBI_WRITE_NO_STDIO + + +////////////////////////////////////////////////////////////////////////////// +// +// PNG writer +// + +#ifndef STBIW_ZLIB_COMPRESS +// stretchy buffer; stbiw__sbpush() == vector<>::push_back() -- stbiw__sbcount() == vector<>::size() +#define stbiw__sbraw(a) ((int *) (void *) (a) - 2) +#define stbiw__sbm(a) stbiw__sbraw(a)[0] +#define stbiw__sbn(a) stbiw__sbraw(a)[1] + +#define stbiw__sbneedgrow(a,n) ((a)==0 || stbiw__sbn(a)+n >= stbiw__sbm(a)) +#define stbiw__sbmaybegrow(a,n) (stbiw__sbneedgrow(a,(n)) ? stbiw__sbgrow(a,n) : 0) +#define stbiw__sbgrow(a,n) stbiw__sbgrowf((void **) &(a), (n), sizeof(*(a))) + +#define stbiw__sbpush(a, v) (stbiw__sbmaybegrow(a,1), (a)[stbiw__sbn(a)++] = (v)) +#define stbiw__sbcount(a) ((a) ? stbiw__sbn(a) : 0) +#define stbiw__sbfree(a) ((a) ? STBIW_FREE(stbiw__sbraw(a)),0 : 0) + +static void *stbiw__sbgrowf(void **arr, int increment, int itemsize) +{ + int m = *arr ? 2*stbiw__sbm(*arr)+increment : increment+1; + void *p = STBIW_REALLOC_SIZED(*arr ? stbiw__sbraw(*arr) : 0, *arr ? (stbiw__sbm(*arr)*itemsize + sizeof(int)*2) : 0, itemsize * m + sizeof(int)*2); + STBIW_ASSERT(p); + if (p) { + if (!*arr) ((int *) p)[1] = 0; + *arr = (void *) ((int *) p + 2); + stbiw__sbm(*arr) = m; + } + return *arr; +} + +static unsigned char *stbiw__zlib_flushf(unsigned char *data, unsigned int *bitbuffer, int *bitcount) +{ + while (*bitcount >= 8) { + stbiw__sbpush(data, STBIW_UCHAR(*bitbuffer)); + *bitbuffer >>= 8; + *bitcount -= 8; + } + return data; +} + +static int stbiw__zlib_bitrev(int code, int codebits) +{ + int res=0; + while (codebits--) { + res = (res << 1) | (code & 1); + code >>= 1; + } + return res; +} + +static unsigned int stbiw__zlib_countm(unsigned char *a, unsigned char *b, int limit) +{ + int i; + for (i=0; i < limit && i < 258; ++i) + if (a[i] != b[i]) break; + return i; +} + +static unsigned int stbiw__zhash(unsigned char *data) +{ + stbiw_uint32 hash = data[0] + (data[1] << 8) + (data[2] << 16); + hash ^= hash << 3; + hash += hash >> 5; + hash ^= hash << 4; + hash += hash >> 17; + hash ^= hash << 25; + hash += hash >> 6; + return hash; +} + +#define stbiw__zlib_flush() (out = stbiw__zlib_flushf(out, &bitbuf, &bitcount)) +#define stbiw__zlib_add(code,codebits) \ + (bitbuf |= (code) << bitcount, bitcount += (codebits), stbiw__zlib_flush()) +#define stbiw__zlib_huffa(b,c) stbiw__zlib_add(stbiw__zlib_bitrev(b,c),c) +// default huffman tables +#define stbiw__zlib_huff1(n) stbiw__zlib_huffa(0x30 + (n), 8) +#define stbiw__zlib_huff2(n) stbiw__zlib_huffa(0x190 + (n)-144, 9) +#define stbiw__zlib_huff3(n) stbiw__zlib_huffa(0 + (n)-256,7) +#define stbiw__zlib_huff4(n) stbiw__zlib_huffa(0xc0 + (n)-280,8) +#define stbiw__zlib_huff(n) ((n) <= 143 ? stbiw__zlib_huff1(n) : (n) <= 255 ? stbiw__zlib_huff2(n) : (n) <= 279 ? stbiw__zlib_huff3(n) : stbiw__zlib_huff4(n)) +#define stbiw__zlib_huffb(n) ((n) <= 143 ? stbiw__zlib_huff1(n) : stbiw__zlib_huff2(n)) + +#define stbiw__ZHASH 16384 + +#endif // STBIW_ZLIB_COMPRESS + +STBIWDEF unsigned char * stbi_zlib_compress(unsigned char *data, int data_len, int *out_len, int quality) +{ +#ifdef STBIW_ZLIB_COMPRESS + // user provided a zlib compress implementation, use that + return STBIW_ZLIB_COMPRESS(data, data_len, out_len, quality); +#else // use builtin + static unsigned short lengthc[] = { 3,4,5,6,7,8,9,10,11,13,15,17,19,23,27,31,35,43,51,59,67,83,99,115,131,163,195,227,258, 259 }; + static unsigned char lengtheb[]= { 0,0,0,0,0,0,0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0 }; + static unsigned short distc[] = { 1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193,257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577, 32768 }; + static unsigned char disteb[] = { 0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13 }; + unsigned int bitbuf=0; + int i,j, bitcount=0; + unsigned char *out = NULL; + unsigned char ***hash_table = (unsigned char***) STBIW_MALLOC(stbiw__ZHASH * sizeof(unsigned char**)); + if (hash_table == NULL) + return NULL; + if (quality < 5) quality = 5; + + stbiw__sbpush(out, 0x78); // DEFLATE 32K window + stbiw__sbpush(out, 0x5e); // FLEVEL = 1 + stbiw__zlib_add(1,1); // BFINAL = 1 + stbiw__zlib_add(1,2); // BTYPE = 1 -- fixed huffman + + for (i=0; i < stbiw__ZHASH; ++i) + hash_table[i] = NULL; + + i=0; + while (i < data_len-3) { + // hash next 3 bytes of data to be compressed + int h = stbiw__zhash(data+i)&(stbiw__ZHASH-1), best=3; + unsigned char *bestloc = 0; + unsigned char **hlist = hash_table[h]; + int n = stbiw__sbcount(hlist); + for (j=0; j < n; ++j) { + if (hlist[j]-data > i-32768) { // if entry lies within window + int d = stbiw__zlib_countm(hlist[j], data+i, data_len-i); + if (d >= best) { best=d; bestloc=hlist[j]; } + } + } + // when hash table entry is too long, delete half the entries + if (hash_table[h] && stbiw__sbn(hash_table[h]) == 2*quality) { + STBIW_MEMMOVE(hash_table[h], hash_table[h]+quality, sizeof(hash_table[h][0])*quality); + stbiw__sbn(hash_table[h]) = quality; + } + stbiw__sbpush(hash_table[h],data+i); + + if (bestloc) { + // "lazy matching" - check match at *next* byte, and if it's better, do cur byte as literal + h = stbiw__zhash(data+i+1)&(stbiw__ZHASH-1); + hlist = hash_table[h]; + n = stbiw__sbcount(hlist); + for (j=0; j < n; ++j) { + if (hlist[j]-data > i-32767) { + int e = stbiw__zlib_countm(hlist[j], data+i+1, data_len-i-1); + if (e > best) { // if next match is better, bail on current match + bestloc = NULL; + break; + } + } + } + } + + if (bestloc) { + int d = (int) (data+i - bestloc); // distance back + STBIW_ASSERT(d <= 32767 && best <= 258); + for (j=0; best > lengthc[j+1]-1; ++j); + stbiw__zlib_huff(j+257); + if (lengtheb[j]) stbiw__zlib_add(best - lengthc[j], lengtheb[j]); + for (j=0; d > distc[j+1]-1; ++j); + stbiw__zlib_add(stbiw__zlib_bitrev(j,5),5); + if (disteb[j]) stbiw__zlib_add(d - distc[j], disteb[j]); + i += best; + } else { + stbiw__zlib_huffb(data[i]); + ++i; + } + } + // write out final bytes + for (;i < data_len; ++i) + stbiw__zlib_huffb(data[i]); + stbiw__zlib_huff(256); // end of block + // pad with 0 bits to byte boundary + while (bitcount) + stbiw__zlib_add(0,1); + + for (i=0; i < stbiw__ZHASH; ++i) + (void) stbiw__sbfree(hash_table[i]); + STBIW_FREE(hash_table); + + // store uncompressed instead if compression was worse + if (stbiw__sbn(out) > data_len + 2 + ((data_len+32766)/32767)*5) { + stbiw__sbn(out) = 2; // truncate to DEFLATE 32K window and FLEVEL = 1 + for (j = 0; j < data_len;) { + int blocklen = data_len - j; + if (blocklen > 32767) blocklen = 32767; + stbiw__sbpush(out, data_len - j == blocklen); // BFINAL = ?, BTYPE = 0 -- no compression + stbiw__sbpush(out, STBIW_UCHAR(blocklen)); // LEN + stbiw__sbpush(out, STBIW_UCHAR(blocklen >> 8)); + stbiw__sbpush(out, STBIW_UCHAR(~blocklen)); // NLEN + stbiw__sbpush(out, STBIW_UCHAR(~blocklen >> 8)); + memcpy(out+stbiw__sbn(out), data+j, blocklen); + stbiw__sbn(out) += blocklen; + j += blocklen; + } + } + + { + // compute adler32 on input + unsigned int s1=1, s2=0; + int blocklen = (int) (data_len % 5552); + j=0; + while (j < data_len) { + for (i=0; i < blocklen; ++i) { s1 += data[j+i]; s2 += s1; } + s1 %= 65521; s2 %= 65521; + j += blocklen; + blocklen = 5552; + } + stbiw__sbpush(out, STBIW_UCHAR(s2 >> 8)); + stbiw__sbpush(out, STBIW_UCHAR(s2)); + stbiw__sbpush(out, STBIW_UCHAR(s1 >> 8)); + stbiw__sbpush(out, STBIW_UCHAR(s1)); + } + *out_len = stbiw__sbn(out); + // make returned pointer freeable + STBIW_MEMMOVE(stbiw__sbraw(out), out, *out_len); + return (unsigned char *) stbiw__sbraw(out); +#endif // STBIW_ZLIB_COMPRESS +} + +static unsigned int stbiw__crc32(unsigned char *buffer, int len) +{ +#ifdef STBIW_CRC32 + return STBIW_CRC32(buffer, len); +#else + static unsigned int crc_table[256] = + { + 0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA, 0x076DC419, 0x706AF48F, 0xE963A535, 0x9E6495A3, + 0x0eDB8832, 0x79DCB8A4, 0xE0D5E91E, 0x97D2D988, 0x09B64C2B, 0x7EB17CBD, 0xE7B82D07, 0x90BF1D91, + 0x1DB71064, 0x6AB020F2, 0xF3B97148, 0x84BE41DE, 0x1ADAD47D, 0x6DDDE4EB, 0xF4D4B551, 0x83D385C7, + 0x136C9856, 0x646BA8C0, 0xFD62F97A, 0x8A65C9EC, 0x14015C4F, 0x63066CD9, 0xFA0F3D63, 0x8D080DF5, + 0x3B6E20C8, 0x4C69105E, 0xD56041E4, 0xA2677172, 0x3C03E4D1, 0x4B04D447, 0xD20D85FD, 0xA50AB56B, + 0x35B5A8FA, 0x42B2986C, 0xDBBBC9D6, 0xACBCF940, 0x32D86CE3, 0x45DF5C75, 0xDCD60DCF, 0xABD13D59, + 0x26D930AC, 0x51DE003A, 0xC8D75180, 0xBFD06116, 0x21B4F4B5, 0x56B3C423, 0xCFBA9599, 0xB8BDA50F, + 0x2802B89E, 0x5F058808, 0xC60CD9B2, 0xB10BE924, 0x2F6F7C87, 0x58684C11, 0xC1611DAB, 0xB6662D3D, + 0x76DC4190, 0x01DB7106, 0x98D220BC, 0xEFD5102A, 0x71B18589, 0x06B6B51F, 0x9FBFE4A5, 0xE8B8D433, + 0x7807C9A2, 0x0F00F934, 0x9609A88E, 0xE10E9818, 0x7F6A0DBB, 0x086D3D2D, 0x91646C97, 0xE6635C01, + 0x6B6B51F4, 0x1C6C6162, 0x856530D8, 0xF262004E, 0x6C0695ED, 0x1B01A57B, 0x8208F4C1, 0xF50FC457, + 0x65B0D9C6, 0x12B7E950, 0x8BBEB8EA, 0xFCB9887C, 0x62DD1DDF, 0x15DA2D49, 0x8CD37CF3, 0xFBD44C65, + 0x4DB26158, 0x3AB551CE, 0xA3BC0074, 0xD4BB30E2, 0x4ADFA541, 0x3DD895D7, 0xA4D1C46D, 0xD3D6F4FB, + 0x4369E96A, 0x346ED9FC, 0xAD678846, 0xDA60B8D0, 0x44042D73, 0x33031DE5, 0xAA0A4C5F, 0xDD0D7CC9, + 0x5005713C, 0x270241AA, 0xBE0B1010, 0xC90C2086, 0x5768B525, 0x206F85B3, 0xB966D409, 0xCE61E49F, + 0x5EDEF90E, 0x29D9C998, 0xB0D09822, 0xC7D7A8B4, 0x59B33D17, 0x2EB40D81, 0xB7BD5C3B, 0xC0BA6CAD, + 0xEDB88320, 0x9ABFB3B6, 0x03B6E20C, 0x74B1D29A, 0xEAD54739, 0x9DD277AF, 0x04DB2615, 0x73DC1683, + 0xE3630B12, 0x94643B84, 0x0D6D6A3E, 0x7A6A5AA8, 0xE40ECF0B, 0x9309FF9D, 0x0A00AE27, 0x7D079EB1, + 0xF00F9344, 0x8708A3D2, 0x1E01F268, 0x6906C2FE, 0xF762575D, 0x806567CB, 0x196C3671, 0x6E6B06E7, + 0xFED41B76, 0x89D32BE0, 0x10DA7A5A, 0x67DD4ACC, 0xF9B9DF6F, 0x8EBEEFF9, 0x17B7BE43, 0x60B08ED5, + 0xD6D6A3E8, 0xA1D1937E, 0x38D8C2C4, 0x4FDFF252, 0xD1BB67F1, 0xA6BC5767, 0x3FB506DD, 0x48B2364B, + 0xD80D2BDA, 0xAF0A1B4C, 0x36034AF6, 0x41047A60, 0xDF60EFC3, 0xA867DF55, 0x316E8EEF, 0x4669BE79, + 0xCB61B38C, 0xBC66831A, 0x256FD2A0, 0x5268E236, 0xCC0C7795, 0xBB0B4703, 0x220216B9, 0x5505262F, + 0xC5BA3BBE, 0xB2BD0B28, 0x2BB45A92, 0x5CB36A04, 0xC2D7FFA7, 0xB5D0CF31, 0x2CD99E8B, 0x5BDEAE1D, + 0x9B64C2B0, 0xEC63F226, 0x756AA39C, 0x026D930A, 0x9C0906A9, 0xEB0E363F, 0x72076785, 0x05005713, + 0x95BF4A82, 0xE2B87A14, 0x7BB12BAE, 0x0CB61B38, 0x92D28E9B, 0xE5D5BE0D, 0x7CDCEFB7, 0x0BDBDF21, + 0x86D3D2D4, 0xF1D4E242, 0x68DDB3F8, 0x1FDA836E, 0x81BE16CD, 0xF6B9265B, 0x6FB077E1, 0x18B74777, + 0x88085AE6, 0xFF0F6A70, 0x66063BCA, 0x11010B5C, 0x8F659EFF, 0xF862AE69, 0x616BFFD3, 0x166CCF45, + 0xA00AE278, 0xD70DD2EE, 0x4E048354, 0x3903B3C2, 0xA7672661, 0xD06016F7, 0x4969474D, 0x3E6E77DB, + 0xAED16A4A, 0xD9D65ADC, 0x40DF0B66, 0x37D83BF0, 0xA9BCAE53, 0xDEBB9EC5, 0x47B2CF7F, 0x30B5FFE9, + 0xBDBDF21C, 0xCABAC28A, 0x53B39330, 0x24B4A3A6, 0xBAD03605, 0xCDD70693, 0x54DE5729, 0x23D967BF, + 0xB3667A2E, 0xC4614AB8, 0x5D681B02, 0x2A6F2B94, 0xB40BBE37, 0xC30C8EA1, 0x5A05DF1B, 0x2D02EF8D + }; + + unsigned int crc = ~0u; + int i; + for (i=0; i < len; ++i) + crc = (crc >> 8) ^ crc_table[buffer[i] ^ (crc & 0xff)]; + return ~crc; +#endif +} + +#define stbiw__wpng4(o,a,b,c,d) ((o)[0]=STBIW_UCHAR(a),(o)[1]=STBIW_UCHAR(b),(o)[2]=STBIW_UCHAR(c),(o)[3]=STBIW_UCHAR(d),(o)+=4) +#define stbiw__wp32(data,v) stbiw__wpng4(data, (v)>>24,(v)>>16,(v)>>8,(v)); +#define stbiw__wptag(data,s) stbiw__wpng4(data, s[0],s[1],s[2],s[3]) + +static void stbiw__wpcrc(unsigned char **data, int len) +{ + unsigned int crc = stbiw__crc32(*data - len - 4, len+4); + stbiw__wp32(*data, crc); +} + +static unsigned char stbiw__paeth(int a, int b, int c) +{ + int p = a + b - c, pa = abs(p-a), pb = abs(p-b), pc = abs(p-c); + if (pa <= pb && pa <= pc) return STBIW_UCHAR(a); + if (pb <= pc) return STBIW_UCHAR(b); + return STBIW_UCHAR(c); +} + +// @OPTIMIZE: provide an option that always forces left-predict or paeth predict +static void stbiw__encode_png_line(unsigned char *pixels, int stride_bytes, int width, int height, int y, int n, int filter_type, signed char *line_buffer) +{ + static int mapping[] = { 0,1,2,3,4 }; + static int firstmap[] = { 0,1,0,5,6 }; + int *mymap = (y != 0) ? mapping : firstmap; + int i; + int type = mymap[filter_type]; + unsigned char *z = pixels + stride_bytes * (stbi__flip_vertically_on_write ? height-1-y : y); + int signed_stride = stbi__flip_vertically_on_write ? -stride_bytes : stride_bytes; + + if (type==0) { + memcpy(line_buffer, z, width*n); + return; + } + + // first loop isn't optimized since it's just one pixel + for (i = 0; i < n; ++i) { + switch (type) { + case 1: line_buffer[i] = z[i]; break; + case 2: line_buffer[i] = z[i] - z[i-signed_stride]; break; + case 3: line_buffer[i] = z[i] - (z[i-signed_stride]>>1); break; + case 4: line_buffer[i] = (signed char) (z[i] - stbiw__paeth(0,z[i-signed_stride],0)); break; + case 5: line_buffer[i] = z[i]; break; + case 6: line_buffer[i] = z[i]; break; + } + } + switch (type) { + case 1: for (i=n; i < width*n; ++i) line_buffer[i] = z[i] - z[i-n]; break; + case 2: for (i=n; i < width*n; ++i) line_buffer[i] = z[i] - z[i-signed_stride]; break; + case 3: for (i=n; i < width*n; ++i) line_buffer[i] = z[i] - ((z[i-n] + z[i-signed_stride])>>1); break; + case 4: for (i=n; i < width*n; ++i) line_buffer[i] = z[i] - stbiw__paeth(z[i-n], z[i-signed_stride], z[i-signed_stride-n]); break; + case 5: for (i=n; i < width*n; ++i) line_buffer[i] = z[i] - (z[i-n]>>1); break; + case 6: for (i=n; i < width*n; ++i) line_buffer[i] = z[i] - stbiw__paeth(z[i-n], 0,0); break; + } +} + +STBIWDEF unsigned char *stbi_write_png_to_mem(const unsigned char *pixels, int stride_bytes, int x, int y, int n, int *out_len) +{ + int force_filter = stbi_write_force_png_filter; + int ctype[5] = { -1, 0, 4, 2, 6 }; + unsigned char sig[8] = { 137,80,78,71,13,10,26,10 }; + unsigned char *out,*o, *filt, *zlib; + signed char *line_buffer; + int j,zlen; + + if (stride_bytes == 0) + stride_bytes = x * n; + + if (force_filter >= 5) { + force_filter = -1; + } + + filt = (unsigned char *) STBIW_MALLOC((x*n+1) * y); if (!filt) return 0; + line_buffer = (signed char *) STBIW_MALLOC(x * n); if (!line_buffer) { STBIW_FREE(filt); return 0; } + for (j=0; j < y; ++j) { + int filter_type; + if (force_filter > -1) { + filter_type = force_filter; + stbiw__encode_png_line((unsigned char*)(pixels), stride_bytes, x, y, j, n, force_filter, line_buffer); + } else { // Estimate the best filter by running through all of them: + int best_filter = 0, best_filter_val = 0x7fffffff, est, i; + for (filter_type = 0; filter_type < 5; filter_type++) { + stbiw__encode_png_line((unsigned char*)(pixels), stride_bytes, x, y, j, n, filter_type, line_buffer); + + // Estimate the entropy of the line using this filter; the less, the better. + est = 0; + for (i = 0; i < x*n; ++i) { + est += abs((signed char) line_buffer[i]); + } + if (est < best_filter_val) { + best_filter_val = est; + best_filter = filter_type; + } + } + if (filter_type != best_filter) { // If the last iteration already got us the best filter, don't redo it + stbiw__encode_png_line((unsigned char*)(pixels), stride_bytes, x, y, j, n, best_filter, line_buffer); + filter_type = best_filter; + } + } + // when we get here, filter_type contains the filter type, and line_buffer contains the data + filt[j*(x*n+1)] = (unsigned char) filter_type; + STBIW_MEMMOVE(filt+j*(x*n+1)+1, line_buffer, x*n); + } + STBIW_FREE(line_buffer); + zlib = stbi_zlib_compress(filt, y*( x*n+1), &zlen, stbi_write_png_compression_level); + STBIW_FREE(filt); + if (!zlib) return 0; + + // each tag requires 12 bytes of overhead + out = (unsigned char *) STBIW_MALLOC(8 + 12+13 + 12+zlen + 12); + if (!out) return 0; + *out_len = 8 + 12+13 + 12+zlen + 12; + + o=out; + STBIW_MEMMOVE(o,sig,8); o+= 8; + stbiw__wp32(o, 13); // header length + stbiw__wptag(o, "IHDR"); + stbiw__wp32(o, x); + stbiw__wp32(o, y); + *o++ = 8; + *o++ = STBIW_UCHAR(ctype[n]); + *o++ = 0; + *o++ = 0; + *o++ = 0; + stbiw__wpcrc(&o,13); + + stbiw__wp32(o, zlen); + stbiw__wptag(o, "IDAT"); + STBIW_MEMMOVE(o, zlib, zlen); + o += zlen; + STBIW_FREE(zlib); + stbiw__wpcrc(&o, zlen); + + stbiw__wp32(o,0); + stbiw__wptag(o, "IEND"); + stbiw__wpcrc(&o,0); + + STBIW_ASSERT(o == out + *out_len); + + return out; +} + +#ifndef STBI_WRITE_NO_STDIO +STBIWDEF int stbi_write_png(char const *filename, int x, int y, int comp, const void *data, int stride_bytes) +{ + FILE *f; + int len; + unsigned char *png = stbi_write_png_to_mem((const unsigned char *) data, stride_bytes, x, y, comp, &len); + if (png == NULL) return 0; + + f = stbiw__fopen(filename, "wb"); + if (!f) { STBIW_FREE(png); return 0; } + fwrite(png, 1, len, f); + fclose(f); + STBIW_FREE(png); + return 1; +} +#endif + +STBIWDEF int stbi_write_png_to_func(stbi_write_func *func, void *context, int x, int y, int comp, const void *data, int stride_bytes) +{ + int len; + unsigned char *png = stbi_write_png_to_mem((const unsigned char *) data, stride_bytes, x, y, comp, &len); + if (png == NULL) return 0; + func(context, png, len); + STBIW_FREE(png); + return 1; +} + + +/* *************************************************************************** + * + * JPEG writer + * + * This is based on Jon Olick's jo_jpeg.cpp: + * public domain Simple, Minimalistic JPEG writer - http://www.jonolick.com/code.html + */ + +static const unsigned char stbiw__jpg_ZigZag[] = { 0,1,5,6,14,15,27,28,2,4,7,13,16,26,29,42,3,8,12,17,25,30,41,43,9,11,18, + 24,31,40,44,53,10,19,23,32,39,45,52,54,20,22,33,38,46,51,55,60,21,34,37,47,50,56,59,61,35,36,48,49,57,58,62,63 }; + +static void stbiw__jpg_writeBits(stbi__write_context *s, int *bitBufP, int *bitCntP, const unsigned short *bs) { + int bitBuf = *bitBufP, bitCnt = *bitCntP; + bitCnt += bs[1]; + bitBuf |= bs[0] << (24 - bitCnt); + while(bitCnt >= 8) { + unsigned char c = (bitBuf >> 16) & 255; + stbiw__putc(s, c); + if(c == 255) { + stbiw__putc(s, 0); + } + bitBuf <<= 8; + bitCnt -= 8; + } + *bitBufP = bitBuf; + *bitCntP = bitCnt; +} + +static void stbiw__jpg_DCT(float *d0p, float *d1p, float *d2p, float *d3p, float *d4p, float *d5p, float *d6p, float *d7p) { + float d0 = *d0p, d1 = *d1p, d2 = *d2p, d3 = *d3p, d4 = *d4p, d5 = *d5p, d6 = *d6p, d7 = *d7p; + float z1, z2, z3, z4, z5, z11, z13; + + float tmp0 = d0 + d7; + float tmp7 = d0 - d7; + float tmp1 = d1 + d6; + float tmp6 = d1 - d6; + float tmp2 = d2 + d5; + float tmp5 = d2 - d5; + float tmp3 = d3 + d4; + float tmp4 = d3 - d4; + + // Even part + float tmp10 = tmp0 + tmp3; // phase 2 + float tmp13 = tmp0 - tmp3; + float tmp11 = tmp1 + tmp2; + float tmp12 = tmp1 - tmp2; + + d0 = tmp10 + tmp11; // phase 3 + d4 = tmp10 - tmp11; + + z1 = (tmp12 + tmp13) * 0.707106781f; // c4 + d2 = tmp13 + z1; // phase 5 + d6 = tmp13 - z1; + + // Odd part + tmp10 = tmp4 + tmp5; // phase 2 + tmp11 = tmp5 + tmp6; + tmp12 = tmp6 + tmp7; + + // The rotator is modified from fig 4-8 to avoid extra negations. + z5 = (tmp10 - tmp12) * 0.382683433f; // c6 + z2 = tmp10 * 0.541196100f + z5; // c2-c6 + z4 = tmp12 * 1.306562965f + z5; // c2+c6 + z3 = tmp11 * 0.707106781f; // c4 + + z11 = tmp7 + z3; // phase 5 + z13 = tmp7 - z3; + + *d5p = z13 + z2; // phase 6 + *d3p = z13 - z2; + *d1p = z11 + z4; + *d7p = z11 - z4; + + *d0p = d0; *d2p = d2; *d4p = d4; *d6p = d6; +} + +static void stbiw__jpg_calcBits(int val, unsigned short bits[2]) { + int tmp1 = val < 0 ? -val : val; + val = val < 0 ? val-1 : val; + bits[1] = 1; + while(tmp1 >>= 1) { + ++bits[1]; + } + bits[0] = val & ((1<0)&&(DU[end0pos]==0); --end0pos) { + } + // end0pos = first element in reverse order !=0 + if(end0pos == 0) { + stbiw__jpg_writeBits(s, bitBuf, bitCnt, EOB); + return DU[0]; + } + for(i = 1; i <= end0pos; ++i) { + int startpos = i; + int nrzeroes; + unsigned short bits[2]; + for (; DU[i]==0 && i<=end0pos; ++i) { + } + nrzeroes = i-startpos; + if ( nrzeroes >= 16 ) { + int lng = nrzeroes>>4; + int nrmarker; + for (nrmarker=1; nrmarker <= lng; ++nrmarker) + stbiw__jpg_writeBits(s, bitBuf, bitCnt, M16zeroes); + nrzeroes &= 15; + } + stbiw__jpg_calcBits(DU[i], bits); + stbiw__jpg_writeBits(s, bitBuf, bitCnt, HTAC[(nrzeroes<<4)+bits[1]]); + stbiw__jpg_writeBits(s, bitBuf, bitCnt, bits); + } + if(end0pos != 63) { + stbiw__jpg_writeBits(s, bitBuf, bitCnt, EOB); + } + return DU[0]; +} + +static int stbi_write_jpg_core(stbi__write_context *s, int width, int height, int comp, const void* data, int quality) { + // Constants that don't pollute global namespace + static const unsigned char std_dc_luminance_nrcodes[] = {0,0,1,5,1,1,1,1,1,1,0,0,0,0,0,0,0}; + static const unsigned char std_dc_luminance_values[] = {0,1,2,3,4,5,6,7,8,9,10,11}; + static const unsigned char std_ac_luminance_nrcodes[] = {0,0,2,1,3,3,2,4,3,5,5,4,4,0,0,1,0x7d}; + static const unsigned char std_ac_luminance_values[] = { + 0x01,0x02,0x03,0x00,0x04,0x11,0x05,0x12,0x21,0x31,0x41,0x06,0x13,0x51,0x61,0x07,0x22,0x71,0x14,0x32,0x81,0x91,0xa1,0x08, + 0x23,0x42,0xb1,0xc1,0x15,0x52,0xd1,0xf0,0x24,0x33,0x62,0x72,0x82,0x09,0x0a,0x16,0x17,0x18,0x19,0x1a,0x25,0x26,0x27,0x28, + 0x29,0x2a,0x34,0x35,0x36,0x37,0x38,0x39,0x3a,0x43,0x44,0x45,0x46,0x47,0x48,0x49,0x4a,0x53,0x54,0x55,0x56,0x57,0x58,0x59, + 0x5a,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6a,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7a,0x83,0x84,0x85,0x86,0x87,0x88,0x89, + 0x8a,0x92,0x93,0x94,0x95,0x96,0x97,0x98,0x99,0x9a,0xa2,0xa3,0xa4,0xa5,0xa6,0xa7,0xa8,0xa9,0xaa,0xb2,0xb3,0xb4,0xb5,0xb6, + 0xb7,0xb8,0xb9,0xba,0xc2,0xc3,0xc4,0xc5,0xc6,0xc7,0xc8,0xc9,0xca,0xd2,0xd3,0xd4,0xd5,0xd6,0xd7,0xd8,0xd9,0xda,0xe1,0xe2, + 0xe3,0xe4,0xe5,0xe6,0xe7,0xe8,0xe9,0xea,0xf1,0xf2,0xf3,0xf4,0xf5,0xf6,0xf7,0xf8,0xf9,0xfa + }; + static const unsigned char std_dc_chrominance_nrcodes[] = {0,0,3,1,1,1,1,1,1,1,1,1,0,0,0,0,0}; + static const unsigned char std_dc_chrominance_values[] = {0,1,2,3,4,5,6,7,8,9,10,11}; + static const unsigned char std_ac_chrominance_nrcodes[] = {0,0,2,1,2,4,4,3,4,7,5,4,4,0,1,2,0x77}; + static const unsigned char std_ac_chrominance_values[] = { + 0x00,0x01,0x02,0x03,0x11,0x04,0x05,0x21,0x31,0x06,0x12,0x41,0x51,0x07,0x61,0x71,0x13,0x22,0x32,0x81,0x08,0x14,0x42,0x91, + 0xa1,0xb1,0xc1,0x09,0x23,0x33,0x52,0xf0,0x15,0x62,0x72,0xd1,0x0a,0x16,0x24,0x34,0xe1,0x25,0xf1,0x17,0x18,0x19,0x1a,0x26, + 0x27,0x28,0x29,0x2a,0x35,0x36,0x37,0x38,0x39,0x3a,0x43,0x44,0x45,0x46,0x47,0x48,0x49,0x4a,0x53,0x54,0x55,0x56,0x57,0x58, + 0x59,0x5a,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6a,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7a,0x82,0x83,0x84,0x85,0x86,0x87, + 0x88,0x89,0x8a,0x92,0x93,0x94,0x95,0x96,0x97,0x98,0x99,0x9a,0xa2,0xa3,0xa4,0xa5,0xa6,0xa7,0xa8,0xa9,0xaa,0xb2,0xb3,0xb4, + 0xb5,0xb6,0xb7,0xb8,0xb9,0xba,0xc2,0xc3,0xc4,0xc5,0xc6,0xc7,0xc8,0xc9,0xca,0xd2,0xd3,0xd4,0xd5,0xd6,0xd7,0xd8,0xd9,0xda, + 0xe2,0xe3,0xe4,0xe5,0xe6,0xe7,0xe8,0xe9,0xea,0xf2,0xf3,0xf4,0xf5,0xf6,0xf7,0xf8,0xf9,0xfa + }; + // Huffman tables + static const unsigned short YDC_HT[256][2] = { {0,2},{2,3},{3,3},{4,3},{5,3},{6,3},{14,4},{30,5},{62,6},{126,7},{254,8},{510,9}}; + static const unsigned short UVDC_HT[256][2] = { {0,2},{1,2},{2,2},{6,3},{14,4},{30,5},{62,6},{126,7},{254,8},{510,9},{1022,10},{2046,11}}; + static const unsigned short YAC_HT[256][2] = { + {10,4},{0,2},{1,2},{4,3},{11,4},{26,5},{120,7},{248,8},{1014,10},{65410,16},{65411,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {12,4},{27,5},{121,7},{502,9},{2038,11},{65412,16},{65413,16},{65414,16},{65415,16},{65416,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {28,5},{249,8},{1015,10},{4084,12},{65417,16},{65418,16},{65419,16},{65420,16},{65421,16},{65422,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {58,6},{503,9},{4085,12},{65423,16},{65424,16},{65425,16},{65426,16},{65427,16},{65428,16},{65429,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {59,6},{1016,10},{65430,16},{65431,16},{65432,16},{65433,16},{65434,16},{65435,16},{65436,16},{65437,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {122,7},{2039,11},{65438,16},{65439,16},{65440,16},{65441,16},{65442,16},{65443,16},{65444,16},{65445,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {123,7},{4086,12},{65446,16},{65447,16},{65448,16},{65449,16},{65450,16},{65451,16},{65452,16},{65453,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {250,8},{4087,12},{65454,16},{65455,16},{65456,16},{65457,16},{65458,16},{65459,16},{65460,16},{65461,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {504,9},{32704,15},{65462,16},{65463,16},{65464,16},{65465,16},{65466,16},{65467,16},{65468,16},{65469,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {505,9},{65470,16},{65471,16},{65472,16},{65473,16},{65474,16},{65475,16},{65476,16},{65477,16},{65478,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {506,9},{65479,16},{65480,16},{65481,16},{65482,16},{65483,16},{65484,16},{65485,16},{65486,16},{65487,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {1017,10},{65488,16},{65489,16},{65490,16},{65491,16},{65492,16},{65493,16},{65494,16},{65495,16},{65496,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {1018,10},{65497,16},{65498,16},{65499,16},{65500,16},{65501,16},{65502,16},{65503,16},{65504,16},{65505,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {2040,11},{65506,16},{65507,16},{65508,16},{65509,16},{65510,16},{65511,16},{65512,16},{65513,16},{65514,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {65515,16},{65516,16},{65517,16},{65518,16},{65519,16},{65520,16},{65521,16},{65522,16},{65523,16},{65524,16},{0,0},{0,0},{0,0},{0,0},{0,0}, + {2041,11},{65525,16},{65526,16},{65527,16},{65528,16},{65529,16},{65530,16},{65531,16},{65532,16},{65533,16},{65534,16},{0,0},{0,0},{0,0},{0,0},{0,0} + }; + static const unsigned short UVAC_HT[256][2] = { + {0,2},{1,2},{4,3},{10,4},{24,5},{25,5},{56,6},{120,7},{500,9},{1014,10},{4084,12},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {11,4},{57,6},{246,8},{501,9},{2038,11},{4085,12},{65416,16},{65417,16},{65418,16},{65419,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {26,5},{247,8},{1015,10},{4086,12},{32706,15},{65420,16},{65421,16},{65422,16},{65423,16},{65424,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {27,5},{248,8},{1016,10},{4087,12},{65425,16},{65426,16},{65427,16},{65428,16},{65429,16},{65430,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {58,6},{502,9},{65431,16},{65432,16},{65433,16},{65434,16},{65435,16},{65436,16},{65437,16},{65438,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {59,6},{1017,10},{65439,16},{65440,16},{65441,16},{65442,16},{65443,16},{65444,16},{65445,16},{65446,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {121,7},{2039,11},{65447,16},{65448,16},{65449,16},{65450,16},{65451,16},{65452,16},{65453,16},{65454,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {122,7},{2040,11},{65455,16},{65456,16},{65457,16},{65458,16},{65459,16},{65460,16},{65461,16},{65462,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {249,8},{65463,16},{65464,16},{65465,16},{65466,16},{65467,16},{65468,16},{65469,16},{65470,16},{65471,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {503,9},{65472,16},{65473,16},{65474,16},{65475,16},{65476,16},{65477,16},{65478,16},{65479,16},{65480,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {504,9},{65481,16},{65482,16},{65483,16},{65484,16},{65485,16},{65486,16},{65487,16},{65488,16},{65489,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {505,9},{65490,16},{65491,16},{65492,16},{65493,16},{65494,16},{65495,16},{65496,16},{65497,16},{65498,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {506,9},{65499,16},{65500,16},{65501,16},{65502,16},{65503,16},{65504,16},{65505,16},{65506,16},{65507,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {2041,11},{65508,16},{65509,16},{65510,16},{65511,16},{65512,16},{65513,16},{65514,16},{65515,16},{65516,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {16352,14},{65517,16},{65518,16},{65519,16},{65520,16},{65521,16},{65522,16},{65523,16},{65524,16},{65525,16},{0,0},{0,0},{0,0},{0,0},{0,0}, + {1018,10},{32707,15},{65526,16},{65527,16},{65528,16},{65529,16},{65530,16},{65531,16},{65532,16},{65533,16},{65534,16},{0,0},{0,0},{0,0},{0,0},{0,0} + }; + static const int YQT[] = {16,11,10,16,24,40,51,61,12,12,14,19,26,58,60,55,14,13,16,24,40,57,69,56,14,17,22,29,51,87,80,62,18,22, + 37,56,68,109,103,77,24,35,55,64,81,104,113,92,49,64,78,87,103,121,120,101,72,92,95,98,112,100,103,99}; + static const int UVQT[] = {17,18,24,47,99,99,99,99,18,21,26,66,99,99,99,99,24,26,56,99,99,99,99,99,47,66,99,99,99,99,99,99, + 99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99}; + static const float aasf[] = { 1.0f * 2.828427125f, 1.387039845f * 2.828427125f, 1.306562965f * 2.828427125f, 1.175875602f * 2.828427125f, + 1.0f * 2.828427125f, 0.785694958f * 2.828427125f, 0.541196100f * 2.828427125f, 0.275899379f * 2.828427125f }; + + int row, col, i, k, subsample; + float fdtbl_Y[64], fdtbl_UV[64]; + unsigned char YTable[64], UVTable[64]; + + if(!data || !width || !height || comp > 4 || comp < 1) { + return 0; + } + + quality = quality ? quality : 90; + subsample = quality <= 90 ? 1 : 0; + quality = quality < 1 ? 1 : quality > 100 ? 100 : quality; + quality = quality < 50 ? 5000 / quality : 200 - quality * 2; + + for(i = 0; i < 64; ++i) { + int uvti, yti = (YQT[i]*quality+50)/100; + YTable[stbiw__jpg_ZigZag[i]] = (unsigned char) (yti < 1 ? 1 : yti > 255 ? 255 : yti); + uvti = (UVQT[i]*quality+50)/100; + UVTable[stbiw__jpg_ZigZag[i]] = (unsigned char) (uvti < 1 ? 1 : uvti > 255 ? 255 : uvti); + } + + for(row = 0, k = 0; row < 8; ++row) { + for(col = 0; col < 8; ++col, ++k) { + fdtbl_Y[k] = 1 / (YTable [stbiw__jpg_ZigZag[k]] * aasf[row] * aasf[col]); + fdtbl_UV[k] = 1 / (UVTable[stbiw__jpg_ZigZag[k]] * aasf[row] * aasf[col]); + } + } + + // Write Headers + { + static const unsigned char head0[] = { 0xFF,0xD8,0xFF,0xE0,0,0x10,'J','F','I','F',0,1,1,0,0,1,0,1,0,0,0xFF,0xDB,0,0x84,0 }; + static const unsigned char head2[] = { 0xFF,0xDA,0,0xC,3,1,0,2,0x11,3,0x11,0,0x3F,0 }; + const unsigned char head1[] = { 0xFF,0xC0,0,0x11,8,(unsigned char)(height>>8),STBIW_UCHAR(height),(unsigned char)(width>>8),STBIW_UCHAR(width), + 3,1,(unsigned char)(subsample?0x22:0x11),0,2,0x11,1,3,0x11,1,0xFF,0xC4,0x01,0xA2,0 }; + s->func(s->context, (void*)head0, sizeof(head0)); + s->func(s->context, (void*)YTable, sizeof(YTable)); + stbiw__putc(s, 1); + s->func(s->context, UVTable, sizeof(UVTable)); + s->func(s->context, (void*)head1, sizeof(head1)); + s->func(s->context, (void*)(std_dc_luminance_nrcodes+1), sizeof(std_dc_luminance_nrcodes)-1); + s->func(s->context, (void*)std_dc_luminance_values, sizeof(std_dc_luminance_values)); + stbiw__putc(s, 0x10); // HTYACinfo + s->func(s->context, (void*)(std_ac_luminance_nrcodes+1), sizeof(std_ac_luminance_nrcodes)-1); + s->func(s->context, (void*)std_ac_luminance_values, sizeof(std_ac_luminance_values)); + stbiw__putc(s, 1); // HTUDCinfo + s->func(s->context, (void*)(std_dc_chrominance_nrcodes+1), sizeof(std_dc_chrominance_nrcodes)-1); + s->func(s->context, (void*)std_dc_chrominance_values, sizeof(std_dc_chrominance_values)); + stbiw__putc(s, 0x11); // HTUACinfo + s->func(s->context, (void*)(std_ac_chrominance_nrcodes+1), sizeof(std_ac_chrominance_nrcodes)-1); + s->func(s->context, (void*)std_ac_chrominance_values, sizeof(std_ac_chrominance_values)); + s->func(s->context, (void*)head2, sizeof(head2)); + } + + // Encode 8x8 macroblocks + { + static const unsigned short fillBits[] = {0x7F, 7}; + int DCY=0, DCU=0, DCV=0; + int bitBuf=0, bitCnt=0; + // comp == 2 is grey+alpha (alpha is ignored) + int ofsG = comp > 2 ? 1 : 0, ofsB = comp > 2 ? 2 : 0; + const unsigned char *dataR = (const unsigned char *)data; + const unsigned char *dataG = dataR + ofsG; + const unsigned char *dataB = dataR + ofsB; + int x, y, pos; + if(subsample) { + for(y = 0; y < height; y += 16) { + for(x = 0; x < width; x += 16) { + float Y[256], U[256], V[256]; + for(row = y, pos = 0; row < y+16; ++row) { + // row >= height => use last input row + int clamped_row = (row < height) ? row : height - 1; + int base_p = (stbi__flip_vertically_on_write ? (height-1-clamped_row) : clamped_row)*width*comp; + for(col = x; col < x+16; ++col, ++pos) { + // if col >= width => use pixel from last input column + int p = base_p + ((col < width) ? col : (width-1))*comp; + float r = dataR[p], g = dataG[p], b = dataB[p]; + Y[pos]= +0.29900f*r + 0.58700f*g + 0.11400f*b - 128; + U[pos]= -0.16874f*r - 0.33126f*g + 0.50000f*b; + V[pos]= +0.50000f*r - 0.41869f*g - 0.08131f*b; + } + } + DCY = stbiw__jpg_processDU(s, &bitBuf, &bitCnt, Y+0, 16, fdtbl_Y, DCY, YDC_HT, YAC_HT); + DCY = stbiw__jpg_processDU(s, &bitBuf, &bitCnt, Y+8, 16, fdtbl_Y, DCY, YDC_HT, YAC_HT); + DCY = stbiw__jpg_processDU(s, &bitBuf, &bitCnt, Y+128, 16, fdtbl_Y, DCY, YDC_HT, YAC_HT); + DCY = stbiw__jpg_processDU(s, &bitBuf, &bitCnt, Y+136, 16, fdtbl_Y, DCY, YDC_HT, YAC_HT); + + // subsample U,V + { + float subU[64], subV[64]; + int yy, xx; + for(yy = 0, pos = 0; yy < 8; ++yy) { + for(xx = 0; xx < 8; ++xx, ++pos) { + int j = yy*32+xx*2; + subU[pos] = (U[j+0] + U[j+1] + U[j+16] + U[j+17]) * 0.25f; + subV[pos] = (V[j+0] + V[j+1] + V[j+16] + V[j+17]) * 0.25f; + } + } + DCU = stbiw__jpg_processDU(s, &bitBuf, &bitCnt, subU, 8, fdtbl_UV, DCU, UVDC_HT, UVAC_HT); + DCV = stbiw__jpg_processDU(s, &bitBuf, &bitCnt, subV, 8, fdtbl_UV, DCV, UVDC_HT, UVAC_HT); + } + } + } + } else { + for(y = 0; y < height; y += 8) { + for(x = 0; x < width; x += 8) { + float Y[64], U[64], V[64]; + for(row = y, pos = 0; row < y+8; ++row) { + // row >= height => use last input row + int clamped_row = (row < height) ? row : height - 1; + int base_p = (stbi__flip_vertically_on_write ? (height-1-clamped_row) : clamped_row)*width*comp; + for(col = x; col < x+8; ++col, ++pos) { + // if col >= width => use pixel from last input column + int p = base_p + ((col < width) ? col : (width-1))*comp; + float r = dataR[p], g = dataG[p], b = dataB[p]; + Y[pos]= +0.29900f*r + 0.58700f*g + 0.11400f*b - 128; + U[pos]= -0.16874f*r - 0.33126f*g + 0.50000f*b; + V[pos]= +0.50000f*r - 0.41869f*g - 0.08131f*b; + } + } + + DCY = stbiw__jpg_processDU(s, &bitBuf, &bitCnt, Y, 8, fdtbl_Y, DCY, YDC_HT, YAC_HT); + DCU = stbiw__jpg_processDU(s, &bitBuf, &bitCnt, U, 8, fdtbl_UV, DCU, UVDC_HT, UVAC_HT); + DCV = stbiw__jpg_processDU(s, &bitBuf, &bitCnt, V, 8, fdtbl_UV, DCV, UVDC_HT, UVAC_HT); + } + } + } + + // Do the bit alignment of the EOI marker + stbiw__jpg_writeBits(s, &bitBuf, &bitCnt, fillBits); + } + + // EOI + stbiw__putc(s, 0xFF); + stbiw__putc(s, 0xD9); + + return 1; +} + +STBIWDEF int stbi_write_jpg_to_func(stbi_write_func *func, void *context, int x, int y, int comp, const void *data, int quality) +{ + stbi__write_context s = { 0 }; + stbi__start_write_callbacks(&s, func, context); + return stbi_write_jpg_core(&s, x, y, comp, (void *) data, quality); +} + + +#ifndef STBI_WRITE_NO_STDIO +STBIWDEF int stbi_write_jpg(char const *filename, int x, int y, int comp, const void *data, int quality) +{ + stbi__write_context s = { 0 }; + if (stbi__start_write_file(&s,filename)) { + int r = stbi_write_jpg_core(&s, x, y, comp, data, quality); + stbi__end_write_file(&s); + return r; + } else + return 0; +} +#endif + +#endif // STB_IMAGE_WRITE_IMPLEMENTATION + +/* Revision history + 1.16 (2021-07-11) + make Deflate code emit uncompressed blocks when it would otherwise expand + support writing BMPs with alpha channel + 1.15 (2020-07-13) unknown + 1.14 (2020-02-02) updated JPEG writer to downsample chroma channels + 1.13 + 1.12 + 1.11 (2019-08-11) + + 1.10 (2019-02-07) + support utf8 filenames in Windows; fix warnings and platform ifdefs + 1.09 (2018-02-11) + fix typo in zlib quality API, improve STB_I_W_STATIC in C++ + 1.08 (2018-01-29) + add stbi__flip_vertically_on_write, external zlib, zlib quality, choose PNG filter + 1.07 (2017-07-24) + doc fix + 1.06 (2017-07-23) + writing JPEG (using Jon Olick's code) + 1.05 ??? + 1.04 (2017-03-03) + monochrome BMP expansion + 1.03 ??? + 1.02 (2016-04-02) + avoid allocating large structures on the stack + 1.01 (2016-01-16) + STBIW_REALLOC_SIZED: support allocators with no realloc support + avoid race-condition in crc initialization + minor compile issues + 1.00 (2015-09-14) + installable file IO function + 0.99 (2015-09-13) + warning fixes; TGA rle support + 0.98 (2015-04-08) + added STBIW_MALLOC, STBIW_ASSERT etc + 0.97 (2015-01-18) + fixed HDR asserts, rewrote HDR rle logic + 0.96 (2015-01-17) + add HDR output + fix monochrome BMP + 0.95 (2014-08-17) + add monochrome TGA output + 0.94 (2014-05-31) + rename private functions to avoid conflicts with stb_image.h + 0.93 (2014-05-27) + warning fixes + 0.92 (2010-08-01) + casts to unsigned char to fix warnings + 0.91 (2010-07-17) + first public release + 0.90 first internal release +*/ + +/* +------------------------------------------------------------------------------ +This software is available under 2 licenses -- choose whichever you prefer. +------------------------------------------------------------------------------ +ALTERNATIVE A - MIT License +Copyright (c) 2017 Sean Barrett +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +------------------------------------------------------------------------------ +ALTERNATIVE B - Public Domain (www.unlicense.org) +This is free and unencumbered software released into the public domain. +Anyone is free to copy, modify, publish, use, compile, sell, or distribute this +software, either in source code form or as a compiled binary, for any purpose, +commercial or non-commercial, and by any means. +In jurisdictions that recognize copyright laws, the author or authors of this +software dedicate any and all copyright interest in the software to the public +domain. We make this dedication for the benefit of the public at large and to +the detriment of our heirs and successors. We intend this dedication to be an +overt act of relinquishment in perpetuity of all present and future rights to +this software under copyright law. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +------------------------------------------------------------------------------ +*/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..c412bab --- /dev/null +++ b/README.md @@ -0,0 +1,508 @@ +# 3DS Webcam Bridge + +Stream your Nintendo 3DS cameras to your PC as a virtual webcam! Support for both **mono** and **stereo (3D)** modes using the outer dual cameras. + +![License](https://img.shields.io/badge/license-MIT-blue.svg) +![Platform](https://img.shields.io/badge/platform-3DS%20%7C%20Windows%20%7C%20Linux-lightgrey) + +## Features + +- **📷 Multiple Camera Modes** + - Inner camera (single lens, mono) + - Outer cameras (dual lens, mono or stereo) + - True 3D support with synchronized stereo capture + +- **🎥 Stereo Output Modes** + - **Off**: Left eye only + - **Side-by-Side**: Horizontal stereo for 3D displays/VR + - **Anaglyph**: Red-cyan 3D glasses + - **Dual**: Separate left/right virtual cameras + +- **💻 Cross-Platform PC Support** + - **Windows 10/11**: Via OBS Virtual Camera + - **Linux**: Via v4l2loopback + pyvirtualcam/pyfakewebcam/ffmpeg + +- **⚡ Real-time Streaming** + - ~15 FPS @ 400×240 per camera + - JPEG compression for efficient Wi-Fi streaming + - Auto-reconnect on connection loss + +--- + +## 📋 Table of Contents + +- [Security & Ethics Notice](#security--ethics-notice) +- [Quick Start](#quick-start) + - [Linux (Ubuntu/Debian)](#linux-ubuntudebian) + - [Windows 10/11](#windows-1011) + - [Building for 3DS](#building-for-3ds) +- [Usage](#usage) + - [3DS Application](#3ds-application) + - [PC Server Options](#pc-server-options) +- [Project Structure](#project-structure) +- [Requirements](#requirements) +- [Troubleshooting](#troubleshooting) +- [Documentation](#documentation) +- [Contributing](#contributing) +- [License](#license) + +--- + +## ⚠️ Security & Ethics Notice + +**IMPORTANT**: This software is intended for personal, educational, and creative use only. + +- **Privacy**: Streaming camera feeds creates privacy risks. Always obtain consent from anyone who may be captured by the cameras. +- **Network Security**: The stream is **not encrypted** by default. Only use on trusted networks. +- **Responsible Use**: Do not use this software for surveillance, harassment, or any illegal activities. +- **Children**: If used by or around minors, ensure appropriate adult supervision and consent. + +By using this software, you accept full responsibility for its use and agree to comply with all applicable laws and regulations. + +--- + +## 🚀 Quick Start + +### Linux (Ubuntu/Debian) + +#### 1. Install PC Server Dependencies + +```bash +# Clone or navigate to the repository +cd 3ds-webcam-bridge + +# Install dependencies (Python, v4l2loopback, ffmpeg, etc.) +./scripts/pc-install.sh + +# Create virtual camera device +./scripts/create-v4l2.sh + +# For dual devices (stereo mode): +./scripts/create-v4l2.sh --dual +``` + +#### 2. Run the Server + +```bash +# Mono mode (default) +./pc-server/run_server.sh --device /dev/video0 + +# Side-by-side stereo +./pc-server/run_server.sh --stereo-mode sbs --width 400 --height 240 --device /dev/video0 + +# Dual device stereo +./pc-server/run_server.sh --stereo-mode dual --dual-devices /dev/video0,/dev/video1 +``` + +#### 3. Use in Applications + +Open any application that supports webcams (Zoom, OBS, Chrome, etc.) and select: +- **Single mode**: `/dev/video0` (or "3DS Webcam") +- **Dual mode**: `/dev/video0` (Left) and `/dev/video1` (Right) + +--- + +### Windows 10/11 + +#### 1. Install PC Server Dependencies + +**Run PowerShell as Administrator:** + +```powershell +# Allow script execution for this session +Set-ExecutionPolicy Bypass -Scope Process -Force + +# Navigate to repository +cd 3ds-webcam-bridge + +# Run installer (installs Python, OBS Studio, dependencies) +.\scripts\windows\pc-install.ps1 -Port 9000 +``` + +The installer will: +- ✅ Install Python 3.11 (if needed) +- ✅ Install OBS Studio with Virtual Camera +- ✅ Create Python virtual environment +- ✅ Install Python packages +- ✅ Add firewall rule for TCP port 9000 + +#### 2. Initialize OBS Virtual Camera + +**Important first-time step:** + +1. Launch **OBS Studio** +2. Go to **Tools → VirtualCam → Start** +3. Close OBS (the virtual camera will remain available) + +#### 3. Run the Server + +```powershell +# Mono mode +.\pc-server\run_server.ps1 -Port 9000 -Device "OBS Virtual Camera" + +# Side-by-side stereo +.\pc-server\run_server.ps1 -StereoMode sbs -Width 400 -Height 240 -Device "OBS Virtual Camera" + +# Dual device stereo (requires multiple OBS virtual cameras) +.\pc-server\run_server.ps1 -StereoMode dual -DualDevices "OBS Virtual Camera,OBS Virtual Camera 2" +``` + +#### 4. Use in Applications + +Open any application (Zoom, Discord, Chrome, etc.) and select: +- **"OBS Virtual Camera"** as your camera device + +--- + +### Building for 3DS + +#### 1. Install devkitPro (Linux) + +```bash +# Install devkitPro toolchain +./scripts/install-devkitpro.sh + +# Set environment variables (add to ~/.bashrc for persistence) +export DEVKITPRO=/opt/devkitpro +export DEVKITARM=$DEVKITPRO/devkitARM +export PATH=$DEVKITARM/bin:$PATH + +# Reload environment +source ~/.bashrc +``` + +#### 2. Build the 3DS App + +```bash +# Build (downloads stb_image_write.h if needed) +./scripts/build-3ds.sh + +# Output: dist/3ds-webcam-bridge.3dsx +``` + +#### 3. Install on 3DS + +1. Copy `dist/3ds-webcam-bridge.3dsx` to your SD card: + ``` + SD:/3ds/3ds-webcam-bridge/3ds-webcam-bridge.3dsx + ``` + +2. Insert SD card into 3DS +3. Launch **Homebrew Launcher** +4. Launch **3DS Webcam Bridge** + +--- + +## 📖 Usage + +### 3DS Application + +#### Controls + +| Button | Action | +|--------|--------| +| **A** | Start/Stop streaming | +| **SELECT** | Toggle between Inner/Outer cameras | +| **X** | Toggle 3D mode (outer cameras only) | +| **Y** | Edit IP address and port | +| **START** | Exit application | + +#### UI Screens + +**Top Screen**: Status display +- Server IP and port +- Connection status +- Camera mode +- Frame count, data sent, FPS + +**Bottom Screen**: Settings +- **Network configuration** (IP/Port) - Press Y to edit +- Camera selection (Inner/Outer) +- 3D checkbox (enabled only for Outer cameras) +- Resolution and quality info + +#### Camera Modes + +| Mode | Description | 3D Support | +|------|-------------|------------| +| **Inner** | Front-facing camera (single lens) | ❌ No | +| **Outer** | Rear cameras (dual lens) | ✅ Yes | + +**Important**: The **3D checkbox** is only enabled when **Outer** cameras are selected. The inner camera has only one lens and cannot provide stereo 3D. + +#### Configuration + +**Method 1: Using Software Keyboard (Recommended)** + +1. Press **Y** button in the app +2. Select **A** to edit IP address or **B** to edit port +3. Use the 3DS keyboard to enter your PC's IP address +4. Configuration is automatically saved to SD card + +**Method 2: Manual Config File** + +Create a config file at **SD:/3ds/3ds-webcam-bridge/config.txt**: + +``` +# Your PC's IP address +ip=192.168.1.100 + +# Server port +port=9000 +``` + +The app will load this config on startup. Any changes made via the Y button will update this file. + +--- + +### PC Server Options + +#### Full Command-Line Reference + +```bash +server.py [options] + +Options: + --listen ADDRESS Listen address (default: 0.0.0.0) + --port PORT TCP port (default: 9000) + --mode MODE Force mono or auto-detect (default: auto) + --stereo-mode MODE Stereo output: off, sbs, anaglyph, dual (default: off) + --device DEVICE Virtual camera device + Linux: /dev/videoX + Windows: "OBS Virtual Camera" or index + --dual-devices DEVICES Two devices for dual mode (comma-separated) + --width WIDTH Output width per eye (default: 640) + --height HEIGHT Output height (default: 480) + --fps FPS Output frame rate (default: 15) + --prefer-backend BACKEND Backend: pyvirtualcam, pyfakewebcam, ffmpeg + --verbose Verbose logging +``` + +#### Examples + +**Linux**: +```bash +# Basic mono +./run_server.sh --device /dev/video0 + +# Side-by-side stereo (1280x480 output) +./run_server.sh --stereo-mode sbs --width 640 --height 480 + +# Anaglyph 3D (red-cyan) +./run_server.sh --stereo-mode anaglyph --width 640 --height 480 + +# Dual outputs +./run_server.sh --stereo-mode dual --dual-devices /dev/video0,/dev/video1 + +# Use pyfakewebcam backend +./run_server.sh --prefer-backend pyfakewebcam --device /dev/video0 +``` + +**Windows**: +```powershell +# Basic mono +.\run_server.ps1 -Device "OBS Virtual Camera" + +# Side-by-side stereo +.\run_server.ps1 -StereoMode sbs -Width 640 -Height 480 + +# Dual outputs (requires configuring multiple OBS virtual cameras) +.\run_server.ps1 -StereoMode dual -DualDevices "OBS Virtual Camera,OBS Virtual Camera 2" +``` + +--- + +## 📁 Project Structure + +``` +3ds-webcam-bridge/ +├── README.md # This file +├── LICENSE # MIT license +├── .gitignore +├── examples/ +│ └── sample-config.json # Example configuration +├── 3ds-app/ # 3DS homebrew application +│ ├── Makefile +│ └── source/ +│ ├── main.c # Main app with UI +│ ├── camera_stream.c # Camera capture & streaming +│ ├── camera_stream.h +│ └── third_party/ +│ └── stb_image_write.h # JPEG encoder +├── pc-server/ # PC server (Python) +│ ├── server.py # Main server application +│ ├── requirements.txt +│ ├── run_server.sh # Linux launcher +│ └── run_server.ps1 # Windows launcher +├── scripts/ # Installation & build scripts +│ ├── install-devkitpro.sh # Install 3DS toolchain (Linux) +│ ├── build-3ds.sh # Build 3DS app +│ ├── pc-install.sh # Install PC dependencies (Linux) +│ ├── create-v4l2.sh # Create v4l2loopback devices (Linux) +│ └── windows/ +│ ├── pc-install.ps1 # Install PC dependencies (Windows) +│ └── helpers.psm1 # PowerShell helper functions +├── docs/ # Documentation +│ ├── design.md # Technical design details +│ └── troubleshooting.md # Common issues and solutions +└── dist/ # Build output + └── 3ds-webcam-bridge.3dsx +``` + +--- + +## 📦 Requirements + +### 3DS + +- **Hardware**: Nintendo 3DS/2DS (Old or New) with Homebrew Launcher +- **Firmware**: Any version with Homebrew access +- **SD Card**: For installing the `.3dsx` file +- **Wi-Fi**: 2.4 GHz network (3DS doesn't support 5 GHz) + +### PC - Linux + +- **OS**: Ubuntu 20.04+, Debian 11+, or equivalent +- **Kernel**: 4.4+ with v4l2loopback support +- **Python**: 3.8+ +- **Packages**: + - `v4l2loopback-dkms` + - `v4l2loopback-utils` + - `ffmpeg` + - `python3-venv` + +### PC - Windows + +- **OS**: Windows 10 (1809+) or Windows 11 +- **Python**: 3.8+ (auto-installed by script) +- **OBS Studio**: 26.0+ (auto-installed by script) +- **winget**: For automated installation + +### Network + +- **Both devices on same network** (LAN/Wi-Fi) +- **TCP port 9000** open (configurable) +- **Recommended**: 5 GHz Wi-Fi for PC (3DS uses 2.4 GHz) + +--- + +## 🔧 Troubleshooting + +### Common Issues + +#### "No virtual camera found" (Windows) +- **Solution**: Start OBS Studio, go to **Tools → VirtualCam → Start**, then try again +- Ensure OBS Studio 26.0+ is installed (check via `winget list OBSProject.OBSStudio`) + +#### "/dev/video0: Device or resource busy" (Linux) +- **Solution**: Close all applications using the device (browser, Zoom, etc.) +- Check: `sudo fuser /dev/video0` +- Kill processes: `sudo fuser -k /dev/video0` + +#### "Connection refused" / "Timeout" +- Check that both devices are on the same network +- Verify the server IP address on 3DS matches your PC +- Find your PC IP: + - **Linux**: `ip addr show` or `hostname -I` + - **Windows**: `ipconfig` (look for IPv4) +- Check firewall: + - **Linux**: `sudo ufw allow 9000/tcp` + - **Windows**: Run installer as Administrator to add rule + +#### Low FPS / Laggy stream +- Reduce resolution: `--width 320 --height 240` +- Lower JPEG quality (edit `3ds-app/source/camera_stream.h`, change `JPEG_QUALITY`) +- Use 5 GHz Wi-Fi for PC (3DS is limited to 2.4 GHz) +- Ensure 3DS is close to Wi-Fi router + +#### "v4l2loopback not loaded" (Linux) +- **Solution**: Run `./scripts/create-v4l2.sh` +- Manual: `sudo modprobe v4l2loopback` +- Check: `lsmod | grep v4l2loopback` + +#### Stereo mode not working +- Ensure 3DS is set to **Outer** cameras with **3D enabled** +- Verify server is using `--stereo-mode sbs` (or anaglyph/dual) +- Check logs with `--verbose` flag + +For more detailed troubleshooting, see [`docs/troubleshooting.md`](docs/troubleshooting.md). + +--- + +## 📚 Documentation + +- **[design.md](docs/design.md)**: Protocol specification, architecture, performance tuning +- **[troubleshooting.md](docs/troubleshooting.md)**: Detailed problem-solving guide +- **[sample-config.json](examples/sample-config.json)**: Example configuration + +### External Resources + +- **devkitPro**: https://devkitpro.org/wiki/Getting_Started +- **libctru Camera API**: https://libctru.devkitpro.org/cam_8h.html +- **pyvirtualcam**: https://github.com/letmaik/pyvirtualcam +- **v4l2loopback**: https://github.com/umlaeute/v4l2loopback +- **stb_image_write**: https://github.com/nothings/stb + +--- + +## 🤝 Contributing + +Contributions are welcome! Please: + +1. Fork the repository +2. Create a feature branch +3. Make your changes +4. Test on both platforms (if possible) +5. Submit a pull request + +### Ideas for Contributions + +- macOS support +- Config UI for IP entry on 3DS +- Recording to file option +- H.264 encoding for lower bandwidth +- Portrait/landscape rotation +- Multiple 3DS clients simultaneously + +--- + +## 📄 License + +This project is licensed under the **MIT License** - see the [LICENSE](LICENSE) file for details. + +### Third-Party Libraries + +- **libctru**: https://github.com/devkitPro/libctru (Zlib license) +- **stb_image_write**: https://github.com/nothings/stb (Public domain / MIT) +- **pyvirtualcam**: https://github.com/letmaik/pyvirtualcam (GPL-2.0) + +--- + +## 🙏 Acknowledgments + +- devkitPro team for the amazing 3DS toolchain +- stb libraries by Sean Barrett +- v4l2loopback developers +- pyvirtualcam and OBS Studio teams + +--- + +## ⚡ Performance Tips + +### 3DS Side +- Keep 3DS close to Wi-Fi router +- Close other applications/homebrew +- Use lower resolution if needed (edit source) + +### PC Side +- Use wired Ethernet for PC (3DS will be on Wi-Fi) +- Close unnecessary applications +- For dual mode, ensure both v4l2 devices are created + +### Network +- Use QoS to prioritize 3DS traffic if possible +- Avoid network congestion (large downloads, streaming) + +--- + +**Made with ❤️ for the 3DS homebrew community** diff --git a/docs/design.md b/docs/design.md new file mode 100644 index 0000000..c023c57 --- /dev/null +++ b/docs/design.md @@ -0,0 +1,479 @@ +# Technical Design Document + +## Architecture Overview + +The 3DS Webcam Bridge consists of two main components: + +1. **3DS Homebrew Application**: Captures camera frames, encodes to JPEG, and streams over TCP +2. **PC Server**: Receives frames, decodes, and exposes as virtual webcam(s) + +``` +┌─────────────────┐ ┌──────────────────┐ +│ Nintendo 3DS │ │ PC (Server) │ +│ │ │ │ +│ ┌───────────┐ │ │ ┌────────────┐ │ +│ │ Camera │ │ TCP/IP (Wi-Fi) │ │ Server │ │ +│ │ Capture │ │───────────────────► │ │ (Python) │ │ +│ └─────┬─────┘ │ Port 9000 │ └──────┬─────┘ │ +│ │ │ │ │ │ +│ ┌─────▼─────┐ │ │ ┌──────▼─────┐ │ +│ │ JPEG │ │ │ │ JPEG │ │ +│ │ Encoder │ │ │ │ Decoder │ │ +│ └─────┬─────┘ │ │ └──────┬─────┘ │ +│ │ │ │ │ │ +│ ┌─────▼─────┐ │ │ ┌──────▼─────┐ │ +│ │ Network │ │ │ │ Virtual │ │ +│ │ Protocol │ │ │ │ Webcam │ │ +│ └───────────┘ │ │ └────────────┘ │ +└─────────────────┘ └──────────────────┘ +``` + +--- + +## Protocol Specification + +### Wire Protocol + +The protocol is designed to be backward-compatible and support both mono and stereo streams. + +#### Header Format + +All frames begin with a 2-byte header: + +``` +[FLAGS (1 byte)][VERSION (1 byte)] +``` + +**FLAGS**: +- Bit 0: Stereo flag (1 = stereo, 0 = mono) +- Bits 1-7: Reserved (must be 0) + +**VERSION**: Protocol version (currently 0x01) + +#### Mono Frame Format + +``` +┌─────────┬─────────┬────────────┬──────────────┐ +│ FLAGS │ VERSION │ LENGTH │ JPEG DATA │ +│ (1 byte)│ (1 byte)│ (4 bytes) │ (LENGTH) │ +│ 0x00 │ 0x01 │ Big-endian │ │ +└─────────┴─────────┴────────────┴──────────────┘ +``` + +**Total header**: 6 bytes +- FLAGS: 0x00 (no stereo) +- VERSION: 0x01 +- LENGTH: 32-bit unsigned big-endian integer (JPEG size) + +#### Stereo Frame Format + +``` +┌─────────┬─────────┬────────────────────────────────┬─────────────┬──────────────┬─────────────┬──────────────┐ +│ FLAGS │ VERSION │ DIMENSIONS │ LEFT_LEN │ RIGHT_LEN │ LEFT_JPEG │ RIGHT_JPEG │ +│ (1 byte)│ (1 byte)│ (8 bytes) │ (4 bytes) │ (4 bytes) │ (LEFT_LEN) │ (RIGHT_LEN) │ +│ 0x01 │ 0x01 │ wL,hL,wR,hR (16-bit BE each) │ Big-endian │ Big-endian │ │ │ +└─────────┴─────────┴────────────────────────────────┴─────────────┴──────────────┴─────────────┴──────────────┘ +``` + +**Total header**: 18 bytes +- FLAGS: 0x01 (stereo enabled) +- VERSION: 0x01 +- DIMENSIONS: + - wL: Left width (16-bit BE) + - hL: Left height (16-bit BE) + - wR: Right width (16-bit BE) + - hR: Right height (16-bit BE) +- LEFT_LEN: Left JPEG size (32-bit BE) +- RIGHT_LEN: Right JPEG size (32-bit BE) + +### Network Transport + +- **Protocol**: TCP +- **Port**: 9000 (default, configurable) +- **Connection**: Client (3DS) → Server (PC) +- **Reconnection**: Automatic with exponential backoff (3s delay) +- **Socket Options**: `TCP_NODELAY` enabled for lower latency + +--- + +## 3DS Implementation Details + +### Camera Subsystem + +The 3DS has multiple camera configurations: + +| Camera | Hardware | Purpose | Stereo? | +|--------|----------|---------|---------| +| **Inner (SELECT_IN1)** | Single lens | Front-facing selfie camera | ❌ No | +| **Outer (SELECT_OUT1, SELECT_OUT2)** | Dual lens | Rear stereo cameras | ✅ Yes | + +#### Camera Configuration + +```c +CAMU_SetSize(select, SIZE_CTR_TOP_LCD, CONTEXT_A); // 400×240 +CAMU_SetOutputFormat(select, OUTPUT_YUV_422, CONTEXT_A); // YUV422 +CAMU_SetFrameRate(select, FRAME_RATE_15); // 15 FPS target +CAMU_SetNoiseFilter(select, true); // Reduce noise +CAMU_SetAutoExposure(select, true); // Auto exposure +CAMU_SetAutoWhiteBalance(select, true); // Auto WB +``` + +#### Capture Process + +1. **Select camera(s)**: `SELECT_IN1` or `SELECT_OUT1|SELECT_OUT2` +2. **Set transfer size**: `CAMU_GetMaxBytes()` → `CAMU_SetTransferBytes()` +3. **Clear buffer**: `CAMU_ClearBuffer()` +4. **Start capture**: `CAMU_StartCapture()` +5. **Set receiving**: `CAMU_SetReceiving()` → returns handle +6. **Wait for completion**: `svcWaitSynchronization(handle)` +7. **Stop capture**: `CAMU_StopCapture()` + +For stereo, both cameras are captured sequentially but as quickly as possible to minimize desynchronization. + +### Color Space Conversion + +3DS outputs **YUV422** format. We convert to **RGB24** for JPEG encoding: + +```c +// YUV422 format: [Y0 U Y1 V] packed in 16-bit values +// Conversion matrix: +R = 1.164*(Y - 16) + 1.596*(V - 128) +G = 1.164*(Y - 16) - 0.391*(U - 128) - 0.813*(V - 128) +B = 1.164*(Y - 16) + 2.018*(U - 128) +``` + +Fixed-point integer math is used for performance: +```c +R = (298*c + 409*e + 128) >> 8 +G = (298*c - 100*d - 208*e + 128) >> 8 +B = (298*c + 516*d + 128) >> 8 +``` + +Where: `c = Y-16`, `d = U-128`, `e = V-128` + +### JPEG Encoding + +We use **stb_image_write.h** (public domain, single-header library): + +```c +stbi_write_jpg_to_func(callback, context, width, height, 3, rgb_data, quality); +``` + +- **Quality**: 65 (configurable via `JPEG_QUALITY` define) +- **Typical size**: 10-20 KB per 400×240 frame +- **Encoding time**: ~30-50ms on 3DS hardware + +### Threading Model + +``` +Main Thread: + - UI rendering (top/bottom screens) + - Input handling + - State management + +Stream Thread: + - Socket connection/reconnection + - Camera capture loop + - JPEG encoding + - Network transmission + - Statistics tracking +``` + +Synchronization via `LightLock` for shared state access. + +### Performance Characteristics + +| Metric | Mono | Stereo | +|--------|------|--------| +| **Capture Time** | ~30ms | ~60ms | +| **Encode Time** | ~40ms | ~80ms | +| **Network Time** | ~20ms | ~40ms | +| **Total Latency** | ~90ms | ~180ms | +| **Target FPS** | 12-15 | 10-12 | +| **Bandwidth** | ~150 KB/s | ~300 KB/s | + +*Note: Actual performance varies based on scene complexity, network conditions, and JPEG quality settings.* + +--- + +## PC Server Implementation + +### Backend Architecture + +The server supports multiple virtual camera backends: + +``` +┌────────────────────────────────────┐ +│ Server Core (server.py) │ +│ - Protocol parsing │ +│ - Frame decoding (JPEG → RGB) │ +│ - Stereo composition │ +└──────────────┬─────────────────────┘ + │ + ┌───────┴────────┐ + │ │ + ┌───▼───┐ ┌────▼────┐ ┌────────┐ + │ pyvir │ │ pyfake │ │ ffmpeg │ + │tualcam│ │ webcam │ │ pipe │ + └───┬───┘ └────┬────┘ └────┬───┘ + │ │ │ + ┌───▼────┐ ┌────▼────┐ ┌────▼───┐ + │Windows │ │ Linux │ │ Linux │ + │OBS Virt│ │v4l2loop │ │v4l2loop│ + └────────┘ └─────────┘ └────────┘ +``` + +### Stereo Modes + +#### 1. Off (Mono) +- Send left eye only +- Resolution: `W × H` +- Use case: Standard webcam applications + +#### 2. Side-by-Side (SBS) +- Compose horizontally: `[LEFT | RIGHT]` +- Resolution: `2W × H` +- Use case: 3D displays, VR headsets +- Implementation: + ```python + sbs = np.concatenate([left_arr, right_arr], axis=1) + ``` + +#### 3. Anaglyph (Red-Cyan) +- Red channel from left, green+blue from right +- Resolution: `W × H` +- Use case: Red-cyan 3D glasses +- Implementation: + ```python + anaglyph[:,:,0] = left[:,:,0] # Red from left + anaglyph[:,:,1] = right[:,:,1] # Green from right + anaglyph[:,:,2] = right[:,:,2] # Blue from right + ``` + +#### 4. Dual Devices +- Two separate virtual cameras +- Resolution: `W × H` each +- Use case: Advanced VR setups, custom stereo applications + +### Backend Selection Logic + +``` +User preference: --prefer-backend + │ + ├─ pyvirtualcam? + │ ├─ Available? → Use it + │ └─ Not available → Fallback + │ + ├─ pyfakewebcam? (Linux only) + │ ├─ Available? → Use it + │ └─ Not available → Fallback + │ + └─ ffmpeg? (Linux only) + ├─ Available? → Use it + └─ Not available → Error +``` + +### Frame Processing Pipeline + +``` +1. TCP Receive + ├─ Read 2-byte header + ├─ Parse FLAGS and VERSION + └─ Determine mono/stereo + +2. Decode + ├─ Mono: Read 4-byte length → JPEG data + └─ Stereo: Read 8-byte dims → 8-byte lengths → 2× JPEG data + +3. PIL Decoding + ├─ JPEG bytes → PIL Image + └─ Convert to RGB mode + +4. Composition (if stereo) + ├─ Off: left only + ├─ SBS: horizontal concat + ├─ Anaglyph: color channel merge + └─ Dual: process separately + +5. Resize + ├─ Target resolution (--width, --height) + └─ BILINEAR interpolation + +6. NumPy Conversion + └─ RGB24 format (uint8, HxWx3) + +7. Backend Send + ├─ pyvirtualcam: camera.send(frame) + ├─ pyfakewebcam: camera.schedule_frame(frame) + └─ ffmpeg: stdin.write(frame.tobytes()) +``` + +--- + +## Platform-Specific Details + +### Windows: OBS Virtual Camera + +- **Provider**: OBS Studio 26.0+ +- **Driver**: OBS VirtualCam plugin (bundled) +- **Location**: Installed with OBS, typically `C:\Program Files\obs-studio\` +- **Access**: Via pyvirtualcam library +- **Multiple devices**: User must configure in OBS settings + +**First-time setup**: +1. Launch OBS Studio +2. Tools → VirtualCam → Start +3. This initializes the driver +4. Close OBS (driver remains active) + +**Device naming**: +- Default: `"OBS Virtual Camera"` +- Can be referenced by index: `0`, `1`, etc. + +### Linux: v4l2loopback + +- **Provider**: Kernel module `v4l2loopback` +- **Installation**: `sudo apt install v4l2loopback-dkms v4l2loopback-utils` +- **Device creation**: + ```bash + sudo modprobe v4l2loopback devices=1 video_nr=0 card_label="3DS Webcam" exclusive_caps=1 + ``` + +**Persistent setup** (`/etc/modprobe.d/v4l2loopback.conf`): +``` +options v4l2loopback devices=1 video_nr=0 card_label="3DS Webcam" exclusive_caps=1 +``` + +**Multiple devices**: +```bash +sudo modprobe v4l2loopback devices=2 video_nr=0,1 \ + card_label="3DS Left","3DS Right" exclusive_caps=1,1 +``` + +**Verification**: +```bash +v4l2-ctl --list-devices +ls -l /dev/video* +``` + +--- + +## Performance Tuning + +### Bandwidth Optimization + +**JPEG Quality vs Size**: +| Quality | Avg Size (400×240) | Bandwidth @ 15 FPS | +|---------|--------------------|--------------------| +| 50 | 8 KB | 120 KB/s | +| 65 | 12 KB | 180 KB/s | +| 80 | 18 KB | 270 KB/s | +| 95 | 30 KB | 450 KB/s | + +**Recommendation**: Quality 60-70 for good balance. + +### Resolution Options + +| Resolution | Capture Time | Bandwidth @ Q65 | +|------------|--------------|-----------------| +| 320×240 | ~25ms | ~140 KB/s | +| 400×240 | ~30ms | ~180 KB/s | +| 512×384* | N/A | (Unsupported) | + +*Note: 3DS hardware maximum is 640×480, but we use 400×240 for performance.* + +### Network Recommendations + +- **Topology**: Wired PC + wireless 3DS ideal +- **Wi-Fi**: 2.4 GHz (3DS limitation), keep 3DS close to AP +- **QoS**: Prioritize port 9000 if router supports it +- **Interference**: Minimize other 2.4 GHz devices + +### CPU Usage + +**3DS**: +- Mono: ~40% CPU (one ARM11 core) +- Stereo: ~70% CPU +- Leaves headroom for system processes + +**PC**: +- Negligible (<5% on modern CPU) +- JPEG decode + resize are lightweight + +--- + +## Security Considerations + +### Threat Model + +**In Scope**: +- Local network (trusted environment) +- Personal/educational use + +**Out of Scope**: +- Internet-facing deployment +- Untrusted network actors + +### Current Security Posture + +⚠️ **No Encryption**: Traffic is plaintext over TCP +⚠️ **No Authentication**: Server accepts any connection +⚠️ **No Integrity Checks**: Corrupted data may cause crashes + +### Recommendations for Production Use + +If deploying in a less-trusted environment: + +1. **VPN Tunnel**: Use Wireguard/OpenVPN between 3DS and PC +2. **SSH Tunnel**: `ssh -L 9000:localhost:9000 user@server` +3. **Firewall Rules**: Restrict to known 3DS IP only +4. **Protocol Extension**: Add HMAC for integrity checking + +Example SSH tunnel: +```bash +# On PC +ssh -L 9000:localhost:9000 user@localhost -N + +# On 3DS +# Connect to 127.0.0.1:9000 (tunneled) +``` + +--- + +## Future Enhancements + +### Potential Improvements + +1. **H.264 Encoding**: Lower bandwidth, but requires hardware encoder (limited on 3DS) +2. **Adaptive Quality**: Adjust JPEG quality based on network conditions +3. **Frame Skipping**: Drop frames under high load to maintain real-time +4. **Audio Capture**: Add microphone support (separate audio stream) +5. **Recording**: Save to file on PC side +6. **Multi-Client**: Support multiple 3DS devices simultaneously +7. **WebRTC**: Browser-based viewing without virtual camera + +### Known Limitations + +- **3DS Hardware**: Max resolution 640×480, limited CPU/RAM +- **Network Latency**: ~100-200ms typical, inherent in Wi-Fi +- **Synchronization**: Stereo frames not hardware-synced (~10ms difference) +- **Frame Rate**: Limited by capture/encode pipeline, not true 30 FPS + +--- + +## References + +### 3DS Development +- libctru documentation: https://libctru.devkitpro.org/ +- devkitPro: https://devkitpro.org/ +- 3DS Homebrew: https://www.3dbrew.org/ + +### Libraries +- stb_image_write: https://github.com/nothings/stb +- pyvirtualcam: https://github.com/letmaik/pyvirtualcam +- v4l2loopback: https://github.com/umlaeute/v4l2loopback + +### Standards +- YUV color space: ITU-R BT.601 +- JPEG: ITU-T T.81 / ISO/IEC 10918-1 +- TCP: RFC 793 diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md new file mode 100644 index 0000000..9bf678b --- /dev/null +++ b/docs/troubleshooting.md @@ -0,0 +1,733 @@ +# Troubleshooting Guide + +This guide covers common issues and solutions for the 3DS Webcam Bridge. + +--- + +## Table of Contents + +- [Connection Issues](#connection-issues) +- [Virtual Camera Issues](#virtual-camera-issues) +- [3DS Application Issues](#3ds-application-issues) +- [Performance Issues](#performance-issues) +- [Build Issues](#build-issues) +- [Platform-Specific Issues](#platform-specific-issues) + +--- + +## Connection Issues + +### 3DS Shows "CONNECTING..." But Never Connects + +**Symptoms**: 3DS app shows "CONNECTING..." indefinitely, never reaches "STREAMING" status. + +**Possible Causes**: + +1. **Wrong IP Address** + - **Check**: Verify the server IP on your 3DS matches your PC's IP + - **Linux**: Run `ip addr show` or `hostname -I` + - **Windows**: Run `ipconfig` in Command Prompt, look for IPv4 Address + - **Solution**: Update the IP in the 3DS app or config file + +2. **PC Server Not Running** + - **Check**: Is `server.py` running on your PC? + - **Solution**: Start the server first, then start streaming on 3DS + +3. **Firewall Blocking Connection** + - **Linux**: + ```bash + # Check firewall status + sudo ufw status + + # Allow port 9000 + sudo ufw allow 9000/tcp + ``` + - **Windows**: + ```powershell + # Run installer as Admin to add firewall rule automatically + .\scripts\windows\pc-install.ps1 -Port 9000 + + # Or manually add rule in Windows Defender Firewall + ``` + +4. **Different Networks** + - **Check**: Ensure 3DS and PC are on the same network + - **Solution**: Connect both devices to the same Wi-Fi network + +5. **Wrong Port** + - **Check**: Server port matches (default: 9000) + - **Solution**: Use same port on both sides, or specify with `--port` flag + +**Verification Steps**: + +```bash +# On PC (Linux/Mac) +nc -l 9000 # Listen on port 9000 + +# On PC (Windows PowerShell) +Test-NetConnection -ComputerName localhost -Port 9000 + +# Try connecting from 3DS +# If connection fails, it's a network/firewall issue +``` + +### Connection Drops Frequently + +**Symptoms**: Stream starts but disconnects after a few seconds/minutes. + +**Possible Causes**: + +1. **Weak Wi-Fi Signal** + - **Solution**: Move 3DS closer to Wi-Fi router + - **Solution**: Reduce interference (microwave, cordless phones, etc.) + +2. **Network Congestion** + - **Solution**: Pause large downloads/uploads + - **Solution**: Use QoS on router to prioritize 3DS + +3. **PC Sleep/Screensaver** + - **Solution**: Disable sleep mode while streaming + - **Linux**: `sudo systemctl mask sleep.target suspend.target` + - **Windows**: Settings → Power & Sleep → Never + +4. **Timeout Issues** + - **Solution**: Keep 3DS active (prevent sleep) + - **Note**: Auto-reconnect should kick in after 3 seconds + +### "Connection Refused" Error + +**Symptoms**: Server logs show "Connection refused" or similar error. + +**Causes**: +- Port already in use +- Server crashed + +**Solutions**: + +```bash +# Linux: Check what's using port 9000 +sudo lsof -i :9000 +sudo netstat -tulpn | grep 9000 + +# Kill the process if needed +sudo kill + +# Windows: Check port usage +netstat -ano | findstr :9000 + +# Kill process by PID +taskkill /PID /F +``` + +--- + +## Virtual Camera Issues + +### Windows: "No Virtual Camera Found" + +**Symptoms**: Server fails to start or can't find OBS Virtual Camera. + +**Solutions**: + +1. **Install OBS Studio** + ```powershell + winget install -e --id OBSProject.OBSStudio + ``` + +2. **Initialize Virtual Camera** + - Open OBS Studio + - Go to **Tools → VirtualCam → Start** + - Wait for confirmation + - Close OBS (virtual camera stays active) + +3. **Verify Installation** + - Open Camera app (Windows built-in) + - Check if "OBS Virtual Camera" appears in device list + +4. **Restart Required** + - Sometimes requires system restart after OBS installation + +5. **pyvirtualcam Not Installed** + ```powershell + .venv\Scripts\Activate.ps1 + pip install pyvirtualcam + ``` + +### Linux: "/dev/video0: Device or Resource Busy" + +**Symptoms**: Server fails to open `/dev/video0`, "device busy" error. + +**Causes**: Another application is using the device. + +**Solutions**: + +1. **Find Processes Using Device** + ```bash + sudo fuser /dev/video0 + ``` + +2. **Close Applications** + - Close browser tabs with camera access + - Close Zoom, Discord, etc. + - Close any other camera applications + +3. **Force Kill Processes** + ```bash + sudo fuser -k /dev/video0 + ``` + +4. **Use Different Device** + ```bash + # List available video devices + v4l2-ctl --list-devices + + # Use different device + ./pc-server/run_server.sh --device /dev/video2 + ``` + +### Linux: "/dev/video0: No Such Device" + +**Symptoms**: v4l2loopback device doesn't exist. + +**Solutions**: + +1. **Load v4l2loopback Module** + ```bash + ./scripts/create-v4l2.sh + + # Or manually + sudo modprobe v4l2loopback devices=1 video_nr=0 card_label="3DS Webcam" exclusive_caps=1 + ``` + +2. **Verify Module Loaded** + ```bash + lsmod | grep v4l2loopback + v4l2-ctl --list-devices + ``` + +3. **Install v4l2loopback** + ```bash + sudo apt install v4l2loopback-dkms v4l2loopback-utils + + # Or run installer + ./scripts/pc-install.sh + ``` + +4. **Check Device Permissions** + ```bash + ls -l /dev/video* + + # Add user to video group if needed + sudo usermod -a -G video $USER + # Log out and back in for group to take effect + ``` + +### Virtual Camera Not Showing in Applications + +**Symptoms**: Applications (Zoom, Chrome, etc.) don't see the virtual camera. + +**Solutions**: + +1. **Verify Device Exists** + - **Linux**: `ls /dev/video*` + - **Windows**: Check Camera app + +2. **Restart Application** + - Some apps only detect cameras on launch + - Close and reopen the application + +3. **Check exclusive_caps** + ```bash + # Linux: Reload with exclusive_caps + sudo modprobe -r v4l2loopback + sudo modprobe v4l2loopback exclusive_caps=1 + ``` + +4. **Browser Permissions** + - Grant camera permission in browser settings + - Chrome: `chrome://settings/content/camera` + - Firefox: `about:preferences#privacy` → Permissions → Camera + +5. **Server Must Be Running** + - Virtual camera only works when server is actively sending frames + - Start server first, then open application + +--- + +## 3DS Application Issues + +### 3DS App Won't Launch / Crashes on Start + +**Symptoms**: App doesn't start or crashes immediately. + +**Solutions**: + +1. **Check Homebrew Launcher** + - Ensure Homebrew Launcher is up to date + - Try other homebrew apps to verify it's working + +2. **Verify File Placement** + ``` + SD:/3ds/3ds-webcam-bridge/3ds-webcam-bridge.3dsx + ``` + +3. **SD Card Issues** + - Check SD card is properly inserted + - Verify SD card is not corrupted + - Try different SD card if available + +4. **Insufficient Memory** + - Close other homebrew apps before launching + - Restart 3DS to clear memory + +5. **Build Issues** + - Rebuild the app: `./scripts/build-3ds.sh` + - Ensure stb_image_write.h is properly downloaded + +### Camera Initialization Failed + +**Symptoms**: App starts but shows camera initialization error. + +**Solutions**: + +1. **Close Other Apps** + - Some homebrew apps may hold camera resources + - Restart 3DS and try again + +2. **Try Different Camera** + - Press SELECT to switch between Inner/Outer + - One camera may work if the other doesn't + +3. **Camera Hardware Issue** + - Test camera with official Camera app + - If official app fails, hardware issue likely + +### 3D Mode Checkbox Disabled + +**Symptoms**: Can't enable 3D checkbox. + +**This is Normal**: The 3D checkbox is **intentionally disabled** when Inner camera is selected. + +**Reason**: The inner (front-facing) camera has only one lens and cannot provide stereo 3D. + +**Solution**: +1. Press **SELECT** to switch to Outer cameras +2. The 3D checkbox will become enabled +3. Press **X** to toggle 3D mode + +### Black Screen / No Image + +**Symptoms**: Stream connects but shows black screen on PC. + +**Solutions**: + +1. **Check Camera Lens** + - Ensure lens cover is removed + - Clean lens with soft cloth + +2. **Lighting Conditions** + - 3DS cameras need adequate lighting + - Try in brighter environment + +3. **Auto-Exposure Issues** + - Point camera at evenly-lit scene + - Avoid pointing directly at lights + +--- + +## Performance Issues + +### Low Frame Rate (< 10 FPS) + +**Symptoms**: Choppy/laggy stream, low FPS reported. + +**Solutions**: + +1. **Reduce Resolution** (requires rebuilding) + - Edit `3ds-app/source/camera_stream.h` + - Change `CAM_WIDTH` to 320 and rebuild + - Lower resolution = faster capture/encode + +2. **Lower JPEG Quality** + - Edit `3ds-app/source/camera_stream.h` + - Change `JPEG_QUALITY` from 65 to 50 + - Lower quality = faster encoding, less bandwidth + +3. **Network Optimization** + - Move 3DS closer to Wi-Fi router + - Use 5 GHz Wi-Fi for PC (3DS stays on 2.4 GHz) + - Reduce network congestion + +4. **Use Mono Mode** + - Disable 3D mode on 3DS + - Stereo mode requires 2× bandwidth and processing + +5. **PC Performance** + ```bash + # Check CPU usage + top # Linux + Task Manager # Windows + + # Close unnecessary applications + ``` + +### High Latency / Delay + +**Symptoms**: Noticeable delay between 3DS movement and PC display. + +**Expected Latency**: 100-200ms is normal due to: +- Camera capture: ~30ms +- JPEG encoding: ~40ms +- Network transmission: ~20-50ms +- JPEG decoding: ~10ms +- Display pipeline: ~20-40ms + +**To Reduce**: +1. Use wired Ethernet for PC +2. Minimize Wi-Fi interference +3. Lower JPEG quality (trades quality for speed) +4. Use mono mode instead of stereo + +### Stuttering / Frame Drops + +**Symptoms**: Uneven frame delivery, periodic freezes. + +**Solutions**: + +1. **Network Issues** + - Check Wi-Fi signal strength + - Move 3DS closer to router + - Change Wi-Fi channel to less congested one + +2. **CPU Throttling** + - 3DS may throttle when hot + - Let 3DS cool down + - Use in cooler environment + +3. **Bandwidth Saturation** + - Pause downloads/uploads + - Close streaming services + - Monitor network with `iftop` (Linux) or Task Manager (Windows) + +--- + +## Build Issues + +### "DEVKITARM is not set" + +**Symptoms**: Build fails with environment variable error. + +**Solution**: + +```bash +# Set environment variables +export DEVKITPRO=/opt/devkitpro +export DEVKITARM=$DEVKITPRO/devkitARM +export PATH=$DEVKITARM/bin:$PATH + +# Add to ~/.bashrc for persistence +echo 'export DEVKITPRO=/opt/devkitpro' >> ~/.bashrc +echo 'export DEVKITARM=$DEVKITPRO/devkitARM' >> ~/.bashrc +echo 'export PATH=$DEVKITARM/bin:$PATH' >> ~/.bashrc +source ~/.bashrc +``` + +### "stb_image_write.h: No Such File" + +**Symptoms**: Compilation fails, can't find stb_image_write.h. + +**Solution**: + +```bash +# Download stb_image_write.h +wget https://raw.githubusercontent.com/nothings/stb/master/stb_image_write.h \ + -O 3ds-app/source/third_party/stb_image_write.h + +# Or run build script (downloads automatically) +./scripts/build-3ds.sh +``` + +### "libctru Not Found" + +**Symptoms**: Linker errors, missing libctru symbols. + +**Solution**: + +```bash +# Install libctru +sudo dkp-pacman -S libctru + +# Or run full installer +./scripts/install-devkitpro.sh +``` + +### Build Succeeds But .3dsx Not Created + +**Symptoms**: Make completes but no output file. + +**Check**: + +```bash +# Look in dist directory +ls -la dist/ + +# Check for errors in make output +./scripts/build-3ds.sh 2>&1 | tee build.log +grep -i error build.log +``` + +**Solution**: +- Ensure `dist/` directory exists: `mkdir -p dist` +- Check Makefile paths +- Verify devkitARM installation + +--- + +## Platform-Specific Issues + +### Windows: "winget: command not found" + +**Symptoms**: Installer script fails, winget not available. + +**Solution**: + +1. **Install App Installer** from Microsoft Store +2. **Or download manually**: https://github.com/microsoft/winget-cli/releases +3. **Update Windows**: winget requires Windows 10 1809 or later + +### Windows: PowerShell Execution Policy Error + +**Symptoms**: Scripts won't run, "execution policy" error. + +**Solution**: + +```powershell +# Temporary (current session only) +Set-ExecutionPolicy Bypass -Scope Process -Force + +# Or run scripts with bypass flag +powershell -ExecutionPolicy Bypass -File .\scripts\windows\pc-install.ps1 +``` + +### Linux: Permission Denied on Scripts + +**Symptoms**: Can't execute `.sh` scripts. + +**Solution**: + +```bash +# Make scripts executable +chmod +x scripts/*.sh +chmod +x pc-server/run_server.sh + +# Or run with bash +bash scripts/build-3ds.sh +``` + +### Linux: "apt-get: command not found" + +**Symptoms**: Installer fails on non-Debian systems. + +**Solution** (for other distros): + +**Fedora/RHEL**: +```bash +sudo dnf install v4l2loopback akmod-v4l2loopback ffmpeg python3 python3-pip +``` + +**Arch**: +```bash +sudo pacman -S v4l2loopback-dkms ffmpeg python python-pip +``` + +Then manually create venv and install Python packages: +```bash +python3 -m venv .venv +source .venv/bin/activate +pip install -r pc-server/requirements.txt +``` + +--- + +## Diagnostic Commands + +### Check System Status + +**Linux**: +```bash +# Check v4l2loopback +lsmod | grep v4l2loopback +v4l2-ctl --list-devices + +# Check network +ip addr show +ss -tulpn | grep 9000 + +# Test virtual camera +ffplay /dev/video0 # Requires ffmpeg + +# Check Python environment +source .venv/bin/activate +python --version +pip list | grep -E "pyvirtualcam|pillow|numpy" +``` + +**Windows**: +```powershell +# Check OBS installation +Test-Path "C:\Program Files\obs-studio\bin\64bit\obs64.exe" + +# Check Python +python --version +pip list | findstr /I "pyvirtualcam pillow numpy" + +# Check network +ipconfig +netstat -ano | findstr :9000 + +# Test virtual camera +# Open Camera app and check device list +``` + +### Enable Verbose Logging + +```bash +# Linux +./pc-server/run_server.sh --verbose + +# Windows +.\pc-server\run_server.ps1 -Verbose +``` + +This will show detailed frame information: +- Frame type (mono/stereo) +- JPEG sizes +- Dimensions +- Processing time + +--- + +## Getting Help + +### Before Asking for Help + +1. **Check this guide** for your specific issue +2. **Review logs** with `--verbose` flag +3. **Verify requirements** (network, versions, etc.) +4. **Try basic troubleshooting** (restart, reconnect, etc.) + +### When Reporting Issues + +Include: + +1. **Platform Information** + - OS (Ubuntu 22.04, Windows 11, etc.) + - Python version: `python --version` + - 3DS model (Old 3DS, New 3DS XL, etc.) + +2. **Error Messages** + - Full error output + - Verbose logs if available + +3. **Steps to Reproduce** + - What were you doing when it failed? + - Can you reproduce it consistently? + +4. **Configuration** + - Command line arguments used + - Config file contents (if any) + - Network topology (router, switches, etc.) + +5. **What You've Tried** + - Troubleshooting steps already attempted + - Results of those attempts + +### Community Resources + +- **GitHub Issues**: https://github.com/your-username/3ds-webcam-bridge/issues +- **3DS Homebrew Community**: https://www.3dbrew.org/ +- **devkitPro Forums**: https://devkitpro.org/viewforum.php + +--- + +## Known Issues & Workarounds + +### Issue: Stereo Frames Slightly Desynchronized + +**Impact**: Left and right frames may be 1-2 frames apart. + +**Workaround**: None currently. This is due to sequential capture. Impact is minimal (<50ms difference). + +**Future Fix**: Hardware-synchronized capture (if possible via libctru extensions). + +### Issue: Windows Dual Mode Requires Manual OBS Config + +**Impact**: Can't easily create multiple virtual cameras on Windows. + +**Workaround**: +1. Use OBS plugins to create multiple virtual cameras +2. Or use side-by-side mode instead + +**Future Fix**: Investigate alternative virtual camera drivers for Windows. + +### Issue: 3DS Sleep Mode Disconnects + +**Impact**: 3DS entering sleep mode kills the stream. + +**Workaround**: Keep 3DS active, disable auto-sleep in system settings. + +**Future Fix**: Detect sleep and reconnect automatically. + +--- + +## Advanced Troubleshooting + +### Network Packet Capture + +**Linux**: +```bash +# Capture traffic on port 9000 +sudo tcpdump -i any -w capture.pcap port 9000 + +# Analyze with Wireshark +wireshark capture.pcap +``` + +**Windows**: +```powershell +# Use Wireshark directly +# Filter: tcp.port == 9000 +``` + +### Debugging Python Server + +```bash +# Run with Python debugger +python -m pdb pc-server/server.py --verbose + +# Or add breakpoints in code +import pdb; pdb.set_trace() +``` + +### Testing Without 3DS + +Create a test client to verify server: + +```python +import socket +import struct + +sock = socket.socket() +sock.connect(('localhost', 9000)) + +# Send mono frame header +header = b'\x00\x01' + struct.pack('>I', 1024) # 1KB test frame +sock.send(header) +sock.send(b'\xff\xd8' + b'\x00' * 1022 + b'\xff\xd9') # Fake JPEG + +sock.close() +``` + +--- + +**Last Updated**: 2025-10-28 diff --git a/examples/sample-config.txt b/examples/sample-config.txt new file mode 100644 index 0000000..20486e5 --- /dev/null +++ b/examples/sample-config.txt @@ -0,0 +1,11 @@ +# 3DS Webcam Bridge - Configuration File +# Place this file at: SD:/3ds/3ds-webcam-bridge/config.txt +# +# You can also edit IP/Port by pressing Y button in the app, +# which will automatically save to this file. + +# Server IP address (your PC's IP) +ip=192.168.1.100 + +# Server port +port=9000 diff --git a/pc-server/requirements.txt b/pc-server/requirements.txt new file mode 100644 index 0000000..3c0d5dd --- /dev/null +++ b/pc-server/requirements.txt @@ -0,0 +1,4 @@ +pillow>=10.0.0 +numpy>=1.24.0 +pyvirtualcam>=0.11.0 +pyfakewebcam>=0.1.0; platform_system == "Linux" diff --git a/pc-server/run_server.ps1 b/pc-server/run_server.ps1 new file mode 100644 index 0000000..93040ce --- /dev/null +++ b/pc-server/run_server.ps1 @@ -0,0 +1,55 @@ +# Run the 3DS Webcam Bridge PC server (Windows) +param( + [string]$Listen = "0.0.0.0", + [int]$Port = 9000, + [string]$StereoMode = "off", + [string]$Device = $null, + [string]$DualDevices = $null, + [int]$Width = 640, + [int]$Height = 480, + [int]$Fps = 15, + [string]$PreferBackend = "pyvirtualcam", + [switch]$Verbose +) + +$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$VenvDir = Join-Path (Split-Path -Parent $ScriptDir) ".venv" +$VenvActivate = Join-Path $VenvDir "Scripts\Activate.ps1" + +# Activate virtual environment if it exists +if (Test-Path $VenvActivate) { + Write-Host "Activating virtual environment..." + & $VenvActivate +} else { + Write-Warning "Virtual environment not found at $VenvDir" + Write-Warning "Run ..\scripts\windows\pc-install.ps1 first to set up dependencies" +} + +# Build command line arguments +$Args = @( + "--listen", $Listen, + "--port", $Port, + "--stereo-mode", $StereoMode, + "--width", $Width, + "--height", $Height, + "--fps", $Fps, + "--prefer-backend", $PreferBackend +) + +if ($Device) { + $Args += "--device" + $Args += $Device +} + +if ($DualDevices) { + $Args += "--dual-devices" + $Args += $DualDevices +} + +if ($Verbose) { + $Args += "--verbose" +} + +# Run server +$ServerScript = Join-Path $ScriptDir "server.py" +python $ServerScript @Args diff --git a/pc-server/run_server.sh b/pc-server/run_server.sh new file mode 100644 index 0000000..5559b4b --- /dev/null +++ b/pc-server/run_server.sh @@ -0,0 +1,16 @@ +#!/bin/bash +# Run the 3DS Webcam Bridge PC server (Linux) + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +VENV_DIR="$SCRIPT_DIR/../.venv" + +# Activate virtual environment if it exists +if [ -d "$VENV_DIR" ]; then + source "$VENV_DIR/bin/activate" +else + echo "Warning: Virtual environment not found at $VENV_DIR" + echo "Run ../scripts/pc-install.sh first to set up dependencies" +fi + +# Run server with provided arguments or defaults +python3 "$SCRIPT_DIR/server.py" "$@" diff --git a/pc-server/server.py b/pc-server/server.py new file mode 100644 index 0000000..ad831e6 --- /dev/null +++ b/pc-server/server.py @@ -0,0 +1,599 @@ +#!/usr/bin/env python3 +""" +3DS Webcam Bridge - PC Server +Receives camera streams from 3DS and exposes them as virtual webcams. +Supports mono and stereo modes with multiple output configurations. +""" + +import argparse +import io +import socket +import struct +import sys +import time +import platform +from typing import Optional, Tuple + +import numpy as np +from PIL import Image + +# Backend selection +BACKEND_PYVIRTUALCAM = "pyvirtualcam" +BACKEND_PYFAKEWEBCAM = "pyfakewebcam" +BACKEND_FFMPEG = "ffmpeg" + +# Protocol constants +PROTO_VERSION = 0x01 +PROTO_FLAG_STEREO = 0x01 + +# Global state +g_verbose = False + + +def log(msg): + """Print log message.""" + print(f"[INFO] {msg}") + + +def log_verbose(msg): + """Print verbose log message.""" + if g_verbose: + print(f"[VERBOSE] {msg}") + + +def log_error(msg): + """Print error message.""" + print(f"[ERROR] {msg}", file=sys.stderr) + + +def recv_all(conn: socket.socket, n: int) -> bytes: + """Receive exactly n bytes from socket.""" + data = b'' + while len(data) < n: + chunk = conn.recv(n - len(data)) + if not chunk: + raise ConnectionError("Connection closed by client") + data += chunk + return data + + +def read_frame(conn: socket.socket) -> Tuple[Optional[Image.Image], Optional[Image.Image]]: + """ + Read a frame from the 3DS. + Returns (left_image, right_image) where right_image is None for mono mode. + """ + # Read header + header = recv_all(conn, 2) + flags = header[0] + version = header[1] + + if version != PROTO_VERSION: + log_error(f"Unsupported protocol version: {version}") + return None, None + + stereo = (flags & PROTO_FLAG_STEREO) != 0 + + if not stereo: + # Mono frame: [4-byte length][JPEG] + length_bytes = recv_all(conn, 4) + length = struct.unpack('>I', length_bytes)[0] + + log_verbose(f"Receiving mono frame: {length} bytes") + + jpeg_data = recv_all(conn, length) + left_img = Image.open(io.BytesIO(jpeg_data)).convert('RGB') + + return left_img, None + else: + # Stereo frame: [2B wL][2B hL][2B wR][2B hR][4B left_len][4B right_len][left_jpeg][right_jpeg] + dims = recv_all(conn, 8) + wL, hL, wR, hR = struct.unpack('>HHHH', dims) + + lengths = recv_all(conn, 8) + left_len, right_len = struct.unpack('>II', lengths) + + log_verbose(f"Receiving stereo frame: left={wL}x{hL} ({left_len} bytes), right={wR}x{hR} ({right_len} bytes)") + + left_data = recv_all(conn, left_len) + right_data = recv_all(conn, right_len) + + left_img = Image.open(io.BytesIO(left_data)).convert('RGB') + right_img = Image.open(io.BytesIO(right_data)).convert('RGB') + + return left_img, right_img + + +def compose_sbs(left: Image.Image, right: Image.Image, width: int, height: int) -> np.ndarray: + """Compose side-by-side stereo image.""" + left_resized = left.resize((width, height), Image.BILINEAR) + right_resized = right.resize((width, height), Image.BILINEAR) + + left_arr = np.array(left_resized) + right_arr = np.array(right_resized) + + # Concatenate horizontally + sbs = np.concatenate([left_arr, right_arr], axis=1) + return sbs + + +def compose_anaglyph(left: Image.Image, right: Image.Image, width: int, height: int) -> np.ndarray: + """Compose red-cyan anaglyph image.""" + left_resized = left.resize((width, height), Image.BILINEAR) + right_resized = right.resize((width, height), Image.BILINEAR) + + left_arr = np.array(left_resized) + right_arr = np.array(right_resized) + + # Red-cyan anaglyph: red from left, green+blue from right + anaglyph = np.zeros_like(left_arr) + anaglyph[:, :, 0] = left_arr[:, :, 0] # Red from left + anaglyph[:, :, 1] = right_arr[:, :, 1] # Green from right + anaglyph[:, :, 2] = right_arr[:, :, 2] # Blue from right + + return anaglyph + + +class VirtualCameraBackend: + """Base class for virtual camera backends.""" + + def __init__(self, args): + self.args = args + + def send_frame(self, frame: np.ndarray): + """Send a frame to the virtual camera.""" + raise NotImplementedError + + def send_dual_frames(self, left: np.ndarray, right: np.ndarray): + """Send dual frames (for dual device mode).""" + raise NotImplementedError + + def close(self): + """Close the backend.""" + pass + + +class PyVirtualCamBackend(VirtualCameraBackend): + """Backend using pyvirtualcam.""" + + def __init__(self, args): + super().__init__(args) + + try: + import pyvirtualcam + self.pyvirtualcam = pyvirtualcam + except ImportError: + raise RuntimeError("pyvirtualcam is not installed. Install with: pip install pyvirtualcam") + + self.camera = None + self.camera_left = None + self.camera_right = None + + def _open_single_camera(self, width, height, fps, device=None): + """Open a single pyvirtualcam device.""" + kwargs = { + 'width': width, + 'height': height, + 'fps': fps, + 'print_fps': g_verbose + } + + if device is not None: + # Device can be a name (string) or index (int) + try: + device_int = int(device) + kwargs['device'] = device_int + except ValueError: + kwargs['device'] = device + + return self.pyvirtualcam.Camera(**kwargs) + + def setup_single(self): + """Setup single camera output.""" + width = self.args.width + height = self.args.height + + # For SBS mode, double the width + if self.args.stereo_mode == 'sbs': + width = width * 2 + + self.camera = self._open_single_camera( + width=width, + height=height, + fps=self.args.fps, + device=self.args.device + ) + log(f"Opened virtual camera: {width}x{height} @ {self.args.fps} FPS") + + def setup_dual(self): + """Setup dual camera output.""" + devices = self.args.dual_devices.split(',') + if len(devices) != 2: + raise ValueError("dual_devices must specify exactly 2 devices separated by comma") + + left_device = devices[0].strip() + right_device = devices[1].strip() + + self.camera_left = self._open_single_camera( + width=self.args.width, + height=self.args.height, + fps=self.args.fps, + device=left_device if left_device else None + ) + + self.camera_right = self._open_single_camera( + width=self.args.width, + height=self.args.height, + fps=self.args.fps, + device=right_device if right_device else None + ) + + log(f"Opened dual virtual cameras: {self.args.width}x{self.args.height} @ {self.args.fps} FPS") + + def send_frame(self, frame: np.ndarray): + """Send frame to single camera.""" + if self.camera is None: + self.setup_single() + self.camera.send(frame) + + def send_dual_frames(self, left: np.ndarray, right: np.ndarray): + """Send frames to dual cameras.""" + if self.camera_left is None or self.camera_right is None: + self.setup_dual() + self.camera_left.send(left) + self.camera_right.send(right) + + def close(self): + """Close cameras.""" + if self.camera: + self.camera.close() + if self.camera_left: + self.camera_left.close() + if self.camera_right: + self.camera_right.close() + + +class PyFakeWebcamBackend(VirtualCameraBackend): + """Backend using pyfakewebcam (Linux only).""" + + def __init__(self, args): + super().__init__(args) + + if platform.system() != 'Linux': + raise RuntimeError("pyfakewebcam is only available on Linux") + + try: + import pyfakewebcam + self.pyfakewebcam = pyfakewebcam + except ImportError: + raise RuntimeError("pyfakewebcam is not installed. Install with: pip install pyfakewebcam") + + self.camera = None + self.camera_left = None + self.camera_right = None + + def setup_single(self): + """Setup single camera output.""" + width = self.args.width + height = self.args.height + + if self.args.stereo_mode == 'sbs': + width = width * 2 + + device = self.args.device if self.args.device else '/dev/video0' + self.camera = self.pyfakewebcam.FakeWebcam(device, width, height) + log(f"Opened pyfakewebcam device: {device} ({width}x{height})") + + def setup_dual(self): + """Setup dual camera output.""" + devices = self.args.dual_devices.split(',') + if len(devices) != 2: + raise ValueError("dual_devices must specify exactly 2 devices separated by comma") + + self.camera_left = self.pyfakewebcam.FakeWebcam( + devices[0].strip(), self.args.width, self.args.height) + self.camera_right = self.pyfakewebcam.FakeWebcam( + devices[1].strip(), self.args.width, self.args.height) + log(f"Opened dual pyfakewebcam devices: {devices[0]}, {devices[1]}") + + def send_frame(self, frame: np.ndarray): + """Send frame to single camera.""" + if self.camera is None: + self.setup_single() + self.camera.schedule_frame(frame) + + def send_dual_frames(self, left: np.ndarray, right: np.ndarray): + """Send frames to dual cameras.""" + if self.camera_left is None or self.camera_right is None: + self.setup_dual() + self.camera_left.schedule_frame(left) + self.camera_right.schedule_frame(right) + + +class FFmpegBackend(VirtualCameraBackend): + """Backend using ffmpeg pipe to v4l2 (Linux only).""" + + def __init__(self, args): + super().__init__(args) + + if platform.system() != 'Linux': + raise RuntimeError("ffmpeg backend is only available on Linux") + + import subprocess + self.subprocess = subprocess + self.process = None + self.process_left = None + self.process_right = None + + def setup_single(self): + """Setup single ffmpeg output.""" + width = self.args.width + height = self.args.height + + if self.args.stereo_mode == 'sbs': + width = width * 2 + + device = self.args.device if self.args.device else '/dev/video0' + + cmd = [ + 'ffmpeg', + '-f', 'rawvideo', + '-pixel_format', 'rgb24', + '-video_size', f'{width}x{height}', + '-framerate', str(self.args.fps), + '-i', '-', + '-f', 'v4l2', + device + ] + + self.process = self.subprocess.Popen(cmd, stdin=self.subprocess.PIPE) + log(f"Opened ffmpeg pipe to {device} ({width}x{height})") + + def setup_dual(self): + """Setup dual ffmpeg output.""" + devices = self.args.dual_devices.split(',') + if len(devices) != 2: + raise ValueError("dual_devices must specify exactly 2 devices") + + cmd_template = [ + 'ffmpeg', + '-f', 'rawvideo', + '-pixel_format', 'rgb24', + '-video_size', f'{self.args.width}x{self.args.height}', + '-framerate', str(self.args.fps), + '-i', '-', + '-f', 'v4l2', + None # Device placeholder + ] + + cmd_left = cmd_template[:] + cmd_left[-1] = devices[0].strip() + self.process_left = self.subprocess.Popen(cmd_left, stdin=self.subprocess.PIPE) + + cmd_right = cmd_template[:] + cmd_right[-1] = devices[1].strip() + self.process_right = self.subprocess.Popen(cmd_right, stdin=self.subprocess.PIPE) + + log(f"Opened dual ffmpeg pipes to {devices[0]}, {devices[1]}") + + def send_frame(self, frame: np.ndarray): + """Send frame via ffmpeg.""" + if self.process is None: + self.setup_single() + self.process.stdin.write(frame.tobytes()) + self.process.stdin.flush() + + def send_dual_frames(self, left: np.ndarray, right: np.ndarray): + """Send frames via dual ffmpeg pipes.""" + if self.process_left is None or self.process_right is None: + self.setup_dual() + self.process_left.stdin.write(left.tobytes()) + self.process_left.stdin.flush() + self.process_right.stdin.write(right.tobytes()) + self.process_right.stdin.flush() + + def close(self): + """Close ffmpeg processes.""" + if self.process: + self.process.stdin.close() + self.process.wait() + if self.process_left: + self.process_left.stdin.close() + self.process_left.wait() + if self.process_right: + self.process_right.stdin.close() + self.process_right.wait() + + +def create_backend(args) -> VirtualCameraBackend: + """Create appropriate backend based on preference.""" + backend_name = args.prefer_backend + + if backend_name == BACKEND_PYVIRTUALCAM: + try: + return PyVirtualCamBackend(args) + except RuntimeError as e: + log_error(f"Failed to create pyvirtualcam backend: {e}") + if platform.system() == 'Linux': + log("Falling back to pyfakewebcam...") + backend_name = BACKEND_PYFAKEWEBCAM + else: + raise + + if backend_name == BACKEND_PYFAKEWEBCAM: + try: + return PyFakeWebcamBackend(args) + except RuntimeError as e: + log_error(f"Failed to create pyfakewebcam backend: {e}") + log("Falling back to ffmpeg...") + backend_name = BACKEND_FFMPEG + + if backend_name == BACKEND_FFMPEG: + return FFmpegBackend(args) + + raise RuntimeError(f"No suitable backend found") + + +def process_stream(conn: socket.socket, addr, args): + """Process incoming stream from 3DS.""" + log(f"Client connected from {addr}") + + backend = create_backend(args) + frame_count = 0 + start_time = time.time() + + try: + while True: + # Read frame from 3DS + left_img, right_img = read_frame(conn) + + if left_img is None: + break + + # Process based on mode + if right_img is None: + # Mono mode + log_verbose("Processing mono frame") + + left_resized = left_img.resize((args.width, args.height), Image.BILINEAR) + frame = np.array(left_resized) + + backend.send_frame(frame) + + else: + # Stereo mode + log_verbose("Processing stereo frame") + + if args.stereo_mode == 'off': + # Send left eye only + left_resized = left_img.resize((args.width, args.height), Image.BILINEAR) + frame = np.array(left_resized) + backend.send_frame(frame) + + elif args.stereo_mode == 'sbs': + # Side-by-side + frame = compose_sbs(left_img, right_img, args.width, args.height) + backend.send_frame(frame) + + elif args.stereo_mode == 'anaglyph': + # Red-cyan anaglyph + frame = compose_anaglyph(left_img, right_img, args.width, args.height) + backend.send_frame(frame) + + elif args.stereo_mode == 'dual': + # Dual devices + left_resized = left_img.resize((args.width, args.height), Image.BILINEAR) + right_resized = right_img.resize((args.width, args.height), Image.BILINEAR) + left_arr = np.array(left_resized) + right_arr = np.array(right_resized) + backend.send_dual_frames(left_arr, right_arr) + + frame_count += 1 + + if frame_count % 30 == 0: + elapsed = time.time() - start_time + fps = frame_count / elapsed + log(f"Processed {frame_count} frames ({fps:.1f} FPS)") + + except ConnectionError as e: + log_error(f"Connection error: {e}") + except KeyboardInterrupt: + log("Interrupted by user") + except Exception as e: + log_error(f"Error processing stream: {e}") + import traceback + traceback.print_exc() + finally: + backend.close() + log(f"Client disconnected: {addr}") + + +def main(): + global g_verbose + + parser = argparse.ArgumentParser( + description='3DS Webcam Bridge - PC Server', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + # Linux - mono mode with default settings + %(prog)s --device /dev/video0 + + # Linux - side-by-side stereo + %(prog)s --stereo-mode sbs --width 400 --height 240 --device /dev/video0 + + # Linux - dual devices for left/right eyes + %(prog)s --stereo-mode dual --dual-devices /dev/video0,/dev/video1 + + # Windows - side-by-side with OBS Virtual Camera + %(prog)s --stereo-mode sbs --device "OBS Virtual Camera" + + # Windows - dual OBS virtual cameras + %(prog)s --stereo-mode dual --dual-devices "OBS Virtual Camera,OBS Virtual Camera 2" + """ + ) + + parser.add_argument('--listen', default='0.0.0.0', + help='Listen address (default: 0.0.0.0)') + parser.add_argument('--port', type=int, default=9000, + help='Listen port (default: 9000)') + parser.add_argument('--mode', choices=['mono', 'auto'], default='auto', + help='Force mono or auto-detect (default: auto)') + parser.add_argument('--stereo-mode', choices=['off', 'sbs', 'anaglyph', 'dual'], + default='off', + help='Stereo output mode (default: off)') + parser.add_argument('--device', default=None, + help='Virtual camera device (Linux: /dev/videoX, Windows: device name or index)') + parser.add_argument('--dual-devices', default=None, + help='Dual devices for stereo (comma-separated)') + parser.add_argument('--width', type=int, default=640, + help='Output width per eye (default: 640)') + parser.add_argument('--height', type=int, default=480, + help='Output height (default: 480)') + parser.add_argument('--fps', type=int, default=15, + help='Output FPS (default: 15)') + parser.add_argument('--prefer-backend', + choices=[BACKEND_PYVIRTUALCAM, BACKEND_PYFAKEWEBCAM, BACKEND_FFMPEG], + default=BACKEND_PYVIRTUALCAM, + help='Preferred backend (default: pyvirtualcam)') + parser.add_argument('--verbose', action='store_true', + help='Verbose output') + + args = parser.parse_args() + g_verbose = args.verbose + + # Validate arguments + if args.stereo_mode == 'dual' and not args.dual_devices: + parser.error("--dual-devices is required when using --stereo-mode dual") + + # Create listening socket + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + + try: + sock.bind((args.listen, args.port)) + sock.listen(1) + log(f"Listening on {args.listen}:{args.port}") + log(f"Stereo mode: {args.stereo_mode}") + log(f"Backend: {args.prefer_backend}") + + while True: + log("Waiting for connection...") + conn, addr = sock.accept() + + try: + process_stream(conn, addr, args) + finally: + conn.close() + + except KeyboardInterrupt: + log("\nShutting down...") + except Exception as e: + log_error(f"Server error: {e}") + import traceback + traceback.print_exc() + finally: + sock.close() + + +if __name__ == '__main__': + main() diff --git a/scripts/build-3ds.sh b/scripts/build-3ds.sh new file mode 100644 index 0000000..9795913 --- /dev/null +++ b/scripts/build-3ds.sh @@ -0,0 +1,66 @@ +#!/bin/bash +# Build the 3DS webcam app + +set -e + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" +APP_DIR="$PROJECT_ROOT/3ds-app" +DIST_DIR="$PROJECT_ROOT/dist" + +echo "======================================" +echo "Building 3DS Webcam Bridge" +echo "======================================" +echo "" + +# Check for devkitARM +if [ -z "$DEVKITARM" ]; then + echo "Error: DEVKITARM is not set!" + echo "" + echo "Please run: export DEVKITARM=/opt/devkitpro/devkitARM" + echo "Or run: ./scripts/install-devkitpro.sh" + exit 1 +fi + +# Check for stb_image_write.h +STB_HEADER="$APP_DIR/source/third_party/stb_image_write.h" +if ! grep -q "stbi_write_jpg_to_func" "$STB_HEADER" 2>/dev/null || grep -q "PLACEHOLDER" "$STB_HEADER"; then + echo "Warning: stb_image_write.h appears to be a placeholder!" + echo "" + echo "Downloading the real stb_image_write.h..." + wget https://raw.githubusercontent.com/nothings/stb/master/stb_image_write.h \ + -O "$STB_HEADER" 2>/dev/null || { + echo "" + echo "Error: Could not download stb_image_write.h" + echo "Please download it manually from:" + echo " https://raw.githubusercontent.com/nothings/stb/master/stb_image_write.h" + echo "And place it at:" + echo " $STB_HEADER" + exit 1 + } + echo "Downloaded successfully!" + echo "" +fi + +# Create dist directory +mkdir -p "$DIST_DIR" + +# Build +echo "Building..." +cd "$APP_DIR" +make clean +make -j$(nproc) + +echo "" +echo "======================================" +echo "Build complete!" +echo "======================================" +echo "" +echo "Output files:" +ls -lh "$DIST_DIR"/3ds-webcam-bridge.* +echo "" +echo "To install on your 3DS:" +echo " 1. Copy $DIST_DIR/3ds-webcam-bridge.3dsx to your SD card at:" +echo " SD:/3ds/3ds-webcam-bridge/" +echo " 2. Launch via Homebrew Launcher" +echo "" diff --git a/scripts/create-v4l2.sh b/scripts/create-v4l2.sh new file mode 100644 index 0000000..404633a --- /dev/null +++ b/scripts/create-v4l2.sh @@ -0,0 +1,82 @@ +#!/bin/bash +# Create v4l2loopback virtual camera device(s) + +set -e + +DUAL=false + +# Parse arguments +while [[ $# -gt 0 ]]; do + case $1 in + --dual) + DUAL=true + shift + ;; + *) + echo "Unknown option: $1" + echo "Usage: $0 [--dual]" + exit 1 + ;; + esac +done + +echo "======================================" +echo "v4l2loopback Device Setup" +echo "======================================" +echo "" + +# Check if v4l2loopback is installed +if ! modinfo v4l2loopback &> /dev/null; then + echo "Error: v4l2loopback is not installed!" + echo "Run: ./scripts/pc-install.sh" + exit 1 +fi + +# Unload existing module if loaded +if lsmod | grep -q v4l2loopback; then + echo "Unloading existing v4l2loopback module..." + sudo modprobe -r v4l2loopback +fi + +# Load module with appropriate parameters +if [ "$DUAL" = true ]; then + echo "Creating dual virtual camera devices..." + sudo modprobe v4l2loopback \ + devices=2 \ + video_nr=0,1 \ + card_label="3DS Webcam Left","3DS Webcam Right" \ + exclusive_caps=1,1 + + echo "" + echo "Created devices:" + echo " /dev/video0 - 3DS Webcam Left" + echo " /dev/video1 - 3DS Webcam Right" + echo "" + echo "To use with dual mode:" + echo " ./pc-server/run_server.sh --stereo-mode dual --dual-devices /dev/video0,/dev/video1" +else + echo "Creating single virtual camera device..." + sudo modprobe v4l2loopback \ + devices=1 \ + video_nr=0 \ + card_label="3DS Webcam" \ + exclusive_caps=1 + + echo "" + echo "Created device:" + echo " /dev/video0 - 3DS Webcam" + echo "" + echo "To use:" + echo " ./pc-server/run_server.sh --device /dev/video0" +fi + +echo "" +echo "Verify with: v4l2-ctl --list-devices" +echo "" +echo "To make this persistent across reboots, add to /etc/modules-load.d/v4l2loopback.conf:" +if [ "$DUAL" = true ]; then + echo " options v4l2loopback devices=2 video_nr=0,1 card_label=\"3DS Webcam Left\",\"3DS Webcam Right\" exclusive_caps=1,1" +else + echo " options v4l2loopback devices=1 video_nr=0 card_label=\"3DS Webcam\" exclusive_caps=1" +fi +echo "" diff --git a/scripts/install-devkitpro.sh b/scripts/install-devkitpro.sh new file mode 100644 index 0000000..c06bbf8 --- /dev/null +++ b/scripts/install-devkitpro.sh @@ -0,0 +1,55 @@ +#!/bin/bash +# Install devkitPro toolchain for 3DS development (Debian/Ubuntu) + +set -e + +echo "======================================" +echo "devkitPro Toolchain Installer" +echo "======================================" +echo "" + +# Check if already installed +if command -v dkp-pacman &> /dev/null; then + echo "devkitPro is already installed!" + echo "devkitARM location: $DEVKITARM" + echo "" + echo "To update, run: sudo dkp-pacman -Syu" + exit 0 +fi + +echo "This script will install devkitPro and the 3DS development tools." +echo "You may be prompted for your password (sudo)." +echo "" + +# Download and install devkitPro pacman +echo "Downloading devkitPro pacman package..." +wget https://github.com/devkitPro/pacman/releases/latest/download/devkitpro-pacman.amd64.deb -O /tmp/devkitpro-pacman.deb + +echo "Installing devkitPro pacman..." +sudo dpkg -i /tmp/devkitpro-pacman.deb || sudo apt-get install -f -y + +rm /tmp/devkitpro-pacman.deb + +# Install devkitARM and libraries +echo "" +echo "Installing devkitARM and 3DS libraries..." +sudo dkp-pacman -S --noconfirm devkitARM libctru citro3d 3dstools + +echo "" +echo "======================================" +echo "Installation complete!" +echo "======================================" +echo "" +echo "IMPORTANT: Add these lines to your ~/.bashrc or ~/.zshrc:" +echo "" +echo " export DEVKITPRO=/opt/devkitpro" +echo " export DEVKITARM=\$DEVKITPRO/devkitARM" +echo " export PATH=\$DEVKITARM/bin:\$PATH" +echo "" +echo "Then run: source ~/.bashrc" +echo "" +echo "Or run these commands now:" +echo " export DEVKITPRO=/opt/devkitpro" +echo " export DEVKITARM=\$DEVKITPRO/devkitARM" +echo " export PATH=\$DEVKITARM/bin:\$PATH" +echo "" diff --git a/scripts/pc-install.sh b/scripts/pc-install.sh new file mode 100644 index 0000000..785d003 --- /dev/null +++ b/scripts/pc-install.sh @@ -0,0 +1,69 @@ +#!/bin/bash +# Install PC server dependencies (Linux - Debian/Ubuntu) + +set -e + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" +VENV_DIR="$PROJECT_ROOT/.venv" + +echo "======================================" +echo "PC Server Installer (Linux)" +echo "======================================" +echo "" + +# Update package list +echo "Updating package list..." +sudo apt-get update + +# Install system dependencies +echo "" +echo "Installing system dependencies..." +sudo apt-get install -y \ + v4l2loopback-dkms \ + v4l2loopback-utils \ + ffmpeg \ + python3 \ + python3-venv \ + python3-pip \ + python3-dev \ + build-essential + +# Create virtual environment +echo "" +echo "Creating Python virtual environment..." +if [ -d "$VENV_DIR" ]; then + echo "Virtual environment already exists, updating..." +else + python3 -m venv "$VENV_DIR" +fi + +# Activate and install Python packages +echo "" +echo "Installing Python packages..." +source "$VENV_DIR/bin/activate" +pip install --upgrade pip +pip install -r "$PROJECT_ROOT/pc-server/requirements.txt" + +echo "" +echo "======================================" +echo "Installation complete!" +echo "======================================" +echo "" +echo "Next steps:" +echo "" +echo "1. Create v4l2loopback device(s):" +echo " For single device:" +echo " ./scripts/create-v4l2.sh" +echo " For dual devices (stereo):" +echo " ./scripts/create-v4l2.sh --dual" +echo "" +echo "2. Run the server:" +echo " ./pc-server/run_server.sh --device /dev/video0" +echo "" +echo "3. For stereo side-by-side:" +echo " ./pc-server/run_server.sh --stereo-mode sbs --width 400 --height 240" +echo "" +echo "4. For dual devices:" +echo " ./pc-server/run_server.sh --stereo-mode dual --dual-devices /dev/video0,/dev/video1" +echo "" diff --git a/scripts/windows/helpers.psm1 b/scripts/windows/helpers.psm1 new file mode 100644 index 0000000..b27f712 --- /dev/null +++ b/scripts/windows/helpers.psm1 @@ -0,0 +1,104 @@ +# Helper functions for Windows installation + +function Test-WingetInstalled { + <# + .SYNOPSIS + Check if winget is installed + #> + try { + $null = Get-Command winget -ErrorAction Stop + return $true + } catch { + return $false + } +} + +function Test-CommandInstalled { + <# + .SYNOPSIS + Check if a command is available + #> + param( + [string]$Command + ) + + try { + $null = Get-Command $Command -ErrorAction Stop + return $true + } catch { + return $false + } +} + +function Install-WithWinget { + <# + .SYNOPSIS + Install a package using winget + #> + param( + [string]$PackageId, + [string]$Name + ) + + Write-Host "Installing $Name..." -ForegroundColor Cyan + $result = winget install -e --id $PackageId --accept-package-agreements --accept-source-agreements + + if ($LASTEXITCODE -eq 0) { + Write-Host "$Name installed successfully!" -ForegroundColor Green + return $true + } else { + Write-Warning "$Name installation may have failed. Exit code: $LASTEXITCODE" + return $false + } +} + +function Add-FirewallRule { + <# + .SYNOPSIS + Add a Windows Defender Firewall rule for the server + #> + param( + [int]$Port, + [string]$RuleName = "3DS Webcam Bridge" + ) + + Write-Host "Checking firewall rules..." -ForegroundColor Cyan + + # Check if rule already exists + $existingRule = Get-NetFirewallRule -DisplayName $RuleName -ErrorAction SilentlyContinue + + if ($existingRule) { + Write-Host "Firewall rule already exists." -ForegroundColor Yellow + return $true + } + + try { + New-NetFirewallRule ` + -DisplayName $RuleName ` + -Direction Inbound ` + -Protocol TCP ` + -LocalPort $Port ` + -Action Allow ` + -Profile Private,Domain ` + -ErrorAction Stop | Out-Null + + Write-Host "Firewall rule added successfully!" -ForegroundColor Green + return $true + } catch { + Write-Warning "Failed to add firewall rule: $_" + Write-Warning "You may need to manually add a firewall rule for port $Port" + return $false + } +} + +function Test-IsAdmin { + <# + .SYNOPSIS + Check if running as administrator + #> + $currentUser = [Security.Principal.WindowsIdentity]::GetCurrent() + $principal = New-Object Security.Principal.WindowsPrincipal($currentUser) + return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) +} + +Export-ModuleMember -Function Test-WingetInstalled, Test-CommandInstalled, Install-WithWinget, Add-FirewallRule, Test-IsAdmin diff --git a/scripts/windows/pc-install.ps1 b/scripts/windows/pc-install.ps1 new file mode 100644 index 0000000..4ad9557 --- /dev/null +++ b/scripts/windows/pc-install.ps1 @@ -0,0 +1,160 @@ +# PC Server Installer for Windows 10/11 +# This script installs all dependencies for the 3DS Webcam Bridge PC server + +param( + [int]$Port = 9000, + [switch]$SkipFirewall, + [switch]$Force +) + +$ErrorActionPreference = "Stop" + +$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$ProjectRoot = Split-Path -Parent (Split-Path -Parent $ScriptDir) +$HelpersModule = Join-Path $ScriptDir "helpers.psm1" + +# Import helpers +Import-Module $HelpersModule -Force + +Write-Host "======================================" -ForegroundColor Cyan +Write-Host "3DS Webcam Bridge - PC Server Installer" -ForegroundColor Cyan +Write-Host "Windows 10/11" -ForegroundColor Cyan +Write-Host "======================================" -ForegroundColor Cyan +Write-Host "" + +# Check for admin privileges (required for some operations) +$isAdmin = Test-IsAdmin +if (-not $isAdmin) { + Write-Warning "Not running as Administrator. Some features may require admin privileges." + Write-Warning "To enable firewall rules and ensure full functionality, run as Administrator." + Write-Host "" + $response = Read-Host "Continue anyway? (y/n)" + if ($response -ne 'y') { + exit 1 + } +} + +# Check for winget +Write-Host "Checking for winget..." -ForegroundColor Cyan +if (-not (Test-WingetInstalled)) { + Write-Host "" + Write-Host "ERROR: winget is not installed!" -ForegroundColor Red + Write-Host "" + Write-Host "winget is required to install dependencies automatically." + Write-Host "Please install 'App Installer' from the Microsoft Store, or" + Write-Host "download it from:" + Write-Host " https://github.com/microsoft/winget-cli/releases" + Write-Host "" + exit 1 +} +Write-Host "winget found!" -ForegroundColor Green +Write-Host "" + +# Install Python 3 +Write-Host "Checking for Python 3..." -ForegroundColor Cyan +if (-not (Test-CommandInstalled python) -and -not (Test-CommandInstalled python3)) { + Write-Host "Python 3 not found. Installing..." -ForegroundColor Yellow + Install-WithWinget -PackageId "Python.Python.3.11" -Name "Python 3.11" + + # Refresh PATH + $env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User") + + Write-Host "" + Write-Host "NOTE: You may need to restart your terminal for Python to be available in PATH" -ForegroundColor Yellow + Write-Host "" +} else { + Write-Host "Python 3 is already installed!" -ForegroundColor Green + $pythonVersion = python --version 2>&1 + Write-Host " $pythonVersion" -ForegroundColor Gray +} +Write-Host "" + +# Install OBS Studio (for Virtual Camera) +Write-Host "Checking for OBS Studio..." -ForegroundColor Cyan +$obsInstalled = Test-Path "C:\Program Files\obs-studio\bin\64bit\obs64.exe" +if (-not $obsInstalled -or $Force) { + Write-Host "OBS Studio not found. Installing..." -ForegroundColor Yellow + Write-Host "" + Write-Host "OBS Studio provides the Virtual Camera that pyvirtualcam uses on Windows." -ForegroundColor Yellow + Write-Host "" + Install-WithWinget -PackageId "OBSProject.OBSStudio" -Name "OBS Studio" + Write-Host "" +} else { + Write-Host "OBS Studio is already installed!" -ForegroundColor Green +} +Write-Host "" + +# Create virtual environment +$VenvDir = Join-Path $ProjectRoot ".venv" +Write-Host "Setting up Python virtual environment..." -ForegroundColor Cyan + +if (Test-Path $VenvDir) { + if ($Force) { + Write-Host "Removing existing virtual environment..." -ForegroundColor Yellow + Remove-Item -Recurse -Force $VenvDir + } else { + Write-Host "Virtual environment already exists. Updating..." -ForegroundColor Yellow + } +} + +if (-not (Test-Path $VenvDir)) { + Write-Host "Creating virtual environment..." -ForegroundColor Cyan + python -m venv $VenvDir +} + +# Activate virtual environment and install packages +$ActivateScript = Join-Path $VenvDir "Scripts\Activate.ps1" +Write-Host "Installing Python packages..." -ForegroundColor Cyan + +& $ActivateScript + +$RequirementsFile = Join-Path $ProjectRoot "pc-server\requirements.txt" +python -m pip install --upgrade pip +python -m pip install -r $RequirementsFile + +Write-Host "Python packages installed successfully!" -ForegroundColor Green +Write-Host "" + +# Add firewall rule +if (-not $SkipFirewall -and $isAdmin) { + Add-FirewallRule -Port $Port + Write-Host "" +} + +Write-Host "======================================" -ForegroundColor Green +Write-Host "Installation Complete!" -ForegroundColor Green +Write-Host "======================================" -ForegroundColor Green +Write-Host "" + +Write-Host "Next Steps:" -ForegroundColor Cyan +Write-Host "" +Write-Host "1. Start OBS Studio at least once to initialize the Virtual Camera:" -ForegroundColor White +Write-Host " - Open OBS Studio" -ForegroundColor Gray +Write-Host " - Go to Tools -> VirtualCam -> Start" -ForegroundColor Gray +Write-Host " - You can close OBS after this" -ForegroundColor Gray +Write-Host "" + +Write-Host "2. Run the server:" -ForegroundColor White +Write-Host " .\pc-server\run_server.ps1 -Port $Port" -ForegroundColor Gray +Write-Host "" + +Write-Host "3. For stereo side-by-side mode:" -ForegroundColor White +Write-Host " .\pc-server\run_server.ps1 -StereoMode sbs -Width 400 -Height 240" -ForegroundColor Gray +Write-Host "" + +Write-Host "4. To specify a device:" -ForegroundColor White +Write-Host " .\pc-server\run_server.ps1 -Device `"OBS Virtual Camera`"" -ForegroundColor Gray +Write-Host "" + +Write-Host "5. For dual devices (if you have multiple virtual cameras set up):" -ForegroundColor White +Write-Host " .\pc-server\run_server.ps1 -StereoMode dual -DualDevices `"OBS Virtual Camera,OBS Virtual Camera 2`"" -ForegroundColor Gray +Write-Host "" + +if (-not $SkipFirewall -and -not $isAdmin) { + Write-Warning "Firewall rule was not added (requires admin privileges)." + Write-Warning "If you have connection issues, manually allow TCP port $Port in Windows Defender Firewall." +} + +Write-Host "" +Write-Host "For more information, see README.md" -ForegroundColor Cyan +Write-Host ""