# HG changeset patch # User Michael Pavone # Date 1676099863 28800 # Node ID eb45ad9d8a3fec471313e4bd39b71a3baf53ba1b # Parent 7e995fb948c3ec577f20e7c2f32b961137501fdc WIP "video" recording in APNG format diff -r 7e995fb948c3 -r eb45ad9d8a3f bindings.c --- a/bindings.c Fri Feb 10 21:37:59 2023 -0800 +++ b/bindings.c Fri Feb 10 23:17:43 2023 -0800 @@ -37,6 +37,7 @@ UI_RELOAD, UI_SMS_PAUSE, UI_SCREENSHOT, + UI_RECORD_VIDEO, UI_VGM_LOG, UI_EXIT, UI_PLANE_DEBUG, @@ -413,6 +414,16 @@ render_save_screenshot(path); } break; + case UI_RECORD_VIDEO: + if (allow_content_binds) { + if (render_saving_video()) { + render_end_video(); + } else { + char *path = get_content_config_path("ui\0video_path\0", "ui\0video_template\0", "blastem_%c.apng"); + render_save_video(path); + } + } + break; case UI_VGM_LOG: if (allow_content_binds && current_system->start_vgm_log) { if (current_system->vgm_logging) { @@ -678,6 +689,8 @@ *subtype_a = UI_SMS_PAUSE; } else if (!strcmp(target + 3, "screenshot")) { *subtype_a = UI_SCREENSHOT; + } else if (!strcmp(target + 3, "record_video")) { + *subtype_a = UI_RECORD_VIDEO; } else if (!strcmp(target + 3, "vgm_log")) { *subtype_a = UI_VGM_LOG; } else if(!strcmp(target + 3, "exit")) { diff -r 7e995fb948c3 -r eb45ad9d8a3f default.cfg --- a/default.cfg Fri Feb 10 21:37:59 2023 -0800 +++ b/default.cfg Fri Feb 10 23:17:43 2023 -0800 @@ -17,6 +17,7 @@ [ ui.vdp_debug_mode u ui.enter_debugger p ui.screenshot + i ui.record_video b ui.plane_debug v ui.vram_debug c ui.cram_debug diff -r 7e995fb948c3 -r eb45ad9d8a3f png.c --- a/png.c Fri Feb 10 21:37:59 2023 -0800 +++ b/png.c Fri Feb 10 23:17:43 2023 -0800 @@ -3,12 +3,16 @@ #include #include #include "zlib/zlib.h" +#include "png.h" static const char png_magic[] = {0x89, 'P', 'N', 'G', '\r', '\n', 0x1A, '\n'}; static const char ihdr[] = {'I', 'H', 'D', 'R'}; static const char plte[] = {'P', 'L', 'T', 'E'}; static const char idat[] = {'I', 'D', 'A', 'T'}; static const char iend[] = {'I', 'E', 'N', 'D'}; +static const char actl[] = {'a', 'c', 'T', 'L'}; +static const char fctl[] = {'f', 'c', 'T', 'L'}; +static const char fdat[] = {'f', 'd', 'A', 'T'}; enum { COLOR_GRAY, @@ -56,7 +60,7 @@ write_chunk(f, ihdr, chunk, sizeof(chunk)); } -void save_png24(FILE *f, uint32_t *buffer, uint32_t width, uint32_t height, uint32_t pitch) +void save_png24_frame(FILE *f, uint32_t *buffer, apng_state *apng, uint32_t width, uint32_t height, uint32_t pitch) { uint32_t idat_size = (1 + width*3) * height; uint8_t *idat_buffer = malloc(idat_size); @@ -76,14 +80,78 @@ } pixel = start + pitch / sizeof(uint32_t); } - write_header(f, width, height, COLOR_TRUE); + uLongf compress_buffer_size = idat_size + 5 * (idat_size/16383 + 1) + 3; + uint32_t offset = 0; + if (apng) { + uint8_t chunk[26] = { + apng->sequence_number >> 24, apng->sequence_number >> 16, + apng->sequence_number >> 8, apng->sequence_number, + width >> 24, width >> 16, width >> 8, width, + height >> 24, height >> 16, height >> 8, height, + 0, 0, 0, 0, //x offset + 0, 0, 0, 0, //y offset + apng->delay_num >> 8, apng->delay_num, + apng->delay_den >> 8, apng->delay_den, + 0, 0 //dispose and blend ops + }; + write_chunk(f, fctl, chunk, sizeof(chunk)); + apng->sequence_number++; + apng->num_frames++; + if (apng->sequence_number > 1) { + offset = sizeof(uint32_t); + compress_buffer_size += offset; + } + } uint8_t *compressed = malloc(compress_buffer_size); - compress(compressed, &compress_buffer_size, idat_buffer, idat_size); + compress_buffer_size -= offset; + compress(compressed + offset, &compress_buffer_size, idat_buffer, idat_size); free(idat_buffer); - write_chunk(f, idat, compressed, compress_buffer_size); + if (offset) { + cur = compressed; + *(cur++) = apng->sequence_number >> 24; + *(cur++) = apng->sequence_number >> 16; + *(cur++) = apng->sequence_number >> 8; + *(cur++) = apng->sequence_number; + apng->sequence_number++; + } + write_chunk(f, offset ? fdat : idat, compressed, compress_buffer_size + offset); + free(compressed); +} + +apng_state* start_apng(FILE *f, uint32_t width, uint32_t height, float frame_rate) +{ + write_header(f, width, height, COLOR_TRUE); + apng_state *apng = calloc(1, sizeof(apng_state)); + uint8_t chunk[] = { + 0, 0, 0, 0, + 0, 0, 0, 1 + }; + apng->num_frame_offset = ftell(f) + 8; + write_chunk(f, actl, chunk, sizeof(chunk)); + apng->delay_num = 65535.0f / frame_rate; + apng->delay_den = frame_rate * apng->delay_num; + return apng; +} + +void end_apng(FILE *f, apng_state *apng) +{ write_chunk(f, iend, NULL, 0); - free(compressed); + fseek(f, apng->num_frame_offset, SEEK_SET); + uint8_t bytes[] = { + apng->num_frames >> 24, apng->num_frames >> 16, + apng->num_frames >> 8, apng->num_frames + }; + fwrite(bytes, 1, sizeof(bytes), f); + fclose(f); + free(apng); +} + +void save_png24(FILE *f, uint32_t *buffer, uint32_t width, uint32_t height, uint32_t pitch) +{ + write_header(f, width, height, COLOR_TRUE); + save_png24_frame(f, buffer, NULL, width, height, pitch); + write_chunk(f, iend, NULL, 0); } void save_png(FILE *f, uint32_t *buffer, uint32_t width, uint32_t height, uint32_t pitch) diff -r 7e995fb948c3 -r eb45ad9d8a3f png.h --- a/png.h Fri Feb 10 21:37:59 2023 -0800 +++ b/png.h Fri Feb 10 23:17:43 2023 -0800 @@ -1,8 +1,19 @@ #ifndef PNG_H_ #define PNG_H_ +typedef struct { + uint32_t sequence_number; + uint32_t num_frames; + uint32_t num_frame_offset; + uint16_t delay_num; + uint16_t delay_den; +} apng_state; + +void save_png24_frame(FILE *f, uint32_t *buffer, apng_state *apng, uint32_t width, uint32_t height, uint32_t pitch); void save_png24(FILE *f, uint32_t *buffer, uint32_t width, uint32_t height, uint32_t pitch); void save_png(FILE *f, uint32_t *buffer, uint32_t width, uint32_t height, uint32_t pitch); +apng_state* start_apng(FILE *f, uint32_t width, uint32_t height, float frame_rate); +void end_apng(FILE *f, apng_state *apng); uint32_t *load_png(uint8_t *buffer, uint32_t buf_size, uint32_t *width, uint32_t *height); #endif //PNG_H_ diff -r 7e995fb948c3 -r eb45ad9d8a3f render.h --- a/render.h Fri Feb 10 21:37:59 2023 -0800 +++ b/render.h Fri Feb 10 23:17:43 2023 -0800 @@ -101,6 +101,9 @@ uint32_t render_map_color(uint8_t r, uint8_t g, uint8_t b); void render_save_screenshot(char *path); +uint8_t render_saving_video(void); +void render_end_video(void); +void render_save_video(char *path); uint8_t render_create_window(char *caption, uint32_t width, uint32_t height, window_close_handler close_handler); void render_destroy_window(uint8_t which); uint32_t *render_get_framebuffer(uint8_t which, int *pitch); diff -r 7e995fb948c3 -r eb45ad9d8a3f render_sdl.c --- a/render_sdl.c Fri Feb 10 21:37:59 2023 -0800 +++ b/render_sdl.c Fri Feb 10 23:17:43 2023 -0800 @@ -1361,6 +1361,43 @@ screenshot_path = path; } +#ifndef DISABLE_ZLIB +static apng_state *apng; +static FILE *apng_file; +#endif +uint8_t render_saving_video(void) +{ +#ifdef DISABLE_ZLIB + return apng_file != NULL; +#else + return 0; +#endif +} + +void render_end_video(void) +{ +#ifndef DISABLE_ZLIB + if (apng) { + puts("Ending recording"); + end_apng(apng_file, apng); + apng = NULL; + apng_file = NULL; + } +#endif +} +void render_save_video(char *path) +{ + render_end_video(); +#ifndef DISABLE_ZLIB + apng_file = fopen(path, "wb"); + if (apng_file) { + printf("Saving video to %s\n", path); + } else { + warning("Failed to open %s for writing\n", path); + } +#endif +} + uint8_t render_create_window(char *caption, uint32_t width, uint32_t height, window_close_handler close_handler) { uint8_t win_idx = 0xFF; @@ -1546,6 +1583,19 @@ } #endif } +#ifndef DISABLE_ZLIB + if (apng_file) { + if (!apng) { + //TODO: more precise frame rate + apng = start_apng(apng_file, width, height, video_standard == VID_PAL ? 50.0 : 60.0); + } + save_png24_frame( + apng_file, + buffer + overscan_left[video_standard] + LINEBUF_SIZE * overscan_top[video_standard], + apng, width, height, LINEBUF_SIZE*sizeof(uint32_t) + ); + } +#endif } else { #endif //TODO: Support SYNC_AUDIO_THREAD/SYNC_EXTERNAL for render API framebuffers