changeset 2295:eb45ad9d8a3f

WIP "video" recording in APNG format
author Michael Pavone <pavone@retrodev.com>
date Fri, 10 Feb 2023 23:17:43 -0800
parents 7e995fb948c3
children 789802d99629 c79896ff1a2d
files bindings.c default.cfg png.c png.h render.h render_sdl.c
diffstat 6 files changed, 151 insertions(+), 5 deletions(-) [+]
line wrap: on
line diff
--- 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")) {
--- 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
--- 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 <stdio.h>
 #include <string.h>
 #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)
--- 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_
--- 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);
--- 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