view io.c @ 2271:3ef80963c2a7

Fix stamp address mask and add WIP CD graphics debug view
author Michael Pavone <pavone@retrodev.com>
date Thu, 29 Dec 2022 15:47:19 -0800
parents 0a107b2d5837
children 1978bd770023
line wrap: on
line source

/*
 Copyright 2013 Michael Pavone
 This file is part of BlastEm.
 BlastEm is free software distributed under the terms of the GNU General Public License version 3 or greater. See COPYING for full license text.
*/
#ifndef _WIN32
#include <unistd.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#endif
#include <string.h>
#include <stdlib.h>

#include "serialize.h"
#include "io.h"
#include "blastem.h"
#include "render.h"
#include "util.h"
#include "bindings.h"

#define CYCLE_NEVER 0xFFFFFFFF
#define MIN_POLL_INTERVAL 6840

const char * device_type_names[] = {
	"None",
	"SMS gamepad",
	"3-button gamepad",
	"6-button gamepad",
	"Mega Mouse",
	"Saturn Keyboard",
	"XBAND Keyboard",
	"Menacer",
	"Justifier",
	"Sega Multi-tap",
	"EA 4-way Play",
	"EA 4-way Play",
	"Sega Parallel Transfer Board",
	"Generic Device",
	"Generic Serial",
	"Heartbeat Personal Trainer"
};

#define GAMEPAD_TH0 0
#define GAMEPAD_TH1 1
#define GAMEPAD_EXTRA 2
#define GAMEPAD_NONE 0xF

#define IO_TH0 0
#define IO_TH1 1
#define IO_STATE 2

enum {
	IO_WRITE_PENDING,
	IO_WRITTEN,
	IO_READ_PENDING,
	IO_READ
};

enum {
	HBPT_NEED_INIT,
	HBPT_IDLE,
	HBPT_CMD_PAYLOAD,
	HBPT_REPLY
};

#define EA_PASSTHRU_MODE 0xFF

typedef struct {
	uint8_t states[2], value;
} gp_button_def;


static gp_button_def button_defs[NUM_GAMEPAD_BUTTONS] = {
	[DPAD_UP] = {.states = {GAMEPAD_TH0, GAMEPAD_TH1}, .value = 0x1},
	[DPAD_DOWN] = {.states = {GAMEPAD_TH0, GAMEPAD_TH1}, .value = 0x2},
	[DPAD_LEFT] = {.states = {GAMEPAD_TH1, GAMEPAD_NONE}, .value = 0x4},
	[DPAD_RIGHT] = {.states = {GAMEPAD_TH1, GAMEPAD_NONE}, .value = 0x8},
	[BUTTON_A] = {.states = {GAMEPAD_TH0, GAMEPAD_NONE}, .value = 0x10},
	[BUTTON_B] = {.states = {GAMEPAD_TH1, GAMEPAD_NONE}, .value = 0x10},
	[BUTTON_C] = {.states = {GAMEPAD_TH1, GAMEPAD_NONE}, .value = 0x20},
	[BUTTON_START] = {.states = {GAMEPAD_TH0, GAMEPAD_NONE}, .value = 0x20},
	[BUTTON_X] = {.states = {GAMEPAD_EXTRA, GAMEPAD_NONE}, .value = 0x4},
	[BUTTON_Y] = {.states = {GAMEPAD_EXTRA, GAMEPAD_NONE}, .value = 0x2},
	[BUTTON_Z] = {.states = {GAMEPAD_EXTRA, GAMEPAD_NONE}, .value = 0x1},
	[BUTTON_MODE] = {.states = {GAMEPAD_EXTRA, GAMEPAD_NONE}, .value = 0x8},
};

static io_port *find_gamepad(sega_io *io, uint8_t gamepad_num)
{
	for (int i = 0; i < 3; i++)
	{
		io_port *port = io->ports + i;
		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;
		}
		if (port->device_type == IO_SEGA_MULTI || port->device_type == IO_EA_MULTI_A) {
			for (int j = 0; j < 4; j++)
			{
				io_port *tap_port = port->device.multitap.ports + j;
				if (tap_port->device_type < IO_MOUSE && tap_port->device.pad.gamepad_num == gamepad_num) {
					return tap_port;
				}
			}
		}
	}
	return NULL;
}

static io_port *find_mouse(sega_io *io, uint8_t mouse_num)
{
	for (int i = 0; i < 3; i++)
	{
		io_port *port = io->ports + i;
		if (port->device_type == IO_MOUSE && port->device.mouse.mouse_num == mouse_num) {
			return port;
		}
		if (port->device_type == IO_SEGA_MULTI) {
			for (int j = 0; j < 4; j++)
			{
				io_port *tap_port = port->device.multitap.ports + j;
				if (tap_port->device_type == IO_MOUSE && tap_port->device.mouse.mouse_num == mouse_num) {
					return tap_port;
				}
			}
		}
	}
	return NULL;
}

static io_port *find_keyboard(sega_io *io)
{
	for (int i = 0; i < 3; i++)
	{
		io_port *port = io->ports + i;
		if (port->device_type == IO_SATURN_KEYBOARD || port->device_type == IO_XBAND_KEYBOARD) {
			return port;
		}
	}
	return NULL;
}

void io_port_gamepad_down(io_port *port, uint8_t button)
{
	gp_button_def *def = button_defs + button;
	port->input[def->states[0]] |= def->value;
	if (def->states[1] != GAMEPAD_NONE) {
		port->input[def->states[1]] |= def->value;
	}
}

void io_port_gamepad_up(io_port *port, uint8_t button)
{
	gp_button_def *def = button_defs + button;
	port->input[def->states[0]] &= ~def->value;
	if (def->states[1] != GAMEPAD_NONE) {
		port->input[def->states[1]] &= ~def->value;
	}
}

void io_gamepad_down(sega_io *io, uint8_t gamepad_num, uint8_t button)
{
	io_port *port = find_gamepad(io, gamepad_num);
	if (port) {
		io_port_gamepad_down(port, button);
	}
}

void io_gamepad_up(sega_io *io, uint8_t gamepad_num, uint8_t button)
{
	io_port *port = find_gamepad(io, gamepad_num);
	if (port) {
		io_port_gamepad_up(port, button);
	}
}

void io_mouse_down(sega_io *io, uint8_t mouse_num, uint8_t button)
{
	io_port *port = find_mouse(io, mouse_num);
	if (port) {
		port->input[0] |= button;
	}
}

void io_mouse_up(sega_io *io, uint8_t mouse_num, uint8_t button)
{
	io_port *port = find_mouse(io, mouse_num);
	if (port) {
		port->input[0] &= ~button;
	}
}

void io_mouse_motion_absolute(sega_io *io, uint8_t mouse_num, uint16_t x, uint16_t y)
{
	io_port *port = find_mouse(io, mouse_num);
	if (port) {
		port->device.mouse.cur_x = x;
		port->device.mouse.cur_y = y;
	}
}

void io_mouse_motion_relative(sega_io *io, uint8_t mouse_num, int32_t x, int32_t y)
{
	io_port *port = find_mouse(io, mouse_num);
	if (port) {
		port->device.mouse.cur_x += x;
		port->device.mouse.cur_y += y;
	}
}

void store_key_event(io_port *keyboard_port, uint16_t code)
{
	if (keyboard_port && keyboard_port->device.keyboard.write_pos != keyboard_port->device.keyboard.read_pos) {
		//there's room in the buffer, record this event
		keyboard_port->device.keyboard.events[keyboard_port->device.keyboard.write_pos] = code;
		if (keyboard_port->device.keyboard.read_pos == 0xFF) {
			//ring buffer was empty, update read_pos to indicate there is now data
			keyboard_port->device.keyboard.read_pos = keyboard_port->device.keyboard.write_pos;
		}
		keyboard_port->device.keyboard.write_pos = (keyboard_port->device.keyboard.write_pos + 1) & 7;
	}
}

void io_keyboard_down(sega_io *io, uint8_t scancode)
{
	store_key_event(find_keyboard(io), scancode);
}

void io_keyboard_up(sega_io *io, uint8_t scancode)
{
	store_key_event(find_keyboard(io), 0xF000 | scancode);
}

uint8_t io_has_keyboard(sega_io *io)
{
	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)
	{
		return;
	}
	io_port *old_ports = NULL;
	if (port->device_type == IO_SEGA_MULTI || port->device_type == IO_EA_MULTI_A) {
		old_ports = port->device.multitap.ports;
	}

	const int gamepad_len = strlen("gamepad");
	if (startswith(device_type, "gamepad"))
	{
		if (
			(device_type[gamepad_len] != '3' && device_type[gamepad_len] != '6' && device_type[gamepad_len] != '2')
			|| device_type[gamepad_len+1] != '.' || device_type[gamepad_len+2] < '1'
			|| device_type[gamepad_len+2] > '8' || device_type[gamepad_len+3] != 0
		) {
			warning("%s is not a valid gamepad type\n", device_type);
		} else if (device_type[gamepad_len] == '3') {
			port->device_type = IO_GAMEPAD3;
		} else if (device_type[gamepad_len] == '2') {
			port->device_type = IO_GAMEPAD2;
		} else {
			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;
			port->device.mouse.mouse_num = device_type[strlen("mouse")+1] - '0';
			port->device.mouse.last_read_x = 0;
			port->device.mouse.last_read_y = 0;
			port->device.mouse.cur_x = 0;
			port->device.mouse.cur_y = 0;
			port->device.mouse.latched_x = 0;
			port->device.mouse.latched_y = 0;
			port->device.mouse.ready_cycle = CYCLE_NEVER;
			port->device.mouse.tr_counter = 0;
		}
	} else if(!strcmp(device_type, "saturn keyboard")) {
		if (port->device_type != IO_SATURN_KEYBOARD) {
			port->device_type = IO_SATURN_KEYBOARD;
			port->device.keyboard.read_pos = 0xFF;
			port->device.keyboard.write_pos = 0;
		}
	} else if(!strcmp(device_type, "xband keyboard")) {
		if (port->device_type != IO_XBAND_KEYBOARD) {
			port->device_type = IO_XBAND_KEYBOARD;
			port->device.keyboard.read_pos = 0xFF;
			port->device.keyboard.write_pos = 0;
		}
	} else if(!strcmp(device_type, "sega_parallel")) {
		if (port->device_type != IO_SEGA_PARALLEL) {
			port->device_type = IO_SEGA_PARALLEL;
			port->device.stream.data_fd = -1;
			port->device.stream.listen_fd = -1;
		}
	} else if(!strcmp(device_type, "generic")) {
		if (port->device_type != IO_GENERIC) {
			port->device_type = IO_GENERIC;
			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;
		}
	} else if(startswith(device_type, "sega_multitap.")) {
		if (port->device_type != IO_SEGA_MULTI) {
			port->device_type = IO_SEGA_MULTI;
			port->device.multitap.ports = old_ports ? old_ports : calloc(4, sizeof(io_port));
			port->device.multitap.tap_num = device_type[strlen("sega_multitap.")] - '0';
			if (!old_ports) {
				port->device.multitap.tr_counter = 0;
				port->device.multitap.ready_cycle = CYCLE_NEVER;
				port->input[0] = 0x13;
			}
			old_ports = NULL;
		}
	} else if(!strcmp(device_type, "ea_multitap_port_a")) {
		if (port->device_type != IO_EA_MULTI_A) {
			port->device_type = IO_EA_MULTI_A;
			port->device.multitap.ports = old_ports ? old_ports : calloc(4, sizeof(io_port));
			port->device.multitap.tap_num = 1;
			port->device.multitap.cur_port = EA_PASSTHRU_MODE;
			old_ports = NULL;
		}
	} else if(!strcmp(device_type, "ea_multitap_port_b")) {
		port->device_type = IO_EA_MULTI_B;
		port->device.multitap.ports = NULL;
		port->device.multitap.tap_num = 1;
	}
	free(old_ports);
}

char * io_name(int i)
{
	switch (i)
	{
	case 0:
		return "1";
	case 1:
		return "2";
	case 2:
		return "EXT";
	default:
		return "invalid";
	}
}

static char * sockfile_name;
static void cleanup_sockfile()
{
	unlink(sockfile_name);
}

void setup_io_devices(tern_node * config, rom_info *rom, sega_io *io)
{
	io_port * ports = io->ports;
	tern_node *io_nodes = tern_find_path(config, "io\0devices\0", TVAL_NODE).ptrval;
	char * io_1 = rom->port1_override ? rom->port1_override : tern_find_ptr_default(io_nodes, "1", "gamepad6.1");
	char * io_2 = rom->port2_override ? rom->port2_override : tern_find_ptr_default(io_nodes, "2", "gamepad6.2");
	char * io_ext = rom->ext_override ? rom->ext_override : tern_find_ptr(io_nodes, "ext");

	process_device(io_1, ports);
	process_device(io_2, ports+1);
	process_device(io_ext, ports+2);

	uint8_t mouse_mode;
	if (ports[0].device_type == IO_MOUSE || ports[1].device_type == IO_MOUSE || ports[2].device_type == IO_MOUSE) {
		if (render_fullscreen()) {
				mouse_mode = MOUSE_RELATIVE;
		} else {
			if (rom->mouse_mode && !strcmp(rom->mouse_mode, "absolute")) {
				mouse_mode = MOUSE_ABSOLUTE;
			} else {
				mouse_mode = MOUSE_CAPTURE;
			}
		}
	} else {
		mouse_mode = MOUSE_NONE;
	}
	bindings_set_mouse_mode(mouse_mode);

	for (int i = 0; i < 3; i++)
	{
#ifndef _WIN32
		if (ports[i].device_type == IO_SEGA_PARALLEL && ports[i].device.stream.data_fd == -1)
		{
			char *pipe_name = tern_find_path(config, "io\0parallel_pipe\0", TVAL_PTR).ptrval;
			if (!pipe_name)
			{
				warning("IO port %s is configured to use the sega parallel board, but no paralell_pipe is set!\n", io_name(i));
				ports[i].device_type = IO_NONE;
			} else {
				debug_message("IO port: %s connected to device '%s' with pipe name: %s\n", io_name(i), device_type_names[ports[i].device_type], pipe_name);
				if (!strcmp("stdin", pipe_name))
				{
					ports[i].device.stream.data_fd = STDIN_FILENO;
				} else {
					if (mkfifo(pipe_name, 0666) && errno != EEXIST)
					{
						warning("Failed to create fifo %s for Sega parallel board emulation: %d %s\n", pipe_name, errno, strerror(errno));
						ports[i].device_type = IO_NONE;
					} else {
						ports[i].device.stream.data_fd = open(pipe_name, O_NONBLOCK | O_RDONLY);
						if (ports[i].device.stream.data_fd == -1)
						{
							warning("Failed to open fifo %s for Sega parallel board emulation: %d %s\n", pipe_name, errno, strerror(errno));
							ports[i].device_type = IO_NONE;
						}
					}
				}
			}
		} 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)
			{
				warning("IO port %s is configured to use generic IO, but no socket is set!\n", io_name(i));
				ports[i].device_type = IO_NONE;
			} else {
				debug_message("IO port: %s connected to device '%s' with socket name: %s\n", io_name(i), device_type_names[ports[i].device_type], sock_name);
				ports[i].device.stream.data_fd = -1;
				ports[i].device.stream.listen_fd = socket(AF_UNIX, SOCK_STREAM, 0);
				size_t pathlen = strlen(sock_name);
				size_t addrlen = offsetof(struct sockaddr_un, sun_path) + pathlen + 1;
				struct sockaddr_un *saddr = malloc(addrlen);
				saddr->sun_family = AF_UNIX;
				memcpy(saddr->sun_path, sock_name, pathlen+1);
				if (bind(ports[i].device.stream.listen_fd, (struct sockaddr *)saddr, addrlen))
				{
					warning("Failed to bind socket for IO Port %s to path %s: %d %s\n", io_name(i), sock_name, errno, strerror(errno));
					goto cleanup_sock;
				}
				if (listen(ports[i].device.stream.listen_fd, 1))
				{
					warning("Failed to listen on socket for IO Port %s: %d %s\n", io_name(i), errno, strerror(errno));
					goto cleanup_sockfile;
				}
				sockfile_name = sock_name;
				atexit(cleanup_sockfile);
				continue;
cleanup_sockfile:
				unlink(sock_name);
cleanup_sock:
				close(ports[i].device.stream.listen_fd);
				ports[i].device_type = IO_NONE;
			}
		} else
#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 if (ports[i].device_type == IO_SEGA_MULTI) {
			char path[] = "io\0sega_multitap.1\0";
			path[17] = '0' + ports[i].device.multitap.tap_num;
			tern_node *port_defs = tern_find_path(config, path, TVAL_NODE).ptrval;
			debug_message("IO port %s connected to Sega multitap %d\n", io_name(i), ports[i].device.multitap.tap_num);
			for (int j = 0; j < 4; j++)
			{
				char port_num[] = {'1' + j, 0, 0};
				char *dev_type = tern_find_ptr(port_defs, port_num);
				process_device(dev_type, ports[i].device.multitap.ports + j);
				debug_message("\tTap port %d connected to device '%s'\n", j + 1, device_type_names[ports[i].device.multitap.ports[j].device_type]);
				if (ports[i].control & ports[i].output & 0x40) {
					io_control_write(ports[i].device.multitap.ports + j, 0x40, 0);
					io_data_write(ports[i].device.multitap.ports + j, 0x40, 0);
				}
			}
		} else if (ports[i].device_type == IO_EA_MULTI_A) {
			char path[] = "io\0ea_multitap\0";
			tern_node *port_defs = tern_find_path(config, path, TVAL_NODE).ptrval;
			debug_message("IO port %s connected to EA 4-way Play A-side\n", io_name(i));
			for (int j = 0; j < 4; j++)
			{
				char port_num[] = {'1' + j, 0, 0};
				char *dev_type = tern_find_ptr(port_defs, port_num);
				process_device(dev_type, ports[i].device.multitap.ports + j);
				debug_message("\tTap port %d connected to device '%s'\n", j + 1, device_type_names[ports[i].device.multitap.ports[j].device_type]);
				io_control_write(ports[i].device.multitap.ports + j, 0x40, 0);
				io_data_write(ports[i].device.multitap.ports + j, 0, 0);
			}
		} else if (ports[i].device_type == IO_EA_MULTI_B) {
			debug_message("IO port %s connected to EA 4-way Play B-side\n", io_name(i));
			for (int j = 0; j < 3; j++)
			{
				if (ports[j].device_type == IO_EA_MULTI_A) {
					ports[i].device.multitap.ports = ports + j;
					break;
				}
			}
		} else {
			debug_message("IO port %s connected to device '%s'\n", io_name(i), device_type_names[ports[i].device_type]);
		}
	}
}


#define TH 0x40
#define TR 0x20
#define TL 0x10
#define TH_TIMEOUT 56000
#define SLOW_RISE_DEVICE (30*7)
#define SLOW_RISE_INPUT (12*7)

static uint8_t get_output_value(io_port *port, uint32_t current_cycle, uint32_t slow_rise_delay)
{
	uint8_t output = (port->control | 0x80) & port->output;
	for (int i = 0; i < 8; i++)
	{
		if (!(port->control & 1 << i)) {
			if (port->slow_rise_start[i] != CYCLE_NEVER) {
				if (current_cycle - port->slow_rise_start[i] >= slow_rise_delay) {
					output |= 1 << i;
				}
			} else {
				output |= 1 << i;
			}
		}
	}
	return output;
}

void mouse_check_ready(io_port *port, uint32_t current_cycle)
{
	if (current_cycle >= port->device.mouse.ready_cycle) {
		port->device.mouse.tr_counter++;
		port->device.mouse.ready_cycle = CYCLE_NEVER;
		if (port->device.mouse.tr_counter == 3) {
			port->device.mouse.latched_x = port->device.mouse.cur_x;
			port->device.mouse.latched_y = port->device.mouse.cur_y;
			/* FIXME mouse mode owned by bindings now
			if (current_io->mouse_mode == MOUSE_ABSOLUTE) {
				//avoid overflow in absolute mode
				int deltax = port->device.mouse.latched_x - port->device.mouse.last_read_x;
				if (abs(deltax) > 255) {
					port->device.mouse.latched_x = port->device.mouse.last_read_x + (deltax > 0 ? 255 : -255);
				}
				int deltay = port->device.mouse.latched_y - port->device.mouse.last_read_y;
				if (abs(deltay) > 255) {
					port->device.mouse.latched_y = port->device.mouse.last_read_y + (deltay > 0 ? 255 : -255);
				}
			}*/
		}
	}
}

void multitap_check_ready(io_port *port, uint32_t current_cycle)
{
	if (current_cycle >= port->device.multitap.ready_cycle) {
		if (port->device.multitap.reset_state) {
			uint8_t output = get_output_value(port, current_cycle, SLOW_RISE_DEVICE);
			if (output & TR) {
				port->input[0] |= TL;
				port->device.multitap.reset_state = 0;
			} else {
				port->input[0] &= TR;
			}
			port->device.multitap.ready_cycle = CYCLE_NEVER;
			return;
		}
		port->device.multitap.tr_counter++;
		port->device.multitap.ready_cycle = CYCLE_NEVER;
		switch (port->device.multitap.tr_counter)
		{
		case 1:
			for (int i = 0; i < 4; i++)
			{
				uint8_t id = io_data_read(port->device.multitap.ports + i, current_cycle);
				io_data_write(port->device.multitap.ports + i, 0, current_cycle);
				uint8_t value = io_data_read(port->device.multitap.ports + i, current_cycle);
				uint8_t pad_data = (id & 0x3F) | (value << 2 & 0xC0);
				id = (id & 0xA) | (id << 1 & 0xA);
				id |= (value & 0x5) | (value >> 1 & 0x5);
				id = (id & 0x9) | (id << 1 & 0x4) | (id >> 1 & 0x2);
				if (id == 0xD || id == 0xC) {
					port->device.multitap.data[i] = pad_data;
					io_data_write(port->device.multitap.ports + i, 0x40, current_cycle);
				} else if (id == 0x3) {
					//set TR to output for mouse
					io_control_write(port->device.multitap.ports + i, 0x60, current_cycle);
				}
				port->device.multitap.device_ids[i] = id;
			}
			port->input[0] = 0;
			break;
		case 2:
			for (int i = 0; i < 4; i++)
			{
				if (port->device.multitap.device_ids[i] == 0xC || port->device.multitap.device_ids[i] == 0xD) {
					io_data_write(port->device.multitap.ports + i, 0, current_cycle);
				} else if (port->device.multitap.device_ids[i] == 0x3) {
					//TODO: Fix delays so mouse has enough time to respond
					io_data_write(port->device.multitap.ports + i, 0x20, current_cycle);
				}
			}
			port->input[0] = 0x10;
			break;
		case 3:
			for (int i = 0; i < 4; i++)
			{
				if (port->device.multitap.device_ids[i] == 0xC || port->device.multitap.device_ids[i] == 0xD) {
					io_data_write(port->device.multitap.ports + i, 0x40, current_cycle);
					io_data_write(port->device.multitap.ports + i, 0, current_cycle);
					uint8_t value = io_data_read(port->device.multitap.ports + i, current_cycle);
					if (value & 0xF) {
						//3 button
						port->device.multitap.device_ids[i] = 0;
					} else {
						port->device.multitap.device_ids[i] = 1;
					}
				} else if (port->device.multitap.device_ids[i] == 0xC) {
					//TODO: Fix delays so mouse has enough time to respond
					io_data_write(port->device.multitap.ports + i, 0, current_cycle);
					port->device.multitap.device_ids[i] = 2;
				} else {
					port->device.multitap.device_ids[i] = 0xF;
				}
			}
			port->input[0] = port->device.multitap.device_ids[0];
			break;
		case 4:
			port->input[0] = 0x10 | port->device.multitap.device_ids[1];
			break;
		case 5:
			port->input[0] = port->device.multitap.device_ids[2];
			break;
		case 6:
			port->input[0] = 0x10 | port->device.multitap.device_ids[3];
			port->device.multitap.cur_port = 0;
			port->device.multitap.port_start = 7;
			break;
		default: {
			port->input[0] = (port->input[0] & ~TL) | ((~port->input[0]) & TL);
			uint8_t tr_diff = port->device.multitap.tr_counter - port->device.multitap.port_start;
			for (;;)
			{
				if (port->device.multitap.cur_port > 3) {
					return;
				}
				switch (port->device.multitap.device_ids[port->device.multitap.cur_port])
				{
				case 0:
					if (tr_diff) {
						port->input[0] = (port->input[0] & 0xF0) | (port->device.multitap.data[port->device.multitap.cur_port] >> 4);
						port->device.multitap.cur_port++;
						port->device.multitap.port_start = port->device.multitap.tr_counter + 1;
					} else {
						port->input[0] = (port->input[0] & 0xF0) | (port->device.multitap.data[port->device.multitap.cur_port] & 0xF);
					}
					return;
				case 1:
					if (tr_diff == 2) {
						uint8_t value = io_data_read(port->device.multitap.ports + port->device.multitap.cur_port, current_cycle);
						port->input[0] = (port->input[0] & 0xF0) | (value & 0xF);
						//finish 6-button cycle
						io_data_write(port->device.multitap.ports + port->device.multitap.cur_port, 0, current_cycle);
						port->device.multitap.cur_port++;
						port->device.multitap.port_start = port->device.multitap.tr_counter + 1;
					} else if (tr_diff) {
						io_data_write(port->device.multitap.ports + port->device.multitap.cur_port, 0x40, current_cycle);
						port->input[0] = (port->input[0] & 0xF0) | (port->device.multitap.data[port->device.multitap.cur_port] >> 4);
					} else {
						port->input[0] = (port->input[0] & 0xF0) | (port->device.multitap.data[port->device.multitap.cur_port] & 0xF);
					}
					return;
				case 2: {
					io_port *dst_port = port->device.multitap.ports + port->device.multitap.cur_port;
					uint8_t value = io_data_read(dst_port, current_cycle);
					io_data_write(dst_port, (~dst_port->output) & TR, current_cycle);
					if (tr_diff == 5) {
						port->device.multitap.cur_port++;
						port->device.multitap.port_start = port->device.multitap.tr_counter + 1;
					}
					return;
				}
				default:
					port->device.multitap.cur_port++;
					port->device.multitap.port_start = port->device.multitap.tr_counter;
				}
			}
		}
		}
	}
}

void io_adjust_cycles(io_port * port, uint32_t current_cycle, uint32_t deduction)
{
	/*uint8_t control = pad->control | 0x80;
	uint8_t th = control & pad->output;
	if (pad->input[GAMEPAD_TH0] || pad->input[GAMEPAD_TH1]) {
		printf("adjust_cycles | control: %X, TH: %X, GAMEPAD_TH0: %X, GAMEPAD_TH1: %X, TH Counter: %d, Timeout: %d, Cycle: %d\n", control, th, pad->input[GAMEPAD_TH0], pad->input[GAMEPAD_TH1], pad->th_counter,pad->timeout_cycle, current_cycle);
	}*/
	if (port->device_type == IO_GAMEPAD6)
	{
		if (current_cycle >= port->device.pad.timeout_cycle)
		{
			port->device.pad.th_counter = 0;
		} else {
			port->device.pad.timeout_cycle -= deduction;
		}
	} else if (port->device_type == IO_MOUSE) {
		mouse_check_ready(port, current_cycle);
		if (port->device.mouse.ready_cycle != CYCLE_NEVER) {
			port->device.mouse.ready_cycle -= deduction;
		}
	} else if (port->device_type == IO_SEGA_MULTI) {
		multitap_check_ready(port, current_cycle);
		if (port->device.multitap.ready_cycle != CYCLE_NEVER) {
			port->device.multitap.ready_cycle -= deduction;
		}
		for (int i = 0; i < 4; i++)
		{
			io_adjust_cycles(port->device.multitap.ports + i, current_cycle, deduction);
		}
	}
	for (int i = 0; i < 8; i++)
	{
		if (port->slow_rise_start[i] != CYCLE_NEVER) {
			if (port->slow_rise_start[i] >= deduction) {
				port->slow_rise_start[i] -= deduction;
			} else {
				port->slow_rise_start[i] = CYCLE_NEVER;
			}
		}
	}
	if (port->transmit_end >= deduction) {
		port->transmit_end -= deduction;
	} else {
		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)
{
	if (port->device.stream.data_fd == -1)
	{
		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 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));
	if (numRead > 0)
	{
		port->input[IO_TH0] = (value & 0xF) | 0x10;
		port->input[IO_TH1] = (value >> 4) | 0x10;
	} else if(numRead == -1 && errno != EAGAIN && errno != EWOULDBLOCK) {
		warning("Error reading pipe for IO port: %d %s\n", errno, strerror(errno));
	}
}

static void service_socket(io_port *port)
{
	uint8_t buf[32];
	uint8_t blocking = 0;
	int numRead = 0;
	while (numRead <= 0)
	{
		numRead = recv(port->device.stream.data_fd, buf, sizeof(buf), 0);
		if (numRead > 0)
		{
			port->input[IO_TH0] = buf[numRead-1];
			if (port->input[IO_STATE] == IO_READ_PENDING)
			{
				port->input[IO_STATE] = IO_READ;
				if (blocking)
				{
					//pending read satisfied, back to non-blocking mode
					fcntl(port->device.stream.data_fd, F_SETFL, O_RDWR | O_NONBLOCK);
				}
			} else if (port->input[IO_STATE] == IO_WRITTEN) {
				port->input[IO_STATE] = IO_READ;
			}
		} else if (numRead == 0) {
			port->device.stream.data_fd = -1;
			wait_for_connection(port);
		} else if (errno != EAGAIN && errno != EWOULDBLOCK) {
			warning("Error reading from socket for IO port: %d %s\n", errno, strerror(errno));
			close(port->device.stream.data_fd);
			wait_for_connection(port);
		} else if (port->input[IO_STATE] == IO_READ_PENDING) {
			//clear the nonblocking flag so the next read will block
			if (!blocking)
			{
				fcntl(port->device.stream.data_fd, F_SETFL, O_RDWR);
				blocking = 1;
			}
		} else {
			//no new data, but that's ok
			break;
		}
	}

	if (port->input[IO_STATE] == IO_WRITE_PENDING)
	{
		uint8_t value = port->output & port->control;
		int written = 0;
		blocking = 0;
		while (written <= 0)
		{
			send(port->device.stream.data_fd, &value, sizeof(value), 0);
			if (written > 0)
			{
				port->input[IO_STATE] = IO_WRITTEN;
				if (blocking)
				{
					//pending write satisfied, back to non-blocking mode
					fcntl(port->device.stream.data_fd, F_SETFL, O_RDWR | O_NONBLOCK);
				}
			} else if (written == 0) {
				port->device.stream.data_fd = -1;
				wait_for_connection(port);
			} else if (errno != EAGAIN && errno != EWOULDBLOCK) {
				warning("Error writing to socket for IO port: %d %s\n", errno, strerror(errno));
				close(port->device.stream.data_fd);
				wait_for_connection(port);
			} else {
				//clear the nonblocking flag so the next write will block
				if (!blocking)
				{
					fcntl(port->device.stream.data_fd, F_SETFL, O_RDWR);
					blocking = 1;
				}
			}
		}
	}
}
#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 {
	KB_SETUP,
	KB_READ,
	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;
	if (changes) {
		for (int i = 0; i < 8; i++)
		{
			if (!(value & 1 << i) && !(port->output & 1 << i)) {
				//port switched from output to input and the output value was 0
				//since there is a weak pull-up on input pins, this will lead
				//to a slow rise from 0 to 1 if the pin isn't being externally driven
				port->slow_rise_start[i] = current_cycle;
			} else {
				port->slow_rise_start[i] = CYCLE_NEVER;
			}
		}
		port->control = value;
	}
}

void io_data_write(io_port * port, uint8_t value, uint32_t current_cycle)
{
	uint8_t old_output = get_output_value(port, current_cycle, SLOW_RISE_DEVICE);
	port->output = value;
	uint8_t output = get_output_value(port, current_cycle, SLOW_RISE_DEVICE);
	switch (port->device_type)
	{
	case IO_GAMEPAD6:
		//check if TH has changed
		if ((old_output & TH) ^ (output & TH)) {
			if (current_cycle >= port->device.pad.timeout_cycle) {
				port->device.pad.th_counter = 0;
			}
			if ((output & TH)) {
				port->device.pad.th_counter++;
			}
			port->device.pad.timeout_cycle = current_cycle + TH_TIMEOUT;
		}
		break;
	case IO_MOUSE:
		mouse_check_ready(port, current_cycle);
		if (output & TH) {
			//request is over or mouse is being reset
			if (port->device.mouse.tr_counter) {
				//request is over
				port->device.mouse.last_read_x = port->device.mouse.latched_x;
				port->device.mouse.last_read_y = port->device.mouse.latched_y;
			}
			port->device.mouse.tr_counter = 0;
			port->device.mouse.ready_cycle = CYCLE_NEVER;
		} else {
			if ((output & TR) != (old_output & TR)) {
				int delay_index = port->device.mouse.tr_counter >= sizeof(mouse_delays) ? sizeof(mouse_delays)-1 : port->device.mouse.tr_counter;
				port->device.mouse.ready_cycle = current_cycle + mouse_delays[delay_index];
			}
		}
		break;
	case IO_SATURN_KEYBOARD:
		if (output & TH) {
			//request is over
			if (port->device.keyboard.tr_counter >= 10 && port->device.keyboard.read_pos != 0xFF) {
				//remove scan code from buffer
				port->device.keyboard.read_pos++;
				port->device.keyboard.read_pos &= 7;
				if (port->device.keyboard.read_pos == port->device.keyboard.write_pos) {
					port->device.keyboard.read_pos = 0xFF;
				}
			}
			port->device.keyboard.tr_counter = 0;
		} else {
			if ((output & TR) != (old_output & TR)) {
				port->device.keyboard.tr_counter++;
			}
		}
		break;
	case IO_XBAND_KEYBOARD:
		if (output & TH) {
			//request is over
			if (
				port->device.keyboard.mode == KB_READ && port->device.keyboard.tr_counter > 6
				&& (port->device.keyboard.tr_counter & 1)
			) {
				if (port->device.keyboard.events[port->device.keyboard.read_pos] & 0xFF00) {
					port->device.keyboard.events[port->device.keyboard.read_pos] &= 0xFF;
				} else {
					port->device.keyboard.read_pos++;
					port->device.keyboard.read_pos &= 7;
					if (port->device.keyboard.read_pos == port->device.keyboard.write_pos) {
						port->device.keyboard.read_pos = 0xFF;
					}
				}
			}
			port->device.keyboard.tr_counter = 0;
			port->device.keyboard.mode = KB_SETUP;
		} else {
			if ((output & TR) != (old_output & TR)) {
				port->device.keyboard.tr_counter++;
				if (port->device.keyboard.tr_counter == 2) {
					port->device.keyboard.mode = (output & 0xF) ? KB_READ : KB_WRITE;
				} else if (port->device.keyboard.mode == KB_WRITE) {
					switch (port->device.keyboard.tr_counter)
					{
					case 3:
						//host writes 0b0001
						break;
					case 4:
						//host writes 0b0000
						break;
					case 5:
						//host writes 0b0000
						break;
					case 6:
						port->device.keyboard.cmd = output << 4;
						break;
					case 7:
						port->device.keyboard.cmd |= output & 0xF;
						//TODO: actually do something with the command
						break;
					}
				} else if (
					port->device.keyboard.mode == KB_READ && port->device.keyboard.tr_counter > 7
					&& !(port->device.keyboard.tr_counter & 1)
				) {
					
					if (port->device.keyboard.events[port->device.keyboard.read_pos] & 0xFF00) {
						port->device.keyboard.events[port->device.keyboard.read_pos] &= 0xFF;
					} else {
						port->device.keyboard.read_pos++;
						port->device.keyboard.read_pos &= 7;
						if (port->device.keyboard.read_pos == port->device.keyboard.write_pos) {
							port->device.keyboard.read_pos = 0xFF;
						}
					}
				}
			}
		}
		break;
	case IO_SEGA_MULTI:
		multitap_check_ready(port, current_cycle);
		if (output & TH) {
			//request is over
			port->device.multitap.tr_counter = 0;
			if ((output & TR) != (old_output & TR)) {
				port->device.multitap.ready_cycle = current_cycle + 16 * 7;
				port->device.multitap.reset_state = 1;
			} else if (!port->device.multitap.reset_state) {
				port->device.multitap.ready_cycle = CYCLE_NEVER;
				port->input[0] = 0x13;
			}
			for (int i = 0; i < 4; i++)
			{
				io_control_write(port->device.multitap.ports + i, 0x40, current_cycle);
				io_data_write(port->device.multitap.ports + i, 0x40, current_cycle);
			}
		} else {
			if (old_output & TH) {
				port->input[0] = 0x1F;
			}
			if ((output & TR) != (old_output & TR)) {
				//TODO: measure actual delays
				port->device.multitap.ready_cycle = current_cycle + 16 * 7;
			}
		}
		break;
	case IO_EA_MULTI_A:
		if ((output & TH) != (old_output & TH)) {
			uint8_t port_num = port->device.multitap.cur_port == EA_PASSTHRU_MODE ? 1 : port->device.multitap.cur_port;
			if (port_num < 4) {
				io_data_write(port->device.multitap.ports + port_num, output & 0x40, current_cycle);
			}
		}
		break;
	case IO_EA_MULTI_B: {
		io_port *main_port = port->device.multitap.ports;
		io_port *passthru = main_port->device.multitap.ports + 1;
		if (main_port->device.multitap.cur_port == EA_PASSTHRU_MODE) {
			output &= port->control | 0x40;
			output |= io_data_read(passthru, current_cycle) & ~(port->control | 0x40);
		}
		uint8_t old_port = main_port->device.multitap.cur_port;
		if ((output & 0xF) == 0xC) {
			main_port->device.multitap.cur_port = output >> 4 & 7;
		} else {
			main_port->device.multitap.cur_port = EA_PASSTHRU_MODE;
		}
		if (old_port == EA_PASSTHRU_MODE && main_port->device.multitap.cur_port < 4) {
			//switched from passthru to multitap mode, set TH for selected controller to port A value
			output = get_output_value(main_port, current_cycle, SLOW_RISE_DEVICE);
			io_data_write(main_port->device.multitap.ports + main_port->device.multitap.cur_port, output & 0x40, current_cycle);
		} else if (main_port->device.multitap.cur_port == EA_PASSTHRU_MODE) {
			//in passhtru mode, set TH to controller 2 to port B value
			io_data_write(main_port->device.multitap.ports + 1, output & 0x40, current_cycle);
		}
		break;
	}
#ifndef _WIN32
	case IO_GENERIC:
		wait_for_connection(port);
		port->input[IO_STATE] = IO_WRITE_PENDING;
		service_socket(port);
		break;
#endif
	}
	port->output = value;

}

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) {
		return 0;
	}
	uint8_t bytes = 0, read_pos = port->device.keyboard.read_pos;
	do {
		bytes += port->device.keyboard.events[read_pos] & 0xFF00 ? 2 : 1;
		read_pos++;
		read_pos &= 7;
	} while (read_pos != port->device.keyboard.write_pos);
	
	return bytes;
}

uint8_t io_data_read(io_port * port, uint32_t current_cycle)
{
	uint8_t output = get_output_value(port, current_cycle, SLOW_RISE_DEVICE);
	uint8_t control = port->control | 0x80;
	uint8_t th = output & 0x40;
	uint8_t input;
	uint8_t device_driven;
	if (current_cycle - port->last_poll_cycle > MIN_POLL_INTERVAL) {
		process_events();
		port->last_poll_cycle = current_cycle;
	}
	switch (port->device_type)
	{
	case IO_GAMEPAD2:
		input = ~port->input[GAMEPAD_TH1];
		device_driven = 0x3F;
		break;
	case IO_GAMEPAD3:
	{
		input = port->input[th ? GAMEPAD_TH1 : GAMEPAD_TH0];
		if (!th) {
			input |= 0xC;
		}
		//controller output is logically inverted
		input = ~input;
		device_driven = 0x3F;
		break;
	}
	case IO_GAMEPAD6:
	{
		if (current_cycle >= port->device.pad.timeout_cycle) {
			port->device.pad.th_counter = 0;
		}
		/*if (port->input[GAMEPAD_TH0] || port->input[GAMEPAD_TH1]) {
			printf("io_data_read | control: %X, TH: %X, GAMEPAD_TH0: %X, GAMEPAD_TH1: %X, TH Counter: %d, Timeout: %d, Cycle: %d\n", control, th, port->input[GAMEPAD_TH0], port->input[GAMEPAD_TH1], port->th_counter,port->timeout_cycle, context->current_cycle);
		}*/
		if (th) {
			if (port->device.pad.th_counter == 3) {
				input = port->input[GAMEPAD_EXTRA];
			} else {
				input = port->input[GAMEPAD_TH1];
			}
		} else {
			if (port->device.pad.th_counter == 2) {
				input = port->input[GAMEPAD_TH0] | 0xF;
			} else if(port->device.pad.th_counter == 3) {
				input = port->input[GAMEPAD_TH0]  & 0x30;
			} else {
				input = port->input[GAMEPAD_TH0] | 0xC;
			}
		}
		//controller output is logically inverted
		input = ~input;
		device_driven = 0x3F;
		break;
	}
	case IO_MOUSE:
	{
		mouse_check_ready(port, current_cycle);
		uint8_t tr = output & TR;
		if (th) {
			if (tr) {
				input = 0x10;
			} else {
				input = 0;
			}
		} else {

			int16_t delta_x = port->device.mouse.latched_x - port->device.mouse.last_read_x;
			int16_t delta_y = port->device.mouse.last_read_y - port->device.mouse.latched_y;
			switch (port->device.mouse.tr_counter)
			{
			case 0:
				input = 0xB;
				break;
			case 1:
			case 2:
				input = 0xF;
				break;
			case 3:
				input = 0;
				if (delta_y > 255 || delta_y < -255) {
					input |= 8;
				}
				if (delta_x > 255 || delta_x < -255) {
					input |= 4;
				}
				if (delta_y < 0) {
					input |= 2;
				}
				if (delta_x < 0) {
					input |= 1;
				}
				break;
			case 4:
				input = port->input[0];
				break;
			case 5:
				input = delta_x >> 4 & 0xF;
				break;
			case 6:
				input = delta_x & 0xF;
				break;
			case 7:
				input = delta_y >> 4 & 0xF;
				break;
			case 8:
			default:
				input = delta_y & 0xF;
				break;
			}
			input |= ((port->device.mouse.tr_counter & 1) == 0) << 4;
		}
		device_driven = 0x1F;
		break;
	}
	case IO_SATURN_KEYBOARD:
	{
		if (th) {
			input = 0x11;
		} else {
			uint8_t tr = output & TR;
			uint16_t code = port->device.keyboard.read_pos == 0xFF ? 0 
				: port->device.keyboard.events[port->device.keyboard.read_pos];
			switch (port->device.keyboard.tr_counter)
			{
			case 0:
				input = 1;
				break;
			case 1:
				//Saturn peripheral ID
				input = 3;
				break;
			case 2:
				//data size
				input = 4;
				break;
			case 3:
				//d-pad
				//TODO: set these based on keyboard state
				input = 0xF;
				break;
			case 4:
				//Start ABC
				//TODO: set these based on keyboard state
				input = 0xF;
				break;
			case 5:
				//R XYZ
				//TODO: set these based on keyboard state
				input = 0xF;
				break;
			case 6:
				//L and KBID
				//TODO: set L based on keyboard state
				input = 0x8;
				break;
			case 7:
				//Capslock, Numlock, Scrolllock
				//TODO: set these based on keyboard state
				input = 0;
				break;
			case 8:
				input = 6;
				if (code & 0xFF00) {
					//break
					input |= 1;
				} else if (code) {
					input |= 8;
				}
				break;
			case 9:
				input = code >> 4 & 0xF;
				break;
			case 10:
				input = code & 0xF;
				break;
			case 11:
				input = 0;
				break;
			default:
				input = 1;
				break;
			}
			input |= ((port->device.keyboard.tr_counter & 1) == 0) << 4;
		}
		device_driven = 0x1F;
		break;
	}
	case IO_XBAND_KEYBOARD:
	{
		if (th) {
			input = 0x1C;
		} else {
			uint8_t size;
			if (port->device.keyboard.mode == KB_SETUP || port->device.keyboard.mode == KB_READ) {
				switch (port->device.keyboard.tr_counter)
				{
				case 0:
					input = 0x3;
					break;
				case 1:
					input = 0x6;
					break;
				case 2:
					//This is where thoe host indicates a read or write
					//presumably, the keyboard only outputs this if the host
					//is not already driving the data bus low
					input = 0x9;
					break;
				case 3:
					size = get_scancode_bytes(port);
					if (size) {
						++size;
					}
					if (size > 15) {
						size = 15;
					}
					input = size;
					break;
				case 4:
				case 5:
					//always send packet type 0 for now
					input = 0;
					break;
				default:
					if (port->device.keyboard.read_pos == 0xFF) {
						//we've run out of bytes
						input = 0;
					} else if (port->device.keyboard.events[port->device.keyboard.read_pos] & 0xFF00) {
						if (port->device.keyboard.tr_counter & 1) {
							input = port->device.keyboard.events[port->device.keyboard.read_pos] >> 8 & 0xF;
						} else {
							input = port->device.keyboard.events[port->device.keyboard.read_pos] >> 12;
						}
					} else {
						if (port->device.keyboard.tr_counter & 1) {
							input = port->device.keyboard.events[port->device.keyboard.read_pos] & 0xF;
						} else {
							input = port->device.keyboard.events[port->device.keyboard.read_pos] >> 4;
						}
					}
					break;
				}
			} else {
				input = 0xF;
			}
			input |= ((port->device.keyboard.tr_counter & 1) == 0) << 4;
		}
		//this is not strictly correct at all times, but good enough for now
		device_driven = 0x1F;
		break;
	}
	case IO_SEGA_MULTI:
		multitap_check_ready(port, current_cycle);
		device_driven = 0x1F;
		input = port->input[0];
		break;
	case IO_EA_MULTI_A:
		device_driven = 0x3F;
		if (port->device.multitap.cur_port == EA_PASSTHRU_MODE) {
			input = io_data_read(port->device.multitap.ports, current_cycle);
		} else if (port->device.multitap.cur_port < 4) {
			input = io_data_read(port->device.multitap.ports + port->device.multitap.cur_port, current_cycle);
		} else {
			input = 0x3C;
		}
		break;
	case IO_EA_MULTI_B: {
		io_port *main_port = port->device.multitap.ports;
		if (main_port->device.multitap.cur_port == EA_PASSTHRU_MODE) {
			input = io_data_read(main_port->device.multitap.ports + 1, current_cycle);
			device_driven = 0x3F;
		} else {
			input = 0;
			device_driven = 0;
		}
		break;
	}
#ifndef _WIN32
	case IO_SEGA_PARALLEL:
		if (!th)
		{
			service_pipe(port);
		}
		input = port->input[th ? IO_TH1 : IO_TH0];
		device_driven = 0x3F;
		break;
	case IO_GENERIC:
		if (port->input[IO_TH0] & 0x80 && port->input[IO_STATE] == IO_WRITTEN)
		{
			//device requested a blocking read after writes
			port->input[IO_STATE] = IO_READ_PENDING;
		}
		service_socket(port);
		input = port->input[IO_TH0];
		device_driven = 0x7F;
		break;
#endif
	default:
		input = 0;
		device_driven = 0;
		break;
	}
	uint8_t value = (input & (~control) & device_driven) | (port->output & control);
	//deal with pins that are configured as inputs, but not being actively driven by the device
	uint8_t floating = (~device_driven) & (~control);
	if (floating) {
		value |= get_output_value(port, current_cycle, SLOW_RISE_INPUT) & floating;
	}
	/*if (port->input[GAMEPAD_TH0] || port->input[GAMEPAD_TH1]) {
		printf ("value: %X\n", value);
	}*/
	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);
	save_int8(buf, port->control);
	save_int8(buf, port->serial_out);
	save_int8(buf, port->serial_in);
	save_int8(buf, port->serial_ctrl);
	save_int8(buf, port->device_type);
	save_buffer32(buf, port->slow_rise_start, 8);
	switch (port->device_type)
	{
	case IO_GAMEPAD6:
		save_int32(buf, port->device.pad.timeout_cycle);
		save_int16(buf, port->device.pad.th_counter);
		break;
	case IO_MOUSE:
		save_int32(buf, port->device.mouse.ready_cycle);
		save_int16(buf, port->device.mouse.last_read_x);
		save_int16(buf, port->device.mouse.last_read_y);
		save_int16(buf, port->device.mouse.latched_x);
		save_int16(buf, port->device.mouse.latched_y);
		save_int8(buf, port->device.mouse.tr_counter);
		break;
	case IO_SATURN_KEYBOARD:
	case IO_XBAND_KEYBOARD:
		save_int8(buf, port->device.keyboard.tr_counter);
		if (port->device_type == IO_XBAND_KEYBOARD) {
			save_int8(buf, port->device.keyboard.mode);
			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)
{
	io_port *port = vport;
	port->output = load_int8(buf);
	port->control = load_int8(buf);
	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;
	}
	switch (port->device_type)
	{
	case IO_GAMEPAD6:
		port->device.pad.timeout_cycle = load_int32(buf);
		port->device.pad.th_counter = load_int16(buf);
		break;
	case IO_MOUSE:
		port->device.mouse.ready_cycle = load_int32(buf);
		port->device.mouse.last_read_x = load_int16(buf);
		port->device.mouse.last_read_y = load_int16(buf);
		port->device.mouse.latched_x = load_int16(buf);
		port->device.mouse.latched_y = load_int16(buf);
		port->device.mouse.tr_counter = load_int8(buf);
		break;
	case IO_SATURN_KEYBOARD:
	case IO_XBAND_KEYBOARD:
		port->device.keyboard.tr_counter = load_int8(buf);
		if (port->device_type == IO_XBAND_KEYBOARD) {
			port->device.keyboard.mode = load_int8(buf);
			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);
	}
}