#include #include #include #include #include #include #include #include "main.h" #include "utils.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; 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; 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, .blend_frames = true, .rewind_length = 60 * 2, .model = MODEL_CGB }; // 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 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, VIDEO_HEIGHT, 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); renderer = SDL_CreateRenderer( window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC ); if (!renderer) { fprintf(stderr, "SDL_CreateRenderer Error: %s\n", SDL_GetError()); return EXIT_FAILURE; } screen = SDL_CreateRGBSurface( 0, VIDEO_WIDTH, VIDEO_HEIGHT, 32, 0, 0, 0, 0 ); if (!screen) { SDL_Log("SDL_CreateRGBSurface() failed: %s", SDL_GetError()); exit(1); } pixel_format = screen->format; texture = SDL_CreateTextureFromSurface(renderer, screen); if (!texture) { fprintf(stderr, "SDL_CreateTextureFromSurface Error: %s\n", SDL_GetError()); return EXIT_FAILURE; } 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(); 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); }