changeset 2686:05915f01046d

WIP attempt to move VDP rendering to a separate thread
author Michael Pavone <pavone@retrodev.com>
date Mon, 31 Mar 2025 21:02:17 -0700
parents da2e06c42d16
children 948ddc60813e
files event_log.c event_log.h render.h render_sdl.c vdp.c vdp.h
diffstat 6 files changed, 935 insertions(+), 164 deletions(-) [+]
line wrap: on
line diff
--- a/event_log.c	Sun Mar 30 00:06:53 2025 -0700
+++ b/event_log.c	Mon Mar 31 21:02:17 2025 -0700
@@ -9,6 +9,7 @@
 #include <netdb.h>
 #include <netinet/in.h>
 #include <netinet/tcp.h>
+#include <pthread.h>
 #endif
 
 #include <stdlib.h>
@@ -127,11 +128,29 @@
 	freeaddrinfo(result);
 }
 
+static event_reader *mem_reader;
+static uint8_t mem_reader_quit = 1;
+#ifndef _WIN32
+void event_log_mem(void)
+{
+	event_log_common_init();
+	free(buffer.data);
+	buffer.storage = 1024 * 1024;
+	buffer.data = malloc(buffer.storage);
+	mem_reader = calloc(1, sizeof(event_reader));
+	mem_reader->last_cycle = 0;
+	mem_reader->repeat_event = 0xFF;
+	mem_reader->storage = buffer.storage;
+	init_deserialize(&mem_reader->buffer, buffer.data, 0);
+	mem_reader_quit = 0;
+}
+#endif
+
 static uint8_t *system_start;
 static size_t system_start_size;
 void event_system_start(system_type stype, vid_std video_std, char *name)
 {
-	if (!active) {
+	if (!active || mem_reader) {
 		return;
 	}
 	save_int8(&buffer, stype);
@@ -202,7 +221,7 @@
 
 void event_cycle_adjust(uint32_t cycle, uint32_t deduction)
 {
-	if (!fully_active) {
+	if (!fully_active && !mem_reader) {
 		return;
 	}
 	event_header(EVENT_ADJUST, cycle);
@@ -322,39 +341,70 @@
 	}
 }
 
+#ifndef _WIN32
+static pthread_mutex_t event_log_lock = PTHREAD_MUTEX_INITIALIZER;
+static pthread_cond_t event_cond = PTHREAD_COND_INITIALIZER;
+#endif
+static event_reader *mem_reader;
 uint8_t wrote_since_last_flush;
 void event_log(uint8_t type, uint32_t cycle, uint8_t size, uint8_t *payload)
 {
-	if (!fully_active) {
+	if (!fully_active && (!mem_reader || type == EVENT_PSG_REG || type == EVENT_YM_REG)) {
 		return;
 	}
+	
 	event_header(type, cycle);
 	last = cycle;
 	save_buffer8(&buffer, payload, size);
 	if (!multi_count) {
 		last_event_type = 0xFF;
-		output_stream.avail_in = buffer.size - (output_stream.next_in - buffer.data);
-		int result = deflate(&output_stream, Z_NO_FLUSH);
-		if (result != Z_OK) {
-			fatal_error("deflate returned %d\n", result);
-		}
-		if (listen_sock) {
-			if ((output_stream.next_out - compressed) > 1280 || !output_stream.avail_out) {
-				flush_socket();
-				wrote_since_last_flush = 1;
+#ifndef _WIN32
+		if (mem_reader) {
+			pthread_mutex_lock(&event_log_lock);
+				if (mem_reader->buffer.cur_pos) {
+					memmove(buffer.data, buffer.data + mem_reader->buffer.cur_pos, buffer.size - mem_reader->buffer.cur_pos);
+					buffer.size -= mem_reader->buffer.cur_pos;
+					mem_reader->buffer.cur_pos = 0;
+				}
+				mem_reader->buffer.size = buffer.size;
+				pthread_cond_signal(&event_cond);
+			pthread_mutex_unlock(&event_log_lock);
+		} else 
+#endif
+		{
+			output_stream.avail_in = buffer.size - (output_stream.next_in - buffer.data);
+			int result = deflate(&output_stream, Z_NO_FLUSH);
+			if (result != Z_OK) {
+				fatal_error("deflate returned %d\n", result);
 			}
-		} else if (!output_stream.avail_out) {
-			fwrite(compressed, 1, compressed_storage, event_file);
-			output_stream.next_out = compressed;
-			output_stream.avail_out = compressed_storage;
-		}
-		if (!output_stream.avail_in) {
-			buffer.size = 0;
-			output_stream.next_in = buffer.data;
+			if (listen_sock) {
+				if ((output_stream.next_out - compressed) > 1280 || !output_stream.avail_out) {
+					flush_socket();
+					wrote_since_last_flush = 1;
+				}
+			} else if (!output_stream.avail_out) {
+				fwrite(compressed, 1, compressed_storage, event_file);
+				output_stream.next_out = compressed;
+				output_stream.avail_out = compressed_storage;
+			}
+			if (!output_stream.avail_in) {
+				buffer.size = 0;
+				output_stream.next_in = buffer.data;
+			}
 		}
 	}
 }
 
+#ifndef _WIN32
+void event_log_mem_stop(void)
+{
+	pthread_mutex_lock(&event_log_lock);
+		mem_reader_quit = 1;
+		pthread_cond_signal(&event_cond);
+	pthread_mutex_unlock(&event_log_lock);
+}
+#endif
+
 static uint32_t last_word_address;
 void event_vram_word(uint32_t cycle, uint32_t address, uint16_t value)
 {
@@ -499,11 +549,26 @@
 	if (!active) {
 		return;
 	}
-	if (fully_active) {
+	if (fully_active || mem_reader) {
 		event_header(EVENT_FLUSH, cycle);
 		last = cycle;
 		
-		deflate_flush(0);
+#ifndef _WIN32
+		if (mem_reader) {
+			pthread_mutex_lock(&event_log_lock);
+				if (mem_reader->buffer.cur_pos) {
+					memmove(buffer.data, buffer.data + mem_reader->buffer.cur_pos, buffer.size - mem_reader->buffer.cur_pos);
+					buffer.size -= mem_reader->buffer.cur_pos;
+					mem_reader->buffer.cur_pos = 0;
+				}
+				mem_reader->buffer.size = buffer.size;
+				pthread_cond_signal(&event_cond);
+			pthread_mutex_unlock(&event_log_lock);
+		} else
+#endif
+		{
+			deflate_flush(0);
+		}
 	}
 	if (event_file) {
 		fwrite(compressed, 1, output_stream.next_out - compressed, event_file);
@@ -518,14 +583,29 @@
 
 void event_soft_flush(uint32_t cycle)
 {
-	if (!fully_active || wrote_since_last_flush || event_file) {
+	if ((!fully_active && !mem_reader) || wrote_since_last_flush || event_file) {
 		return;
 	}
 	event_header(EVENT_FLUSH, cycle);
 	last = cycle;
 	
-	deflate_flush(0);
-	flush_socket();
+#ifndef _WIN32
+	if (mem_reader) {
+		pthread_mutex_lock(&event_log_lock);
+			if (mem_reader->buffer.cur_pos) {
+				memmove(buffer.data, buffer.data + mem_reader->buffer.cur_pos, buffer.size - mem_reader->buffer.cur_pos);
+				buffer.size -= mem_reader->buffer.cur_pos;
+				mem_reader->buffer.cur_pos = 0;
+			}
+			mem_reader->buffer.size = buffer.size;
+			pthread_cond_signal(&event_cond);
+		pthread_mutex_unlock(&event_log_lock);
+	} else
+#endif
+	{
+		deflate_flush(0);
+		flush_socket();
+	}
 }
 
 static void init_event_reader_common(event_reader *reader)
@@ -664,15 +744,24 @@
 
 void reader_ensure_data(event_reader *reader, size_t bytes)
 {
-	if (reader->buffer.size - reader->buffer.cur_pos < bytes) {
-		if (reader->input_stream.avail_in) {
-			inflate_flush(reader);
+#ifndef _WIN32
+	if (reader == mem_reader) {
+		while (!mem_reader_quit && reader->buffer.size - reader->buffer.cur_pos < bytes) {
+			pthread_cond_wait(&event_cond, &event_log_lock);
 		}
-		if (reader->socket) {
-			while (reader->buffer.size - reader->buffer.cur_pos < bytes) {
-				read_from_socket(reader);
+	} else
+#endif
+	{
+		if (reader->buffer.size - reader->buffer.cur_pos < bytes) {
+			if (reader->input_stream.avail_in) {
 				inflate_flush(reader);
 			}
+			if (reader->socket) {
+				while (reader->buffer.size - reader->buffer.cur_pos < bytes) {
+					read_from_socket(reader);
+					inflate_flush(reader);
+				}
+			}
 		}
 	}
 }
@@ -686,6 +775,9 @@
 		return reader->repeat_event;
 	}
 	reader_ensure_data(reader, 1);
+	if (reader == mem_reader && mem_reader_quit) {
+		return EVENT_EOF;
+	}
 	uint8_t header = load_int8(&reader->buffer);
 	uint8_t ret;
 	uint32_t delta;
@@ -735,6 +827,78 @@
 	return ret;
 }
 
+#ifndef _WIN32
+uint8_t mem_reader_next_event(event_out *out)
+{
+	uint8_t ret = EVENT_EOF;
+	if (mem_reader->repeat_remaining) {
+		mem_reader->repeat_remaining--;
+		mem_reader->last_cycle += mem_reader->repeat_delta;
+		out->cycle = mem_reader->last_cycle;
+		ret = mem_reader->repeat_event;
+	}
+	if (ret < EVENT_PSG_REG) {
+		return ret;
+	}
+	pthread_mutex_lock(&event_log_lock);
+		if (ret == EVENT_EOF) {
+			ret = reader_next_event(mem_reader, &out->cycle);
+		}
+		switch (ret)
+		{
+		case EVENT_ADJUST:
+			out->address = load_int32(&mem_reader->buffer);
+			break;
+		case EVENT_VRAM_BYTE:
+			out->address = load_int16(&mem_reader->buffer);
+			break;
+		case EVENT_VRAM_BYTE_DELTA:
+			out->address = mem_reader->last_byte_address + load_int8(&mem_reader->buffer);
+			break;
+		case EVENT_VRAM_BYTE_ONE:
+			out->address = mem_reader->last_byte_address + 1;
+			break;
+		case EVENT_VRAM_BYTE_AUTO:
+			out->address = mem_reader->last_byte_address + out->autoinc;
+			break;
+		case EVENT_VRAM_WORD:
+			out->address = load_int8(&mem_reader->buffer) << 16;
+			out->address |= load_int16(&mem_reader->buffer);
+			break;
+		case EVENT_VRAM_WORD_DELTA:
+			out->address = mem_reader->last_word_address + load_int8(&mem_reader->buffer);
+			break;
+		case EVENT_VDP_REG:
+		case EVENT_VDP_INTRAM:
+			out->address = load_int8(&mem_reader->buffer);
+			break;
+		}
+		switch (ret)
+		{
+		case EVENT_VRAM_BYTE:
+		case EVENT_VRAM_BYTE_DELTA:
+		case EVENT_VRAM_BYTE_ONE:
+		case EVENT_VRAM_BYTE_AUTO:
+			mem_reader->last_byte_address = out->address;
+		case EVENT_VDP_REG:
+			out->value = load_int8(&mem_reader->buffer);
+			break;
+		case EVENT_VRAM_WORD:
+		case EVENT_VRAM_WORD_DELTA:
+			mem_reader->last_word_address = out->address;
+		case EVENT_VDP_INTRAM:
+			out->value = load_int16(&mem_reader->buffer);
+			break;
+		case EVENT_EOF:
+			free(mem_reader);
+			mem_reader = NULL;
+			break;
+		}
+	pthread_mutex_unlock(&event_log_lock);
+	return ret;
+}
+#endif
+
 uint8_t reader_system_type(event_reader *reader)
 {
 	return load_int8(&reader->buffer);
--- a/event_log.h	Sun Mar 30 00:06:53 2025 -0700
+++ b/event_log.h	Mon Mar 31 21:02:17 2025 -0700
@@ -15,8 +15,9 @@
 	EVENT_VRAM_WORD_DELTA = 10,
 	EVENT_VDP_INTRAM = 11,
 	EVENT_STATE = 12,
-	EVENT_MULTI = 13
+	EVENT_MULTI = 13,
 	//14 and 15 are reserved for header types
+	EVENT_EOF = 16
 };
 
 #include "serialize.h"
@@ -36,11 +37,20 @@
 	uint8_t repeat_remaining;
 } event_reader;
 
+typedef struct {
+	uint32_t cycle;
+	uint32_t autoinc;
+	uint32_t address;
+	uint16_t value;
+} event_out;
+
 #include "system.h"
 #include "render.h"
 
 void event_log_file(char *fname);
 void event_log_tcp(char *address, char *port);
+void event_log_mem(void);
+void event_log_mem_stop(void);
 void event_system_start(system_type stype, vid_std video_std, char *name);
 void event_cycle_adjust(uint32_t cycle, uint32_t deduction);
 void event_log(uint8_t type, uint32_t cycle, uint8_t size, uint8_t *payload);
@@ -56,5 +66,6 @@
 void reader_ensure_data(event_reader *reader, size_t bytes);
 uint8_t reader_system_type(event_reader *reader);
 void reader_send_gamepad_event(event_reader *reader, uint8_t pad, uint8_t button, uint8_t down);
+uint8_t mem_reader_next_event(event_out *out);
 
 #endif //EVENT_LOG_H_
--- a/render.h	Sun Mar 30 00:06:53 2025 -0700
+++ b/render.h	Mon Mar 31 21:02:17 2025 -0700
@@ -154,6 +154,7 @@
 int render_ui_to_pixels_x(int ui);
 int render_ui_to_pixels_y(int ui);
 char *render_read_clipboard(void);
+uint8_t render_is_threaded_video(void);
 #ifndef IS_LIB
 uint8_t render_create_thread(render_thread *thread, const char *name, render_thread_fun fun, void *data);
 uint8_t render_static_image(uint8_t window, uint8_t *buffer, uint32_t size);
--- a/render_sdl.c	Sun Mar 30 00:06:53 2025 -0700
+++ b/render_sdl.c	Mon Mar 31 21:02:17 2025 -0700
@@ -108,6 +108,11 @@
 	return sync_src < SYNC_VIDEO;
 }
 
+uint8_t render_is_threaded_video(void)
+{
+	return sync_src == SYNC_AUDIO_THREAD || sync_src == SYNC_EXTERNAL;
+}
+
 uint8_t render_should_release_on_exit(void)
 {
 #ifdef __EMSCRIPTEN__
--- a/vdp.c	Sun Mar 30 00:06:53 2025 -0700
+++ b/vdp.c	Mon Mar 31 21:02:17 2025 -0700
@@ -156,16 +156,9 @@
 
 static uint8_t static_table_init_done;
 
-vdp_context *init_vdp_context(uint8_t region_pal, uint8_t has_max_vsram, uint8_t type)
+vdp_context *init_vdp_context_int(uint8_t region_pal, uint8_t has_max_vsram, uint8_t type)
 {
 	vdp_context *context = calloc(1, sizeof(vdp_context) + VRAM_SIZE);
-	if (headless) {
-		context->fb = malloc(512 * LINEBUF_SIZE * sizeof(pixel_t));
-		context->output_pitch = LINEBUF_SIZE * sizeof(pixel_t);
-	} else {
-		context->cur_buffer = FRAMEBUFFER_ODD;
-		context->fb = render_get_framebuffer(FRAMEBUFFER_ODD, &context->output_pitch);
-	}
 	context->sprite_draws = MAX_SPRITES_LINE;
 	context->fifo_write = 0;
 	context->fifo_read = -1;
@@ -339,8 +332,200 @@
 		context->flags2 |= FLAG2_REGION_PAL;
 	}
 	update_video_params(context);
+	
+	return context;
+}
+
+static uint32_t mode5_sat_address(vdp_context *context)
+{
+	uint32_t addr = context->regs[REG_SAT] << 9;
+	if (!(context->regs[REG_MODE_2] & BIT_128K_VRAM)) {
+		addr &= 0xFFFF;
+	}
+	if (context->regs[REG_MODE_4] & BIT_H40) {
+		addr &= 0x1FC00;
+	}
+	return addr;
+}
+
+static void vdp_check_update_sat(vdp_context *context, uint32_t address, uint16_t value)
+{
+	if (context->regs[REG_MODE_2] & BIT_MODE_5) {
+		if (!(address & 4)) {
+			uint32_t sat_address = mode5_sat_address(context);
+			if(address >= sat_address && address < (sat_address + SAT_CACHE_SIZE*2)) {
+				uint16_t cache_address = address - sat_address;
+				cache_address = (cache_address & 3) | (cache_address >> 1 & 0x1FC);
+				context->sat_cache[cache_address] = value >> 8;
+				context->sat_cache[cache_address^1] = value;
+			}
+		}
+	}
+}
+
+void vdp_check_update_sat_byte(vdp_context *context, uint32_t address, uint8_t value)
+{
+	if (context->regs[REG_MODE_2] & BIT_MODE_5) {
+		if (!(address & 4)) {
+			uint32_t sat_address = mode5_sat_address(context);
+			if(address >= sat_address && address < (sat_address + SAT_CACHE_SIZE*2)) {
+				uint16_t cache_address = address - sat_address;
+				cache_address = (cache_address & 3) | (cache_address >> 1 & 0x1FC);
+				context->sat_cache[cache_address] = value;
+			}
+		}
+	}
+}
+
+static void write_vram_word(vdp_context *context, uint32_t address, uint16_t value)
+{
+	address = (address & 0x3FC) | (address >> 1 & 0xFC01) | (address >> 9 & 0x2);
+	address ^= 1;
+	//TODO: Support an option to actually have 128KB of VRAM
+	context->vdpmem[address] = value;
+}
+
+static void write_vram_byte(vdp_context *context, uint32_t address, uint8_t value)
+{
+	if (context->regs[REG_MODE_2] & BIT_MODE_5) {
+		address &= 0xFFFF;
+	} else {
+		address = mode4_address_map[address & 0x3FFF];
+	}
+	context->vdpmem[address] = value;
+}
+
+#define VSRAM_DIRTY_BITS 0xF800
+
+//rough estimate of slot number at which border display starts
+#define BG_START_SLOT 6
+
+static void update_color_map(vdp_context *context, uint16_t index, uint16_t value)
+{
+	context->colors[index] = context->color_map[value & CRAM_BITS];
+	context->colors[index + SHADOW_OFFSET] = context->color_map[(value & CRAM_BITS) | FBUF_SHADOW];
+	context->colors[index + HIGHLIGHT_OFFSET] = context->color_map[(value & CRAM_BITS) | FBUF_HILIGHT];
+	if (context->type == VDP_GAMEGEAR) {
+		context->colors[index + MODE4_OFFSET] = context->color_map[value & 0xFFF];
+	} else {
+		context->colors[index + MODE4_OFFSET] = context->color_map[(value & CRAM_BITS) | FBUF_MODE4];
+	}
+}
+
+void write_cram_internal(vdp_context * context, uint16_t addr, uint16_t value)
+{
+	context->cram[addr] = value;
+	update_color_map(context, addr, value);
+}
+
+static void write_cram(vdp_context * context, uint16_t address, uint16_t value)
+{
+	uint16_t addr;
+	if (context->regs[REG_MODE_2] & BIT_MODE_5) {
+		addr = (address/2) & (CRAM_SIZE-1);
+	} else if (context->type == VDP_GAMEGEAR) {
+		addr = (address/2) & 31;
+	} else {
+		addr = address & 0x1F;
+		value = (value << 1 & 0xE) | (value << 2 & 0xE0) | (value & 0xE00);
+	}
+	write_cram_internal(context, addr, value);
+
+	if (context->output && context->hslot >= BG_START_SLOT && (
+		context->vcounter < context->inactive_start + context->border_bot
+		|| context->vcounter > 0x200 - context->border_top
+	)) {
+		uint8_t bg_end_slot = BG_START_SLOT + (context->regs[REG_MODE_4] & BIT_H40) ? LINEBUF_SIZE/2 : (256+HORIZ_BORDER)/2;
+		if (context->hslot < bg_end_slot) {
+			pixel_t color = (context->regs[REG_MODE_2] & BIT_MODE_5) ? context->colors[addr] : context->colors[addr + MODE4_OFFSET];
+			context->output[(context->hslot - BG_START_SLOT)*2 + 1] = color;
+		}
+	}
+}
+
+#ifndef _WIN32
+static int vdp_render_thread_main(void *vcontext)
+{
+	vdp_context *context = vcontext;
+	event_out event;
+	for (;;)
+	{
+		event.autoinc = context->regs[REG_AUTOINC];
+		uint8_t etype = mem_reader_next_event(&event);
+		if (etype == EVENT_EOF) {
+			break;
+		}
+		vdp_run_context(context, event.cycle);
+		switch (etype)
+		{
+		case EVENT_ADJUST:
+			vdp_adjust_cycles(context, event.address);
+			break;
+		case EVENT_VDP_REG:
+			context->regs[event.address] = event.value;
+			if (event.address == REG_MODE_4) {
+				context->double_res = (event.value & (BIT_INTERLACE | BIT_DOUBLE_RES)) == (BIT_INTERLACE | BIT_DOUBLE_RES);
+				if (!context->double_res) {
+					context->flags2 &= ~FLAG2_EVEN_FIELD;
+				}
+			}
+			if (event.address == REG_MODE_1 || event.address == REG_MODE_2 || event.address == REG_MODE_4) {
+				update_video_params(context);
+			}
+			break;
+		case EVENT_VRAM_BYTE:
+		case EVENT_VRAM_BYTE_DELTA:
+		case EVENT_VRAM_BYTE_ONE:
+		case EVENT_VRAM_BYTE_AUTO:
+			vdp_check_update_sat_byte(context, event.address ^ 1, event.value);
+			write_vram_byte(context, event.address ^ 1, event.value);
+			break;
+		case EVENT_VRAM_WORD:
+		case EVENT_VRAM_WORD_DELTA:
+			vdp_check_update_sat(context, event.address, event.value);
+			write_vram_word(context, event.address, event.value);
+			break;
+		case EVENT_VDP_INTRAM:
+			if (event.address < 128) {
+				write_cram(context, event.address, event.value);
+			} else {
+				context->vsram[event.address&63] = event.value;
+			}
+			break;
+		}
+	}
+	return 0;
+}
+#endif
+
+static render_thread vdp_thread;
+vdp_context *init_vdp_context(uint8_t region_pal, uint8_t has_max_vsram, uint8_t type)
+{
+	vdp_context *ret = init_vdp_context_int(region_pal, has_max_vsram, type);
+	vdp_context *context;
+#ifndef _WIN32
+	if (render_is_threaded_video()) {
+		context = ret->renderer = init_vdp_context_int(region_pal, has_max_vsram, type);
+	} else
+#endif
+	{
+		context = ret;
+	}
+	if (headless) {
+		context->fb = malloc(512 * LINEBUF_SIZE * sizeof(pixel_t));
+		context->output_pitch = LINEBUF_SIZE * sizeof(pixel_t);
+	} else {
+		context->cur_buffer = FRAMEBUFFER_ODD;
+		context->fb = render_get_framebuffer(FRAMEBUFFER_ODD, &context->output_pitch);
+	}
 	context->output = (pixel_t *)(((char *)context->fb) + context->output_pitch * context->border_top);
-	return context;
+#ifndef _WIN32
+	if (ret->renderer) {
+		event_log_mem();
+		render_create_thread(&vdp_thread, "vdp_render", vdp_render_thread_main, ret->renderer);
+	}
+#endif
+	return ret;
 }
 
 void vdp_free(vdp_context *context)
@@ -354,6 +539,12 @@
 			vdp_toggle_debug_view(context, i);
 		}
 	}
+#ifndef _WIN32
+	if (context->renderer) {
+		event_log_mem_stop();
+		vdp_free(context->renderer);
+	}
+#endif
 	free(context);
 }
 
@@ -520,18 +711,6 @@
 	}
 }
 
-static uint32_t mode5_sat_address(vdp_context *context)
-{
-	uint32_t addr = context->regs[REG_SAT] << 9;
-	if (!(context->regs[REG_MODE_2] & BIT_128K_VRAM)) {
-		addr &= 0xFFFF;
-	}
-	if (context->regs[REG_MODE_4] & BIT_H40) {
-		addr &= 0x1FC00;
-	}
-	return addr;
-}
-
 void vdp_print_sprite_table(vdp_context * context)
 {
 	if (context->type == VDP_GENESIS && context->regs[REG_MODE_2] & BIT_MODE_5) {
@@ -957,54 +1136,6 @@
 	}
 }
 
-#define VSRAM_DIRTY_BITS 0xF800
-
-//rough estimate of slot number at which border display starts
-#define BG_START_SLOT 6
-
-static void update_color_map(vdp_context *context, uint16_t index, uint16_t value)
-{
-	context->colors[index] = context->color_map[value & CRAM_BITS];
-	context->colors[index + SHADOW_OFFSET] = context->color_map[(value & CRAM_BITS) | FBUF_SHADOW];
-	context->colors[index + HIGHLIGHT_OFFSET] = context->color_map[(value & CRAM_BITS) | FBUF_HILIGHT];
-	if (context->type == VDP_GAMEGEAR) {
-		context->colors[index + MODE4_OFFSET] = context->color_map[value & 0xFFF];
-	} else {
-		context->colors[index + MODE4_OFFSET] = context->color_map[(value & CRAM_BITS) | FBUF_MODE4];
-	}
-}
-
-void write_cram_internal(vdp_context * context, uint16_t addr, uint16_t value)
-{
-	context->cram[addr] = value;
-	update_color_map(context, addr, value);
-}
-
-static void write_cram(vdp_context * context, uint16_t address, uint16_t value)
-{
-	uint16_t addr;
-	if (context->regs[REG_MODE_2] & BIT_MODE_5) {
-		addr = (address/2) & (CRAM_SIZE-1);
-	} else if (context->type == VDP_GAMEGEAR) {
-		addr = (address/2) & 31;
-	} else {
-		addr = address & 0x1F;
-		value = (value << 1 & 0xE) | (value << 2 & 0xE0) | (value & 0xE00);
-	}
-	write_cram_internal(context, addr, value);
-
-	if (context->output && context->hslot >= BG_START_SLOT && (
-		context->vcounter < context->inactive_start + context->border_bot
-		|| context->vcounter > 0x200 - context->border_top
-	)) {
-		uint8_t bg_end_slot = BG_START_SLOT + (context->regs[REG_MODE_4] & BIT_H40) ? LINEBUF_SIZE/2 : (256+HORIZ_BORDER)/2;
-		if (context->hslot < bg_end_slot) {
-			pixel_t color = (context->regs[REG_MODE_2] & BIT_MODE_5) ? context->colors[addr] : context->colors[addr + MODE4_OFFSET];
-			context->output[(context->hslot - BG_START_SLOT)*2 + 1] = color;
-		}
-	}
-}
-
 static void vdp_advance_dma(vdp_context * context)
 {
 	context->regs[REG_DMASRC_L] += 1;
@@ -1021,53 +1152,6 @@
 	}
 }
 
-static void vdp_check_update_sat(vdp_context *context, uint32_t address, uint16_t value)
-{
-	if (context->regs[REG_MODE_2] & BIT_MODE_5) {
-		if (!(address & 4)) {
-			uint32_t sat_address = mode5_sat_address(context);
-			if(address >= sat_address && address < (sat_address + SAT_CACHE_SIZE*2)) {
-				uint16_t cache_address = address - sat_address;
-				cache_address = (cache_address & 3) | (cache_address >> 1 & 0x1FC);
-				context->sat_cache[cache_address] = value >> 8;
-				context->sat_cache[cache_address^1] = value;
-			}
-		}
-	}
-}
-
-void vdp_check_update_sat_byte(vdp_context *context, uint32_t address, uint8_t value)
-{
-	if (context->regs[REG_MODE_2] & BIT_MODE_5) {
-		if (!(address & 4)) {
-			uint32_t sat_address = mode5_sat_address(context);
-			if(address >= sat_address && address < (sat_address + SAT_CACHE_SIZE*2)) {
-				uint16_t cache_address = address - sat_address;
-				cache_address = (cache_address & 3) | (cache_address >> 1 & 0x1FC);
-				context->sat_cache[cache_address] = value;
-			}
-		}
-	}
-}
-
-static void write_vram_word(vdp_context *context, uint32_t address, uint16_t value)
-{
-	address = (address & 0x3FC) | (address >> 1 & 0xFC01) | (address >> 9 & 0x2);
-	address ^= 1;
-	//TODO: Support an option to actually have 128KB of VRAM
-	context->vdpmem[address] = value;
-}
-
-static void write_vram_byte(vdp_context *context, uint32_t address, uint8_t value)
-{
-	if (context->regs[REG_MODE_2] & BIT_MODE_5) {
-		address &= 0xFFFF;
-	} else {
-		address = mode4_address_map[address & 0x3FFF];
-	}
-	context->vdpmem[address] = value;
-}
-
 #define DMA_FILL 0x80
 #define DMA_COPY 0xC0
 #define DMA_TYPE_MASK 0xC0
@@ -2148,6 +2232,9 @@
 	}
 
 	context->vcounter++;
+	if (context->renderer && context->vcounter == context->inactive_start) {
+		context->frame++;
+	}
 	if (is_mode_5) {
 		context->window_h_latch = context->regs[REG_WINDOW_H];
 		context->window_v_latch = context->regs[REG_WINDOW_V];
@@ -3111,6 +3198,48 @@
 		render_map_output(context->vcounter, column, context);\
 		CHECK_LIMIT
 
+#define COLUMN_RENDER_BLOCK_PHONY(column, startcyc) \
+	case startcyc:\
+		CHECK_LIMIT\
+	case ((startcyc+1)&0xFF):\
+		external_slot(context);\
+		CHECK_LIMIT\
+	case ((startcyc+2)&0xFF):\
+		CHECK_LIMIT\
+	case ((startcyc+3)&0xFF):\
+		CHECK_LIMIT\
+	case ((startcyc+4)&0xFF):\
+		CHECK_LIMIT\
+	case ((startcyc+5)&0xFF):\
+		read_sprite_x(context->vcounter, context);\
+		CHECK_LIMIT\
+	case ((startcyc+6)&0xFF):\
+		CHECK_LIMIT\
+	case ((startcyc+7)&0xFF):\
+		CHECK_LIMIT
+
+#define COLUMN_RENDER_BLOCK_REFRESH_PHONY(column, startcyc) \
+	case startcyc:\
+		CHECK_LIMIT\
+	case (startcyc+1):\
+		/* refresh, so don't run dma src */\
+		context->hslot++;\
+		context->cycles += slot_cycles;\
+		CHECK_ONLY\
+	case (startcyc+2):\
+		CHECK_LIMIT\
+	case (startcyc+3):\
+		CHECK_LIMIT\
+	case (startcyc+4):\
+		CHECK_LIMIT\
+	case (startcyc+5):\
+		read_sprite_x(context->vcounter, context);\
+		CHECK_LIMIT\
+	case (startcyc+6):\
+		CHECK_LIMIT\
+	case (startcyc+7):\
+		CHECK_LIMIT
+
 #define COLUMN_RENDER_BLOCK_MODE4(column, startcyc) \
 	case startcyc:\
 		OUTPUT_PIXEL_MODE4(startcyc)\
@@ -3183,6 +3312,12 @@
 		scan_sprite_table(context->vcounter, context);\
 		CHECK_LIMIT_HSYNC(slot)
 
+#define SPRITE_RENDER_H40_PHONY(slot) \
+	case slot:\
+		render_sprite_cells( context);\
+		scan_sprite_table(context->vcounter, context);\
+		CHECK_LIMIT_HSYNC(slot)
+
 //Note that the line advancement check will fail if BG_START_SLOT is > 6
 //as we're bumping up against the hcounter jump
 #define SPRITE_RENDER_H32(slot) \
@@ -3220,6 +3355,19 @@
 		context->cycles += slot_cycles;\
 		CHECK_ONLY
 
+#define SPRITE_RENDER_H32_PHONY(slot) \
+	case slot:\
+		render_sprite_cells( context);\
+		scan_sprite_table(context->vcounter, context);\
+		if (context->flags & FLAG_DMA_RUN) { run_dma_src(context, -1); } \
+		if (slot == 147) {\
+			context->hslot = 233;\
+		} else {\
+			context->hslot++;\
+		}\
+		context->cycles += slot_cycles;\
+		CHECK_ONLY
+
 #define MODE4_CHECK_SLOT_LINE(slot) \
 		if (context->flags & FLAG_DMA_RUN) { run_dma_src(context, -1); } \
 		if ((slot) == BG_START_SLOT + (256+HORIZ_BORDER)/2) {\
@@ -3707,6 +3855,161 @@
 	}
 }
 
+static void vdp_h40_phony(vdp_context * context, uint32_t target_cycles)
+{
+	uint32_t const slot_cycles = MCLKS_SLOT_H40;
+	switch(context->hslot)
+	{
+	for (;;)
+	{
+	case 165:
+		if (context->state == PREPARING) {
+			external_slot(context);
+		} else {
+			render_sprite_cells(context);
+		}
+		CHECK_LIMIT
+	case 166:
+		if (context->state == PREPARING) {
+			external_slot(context);
+		} else {
+			render_sprite_cells(context);
+		}
+		if (context->vcounter == context->inactive_start) {
+			context->hslot++;
+			context->cycles += slot_cycles;
+			return;
+		}
+		CHECK_LIMIT
+	//sprite attribute table scan starts
+	case 167:
+		context->sprite_index = 0x80;
+		context->slot_counter = 0;
+		render_sprite_cells(context);
+		scan_sprite_table(context->vcounter, context);
+		CHECK_LIMIT
+	SPRITE_RENDER_H40_PHONY(168)
+	SPRITE_RENDER_H40_PHONY(169)
+	SPRITE_RENDER_H40_PHONY(170)
+	SPRITE_RENDER_H40_PHONY(171)
+	SPRITE_RENDER_H40_PHONY(172)
+	SPRITE_RENDER_H40_PHONY(173)
+	SPRITE_RENDER_H40_PHONY(174)
+	SPRITE_RENDER_H40_PHONY(175)
+	SPRITE_RENDER_H40_PHONY(176)
+	SPRITE_RENDER_H40_PHONY(177)//End of border?
+	SPRITE_RENDER_H40_PHONY(178)
+	SPRITE_RENDER_H40_PHONY(179)
+	SPRITE_RENDER_H40_PHONY(180)
+	SPRITE_RENDER_H40_PHONY(181)
+	SPRITE_RENDER_H40_PHONY(182)
+	SPRITE_RENDER_H40_PHONY(229)
+	//!HSYNC asserted
+	SPRITE_RENDER_H40_PHONY(230)
+	SPRITE_RENDER_H40_PHONY(231)
+	case 232:
+		external_slot(context);
+		CHECK_LIMIT_HSYNC(232)
+	SPRITE_RENDER_H40_PHONY(233)
+	SPRITE_RENDER_H40_PHONY(234)
+	SPRITE_RENDER_H40_PHONY(235)
+	SPRITE_RENDER_H40_PHONY(236)
+	SPRITE_RENDER_H40_PHONY(237)
+	SPRITE_RENDER_H40_PHONY(238)
+	SPRITE_RENDER_H40_PHONY(239)
+	SPRITE_RENDER_H40_PHONY(240)
+	SPRITE_RENDER_H40_PHONY(241)
+	SPRITE_RENDER_H40_PHONY(242)
+	SPRITE_RENDER_H40_PHONY(243) //provides "garbage" for border when plane A selected
+	case 244:
+		if (context->flags & FLAG_DMA_RUN) { run_dma_src(context, -1); }
+		context->hslot++;
+		context->cycles += h40_hsync_cycles[14];
+		CHECK_ONLY //provides "garbage" for border when plane A selected
+	//!HSYNC high
+	SPRITE_RENDER_H40_PHONY(245)
+	SPRITE_RENDER_H40_PHONY(246)
+	SPRITE_RENDER_H40_PHONY(247) //provides "garbage" for border when plane B selected
+	SPRITE_RENDER_H40_PHONY(248) //provides "garbage" for border when plane B selected
+	case 249:
+		CHECK_LIMIT
+	SPRITE_RENDER_H40_PHONY(250)
+	case 251:
+		scan_sprite_table(context->vcounter, context);//Just a guess
+		CHECK_LIMIT
+	case 252:
+		scan_sprite_table(context->vcounter, context);//Just a guess
+		CHECK_LIMIT
+	case 253:
+		CHECK_LIMIT
+	SPRITE_RENDER_H40_PHONY(254)
+	case 255:
+		scan_sprite_table(context->vcounter, context);//Just a guess
+		CHECK_LIMIT
+	case 0:
+		scan_sprite_table(context->vcounter, context);//Just a guess
+		//seems like the sprite table scan fills a shift register
+		//values are FIFO, but unused slots precede used slots
+		//so we set cur_slot to slot_counter and let it wrap around to
+		//the beginning of the list
+		context->cur_slot = context->slot_counter;
+		context->sprite_x_offset = 0;
+		context->sprite_draws = MAX_SPRITES_LINE;
+		CHECK_LIMIT
+	COLUMN_RENDER_BLOCK_PHONY(2, 1)
+	COLUMN_RENDER_BLOCK_PHONY(4, 9)
+	COLUMN_RENDER_BLOCK_PHONY(6, 17)
+	COLUMN_RENDER_BLOCK_REFRESH_PHONY(8, 25)
+	COLUMN_RENDER_BLOCK_PHONY(10, 33)
+	COLUMN_RENDER_BLOCK_PHONY(12, 41)
+	COLUMN_RENDER_BLOCK_PHONY(14, 49)
+	COLUMN_RENDER_BLOCK_REFRESH_PHONY(16, 57)
+	COLUMN_RENDER_BLOCK_PHONY(18, 65)
+	COLUMN_RENDER_BLOCK_PHONY(20, 73)
+	COLUMN_RENDER_BLOCK_PHONY(22, 81)
+	COLUMN_RENDER_BLOCK_REFRESH_PHONY(24, 89)
+	COLUMN_RENDER_BLOCK_PHONY(26, 97)
+	COLUMN_RENDER_BLOCK_PHONY(28, 105)
+	COLUMN_RENDER_BLOCK_PHONY(30, 113)
+	COLUMN_RENDER_BLOCK_REFRESH_PHONY(32, 121)
+	COLUMN_RENDER_BLOCK_PHONY(34, 129)
+	COLUMN_RENDER_BLOCK_PHONY(36, 137)
+	COLUMN_RENDER_BLOCK_PHONY(38, 145)
+	COLUMN_RENDER_BLOCK_REFRESH_PHONY(40, 153)
+	case 161:
+		external_slot(context);
+		CHECK_LIMIT
+	case 162:
+		external_slot(context);
+		CHECK_LIMIT
+	//sprite render to line buffer starts
+	case 163:
+		context->cur_slot = MAX_SPRITES_LINE-1;
+		memset(context->linebuf, 0, LINEBUF_SIZE);
+		context->flags &= ~FLAG_MASKED;
+		while (context->sprite_draws) {
+			context->sprite_draws--;
+			context->sprite_draw_list[context->sprite_draws].x_pos = 0;
+		}
+		render_sprite_cells(context);
+		CHECK_LIMIT
+	case 164:
+		render_sprite_cells(context);
+		if (context->flags & FLAG_DMA_RUN) {
+			run_dma_src(context, -1);
+		}
+		context->hslot++;
+		context->cycles += slot_cycles;
+		vdp_advance_line(context);
+		CHECK_ONLY
+	}
+	default:
+		context->hslot++;
+		context->cycles += slot_cycles;
+		return;
+	}
+}
+
 static void vdp_h32(vdp_context * context, uint32_t target_cycles)
 {
 	uint16_t address;
@@ -3925,6 +4228,150 @@
 	}
 }
 
+static void vdp_h32_phony(vdp_context * context, uint32_t target_cycles)
+{
+	uint32_t const slot_cycles = MCLKS_SLOT_H32;
+	switch(context->hslot)
+	{
+	for (;;)
+	{
+	case 133:
+		if (context->state == PREPARING) {
+			external_slot(context);
+		} else {
+			render_sprite_cells(context);
+		}
+		CHECK_LIMIT
+	case 134:
+		if (context->state == PREPARING) {
+			external_slot(context);
+		} else {
+			render_sprite_cells(context);
+		}
+		if (context->vcounter == context->inactive_start) {
+			context->hslot++;
+			context->cycles += slot_cycles;
+			return;
+		}
+		CHECK_LIMIT
+	//sprite attribute table scan starts
+	case 135:
+		context->sprite_index = 0x80;
+		context->slot_counter = 0;
+		render_sprite_cells(context);
+		scan_sprite_table(context->vcounter, context);
+		CHECK_LIMIT
+	SPRITE_RENDER_H32_PHONY(136)
+	SPRITE_RENDER_H32_PHONY(137)
+	SPRITE_RENDER_H32_PHONY(138)
+	SPRITE_RENDER_H32_PHONY(139)
+	SPRITE_RENDER_H32_PHONY(140)
+	SPRITE_RENDER_H32_PHONY(141)
+	SPRITE_RENDER_H32_PHONY(142)
+	SPRITE_RENDER_H32_PHONY(143)
+	SPRITE_RENDER_H32_PHONY(144)
+	case 145:
+		external_slot(context);
+		CHECK_LIMIT
+	SPRITE_RENDER_H32_PHONY(146)
+	SPRITE_RENDER_H32_PHONY(147)
+	SPRITE_RENDER_H32_PHONY(233)
+	SPRITE_RENDER_H32_PHONY(234)
+	SPRITE_RENDER_H32_PHONY(235)
+	//HSYNC start
+	SPRITE_RENDER_H32_PHONY(236)
+	SPRITE_RENDER_H32_PHONY(237)
+	SPRITE_RENDER_H32_PHONY(238)
+	SPRITE_RENDER_H32_PHONY(239)
+	SPRITE_RENDER_H32_PHONY(240)
+	SPRITE_RENDER_H32_PHONY(241)
+	SPRITE_RENDER_H32_PHONY(242)
+	case 243:
+		external_slot(context);
+		CHECK_LIMIT
+	case 244:
+		CHECK_LIMIT //provides "garbage" for border when plane A selected
+	SPRITE_RENDER_H32_PHONY(245)
+	SPRITE_RENDER_H32_PHONY(246)
+	SPRITE_RENDER_H32_PHONY(247) //provides "garbage" for border when plane B selected
+	SPRITE_RENDER_H32_PHONY(248) //provides "garbage" for border when plane B selected
+	//!HSYNC high
+	case 249:
+		CHECK_LIMIT
+	SPRITE_RENDER_H32_PHONY(250)
+	case 251:
+		scan_sprite_table(context->vcounter, context);//Just a guess
+		CHECK_LIMIT
+	case 252:
+		scan_sprite_table(context->vcounter, context);//Just a guess
+		CHECK_LIMIT
+	case 253:
+		CHECK_LIMIT
+	case 254:
+		render_sprite_cells(context);
+		scan_sprite_table(context->vcounter, context);
+		CHECK_LIMIT
+	case 255:
+		scan_sprite_table(context->vcounter, context);//Just a guess
+		CHECK_LIMIT
+	case 0:
+		scan_sprite_table(context->vcounter, context);//Just a guess
+		//reverse context slot counter so it counts the number of sprite slots
+		//filled rather than the number of available slots
+		//context->slot_counter = MAX_SPRITES_LINE - context->slot_counter;
+		context->cur_slot = context->slot_counter;
+		context->sprite_x_offset = 0;
+		context->sprite_draws = MAX_SPRITES_LINE_H32;
+		CHECK_LIMIT
+	COLUMN_RENDER_BLOCK_PHONY(2, 1)
+	COLUMN_RENDER_BLOCK_PHONY(4, 9)
+	COLUMN_RENDER_BLOCK_PHONY(6, 17)
+	COLUMN_RENDER_BLOCK_REFRESH_PHONY(8, 25)
+	COLUMN_RENDER_BLOCK_PHONY(10, 33)
+	COLUMN_RENDER_BLOCK_PHONY(12, 41)
+	COLUMN_RENDER_BLOCK_PHONY(14, 49)
+	COLUMN_RENDER_BLOCK_REFRESH_PHONY(16, 57)
+	COLUMN_RENDER_BLOCK_PHONY(18, 65)
+	COLUMN_RENDER_BLOCK_PHONY(20, 73)
+	COLUMN_RENDER_BLOCK_PHONY(22, 81)
+	COLUMN_RENDER_BLOCK_REFRESH_PHONY(24, 89)
+	COLUMN_RENDER_BLOCK_PHONY(26, 97)
+	COLUMN_RENDER_BLOCK_PHONY(28, 105)
+	COLUMN_RENDER_BLOCK_PHONY(30, 113)
+	COLUMN_RENDER_BLOCK_REFRESH_PHONY(32, 121)
+	case 129:
+		external_slot(context);
+		CHECK_LIMIT
+	case 130: {
+		external_slot(context);
+		CHECK_LIMIT
+	}
+	//sprite render to line buffer starts
+	case 131:
+		context->cur_slot = MAX_SPRITES_LINE_H32-1;
+		context->flags &= ~FLAG_MASKED;
+		while (context->sprite_draws) {
+			context->sprite_draws--;
+			context->sprite_draw_list[context->sprite_draws].x_pos = 0;
+		}
+		render_sprite_cells(context);
+		CHECK_LIMIT
+	case 132:
+		render_sprite_cells(context);
+		if (context->flags & FLAG_DMA_RUN) {
+			run_dma_src(context, -1);
+		}
+		context->hslot++;
+		context->cycles += slot_cycles;
+		vdp_advance_line(context);
+		CHECK_ONLY
+	}
+	default:
+		context->hslot++;
+		context->cycles += MCLKS_SLOT_H32;
+	}
+}
+
 static void vdp_h32_mode4(vdp_context * context, uint32_t target_cycles)
 {
 	uint16_t address;
@@ -4954,30 +5401,172 @@
 	}
 }
 
+static void vdp_inactive_phony(vdp_context *context, uint32_t target_cycles, uint8_t is_h40, uint8_t mode_5)
+{
+	uint8_t buf_clear_slot, index_reset_slot, bg_end_slot, vint_slot, line_change, jump_start, jump_dest, latch_slot;
+	uint8_t index_reset_value, max_draws, max_sprites;
+	uint16_t vint_line, active_line;
+
+	if (mode_5) {
+		if (is_h40) {
+			latch_slot = 165;
+			buf_clear_slot = 163;
+			index_reset_slot = 167;
+			bg_end_slot = BG_START_SLOT + LINEBUF_SIZE/2;
+			max_draws = MAX_SPRITES_LINE-1;
+			max_sprites = MAX_SPRITES_LINE;
+			index_reset_value = 0x80;
+			vint_slot = VINT_SLOT_H40;
+			line_change = LINE_CHANGE_H40;
+			jump_start = 182;
+			jump_dest = 229;
+		} else {
+			bg_end_slot = BG_START_SLOT + (256+HORIZ_BORDER)/2;
+			max_draws = MAX_SPRITES_LINE_H32-1;
+			max_sprites = MAX_SPRITES_LINE_H32;
+			buf_clear_slot = 128;
+			index_reset_slot = 132;
+			index_reset_value = 0x80;
+			vint_slot = VINT_SLOT_H32;
+			line_change = LINE_CHANGE_H32;
+			jump_start = 147;
+			jump_dest = 233;
+			latch_slot = 243;
+		}
+		vint_line = context->inactive_start;
+		active_line = 0x1FF;
+		if (context->regs[REG_MODE_3] & BIT_VSCROLL) {
+			latch_slot = 220;
+		}
+	} else {
+		latch_slot = 220;
+		bg_end_slot = BG_START_SLOT + (256+HORIZ_BORDER)/2;
+		max_draws = MAX_DRAWS_H32_MODE4;
+		max_sprites = 8;
+		buf_clear_slot = 136;
+		index_reset_slot = 253;
+		index_reset_value = 0;
+		vint_line = context->inactive_start + 1;
+		vint_slot = VINT_SLOT_MODE4;
+		line_change = LINE_CHANGE_MODE4;
+		jump_start = 147;
+		jump_dest = 233;
+		if ((context->regs[REG_MODE_1] & BIT_MODE_4) || context->type != VDP_GENESIS) {
+			active_line = 0x1FF;
+		} else {
+			//never active unless either mode 4 or mode 5 is turned on
+			active_line = 0x200;
+		}
+	}
+
+	while(context->cycles < target_cycles)
+	{
+		check_switch_inactive(context, is_h40);
+		//this will need some tweaking to properly interact with 128K mode,
+		//but this should be good enough for now
+		context->serial_address += 1024;
+
+		if (context->hslot == buf_clear_slot) {
+			if (mode_5) {
+				context->cur_slot = max_draws;
+			} else if ((context->regs[REG_MODE_1] & BIT_MODE_4) || context->type == VDP_GENESIS) {
+				context->cur_slot = context->sprite_index = MAX_DRAWS_H32_MODE4-1;
+				context->sprite_draws = MAX_DRAWS_H32_MODE4;
+			} else {
+				context->sprite_draws = 0;
+			}
+			memset(context->linebuf, 0, LINEBUF_SIZE);
+		} else if (context->hslot == index_reset_slot) {
+			context->sprite_index = index_reset_value;
+			context->slot_counter = mode_5 ? 0 : max_sprites;
+		} else if (context->vcounter == vint_line && context->hslot == vint_slot) {
+			context->flags2 |= FLAG2_VINT_PENDING;
+			context->pending_vint_start = context->cycles;
+		} else if (context->vcounter == context->inactive_start && context->hslot == 1 && (context->regs[REG_MODE_4] & BIT_INTERLACE)) {
+			context->flags2 ^= FLAG2_EVEN_FIELD;
+		}
+
+		if (!is_refresh(context, context->hslot)) {
+			external_slot(context);
+			if (context->flags & FLAG_DMA_RUN && !is_refresh(context, context->hslot)) {
+				run_dma_src(context, context->hslot);
+			}
+		}
+
+		if (is_h40) {
+			if (context->hslot >= HSYNC_SLOT_H40 && context->hslot < HSYNC_END_H40) {
+				context->cycles += h40_hsync_cycles[context->hslot - HSYNC_SLOT_H40];
+			} else {
+				context->cycles += MCLKS_SLOT_H40;
+			}
+		} else {
+			context->cycles += MCLKS_SLOT_H32;
+		}
+		if (context->hslot == jump_start) {
+			context->hslot = jump_dest;
+		} else {
+			context->hslot++;
+		}
+		if (context->hslot == line_change) {
+			vdp_advance_line(context);
+			if (context->vcounter == active_line) {
+				context->state = PREPARING;
+				return;
+			}
+		}
+	}
+}
+
 void vdp_run_context_full(vdp_context * context, uint32_t target_cycles)
 {
 	uint8_t is_h40 = context->regs[REG_MODE_4] & BIT_H40;
 	uint8_t mode_5 = context->regs[REG_MODE_2] & BIT_MODE_5;
-	while(context->cycles < target_cycles)
-	{
-		check_switch_inactive(context, is_h40);
-
-		if (is_active(context)) {
-			if (mode_5) {
-				if (is_h40) {
-					vdp_h40(context, target_cycles);
+	if (context->renderer) {
+		while(context->cycles < target_cycles)
+		{
+			check_switch_inactive(context, is_h40);
+
+			if (is_active(context)) {
+				if (mode_5) {
+					if (is_h40) {
+						vdp_h40_phony(context, target_cycles);
+					} else {
+						vdp_h32_phony(context, target_cycles);
+					}
+				} else if (context->regs[REG_MODE_1] & BIT_MODE_4) {
+					//TODO: phonyfy this
+					vdp_h32_mode4(context, target_cycles);
+				} else if (context->regs[REG_MODE_2] & BIT_M1) {
+						vdp_tms_text(context, target_cycles);
 				} else {
-					vdp_h32(context, target_cycles);
+					vdp_tms_graphics(context, target_cycles);
 				}
-			} else if (context->regs[REG_MODE_1] & BIT_MODE_4) {
-				vdp_h32_mode4(context, target_cycles);
-			} else if (context->regs[REG_MODE_2] & BIT_M1) {
-					vdp_tms_text(context, target_cycles);
 			} else {
-				vdp_tms_graphics(context, target_cycles);
+				vdp_inactive_phony(context, target_cycles, is_h40, mode_5);
 			}
-		} else {
-			vdp_inactive(context, target_cycles, is_h40, mode_5);
+		}
+	} else {
+		while(context->cycles < target_cycles)
+		{
+			check_switch_inactive(context, is_h40);
+
+			if (is_active(context)) {
+				if (mode_5) {
+					if (is_h40) {
+						vdp_h40(context, target_cycles);
+					} else {
+						vdp_h32(context, target_cycles);
+					}
+				} else if (context->regs[REG_MODE_1] & BIT_MODE_4) {
+					vdp_h32_mode4(context, target_cycles);
+				} else if (context->regs[REG_MODE_2] & BIT_M1) {
+						vdp_tms_text(context, target_cycles);
+				} else {
+					vdp_tms_graphics(context, target_cycles);
+				}
+			} else {
+				vdp_inactive(context, target_cycles, is_h40, mode_5);
+			}
 		}
 	}
 }
--- a/vdp.h	Sun Mar 30 00:06:53 2025 -0700
+++ b/vdp.h	Mon Mar 31 21:02:17 2025 -0700
@@ -183,6 +183,7 @@
 
 struct vdp_context {
 	system_header  *system;
+	struct vdp_context *renderer;
 	//pointer to current line in framebuffer
 	pixel_t        *output;
 	//pointer to current framebuffer