From 1b7592e9b7a81e40fe09dbef54a0ebb220aee5eb Mon Sep 17 00:00:00 2001 From: Jurn Wubben Date: Sun, 28 Sep 2025 12:53:48 +0200 Subject: [PATCH] [BROKEN] playback works, taking a pic doesn't and it's 15fps --- Makefile | 2 +- src/main.c | 603 +++++++++++++++++++---------------------------------- 2 files changed, 219 insertions(+), 386 deletions(-) diff --git a/Makefile b/Makefile index 9cfdee9..70caab2 100644 --- a/Makefile +++ b/Makefile @@ -58,7 +58,7 @@ CXXFLAGS := $(CFLAGS) -fno-rtti -fno-exceptions -std=gnu++14 ASFLAGS := -g $(ARCH) LDFLAGS = -specs=3dsx.specs -g $(ARCH) -Wl,-Map,$(notdir $*.map) -LIBS := -lbox2d -lcitro2d -lcitro3d -ljpeg -lctru -lm +LIBS := -lbox2d -lcitro2d -lcitro3d -lctru -lm -ljpeg -lcurl #--------------------------------------------------------------------------------- # list of directories containing libraries, this must be the top level containing diff --git a/src/main.c b/src/main.c index b964fe5..cd034fe 100644 --- a/src/main.c +++ b/src/main.c @@ -1,414 +1,247 @@ -#include - #include <3ds.h> -#include -#include -#include -#include #include -#include +#include +#include +#include #include #include -#include -#include +#include +#include +#include + +#define WAIT_TIMEOUT 1000000000ULL #define WIDTH 400 #define HEIGHT 240 -#define FB_SIZE (WIDTH * HEIGHT * 3) -#define STORAGE_DIR "sdmc:/printcam" -#define ENDPOINT_PATH STORAGE_DIR "/endpoint.txt" -#define API_ENDPOINT_DEFAULT "https://example.com/upload" -#define JPEG_QUALITY 100 -static volatile bool exitRequested = false; -static Thread camThread = NULL; -static Handle ready[2] = {0, 0}; -static u8 *camBuf[2] = {NULL, NULL}; -static u8 *rgbBuf = NULL; -static volatile u8 writeIdx = 0, dispIdx = 0; -static volatile bool camera_reinit = false; -static volatile int camera_index = 0; -static const u32 cam_selects[2] = {SELECT_OUT1, SELECT_IN1}; -static char api_endpoint[512] = API_ENDPOINT_DEFAULT; -static PrintConsole bottomScreen; +#define PIC_WIDTH 640 +#define PIC_HEIGHT 480 -static bool ensure_storage_dir(void) { - if (mkdir(STORAGE_DIR, 0777) == 0) - return true; - return errno == EEXIST; +#define SCREEN_SIZE WIDTH *HEIGHT * 2 +#define PIC_SIZE PIC_WIDTH *PIC_HEIGHT * 2 +#define BUF_SIZE SCREEN_SIZE * 2 + +#define CAMS SELECT_IN1_OUT1 +#define PORT PORT_CAM1 + +static jmp_buf exitJmp; +bool activeCam = false; + +Result getActiveSelect() { return activeCam ? SELECT_IN1 : SELECT_OUT1; }; + +inline void clearScreen(void) { + u8 *frame = gfxGetFramebuffer(GFX_BOTTOM, GFX_LEFT, NULL, NULL); + memset(frame, 0, 320 * 240 * 3); } -static bool ensure_default_endpoint_file(void) { - FILE *f = fopen(ENDPOINT_PATH, "r"); - if (f) { - fclose(f); - return true; - } - f = fopen(ENDPOINT_PATH, "w"); - if (!f) - return false; - if (fprintf(f, "%s\n", API_ENDPOINT_DEFAULT) < 0) { - fclose(f); - return false; - } - fclose(f); - return true; -} +void hang(char *message) { + clearScreen(); + printf("%s", message); + printf("Press start to exit"); -static int load_endpoint_from_sd(void) { - FILE *f = fopen(ENDPOINT_PATH, "r"); - if (!f) - return -1; - if (!fgets(api_endpoint, sizeof(api_endpoint), f)) { - fclose(f); - return -1; - } - size_t len = strlen(api_endpoint); - while (len && - (api_endpoint[len - 1] == '\n' || api_endpoint[len - 1] == '\r')) { - api_endpoint[len - 1] = '\0'; - --len; - } - fclose(f); - return 0; -} + while (aptMainLoop()) { + hidScanInput(); -static inline void writePix(u8 *dst, int x, int y, u8 r, u8 g, u8 b) { - int off = (x * HEIGHT + (HEIGHT - 1 - y)) * 3; - dst[off] = b; - dst[off + 1] = g; - dst[off + 2] = r; -} - -static void blitYUYVtoRGB(const u8 *src, u8 *dst) { - for (int y = 0; y < HEIGHT; ++y) { - for (int x = 0; x < WIDTH; x += 2) { - u8 y0 = src[0], u = src[1], y1 = src[2], v = src[3]; - src += 4; - int8_t u2 = (int8_t)(u - 128), v2 = (int8_t)(v - 128); - int r = (int)(1.402f * v2), g = (int)(-0.344f * u2 - 0.714f * v2), - b = (int)(1.772f * u2); - int R = y0 + r, G = y0 + g, B = y0 + b; - R = R < 0 ? 0 : (R > 255 ? 255 : R); - G = G < 0 ? 0 : (G > 255 ? 255 : G); - B = B < 0 ? 0 : (B > 255 ? 255 : B); - writePix(dst, x, y, (u8)R, (u8)G, (u8)B); - R = y1 + r; - G = y1 + g; - B = y1 + b; - R = R < 0 ? 0 : (R > 255 ? 255 : R); - G = G < 0 ? 0 : (G > 255 ? 255 : G); - B = B < 0 ? 0 : (B > 255 ? 255 : B); - writePix(dst, x + 1, y, (u8)R, (u8)G, (u8)B); - } + u32 kHeld = hidKeysHeld(); + if (kHeld & KEY_START) + longjmp(exitJmp, 1); } } -static void convert_display_to_row_major(const u8 *disp, u8 *out) { - for (int y = 0; y < HEIGHT; ++y) { - for (int x = 0; x < WIDTH; ++x) { - int disp_off = (x * HEIGHT + (HEIGHT - 1 - y)) * 3; - int out_off = (y * WIDTH + x) * 3; - out[out_off + 0] = disp[disp_off + 0]; - out[out_off + 1] = disp[disp_off + 1]; - out[out_off + 2] = disp[disp_off + 2]; - } - } -} - -static int encode_bgr_to_jpeg_file(const u8 *in_bgr, int width, int height, - int quality, const char *path) { - struct jpeg_compress_struct cinfo; - struct jpeg_error_mgr jerr; - FILE *f = NULL; - unsigned char *row = NULL; - int row_stride = width * 3; - if (!in_bgr || !path) - return -1; - f = fopen(path, "wb"); - if (!f) - return -1; - cinfo.err = jpeg_std_error(&jerr); - jpeg_create_compress(&cinfo); - jpeg_stdio_dest(&cinfo, f); - cinfo.image_width = width; - cinfo.image_height = height; - cinfo.input_components = 3; - cinfo.in_color_space = JCS_RGB; - jpeg_set_defaults(&cinfo); - jpeg_set_quality(&cinfo, quality, TRUE); - jpeg_start_compress(&cinfo, TRUE); - row = malloc(row_stride); - if (!row) { - jpeg_finish_compress(&cinfo); - jpeg_destroy_compress(&cinfo); - fclose(f); - return -1; - } - while (cinfo.next_scanline < cinfo.image_height) { - const u8 *src = in_bgr + (size_t)cinfo.next_scanline * (size_t)row_stride; - for (int x = 0; x < width; ++x) { - row[x * 3 + 0] = src[x * 3 + 2]; - row[x * 3 + 1] = src[x * 3 + 1]; - row[x * 3 + 2] = src[x * 3 + 0]; - } - JSAMPROW row_pointer[1]; - row_pointer[0] = row; - jpeg_write_scanlines(&cinfo, row_pointer, 1); - } - free(row); - jpeg_finish_compress(&cinfo); - jpeg_destroy_compress(&cinfo); - fclose(f); - return 0; -} - -Result http_post_file(const char *url, const char *filename) { - Result ret = 0; - httpcContext context; - u32 statuscode = 0; - FILE *f = fopen(filename, "rb"); - if (!f) - return -1; - if (fseek(f, 0, SEEK_END) != 0) { - fclose(f); - return -1; - } - long fsize = ftell(f); - if (fsize < 0) { - fclose(f); - return -1; - } - rewind(f); - u8 *filebuf = malloc((size_t)fsize); - if (!filebuf) { - fclose(f); - return -1; - } - if (fread(filebuf, 1, (size_t)fsize, f) != (size_t)fsize) { - free(filebuf); - fclose(f); - return -1; - } - fclose(f); - char boundary[64]; - srand((unsigned)time(NULL)); - snprintf(boundary, sizeof(boundary), "----printcam%08x", rand()); - const char *filename_only = filename; - const char *p = strrchr(filename, '/'); - if (p) - filename_only = p + 1; - char preamble[512]; - int pre_len = snprintf( - preamble, sizeof(preamble), - "--%s\r\n" - "Content-Disposition: form-data; name=\"file\"; filename=\"%s\"\r\n" - "Content-Type: image/jpeg\r\n\r\n", - boundary, filename_only); - const char *epilogue_fmt = "\r\n--%s--\r\n"; - int epilogue_len = snprintf(NULL, 0, epilogue_fmt, boundary); - char *epilogue = malloc((size_t)epilogue_len + 1); - if (!epilogue) { - free(filebuf); - return -1; - } - snprintf(epilogue, (size_t)epilogue_len + 1, epilogue_fmt, boundary); - size_t multipart_size = - (size_t)pre_len + (size_t)fsize + (size_t)epilogue_len; - u8 *multipart_buf = malloc(multipart_size); - if (!multipart_buf) { - free(filebuf); - free(epilogue); - return -1; - } - memcpy(multipart_buf, preamble, (size_t)pre_len); - memcpy(multipart_buf + pre_len, filebuf, (size_t)fsize); - memcpy(multipart_buf + pre_len + fsize, epilogue, (size_t)epilogue_len); - free(filebuf); - free(epilogue); - ret = httpcOpenContext(&context, HTTPC_METHOD_POST, url, 0); - if (ret != 0) { - free(multipart_buf); - return ret; - } - httpcSetSSLOpt(&context, SSLCOPT_DisableVerify); - httpcSetKeepAlive(&context, HTTPC_KEEPALIVE_ENABLED); - char content_type_header[128]; - snprintf(content_type_header, sizeof(content_type_header), - "multipart/form-data; boundary=%s", boundary); - httpcAddRequestHeaderField(&context, "User-Agent", "printcam/1.0"); - httpcAddRequestHeaderField(&context, "Content-Type", content_type_header); - ret = - httpcAddPostDataRaw(&context, (u32 *)multipart_buf, (u32)multipart_size); - if (ret != 0) { - httpcCloseContext(&context); - free(multipart_buf); - return ret; - } - ret = httpcBeginRequest(&context); - if (ret != 0) { - httpcCloseContext(&context); - free(multipart_buf); - return ret; - } - ret = httpcGetResponseStatusCode(&context, &statuscode); - if (ret == 0) { - u32 downloaded = 0; - u32 contentsize = 0; - while (true) { - ret = httpcGetDownloadSizeState(&context, &downloaded, &contentsize); - if (ret != 0) - break; - if (contentsize == 0) - break; - if (downloaded >= contentsize) - break; - u32 remaining = contentsize - downloaded; - u32 toread = remaining > 4096 ? 4096 : remaining; - u8 tmp[4096]; - ret = httpcReceiveData(&context, tmp, toread); - if (ret != 0) - break; - } - } - httpcCloseContext(&context); - free(multipart_buf); - return ret; -} - -static void cameraThreadFunc(void *arg) { - acInit(); - camInit(); - u32 sel = cam_selects[camera_index]; - CAMU_SetSize(sel, SIZE_CTR_TOP_LCD, CONTEXT_A); - CAMU_SetOutputFormat(sel, OUTPUT_YUV_422, CONTEXT_A); - CAMU_SetFrameRate(sel, FRAME_RATE_30); - CAMU_SetNoiseFilter(sel, true); - CAMU_SetAutoExposure(sel, true); - CAMU_SetAutoWhiteBalance(sel, true); - u32 maxBytes; - CAMU_GetMaxBytes(&maxBytes, WIDTH, HEIGHT); - CAMU_SetTransferBytes(PORT_BOTH, maxBytes, WIDTH, HEIGHT); - CAMU_Activate(sel); - Handle ev = 0; - CAMU_ClearBuffer(PORT_BOTH); - CAMU_SynchronizeVsyncTiming(SELECT_OUT1, SELECT_OUT2); - CAMU_StartCapture(PORT_BOTH); - while (!exitRequested) { - if (camera_reinit) { - sel = cam_selects[camera_index]; - CAMU_StopCapture(PORT_BOTH); - CAMU_ClearBuffer(PORT_BOTH); - CAMU_SetSize(sel, SIZE_CTR_TOP_LCD, CONTEXT_A); - CAMU_SetOutputFormat(sel, OUTPUT_YUV_422, CONTEXT_A); - CAMU_SetFrameRate(sel, FRAME_RATE_30); - CAMU_SetNoiseFilter(sel, true); - CAMU_SetAutoExposure(sel, true); - CAMU_SetAutoWhiteBalance(sel, true); - CAMU_SetTransferBytes(PORT_BOTH, maxBytes, WIDTH, HEIGHT); - CAMU_Activate(sel); - CAMU_StartCapture(PORT_BOTH); - camera_reinit = false; - } - CAMU_SetReceiving(&ev, camBuf[writeIdx], PORT_CAM1, WIDTH * HEIGHT * 2, - maxBytes); - svcWaitSynchronization(ev, U64_MAX); - svcCloseHandle(ev); - svcSignalEvent(ready[writeIdx]); - writeIdx ^= 1; - } - CAMU_StopCapture(PORT_BOTH); - CAMU_Activate(SELECT_NONE); +void cleanup() { camExit(); + gfxExit(); acExit(); } -int main(int argc, char *argv[]) { - gfxInitDefault(); - httpcInit(4 * 1024 * 1024); - consoleInit(GFX_BOTTOM, &bottomScreen); - consoleSelect(&bottomScreen); - printf("printcam\nL/R=take X=switch-front/back Y=reload-url START=exit\n"); - if (!ensure_storage_dir()) - printf("Failed to create %s\n", STORAGE_DIR); - if (!ensure_default_endpoint_file()) - printf("Failed to create default endpoint file\n"); - if (load_endpoint_from_sd() == 0) - printf("Endpoint: %s\n", api_endpoint); - camBuf[0] = (u8 *)memalign(0x40, WIDTH * HEIGHT * 2); - camBuf[1] = (u8 *)memalign(0x40, WIDTH * HEIGHT * 2); - rgbBuf = (u8 *)memalign(0x40, FB_SIZE); - u8 *linearBuf = malloc(FB_SIZE); - if (!camBuf[0] || !camBuf[1] || !rgbBuf || !linearBuf) { - printf("Out of memory\n"); - goto cleanup; +void writePictureToFramebufferRGB565(void *fb, void *img, u16 x, u16 y, + u16 width, u16 height) { + u8 *fb_8 = (u8 *)fb; + u16 *img_16 = (u16 *)img; + int i, j, draw_x, draw_y; + for (j = 0; j < height; j++) { + for (i = 0; i < width; i++) { + draw_y = y + height - j; + draw_x = x + i; + u32 v = (draw_y + draw_x * height) * 3; + u16 data = img_16[j * width + i]; + uint8_t b = ((data >> 11) & 0x1F) << 3; + uint8_t g = ((data >> 5) & 0x3F) << 2; + uint8_t r = (data & 0x1F) << 3; + fb_8[v] = r; + fb_8[v + 1] = g; + fb_8[v + 2] = b; + } } - svcCreateEvent(&ready[0], RESET_ONESHOT); - svcCreateEvent(&ready[1], RESET_ONESHOT); +} + +void takePicture() { + u8 *buf = malloc(BUF_SIZE); + if (!buf) { + hang("Failed to allocate memory!"); + } + u32 bufSize; + + CAMU_GetMaxBytes(&bufSize, PIC_WIDTH, PIC_HEIGHT); + CAMU_SetTransferBytes(PORT, bufSize, PIC_WIDTH, PIC_HEIGHT); + CAMU_SwitchContext(getActiveSelect(), CONTEXT_B); + + Handle camReceiveEvent = 0; + CAMU_Activate(getActiveSelect()); + CAMU_ClearBuffer(PORT); + CAMU_StartCapture(PORT); + CAMU_SetReceiving(&camReceiveEvent, buf, PORT, PIC_SIZE, (s16)bufSize); + svcWaitSynchronization(camReceiveEvent, WAIT_TIMEOUT); + + CAMU_StopCapture(PORT); + CAMU_Activate(PORT_NONE); + svcCloseHandle(camReceiveEvent); + CAMU_SwitchContext(getActiveSelect(), CONTEXT_A); + + for (int i = 0; i < 5; i++) { + printf("%02x", buf[i]); + } + + free(buf); +} + +void setupCam(u32 *bufSize) { + camInit(); + + CAMU_SetSize(CAMS, SIZE_CTR_TOP_LCD, CONTEXT_A); + CAMU_SetSize(CAMS, SIZE_VGA, CONTEXT_B); + CAMU_SetOutputFormat(CAMS, OUTPUT_RGB_565, CONTEXT_BOTH); + CAMU_SetFrameRate(CAMS, FRAME_RATE_30); + CAMU_SetNoiseFilter(CAMS, true); + CAMU_SetAutoExposure(CAMS, true); + CAMU_SetAutoWhiteBalance(CAMS, true); + CAMU_SetTrimming(PORT_BOTH, false); + CAMU_PlayShutterSound(SHUTTER_SOUND_TYPE_MOVIE); + + CAMU_GetMaxBytes(bufSize, WIDTH, HEIGHT); + CAMU_SetTransferBytes(PORT_BOTH, *bufSize, WIDTH, HEIGHT); +} +void activateCam(Handle camReceiveEvent[2]) { + CAMU_Activate(getActiveSelect()); + CAMU_GetBufferErrorInterruptEvent(&camReceiveEvent[0], PORT); + CAMU_ClearBuffer(PORT); + CAMU_StartCapture(PORT); + + gfxFlushBuffers(); + gspWaitForVBlank(); + gfxSwapBuffers(); +} + +int main() { + acInit(); + gfxInitDefault(); + consoleInit(GFX_BOTTOM, NULL); + gfxSetDoubleBuffering(GFX_TOP, true); - camThread = threadCreate(cameraThreadFunc, NULL, 0x1000, 0x2F, -2, false); + gfxSetDoubleBuffering(GFX_BOTTOM, false); + + if (setjmp(exitJmp)) { + cleanup(); + return 0; + } + + u32 kDown; + u8 *buf = malloc(BUF_SIZE); + u32 bufSize; + Handle camReceiveEvent[2] = {0}; + bool captureInterrupted = false; + s32 index = 0; + + if (!buf) { + hang("Failed to allocate memory!"); + } + + printf("Initializing camera\n"); + + setupCam(&bufSize); + activateCam(camReceiveEvent); + + printf("Press Start to exit to Homebrew Launcher\n"); + while (aptMainLoop()) { - hidScanInput(); - u32 k = hidKeysDown(); - if (k & KEY_START) - break; - if (k & KEY_X) { - camera_index ^= 1; - camera_reinit = true; - printf("Switched to %s\n", - camera_index ? "front/input1" : "back/output1"); - } - if (k & KEY_Y) { - if (load_endpoint_from_sd() == 0) - printf("Reloaded endpoint: %s\n", api_endpoint); - else - printf("Failed to reload endpoint\n"); - } - if (k & (KEY_L | KEY_R)) { - consoleSelect(&bottomScreen); - printf("Snapshot requested\n"); - convert_display_to_row_major(rgbBuf, linearBuf); - char jpgpath[128]; - time_t t = time(NULL); - strftime(jpgpath, sizeof(jpgpath), STORAGE_DIR "/%Y%m%d_%H%M%S.jpg", - localtime(&t)); - if (encode_bgr_to_jpeg_file(linearBuf, WIDTH, HEIGHT, JPEG_QUALITY, - jpgpath) == 0) { - printf("Wrote %s\n", jpgpath); - Result res = http_post_file(api_endpoint, jpgpath); - printf("http_post_file returned: %" PRIx32 "\n", (u32)res); - } else { - printf("JPEG write failed\n"); + if (!captureInterrupted) { + hidScanInput(); + kDown = hidKeysDown(); + + if (kDown & KEY_START) { + break; + } + if (kDown & KEY_A) { + printf("Switching cameras...\n"); + + CAMU_StopCapture(PORT); + svcCloseHandle(camReceiveEvent[0]); + svcCloseHandle(camReceiveEvent[1]); + + activeCam ^= true; + activateCam(camReceiveEvent); + } + + if (kDown & KEY_R) { + printf("Taking pic...\n"); + + CAMU_StopCapture(PORT); + svcCloseHandle(camReceiveEvent[0]); + svcCloseHandle(camReceiveEvent[1]); + + takePicture(); + // setupCam(); + activateCam(camReceiveEvent); } } - gspWaitForVBlank(); - if (svcWaitSynchronization(ready[dispIdx], 0) == 0) { - blitYUYVtoRGB(camBuf[dispIdx], rgbBuf); - dispIdx ^= 1; + + if (camReceiveEvent[1] == 0) { + CAMU_SetReceiving(&camReceiveEvent[1], buf, PORT, SCREEN_SIZE, + (s16)bufSize); } - u8 *fb = gfxGetFramebuffer(GFX_TOP, GFX_LEFT, NULL, NULL); - memcpy(fb, rgbBuf, FB_SIZE); - gfxSwapBuffersGpu(); + if (captureInterrupted) { + CAMU_StartCapture(PORT); + captureInterrupted = false; + } + + svcWaitSynchronizationN(&index, camReceiveEvent, 2, false, WAIT_TIMEOUT); + switch (index) { + case 0: + svcCloseHandle(camReceiveEvent[1]); + camReceiveEvent[1] = 0; + + captureInterrupted = true; + continue; // skip screen update + break; + case 1: + svcCloseHandle(camReceiveEvent[1]); + camReceiveEvent[1] = 0; + break; + default: + break; + } + + writePictureToFramebufferRGB565( + gfxGetFramebuffer(GFX_TOP, GFX_LEFT, NULL, NULL), buf, 0, 0, WIDTH, + HEIGHT); + + gfxFlushBuffers(); + gspWaitForVBlank(); + gfxSwapBuffers(); } -cleanup: - exitRequested = true; - if (camThread) - threadJoin(camThread, U64_MAX); - if (camThread) - threadFree(camThread); - if (ready[0]) - svcCloseHandle(ready[0]); - if (ready[1]) - svcCloseHandle(ready[1]); - if (camBuf[0]) - free(camBuf[0]); - if (camBuf[1]) - free(camBuf[1]); - if (rgbBuf) - free(rgbBuf); - if (linearBuf) - free(linearBuf); - httpcExit(); - gfxExit(); + + CAMU_StopCapture(PORT); + for (int i = 0; i < 2; i++) { + if (camReceiveEvent[i] != 0) { + svcCloseHandle(camReceiveEvent[i]); + } + } + CAMU_Activate(PORT_NONE); + + // Exit + free(buf); + cleanup(); + + // Return to hbmenu return 0; }