diff render_sdl.c @ 1648:b7ecd0d6a77b mame_interp

Merge from default
author Michael Pavone <pavone@retrodev.com>
date Tue, 25 Dec 2018 11:12:26 -0800
parents c6b2c0f8cc61
children b500e971da75
line wrap: on
line diff
--- a/render_sdl.c	Sun Dec 31 10:11:16 2017 -0800
+++ b/render_sdl.c	Tue Dec 25 11:12:26 2018 -0800
@@ -8,11 +8,15 @@
 #include <string.h>
 #include <math.h>
 #include "render.h"
+#include "render_sdl.h"
 #include "blastem.h"
 #include "genesis.h"
-#include "io.h"
+#include "bindings.h"
 #include "util.h"
 #include "ppm.h"
+#include "png.h"
+#include "config.h"
+#include "controller_info.h"
 
 #ifndef DISABLE_OPENGL
 #include <GL/glew.h>
@@ -21,7 +25,9 @@
 #define MAX_EVENT_POLL_PER_FRAME 2
 
 static SDL_Window *main_window;
+static SDL_Window **extra_windows;
 static SDL_Renderer *main_renderer;
+static SDL_Renderer **extra_renderers;
 static SDL_Texture  **sdl_textures;
 static uint8_t num_textures;
 static SDL_Rect      main_clip;
@@ -34,72 +40,193 @@
 
 static uint32_t last_frame = 0;
 
-static int16_t * current_psg = NULL;
-static int16_t * current_ym = NULL;
-
 static uint32_t buffer_samples, sample_rate;
 static uint32_t missing_count;
 
 static SDL_mutex * audio_mutex;
 static SDL_cond * audio_ready;
-static SDL_cond * psg_cond;
-static SDL_cond * ym_cond;
 static uint8_t quitting = 0;
-static uint8_t ym_enabled = 1;
+
+struct audio_source {
+	SDL_cond *cond;
+	int16_t  *front;
+	int16_t  *back;
+	double   dt;
+	uint64_t buffer_fraction;
+	uint64_t buffer_inc;
+	uint32_t buffer_pos;
+	uint32_t read_start;
+	uint32_t read_end;
+	uint32_t lowpass_alpha;
+	uint32_t mask;
+	int16_t  last_left;
+	int16_t  last_right;
+	uint8_t  num_channels;
+	uint8_t  front_populated;
+};
+
+static audio_source *audio_sources[8];
+static audio_source *inactive_audio_sources[8];
+static uint8_t num_audio_sources;
+static uint8_t num_inactive_audio_sources;
+static uint8_t sync_to_audio;
+static uint32_t min_buffered;
+
+typedef int32_t (*mix_func)(audio_source *audio, void *vstream, int len);
+
+static int32_t mix_s16(audio_source *audio, void *vstream, int len)
+{
+	int samples = len/(sizeof(int16_t)*2);
+	int16_t *stream = vstream;
+	int16_t *end = stream + 2*samples;
+	int16_t *src = audio->front;
+	uint32_t i = audio->read_start;
+	uint32_t i_end = audio->read_end;
+	int16_t *cur = stream;
+	if (audio->num_channels == 1) {
+		while (cur < end && i != i_end)
+		{
+			*(cur++) += src[i];
+			*(cur++) += src[i++];
+			i &= audio->mask;
+		}
+	} else {
+		while (cur < end && i != i_end)
+		{
+			*(cur++) += src[i++];
+			*(cur++) += src[i++];
+			i &= audio->mask;
+		}
+	}
+	
+	if (cur != end) {
+		printf("Underflow of %d samples, read_start: %d, read_end: %d, mask: %X\n", (int)(end-cur)/2, audio->read_start, audio->read_end, audio->mask);
+	}
+	if (!sync_to_audio) {
+		audio->read_start = i;
+	}
+	if (cur != end) {
+		//printf("Underflow of %d samples, read_start: %d, read_end: %d, mask: %X\n", (int)(end-cur)/2, audio->read_start, audio->read_end, audio->mask);
+		return (cur-end)/2;
+	} else {
+		return ((i_end - i) & audio->mask) / audio->num_channels;
+	}
+}
+
+static int32_t mix_f32(audio_source *audio, void *vstream, int len)
+{
+	int samples = len/(sizeof(float)*2);
+	float *stream = vstream;
+	float *end = stream + 2*samples;
+	int16_t *src = audio->front;
+	uint32_t i = audio->read_start;
+	uint32_t i_end = audio->read_end;
+	float *cur = stream;
+	if (audio->num_channels == 1) {
+		while (cur < end && i != i_end)
+		{
+			*(cur++) += ((float)src[i]) / 0x7FFF;
+			*(cur++) += ((float)src[i++]) / 0x7FFF;
+			i &= audio->mask;
+		}
+	} else {
+		while(cur < end && i != i_end)
+		{
+			*(cur++) += ((float)src[i++]) / 0x7FFF;
+			*(cur++) += ((float)src[i++]) / 0x7FFF;
+			i &= audio->mask;
+		}
+	}
+	if (!sync_to_audio) {
+		audio->read_start = i;
+	}
+	if (cur != end) {
+		printf("Underflow of %d samples, read_start: %d, read_end: %d, mask: %X\n", (int)(end-cur)/2, audio->read_start, audio->read_end, audio->mask);
+		return (cur-end)/2;
+	} else {
+		return ((i_end - i) & audio->mask) / audio->num_channels;
+	}
+}
+
+static int32_t mix_null(audio_source *audio, void *vstream, int len)
+{
+	return 0;
+}
+
+static mix_func mix;
 
 static void audio_callback(void * userdata, uint8_t *byte_stream, int len)
 {
-	//puts("audio_callback");
-	int16_t * stream = (int16_t *)byte_stream;
-	int samples = len/(sizeof(int16_t)*2);
-	int16_t * psg_buf, * ym_buf;
-	uint8_t local_quit;
+	uint8_t num_populated;
+	memset(byte_stream, 0, len);
 	SDL_LockMutex(audio_mutex);
-		psg_buf = NULL;
-		ym_buf = NULL;
 		do {
-			if (!psg_buf) {
-				psg_buf = current_psg;
-				current_psg = NULL;
-				SDL_CondSignal(psg_cond);
+			num_populated = 0;
+			for (uint8_t i = 0; i < num_audio_sources; i++)
+			{
+				if (audio_sources[i]->front_populated) {
+					num_populated++;
+				}
 			}
-			if (ym_enabled && !ym_buf) {
-				ym_buf = current_ym;
-				current_ym = NULL;
-				SDL_CondSignal(ym_cond);
-			}
-			if (!quitting && (!psg_buf || (ym_enabled && !ym_buf))) {
+			if (!quitting && num_populated < num_audio_sources) {
+				fflush(stdout);
 				SDL_CondWait(audio_ready, audio_mutex);
 			}
-		} while(!quitting && (!psg_buf || (ym_enabled && !ym_buf)));
-
-		local_quit = quitting;
-	SDL_UnlockMutex(audio_mutex);
-	if (!local_quit) {
-		if (ym_enabled) {
-			for (int i = 0; i < samples; i++)
+		} while(!quitting && num_populated < num_audio_sources);
+		if (!quitting) {
+			for (uint8_t i = 0; i < num_audio_sources; i++)
 			{
-				*(stream++) = psg_buf[i] + *(ym_buf++);
-				*(stream++) = psg_buf[i] + *(ym_buf++);
-			}
-		} else {
-			for (int i = 0; i < samples; i++)
-			{
-				*(stream++) = psg_buf[i];
-				*(stream++) = psg_buf[i];
+				mix(audio_sources[i], byte_stream, len);
+				audio_sources[i]->front_populated = 0;
+				SDL_CondSignal(audio_sources[i]->cond);
 			}
 		}
+	SDL_UnlockMutex(audio_mutex);
+}
+
+#define NO_LAST_BUFFERED -2000000000
+static int32_t last_buffered = NO_LAST_BUFFERED;
+static float average_change;
+#define BUFFER_FRAMES_THRESHOLD 6
+#define BASE_MAX_ADJUST 0.0125
+static float max_adjust;
+static int32_t cur_min_buffered;
+static uint32_t min_remaining_buffer;
+static void audio_callback_drc(void *userData, uint8_t *byte_stream, int len)
+{
+	memset(byte_stream, 0, len);
+	if (cur_min_buffered < 0) {
+		//underflow last frame, but main thread hasn't gotten a chance to call SDL_PauseAudio yet
+		return;
+	}
+	cur_min_buffered = 0x7FFFFFFF;
+	min_remaining_buffer = 0xFFFFFFFF;
+	for (uint8_t i = 0; i < num_audio_sources; i++)
+	{
+		
+		int32_t buffered = mix(audio_sources[i], byte_stream, len);
+		cur_min_buffered = buffered < cur_min_buffered ? buffered : cur_min_buffered;
+		uint32_t remaining = (audio_sources[i]->mask + 1)/audio_sources[i]->num_channels - buffered;
+		min_remaining_buffer = remaining < min_remaining_buffer ? remaining : min_remaining_buffer;
 	}
 }
 
-void render_disable_ym()
+static void lock_audio()
 {
-	ym_enabled = 0;
+	if (sync_to_audio) {
+		SDL_LockMutex(audio_mutex);
+	} else {
+		SDL_LockAudio();
+	}
 }
 
-void render_enable_ym()
+static void unlock_audio()
 {
-	ym_enabled = 1;
+	if (sync_to_audio) {
+		SDL_UnlockMutex(audio_mutex);
+	} else {
+		SDL_UnlockAudio();
+	}
 }
 
 static void render_close_audio()
@@ -111,6 +238,184 @@
 	SDL_CloseAudio();
 }
 
+#define BUFFER_INC_RES 0x40000000UL
+
+void render_audio_adjust_clock(audio_source *src, uint64_t master_clock, uint64_t sample_divider)
+{
+	src->buffer_inc = ((BUFFER_INC_RES * (uint64_t)sample_rate) / master_clock) * sample_divider;
+}
+
+audio_source *render_audio_source(uint64_t master_clock, uint64_t sample_divider, uint8_t channels)
+{
+	audio_source *ret = NULL;
+	uint32_t alloc_size = sync_to_audio ? channels * buffer_samples : nearest_pow2(min_buffered * 4 * channels);
+	lock_audio();
+		if (num_audio_sources < 8) {
+			ret = malloc(sizeof(audio_source));
+			ret->back = malloc(alloc_size * sizeof(int16_t));
+			ret->front = sync_to_audio ? malloc(alloc_size * sizeof(int16_t)) : ret->back;
+			ret->front_populated = 0;
+			ret->cond = SDL_CreateCond();
+			ret->num_channels = channels;
+			audio_sources[num_audio_sources++] = ret;
+		}
+	unlock_audio();
+	if (!ret) {
+		fatal_error("Too many audio sources!");
+	} else {
+		render_audio_adjust_clock(ret, master_clock, sample_divider);
+		double lowpass_cutoff = get_lowpass_cutoff(config);
+		double rc = (1.0 / lowpass_cutoff) / (2.0 * M_PI);
+		ret->dt = 1.0 / ((double)master_clock / (double)(sample_divider));
+		double alpha = ret->dt / (ret->dt + rc);
+		ret->lowpass_alpha = (int32_t)(((double)0x10000) * alpha);
+		ret->buffer_pos = 0;
+		ret->buffer_fraction = 0;
+		ret->last_left = ret->last_right = 0;
+		ret->read_start = 0;
+		ret->read_end = sync_to_audio ? buffer_samples * channels : 0;
+		ret->mask = sync_to_audio ? 0xFFFFFFFF : alloc_size-1;
+	}
+	if (sync_to_audio && SDL_GetAudioStatus() == SDL_AUDIO_PAUSED) {
+		SDL_PauseAudio(0);
+	}
+	return ret;
+}
+
+void render_pause_source(audio_source *src)
+{
+	uint8_t need_pause = 0;
+	lock_audio();
+		for (uint8_t i = 0; i < num_audio_sources; i++)
+		{
+			if (audio_sources[i] == src) {
+				audio_sources[i] = audio_sources[--num_audio_sources];
+				if (sync_to_audio) {
+					SDL_CondSignal(audio_ready);
+				}
+				break;
+			}
+		}
+		if (!num_audio_sources) {
+			need_pause = 1;
+		}
+	unlock_audio();
+	if (need_pause) {
+		SDL_PauseAudio(1);
+	}
+	inactive_audio_sources[num_inactive_audio_sources++] = src;
+}
+
+void render_resume_source(audio_source *src)
+{
+	lock_audio();
+		if (num_audio_sources < 8) {
+			audio_sources[num_audio_sources++] = src;
+		}
+	unlock_audio();
+	for (uint8_t i = 0; i < num_inactive_audio_sources; i++)
+	{
+		if (inactive_audio_sources[i] == src) {
+			inactive_audio_sources[i] = inactive_audio_sources[--num_inactive_audio_sources];
+		}
+	}
+	if (sync_to_audio) {
+		SDL_PauseAudio(0);
+	}
+}
+
+void render_free_source(audio_source *src)
+{
+	render_pause_source(src);
+	
+	free(src->front);
+	if (sync_to_audio) {
+		free(src->back);
+		SDL_DestroyCond(src->cond);
+	}
+	free(src);
+}
+static uint32_t sync_samples;
+static void do_audio_ready(audio_source *src)
+{
+	if (sync_to_audio) {
+		SDL_LockMutex(audio_mutex);
+			while (src->front_populated) {
+				SDL_CondWait(src->cond, audio_mutex);
+			}
+			int16_t *tmp = src->front;
+			src->front = src->back;
+			src->back = tmp;
+			src->front_populated = 1;
+			src->buffer_pos = 0;
+			SDL_CondSignal(audio_ready);
+		SDL_UnlockMutex(audio_mutex);
+	} else {
+		uint32_t num_buffered;
+		SDL_LockAudio();
+			src->read_end = src->buffer_pos;
+			num_buffered = ((src->read_end - src->read_start) & src->mask) / src->num_channels;
+		SDL_UnlockAudio();
+		if (num_buffered >= min_buffered && SDL_GetAudioStatus() == SDL_AUDIO_PAUSED) {
+			SDL_PauseAudio(0);
+		}
+	}
+}
+
+static int16_t lowpass_sample(audio_source *src, int16_t last, int16_t current)
+{
+	int32_t tmp = current * src->lowpass_alpha + last * (0x10000 - src->lowpass_alpha);
+	current = tmp >> 16;
+	return current;
+}
+
+static void interp_sample(audio_source *src, int16_t last, int16_t current)
+{
+	int64_t tmp = last * ((src->buffer_fraction << 16) / src->buffer_inc);
+	tmp += current * (0x10000 - ((src->buffer_fraction << 16) / src->buffer_inc));
+	src->back[src->buffer_pos++] = tmp >> 16;
+}
+
+void render_put_mono_sample(audio_source *src, int16_t value)
+{
+	value = lowpass_sample(src, src->last_left, value);
+	src->buffer_fraction += src->buffer_inc;
+	uint32_t base = sync_to_audio ? 0 : src->read_end;
+	while (src->buffer_fraction > BUFFER_INC_RES)
+	{
+		src->buffer_fraction -= BUFFER_INC_RES;
+		interp_sample(src, src->last_left, value);
+		
+		if (((src->buffer_pos - base) & src->mask) >= sync_samples) {
+			do_audio_ready(src);
+		}
+		src->buffer_pos &= src->mask;
+	}
+	src->last_left = value;
+}
+
+void render_put_stereo_sample(audio_source *src, int16_t left, int16_t right)
+{
+	left = lowpass_sample(src, src->last_left, left);
+	right = lowpass_sample(src, src->last_right, right);
+	src->buffer_fraction += src->buffer_inc;
+	uint32_t base = sync_to_audio ? 0 : src->read_end;
+	while (src->buffer_fraction > BUFFER_INC_RES)
+	{
+		src->buffer_fraction -= BUFFER_INC_RES;
+		
+		interp_sample(src, src->last_left, left);
+		interp_sample(src, src->last_right, right);
+		
+		if (((src->buffer_pos - base) & src->mask)/2 >= sync_samples) {
+			do_audio_ready(src);
+		}
+		src->buffer_pos &= src->mask;
+	}
+	src->last_left = left;
+	src->last_right = right;
+}
+
 static SDL_Joystick * joysticks[MAX_JOYSTICKS];
 static int joystick_sdl_index[MAX_JOYSTICKS];
 
@@ -239,12 +544,20 @@
 	un_height = glGetUniformLocation(program, "height");
 	at_pos = glGetAttribLocation(program, "pos");
 }
+
+static void gl_teardown()
+{
+	glDeleteProgram(program);
+	glDeleteShader(vshader);
+	glDeleteShader(fshader);
+	glDeleteBuffers(2, buffers);
+	glDeleteTextures(3, textures);
+}
 #endif
 
+static uint8_t texture_init;
 static void render_alloc_surfaces()
 {
-	static uint8_t texture_init;
-
 	if (texture_init) {
 		return;
 	}
@@ -267,18 +580,32 @@
 #endif
 }
 
+static void free_surfaces(void)
+{
+	for (int i = 0; i < num_textures; i++)
+	{
+		if (sdl_textures[i]) {
+			SDL_DestroyTexture(sdl_textures[i]);
+		}
+	}
+	free(sdl_textures);
+	sdl_textures = NULL;
+	texture_init = 0;
+}
+
 static char * caption = NULL;
 static char * fps_caption = NULL;
 
 static void render_quit()
 {
 	render_close_audio();
-	for (int i = 0; i < num_textures; i++)
-	{
-		if (sdl_textures[i]) {
-			SDL_DestroyTexture(sdl_textures[i]);
-		}
+	free_surfaces();
+#ifndef DISABLE_OPENGL
+	if (render_gl) {
+		gl_teardown();
+		SDL_GL_DeleteContext(main_context);
 	}
+#endif
 }
 
 static float config_aspect()
@@ -338,520 +665,11 @@
 	}
 }
 
-static uint32_t overscan_top[NUM_VID_STD] = {2, 21};
-static uint32_t overscan_bot[NUM_VID_STD] = {1, 17};
-static uint32_t overscan_left[NUM_VID_STD] = {13, 13};
-static uint32_t overscan_right[NUM_VID_STD] = {14, 14};
-static vid_std video_standard = VID_NTSC;
-static char *vid_std_names[NUM_VID_STD] = {"ntsc", "pal"};
-void render_init(int width, int height, char * title, uint8_t fullscreen)
-{
-	if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER) < 0) {
-		fatal_error("Unable to init SDL: %s\n", SDL_GetError());
-	}
-	atexit(SDL_Quit);
-	if (height <= 0) {
-		float aspect = config_aspect() > 0.0f ? config_aspect() : 4.0f/3.0f;
-		height = ((float)width / aspect) + 0.5f;
-	}
-	printf("width: %d, height: %d\n", width, height);
-	windowed_width = width;
-	windowed_height = height;
-	
-	uint32_t flags = SDL_WINDOW_RESIZABLE;
-
-	if (fullscreen) {
-		flags |= SDL_WINDOW_FULLSCREEN_DESKTOP;
-		SDL_DisplayMode mode;
-		//TODO: Multiple monitor support
-		SDL_GetCurrentDisplayMode(0, &mode);
-		//the SDL2 migration guide suggests setting width and height to 0 when using SDL_WINDOW_FULLSCREEN_DESKTOP
-		//but that doesn't seem to work right when using OpenGL, at least on Linux anyway
-		width = mode.w;
-		height = mode.h;
-	}
-	main_width = width;
-	main_height = height;
-	is_fullscreen = fullscreen;
-
-	render_gl = 0;
-	tern_val def = {.ptrval = "off"};
-	char *vsync = tern_find_path_default(config, "video\0vsync\0", def, TVAL_PTR).ptrval;
-	
-	tern_node *video = tern_find_node(config, "video");
-	if (video)
-	{
-		for (int i = 0; i < NUM_VID_STD; i++)
-		{
-			tern_node *std_settings = tern_find_node(video, vid_std_names[i]);
-			if (std_settings) {
-				char *val = tern_find_path_default(std_settings, "overscan\0top\0", (tern_val){.ptrval = NULL}, TVAL_PTR).ptrval;
-				if (val) {
-					overscan_top[i] = atoi(val);
-				}
-				val = tern_find_path_default(std_settings, "overscan\0bottom\0", (tern_val){.ptrval = NULL}, TVAL_PTR).ptrval;
-				if (val) {
-					overscan_bot[i] = atoi(val);
-				}
-				val = tern_find_path_default(std_settings, "overscan\0left\0", (tern_val){.ptrval = NULL}, TVAL_PTR).ptrval;
-				if (val) {
-					overscan_left[i] = atoi(val);
-				}
-				val = tern_find_path_default(std_settings, "overscan\0right\0", (tern_val){.ptrval = NULL}, TVAL_PTR).ptrval;
-				if (val) {
-					overscan_right[i] = atoi(val);
-				}
-			}
-		}
-	}
-
-#ifndef DISABLE_OPENGL
-	char *gl_enabled_str = tern_find_path_default(config, "video\0gl\0", def, TVAL_PTR).ptrval;
-	uint8_t gl_enabled = strcmp(gl_enabled_str, "off") != 0;
-	if (gl_enabled)
-	{
-		flags |= SDL_WINDOW_OPENGL;
-		SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 5);
-		SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 5);
-		SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 5);
-		SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 0);
-		SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
-	}
-#endif
-	main_window = SDL_CreateWindow(title, SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, width, height, flags);
-	if (!main_window) {
-		fatal_error("Unable to create SDL window: %s\n", SDL_GetError());
-	}
-#ifndef DISABLE_OPENGL
-	if (gl_enabled)
-	{
-		main_context = SDL_GL_CreateContext(main_window);
-		GLenum res = glewInit();
-		if (res != GLEW_OK) {
-			warning("Initialization of GLEW failed with code %d\n", res);
-		}
-
-		if (res == GLEW_OK && GLEW_VERSION_2_0) {
-			render_gl = 1;
-			if (!strcmp("tear", vsync)) {
-				if (SDL_GL_SetSwapInterval(-1) < 0) {
-					warning("late tear is not available (%s), using normal vsync\n", SDL_GetError());
-					vsync = "on";
-				} else {
-					vsync = NULL;
-				}
-			}
-			if (vsync) {
-				if (SDL_GL_SetSwapInterval(!strcmp("on", vsync)) < 0) {
-					warning("Failed to set vsync to %s: %s\n", vsync, SDL_GetError());
-				}
-			}
-		} else {
-			warning("OpenGL 2.0 is unavailable, falling back to SDL2 renderer\n");
-		}
-	}
-	if (!render_gl) {
-#endif
-		flags = SDL_RENDERER_ACCELERATED;
-		if (!strcmp("on", vsync) || !strcmp("tear", vsync)) {
-			flags |= SDL_RENDERER_PRESENTVSYNC;
-		}
-		main_renderer = SDL_CreateRenderer(main_window, -1, flags);
-
-		if (!main_renderer) {
-			fatal_error("unable to create SDL renderer: %s\n", SDL_GetError());
-		}
-		main_clip.x = main_clip.y = 0;
-		main_clip.w = width;
-		main_clip.h = height;
-#ifndef DISABLE_OPENGL
-	}
-#endif
-
-	SDL_GetWindowSize(main_window, &main_width, &main_height);
-	printf("Window created with size: %d x %d\n", main_width, main_height);
-	update_aspect();
-	render_alloc_surfaces();
-	def.ptrval = "off";
-	scanlines = !strcmp(tern_find_path_default(config, "video\0scanlines\0", def, TVAL_PTR).ptrval, "on");
-
-	caption = title;
-
-	audio_mutex = SDL_CreateMutex();
-	psg_cond = SDL_CreateCond();
-	ym_cond = SDL_CreateCond();
-	audio_ready = SDL_CreateCond();
-
-	SDL_AudioSpec desired, actual;
-    char * rate_str = tern_find_path(config, "audio\0rate\0", TVAL_PTR).ptrval;
-   	int rate = rate_str ? atoi(rate_str) : 0;
-   	if (!rate) {
-   		rate = 48000;
-   	}
-    desired.freq = rate;
-	desired.format = AUDIO_S16SYS;
-	desired.channels = 2;
-    char * samples_str = tern_find_path(config, "audio\0buffer\0", TVAL_PTR).ptrval;
-   	int samples = samples_str ? atoi(samples_str) : 0;
-   	if (!samples) {
-   		samples = 512;
-   	}
-    printf("config says: %d\n", samples);
-    desired.samples = samples*2;
-	desired.callback = audio_callback;
-	desired.userdata = NULL;
-
-	if (SDL_OpenAudio(&desired, &actual) < 0) {
-		fatal_error("Unable to open SDL audio: %s\n", SDL_GetError());
-	}
-	buffer_samples = actual.samples;
-	sample_rate = actual.freq;
-	printf("Initialized audio at frequency %d with a %d sample buffer\n", actual.freq, actual.samples);
-	SDL_PauseAudio(0);
-	
-	uint32_t db_size;
-	char *db_data = read_bundled_file("gamecontrollerdb.txt", &db_size);
-	if (db_data) {
-		int added = SDL_GameControllerAddMappingsFromRW(SDL_RWFromMem(db_data, db_size), 1);
-		free(db_data);
-		printf("Added %d game controller mappings from gamecontrollerdb.txt\n", added);
-	}
-	
-	SDL_JoystickEventState(SDL_ENABLE);
-
-	atexit(render_quit);
-}
-
-void render_set_video_standard(vid_std std)
-{
-	video_standard = std;
-}
-
-void render_update_caption(char *title)
-{
-	caption = title;
-	free(fps_caption);
-	fps_caption = NULL;
-}
-
-static char *screenshot_path;
-void render_save_screenshot(char *path)
-{
-	if (screenshot_path) {
-		free(screenshot_path);
-	}
-	screenshot_path = path;
-}
-
-uint32_t *locked_pixels;
-uint32_t locked_pitch;
-uint32_t *render_get_framebuffer(uint8_t which, int *pitch)
-{
-#ifndef DISABLE_OPENGL
-	if (render_gl && which <= FRAMEBUFFER_EVEN) {
-		*pitch = LINEBUF_SIZE * sizeof(uint32_t);
-		return texture_buf;
-	} else {
-#endif
-		if (which >= num_textures) {
-			warning("Request for invalid framebuffer number %d\n", which);
-			return NULL;
-		}
-		void *pixels;
-		if (SDL_LockTexture(sdl_textures[which], NULL, &pixels, pitch) < 0) {
-			warning("Failed to lock texture: %s\n", SDL_GetError());
-			return NULL;
-		}
-		static uint8_t last;
-		if (which <= FRAMEBUFFER_EVEN) {
-			locked_pixels = pixels;
-			if (which == FRAMEBUFFER_EVEN) {
-				pixels += *pitch;
-			}
-			locked_pitch = *pitch;
-			if (which != last) {
-				*pitch *= 2;
-			}
-			last = which;
-		}
-		return pixels;
-#ifndef DISABLE_OPENGL
-	}
-#endif
-}
-
-uint8_t events_processed;
-#ifdef __ANDROID__
-#define FPS_INTERVAL 10000
-#else
-#define FPS_INTERVAL 1000
-#endif
-
-static uint32_t last_width;
-void render_framebuffer_updated(uint8_t which, int width)
+static ui_render_fun on_context_destroyed, on_context_created;
+void render_set_gl_context_handlers(ui_render_fun destroy, ui_render_fun create)
 {
-	static uint8_t last;
-	last_width = width;
-	uint32_t height = which <= FRAMEBUFFER_EVEN 
-		? (video_standard == VID_NTSC ? 243 : 294) - (overscan_top[video_standard] + overscan_bot[video_standard])
-		: 240;
-	FILE *screenshot_file = NULL;
-	uint32_t shot_height, shot_width;
-	if (screenshot_path && which == FRAMEBUFFER_ODD) {
-		screenshot_file = fopen(screenshot_path, "wb");
-		if (screenshot_file) {
-			info_message("Saving screenshot to %s\n", screenshot_path);
-		} else {
-			warning("Failed to open screenshot file %s for writing\n", screenshot_path);
-		}
-		free(screenshot_path);
-		screenshot_path = NULL;
-		shot_height = video_standard == VID_NTSC ? 243 : 294;
-		shot_width = width;
-	}
-	width -= overscan_left[video_standard] + overscan_right[video_standard];
-#ifndef DISABLE_OPENGL
-	if (render_gl && which <= FRAMEBUFFER_EVEN) {
-		glBindTexture(GL_TEXTURE_2D, textures[which]);
-		glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, LINEBUF_SIZE, height, GL_BGRA, GL_UNSIGNED_BYTE, texture_buf + overscan_left[video_standard] + LINEBUF_SIZE * overscan_top[video_standard]);
-
-		glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
-		glClear(GL_COLOR_BUFFER_BIT);
-
-		glUseProgram(program);
-		glActiveTexture(GL_TEXTURE0);
-		glBindTexture(GL_TEXTURE_2D, textures[0]);
-		glUniform1i(un_textures[0], 0);
-
-		glActiveTexture(GL_TEXTURE1);
-		glBindTexture(GL_TEXTURE_2D, textures[last != which ? 1 : scanlines ? 2 : 0]);
-		glUniform1i(un_textures[1], 1);
-
-		glUniform1f(un_width, width);
-		glUniform1f(un_height, height);
-
-		glBindBuffer(GL_ARRAY_BUFFER, buffers[0]);
-		glVertexAttribPointer(at_pos, 2, GL_FLOAT, GL_FALSE, sizeof(GLfloat[2]), (void *)0);
-		glEnableVertexAttribArray(at_pos);
-
-		glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, buffers[1]);
-		glDrawElements(GL_TRIANGLE_STRIP, 4, GL_UNSIGNED_SHORT, (void *)0);
-
-		glDisableVertexAttribArray(at_pos);
-
-		SDL_GL_SwapWindow(main_window);
-		
-		if (screenshot_file) {
-			//properly supporting interlaced modes here is non-trivial, so only save the odd field for now
-			save_ppm(screenshot_file, texture_buf, shot_width, shot_height, LINEBUF_SIZE*sizeof(uint32_t));
-		}
-	} else {
-#endif
-		if (which <= FRAMEBUFFER_EVEN && last != which) {
-			uint8_t *cur_dst = (uint8_t *)locked_pixels;
-			uint8_t *cur_saved = (uint8_t *)texture_buf;
-			uint32_t dst_off = which == FRAMEBUFFER_EVEN ? 0 : locked_pitch;
-			uint32_t src_off = which == FRAMEBUFFER_EVEN ? locked_pitch : 0;
-			for (int i = 0; i < height; ++i)
-			{
-				//copy saved line from other field
-				memcpy(cur_dst + dst_off, cur_saved, locked_pitch);
-				//save line from this field to buffer for next frame
-				memcpy(cur_saved, cur_dst + src_off, locked_pitch);
-				cur_dst += locked_pitch * 2;
-				cur_saved += locked_pitch;
-			}
-			height = 480;
-		}
-		if (screenshot_file) {
-			uint32_t shot_pitch = locked_pitch;
-			if (which == FRAMEBUFFER_EVEN) {
-				shot_height *= 2;
-			} else {
-				shot_pitch *= 2;
-			}
-			save_ppm(screenshot_file, locked_pixels, shot_width, shot_height, shot_pitch);
-		}
-		SDL_UnlockTexture(sdl_textures[which]);
-		SDL_Rect src_clip = {
-			.x = overscan_left[video_standard],
-			.y = overscan_top[video_standard],
-			.w = width,
-			.h = height
-		};
-		SDL_SetRenderDrawColor(main_renderer, 0, 0, 0, 255);
-		SDL_RenderClear(main_renderer);
-		SDL_RenderCopy(main_renderer, sdl_textures[which], &src_clip, &main_clip);
-		SDL_RenderPresent(main_renderer);
-#ifndef DISABLE_OPENGL
-	}
-#endif
-	if (screenshot_file) {
-		fclose(screenshot_file);
-	}
-	if (which <= FRAMEBUFFER_EVEN) {
-		last = which;
-		static uint32_t frame_counter, start;
-		frame_counter++;
-		last_frame= SDL_GetTicks();
-		if ((last_frame - start) > FPS_INTERVAL) {
-			if (start && (last_frame-start)) {
-	#ifdef __ANDROID__
-				info_message("%s - %.1f fps", caption, ((float)frame_counter) / (((float)(last_frame-start)) / 1000.0));
-	#else
-				if (!fps_caption) {
-					fps_caption = malloc(strlen(caption) + strlen(" - 100000000.1 fps") + 1);
-				}
-				sprintf(fps_caption, "%s - %.1f fps", caption, ((float)frame_counter) / (((float)(last_frame-start)) / 1000.0));
-				SDL_SetWindowTitle(main_window, fps_caption);
-	#endif
-			}
-			start = last_frame;
-			frame_counter = 0;
-		}
-	}
-	if (!events_processed) {
-		process_events();
-	}
-	events_processed = 0;
-}
-
-uint32_t render_emulated_width()
-{
-	return last_width - overscan_left[video_standard] - overscan_right[video_standard];
-}
-
-uint32_t render_emulated_height()
-{
-	return (video_standard == VID_NTSC ? 243 : 294) - overscan_top[video_standard] - overscan_bot[video_standard];
-}
-
-uint32_t render_overscan_left()
-{
-	return overscan_left[video_standard];
-}
-
-uint32_t render_overscan_top()
-{
-	return overscan_top[video_standard];
-}
-
-void render_wait_quit(vdp_context * context)
-{
-	SDL_Event event;
-	while(SDL_WaitEvent(&event)) {
-		switch (event.type) {
-		case SDL_QUIT:
-			return;
-		}
-	}
-}
-
-static int find_joystick_index(SDL_JoystickID instanceID)
-{
-	for (int i = 0; i < MAX_JOYSTICKS; i++) {
-		if (joysticks[i] && SDL_JoystickInstanceID(joysticks[i]) == instanceID) {
-			return i;
-		}
-	}
-	return -1;
-}
-
-static int lowest_unused_joystick_index()
-{
-	for (int i = 0; i < MAX_JOYSTICKS; i++) {
-		if (!joysticks[i]) {
-			return i;
-		}
-	}
-	return -1;
-}
-
-int32_t render_translate_input_name(int32_t controller, char *name, uint8_t is_axis)
-{
-	static tern_node *button_lookup, *axis_lookup;
-	if (controller > MAX_JOYSTICKS || !joysticks[controller]) {
-		return RENDER_NOT_PLUGGED_IN;
-	}
-	
-	if (!SDL_IsGameController(joystick_sdl_index[controller])) {
-		return RENDER_NOT_MAPPED;
-	}
-	SDL_GameController *control = SDL_GameControllerOpen(joystick_sdl_index[controller]);
-	if (!control) {
-		warning("Failed to open game controller %d: %s\n", controller, SDL_GetError());
-		return RENDER_NOT_PLUGGED_IN;
-	}
-	
-	SDL_GameControllerButtonBind cbind;
-	if (is_axis) {
-		if (!axis_lookup) {
-			for (int i = SDL_CONTROLLER_AXIS_LEFTX; i < SDL_CONTROLLER_AXIS_MAX; i++)
-			{
-				axis_lookup = tern_insert_int(axis_lookup, SDL_GameControllerGetStringForAxis(i), i);
-			}
-			//alternative Playstation-style names
-			axis_lookup = tern_insert_int(axis_lookup, "l2", SDL_CONTROLLER_AXIS_TRIGGERLEFT);
-			axis_lookup = tern_insert_int(axis_lookup, "r2", SDL_CONTROLLER_AXIS_TRIGGERRIGHT);
-		}
-		intptr_t sdl_axis = tern_find_int(axis_lookup, name, SDL_CONTROLLER_AXIS_INVALID);
-		if (sdl_axis == SDL_CONTROLLER_AXIS_INVALID) {
-			SDL_GameControllerClose(control);
-			return RENDER_INVALID_NAME;
-		}
-		cbind = SDL_GameControllerGetBindForAxis(control, sdl_axis);
-	} else {
-		if (!button_lookup) {
-			for (int i = SDL_CONTROLLER_BUTTON_A; i < SDL_CONTROLLER_BUTTON_MAX; i++)
-			{
-				button_lookup = tern_insert_int(button_lookup, SDL_GameControllerGetStringForButton(i), i);
-			}
-			//alternative Playstation-style names
-			button_lookup = tern_insert_int(button_lookup, "cross", SDL_CONTROLLER_BUTTON_A);
-			button_lookup = tern_insert_int(button_lookup, "circle", SDL_CONTROLLER_BUTTON_B);
-			button_lookup = tern_insert_int(button_lookup, "square", SDL_CONTROLLER_BUTTON_X);
-			button_lookup = tern_insert_int(button_lookup, "triangle", SDL_CONTROLLER_BUTTON_Y);
-			button_lookup = tern_insert_int(button_lookup, "share", SDL_CONTROLLER_BUTTON_BACK);
-			button_lookup = tern_insert_int(button_lookup, "select", SDL_CONTROLLER_BUTTON_BACK);
-			button_lookup = tern_insert_int(button_lookup, "options", SDL_CONTROLLER_BUTTON_START);
-			button_lookup = tern_insert_int(button_lookup, "l1", SDL_CONTROLLER_BUTTON_LEFTSHOULDER);
-			button_lookup = tern_insert_int(button_lookup, "r1", SDL_CONTROLLER_BUTTON_RIGHTSHOULDER);
-			button_lookup = tern_insert_int(button_lookup, "l3", SDL_CONTROLLER_BUTTON_LEFTSTICK);
-			button_lookup = tern_insert_int(button_lookup, "r3", SDL_CONTROLLER_BUTTON_RIGHTSTICK);
-		}
-		intptr_t sdl_button = tern_find_int(button_lookup, name, SDL_CONTROLLER_BUTTON_INVALID);
-		if (sdl_button == SDL_CONTROLLER_BUTTON_INVALID) {
-			SDL_GameControllerClose(control);
-			return RENDER_INVALID_NAME;
-		}
-		cbind = SDL_GameControllerGetBindForButton(control, sdl_button);
-	}
-	SDL_GameControllerClose(control);
-	switch (cbind.bindType)
-	{
-	case SDL_CONTROLLER_BINDTYPE_BUTTON:
-		return cbind.value.button;
-	case SDL_CONTROLLER_BINDTYPE_AXIS:
-		return RENDER_AXIS_BIT | cbind.value.axis;
-	case SDL_CONTROLLER_BINDTYPE_HAT:
-		return RENDER_DPAD_BIT | (cbind.value.hat.hat << 4) | cbind.value.hat.hat_mask;
-	}
-	return RENDER_NOT_MAPPED;
-}
-
-int32_t render_dpad_part(int32_t input)
-{
-	return input >> 4 & 0xFFFFFF;
-}
-
-uint8_t render_direction_part(int32_t input)
-{
-	return input & 0xF;
-}
-
-int32_t render_axis_part(int32_t input)
-{
-	return input & 0xFFFFFFF;
+	on_context_destroyed = destroy;
+	on_context_created = create;
 }
 
 static uint8_t scancode_map[SDL_NUM_SCANCODES] = {
@@ -964,8 +782,70 @@
 	drag_drop_handler = handler;
 }
 
+static event_handler custom_event_handler;
+void render_set_event_handler(event_handler handler)
+{
+	custom_event_handler = handler;
+}
+
+static int find_joystick_index(SDL_JoystickID instanceID)
+{
+	for (int i = 0; i < MAX_JOYSTICKS; i++) {
+		if (joysticks[i] && SDL_JoystickInstanceID(joysticks[i]) == instanceID) {
+			return i;
+		}
+	}
+	return -1;
+}
+
+static int lowest_unused_joystick_index()
+{
+	for (int i = 0; i < MAX_JOYSTICKS; i++) {
+		if (!joysticks[i]) {
+			return i;
+		}
+	}
+	return -1;
+}
+
+SDL_Joystick *render_get_joystick(int index)
+{
+	if (index >= MAX_JOYSTICKS) {
+		return NULL;
+	}
+	return joysticks[index];
+}
+
+char* render_joystick_type_id(int index)
+{
+	SDL_Joystick *stick = render_get_joystick(index);
+	if (!stick) {
+		return NULL;
+	}
+	char *guid_string = malloc(33);
+	SDL_JoystickGetGUIDString(SDL_JoystickGetGUID(stick), guid_string, 33);
+	return guid_string;
+}
+
+SDL_GameController *render_get_controller(int index)
+{
+	if (index >= MAX_JOYSTICKS) {
+		return NULL;
+	}
+	return SDL_GameControllerOpen(joystick_sdl_index[index]);
+}
+
+static uint32_t overscan_top[NUM_VID_STD] = {2, 21};
+static uint32_t overscan_bot[NUM_VID_STD] = {1, 17};
+static uint32_t overscan_left[NUM_VID_STD] = {13, 13};
+static uint32_t overscan_right[NUM_VID_STD] = {14, 14};
+static vid_std video_standard = VID_NTSC;
+
 static int32_t handle_event(SDL_Event *event)
 {
+	if (custom_event_handler) {
+		custom_event_handler(event);
+	}
 	switch (event->type) {
 	case SDL_KEYDOWN:
 		handle_keydown(event->key.keysym.sym, scancode_map[event->key.keysym.scancode]);
@@ -980,7 +860,7 @@
 		handle_joyup(find_joystick_index(event->jbutton.which), event->jbutton.button);
 		break;
 	case SDL_JOYHATMOTION:
-		handle_joy_dpad(find_joystick_index(event->jbutton.which), event->jhat.hat, event->jhat.value);
+		handle_joy_dpad(find_joystick_index(event->jhat.which), event->jhat.hat, event->jhat.value);
 		break;
 	case SDL_JOYAXISMOTION:
 		handle_joy_axis(find_joystick_index(event->jaxis.which), event->jaxis.axis, event->jaxis.value);
@@ -1028,9 +908,16 @@
 			update_aspect();
 #ifndef DISABLE_OPENGL
 			if (render_gl) {
+				if (on_context_destroyed) {
+					on_context_destroyed();
+				}
+				gl_teardown();
 				SDL_GL_DeleteContext(main_context);
 				main_context = SDL_GL_CreateContext(main_window);
 				gl_setup();
+				if (on_context_created) {
+					on_context_created();
+				}
 			}
 #endif
 			break;
@@ -1058,6 +945,869 @@
 	}
 }
 
+static char *vid_std_names[NUM_VID_STD] = {"ntsc", "pal"};
+static int display_hz;
+static int source_hz;
+static int source_frame;
+static int source_frame_count;
+static int frame_repeat[60];
+
+static void init_audio()
+{
+	SDL_AudioSpec desired, actual;
+    char * rate_str = tern_find_path(config, "audio\0rate\0", TVAL_PTR).ptrval;
+   	int rate = rate_str ? atoi(rate_str) : 0;
+   	if (!rate) {
+   		rate = 48000;
+   	}
+    desired.freq = rate;
+	desired.format = AUDIO_S16SYS;
+	desired.channels = 2;
+    char * samples_str = tern_find_path(config, "audio\0buffer\0", TVAL_PTR).ptrval;
+   	int samples = samples_str ? atoi(samples_str) : 0;
+   	if (!samples) {
+   		samples = 512;
+   	}
+    printf("config says: %d\n", samples);
+    desired.samples = samples*2;
+	desired.callback = sync_to_audio ? audio_callback : audio_callback_drc;
+	desired.userdata = NULL;
+
+	if (SDL_OpenAudio(&desired, &actual) < 0) {
+		fatal_error("Unable to open SDL audio: %s\n", SDL_GetError());
+	}
+	buffer_samples = actual.samples;
+	sample_rate = actual.freq;
+	printf("Initialized audio at frequency %d with a %d sample buffer, ", actual.freq, actual.samples);
+	if (actual.format == AUDIO_S16SYS) {
+		puts("signed 16-bit int format");
+		mix = mix_s16;
+	} else if (actual.format == AUDIO_F32SYS) {
+		puts("32-bit float format");
+		mix = mix_f32;
+	} else {
+		printf("unsupported format %X\n", actual.format);
+		warning("Unsupported audio sample format: %X\n", actual.format);
+		mix = mix_null;
+	}
+}
+
+void window_setup(void)
+{
+	uint32_t flags = SDL_WINDOW_RESIZABLE;
+	if (is_fullscreen) {
+		flags |= SDL_WINDOW_FULLSCREEN_DESKTOP;
+	}
+	
+	tern_val def = {.ptrval = "video"};
+	char *sync_src = tern_find_path_default(config, "system\0sync_source\0", def, TVAL_PTR).ptrval;
+	sync_to_audio = !strcmp(sync_src, "audio");
+	
+	const char *vsync;
+	if (sync_to_audio) {
+		def.ptrval = "off";
+		vsync = tern_find_path_default(config, "video\0vsync\0", def, TVAL_PTR).ptrval;
+	} else {
+		vsync = "on";
+	}
+	
+	tern_node *video = tern_find_node(config, "video");
+	if (video)
+	{
+		for (int i = 0; i < NUM_VID_STD; i++)
+		{
+			tern_node *std_settings = tern_find_node(video, vid_std_names[i]);
+			if (std_settings) {
+				char *val = tern_find_path_default(std_settings, "overscan\0top\0", (tern_val){.ptrval = NULL}, TVAL_PTR).ptrval;
+				if (val) {
+					overscan_top[i] = atoi(val);
+				}
+				val = tern_find_path_default(std_settings, "overscan\0bottom\0", (tern_val){.ptrval = NULL}, TVAL_PTR).ptrval;
+				if (val) {
+					overscan_bot[i] = atoi(val);
+				}
+				val = tern_find_path_default(std_settings, "overscan\0left\0", (tern_val){.ptrval = NULL}, TVAL_PTR).ptrval;
+				if (val) {
+					overscan_left[i] = atoi(val);
+				}
+				val = tern_find_path_default(std_settings, "overscan\0right\0", (tern_val){.ptrval = NULL}, TVAL_PTR).ptrval;
+				if (val) {
+					overscan_right[i] = atoi(val);
+				}
+			}
+		}
+	}
+	render_gl = 0;
+	
+#ifndef DISABLE_OPENGL
+	char *gl_enabled_str = tern_find_path_default(config, "video\0gl\0", def, TVAL_PTR).ptrval;
+	uint8_t gl_enabled = strcmp(gl_enabled_str, "off") != 0;
+	if (gl_enabled)
+	{
+		flags |= SDL_WINDOW_OPENGL;
+		SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 5);
+		SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 5);
+		SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 5);
+		SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 0);
+		SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
+	}
+#endif
+	main_window = SDL_CreateWindow(caption, SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, main_width, main_height, flags);
+	if (!main_window) {
+		fatal_error("Unable to create SDL window: %s\n", SDL_GetError());
+	}
+#ifndef DISABLE_OPENGL
+	if (gl_enabled)
+	{
+		main_context = SDL_GL_CreateContext(main_window);
+		GLenum res = glewInit();
+		if (res != GLEW_OK) {
+			warning("Initialization of GLEW failed with code %d\n", res);
+		}
+
+		if (res == GLEW_OK && GLEW_VERSION_2_0) {
+			render_gl = 1;
+			if (!strcmp("tear", vsync)) {
+				if (SDL_GL_SetSwapInterval(-1) < 0) {
+					warning("late tear is not available (%s), using normal vsync\n", SDL_GetError());
+					vsync = "on";
+				} else {
+					vsync = NULL;
+				}
+			}
+			if (vsync) {
+				if (SDL_GL_SetSwapInterval(!strcmp("on", vsync)) < 0) {
+					warning("Failed to set vsync to %s: %s\n", vsync, SDL_GetError());
+				}
+			}
+		} else {
+			warning("OpenGL 2.0 is unavailable, falling back to SDL2 renderer\n");
+		}
+	}
+	if (!render_gl) {
+#endif
+		flags = SDL_RENDERER_ACCELERATED;
+		if (!strcmp("on", vsync) || !strcmp("tear", vsync)) {
+			flags |= SDL_RENDERER_PRESENTVSYNC;
+		}
+		main_renderer = SDL_CreateRenderer(main_window, -1, flags);
+
+		if (!main_renderer) {
+			fatal_error("unable to create SDL renderer: %s\n", SDL_GetError());
+		}
+		main_clip.x = main_clip.y = 0;
+		main_clip.w = main_width;
+		main_clip.h = main_height;
+#ifndef DISABLE_OPENGL
+	}
+#endif
+
+	SDL_GetWindowSize(main_window, &main_width, &main_height);
+	printf("Window created with size: %d x %d\n", main_width, main_height);
+	update_aspect();
+	render_alloc_surfaces();
+	def.ptrval = "off";
+	scanlines = !strcmp(tern_find_path_default(config, "video\0scanlines\0", def, TVAL_PTR).ptrval, "on");
+}
+
+void render_init(int width, int height, char * title, uint8_t fullscreen)
+{
+	if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER) < 0) {
+		fatal_error("Unable to init SDL: %s\n", SDL_GetError());
+	}
+	atexit(SDL_Quit);
+	if (height <= 0) {
+		float aspect = config_aspect() > 0.0f ? config_aspect() : 4.0f/3.0f;
+		height = ((float)width / aspect) + 0.5f;
+	}
+	printf("width: %d, height: %d\n", width, height);
+	windowed_width = width;
+	windowed_height = height;
+	
+	SDL_DisplayMode mode;
+	//TODO: Explicit multiple monitor support
+	SDL_GetCurrentDisplayMode(0, &mode);
+	display_hz = mode.refresh_rate;
+
+	if (fullscreen) {
+		//the SDL2 migration guide suggests setting width and height to 0 when using SDL_WINDOW_FULLSCREEN_DESKTOP
+		//but that doesn't seem to work right when using OpenGL, at least on Linux anyway
+		width = mode.w;
+		height = mode.h;
+	}
+	main_width = width;
+	main_height = height;
+	is_fullscreen = fullscreen;
+	
+	caption = title;
+	
+	window_setup();
+
+	audio_mutex = SDL_CreateMutex();
+	audio_ready = SDL_CreateCond();
+	
+	init_audio();
+	
+	uint32_t db_size;
+	char *db_data = read_bundled_file("gamecontrollerdb.txt", &db_size);
+	if (db_data) {
+		int added = SDL_GameControllerAddMappingsFromRW(SDL_RWFromMem(db_data, db_size), 1);
+		free(db_data);
+		printf("Added %d game controller mappings from gamecontrollerdb.txt\n", added);
+	}
+	
+	controller_add_mappings();
+	
+	SDL_JoystickEventState(SDL_ENABLE);
+	
+	render_set_video_standard(VID_NTSC);
+
+	atexit(render_quit);
+}
+#include<unistd.h>
+static int in_toggle;
+static void update_source(audio_source *src, double rc, uint8_t sync_changed)
+{
+	double alpha = src->dt / (src->dt + rc);
+	int32_t lowpass_alpha = (int32_t)(((double)0x10000) * alpha);
+	src->lowpass_alpha = lowpass_alpha;
+	if (sync_changed) {
+		uint32_t alloc_size = sync_to_audio ? src->num_channels * buffer_samples : nearest_pow2(min_buffered * 4 * src->num_channels);
+		src->back = realloc(src->back, alloc_size * sizeof(int16_t));
+		if (sync_to_audio) {
+			src->front = malloc(alloc_size * sizeof(int16_t));
+		} else {
+			free(src->front);
+			src->front = src->back;
+		}
+		src->mask = sync_to_audio ? 0xFFFFFFFF : alloc_size-1;
+		src->read_start = 0;
+		src->read_end = sync_to_audio ? buffer_samples * src->num_channels : 0;
+		src->buffer_pos = 0;
+	}
+}
+
+void render_config_updated(void)
+{
+	uint8_t old_sync_to_audio = sync_to_audio;
+	
+	free_surfaces();
+#ifndef DISABLE_OPENGL
+	if (render_gl) {
+		if (on_context_destroyed) {
+			on_context_destroyed();
+		}
+		gl_teardown();
+		SDL_GL_DeleteContext(main_context);
+	} else {
+#endif
+		SDL_DestroyRenderer(main_renderer);
+#ifndef DISABLE_OPENGL
+	}
+#endif
+	in_toggle = 1;
+	SDL_DestroyWindow(main_window);
+	drain_events();
+	
+	char *config_width = tern_find_path(config, "video\0width\0", TVAL_PTR).ptrval;
+	if (config_width) {
+		windowed_width = atoi(config_width);
+	}
+	char *config_height = tern_find_path(config, "video\0height\0", TVAL_PTR).ptrval;
+	if (config_height) {
+		windowed_height = atoi(config_height);
+	} else {
+		float aspect = config_aspect() > 0.0f ? config_aspect() : 4.0f/3.0f;
+		windowed_height = ((float)windowed_width / aspect) + 0.5f;
+	}
+	char *config_fullscreen = tern_find_path(config, "video\0fullscreen\0", TVAL_PTR).ptrval;
+	is_fullscreen = config_fullscreen && !strcmp("on", config_fullscreen);
+	if (is_fullscreen) {
+		SDL_DisplayMode mode;
+		//TODO: Multiple monitor support
+		SDL_GetCurrentDisplayMode(0, &mode);
+		main_width = mode.w;
+		main_height = mode.h;
+	} else {
+		main_width = windowed_width;
+		main_height = windowed_height;
+	}
+	
+	window_setup();
+	update_aspect();
+#ifndef DISABLE_OPENGL
+	//need to check render_gl again after window_setup as render option could have changed
+	if (render_gl && on_context_created) {
+		on_context_created();
+	}
+#endif
+
+	uint8_t was_paused = SDL_GetAudioStatus() == SDL_AUDIO_PAUSED;
+	render_close_audio();
+	quitting = 0;
+	init_audio();
+	render_set_video_standard(video_standard);
+	
+	double lowpass_cutoff = get_lowpass_cutoff(config);
+	double rc = (1.0 / lowpass_cutoff) / (2.0 * M_PI);
+	lock_audio();
+		for (uint8_t i = 0; i < num_audio_sources; i++)
+		{
+			update_source(audio_sources[i], rc, old_sync_to_audio != sync_to_audio);
+		}
+	unlock_audio();
+	for (uint8_t i = 0; i < num_inactive_audio_sources; i++)
+	{
+		update_source(inactive_audio_sources[i], rc, old_sync_to_audio != sync_to_audio);
+	}
+	drain_events();
+	in_toggle = 0;
+	if (!was_paused) {
+		SDL_PauseAudio(0);
+	}
+}
+
+SDL_Window *render_get_window(void)
+{
+	return main_window;
+}
+
+void render_set_video_standard(vid_std std)
+{
+	video_standard = std;
+	source_hz = std == VID_PAL ? 50 : 60;
+	uint32_t max_repeat = 0;
+	if (abs(source_hz - display_hz) < 2) {
+		memset(frame_repeat, 0, sizeof(int)*display_hz);
+	} else {
+		int inc = display_hz * 100000 / source_hz;
+		int accum = 0;
+		int dst_frames = 0;
+		for (int src_frame = 0; src_frame < source_hz; src_frame++)
+		{
+			frame_repeat[src_frame] = -1;
+			accum += inc;
+			while (accum > 100000)
+			{
+				accum -= 100000;
+				frame_repeat[src_frame]++;
+				max_repeat = frame_repeat[src_frame] > max_repeat ? frame_repeat[src_frame] : max_repeat;
+				dst_frames++;
+			}
+		}
+		if (dst_frames != display_hz) {
+			frame_repeat[source_hz-1] += display_hz - dst_frames;
+		}
+	}
+	source_frame = 0;
+	source_frame_count = frame_repeat[0];
+	//sync samples with audio thread approximately every 8 lines
+	sync_samples = sync_to_audio ? buffer_samples : 8 * sample_rate / (source_hz * (VID_PAL ? 313 : 262));
+	max_repeat++;
+	min_buffered = (((float)max_repeat * (float)sample_rate/(float)source_hz)/* / (float)buffer_samples*/);// + 0.9999;
+	//min_buffered *= buffer_samples;
+	printf("Min samples buffered before audio start: %d\n", min_buffered);
+	max_adjust = BASE_MAX_ADJUST / source_hz;
+}
+
+void render_update_caption(char *title)
+{
+	caption = title;
+	free(fps_caption);
+	fps_caption = NULL;
+}
+
+static char *screenshot_path;
+void render_save_screenshot(char *path)
+{
+	if (screenshot_path) {
+		free(screenshot_path);
+	}
+	screenshot_path = path;
+}
+
+uint8_t render_create_window(char *caption, uint32_t width, uint32_t height)
+{
+	uint8_t win_idx = 0xFF;
+	for (int i = 0; i < num_textures - FRAMEBUFFER_USER_START; i++)
+	{
+		if (!extra_windows[i]) {
+			win_idx = i;
+			break;
+		}
+	}
+	
+	if (win_idx == 0xFF) {
+		num_textures++;
+		sdl_textures = realloc(sdl_textures, num_textures * sizeof(*sdl_textures));
+		extra_windows = realloc(extra_windows, (num_textures - FRAMEBUFFER_USER_START) * sizeof(*extra_windows));
+		extra_renderers = realloc(extra_renderers, (num_textures - FRAMEBUFFER_USER_START) * sizeof(*extra_renderers));
+		win_idx = num_textures - FRAMEBUFFER_USER_START - 1;
+	}
+	extra_windows[win_idx] = SDL_CreateWindow(caption, SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, width, height, 0);
+	if (!extra_windows[win_idx]) {
+		goto fail_window;
+	}
+	extra_renderers[win_idx] = SDL_CreateRenderer(extra_windows[win_idx], -1, SDL_RENDERER_ACCELERATED);
+	if (!extra_renderers[win_idx]) {
+		goto fail_renderer;
+	}
+	uint8_t texture_idx = win_idx + FRAMEBUFFER_USER_START;
+	sdl_textures[texture_idx] = SDL_CreateTexture(extra_renderers[win_idx], SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, width, height);
+	if (!sdl_textures[texture_idx]) {
+		goto fail_texture;
+	}
+	return texture_idx;
+	
+fail_texture:
+	SDL_DestroyRenderer(extra_renderers[win_idx]);
+fail_renderer:
+	SDL_DestroyWindow(extra_windows[win_idx]);
+fail_window:
+	num_textures--;
+	return 0;
+}
+
+void render_destroy_window(uint8_t which)
+{
+	uint8_t win_idx = which - FRAMEBUFFER_USER_START;
+	//Destroying the renderers also frees the textures
+	SDL_DestroyRenderer(extra_renderers[win_idx]);
+	SDL_DestroyWindow(extra_windows[win_idx]);
+	
+	extra_renderers[win_idx] = NULL;
+	extra_windows[win_idx] = NULL;
+}
+
+uint32_t *locked_pixels;
+uint32_t locked_pitch;
+uint32_t *render_get_framebuffer(uint8_t which, int *pitch)
+{
+#ifndef DISABLE_OPENGL
+	if (render_gl && which <= FRAMEBUFFER_EVEN) {
+		*pitch = LINEBUF_SIZE * sizeof(uint32_t);
+		return texture_buf;
+	} else {
+#endif
+		if (which >= num_textures) {
+			warning("Request for invalid framebuffer number %d\n", which);
+			return NULL;
+		}
+		void *pixels;
+		if (SDL_LockTexture(sdl_textures[which], NULL, &pixels, pitch) < 0) {
+			warning("Failed to lock texture: %s\n", SDL_GetError());
+			return NULL;
+		}
+		static uint8_t last;
+		if (which <= FRAMEBUFFER_EVEN) {
+			locked_pixels = pixels;
+			if (which == FRAMEBUFFER_EVEN) {
+				pixels += *pitch;
+			}
+			locked_pitch = *pitch;
+			if (which != last) {
+				*pitch *= 2;
+			}
+			last = which;
+		}
+		return pixels;
+#ifndef DISABLE_OPENGL
+	}
+#endif
+}
+
+uint8_t events_processed;
+#ifdef __ANDROID__
+#define FPS_INTERVAL 10000
+#else
+#define FPS_INTERVAL 1000
+#endif
+
+static uint32_t last_width, last_height;
+static uint8_t interlaced;
+void render_framebuffer_updated(uint8_t which, int width)
+{
+	static uint8_t last;
+	if (!sync_to_audio && which <= FRAMEBUFFER_EVEN && source_frame_count < 0) {
+		source_frame++;
+		if (source_frame >= source_hz) {
+			source_frame = 0;
+		}
+		source_frame_count = frame_repeat[source_frame];
+		//TODO: Figure out what to do about SDL Render API texture locking
+		return;
+	}
+	
+	last_width = width;
+	uint32_t height = which <= FRAMEBUFFER_EVEN 
+		? (video_standard == VID_NTSC ? 243 : 294) - (overscan_top[video_standard] + overscan_bot[video_standard])
+		: 240;
+	FILE *screenshot_file = NULL;
+	uint32_t shot_height, shot_width;
+	char *ext;
+	if (screenshot_path && which == FRAMEBUFFER_ODD) {
+		screenshot_file = fopen(screenshot_path, "wb");
+		if (screenshot_file) {
+#ifndef DISABLE_ZLIB
+			ext = path_extension(screenshot_path);
+#endif
+			info_message("Saving screenshot to %s\n", screenshot_path);
+		} else {
+			warning("Failed to open screenshot file %s for writing\n", screenshot_path);
+		}
+		free(screenshot_path);
+		screenshot_path = NULL;
+		shot_height = video_standard == VID_NTSC ? 243 : 294;
+		shot_width = width;
+	}
+	interlaced = last != which;
+	width -= overscan_left[video_standard] + overscan_right[video_standard];
+#ifndef DISABLE_OPENGL
+	if (render_gl && which <= FRAMEBUFFER_EVEN) {
+		SDL_GL_MakeCurrent(main_window, main_context);
+		glBindTexture(GL_TEXTURE_2D, textures[which]);
+		glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, LINEBUF_SIZE, height, GL_BGRA, GL_UNSIGNED_BYTE, texture_buf + overscan_left[video_standard] + LINEBUF_SIZE * overscan_top[video_standard]);
+		
+		if (screenshot_file) {
+			//properly supporting interlaced modes here is non-trivial, so only save the odd field for now
+#ifndef DISABLE_ZLIB
+			if (!strcasecmp(ext, "png")) {
+				free(ext);
+				save_png(screenshot_file, texture_buf, shot_width, shot_height, LINEBUF_SIZE*sizeof(uint32_t));
+			} else {
+				free(ext);
+#endif
+				save_ppm(screenshot_file, texture_buf, shot_width, shot_height, LINEBUF_SIZE*sizeof(uint32_t));
+#ifndef DISABLE_ZLIB
+			}
+#endif
+		}
+	} else {
+#endif
+		if (which <= FRAMEBUFFER_EVEN && last != which) {
+			uint8_t *cur_dst = (uint8_t *)locked_pixels;
+			uint8_t *cur_saved = (uint8_t *)texture_buf;
+			uint32_t dst_off = which == FRAMEBUFFER_EVEN ? 0 : locked_pitch;
+			uint32_t src_off = which == FRAMEBUFFER_EVEN ? locked_pitch : 0;
+			for (int i = 0; i < height; ++i)
+			{
+				//copy saved line from other field
+				memcpy(cur_dst + dst_off, cur_saved, locked_pitch);
+				//save line from this field to buffer for next frame
+				memcpy(cur_saved, cur_dst + src_off, locked_pitch);
+				cur_dst += locked_pitch * 2;
+				cur_saved += locked_pitch;
+			}
+			height = 480;
+		}
+		if (screenshot_file) {
+			uint32_t shot_pitch = locked_pitch;
+			if (which == FRAMEBUFFER_EVEN) {
+				shot_height *= 2;
+			} else {
+				shot_pitch *= 2;
+			}
+#ifndef DISABLE_ZLIB
+			if (!strcasecmp(ext, "png")) {
+				free(ext);
+				save_png(screenshot_file, locked_pixels, shot_width, shot_height, shot_pitch);
+			} else {
+				free(ext);
+#endif
+				save_ppm(screenshot_file, locked_pixels, shot_width, shot_height, shot_pitch);
+#ifndef DISABLE_ZLIB
+			}
+#endif
+		}
+		SDL_UnlockTexture(sdl_textures[which]);
+#ifndef DISABLE_OPENGL
+	}
+#endif
+	last_height = height;
+	if (which <= FRAMEBUFFER_EVEN) {
+		render_update_display();
+	} else {
+		SDL_RenderCopy(extra_renderers[which - FRAMEBUFFER_USER_START], sdl_textures[which], NULL, NULL);
+		SDL_RenderPresent(extra_renderers[which - FRAMEBUFFER_USER_START]);
+	}
+	if (screenshot_file) {
+		fclose(screenshot_file);
+	}
+	if (which <= FRAMEBUFFER_EVEN) {
+		last = which;
+		static uint32_t frame_counter, start;
+		frame_counter++;
+		last_frame= SDL_GetTicks();
+		if ((last_frame - start) > FPS_INTERVAL) {
+			if (start && (last_frame-start)) {
+	#ifdef __ANDROID__
+				info_message("%s - %.1f fps", caption, ((float)frame_counter) / (((float)(last_frame-start)) / 1000.0));
+	#else
+				if (!fps_caption) {
+					fps_caption = malloc(strlen(caption) + strlen(" - 100000000.1 fps") + 1);
+				}
+				sprintf(fps_caption, "%s - %.1f fps", caption, ((float)frame_counter) / (((float)(last_frame-start)) / 1000.0));
+				SDL_SetWindowTitle(main_window, fps_caption);
+	#endif
+			}
+			start = last_frame;
+			frame_counter = 0;
+		}
+	}
+	if (!sync_to_audio) {
+		int32_t local_cur_min, local_min_remaining;
+		SDL_LockAudio();
+			if (last_buffered > NO_LAST_BUFFERED) {
+				average_change *= 0.9f;
+				average_change += (cur_min_buffered - last_buffered) * 0.1f;
+			}
+			local_cur_min = cur_min_buffered;
+			local_min_remaining = min_remaining_buffer;
+			last_buffered = cur_min_buffered;
+		SDL_UnlockAudio();
+		float frames_to_problem;
+		if (average_change < 0) {
+			frames_to_problem = (float)local_cur_min / -average_change;
+		} else {
+			frames_to_problem = (float)local_min_remaining / average_change;
+		}
+		float adjust_ratio = 0.0f;
+		if (
+			frames_to_problem < BUFFER_FRAMES_THRESHOLD
+			|| (average_change < 0 && local_cur_min < 3*min_buffered / 4)
+			|| (average_change >0 && local_cur_min > 5 * min_buffered / 4)
+			|| cur_min_buffered < 0
+		) {
+			
+			if (cur_min_buffered < 0) {
+				adjust_ratio = max_adjust;
+				SDL_PauseAudio(1);
+				last_buffered = NO_LAST_BUFFERED;
+				cur_min_buffered = 0;
+			} else {
+				adjust_ratio = -1.0 * average_change / ((float)sample_rate / (float)source_hz);
+				adjust_ratio /= 2.5 * source_hz;
+				if (fabsf(adjust_ratio) > max_adjust) {
+					adjust_ratio = adjust_ratio > 0 ? max_adjust : -max_adjust;
+				}
+			}
+		} else if (local_cur_min < min_buffered / 2) {
+			adjust_ratio = max_adjust;
+		}
+		if (adjust_ratio != 0.0f) {
+			average_change = 0;
+			for (uint8_t i = 0; i < num_audio_sources; i++)
+			{
+				audio_sources[i]->buffer_inc = ((double)audio_sources[i]->buffer_inc) + ((double)audio_sources[i]->buffer_inc) * adjust_ratio + 0.5;
+			}
+		}
+		while (source_frame_count > 0)
+		{
+			render_update_display();
+			source_frame_count--;
+		}
+		source_frame++;
+		if (source_frame >= source_hz) {
+			source_frame = 0;
+		}
+		source_frame_count = frame_repeat[source_frame];
+	}
+}
+
+static ui_render_fun render_ui;
+void render_set_ui_render_fun(ui_render_fun fun)
+{
+	render_ui = fun;
+}
+
+void render_update_display()
+{
+#ifndef DISABLE_OPENGL
+	if (render_gl) {
+		glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
+		glClear(GL_COLOR_BUFFER_BIT);
+
+		glUseProgram(program);
+		glActiveTexture(GL_TEXTURE0);
+		glBindTexture(GL_TEXTURE_2D, textures[0]);
+		glUniform1i(un_textures[0], 0);
+
+		glActiveTexture(GL_TEXTURE1);
+		glBindTexture(GL_TEXTURE_2D, textures[interlaced ? 1 : scanlines ? 2 : 0]);
+		glUniform1i(un_textures[1], 1);
+
+		glUniform1f(un_width, render_emulated_width());
+		glUniform1f(un_height, last_height);
+
+		glBindBuffer(GL_ARRAY_BUFFER, buffers[0]);
+		glVertexAttribPointer(at_pos, 2, GL_FLOAT, GL_FALSE, sizeof(GLfloat[2]), (void *)0);
+		glEnableVertexAttribArray(at_pos);
+
+		glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, buffers[1]);
+		glDrawElements(GL_TRIANGLE_STRIP, 4, GL_UNSIGNED_SHORT, (void *)0);
+
+		glDisableVertexAttribArray(at_pos);
+		
+		if (render_ui) {
+			render_ui();
+		}
+
+		SDL_GL_SwapWindow(main_window);
+	} else {
+#endif
+		SDL_Rect src_clip = {
+			.x = overscan_left[video_standard],
+			.y = overscan_top[video_standard],
+			.w = render_emulated_width(),
+			.h = last_height
+		};
+		SDL_SetRenderDrawColor(main_renderer, 0, 0, 0, 255);
+		SDL_RenderClear(main_renderer);
+		SDL_RenderCopy(main_renderer, sdl_textures[FRAMEBUFFER_ODD], &src_clip, &main_clip);
+		if (render_ui) {
+			render_ui();
+		}
+		SDL_RenderPresent(main_renderer);
+#ifndef DISABLE_OPENGL
+	}
+#endif
+	if (!events_processed) {
+		process_events();
+	}
+	events_processed = 0;
+}
+
+uint32_t render_emulated_width()
+{
+	return last_width - overscan_left[video_standard] - overscan_right[video_standard];
+}
+
+uint32_t render_emulated_height()
+{
+	return (video_standard == VID_NTSC ? 243 : 294) - overscan_top[video_standard] - overscan_bot[video_standard];
+}
+
+uint32_t render_overscan_left()
+{
+	return overscan_left[video_standard];
+}
+
+uint32_t render_overscan_top()
+{
+	return overscan_top[video_standard];
+}
+
+void render_wait_quit(vdp_context * context)
+{
+	SDL_Event event;
+	while(SDL_WaitEvent(&event)) {
+		switch (event.type) {
+		case SDL_QUIT:
+			return;
+		}
+	}
+}
+
+int render_lookup_button(char *name)
+{
+	static tern_node *button_lookup;
+	if (!button_lookup) {
+		for (int i = SDL_CONTROLLER_BUTTON_A; i < SDL_CONTROLLER_BUTTON_MAX; i++)
+		{
+			button_lookup = tern_insert_int(button_lookup, SDL_GameControllerGetStringForButton(i), i);
+		}
+		//alternative Playstation-style names
+		button_lookup = tern_insert_int(button_lookup, "cross", SDL_CONTROLLER_BUTTON_A);
+		button_lookup = tern_insert_int(button_lookup, "circle", SDL_CONTROLLER_BUTTON_B);
+		button_lookup = tern_insert_int(button_lookup, "square", SDL_CONTROLLER_BUTTON_X);
+		button_lookup = tern_insert_int(button_lookup, "triangle", SDL_CONTROLLER_BUTTON_Y);
+		button_lookup = tern_insert_int(button_lookup, "share", SDL_CONTROLLER_BUTTON_BACK);
+		button_lookup = tern_insert_int(button_lookup, "select", SDL_CONTROLLER_BUTTON_BACK);
+		button_lookup = tern_insert_int(button_lookup, "options", SDL_CONTROLLER_BUTTON_START);
+		button_lookup = tern_insert_int(button_lookup, "l1", SDL_CONTROLLER_BUTTON_LEFTSHOULDER);
+		button_lookup = tern_insert_int(button_lookup, "r1", SDL_CONTROLLER_BUTTON_RIGHTSHOULDER);
+		button_lookup = tern_insert_int(button_lookup, "l3", SDL_CONTROLLER_BUTTON_LEFTSTICK);
+		button_lookup = tern_insert_int(button_lookup, "r3", SDL_CONTROLLER_BUTTON_RIGHTSTICK);
+	}
+	return (int)tern_find_int(button_lookup, name, SDL_CONTROLLER_BUTTON_INVALID);
+}
+
+int render_lookup_axis(char *name)
+{
+	static tern_node *axis_lookup;
+	if (!axis_lookup) {
+		for (int i = SDL_CONTROLLER_AXIS_LEFTX; i < SDL_CONTROLLER_AXIS_MAX; i++)
+		{
+			axis_lookup = tern_insert_int(axis_lookup, SDL_GameControllerGetStringForAxis(i), i);
+		}
+		//alternative Playstation-style names
+		axis_lookup = tern_insert_int(axis_lookup, "l2", SDL_CONTROLLER_AXIS_TRIGGERLEFT);
+		axis_lookup = tern_insert_int(axis_lookup, "r2", SDL_CONTROLLER_AXIS_TRIGGERRIGHT);
+	}
+	return (int)tern_find_int(axis_lookup, name, SDL_CONTROLLER_AXIS_INVALID);
+}
+
+int32_t render_translate_input_name(int32_t controller, char *name, uint8_t is_axis)
+{
+	tern_node *button_lookup, *axis_lookup;
+	if (controller > MAX_JOYSTICKS || !joysticks[controller]) {
+		return RENDER_NOT_PLUGGED_IN;
+	}
+	
+	if (!SDL_IsGameController(joystick_sdl_index[controller])) {
+		return RENDER_NOT_MAPPED;
+	}
+	SDL_GameController *control = SDL_GameControllerOpen(joystick_sdl_index[controller]);
+	if (!control) {
+		warning("Failed to open game controller %d: %s\n", controller, SDL_GetError());
+		return RENDER_NOT_PLUGGED_IN;
+	}
+	
+	SDL_GameControllerButtonBind cbind;
+	if (is_axis) {
+		
+		int sdl_axis = render_lookup_axis(name);
+		if (sdl_axis == SDL_CONTROLLER_AXIS_INVALID) {
+			SDL_GameControllerClose(control);
+			return RENDER_INVALID_NAME;
+		}
+		cbind = SDL_GameControllerGetBindForAxis(control, sdl_axis);
+	} else {
+		int sdl_button = render_lookup_button(name);
+		if (sdl_button == SDL_CONTROLLER_BUTTON_INVALID) {
+			SDL_GameControllerClose(control);
+			return RENDER_INVALID_NAME;
+		}
+		cbind = SDL_GameControllerGetBindForButton(control, sdl_button);
+	}
+	SDL_GameControllerClose(control);
+	switch (cbind.bindType)
+	{
+	case SDL_CONTROLLER_BINDTYPE_BUTTON:
+		return cbind.value.button;
+	case SDL_CONTROLLER_BINDTYPE_AXIS:
+		return RENDER_AXIS_BIT | cbind.value.axis;
+	case SDL_CONTROLLER_BINDTYPE_HAT:
+		return RENDER_DPAD_BIT | (cbind.value.hat.hat << 4) | cbind.value.hat.hat_mask;
+	}
+	return RENDER_NOT_MAPPED;
+}
+
+int32_t render_dpad_part(int32_t input)
+{
+	return input >> 4 & 0xFFFFFF;
+}
+
+uint8_t render_direction_part(int32_t input)
+{
+	return input & 0xF;
+}
+
+int32_t render_axis_part(int32_t input)
+{
+	return input & 0xFFFFFFF;
+}
+
 void process_events()
 {
 	if (events_processed > MAX_EVENT_POLL_PER_FRAME) {
@@ -1070,7 +1820,6 @@
 #define TOGGLE_MIN_DELAY 250
 void render_toggle_fullscreen()
 {
-	static int in_toggle;
 	//protect against event processing causing us to attempt to toggle while still toggling
 	if (in_toggle) {
 		return;
@@ -1110,36 +1859,6 @@
 	in_toggle = 0;
 }
 
-void render_wait_psg(psg_context * context)
-{
-	SDL_LockMutex(audio_mutex);
-		while (current_psg != NULL) {
-			SDL_CondWait(psg_cond, audio_mutex);
-		}
-		current_psg = context->audio_buffer;
-		SDL_CondSignal(audio_ready);
-
-		context->audio_buffer = context->back_buffer;
-		context->back_buffer = current_psg;
-	SDL_UnlockMutex(audio_mutex);
-	context->buffer_pos = 0;
-}
-
-void render_wait_ym(ym2612_context * context)
-{
-	SDL_LockMutex(audio_mutex);
-		while (current_ym != NULL) {
-			SDL_CondWait(ym_cond, audio_mutex);
-		}
-		current_ym = context->audio_buffer;
-		SDL_CondSignal(audio_ready);
-
-		context->audio_buffer = context->back_buffer;
-		context->back_buffer = current_ym;
-	SDL_UnlockMutex(audio_mutex);
-	context->buffer_pos = 0;
-}
-
 uint32_t render_audio_buffer()
 {
 	return buffer_samples;
@@ -1165,3 +1884,31 @@
 	SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_INFORMATION, title, message, NULL);
 }
 
+uint32_t render_elapsed_ms(void)
+{
+	return SDL_GetTicks();
+}
+
+void render_sleep_ms(uint32_t delay)
+{
+	return SDL_Delay(delay);
+}
+
+uint8_t render_has_gl(void)
+{
+	return render_gl;
+}
+
+uint8_t render_get_active_framebuffer(void)
+{
+	if (SDL_GetWindowFlags(main_window) & SDL_WINDOW_INPUT_FOCUS) {
+		return FRAMEBUFFER_ODD;
+	}
+	for (int i = 0; i < num_textures - 2; i++)
+	{
+		if (extra_windows[i] && (SDL_GetWindowFlags(extra_windows[i]) & SDL_WINDOW_INPUT_FOCUS)) {
+			return FRAMEBUFFER_USER_START + i; 
+		}
+	}
+	return 0xFF;
+}