#define G_LOG_USE_STRUCTURED #include #include #include #include #include #include #include "types.h" #include "config.h" #include "util.h" #include "check_menu_radio_group.h" #include "gb_screen.h" #include "console_window.h" #include "preferences_window.h" #include "vram_viewer_window.h" // used for audio and game controllers #include "SDL.h" #include "../SDL/audio/audio.h" #define JOYSTICK_HIGH 0x4000 #define JOYSTICK_LOW 0x3800 #define BUTTON_MASK_A 0x01 #define BUTTON_MASK_B 0x02 #define BUTTON_MASK_START 0x04 #define BUTTON_MASK_SELECT 0x08 #define BUTTON_MASK_UP 0x10 #define BUTTON_MASK_DOWN 0x20 #define BUTTON_MASK_LEFT 0x40 #define BUTTON_MASK_RIGHT 0x80 #define str(x) #x #define xstr(x) str(x) #define get_object(id) gtk_builder_get_object(gui_data.builder, id) #define builder_get(type, id) type(get_object(id)) #define action_set_enabled(map, name, value) g_simple_action_set_enabled(G_SIMPLE_ACTION(g_action_map_lookup_action(G_ACTION_MAP(map), name)), value); // Initialize the GuiData static GuiData gui_data = { .cli_options = { .fullscreen = false, .model = -1, }, .prev_model = -1, .running = false, .stopping = false, .stopped = false, .audio_initialized = false, .border_mode_changed = false, .underclock_down = false, .rewind_down = false, .rewind_paused = false, .turbo_down = false, .clock_mutliplier = 1.0, .analog_clock_multiplier = 1.0, }; static GB_gameboy_t gb; // Forward declarations of the actions static void activate_open(GSimpleAction *action, GVariant *parameter, gpointer app); static void activate_close(GSimpleAction *action, GVariant *parameter, gpointer app); static void activate_reset(GSimpleAction *action, GVariant *parameter, gpointer app); static void activate_show_console(GSimpleAction *action, GVariant *parameter, gpointer app); static void activate_open_gtk_debugger(GSimpleAction *action, GVariant *parameter, gpointer app); static void activate_open_memory_viewer(GSimpleAction *action, GVariant *parameter, gpointer app); static void activate_open_vram_viewer(GSimpleAction *action, GVariant *parameter, gpointer app); static void activate_clear_console(GSimpleAction *action, GVariant *parameter, gpointer app); static void activate_quit(GSimpleAction *action, GVariant *parameter, gpointer app); static void activate_about(GSimpleAction *action, GVariant *parameter, gpointer app); static void activate_preferences(GSimpleAction *action, GVariant *parameter, gpointer app); static void on_pause_changed(GSimpleAction *action, GVariant *value, gpointer user_data_ptr); static void on_mute_changed(GSimpleAction *action, GVariant *value, gpointer user_data_ptr); static const GActionEntry file_entries[] = { { "open", activate_open, NULL, NULL, NULL }, { "close", activate_close, NULL, NULL, NULL }, }; static const GActionEntry edit_entries[] = { }; static const GActionEntry emulation_entries[] = { { "reset", activate_reset, NULL, NULL, NULL }, { "pause", NULL, NULL, "false", on_pause_changed }, { "save_state", NULL, NULL, NULL, NULL }, { "load_state", NULL, NULL, NULL, NULL }, }; static const GActionEntry developer_entries[] = { { "show_console", activate_show_console, NULL, NULL, NULL }, { "open_gtk_debugger", activate_open_gtk_debugger, NULL, NULL, NULL }, // { "open_memory_viewer", activate_open_memory_viewer, NULL, NULL, NULL }, { "open_vram_viewer", activate_open_vram_viewer, NULL, NULL, NULL }, { "toggle_developer_mode", NULL, NULL, "false", NULL }, { "clear_console", activate_clear_console, NULL, NULL, NULL }, }; static const GActionEntry app_entries[] = { { "quit", activate_quit, NULL, NULL, NULL }, { "about", activate_about, NULL, NULL, NULL }, { "preferences", activate_preferences, NULL, NULL, NULL }, { "toggle_mute", NULL, NULL, "false", on_mute_changed }, }; static const char* get_sdl_joystick_power_level_name(SDL_JoystickPowerLevel level) { switch (level) { case SDL_JOYSTICK_POWER_EMPTY: return "Empty"; case SDL_JOYSTICK_POWER_LOW: return "Low"; case SDL_JOYSTICK_POWER_MEDIUM: return "Medium"; case SDL_JOYSTICK_POWER_FULL: return "Full"; case SDL_JOYSTICK_POWER_WIRED: return "Wired"; case SDL_JOYSTICK_POWER_MAX: return "Max"; case SDL_JOYSTICK_POWER_UNKNOWN: default: return "Unknown"; } } // This function gets called after the parsing of the commandline options has occurred. static gint handle_local_options(GApplication *app, GVariantDict *options, gpointer null_ptr) { guint32 count; if (g_variant_dict_lookup(options, "version", "b", &count)) { g_message("SameBoy v" xstr(VERSION)); return EXIT_SUCCESS; } // Handle model override GVariant *model_name_var = g_variant_dict_lookup_value(options, "model", G_VARIANT_TYPE_STRING); if (model_name_var != NULL) { const gchar *model_name = g_variant_get_string(model_name_var, NULL); // TODO: Synchronize with GB_model_t (Core/gb.h) if (g_str_has_prefix(model_name, "DMG")) { if (g_str_has_suffix(model_name, "-B") || g_strcmp0(model_name, "DMG") == 0) { gui_data.cli_options.model = GB_MODEL_DMG_B; } else { gui_data.cli_options.model = GB_MODEL_DMG_B; g_warning("Unsupported revision: %s\nFalling back to DMG-B", model_name); } } else if (g_str_has_prefix(model_name, "SGB")) { if (g_str_has_suffix(model_name, "-NTSC") || g_strcmp0(model_name, "SGB") == 0) { gui_data.cli_options.model = GB_MODEL_SGB; } else if (g_str_has_suffix(model_name, "-PAL")) { gui_data.cli_options.model = GB_MODEL_SGB | GB_MODEL_PAL_BIT; } else if (g_str_has_suffix(model_name, "2")) { gui_data.cli_options.model = GB_MODEL_SGB2; } else { gui_data.cli_options.model = GB_MODEL_SGB2; g_warning("Unsupported revision: %s\nFalling back to SGB2", model_name); } } else if (g_str_has_prefix(model_name, "CGB")) { if (g_str_has_suffix(model_name, "-C")) { gui_data.cli_options.model = GB_MODEL_CGB_C; } else if (g_str_has_suffix(model_name, "-E") || g_strcmp0(model_name, "CGB") == 0) { gui_data.cli_options.model = GB_MODEL_CGB_E; } else { gui_data.cli_options.model = GB_MODEL_CGB_E; g_warning("Unsupported revision: %s\nFalling back to CGB-E", model_name); } } else if (g_str_has_prefix(model_name, "AGB")) { gui_data.cli_options.model = GB_MODEL_AGB; } else { g_warning("Unknown model: %s", model_name); exit(EXIT_FAILURE); } } return -1; } static gboolean init_controllers(void) { SDL_version compiled; SDL_version linked; SDL_VERSION(&compiled); SDL_GetVersion(&linked); g_debug("Compiled against SDL version %d.%d.%d", compiled.major, compiled.minor, compiled.patch); g_debug("Linked against SDL version %d.%d.%d", linked.major, linked.minor, linked.patch); g_debug("Initializing game controllers"); if (SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) < 0) { g_warning("Failed to initialize game controller support: %s", SDL_GetError()); return false; } g_debug("Initializing haptic feedback"); if (SDL_InitSubSystem(SDL_INIT_HAPTIC) < 0) { g_warning("Failed to initialize haptic feedback support: %s", SDL_GetError()); } g_debug("Loading custom game controller database"); GError *error = NULL; GBytes *db_f = g_resources_lookup_data(RESOURCE_PREFIX "gamecontrollerdb.txt", G_RESOURCE_LOOKUP_FLAGS_NONE, &error); if (db_f != NULL) { gsize db_data_size = 0; const guchar *db_data = g_bytes_get_data(db_f, &db_data_size); const gint val = SDL_GameControllerAddMappingsFromRW(SDL_RWFromMem((void *)db_data, db_data_size), 1); if (val < 0) { g_warning("Failed to load controller mappings: %s", SDL_GetError()); } g_bytes_unref(db_f); } if (error != NULL) g_clear_error(&error); g_message("Number of found controllers: %d", SDL_NumJoysticks()); // In the “worst” case all joysticks are valid game controllers gui_data.controllers = g_malloc0(sizeof(struct Controller_t) * SDL_NumJoysticks()); for (int i = 0; i < SDL_NumJoysticks(); ++i) { if (SDL_IsGameController(i)) { struct Controller_t *s = &gui_data.controllers[i]; s->controller = SDL_GameControllerOpen(i); if (s->controller) { SDL_Joystick *joystick = SDL_GameControllerGetJoystick(s->controller); SDL_JoystickPowerLevel power_level = SDL_JoystickCurrentPowerLevel(joystick); if (SDL_JoystickIsHaptic(joystick)) { s->haptic = SDL_HapticOpenFromJoystick(joystick); if (s->haptic && SDL_HapticRumbleSupported(s->haptic)) { SDL_HapticRumbleInit(s->haptic); } else { if (s->haptic == NULL) { g_warning("%s", SDL_GetError()); } SDL_HapticClose(s->haptic); s->haptic = NULL; } } // Blacklist the WUP-028 for now if (SDL_JoystickGetVendor(joystick) == 0x057e && SDL_JoystickGetProduct(joystick) == 0x0337 && SDL_JoystickGetProductVersion(joystick) == 0x0100) { s->ignore_rumble = true; } char guid_str[33]; SDL_JoystickGUID guid = SDL_JoystickGetGUID(joystick); SDL_JoystickGetGUIDString(guid, guid_str, sizeof(guid_str)); g_message("Controller #%u (%s): %s; Haptic Feedback: %d; Power level: %s; Player index: %u; Instance ID: %u", i, guid_str, SDL_GameControllerName(s->controller), s->haptic != NULL, get_sdl_joystick_power_level_name(power_level), SDL_JoystickGetPlayerIndex(joystick), SDL_JoystickInstanceID(joystick)); gui_data.controller_count++; } else { g_warning("Could not open controller %i: %s", i, SDL_GetError()); } } } return true; } static gboolean init_audio(void) { bool audio_playing = GB_audio_is_playing(); if (gui_data.audio_initialized) { GB_audio_destroy(); gui_data.audio_initialized = false; } #ifdef USE_SDL_AUDIO if (SDL_InitSubSystem(SDL_INIT_AUDIO) < 0) { g_warning("Failed to initialize audio: %s", SDL_GetError()); return false; } #endif GB_audio_init(gui_data.sample_rate); GB_set_sample_rate(&gb, GB_audio_get_sample_rate()); // restore playing state GB_audio_set_paused(!audio_playing); return gui_data.audio_initialized = true; } static void gb_audio_callback(GB_gameboy_t *gb, GB_sample_t *sample) { if (gui_data.turbo_down) { static unsigned skip = 0; skip++; if (skip == GB_audio_get_sample_rate() / 8) { skip = 0; } if (skip > GB_audio_get_sample_rate() / 16) { return; } } if (GB_audio_get_queue_length() / sizeof(*sample) > GB_audio_get_sample_rate() / 4) { return; } GB_audio_queue_sample(sample); } static void rumble_callback(GB_gameboy_t *gb, double amp) { if (!gui_data.controllers || gui_data.controller_count == 0 || !gui_data.last_used_controller) return; struct Controller_t *s = gui_data.last_used_controller; if (s->ignore_rumble) return; if (s->haptic) { if (amp > 0.0) { SDL_HapticRumblePlay(s->haptic, amp, 100.0); } else { SDL_HapticRumbleStop(s->haptic); } } else { if (amp == 0.0) { SDL_GameControllerRumble(s->controller, 0, 0, 0); } else { Uint16 intensity = (float) 0xFFFF * amp; SDL_GameControllerRumble(s->controller, intensity, intensity, 100); } } } // Creating these items in the UI defintion files was buggy in some desktop // environments and the manual call of `g_signal_connect` was needed anyway // because the UI definition can’t define string arguments for signal handlers. static void create_model_menu_items() { bool on_change_model(GtkWidget *, gpointer); static const char *const model_names[] = { "Game Boy", "Super Game Boy", "Game Boy Color", "Game Boy Advance", NULL }; static const char *const model_codes[] = { "DMG", "SGB", "CGB", "GBA", NULL }; // Find the menu item index of the previous sibling of the new menu items GtkWidget *before = builder_get(GTK_WIDGET, "before_model_changer"); GtkContainer *parent = GTK_CONTAINER(gtk_widget_get_parent(before)); g_autoptr(GList) list = gtk_container_get_children(parent); gint position = g_list_index(list, before); CheckMenuItemGroup *model_group = check_menu_item_group_new((char **) model_names, (char **) model_codes); check_menu_item_group_insert_into_menu_shell(model_group, GTK_MENU_SHELL(parent), position + 1); check_menu_item_group_connect_toggle_signal(model_group, on_change_model); check_menu_item_group_activate(model_group, config.emulation.model); static const char *const peripheral_names[] = { "None", "Game Boy Printer", NULL }; static const char *const peripheral_codes[] = { "NONE", "PRINTER", NULL, }; CheckMenuItemGroup *link_group = check_menu_item_group_new((char **) peripheral_names, (char **) peripheral_codes); check_menu_item_group_insert_into_menu_shell(link_group, GTK_MENU_SHELL(builder_get(GTK_MENU_SHELL, "link_menu")), 0); // check_menu_item_group_connect_toggle_signal(link_group, on_change_linked_device); check_menu_item_group_activate(link_group, "NONE"); } // Create our application’s menu. // // This function tries to stick to the desktop environment’s conventions. // For the GNOME Shell it uses a hamburger menu, otherwise it either lets // the desktop environment shell handle the menu if it signals support for it // or uses a standard menubar inside the window. static void setup_menu(GApplication *app) { create_model_menu_items(); GtkMenuBar *menubar = builder_get(GTK_MENU_BAR, "main_menu"); gtk_box_pack_start(GTK_BOX(gui_data.main_window_container), GTK_WIDGET(menubar), false, false, 0); } // WHY DO WE NEED SUCH AN UGLY METHOD, GTK?! static void action_entries_set_enabled(const GActionEntry *entries, unsigned n_entries, bool value) { // Assumes null-terminated if n_entries == -1 for (unsigned i = 0; n_entries == -1 ? entries[i].name != NULL : i < n_entries; i++) { const GActionEntry *entry = &entries[i]; if (entry->name == NULL) continue; action_set_enabled(gui_data.main_application, entry->name, value); } } static void stop(void) { if (!gui_data.running) return; GB_audio_set_paused(true); GB_debugger_set_disabled(&gb, true); if (GB_debugger_is_stopped(&gb)) { // [self interruptDebugInputRead]; } gui_data.stopping = true; gui_data.running = false; while (gui_data.stopping); GB_debugger_set_disabled(&gb, false); gui_data.stopped = true; } static gboolean on_vblank(GB_gameboy_t *gb) { gb_screen_queue_render(gui_data.screen); gtk_widget_queue_draw(GTK_WIDGET(gui_data.vram_viewer)); return false; } static void vblank(GB_gameboy_t *gb) { gb_screen_flip(gui_data.screen); if (gui_data.border_mode_changed) { GB_set_border_mode(gb, config_get_display_border_mode()); gb_screen_set_resolution(gui_data.screen, GB_get_screen_width(gb), GB_get_screen_height(gb)); GB_set_pixels_output(gb, gb_screen_get_pixels(gui_data.screen)); gui_data.border_mode_changed = false; } GB_set_pixels_output(gb, gb_screen_get_pixels(gui_data.screen)); // Handle the speed modifiers: // The binary slowdown is limited to half speed. // The analog multiplier can go down to a third and up to three times full speed. if (gui_data.underclock_down && gui_data.clock_mutliplier > 0.5) { gui_data.clock_mutliplier -= 1.0 / 16; //gui_data.clock_mutliplier = clamp_double(0.5, 1.0, gui_data.clock_mutliplier - 1.0 / 16); GB_set_clock_multiplier(gb, gui_data.clock_mutliplier); } else if (!gui_data.underclock_down && gui_data.clock_mutliplier < 1.0) { gui_data.clock_mutliplier += 1.0 / 16; //gui_data.clock_mutliplier = clamp_double(0.5, 1.0, gui_data.clock_mutliplier + 1.0 / 16); GB_set_clock_multiplier(gb, gui_data.clock_mutliplier); } else if (config.controls.analog_speed_controls && gui_data.analog_clock_multiplier_valid) { GB_set_clock_multiplier(gb, gui_data.analog_clock_multiplier); if (gui_data.analog_clock_multiplier == 1.0) { gui_data.analog_clock_multiplier_valid = false; } } gui_data.do_rewind = gui_data.rewind_down; vram_viewer_update(gui_data.vram_viewer, gb); GB_frame_blending_mode_t mode = config_get_frame_blending_mode(); if (!gb_screen_get_previous_buffer(gui_data.screen)) { mode = GB_FRAME_BLENDING_MODE_DISABLED; } else if (mode == GB_FRAME_BLENDING_MODE_ACCURATE) { mode = GB_FRAME_BLENDING_MODE_DISABLED; if (GB_is_sgb(gb)) { mode = GB_FRAME_BLENDING_MODE_SIMPLE; } else { mode = GB_is_odd_frame(gb)? GB_FRAME_BLENDING_MODE_ACCURATE_ODD : GB_FRAME_BLENDING_MODE_ACCURATE_EVEN; } } gb_screen_set_blending_mode(gui_data.screen, mode); g_idle_add((GSourceFunc) on_vblank, gb); } static void handle_events(GB_gameboy_t *gb) { SDL_GameControllerUpdate(); uint8_t controller_state = 0; gui_data.analog_clock_multiplier = 1.0; for (unsigned i = 0; i < gui_data.controller_count; i++) { struct Controller_t *s = &gui_data.controllers[i]; int16_t left_x_axis = SDL_GameControllerGetAxis(s->controller, SDL_CONTROLLER_AXIS_LEFTX); int16_t left_y_axis = SDL_GameControllerGetAxis(s->controller, SDL_CONTROLLER_AXIS_LEFTY); int16_t right_x_axis = SDL_GameControllerGetAxis(s->controller, SDL_CONTROLLER_AXIS_RIGHTX); int16_t right_y_axis = SDL_GameControllerGetAxis(s->controller, SDL_CONTROLLER_AXIS_RIGHTY); if (config.controls.analog_speed_controls) { double left_trigger = (double) SDL_GameControllerGetAxis(s->controller, SDL_CONTROLLER_AXIS_TRIGGERLEFT) / (double) 32767; double right_trigger = (double) SDL_GameControllerGetAxis(s->controller, SDL_CONTROLLER_AXIS_TRIGGERRIGHT) / (double) 32767; if (left_trigger > 0.0) { gui_data.analog_clock_multiplier = min_double(gui_data.analog_clock_multiplier, clamp_double(1.0 / 3, 1.0, 1 - left_trigger + 0.2)); gui_data.analog_clock_multiplier_valid = true; } else if (right_trigger > 0.0) { gui_data.analog_clock_multiplier = max_double(gui_data.analog_clock_multiplier, clamp_double(1.0, 3.0, right_trigger * 3 + 0.8)); gui_data.analog_clock_multiplier_valid = true; } } if (left_x_axis >= JOYSTICK_HIGH || right_x_axis >= JOYSTICK_HIGH) { gui_data.last_used_controller = s; controller_state |= BUTTON_MASK_RIGHT; } else if (left_x_axis <= -JOYSTICK_HIGH || right_x_axis <= -JOYSTICK_HIGH) { gui_data.last_used_controller = s; controller_state |= BUTTON_MASK_LEFT; } if (left_y_axis >= JOYSTICK_HIGH || right_y_axis >= JOYSTICK_HIGH) { gui_data.last_used_controller = s; controller_state |= BUTTON_MASK_DOWN; } else if (left_y_axis <= -JOYSTICK_HIGH || right_y_axis <= -JOYSTICK_HIGH) { gui_data.last_used_controller = s; controller_state |= BUTTON_MASK_UP; } if (SDL_GameControllerGetButton(s->controller, SDL_CONTROLLER_BUTTON_DPAD_RIGHT)) { gui_data.last_used_controller = s; controller_state |= BUTTON_MASK_RIGHT; } if (SDL_GameControllerGetButton(s->controller, SDL_CONTROLLER_BUTTON_DPAD_LEFT)) { gui_data.last_used_controller = s; controller_state |= BUTTON_MASK_LEFT; } if (SDL_GameControllerGetButton(s->controller, SDL_CONTROLLER_BUTTON_DPAD_UP)) { gui_data.last_used_controller = s; controller_state |= BUTTON_MASK_UP; } if (SDL_GameControllerGetButton(s->controller, SDL_CONTROLLER_BUTTON_DPAD_DOWN)) { gui_data.last_used_controller = s; controller_state |= BUTTON_MASK_DOWN; } if (SDL_GameControllerGetButton(s->controller, SDL_CONTROLLER_BUTTON_A)) { gui_data.last_used_controller = s; controller_state |= BUTTON_MASK_A; } if (SDL_GameControllerGetButton(s->controller, SDL_CONTROLLER_BUTTON_B)) { gui_data.last_used_controller = s; controller_state |= BUTTON_MASK_B; } if (SDL_GameControllerGetButton(s->controller, SDL_CONTROLLER_BUTTON_BACK)) { gui_data.last_used_controller = s; controller_state |= BUTTON_MASK_SELECT; } if (SDL_GameControllerGetButton(s->controller, SDL_CONTROLLER_BUTTON_START)) { gui_data.last_used_controller = s; controller_state |= BUTTON_MASK_START; } } GB_set_key_state(gb, GB_KEY_RIGHT, (gui_data.pressed_buttons & BUTTON_MASK_RIGHT) | (controller_state & BUTTON_MASK_RIGHT)); GB_set_key_state(gb, GB_KEY_LEFT, (gui_data.pressed_buttons & BUTTON_MASK_LEFT) | (controller_state & BUTTON_MASK_LEFT)); GB_set_key_state(gb, GB_KEY_UP, (gui_data.pressed_buttons & BUTTON_MASK_UP) | (controller_state & BUTTON_MASK_UP)); GB_set_key_state(gb, GB_KEY_DOWN, (gui_data.pressed_buttons & BUTTON_MASK_DOWN) | (controller_state & BUTTON_MASK_DOWN)); GB_set_key_state(gb, GB_KEY_A, (gui_data.pressed_buttons & BUTTON_MASK_A) | (controller_state & BUTTON_MASK_A)); GB_set_key_state(gb, GB_KEY_B, (gui_data.pressed_buttons & BUTTON_MASK_B) | (controller_state & BUTTON_MASK_B)); GB_set_key_state(gb, GB_KEY_SELECT, (gui_data.pressed_buttons & BUTTON_MASK_SELECT) | (controller_state & BUTTON_MASK_SELECT)); GB_set_key_state(gb, GB_KEY_START, (gui_data.pressed_buttons & BUTTON_MASK_START) | (controller_state & BUTTON_MASK_START)); } static void load_boot_rom(GB_gameboy_t *gb, GB_boot_rom_t type) { GError *error = NULL; char *boot_rom_path = NULL; GBytes *boot_rom_f = NULL; const guchar *boot_rom_data; gsize boot_rom_size; static const char *const names[] = { [GB_BOOT_ROM_DMG0] = "dmg0_boot.bin", [GB_BOOT_ROM_DMG] = "dmg_boot.bin", [GB_BOOT_ROM_MGB] = "mgb_boot.bin", [GB_BOOT_ROM_SGB] = "sgb_boot.bin", [GB_BOOT_ROM_SGB2] = "sgb2_boot.bin", [GB_BOOT_ROM_CGB0] = "cgb0_boot.bin", [GB_BOOT_ROM_CGB] = "cgb_boot.bin", [GB_BOOT_ROM_AGB] = "agb_boot.bin", }; const char *const boot_rom_name = names[type]; if (gui_data.cli_options.boot_rom_path != NULL) { g_message("[CLI override] Trying to load boot ROM from %s", gui_data.cli_options.boot_rom_path); if (GB_load_boot_rom(gb, gui_data.cli_options.boot_rom_path)) { g_warning("Falling back to boot ROM from config"); goto config_boot_rom; } } else { config_boot_rom: if (config.emulation.boot_rom_path != NULL && g_strcmp0(config.emulation.boot_rom_path, "other") != 0 && g_strcmp0(config.emulation.boot_rom_path, "auto") != 0) { boot_rom_path = g_build_filename(config.emulation.boot_rom_path, boot_rom_name, NULL); g_message("Trying to load boot ROM from %s", boot_rom_path); if (GB_load_boot_rom(gb, boot_rom_path)) { g_free(boot_rom_path); g_warning("Falling back to internal boot ROM"); goto internal_boot_rom; } g_free(boot_rom_path); } else { internal_boot_rom: boot_rom_path = g_build_filename(RESOURCE_PREFIX "bootroms/", boot_rom_name, NULL); boot_rom_f = g_resources_lookup_data(boot_rom_path, G_RESOURCE_LOOKUP_FLAGS_NONE, &error); g_message("Loading internal boot ROM: %s", boot_rom_path); g_free(boot_rom_path); if (boot_rom_f == NULL) { g_warning("Failed to load internal boot ROM: %s", boot_rom_path); g_error_free(error); exit(EXIT_FAILURE); } boot_rom_data = g_bytes_get_data(boot_rom_f, &boot_rom_size); GB_load_boot_rom_from_buffer(gb, boot_rom_data, boot_rom_size); g_bytes_unref(boot_rom_f); } } } static char *wrapped_console_get_async_input(GB_gameboy_t *gb) { return console_get_async_input(gui_data.console, gb); } static char *wrapped_console_get_sync_input(GB_gameboy_t *gb) { return console_get_sync_input(gui_data.console, gb); } static void wrapped_console_log(GB_gameboy_t *gb, const char *message, GB_log_attributes attributes) { console_log(gui_data.console, message, attributes); } static void init(void) { if (GB_is_inited(&gb)) return; GB_init(&gb, config_get_model_type(&gui_data)); GB_set_vblank_callback(&gb, vblank); GB_set_rgb_encode_callback(&gb, rgb_encode); GB_set_pixels_output(&gb, gb_screen_get_current_buffer(gui_data.screen)); GB_set_color_correction_mode(&gb, config_get_color_correction_mode()); if (config_get_display_border_mode() <= GB_BORDER_ALWAYS) { GB_set_border_mode(&gb, config_get_display_border_mode()); } GB_apu_set_sample_callback(&gb, gb_audio_callback); GB_set_sample_rate(&gb, GB_audio_get_sample_rate()); GB_set_highpass_filter_mode(&gb, config_get_highpass_mode()); GB_set_log_callback(&gb, wrapped_console_log); GB_set_input_callback(&gb, wrapped_console_get_sync_input); GB_set_async_input_callback(&gb, wrapped_console_get_async_input); GB_set_boot_rom_load_callback(&gb, load_boot_rom); GB_set_update_input_hint_callback(&gb, handle_events); GB_set_rumble_callback(&gb, rumble_callback); GB_set_rumble_mode(&gb, config_get_rumble_mode()); GB_set_rewind_length(&gb, config.emulation.rewind_duration); } static void reset(void) { g_debug("Reset: %d == %d", config_get_model_type(&gui_data), gui_data.prev_model); GB_model_t current_model = config_get_model_type(&gui_data); if (gui_data.prev_model == -1 || gui_data.prev_model == current_model) { GB_reset(&gb); } else { GB_switch_model_and_reset(&gb, current_model); } GB_set_palette(&gb, config_get_monochrome_palette()); gui_data.prev_model = config_get_model_type(&gui_data); // Check SGB -> non-SGB and non-SGB to SGB transitions if (GB_get_screen_width(&gb) != gui_data.last_screen_width || GB_get_screen_height(&gb) != gui_data.last_screen_height) { gb_screen_set_resolution(gui_data.screen, GB_get_screen_width(&gb), GB_get_screen_height(&gb)); GB_set_pixels_output(&gb, gb_screen_get_pixels(gui_data.screen)); } bool success = false; if (gui_data.file) { char *path = g_file_get_path(gui_data.file); char *ext = strrchr(path, '.'); int result; GB_debugger_clear_symbols(&gb); if (g_strcmp0(ext + 1, "isx") == 0) { result = GB_load_isx(&gb, path); } else { result = GB_load_rom(&gb, path); } if (result == 0) { success = true; } else { g_warning("Failed to load ROM: %s", path); } GB_load_battery(&gb, gui_data.battery_save_path); GB_load_cheats(&gb, gui_data.cheats_save_path); GError *error = NULL; GBytes *register_sym_f = g_resources_lookup_data(RESOURCE_PREFIX "Misc/registers.sym", G_RESOURCE_LOOKUP_FLAGS_NONE, &error); if (register_sym_f) { gsize register_sym_size; const gchar *register_sym_data = g_bytes_get_data(register_sym_f, ®ister_sym_size); GB_debugger_load_symbol_file_from_buffer(&gb, register_sym_data, register_sym_size); g_bytes_unref(register_sym_f); } size_t path_length = strlen(path); char sym_file_path[path_length + 5]; replace_extension(path, path_length, sym_file_path, ".sym"); GB_debugger_load_symbol_file(&gb, sym_file_path); g_free(path); } action_set_enabled(gui_data.main_application, "close", success); action_entries_set_enabled(emulation_entries, G_N_ELEMENTS(emulation_entries), success); } static void start(void) { gui_data.running = true; gui_data.stopped = false; GB_audio_clear_queue(); GB_audio_set_paused(config.audio.muted); /* Run emulation */ while (gui_data.running) { if (gui_data.rewind_paused) { handle_events(&gb); g_usleep(G_USEC_PER_SEC / 8); } else { if (gui_data.do_rewind) { GB_rewind_pop(&gb); if (gui_data.turbo_down) { GB_rewind_pop(&gb); } if (!GB_rewind_pop(&gb)) { gui_data.rewind_paused = true; } gui_data.do_rewind = false; } GB_run(&gb); } } if (gui_data.file) { GB_save_battery(&gb, gui_data.battery_save_path); GB_save_cheats(&gb, gui_data.cheats_save_path); } gui_data.stopping = false; } // Prevent dependency loop static void run(void); // app.reset GAction // Resets the emulation static void activate_reset(GSimpleAction *action, GVariant *parameter, gpointer app) { if (!GB_is_inited(&gb)) { init(); } stop(); reset(); run(); } static gpointer run_thread(gpointer null_ptr) { if (!gui_data.file) return NULL; char *path = g_file_get_path(gui_data.file); size_t path_length = strlen(path); /* At the worst case, size is strlen(path) + 4 bytes for .sav + NULL */ char battery_save_path[path_length + 5]; char cheats_save_path[path_length + 5]; replace_extension(path, path_length, battery_save_path, ".sav"); replace_extension(path, path_length, cheats_save_path, ".cht"); gui_data.battery_save_path = battery_save_path; gui_data.cheats_save_path = cheats_save_path; if (!GB_is_inited(&gb)) { init(); } if (gui_data.stopped) { start(); } else { reset(); start(); } return NULL; } static void run(void) { if (gui_data.running) return; while (gui_data.stopping); g_thread_new("CoreLoop", run_thread, NULL); } // Tell our application to quit. // After this functions has been called the `shutdown` signal will be issued. // // TODO: Make sure we have a way to quit our emulation loop before `shutdown` gets called static void quit(void) { g_debug("quit(void);"); stop(); GtkWindow *window = gui_data.main_window ? GTK_WINDOW(gui_data.main_window) : NULL; save_config(window, gui_data.config_modification_date); free_config(); for (unsigned i = 0; i < gui_data.controller_count; i++) { struct Controller_t *s = &gui_data.controllers[i]; SDL_HapticClose(s->haptic); SDL_GameControllerClose(s->controller); } // Quit our application properly. // This fires the “shutdown” signal. g_application_quit(G_APPLICATION(gui_data.main_application)); } static void quit_interrupt(int ignored) { g_debug("quit_interrupt(%d);", ignored); quit(); } static void create_action_groups(GApplication *app) { g_action_map_add_action_entries(G_ACTION_MAP(app), emulation_entries, G_N_ELEMENTS(emulation_entries), NULL); g_action_map_add_action_entries(G_ACTION_MAP(app), developer_entries, G_N_ELEMENTS(developer_entries), NULL); g_action_map_add_action_entries(G_ACTION_MAP(app), app_entries, G_N_ELEMENTS(app_entries), NULL); g_action_map_add_action_entries(G_ACTION_MAP(app), file_entries, G_N_ELEMENTS(file_entries), NULL); g_action_map_add_action_entries(G_ACTION_MAP(app), edit_entries, G_N_ELEMENTS(edit_entries), NULL); action_set_enabled(app, "close", false); action_entries_set_enabled(emulation_entries, G_N_ELEMENTS(emulation_entries), false); } static gboolean on_key_press(GtkWidget *w, GdkEventKey *event, gpointer data) { uint8_t mask; if (event->keyval == key_map[INPUT_UP]) mask = BUTTON_MASK_UP; if (event->keyval == key_map[INPUT_DOWN]) mask = BUTTON_MASK_DOWN; if (event->keyval == key_map[INPUT_LEFT]) mask = BUTTON_MASK_LEFT; if (event->keyval == key_map[INPUT_RIGHT]) mask = BUTTON_MASK_RIGHT; if (event->keyval == key_map[INPUT_START]) mask = BUTTON_MASK_START; if (event->keyval == key_map[INPUT_SELECT]) mask = BUTTON_MASK_SELECT; if (event->keyval == key_map[INPUT_A]) mask = BUTTON_MASK_A; if (event->keyval == key_map[INPUT_B]) mask = BUTTON_MASK_B; if (event->keyval == key_map[INPUT_REWIND]) { gui_data.rewind_down = event->type == GDK_KEY_PRESS; GB_set_turbo_mode(&gb, gui_data.turbo_down, gui_data.turbo_down && gui_data.rewind_down); if (event->type == GDK_KEY_RELEASE) { gui_data.rewind_paused = false; } } if (event->keyval == key_map[INPUT_TURBO]) { gui_data.turbo_down = event->type == GDK_KEY_PRESS; gui_data.analog_clock_multiplier_valid = false; GB_audio_clear_queue(); GB_set_turbo_mode(&gb, gui_data.turbo_down, gui_data.turbo_down && gui_data.rewind_down); } if (event->keyval == key_map[INPUT_SLOWDOWN]) { gui_data.underclock_down = event->type == GDK_KEY_PRESS; gui_data.analog_clock_multiplier_valid = false; } if (event->keyval == key_map[INPUT_FULLSCREEN]) { if (event->type == GDK_KEY_RELEASE) { if (gui_data.is_fullscreen) { gtk_window_unfullscreen(GTK_WINDOW(gui_data.main_window)); } else { gtk_window_fullscreen(GTK_WINDOW(gui_data.main_window)); } } } if (event->type == GDK_KEY_PRESS) { gui_data.pressed_buttons |= mask; } else if (event->type == GDK_KEY_RELEASE) { gui_data.pressed_buttons &= ~mask; } return false; } static void on_window_state_change(GtkWidget *w, GdkEventWindowState *event, gpointer data) { gui_data.is_fullscreen = event->new_window_state & GDK_WINDOW_STATE_FULLSCREEN; } // This functions gets called immediately after registration of the GApplication static void startup(GApplication *app, gpointer null_ptr) { signal(SIGINT, quit_interrupt); g_debug("GTK version %u.%u.%u", gtk_get_major_version(), gtk_get_minor_version(), gtk_get_micro_version()); gui_data.builder = gtk_builder_new_from_resource(RESOURCE_PREFIX "ui/window.ui"); gtk_builder_connect_signals(gui_data.builder, NULL); create_action_groups(app); #if NDEBUG // Disable when not compiled in debug mode action_set_enabled(app, "open_gtk_debugger", false); #endif init_config(app, gui_data.cli_options.config_path, &gui_data.config_modification_date); gui_data.screen = gb_screen_new(gui_data.cli_options.force_software_renderer); gui_data.console = console_window_new(); gui_data.preferences = preferences_window_new(&gb); gui_data.vram_viewer = vram_viewer_window_new(); gui_data.memory_viewer = GTK_WINDOW(get_object("memory_viewer")); gui_data.printer = GTK_WINDOW(get_object("printer")); if (config.audio.sample_rate == -1) { gui_data.sample_rate = GB_audio_default_sample_rate(); } else { gui_data.sample_rate = config.audio.sample_rate; } // setup main window gui_data.main_window = GTK_APPLICATION_WINDOW(gtk_application_window_new(GTK_APPLICATION(app))); gui_data.main_window_container = GTK_BOX(gtk_box_new(GTK_ORIENTATION_VERTICAL, 0)); gtk_window_set_title(GTK_WINDOW(gui_data.main_window), "SameBoy"); gtk_application_window_set_show_menubar(gui_data.main_window, false); gtk_container_add(GTK_CONTAINER(gui_data.main_window), GTK_WIDGET(gui_data.main_window_container)); gtk_box_pack_end(GTK_BOX(gui_data.main_window_container), GTK_WIDGET(gui_data.screen), true, true, 0); setup_menu(app); // Insert separators into `GtkComboBox`es set_combo_box_row_separator_func(GTK_CONTAINER(gui_data.memory_viewer)); // Define a set of window icons GList *icon_list = NULL; static char* icons[] = { RESOURCE_PREFIX "logo_256.png", RESOURCE_PREFIX "logo_128.png", RESOURCE_PREFIX "logo_64.png", RESOURCE_PREFIX "logo_48.png", RESOURCE_PREFIX "logo_32.png", RESOURCE_PREFIX "logo_16.png" }; // Create list of GdkPixbufs for (int i = 0; i < (sizeof(icons) / sizeof(const char*)); ++i) { GdkPixbuf *icon = gdk_pixbuf_new_from_resource(icons[i], NULL); if (!icon) continue; icon_list = g_list_prepend(icon_list, icon); } // Let GTK choose the proper icon gtk_window_set_default_icon_list(icon_list); // Add missing information to the about dialog GtkAboutDialog *about_dialog = GTK_ABOUT_DIALOG(get_object("about_dialog")); gtk_about_dialog_set_logo(about_dialog, gdk_pixbuf_new_from_resource(icons[2], NULL)); // reuse the 64x64 icon gtk_about_dialog_set_version(about_dialog, "v" xstr(VERSION)); g_list_free_full(icon_list, g_object_unref); GdkScreen *screen = gdk_screen_get_default(); GtkCssProvider *provider = gtk_css_provider_new(); gtk_css_provider_load_from_resource(provider, RESOURCE_PREFIX "css/main.css"); gtk_style_context_add_provider_for_screen(screen, GTK_STYLE_PROVIDER(provider), GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); } G_MODULE_EXPORT void on_quit_activate(GtkWidget *w, gpointer user_data_ptr) { quit(); } bool on_change_model(GtkWidget *widget, gpointer user_data) { GtkCheckMenuItem *check_menu_item = GTK_CHECK_MENU_ITEM(widget); gchar *model_str = (gchar *) user_data; if (!gtk_check_menu_item_get_active(check_menu_item)) { return true; } else if (!GB_is_inited(&gb)) { gui_data.cli_options.model = -1; config.emulation.model = model_str; return false; } GtkMessageDialog *dialog = GTK_MESSAGE_DIALOG(gtk_message_dialog_new( GTK_WINDOW(gui_data.main_window), GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT, GTK_MESSAGE_QUESTION, GTK_BUTTONS_YES_NO, "Changing the emulated model requires a reset.\nChange model and reset game?" )); stop(); gint result = gtk_dialog_run(GTK_DIALOG(dialog)); switch (result) { case GTK_RESPONSE_YES: // Reset the CLI model override gui_data.cli_options.model = -1; config.emulation.model = model_str; reset(); break; default: // Action has been canceled break; } run(); gtk_widget_destroy(GTK_WIDGET(dialog)); return result != GTK_RESPONSE_YES; } void on_preferences_notify_border(PreferencesWindow *pref, const gchar *name) { gui_data.border_mode_changed = true; } void on_preferences_notify_shader(PreferencesWindow *pref, const gchar *name) { gb_screen_set_shader(gui_data.screen, name); } void on_preferences_notify_sample_rate(PreferencesWindow *pref, const guint *sample_rate) { if (*sample_rate == -1) { gui_data.sample_rate = GB_audio_default_sample_rate(); } else { gui_data.sample_rate = *sample_rate; } init_audio(); } static void connect_signal_handlers(GApplication *app) { // Connect signal handlers gtk_widget_add_events(GTK_WIDGET(gui_data.main_window), GDK_KEY_PRESS_MASK); gtk_widget_add_events(GTK_WIDGET(gui_data.main_window), GDK_KEY_RELEASE_MASK); g_signal_connect(gui_data.main_window, "destroy", G_CALLBACK(on_quit_activate), app); g_signal_connect(gui_data.main_window, "key-press-event", G_CALLBACK(on_key_press), NULL); g_signal_connect(gui_data.main_window, "key-release-event", G_CALLBACK(on_key_press), NULL); g_signal_connect(gui_data.main_window, "window-state-event", G_CALLBACK(on_window_state_change), NULL); // Just hide our sub-windows when closing them g_signal_connect(gui_data.preferences, "delete-event", G_CALLBACK(gtk_widget_hide_on_delete), NULL); g_signal_connect(gui_data.vram_viewer, "delete-event", G_CALLBACK(gtk_widget_hide_on_delete), NULL); g_signal_connect(gui_data.memory_viewer, "delete-event", G_CALLBACK(gtk_widget_hide_on_delete), NULL); g_signal_connect(gui_data.console, "delete-event", G_CALLBACK(gtk_widget_hide_on_delete), NULL); g_signal_connect(gui_data.printer, "delete-event", G_CALLBACK(gtk_widget_hide_on_delete), NULL); g_signal_connect(gui_data.preferences, "pref-update::video-display-border-mode", G_CALLBACK(on_preferences_notify_border), NULL); g_signal_connect(gui_data.preferences, "pref-update::video-shader", G_CALLBACK(on_preferences_notify_shader), NULL); g_signal_connect(gui_data.preferences, "pref-update::audio-sample-rate", G_CALLBACK(on_preferences_notify_sample_rate), NULL); } // This function gets called when the GApplication gets activated, i.e. it is ready to show widgets. static void activate(GApplication *app, gpointer null_ptr) { init_audio(); init_controllers(); connect_signal_handlers(app); if (gui_data.cli_options.fullscreen) { gtk_window_fullscreen(GTK_WINDOW(gui_data.main_window)); } gtk_application_add_window(GTK_APPLICATION(app), GTK_WINDOW(gui_data.main_window)); gtk_widget_show_all(GTK_WIDGET(gui_data.main_window)); // Start the emulation thread run(); } // This function gets called when the application is closed. static void shutdown(GApplication *app, GFile **files, gint n_files, const gchar *hint, gpointer null_ptr) { g_debug("SHUTDOWN"); stop(); SDL_Quit(); GB_free(&gb); g_object_unref(gui_data.builder); } // This function gets called when there are files to open. // Note: When `open` gets called `activate` won’t fire unless we call it ourselves. static void open(GApplication *app, GFile **files, gint n_files, const gchar *hint, gpointer null_ptr) { if (n_files > 1) { g_warning("More than one file specified"); exit(EXIT_FAILURE); } gui_data.file = g_file_dup(files[0]); // We have handled the files, now activate the application activate(app, NULL); } // app.about GAction // Opens the about dialog static void activate_about(GSimpleAction *action, GVariant *parameter, gpointer app) { GObject *dialog = get_object("about_dialog"); gtk_dialog_run(GTK_DIALOG(dialog)); gtk_widget_hide(GTK_WIDGET(dialog)); } // app.preferences GAction // Opens the preferences window static void activate_preferences(GSimpleAction *action, GVariant *parameter, gpointer app) { gtk_widget_show_all(GTK_WIDGET(gui_data.preferences)); } // app.show_console GAction // Opens the console static void activate_show_console(GSimpleAction *action, GVariant *parameter, gpointer app) { gtk_widget_show_all(GTK_WIDGET(gui_data.console)); } // app.open_gtk_debugger GAction // Opens the GTK debugger static void activate_open_gtk_debugger(GSimpleAction *action, GVariant *parameter, gpointer app) { gtk_window_set_interactive_debugging(true); } // app.open_memory_viewer GAction // Opens the memory viewer window static void activate_open_memory_viewer(GSimpleAction *action, GVariant *parameter, gpointer app) { gtk_widget_show_all(GTK_WIDGET(gui_data.memory_viewer)); } // app.open_vram_viewer GAction // Opens the VRAM viewer window static void activate_open_vram_viewer(GSimpleAction *action, GVariant *parameter, gpointer app) { gtk_widget_show_all(GTK_WIDGET(gui_data.vram_viewer)); } // app.clear_console GAction // Clears the debugger console static void activate_clear_console(GSimpleAction *action, GVariant *parameter, gpointer app) { console_clear(gui_data.console); } // Closes a ROM static void close_rom(void) { stop(); GB_free(&gb); gb_screen_clear(gui_data.screen); gb_screen_queue_render(gui_data.screen); vram_viewer_clear(gui_data.vram_viewer); gtk_widget_queue_draw(GTK_WIDGET(gui_data.vram_viewer)); // Update menu action states action_set_enabled(gui_data.main_application, "close", false); action_entries_set_enabled(emulation_entries, G_N_ELEMENTS(emulation_entries), false); // Try force the queued redraws while (g_main_context_pending(NULL)) { g_main_context_iteration(NULL, FALSE); } } // app.open GAction // Opens a ROM file static void activate_open(GSimpleAction *action, GVariant *parameter, gpointer app) { stop(); GtkFileChooserNative *native = gtk_file_chooser_native_new("Open File", GTK_WINDOW(gui_data.main_window), GTK_FILE_CHOOSER_ACTION_OPEN, "_Open", "_Cancel"); gint res = gtk_native_dialog_run(GTK_NATIVE_DIALOG(native)); if (res == GTK_RESPONSE_ACCEPT) { const char* path = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(native)); gui_data.file = g_file_new_for_path(path); activate_reset(action, parameter, app); } else { run(); } g_object_unref(native); } // app.close GAction static void activate_close(GSimpleAction *action, GVariant *parameter, gpointer app) { close_rom(); } // app.quit GAction // Exits the application static void activate_quit(GSimpleAction *action, GVariant *parameter, gpointer app) { quit(); } static void on_mute_changed(GSimpleAction *action, GVariant *value, gpointer user_data_ptr) { config.audio.muted = g_variant_get_boolean(value); GB_audio_set_paused(config.audio.muted); g_simple_action_set_state(action, value); } static void on_pause_changed(GSimpleAction *action, GVariant *value, gpointer user_data_ptr) { if (g_variant_get_boolean(value)) { stop(); } else { run(); } g_simple_action_set_state(action, value); } G_MODULE_EXPORT void on_open_recent_activate(GtkRecentChooser *chooser, gpointer user_data_ptr) { stop(); gchar *uri = gtk_recent_chooser_get_current_uri(chooser); GFile *file = g_file_new_for_uri(uri); if (g_file_query_exists(file, NULL)) { gui_data.file = file; // Add the file back to the top of the list GtkRecentManager *manager = gtk_recent_manager_get_default(); gtk_recent_manager_add_item(manager, uri); // TODO: Not nice activate_reset(NULL, NULL, NULL); } else { // TODO g_warning("File not found: %s", uri); close_rom(); } } int main(int argc, char *argv[]) { gui_data.main_thread = g_thread_self(); // Create our GApplication and tell GTK that we are able to handle files gui_data.main_application = gtk_application_new(APP_ID, G_APPLICATION_NON_UNIQUE | G_APPLICATION_HANDLES_OPEN); // Define our command line parameters GOptionEntry entries[] = { { "version", 'v', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, NULL, "Show the application version", NULL }, { "fullscreen", 'f', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &gui_data.cli_options.fullscreen, "Start in fullscreen mode", NULL }, { "bootrom", 'b', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &gui_data.cli_options.boot_rom_path, "Path to the boot ROM to use", "" }, { "model", 'm', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, NULL, "Override the model type to emulate", "" }, { "config", 'c', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &gui_data.cli_options.config_path, "Override the path of the configuration file", "" }, { "no-gl", 's', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &gui_data.cli_options.force_software_renderer, "Do not use OpenGL for rendering", NULL }, { NULL } }; // Setup our command line information g_application_add_main_option_entries(G_APPLICATION(gui_data.main_application), entries); g_application_set_option_context_parameter_string(G_APPLICATION(gui_data.main_application), "[FILE…]"); g_application_set_option_context_summary(G_APPLICATION(gui_data.main_application), "SameBoy is an open source Game Boy (DMG) and Game Boy Color (CGB) emulator."); // Add signal handlers g_signal_connect(gui_data.main_application, "handle-local-options", G_CALLBACK(handle_local_options), NULL); g_signal_connect(gui_data.main_application, "startup", G_CALLBACK(startup), NULL); g_signal_connect(gui_data.main_application, "activate", G_CALLBACK(activate), NULL); g_signal_connect(gui_data.main_application, "open", G_CALLBACK(open), NULL); g_signal_connect(gui_data.main_application, "shutdown", G_CALLBACK(shutdown), NULL); // Start our GApplication main loop int status = g_application_run(G_APPLICATION(gui_data.main_application), argc, argv); g_object_unref(gui_data.main_application); return status; }