Mercurial > repos > blastem
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; +}