changeset 1983:a7b753e260a2 mame_interp

Merge from default
author Michael Pavone <pavone@retrodev.com>
date Sat, 09 May 2020 23:39:44 -0700
parents cafde1255ad3 (current diff) 81df9aa2de9b (diff)
children 0d5f88e53dca
files Makefile backend.c blastem.c debug.c gdb_remote.c genesis.c genesis.h sms.c stateview.c
diffstat 46 files changed, 2563 insertions(+), 766 deletions(-) [+]
line wrap: on
line diff
--- a/Makefile	Sun Apr 19 00:59:49 2020 -0700
+++ b/Makefile	Sat May 09 23:39:44 2020 -0700
@@ -31,7 +31,7 @@
 GLUDIR:=x64
 endif
 GLEW32S_LIB:=$(GLEW_PREFIX)/lib/Release/$(GLUDIR)/glew32s.lib
-CFLAGS:=-std=gnu99 -Wreturn-type -Werror=return-type -Werror=implicit-function-declaration
+CFLAGS:=-std=gnu99 -Wreturn-type -Werror=return-type -Werror=implicit-function-declaration -Wpointer-arith -Werror=pointer-arith
 LDFLAGS:=-lm -lmingw32 -lws2_32 -mwindows
 ifneq ($(MAKECMDGOALS),libblastem.dll)
 CFLAGS+= -I"$(SDL2_PREFIX)/include/SDL2" -I"$(GLEW_PREFIX)/include" -DGLEW_STATIC
@@ -47,7 +47,7 @@
 EXE:=
 
 HAS_PROC:=$(shell if [ -d /proc ]; then /bin/echo -e -DHAS_PROC; fi)
-CFLAGS:=-std=gnu99 -Wreturn-type -Werror=return-type -Werror=implicit-function-declaration -Wno-unused-value $(HAS_PROC) -DHAVE_UNISTD_H
+CFLAGS:=-std=gnu99 -Wreturn-type -Werror=return-type -Werror=implicit-function-declaration -Wno-unused-value  -Wpointer-arith -Werror=pointer-arith $(HAS_PROC) -DHAVE_UNISTD_H
 
 ifeq ($(OS),Darwin)
 LIBS=sdl2 glew
@@ -199,7 +199,7 @@
 endif
 endif
 endif
-AUDIOOBJS=ym2612.o psg.o wave.o vgm.o render_audio.o
+AUDIOOBJS=ym2612.o psg.o wave.o vgm.o event_log.o render_audio.o
 CONFIGOBJS=config.o tern.o util.o paths.o 
 NUKLEAROBJS=$(FONT) nuklear_ui/blastem_nuklear.o nuklear_ui/sfnt.o
 RENDEROBJS=ppm.o controller_info.o
@@ -217,8 +217,8 @@
 
 #MAINOBJS=blastem.o system.o genesis.o debug.o gdb_remote.o vdp.o $(RENDEROBJS) io.o romdb.o hash.o menu.o xband.o 
 MAINOBJS=blastem.o system.o genesis.o vdp.o $(RENDEROBJS) io.o romdb.o hash.o menu.o xband.o \
-	realtec.o i2c.o nor.o sega_mapper.o multi_game.o megawifi.o $(NET) serialize.o $(TERMINAL) $(CONFIGOBJS) \
-	$(M68KOBJS) $(TRANSOBJS) $(AUDIOOBJS) saves.o zip.o bindings.o jcart.o
+	realtec.o i2c.o nor.o sega_mapper.o multi_game.o megawifi.o $(NET) serialize.o $(TERMINAL) $(CONFIGOBJS) gst.o \
+	$(M68KOBJS) $(TRANSOBJS) $(AUDIOOBJS) saves.o zip.o bindings.o jcart.o gen_player.o
 
 LIBOBJS=libblastem.o system.o genesis.o debug.o gdb_remote.o vdp.o io.o romdb.o hash.o xband.o realtec.o \
 	i2c.o nor.o sega_mapper.o multi_game.o megawifi.o $(NET) serialize.o $(TERMINAL) $(CONFIGOBJS) gst.o \
@@ -265,7 +265,7 @@
 CFLAGS+= -DFONT_PATH='"'$(FONT_PATH)'"'
 endif
 
-ALL=dis$(EXE) zdis$(EXE) stateview$(EXE) vgmplay$(EXE) blastem$(EXE)
+ALL=dis$(EXE) zdis$(EXE) vgmplay$(EXE) blastem$(EXE)
 ifneq ($(OS),Windows)
 ALL+= termhelper
 endif
@@ -313,10 +313,6 @@
 ztestgen : ztestgen.o z80inst.o
 	$(CC) -ggdb -o ztestgen ztestgen.o z80inst.o
 
-stateview$(EXE) : stateview.o vdp.o $(RENDEROBJS) serialize.o $(CONFIGOBJS) gst.o render_audio.o
-	$(CC) -o $@ $^ $(LDFLAGS)
-	$(FIXUP) ./$@
-
 vgmplay$(EXE) : vgmplay.o $(RENDEROBJS) serialize.o $(CONFIGOBJS) $(AUDIOOBJS)
 	$(CC) -o $@ $^ $(LDFLAGS)
 	$(FIXUP) ./$@
@@ -350,7 +346,7 @@
 	
 m68k.c : m68k.cpu cpu_dsl.py
 	./cpu_dsl.py -d call $< > $@
-	
+
 %.c : %.cpu cpu_dsl.py
 	./cpu_dsl.py -d goto $< > $@
 
--- a/backend.c	Sun Apr 19 00:59:49 2020 -0700
+++ b/backend.c	Sat May 09 23:39:44 2020 -0700
@@ -85,7 +85,7 @@
 				: memmap[chunk].buffer;
 			if (!base) {
 				if (memmap[chunk].flags & MMAP_AUX_BUFF) {
-					return memmap[chunk].buffer + (address & memmap[chunk].aux_mask);
+					return ((uint8_t *)memmap[chunk].buffer) + (address & memmap[chunk].aux_mask);
 				}
 				return NULL;
 			}
@@ -110,7 +110,7 @@
 				: memmap[chunk].buffer;
 			if (!base) {
 				if (memmap[chunk].flags & MMAP_AUX_BUFF) {
-					return memmap[chunk].buffer + (address & memmap[chunk].aux_mask);
+					return ((uint8_t *)memmap[chunk].buffer) + (address & memmap[chunk].aux_mask);
 				}
 				return NULL;
 			}
--- a/bindings.c	Sun Apr 19 00:59:49 2020 -0700
+++ b/bindings.c	Sat May 09 23:39:44 2020 -0700
@@ -409,7 +409,7 @@
 				show_pause_menu();
 			} else {
 #endif
-			current_system->request_exit(current_system);
+			system_request_exit(current_system, 1);
 			if (current_system->type == SYSTEM_GENESIS) {
 				genesis_context *gen = (genesis_context *)current_system;
 				if (gen->extra) {
--- a/blastem.c	Sun Apr 19 00:59:49 2020 -0700
+++ b/blastem.c	Sat May 09 23:39:44 2020 -0700
@@ -30,6 +30,7 @@
 #include "bindings.h"
 #include "menu.h"
 #include "zip.h"
+#include "event_log.h"
 #ifndef DISABLE_NUKLEAR
 #include "nuklear_ui/blastem_nuklear.h"
 #endif
@@ -157,8 +158,9 @@
 						for (offset = 0; offset + SMD_BLOCK_SIZE + SMD_HEADER_SIZE <= out_size; offset += SMD_BLOCK_SIZE)
 						{
 							uint8_t tmp[SMD_BLOCK_SIZE];
-							memcpy(tmp, *dst + offset + SMD_HEADER_SIZE, SMD_BLOCK_SIZE);
-							process_smd_block(*dst + offset, tmp, SMD_BLOCK_SIZE);
+							uint8_t *u8dst = *dst;
+							memcpy(tmp, u8dst + offset + SMD_HEADER_SIZE, SMD_BLOCK_SIZE);
+							process_smd_block((void *)(u8dst + offset), tmp, SMD_BLOCK_SIZE);
 						}
 						out_size = offset;
 					}
@@ -325,7 +327,7 @@
 			free(current_system->next_rom);
 		}
 		current_system->next_rom = strdup(filename);
-		current_system->request_exit(current_system);
+		system_request_exit(current_system, 1);
 		if (menu_system && menu_system->type == SYSTEM_GENESIS) {
 			genesis_context *gen = (genesis_context *)menu_system;
 			if (gen->extra) {
@@ -366,7 +368,7 @@
 		num_parts--;
 	}
 	current_system->next_rom = alloc_concat_m(num_parts, start);
-	current_system->request_exit(current_system);
+	system_request_exit(current_system, 1);
 }
 
 void lockon_media(char *lock_on_path)
@@ -434,6 +436,23 @@
 	update_title(game_system->info.name);
 }
 
+char *parse_addr_port(char *arg)
+{
+	while (*arg && *arg != ':') {
+		++arg;
+	}
+	if (!*arg) {
+		return NULL;
+	}
+	char *end;
+	int port = strtol(arg + 1, &end, 10);
+	if (port && !*end) {
+		*arg = 0;
+		return arg + 1;
+	}
+	return NULL;
+}
+
 int main(int argc, char ** argv)
 {
 	set_exe_str(argv[0]);
@@ -445,10 +464,13 @@
 	system_type stype = SYSTEM_UNKNOWN, force_stype = SYSTEM_UNKNOWN;
 	char * romfname = NULL;
 	char * statefile = NULL;
+	char *reader_addr = NULL, *reader_port = NULL;
+	event_reader reader = {0};
 	debugger_type dtype = DEBUGGER_NATIVE;
 	uint8_t start_in_debugger = 0;
 	uint8_t fullscreen = FULLSCREEN_DEFAULT, use_gl = 1;
 	uint8_t debug_target = 0;
+	char *port;
 	for (int i = 1; i < argc; i++) {
 		if (argv[i][0] == '-') {
 			switch(argv[i][1]) {
@@ -474,6 +496,18 @@
 				start_in_debugger = 1;
 				break;
 #endif
+			case 'e':
+				i++;
+				if (i >= argc) {
+					fatal_error("-e must be followed by a file name\n");
+				}
+				port = parse_addr_port(argv[i]);
+				if (port) {
+					event_log_tcp(argv[i], port);
+				} else {
+					event_log_file(argv[i]);
+				}
+				break;
 			case 'f':
 				fullscreen = !fullscreen;
 				break;
@@ -561,18 +595,24 @@
 					"	-v          Display version number and exit\n"
 					"	-l          Log 68K code addresses (useful for assemblers)\n"
 					"	-y          Log individual YM-2612 channels to WAVE files\n"
+					"   -e FILE     Write hardware event log to FILE\n"
 				);
 				return 0;
 			default:
 				fatal_error("Unrecognized switch %s\n", argv[i]);
 			}
 		} else if (!loaded) {
+			reader_port = parse_addr_port(argv[i]);
+			if (reader_port) {
+				reader_addr = argv[i];
+			} else {
 			if (!(cart.size = load_rom(argv[i], &cart.buffer, stype == SYSTEM_UNKNOWN ? &stype : NULL))) {
 				fatal_error("Failed to open %s for reading\n", argv[i]);
 			}
 			cart.dir = path_dirname(argv[i]);
 			cart.name = basename_no_extension(argv[i]);
 			cart.extension = path_extension(argv[i]);
+			}
 			romfname = argv[i];
 			loaded = 1;
 		} else if (width < 0) {
@@ -605,6 +645,9 @@
 		fullscreen = !fullscreen;
 	}
 	if (!headless) {
+		if (reader_addr) {
+			render_set_external_sync(1);
+		}
 		render_init(width, height, "BlastEm", fullscreen);
 		render_set_drag_drop_handler(on_drag_drop);
 	}
@@ -650,13 +693,14 @@
 		warning("%s is not a valid value for the ui.state_format setting. Valid values are gst and native\n", state_format);
 	}
 
-	if (loaded) {
+	if (loaded && !reader_addr) {
 		if (stype == SYSTEM_UNKNOWN) {
 			stype = detect_system_type(&cart);
 		}
 		if (stype == SYSTEM_UNKNOWN) {
 			fatal_error("Failed to detect system type for %s\n", romfname);
 		}
+		
 		current_system = alloc_config_system(stype, &cart, menu ? 0 : opts, force_region);
 		if (!current_system) {
 			fatal_error("Failed to configure emulated machine for %s\n", romfname);
@@ -679,6 +723,19 @@
 	}
 #endif
 	
+	if (reader_addr) {
+		init_event_reader_tcp(&reader, reader_addr, reader_port);
+		stype = reader_system_type(&reader);
+		if (stype == SYSTEM_UNKNOWN) {
+			fatal_error("Failed to detect system type for %s\n", romfname);
+		}
+		game_system = current_system = alloc_config_player(stype, &reader);
+		//free inflate stream as it was inflateCopied to an internal event reader in the player
+		inflateEnd(&reader.input_stream);
+		setup_saves(&cart, current_system);
+		update_title(current_system->info.name);
+	}
+	
 	current_system->debugger_type = dtype;
 	current_system->enter_debugger = start_in_debugger && menu == debug_target;
 	current_system->start_context(current_system,  menu ? NULL : statefile);
@@ -698,6 +755,7 @@
 			current_system->debugger_type = dtype;
 			current_system->enter_debugger = start_in_debugger && menu == debug_target;
 			current_system->start_context(current_system, statefile);
+			render_video_loop();
 		} else if (menu && game_system) {
 #ifndef NEW_CORE
 			current_system->arena = set_current_arena(game_system->arena);
@@ -719,6 +777,7 @@
 			}
 			if (!current_system->next_rom) {
 				current_system->resume_context(current_system);
+				render_video_loop();
 			}
 		} else {
 			break;
--- a/build_release	Sun Apr 19 00:59:49 2020 -0700
+++ b/build_release	Sat May 09 23:39:44 2020 -0700
@@ -34,11 +34,11 @@
 fi
 make menu.bin
 if [ $OS = "Windows" -o $OS = "Win64" ]; then
-	binaries="dis.exe zdis.exe stateview.exe vgmplay.exe blastem.exe $SDLDLLPATH/SDL2.dll"
+	binaries="dis.exe zdis.exe vgmplay.exe blastem.exe $SDLDLLPATH/SDL2.dll"
 	verstr=`sed -E -n 's/^[^B]+BLASTEM_VERSION "([^"]+)"/blastem \1/p' blastem.c`
 	txt=".txt"
 else
-	binaries="dis zdis stateview vgmplay blastem termhelper"
+	binaries="dis zdis vgmplay blastem termhelper"
 	if [ $OS = "Darwin" ]; then
 		binaries="$binaries Frameworks"
 	else
--- a/cpu_dsl.py	Sun Apr 19 00:59:49 2020 -0700
+++ b/cpu_dsl.py	Sat May 09 23:39:44 2020 -0700
@@ -1597,7 +1597,11 @@
 				pieces.append('\n\t\tif (context->cycles >= context->sync_cycle) {')
 				self.meta = {}
 				self.temp = {}
-				self.subroutines[self.interrupt].inline(self, [], pieces, otype, None)
+				intpieces = []
+				self.subroutines[self.interrupt].inline(self, [], intpieces, otype, None)
+				for size in self.temp:
+					pieces.append('\n\tuint{sz}_t gen_tmp{sz}__;'.format(sz=size))
+				pieces += intpieces
 				pieces.append('\n\t\t}')
 			self.meta = {}
 			self.temp = {}
--- a/debug.c	Sun Apr 19 00:59:49 2020 -0700
+++ b/debug.c	Sat May 09 23:39:44 2020 -0700
@@ -95,6 +95,12 @@
 	}
 }
 
+static uint8_t m68k_read_byte(uint32_t address, m68k_context *context)
+{
+	//TODO: share this implementation with GDB debugger
+	return read_byte(address, (void **)context->mem_pointers, &context->options->gen, context);
+}
+
 uint16_t m68k_read_word(uint32_t address, m68k_context *context)
 {
 	return read_word(address, (void **)context->mem_pointers, &context->options->gen, context);
@@ -158,6 +164,8 @@
 		uint32_t p_addr = strtol(param+(param[0] == '0' ? 2 : 1), &after, 16);
 		if (after[0] == '.' && after[1] == 'l') {
 			value = m68k_read_long(p_addr, context);
+		} else if (after[0] == '.' && after[1] == 'b') {
+			value = m68k_read_byte(p_addr, context);
 		} else {
 			value = m68k_read_word(p_addr, context);
 		}
@@ -166,6 +174,8 @@
 		uint32_t p_addr = param[1] == 'a' ? context->aregs[reg] : context->dregs[reg];
 		if (param[4] == '.' && param[5] == 'l') {
 			value = m68k_read_long(p_addr, context);
+		} else if (param[4] == '.' && param[5] == 'b') {
+			value = m68k_read_byte(p_addr, context);
 		} else {
 			value = m68k_read_word(p_addr, context);
 		}
--- a/default.cfg	Sun Apr 19 00:59:49 2020 -0700
+++ b/default.cfg	Sat May 09 23:39:44 2020 -0700
@@ -296,6 +296,9 @@
 	gl on
 	#scaling can be linear (for linear interpolation) or nearest (for nearest neighbor)
 	scaling linear
+	#When off, a 512x512 texture is used for each field, when turned on a smaller texture is used
+	#turning this on seems to help performance on certain mobile GPUs like Mali
+	npot_textures off
 	ntsc {
 		overscan {
 			#these values will result in square pixels in H40 mode
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/event_log.c	Sat May 09 23:39:44 2020 -0700
@@ -0,0 +1,746 @@
+#ifdef _WIN32
+#define WINVER 0x501
+#include <winsock2.h>
+#include <ws2tcpip.h>
+#else
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <unistd.h>
+#include <netdb.h>
+#include <netinet/tcp.h>
+#endif
+
+#include <errno.h>
+#include "event_log.h"
+#include "util.h"
+#include "blastem.h"
+#include "saves.h"
+#include "zlib/zlib.h"
+
+enum {
+	CMD_GAMEPAD_DOWN,
+	CMD_GAMEPAD_UP,
+};
+
+static uint8_t active, fully_active;
+static FILE *event_file;
+static serialize_buffer buffer;
+static uint8_t *compressed;
+static size_t compressed_storage;
+static z_stream output_stream;
+static uint32_t last;
+
+static void event_log_common_init(void)
+{
+	init_serialize(&buffer);
+	compressed_storage = 128*1024;
+	compressed = malloc(compressed_storage);
+	deflateInit(&output_stream, 9);
+	output_stream.avail_out = compressed_storage;
+	output_stream.next_out = compressed;
+	output_stream.avail_in = 0;
+	output_stream.next_in = buffer.data;
+	last = 0;
+	active = 1;
+}
+
+static uint8_t multi_count;
+static size_t multi_start;
+static void finish_multi(void)
+{
+	buffer.data[multi_start] |= multi_count - 2;
+	multi_count = 0;
+}
+
+static void file_finish(void)
+{
+	fwrite(compressed, 1, output_stream.next_out - compressed, event_file);
+	output_stream.next_out = compressed;
+	output_stream.avail_out = compressed_storage;
+	int result = deflate(&output_stream, Z_FINISH);
+	if (Z_STREAM_END != result) {
+		fatal_error("Final deflate call returned %d\n", result);
+	}
+	fwrite(compressed, 1, output_stream.next_out - compressed, event_file);
+	fclose(event_file);
+}
+
+static const char el_ident[] = "BLSTEL\x02\x00";
+void event_log_file(char *fname)
+{
+	event_file = fopen(fname, "wb");
+	if (!event_file) {
+		warning("Failed to open event file %s for writing\n", fname);
+		return;
+	}
+	fwrite(el_ident, 1, sizeof(el_ident) - 1, event_file);
+	event_log_common_init();
+	fully_active = 1;
+	atexit(file_finish);
+}
+
+typedef struct {
+	uint8_t  *send_progress;
+	int      sock;
+	uint8_t  players[1]; //TODO: Expand when support for multiple players per remote is added
+	uint8_t  num_players;
+} remote;
+
+static int listen_sock;
+static remote remotes[7];
+static int num_remotes;
+static uint8_t available_players[7] = {2,3,4,5,6,7,8};
+static int num_available_players = 7;
+void event_log_tcp(char *address, char *port)
+{
+	struct addrinfo request, *result;
+	socket_init();
+	memset(&request, 0, sizeof(request));
+	request.ai_family = AF_INET;
+	request.ai_socktype = SOCK_STREAM;
+	request.ai_flags = AI_PASSIVE;
+	getaddrinfo(address, port, &request, &result);
+	
+	listen_sock = socket(result->ai_family, result->ai_socktype, result->ai_protocol);
+	if (listen_sock < 0) {
+		warning("Failed to open event log listen socket on %s:%s\n", address, port);
+		goto cleanup_address;
+	}
+	int param = 1;
+	setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, (const char *)&param, sizeof(param));
+	if (bind(listen_sock, result->ai_addr, result->ai_addrlen) < 0) {
+		warning("Failed to bind event log listen socket on %s:%s\n", address, port);
+		socket_close(listen_sock);
+		goto cleanup_address;
+	}
+	if (listen(listen_sock, 3) < 0) {
+		warning("Failed to listen for event log remotes on %s:%s\n", address, port);
+		socket_close(listen_sock);
+		goto cleanup_address;
+	}
+	socket_blocking(listen_sock, 0);
+	event_log_common_init();
+cleanup_address:
+	freeaddrinfo(result);
+}
+
+static uint8_t *system_start;
+static size_t system_start_size;
+void event_system_start(system_type stype, vid_std video_std, char *name)
+{
+	if (!active) {
+		return;
+	}
+	save_int8(&buffer, stype);
+	save_int8(&buffer, video_std);
+	size_t name_len = strlen(name);
+	if (name_len > 255) {
+		name_len = 255;
+	}
+	save_int8(&buffer, name_len);
+	save_buffer8(&buffer, name, strlen(name));
+	if (listen_sock) {
+		system_start = malloc(buffer.size);
+		system_start_size = buffer.size;
+		memcpy(system_start, buffer.data, buffer.size);
+	} else {
+		//system start header is never compressed, so write to file immediately
+		fwrite(buffer.data, 1, buffer.size, event_file);
+	}
+	buffer.size = 0;
+}
+
+//header formats
+//Single byte: 4 bit type, 4 bit delta (16-31)
+//Three Byte: 8 bit type, 16-bit delta
+//Four byte: 8-bit type, 24-bit signed delta
+#define FORMAT_3BYTE 0xE0
+#define FORMAT_4BYTE 0xF0
+static uint8_t last_event_type = 0xFF;
+static uint32_t last_delta;
+static void event_header(uint8_t type, uint32_t cycle)
+{
+	uint32_t delta = cycle - last;
+	if (multi_count) {
+		if (type != last_event_type || delta != last_delta) {
+			finish_multi();
+		} else {
+			++multi_count;
+			if (multi_count == 17) {
+				finish_multi();
+				last_event_type = 0xFF;
+			}
+			return;
+		}
+	} else if (type == last_event_type && delta == last_delta && type != EVENT_FLUSH) {
+		//make some room
+		save_int8(&buffer, 0);
+		//shift existing command
+		memmove(buffer.data + multi_start + 1, buffer.data + multi_start, buffer.size - multi_start - 1);
+		buffer.data[multi_start] = EVENT_MULTI << 4;
+		multi_count = 2;
+		return;
+	}
+	multi_start = buffer.size;
+	last_event_type = type;
+	last_delta = delta;
+	
+	if (delta > 65535) {
+		save_int8(&buffer, FORMAT_4BYTE | type);
+		save_int8(&buffer, delta >> 16);
+		save_int16(&buffer, delta);
+	} else if (delta >= 16 && delta < 32) {
+		save_int8(&buffer, type << 4 | (delta - 16));
+	} else {
+		save_int8(&buffer, FORMAT_3BYTE | type);
+		save_int16(&buffer, delta);
+	}
+}
+
+void event_cycle_adjust(uint32_t cycle, uint32_t deduction)
+{
+	if (!fully_active) {
+		return;
+	}
+	event_header(EVENT_ADJUST, cycle);
+	last = cycle - deduction;
+	save_int32(&buffer, deduction);
+}
+
+static uint8_t next_available_player(void)
+{
+	uint8_t lowest = 0xFF;
+	int lowest_index = -1;
+	for (int i = 0; i < num_available_players; i++)
+	{
+		if (available_players[i] < lowest) {
+			lowest = available_players[i];
+			lowest_index = i;
+		}
+	}
+	if (lowest_index >= 0) {
+		available_players[lowest_index] = available_players[num_available_players - 1];
+		--num_available_players;
+	}
+	return lowest;
+}
+
+static void flush_socket(void)
+{
+	int remote_sock = accept(listen_sock, NULL, NULL);
+	if (remote_sock != -1) {
+		if (num_remotes == 7) {
+			socket_close(remote_sock);
+		} else {
+			printf("remote %d connected\n", num_remotes);
+			uint8_t player = next_available_player();
+			remotes[num_remotes++] = (remote){
+				.sock = remote_sock,
+				.send_progress = NULL,
+				.players = {player},
+				.num_players = player == 0xFF ? 0 : 1
+			};
+			current_system->save_state = EVENTLOG_SLOT + 1;
+		}
+	}
+	uint8_t *min_progress = compressed;
+	for (int i = 0; i < num_remotes; i++) {
+		if (remotes[i].send_progress) {
+			uint8_t recv_buffer[1500];
+			int bytes = recv(remotes[i].sock, recv_buffer, sizeof(recv_buffer), 0);
+			for (int j = 0; j < bytes; j++)
+			{
+				uint8_t cmd = recv_buffer[j];
+				switch(cmd)
+				{
+				case CMD_GAMEPAD_DOWN:
+				case CMD_GAMEPAD_UP: {
+					++j;
+					if (j < bytes) {
+						uint8_t button = recv_buffer[j];
+						uint8_t pad = (button >> 5) - 1;
+						button &= 0x1F;
+						if (pad <  remotes[i].num_players) {
+							pad = remotes[i].players[pad];
+							if (cmd == CMD_GAMEPAD_DOWN) {
+								current_system->gamepad_down(current_system, pad, button);
+							} else {
+								current_system->gamepad_up(current_system, pad, button);
+							}
+						}
+					} else {
+						warning("Received incomplete command %X\n", cmd);
+					}
+					break;
+				}
+				default:
+					warning("Unrecognized remote command %X\n", cmd);
+					j = bytes;
+				}
+			}
+			int sent = 1;
+			while (sent && output_stream.next_out > remotes[i].send_progress)
+			{
+				sent = send(remotes[i].sock, remotes[i].send_progress, output_stream.next_out - remotes[i].send_progress, 0);
+				if (sent >= 0) {
+					remotes[i].send_progress += sent;
+				} else if (!socket_error_is_wouldblock()) {
+					socket_close(remotes[i].sock);
+					for (int j = 0; j < remotes[i].num_players; j++) {
+						available_players[num_available_players++] = remotes[i].players[j];
+					}
+					remotes[i] = remotes[num_remotes-1];
+					num_remotes--;
+					if (!num_remotes) {
+						//last remote disconnected, reset buffers/deflate
+						fully_active = 0;
+						deflateReset(&output_stream);
+						output_stream.next_out = compressed;
+						output_stream.avail_out = compressed_storage;
+						buffer.size = 0;
+					}
+					i--;
+					break;
+				}
+				if (remotes[i].send_progress > min_progress) {
+					min_progress = remotes[i].send_progress;
+				}
+			}
+		}
+	}
+	if (min_progress == output_stream.next_out) {
+		output_stream.next_out = compressed;
+		output_stream.avail_out = compressed_storage;
+		for (int i = 0; i < num_remotes; i++) {
+			if (remotes[i].send_progress) {
+				remotes[i].send_progress = compressed;
+			}
+		}
+	}
+}
+
+uint8_t wrote_since_last_flush;
+void event_log(uint8_t type, uint32_t cycle, uint8_t size, uint8_t *payload)
+{
+	if (!fully_active) {
+		return;
+	}
+	event_header(type, cycle);
+	last = cycle;
+	save_buffer8(&buffer, payload, size);
+	if (!multi_count) {
+		last_event_type = 0xFF;
+		output_stream.avail_in = buffer.size - (output_stream.next_in - buffer.data);
+		int result = deflate(&output_stream, Z_NO_FLUSH);
+		if (result != Z_OK) {
+			fatal_error("deflate returned %d\n", result);
+		}
+		if (listen_sock) {
+			if ((output_stream.next_out - compressed) > 1280 || !output_stream.avail_out) {
+				flush_socket();
+				wrote_since_last_flush = 1;
+			}
+		} else if (!output_stream.avail_out) {
+			fwrite(compressed, 1, compressed_storage, event_file);
+			output_stream.next_out = compressed;
+			output_stream.avail_out = compressed_storage;
+		}
+		if (!output_stream.avail_in) {
+			buffer.size = 0;
+			output_stream.next_in = buffer.data;
+		}
+	}
+}
+
+static uint32_t last_word_address;
+void event_vram_word(uint32_t cycle, uint32_t address, uint16_t value)
+{
+	uint32_t delta = address - last_word_address;
+	if (delta < 256) {
+		uint8_t buffer[3] = {delta, value >> 8, value};
+		event_log(EVENT_VRAM_WORD_DELTA, cycle, sizeof(buffer), buffer);
+	} else {
+		uint8_t buffer[5] = {address >> 16, address >> 8, address, value >> 8, value};
+		event_log(EVENT_VRAM_WORD, cycle, sizeof(buffer), buffer);
+	}
+	last_word_address = address;
+}
+
+static uint32_t last_byte_address;
+void event_vram_byte(uint32_t cycle, uint16_t address, uint8_t byte, uint8_t auto_inc)
+{
+	uint32_t delta = address - last_byte_address;
+	if (delta == 1) {
+		event_log(EVENT_VRAM_BYTE_ONE, cycle, sizeof(byte), &byte);
+	} else if (delta == auto_inc) {
+		event_log(EVENT_VRAM_BYTE_AUTO, cycle, sizeof(byte), &byte);
+	} else if (delta < 256) {
+		uint8_t buffer[2] = {delta, byte};
+		event_log(EVENT_VRAM_BYTE_DELTA, cycle, sizeof(buffer), buffer);
+	} else {
+		uint8_t buffer[3] = {address >> 8, address, byte};
+		event_log(EVENT_VRAM_BYTE, cycle, sizeof(buffer), buffer);
+	}
+	last_byte_address = address;
+}
+
+static size_t send_all(int sock, uint8_t *data, size_t size, int flags)
+{
+	size_t total = 0, sent = 1;
+	while(sent > 0 && total < size)
+	{
+		sent = send(sock, data + total, size - total, flags);
+		if (sent > 0) {
+			total += sent;
+		}
+	}
+	return total;
+}
+
+void deflate_flush(uint8_t full)
+{
+	output_stream.avail_in = buffer.size - (output_stream.next_in - buffer.data);
+	uint8_t force = full;
+	while (output_stream.avail_in || force)
+	{
+		if (!output_stream.avail_out) {
+			size_t old_storage = compressed_storage;
+			uint8_t *old_compressed = compressed;
+			compressed_storage *= 2;
+			compressed = realloc(compressed, compressed_storage);
+			output_stream.next_out = compressed + old_storage;
+			output_stream.avail_out = old_storage;
+			for (int i = 0; i < num_remotes; i++) {
+				if (remotes[i].send_progress) {
+					remotes[i].send_progress = compressed + (remotes[i].send_progress - old_compressed);
+				}
+			}
+		}
+		int result = deflate(&output_stream, full ? Z_FINISH : Z_SYNC_FLUSH);
+		if (result != (full ? Z_STREAM_END : Z_OK)) {
+			fatal_error("deflate returned %d\n", result);
+		}
+		if (full && result == Z_STREAM_END) {
+			result = deflateReset(&output_stream);
+			if (result != Z_OK) {
+				fatal_error("deflateReset returned %d\n", result);
+			}
+		}
+		force = 0;
+	}
+	output_stream.next_in = buffer.data;
+	buffer.size = 0;
+}
+
+void event_state(uint32_t cycle, serialize_buffer *state)
+{
+	if (!fully_active) {
+		last = cycle;
+	}
+	uint8_t header[] = {
+		EVENT_STATE << 4, last >> 24, last >> 16, last >> 8, last,
+		last_word_address >> 16, last_word_address >> 8, last_word_address,
+		last_byte_address >> 8, last_byte_address,
+		state->size >> 16, state->size >> 8, state->size
+	};
+	uint8_t sent_system_start = 0;
+	for (int i = 0; i < num_remotes; i++)
+	{
+		if (!remotes[i].send_progress) {
+			if (send_all(remotes[i].sock, system_start, system_start_size, 0) == system_start_size) {
+				sent_system_start = 1;
+			} else {
+				socket_close(remotes[i].sock);
+				remotes[i] = remotes[num_remotes-1];
+				num_remotes--;
+				i--;
+			}
+		}
+	}
+	if (sent_system_start) {
+		if (fully_active) {
+			if (multi_count) {
+				finish_multi();
+			}
+			//full flush is needed so new and old clients can share a stream
+			deflate_flush(1);
+		}
+		save_buffer8(&buffer, header, sizeof(header));
+		save_buffer8(&buffer, state->data, state->size);
+		size_t old_compressed_size = output_stream.next_out - compressed;
+		deflate_flush(1);
+		size_t state_size = output_stream.next_out - compressed - old_compressed_size;
+		for (int i = 0; i < num_remotes; i++) {
+			if (!remotes[i].send_progress) {
+				if (send_all(remotes[i].sock, compressed + old_compressed_size, state_size, 0) == state_size) {
+					remotes[i].send_progress = compressed + old_compressed_size;
+					socket_blocking(remotes[i].sock, 0);
+					int flag = 1;
+					setsockopt(remotes[i].sock, IPPROTO_TCP, TCP_NODELAY, (const char *)&flag, sizeof(flag));
+					fully_active = 1;
+				} else {
+					socket_close(remotes[i].sock);
+					remotes[i] = remotes[num_remotes-1];
+					num_remotes--;
+					i--;
+				}
+			}
+		}
+		output_stream.next_out = compressed + old_compressed_size;
+		output_stream.avail_out = compressed_storage - old_compressed_size;
+	}
+}
+
+void event_flush(uint32_t cycle)
+{
+	if (!active) {
+		return;
+	}
+	if (fully_active) {
+		event_header(EVENT_FLUSH, cycle);
+		last = cycle;
+		
+		deflate_flush(0);
+	}
+	if (event_file) {
+		fwrite(compressed, 1, output_stream.next_out - compressed, event_file);
+		fflush(event_file);
+		output_stream.next_out = compressed;
+		output_stream.avail_out = compressed_storage;
+	} else if (listen_sock) {
+		flush_socket();
+		wrote_since_last_flush = 0;
+	}
+}
+
+void event_soft_flush(uint32_t cycle)
+{
+	if (!fully_active || wrote_since_last_flush || event_file) {
+		return;
+	}
+	event_header(EVENT_FLUSH, cycle);
+	last = cycle;
+	
+	deflate_flush(0);
+	flush_socket();
+}
+
+static void init_event_reader_common(event_reader *reader)
+{
+	reader->last_cycle = 0;
+	reader->repeat_event = 0xFF;
+	reader->storage = 512 * 1024;
+	init_deserialize(&reader->buffer, malloc(reader->storage), reader->storage);
+	reader->buffer.size = 0;
+	memset(&reader->input_stream, 0, sizeof(reader->input_stream));
+	
+}
+
+void init_event_reader(event_reader *reader, uint8_t *data, size_t size)
+{
+	reader->socket = 0;
+	reader->last_cycle = 0;
+	reader->repeat_event = 0xFF;
+	init_event_reader_common(reader);
+	uint8_t name_len = data[1];
+	reader->buffer.size = name_len + 2;
+	memcpy(reader->buffer.data, data, reader->buffer.size);
+	reader->input_stream.next_in = data + reader->buffer.size;
+	reader->input_stream.avail_in = size - reader->buffer.size;
+	
+	int result = inflateInit(&reader->input_stream);
+	if (Z_OK != result) {
+		fatal_error("inflateInit returned %d\n", result);
+	}
+	reader->input_stream.next_out = reader->buffer.data + reader->buffer.size;
+	reader->input_stream.avail_out = reader->storage - reader->buffer.size;
+	result = inflate(&reader->input_stream, Z_NO_FLUSH);
+	if (Z_OK != result && Z_STREAM_END != result) {
+		fatal_error("inflate returned %d\n", result);
+	}
+	reader->buffer.size = reader->input_stream.next_out - reader->buffer.data;
+}
+
+void init_event_reader_tcp(event_reader *reader, char *address, char *port)
+{
+	struct addrinfo request, *result;
+	socket_init();
+	memset(&request, 0, sizeof(request));
+	request.ai_family = AF_INET;
+	request.ai_socktype = SOCK_STREAM;
+	request.ai_flags = AI_PASSIVE;
+	getaddrinfo(address, port, &request, &result);
+	
+	reader->socket = socket(result->ai_family, result->ai_socktype, result->ai_protocol);
+	if (reader->socket < 0) {
+		fatal_error("Failed to create socket for event log connection to %s:%s\n", address, port);
+	}
+	if (connect(reader->socket, result->ai_addr, result->ai_addrlen) < 0) {
+		fatal_error("Failed to connect to %s:%s for event log stream\n", address, port);
+	}
+	
+	init_event_reader_common(reader);
+	reader->socket_buffer_size = 256 * 1024;
+	reader->socket_buffer = malloc(reader->socket_buffer_size);
+	
+	while(reader->buffer.size < 3 || reader->buffer.size < 3 + reader->buffer.data[2])
+	{
+		int bytes = recv(reader->socket, reader->buffer.data + reader->buffer.size, reader->storage - reader->buffer.size, 0);
+		if (bytes < 0) {
+			fatal_error("Failed to receive system init from %s:%s\n", address, port);
+		}
+		reader->buffer.size += bytes;
+	}
+	size_t init_msg_len = 3 + reader->buffer.data[2];
+	memcpy(reader->socket_buffer, reader->buffer.data + init_msg_len, reader->buffer.size - init_msg_len);
+	reader->input_stream.next_in = reader->socket_buffer;
+	reader->input_stream.avail_in = reader->buffer.size - init_msg_len;
+	reader->buffer.size = init_msg_len;
+	int res = inflateInit(&reader->input_stream);
+	if (Z_OK != res) {
+		fatal_error("inflateInit returned %d\n", res);
+	}
+	reader->input_stream.next_out = reader->buffer.data + init_msg_len;
+	reader->input_stream.avail_out = reader->storage - init_msg_len;
+	res = inflate(&reader->input_stream, Z_NO_FLUSH);
+	if (Z_OK != res && Z_BUF_ERROR != res) {
+		fatal_error("inflate returned %d in init_event_reader_tcp\n", res);
+	}
+	int flag = 1;
+	setsockopt(reader->socket, IPPROTO_TCP, TCP_NODELAY, (const char *)&flag, sizeof(flag));
+}
+
+static void read_from_socket(event_reader *reader)
+{
+	if (reader->socket_buffer_size - reader->input_stream.avail_in < 128 * 1024) {
+		reader->socket_buffer_size *= 2;
+		uint8_t *new_buf = malloc(reader->socket_buffer_size);
+		memcpy(new_buf, reader->input_stream.next_in, reader->input_stream.avail_in);
+		free(reader->socket_buffer);
+		reader->socket_buffer = new_buf;
+		reader->input_stream.next_in = new_buf;
+	} else if (
+		reader->input_stream.next_in - reader->socket_buffer >= reader->input_stream.avail_in 
+		&& reader->input_stream.next_in - reader->socket_buffer + reader->input_stream.avail_in >= reader->socket_buffer_size/2
+	) {
+		memmove(reader->socket_buffer, reader->input_stream.next_in, reader->input_stream.avail_in);
+		reader->input_stream.next_in = reader->socket_buffer;
+	}
+	uint8_t *space_start = reader->input_stream.next_in + reader->input_stream.avail_in;
+	size_t space = (reader->socket_buffer + reader->socket_buffer_size) - space_start;
+	int bytes = recv(reader->socket, space_start, space, 0);
+	if (bytes >= 0) {
+		reader->input_stream.avail_in += bytes;
+	} else if (!socket_error_is_wouldblock()) {
+		fatal_error("Connection closed, error = %X\n", socket_last_error());
+	}
+}
+
+static void inflate_flush(event_reader *reader)
+{
+	if (reader->buffer.cur_pos > reader->storage / 2) {
+		memmove(reader->buffer.data, reader->buffer.data + reader->buffer.cur_pos, reader->buffer.size - reader->buffer.cur_pos);
+		reader->buffer.size -= reader->buffer.cur_pos;
+		reader->buffer.cur_pos = 0;
+		reader->input_stream.next_out = reader->buffer.data + reader->buffer.size;
+		reader->input_stream.avail_out = reader->storage - reader->buffer.size;
+	}
+	int result = inflate(&reader->input_stream, Z_SYNC_FLUSH);
+	if (Z_OK != result && Z_STREAM_END != result) {
+		fatal_error("inflate returned %d\n", result);
+	}
+	reader->buffer.size = reader->input_stream.next_out - reader->buffer.data;
+	if (result == Z_STREAM_END && (reader->socket || reader->input_stream.avail_in)) {
+		inflateReset(&reader->input_stream);
+		if (reader->input_stream.avail_in) {
+			inflate_flush(reader);
+		}
+	}
+	
+}
+
+void reader_ensure_data(event_reader *reader, size_t bytes)
+{
+	if (reader->buffer.size - reader->buffer.cur_pos < bytes) {
+		if (reader->input_stream.avail_in) {
+			inflate_flush(reader);
+		}
+		if (reader->socket) {
+			while (reader->buffer.size - reader->buffer.cur_pos < bytes) {
+				read_from_socket(reader);
+				inflate_flush(reader);
+			}
+		}
+	}
+}
+
+uint8_t reader_next_event(event_reader *reader, uint32_t *cycle_out)
+{
+	if (reader->repeat_remaining) {
+		reader->repeat_remaining--;
+		*cycle_out = reader->last_cycle + reader->repeat_delta;
+		reader->last_cycle = *cycle_out;
+		return reader->repeat_event;
+	}
+	reader_ensure_data(reader, 1);
+	uint8_t header = load_int8(&reader->buffer);
+	uint8_t ret;
+	uint32_t delta;
+	uint8_t multi_start = 0;
+	if ((header & 0xF0) == (EVENT_MULTI << 4)) {
+		reader->repeat_remaining = (header & 0xF) + 1;
+		multi_start = 1;
+		reader_ensure_data(reader, 1);
+		header = load_int8(&reader->buffer);
+	}
+	if ((header & 0xF0) < FORMAT_3BYTE) {
+		delta = (header & 0xF) + 16;
+		ret = header >> 4;
+	} else if ((header & 0xF0) == FORMAT_3BYTE) {
+		reader_ensure_data(reader, 2);
+		delta = load_int16(&reader->buffer);
+		ret = header & 0xF;
+	} else {
+		reader_ensure_data(reader, 3);
+		delta = load_int8(&reader->buffer) << 16;
+		//sign extend 24-bit delta to 32-bit
+		if (delta & 0x800000) {
+			delta |= 0xFF000000;
+		}
+		delta |= load_int16(&reader->buffer);
+		ret = header & 0xF;
+	}
+	if (multi_start) {
+		reader->repeat_event = ret;
+		reader->repeat_delta = delta;
+	}
+	*cycle_out = reader->last_cycle + delta;
+	reader->last_cycle = *cycle_out;
+	if (ret == EVENT_ADJUST) {
+		reader_ensure_data(reader, 4);
+		size_t old_pos = reader->buffer.cur_pos;
+		uint32_t adjust = load_int32(&reader->buffer);
+		reader->buffer.cur_pos = old_pos;
+		reader->last_cycle -= adjust;
+	} else if (ret == EVENT_STATE) {
+		reader_ensure_data(reader, 8);
+		reader->last_cycle = load_int32(&reader->buffer);
+		reader->last_word_address = load_int8(&reader->buffer) << 16;
+		reader->last_word_address |= load_int16(&reader->buffer);
+		reader->last_byte_address = load_int16(&reader->buffer);
+	}
+	return ret;
+}
+
+uint8_t reader_system_type(event_reader *reader)
+{
+	return load_int8(&reader->buffer);
+}
+
+void reader_send_gamepad_event(event_reader *reader, uint8_t pad, uint8_t button, uint8_t down)
+{
+	uint8_t buffer[] = {down ? CMD_GAMEPAD_DOWN : CMD_GAMEPAD_UP, pad << 5 | button};
+	//TODO: Deal with the fact that we're not in blocking mode so this may not actually send all
+	//if the buffer is full
+	send_all(reader->socket, buffer, sizeof(buffer), 0);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/event_log.h	Sat May 09 23:39:44 2020 -0700
@@ -0,0 +1,60 @@
+#ifndef EVENT_LOG_H_
+#define EVENT_LOG_H_
+
+enum {
+	EVENT_FLUSH = 0,
+	EVENT_ADJUST = 1,
+	EVENT_PSG_REG = 2,
+	EVENT_YM_REG = 3,
+	EVENT_VDP_REG = 4,
+	EVENT_VRAM_BYTE = 5,
+	EVENT_VRAM_BYTE_DELTA = 6,
+	EVENT_VRAM_BYTE_ONE = 7,
+	EVENT_VRAM_BYTE_AUTO = 8,
+	EVENT_VRAM_WORD = 9,
+	EVENT_VRAM_WORD_DELTA = 10,
+	EVENT_VDP_INTRAM = 11,
+	EVENT_STATE = 12,
+	EVENT_MULTI = 13
+	//14 and 15 are reserved for header types
+};
+
+#include "serialize.h"
+#include "zlib/zlib.h"
+typedef struct {
+	size_t storage;
+	uint8_t *socket_buffer;
+	size_t socket_buffer_size;
+	int socket;
+	uint32_t last_cycle;
+	uint32_t last_word_address;
+	uint32_t last_byte_address;
+	uint32_t repeat_delta;
+	deserialize_buffer buffer;
+	z_stream input_stream;
+	uint8_t repeat_event;
+	uint8_t repeat_remaining;
+} event_reader;
+
+#include "system.h"
+#include "render.h"
+
+void event_log_file(char *fname);
+void event_log_tcp(char *address, char *port);
+void event_system_start(system_type stype, vid_std video_std, char *name);
+void event_cycle_adjust(uint32_t cycle, uint32_t deduction);
+void event_log(uint8_t type, uint32_t cycle, uint8_t size, uint8_t *payload);
+void event_vram_word(uint32_t cycle, uint32_t address, uint16_t value);
+void event_vram_byte(uint32_t cycle, uint16_t address, uint8_t byte, uint8_t auto_inc);
+void event_state(uint32_t cycle, serialize_buffer *state);
+void event_flush(uint32_t cycle);
+void event_soft_flush(uint32_t cycle);
+
+void init_event_reader(event_reader *reader, uint8_t *data, size_t size);
+void init_event_reader_tcp(event_reader *reader, char *address, char *port);
+uint8_t reader_next_event(event_reader *reader, uint32_t *cycle_out);
+void reader_ensure_data(event_reader *reader, size_t bytes);
+uint8_t reader_system_type(event_reader *reader);
+void reader_send_gamepad_event(event_reader *reader, uint8_t pad, uint8_t button, uint8_t down);
+
+#endif //EVENT_LOG_H_
--- a/gdb_remote.c	Sun Apr 19 00:59:49 2020 -0700
+++ b/gdb_remote.c	Sat May 09 23:39:44 2020 -0700
@@ -18,13 +18,13 @@
 #define GDB_OUT_FD STDOUT_FILENO
 #define GDB_READ read
 #define GDB_WRITE write
+#include <unistd.h>
 #endif
 
 #include "gdb_remote.h"
 #include "68kinst.h"
 #include "debug.h"
 #include "util.h"
-#include <unistd.h>
 #include <fcntl.h>
 #include <stddef.h>
 #include <stdlib.h>
@@ -132,22 +132,10 @@
 	}
 }
 
-static uint8_t m68k_read_byte(m68k_context * context, uint32_t address)
+static uint8_t m68k_read_byte(m68k_context *context, uint32_t address)
 {
-	
-	genesis_context *gen = context->system;
-	//TODO: Use generated read/write functions to support access to hardware that is not ROM or RAM
-	uint16_t * word = get_native_pointer(address & 0xFFFFFFFE, (void **)context->mem_pointers, &context->options->gen);
-	if (word) {	
-	if (address & 1) {
-		return *word;
-	}
-	return *word >> 8;
-}
-	if (address >= 0xA00000 && address < 0xA04000) {
-		return gen->zram[address & 0x1FFF];
-	}
-	return 0;
+	//TODO: share this implementation with builtin debugger
+	return read_byte(address, (void **)context->mem_pointers, &context->options->gen, context);
 }
 
 static void m68k_write_byte(m68k_context * context, uint32_t address, uint8_t value)
@@ -401,6 +389,10 @@
 			gdb_send_command("m1");
 		} else if (!strcmp("sThreadInfo", command + 1)) {
 			gdb_send_command("l");
+		} else if (!memcmp("ThreadExtraInfo", command+1, strlen("ThreadExtraInfo"))) {
+			gdb_send_command("");
+		} else if (command[1] == 'P') {
+			gdb_send_command("");
 		} else {
 			goto not_impl;
 		}
@@ -558,21 +550,13 @@
 	}
 }
 
-#ifdef _WIN32
-void gdb_cleanup(void)
-{
-	WSACleanup();
-}
-WSADATA wsa_data;
-#endif
-
 void gdb_remote_init(void)
 {
 	buf = malloc(INITIAL_BUFFER_SIZE);
 	curbuf = NULL;
 	bufsize = INITIAL_BUFFER_SIZE;
 #ifdef _WIN32
-	WSAStartup(MAKEWORD(2,2), &wsa_data);
+	socket_init();
 
 	struct addrinfo request, *result;
 	memset(&request, 0, sizeof(request));
@@ -588,6 +572,7 @@
 	if (bind(listen_sock, result->ai_addr, result->ai_addrlen) < 0) {
 		fatal_error("Failed to bind GDB remote debugging socket");
 	}
+	freeaddrinfo(result);
 	if (listen(listen_sock, 1) < 0) {
 		fatal_error("Failed to listen on GDB remote debugging socket");
 	}
@@ -595,7 +580,7 @@
 	if (gdb_sock < 0) {
 		fatal_error("accept returned an error while listening on GDB remote debugging socket");
 	}
-	closesocket(listen_sock);
+	socket_close(listen_sock);
 #else
 	disable_stdout_messages();
 #endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gen_player.c	Sat May 09 23:39:44 2020 -0700
@@ -0,0 +1,172 @@
+#include "gen_player.h"
+#include "event_log.h"
+#include "render.h"
+
+#ifdef IS_LIB
+#define MAX_SOUND_CYCLES (MCLKS_PER_YM*NUM_OPERATORS*6*4)
+#else
+#define MAX_SOUND_CYCLES 100000	
+#endif
+
+static void sync_sound(gen_player *gen, uint32_t target)
+{
+	//printf("YM | Cycle: %d, bpos: %d, PSG | Cycle: %d, bpos: %d\n", gen->ym->current_cycle, gen->ym->buffer_pos, gen->psg->cycles, gen->psg->buffer_pos * 2);
+	while (target > gen->psg->cycles && target - gen->psg->cycles > MAX_SOUND_CYCLES) {
+		uint32_t cur_target = gen->psg->cycles + MAX_SOUND_CYCLES;
+		//printf("Running PSG to cycle %d\n", cur_target);
+		psg_run(gen->psg, cur_target);
+		//printf("Running YM-2612 to cycle %d\n", cur_target);
+		ym_run(gen->ym, cur_target);
+	}
+	psg_run(gen->psg, target);
+	ym_run(gen->ym, target);
+
+	//printf("Target: %d, YM bufferpos: %d, PSG bufferpos: %d\n", target, gen->ym->buffer_pos, gen->psg->buffer_pos * 2);
+}
+
+static void run(gen_player *player)
+{
+	while(player->reader.socket || player->reader.buffer.cur_pos < player->reader.buffer.size)
+	{
+		uint32_t cycle;
+		uint8_t event = reader_next_event(&player->reader, &cycle);
+		switch (event)
+		{
+		case EVENT_FLUSH:
+			sync_sound(player, cycle);
+			vdp_run_context(player->vdp, cycle);
+			break;
+		case EVENT_ADJUST: {
+			sync_sound(player, cycle);
+			vdp_run_context(player->vdp, cycle);
+			uint32_t deduction = load_int32(&player->reader.buffer);
+			ym_adjust_cycles(player->ym, deduction);
+			vdp_adjust_cycles(player->vdp, deduction);
+			player->psg->cycles -= deduction;
+			break;
+		case EVENT_PSG_REG:
+			sync_sound(player, cycle);
+			reader_ensure_data(&player->reader, 1);
+			psg_write(player->psg, load_int8(&player->reader.buffer));
+			break;
+		case EVENT_YM_REG: {
+			sync_sound(player, cycle);
+			reader_ensure_data(&player->reader, 3);
+			uint8_t part = load_int8(&player->reader.buffer);
+			uint8_t reg = load_int8(&player->reader.buffer);
+			uint8_t value = load_int8(&player->reader.buffer);
+			if (part) {
+				ym_address_write_part2(player->ym, reg);
+			} else {
+				ym_address_write_part1(player->ym, reg);
+			}
+			ym_data_write(player->ym, value);
+			break;
+		case EVENT_STATE: {
+			reader_ensure_data(&player->reader, 3);
+			uint32_t size = load_int8(&player->reader.buffer) << 16;
+			size |= load_int16(&player->reader.buffer);
+			reader_ensure_data(&player->reader, size);
+			deserialize_buffer buffer;
+			init_deserialize(&buffer, player->reader.buffer.data + player->reader.buffer.cur_pos, size);
+			register_section_handler(&buffer, (section_handler){.fun = vdp_deserialize, .data = player->vdp}, SECTION_VDP);
+			register_section_handler(&buffer, (section_handler){.fun = ym_deserialize, .data = player->ym}, SECTION_YM2612);
+			register_section_handler(&buffer, (section_handler){.fun = psg_deserialize, .data = player->psg}, SECTION_PSG);
+			while (buffer.cur_pos < buffer.size)
+			{
+				load_section(&buffer);
+			}
+			player->reader.buffer.cur_pos += size;
+			free(buffer.handlers);
+			break;
+		}
+		default:
+			vdp_run_context(player->vdp, cycle);
+			vdp_replay_event(player->vdp, event, &player->reader);
+		}
+		}
+			
+		}
+		if (!player->reader.socket) {
+			reader_ensure_data(&player->reader, 1);
+		}
+	}
+}
+
+static int thread_main(void *player)
+{
+	run(player);
+	return 0;
+}
+
+void start_context(system_header *sys, char *statefile)
+{
+	gen_player *player = (gen_player *)sys;
+	if (player->reader.socket) {
+		render_create_thread(&player->thread, "player", thread_main, player);
+	} else {
+		run(player);
+	}
+}
+
+static void gamepad_down(system_header *system, uint8_t gamepad_num, uint8_t button)
+{
+	gen_player *player = (gen_player *)system;
+	reader_send_gamepad_event(&player->reader, gamepad_num, button, 1);
+}
+
+static void gamepad_up(system_header *system, uint8_t gamepad_num, uint8_t button)
+{
+	gen_player *player = (gen_player *)system;
+	reader_send_gamepad_event(&player->reader, gamepad_num, button, 0);
+}
+
+#define MCLKS_NTSC 53693175
+#define MCLKS_PAL  53203395
+#define MCLKS_PER_YM  7
+#define MCLKS_PER_Z80 15
+#define MCLKS_PER_PSG (MCLKS_PER_Z80*16)
+
+static void config_common(gen_player *player)
+{
+	uint8_t vid_std = load_int8(&player->reader.buffer);
+	uint8_t name_len = load_int8(&player->reader.buffer);
+	player->header.info.name = calloc(1, name_len + 1);
+	load_buffer8(&player->reader.buffer, player->header.info.name, name_len);
+	
+	player->vdp = init_vdp_context(vid_std == VID_PAL, 0);
+	render_set_video_standard(vid_std);
+	uint32_t master_clock = vid_std == VID_NTSC ? MCLKS_NTSC : MCLKS_PAL;
+	
+	player->ym = malloc(sizeof(ym2612_context));
+	ym_init(player->ym, master_clock, MCLKS_PER_YM, 0);
+	
+	player->psg = malloc(sizeof(psg_context));
+	psg_init(player->psg, master_clock, MCLKS_PER_PSG);
+	
+	player->header.start_context = start_context;
+	player->header.gamepad_down = gamepad_down;
+	player->header.gamepad_up = gamepad_up;
+	player->header.type = SYSTEM_GENESIS_PLAYER;
+	player->header.info.save_type = SAVE_NONE;
+}
+
+gen_player *alloc_config_gen_player(void *stream, uint32_t rom_size)
+{
+	uint8_t *data = stream;
+	gen_player *player = calloc(1, sizeof(gen_player));
+	init_event_reader(&player->reader, data + 9, rom_size - 9);
+	config_common(player);
+	return player;
+}
+
+gen_player *alloc_config_gen_player_reader(event_reader *reader)
+{
+	gen_player *player = calloc(1, sizeof(gen_player));
+	player->reader = *reader;
+	inflateCopy(&player->reader.input_stream, &reader->input_stream);
+	render_set_external_sync(1);
+	config_common(player);
+	return player;
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gen_player.h	Sat May 09 23:39:44 2020 -0700
@@ -0,0 +1,24 @@
+#ifndef GEN_PLAYER_H_
+#define GEN_PLAYER_H_
+
+#include "render.h"
+#include "system.h"
+#include "vdp.h"
+#include "psg.h"
+#include "ym2612.h"
+#include "event_log.h"
+
+typedef struct {
+	system_header   header;
+	
+	vdp_context     *vdp;
+	ym2612_context  *ym;
+	psg_context     *psg;
+	render_thread   thread;
+	event_reader    reader;
+} gen_player;
+
+gen_player *alloc_config_gen_player(void *stream, uint32_t rom_size);
+gen_player *alloc_config_gen_player_reader(event_reader *reader);
+
+#endif //GEN_PLAYER_H_
--- a/gen_x86.c	Sun Apr 19 00:59:49 2020 -0700
+++ b/gen_x86.c	Sat May 09 23:39:44 2020 -0700
@@ -171,7 +171,7 @@
 };
 
 #ifdef X86_64
-#define CHECK_DISP(disp) (disp <= 0x7FFFFFFF && disp >= -2147483648)
+#define CHECK_DISP(disp) (disp <= ((ptrdiff_t)INT32_MAX) && disp >= ((ptrdiff_t)INT32_MIN))
 #else
 #define CHECK_DISP(disp) 1
 #endif
@@ -1261,7 +1261,7 @@
 	check_alloc_code(code, 14);
 	code_ptr out = code->cur;
 	uint8_t sign_extend = 0;
-	if (size == SZ_Q && val <= 0x7FFFFFFF && val >= -2147483648) {
+	if (size == SZ_Q && val <= ((int64_t)INT32_MAX) && val >= ((int64_t)INT32_MIN)) {
 		sign_extend = 1;
 	}
 	if (size == SZ_W) {
--- a/genesis.c	Sun Apr 19 00:59:49 2020 -0700
+++ b/genesis.c	Sat May 09 23:39:44 2020 -0700
@@ -19,6 +19,7 @@
 #include "bindings.h"
 #include "jcart.h"
 #include "config.h"
+#include "event_log.h"
 #define MCLKS_NTSC 53693175
 #define MCLKS_PAL  53203395
 
@@ -50,15 +51,17 @@
 #define Z80_OPTS options
 #endif
 
-void genesis_serialize(genesis_context *gen, serialize_buffer *buf, uint32_t m68k_pc)
+void genesis_serialize(genesis_context *gen, serialize_buffer *buf, uint32_t m68k_pc, uint8_t all)
 {
-	start_section(buf, SECTION_68000);
-	m68k_serialize(gen->m68k, m68k_pc, buf);
-	end_section(buf);
-	
-	start_section(buf, SECTION_Z80);
-	z80_serialize(gen->z80, buf);
-	end_section(buf);
+	if (all) {
+		start_section(buf, SECTION_68000);
+		m68k_serialize(gen->m68k, m68k_pc, buf);
+		end_section(buf);
+		
+		start_section(buf, SECTION_Z80);
+		z80_serialize(gen->z80, buf);
+		end_section(buf);
+	}
 	
 	start_section(buf, SECTION_VDP);
 	vdp_serialize(gen->vdp, buf);
@@ -72,35 +75,37 @@
 	psg_serialize(gen->psg, buf);
 	end_section(buf);
 	
-	start_section(buf, SECTION_GEN_BUS_ARBITER);
-	save_int8(buf, gen->z80->reset);
-	save_int8(buf, gen->z80->busreq);
-	save_int16(buf, gen->z80_bank_reg);
-	end_section(buf);
-	
-	start_section(buf, SECTION_SEGA_IO_1);
-	io_serialize(gen->io.ports, buf);
-	end_section(buf);
-	
-	start_section(buf, SECTION_SEGA_IO_2);
-	io_serialize(gen->io.ports + 1, buf);
-	end_section(buf);
-	
-	start_section(buf, SECTION_SEGA_IO_EXT);
-	io_serialize(gen->io.ports + 2, buf);
-	end_section(buf);
-	
-	start_section(buf, SECTION_MAIN_RAM);
-	save_int8(buf, RAM_WORDS * 2 / 1024);
-	save_buffer16(buf, gen->work_ram, RAM_WORDS);
-	end_section(buf);
-	
-	start_section(buf, SECTION_SOUND_RAM);
-	save_int8(buf, Z80_RAM_BYTES / 1024);
-	save_buffer8(buf, gen->zram, Z80_RAM_BYTES);
-	end_section(buf);
-	
-	cart_serialize(&gen->header, buf);
+	if (all) {
+		start_section(buf, SECTION_GEN_BUS_ARBITER);
+		save_int8(buf, gen->z80->reset);
+		save_int8(buf, gen->z80->busreq);
+		save_int16(buf, gen->z80_bank_reg);
+		end_section(buf);
+		
+		start_section(buf, SECTION_SEGA_IO_1);
+		io_serialize(gen->io.ports, buf);
+		end_section(buf);
+		
+		start_section(buf, SECTION_SEGA_IO_2);
+		io_serialize(gen->io.ports + 1, buf);
+		end_section(buf);
+		
+		start_section(buf, SECTION_SEGA_IO_EXT);
+		io_serialize(gen->io.ports + 2, buf);
+		end_section(buf);
+		
+		start_section(buf, SECTION_MAIN_RAM);
+		save_int8(buf, RAM_WORDS * 2 / 1024);
+		save_buffer16(buf, gen->work_ram, RAM_WORDS);
+		end_section(buf);
+		
+		start_section(buf, SECTION_SOUND_RAM);
+		save_int8(buf, Z80_RAM_BYTES / 1024);
+		save_buffer8(buf, gen->zram, Z80_RAM_BYTES);
+		end_section(buf);
+		
+		cart_serialize(&gen->header, buf);
+	}
 }
 
 static uint8_t *serialize(system_header *sys, size_t *size_out)
@@ -120,7 +125,7 @@
 		init_serialize(&state);
 		uint32_t address = read_word(4, (void **)gen->m68k->mem_pointers, &gen->m68k->options->gen, gen->m68k) << 16;
 		address |= read_word(6, (void **)gen->m68k->mem_pointers, &gen->m68k->options->gen, gen->m68k);
-		genesis_serialize(gen, &state, address);
+		genesis_serialize(gen, &state, address, 1);
 		if (size_out) {
 			*size_out = state.size;
 		}
@@ -351,9 +356,6 @@
 	//printf("Target: %d, YM bufferpos: %d, PSG bufferpos: %d\n", target, gen->ym->buffer_pos, gen->psg->buffer_pos * 2);
 }
 
-//TODO: move this inside the system context
-static uint32_t last_frame_num;
-
 //My refresh emulation isn't currently good enough and causes more problems than it solves
 #define REFRESH_EMULATION
 #ifdef REFRESH_EMULATION
@@ -392,9 +394,11 @@
 		context->should_return = 1;
 		gen->reset_cycle = CYCLE_NEVER;
 	}
-	if (v_context->frame != last_frame_num) {
-		//printf("reached frame end %d | MCLK Cycles: %d, Target: %d, VDP cycles: %d, vcounter: %d, hslot: %d\n", last_frame_num, mclks, gen->frame_end, v_context->cycles, v_context->vcounter, v_context->hslot);
-		last_frame_num = v_context->frame;
+	if (v_context->frame != gen->last_frame) {
+		//printf("reached frame end %d | MCLK Cycles: %d, Target: %d, VDP cycles: %d, vcounter: %d, hslot: %d\n", gen->last_frame, mclks, gen->frame_end, v_context->cycles, v_context->vcounter, v_context->hslot);
+		gen->last_frame = v_context->frame;
+		event_flush(mclks);
+		gen->last_flush_cycle = mclks;
 
 		if(exit_after){
 			--exit_after;
@@ -421,7 +425,12 @@
 			if (gen->reset_cycle != CYCLE_NEVER) {
 				gen->reset_cycle -= deduction;
 			}
+			event_cycle_adjust(mclks, deduction);
+			gen->last_flush_cycle -= deduction;
 		}
+	} else if (mclks - gen->last_flush_cycle > gen->soft_flush_cycles) {
+		event_soft_flush(mclks);
+		gen->last_flush_cycle = mclks;
 	}
 	gen->frame_end = vdp_cycles_to_frame_end(v_context);
 	context->sync_cycle = gen->frame_end;
@@ -461,9 +470,9 @@
 				}
 			}
 #endif
-			char *save_path = slot == SERIALIZE_SLOT ? NULL : get_slot_name(&gen->header, slot, use_native_states ? "state" : "gst");
+			char *save_path = slot >= SERIALIZE_SLOT ? NULL : get_slot_name(&gen->header, slot, use_native_states ? "state" : "gst");
 #ifndef NEW_CORE
-			if (use_native_states || slot == SERIALIZE_SLOT) {
+			if (use_native_states || slot >= SERIALIZE_SLOT) {
 #endif
 				serialize_buffer state;
 				init_serialize(&state);
@@ -473,6 +482,8 @@
 					gen->serialize_size = state.size;
 					context->sync_cycle = context->current_cycle;
 					context->should_return = 1;
+				} else if (slot == EVENTLOG_SLOT) {
+					event_state(context->current_cycle, &state);
 				} else {
 					save_to_file(&state, save_path);
 					free(state.data);
@@ -482,7 +493,9 @@
 				save_gst(gen, save_path, address);
 			}
 #endif
-			printf("Saved state to %s\n", save_path);
+			if (slot != SERIALIZE_SLOT) {
+				debug_message("Saved state to %s\n", save_path);
+			}
 			free(save_path);
 		} else if(gen->header.save_state) {
 			context->sync_cycle = context->current_cycle + 1;
@@ -644,11 +657,11 @@
 	uint16_t value;
 #ifdef REFRESH_EMULATION
 	if (context->current_cycle - 4*MCLKS_PER_68K > last_sync_cycle) {
-		//do refresh check here so we can avoid adding a penalty for a refresh that happens during a VDP access
-		refresh_counter += context->current_cycle - 4*MCLKS_PER_68K - last_sync_cycle;
-		context->current_cycle += REFRESH_DELAY * MCLKS_PER_68K * (refresh_counter / (MCLKS_PER_68K * REFRESH_INTERVAL));
-		refresh_counter = refresh_counter % (MCLKS_PER_68K * REFRESH_INTERVAL);
-		last_sync_cycle = context->current_cycle;
+	//do refresh check here so we can avoid adding a penalty for a refresh that happens during a VDP access
+	refresh_counter += context->current_cycle - 4*MCLKS_PER_68K - last_sync_cycle;
+	context->current_cycle += REFRESH_DELAY * MCLKS_PER_68K * (refresh_counter / (MCLKS_PER_68K * REFRESH_INTERVAL));
+	refresh_counter = refresh_counter % (MCLKS_PER_68K * REFRESH_INTERVAL);
+	last_sync_cycle = context->current_cycle;
 	}
 #endif
 	genesis_context *gen = context->system;
@@ -1198,8 +1211,10 @@
 	
 	if (region & HZ50) {
 		gen->normal_clock = MCLKS_PAL;
+		gen->soft_flush_cycles = MCLKS_LINE * 262 / 3 + 2;
 	} else {
 		gen->normal_clock = MCLKS_NTSC;
+		gen->soft_flush_cycles = MCLKS_LINE * 313 / 3 + 2;
 	}
 	gen->master_clock = gen->normal_clock;
 }
@@ -1265,7 +1280,7 @@
 			resume_68k(gen->m68k);
 		}
 	}
-	if (render_should_release_on_exit()) {
+	if (gen->header.force_release || render_should_release_on_exit()) {
 		bindings_release_capture();
 		vdp_release_framebuffer(gen->vdp);
 		render_pause_source(gen->ym->audio);
@@ -1321,7 +1336,8 @@
 static void resume_genesis(system_header *system)
 {
 	genesis_context *gen = (genesis_context *)system;
-	if (render_should_release_on_exit()) {
+	if (gen->header.force_release || render_should_release_on_exit()) {
+		gen->header.force_release = 0;
 		render_set_video_standard((gen->version_reg & HZ50) ? VID_PAL : VID_NTSC);
 		bindings_reacquire_capture();
 		vdp_reacquire_framebuffer(gen->vdp);
@@ -1565,8 +1581,13 @@
 	gen->int_latency_prev2 = MCLKS_PER_68K * 16;
 	
 	render_set_video_standard((gen->version_reg & HZ50) ? VID_PAL : VID_NTSC);
+	event_system_start(SYSTEM_GENESIS, (gen->version_reg & HZ50) ? VID_PAL : VID_NTSC, rom->name);
 	
 	gen->ym = malloc(sizeof(ym2612_context));
+	char *fm = tern_find_ptr_default(model, "fm", "discrete 2612");
+	if (!strcmp(fm + strlen(fm) -4, "3834")) {
+		system_opts |= YM_OPT_3834;
+	}
 	ym_init(gen->ym, gen->master_clock, MCLKS_PER_YM, system_opts);
 
 	gen->psg = malloc(sizeof(psg_context));
@@ -1676,6 +1697,7 @@
 			gen->bank_regs[i] = i;
 		}
 	}
+	gen->reset_cycle = CYCLE_NEVER;
 
 	return gen;
 }
--- a/genesis.h	Sun Apr 19 00:59:49 2020 -0700
+++ b/genesis.h	Sat May 09 23:39:44 2020 -0700
@@ -51,6 +51,9 @@
 	uint32_t        int_latency_prev1;
 	uint32_t        int_latency_prev2;
 	uint32_t        reset_cycle;
+	uint32_t        last_frame;
+	uint32_t        last_flush_cycle;
+	uint32_t        soft_flush_cycles;
 	uint8_t         bank_regs[8];
 	uint16_t        z80_bank_reg;
 	uint16_t        tmss_lock[2];
@@ -71,7 +74,7 @@
 
 m68k_context * sync_components(m68k_context *context, uint32_t address);
 genesis_context *alloc_config_genesis(void *rom, uint32_t rom_size, void *lock_on, uint32_t lock_on_size, uint32_t system_opts, uint8_t force_region);
-void genesis_serialize(genesis_context *gen, serialize_buffer *buf, uint32_t m68k_pc);
+void genesis_serialize(genesis_context *gen, serialize_buffer *buf, uint32_t m68k_pc, uint8_t all);
 void genesis_deserialize(deserialize_buffer *buf, genesis_context *gen);
 
 #endif //GENESIS_H_
--- a/libblastem.c	Sun Apr 19 00:59:49 2020 -0700
+++ b/libblastem.c	Sat May 09 23:39:44 2020 -0700
@@ -381,7 +381,7 @@
 		last_height = height;
 	}
 	retro_video_refresh(fb + overscan_left + LINEBUF_SIZE * overscan_top, width, height, LINEBUF_SIZE * sizeof(uint32_t));
-	current_system->request_exit(current_system);
+	system_request_exit(current_system, 0);
 }
 
 uint8_t render_get_active_framebuffer(void)
--- a/m68k.cpu	Sun Apr 19 00:59:49 2020 -0700
+++ b/m68k.cpu	Sat May 09 23:39:44 2020 -0700
@@ -401,6 +401,8 @@
 	m68k_prefetch
 	
 1101DDD1ZZMMMRRR add_dn_ea
+	invalid M 0
+	invalid M 1
 	invalid M 7 R 2
 	invalid M 7 R 3
 	invalid M 7 R 4
@@ -439,6 +441,7 @@
 00000110ZZMMMRRR addi
 	local immed 32
 	invalid Z 3
+	invalid M 1
 	invalid M 7 R 2
 	invalid M 7 R 3
 	invalid M 7 R 4
@@ -578,7 +581,286 @@
 	mov aregs.D scratch2
 	m68k_write_size Z 0
 	m68k_prefetch
+
+1100DDD0ZZMMMRRR and_ea_dn
+	invalid M 1
+	invalid M 7 R 5
+	invalid M 7 R 6
+	invalid M 7 R 7
+	invalid Z 3
+	m68k_fetch_src_ea M R Z
 	
+	and src dregs.D dregs.D Z
+	update_flags NZV0C0
+	m68k_prefetch
+	
+1100DDD1ZZMMMRRR and_dn_ea
+	invalid M 0
+	invalid M 1
+	invalid M 7 R 2
+	invalid M 7 R 3
+	invalid M 7 R 4
+	invalid M 7 R 5
+	invalid M 7 R 6
+	invalid M 7 R 7
+	invalid Z 3
+	m68k_fetch_dst_ea M R Z
+	
+	and dregs.D dst dst Z
+	update_flags NZV0C0
+	m68k_save_dst Z
+	m68k_prefetch
+	
+00000010ZZMMMRRR andi
+	local immed 32
+	invalid Z 3
+	invalid M 1
+	invalid M 7 R 2
+	invalid M 7 R 3
+	invalid M 7 R 4
+	invalid M 7 R 5
+	invalid M 7 R 6
+	invalid M 7 R 7
+	#fetch immediate operand
+	m68k_prefetch
+	switch Z
+	case 2
+		lsl prefetch 16 immed
+		m68k_prefetch
+		or prefetch immed immed
+	default
+		mov prefetch immed
+	end
+	#fetch dst EA
+	m68k_fetch_dst_ea M R Z
+	
+	and immed dst dst Z
+	update_flags NZV0C0
+	m68k_save_dst Z
+	m68k_prefetch
+
+0000001000111100 andi_to_ccr
+	#fetch immediate operand
+	m68k_prefetch
+	and prefetch ccr ccr
+	m68k_prefetch
+	
+1011DDD1ZZMMMRRR eor_dn_ea
+	invalid M 1
+	invalid M 7 R 2
+	invalid M 7 R 3
+	invalid M 7 R 4
+	invalid M 7 R 5
+	invalid M 7 R 6
+	invalid M 7 R 7
+	invalid Z 3
+	m68k_fetch_dst_ea M R Z
+	
+	xor dregs.D dst dst Z
+	update_flags NZV0C0
+	m68k_save_dst Z
+	m68k_prefetch
+
+00001010ZZMMMRRR eori
+	local immed 32
+	invalid Z 3
+	invalid M 1
+	invalid M 7 R 2
+	invalid M 7 R 3
+	invalid M 7 R 4
+	invalid M 7 R 5
+	invalid M 7 R 6
+	invalid M 7 R 7
+	#fetch immediate operand
+	m68k_prefetch
+	switch Z
+	case 2
+		lsl prefetch 16 immed
+		m68k_prefetch
+		or prefetch immed immed
+	default
+		mov prefetch immed
+	end
+	#fetch dst EA
+	m68k_fetch_dst_ea M R Z
+	
+	xor immed dst dst Z
+	update_flags NZV0C0
+	m68k_save_dst Z
+	m68k_prefetch
+	
+0000001000111100 eori_to_ccr
+	#fetch immediate operand
+	m68k_prefetch
+	xor prefetch ccr ccr
+	m68k_prefetch
+	
+1000DDD0ZZMMMRRR or_ea_dn
+	invalid M 1
+	invalid M 7 R 5
+	invalid M 7 R 6
+	invalid M 7 R 7
+	invalid Z 3
+	m68k_fetch_src_ea M R Z
+	
+	or src dregs.D dregs.D Z
+	update_flags NZV0C0
+	m68k_prefetch
+	
+1000DDD1ZZMMMRRR or_dn_ea
+	invalid M 0
+	invalid M 1
+	invalid M 7 R 2
+	invalid M 7 R 3
+	invalid M 7 R 4
+	invalid M 7 R 5
+	invalid M 7 R 6
+	invalid M 7 R 7
+	invalid Z 3
+	m68k_fetch_dst_ea M R Z
+	
+	or dregs.D dst dst Z
+	update_flags NZV0C0
+	m68k_save_dst Z
+	m68k_prefetch
+	
+00000000ZZMMMRRR ori
+	local immed 32
+	invalid Z 3
+	invalid M 1
+	invalid M 7 R 2
+	invalid M 7 R 3
+	invalid M 7 R 4
+	invalid M 7 R 5
+	invalid M 7 R 6
+	invalid M 7 R 7
+	#fetch immediate operand
+	m68k_prefetch
+	switch Z
+	case 2
+		lsl prefetch 16 immed
+		m68k_prefetch
+		or prefetch immed immed
+	default
+		mov prefetch immed
+	end
+	#fetch dst EA
+	m68k_fetch_dst_ea M R Z
+	
+	or immed dst dst Z
+	update_flags NZV0C0
+	m68k_save_dst Z
+	m68k_prefetch
+
+0000000000111100 ori_to_ccr
+	#fetch immediate operand
+	m68k_prefetch
+	or prefetch ccr ccr
+	m68k_prefetch
+	
+1001DDD0ZZMMMRRR sub_ea_dn
+	invalid M 7 R 5
+	invalid M 7 R 6
+	invalid M 7 R 7
+	invalid Z 3
+	m68k_fetch_src_ea M R Z
+	
+	sub src dregs.D dregs.D Z
+	update_flags XNZVC
+	m68k_prefetch
+	
+1001DDD1ZZMMMRRR sub_dn_ea
+	invalid M 0
+	invalid M 1
+	invalid M 7 R 2
+	invalid M 7 R 3
+	invalid M 7 R 4
+	invalid M 7 R 5
+	invalid M 7 R 6
+	invalid M 7 R 7
+	invalid Z 3
+	m68k_fetch_dst_ea M R Z
+	
+	sub dregs.D dst dst Z
+	update_flags XNZVC
+	m68k_save_dst Z
+	m68k_prefetch
+
+1001AAAZ11MMMRRR suba
+	invalid M 7 R 5
+	invalid M 7 R 6
+	invalid M 7 R 7
+	local size 16
+	local ext_src 32
+	if Z
+	mov 2 size
+	else
+	mov 1 size
+	end
+	m68k_fetch_src_ea M R size
+	switch size
+	case 1
+	sext 32 src ext_src
+	meta src ext_src
+	end
+	
+	sub src aregs.A aregs.A
+	m68k_prefetch
+
+00000100ZZMMMRRR subi
+	local immed 32
+	invalid Z 3
+	invalid M 1
+	invalid M 7 R 2
+	invalid M 7 R 3
+	invalid M 7 R 4
+	invalid M 7 R 5
+	invalid M 7 R 6
+	invalid M 7 R 7
+	#fetch immediate operand
+	m68k_prefetch
+	switch Z
+	case 2
+		lsl prefetch 16 immed
+		m68k_prefetch
+		or prefetch immed immed
+	default
+		mov prefetch immed
+	end
+	#fetch dst EA
+	m68k_fetch_dst_ea M R Z
+	
+	sub immed dst dst Z
+	update_flags XNZVC
+	m68k_save_dst Z
+	m68k_prefetch
+	
+0101III1ZZMMMRRR subq
+	invalid Z 3
+	invalid M 7 R 2
+	invalid M 7 R 3
+	invalid M 7 R 4
+	invalid M 7 R 5
+	invalid M 7 R 6
+	invalid M 7 R 7
+	local src 32
+	switch I
+	case 0
+	mov 8 src
+	default
+	mov I src
+	end
+	
+	m68k_fetch_dst_ea M R Z
+	switch M
+	case 1
+		sub src dst dst Z
+	default
+		sub src dst dst Z
+		update_flags XNZVC
+	end
+	m68k_save_dst Z
+	m68k_prefetch
 
 00ZZRRRMMMEEESSS move
 	invalid Z 0
--- a/m68k_core_x86.c	Sun Apr 19 00:59:49 2020 -0700
+++ b/m68k_core_x86.c	Sat May 09 23:39:44 2020 -0700
@@ -1315,8 +1315,6 @@
 			numcycles = 6;
 		} else if (inst->op == M68K_AND && inst->variant == VAR_IMMEDIATE) {
 			numcycles = 6;
-		} else if (inst->op == M68K_ADD && inst->dst.addr_mode == MODE_AREG && inst->extra.size == OPSIZE_WORD && inst->variant == VAR_QUICK) {
-			numcycles = 4;
 		} else if (inst->dst.addr_mode <= MODE_AREG) {
 			numcycles = inst->src.addr_mode <= MODE_AREG || inst->src.addr_mode == MODE_IMMEDIATE ? 8 : 6;
 		} else {
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/m68k_util.c	Sat May 09 23:39:44 2020 -0700
@@ -0,0 +1,89 @@
+#include <string.h>
+
+void m68k_read_8(m68k_context *context)
+{
+	context->cycles += 4 * context->opts->gen.clock_divider;
+	context->scratch1 = read_byte(context->scratch1, context->mem_pointers, &context->opts->gen, context);
+}
+
+void m68k_read_16(m68k_context *context)
+{
+	context->cycles += 4 * context->opts->gen.clock_divider;
+	context->scratch1 = read_word(context->scratch1, context->mem_pointers, &context->opts->gen, context);
+}
+
+void m68k_write_8(m68k_context *context)
+{
+	context->cycles += 4 * context->opts->gen.clock_divider;
+	write_byte(context->scratch2, context->scratch1, context->mem_pointers, &context->opts->gen, context);
+}
+
+void m68k_write_16(m68k_context *context)
+{
+	context->cycles += 4 * context->opts->gen.clock_divider;
+	write_word(context->scratch2, context->scratch1, context->mem_pointers, &context->opts->gen, context);
+}
+
+void m68k_sync_cycle(m68k_context *context, uint32_t target_cycle)
+{
+	//TODO: interrupt stuff
+	context->sync_cycle = target_cycle;
+}
+
+void init_m68k_opts(m68k_options *opts, memmap_chunk * memmap, uint32_t num_chunks, uint32_t clock_divider)
+{
+	memset(opts, 0, sizeof(*opts));
+	opts->gen.memmap = memmap;
+	opts->gen.memmap_chunks = num_chunks;
+	opts->gen.address_mask = 0xFFFFFF;
+	opts->gen.byte_swap = 1;
+	opts->gen.max_address = 0x1000000;
+	opts->gen.bus_cycles = 4;
+	opts->gen.clock_divider = clock_divider;
+}
+
+m68k_context *init_68k_context(m68k_options * opts, m68k_reset_handler reset_handler)
+{
+	m68k_context *context = calloc(1, sizeof(m68k_context));
+	context->opts = opts;
+	context->reset_handler = reset_handler;
+	context->int_cycle = 0xFFFFFFFFU;
+	return context;
+}
+
+void m68k_reset(m68k_context *context)
+{
+	//read initial SP
+	context->scratch1 = 0;
+	m68k_read_16(context);
+	context->aregs[7] = context->scratch1 << 16;
+	context->scratch1 = 2;
+	m68k_read_16(context);
+	context->aregs[7] |= context->scratch1;
+	
+	//read initial PC
+	context->scratch1 = 4;
+	m68k_read_16(context);
+	context->pc = context->scratch1 << 16;
+	context->scratch1 = 6;
+	m68k_read_16(context);
+	context->pc |= context->scratch1;
+	
+	context->scratch1 = context->pc;
+	m68k_read_16(context);
+	context->prefetch = context->scratch1;
+	context->pc += 2;
+	
+	context->status = 0x27;
+}
+
+void m68k_print_regs(m68k_context *context)
+{
+	printf("XNZVC\n%d%d%d%d%d\n", context->xflag != 0, context->nflag != 0, context->zflag != 0, context->vflag != 0, context->cflag != 0);
+	for (int i = 0; i < 8; i++) {
+		printf("d%d: %X\n", i, context->dregs[i]);
+	}
+	for (int i = 0; i < 8; i++) {
+		printf("a%d: %X\n", i, context->aregs[i]);
+	}
+}
--- a/megawifi.c	Sun Apr 19 00:59:49 2020 -0700
+++ b/megawifi.c	Sat May 09 23:39:44 2020 -0700
@@ -6,15 +6,28 @@
 #define WINVER 0x501
 #include <winsock2.h>
 #include <ws2tcpip.h>
+#include <sys/param.h>
 #else
 #include <sys/socket.h>
+#include <arpa/inet.h>
 #include <unistd.h>
 #include <netinet/in.h>
+#include <netdb.h>
 #endif
 #include <errno.h>
 #include <fcntl.h>
+#include <time.h>
 #include "genesis.h"
 #include "net.h"
+#include "util.h"
+
+#ifdef _WIN32
+#  if BYTE_ORDER == LITTLE_ENDIAN
+#define htobe64(val)   ((((uint64_t)htonl((val)&0xFFFFFFFF))<<32) | htonl((val)>>32))
+#  else
+#define htobe64(val)	(val)
+#  endif
+#endif
 
 enum {
 	TX_IDLE,
@@ -25,7 +38,7 @@
 };
 #define STX 0x7E
 #define ETX 0x7E
-#define MAX_RECV_SIZE 1440
+#define MAX_RECV_SIZE 1460
 
 #define E(N) N
 enum {
@@ -43,7 +56,7 @@
 #define MSG_NOSIGNAL 0
 #endif
 
-enum {
+enum mw_state {
 	STATE_IDLE=1,
 	STATE_AP_JOIN,
 	STATE_SCAN,
@@ -51,6 +64,21 @@
 	STATE_TRANSPARENT
 };
 
+enum {
+	SOCKST_NONE = 0,
+	SOCKST_TCP_LISTEN,
+	SOCKST_TCP_EST,
+	SOCKST_UDP_READY
+};
+
+// TCP/UDP address message
+struct mw_addr_msg {
+	char dst_port[6];
+	char src_port[6];
+	uint8_t channel;
+	char host[];
+};
+
 #define FLAG_ONLINE 
 
 typedef struct {
@@ -68,6 +96,7 @@
 	uint8_t  flags;
 	uint8_t  transmit_buffer[4096];
 	uint8_t  receive_buffer[4096];
+	struct sockaddr_in remote_addr[15];	// Needed for UDP sockets
 } megawifi;
 
 static megawifi *get_megawifi(void *context)
@@ -75,11 +104,12 @@
 	m68k_context *m68k = context;
 	genesis_context *gen = m68k->system;
 	if (!gen->extra) {
+		socket_init();
 		gen->extra = calloc(1, sizeof(megawifi));
 		megawifi *mw = gen->extra;
 		mw->module_state = STATE_IDLE;
-		for (int i = 0; i < 15; i++)
-		{
+		mw->flags = 0xE0; // cfg_ok, dt_ok, online
+		for (int i = 0; i < 15; i++) {
 			mw->sock_fds[i] = -1;
 		}
 	}
@@ -112,14 +142,88 @@
 	mw->receive_bytes += count;
 }
 
-static void mw_puts(megawifi *mw, char *s)
+static void mw_puts(megawifi *mw, const char *s)
+{
+	size_t len = strlen(s);
+	mw_copy(mw, (uint8_t*)s, len);
+}
+
+static void udp_recv(megawifi *mw, uint8_t idx)
 {
-	uint32_t len = strlen(s);
-	if ((mw->receive_bytes + len) > sizeof(mw->receive_buffer)) {
-		return;
+	ssize_t recvd;
+	int s = mw->sock_fds[idx];
+	struct sockaddr_in remote;
+	socklen_t addr_len = sizeof(struct sockaddr_in);
+
+	if (mw->remote_addr[idx].sin_addr.s_addr != htonl(INADDR_ANY)) {
+		// Receive only from specified address
+		recvd = recvfrom(s, (char*)mw->receive_buffer + 3, MAX_RECV_SIZE, 0,
+				(struct sockaddr*)&remote, &addr_len);
+		if (recvd > 0) {
+			if (remote.sin_addr.s_addr != mw->remote_addr[idx].sin_addr.s_addr) {
+				printf("Discarding UDP packet from unknown addr %s:%d\n",
+						inet_ntoa(remote.sin_addr), ntohs(remote.sin_port));
+				recvd = 0;
+			}
+		}
+	} else {
+		// Reuse mode, data is preceded by remote IPv4 and port
+		recvd = recvfrom(s, (char*)mw->receive_buffer + 9, MAX_RECV_SIZE - 6,
+				0, (struct sockaddr*)&remote, &addr_len);
+		if (recvd > 0) {
+			mw->receive_buffer[3] = remote.sin_addr.s_addr;
+			mw->receive_buffer[4] = remote.sin_addr.s_addr>>8;
+			mw->receive_buffer[5] = remote.sin_addr.s_addr>>16;
+			mw->receive_buffer[6] = remote.sin_addr.s_addr>>24;
+			mw->receive_buffer[7] = remote.sin_port;
+			mw->receive_buffer[8] = remote.sin_port>>8;
+			recvd += 6;
+		}
 	}
-	memcpy(mw->receive_buffer + mw->receive_bytes, s, len);
-	mw->receive_bytes += len;
+
+	if (recvd > 0) {
+		mw_putc(mw, STX);
+		mw_putc(mw, (recvd >> 8) | ((idx+1) << 4));
+		mw_putc(mw, recvd);
+		mw->receive_bytes += recvd;
+		mw_putc(mw, ETX);
+		//should this set the channel flag?
+	} else if (recvd < 0 && !socket_error_is_wouldblock()) {
+		socket_close(mw->sock_fds[idx]);
+		mw->channel_state[idx] = SOCKST_NONE;
+		mw->channel_flags |= 1 << (idx + 1);
+	}
+}
+
+static void udp_send(megawifi *mw, uint8_t idx)
+{
+	struct sockaddr_in remote;
+	int s = mw->sock_fds[idx];
+	int sent;
+	char *data = (char*)mw->transmit_buffer;
+
+	if (mw->remote_addr[idx].sin_addr.s_addr != htonl(INADDR_ANY)) {
+		sent = sendto(s, data, mw->transmit_bytes, 0, (struct sockaddr*)&mw->remote_addr[idx],
+				sizeof(struct sockaddr_in));
+	} else {
+		// Reuse mode, extract address from leading bytes
+		// NOTE: mw->remote_addr[idx].sin_addr.s_addr == INADDR_ANY
+		remote.sin_addr.s_addr = *((int32_t*)data);
+		remote.sin_port = *((int16_t*)(data + 4));
+		remote.sin_family = AF_INET;
+		memset(remote.sin_zero, 0, sizeof(remote.sin_zero));
+		sent = sendto(s, data + 6, mw->transmit_bytes - 6, 0, (struct sockaddr*)&remote,
+				sizeof(struct sockaddr_in)) + 6;
+	}
+	if (sent < 0 && !socket_error_is_wouldblock()) {
+		socket_close(s);
+		mw->sock_fds[idx] = -1;
+		mw->channel_state[idx] = SOCKST_NONE;
+		mw->channel_flags |= 1 << (idx + 1);
+	} else if (sent < mw->transmit_bytes) {
+		//TODO: save this data somewhere so it can be sent in poll_socket
+		printf("Sent %d bytes on channel %d, but %d were requested\n", sent, idx + 1, mw->transmit_bytes);
+	}
 }
 
 static void poll_socket(megawifi *mw, uint8_t channel)
@@ -127,28 +231,25 @@
 	if (mw->sock_fds[channel] < 0) {
 		return;
 	}
-	if (mw->channel_state[channel] == 1) {
+	if (mw->channel_state[channel] == SOCKST_TCP_LISTEN) {
 		int res = accept(mw->sock_fds[channel], NULL, NULL);
 		if (res >= 0) {
-			close(mw->sock_fds[channel]);
-#ifndef _WIN32
-//FIXME: Set nonblocking on Windows too
-			fcntl(res, F_SETFL, O_NONBLOCK);
-#endif
+			socket_close(mw->sock_fds[channel]);
+			socket_blocking(res, 0);
 			mw->sock_fds[channel] = res;
-			mw->channel_state[channel] = 2;
+			mw->channel_state[channel] = SOCKST_TCP_EST;
 			mw->channel_flags |= 1 << (channel + 1);
 		} else if (errno != EAGAIN && errno != EWOULDBLOCK) {
-			close(mw->sock_fds[channel]);
-			mw->channel_state[channel] = 0;
+			socket_close(mw->sock_fds[channel]);
+			mw->channel_state[channel] = SOCKST_NONE;
 			mw->channel_flags |= 1 << (channel + 1);
 		}
-	} else if (mw->channel_state[channel] == 2 && mw->receive_bytes < sizeof(mw->receive_buffer) - 4) {
+	} else if (mw->channel_state[channel] == SOCKST_TCP_EST && mw->receive_bytes < (sizeof(mw->receive_buffer) - 4)) {
 		size_t max = sizeof(mw->receive_buffer) - 4 - mw->receive_bytes;
 		if (max > MAX_RECV_SIZE) {
 			max = MAX_RECV_SIZE;
 		}
-		int bytes = recv(mw->sock_fds[channel], mw->receive_buffer + mw->receive_bytes + 3, max, 0);
+		int bytes = recv(mw->sock_fds[channel], (char*)(mw->receive_buffer + mw->receive_bytes + 3), max, 0);
 		if (bytes > 0) {
 			mw_putc(mw, STX);
 			mw_putc(mw, bytes >> 8 | (channel+1) << 4);
@@ -156,11 +257,13 @@
 			mw->receive_bytes += bytes;
 			mw_putc(mw, ETX);
 			//should this set the channel flag?
-		} else if (bytes < 0 && errno != EAGAIN && errno != EWOULDBLOCK) {
-			close(mw->sock_fds[channel]);
-			mw->channel_state[channel] = 0;
+		} else if (bytes < 0 && !socket_error_is_wouldblock()) {
+			socket_close(mw->sock_fds[channel]);
+			mw->channel_state[channel] = SOCKST_NONE;
 			mw->channel_flags |= 1 << (channel + 1);
 		}
+	} else if (mw->channel_state[channel] == SOCKST_UDP_READY && !mw->receive_bytes) {
+		udp_recv(mw, channel);
 	}
 }
 
@@ -172,6 +275,7 @@
 	}
 }
 
+
 static void start_reply(megawifi *mw, uint8_t cmd)
 {
 	mw_putc(mw, STX);
@@ -197,152 +301,443 @@
 	mw_putc(mw, ETX);
 }
 
-static void process_packet(megawifi *mw)
+static void cmd_ap_cfg_get(megawifi *mw)
+{
+	char ssid[32] = {0};
+	char pass[64] = {0};
+	uint8_t slot = mw->transmit_buffer[4];
+
+	sprintf(ssid, "BLASTEM! SSID %d", slot + 1);
+	sprintf(pass, "BLASTEM! PASS %d", slot + 1);
+	start_reply(mw, CMD_OK);
+	mw_putc(mw, slot);
+	mw_putc(mw, 7);	/// 11bgn
+	mw_copy(mw, (uint8_t*)ssid, 32);
+	mw_copy(mw, (uint8_t*)pass, 64);
+	end_reply(mw);
+}
+
+static void cmd_ip_cfg_get(megawifi *mw)
+{
+	uint32_t ipv4s[5] = {0};
+
+	start_reply(mw, CMD_OK);
+	mw_putc(mw, mw->transmit_buffer[4]);
+	mw_putc(mw, 0);
+	mw_putc(mw, 0);
+	mw_putc(mw, 0);
+	mw_copy(mw, (uint8_t*)ipv4s, sizeof(ipv4s));
+	end_reply(mw);
+}
+
+static void cmd_tcp_con(megawifi *mw, uint32_t size)
+{
+	struct mw_addr_msg *addr = (struct mw_addr_msg*)(mw->transmit_buffer + 4);
+	struct addrinfo hints;
+	struct addrinfo *res = NULL;
+	int s;
+	int err;
+
+	uint8_t channel = addr->channel;
+	if (!channel || channel > 15 || mw->sock_fds[channel - 1] >= 0) {
+		start_reply(mw, CMD_ERROR);
+		end_reply(mw);
+		return;
+	}
+	channel--;
+
+	memset(&hints, 0, sizeof(hints));
+	hints.ai_family = AF_INET;
+#ifndef _WIN32
+	hints.ai_flags = AI_NUMERICSERV;
+#endif
+	hints.ai_socktype = SOCK_STREAM;
+
+	if ((err = getaddrinfo(addr->host, addr->dst_port, &hints, &res)) != 0) {
+		printf("getaddrinfo failed: %s\n", gai_strerror(err));
+		start_reply(mw, CMD_ERROR);
+		end_reply(mw);
+		return;
+	}
+
+	s = socket(AF_INET, SOCK_STREAM, 0);
+	if (s < 0) {
+		goto err;
+	}
+
+	// Should this be handled in a separate thread to avoid blocking emulation?
+	if (connect(s, res->ai_addr, res->ai_addrlen) != 0) {
+		goto err;
+	}
+
+	socket_blocking(s, 0);
+	mw->sock_fds[channel] = s;
+	mw->channel_state[channel] = SOCKST_TCP_EST;
+	mw->channel_flags |= 1 << (channel + 1);
+	printf("Connection established on ch %d with %s:%s\n", channel + 1,
+			addr->host, addr->dst_port);
+
+	if (res) {
+		freeaddrinfo(res);
+	}
+	start_reply(mw, CMD_OK);
+	end_reply(mw);
+	return;
+
+err:
+	freeaddrinfo(res);
+	printf("Connection to %s:%s failed, %s\n", addr->host, addr->dst_port, strerror(errno));
+	start_reply(mw, CMD_ERROR);
+	end_reply(mw);
+}
+
+static void cmd_close(megawifi *mw)
+{
+	int channel = mw->transmit_buffer[4] - 1;
+
+	if (channel >= 15 || mw->sock_fds[channel] < 0) {
+		start_reply(mw, CMD_ERROR);
+		end_reply(mw);
+		return;
+	}
+
+	socket_close(mw->sock_fds[channel]);
+	mw->sock_fds[channel] = -1;
+	mw->channel_state[channel] = SOCKST_NONE;
+	mw->channel_flags |= 1 << (channel + 1);
+	start_reply(mw, CMD_OK);
+	end_reply(mw);
+}
+
+static void cmd_udp_set(megawifi *mw)
 {
-	if (mw->transmit_channel == 0) {
-		uint32_t command = mw->transmit_buffer[0] << 8 | mw->transmit_buffer[1];
-		uint32_t size = mw->transmit_buffer[2] << 8 | mw->transmit_buffer[3];
-		if (size > mw->transmit_bytes - 4) {
-			size = mw->transmit_bytes - 4;
+	struct mw_addr_msg *addr = (struct mw_addr_msg*)(mw->transmit_buffer + 4);
+	unsigned int local_port, remote_port;
+	int s;
+	struct addrinfo *raddr;
+	struct addrinfo hints;
+	struct sockaddr_in local;
+	int err;
+
+	uint8_t channel = addr->channel;
+	if (!channel || channel > 15 || mw->sock_fds[channel - 1] >= 0) {
+		goto err;
+	}
+	channel--;
+	local_port = atoi(addr->src_port);
+	remote_port = atoi(addr->dst_port);
+
+	if ((s = socket(PF_INET, SOCK_DGRAM, 0)) < 0) {
+		printf("Datagram socket creation failed\n");
+		goto err;
+	}
+
+	memset(local.sin_zero, 0, sizeof(local.sin_zero));
+	local.sin_family = AF_INET;
+	local.sin_addr.s_addr = htonl(INADDR_ANY);
+	local.sin_port = htons(local_port);
+	if (remote_port && addr->host[0]) {
+		// Communication with remote peer
+		printf("Set UDP ch %d, port %d to addr %s:%d\n", addr->channel,
+				local_port, addr->host, remote_port);
+
+		memset(&hints, 0, sizeof(hints));
+		hints.ai_family = AF_INET;
+#ifndef _WIN32
+		hints.ai_flags = AI_NUMERICSERV;
+#endif
+		hints.ai_socktype = SOCK_DGRAM;
+
+		if ((err = getaddrinfo(addr->host, addr->dst_port, &hints, &raddr)) != 0) {
+			printf("getaddrinfo failed: %s\n", gai_strerror(err));
+			goto err;
 		}
-		int orig_receive_bytes = mw->receive_bytes;
-		switch (command)
-		{
-		case CMD_VERSION:
+		mw->remote_addr[channel] = *((struct sockaddr_in*)raddr->ai_addr);
+		freeaddrinfo(raddr);
+	} else if (local_port) {
+		// Server in reuse mode
+		printf("Set UDP ch %d, src port %d\n", addr->channel, local_port);
+		mw->remote_addr[channel] = local;
+	} else {
+		printf("Invalid UDP socket data\n");
+		goto err;
+	}
+
+	if (bind(s, (struct sockaddr*)&local, sizeof(struct sockaddr_in)) < 0) {
+		printf("bind to port %d failed\n", local_port);
+		goto err;
+	}
+
+	socket_blocking(s, 0);
+	mw->sock_fds[channel] = s;
+	mw->channel_state[channel] = SOCKST_UDP_READY;
+	mw->channel_flags |= 1 << (channel + 1);
+
+	start_reply(mw, CMD_OK);
+	end_reply(mw);
+
+	return;
+
+err:
+	start_reply(mw, CMD_ERROR);
+	end_reply(mw);
+}
+
+#define AVATAR_BYTES	(32 * 48 / 2)
+static void cmd_gamertag_get(megawifi *mw)
+{
+	uint32_t id = htonl(1);
+	char buf[AVATAR_BYTES];
+
+	start_reply(mw, CMD_OK);
+	// TODO Get items from config file
+	mw_copy(mw, (uint8_t*)&id, 4);
+	strncpy(buf, "doragasu on Blastem!", 32);
+	mw_copy(mw, (uint8_t*)buf, 32);
+	strncpy(buf, "My cool password", 32);
+	mw_copy(mw, (uint8_t*)buf, 32);
+	strncpy(buf, "All your WiFi are belong to me!", 32);
+	mw_copy(mw, (uint8_t*)buf, 32);
+	memset(buf, 0, 64); // Telegram token
+	mw_copy(mw, (uint8_t*)buf, 64);
+	mw_copy(mw, (uint8_t*)buf, AVATAR_BYTES); // Avatar tiles
+	mw_copy(mw, (uint8_t*)buf, 32); // Avatar palette
+	end_reply(mw);
+}
+
+static void cmd_hrng_get(megawifi *mw)
+{
+	uint16_t len = (mw->transmit_buffer[4]<<8) + mw->transmit_buffer[5];
+	if (len > (MAX_RECV_SIZE - 4)) {
+		start_reply(mw, CMD_ERROR);
+		end_reply(mw);
+		return;
+	}
+	// Pseudo-random, but who cares
+	start_reply(mw, CMD_OK);
+	srand(time(NULL));
+	for (uint16_t i = 0; i < len; i++) {
+		mw_putc(mw, rand());
+	}
+	end_reply(mw);
+}
+
+static void cmd_datetime(megawifi *mw)
+{
+	start_reply(mw, CMD_OK);
+#ifdef _WIN32
+	__time64_t t = _time64(NULL);
+	int64_t t_be = htobe64(t);
+	mw_copy(mw, (uint8_t*)&t_be, sizeof(int64_t));
+	mw_puts(mw, _ctime64(&t));
+#else
+	time_t t = time(NULL);
+	int64_t t_be = htobe64(t);
+	mw_copy(mw, (uint8_t*)&t_be, sizeof(int64_t));
+	mw_puts(mw, ctime(&t));
+#endif
+
+	mw_putc(mw, '\0');
+	end_reply(mw);
+}
+
+static void process_command(megawifi *mw)
+{
+	uint32_t command = mw->transmit_buffer[0] << 8 | mw->transmit_buffer[1];
+	uint32_t size = mw->transmit_buffer[2] << 8 | mw->transmit_buffer[3];
+	if (size > mw->transmit_bytes - 4) {
+		size = mw->transmit_bytes - 4;
+	}
+	int orig_receive_bytes = mw->receive_bytes;
+	switch (command)
+	{
+	case CMD_VERSION:
+		start_reply(mw, CMD_OK);
+		mw_putc(mw, 1);
+		mw_putc(mw, 3);
+		mw_putc(mw, 0);
+		mw_puts(mw, "blastem");
+		mw_putc(mw, '\0');
+		end_reply(mw);
+		break;
+	case CMD_ECHO:
+		mw->receive_bytes = mw->transmit_bytes;
+		memcpy(mw->receive_buffer, mw->transmit_buffer, mw->transmit_bytes);
+		break;
+	case CMD_AP_CFG_GET:
+		cmd_ap_cfg_get(mw);
+		break;
+	case CMD_IP_CURRENT: {
+		iface_info i;
+		if (get_host_address(&i)) {
 			start_reply(mw, CMD_OK);
-			mw_putc(mw, 1);
-			mw_putc(mw, 0);
-			mw_puts(mw, "blastem");
-			end_reply(mw);
-			break;
-		case CMD_ECHO:
-			mw->receive_bytes = mw->transmit_bytes;
-			memcpy(mw->receive_buffer, mw->transmit_buffer, mw->transmit_bytes);
-			break;
-		case CMD_IP_CURRENT: {
-			iface_info i;
-			if (get_host_address(&i)) {
-				start_reply(mw, CMD_OK);
-				//config number and reserved bytes
-				mw_set(mw, 0, 4);
-				//ip
-				mw_copy(mw, i.ip, sizeof(i.ip));
-				//net mask
-				mw_copy(mw, i.net_mask, sizeof(i.net_mask));
-				//gateway guess
-				mw_putc(mw, i.ip[0] & i.net_mask[0]);
-				mw_putc(mw, i.ip[1] & i.net_mask[1]);
-				mw_putc(mw, i.ip[2] & i.net_mask[2]);
-				mw_putc(mw, (i.ip[3] & i.net_mask[3]) + 1);
-				//dns
-				static const uint8_t localhost[] = {127,0,0,1};
-				mw_copy(mw, localhost, sizeof(localhost));
-				mw_copy(mw, localhost, sizeof(localhost));
-				
-			} else {
-				start_reply(mw, CMD_ERROR);
-			}
+			//config number and reserved bytes
+			mw_set(mw, 0, 4);
+			//ip
+			mw_copy(mw, i.ip, sizeof(i.ip));
+			//net mask
+			mw_copy(mw, i.net_mask, sizeof(i.net_mask));
+			//gateway guess
+			mw_putc(mw, i.ip[0] & i.net_mask[0]);
+			mw_putc(mw, i.ip[1] & i.net_mask[1]);
+			mw_putc(mw, i.ip[2] & i.net_mask[2]);
+			mw_putc(mw, (i.ip[3] & i.net_mask[3]) + 1);
+			//dns
+			static const uint8_t localhost[] = {127,0,0,1};
+			mw_copy(mw, localhost, sizeof(localhost));
+			mw_copy(mw, localhost, sizeof(localhost));
+			
+		} else {
+			start_reply(mw, CMD_ERROR);
+		}
+		end_reply(mw);
+		break;
+	}
+	case CMD_IP_CFG_GET:
+		cmd_ip_cfg_get(mw);
+		break;
+	case CMD_DEF_AP_CFG_GET:
+		start_reply(mw, CMD_OK);
+		mw_putc(mw, 0);
+		end_reply(mw);
+		break;
+	case CMD_AP_JOIN:
+		mw->module_state = STATE_READY;
+		start_reply(mw, CMD_OK);
+		end_reply(mw);
+		break;
+	case CMD_TCP_CON:
+		cmd_tcp_con(mw, size);
+		break;
+	case CMD_TCP_BIND:{
+		if (size < 7){
+			start_reply(mw, CMD_ERROR);
 			end_reply(mw);
 			break;
 		}
-		case CMD_AP_JOIN:
-			mw->module_state = STATE_READY;
-			start_reply(mw, CMD_OK);
+		uint8_t channel = mw->transmit_buffer[10];
+		if (!channel || channel > 15) {
+			start_reply(mw, CMD_ERROR);
+			end_reply(mw);
+			break;
+		}
+		channel--;
+		if (mw->sock_fds[channel] >= 0) {
+			socket_close(mw->sock_fds[channel]);
+		}
+		mw->sock_fds[channel] = socket(AF_INET, SOCK_STREAM, 0);
+		if (mw->sock_fds[channel] < 0) {
+			start_reply(mw, CMD_ERROR);
 			end_reply(mw);
 			break;
-		case CMD_TCP_BIND:{
-			if (size < 7){
-				start_reply(mw, CMD_ERROR);
-				end_reply(mw);
-				break;
-			}
-			uint8_t channel = mw->transmit_buffer[10];
-			if (!channel || channel > 15) {
-				start_reply(mw, CMD_ERROR);
-				end_reply(mw);
-				break;
-			}
-			channel--;
-			if (mw->sock_fds[channel] >= 0) {
-				close(mw->sock_fds[channel]);
-			}
-			mw->sock_fds[channel] = socket(AF_INET, SOCK_STREAM, 0);
-			if (mw->sock_fds[channel] < 0) {
-				start_reply(mw, CMD_ERROR);
-				end_reply(mw);
-				break;
-			}
-			int value = 1;
-			setsockopt(mw->sock_fds[channel], SOL_SOCKET, SO_REUSEADDR, &value, sizeof(value));
-			struct sockaddr_in bind_addr;
-			memset(&bind_addr, 0, sizeof(bind_addr));
-			bind_addr.sin_family = AF_INET;
-			bind_addr.sin_port = htons(mw->transmit_buffer[8] << 8 | mw->transmit_buffer[9]);
-			if (bind(mw->sock_fds[channel], (struct sockaddr *)&bind_addr, sizeof(bind_addr)) != 0) {
-				close(mw->sock_fds[channel]);
-				mw->sock_fds[channel] = -1;
-				start_reply(mw, CMD_ERROR);
-				end_reply(mw);
-				break;
-			}
-			int res = listen(mw->sock_fds[channel], 2);
-			start_reply(mw, res ? CMD_ERROR : CMD_OK);
-			if (res) {
-				close(mw->sock_fds[channel]);
-				mw->sock_fds[channel] = -1;
-			} else {
-				mw->channel_flags |= 1 << (channel + 1);
-				mw->channel_state[channel] = 1;
-#ifndef _WIN32
-//FIXME: Set nonblocking on Windows too
-				fcntl(mw->sock_fds[channel], F_SETFL, O_NONBLOCK);
-#endif
-			}
+		}
+		int value = 1;
+		setsockopt(mw->sock_fds[channel], SOL_SOCKET, SO_REUSEADDR, (char*)&value, sizeof(value));
+		struct sockaddr_in bind_addr;
+		memset(&bind_addr, 0, sizeof(bind_addr));
+		bind_addr.sin_family = AF_INET;
+		bind_addr.sin_port = htons(mw->transmit_buffer[8] << 8 | mw->transmit_buffer[9]);
+		if (bind(mw->sock_fds[channel], (struct sockaddr *)&bind_addr, sizeof(bind_addr)) != 0) {
+			socket_close(mw->sock_fds[channel]);
+			mw->sock_fds[channel] = -1;
+			start_reply(mw, CMD_ERROR);
+			end_reply(mw);
+			break;
+		}
+		int res = listen(mw->sock_fds[channel], 2);
+		start_reply(mw, res ? CMD_ERROR : CMD_OK);
+		if (res) {
+			socket_close(mw->sock_fds[channel]);
+			mw->sock_fds[channel] = -1;
+		} else {
+			mw->channel_flags |= 1 << (channel + 1);
+			mw->channel_state[channel] = SOCKST_TCP_LISTEN;
+			socket_blocking(mw->sock_fds[channel], 0);
+		}
+		end_reply(mw);
+		break;
+	}
+	case CMD_CLOSE:
+		cmd_close(mw);
+		break;
+	case CMD_UDP_SET:
+		cmd_udp_set(mw);
+		break;
+	case CMD_SOCK_STAT: {
+		uint8_t channel = mw->transmit_buffer[4];
+		if (!channel || channel > 15) {
+			start_reply(mw, CMD_ERROR);
 			end_reply(mw);
 			break;
 		}
-		case CMD_SOCK_STAT: {
-			uint8_t channel = mw->transmit_buffer[4];
-			if (!channel || channel > 15) {
-				start_reply(mw, CMD_ERROR);
-				end_reply(mw);
-				break;
+		mw->channel_flags &= ~(1 << channel);
+		channel--;
+		poll_socket(mw, channel);
+		start_reply(mw, CMD_OK);
+		mw_putc(mw, mw->channel_state[channel]);
+		end_reply(mw);
+		break;
+	}
+	case CMD_DATETIME:
+		cmd_datetime(mw);
+		break;
+	case CMD_SYS_STAT:
+		poll_all_sockets(mw);
+		start_reply(mw, CMD_OK);
+		mw_putc(mw, mw->module_state);
+		mw_putc(mw, mw->flags);
+		mw_putc(mw, mw->channel_flags >> 8);
+		mw_putc(mw, mw->channel_flags);
+		end_reply(mw);
+		break;
+	case CMD_GAMERTAG_GET:
+		cmd_gamertag_get(mw);
+		break;
+	case CMD_LOG:
+		start_reply(mw, CMD_OK);
+		puts((char*)&mw->transmit_buffer[4]);
+		end_reply(mw);
+		break;
+	case CMD_HRNG_GET:
+		cmd_hrng_get(mw);
+		break;
+	case CMD_SERVER_URL_GET:
+		start_reply(mw, CMD_OK);
+		// FIXME: This should be get from config file
+		mw_puts(mw, "doragasu.com");
+		mw_putc(mw,'\0');
+		end_reply(mw);
+		break;
+	default:
+		printf("Unhandled MegaWiFi command %s(%d) with length %X\n", cmd_names[command], command, size);
+		break;
+	}
+}
+
+static void process_packet(megawifi *mw)
+{
+	if (mw->transmit_channel == 0) {
+		process_command(mw);
+	} else {
+		uint8_t channel = mw->transmit_channel - 1;
+		int channel_state = mw->channel_state[channel];
+		int sock_fd = mw->sock_fds[channel];
+		if (sock_fd >= 0 && channel_state == SOCKST_TCP_EST) {
+			int sent = send(sock_fd, (char*)mw->transmit_buffer, mw->transmit_bytes, 0);
+			if (sent < 0 && !socket_error_is_wouldblock()) {
+				socket_close(sock_fd);
+				mw->sock_fds[channel] = -1;
+				mw->channel_state[channel] = SOCKST_NONE;
+				mw->channel_flags |= 1 << mw->transmit_channel;
+			} else if (sent < mw->transmit_bytes) {
+				//TODO: save this data somewhere so it can be sent in poll_socket
+				printf("Sent %d bytes on channel %d, but %d were requested\n", sent, mw->transmit_channel, mw->transmit_bytes);
 			}
-			mw->channel_flags &= ~(1 << channel);
-			channel--;
-			poll_socket(mw, channel);
-			start_reply(mw, CMD_OK);
-			mw_putc(mw, mw->channel_state[channel]);
-			end_reply(mw);
-			break;
+		} else if (sock_fd >= 0 && channel_state == SOCKST_UDP_READY) {
+			udp_send(mw, channel);
+		} else {
+			printf("Unhandled receive of MegaWiFi data on channel %d\n", mw->transmit_channel);
 		}
-		case CMD_SYS_STAT:
-			poll_all_sockets(mw);
-			start_reply(mw, CMD_OK);
-			mw_putc(mw, mw->module_state);
-			mw_putc(mw, mw->flags);
-			mw_putc(mw, mw->channel_flags >> 8);
-			mw_putc(mw, mw->channel_flags);
-			end_reply(mw);
-			break;
-		default:
-			printf("Unhandled MegaWiFi command %s(%d) with length %X\n", cmd_names[command], command, size);
-			break;
-		}
-	} else if (mw->sock_fds[mw->transmit_channel - 1] >= 0 && mw->channel_state[mw->transmit_channel - 1] == 2) {
-		uint8_t channel = mw->transmit_channel - 1;
-		int sent = send(mw->sock_fds[channel], mw->transmit_buffer, mw->transmit_bytes, MSG_NOSIGNAL);
-		if (sent < 0 && errno != EAGAIN && errno != EWOULDBLOCK) {
-			close(mw->sock_fds[channel]);
-			mw->sock_fds[channel] = -1;
-			mw->channel_state[channel] = 0;
-			mw->channel_flags |= 1 << mw->transmit_channel;
-		} else if (sent < mw->transmit_bytes) {
-			//TODO: save this data somewhere so it can be sent in poll_socket
-			printf("Sent %d bytes on channel %d, but %d were requested\n", sent, mw->transmit_channel, mw->transmit_bytes);
-		}
-	} else {
-		printf("Unhandled receive of MegaWiFi data on channel %d\n", mw->transmit_channel);
 	}
 	mw->transmit_bytes = mw->expected_bytes = 0;
 }
--- a/mw_commands.c	Sun Apr 19 00:59:49 2020 -0700
+++ b/mw_commands.c	Sat May 09 23:39:44 2020 -0700
@@ -5,7 +5,7 @@
 	E(CMD_AP_CFG),
 	E(CMD_AP_CFG_GET),
 	E(CMD_IP_CURRENT),
-	E(CMD_RESERVED),
+	E(CMD_RESERVED_7),
 	E(CMD_IP_CFG),
 	E(CMD_IP_CFG_GET),
 	E(CMD_DEF_AP_CFG),
@@ -14,10 +14,10 @@
 	E(CMD_AP_LEAVE),
 	E(CMD_TCP_CON),
 	E(CMD_TCP_BIND),
-	E(CMD_TCP_ACCEPT),
-	E(CMD_TCP_DISC),
+	E(CMD_RESERVED_16),
+	E(CMD_CLOSE),
 	E(CMD_UDP_SET),
-	E(CMD_UDP_CLR),
+	E(CMD_RESERVED_19),
 	E(CMD_SOCK_STAT),
 	E(CMD_PING),
 	E(CMD_SNTP_CFG),
@@ -30,4 +30,27 @@
 	E(CMD_FLASH_ID),
 	E(CMD_SYS_STAT),
 	E(CMD_DEF_CFG_SET),
-	E(CMD_HRNG_GET),
\ No newline at end of file
+	E(CMD_HRNG_GET),
+	E(CMD_BSSID_GET),
+	E(CMD_GAMERTAG_SET),
+	E(CMD_GAMERTAG_GET),
+	E(CMD_LOG),
+	E(CMD_FACTORY_RESET),
+	E(CMD_SLEEP),
+	E(CMD_HTTP_URL_SET),
+	E(CMD_HTTP_METHOD_SET),
+	E(CMD_HTTP_CERT_QUERY),
+	E(CMD_HTTP_CERT_SET),
+	E(CMD_HTTP_HDR_ADD),
+	E(CMD_HTTP_HDR_DEL),
+	E(CMD_HTTP_OPEN),
+	E(CMD_HTTP_FINISH),
+	E(CMD_HTTP_CLEANUP),
+	E(CMD_RESERVED_48),
+	E(CMD_SERVER_URL_GET),
+	E(CMD_SERVER_URL_SET),
+	E(CMD_WIFI_ADV_GET),
+	E(CMD_WIFI_ADV_SET),
+	E(CMD_NV_CFG_SAVE),
+	E(CMD_UPGRADE_LIST),
+	E(CMD_UPGRADE_PERFORM),
--- a/nuklear_ui/blastem_nuklear.c	Sun Apr 19 00:59:49 2020 -0700
+++ b/nuklear_ui/blastem_nuklear.c	Sat May 09 23:39:44 2020 -0700
@@ -2220,7 +2220,7 @@
 		context->style.window.fixed_background = nk_style_item_color(nk_rgba(0, 0, 0, 128));
 		current_view = view_pause;
 		context->input.selected_widget = 0;
-		current_system->request_exit(current_system);
+		system_request_exit(current_system, 1);
 	} else if (current_system && !set_binding) {
 		clear_view_stack();
 		show_play_view();
--- a/nuklear_ui/font.c	Sun Apr 19 00:59:49 2020 -0700
+++ b/nuklear_ui/font.c	Sat May 09 23:39:44 2020 -0700
@@ -14,7 +14,8 @@
 		return strdup(FONT_PATH);
 	}
 #endif
-	FILE *fc_pipe = popen("fc-match -f '%{file}'", "r");
+	//TODO: specify language dynamically once BlastEm is localized
+	FILE *fc_pipe = popen("fc-match :lang=en -f '%{file}'", "r");
 	if (!fc_pipe) {
 		return NULL;
 	}
--- a/psg.c	Sun Apr 19 00:59:49 2020 -0700
+++ b/psg.c	Sat May 09 23:39:44 2020 -0700
@@ -5,6 +5,7 @@
 */
 #include "psg.h"
 #include "blastem.h"
+#include "event_log.h"
 #include <string.h>
 #include <stdlib.h>
 #include <stdio.h>
@@ -35,6 +36,7 @@
 	if (context->vgm) {
 		vgm_sn76489_write(context->vgm, context->cycles, value);
 	}
+	event_log(EVENT_PSG_REG, context->cycles, sizeof(value), &value);
 	if (value & 0x80) {
 		context->latch = value & 0x70;
 		uint8_t channel = value >> 5 & 0x3;
--- a/render.h	Sun Apr 19 00:59:49 2020 -0700
+++ b/render.h	Sat May 09 23:39:44 2020 -0700
@@ -65,6 +65,7 @@
 #define RENDER_DPAD_LEFT   SDL_HAT_LEFT
 #define RENDER_DPAD_RIGHT  SDL_HAT_RIGHT
 #define render_relative_mouse SDL_SetRelativeMouseMode
+typedef SDL_Thread* render_thread;
 #endif
 #endif
 
@@ -77,8 +78,6 @@
 #define FRAMEBUFFER_UI 2
 #define FRAMEBUFFER_USER_START 3
 
-#include "vdp.h"
-
 typedef enum {
 	VID_NTSC,
 	VID_PAL,
@@ -95,6 +94,7 @@
 typedef void (*drop_handler)(const char *filename);
 typedef void (*window_close_handler)(uint8_t which);
 typedef void (*ui_render_fun)(void);
+typedef int (*render_thread_fun)(void*);
 
 uint32_t render_map_color(uint8_t r, uint8_t g, uint8_t b);
 void render_save_screenshot(char *path);
@@ -137,6 +137,8 @@
 void render_set_ui_fb_resize_handler(ui_render_fun resize);
 void render_video_loop(void);
 uint8_t render_should_release_on_exit(void);
+void render_set_external_sync(uint8_t ext_sync_on);
+uint8_t render_create_thread(render_thread *thread, const char *name, render_thread_fun fun, void *data);
 
 #endif //RENDER_H_
 
--- a/render_fbdev.c	Sun Apr 19 00:59:49 2020 -0700
+++ b/render_fbdev.c	Sat May 09 23:39:44 2020 -0700
@@ -50,200 +50,21 @@
 
 static uint32_t last_frame = 0;
 static snd_pcm_uframes_t buffer_samples;
+static size_t buffer_bytes;
 static unsigned int output_channels, sample_rate;
-static uint32_t missing_count;
 
 
 static uint8_t quitting = 0;
 
-struct audio_source {
-	int16_t  *front;
-	int16_t  *back;
-	double   dt;
-	uint64_t buffer_fraction;
-	uint64_t buffer_inc;
-	uint32_t buffer_pos;
-	uint32_t read_start;
-	uint32_t read_end;
-	uint32_t lowpass_alpha;
-	uint32_t mask;
-	int16_t  last_left;
-	int16_t  last_right;
-	uint8_t  num_channels;
-	uint8_t  front_populated;
-};
-
-static audio_source *audio_sources[8];
-static audio_source *inactive_audio_sources[8];
-static uint8_t num_audio_sources;
-static uint8_t num_inactive_audio_sources;
-static uint32_t min_buffered;
-
-typedef int32_t (*mix_func)(audio_source *audio, void *vstream, int len);
-
-static int32_t mix_s16(audio_source *audio, void *vstream, int len)
-{
-	int samples = len/(sizeof(int16_t)*output_channels);
-	int16_t *stream = vstream;
-	int16_t *end = stream + output_channels*samples;
-	int16_t *src = audio->front;
-	uint32_t i = audio->read_start;
-	uint32_t i_end = audio->read_end;
-	int16_t *cur = stream;
-	size_t first_add = output_channels > 1 ? 1 : 0, second_add = output_channels > 1 ? output_channels - 1 : 1;
-	if (audio->num_channels == 1) {
-		while (cur < end && i != i_end)
-		{
-			*cur += src[i];
-			cur += first_add;
-			*cur += src[i++];
-			cur += second_add;
-			i &= audio->mask;
-		}
-	} else {
-		while (cur < end && i != i_end)
-		{
-			*cur += src[i++];
-			cur += first_add;
-			*cur += src[i++];
-			cur += second_add;
-			i &= audio->mask;
-		}
-	}
-	
-	if (cur != end) {
-		printf("Underflow of %d samples, read_start: %d, read_end: %d, mask: %X\n", (int)(end-cur)/2, audio->read_start, audio->read_end, audio->mask);
-	}
-	if (cur != end) {
-		//printf("Underflow of %d samples, read_start: %d, read_end: %d, mask: %X\n", (int)(end-cur)/2, audio->read_start, audio->read_end, audio->mask);
-		return (cur-end)/2;
-	} else {
-		return ((i_end - i) & audio->mask) / audio->num_channels;
-	}
-}
-
-static int32_t mix_f32(audio_source *audio, void *vstream, int len)
-{
-	int samples = len/(sizeof(float)*output_channels);
-	float *stream = vstream;
-	float *end = stream + output_channels*samples;
-	int16_t *src = audio->front;
-	uint32_t i = audio->read_start;
-	uint32_t i_end = audio->read_end;
-	float *cur = stream;
-	size_t first_add = output_channels > 1 ? 1 : 0, second_add = output_channels > 1 ? output_channels - 1 : 1;
-	if (audio->num_channels == 1) {
-		while (cur < end && i != i_end)
-		{
-			*cur += ((float)src[i]) / 0x7FFF;
-			cur += first_add;
-			*cur += ((float)src[i++]) / 0x7FFF;
-			cur += second_add;
-			i &= audio->mask;
-		}
-	} else {
-		while(cur < end && i != i_end)
-		{
-			*cur += ((float)src[i++]) / 0x7FFF;
-			cur += first_add;
-			*cur += ((float)src[i++]) / 0x7FFF;
-			cur += second_add;
-			i &= audio->mask;
-		}
-	}
-	if (cur != end) {
-		printf("Underflow of %d samples, read_start: %d, read_end: %d, mask: %X\n", (int)(end-cur)/2, audio->read_start, audio->read_end, audio->mask);
-		return (cur-end)/2;
-	} else {
-		return ((i_end - i) & audio->mask) / audio->num_channels;
-	}
-}
-
-static int32_t mix_null(audio_source *audio, void *vstream, int len)
-{
-	return 0;
-}
-
-static mix_func mix;
 
 static void render_close_audio()
 {
 
 }
 
-#define BUFFER_INC_RES 0x40000000UL
-
-void render_audio_adjust_clock(audio_source *src, uint64_t master_clock, uint64_t sample_divider)
-{
-	src->buffer_inc = ((BUFFER_INC_RES * (uint64_t)sample_rate) / master_clock) * sample_divider;
-}
-
-audio_source *render_audio_source(uint64_t master_clock, uint64_t sample_divider, uint8_t channels)
-{
-	audio_source *ret = NULL;
-	uint32_t alloc_size = channels * buffer_samples;
-	if (num_audio_sources < 8) {
-		ret = malloc(sizeof(audio_source));
-		ret->back = malloc(alloc_size * sizeof(int16_t));
-		ret->front = malloc(alloc_size * sizeof(int16_t));
-		ret->front_populated = 0;
-		ret->num_channels = channels;
-		audio_sources[num_audio_sources++] = ret;
-	}
-	if (!ret) {
-		fatal_error("Too many audio sources!");
-	} else {
-		render_audio_adjust_clock(ret, master_clock, sample_divider);
-		double lowpass_cutoff = get_lowpass_cutoff(config);
-		double rc = (1.0 / lowpass_cutoff) / (2.0 * M_PI);
-		ret->dt = 1.0 / ((double)master_clock / (double)(sample_divider));
-		double alpha = ret->dt / (ret->dt + rc);
-		ret->lowpass_alpha = (int32_t)(((double)0x10000) * alpha);
-		ret->buffer_pos = 0;
-		ret->buffer_fraction = 0;
-		ret->last_left = ret->last_right = 0;
-		ret->read_start = 0;
-		ret->read_end = buffer_samples * channels;
-		ret->mask = 0xFFFFFFFF;
-	}
-	return ret;
-}
-
-void render_pause_source(audio_source *src)
-{
-	for (uint8_t i = 0; i < num_audio_sources; i++)
-	{
-		if (audio_sources[i] == src) {
-			audio_sources[i] = audio_sources[--num_audio_sources];
-			break;
-		}
-	}
-	inactive_audio_sources[num_inactive_audio_sources++] = src;
-}
-
-void render_resume_source(audio_source *src)
-{
-	if (num_audio_sources < 8) {
-		audio_sources[num_audio_sources++] = src;
-	}
-	for (uint8_t i = 0; i < num_inactive_audio_sources; i++)
-	{
-		if (inactive_audio_sources[i] == src) {
-			inactive_audio_sources[i] = inactive_audio_sources[--num_inactive_audio_sources];
-		}
-	}
-}
-
-void render_free_source(audio_source *src)
-{
-	render_pause_source(src);
-	
-	free(src->front);
-	free(src->back);
-	free(src);
-}
-snd_pcm_t *audio_handle;
-static void do_audio_ready(audio_source *src)
+static snd_pcm_t *audio_handle;
+static void *output_buffer;
+void render_do_audio_ready(audio_source *src)
 {
 	if (src->front_populated) {
 		fatal_error("Audio source filled up a buffer a second time before other sources finished their first\n");
@@ -254,22 +75,12 @@
 	src->front_populated = 1;
 	src->buffer_pos = 0;
 	
-	for (uint8_t i = 0; i < num_audio_sources; i++)
-	{
-		if (!audio_sources[i]->front_populated) {
-			//at least one audio source is not ready yet.
-			return;
-		}
+	if (!all_sources_ready()) {
+		return;
 	}
-	
-	size_t bytes = (mix == mix_s16 ? sizeof(int16_t) : sizeof(float)) * output_channels * buffer_samples;
-	void *buffer = malloc(bytes);
-	for (uint8_t i = 0; i < num_audio_sources; i++)
-	{
-		mix(audio_sources[i], buffer, bytes);
-		audio_sources[i]->front_populated = 0;
-	}
-	int frames = snd_pcm_writei(audio_handle, buffer, buffer_samples);
+	mix_and_convert(output_buffer, buffer_bytes, NULL);
+
+	int frames = snd_pcm_writei(audio_handle, output_buffer, buffer_samples);
 	if (frames < 0) {
 		frames = snd_pcm_recover(audio_handle, frames, 0);
 	}
@@ -278,60 +89,6 @@
 	}
 }
 
-static int16_t lowpass_sample(audio_source *src, int16_t last, int16_t current)
-{
-	int32_t tmp = current * src->lowpass_alpha + last * (0x10000 - src->lowpass_alpha);
-	current = tmp >> 16;
-	return current;
-}
-
-static void interp_sample(audio_source *src, int16_t last, int16_t current)
-{
-	int64_t tmp = last * ((src->buffer_fraction << 16) / src->buffer_inc);
-	tmp += current * (0x10000 - ((src->buffer_fraction << 16) / src->buffer_inc));
-	src->back[src->buffer_pos++] = tmp >> 16;
-}
-
-void render_put_mono_sample(audio_source *src, int16_t value)
-{
-	value = lowpass_sample(src, src->last_left, value);
-	src->buffer_fraction += src->buffer_inc;
-	uint32_t base = 0;
-	while (src->buffer_fraction > BUFFER_INC_RES)
-	{
-		src->buffer_fraction -= BUFFER_INC_RES;
-		interp_sample(src, src->last_left, value);
-		
-		if (((src->buffer_pos - base) & src->mask) >= buffer_samples) {
-			do_audio_ready(src);
-		}
-		src->buffer_pos &= src->mask;
-	}
-	src->last_left = value;
-}
-
-void render_put_stereo_sample(audio_source *src, int16_t left, int16_t right)
-{
-	left = lowpass_sample(src, src->last_left, left);
-	right = lowpass_sample(src, src->last_right, right);
-	src->buffer_fraction += src->buffer_inc;
-	uint32_t base = 0;
-	while (src->buffer_fraction > BUFFER_INC_RES)
-	{
-		src->buffer_fraction -= BUFFER_INC_RES;
-		
-		interp_sample(src, src->last_left, left);
-		interp_sample(src, src->last_right, right);
-		
-		if (((src->buffer_pos - base) & src->mask)/2 >= buffer_samples) {
-			do_audio_ready(src);
-		}
-		src->buffer_pos &= src->mask;
-	}
-	src->last_left = left;
-	src->last_right = right;
-}
-
 int render_width()
 {
 	return main_width;
--- a/render_sdl.c	Sun Apr 19 00:59:49 2020 -0700
+++ b/render_sdl.c	Sat May 09 23:39:44 2020 -0700
@@ -50,7 +50,14 @@
 static SDL_cond *audio_ready, *frame_ready;
 static uint8_t quitting = 0;
 
-static uint8_t sync_to_audio, run_on_audio_thread;
+enum {
+	SYNC_AUDIO,
+	SYNC_AUDIO_THREAD,
+	SYNC_VIDEO,
+	SYNC_EXTERNAL
+};
+
+static uint8_t sync_src;
 static uint32_t min_buffered;
 
 uint32_t **frame_buffers;
@@ -64,12 +71,12 @@
 
 uint8_t render_is_audio_sync(void)
 {
-	return sync_to_audio || run_on_audio_thread;
+	return sync_src < SYNC_VIDEO;
 }
 
 uint8_t render_should_release_on_exit(void)
 {
-	return !run_on_audio_thread;
+	return sync_src != SYNC_AUDIO_THREAD;
 }
 
 void render_buffer_consumed(audio_source *src)
@@ -120,7 +127,7 @@
 
 void render_lock_audio()
 {
-	if (sync_to_audio) {
+	if (sync_src == SYNC_AUDIO) {
 		SDL_LockMutex(audio_mutex);
 	} else {
 		SDL_LockAudio();
@@ -129,7 +136,7 @@
 
 void render_unlock_audio()
 {
-	if (sync_to_audio) {
+	if (sync_src == SYNC_AUDIO) {
 		SDL_UnlockMutex(audio_mutex);
 	} else {
 		SDL_UnlockAudio();
@@ -164,34 +171,40 @@
 
 void render_audio_created(audio_source *source)
 {
-	if (sync_to_audio && SDL_GetAudioStatus() == SDL_AUDIO_PAUSED) {
+	if (render_is_audio_sync()) {
 		SDL_PauseAudio(0);
 	}
-	if (current_system) {
-		current_system->request_exit(current_system);
+	if (current_system && sync_src == SYNC_AUDIO_THREAD) {
+		system_request_exit(current_system, 0);
 	}
 }
 
 void render_source_paused(audio_source *src, uint8_t remaining_sources)
 {
-	if (sync_to_audio) {
+	if (sync_src == SYNC_AUDIO) {
 		SDL_CondSignal(audio_ready);
 	}
-	if (!remaining_sources) {
-		SDL_PauseAudio(0);
+	if (!remaining_sources && render_is_audio_sync()) {
+		SDL_PauseAudio(1);
+		if (sync_src == SYNC_AUDIO_THREAD) {
+			SDL_CondSignal(frame_ready);
+		}
 	}
 }
 
 void render_source_resumed(audio_source *src)
 {
-	if (sync_to_audio) {
+	if (render_is_audio_sync()) {
 		SDL_PauseAudio(0);
 	}
+	if (current_system && sync_src == SYNC_AUDIO_THREAD) {
+		system_request_exit(current_system, 0);
+	}
 }
 
 void render_do_audio_ready(audio_source *src)
 {
-	if (run_on_audio_thread) {
+	if (sync_src == SYNC_AUDIO_THREAD) {
 		int16_t *tmp = src->front;
 		src->front = src->back;
 		src->back = tmp;
@@ -199,9 +212,9 @@
 		src->buffer_pos = 0;
 		if (all_sources_ready()) {
 			//we've emulated far enough to fill the current buffer
-			current_system->request_exit(current_system);
+			system_request_exit(current_system, 0);
 		}
-	} else if (sync_to_audio) {
+	} else if (sync_src == SYNC_AUDIO) {
 		SDL_LockMutex(audio_mutex);
 			while (src->front_populated) {
 				SDL_CondWait(src->opaque, audio_mutex);
@@ -253,8 +266,21 @@
 #endif
 }
 
+static uint8_t external_sync;
+void render_set_external_sync(uint8_t ext_sync_on)
+{
+	if (ext_sync_on != external_sync) {
+		external_sync = ext_sync_on;
+		if (windowed_width) {
+			//only do this if render_init has already been called
+			render_config_updated();
+		}
+	}
+}
+
 #ifndef DISABLE_OPENGL
-static GLuint textures[3], buffers[2], vshader, fshader, program, un_textures[2], un_width, un_height, at_pos;
+static GLuint textures[3], buffers[2], vshader, fshader, program, un_textures[2], un_width, un_height, un_texsize, at_pos;
+static int tex_width, tex_height;
 
 static GLfloat vertex_data_default[] = {
 	-1.0f, -1.0f,
@@ -360,6 +386,15 @@
 	char *scaling = tern_find_path_default(config, "video\0scaling\0", def, TVAL_PTR).ptrval;
 	GLint filter = strcmp(scaling, "linear") ? GL_NEAREST : GL_LINEAR;
 	glGenTextures(3, textures);
+	def.ptrval = "off";
+	char *npot_textures = tern_find_path_default(config, "video\0npot_textures\0", def, TVAL_PTR).ptrval;
+	if (!strcmp(npot_textures, "on")) {
+		tex_width = LINEBUF_SIZE;
+		tex_height = 294; //PAL height with full borders
+	} else {
+		tex_width = tex_height = 512;
+	}
+	printf("Using %dx%d textures\n", tex_width, tex_height);
 	for (int i = 0; i < 3; i++)
 	{
 		glBindTexture(GL_TEXTURE_2D, textures[i]);
@@ -369,7 +404,7 @@
 		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
 		if (i < 2) {
 			//TODO: Fixme for PAL + invalid display mode
-			glTexImage2D(GL_TEXTURE_2D, 0, INTERNAL_FORMAT, 512, 512, 0, SRC_FORMAT, GL_UNSIGNED_BYTE, texture_buf);
+			glTexImage2D(GL_TEXTURE_2D, 0, INTERNAL_FORMAT, tex_width, tex_height, 0, SRC_FORMAT, GL_UNSIGNED_BYTE, texture_buf);
 		} else {
 			uint32_t blank = 255 << 24;
 			glTexImage2D(GL_TEXTURE_2D, 0, INTERNAL_FORMAT, 1, 1, 0, SRC_FORMAT, GL_UNSIGNED_BYTE, &blank);
@@ -398,6 +433,7 @@
 	un_textures[1] = glGetUniformLocation(program, "textures[1]");
 	un_width = glGetUniformLocation(program, "width");
 	un_height = glGetUniformLocation(program, "height");
+	un_texsize = glGetUniformLocation(program, "texsize");
 	at_pos = glGetAttribLocation(program, "pos");
 }
 
@@ -913,7 +949,17 @@
    	}
     debug_message("config says: %d\n", samples);
     desired.samples = samples*2;
-	desired.callback = sync_to_audio ? audio_callback : run_on_audio_thread ? audio_callback_run_on_audio : audio_callback_drc;
+	switch (sync_src)
+	{
+	case SYNC_AUDIO:
+		desired.callback = audio_callback;
+		break;
+	case SYNC_AUDIO_THREAD:
+		desired.callback = audio_callback_run_on_audio;
+		break;
+	default:
+		desired.callback = audio_callback_drc;
+	}
 	desired.userdata = NULL;
 
 	if (SDL_OpenAudio(&desired, &actual) < 0) {
@@ -943,12 +989,31 @@
 	}
 	
 	tern_val def = {.ptrval = "audio"};
-	char *sync_src = tern_find_path_default(config, "system\0sync_source\0", def, TVAL_PTR).ptrval;
-	sync_to_audio = !strcmp(sync_src, "audio");
-	run_on_audio_thread = !strcmp(sync_src, "audio_thread");
+	if (external_sync) {
+		sync_src = SYNC_EXTERNAL;
+	} else {
+		char *sync_src_str = tern_find_path_default(config, "system\0sync_source\0", def, TVAL_PTR).ptrval;
+		if (!strcmp(sync_src_str, "audio")) {
+			sync_src = SYNC_AUDIO;
+		} else if (!strcmp(sync_src_str, "audio_thread")) {
+			sync_src = SYNC_AUDIO_THREAD;
+		} else {
+			sync_src = SYNC_VIDEO;
+		}
+	}
+	
+	if (!num_buffers && (sync_src == SYNC_AUDIO_THREAD || sync_src == SYNC_EXTERNAL)) {
+		frame_mutex = SDL_CreateMutex();
+		free_buffer_mutex = SDL_CreateMutex();
+		frame_ready = SDL_CreateCond();
+		buffer_storage = 4;
+		frame_buffers = calloc(buffer_storage, sizeof(uint32_t*));
+		frame_buffers[0] = texture_buf;
+		num_buffers = 1;
+	}
 	
 	const char *vsync;
-	if (sync_to_audio) {
+	if (sync_src == SYNC_AUDIO) {
 		def.ptrval = "off";
 		vsync = tern_find_path_default(config, "video\0vsync\0", def, TVAL_PTR).ptrval;
 	} else {
@@ -1108,16 +1173,6 @@
 	audio_mutex = SDL_CreateMutex();
 	audio_ready = SDL_CreateCond();
 	
-	if (run_on_audio_thread) {
-		frame_mutex = SDL_CreateMutex();
-		free_buffer_mutex = SDL_CreateMutex();
-		frame_ready = SDL_CreateCond();
-		buffer_storage = 4;
-		frame_buffers = calloc(buffer_storage, sizeof(uint32_t*));
-		frame_buffers[0] = texture_buf;
-		num_buffers = 1;
-	}
-	
 	init_audio();
 	
 	uint32_t db_size;
@@ -1136,13 +1191,10 @@
 
 	atexit(render_quit);
 }
-#include<unistd.h>
 static int in_toggle;
 
 void render_config_updated(void)
 {
-	uint8_t old_sync_to_audio = sync_to_audio;
-	
 	free_surfaces();
 #ifndef DISABLE_OPENGL
 	if (render_gl) {
@@ -1336,14 +1388,14 @@
 uint32_t locked_pitch;
 uint32_t *render_get_framebuffer(uint8_t which, int *pitch)
 {
-	if (run_on_audio_thread) {
+	if (sync_src == SYNC_AUDIO_THREAD || sync_src == SYNC_EXTERNAL) {
 		*pitch = LINEBUF_SIZE * sizeof(uint32_t);
 		uint32_t *buffer;
 		SDL_LockMutex(free_buffer_mutex);
 			if (num_buffers) {
 				buffer = frame_buffers[--num_buffers];
 			} else {
-				buffer = calloc(512*512, sizeof(uint32_t));
+				buffer = calloc(tex_width*(tex_height + 1), sizeof(uint32_t));
 			}
 		SDL_UnlockMutex(free_buffer_mutex);
 		locked_pixels = buffer;
@@ -1362,14 +1414,14 @@
 			warning("Request for invalid framebuffer number %d\n", which);
 			return NULL;
 		}
-		void *pixels;
-		if (SDL_LockTexture(sdl_textures[which], NULL, &pixels, pitch) < 0) {
+		uint8_t *pixels;
+		if (SDL_LockTexture(sdl_textures[which], NULL, (void **)&pixels, pitch) < 0) {
 			warning("Failed to lock texture: %s\n", SDL_GetError());
 			return NULL;
 		}
 		static uint8_t last;
 		if (which <= FRAMEBUFFER_EVEN) {
-			locked_pixels = pixels;
+			locked_pixels = (uint32_t *)pixels;
 			if (which == FRAMEBUFFER_EVEN) {
 				pixels += *pitch;
 			}
@@ -1379,7 +1431,7 @@
 			}
 			last = which;
 		}
-		return pixels;
+		return (uint32_t *)pixels;
 #ifndef DISABLE_OPENGL
 	}
 #endif
@@ -1408,7 +1460,7 @@
 static void process_framebuffer(uint32_t *buffer, uint8_t which, int width)
 {
 	static uint8_t last;
-	if (!render_is_audio_sync() && which <= FRAMEBUFFER_EVEN && source_frame_count < 0) {
+	if (sync_src == SYNC_VIDEO && which <= FRAMEBUFFER_EVEN && source_frame_count < 0) {
 		source_frame++;
 		if (source_frame >= source_hz) {
 			source_frame = 0;
@@ -1464,7 +1516,7 @@
 		}
 	} else {
 #endif
-		//TODO: Support run_on_audio_thread for render API framebuffers
+		//TODO: Support SYNC_AUDIO_THREAD/SYNC_EXTERNAL for render API framebuffers
 		if (which <= FRAMEBUFFER_EVEN && last != which) {
 			uint8_t *cur_dst = (uint8_t *)locked_pixels;
 			uint8_t *cur_saved = (uint8_t *)texture_buf;
@@ -1613,7 +1665,7 @@
 
 void render_framebuffer_updated(uint8_t which, int width)
 {
-	if (run_on_audio_thread) {
+	if (sync_src == SYNC_AUDIO_THREAD || sync_src == SYNC_EXTERNAL) {
 		SDL_LockMutex(frame_mutex);
 			while (frame_queue_len == 4) {
 				SDL_CondSignal(frame_ready);
@@ -1651,25 +1703,29 @@
 
 void render_video_loop(void)
 {
-	if (!run_on_audio_thread) {
+	if (sync_src != SYNC_AUDIO_THREAD && sync_src != SYNC_EXTERNAL) {
 		return;
 	}
-	SDL_PauseAudio(0);
 	SDL_LockMutex(frame_mutex);
 		for(;;)
 		{
-			while (!frame_queue_len)
+			while (!frame_queue_len && SDL_GetAudioStatus() == SDL_AUDIO_PLAYING)
 			{
 				SDL_CondWait(frame_ready, frame_mutex);
 			}
-			for (int i = 0; i < frame_queue_len; i++)
+			while (frame_queue_len)
 			{
 				frame f = frame_queue[frame_queue_read++];
 				frame_queue_read &= 0x3;
+				frame_queue_len--;
+				SDL_UnlockMutex(frame_mutex);
 				process_framebuffer(f.buffer, f.which, f.width);
 				release_buffer(f.buffer);
+				SDL_LockMutex(frame_mutex);
 			}
-			frame_queue_len = 0;
+			if (SDL_GetAudioStatus() != SDL_AUDIO_PLAYING) {
+				break;
+			}
 		}
 	
 	SDL_UnlockMutex(frame_mutex);
@@ -1699,6 +1755,7 @@
 
 		glUniform1f(un_width, render_emulated_width());
 		glUniform1f(un_height, last_height);
+		glUniform2f(un_texsize, tex_width, tex_height);
 
 		glBindBuffer(GL_ARRAY_BUFFER, buffers[0]);
 		glVertexAttribPointer(at_pos, 2, GL_FLOAT, GL_FALSE, sizeof(GLfloat[2]), (void *)0);
@@ -1974,3 +2031,9 @@
 	}
 	return 0xFF;
 }
+
+uint8_t render_create_thread(render_thread *thread, const char *name, render_thread_fun fun, void *data)
+{
+	*thread = SDL_CreateThread(fun, name, data);
+	return *thread != 0;
+}
--- a/romdb.c	Sun Apr 19 00:59:49 2020 -0700
+++ b/romdb.c	Sat May 09 23:39:44 2020 -0700
@@ -675,7 +675,9 @@
 			*map = lock_info.map[i];
 			if (map->start < 0x200000) {
 				if (map->buffer) {
-					map->buffer += (0x200000 - map->start) & ((map->flags & MMAP_AUX_BUFF) ? map->aux_mask : map->mask);
+					uint8_t *buf = map->buffer;
+					buf += (0x200000 - map->start) & ((map->flags & MMAP_AUX_BUFF) ? map->aux_mask : map->mask);
+					map->buffer = buf;
 				}
 				map->start = 0x200000;
 			}
--- a/saves.h	Sun Apr 19 00:59:49 2020 -0700
+++ b/saves.h	Sat May 09 23:39:44 2020 -0700
@@ -7,6 +7,7 @@
 
 #define QUICK_SAVE_SLOT 10
 #define SERIALIZE_SLOT 11
+#define EVENTLOG_SLOT 12
 
 typedef struct {
 	char   *desc;
--- a/serialize.c	Sun Apr 19 00:59:49 2020 -0700
+++ b/serialize.c	Sat May 09 23:39:44 2020 -0700
@@ -20,8 +20,13 @@
 static void reserve(serialize_buffer *buf, size_t amount)
 {
 	if (amount > (buf->storage - buf->size)) {
-		buf->storage *= 2;
-		buf = realloc(buf, buf->storage + sizeof(*buf));
+		if (amount < buf->storage) {
+			buf->storage *= 2;
+		} else {
+			//doublign isn't enough, increase by the precise amount needed
+			buf->storage += amount - (buf->storage - buf->size);
+		}
+		buf->data = realloc(buf->data, buf->storage + sizeof(*buf));
 	}
 }
 
--- a/shaders/crt.f.glsl	Sun Apr 19 00:59:49 2020 -0700
+++ b/shaders/crt.f.glsl	Sat May 09 23:39:44 2020 -0700
@@ -1,5 +1,3 @@
-#version 110
-
 /* Subtle CRT shader usable in fullscreen - Anaël Seghezzi [anael(at)maratis3d.com]
    This shader is free software distributed under the terms of the GNU General Public
    License version 3 or higher. This gives you the right to redistribute and/or
@@ -10,63 +8,64 @@
 #define M_PI 3.14159265358979323846
 
 uniform sampler2D textures[2];
-uniform float width, height;
-varying vec2 texcoord;
-varying vec2 screencoord;
+uniform mediump float width, height;
+uniform mediump vec2 texsize;
+varying mediump vec2 texcoord;
+varying mediump vec2 screencoord;
 
 
-float nrand(vec2 n) {
+mediump float nrand(vec2 n) {
 	return fract(sin(dot(n.xy, vec2(12.9898, 78.233))) * 43758.5453);
 }
 
-float scanline(vec2 texco)
+mediump float scanline(vec2 texco)
 {
-	return (1.0 - abs(cos(texco.y * 512.0 * M_PI)));
+	return (1.0 - abs(cos(texco.y * texsize.y * M_PI)));
 }
 
-vec2 sharp_coord(vec2 texco, vec2 dim, vec2 sharpness)
+mediump vec2 sharp_coord(mediump vec2 texco, mediump vec2 dim, mediump vec2 sharpness)
 {
-	vec2 texcoif = texco * dim;
-	vec2 texcoi = floor(texcoif);
-	vec2 mu = (texcoif - 0.5) - texcoi;
-	vec2 mub = pow(abs(mu) * 2.0, sharpness) * sign(mu) * 0.5;
+	mediump vec2 texcoif = texco * dim;
+	mediump vec2 texcoi = floor(texcoif);
+	mediump vec2 mu = (texcoif - 0.5) - texcoi;
+	mediump vec2 mub = pow(abs(mu) * 2.0, sharpness) * sign(mu) * 0.5;
 	return (texcoi + mub + 0.5) / dim;	
 }
 
 void main()
 {
-	float v = 1.0 / 512.0;
-	float yforce = 0.175;
-	float vign = length(screencoord);
+	mediump float v = 1.0 / texsize.y;
+	mediump float yforce = 0.175;
+	mediump float vign = length(screencoord);
 
 	// monitor deformation
-	vec2 monitorcoord = (screencoord + screencoord * vign * 0.025);
+	mediump vec2 monitorcoord = (screencoord + screencoord * vign * 0.025);
 	
 	if (monitorcoord.x < -1.0 || monitorcoord.y < -1.0 || monitorcoord.x > 1.0 || monitorcoord.y > 1.0) {
 		gl_FragColor = vec4(0.0);
 		return;
 	}
 
-	vec2 texco = monitorcoord * vec2(width/1024.0, height/-1024.0) + vec2(width/1024.0, height/1024.0);
+	mediump vec2 texco = monitorcoord * vec2(0.5*width/texsize.x, -0.5 * height/texsize.y) + vec2(0.5*width/texsize.x, 0.5*height/texsize.y);
 
 	// mask
-	float maskx = 1.0 - pow(abs(monitorcoord.x), 200.0);
-	float masky = 1.0 - pow(abs(-monitorcoord.y), 200.0);
-	float mask = clamp(maskx * masky, 0.0, 1.0);
+	mediump float maskx = 1.0 - pow(abs(monitorcoord.x), 200.0);
+	mediump float masky = 1.0 - pow(abs(-monitorcoord.y), 200.0);
+	mediump float mask = clamp(maskx * masky, 0.0, 1.0);
 
 	// sharp texcoord
-	vec2 texco_sharp0 = sharp_coord(texco, vec2(512.0, 512.0), vec2(4.0, 8.0));
-	vec2 texco_sharp1 = sharp_coord(texco - vec2(0.0, 1.0 / 1024.0), vec2(512.0, 512.0), vec2(4.0, 8.0));
+	mediump vec2 texco_sharp0 = sharp_coord(texco, texsize, vec2(4.0, 8.0));
+	mediump vec2 texco_sharp1 = sharp_coord(texco - vec2(0.0, 0.5 / texsize.y), texsize, vec2(4.0, 8.0));
 
-	vec4 src0 = texture2D(textures[0], texco_sharp0);
-	vec4 src1 = texture2D(textures[1], texco_sharp1);
+	mediump vec4 src0 = texture2D(textures[0], texco_sharp0);
+	mediump vec4 src1 = texture2D(textures[1], texco_sharp1);
 
 	// interlace mix
-	float interlace = cos((texco.y * 1024.0) * M_PI);
-	vec4 src_mix = mix(src0, src1, interlace * 0.5 + 0.5);
+	mediump float interlace = cos((texco.y * 2.0 * texsize.y) * M_PI);
+	mediump vec4 src_mix = mix(src0, src1, interlace * 0.5 + 0.5);
 
 	// blur
-	vec4 src_blur = mix(texture2D(textures[0], texco), texture2D(textures[1], texco), 0.5);
+	mediump vec4 src_blur = mix(texture2D(textures[0], texco), texture2D(textures[1], texco), 0.5);
 
 #ifdef NO_SCANLINE
 
@@ -75,11 +74,11 @@
 #else
 	// multisample scanline with grain
 	// TODO: offset grain with time (needs a "frame" uniform)
-	float cosy;
-	cosy  = scanline(texco + vec2(0.125, v * (nrand(texcoord + vec2(0.0, 1.0)) * 0.25) + 0.3333));
-	cosy += scanline(texco + vec2(0.25, v * (nrand(texcoord + vec2(0.0, 2.0)) * 0.25) + 0.25));
-	cosy += scanline(texco + vec2(0.50, v * (nrand(texcoord + vec2(0.0, 3.0)) * 0.25) + 0.6666));
-	cosy += scanline(texco + vec2(0.75, v * (nrand(texcoord + vec2(0.0, 4.0)) * 0.25) + 0.75));
+	mediump float cosy;
+	cosy  = scanline(texco + vec2(0.125, v * (nrand(texcoord + vec2(0.0, 512.0/texsize.y)) * 0.25) + 512.0*0.3333/texsize.y));
+	cosy += scanline(texco + vec2(0.25, v * (nrand(texcoord + vec2(0.0, 1024.0/texsize.y)) * 0.25) + 512.0*0.25/texsize.y));
+	cosy += scanline(texco + vec2(0.50, v * (nrand(texcoord + vec2(0.0, 1536.0/texsize.y)) * 0.25) + 512.0*0.6666/texsize.y));
+	cosy += scanline(texco + vec2(0.75, v * (nrand(texcoord + vec2(0.0, 2048.0/texsize.y)) * 0.25) + 512.0*0.75/texsize.y));
 	cosy *= 0.25;
 
 	// final scanline + burn
--- a/shaders/crt.v.glsl	Sun Apr 19 00:59:49 2020 -0700
+++ b/shaders/crt.v.glsl	Sat May 09 23:39:44 2020 -0700
@@ -1,13 +1,13 @@
-#version 110
 
 attribute vec2 pos;
-varying vec2 texcoord;
-varying vec2 screencoord;
-uniform float width, height;
+varying mediump vec2 texcoord;
+varying mediump vec2 screencoord;
+uniform mediump float width, height;
+uniform mediump vec2 texsize;
 
 void main()
 {
 	gl_Position = vec4(pos, 0.0, 1.0);
-	texcoord = sign(pos) * vec2(width/1024.0, height/-1024.0) + vec2(width/1024.0, height/1024.0);
+	texcoord = sign(pos) * vec2(0.5*width/texsize.x, -0.5*height/texsize.y) + vec2(0.5*width/texsize.x, 0.5*height/texsize.y);
 	screencoord = sign(pos);
 }
\ No newline at end of file
--- a/shaders/default.f.glsl	Sun Apr 19 00:59:49 2020 -0700
+++ b/shaders/default.f.glsl	Sat May 09 23:39:44 2020 -0700
@@ -1,15 +1,16 @@
 
 uniform sampler2D textures[2];
+uniform mediump vec2 texsize;
 
 varying mediump vec2 texcoord;
 
 void main()
 {
-	mediump vec2 modifiedCoord0 = vec2(texcoord.x, (floor(texcoord.y * 512.0 + 0.25) + 0.5)/512.0);
-	mediump vec2 modifiedCoord1 = vec2(texcoord.x, (floor(texcoord.y * 512.0 - 0.25) + 0.5)/512.0);
+	mediump vec2 modifiedCoord0 = vec2(texcoord.x, (floor(texcoord.y * texsize.y + 0.25) + 0.5)/texsize.y);
+	mediump vec2 modifiedCoord1 = vec2(texcoord.x, (floor(texcoord.y * texsize.y - 0.25) + 0.5)/texsize.y);
 	gl_FragColor = mix(
 		texture2D(textures[1], modifiedCoord1),
 		texture2D(textures[0], modifiedCoord0),
-		(sin(texcoord.y * 1024.0 * 3.14159265359) + 1.0) * 0.5
+		(sin(texcoord.y * texsize.y * 6.283185307) + 1.0) * 0.5
 	);
 }
--- a/shaders/default.v.glsl	Sun Apr 19 00:59:49 2020 -0700
+++ b/shaders/default.v.glsl	Sat May 09 23:39:44 2020 -0700
@@ -2,9 +2,10 @@
 attribute vec2 pos;
 varying mediump vec2 texcoord;
 uniform mediump float width, height;
+uniform mediump vec2 texsize;
 
 void main()
 {
 	gl_Position = vec4(pos, 0.0, 1.0);
-	texcoord = sign(pos) * vec2(width / 1024.0, height / -1024.0) + vec2(width / 1024.0, height / 1024.0);
+	texcoord = sign(pos) * vec2(0.5 * width / texsize.x, -0.5 * height / texsize.y) + vec2(0.5 * width / texsize.x, 0.5 * height / texsize.y);
 }
--- a/shaders/sharp.f.glsl	Sun Apr 19 00:59:49 2020 -0700
+++ b/shaders/sharp.f.glsl	Sat May 09 23:39:44 2020 -0700
@@ -1,20 +1,18 @@
 
 uniform sampler2D textures[2];
+uniform mediump vec2 texsize;
 
 varying mediump vec2 texcoord;
 
 void main()
 {
-	mediump float x0 = (floor(texcoord.x * 512.0 - 0.25) + 0.5)/512.0;
-	mediump float x1 = (floor(texcoord.x * 512.0 + 0.25) + 0.5)/512.0;
-	mediump float y0 = (floor(texcoord.y * 512.0 + 0.25) + 0.5)/512.0;
-	mediump float y1 = (floor(texcoord.y * 512.0 - 0.25) + 0.5)/512.0;
+	mediump float x0 = (floor(texcoord.x * texsize.x - 0.25) + 0.5)/texsize.x;
+	mediump float x1 = (floor(texcoord.x * texsize.x + 0.25) + 0.5)/texsize.x;
+	mediump float y0 = (floor(texcoord.y * texsize.y + 0.25) + 0.5)/texsize.y;
+	mediump float y1 = (floor(texcoord.y * texsize.y - 0.25) + 0.5)/texsize.y;
 	
-	
-	mediump vec2 modifiedCoord0 = vec2(texcoord.x, (floor(texcoord.y * 512.0 + 0.25) + 0.5)/512.0);
-	mediump vec2 modifiedCoord1 = vec2(texcoord.x, (floor(texcoord.y * 512.0 - 0.25) + 0.5)/512.0);
-	mediump float ymix = (sin(texcoord.y * 1024.0 * 3.14159265359) + 1.0) * 0.5;
-	mediump float xmix = (sin(texcoord.x * 1024.0 * 3.14159265359) + 1.0) * 0.5;
+	mediump float ymix = (sin(texcoord.y * texsize.y * 6.283185307) + 1.0) * 0.5;
+	mediump float xmix = (sin(texcoord.x * texsize.x * 6.283185307) + 1.0) * 0.5;
 	gl_FragColor = mix(
 		mix(
 			texture2D(textures[1], vec2(x0, y1)),
--- a/sms.c	Sun Apr 19 00:59:49 2020 -0700
+++ b/sms.c	Sat May 09 23:39:44 2020 -0700
@@ -430,7 +430,7 @@
 			target_cycle -= adjust;
 		}
 	}
-	if (render_should_release_on_exit()) {
+	if (sms->header.force_release || render_should_release_on_exit()) {
 		bindings_release_capture();
 		vdp_release_framebuffer(sms->vdp);
 		render_pause_source(sms->psg->audio);
@@ -441,7 +441,8 @@
 static void resume_sms(system_header *system)
 {
 	sms_context *sms = (sms_context *)system;
-	if (render_should_release_on_exit()) {
+	if (sms->header.force_release || render_should_release_on_exit()) {
+		sms->header.force_release = 0;
 		bindings_reacquire_capture();
 		vdp_reacquire_framebuffer(sms->vdp);
 		render_resume_source(sms->psg->audio);
--- a/stateview.c	Sun Apr 19 00:59:49 2020 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,127 +0,0 @@
-/*
- Copyright 2013 Michael Pavone
- This file is part of BlastEm. 
- BlastEm is free software distributed under the terms of the GNU General Public License version 3 or greater. See COPYING for full license text.
-*/
-#include <stdlib.h>
-#include <stdio.h>
-#include "vdp.h"
-#include "render.h"
-#include "util.h"
-#include "genesis.h"
-#include "config.h"
-
-
-uint16_t read_dma_value(uint32_t address)
-{
-	return 0;
-}
-
-m68k_context *m68k_handle_code_write(uint32_t address, m68k_context *context)
-{
-	return NULL;
-}
-
-z80_context *z80_handle_code_write(uint32_t address, z80_context *context)
-{
-	return NULL;
-}
-
-void ym_data_write(ym2612_context * context, uint8_t value)
-{
-}
-
-void ym_address_write_part1(ym2612_context * context, uint8_t address)
-{
-}
-
-void ym_address_write_part2(ym2612_context * context, uint8_t address)
-{
-}
-
-void handle_keydown(int keycode, uint8_t scancode)
-{
-}
-
-void handle_keyup(int keycode, uint8_t scancode)
-{
-}
-
-void handle_joydown(int joystick, int button)
-{
-}
-
-void handle_joyup(int joystick, int button)
-{
-}
-
-void handle_joy_dpad(int joystick, int dpadnum, uint8_t value)
-{
-}
-
-void handle_joy_axis(int joystick, int axis, int16_t value)
-{
-}
-
-void handle_joy_added(int joystick)
-{
-}
-
-void handle_mousedown(int mouse, int button)
-{
-}
-
-void handle_mouseup(int mouse, int button)
-{
-}
-
-void handle_mouse_moved(int mouse, uint16_t x, uint16_t y, int16_t deltax, int16_t deltay)
-{
-}
-
-tern_node * config;
-int headless = 0;
-
-int main(int argc, char ** argv)
-{
-	if (argc < 2) {
-		fatal_error("Usage: stateview FILENAME\n");
-	}
-	FILE * state_file = fopen(argv[1], "rb");
-	if (!state_file) {
-		fatal_error("Failed to open %s\n", argv[1]);
-	}
-	set_exe_str(argv[0]);
-	config = load_config(argv[0]);
-	int width = -1;
-	int height = -1;
-	if (argc > 2) {
-		width = atoi(argv[2]);
-		if (argc > 3) {
-			height = atoi(argv[3]);
-		}
-	}
-	int def_width = 0;
-	char *config_width = tern_find_ptr(config, "videowidth");
-	if (config_width) {
-		def_width = atoi(config_width);
-	}
-	if (!def_width) {
-		def_width = 640;
-	}
-	width = width < 320 ? def_width : width;
-	height = height < 240 ? (width/320) * 240 : height;
-
-	render_init(width, height, "GST State Viewer", 0);
-	vdp_context *context = init_vdp_context(0, 0);
-	vdp_load_gst(context, state_file);
-	vdp_run_to_vblank(context);
-	vdp_print_sprite_table(context);
-	printf("Display %s\n", (context->regs[REG_MODE_2] & DISPLAY_ENABLE) ? "enabled" : "disabled");
-	if (!(context->regs[REG_MODE_2] & DISPLAY_ENABLE)) {
-		puts("Forcing display on");
-		vdp_control_port_write(context, 0x8000 | REG_MODE_2 << 8 | context->regs[REG_MODE_2] | DISPLAY_ENABLE);
-	}
-    render_wait_quit();
-    return 0;
-}
--- a/system.c	Sun Apr 19 00:59:49 2020 -0700
+++ b/system.c	Sat May 09 23:39:44 2020 -0700
@@ -1,6 +1,7 @@
 #include <string.h>
 #include "system.h"
 #include "genesis.h"
+#include "gen_player.h"
 #include "sms.h"
 
 uint8_t safe_cmp(char *str, long offset, uint8_t *buffer, long filesize)
@@ -21,6 +22,14 @@
 	) {
 		return SYSTEM_SMS;
 	}
+	if (safe_cmp("BLSTEL\x02", 0, media->buffer, media->size)) {
+		uint8_t *buffer = media->buffer;
+		if (media->size > 9 && buffer[7] == 0) {
+			return buffer[8] + 1;
+		}
+	}
+		
+	
 	//TODO: Detect Jaguar ROMs here
 	
 	//Header based detection failed, examine filename for clues
@@ -60,6 +69,8 @@
 	{
 	case SYSTEM_GENESIS:
 		return &(alloc_config_genesis(media->buffer, media->size, lock_on, lock_on_size, opts, force_region))->header;
+	case SYSTEM_GENESIS_PLAYER:
+		return &(alloc_config_gen_player(media->buffer, media->size))->header;
 #ifndef NO_Z80
 	case SYSTEM_SMS:
 		return &(alloc_configure_sms(media, opts, force_region))->header;
@@ -68,3 +79,19 @@
 		return NULL;
 	}
 }
+
+system_header *alloc_config_player(system_type stype, event_reader *reader)
+{
+	switch(stype)
+	{
+	case SYSTEM_GENESIS:
+		return &(alloc_config_gen_player_reader(reader))->header;
+	}
+	return NULL;
+}
+
+void system_request_exit(system_header *system, uint8_t force_release)
+{
+	system->force_release = force_release;
+	system->request_exit(system);
+}
--- a/system.h	Sun Apr 19 00:59:49 2020 -0700
+++ b/system.h	Sat May 09 23:39:44 2020 -0700
@@ -9,8 +9,10 @@
 typedef enum {
 	SYSTEM_UNKNOWN,
 	SYSTEM_GENESIS,
+	SYSTEM_GENESIS_PLAYER,
 	SYSTEM_SMS,
-	SYSTEM_JAGUAR
+	SYSTEM_SMS_PLAYER,
+	SYSTEM_JAGUAR,
 } system_type;
 
 typedef enum {
@@ -33,6 +35,7 @@
 
 #include "arena.h"
 #include "romdb.h"
+#include "event_log.h"
 
 struct system_header {
 	system_header           *next_context;
@@ -70,6 +73,7 @@
 	uint8_t                 delayed_load_slot;
 	uint8_t                 has_keyboard;
 	uint8_t                 vgm_logging;
+	uint8_t                 force_release;
 	debugger_type           debugger_type;
 	system_type             type;
 };
@@ -87,5 +91,7 @@
 
 system_type detect_system_type(system_media *media);
 system_header *alloc_config_system(system_type stype, system_media *media, uint32_t opts, uint8_t force_region);
+system_header *alloc_config_player(system_type stype, event_reader *reader);
+void system_request_exit(system_header *system, uint8_t force_release);
 
 #endif //SYSTEM_H_
--- a/util.c	Sun Apr 19 00:59:49 2020 -0700
+++ b/util.c	Sat May 09 23:39:44 2020 -0700
@@ -7,7 +7,6 @@
 
 #include <sys/types.h>
 #include <sys/stat.h>
-#include <unistd.h>
 #include <errno.h>
 
 #ifdef __ANDROID__
@@ -442,6 +441,10 @@
 	exit(1);
 }
 
+#ifndef _WIN32
+#include <unistd.h>
+#endif
+
 void warning(char *format, ...)
 {
 	va_list args;
@@ -532,6 +535,8 @@
 }
 
 #ifdef _WIN32
+#define WINVER 0x501
+#include <winsock2.h>
 #include <windows.h>
 #include <shlobj.h>
 
@@ -679,7 +684,80 @@
 	return CreateDirectory(path, NULL);
 }
 
+static WSADATA wsa_data;
+static void socket_cleanup(void)
+{
+	WSACleanup();
+}
+
+void socket_init(void)
+{
+	static uint8_t started;
+	if (!started) {
+		started = 1;
+		WSAStartup(MAKEWORD(2,2), &wsa_data);
+		atexit(socket_cleanup);
+	}
+}
+
+int socket_blocking(int sock, int should_block)
+{
+	u_long param = !should_block;
+	if (ioctlsocket(sock, FIONBIO, &param)) {
+		return WSAGetLastError();
+	}
+	return 0;
+}
+
+void socket_close(int sock)
+{
+	closesocket(sock);
+}
+
+int socket_last_error(void)
+{
+	return WSAGetLastError();
+}
+
+int socket_error_is_wouldblock(void)
+{
+	return WSAGetLastError() == WSAEWOULDBLOCK;
+}
+
 #else
+#include <fcntl.h>
+#include <signal.h>
+
+void socket_init(void)
+{
+	//SIGPIPE on network sockets is not desired
+	//would be better to do this in a more limited way,
+	//but the alternatives are not portable
+	signal(SIGPIPE, SIG_IGN);
+}
+
+int socket_blocking(int sock, int should_block)
+{
+	if (fcntl(sock, F_SETFL, should_block ? 0 : O_NONBLOCK)) {
+		return errno;
+	}
+	return 0;
+}
+
+void socket_close(int sock)
+{
+	close(sock);
+}
+
+int socket_last_error(void)
+{
+	return errno;
+}
+
+int socket_error_is_wouldblock(void)
+{
+	return errno == EAGAIN || errno == EWOULDBLOCK;
+}
 
 char * get_home_dir()
 {
--- a/util.h	Sun Apr 19 00:59:49 2020 -0700
+++ b/util.h	Sat May 09 23:39:44 2020 -0700
@@ -90,5 +90,15 @@
 void disable_stdout_messages(void);
 //Deletes a file, returns true on success, false on failure
 uint8_t delete_file(char *path);
+//Initializes the socket library on platforms that need it
+void socket_init(void);
+//Sets a sockt to blocking or non-blocking mode
+int socket_blocking(int sock, int should_block);
+//Close a socket
+void socket_close(int sock);
+//Return the last error on a socket operation
+int socket_last_error(void);
+//Returns if the last socket error was EAGAIN/EWOULDBLOCK
+int socket_error_is_wouldblock(void);
 
 #endif //UTIL_H_
--- a/vdp.c	Sun Apr 19 00:59:49 2020 -0700
+++ b/vdp.c	Sat May 09 23:39:44 2020 -0700
@@ -9,6 +9,7 @@
 #include <string.h>
 #include "render.h"
 #include "util.h"
+#include "event_log.h"
 
 #define NTSC_INACTIVE_START 224
 #define PAL_INACTIVE_START 240
@@ -908,14 +909,17 @@
 		{
 		case VRAM_WRITE:
 			if ((context->regs[REG_MODE_2] & (BIT_128K_VRAM|BIT_MODE_5)) == (BIT_128K_VRAM|BIT_MODE_5)) {
+				event_vram_word(context->cycles, start->address, start->value);
 				vdp_check_update_sat(context, start->address, start->value);
 				write_vram_word(context, start->address, start->value);
 			} else {
 				uint8_t byte = start->partial == 1 ? start->value >> 8 : start->value;
-				vdp_check_update_sat_byte(context, start->address ^ 1, byte);
-				write_vram_byte(context, start->address ^ 1, byte);
+				uint32_t address = start->address ^ 1;
+				event_vram_byte(context->cycles, start->address, byte, context->regs[REG_AUTOINC]);
+				vdp_check_update_sat_byte(context, address, byte);
+				write_vram_byte(context, address, byte);
 				if (!start->partial) {
-					start->address = start->address ^ 1;
+					start->address = address;
 					start->partial = 1;
 					//skip auto-increment and removal of entry from fifo
 					return;
@@ -924,18 +928,20 @@
 			break;
 		case CRAM_WRITE: {
 			//printf("CRAM Write | %X to %X\n", start->value, (start->address/2) & (CRAM_SIZE-1));
+			uint16_t val;
 			if (start->partial == 3) {
-				uint16_t val;
 				if ((start->address & 1) && (context->regs[REG_MODE_2] & BIT_MODE_5)) {
 					val = (context->cram[start->address >> 1 & (CRAM_SIZE-1)] & 0xFF) | start->value << 8;
 				} else {
 					uint16_t address = (context->regs[REG_MODE_2] & BIT_MODE_5) ? start->address >> 1 & (CRAM_SIZE-1) : start->address & 0x1F;
 					val = (context->cram[address] & 0xFF00) | start->value;
 				}
-				write_cram(context, start->address, val);
 			} else {
-				write_cram(context, start->address, start->partial ? context->fifo[context->fifo_write].value : start->value);
+				val = start->partial ? context->fifo[context->fifo_write].value : start->value;
 			}
+			uint8_t buffer[3] = {start->address & 127, val >> 8, val};
+			event_log(EVENT_VDP_INTRAM, context->cycles, sizeof(buffer), buffer);
+			write_cram(context, start->address, val);
 			break;
 		}
 		case VSRAM_WRITE:
@@ -952,6 +958,8 @@
 				} else {
 					context->vsram[(start->address/2) & 63] = start->partial ? context->fifo[context->fifo_write].value : start->value;
 				}
+				uint8_t buffer[3] = {((start->address/2) & 63) + 128, context->vsram[(start->address/2) & 63] >> 8, context->vsram[(start->address/2) & 63]};
+				event_log(EVENT_VDP_INTRAM, context->cycles, sizeof(buffer), buffer);
 			}
 
 			break;
@@ -3760,6 +3768,8 @@
 				/*if (reg == REG_MODE_4 && ((value ^ context->regs[reg]) & BIT_H40)) {
 					printf("Mode changed from H%d to H%d @ %d, frame: %d\n", context->regs[reg] & BIT_H40 ? 40 : 32, value & BIT_H40 ? 40 : 32, context->cycles, context->frame);
 				}*/
+				uint8_t buffer[2] = {reg, value};
+				event_log(EVENT_VDP_REG, context->cycles, sizeof(buffer), buffer);
 				context->regs[reg] = value;
 				if (reg == REG_MODE_4) {
 					context->double_res = (value & (BIT_INTERLACE | BIT_DOUBLE_RES)) == (BIT_INTERLACE | BIT_DOUBLE_RES);
@@ -4551,3 +4561,85 @@
 		}
 	}
 }
+
+void vdp_replay_event(vdp_context *context, uint8_t event, event_reader *reader)
+{
+	uint32_t address;
+	deserialize_buffer *buffer = &reader->buffer;
+	switch (event)
+	{
+	case EVENT_VRAM_BYTE:
+		reader_ensure_data(reader, 3);
+		address = load_int16(buffer);
+		break;
+	case EVENT_VRAM_BYTE_DELTA:
+		reader_ensure_data(reader, 2);
+		address = reader->last_byte_address + load_int8(buffer);
+		break;
+	case EVENT_VRAM_BYTE_ONE:
+		reader_ensure_data(reader, 1);
+		address = reader->last_byte_address + 1;
+		break;
+	case EVENT_VRAM_BYTE_AUTO:
+		reader_ensure_data(reader, 1);
+		address = reader->last_byte_address + context->regs[REG_AUTOINC];
+		break;
+	case EVENT_VRAM_WORD:
+		reader_ensure_data(reader, 4);
+		address = load_int8(buffer) << 16;
+		address |= load_int16(buffer);
+		break;
+	case EVENT_VRAM_WORD_DELTA:
+		reader_ensure_data(reader, 3);
+		address = reader->last_word_address + load_int8(buffer);
+		break;
+	case EVENT_VDP_REG:
+	case EVENT_VDP_INTRAM:
+		reader_ensure_data(reader, event == EVENT_VDP_REG ? 2 : 3);
+		address = load_int8(buffer);
+		break;
+	}
+	
+	switch (event)
+	{
+	case EVENT_VDP_REG: {
+		uint8_t value = load_int8(buffer);
+		context->regs[address] = value;
+		if (address == REG_MODE_4) {
+			context->double_res = (value & (BIT_INTERLACE | BIT_DOUBLE_RES)) == (BIT_INTERLACE | BIT_DOUBLE_RES);
+			if (!context->double_res) {
+				context->flags2 &= ~FLAG2_EVEN_FIELD;
+			}
+		}
+		if (address == REG_MODE_1 || address == REG_MODE_2 || address == REG_MODE_4) {
+			update_video_params(context);
+		}
+		break;
+	}
+	case EVENT_VRAM_BYTE:
+	case EVENT_VRAM_BYTE_DELTA:
+	case EVENT_VRAM_BYTE_ONE:
+	case EVENT_VRAM_BYTE_AUTO: {
+		uint8_t byte = load_int8(buffer);
+		reader->last_byte_address = address;
+		vdp_check_update_sat_byte(context, address ^ 1, byte);
+		write_vram_byte(context, address ^ 1, byte);
+		break;
+	}
+	case EVENT_VRAM_WORD:
+	case EVENT_VRAM_WORD_DELTA: {
+		uint16_t value = load_int16(buffer);
+		reader->last_word_address = address;
+		vdp_check_update_sat(context, address, value);
+		write_vram_word(context, address, value);
+		break;
+	}
+	case EVENT_VDP_INTRAM:
+		if (address < 128) {
+			write_cram(context, address, load_int16(buffer));
+		} else {
+			context->vsram[address&63] = load_int16(buffer);
+		}
+		break;
+	}
+}
--- a/vdp.h	Sun Apr 19 00:59:49 2020 -0700
+++ b/vdp.h	Sat May 09 23:39:44 2020 -0700
@@ -285,5 +285,6 @@
 void vdp_inc_debug_mode(vdp_context *context);
 //to be implemented by the host system
 uint16_t read_dma_value(uint32_t address);
+void vdp_replay_event(vdp_context *context, uint8_t event, event_reader *reader);
 
 #endif //VDP_H_
--- a/vgmplay.c	Sun Apr 19 00:59:49 2020 -0700
+++ b/vgmplay.c	Sat May 09 23:39:44 2020 -0700
@@ -13,6 +13,7 @@
 #include <stdlib.h>
 #include <string.h>
 #include "vgm.h"
+#include "system.h"
 
 #define MCLKS_NTSC 53693175
 #define MCLKS_PAL  53203395
@@ -22,6 +23,8 @@
 #define MCLKS_PER_Z80 15
 #define MCLKS_PER_PSG (MCLKS_PER_Z80*16)
 
+system_header *current_system;
+
 void handle_keydown(int keycode)
 {
 }
--- a/ym2612.c	Sun Apr 19 00:59:49 2020 -0700
+++ b/ym2612.c	Sat May 09 23:39:44 2020 -0700
@@ -11,6 +11,7 @@
 #include "render.h"
 #include "wave.h"
 #include "blastem.h"
+#include "event_log.h"
 
 //#define DO_DEBUG_PRINT
 #ifdef DO_DEBUG_PRINT
@@ -825,6 +826,8 @@
 		}
 		context->part1_regs[context->selected_reg - YM_PART1_START] = value;
 	}
+	uint8_t buffer[3] = {context->selected_part, context->selected_reg, value};
+	event_log(EVENT_YM_REG, context->current_cycle, sizeof(buffer), buffer);
 	dfprintf(debug_file, "write of %X to reg %X in part %d\n", value, context->selected_reg, context->selected_part+1);
 	if (context->selected_reg < 0x30) {
 		//Shared regs