#include #include #include #include #include #include #include "main.h" #include "utils.h" #include "shader.h" GB_gameboy_t gb; SDL_Window *window; SDL_Renderer *renderer; SDL_Surface *screen; SDL_Texture *texture; SDL_PixelFormat *pixel_format; SDL_AudioDeviceID device_id; shader_t shader; static SDL_Rect rect; static unsigned factor; static SDL_AudioSpec want_aspec, have_aspec; static uint32_t pixel_buffer_1[256 * 224], pixel_buffer_2[256 * 224]; static uint32_t *active_pixel_buffer = pixel_buffer_1; static uint32_t *previous_pixel_buffer = pixel_buffer_2; static char *battery_save_path_ptr; struct shader_name { const char *file_name; const char *display_name; } shaders[] = { {"NearestNeighbor", "Nearest Neighbor"}, {"Bilinear", "Bilinear"}, {"SmoothBilinear", "Smooth Bilinear"}, {"LCD", "LCD Display"}, {"CRT", "CRT Display"}, {"Scale2x", "Scale2x"}, {"Scale4x", "Scale4x"}, {"AAScale2x", "Anti-aliased Scale2x"}, {"AAScale4x", "Anti-aliased Scale4x"}, // {"HQ2x", "HQ2x"}, // requires OpenGL ES 1.30 features // {"OmniScale", "OmniScale"}, // requires OpenGL ES 1.30 features {"OmniScaleLegacy", "OmniScale Legacy"}, {"AAOmniScaleLegacy", "AA OmniScale Legacy"}, }; configuration_t configuration = { .keys = { SDL_SCANCODE_RIGHT, SDL_SCANCODE_LEFT, SDL_SCANCODE_UP, SDL_SCANCODE_DOWN, SDL_SCANCODE_X, SDL_SCANCODE_Z, SDL_SCANCODE_BACKSPACE, SDL_SCANCODE_RETURN, SDL_SCANCODE_SPACE }, .keys_2 = { SDL_SCANCODE_TAB, SDL_SCANCODE_LSHIFT, }, .joypad_configuration = { 13, 14, 11, 12, 0, 1, 9, 8, 10, 4, -1, 5, }, .joypad_axises = { 0, 1, }, .color_correction_mode = GB_COLOR_CORRECTION_EMULATE_HARDWARE, .highpass_mode = GB_HIGHPASS_ACCURATE, .scaling_mode = GB_SDL_SCALING_INTEGER_FACTOR, .blend_frames = true, .rewind_length = 60 * 2, .model = MODEL_CGB, .filter = "OmniScale", }; // Use this function instead of GB_save_battery() int save_battery(GB_gameboy_t *gb, const char *path) { int result = GB_save_battery(gb, path); fprintf(stderr, "Saving battery: \"%s\": %d\n", path, result); EM_ASM(Module.sync_fs()); return result; } unsigned query_sample_rate_of_audiocontexts() { return EM_ASM_INT({ if (!Module.SDL2 || !Module.SDL2.audioContext) { const AudioContext = window.AudioContext || window.webkitAudioContext; const ctx = new AudioContext(); const sr = ctx.sampleRate; ctx.close(); return sr; } return Module.SDL2.audioContext.sampleRate; }); } static void audio_callback(void *gb, Uint8 *stream, int len) { if (GB_is_inited(gb)) { GB_apu_copy_buffer(gb, (GB_sample_t *) stream, len / sizeof(GB_sample_t)); } else { memset(stream, 0, len); } } void update_viewport(void) { int win_width, win_height; SDL_GL_GetDrawableSize(window, &win_width, &win_height); int logical_width, logical_height; SDL_GetWindowSize(window, &logical_width, &logical_height); factor = win_width / logical_width; double x_factor = win_width / (double) GB_get_screen_width(&gb); double y_factor = win_height / (double) GB_get_screen_height(&gb); if (configuration.scaling_mode == GB_SDL_SCALING_INTEGER_FACTOR) { x_factor = (int)(x_factor); y_factor = (int)(y_factor); } if (configuration.scaling_mode != GB_SDL_SCALING_ENTIRE_WINDOW) { if (x_factor > y_factor) { x_factor = y_factor; } else { y_factor = x_factor; } } unsigned new_width = x_factor * GB_get_screen_width(&gb); unsigned new_height = y_factor * GB_get_screen_height(&gb); rect = (SDL_Rect){(win_width - new_width) / 2, (win_height - new_height) / 2, new_width, new_height}; if (renderer) { SDL_RenderSetViewport(renderer, &rect); } else { glViewport(rect.x, rect.y, rect.w, rect.h); } } void render_texture(void *pixels, void *previous) { if (renderer) { if (pixels) { SDL_UpdateTexture(texture, NULL, pixels, 160 * sizeof (uint32_t)); } SDL_RenderClear(renderer); SDL_RenderCopy(renderer, texture, NULL, NULL); SDL_RenderPresent(renderer); } else { static void *_pixels = NULL; if (pixels) { _pixels = pixels; } glClearColor(0, 0, 0, 1); glClear(GL_COLOR_BUFFER_BIT); render_bitmap_with_shader(&shader, _pixels, previous, GB_get_screen_width(&gb), GB_get_screen_height(&gb), rect.x, rect.y, rect.w, rect.h); SDL_GL_SwapWindow(window); } } static void handle_events(GB_gameboy_t *gb) { GB_set_key_state(gb, GB_KEY_START, true); } static void vblank(GB_gameboy_t *gb) { if (configuration.blend_frames) { render_texture(active_pixel_buffer, previous_pixel_buffer); uint32_t *temp = active_pixel_buffer; active_pixel_buffer = previous_pixel_buffer; previous_pixel_buffer = temp; GB_set_pixels_output(gb, active_pixel_buffer); } else { render_texture(active_pixel_buffer, NULL); } handle_events(gb); } static uint32_t rgb_encode(GB_gameboy_t *gb, uint8_t r, uint8_t g, uint8_t b) { return SDL_MapRGB(pixel_format, r, g, b); } void init_gb() { GB_model_t model; model = (GB_model_t []) { [MODEL_DMG] = GB_MODEL_DMG_B, [MODEL_CGB] = GB_MODEL_CGB_E, [MODEL_AGB] = GB_MODEL_AGB, [MODEL_SGB] = (GB_model_t []) { [SGB_NTSC] = GB_MODEL_SGB_NTSC, [SGB_PAL] = GB_MODEL_SGB_PAL, [SGB_2] = GB_MODEL_SGB2, }[configuration.sgb_revision], }[configuration.model]; fprintf(stderr, "Initializing ...\n"); if (GB_is_inited(&gb)) { fprintf(stderr, "Already initialized, switching model ...\n"); GB_switch_model_and_reset(&gb, model); } else { fprintf(stderr, "Initializing new GB ...\n"); GB_init(&gb, model); GB_set_input_callback(&gb, NULL); GB_set_async_input_callback(&gb, NULL); GB_set_vblank_callback(&gb, (GB_vblank_callback_t) vblank); GB_set_pixels_output(&gb, active_pixel_buffer); GB_set_rgb_encode_callback(&gb, rgb_encode); GB_set_sample_rate(&gb, have_aspec.freq); GB_set_color_correction_mode(&gb, configuration.color_correction_mode); GB_set_highpass_filter_mode(&gb, configuration.highpass_mode); GB_set_rewind_length(&gb, 0); } SDL_DestroyTexture(texture); texture = SDL_CreateTexture( renderer, SDL_GetWindowPixelFormat(window), SDL_TEXTUREACCESS_STREAMING, GB_get_screen_width(&gb), GB_get_screen_height(&gb) ); SDL_SetWindowMinimumSize(window, GB_get_screen_width(&gb), GB_get_screen_height(&gb)); bool error = false; const char * const boot_roms[] = { "dmg_boot.bin", "cgb_boot.bin", "agb_boot.bin", "sgb_boot.bin" }; const char *boot_rom = boot_roms[configuration.model]; if (configuration.model == GB_MODEL_SGB && configuration.sgb_revision == SGB_2) { boot_rom = "sgb2_boot.bin"; } const char *boot_rom_path = resource_path(concat("BootROMs/", boot_rom)); fprintf(stderr, "Loading boot ROM: %s\n", boot_rom_path); error = GB_load_boot_rom(&gb, boot_rom_path); } int EMSCRIPTEN_KEEPALIVE init() { #define str(x) #x #define xstr(x) str(x) pixel_format = (SDL_PixelFormat *) malloc(sizeof(SDL_PixelFormat)); if (!pixel_format) { fprintf(stderr, "Failed to allocate memory\n"); return EXIT_FAILURE; } // emscripten_sample_gamepad_data(); if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO) != 0) { fprintf(stderr, "SDL_Init Error: %s\n", SDL_GetError()); return EXIT_FAILURE; } fprintf(stderr, "SameBoy v" xstr(VERSION) "\n"); window = SDL_CreateWindow( "SameBoy v" xstr(VERSION), SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, VIDEO_WIDTH * 4, VIDEO_HEIGHT * 4, SDL_WINDOW_OPENGL | SDL_WINDOW_BORDERLESS | SDL_WINDOW_ALLOW_HIGHDPI ); if (!window) { printf("Could not create window: %s\n", SDL_GetError()); return EXIT_FAILURE; } SDL_SetWindowMinimumSize(window, VIDEO_WIDTH, VIDEO_HEIGHT); SDL_SetWindowMaximumSize(window, VIDEO_WIDTH, VIDEO_HEIGHT); // Try to get a GLES 3.0 context SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0); SDL_GLContext gl_context = SDL_GL_CreateContext(window); if (gl_context == NULL) { // Try to get a GLES 2.0 context SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 2); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0); gl_context = SDL_GL_CreateContext(window); } if (gl_context == NULL) { fprintf(stderr, "Using software renderer!\n"); renderer = SDL_CreateRenderer(window, -1, 0); texture = SDL_CreateTexture(renderer, SDL_GetWindowPixelFormat(window), SDL_TEXTUREACCESS_STREAMING, 160, 144); pixel_format = SDL_AllocFormat(SDL_GetWindowPixelFormat(window)); } else { fprintf(stderr, "Using OpenGL renderer!\n"); pixel_format = SDL_AllocFormat(SDL_PIXELFORMAT_ABGR8888); fprintf(stderr, "GLES: %s\n", glGetString(GL_VERSION)); fprintf(stderr, "GLSL: %s\n", glGetString(GL_SHADING_LANGUAGE_VERSION)); fprintf(stderr, "Parsed GL version: %hu\n", get_gl_version()); } unsigned audio_sample_rate = query_sample_rate_of_audiocontexts(); fprintf(stderr, "Sample rate: %u\n", audio_sample_rate); memset(&want_aspec, 0, sizeof(want_aspec)); want_aspec.freq = audio_sample_rate; want_aspec.format = AUDIO_S16SYS; want_aspec.channels = 2; want_aspec.samples = 2048; want_aspec.callback = audio_callback; want_aspec.userdata = &gb; device_id = SDL_OpenAudioDevice(NULL, 0, &want_aspec, &have_aspec, SDL_AUDIO_ALLOW_FREQUENCY_CHANGE); if (device_id == 0) { fprintf(stderr, "Failed to open audio: %s", SDL_GetError()); } fprintf(stderr, "WANT:\nfreq: %d\nchannels: %d\nsilence: %d\nsamples: %d\nsize: %d\nformat: %d\n", want_aspec.freq, want_aspec.channels, want_aspec.silence, want_aspec.samples, want_aspec.size, want_aspec.format); fprintf(stderr, "HAVE:\nfreq: %d\nchannels: %d\nsilence: %d\nsamples: %d\nsize: %d\nformat: %d\n", have_aspec.freq, have_aspec.channels, have_aspec.silence, have_aspec.samples, have_aspec.size, have_aspec.format); EM_ASM({ function audio_workaround(e) { if (!Module.SDL2 || !Module.SDL2.audioContext || !Module.SDL2.audioContext.resume) return; console.log('Applying audio workarounds...'); if (Module.SDL2.audioContext.state == 'suspended') { Module.SDL2.audioContext.resume(); } if (Module.SDL2.audioContext.state == 'running') { document.removeEventListener('touchstart', audio_workaround); document.removeEventListener('click', audio_workaround); document.removeEventListener('keydown', audio_workaround); if (Module.canvas) { Module.canvas.removeEventListener('touchstart', audio_workaround); Module.canvas.removeEventListener('click', audio_workaround); Module.canvas.removeEventListener('keydown', audio_workaround); } } else if (Module.SDL2.audioContext && Module.SDL2.audioContext.currentTime == 0) { // unlock audio for iOS let buffer = Module.SDL2.audioContext.createBuffer(1, 1, 22050); let source = Module.SDL2.audioContext.createBufferSource(); source.buffer = buffer; source.connect(Module.SDL2.audioContext.destination); source.start(0); } } document.addEventListener('touchstart', audio_workaround); document.addEventListener('click', audio_workaround); document.addEventListener('keydown', audio_workaround); if (Module.canvas) { Module.canvas.addEventListener('touchstart', audio_workaround); Module.canvas.addEventListener('click', audio_workaround); Module.canvas.addEventListener('keydown', audio_workaround); } audio_workaround(); }); init_gb(); if (!init_shader_with_name(&shader, configuration.filter)) { init_shader_with_name(&shader, "NearestNeighbor"); } update_viewport(); SDL_PauseAudioDevice(device_id, 0); return EXIT_SUCCESS; } int EMSCRIPTEN_KEEPALIVE load_boot_rom_from_file(char* filename) { return GB_load_boot_rom(&gb, filename); } int EMSCRIPTEN_KEEPALIVE load_rom_from_file(char* filename, char* battery_save_path) { int result = GB_load_rom(&gb, filename); if (result == 0) { battery_save_path_ptr = battery_save_path; GB_load_battery(&gb, battery_save_path); save_battery(&gb, battery_save_path_ptr); } return result; } void EMSCRIPTEN_KEEPALIVE quit() { fprintf(stderr, "Quitting ...\n"); emscripten_set_main_loop(NULL, 0, false); GB_free(&gb); SDL_FreeSurface(screen); SDL_DestroyTexture(texture); SDL_DestroyRenderer(renderer); SDL_DestroyWindow(window); SDL_Quit(); } void EMSCRIPTEN_KEEPALIVE run_frame() { GB_run_frame(&gb); }