changeset 2041:638eb2d25696 mame_interp

Merge from default
author Michael Pavone <pavone@retrodev.com>
date Thu, 05 Aug 2021 09:29:33 -0700
parents 0d5f88e53dca (current diff) a61b47d5489e (diff)
children 3142602d21d8
files Makefile backend.c blastem.c genesis.c genesis.h m68k_core.h nuklear_ui/nuklear.h trans.c
diffstat 47 files changed, 1914 insertions(+), 183 deletions(-) [+]
line wrap: on
line diff
--- a/Makefile	Sun May 10 00:16:00 2020 -0700
+++ b/Makefile	Thu Aug 05 09:29:33 2021 -0700
@@ -101,7 +101,8 @@
 
 ifeq ($(OS),Darwin)
 SDL_INCLUDE_PATH:=Frameworks/SDL2.framework/Headers
-LDFLAGS+= -FFrameworks -framework SDL2 -framework OpenGL -framework AppKit
+CFLAGS+=  -mmacosx-version-min=10.10
+LDFLAGS+= -FFrameworks -framework SDL2 -framework OpenGL -framework AppKit -mmacosx-version-min=10.10
 FIXUP:=install_name_tool -change @rpath/SDL2.framework/Versions/A/SDL2 @executable_path/Frameworks/SDL2.framework/Versions/A/SDL2
 else
 SDL_INCLUDE_PATH:=sdl/include
@@ -222,7 +223,7 @@
 
 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 \
-	$(M68KOBJS) $(TRANSOBJS) $(AUDIOOBJS) saves.o jcart.o rom.db.o
+	$(M68KOBJS) $(TRANSOBJS) $(AUDIOOBJS) saves.o jcart.o rom.db.o gen_player.o $(LIBZOBJS)
 	
 ifdef NONUKLEAR
 CFLAGS+= -DDISABLE_NUKLEAR
@@ -302,7 +303,7 @@
 	ar rcs libemu68k.a $(M68KOBJS) $(TRANSOBJS)
 
 trans : trans.o serialize.o $(M68KOBJS) $(TRANSOBJS) util.o
-	$(CC) -o trans trans.o $(M68KOBJS) $(TRANSOBJS) util.o $(OPT)
+	$(CC) -o $@ $^ $(OPT)
 
 transz80 : transz80.o $(Z80OBJS) $(TRANSOBJS)
 	$(CC) -o transz80 transz80.o $(Z80OBJS) $(TRANSOBJS)
@@ -371,6 +372,9 @@
 %.bin : %.s68
 	vasmm68k_mot -Fbin -m68000 -no-opt -spaces -o $@ -L $@.list $<
 
+%.md : %.s68
+	vasmm68k_mot -Fbin -m68000 -no-opt -spaces -o $@ -L $@.list $<
+
 %.bin : %.sz8
 	vasmz80_mot -Fbin -spaces -o $@ $<
 res.o : blastem.rc
@@ -383,6 +387,7 @@
 font.tiles : font.png
 
 menu.bin : font_interlace_variable.tiles arrow.tiles cursor.tiles button.tiles font.tiles
+tmss.md : font.tiles
 
 clean :
 	rm -rf $(ALL) trans ztestrun ztestgen *.o nuklear_ui/*.o zlib/*.o
--- a/backend.c	Sun May 10 00:16:00 2020 -0700
+++ b/backend.c	Thu Aug 05 09:29:33 2021 -0700
@@ -58,13 +58,23 @@
 	if (size_sum) {
 		*size_sum = 0;
 	}
+	uint32_t minsize;
+	if (flags == MMAP_CODE) {
+		minsize = 1 << (opts->ram_flags_shift + 3);
+	} else {
+		minsize = 0;
+	}
 	address &= opts->address_mask;
 	for (memmap_chunk const *cur = opts->memmap, *end = opts->memmap + opts->memmap_chunks; cur != end; cur++)
 	{
 		if (address >= cur->start && address < cur->end) {
 			return cur;
 		} else if (size_sum && (cur->flags & flags) == flags) {
-			*size_sum += chunk_size(opts, cur);
+			uint32_t size = chunk_size(opts, cur);
+			if (size < minsize) {
+				size = minsize;
+			}
+			*size_sum += size;
 		}
 	}
 	return NULL;
@@ -271,13 +281,15 @@
 uint32_t ram_size(cpu_options *opts)
 {
 	uint32_t size = 0;
+	uint32_t minsize = 1 << (opts->ram_flags_shift + 3);
 	for (int i = 0; i < opts->memmap_chunks; i++)
 	{
 		if (opts->memmap[i].flags & MMAP_CODE) {
-			if (opts->memmap[i].mask == opts->address_mask) {
-				size += opts->memmap[i].end - opts->memmap[i].start;
+			uint32_t cursize = chunk_size(opts, opts->memmap + i);
+			if (cursize < minsize) {
+				size += minsize;
 			} else {
-				size += opts->memmap[i].mask + 1;
+				size += cursize;
 			}
 		}
 	}
--- a/backend_x86.c	Sun May 10 00:16:00 2020 -0700
+++ b/backend_x86.c	Thu Aug 05 09:29:33 2021 -0700
@@ -299,10 +299,16 @@
 			retn(code);
 		}
 		if (memmap[chunk].flags & MMAP_CODE) {
+			uint32_t added_offset;
 			if (memmap[chunk].mask == opts->address_mask) {
-				ram_flags_off += (memmap[chunk].end - memmap[chunk].start) / (1 << opts->ram_flags_shift) / 8; ;
+				added_offset = (memmap[chunk].end - memmap[chunk].start) / (1 << opts->ram_flags_shift) / 8;
 			} else {
-				ram_flags_off += (memmap[chunk].mask + 1) /  (1 << opts->ram_flags_shift) / 8;;
+				added_offset = (memmap[chunk].mask + 1) /  (1 << opts->ram_flags_shift) / 8;
+			}
+			if (added_offset) {
+				ram_flags_off += added_offset;
+			} else {
+				ram_flags_off += 1;
 			}
 		}
 		if (lb_jcc) {
--- a/blastem.c	Sun May 10 00:16:00 2020 -0700
+++ b/blastem.c	Thu Aug 05 09:29:33 2021 -0700
@@ -281,12 +281,23 @@
 	return save_dir;
 }
 
+const char *get_save_fname(uint8_t save_type)
+{
+	switch(save_type)
+	{
+	case SAVE_I2C: return "save.eeprom";
+	case SAVE_NOR: return "save.nor";
+	case SAVE_HBPT: return "save.hbpt";
+	default: return "save.sram";
+	}
+}
+
 void setup_saves(system_media *media, system_header *context)
 {
 	static uint8_t persist_save_registered;
 	rom_info *info = &context->info;
 	char *save_dir = get_save_dir(info->is_save_lock_on ? media->chain : media);
-	char const *parts[] = {save_dir, PATH_SEP, info->save_type == SAVE_I2C ? "save.eeprom" : info->save_type == SAVE_NOR ? "save.nor" : "save.sram"};
+	char const *parts[] = {save_dir, PATH_SEP, get_save_fname(info->save_type)};
 	free(save_filename);
 	save_filename = alloc_concat_m(3, parts);
 	if (info->is_save_lock_on) {
--- a/build_release	Sun May 10 00:16:00 2020 -0700
+++ b/build_release	Thu Aug 05 09:29:33 2021 -0700
@@ -32,7 +32,7 @@
 	make PORTABLE=1 clean all
 	SDLDLLPATH=sdl/i686-w64-mingw32/bin
 fi
-make menu.bin
+make menu.bin tmss.md
 if [ $OS = "Windows" -o $OS = "Win64" ]; then
 	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`
@@ -47,7 +47,7 @@
 	verstr=`./blastem -v`
 	txt=""
 fi
-binaries="$binaries menu.bin"
+binaries="$binaries menu.bin tmss.md"
 ver=`echo $verstr | awk '/blastem/ { gsub(/\r/, "", $2); print $2 }'`
 if [ $OS = "Windows" ]; then
 	suffix='-win32'
--- a/config.c	Sun May 10 00:16:00 2020 -0700
+++ b/config.c	Thu Aug 05 09:29:33 2021 -0700
@@ -298,6 +298,21 @@
 	persist_config_at(config, config, "blastem.cfg");
 }
 
+void delete_custom_config_at(char *fname)
+{
+	char *confpath = path_append(get_exe_dir(), fname);
+	delete_file(confpath);
+	free(confpath);
+	confpath = path_append(get_config_dir(), fname);
+	delete_file(confpath);
+	free(confpath);
+}
+
+void delete_custom_config(void)
+{
+	delete_custom_config_at("blastem.cfg");
+}
+
 char **get_extension_list(tern_node *config, uint32_t *num_exts_out)
 {
 	char *ext_filter = strdup(tern_find_path_default(config, "ui\0extensions\0", (tern_val){.ptrval = "bin gen md smd sms gg"}, TVAL_PTR).ptrval);
--- a/config.h	Sun May 10 00:16:00 2020 -0700
+++ b/config.h	Thu Aug 05 09:29:33 2021 -0700
@@ -16,6 +16,8 @@
 uint8_t serialize_config_file(tern_node *config, char *path);
 void persist_config_at(tern_node *app_config, tern_node *to_save, char *fname);
 void persist_config(tern_node *config);
+void delete_custom_config_at(char *fname);
+void delete_custom_config(void);
 char **get_extension_list(tern_node *config, uint32_t *num_exts_out);
 uint32_t get_lowpass_cutoff(tern_node *config);
 tern_node *get_systems_config(void);
--- a/controller_info.c	Sun May 10 00:16:00 2020 -0700
+++ b/controller_info.c	Thu Aug 05 09:29:33 2021 -0700
@@ -62,7 +62,10 @@
 static const char *variant_names[] = {
 	"normal",
 	"6b bumpers",
-	"6b right"
+	"6b right",
+	"3button",
+	"6button",
+	"8button"
 };
 
 static void load_ctype_config(void)
@@ -221,6 +224,15 @@
 #endif
 }
 
+void delete_controller_info(void)
+{
+	delete_custom_config_at("controller_types.cfg");
+	loaded = 0;
+	tern_free(info_config);
+	info_config = NULL;
+	render_reset_mappings();
+}
+
 char const *labels_xbox[] = {
 	"A", "B", "X", "Y", "Back", NULL, "Start", "Click", "Click", "White", "Black", "LT", "RT"
 };
@@ -242,6 +254,12 @@
 static char const *labels_genesis[] = {
 	"A", "B", "X", "Y", NULL, NULL, "Start", NULL, NULL, "Z", "C", NULL, "Mode"
 };
+static char const *labels_genesis_3button[] = {
+	"A", "B", NULL, NULL, NULL, NULL, "Start", NULL, NULL, NULL, "C", NULL, "Mode"
+};
+static char const *labels_genesis_8button[] = {
+	"A", "B", "X", "Y", "Mode", NULL, "Start", NULL, NULL, "Z", "C", "L", "R"
+};
 static char const *labels_saturn[] = {
 	"A", "B", "X", "Y", NULL, NULL, "Start", NULL, NULL, "Z", "C", "LT", "RT"
 };
@@ -266,7 +284,13 @@
 		}
 	} else {
 		if (info->subtype == SUBTYPE_GENESIS) {
-			return labels_genesis;
+			if (info->variant == VARIANT_8BUTTON) {
+				return labels_genesis_8button;
+			} else if (info->variant == VARIANT_3BUTTON) {
+				return labels_genesis_3button;
+			} else {
+				return labels_genesis;
+			}
 		} else {
 			return labels_saturn;
 		}
--- a/controller_info.h	Sun May 10 00:16:00 2020 -0700
+++ b/controller_info.h	Thu Aug 05 09:29:33 2021 -0700
@@ -30,6 +30,8 @@
 	VARIANT_NORMAL,
 	VARIANT_6B_BUMPERS, //C and Z positions are RB and LB respectively
 	VARIANT_6B_RIGHT, //C and Z positions are RT and RB respectively
+	VARIANT_3BUTTON, //3-button Gen/MD controller
+	VARIANT_8BUTTON, //Modern 8-button Gen/MD style controller (retro-bit, 8bitdo M30, etc.)
 	VARIANT_NUM
 };
 
@@ -45,6 +47,7 @@
 const char *get_axis_label(controller_info *info, int axis);
 void save_controller_info(int joystick, controller_info *info);
 void save_controller_mapping(int joystick, char *mapping_string);
+void delete_controller_info(void);
 void controller_add_mappings(void);
 char *make_controller_type_key(controller_info *info);
 char *make_human_readable_type_name(controller_info *info);
--- a/cpu_dsl.py	Sun May 10 00:16:00 2020 -0700
+++ b/cpu_dsl.py	Thu Aug 05 09:29:33 2021 -0700
@@ -899,7 +899,7 @@
 			else:
 				param = parent.resolveLocal(param) or param
 				if param in fieldVals:
-					param = fieldVals[index]
+					param = fieldVals[param]
 			prog.meta[self.params[0]] = param
 		elif self.op == 'dis':
 			#TODO: Disassembler
--- a/event_log.c	Sun May 10 00:16:00 2020 -0700
+++ b/event_log.c	Thu Aug 05 09:29:33 2021 -0700
@@ -10,6 +10,8 @@
 #include <netinet/tcp.h>
 #endif
 
+#include <stdlib.h>
+#include <string.h>
 #include <errno.h>
 #include "event_log.h"
 #include "util.h"
--- a/gen_player.c	Sun May 10 00:16:00 2020 -0700
+++ b/gen_player.c	Thu Aug 05 09:29:33 2021 -0700
@@ -1,7 +1,14 @@
+#include <stdlib.h>
 #include "gen_player.h"
 #include "event_log.h"
 #include "render.h"
 
+#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)
+
 #ifdef IS_LIB
 #define MAX_SOUND_CYCLES (MCLKS_PER_YM*NUM_OPERATORS*6*4)
 #else
@@ -103,7 +110,9 @@
 {
 	gen_player *player = (gen_player *)sys;
 	if (player->reader.socket) {
+#ifndef IS_LIB
 		render_create_thread(&player->thread, "player", thread_main, player);
+#endif
 	} else {
 		run(player);
 	}
@@ -121,12 +130,6 @@
 	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);
--- a/gen_player.h	Sun May 10 00:16:00 2020 -0700
+++ b/gen_player.h	Thu Aug 05 09:29:33 2021 -0700
@@ -14,7 +14,9 @@
 	vdp_context     *vdp;
 	ym2612_context  *ym;
 	psg_context     *psg;
+#ifndef IS_LIB
 	render_thread   thread;
+#endif
 	event_reader    reader;
 } gen_player;
 
--- a/gen_x86.c	Sun May 10 00:16:00 2020 -0700
+++ b/gen_x86.c	Thu Aug 05 09:29:33 2021 -0700
@@ -130,7 +130,7 @@
 	X86_R13,
 	X86_R14,
 	X86_R15
-} x86_regs_enc;
+};
 
 char * x86_reg_names[] = {
 #ifdef X86_64
--- a/gen_x86.h	Sun May 10 00:16:00 2020 -0700
+++ b/gen_x86.h	Thu Aug 05 09:29:33 2021 -0700
@@ -30,7 +30,7 @@
 	R13,
 	R14,
 	R15
-} x86_regs;
+};
 
 enum {
 	CC_O = 0,
@@ -51,14 +51,14 @@
 	CC_GE,
 	CC_LE,
 	CC_G
-} x86_cc;
+};
 
 enum {
 	SZ_B = 0,
 	SZ_W,
 	SZ_D,
 	SZ_Q
-} x86_size;
+};
 
 #ifdef X86_64
 #define SZ_PTR SZ_Q
@@ -85,7 +85,7 @@
 	MODE_REG_DIRECT = 0xC0,
 //"phony" mode
 	MODE_IMMED = 0xFF
-} x86_modes;
+};
 
 void rol_ir(code_info *code, uint8_t val, uint8_t dst, uint8_t size);
 void ror_ir(code_info *code, uint8_t val, uint8_t dst, uint8_t size);
--- a/genesis.c	Sun May 10 00:16:00 2020 -0700
+++ b/genesis.c	Thu Aug 05 09:29:33 2021 -0700
@@ -104,6 +104,16 @@
 		save_buffer8(buf, gen->zram, Z80_RAM_BYTES);
 		end_section(buf);
 		
+		if (gen->version_reg & 0xF) {
+			//only save TMSS info if it's present
+			//that will allow a state saved on a model lacking TMSS
+			//to be loaded on a model that has it
+			start_section(buf, SECTION_TMSS);
+			save_int8(buf, gen->tmss);
+			save_buffer16(buf, gen->tmss_lock, 2);
+			end_section(buf);
+		}
+		
 		cart_serialize(&gen->header, buf);
 	}
 }
@@ -173,7 +183,16 @@
 	gen->z80_bank_reg = load_int16(buf) & 0x1FF;
 }
 
+static void tmss_deserialize(deserialize_buffer *buf, void *vgen)
+{
+	genesis_context *gen = vgen;
+	gen->tmss = load_int8(buf);
+	load_buffer16(buf, gen->tmss_lock, 2);
+}
+
 static void adjust_int_cycle(m68k_context * context, vdp_context * v_context);
+static void check_tmss_lock(genesis_context *gen);
+static void toggle_tmss_rom(genesis_context *gen);
 void genesis_deserialize(deserialize_buffer *buf, genesis_context *gen)
 {
 	register_section_handler(buf, (section_handler){.fun = m68k_deserialize, .data = gen->m68k}, SECTION_68000);
@@ -188,10 +207,26 @@
 	register_section_handler(buf, (section_handler){.fun = ram_deserialize, .data = gen}, SECTION_MAIN_RAM);
 	register_section_handler(buf, (section_handler){.fun = zram_deserialize, .data = gen}, SECTION_SOUND_RAM);
 	register_section_handler(buf, (section_handler){.fun = cart_deserialize, .data = gen}, SECTION_MAPPER);
+	register_section_handler(buf, (section_handler){.fun = tmss_deserialize, .data = gen}, SECTION_TMSS);
+	uint8_t tmss_old = gen->tmss;
+	gen->tmss = 0xFF;
 	while (buf->cur_pos < buf->size)
 	{
 		load_section(buf);
 	}
+	if (gen->version_reg & 0xF) {
+		if (gen->tmss == 0xFF) {
+			//state lacked a TMSS section, assume that the game ROM is mapped in
+			//and that the VDP is unlocked
+			gen->tmss_lock[0] = 0x5345;
+			gen->tmss_lock[1] = 0x4741;
+			gen->tmss = 1;
+		}
+		if (gen->tmss != tmss_old) {
+			toggle_tmss_rom(gen);
+		}
+		check_tmss_lock(gen);
+	}
 	update_z80_bank_pointer(gen);
 	adjust_int_cycle(gen->m68k, gen->vdp);
 	free(buf->handlers);
@@ -242,13 +277,14 @@
 		context->sync_cycle = context->current_cycle + gen->max_cycles;
 	}
 	context->int_cycle = CYCLE_NEVER;
-	if ((context->status & 0x7) < 6) {
+	uint8_t mask = context->status & 0x7;
+	if (mask < 6) {
 		uint32_t next_vint = vdp_next_vint(v_context);
 		if (next_vint != CYCLE_NEVER) {
 			context->int_cycle = next_vint;
 			context->int_num = 6;
 		}
-		if ((context->status & 0x7) < 4) {
+		if (mask < 4) {
 			uint32_t next_hint = vdp_next_hint(v_context);
 			if (next_hint != CYCLE_NEVER) {
 				next_hint = next_hint < context->current_cycle ? context->current_cycle : next_hint;
@@ -258,6 +294,21 @@
 
 				}
 			}
+			if (mask < 2 && (v_context->regs[REG_MODE_3] & BIT_EINT_EN)) {
+				uint32_t next_eint_port0 = io_next_interrupt(gen->io.ports, context->current_cycle);
+				uint32_t next_eint_port1 = io_next_interrupt(gen->io.ports + 1, context->current_cycle);
+				uint32_t next_eint_port2 = io_next_interrupt(gen->io.ports + 2, context->current_cycle);
+				uint32_t next_eint = next_eint_port0 < next_eint_port1 
+					? (next_eint_port0 < next_eint_port2 ? next_eint_port0 : next_eint_port2)
+					: (next_eint_port1 < next_eint_port2 ? next_eint_port1 : next_eint_port2);
+				if (next_eint != CYCLE_NEVER) {
+					next_eint = next_eint < context->current_cycle ? context->current_cycle : next_eint;
+					if (next_eint < context->int_cycle) {
+						context->int_cycle = next_eint;
+						context->int_num = 2;
+					}
+				}
+			}
 		}
 	}
 	if (context->int_cycle > context->current_cycle && context->int_pending == INT_PENDING_SR_CHANGE) {
@@ -274,7 +325,7 @@
 	}
 
 	context->target_cycle = context->int_cycle < context->sync_cycle ? context->int_cycle : context->sync_cycle;
-	if (context->should_return) {
+	if (context->should_return || gen->header.enter_debugger) {
 		context->target_cycle = context->current_cycle;
 	} else if (context->target_cycle < context->current_cycle) {
 		//Changes to SR can result in an interrupt cycle that's in the past
@@ -389,6 +440,9 @@
 	sync_z80(z_context, mclks);
 	sync_sound(gen, mclks);
 	vdp_run_context(v_context, mclks);
+	io_run(gen->io.ports, mclks);
+	io_run(gen->io.ports + 1, mclks);
+	io_run(gen->io.ports + 2, mclks);
 	if (mclks >= gen->reset_cycle) {
 		gen->reset_requested = 1;
 		context->should_return = 1;
@@ -451,7 +505,11 @@
 #ifndef NEW_CORE
 		if (gen->header.enter_debugger) {
 			gen->header.enter_debugger = 0;
-			debugger(context, address);
+			if (gen->header.debugger_type == DEBUGGER_NATIVE) {
+				debugger(context, address);
+			} else {
+				gdb_debug_enter(context, address);
+			}
 		}
 #endif
 #ifdef NEW_CORE
@@ -512,6 +570,10 @@
 	if (vdp_port & 0x2700E0) {
 		fatal_error("machine freeze due to write to address %X\n", 0xC00000 | vdp_port);
 	}
+	genesis_context * gen = context->system;
+	if (!gen->vdp_unlocked) {
+		fatal_error("machine freeze due to VDP write to %X without TMSS unlock\n", 0xC00000 | vdp_port);
+	}
 	vdp_port &= 0x1F;
 	//printf("vdp_port write: %X, value: %X, cycle: %d\n", vdp_port, value, context->current_cycle);
 #ifdef REFRESH_EMULATION
@@ -524,7 +586,6 @@
 	}
 #endif
 	sync_components(context, 0);
-	genesis_context * gen = context->system;
 	vdp_context *v_context = gen->vdp;
 	uint32_t before_cycle = v_context->cycles;
 	if (vdp_port < 0x10) {
@@ -601,7 +662,7 @@
 		vdp_test_port_write(gen->vdp, value);
 	}
 #ifdef REFRESH_EMULATION
-	last_sync_cycle -= 4;
+	last_sync_cycle -= 4 * MCLKS_PER_68K;
 	//refresh may have happened while we were waiting on the VDP,
 	//so advance refresh_counter but don't add any delays
 	if (vdp_port >= 4 && vdp_port < 8 && v_context->cycles != before_cycle) {
@@ -653,6 +714,10 @@
 	if (vdp_port & 0x2700E0) {
 		fatal_error("machine freeze due to read from address %X\n", 0xC00000 | vdp_port);
 	}
+	genesis_context *gen = context->system;
+	if (!gen->vdp_unlocked) {
+		fatal_error("machine freeze due to VDP read from %X without TMSS unlock\n", 0xC00000 | vdp_port);
+	}
 	vdp_port &= 0x1F;
 	uint16_t value;
 #ifdef REFRESH_EMULATION
@@ -664,27 +729,15 @@
 	last_sync_cycle = context->current_cycle;
 	}
 #endif
-	genesis_context *gen = context->system;
+	sync_components(context, 0);
 	vdp_context * v_context = gen->vdp;
+	uint32_t before_cycle = v_context->cycles;
 	if (vdp_port < 0x10) {
 		if (vdp_port < 4) {
-			sync_components(context, 0);
-			uint32_t before_cycle = v_context->cycles;
 			value = vdp_data_port_read(v_context);
-			if (v_context->cycles != before_cycle) {
-				//printf("68K paused for %d (%d) cycles at cycle %d (%d) for read\n", v_context->cycles - context->current_cycle, v_context->cycles - before_cycle, context->current_cycle, before_cycle);
-				context->current_cycle = v_context->cycles;
-				//Lock the Z80 out of the bus until the VDP access is complete
-				genesis_context *gen = context->system;
-				gen->bus_busy = 1;
-				sync_z80(gen->z80, v_context->cycles);
-				gen->bus_busy = 0;
-			}
 		} else if(vdp_port < 8) {
-			vdp_run_context(v_context, context->current_cycle);
 			value = vdp_control_port_read(v_context);
 		} else {
-			vdp_run_context(v_context, context->current_cycle);
 			value = vdp_hv_counter_read(v_context);
 			//printf("HV Counter: %X at cycle %d\n", value, v_context->cycles);
 		}
@@ -693,8 +746,17 @@
 	} else {
 		value = get_open_bus_value(&gen->header);
 	}
+	if (v_context->cycles != before_cycle) {
+		//printf("68K paused for %d (%d) cycles at cycle %d (%d) for read\n", v_context->cycles - context->current_cycle, v_context->cycles - before_cycle, context->current_cycle, before_cycle);
+		context->current_cycle = v_context->cycles;
+		//Lock the Z80 out of the bus until the VDP access is complete
+		genesis_context *gen = context->system;
+		gen->bus_busy = 1;
+		sync_z80(gen->z80, v_context->cycles);
+		gen->bus_busy = 0;
+	}
 #ifdef REFRESH_EMULATION
-	last_sync_cycle -= 4;
+	last_sync_cycle -= 4 * MCLKS_PER_68K;
 	//refresh may have happened while we were waiting on the VDP,
 	//so advance refresh_counter but don't add any delays
 	refresh_counter += (context->current_cycle - last_sync_cycle);
@@ -755,6 +817,13 @@
 static m68k_context * io_write(uint32_t location, m68k_context * context, uint8_t value)
 {
 	genesis_context * gen = context->system;
+#ifdef REFRESH_EMULATION
+	//do refresh check here so we can avoid adding a penalty for a refresh that happens during an IO area 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 - 4*MCLKS_PER_68K;
+#endif
 	if (location < 0x10000) {
 		//Access to Z80 memory incurs a one 68K cycle wait state
 		context->current_cycle += MCLKS_PER_68K;
@@ -808,7 +877,7 @@
 				io_control_write(gen->io.ports+2, value, context->current_cycle);
 				break;
 			case 0x7:
-				gen->io.ports[0].serial_out = value;
+				io_tx_write(gen->io.ports, value, context->current_cycle);
 				break;
 			case 0x8:
 			case 0xB:
@@ -816,19 +885,20 @@
 				//serial input port is not writeable
 				break;
 			case 0x9:
+				io_sctrl_write(gen->io.ports, value, context->current_cycle);
 				gen->io.ports[0].serial_ctrl = value;
 				break;
 			case 0xA:
-				gen->io.ports[1].serial_out = value;
+				io_tx_write(gen->io.ports + 1, value, context->current_cycle);
 				break;
 			case 0xC:
-				gen->io.ports[1].serial_ctrl = value;
+				io_sctrl_write(gen->io.ports + 1, value, context->current_cycle);
 				break;
 			case 0xD:
-				gen->io.ports[2].serial_out = value;
+				io_tx_write(gen->io.ports + 2, value, context->current_cycle);
 				break;
 			case 0xF:
-				gen->io.ports[2].serial_ctrl = value;
+				io_sctrl_write(gen->io.ports + 2, value, context->current_cycle);
 				break;
 			}
 		} else {
@@ -879,6 +949,11 @@
 			}
 		}
 	}
+#ifdef REFRESH_EMULATION
+	//no refresh delays during IO access
+	refresh_counter += context->current_cycle - last_sync_cycle;
+	refresh_counter = refresh_counter % (MCLKS_PER_68K * REFRESH_INTERVAL);
+#endif
 	return context;
 }
 
@@ -902,6 +977,13 @@
 {
 	uint8_t value;
 	genesis_context *gen = context->system;
+#ifdef REFRESH_EMULATION
+	//do refresh check here so we can avoid adding a penalty for a refresh that happens during an IO area 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 - 4*MCLKS_PER_68K;
+#endif
 	if (location < 0x10000) {
 		//Access to Z80 memory incurs a one 68K cycle wait state
 		context->current_cycle += MCLKS_PER_68K;
@@ -952,28 +1034,28 @@
 				value = gen->io.ports[0].serial_out;
 				break;
 			case 0x8:
-				value = gen->io.ports[0].serial_in;
+				value = io_rx_read(gen->io.ports, context->current_cycle);
 				break;
 			case 0x9:
-				value = gen->io.ports[0].serial_ctrl;
+				value = io_sctrl_read(gen->io.ports, context->current_cycle);
 				break;
 			case 0xA:
 				value = gen->io.ports[1].serial_out;
 				break;
 			case 0xB:
-				value = gen->io.ports[1].serial_in;
+				value = io_rx_read(gen->io.ports + 1, context->current_cycle);
 				break;
 			case 0xC:
-				value = gen->io.ports[1].serial_ctrl;
+				value = io_sctrl_read(gen->io.ports, context->current_cycle);
 				break;
 			case 0xD:
 				value = gen->io.ports[2].serial_out;
 				break;
 			case 0xE:
-				value = gen->io.ports[2].serial_in;
+				value = io_rx_read(gen->io.ports + 1, context->current_cycle);
 				break;
 			case 0xF:
-				value = gen->io.ports[2].serial_ctrl;
+				value = io_sctrl_read(gen->io.ports, context->current_cycle);
 				break;
 			default:
 				value = get_open_bus_value(&gen->header) >> 8;
@@ -997,6 +1079,11 @@
 			}
 		}
 	}
+#ifdef REFRESH_EMULATION
+	//no refresh delays during IO access
+	refresh_counter += context->current_cycle - last_sync_cycle;
+	refresh_counter = refresh_counter % (MCLKS_PER_68K * REFRESH_INTERVAL);
+#endif
 	return value;
 }
 
@@ -1109,7 +1196,7 @@
 {
 	m68k_context *context = vcontext;
 	genesis_context *gen = context->system;
-	if ((location >= 0xA13000 && location < 0xA13100) || (location >= 0xA12000 && location < 0xA12100)) {
+	if (location < 0x800000 || (location >= 0xA13000 && location < 0xA13100) || (location >= 0xA12000 && location < 0xA12100)) {
 		//Only called if the cart/exp doesn't have a more specific handler for this region
 		return get_open_bus_value(&gen->header);
 	} else if (location == 0xA14000 || location == 0xA14002) {
@@ -1142,6 +1229,23 @@
 	}
 }
 
+static void check_tmss_lock(genesis_context *gen)
+{
+	gen->vdp_unlocked = gen->tmss_lock[0] == 0x5345 && gen->tmss_lock[1] == 0x4741;
+}
+
+static void toggle_tmss_rom(genesis_context *gen)
+{
+	m68k_context *context = gen->m68k;
+	for (int i = 0; i < NUM_MEM_AREAS; i++)
+	{
+		uint16_t *tmp = context->mem_pointers[i];
+		context->mem_pointers[i] = gen->tmss_pointers[i];
+		gen->tmss_pointers[i] = tmp;
+	}
+	m68k_invalidate_code_range(context, 0, 0x400000);
+}
+
 static void *unused_write(uint32_t location, void *vcontext, uint16_t value)
 {
 	m68k_context *context = vcontext;
@@ -1149,9 +1253,16 @@
 	uint8_t has_tmss = gen->version_reg & 0xF;
 	if (has_tmss && (location == 0xA14000 || location == 0xA14002)) {
 		gen->tmss_lock[location >> 1 & 1] = value;
+		check_tmss_lock(gen);
 	} else if (has_tmss && location == 0xA14100) {
-		//TODO: implement TMSS control register
-	} else if (location < 0xA12000 || location >= 0xA13100 || (location >= 0xA12100 && location < 0xA13000)) {
+		value &= 1;
+		if (gen->tmss != value) {
+			gen->tmss = value;
+			toggle_tmss_rom(gen);
+		}
+	} else if (location < 0x800000 || (location >= 0xA13000 && location < 0xA13100) || (location >= 0xA12000 && location < 0xA12100)) {
+		//these writes are ignored when no relevant hardware is present
+	} else {
 		fatal_error("Machine freeze due to unmapped write to %X\n", location);
 	}
 	return vcontext;
@@ -1171,9 +1282,18 @@
 			gen->tmss_lock[offset] &= 0xFF;
 			gen->tmss_lock[offset] |= value << 8;
 		}
+		check_tmss_lock(gen);
 	} else if (has_tmss && (location == 0xA14100 || location == 0xA14101)) {
-		//TODO: implement TMSS control register
-	} else if (location < 0xA12000 || location >= 0xA13100 || (location >= 0xA12100 && location < 0xA13000)) {
+		if (location & 1) {
+			value &= 1;
+			if (gen->tmss != value) {
+				gen->tmss = value;
+				toggle_tmss_rom(gen);
+			}
+		}
+	} else if (location < 0x800000 || (location >= 0xA13000 && location < 0xA13100) || (location >= 0xA12000 && location < 0xA12100)) {
+		//these writes are ignored when no relevant hardware is present
+	} else {
 		fatal_error("Machine freeze due to unmapped byte write to %X\n", location);
 	}
 	return vcontext;
@@ -1528,6 +1648,136 @@
 	gen->header.vgm_logging = 0;
 }
 
+static void *tmss_rom_write_16(uint32_t address, void *context, uint16_t value)
+{
+	m68k_context *m68k = context;
+	genesis_context *gen = m68k->system;
+	if (gen->tmss) {
+		return gen->tmss_write_16(address, context, value);
+	}
+	
+	return context;
+}
+
+static void *tmss_rom_write_8(uint32_t address, void *context, uint8_t value)
+{
+	m68k_context *m68k = context;
+	genesis_context *gen = m68k->system;
+	if (gen->tmss) {
+		return gen->tmss_write_8(address, context, value);
+	}
+	
+	return context;
+}
+
+static uint16_t tmss_rom_read_16(uint32_t address, void *context)
+{
+	m68k_context *m68k = context;
+	genesis_context *gen = m68k->system;
+	if (gen->tmss) {
+		return gen->tmss_read_16(address, context);
+	}
+	return ((uint16_t *)gen->tmss_buffer)[address >> 1];
+}
+
+static uint8_t tmss_rom_read_8(uint32_t address, void *context)
+{
+	m68k_context *m68k = context;
+	genesis_context *gen = m68k->system;
+	if (gen->tmss) {
+		return gen->tmss_read_8(address, context);
+	}
+#ifdef BLASTEM_BIG_ENDIAN
+	return gen->tmss_buffer[address];
+#else
+	return gen->tmss_buffer[address ^ 1];
+#endif
+}
+
+static void *tmss_word_write_16(uint32_t address, void *context, uint16_t value)
+{
+	m68k_context *m68k = context;
+	genesis_context *gen = m68k->system;
+	if (gen->tmss) {
+		address += gen->tmss_write_offset;
+		uint16_t *dest = get_native_pointer(address, (void **)m68k->mem_pointers, &m68k->options->gen);
+		*dest = value;
+		m68k_handle_code_write(address, m68k);
+	}
+	
+	return context;
+}
+
+static void *tmss_word_write_8(uint32_t address, void *context, uint8_t value)
+{
+	m68k_context *m68k = context;
+	genesis_context *gen = m68k->system;
+	if (gen->tmss) {
+		address += gen->tmss_write_offset;
+		uint8_t *dest = get_native_pointer(address & ~1, (void **)m68k->mem_pointers, &m68k->options->gen);
+#ifdef BLASTEM_BIG_ENDIAN
+		dest[address & 1] = value;
+#else
+		dest[address & 1 ^ 1] = value;
+#endif
+		m68k_handle_code_write(address & ~1, m68k);
+	}
+	
+	return context;
+}
+
+static void *tmss_odd_write_16(uint32_t address, void *context, uint16_t value)
+{
+	m68k_context *m68k = context;
+	genesis_context *gen = m68k->system;
+	if (gen->tmss) {
+		memmap_chunk const *chunk = find_map_chunk(address + gen->tmss_write_offset, &m68k->options->gen, 0, NULL);
+		address >>= 1;
+		uint8_t *base = (uint8_t *)m68k->mem_pointers[chunk->ptr_index];
+		base[address] = value;
+	}
+	return context;
+}
+
+static void *tmss_odd_write_8(uint32_t address, void *context, uint8_t value)
+{
+	m68k_context *m68k = context;
+	genesis_context *gen = m68k->system;
+	if (gen->tmss && (address & 1)) {
+		memmap_chunk const *chunk = find_map_chunk(address + gen->tmss_write_offset, &m68k->options->gen, 0, NULL);
+		address >>= 1;
+		uint8_t *base = (uint8_t *)m68k->mem_pointers[chunk->ptr_index];
+		base[address] = value;
+	}
+	return context;
+}
+
+static void *tmss_even_write_16(uint32_t address, void *context, uint16_t value)
+{
+	m68k_context *m68k = context;
+	genesis_context *gen = m68k->system;
+	if (gen->tmss) {
+		memmap_chunk const *chunk = find_map_chunk(address + gen->tmss_write_offset, &m68k->options->gen, 0, NULL);
+		address >>= 1;
+		uint8_t *base = (uint8_t *)m68k->mem_pointers[chunk->ptr_index];
+		base[address] = value >> 8;
+	}
+	return context;
+}
+
+static void *tmss_even_write_8(uint32_t address, void *context, uint8_t value)
+{
+	m68k_context *m68k = context;
+	genesis_context *gen = m68k->system;
+	if (gen->tmss && !(address & 1)) {
+		memmap_chunk const *chunk = find_map_chunk(address + gen->tmss_write_offset, &m68k->options->gen, 0, NULL);
+		address >>= 1;
+		uint8_t *base = (uint8_t *)m68k->mem_pointers[chunk->ptr_index];
+		base[address] = value;
+	}
+	return context;
+}
+
 genesis_context *alloc_init_genesis(rom_info *rom, void *main_rom, void *lock_on, uint32_t system_opts, uint8_t force_region)
 {
 	static memmap_chunk z80_map[] = {
@@ -1569,6 +1819,8 @@
 	uint8_t tmss = !strcmp(tern_find_ptr_default(model, "tmss", "off"), "on");
 	if (tmss) {
 		gen->version_reg |= 1;
+	} else {
+		gen->vdp_unlocked = 1;
 	}
 
 	uint8_t max_vsram = !strcmp(tern_find_ptr_default(model, "vsram", "40"), "64");
@@ -1664,13 +1916,145 @@
 		gen->save_storage = NULL;
 	}
 	
+	gen->mapper_start_index = rom->mapper_start_index;
+	
 	//This must happen before we generate memory access functions in init_m68k_opts
+	uint8_t next_ptr_index = 0;
+	uint32_t tmss_min_alloc = 16 * 1024;
 	for (int i = 0; i < rom->map_chunks; i++)
 	{
 		if (rom->map[i].start == 0xE00000) {
 			rom->map[i].buffer = gen->work_ram;
-			break;
+			if (!tmss) {
+				break;
+			}
+		}
+		if (rom->map[i].flags & MMAP_PTR_IDX && rom->map[i].ptr_index >= next_ptr_index) {
+			next_ptr_index = rom->map[i].ptr_index + 1;
+		}
+		if (rom->map[i].start < 0x400000 && rom->map[i].read_16 != unused_read) {
+			uint32_t highest_offset = (rom->map[i].end & rom->map[i].mask) + 1;
+			if (highest_offset > tmss_min_alloc) {
+				tmss_min_alloc = highest_offset;
+			}
+		}
+	}
+	if (tmss) {
+		char *tmss_path = tern_find_path_default(config, "system\0tmss_path\0", (tern_val){.ptrval = "tmss.md"}, TVAL_PTR).ptrval;
+		uint8_t *buffer = malloc(tmss_min_alloc);
+		uint32_t tmss_size;
+		if (is_absolute_path(tmss_path)) {
+			FILE *f = fopen(tmss_path, "rb");
+			if (!f) {
+				fatal_error("Configured to use a model with TMSS, but failed to load the TMSS ROM from %s\n", tmss_path);
+			}
+			tmss_size = fread(buffer, 1, tmss_min_alloc, f);
+			fclose(f);
+		} else {
+			char *tmp = read_bundled_file(tmss_path, &tmss_size);
+			if (!tmp) {
+				fatal_error("Configured to use a model with TMSS, but failed to load the TMSS ROM from %s\n", tmss_path);
+			}
+			memcpy(buffer, tmp, tmss_size);
+			free(tmp);
+		}
+		for (uint32_t padded = nearest_pow2(tmss_size); tmss_size < padded; tmss_size++)
+		{
+			buffer[tmss_size] = 0xFF;
+		}
+#ifndef BLASTEM_BIG_ENDIAN
+		byteswap_rom(tmss_size, (uint16_t *)buffer);
+#endif
+		//mirror TMSS ROM until we fill up to tmss_min_alloc
+		for (uint32_t dst = tmss_size; dst < tmss_min_alloc; dst += tmss_size)
+		{
+			memcpy(buffer + dst, buffer, dst + tmss_size > tmss_min_alloc ? tmss_min_alloc - dst : tmss_size);
 		}
+		//modify mappings for ROM space to point to the TMSS ROM and fixup flags to allow switching back and forth
+		//WARNING: This code makes some pretty big assumptions about the kinds of map chunks it will encounter
+		for (int i = 0; i < rom->map_chunks; i++)
+		{
+			if (rom->map[i].start < 0x400000 && rom->map[i].read_16 != unused_read) {
+				if (rom->map[i].flags == MMAP_READ) {
+					//Normal ROM
+					rom->map[i].flags |= MMAP_PTR_IDX | MMAP_CODE;
+					rom->map[i].ptr_index = next_ptr_index++;
+					if (rom->map[i].ptr_index >= NUM_MEM_AREAS) {
+						fatal_error("Too many memmap chunks with MMAP_PTR_IDX after TMSS remap\n");
+					}
+					gen->tmss_pointers[rom->map[i].ptr_index] = rom->map[i].buffer;
+					rom->map[i].buffer = buffer + (rom->map[i].start & ~rom->map[i].mask & (tmss_size - 1));
+				} else if (rom->map[i].flags & MMAP_PTR_IDX) {
+					//Sega mapper page or multi-game mapper
+					gen->tmss_pointers[rom->map[i].ptr_index] = rom->map[i].buffer;
+					rom->map[i].buffer = buffer + (rom->map[i].start & ~rom->map[i].mask & (tmss_size - 1));
+					if (rom->map[i].write_16) {
+						if (!gen->tmss_write_16) {
+							gen->tmss_write_16 = rom->map[i].write_16;
+							gen->tmss_write_8 = rom->map[i].write_8;
+							rom->map[i].write_16 = tmss_rom_write_16;
+							rom->map[i].write_8 = tmss_rom_write_8;
+						} else if (gen->tmss_write_16 == rom->map[i].write_16) {
+							rom->map[i].write_16 = tmss_rom_write_16;
+							rom->map[i].write_8 = tmss_rom_write_8;
+						} else {
+							warning("Chunk starting at %X has a write function, but we've already stored a different one for TMSS remap\n", rom->map[i].start);
+						}
+					}
+				} else if ((rom->map[i].flags & (MMAP_READ | MMAP_WRITE)) == (MMAP_READ | MMAP_WRITE)) {
+					//RAM or SRAM
+					rom->map[i].flags |= MMAP_PTR_IDX;
+					rom->map[i].ptr_index = next_ptr_index++;
+					gen->tmss_pointers[rom->map[i].ptr_index] = rom->map[i].buffer;
+					rom->map[i].buffer = buffer + (rom->map[i].start & ~rom->map[i].mask & (tmss_size - 1));
+					if (!gen->tmss_write_offset || gen->tmss_write_offset == rom->map[i].start) {
+						gen->tmss_write_offset = rom->map[i].start;
+						rom->map[i].flags &= ~MMAP_WRITE;
+						if (rom->map[i].flags & MMAP_ONLY_ODD) {
+							rom->map[i].write_16 = tmss_odd_write_16;
+							rom->map[i].write_8 = tmss_odd_write_8;
+						} else if (rom->map[i].flags & MMAP_ONLY_EVEN) {
+							rom->map[i].write_16 = tmss_even_write_16;
+							rom->map[i].write_8 = tmss_even_write_8;
+						} else {
+							rom->map[i].write_16 = tmss_word_write_16;
+							rom->map[i].write_8 = tmss_word_write_8;
+						}
+					} else {
+						warning("Could not remap writes for chunk starting at %X for TMSS because write_offset is %X\n", rom->map[i].start, gen->tmss_write_offset);
+					}
+				} else if (rom->map[i].flags & MMAP_READ_CODE) {
+					//NOR flash
+					rom->map[i].flags |= MMAP_PTR_IDX;
+					rom->map[i].ptr_index = next_ptr_index++;
+					if (rom->map[i].ptr_index >= NUM_MEM_AREAS) {
+						fatal_error("Too many memmap chunks with MMAP_PTR_IDX after TMSS remap\n");
+					}
+					gen->tmss_pointers[rom->map[i].ptr_index] = rom->map[i].buffer;
+					rom->map[i].buffer = buffer + (rom->map[i].start & ~rom->map[i].mask & (tmss_size - 1));
+					if (!gen->tmss_write_16) {
+						gen->tmss_write_16 = rom->map[i].write_16;
+						gen->tmss_write_8 = rom->map[i].write_8;
+						gen->tmss_read_16 = rom->map[i].read_16;
+						gen->tmss_read_8 = rom->map[i].read_8;
+						rom->map[i].write_16 = tmss_rom_write_16;
+						rom->map[i].write_8 = tmss_rom_write_8;
+						rom->map[i].read_16 = tmss_rom_read_16;
+						rom->map[i].read_8 = tmss_rom_read_8;
+					} else if (gen->tmss_write_16 == rom->map[i].write_16) {
+						rom->map[i].write_16 = tmss_rom_write_16;
+						rom->map[i].write_8 = tmss_rom_write_8;
+						rom->map[i].read_16 = tmss_rom_read_16;
+						rom->map[i].read_8 = tmss_rom_read_8;
+					} else {
+						warning("Chunk starting at %X has a write function, but we've already stored a different one for TMSS remap\n", rom->map[i].start);
+					}
+				} else {
+					warning("Didn't remap chunk starting at %X for TMSS because it has flags %X\n", rom->map[i].start, rom->map[i].flags);
+				}
+			}
+		}
+		gen->tmss_buffer = buffer;
 	}
 
 	m68k_options *opts = malloc(sizeof(m68k_options));
--- a/genesis.h	Sun May 10 00:16:00 2020 -0700
+++ b/genesis.h	Thu Aug 05 09:29:33 2021 -0700
@@ -39,6 +39,12 @@
 	uint8_t         *save_storage;
 	void            *mapper_temp;
 	eeprom_map      *eeprom_map;
+	write_16_fun    tmss_write_16;
+	write_8_fun     tmss_write_8;
+	read_16_fun     tmss_read_16;
+	read_8_fun      tmss_read_8;
+	uint16_t        *tmss_pointers[NUM_MEM_AREAS];
+	uint8_t         *tmss_buffer;
 	uint8_t         *serialize_tmp;
 	size_t          serialize_size;
 	uint32_t        num_eeprom;
@@ -54,6 +60,7 @@
 	uint32_t        last_frame;
 	uint32_t        last_flush_cycle;
 	uint32_t        soft_flush_cycles;
+	uint32_t        tmss_write_offset;
 	uint8_t         bank_regs[8];
 	uint16_t        z80_bank_reg;
 	uint16_t        tmss_lock[2];
@@ -65,6 +72,7 @@
 	uint8_t         bus_busy;
 	uint8_t         reset_requested;
 	uint8_t         tmss;
+	uint8_t         vdp_unlocked;
 	eeprom_state    eeprom;
 	nor_state       nor;
 };
Binary file images/genesis_6b.png has changed
Binary file images/wiiu.png has changed
--- a/io.c	Sun May 10 00:16:00 2020 -0700
+++ b/io.c	Thu Aug 05 09:29:33 2021 -0700
@@ -39,7 +39,9 @@
 	"EA 4-way Play cable A",
 	"EA 4-way Play cable B",
 	"Sega Parallel Transfer Board",
-	"Generic Device"
+	"Generic Device",
+	"Generic Serial",
+	"Heartbeat Personal Trainer"
 };
 
 #define GAMEPAD_TH0 0
@@ -58,6 +60,13 @@
 	IO_READ
 };
 
+enum {
+	HBPT_NEED_INIT,
+	HBPT_IDLE,
+	HBPT_CMD_PAYLOAD,
+	HBPT_REPLY
+};
+
 typedef struct {
 	uint8_t states[2], value;
 } gp_button_def;
@@ -86,6 +95,9 @@
 		if (port->device_type < IO_MOUSE && port->device.pad.gamepad_num == gamepad_num) {
 			return port;
 		}
+		if (port->device_type == IO_HEARTBEAT_TRAINER && port->device.heartbeat_trainer.device_num == gamepad_num) {
+			return port;
+		} 
 	}
 	return NULL;
 }
@@ -210,8 +222,20 @@
 	return find_keyboard(io) != NULL;
 }
 
+static void set_serial_clock(io_port *port)
+{
+	switch(port->serial_ctrl >> 6)
+	{
+	case 0: port->serial_divider = 11186; break; //4800 bps
+	case 1: port->serial_divider = 22372; break; //2400 bps
+	case 2: port->serial_divider = 44744; break; //1200 bps
+	case 3: port->serial_divider = 178976; break; //300 bps
+	}
+}
+
 void process_device(char * device_type, io_port * port)
 {
+	set_serial_clock(port);
 	//assuming that the io_port struct has been zeroed if this is the first time this has been called
 	if (!device_type)
 	{
@@ -235,6 +259,10 @@
 			port->device_type = IO_GAMEPAD6;
 		}
 		port->device.pad.gamepad_num = device_type[gamepad_len+2] - '0';
+	} else if(startswith(device_type, "heartbeat_trainer.")) {
+		port->device_type = IO_HEARTBEAT_TRAINER;
+		port->device.heartbeat_trainer.nv_memory = NULL;
+		port->device.heartbeat_trainer.device_num = device_type[strlen("heartbeat_trainer.")] - '0';
 	} else if(startswith(device_type, "mouse")) {
 		if (port->device_type != IO_MOUSE) {
 			port->device_type = IO_MOUSE;
@@ -272,6 +300,12 @@
 			port->device.stream.data_fd = -1;
 			port->device.stream.listen_fd = -1;
 		}
+	} else if(!strcmp(device_type, "serial")) {
+		if (port->device_type != IO_GENERIC_SERIAL) {
+			port->device_type = IO_GENERIC_SERIAL;
+			port->device.stream.data_fd = -1;
+			port->device.stream.listen_fd = -1;
+		}
 	}
 }
 
@@ -354,7 +388,7 @@
 					}
 				}
 			}
-		} else if (ports[i].device_type == IO_GENERIC && ports[i].device.stream.data_fd == -1) {
+		} else if (ports[i].device_type == IO_GENERIC || ports[i].device_type == IO_GENERIC_SERIAL && ports[i].device.stream.data_fd == -1) {
 			char *sock_name = tern_find_path(config, "io\0socket\0", TVAL_PTR).ptrval;
 			if (!sock_name)
 			{
@@ -392,6 +426,30 @@
 #endif
 		if (ports[i].device_type == IO_GAMEPAD3 || ports[i].device_type == IO_GAMEPAD6 || ports[i].device_type == IO_GAMEPAD2) {
 			debug_message("IO port %s connected to gamepad #%d with type '%s'\n", io_name(i), ports[i].device.pad.gamepad_num, device_type_names[ports[i].device_type]);
+		} else if (ports[i].device_type == IO_HEARTBEAT_TRAINER) {
+			debug_message("IO port %s connected to Heartbeat Personal Trainer #%d\n", io_name(i), ports[i].device.heartbeat_trainer.device_num);
+			if (rom->save_type == SAVE_HBPT) {
+				ports[i].device.heartbeat_trainer.nv_memory = rom->save_buffer;
+				uint32_t page_size = 16;
+				for (; page_size < 128; page_size *= 2)
+				{
+					if (rom->save_size / page_size < 256) {
+						break;
+					}
+				}
+				ports[i].device.heartbeat_trainer.nv_page_size = page_size;
+				uint32_t num_pages = rom->save_size / page_size;
+				ports[i].device.heartbeat_trainer.nv_pages = num_pages < 256 ? num_pages : 255;
+			} else {
+				ports[i].device.heartbeat_trainer.nv_page_size = 16;
+				ports[i].device.heartbeat_trainer.nv_pages = 32;
+				size_t bufsize = 
+					ports[i].device.heartbeat_trainer.nv_page_size * ports[i].device.heartbeat_trainer.nv_pages
+					+ 5 + 8;
+				ports[i].device.heartbeat_trainer.nv_memory = malloc(bufsize);
+				memset(ports[i].device.heartbeat_trainer.nv_memory, 0xFF, bufsize);
+			}
+			ports[i].device.heartbeat_trainer.state = HBPT_NEED_INIT;
 		} else {
 			debug_message("IO port %s connected to device '%s'\n", io_name(i), device_type_names[ports[i].device_type]);
 		}
@@ -427,7 +485,6 @@
 	}
 }
 
-uint32_t last_poll_cycle;
 void io_adjust_cycles(io_port * port, uint32_t current_cycle, uint32_t deduction)
 {
 	/*uint8_t control = pad->control | 0x80;
@@ -459,25 +516,80 @@
 			}
 		}
 	}
-	if (last_poll_cycle >= deduction) {
-		last_poll_cycle -= deduction;
+	if (port->transmit_end >= deduction) {
+		port->transmit_end -= deduction;
 	} else {
-		last_poll_cycle = 0;
+		port->transmit_end = 0;
+	}
+	if (port->receive_end >= deduction) {
+		port->receive_end -= deduction;
+	} else {
+		port->receive_end = 0;
+	}
+	if (port->last_poll_cycle >= deduction) {
+		port->last_poll_cycle -= deduction;
+	} else {
+		port->last_poll_cycle = 0;
 	}
 }
 
 #ifndef _WIN32
-static void wait_for_connection(io_port * port)
+static void wait_for_connection(io_port *port)
 {
 	if (port->device.stream.data_fd == -1)
 	{
-		debug_message("Waiting for socket connection...");
+		debug_message("Waiting for socket connection...\n");
 		port->device.stream.data_fd = accept(port->device.stream.listen_fd, NULL, NULL);
 		fcntl(port->device.stream.data_fd, F_SETFL, O_NONBLOCK | O_RDWR);
 	}
 }
 
-static void service_pipe(io_port * port)
+static void poll_for_connection(io_port *port)
+{
+	if (port->device.stream.data_fd == -1)
+	{
+		fcntl(port->device.stream.listen_fd, F_SETFL, O_NONBLOCK | O_RDWR);
+		port->device.stream.data_fd = accept(port->device.stream.listen_fd, NULL, NULL);
+		fcntl(port->device.stream.listen_fd, F_SETFL, O_RDWR);
+		if (port->device.stream.data_fd != -1) {
+			fcntl(port->device.stream.data_fd, F_SETFL, O_NONBLOCK | O_RDWR);
+		}
+	}
+}
+
+static void write_serial_byte(io_port *port)
+{
+	fcntl(port->device.stream.data_fd, F_SETFL, O_RDWR);
+	for (int sent = 0; sent != sizeof(port->serial_transmitting);)
+	{
+		sent = send(port->device.stream.data_fd, &port->serial_transmitting, sizeof(port->serial_transmitting), 0);
+		if (sent < 0) {
+			close(port->device.stream.data_fd);
+			port->device.stream.data_fd = -1;
+			wait_for_connection(port);
+			fcntl(port->device.stream.data_fd, F_SETFL, O_RDWR);
+		}
+	}
+	fcntl(port->device.stream.data_fd, F_SETFL, O_NONBLOCK | O_RDWR);
+}
+
+static void read_serial_byte(io_port *port)
+{
+	poll_for_connection(port);
+	if (port->device.stream.data_fd == -1) {
+		return;
+	}
+	int read = recv(port->device.stream.data_fd, &port->serial_receiving, sizeof(port->serial_receiving), 0);
+	if (read < 0 && errno != EAGAIN && errno != EWOULDBLOCK) {
+		close(port->device.stream.data_fd);
+		port->device.stream.data_fd = -1;
+	}
+	if (read > 0) {
+		port->receive_end = port->serial_cycle + 10 * port->serial_divider;
+	}
+}
+
+static void service_pipe(io_port *port)
 {
 	uint8_t value;
 	int numRead = read(port->device.stream.data_fd, &value, sizeof(value));
@@ -568,6 +680,257 @@
 }
 #endif
 
+enum {
+	HBPT_UNKNOWN1 = 1,
+	HBPT_POLL,
+	HBPT_READ_PAGE = 5,
+	HBPT_WRITE_PAGE,
+	HBPT_READ_RTC,
+	HBPT_SET_RTC,
+	HBPT_GET_STATUS,
+	HBPT_ERASE_NVMEM,
+	HBPT_NVMEM_PARAMS,
+	HBPT_INIT
+};
+
+static void start_reply(io_port *port, uint8_t bytes, const uint8_t *src)
+{
+	port->device.heartbeat_trainer.remaining_bytes = bytes;
+	port->device.heartbeat_trainer.state = HBPT_REPLY;
+	port->device.heartbeat_trainer.cur_buffer = (uint8_t *)src;
+}
+
+static void simple_reply(io_port *port, uint8_t value)
+{
+	port->device.heartbeat_trainer.param = value;
+	start_reply(port, 1, &port->device.heartbeat_trainer.param);
+}
+
+static void expect_payload(io_port *port, uint8_t bytes, uint8_t *dst)
+{
+	port->device.heartbeat_trainer.remaining_bytes = bytes;
+	port->device.heartbeat_trainer.state = HBPT_CMD_PAYLOAD;
+	port->device.heartbeat_trainer.cur_buffer = dst;
+}
+
+void hbpt_check_init(io_port *port)
+{
+	if (port->device.heartbeat_trainer.state == HBPT_NEED_INIT) {
+		port->device.heartbeat_trainer.rtc_base_timestamp = 0;
+		for (int i = 0; i < 8; i ++)
+		{
+			port->device.heartbeat_trainer.rtc_base_timestamp <<= 8;
+			port->device.heartbeat_trainer.rtc_base_timestamp |= port->device.heartbeat_trainer.nv_memory[i];
+		}
+		memcpy(port->device.heartbeat_trainer.rtc_base, port->device.heartbeat_trainer.nv_memory + 8, 5);
+		if (port->device.heartbeat_trainer.rtc_base_timestamp == UINT64_MAX) {
+			//uninitialized save, set the appropriate status bit
+			port->device.heartbeat_trainer.status |= 1;
+		}
+		port->device.heartbeat_trainer.bpm = 60;
+		port->device.heartbeat_trainer.state = HBPT_IDLE;
+	}
+}
+
+void hbpt_check_send_reply(io_port *port)
+{
+	if (port->device.heartbeat_trainer.state == HBPT_REPLY && !port->receive_end) {
+		port->serial_receiving = *(port->device.heartbeat_trainer.cur_buffer++);
+		port->receive_end = port->serial_cycle + 10 * port->serial_divider;
+		if (!--port->device.heartbeat_trainer.remaining_bytes) {
+			port->device.heartbeat_trainer.state = HBPT_IDLE;
+		}
+	}
+}
+
+uint8_t is_leap_year(uint16_t year)
+{
+	if (year & 3) {
+		return 0;
+	}
+	if (year % 100) {
+		return 1;
+	}
+	if (year % 400) {
+		return 0;
+	}
+	return 1;
+}
+
+uint8_t days_in_month(uint8_t month, uint16_t year)
+{
+	static uint8_t days_per_month[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
+	if (month == 2 && is_leap_year(year)) {
+		return 29;
+	}
+	if (month > 12 || !month) {
+		return 30;
+	}
+	return days_per_month[month-1];
+}
+
+void hbpt_write_byte(io_port *port)
+{
+	hbpt_check_init(port);
+	uint8_t reply;
+	switch (port->device.heartbeat_trainer.state)
+	{
+	case HBPT_IDLE:
+		port->device.heartbeat_trainer.cmd = port->serial_transmitting;
+		switch (port->device.heartbeat_trainer.cmd)
+		{
+		case HBPT_UNKNOWN1:
+			start_reply(port, 11, NULL);
+			break;
+		case HBPT_POLL:
+			start_reply(port, 3, &port->device.heartbeat_trainer.bpm);
+			if (port->serial_cycle - port->last_poll_cycle > MIN_POLL_INTERVAL) {
+				process_events();
+				port->last_poll_cycle = port->serial_cycle;
+			}
+			port->device.heartbeat_trainer.buttons = (port->input[GAMEPAD_TH0] << 2 & 0xC0) | (port->input[GAMEPAD_TH1] & 0x1F);
+			if (port->device.heartbeat_trainer.cadence && port->input[GAMEPAD_TH1] & 0x20) {
+				port->device.heartbeat_trainer.cadence--;
+				printf("Cadence: %d\n", port->device.heartbeat_trainer.cadence);
+			} else if (port->device.heartbeat_trainer.cadence < 255 && port->input[GAMEPAD_EXTRA] & 1) {
+				port->device.heartbeat_trainer.cadence++;
+				printf("Cadence: %d\n", port->device.heartbeat_trainer.cadence);
+			}
+			if (port->device.heartbeat_trainer.bpm && port->input[GAMEPAD_EXTRA] & 4) {
+				port->device.heartbeat_trainer.bpm--;
+				printf("Heart Rate: %d\n", port->device.heartbeat_trainer.bpm);
+			} else if (port->device.heartbeat_trainer.bpm < 255 && port->input[GAMEPAD_EXTRA] & 2) {
+				port->device.heartbeat_trainer.bpm++;
+				printf("Heart Rate: %d\n", port->device.heartbeat_trainer.bpm);
+			}
+			
+			break;
+		case HBPT_READ_PAGE:
+		case HBPT_WRITE_PAGE:
+			//strictly speaking for the write case, we want 1 + page size here
+			//but the rest of the payload goes to a different destination
+			expect_payload(port, 1, &port->device.heartbeat_trainer.param);
+			break;
+		case HBPT_READ_RTC: {
+			uint8_t *rtc = port->device.heartbeat_trainer.rtc_base;
+			start_reply(port, 5, rtc);
+			uint64_t now = time(NULL);
+			uint64_t delta = (now - port->device.heartbeat_trainer.rtc_base_timestamp + 30) / 60;
+			rtc[4] += delta % 60;
+			if (rtc[4] > 59) {
+				rtc[4] -= 60;
+				rtc[3]++;
+			}
+			delta /= 60;
+			if (delta) {
+				rtc[3] += delta % 24;
+				delta /= 24;
+				if (rtc[3] > 23) {
+					rtc[3] -= 24;
+					delta++;
+				}
+				if (delta) {
+					uint16_t year = rtc[0] < 81 ? 2000 + rtc[0] : 1900 + rtc[0];
+					uint8_t days_cur_month = days_in_month(rtc[1], year);
+					while (delta + rtc[2] > days_cur_month) {
+						delta -= days_cur_month + 1 - rtc[2];
+						rtc[2] = 1;
+						if (++rtc[1] == 13) {
+							rtc[1] = 1;
+							year++;
+						}
+						days_cur_month = days_in_month(rtc[1], year);
+					}
+					rtc[1] += delta;
+					rtc[0] = year % 100;
+				}
+			}
+			printf("RTC %02d-%02d-%02d %02d:%02d\n", rtc[0], rtc[1], rtc[2], rtc[3], rtc[4]);
+			port->device.heartbeat_trainer.rtc_base_timestamp = now;
+			break;
+		}
+		case HBPT_SET_RTC:
+			port->device.heartbeat_trainer.rtc_base_timestamp = time(NULL);
+			expect_payload(port, 5, port->device.heartbeat_trainer.rtc_base);
+			break;
+		case HBPT_GET_STATUS:
+			simple_reply(port, port->device.heartbeat_trainer.status);
+			break;
+		case HBPT_ERASE_NVMEM:
+			expect_payload(port, 1, &port->device.heartbeat_trainer.param);
+			break;
+		case HBPT_NVMEM_PARAMS:
+			start_reply(port, 2, &port->device.heartbeat_trainer.nv_page_size);
+			break;
+		case HBPT_INIT:
+			expect_payload(port, 19, NULL);
+			break;
+		default:
+			// it's unclear what these commands do as they are unused by Outback Joey
+			// just return 0 to indicate failure
+			simple_reply(port, 0);
+		}
+		break;
+	case HBPT_CMD_PAYLOAD:
+		if (port->device.heartbeat_trainer.cur_buffer) {
+			*(port->device.heartbeat_trainer.cur_buffer++) = port->serial_transmitting;
+		}
+		if (!--port->device.heartbeat_trainer.remaining_bytes) {
+			switch (port->device.heartbeat_trainer.cmd)
+			{
+			case HBPT_READ_PAGE:
+			case HBPT_WRITE_PAGE:
+				if (
+					port->device.heartbeat_trainer.cmd == HBPT_WRITE_PAGE 
+					&& port->device.heartbeat_trainer.cur_buffer != &port->device.heartbeat_trainer.param + 1) {
+					simple_reply(port, 1);
+					break;
+				}
+				port->device.heartbeat_trainer.remaining_bytes = port->device.heartbeat_trainer.nv_page_size;
+				port->device.heartbeat_trainer.cur_buffer =
+					port->device.heartbeat_trainer.param < port->device.heartbeat_trainer.nv_pages
+					? port->device.heartbeat_trainer.nv_memory + 5 + 8
+						+ port->device.heartbeat_trainer.param * port->device.heartbeat_trainer.nv_page_size
+					: NULL;
+				if (port->device.heartbeat_trainer.cmd == HBPT_WRITE_PAGE) {
+					return;
+				}
+				port->device.heartbeat_trainer.state = HBPT_REPLY;
+				break;
+			case HBPT_SET_RTC:
+				//save RTC base values back to nv memory area so it's saved to disk on exit
+				for (int i = 0; i < 8; i++)
+				{
+					port->device.heartbeat_trainer.nv_memory[i] = port->device.heartbeat_trainer.rtc_base_timestamp >> (56 - i*8);
+				}
+				memcpy(port->device.heartbeat_trainer.nv_memory + 8, port->device.heartbeat_trainer.rtc_base, 5);
+				simple_reply(port, 1);
+				break;
+			case HBPT_ERASE_NVMEM:
+				memset(
+					port->device.heartbeat_trainer.nv_memory + 5 + 8, 
+					port->device.heartbeat_trainer.param, 
+					port->device.heartbeat_trainer.nv_pages * port->device.heartbeat_trainer.nv_page_size
+				);
+				simple_reply(port, 1);
+				break;
+			case HBPT_INIT: {
+				static const char reply[] = "(C) HEARTBEAT CORP";
+				start_reply(port, strlen(reply), reply);
+				break;
+			}
+			}
+		}
+	}
+	hbpt_check_send_reply(port);
+}
+
+void hbpt_read_byte(io_port *port)
+{
+	hbpt_check_init(port);
+	hbpt_check_send_reply(port);
+}
+
 const int mouse_delays[] = {112*7, 120*7, 96*7, 132*7, 104*7, 96*7, 112*7, 96*7};
 
 enum {
@@ -576,6 +939,67 @@
 	KB_WRITE
 };
 
+enum {
+	SCTRL_BIT_TX_FULL = 1,
+	SCTRL_BIT_RX_READY = 2,
+	SCTRL_BIT_RX_ERROR = 4,
+	SCTRL_BIT_RX_INTEN = 8,
+	SCTRL_BIT_TX_ENABLE = 0x10,
+	SCTRL_BIT_RX_ENABLE = 0x20
+};
+
+void io_run(io_port *port, uint32_t current_cycle)
+{
+	uint32_t new_serial_cycle = ((current_cycle - port->serial_cycle) / port->serial_divider) * port->serial_divider + port->serial_cycle;
+	if (port->transmit_end && port->transmit_end <= new_serial_cycle) {
+		port->transmit_end = 0;
+		
+		if (port->serial_ctrl & SCTRL_BIT_TX_ENABLE) {
+			switch (port->device_type)
+			{
+			case IO_HEARTBEAT_TRAINER:
+				hbpt_write_byte(port);
+				break;
+#ifndef _WIN32
+			case IO_GENERIC_SERIAL:
+				write_serial_byte(port);
+				break;
+#endif
+			//TODO: think about how serial mode might interact with non-serial peripherals
+			}
+		}
+	}
+	if (!port->transmit_end && new_serial_cycle != port->serial_cycle && (port->serial_ctrl & SCTRL_BIT_TX_FULL)) {
+		//there's a transmit byte pending and no byte is currently being sent
+		port->serial_transmitting = port->serial_out;
+		port->serial_ctrl &= ~SCTRL_BIT_TX_FULL;
+		//1 start bit, 8 data bits and 1 stop bit
+		port->transmit_end = new_serial_cycle + 10 * port->serial_divider;
+	}
+	port->serial_cycle = new_serial_cycle;
+	if (port->serial_ctrl && SCTRL_BIT_RX_ENABLE) {
+		if (port->receive_end && new_serial_cycle >= port->receive_end) {
+			port->serial_in = port->serial_receiving;
+			port->serial_ctrl |= SCTRL_BIT_RX_READY;
+			port->receive_end = 0;
+		}
+		if (!port->receive_end) {
+			switch(port->device_type)
+			{
+			case IO_HEARTBEAT_TRAINER:
+				hbpt_read_byte(port);
+				break;
+#ifndef _WIN32
+			case IO_GENERIC_SERIAL:
+				read_serial_byte(port);
+				break;
+#endif
+			//TODO: think about how serial mode might interact with non-serial peripherals
+			}
+		}
+	}
+}
+
 void io_control_write(io_port *port, uint8_t value, uint32_t current_cycle)
 {
 	uint8_t changes = value ^ port->control;
@@ -723,6 +1147,20 @@
 
 }
 
+void io_tx_write(io_port *port, uint8_t value, uint32_t current_cycle)
+{
+	io_run(port, current_cycle);
+	port->serial_out = value;
+	port->serial_ctrl |= SCTRL_BIT_TX_FULL;
+}
+
+void io_sctrl_write(io_port *port, uint8_t value, uint32_t current_cycle)
+{
+	io_run(port, current_cycle);
+	port->serial_ctrl = (port->serial_ctrl & 0x7) | (value & 0xF8);
+	set_serial_clock(port);
+}
+
 uint8_t get_scancode_bytes(io_port *port)
 {
 	if (port->device.keyboard.read_pos == 0xFF) {
@@ -766,9 +1204,9 @@
 	uint8_t th = output & 0x40;
 	uint8_t input;
 	uint8_t device_driven;
-	if (current_cycle - last_poll_cycle > MIN_POLL_INTERVAL) {
+	if (current_cycle - port->last_poll_cycle > MIN_POLL_INTERVAL) {
 		process_events();
-		last_poll_cycle = current_cycle;
+		port->last_poll_cycle = current_cycle;
 	}
 	switch (port->device_type)
 	{
@@ -1049,6 +1487,36 @@
 	return value;
 }
 
+uint8_t io_rx_read(io_port * port, uint32_t current_cycle)
+{
+	io_run(port, current_cycle);
+	port->serial_ctrl &= ~SCTRL_BIT_RX_READY;
+	return port->serial_in;
+}
+
+uint8_t io_sctrl_read(io_port *port, uint32_t current_cycle)
+{
+	io_run(port, current_cycle);
+	return port->serial_ctrl;
+}
+
+uint32_t io_next_interrupt(io_port *port, uint32_t current_cycle)
+{
+	if (!(port->control & 0x80)) {
+		return CYCLE_NEVER;
+	}
+	if (port->serial_ctrl & SCTRL_BIT_RX_INTEN) {
+		if (port->serial_ctrl & SCTRL_BIT_RX_READY) {
+			return current_cycle;
+		}
+		if ((port->serial_ctrl & SCTRL_BIT_RX_ENABLE) && port->receive_end) {
+			return port->receive_end;
+		}
+	}
+	//TODO: handle external interrupts from TH transitions
+	return CYCLE_NEVER;
+}
+
 void io_serialize(io_port *port, serialize_buffer *buf)
 {
 	save_int8(buf, port->output);
@@ -1080,7 +1548,21 @@
 			save_int8(buf, port->device.keyboard.cmd);
 		}
 		break;
+	case IO_HEARTBEAT_TRAINER:
+		save_int8(buf, port->device.heartbeat_trainer.bpm);
+		save_int8(buf, port->device.heartbeat_trainer.cadence);
+		save_int8(buf, port->device.heartbeat_trainer.param);
+		save_int8(buf, port->device.heartbeat_trainer.state);
+		save_int8(buf, port->device.heartbeat_trainer.status);
+		save_int8(buf, port->device.heartbeat_trainer.cmd);
+		save_int8(buf, port->device.heartbeat_trainer.remaining_bytes);
+		break;
 	}
+	save_int32(buf, port->serial_cycle);
+	save_int32(buf, port->transmit_end);
+	save_int32(buf, port->receive_end);
+	save_int8(buf, port->serial_transmitting);
+	save_int8(buf, port->serial_receiving);
 }
 
 void io_deserialize(deserialize_buffer *buf, void *vport)
@@ -1091,7 +1573,9 @@
 	port->serial_out = load_int8(buf);
 	port->serial_in = load_int8(buf);
 	port->serial_ctrl = load_int8(buf);
+	set_serial_clock(port);
 	uint8_t device_type = load_int8(buf);
+	load_buffer32(buf, port->slow_rise_start, 8);
 	if (device_type != port->device_type) {
 		warning("Loaded save state has a different device type from the current configuration");
 		return;
@@ -1118,5 +1602,21 @@
 			port->device.keyboard.cmd = load_int8(buf);
 		}
 		break;
+	case IO_HEARTBEAT_TRAINER:
+		port->device.heartbeat_trainer.bpm = load_int8(buf);
+		port->device.heartbeat_trainer.cadence = load_int8(buf);
+		port->device.heartbeat_trainer.param = load_int8(buf);
+		port->device.heartbeat_trainer.state = load_int8(buf);
+		port->device.heartbeat_trainer.status = load_int8(buf);
+		port->device.heartbeat_trainer.cmd = load_int8(buf);
+		port->device.heartbeat_trainer.remaining_bytes = load_int8(buf);
+		break;
+	}
+	if (buf->cur_pos < buf->size) {
+		port->serial_cycle = load_int32(buf);
+		port->transmit_end = load_int32(buf);
+		port->receive_end = load_int32(buf);
+		port->serial_transmitting = load_int8(buf);
+		port->serial_receiving = load_int8(buf);
 	}
 }
--- a/io.h	Sun May 10 00:16:00 2020 -0700
+++ b/io.h	Thu Aug 05 09:29:33 2021 -0700
@@ -24,7 +24,9 @@
 	IO_EA_MULTI_A,
 	IO_EA_MULTI_B,
 	IO_SEGA_PARALLEL,
-	IO_GENERIC
+	IO_GENERIC,
+	IO_GENERIC_SERIAL,
+	IO_HEARTBEAT_TRAINER
 };
 
 typedef struct {
@@ -57,13 +59,37 @@
 			uint8_t  mode;
 			uint8_t  cmd;
 		} keyboard;
+		struct {
+			uint8_t  *nv_memory;
+			uint8_t  *cur_buffer;
+			uint64_t rtc_base_timestamp;
+			uint8_t  rtc_base[5];
+			uint8_t  bpm;
+			uint8_t  cadence;
+			uint8_t  buttons;
+			uint8_t  nv_page_size;
+			uint8_t  nv_pages;
+			uint8_t  param;
+			uint8_t  state;
+			uint8_t  status;
+			uint8_t  device_num;
+			uint8_t  cmd;
+			uint8_t  remaining_bytes;
+		} heartbeat_trainer;
 	} device;
 	uint8_t  output;
 	uint8_t  control;
 	uint8_t  input[3];
 	uint32_t slow_rise_start[8];
+	uint32_t serial_cycle;
+	uint32_t serial_divider;
+	uint32_t last_poll_cycle;
+	uint32_t transmit_end;
+	uint32_t receive_end;
 	uint8_t  serial_out;
+	uint8_t  serial_transmitting;
 	uint8_t  serial_in;
+	uint8_t  serial_receiving;
 	uint8_t  serial_ctrl;
 	uint8_t  device_type;
 } io_port;
@@ -106,9 +132,15 @@
 
 void setup_io_devices(tern_node * config, rom_info *rom, sega_io *io);
 void io_adjust_cycles(io_port * pad, uint32_t current_cycle, uint32_t deduction);
+void io_run(io_port *port, uint32_t current_cycle);
 void io_control_write(io_port *port, uint8_t value, uint32_t current_cycle);
 void io_data_write(io_port * pad, uint8_t value, uint32_t current_cycle);
+void io_tx_write(io_port *port, uint8_t value, uint32_t current_cycle);
+void io_sctrl_write(io_port *port, uint8_t value, uint32_t current_cycle);
 uint8_t io_data_read(io_port * pad, uint32_t current_cycle);
+uint8_t io_rx_read(io_port * port, uint32_t current_cycle);
+uint8_t io_sctrl_read(io_port *port, uint32_t current_cycle);
+uint32_t io_next_interrupt(io_port *port, uint32_t current_cycle);
 void io_serialize(io_port *port, serialize_buffer *buf);
 void io_deserialize(deserialize_buffer *buf, void *vport);
 
--- a/libblastem.c	Sun May 10 00:16:00 2020 -0700
+++ b/libblastem.c	Thu Aug 05 09:29:33 2021 -0700
@@ -517,10 +517,22 @@
 {
 }
 
+void render_set_external_sync(uint8_t ext_sync_on)
+{
+}
+
 void bindings_set_mouse_mode(uint8_t mode)
 {
 }
 
+void bindings_release_capture(void)
+{
+}
+
+void bindings_reacquire_capture(void)
+{
+}
+
 extern const char rom_db_data[];
 char *read_bundled_file(char *name, uint32_t *sizeret)
 {
--- a/m68k.cpu	Sun May 10 00:16:00 2020 -0700
+++ b/m68k.cpu	Thu Aug 05 09:29:33 2021 -0700
@@ -861,6 +861,114 @@
 	end
 	m68k_save_dst Z
 	m68k_prefetch
+	
+1110CCC0ZZ001RRR lsri
+	invalid Z 3
+	switch C
+	case 0
+		meta shift 8
+	default
+		meta shift C
+	end
+	lsr dregs.R shift dregs.R Z
+	update_flags XNZV0C
+	add shift shift shift
+	switch Z
+	case 2
+		add 4 shift shift
+	default
+		add 2 shift shift
+	end
+	cycles shift
+	#TODO: should this happen before or after the majority of the shift?
+	m68k_prefetch
+	
+1110CCC0ZZ101RRR lsr_dn
+	invalid Z 3
+	local shift 8
+	and dregs.C 63 shift
+	lsr dregs.R shift dregs.R Z
+	update_flags XNZV0C
+	add shift shift shift
+	switch Z
+	case 2
+		add 4 shift shift
+	default
+		add 2 shift shift
+	end
+	cycles shift
+	#TODO: should this happen before or after the majority of the shift?
+	m68k_prefetch
+	
+1110001011MMMRRR lsr_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
+	
+	m68k_fetch_dst_ea M R 0
+	lsr dst 1 dst
+	update_flags XNZV0C
+	m68k_save_dst 0
+	m68k_prefetch
+	
+1110CCC1ZZ001RRR lsli
+	invalid Z 3
+	switch C
+	case 0
+		meta shift 8
+	default
+		meta shift C
+	end
+	lsl dregs.R shift dregs.R Z
+	update_flags XNZV0C
+	add shift shift shift
+	switch Z
+	case 2
+		add 4 shift shift
+	default
+		add 2 shift shift
+	end
+	cycles shift
+	#TODO: should this happen before or after the majority of the shift?
+	m68k_prefetch
+	
+1110CCC1ZZ101RRR lsl_dn
+	invalid Z 3
+	local shift 8
+	and dregs.C 63 shift
+	lsl dregs.R shift dregs.R Z
+	update_flags XNZV0C
+	add shift shift shift
+	switch Z
+	case 2
+		add 4 shift shift
+	default
+		add 2 shift shift
+	end
+	cycles shift
+	#TODO: should this happen before or after the majority of the shift?
+	m68k_prefetch
+	
+1110001111MMMRRR lsl_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
+	
+	m68k_fetch_dst_ea M R 0
+	lsl dst 1 dst
+	update_flags XNZV0C
+	m68k_save_dst 0
+	m68k_prefetch
 
 00ZZRRRMMMEEESSS move
 	invalid Z 0
--- a/m68k_core.h	Sun May 10 00:16:00 2020 -0700
+++ b/m68k_core.h	Thu Aug 05 09:29:33 2021 -0700
@@ -12,7 +12,7 @@
 //#include "68kinst.h"
 struct m68kinst;
 
-#define NUM_MEM_AREAS 8
+#define NUM_MEM_AREAS 10
 #define NATIVE_MAP_CHUNKS (64*1024)
 #define NATIVE_CHUNK_SIZE ((16 * 1024 * 1024 / NATIVE_MAP_CHUNKS))
 #define MAX_NATIVE_SIZE 255
--- a/m68k_core_x86.c	Sun May 10 00:16:00 2020 -0700
+++ b/m68k_core_x86.c	Thu Aug 05 09:29:33 2021 -0700
@@ -421,7 +421,11 @@
 			push_r(code, opts->gen.scratch1);
 		}
 		dec_amount = inst->extra.size == OPSIZE_WORD ? 2 : (inst->extra.size == OPSIZE_LONG ? 4 : (op->params.regs.pri == 7 ? 2 :1));
-		if (!dst) {
+		if (!dst || (
+			inst->op != M68K_MOVE && inst->op != M68K_MOVEM 
+			&& inst->op != M68K_SUBX && inst->op != M68K_ADDX 
+			&& inst->op != M68K_ABCD && inst->op != M68K_SBCD
+		)) {
 			cycles(&opts->gen, PREDEC_PENALTY);
 		}
 		subi_areg(opts, dec_amount, op->params.regs.pri);
@@ -874,7 +878,7 @@
 		code_ptr end_off = code->cur+1;
 		jmp(code, code->cur+2);
 		*true_off = code->cur - (true_off+1);
-		cycles(&opts->gen, 6);
+		cycles(&opts->gen, inst->dst.addr_mode == MODE_REG ? 6 : 4);
 		if (dst_op.mode == MODE_REG_DIRECT) {
 			mov_ir(code, 0xFF, dst_op.base, SZ_B);
 		} else {
@@ -1190,8 +1194,6 @@
 void translate_m68k_reset(m68k_options *opts, m68kinst *inst)
 {
 	code_info *code = &opts->gen.code;
-	//RESET instructions take a long time to give peripherals time to reset themselves
-	cycles(&opts->gen, 132);
 	mov_rdispr(code, opts->gen.context_reg, offsetof(m68k_context, reset_handler), opts->gen.scratch1, SZ_PTR);
 	cmp_ir(code, 0, opts->gen.scratch1, SZ_PTR);
 	code_ptr no_reset_handler = code->cur + 1;
@@ -1201,6 +1203,8 @@
 	mov_rr(code, RAX, opts->gen.context_reg, SZ_PTR);
 	call(code, opts->gen.load_context);
 	*no_reset_handler = code->cur - (no_reset_handler + 1);
+	//RESET instructions take a long time to give peripherals time to reset themselves
+	cycles(&opts->gen, 132);
 }
 
 void op_ir(code_info *code, m68kinst *inst, int32_t val, uint8_t dst, uint8_t size)
@@ -1309,14 +1313,17 @@
 	
 	uint32_t numcycles;
 	if ((inst->op == M68K_ADDX || inst->op == M68K_SUBX) && inst->src.addr_mode != MODE_REG) {
-		numcycles = 6;
+		numcycles = 4;
 	} else if (size == OPSIZE_LONG) {
 		if (inst->op == M68K_CMP) {
-			numcycles = 6;
-		} else if (inst->op == M68K_AND && inst->variant == VAR_IMMEDIATE) {
+			numcycles = inst->src.addr_mode > MODE_AREG && inst->dst.addr_mode > MODE_AREG ? 4 : 6;
+		} else if (inst->op == M68K_AND && inst->variant == VAR_IMMEDIATE && inst->dst.addr_mode == MODE_REG) {
 			numcycles = 6;
-		} else if (inst->dst.addr_mode <= MODE_AREG) {
+		} else if (inst->dst.addr_mode == MODE_REG) {
 			numcycles = inst->src.addr_mode <= MODE_AREG || inst->src.addr_mode == MODE_IMMEDIATE ? 8 : 6;
+		} else if (inst->dst.addr_mode == MODE_AREG) {
+			numcycles = numcycles = inst->src.addr_mode <= MODE_AREG || inst->src.addr_mode == MODE_IMMEDIATE  
+				|| inst->extra.size == OPSIZE_WORD ? 8 : 6;
 		} else {
 			numcycles = 4;
 		}
@@ -1493,8 +1500,9 @@
 		//destination is in memory so we need to preserve scratch2 for the write at the end
 		push_r(code, opts->gen.scratch2);
 	}
-	//MC68000 User's Manual suggests NBCD hides the 2 cycle penalty during the write cycle somehow
-	cycles(&opts->gen, inst->op == M68K_NBCD && inst->dst.addr_mode != MODE_REG_DIRECT ? BUS : BUS + 2);
+	
+	//reg to reg takes 6 cycles, mem to mem is 4 cycles + all the operand fetch/writing (including 2 cycle predec penalty for first operand)
+	cycles(&opts->gen, inst->dst.addr_mode != MODE_REG ? BUS : BUS + 2);
 	uint8_t other_reg;
 	//WARNING: This may need adjustment if register assignments change
 	if (opts->gen.scratch2 > RBX) {
@@ -2068,7 +2076,7 @@
 void translate_m68k_negx(m68k_options *opts, m68kinst *inst, host_ea *src_op, host_ea *dst_op)
 {
 	code_info *code = &opts->gen.code;
-	cycles(&opts->gen, BUS);
+	cycles(&opts->gen, inst->extra.size == OPSIZE_LONG && inst->dst.addr_mode == MODE_REG ? BUS+2 : BUS);
 	if (dst_op->mode == MODE_REG_DIRECT) {
 		if (dst_op->base == opts->gen.scratch1) {
 			push_r(code, opts->gen.scratch2);
@@ -2134,6 +2142,7 @@
 			}
 			update_flags(opts, init_flags);
 		} else {
+			cycles(&opts->gen, inst->extra.size == OPSIZE_LONG ? 8 : 6);
 			if (src_op->mode == MODE_REG_DIRECT) {
 				if (src_op->base != opts->gen.scratch1) {
 					mov_rr(code, src_op->base, opts->gen.scratch1, SZ_B);
@@ -2441,7 +2450,7 @@
 void translate_m68k_move_from_sr(m68k_options *opts, m68kinst *inst, host_ea *src_op, host_ea *dst_op)
 {
 	code_info *code = &opts->gen.code;
-	cycles(&opts->gen, inst->dst.addr_mode == MODE_REG_DIRECT ? BUS+2 : BUS);
+	cycles(&opts->gen, inst->dst.addr_mode == MODE_REG ? BUS+2 : BUS);
 	call(code, opts->get_sr);
 	if (dst_op->mode == MODE_REG_DIRECT) {
 		mov_rr(code, opts->gen.scratch1, dst_op->base, SZ_W);
--- a/megawifi.c	Sun May 10 00:16:00 2020 -0700
+++ b/megawifi.c	Thu Aug 05 09:29:33 2021 -0700
@@ -21,7 +21,7 @@
 #include "net.h"
 #include "util.h"
 
-#ifdef _WIN32
+#if defined(_WIN32) || defined(__APPLE__)
 #  if BYTE_ORDER == LITTLE_ENDIAN
 #define htobe64(val)   ((((uint64_t)htonl((val)&0xFFFFFFFF))<<32) | htonl((val)>>32))
 #  else
--- a/nuklear_ui/blastem_nuklear.c	Sun May 10 00:16:00 2020 -0700
+++ b/nuklear_ui/blastem_nuklear.c	Thu Aug 05 09:29:33 2021 -0700
@@ -31,7 +31,8 @@
 	struct nk_image  ui;
 } ui_image;
 
-static ui_image **ui_images, *controller_360, *controller_ps4, *controller_ps4_6b;
+static ui_image **ui_images, *controller_360, *controller_ps4, 
+	*controller_ps4_6b, *controller_wiiu, *controller_gen_6b;
 static uint32_t num_ui_images, ui_image_storage;
 
 typedef void (*view_fun)(struct nk_context *);
@@ -88,6 +89,15 @@
 		if (entries) {
 			sort_dir_list(entries, num_entries);
 		}
+		if (!num_entries) {
+			//get_dir_list can fail if the user doesn't have permission
+			//for the current folder, make sure they can still navigate up
+			free_dir_list(entries, num_entries);
+			entries = calloc(1, sizeof(dir_entry));
+			entries[0].name = strdup("..");
+			entries[0].is_dir = 1;
+			num_entries = 1;
+		}
 	}
 	if (!got_ext_list) {
 		ext_list = get_extension_list(config, &num_exts);
@@ -98,7 +108,8 @@
 	if (nk_begin(context, "Load ROM", nk_rect(0, 0, width, height), 0)) {
 		nk_layout_row_static(context, height - context->style.font->height * 3, width - 60, 1);
 		int32_t old_selected = selected_entry;
-		if (nk_group_begin(context, "Select ROM", NK_WINDOW_BORDER | NK_WINDOW_TITLE)) {
+		char *title = alloc_concat("Select ROM: ", current_path);
+		if (nk_group_begin(context, title, NK_WINDOW_BORDER | NK_WINDOW_TITLE)) {
 			nk_layout_row_static(context, context->style.font->height - 2, width-100, 1);
 			for (int32_t i = 0; i < num_entries; i++)
 			{
@@ -118,6 +129,7 @@
 			}
 			nk_group_end(context);
 		}
+		free(title);
 		nk_layout_row_static(context, context->style.font->height * 1.75, width > 600 ? 300 : width / 2, 2);
 		if (nk_button_label(context, "Back")) {
 			pop_view();
@@ -977,10 +989,16 @@
 
 static ui_image *select_best_image(controller_info *info)
 {
-	if (info->variant != VARIANT_NORMAL) {
-		return controller_ps4_6b;
+	if (info->variant != VARIANT_NORMAL || info->type == TYPE_SEGA) {
+		if (info->type == TYPE_PSX) {
+			return controller_ps4_6b;
+		} else {
+			return controller_gen_6b;
+		}
 	} else if (info->type == TYPE_PSX) {
 		return controller_ps4;
+	} else if (info->type == TYPE_NINTENDO) {
+		return controller_wiiu;
 	} else {
 		return controller_360;
 	}
@@ -1243,14 +1261,14 @@
 				)) {
 				if (current_button <= SDL_CONTROLLER_BUTTON_B || axis_moved != button_a_axis) {
 					start_mapping();
+					if (current_button >= SDL_CONTROLLER_BUTTON_DPAD_UP) {
+						mapping_string[mapping_pos++] = axis_value >= 0 ? '+' : '-';
+					}
 					mapping_string[mapping_pos++] = 'a';
 					if (axis_moved > 9) {
 						mapping_string[mapping_pos++] = '0' + axis_moved / 10;
 					}
 					mapping_string[mapping_pos++] = '0' + axis_moved % 10;
-					if (current_button >= SDL_CONTROLLER_BUTTON_DPAD_UP) {
-						mapping_string[mapping_pos++] = axis_value >= 0 ? '+' : '-';
-					}
 					last_axis = axis_moved;
 					last_axis_value = axis_value;
 				}
@@ -1330,26 +1348,41 @@
 		nk_label(context, "Select the layout that", NK_TEXT_CENTERED);
 		nk_label(context, "best matches your controller", NK_TEXT_CENTERED);
 		nk_label(context, "", NK_TEXT_CENTERED);
-		if (nk_button_label(context, "4 face buttons")) {
-			selected_controller_info.variant = VARIANT_NORMAL;
-			selected = 1;
-		}
-		char buffer[512];
-		snprintf(buffer, sizeof(buffer), "6 face buttons including %s and %s", 
-			get_button_label(&selected_controller_info, SDL_CONTROLLER_BUTTON_RIGHTSHOULDER), 
-			get_axis_label(&selected_controller_info, SDL_CONTROLLER_AXIS_TRIGGERRIGHT)
-		);
-		if (nk_button_label(context, buffer)) {
-			selected_controller_info.variant = VARIANT_6B_RIGHT;
-			selected = 1;
-		}
-		snprintf(buffer, sizeof(buffer), "6 face buttons including %s and %s", 
-			get_button_label(&selected_controller_info, SDL_CONTROLLER_BUTTON_LEFTSHOULDER), 
-			get_button_label(&selected_controller_info, SDL_CONTROLLER_BUTTON_RIGHTSHOULDER)
-		);
-		if (nk_button_label(context, buffer)) {
-			selected_controller_info.variant = VARIANT_6B_BUMPERS;
-			selected = 1;
+		if (selected_controller_info.subtype == SUBTYPE_GENESIS) {
+			if (nk_button_label(context, "3 button")) {
+				selected_controller_info.variant = VARIANT_3BUTTON;
+				selected = 1;
+			}
+			if (nk_button_label(context, "Standard 6 button")) {
+				selected_controller_info.variant = VARIANT_6B_BUMPERS;
+				selected = 1;
+			}
+			if (nk_button_label(context, "6 button with 2 shoulder buttons")) {
+				selected_controller_info.variant = VARIANT_8BUTTON;
+				selected = 1;
+			}
+		} else {
+			if (nk_button_label(context, "4 face buttons")) {
+				selected_controller_info.variant = VARIANT_NORMAL;
+				selected = 1;
+			}
+			char buffer[512];
+			snprintf(buffer, sizeof(buffer), "6 face buttons including %s and %s", 
+				get_button_label(&selected_controller_info, SDL_CONTROLLER_BUTTON_RIGHTSHOULDER), 
+				get_axis_label(&selected_controller_info, SDL_CONTROLLER_AXIS_TRIGGERRIGHT)
+			);
+			if (nk_button_label(context, buffer)) {
+				selected_controller_info.variant = VARIANT_6B_RIGHT;
+				selected = 1;
+			}
+			snprintf(buffer, sizeof(buffer), "6 face buttons including %s and %s", 
+				get_button_label(&selected_controller_info, SDL_CONTROLLER_BUTTON_LEFTSHOULDER), 
+				get_button_label(&selected_controller_info, SDL_CONTROLLER_BUTTON_RIGHTSHOULDER)
+			);
+			if (nk_button_label(context, buffer)) {
+				selected_controller_info.variant = VARIANT_6B_BUMPERS;
+				selected = 1;
+			}
 		}
 		nk_end(context);
 	}
@@ -1380,7 +1413,22 @@
 				selected_controller_info.type = type_id;
 				selected_controller_info.subtype = first_subtype_id + i;
 				pop_view();
-				push_view(view_controller_variant);
+				if (selected_controller_info.subtype == SUBTYPE_SATURN) {
+					selected_controller_info.variant = VARIANT_6B_BUMPERS;
+					save_controller_info(selected_controller, &selected_controller_info);
+					if (initial_controller_config) {
+						SDL_GameController *controller = render_get_controller(selected_controller);
+						if (controller) {
+							push_view(view_controller_bindings);
+							controller_binding_changed = 0;
+							SDL_GameControllerClose(controller);
+						} else {
+							show_mapping_view();
+						}
+					}
+				} else {
+					push_view(view_controller_variant);
+				}
 			}
 		}
 		nk_group_end(context);
@@ -1960,17 +2008,19 @@
 	uint32_t desired_width = context->style.font->height * 10;
 	if (nk_begin(context, "System Settings", nk_rect(0, 0, width, height), 0)) {
 		nk_layout_row_static(context, context->style.font->height, desired_width, 2);
+		
+		selected_model = settings_dropdown_ex(context, "Model", model_opts, model_names, num_models, selected_model, "system\0model\0");
+		selected_io_1 = settings_dropdown_ex(context, "IO Port 1 Device", io_opts_1, device_type_names, num_io, selected_io_1, "io\0devices\0""1\0");
+		selected_io_2 = settings_dropdown_ex(context, "IO Port 2 Device", io_opts_2, device_type_names, num_io, selected_io_2, "io\0devices\0""2\0");
+		selected_region = settings_dropdown_ex(context, "Default Region", region_codes, regions, num_regions, selected_region, "system\0default_region\0");
 		selected_sync = settings_dropdown(context, "Sync Source", sync_opts, num_sync_opts, selected_sync, "system\0sync_source\0");
 		settings_int_property(context, "68000 Clock Divider", "", "clocks\0m68k_divider\0", 7, 1, 53);
+		selected_format = settings_dropdown(context, "Save State Format", formats, num_formats, selected_format, "ui\0state_format\0");
+		selected_init = settings_dropdown(context, "Initial RAM Value", ram_inits, num_inits, selected_init, "system\0ram_init\0");
 		settings_toggle(context, "Remember ROM Path", "ui\0remember_path\0", 1);
 		settings_toggle(context, "Save config with EXE", "ui\0config_in_exe_dir\0", 0);
 		settings_string(context, "Game Save Path", "ui\0save_path\0", "$USERDATA/blastem/$ROMNAME");
-		selected_region = settings_dropdown_ex(context, "Default Region", region_codes, regions, num_regions, selected_region, "system\0default_region\0");
-		selected_model = settings_dropdown_ex(context, "Model", model_opts, model_names, num_models, selected_model, "system\0model\0");
-		selected_format = settings_dropdown(context, "Save State Format", formats, num_formats, selected_format, "ui\0state_format\0");
-		selected_init = settings_dropdown(context, "Initial RAM Value", ram_inits, num_inits, selected_init, "system\0ram_init\0");
-		selected_io_1 = settings_dropdown_ex(context, "IO Port 1 Device", io_opts_1, device_type_names, num_io, selected_io_1, "io\0devices\0""1\0");
-		selected_io_2 = settings_dropdown_ex(context, "IO Port 2 Device", io_opts_2, device_type_names, num_io, selected_io_2, "io\0devices\0""2\0");
+		
 		if (nk_button_label(context, "Back")) {
 			pop_view();
 		}
@@ -1978,6 +2028,29 @@
 	}
 }
 
+void view_confirm_reset(struct nk_context *context)
+{
+	if (nk_begin(context, "Reset Confirm", nk_rect(0, 0, render_width(), render_height()), 0)) {
+		uint32_t desired_width = context->style.font->height * 20;
+		nk_layout_row_static(context, context->style.font->height, desired_width, 1);
+		nk_label(context, "This will reset all settings and controller", NK_TEXT_LEFT);
+		nk_label(context, "mappings back to the defaults.", NK_TEXT_LEFT);
+		nk_label(context, "Are you sure you want to proceed?", NK_TEXT_LEFT);
+		nk_layout_row_static(context, context->style.font->height * 1.5, desired_width / 2, 2);
+		if (nk_button_label(context, "Maybe not")) {
+			pop_view();
+		}
+		if (nk_button_label(context, "Yep, delete it all")) {
+			delete_custom_config();
+			config = load_config();
+			delete_controller_info();
+			config_dirty = 1;
+			pop_view();
+		}
+		nk_end(context);
+	}
+}
+
 void view_back(struct nk_context *context)
 {
 	pop_view();
@@ -1993,6 +2066,7 @@
 		{"Video", view_video_settings},
 		{"Audio", view_audio_settings},
 		{"System", view_system_settings},
+		{"Reset to Defaults", view_confirm_reset},
 		{"Back", view_back}
 	};
 	
@@ -2310,6 +2384,8 @@
 	controller_360 = load_ui_image("images/360.png");
 	controller_ps4 = load_ui_image("images/ps4.png");
 	controller_ps4_6b = load_ui_image("images/ps4_6b.png");
+	controller_wiiu = load_ui_image("images/wiiu.png");
+	controller_gen_6b = load_ui_image("images/genesis_6b.png");
 	
 	texture_init();
 	
--- a/nuklear_ui/nuklear.h	Sun May 10 00:16:00 2020 -0700
+++ b/nuklear_ui/nuklear.h	Thu Aug 05 09:29:33 2021 -0700
@@ -11380,10 +11380,13 @@
     NK_ASSERT(alloc);
 
     if (!image_memory || !width || !height || !config_list || !count) return nk_false;
+	int pixel_area_estimate = 0;
     for (config_iter = config_list; config_iter; config_iter = config_iter->next) {
         range_count = nk_range_count(config_iter->range);
         total_range_count += range_count;
-        total_glyph_count += nk_range_glyph_count(config_iter->range, range_count);
+		int glyphs = nk_range_glyph_count(config_iter->range, range_count);
+        total_glyph_count += glyphs;
+		pixel_area_estimate += glyphs * config_iter->size * config_iter->size;
     }
 
     /* setup font baker from temporary memory */
@@ -11394,7 +11397,13 @@
     }
 
     *height = 0;
-    *width = (total_glyph_count > 1000) ? 1024 : 512;
+	int width_estimate = sqrt(pixel_area_estimate) + 0.5;
+	*width = 128;
+	while (*width < width_estimate)
+	{
+		*width *= 2;
+	}
+    //*width = (total_glyph_count > 1000) ? 1024 : 512;
     nk_tt_PackBegin(&baker->spc, 0, (int)*width, (int)max_height, 0, 1, alloc);
     {
         int input_i = 0;
--- a/nuklear_ui/nuklear_sdl_gles2.h	Sun May 10 00:16:00 2020 -0700
+++ b/nuklear_ui/nuklear_sdl_gles2.h	Thu Aug 05 09:29:33 2021 -0700
@@ -168,6 +168,10 @@
     glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
     glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, (GLsizei)width, (GLsizei)height, 0,
                 GL_RGBA, GL_UNSIGNED_BYTE, image);
+	GLenum err = glGetError();
+	if (err != GL_NO_ERROR) {
+		printf("glTexImage2D failed with error %d\n", err);
+	}
 }
 
 NK_API void
--- a/png.c	Sun May 10 00:16:00 2020 -0700
+++ b/png.c	Thu Aug 05 09:29:33 2021 -0700
@@ -204,7 +204,7 @@
 
 static uint32_t pixel_gray(uint8_t **cur, uint8_t **last, uint8_t bpp, uint32_t x, filter_fun filter)
 {
-	uint8_t value = filter(*cur, *last, bpp, x);
+	uint8_t value = **cur = filter(*cur, *last, bpp, x);
 	(*cur)++;
 	if (*last) {
 		(*last)++;
@@ -214,17 +214,17 @@
 
 static uint32_t pixel_true(uint8_t **cur, uint8_t **last, uint8_t bpp, uint32_t x, filter_fun filter)
 {
-	uint8_t red = filter(*cur, *last, bpp, x);
+	uint8_t red = **cur = filter(*cur, *last, bpp, x);
 	(*cur)++;
 	if (*last) {
 		(*last)++;
 	}
-	uint8_t green = filter(*cur, *last, bpp, x);
+	uint8_t green = **cur = filter(*cur, *last, bpp, x);
 	(*cur)++;
 	if (*last) {
 		(*last)++;
 	}
-	uint8_t blue = filter(*cur, *last, bpp, x);
+	uint8_t blue = **cur = filter(*cur, *last, bpp, x);
 	(*cur)++;
 	if (*last) {
 		(*last)++;
@@ -234,12 +234,12 @@
 
 static uint32_t pixel_gray_alpha(uint8_t **cur, uint8_t **last, uint8_t bpp, uint32_t x, filter_fun filter)
 {
-	uint8_t value = filter(*cur, *last, bpp, x);
+	uint8_t value = **cur = filter(*cur, *last, bpp, x);
 	(*cur)++;
 	if (*last) {
 		(*last)++;
 	}
-	uint8_t alpha = filter(*cur, *last, bpp, x);
+	uint8_t alpha = **cur = filter(*cur, *last, bpp, x);
 	(*cur)++;
 	if (*last) {
 		(*last)++;
@@ -249,22 +249,22 @@
 
 static uint32_t pixel_true_alpha(uint8_t **cur, uint8_t **last, uint8_t bpp, uint32_t x, filter_fun filter)
 {
-	uint8_t red = filter(*cur, *last, bpp, x);
+	uint8_t red = **cur = filter(*cur, *last, bpp, x);
 	(*cur)++;
 	if (*last) {
 		(*last)++;
 	}
-	uint8_t green = filter(*cur, *last, bpp, x);
+	uint8_t green = **cur = filter(*cur, *last, bpp, x);
 	(*cur)++;
 	if (*last) {
 		(*last)++;
 	}
-	uint8_t blue = filter(*cur, *last, bpp, x);
+	uint8_t blue = **cur = filter(*cur, *last, bpp, x);
 	(*cur)++;
 	if (*last) {
 		(*last)++;
 	}
-	uint8_t alpha = filter(*cur, *last, bpp, x);
+	uint8_t alpha = **cur = filter(*cur, *last, bpp, x);
 	(*cur)++;
 	if (*last) {
 		(*last)++;
@@ -354,6 +354,7 @@
 					}
 					memcpy(idat_buf + idat_size, buffer + cur, chunk_size);
 					idat_size += chunk_size;
+					idat_needs_free = 1;
 				} else {
 					idat_buf = buffer + cur;
 					idat_size = chunk_size;
--- a/render.h	Sun May 10 00:16:00 2020 -0700
+++ b/render.h	Thu Aug 05 09:29:33 2021 -0700
@@ -6,6 +6,8 @@
 #ifndef RENDER_H_
 #define RENDER_H_
 
+#include <stdint.h>
+
 #ifndef IS_LIB
 #ifdef USE_FBDEV
 #include "special_keys_evdev.h"
@@ -138,7 +140,10 @@
 void render_video_loop(void);
 uint8_t render_should_release_on_exit(void);
 void render_set_external_sync(uint8_t ext_sync_on);
+void render_reset_mappings(void);
+#ifndef IS_LIB
 uint8_t render_create_thread(render_thread *thread, const char *name, render_thread_fun fun, void *data);
+#endif
 
 #endif //RENDER_H_
 
--- a/render_sdl.c	Sun May 10 00:16:00 2020 -0700
+++ b/render_sdl.c	Thu Aug 05 09:29:33 2021 -0700
@@ -171,8 +171,14 @@
 
 void render_audio_created(audio_source *source)
 {
-	if (render_is_audio_sync()) {
-		SDL_PauseAudio(0);
+	if (sync_src == SYNC_AUDIO) {
+		//SDL_PauseAudio acquires the audio device lock, which is held while the callback runs
+		//since our callback can itself be stuck waiting on the audio_ready condition variable
+		//calling SDL_PauseAudio(0) again for audio sources after the first can deadlock
+		//fortunately SDL_GetAudioStatus does not acquire the lock so is safe to call here
+		if (SDL_GetAudioStatus() == SDL_AUDIO_PAUSED) {
+			SDL_PauseAudio(0);
+		}
 	}
 	if (current_system && sync_src == SYNC_AUDIO_THREAD) {
 		system_request_exit(current_system, 0);
@@ -194,8 +200,14 @@
 
 void render_source_resumed(audio_source *src)
 {
-	if (render_is_audio_sync()) {
-		SDL_PauseAudio(0);
+	if (sync_src == SYNC_AUDIO) {
+		//SDL_PauseAudio acquires the audio device lock, which is held while the callback runs
+		//since our callback can itself be stuck waiting on the audio_ready condition variable
+		//calling SDL_PauseAudio(0) again for audio sources after the first can deadlock
+		//fortunately SDL_GetAudioStatus does not acquire the lock so is safe to call here
+		if (SDL_GetAudioStatus() == SDL_AUDIO_PAUSED) {
+			SDL_PauseAudio(0);
+		}
 	}
 	if (current_system && sync_src == SYNC_AUDIO_THREAD) {
 		system_request_exit(current_system, 0);
@@ -394,7 +406,7 @@
 	} else {
 		tex_width = tex_height = 512;
 	}
-	printf("Using %dx%d textures\n", tex_width, tex_height);
+	debug_message("Using %dx%d textures\n", tex_width, tex_height);
 	for (int i = 0; i < 3; i++)
 	{
 		glBindTexture(GL_TEXTURE_2D, textures[i]);
@@ -1191,6 +1203,19 @@
 
 	atexit(render_quit);
 }
+
+void render_reset_mappings(void)
+{
+	SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER);
+	SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER);
+	uint32_t db_size;
+	char *db_data = read_bundled_file("gamecontrollerdb.txt", &db_size);
+	if (db_data) {
+		int added = SDL_GameControllerAddMappingsFromRW(SDL_RWFromMem(db_data, db_size), 1);
+		free(db_data);
+		debug_message("Added %d game controller mappings from gamecontrollerdb.txt\n", added);
+	}
+}
 static int in_toggle;
 
 void render_config_updated(void)
@@ -1706,6 +1731,7 @@
 	if (sync_src != SYNC_AUDIO_THREAD && sync_src != SYNC_EXTERNAL) {
 		return;
 	}
+	SDL_PauseAudio(0);
 	SDL_LockMutex(frame_mutex);
 		for(;;)
 		{
--- a/rom.db	Sun May 10 00:16:00 2020 -0700
+++ b/rom.db	Thu Aug 05 09:29:33 2021 -0700
@@ -1377,3 +1377,61 @@
 		}
 	}
 }
+NETO-001 {
+	name Sonic Delta
+	map {
+		0 {
+			device ROM
+			last 7FFFF
+		}
+		80000 {
+			device Sega mapper
+			last 3FFFFF
+			offset 80000
+		}
+	}
+}
+5e5ca20b39122c86b8f662bd8014af674f9dbed7 {
+	name Rock Heaven
+	map {
+		0 {
+			device ROM
+			last 3FFFFF
+		}
+		500008 {
+			device fixed
+			value 5082
+			last 500009
+		}
+	}
+}
+68366449fd1497538741b5a1949e342202d48948 {
+	name Rock World
+	map {
+		0 {
+			device ROM
+			last 3FFFFF
+		}
+		500008 {
+			device fixed
+			value 4000
+			last 500009
+		}
+		500208 {
+			device fixed
+			value A000
+			last 500209
+		}
+		
+	}
+}
+T-122026 {
+	name Outback Joey
+	HeartbeatTrainer {
+		size 512
+	}
+	device_overrides {
+		1 heartbeat_trainer.1
+		2 gamepad3.2
+	}
+}
--- a/romdb.c	Sun May 10 00:16:00 2020 -0700
+++ b/romdb.c	Thu Aug 05 09:29:33 2021 -0700
@@ -32,6 +32,8 @@
 		return "EEPROM";
 	} else if(save_type == SAVE_NOR) {
 		return "NOR Flash";
+	} else if(save_type == SAVE_HBPT) {
+		return "Heartbeat Personal Trainer";
 	}
 	return "SRAM";
 }
@@ -364,6 +366,8 @@
 					info->map[1].flags |= MMAP_ONLY_ODD;
 				} else if (info->save_type == RAM_FLAG_EVEN) {
 					info->map[1].flags |= MMAP_ONLY_EVEN;
+				} else {
+					info->map[1].flags |= MMAP_CODE;
 				}
 				info->map[1].buffer = info->save_buffer;
 			} else {
@@ -717,6 +721,8 @@
 			map->flags |= MMAP_ONLY_ODD;
 		} else if(state->info->save_type == RAM_FLAG_EVEN) {
 			map->flags |= MMAP_ONLY_EVEN;
+		} else {
+			map->flags |= MMAP_CODE;
 		}
 		map->mask = calc_mask(state->info->save_size, start, end);
 	} else if (!strcmp(dtype, "RAM")) {
@@ -986,6 +992,19 @@
 		info.port1_override = tern_find_ptr(device_overrides, "1");
 		info.port2_override = tern_find_ptr(device_overrides, "2");
 		info.ext_override = tern_find_ptr(device_overrides, "ext");
+		if (
+			info.save_type == SAVE_NONE
+			&& (
+				(info.port1_override && startswith(info.port1_override, "heartbeat_trainer."))
+				|| (info.port2_override && startswith(info.port2_override, "heartbeat_trainer."))
+				|| (info.ext_override && startswith(info.ext_override, "heartbeat_trainer."))
+			)
+		) {
+			info.save_type = SAVE_HBPT;
+			info.save_size = atoi(tern_find_path_default(entry, "HeartbeatTrainer\0size\0", (tern_val){.ptrval="512"}, TVAL_PTR).ptrval);
+			info.save_buffer = calloc(info.save_size + 5 + 8, 1);
+			memset(info.save_buffer, 0xFF, info.save_size);
+		}
 	} else {
 		info.port1_override = info.port2_override = info.ext_override = NULL;
 	}
--- a/romdb.h	Sun May 10 00:16:00 2020 -0700
+++ b/romdb.h	Thu Aug 05 09:29:33 2021 -0700
@@ -11,6 +11,7 @@
 #define RAM_FLAG_MASK RAM_FLAG_ODD
 #define SAVE_I2C      0x01
 #define SAVE_NOR      0x02
+#define SAVE_HBPT     0x03
 #define SAVE_NONE     0xFF
 
 #include "tern.h"
--- a/serialize.h	Sun May 10 00:16:00 2020 -0700
+++ b/serialize.h	Thu Aug 05 09:29:33 2021 -0700
@@ -42,7 +42,8 @@
 	SECTION_SOUND_RAM,
 	SECTION_MAPPER,
 	SECTION_EEPROM,
-	SECTION_CART_RAM
+	SECTION_CART_RAM,
+	SECTION_TMSS
 };
 
 void init_serialize(serialize_buffer *buf);
--- a/tern.c	Sun May 10 00:16:00 2020 -0700
+++ b/tern.c	Thu Aug 05 09:29:33 2021 -0700
@@ -305,12 +305,11 @@
 
 void tern_free(tern_node *head)
 {
-	if (head->left) {
-		tern_free(head->left);
+	if (!head) {
+		return;
 	}
-	if (head->right) {
-		tern_free(head->right);
-	}
+	tern_free(head->left);
+	tern_free(head->right);
 	if (head->el) {
 		tern_free(head->straight.next);
 	}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tmss.s68	Thu Aug 05 09:29:33 2021 -0700
@@ -0,0 +1,253 @@
+	dc.l $0, start
+	dc.l empty_handler
+	dc.l empty_handler
+	;$10
+	dc.l empty_handler
+	dc.l empty_handler
+	dc.l empty_handler
+	dc.l empty_handler
+	;$20
+	dc.l empty_handler
+	dc.l empty_handler
+	dc.l empty_handler
+	dc.l empty_handler
+	;$30
+	dc.l empty_handler
+	dc.l empty_handler
+	dc.l empty_handler
+	dc.l empty_handler
+	;$40
+	dc.l empty_handler
+	dc.l empty_handler
+	dc.l empty_handler
+	dc.l empty_handler
+	;$50
+	dc.l empty_handler
+	dc.l empty_handler
+	dc.l empty_handler
+	dc.l empty_handler
+	;$60
+	dc.l empty_handler
+	dc.l empty_handler
+	dc.l empty_handler
+	dc.l empty_handler
+	;$70
+	dc.l empty_handler
+	dc.l empty_handler
+	dc.l int_6
+	dc.l empty_handler
+	;$80
+	dc.l empty_handler
+	dc.l empty_handler
+	dc.l empty_handler
+	dc.l empty_handler
+	;$90
+	dc.l empty_handler
+	dc.l empty_handler
+	dc.l empty_handler
+	dc.l empty_handler
+	;$A0
+	dc.l empty_handler
+	dc.l empty_handler
+	dc.l empty_handler
+	dc.l empty_handler
+	;$B0
+	dc.l empty_handler
+	dc.l empty_handler
+	dc.l empty_handler
+	dc.l empty_handler
+	;$C0
+	dc.l empty_handler
+	dc.l empty_handler
+	dc.l empty_handler
+	dc.l empty_handler
+	;$D0
+	dc.l empty_handler
+	dc.l empty_handler
+	dc.l empty_handler
+	dc.l empty_handler
+	;$E0
+	dc.l empty_handler
+	dc.l empty_handler
+	dc.l empty_handler
+	dc.l empty_handler
+	;$F0
+	dc.l empty_handler
+	dc.l empty_handler
+	dc.l empty_handler
+	dc.l empty_handler
+	dc.b "SEGA IS COOL    "
+	dc.b "(C)M.Pavone 2021"
+	dc.b "TRAIN MEMES STAN"
+	dc.b "D SILENTLY      "
+	dc.b "                "
+	dc.b "TRIUMPHANT MAMMA"
+	dc.b "LS SALUTE SOCIAL"
+	dc.b "ITES            "
+	dc.b "MP 20210227-01",0,0
+	dc.b "J               "
+	dc.l $0
+	dc.l romend-1
+	dc.l $FF0000
+	dc.l $FFFFFF
+	dc.b "                "
+	dc.b "                "
+	dc.b "                "
+	dc.b "                "
+	dc.b "JUE             "
+	
+frame_counter equ $FFFF8000
+ram_code equ $FFFF8002
+
+empty_handler:
+	rte
+start:
+	lea $A14000, a3
+	move.l #'SEGA', (a3)
+	lea $C00000, a0
+	lea $C00004, a1
+	move.w #$8104, (a1) ;Mode 5, everything turned off
+	move.w #$8004, (a1)
+	move.w #$8220, (a1) ;Scroll a table $8000
+	move.w #$8404, (a1) ;Scroll b table $8000
+	move.w #$8560, (a1) ;SAT table $C000
+	move.w #$8700, (a1) ;backdrop color 0
+	move.w #$8A01, (a1) ;Set HINT counter
+	move.w #$8B00, (a1) ;full screen scroll
+	move.w #$8C81, (a1) ;40 cell mode, no interlace
+	move.w #$8D00, (a1) ;hscroll table at 0
+	move.w #$8F02, (a1) ;autoinc 2
+	move.w #$9011, (a1) ;64x64 scroll size
+	move.l #$C0000000, (a1)
+	move.w #$000, (a0)
+	move.w #$EEE, (a0)
+
+	;clear scroll table
+	move.l #$40000000, (a1)
+	move.l #0, (a0)
+
+	;load tiles
+	move.l #$44000000, (a1)
+	lea font, a2
+	move.w #((fontend-font)/4 - 1), d0
+tloop:
+	move.l (a2)+, (a0)
+	dbra d0, tloop
+
+	;clear name table
+	move.l #$40000002, (a1)
+	moveq #32, d0
+	move.w #(64*64-1), d1
+ploop:
+	move.w d0, (a0)
+	dbra d1, ploop
+	move.l #$45960002, d7
+	move.l d7, (a1)
+	move.l #$800000, d6
+	
+	lea ram_code_src(pc), a6
+	lea ram_code.w, a5
+	moveq #(font-ram_code_src)/2-1, d0
+copy:
+	move.w (a6)+, (a5)+
+	dbra d0, copy
+	lea $101(a3), a4
+	lea $100.w, a5
+	move.l #'SEGA', d5
+	move.l #' SEG', d4
+	moveq #0, d0
+	moveq #1, d2
+	move.w #180, d3
+	btst #6, $A10001
+	beq .not_pal
+	move.w #150, d3
+.not_pal:
+	jmp ram_code.w
+
+ram_code_src:
+	move.b d2, (a4)
+	cmp.l (a5), d5
+	beq.s is_good
+	cmp.l (a5), d4
+	bne.s is_bad
+is_good:
+	move.b d0, (a4)
+	lea good(pc), a6
+	bsr.s print_string
+	
+	add.l d6, d7
+	move.l d7, (a1)
+	bsr.s print_string
+	
+	add.l d6, d7
+	move.l d7, (a1)
+	bsr.s print_string
+
+	move.w #$8164, (a1)
+	move #$2500, SR
+wait:
+	cmp.w frame_counter.w, d3
+	bne.s wait
+	move #$2700, SR
+	move.b d2, (a4)
+	move.l $0.w, a7
+	move.l $4.w, a6
+	move.w #$8104, (a1)
+	move.l d0, (a3)
+	jmp (a6)
+	
+is_bad:
+	move.b d0, (a4)
+	lea bad(pc), a6
+	bsr.s print_string
+	
+	add.l d6, d7
+	move.l d7, (a1)
+	bsr.s print_string
+	
+	add.l d6, d7
+	move.l d7, (a1)
+	bsr.s print_string
+	
+	move.w #$8144, (a1)
+forever:
+	bra.s forever
+
+
+int_6:
+	addq.w #1, frame_counter.w
+	rte
+
+;Prints a null terminated string
+;a6 - pointer to string
+;a0 - VDP data port
+;d0 - base tile attribute
+;
+;Clobbers: d1.w
+print_string:
+.loop
+	moveq #0, d1
+	move.b (a6)+, d1
+	beq .end
+	add.w d0, d1
+	move.w d1, (a0)
+	bra .loop
+.end
+	rts
+	
+good:
+	dc.b "  BLASTEM THINKS", 0
+	dc.b "  THAT THIS CART", 0
+	dc.b " TASTES DELICIOUS!", 0
+	
+bad:
+	dc.b "  *sniff* *sniff*", 0
+	dc.b " something doesn't", 0
+	dc.b "  smell right...", 0
+
+	align 1
+font:
+	incbin font.tiles
+fontend
+
+romend
--- a/trans.c	Sun May 10 00:16:00 2020 -0700
+++ b/trans.c	Thu Aug 05 09:29:33 2021 -0700
@@ -4,7 +4,11 @@
  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 "68kinst.h"
+#ifdef NEW_CORE
+#include "m68k.h"
+#else
 #include "m68k_core.h"
+#endif
 #include "mem.h"
 #include <stdio.h>
 #include <stdlib.h>
@@ -19,6 +23,7 @@
 {
 }
 
+#ifndef NEW_CORE
 uint64_t total_cycles;
 
 m68k_context * sync_components(m68k_context * context, uint32_t address)
@@ -32,12 +37,17 @@
 	}
 	return context;
 }
+#endif
 
 m68k_context *reset_handler(m68k_context *context)
 {
 	m68k_print_regs(context);
+#ifdef NEW_CORE
+	printf("cycles: %d\n", context->cycles);
+#else
 	total_cycles += context->current_cycle;
 	printf("%ld cycles\n", total_cycles);
+#endif
 	exit(0);
 	//unreachable
 	return context;
@@ -75,16 +85,21 @@
 	memmap[1].flags = MMAP_READ | MMAP_WRITE | MMAP_CODE;
 	memmap[1].buffer = malloc(64 * 1024);
 	memset(memmap[1].buffer, 0, 64 * 1024);
-	init_m68k_opts(&opts, memmap, 2, 7);
+	init_m68k_opts(&opts, memmap, 2, 1);
 	m68k_context * context = init_68k_context(&opts, reset_handler);
 	context->mem_pointers[0] = memmap[0].buffer;
 	context->mem_pointers[1] = memmap[1].buffer;
-	context->target_cycle = context->sync_cycle = 0x80000000;
-	/*
-	uint32_t address;
-	address = filebuf[2] << 16 | filebuf[3];
-	translate_m68k_stream(address, context);*/
+#ifdef NEW_CORE
+	context->cycles = 40;
+#else
+	context->current_cycle = 40;
+	context->target_cycle = context->sync_cycle = 8000;
+#endif
 	m68k_reset(context);
+#ifdef NEW_CORE
+	m68k_execute(context, 8000);
+	puts("hit cycle limit");
+#endif
 	return 0;
 }
 
--- a/util.c	Sun May 10 00:16:00 2020 -0700
+++ b/util.c	Thu Aug 05 09:29:33 2021 -0700
@@ -534,6 +534,11 @@
 	output_enabled = 0;
 }
 
+uint8_t is_stdout_enabled(void)
+{
+	return output_enabled;
+}
+
 #ifdef _WIN32
 #define WINVER 0x501
 #include <winsock2.h>
--- a/util.h	Sun May 10 00:16:00 2020 -0700
+++ b/util.h	Thu Aug 05 09:29:33 2021 -0700
@@ -88,6 +88,8 @@
 void debug_message(char *format, ...);
 //Disables output of info and debug messages to stdout
 void disable_stdout_messages(void);
+//Returns stdout disable status
+uint8_t is_stdout_enabled(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
--- a/vdp.c	Sun May 10 00:16:00 2020 -0700
+++ b/vdp.c	Thu Aug 05 09:29:33 2021 -0700
@@ -10,6 +10,7 @@
 #include "render.h"
 #include "util.h"
 #include "event_log.h"
+#include "terminal.h"
 
 #define NTSC_INACTIVE_START 224
 #define PAL_INACTIVE_START 240
@@ -253,6 +254,15 @@
 
 void vdp_free(vdp_context *context)
 {
+	if (headless) {
+		free(context->fb);
+	}
+	for (int i = 0; i < VDP_NUM_DEBUG_TYPES; i++)
+	{
+		if (context->enabled_debuggers & (1 << i)) {
+			vdp_toggle_debug_view(context, i);
+		}
+	}
 	free(context);
 }
 
@@ -1144,8 +1154,12 @@
 			context->v_offset = (line) & v_offset_mask;
 			context->flags |= FLAG_WINDOW;
 			return;
+		} else if (column == right_col) {
+			context->flags |= FLAG_WINDOW_EDGE;
+			context->flags &= ~FLAG_WINDOW;
+		} else {
+			context->flags &= ~(FLAG_WINDOW_EDGE|FLAG_WINDOW);
 		}
-		context->flags &= ~FLAG_WINDOW;
 	}
 	//TODO: Verify behavior for 0x20 case
 	uint16_t vscroll = 0xFF | (context->regs[REG_SCROLL] & 0x30) << 4;
@@ -1159,7 +1173,7 @@
 	vscroll >>= vscroll_shift;
 	//TODO: Verify the behavior for a setting of 2
 	static const uint16_t hscroll_masks[] = {0x1F, 0x3F, 0x1F, 0x7F};
-	static const uint16_t v_shifts[] = {6, 7, 0, 8};
+	static const uint16_t v_shifts[] = {6, 7, 16, 8};
 	uint16_t hscroll_mask = hscroll_masks[context->regs[REG_SCROLL] & 0x3];
 	uint16_t v_shift = v_shifts[context->regs[REG_SCROLL] & 0x3];
 	uint16_t hscroll, offset;
@@ -1338,7 +1352,7 @@
 	return (sh_pixel){.index = pixel, .intensity = intensity};
 }
 
-static void render_normal(vdp_context *context, int32_t col, uint8_t *dst, uint8_t *debug_dst, int plane_a_off, int plane_b_off)
+static void render_normal(vdp_context *context, int32_t col, uint8_t *dst, uint8_t *debug_dst, uint8_t *buf_a, int plane_a_off, int plane_a_mask, int plane_b_off)
 {
 	uint8_t *sprite_buf = context->linebuf + col * 8;
 	if (!col && (context->regs[REG_MODE_1] & BIT_COL0_MASK)) {
@@ -1352,7 +1366,7 @@
 		for (int i = 0; i < 8; ++plane_a_off, ++plane_b_off, ++sprite_buf, ++i)
 		{
 			uint8_t sprite, plane_a, plane_b;
-			plane_a = context->tmp_buf_a[plane_a_off & SCROLL_BUFFER_MASK];
+			plane_a = buf_a[plane_a_off & plane_a_mask];
 			plane_b = context->tmp_buf_b[plane_b_off & SCROLL_BUFFER_MASK];
 			*(dst++) = composite_normal(context, debug_dst, *sprite_buf, plane_a, plane_b, context->regs[REG_BG_COLOR]) & 0x3F;
 			debug_dst++;
@@ -1361,7 +1375,7 @@
 		for (int i = 0; i < 16; ++plane_a_off, ++plane_b_off, ++sprite_buf, ++i)
 		{
 			uint8_t sprite, plane_a, plane_b;
-			plane_a = context->tmp_buf_a[plane_a_off & SCROLL_BUFFER_MASK];
+			plane_a = buf_a[plane_a_off & plane_a_mask];
 			plane_b = context->tmp_buf_b[plane_b_off & SCROLL_BUFFER_MASK];
 			*(dst++) = composite_normal(context, debug_dst, *sprite_buf, plane_a, plane_b, context->regs[REG_BG_COLOR]) & 0x3F;
 			debug_dst++;
@@ -1369,7 +1383,7 @@
 	}
 }
 
-static void render_highlight(vdp_context *context, int32_t col, uint8_t *dst, uint8_t *debug_dst, int plane_a_off, int plane_b_off)
+static void render_highlight(vdp_context *context, int32_t col, uint8_t *dst, uint8_t *debug_dst, uint8_t *buf_a, int plane_a_off, int plane_a_mask, int plane_b_off)
 {
 	int start = 0;
 	if (!col && (context->regs[REG_MODE_1] & BIT_COL0_MASK)) {
@@ -1383,7 +1397,7 @@
 	for (int i = start; i < 16; ++plane_a_off, ++plane_b_off, ++sprite_buf, ++i)
 	{
 		uint8_t sprite, plane_a, plane_b;
-		plane_a = context->tmp_buf_a[plane_a_off & SCROLL_BUFFER_MASK];
+		plane_a = buf_a[plane_a_off & plane_a_mask];
 		plane_b = context->tmp_buf_b[plane_b_off & SCROLL_BUFFER_MASK];
 		sprite = *sprite_buf;
 		sh_pixel pixel = composite_highlight(context, debug_dst, sprite, plane_a, plane_b, context->regs[REG_BG_COLOR]);
@@ -1400,7 +1414,7 @@
 	}
 }
 
-static void render_testreg(vdp_context *context, int32_t col, uint8_t *dst, uint8_t *debug_dst, int plane_a_off, int plane_b_off, uint8_t output_disabled, uint8_t test_layer)
+static void render_testreg(vdp_context *context, int32_t col, uint8_t *dst, uint8_t *debug_dst, uint8_t *buf_a, int plane_a_off, int plane_a_mask, int plane_b_off, uint8_t output_disabled, uint8_t test_layer)
 {
 	if (output_disabled) {
 		switch (test_layer)
@@ -1424,7 +1438,7 @@
 		case 2:
 			for (int i = 0; i < 16; i++)
 			{
-				*(dst++) = context->tmp_buf_a[(plane_a_off++) & SCROLL_BUFFER_MASK] & 0x3F;
+				*(dst++) = buf_a[(plane_a_off++) & plane_a_mask] & 0x3F;
 				*(debug_dst++) = DBG_SRC_A;
 			}
 			break;
@@ -1454,7 +1468,7 @@
 					}
 					break;
 				case 2:
-					pixel &= context->tmp_buf_a[(plane_a_off + i) & SCROLL_BUFFER_MASK];
+					pixel &= buf_a[(plane_a_off + i) & plane_a_mask];
 					if (pixel) {
 						src = DBG_SRC_A;
 					}
@@ -1478,7 +1492,7 @@
 		for (int i = start; i < 16; ++plane_a_off, ++plane_b_off, ++sprite_buf, ++i)
 		{
 			uint8_t sprite, plane_a, plane_b;
-			plane_a = context->tmp_buf_a[plane_a_off & SCROLL_BUFFER_MASK];
+			plane_a = buf_a[plane_a_off & plane_a_mask];
 			plane_b = context->tmp_buf_b[plane_b_off & SCROLL_BUFFER_MASK];
 			sprite = *sprite_buf;
 			uint8_t pixel = composite_normal(context, debug_dst, sprite, plane_a, plane_b, 0x3F) & 0x3F;
@@ -1509,7 +1523,7 @@
 	}
 }
 
-static void render_testreg_highlight(vdp_context *context, int32_t col, uint8_t *dst, uint8_t *debug_dst, int plane_a_off, int plane_b_off, uint8_t output_disabled, uint8_t test_layer)
+static void render_testreg_highlight(vdp_context *context, int32_t col, uint8_t *dst, uint8_t *debug_dst, uint8_t *buf_a, int plane_a_off, int plane_a_mask, int plane_b_off, uint8_t output_disabled, uint8_t test_layer)
 {
 	int start = 0;
 	uint8_t *sprite_buf = context->linebuf + col * 8;
@@ -1528,7 +1542,7 @@
 				}
 				break;
 			case 2:
-				pixel &= context->tmp_buf_a[(plane_a_off + i) & SCROLL_BUFFER_MASK];
+				pixel &= buf_a[(plane_a_off + i) & plane_a_mask];
 				if (pixel) {
 					src = DBG_SRC_A | DBG_SHADOW;
 				}
@@ -1552,7 +1566,7 @@
 	for (int i = start; i < 16; ++plane_a_off, ++plane_b_off, ++sprite_buf, ++i)
 	{
 		uint8_t sprite, plane_a, plane_b;
-		plane_a = context->tmp_buf_a[plane_a_off & SCROLL_BUFFER_MASK];
+		plane_a = buf_a[plane_a_off & plane_a_mask];
 		plane_b = context->tmp_buf_b[plane_b_off & SCROLL_BUFFER_MASK];
 		sprite = *sprite_buf;
 		sh_pixel pixel = composite_highlight(context, debug_dst, sprite, plane_a, plane_b, 0x3F);
@@ -1626,27 +1640,40 @@
 		
 		
 		uint8_t a_src, src;
+		uint8_t *buf_a;
+		int plane_a_mask;
 		if (context->flags & FLAG_WINDOW) {
 			plane_a_off = context->buf_a_off;
+			buf_a = context->tmp_buf_a;
 			a_src = DBG_SRC_W;
+			plane_a_mask = SCROLL_BUFFER_MASK;
 		} else {
-			plane_a_off = context->buf_a_off - context->hscroll_a_fine;
+			if (context->flags & FLAG_WINDOW_EDGE) {
+				buf_a = context->tmp_buf_a + context->buf_a_off;
+				plane_a_mask = 15;
+				plane_a_off = -context->hscroll_a_fine;
+			} else {
+				plane_a_off = context->buf_a_off - context->hscroll_a_fine;
+				plane_a_mask = SCROLL_BUFFER_MASK;
+				buf_a = context->tmp_buf_a;
+			}
 			a_src = DBG_SRC_A;
 		}
+		plane_a_off &= plane_a_mask;
 		plane_b_off = context->buf_b_off - context->hscroll_b_fine;
 		//printf("A | tmp_buf offset: %d\n", 8 - (context->hscroll_a & 0x7));
 
 		if (context->regs[REG_MODE_4] & BIT_HILIGHT) {
 			if (output_disabled || test_layer) {
-				render_testreg_highlight(context, col, dst, debug_dst, plane_a_off, plane_b_off, output_disabled, test_layer);
+				render_testreg_highlight(context, col, dst, debug_dst, buf_a, plane_a_off, plane_a_mask, plane_b_off, output_disabled, test_layer);
 			} else {
-				render_highlight(context, col, dst, debug_dst, plane_a_off, plane_b_off);
+				render_highlight(context, col, dst, debug_dst, buf_a, plane_a_off, plane_a_mask, plane_b_off);
 			}
 		} else {
 			if (output_disabled || test_layer) {
-				render_testreg(context, col, dst, debug_dst, plane_a_off, plane_b_off, output_disabled, test_layer);
+				render_testreg(context, col, dst, debug_dst, buf_a, plane_a_off, plane_a_mask, plane_b_off, output_disabled, test_layer);
 			} else {
-				render_normal(context, col, dst, debug_dst, plane_a_off, plane_b_off);
+				render_normal(context, col, dst, debug_dst, buf_a, plane_a_off, plane_a_mask, plane_b_off);
 			}
 		}
 		dst += 16;
@@ -3368,6 +3395,8 @@
 	//technically the second hcounter check should be different for H40, but this is probably close enough for now
 	if (context->state == ACTIVE && context->vcounter == context->inactive_start && (context->hslot >= (is_h40 ? 167 : 135) || context->hslot < 133)) {
 		context->state = INACTIVE;
+		context->cur_slot = MAX_SPRITES_LINE-1;
+		context->sprite_x_offset = 0;
 	}
 }
 
@@ -3750,13 +3779,15 @@
 		}
 	} else {
 		uint8_t mode_5 = context->regs[REG_MODE_2] & BIT_MODE_5;
-		//contrary to what's in Charles MacDonald's doc, it seems top 2 address bits are cleared
-		//needed for the Mona in 344 Bytes demo
 		context->address_latch = (context->address_latch & 0x1C000) | (value & 0x3FFF);
 		context->cd_latch = (context->cd_latch & 0x3C) | (value >> 14);
 		if ((value & 0xC000) == 0x8000) {
 			//Register write
 			uint8_t reg = (value >> 8) & 0x1F;
+			// The fact that this is needed seems to pour some cold water on my theory
+			// about how the address latch actually works. Needs more search to definitively confirm
+			context->address = (context->address & 0x1C000) | (value & 0x3FFF);
+			context->cd = (context->cd & 0x3C) | (value >> 14);
 			if (reg < (mode_5 ? VDP_REGS : 0xB)) {
 				//printf("register %d set to %X\n", reg, value & 0xFF);
 				if (reg == REG_MODE_1 && (value & BIT_HVC_LATCH) && !(context->regs[reg] & BIT_HVC_LATCH)) {
@@ -3780,6 +3811,43 @@
 				if (reg == REG_MODE_1 || reg == REG_MODE_2 || reg == REG_MODE_4) {
 					update_video_params(context);
 				}
+			} else if (reg == REG_KMOD_CTRL) {
+				if (!(value & 0xFF)) {
+					context->system->enter_debugger = 1;
+				}
+			} else if (reg == REG_KMOD_MSG) {
+				char c = value;
+				if (c) {
+					context->kmod_buffer_length++;
+					if ((context->kmod_buffer_length + 1) > context->kmod_buffer_storage) {
+						context->kmod_buffer_storage = context->kmod_buffer_length ? 128 : context->kmod_buffer_length * 2;
+						context->kmod_msg_buffer = realloc(context->kmod_msg_buffer, context->kmod_buffer_storage);
+					}
+					context->kmod_msg_buffer[context->kmod_buffer_length - 1] = c;
+				} else if (context->kmod_buffer_length) {
+					context->kmod_msg_buffer[context->kmod_buffer_length] = 0;
+					if (is_stdout_enabled()) {
+						init_terminal();
+						printf("KDEBUG MESSAGE: %s\n", context->kmod_msg_buffer);
+					} else {
+						// GDB remote debugging is enabled, use stderr instead
+						fprintf(stderr, "KDEBUG MESSAGE: %s\n", context->kmod_msg_buffer);
+					}
+					context->kmod_buffer_length = 0;
+				}
+			} else if (reg == REG_KMOD_TIMER) {
+				if (!(value & 0x80)) {
+					if (is_stdout_enabled()) {
+						init_terminal();
+						printf("KDEBUG TIMER: %d\n", (context->cycles - context->timer_start_cycle) / 7);
+					} else {
+						// GDB remote debugging is enabled, use stderr instead
+						fprintf(stderr, "KDEBUG TIMER: %d\n", (context->cycles - context->timer_start_cycle) / 7);
+					}
+				}
+				if (value & 0xC0) {
+					context->timer_start_cycle = context->cycles;
+				}
 			}
 		} else if (mode_5) {
 			context->flags |= FLAG_PENDING;
@@ -3787,6 +3855,7 @@
 			//context->flags &= ~FLAG_READ_FETCHED;
 			//context->flags2 &= ~FLAG2_READ_PENDING;
 		} else {
+			clear_pending(context);
 			context->flags &= ~FLAG_READ_FETCHED;
 			context->flags2 &= ~FLAG2_READ_PENDING;
 		}
@@ -3950,6 +4019,20 @@
 	}
 	if (context->cd & 1) {
 		warning("Read from VDP data port while writes are configured, CPU is now frozen. VDP Address: %X, CD: %X\n", context->address, context->cd);
+		context->system->enter_debugger = 1;
+		return context->prefetch;
+	}
+	switch (context->cd)
+	{
+	case VRAM_READ:
+	case VSRAM_READ:
+	case CRAM_READ:
+	case VRAM_READ8:
+		break;
+	default:
+		warning("Read from VDP data port with invalid source, CPU is now frozen. VDP Address: %X, CD: %X\n", context->address, context->cd);
+		context->system->enter_debugger = 1;
+		return context->prefetch;
 	}
 	while (!(context->flags & FLAG_READ_FETCHED)) {
 		vdp_run_context_full(context, context->cycles + ((context->regs[REG_MODE_4] & BIT_H40) ? 16 : 20));
--- a/vdp.h	Sun May 10 00:16:00 2020 -0700
+++ b/vdp.h	Thu Aug 05 09:29:33 2021 -0700
@@ -54,7 +54,7 @@
 #define FLAG_PENDING       0x10
 #define FLAG_READ_FETCHED  0x20
 #define FLAG_DMA_RUN       0x40
-#define FLAG_DMA_PROG      0x80
+#define FLAG_WINDOW_EDGE   0x80
 
 #define FLAG2_VINT_PENDING   0x01
 #define FLAG2_HINT_PENDING   0x02
@@ -91,8 +91,11 @@
 	REG_DMALEN_H,
 	REG_DMASRC_L,
 	REG_DMASRC_M,
-	REG_DMASRC_H
-} vdp_regs;
+	REG_DMASRC_H,
+	REG_KMOD_CTRL=29,
+	REG_KMOD_MSG,
+	REG_KMOD_TIMER
+};
 
 //Mode reg 1
 #define BIT_VSCRL_LOCK 0x80
@@ -115,7 +118,7 @@
 #define BIT_SPRITE_SZ  0x02
 
 //Mode reg 3
-#define BIT_EINT_EN    0x10
+#define BIT_EINT_EN    0x08
 #define BIT_VSCROLL    0x04
 
 //Mode reg 4
@@ -168,6 +171,10 @@
 	uint32_t       *fb;
 	uint8_t        *done_composite;
 	uint32_t       *debug_fbs[VDP_NUM_DEBUG_TYPES];
+	char           *kmod_msg_buffer;
+	uint32_t       kmod_buffer_storage;
+	uint32_t       kmod_buffer_length;
+	uint32_t       timer_start_cycle;
 	uint32_t       output_pitch;
 	uint32_t       debug_fb_pitch[VDP_NUM_DEBUG_TYPES];
 	fifo_entry     fifo[FIFO_SIZE];
--- a/vgm.c	Sun May 10 00:16:00 2020 -0700
+++ b/vgm.c	Thu Aug 05 09:29:33 2021 -0700
@@ -60,22 +60,33 @@
 	}
 }
 
+#include "util.h"
 static void add_wait(vgm_writer *writer, uint32_t cycle)
 {
-	uint64_t delta = cycle - writer->last_cycle;
-	delta *= (uint64_t)44100;
-	delta /= (uint64_t)writer->master_clock;
+	if (cycle < writer->last_cycle) {
+		//This can happen when a YM-2612 write happens immediately after a PSG write
+		//due to the relatively low granularity of the PSG's internal clock
+		//given that VGM only has a granularity of 44.1 kHz ignoring this is harmless
+		return;
+	}
+	uint64_t last_sample = (uint64_t)writer->last_cycle * (uint64_t)44100;
+	last_sample /= (uint64_t)writer->master_clock;
+	uint64_t sample = ((uint64_t)cycle + (uint64_t)writer->extra_delta) * (uint64_t)44100;
+	sample /= (uint64_t)writer->master_clock;
+	uint32_t delta = sample - last_sample;
 	
-	uint32_t mclks_per_sample = writer->master_clock / 44100;
-	writer->last_cycle += delta * mclks_per_sample; 
+	writer->last_cycle = cycle;
+	writer->extra_delta = 0;
 	writer->header.num_samples += delta;
 	wait_commands(writer, delta);
 }
 
+static uint8_t last_cmd;
 void vgm_sn76489_write(vgm_writer *writer, uint32_t cycle, uint8_t value)
 {
 	add_wait(writer, cycle);
 	uint8_t cmd[2] = {CMD_PSG, value};
+	last_cmd = CMD_PSG;
 	fwrite(cmd, 1, sizeof(cmd), writer->f);
 }
 
@@ -88,6 +99,7 @@
 {
 	add_wait(writer, cycle);
 	uint8_t cmd[3] = {CMD_YM2612_0, reg, value};
+	last_cmd = CMD_YM2612_0;
 	fwrite(cmd, 1, sizeof(cmd), writer->f);
 }
 
@@ -95,12 +107,14 @@
 {
 	add_wait(writer, cycle);
 	uint8_t cmd[3] = {CMD_YM2612_1, reg, value};
+	last_cmd = CMD_YM2612_1;
 	fwrite(cmd, 1, sizeof(cmd), writer->f);
 }
 
 void vgm_adjust_cycles(vgm_writer *writer, uint32_t deduction)
 {
 	if (deduction > writer->last_cycle) {
+		writer->extra_delta += deduction - writer->last_cycle;
 		writer->last_cycle = 0;
 	} else {
 		writer->last_cycle -= deduction;
@@ -109,6 +123,8 @@
 
 void vgm_close(vgm_writer *writer)
 {
+	uint8_t cmd = 0x66;
+	fwrite(&cmd, 1, sizeof(cmd), writer->f);
 	writer->header.eof_offset = ftell(writer->f) - offsetof(vgm_header, eof_offset);
 	fseek(writer->f, SEEK_SET, 0);
 	fwrite(&writer->header, sizeof(writer->header), 1, writer->f);
--- a/vgm.h	Sun May 10 00:16:00 2020 -0700
+++ b/vgm.h	Thu Aug 05 09:29:33 2021 -0700
@@ -79,6 +79,7 @@
 	FILE       *f;
 	uint32_t   master_clock;
 	uint32_t   last_cycle;
+	uint32_t   extra_delta;
 } vgm_writer;
 
 vgm_writer *vgm_write_open(char *filename, uint32_t rate, uint32_t clock, uint32_t cycle);
--- a/vgmplay.c	Sun May 10 00:16:00 2020 -0700
+++ b/vgmplay.c	Thu Aug 05 09:29:33 2021 -0700
@@ -25,6 +25,10 @@
 
 system_header *current_system;
 
+void system_request_exit(system_header *system, uint8_t force_release)
+{
+}
+
 void handle_keydown(int keycode)
 {
 }
--- a/ym2612.c	Sun May 10 00:16:00 2020 -0700
+++ b/ym2612.c	Thu Aug 05 09:29:33 2021 -0700
@@ -179,6 +179,11 @@
 	for (int i = 0; i < NUM_CHANNELS; i++) {
 		context->channels[i].lr = 0xC0;
 		context->channels[i].logfile = savedlogs[i];
+		if (i < 3) {
+			context->part1_regs[REG_LR_AMS_PMS - YM_PART1_START + i] = 0xC0;
+		} else {
+			context->part2_regs[REG_LR_AMS_PMS - YM_PART2_START + i - 3] = 0xC0;
+		}
 	}
 	context->write_cycle = CYCLE_NEVER;
 	for (int i = 0; i < NUM_OPERATORS; i++) {
@@ -402,6 +407,9 @@
 				context->timer_b = context->timer_b_load;
 			}
 		}
+	} else if (context->timer_control & BIT_TIMERB_LOAD) {
+		context->timer_control &= ~BIT_TIMERB_LOAD;
+		context->timer_b = context->timer_b_load;
 	}
 	context->sub_timer_b += 0x10;
 	//Update LFO