#define G_LOG_USE_STRUCTURED #include #include #include #include #include #include #include #include "settings.h" #include "shader.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 tileset_buffer_length 256 * 192 * 4 #define tilemap_buffer_length 256 * 256 * 4 #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); typedef struct{ int16_t x, y; uint16_t w, h; } Rect; typedef struct GuiData { struct CliOptionData { gchar *config_path; gchar *boot_rom_path; gchar *prefix; gboolean fullscreen; GB_model_t model; } cli_options; GFile *file; gint sample_rate; char *battery_save_path; char *cheats_save_path; GB_model_t prev_model; const GThread *main_thread; volatile bool running; volatile bool stopping; volatile bool stopped; // GTK pointers GtkApplication *main_application; GtkBuilder *builder; GtkApplicationWindow *main_window; GtkBox *main_window_container; GtkGLArea *gl_area; GtkDrawingArea *fallback_canvas; GtkWindow *preferences; GtkWindow *vram_viewer; GtkWindow *memory_viewer; GtkWindow *console; GtkWindow *printer; // Debugger state GtkTextBuffer *pending_console_output; gboolean in_sync_input; gchar *last_console_input; gboolean log_to_sidebar; gboolean should_clear_sidebar; GMutex debugger_input_mutex; GCond debugger_input_cond; GRecMutex console_output_lock; GPtrArray *debugger_input_queue; bool vram_viewer_visible; bool vram_viewer_updating; gchar *vram_viewer_active_tab; gboolean vram_viewer_is_cgb; uint8_t vram_viewer_palette_data[16][0x40]; GB_oam_info_t oam_info[40]; uint16_t oam_count; uint8_t oam_height; uint32_t tileset_buffer[tileset_buffer_length]; uint32_t tilemap_buffer[tilemap_buffer_length]; GMutex tileset_buffer_mutex; GMutex tilemap_buffer_mutex; Rect scroll_rect; // Audio and video bool audio_initialized; uint32_t *image_buffers[3]; unsigned char current_buffer; Rect viewport; bool border_mode_changed; bool is_fullscreen; bool supports_gl; shader_t shader; // Fast forward / slow motion bool underclock_down; bool rewind_down; bool do_rewind; bool rewind_paused; bool turbo_down; double clock_mutliplier; // Input uint8_t pressed_buttons; struct Controller_t { SDL_GameController *controller; SDL_Haptic *haptic; } *controllers; unsigned controller_count; } GuiData; // Initialize the GuiData static GuiData gui_data = { .cli_options = { .fullscreen = false, .model = -1, }, .prev_model = -1, .running = false, .stopping = false, .stopped = false, .in_sync_input = false, .log_to_sidebar = false, .should_clear_sidebar = false, .vram_viewer_visible = false, .vram_viewer_updating = false, .vram_viewer_active_tab = "", .vram_viewer_is_cgb = false, .audio_initialized = false, .border_mode_changed = false, .underclock_down = false, .rewind_down = false, .rewind_paused = false, .turbo_down = false, .clock_mutliplier = 1.0, }; GB_gameboy_t gb; typedef enum { INPUT_UP, INPUT_DOWN, INPUT_LEFT, INPUT_RIGHT, INPUT_A, INPUT_B, INPUT_START, INPUT_SELECT, INPUT_TURBO, INPUT_REWIND, INPUT_SLOWDOWN, INPUT_FULLSCREEN, } input_names_t; static unsigned key_map[] = { [INPUT_UP] = GDK_KEY_w, [INPUT_LEFT] = GDK_KEY_a, [INPUT_DOWN] = GDK_KEY_s, [INPUT_RIGHT] = GDK_KEY_d, [INPUT_A] = GDK_KEY_l, [INPUT_B] = GDK_KEY_k, [INPUT_START] = GDK_KEY_h, [INPUT_SELECT] = GDK_KEY_g, [INPUT_TURBO] = GDK_KEY_space, [INPUT_REWIND] = GDK_KEY_Tab, [INPUT_SLOWDOWN] = GDK_KEY_Shift_L, [INPUT_FULLSCREEN] = GDK_KEY_F11, }; // 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 void on_model_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 }, { "change_model", NULL, "s", "@s 'CGB'", on_model_changed }, }; static void replace_extension(const char *src, size_t length, char *dest, const char *ext) { memcpy(dest, src, length); dest[length] = 0; /* Remove extension */ for (size_t i = length; i--;) { if (dest[i] == '/') break; if (dest[i] == '.') { dest[i] = 0; break; } } /* Add new extension */ strcat(dest, ext); } static uint32_t rgb_encode(GB_gameboy_t *gb, uint8_t r, uint8_t g, uint8_t b) { return 0xFF000000 | (r << 16) | (g << 8) | b; } static uint32_t convert_color(uint16_t color) { const uint8_t r = ((uint16_t)(color & 0x1F) * 255) / 31; const uint8_t g = ((uint16_t)((color >> 5) & 0x1F) * 255) / 31; const uint8_t b = ((uint16_t)((color >> 10) & 0x1F) * 255) / 31; return (r << 16) | (g << 8) | b; } static void palette_color_data_func(GtkTreeViewColumn *col, GtkCellRenderer *renderer, GtkTreeModel *model, GtkTreeIter *iter, gpointer user_data_ptr) { const gchar *title = gtk_tree_view_column_get_title(col); const uint8_t color_index = g_ascii_strtoll(&title[6], NULL, 10); const uint8_t column_index = 2 + (2 * color_index); GValue color_val = G_VALUE_INIT; gtk_tree_model_get_value(model, iter, column_index, &color_val); gint color = g_value_get_int(&color_val); gchar *color_string = g_strdup_printf("#%06x", color); gint lightness = 0.299 * ((color >> 16) & 0xFF) + 0.587 * ((color >> 8) & 0xFF) + 0.114 * (color & 0xFF); GValue color_str = G_VALUE_INIT; g_value_init(&color_str, G_TYPE_STRING); g_value_set_string(&color_str, color_string); g_object_set_property(G_OBJECT(renderer), "background", &color_str); GValue fg_color_str = G_VALUE_INIT; g_value_init(&fg_color_str, G_TYPE_STRING); g_value_set_static_string(&fg_color_str, (lightness > 0x7F)? "#000000" : "#FFFFFF"); g_object_set_property(G_OBJECT(renderer), "foreground", &fg_color_str); g_value_unset(&color_val); g_value_unset(&color_str); g_value_unset(&fg_color_str); g_free(color_string); } 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; } if (g_variant_dict_lookup(options, "fullscreen", "b", &count)) { gui_data.cli_options.fullscreen = true; } // 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")) { gui_data.cli_options.prefix = "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")) { gui_data.cli_options.prefix = "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")) { gui_data.cli_options.prefix = "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.prefix = "AGB"; gui_data.cli_options.model = GB_MODEL_AGB; } else { gui_data.cli_options.prefix = NULL; g_warning("Unknown model: %s", model_name); exit(EXIT_FAILURE); } } return -1; } // The main function for the OpenGL version check workaround void gl_check_realize(GtkWidget *w, gpointer user_data_ptr) { gboolean *result = (gboolean *) user_data_ptr; GError *error = NULL; GdkWindow *gdk_window = gtk_widget_get_window(w); GdkGLContext *context = gdk_window_create_gl_context(gdk_window, &error); if (error != NULL) { g_warning("Failed to create context: %s", error->message); g_error_free(error); *result = false; } else { gdk_gl_context_make_current(context); int version = epoxy_gl_version(); g_object_run_dispose(G_OBJECT(context)); g_object_unref(context); context = NULL; gdk_gl_context_clear_current(); g_debug("OpenGL version: %d", version); *result = version >= 32; } } // Workaround to figure out if we have proper OpenGL support. // Otherwise the application would crash after our GtkGlArea is realized // and the context it uses is a legacy OpenGL 1.4 context because // GTK3 calls OpenGL 2.0+ functions on it. gboolean test_gl_support(void) { gboolean result = false; GtkWidget *window = gtk_window_new(GTK_WINDOW_TOPLEVEL); g_signal_connect(window, "realize", G_CALLBACK(gl_check_realize), &result); gtk_widget_realize(window); gtk_widget_destroy(window); window = NULL; return result; } 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); if (SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) < 0) { g_warning("Failed to initialize game controller support: %s", SDL_GetError()); return false; } if (SDL_InitSubSystem(SDL_INIT_HAPTIC) < 0) { g_warning("Failed to initialize haptic feedback support: %s", SDL_GetError()); } SDL_QuitSubSystem(SDL_INIT_EVENTS); SDL_GameControllerEventState(SDL_ENABLE); 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()); // Open the first available controller 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); s->haptic = SDL_HapticOpenFromJoystick(joystick); if (s->haptic) { SDL_HapticRumbleInit(s->haptic); } char guid_str[33]; SDL_JoystickGUID guid = SDL_JoystickGetGUID(joystick); SDL_JoystickGetGUIDString(guid, guid_str, sizeof(guid_str)); g_message("Controller #%u (%s): %s; Power level: %s", i, guid_str, SDL_GameControllerName(s->controller), get_sdl_joystick_power_level_name(power_level)); 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 GB_model_t get_model(void) { if (gui_data.cli_options.model != -1) { return gui_data.cli_options.model; } GAction *action = g_action_map_lookup_action(G_ACTION_MAP(gui_data.main_application), "change_model"); GVariant *value = g_action_get_state(action); const gchar *family = g_variant_get_string(value, NULL); if (g_strcmp0(family, "DMG") == 0) { return get_dmg_model(); } else if (g_strcmp0(family, "AGB") == 0) { return GB_MODEL_AGB; } else if (g_strcmp0(family, "SGB") == 0) { return get_sgb_model(); } return get_cgb_model(); } 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 == NULL || gui_data.controller_count == 0) return; // TODO struct Controller_t *s = &gui_data.controllers[0]; if (s->haptic) { if (amp > 0.0) { SDL_HapticRumblePlay(s->haptic, amp, 100.0); } else { SDL_HapticRumbleStop(s->haptic); } } } static void clear_sidebar(void) { GtkTextView *sidebar_output = builder_get(GTK_TEXT_VIEW, "console_sidebar_output"); GtkTextBuffer *sidebar_output_text_buf = gtk_text_view_get_buffer(sidebar_output); gtk_text_buffer_set_text(sidebar_output_text_buf, "", -1); } static gboolean scroll_to_bottom(GtkTextView *textview, GtkTextMark *mark) { GtkTextBuffer *buffer = gtk_text_view_get_buffer(textview); GtkTextIter iter; gtk_text_buffer_get_end_iter(buffer, &iter); gtk_text_iter_set_line_offset(&iter, 0); gtk_text_buffer_move_mark(buffer, mark, &iter); gtk_text_view_scroll_to_mark(textview, mark, 0.0, true, 0.0, 0.10); gtk_text_buffer_delete_mark(buffer, mark); return true; } static void append_pending_output(void) { g_rec_mutex_lock(&gui_data.console_output_lock); if (gui_data.should_clear_sidebar) { clear_sidebar(); gui_data.should_clear_sidebar = false; } if (gtk_text_buffer_get_char_count(gui_data.pending_console_output) > 0) { GtkTextView *text_view = builder_get(GTK_TEXT_VIEW, gui_data.log_to_sidebar? "console_sidebar_output" : "console_screen"); GtkTextBuffer *text_buf = gtk_text_view_get_buffer(text_view); GtkTextIter start; GtkTextIter end; gtk_text_buffer_get_start_iter(gui_data.pending_console_output, &start); gtk_text_buffer_get_end_iter(gui_data.pending_console_output, &end); GtkTextIter iter; gtk_text_buffer_get_end_iter(text_buf, &iter); gtk_text_buffer_insert_range(text_buf, &iter, &start, &end); scroll_to_bottom(text_view, gtk_text_buffer_create_mark(text_buf, NULL, &iter, true)); gtk_text_buffer_set_text(gui_data.pending_console_output, "", -1); } g_rec_mutex_unlock(&gui_data.console_output_lock); } static void update_debugger_sidebar(GB_gameboy_t *gb) { if (!GB_debugger_is_stopped(gb)) { return; } if (gui_data.main_thread != g_thread_self()) { g_idle_add((GSourceFunc) update_debugger_sidebar, gb); return; } g_rec_mutex_lock(&gui_data.console_output_lock); gui_data.should_clear_sidebar = true; append_pending_output(); gui_data.log_to_sidebar = true; g_rec_mutex_unlock(&gui_data.console_output_lock); GtkTextView *sidebar_input = builder_get(GTK_TEXT_VIEW, "console_sidebar_input"); GtkTextBuffer *sidebar_input_text_buf = gtk_text_view_get_buffer(sidebar_input); gint line_count = gtk_text_buffer_get_line_count(sidebar_input_text_buf); for (unsigned line = 0; line < line_count; ++line) { GtkTextIter start_iter; GtkTextIter end_iter; gunichar ch; gtk_text_buffer_get_iter_at_line(sidebar_input_text_buf, &start_iter, line); end_iter = start_iter; do { ch = gtk_text_iter_get_char(&end_iter); if (!gtk_text_iter_forward_char(&end_iter)) { break; } } while (ch != '\n'); gchar *cmd = gtk_text_buffer_get_text(sidebar_input_text_buf, &start_iter, &end_iter, false); g_strchug(cmd); // trim leading whitespace g_strchomp(cmd); // trim trailing whitespace if (g_strcmp0("", cmd) != 0) { char *duped = g_strdup(cmd); GB_attributed_log(gb, GB_LOG_BOLD, "%s:\n", duped); GB_debugger_execute_command(gb, duped); GB_log(gb, "\n"); g_free(duped); } g_free(cmd); } g_rec_mutex_lock(&gui_data.console_output_lock); append_pending_output(); gui_data.log_to_sidebar = false; g_rec_mutex_unlock(&gui_data.console_output_lock); } static void console_log(GB_gameboy_t *gb, const char *string, GB_log_attributes attributes) { g_rec_mutex_lock(&gui_data.console_output_lock); if (string != NULL && !g_str_equal("", string)) { GtkTextIter iter; GtkTextIter start; // Append attributed text to "gui_data.pending_console_output" GtkTextBuffer gtk_text_buffer_get_end_iter(gui_data.pending_console_output, &iter); GtkTextMark *start_mark = gtk_text_buffer_create_mark(gui_data.pending_console_output, NULL, &iter, true); gtk_text_buffer_insert(gui_data.pending_console_output, &iter, g_strdup(string), -1); gtk_text_buffer_get_iter_at_mark(gui_data.pending_console_output, &start, start_mark); if (attributes & GB_LOG_BOLD) { gtk_text_buffer_apply_tag_by_name(gui_data.pending_console_output, "bold", &start, &iter); } if (attributes & GB_LOG_DASHED_UNDERLINE) { gtk_text_buffer_apply_tag_by_name(gui_data.pending_console_output, "dashed_underline", &start, &iter); } if (attributes & GB_LOG_UNDERLINE) { gtk_text_buffer_apply_tag_by_name(gui_data.pending_console_output, "underline", &start, &iter); } gtk_text_buffer_delete_mark(gui_data.pending_console_output, start_mark); g_idle_add((GSourceFunc) append_pending_output, NULL); } g_rec_mutex_unlock(&gui_data.console_output_lock); } // Console TODO: // TODO: clear sidebar when switching to async mode // TODO: Command history (up / down arrow in input) // TODO: reverse search of commands // TODO: search in output static char *sync_console_input(GB_gameboy_t *gb) { update_debugger_sidebar(gb); console_log(gb, "> ", 0); gui_data.in_sync_input = true; g_mutex_lock(&gui_data.debugger_input_mutex); g_cond_wait(&gui_data.debugger_input_cond, &gui_data.debugger_input_mutex); gchar *input = NULL; const gchar *_input = g_ptr_array_index(gui_data.debugger_input_queue, 0); input = g_strdup(_input); gpointer ptr = g_ptr_array_remove_index(gui_data.debugger_input_queue, 0); if (ptr) g_free(ptr); g_mutex_unlock(&gui_data.debugger_input_mutex); gui_data.in_sync_input = false; return input; } static char *async_console_input(GB_gameboy_t *gb) { // TODO: This is rather ugly g_idle_add((GSourceFunc) clear_sidebar, NULL); if (gui_data.debugger_input_queue->len == 0) return NULL; g_mutex_lock(&gui_data.debugger_input_mutex); gchar *input = NULL; const gchar *_input = g_ptr_array_index(gui_data.debugger_input_queue, 0); if (_input) { input = g_strdup(_input); gpointer ptr = g_ptr_array_remove_index(gui_data.debugger_input_queue, 0); if (ptr) g_free(ptr); } g_mutex_unlock(&gui_data.debugger_input_mutex); return input; } // Returns a `GApplication`s `GMenuModel` by ID // GApplication menus are loaded from `gtk/menus.ui`, `gtk/menus-traditional.ui` and `gtk/menus-common.ui`. static GMenuModel *get_menu_model(GApplication *app, const char *id) { GMenu *menu; menu = gtk_application_get_menu_by_id(GTK_APPLICATION(app), id); return menu ? G_MENU_MODEL(g_object_ref_sink(menu)) : NULL; } // 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) { GMenuModel *menubar_model = get_menu_model(app, "menubar"); enum menubar_type_t menubar_type = get_show_menubar(); // Try to use a sane default if (menubar_type == MENUBAR_AUTO) { GtkSettings *settings = gtk_settings_get_default(); gboolean show_in_shell; g_object_get(settings, "gtk-shell-shows-menubar", &show_in_shell, NULL); const gchar *xdg_current_desktop = g_getenv("XDG_CURRENT_DESKTOP"); const gchar *gdm_session = g_getenv("GDMSESSION"); const gchar *desktop_session = g_getenv("DESKTOP_SESSION"); gchar *desktop = (gchar *)xdg_current_desktop; if (desktop == NULL || g_str_equal(desktop, "")) desktop = (gchar *)gdm_session; if (desktop == NULL || g_str_equal(desktop, "")) desktop = (gchar *)desktop_session; g_debug("XDG_CURRENT_DESKTOP: %s\nGDMSESSION: %s\nDESKTOP_SESSION: %s\nChosen value: %s\nShow menu in shell: %d", xdg_current_desktop, gdm_session, desktop_session, desktop, show_in_shell); if (desktop != NULL && show_in_shell) { menubar_type = MENUBAR_SHOW_IN_SHELL; } else if (desktop != NULL && g_str_match_string("GNOME", desktop, false)) { if (g_str_match_string("GNOME-Flashback", desktop, false) || g_str_match_string("GNOME-Classic", desktop, false)) { menubar_type = MENUBAR_SHOW_IN_WINDOW; } else if (gdm_session != NULL && (g_str_match_string("gnome-classic", gdm_session, false) || g_str_match_string("gnome-flashback", gdm_session, false))) { menubar_type = MENUBAR_SHOW_IN_WINDOW; } else { menubar_type = MENUBAR_SHOW_HAMBURGER; } } else { menubar_type = MENUBAR_SHOW_IN_WINDOW; } } switch (menubar_type) { case MENUBAR_AUTO: g_warning("Unreachable"); break; case MENUBAR_SHOW_IN_SHELL: g_debug("Showing menu in the shell"); gtk_application_set_menubar(GTK_APPLICATION(app), menubar_model); break; case MENUBAR_SHOW_IN_WINDOW: { g_debug("Showing menu in the window"); GtkMenuBar *menubar = GTK_MENU_BAR(gtk_menu_bar_new_from_model(menubar_model)); gtk_box_pack_start(GTK_BOX(gui_data.main_window_container), GTK_WIDGET(menubar), false, false, 0); break; } case MENUBAR_SHOW_HAMBURGER: { g_debug("Showing hamburger"); // Attach a custom title bar GtkWidget *titlebar = builder_get(GTK_WIDGET, "main_header_bar"); gtk_header_bar_set_title(GTK_HEADER_BAR(titlebar), gtk_window_get_title(GTK_WINDOW(gui_data.main_window))); gtk_window_set_titlebar(GTK_WINDOW(gui_data.main_window), titlebar); // Disable menubar gtk_application_set_menubar(GTK_APPLICATION(app), NULL); // Hook menubar up to the hamburger button GtkMenuButton *hamburger_button = GTK_MENU_BUTTON(get_object("hamburger_button")); gtk_menu_button_set_menu_model(hamburger_button, menubar_model); break; } } } // Determines if a ComboBox entry should be converted into a separator. // Each element with a text value of `` will be converted into a separator element. static gboolean is_separator(GtkTreeModel *model, GtkTreeIter *iter, gpointer data) { gchar *text = NULL; gtk_tree_model_get(model, iter, 0, &text, -1); gboolean result = g_strcmp0("", text) == 0; g_free(text); return result; } // Recursively goes through all children of the given container and sets // our `is_separator` function to all children of type`GtkComboBox` static void set_combo_box_row_separator_func(GtkContainer *container) { GList *children = gtk_container_get_children(container); for (GList *l = children; l; l = l->next) { if (GTK_IS_COMBO_BOX(l->data)) { gtk_combo_box_set_row_separator_func(GTK_COMBO_BOX(l->data), is_separator, NULL, NULL); } if (GTK_IS_CONTAINER(l->data)) { set_combo_box_row_separator_func(GTK_CONTAINER(l->data)); } } g_list_free(children); } // Determines how many frame buffers to use static unsigned char number_of_buffers(void) { if (gui_data.fallback_canvas) return 2; bool should_blend = get_frame_blending_mode() != GB_FRAME_BLENDING_MODE_DISABLED; return should_blend? 3 : 2; } // Returns the buffer that should be used by the Core to render a new frame to static uint32_t *get_pixels(void) { return gui_data.image_buffers[(gui_data.current_buffer + 1) % number_of_buffers()]; } // Returns the current finished frame static uint32_t *get_current_buffer(void) { return gui_data.image_buffers[gui_data.current_buffer]; } // Returns the previous finished frame static uint32_t *get_previous_buffer(void) { return gui_data.image_buffers[(gui_data.current_buffer + 2) % number_of_buffers()]; } // Cycles the buffers static void flip(void) { gui_data.current_buffer = (gui_data.current_buffer + 1) % number_of_buffers(); } // 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 update_window_geometry(void) { // Set size hints GdkGeometry hints; hints.min_width = GB_get_screen_width(&gb); hints.min_height = GB_get_screen_height(&gb); gtk_window_set_geometry_hints( GTK_WINDOW(gui_data.main_window), NULL, &hints, (GdkWindowHints)(GDK_HINT_MIN_SIZE) ); gtk_window_resize(GTK_WINDOW(gui_data.main_window), GB_get_screen_width(&gb) * 2, GB_get_screen_height(&gb) * 2 ); // Setup our image buffers if (gui_data.image_buffers[0]) g_free(gui_data.image_buffers[0]); if (gui_data.image_buffers[1]) g_free(gui_data.image_buffers[1]); if (gui_data.image_buffers[2]) g_free(gui_data.image_buffers[2]); size_t buffer_size = sizeof(gui_data.image_buffers[0][0]) * GB_get_screen_width(&gb) * GB_get_screen_height(&gb); gui_data.image_buffers[0] = g_malloc0(buffer_size); gui_data.image_buffers[1] = g_malloc0(buffer_size); gui_data.image_buffers[2] = g_malloc0(buffer_size); if (GB_is_inited(&gb)) { GB_set_pixels_output(&gb, get_pixels()); } } 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 void on_vblank(gpointer data) { if (!gui_data.vram_viewer_updating && gui_data.vram_viewer_visible) { gui_data.vram_viewer_updating = true; if (g_strcmp0("vram_viewer_sprites", gui_data.vram_viewer_active_tab) == 0) { GtkTreeIter iter; GtkTreeView *tree_view = builder_get(GTK_TREE_VIEW, "vram_viewer_sprites"); // gtk_tree_view_set_model(tree_view, NULL); // Do we need this? GtkListStore *store = gtk_list_store_new(7, GDK_TYPE_PIXBUF, // Preview image G_TYPE_STRING, // X position G_TYPE_STRING, // Y position G_TYPE_STRING, // Tile G_TYPE_STRING, // Tile Address G_TYPE_STRING, // OAM Address G_TYPE_STRING // Attributes ); gtk_tree_model_get_iter_first(GTK_TREE_MODEL(store), &iter); for (unsigned row = 0; row < gui_data.oam_count; ++row) { GdkPixbuf *pixbuf = gdk_pixbuf_new_from_bytes( g_bytes_new(gui_data.oam_info[row].image, 128 * sizeof(uint32_t)), GDK_COLORSPACE_RGB, true, 8, 8, gui_data.oam_height, 8 * sizeof(uint32_t) ); GdkPixbuf *dest = gdk_pixbuf_new(GDK_COLORSPACE_RGB, true, 8, 8 * 2, gui_data.oam_height * 2); gdk_pixbuf_scale(pixbuf, dest, 0, 0, 8 * 2, gui_data.oam_height * 2, 0, 0, 2.0, 2.0, GDK_INTERP_NEAREST ); gtk_list_store_insert_with_values(store, &iter, -1, 0, dest, 1, g_strdup_printf("%i", gui_data.oam_info[row].x - 8), 2, g_strdup_printf("%i", gui_data.oam_info[row].y - 16), 3, g_strdup_printf("$%02x", gui_data.oam_info[row].tile), 4, g_strdup_printf("$%04x", 0x8000 + gui_data.oam_info[row].tile * 0x10), 5, g_strdup_printf("$%04x", gui_data.oam_info[row].oam_addr), 6, gui_data.vram_viewer_is_cgb ? g_strdup_printf("%c%c%c%d%d", gui_data.oam_info[row].flags & 0x80? 'P' : '-', gui_data.oam_info[row].flags & 0x40? 'Y' : '-', gui_data.oam_info[row].flags & 0x20? 'X' : '-', gui_data.oam_info[row].flags & 0x08? 1 : 0, gui_data.oam_info[row].flags & 0x07) : g_strdup_printf("%c%c%c%d", gui_data.oam_info[row].flags & 0x80? 'P' : '-', gui_data.oam_info[row].flags & 0x40? 'Y' : '-', gui_data.oam_info[row].flags & 0x20? 'X' : '-', gui_data.oam_info[row].flags & 0x10? 1 : 0), -1 ); g_object_unref(pixbuf); g_object_unref(dest); } gtk_tree_view_set_model(tree_view, GTK_TREE_MODEL(store)); g_object_unref(store); } else if (g_strcmp0("vram_viewer_palettes", gui_data.vram_viewer_active_tab) == 0) { GtkTreeIter iter; GtkTreeView *tree_view = builder_get(GTK_TREE_VIEW, "vram_viewer_palettes"); // gtk_tree_view_set_model(tree_view, NULL); // Do we need this? GtkListStore *store = gtk_list_store_new(9, G_TYPE_STRING, // Name G_TYPE_STRING, // Color 0 string G_TYPE_INT, // Color 0 integer G_TYPE_STRING, // Color 1 string G_TYPE_INT, // Color 1 integer G_TYPE_STRING, // Color 2 string G_TYPE_INT, // Color 2 integer G_TYPE_STRING, // Color 3 string G_TYPE_INT // Color 3 integer ); gtk_tree_model_get_iter_first(GTK_TREE_MODEL(store), &iter); for (unsigned row = 0; row < 16; ++row) { uint8_t offset = (row & 7) * 4; uint16_t color_0 = (gui_data.vram_viewer_palette_data[row][((0 + offset) << 1) + 1] << 8) | gui_data.vram_viewer_palette_data[row][((0 + offset) << 1)]; uint16_t color_1 = (gui_data.vram_viewer_palette_data[row][((1 + offset) << 1) + 1] << 8) | gui_data.vram_viewer_palette_data[row][((1 + offset) << 1)]; uint16_t color_2 = (gui_data.vram_viewer_palette_data[row][((2 + offset) << 1) + 1] << 8) | gui_data.vram_viewer_palette_data[row][((2 + offset) << 1)]; uint16_t color_3 = (gui_data.vram_viewer_palette_data[row][((3 + offset) << 1) + 1] << 8) | gui_data.vram_viewer_palette_data[row][((3 + offset) << 1)]; gtk_list_store_insert_with_values(store, &iter, -1, 0, g_strdup_printf("%s %d", row >=8 ? "Object" : "Background", row & 7), 1, g_strdup_printf("$%04x", color_0 & 0x7FFF), 2, convert_color(color_0), 3, g_strdup_printf("$%04x", color_1 & 0x7FFF), 4, convert_color(color_1), 5, g_strdup_printf("$%04x", color_2 & 0x7FFF), 6, convert_color(color_2), 7, g_strdup_printf("$%04x", color_3 & 0x7FFF), 8, convert_color(color_3), -1 ); } GtkTreeViewColumn *column_0 = gtk_tree_view_get_column(tree_view, 1); GtkTreeViewColumn *column_1 = gtk_tree_view_get_column(tree_view, 2); GtkTreeViewColumn *column_2 = gtk_tree_view_get_column(tree_view, 3); GtkTreeViewColumn *column_3 = gtk_tree_view_get_column(tree_view, 4); GtkCellRendererText *cell_renderer_0 = builder_get(GTK_CELL_RENDERER_TEXT, "vram_viewer_palette_cell_renderer_0"); GtkCellRendererText *cell_renderer_1 = builder_get(GTK_CELL_RENDERER_TEXT, "vram_viewer_palette_cell_renderer_1"); GtkCellRendererText *cell_renderer_2 = builder_get(GTK_CELL_RENDERER_TEXT, "vram_viewer_palette_cell_renderer_2"); GtkCellRendererText *cell_renderer_3 = builder_get(GTK_CELL_RENDERER_TEXT, "vram_viewer_palette_cell_renderer_3"); gtk_tree_view_column_set_cell_data_func(column_0, GTK_CELL_RENDERER(cell_renderer_0), palette_color_data_func, NULL, NULL); gtk_tree_view_column_set_cell_data_func(column_1, GTK_CELL_RENDERER(cell_renderer_1), palette_color_data_func, NULL, NULL); gtk_tree_view_column_set_cell_data_func(column_2, GTK_CELL_RENDERER(cell_renderer_2), palette_color_data_func, NULL, NULL); gtk_tree_view_column_set_cell_data_func(column_3, GTK_CELL_RENDERER(cell_renderer_3), palette_color_data_func, NULL, NULL); gtk_tree_view_set_model(tree_view, GTK_TREE_MODEL(store)); g_object_unref(store); } // Queue a redraw of the VRAM viewer gtk_widget_queue_draw(GTK_WIDGET(gui_data.vram_viewer)); gui_data.vram_viewer_updating = false; } // Queue drawing of the current frame if (gui_data.fallback_canvas) { gtk_widget_queue_draw(GTK_WIDGET(gui_data.main_window)); } else if (gui_data.gl_area) { gtk_gl_area_queue_render(gui_data.gl_area); } } static void vblank(GB_gameboy_t *gb) { flip(); if (gui_data.border_mode_changed) { GB_set_border_mode(gb, get_display_border_mode()); update_window_geometry(); gui_data.border_mode_changed = false; } GB_set_pixels_output(gb, get_pixels()); if (gui_data.underclock_down && gui_data.clock_mutliplier > 0.5) { 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; GB_set_clock_multiplier(gb, gui_data.clock_mutliplier); } if (g_strcmp0("vram_viewer_tileset", gui_data.vram_viewer_active_tab) == 0) { const gchar *palette_id = gtk_combo_box_get_active_id(builder_get(GTK_COMBO_BOX, "vram_viewer_tileset_palette_selector")); GB_palette_type_t palette_type = g_str_has_prefix(palette_id, "bg")? GB_PALETTE_BACKGROUND : GB_PALETTE_OAM; uint8_t palette_index = g_ascii_digit_value(palette_id[palette_type == GB_PALETTE_OAM ? 3 : 2]); GB_draw_tileset(gb, gui_data.tileset_buffer, palette_type, palette_index ); } else if (g_strcmp0("vram_viewer_tilemap", gui_data.vram_viewer_active_tab) == 0) { const gchar *palette_id = gtk_combo_box_get_active_id(builder_get(GTK_COMBO_BOX, "vram_viewer_tilemap_palette_selector")); uint8_t palette_index = 0; GB_palette_type_t palette_type = GB_PALETTE_AUTO; if (g_strcmp0("auto", palette_id) != 0) { palette_type = g_str_has_prefix(palette_id, "bg")? GB_PALETTE_BACKGROUND : GB_PALETTE_OAM; palette_index = g_ascii_digit_value(palette_id[palette_type == GB_PALETTE_OAM ? 3 : 2]); } GB_map_type_t map_type = GB_MAP_AUTO; const gchar *map_type_id = gtk_combo_box_get_active_id(builder_get(GTK_COMBO_BOX, "vram_viewer_tilemap_tilemap_selector")); if (g_strcmp0("auto", map_type_id) != 0) { map_type = (g_strcmp0("9800", map_type_id) == 0)? GB_MAP_9800 : GB_MAP_9C00; } GB_tileset_type_t tileset_type = GB_TILESET_AUTO; const gchar *tileset_type_id = gtk_combo_box_get_active_id(builder_get(GTK_COMBO_BOX, "vram_viewer_tilemap_tileset_selector")); if (g_strcmp0("auto", tileset_type_id) != 0) { tileset_type = (g_strcmp0("8800", tileset_type_id) == 0)? GB_TILESET_8800 : GB_TILESET_8000; } GB_draw_tilemap(gb, gui_data.tilemap_buffer, palette_type, palette_index, map_type, tileset_type ); gui_data.scroll_rect = (Rect){ GB_read_memory(gb, 0xFF00 | GB_IO_SCX), GB_read_memory(gb, 0xFF00 | GB_IO_SCY), 160, 144 }; } else if (g_strcmp0("vram_viewer_sprites", gui_data.vram_viewer_active_tab) == 0) { gui_data.oam_count = GB_get_oam_info(gb, gui_data.oam_info, &gui_data.oam_height); gui_data.vram_viewer_is_cgb = GB_is_cgb(gb); } else if (g_strcmp0("vram_viewer_palettes", gui_data.vram_viewer_active_tab) == 0) { size_t size; for (unsigned row = 0; row < 16; ++row) { uint8_t *palette_data = GB_get_direct_access(gb, row >= 8? GB_DIRECT_ACCESS_OBP : GB_DIRECT_ACCESS_BGP, &size, NULL); memcpy(gui_data.vram_viewer_palette_data[row], palette_data, size); } } gui_data.do_rewind = gui_data.rewind_down; g_idle_add((GSourceFunc) on_vblank, NULL); } static void handle_events(GB_gameboy_t *gb) { SDL_GameControllerUpdate(); uint8_t controller_state = 0; if (gui_data.controllers && gui_data.controllers[0].controller) { int16_t x_axis = SDL_GameControllerGetAxis(gui_data.controllers[0].controller, SDL_CONTROLLER_AXIS_LEFTX); int16_t y_axis = SDL_GameControllerGetAxis(gui_data.controllers[0].controller, SDL_CONTROLLER_AXIS_LEFTY); if (x_axis >= JOYSTICK_HIGH) { controller_state |= BUTTON_MASK_RIGHT; } else if (x_axis <= -JOYSTICK_HIGH) { controller_state |= BUTTON_MASK_LEFT; } if (y_axis >= JOYSTICK_HIGH) { controller_state |= BUTTON_MASK_DOWN; } else if (y_axis <= -JOYSTICK_HIGH) { controller_state |= BUTTON_MASK_UP; } if (SDL_GameControllerGetButton(gui_data.controllers[0].controller, SDL_CONTROLLER_BUTTON_DPAD_RIGHT)) controller_state |= BUTTON_MASK_RIGHT; if (SDL_GameControllerGetButton(gui_data.controllers[0].controller, SDL_CONTROLLER_BUTTON_DPAD_LEFT)) controller_state |= BUTTON_MASK_LEFT; if (SDL_GameControllerGetButton(gui_data.controllers[0].controller, SDL_CONTROLLER_BUTTON_DPAD_UP)) controller_state |= BUTTON_MASK_UP; if (SDL_GameControllerGetButton(gui_data.controllers[0].controller, SDL_CONTROLLER_BUTTON_DPAD_DOWN)) controller_state |= BUTTON_MASK_DOWN; if (SDL_GameControllerGetButton(gui_data.controllers[0].controller, SDL_CONTROLLER_BUTTON_A)) controller_state |= BUTTON_MASK_A; if (SDL_GameControllerGetButton(gui_data.controllers[0].controller, SDL_CONTROLLER_BUTTON_B)) controller_state |= BUTTON_MASK_B; if (SDL_GameControllerGetButton(gui_data.controllers[0].controller, SDL_CONTROLLER_BUTTON_BACK)) controller_state |= BUTTON_MASK_SELECT; if (SDL_GameControllerGetButton(gui_data.controllers[0].controller, SDL_CONTROLLER_BUTTON_START)) 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.boot_rom_path != NULL && g_strcmp0(config.boot_rom_path, "other") != 0 && g_strcmp0(config.boot_rom_path, "auto") != 0) { boot_rom_path = g_build_filename(config.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 void init(void) { if (GB_is_inited(&gb)) return; GB_init(&gb, get_model()); GB_set_vblank_callback(&gb, vblank); GB_set_pixels_output(&gb, get_current_buffer()); GB_set_rgb_encode_callback(&gb, rgb_encode); GB_set_sample_rate(&gb, GB_audio_get_sample_rate()); GB_set_color_correction_mode(&gb, get_color_correction_mode()); GB_set_highpass_filter_mode(&gb, get_highpass_mode()); GB_set_rewind_length(&gb, config.rewind_duration); GB_set_update_input_hint_callback(&gb, handle_events); GB_apu_set_sample_callback(&gb, gb_audio_callback); GB_set_input_callback(&gb, sync_console_input); GB_set_async_input_callback(&gb, async_console_input); GB_set_log_callback(&gb, console_log); GB_set_boot_rom_load_callback(&gb, load_boot_rom); GB_set_rumble_callback(&gb, rumble_callback); GB_set_rumble_mode(&gb, GB_RUMBLE_CARTRIDGE_ONLY); // TODO if (get_display_border_mode() <= GB_BORDER_ALWAYS) { GB_set_border_mode(&gb, get_display_border_mode()); } update_window_geometry(); } static void reset(void) { g_debug("Reset: %d == %d", get_model(), gui_data.prev_model); GB_model_t current_model = get_model(); 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, get_monochrome_palette()); gui_data.prev_model = get_model(); GtkRequisition minimum_size; GtkRequisition natural_size; gtk_widget_get_preferred_size(GTK_WIDGET(gui_data.main_window), &minimum_size, &natural_size); // Check SGB -> non-SGB and non-SGB to SGB transitions if (GB_get_screen_width(&gb) != minimum_size.width || GB_get_screen_height(&gb) != minimum_size.height) { update_window_geometry(); } bool success = false; if (gui_data.file) { char *path = g_file_get_path(gui_data.file); if (GB_load_rom(&gb, path) == 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(false); /* 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 (gui_data.stopped) { start(); } else { init(); 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) { stop(); 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) { quit(); } // `destroy` signal GCallback // Exits the application static void on_quit(GtkWidget *w, gpointer app) { 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; 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; } 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; } // Gets called when the VRAM viewer gets realized static void on_vram_viewer_realize(void) { gui_data.vram_viewer_visible = true; gui_data.vram_viewer_active_tab = (gchar *)gtk_stack_get_visible_child_name(builder_get(GTK_STACK, "vram_viewer_stack")); } // Gets called when the VRAM viewer gets unrealized static void on_vram_viewer_unrealize(void) { gui_data.vram_viewer_visible = false; } // Gets called when the tileset viewer should be redrawn static gboolean on_draw_vram_viewer_tileset(GtkWidget *widget, cairo_t *cr, gpointer data) { g_mutex_lock(&gui_data.tileset_buffer_mutex); guint width, height; GtkStyleContext *context; context = gtk_widget_get_style_context(widget); width = gtk_widget_get_allocated_width(widget); height = gtk_widget_get_allocated_height(widget); gtk_render_background(context, cr, 0, 0, width, height); cairo_surface_t *surface = cairo_image_surface_create_for_data( (unsigned char *) gui_data.tileset_buffer, CAIRO_FORMAT_RGB24, 256, 192, cairo_format_stride_for_width(CAIRO_FORMAT_RGB24, 256) ); cairo_scale(cr, 2.0, 2.0); cairo_set_source_surface(cr, surface, 0, 0); cairo_pattern_set_filter(cairo_get_source(cr), CAIRO_FILTER_NEAREST); cairo_paint(cr); if (gtk_toggle_button_get_active(builder_get(GTK_TOGGLE_BUTTON, "vram_viewer_tileset_toggle_grid_button"))) { cairo_set_source_rgba(cr, 0.0, 0.0, 0.0, 0.25); cairo_set_line_width(cr, 1); const int divisions_x = 256 / 8; const int divisions_y = 192 / 8; for (int i = 0; i < divisions_x; i++) { const int j = 256 * i; cairo_move_to(cr, j / divisions_x, 0); cairo_line_to(cr, j / divisions_x, 192); } for (int i = 0; i < divisions_y; i++) { const int j = 192 * i; cairo_move_to(cr, 0, j / divisions_y); cairo_line_to(cr, 256, j / divisions_y); } cairo_stroke(cr); } g_mutex_unlock(&gui_data.tileset_buffer_mutex); return false; } // Gets called when the tilemap viewer should be redrawn static gboolean on_draw_vram_viewer_tilemap(GtkWidget *widget, cairo_t *cr, gpointer data) { g_mutex_lock(&gui_data.tilemap_buffer_mutex); guint width, height; GtkStyleContext *context; context = gtk_widget_get_style_context(widget); width = gtk_widget_get_allocated_width(widget); height = gtk_widget_get_allocated_height(widget); gtk_render_background(context, cr, 0, 0, width, height); cairo_surface_t *surface = cairo_image_surface_create_for_data( (unsigned char *) gui_data.tilemap_buffer, CAIRO_FORMAT_RGB24, 256, 256, cairo_format_stride_for_width(CAIRO_FORMAT_RGB24, 256) ); cairo_scale(cr, 2.0, 2.0); cairo_set_source_surface(cr, surface, 0, 0); cairo_pattern_set_filter(cairo_get_source(cr), CAIRO_FILTER_NEAREST); cairo_paint(cr); if (gtk_toggle_button_get_active(builder_get(GTK_TOGGLE_BUTTON, "vram_viewer_tilemap_toggle_grid_button"))) { cairo_set_source_rgba(cr, 0.0, 0.0, 0.0, 0.25); cairo_set_line_width(cr, 1); const int divisions = 256 / 8; for (int i = 0; i < divisions; i++) { const int j = 256 * i; cairo_move_to(cr, j / divisions, 0); cairo_line_to(cr, j / divisions, 256); cairo_move_to(cr, 0, j / divisions); cairo_line_to(cr, 256, j / divisions); } cairo_stroke(cr); } if (gtk_toggle_button_get_active(builder_get(GTK_TOGGLE_BUTTON, "vram_viewer_tilemap_toggle_scrolling_button"))) { cairo_rectangle(cr, -2, -2, width + 2, height + 2); for (unsigned x = 0; x < 2; x++) { for (unsigned y = 0; y < 2; y++) { Rect rect = gui_data.scroll_rect; rect.x -= 256 * x; rect.y += 256 * y; cairo_rectangle(cr, rect.x, rect.y, rect.w, rect.h); } } cairo_set_fill_rule(cr, CAIRO_FILL_RULE_EVEN_ODD); cairo_set_line_width(cr, 2); cairo_set_line_join(cr, CAIRO_LINE_JOIN_ROUND); cairo_set_source_rgba(cr, 0.2, 0.2, 0.2, 0.5); cairo_fill_preserve(cr); cairo_clip_preserve(cr); cairo_set_source_rgba(cr, 0.0, 0.0, 0.0, 0.6); cairo_stroke(cr); } g_mutex_unlock(&gui_data.tilemap_buffer_mutex); return false; } static gboolean on_motion_vram_viewer_tileset(GtkWidget *widget, GdkEventMotion *event) { int x, y; if (event->is_hint) { gdk_window_get_pointer (event->window, &x, &y, NULL); } else { x = event->x; y = event->y; } // Compensate for our canvas scale x /= 2; y /= 2; uint8_t bank = x >= 128? 1 : 0; x &= 127; uint16_t tile = x / 8 + y / 8 * 16; GtkLabel *status = builder_get(GTK_LABEL, "vram_viewer_status"); gtk_label_set_text(status, g_strdup_printf("Tile number $%02x at %d:$%04x", tile & 0xFF, bank, 0x8000 + tile * 0x10)); return true; } static gboolean on_motion_vram_viewer_tilemap(GtkWidget *widget, GdkEventMotion *event) { int x, y; if (event->is_hint) { gdk_window_get_pointer (event->window, &x, &y, NULL); } else { x = event->x; y = event->y; } // Compensate for our canvas scale x /= 2; y /= 2; GtkLabel *status = builder_get(GTK_LABEL, "vram_viewer_status"); uint16_t map_offset = x / 8 + y / 8 * 32; uint16_t map_base = 0x1800; GB_map_type_t map_type = GB_MAP_AUTO; const gchar *map_type_id = gtk_combo_box_get_active_id(builder_get(GTK_COMBO_BOX, "vram_viewer_tilemap_tilemap_selector")); if (g_strcmp0("auto", map_type_id) != 0) { map_type = (g_strcmp0("9800", map_type_id) == 0)? GB_MAP_9800 : GB_MAP_9C00; } GB_tileset_type_t tileset_type = GB_TILESET_AUTO; const gchar *tileset_type_id = gtk_combo_box_get_active_id(builder_get(GTK_COMBO_BOX, "vram_viewer_tilemap_tileset_selector")); if (g_strcmp0("auto", tileset_type_id) != 0) { tileset_type = (g_strcmp0("8800", tileset_type_id) == 0)? GB_TILESET_8800 : GB_TILESET_8000; } uint8_t lcdc = ((uint8_t *)GB_get_direct_access(&gb, GB_DIRECT_ACCESS_IO, NULL, NULL))[GB_IO_LCDC]; uint8_t *vram = GB_get_direct_access(&gb, GB_DIRECT_ACCESS_VRAM, NULL, NULL); if (map_type == GB_MAP_9C00 || (map_type == GB_MAP_AUTO && lcdc & 0x08)) { map_base = 0x1c00; } if (tileset_type == GB_TILESET_AUTO) { tileset_type = (lcdc & 0x10)? GB_TILESET_8800 : GB_TILESET_8000; } uint8_t tile = vram[map_base + map_offset]; uint16_t tile_address = 0; if (tileset_type == GB_TILESET_8000) { tile_address = 0x8000 + tile * 0x10; } else { tile_address = 0x9000 + (int8_t)tile * 0x10; } if (GB_is_cgb(&gb)) { uint8_t attributes = vram[map_base + map_offset + 0x2000]; gtk_label_set_text(status, g_strdup_printf("Tile number $%02x (%d:$%04x) at map address $%04x (Attributes: %c%c%c%d%d)", tile, attributes & 0x8? 1 : 0, tile_address, 0x8000 + map_base + map_offset, (attributes & 0x80) ? 'P' : '-', (attributes & 0x40) ? 'V' : '-', (attributes & 0x20) ? 'H' : '-', attributes & 0x8? 1 : 0, attributes & 0x7 )); } else { gtk_label_set_text(status, g_strdup_printf("Tile number $%02x ($%04x) at map address $%04x", tile, tile_address, 0x8000 + map_base + map_offset )); } return true; } static void on_vram_tab_change(GtkWidget *widget, GParamSpec *pspec, GtkStackSwitcher *self) { gtk_label_set_text(builder_get(GTK_LABEL, "vram_viewer_status"), ""); gui_data.vram_viewer_active_tab = (gchar *)gtk_stack_get_visible_child_name(builder_get(GTK_STACK, "vram_viewer_stack")); } // This functions gets called immediately after registration of the GApplication static void startup(GApplication *app, gpointer null_ptr) { signal(SIGINT, quit_interrupt); // Very ugly workaround for GtkGlArea! // When a GtkGlArea is realized and it creates a legacy GL 1.4 context // it tries to use GL 2.0 functions to render the window which leads to the application crashing. // So we initialize GTK, create a dummy GtkWindow object, attach a `realize` callback and // in this callback create a GdkGLContext on this window. But instead of running the GTK main loop // we just realize and destroy the dummy window and compare the context’s version in the realize callback. gui_data.supports_gl = test_gl_support(); g_debug("OpenGL supported: %s", gui_data.supports_gl? "Yes" : "No"); 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 (gui_data.cli_options.prefix != NULL) { GAction *action = g_action_map_lookup_action(G_ACTION_MAP(gui_data.main_application), "change_model"); g_action_change_state(action, g_variant_new_string(gui_data.cli_options.prefix)); } #if NDEBUG // Disable when not compiled in debug mode action_set_enabled(app, "open_gtk_debugger", false); // Remove the menubar override gtk_widget_destroy(builder_get(GTK_WIDGET, "menubar_override_selector_label")); gtk_widget_destroy(builder_get(GTK_WIDGET, "menubar_override_selector")); #endif gui_data.preferences = GTK_WINDOW(get_object("preferences")); g_signal_connect(gui_data.preferences, "realize", G_CALLBACK(on_preferences_realize), (gpointer) gui_data.builder); init_settings(gui_data.cli_options.config_path, gui_data.preferences); gui_data.vram_viewer = GTK_WINDOW(get_object("vram_viewer")); gui_data.memory_viewer = GTK_WINDOW(get_object("memory_viewer")); gui_data.console = GTK_WINDOW(get_object("console")); gui_data.printer = GTK_WINDOW(get_object("printer")); if (config.sample_rate == -1) { gui_data.sample_rate = GB_audio_default_sample_rate(); } else { gui_data.sample_rate = config.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)); setup_menu(app); // Insert separators into `GtkComboBox`es set_combo_box_row_separator_func(GTK_CONTAINER(gui_data.preferences)); set_combo_box_row_separator_func(GTK_CONTAINER(gui_data.vram_viewer)); 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_icon_list(GTK_WINDOW(gui_data.main_window), 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); } 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), 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); g_signal_connect(gui_data.vram_viewer, "realize", G_CALLBACK(on_vram_viewer_realize), NULL); g_signal_connect(gui_data.vram_viewer, "unrealize", G_CALLBACK(on_vram_viewer_unrealize), 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(get_object("vram_viewer_tileset_canvas"), "draw", G_CALLBACK(on_draw_vram_viewer_tileset), NULL); g_signal_connect(get_object("vram_viewer_tilemap_canvas"), "draw", G_CALLBACK(on_draw_vram_viewer_tilemap), NULL); gtk_widget_add_events(builder_get(GTK_WIDGET, "vram_viewer_tileset_canvas"), GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK); gtk_widget_add_events(builder_get(GTK_WIDGET, "vram_viewer_tilemap_canvas"), GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK); g_signal_connect(get_object("vram_viewer_tileset_canvas"), "motion_notify_event", G_CALLBACK(on_motion_vram_viewer_tileset), NULL); g_signal_connect(get_object("vram_viewer_tilemap_canvas"), "motion_notify_event", G_CALLBACK(on_motion_vram_viewer_tilemap), NULL); g_signal_connect(get_object("vram_viewer_stack"), "notify::visible-child", G_CALLBACK(on_vram_tab_change), NULL); } static void update_viewport(void) { GtkWidget *w = gui_data.fallback_canvas ? GTK_WIDGET(gui_data.fallback_canvas) : GTK_WIDGET(gui_data.gl_area); int win_width = gtk_widget_get_allocated_width(w); int win_height = gtk_widget_get_allocated_height(w); double x_factor = win_width / (double) GB_get_screen_width(&gb); double y_factor = win_height / (double) GB_get_screen_height(&gb); if (config.use_integer_scaling) { x_factor = (int)(x_factor); y_factor = (int)(y_factor); } if (config.keep_aspect_ratio) { 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); gui_data.viewport = (Rect){ (win_width - new_width) / 2, (win_height - new_height) / 2, new_width, new_height }; if (!gui_data.fallback_canvas) glViewport(gui_data.viewport.x, gui_data.viewport.y, gui_data.viewport.w, gui_data.viewport.h); } // TODO: Comment static void gl_draw(void) { uint32_t *pixels = get_current_buffer(); uint32_t *previous = get_previous_buffer(); static void *_pixels = NULL; if (pixels) { _pixels = pixels; } glClearColor(0, 0, 0, 1); glClear(GL_COLOR_BUFFER_BIT); GB_frame_blending_mode_t mode = get_frame_blending_mode(); if (!previous) { mode = GB_FRAME_BLENDING_MODE_DISABLED; } else if (mode == GB_FRAME_BLENDING_MODE_ACCURATE) { 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; } } render_bitmap_with_shader( &gui_data.shader, _pixels, previous, GB_get_screen_width(&gb), GB_get_screen_height(&gb), gui_data.viewport.x, gui_data.viewport.y, gui_data.viewport.w, gui_data.viewport.h, mode ); } // TODO: Comment static void gl_finish(void) { } // TODO: Comment static void resize(void) { update_viewport(); } // TODO: Comment static gboolean on_draw_fallback(GtkWidget *widget, cairo_t *cr, gpointer data) { GtkStyleContext *context = gtk_widget_get_style_context(widget); guint width = gtk_widget_get_allocated_width(widget); guint height = gtk_widget_get_allocated_height(widget); guint screen_width = GB_get_screen_width(&gb); guint screen_height = GB_get_screen_height(&gb); gtk_render_background(context, cr, 0, 0, width, height); cairo_surface_t *surface = cairo_image_surface_create_for_data( (unsigned char *) get_current_buffer(), CAIRO_FORMAT_RGB24, screen_width, screen_height, cairo_format_stride_for_width(CAIRO_FORMAT_RGB24, screen_width) ); cairo_translate(cr, gui_data.viewport.x, gui_data.viewport.y); cairo_scale(cr, gui_data.viewport.w / screen_width, gui_data.viewport.h / screen_height); cairo_set_source_surface(cr, surface, 0, 0); cairo_pattern_set_filter(cairo_get_source(cr), CAIRO_FILTER_NEAREST); cairo_paint(cr); return false; } // Create a GtkDrawingArea as a fallback in case we can’t use OpenGL static void create_fallback_canvas(void) { gui_data.fallback_canvas = GTK_DRAWING_AREA(gtk_drawing_area_new()); g_signal_connect(gui_data.fallback_canvas, "draw", G_CALLBACK(on_draw_fallback), NULL); g_signal_connect(gui_data.fallback_canvas, "size-allocate", G_CALLBACK(resize), NULL); gtk_box_pack_end(GTK_BOX(gui_data.main_window_container), GTK_WIDGET(gui_data.fallback_canvas), true, true, 0); } // TODO: Comment static void gl_init(GtkWidget *w) { GtkGLArea *gl_area = GTK_GL_AREA(w); g_debug("GL_INIT"); const char *renderer; g_debug("GL Context: %p", gtk_gl_area_get_context(gl_area)); gtk_gl_area_make_current(gl_area); if (gtk_gl_area_get_error(gl_area) != NULL) { goto error; } renderer = (char *)glGetString(GL_RENDERER); g_debug("GtkGLArea on %s", renderer ? renderer : "Unknown"); if (config.shader == NULL || (!init_shader_with_name(&gui_data.shader, config.shader) && !init_shader_with_name(&gui_data.shader, "NearestNeighbor"))) { GError *error = g_error_new_literal(g_quark_from_string("sameboy-gl-error"), 1, "Failed to initialize shaders"); gtk_gl_area_set_error(gl_area, error); } else { g_signal_connect(gl_area, "render", G_CALLBACK(gl_draw), NULL); g_signal_connect(gl_area, "resize", G_CALLBACK(resize), NULL); g_signal_connect(gl_area, "unrealize", G_CALLBACK(gl_finish), NULL); return; } error: if (gtk_gl_area_get_error(gl_area) != NULL) { g_warning("GtkGLArea: %s", gtk_gl_area_get_error(gl_area)->message); } create_fallback_canvas(); } static void create_canvas(void) { // create our renderer area if (gui_data.supports_gl) { gui_data.gl_area = GTK_GL_AREA(gtk_gl_area_new()); gtk_gl_area_set_required_version(gui_data.gl_area, 3, 2); gtk_gl_area_set_auto_render(gui_data.gl_area, false); gtk_gl_area_set_has_alpha(gui_data.gl_area, false); gtk_gl_area_set_has_depth_buffer(gui_data.gl_area, false); gtk_gl_area_set_has_stencil_buffer(gui_data.gl_area, false); g_signal_connect(gui_data.gl_area, "realize", G_CALLBACK(gl_init), NULL); gtk_box_pack_end(GTK_BOX(gui_data.main_window_container), GTK_WIDGET(gui_data.gl_area), true, true, 0); } else { create_fallback_canvas(); } 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); } static void setup_console(void) { GtkTextView *text_view = builder_get(GTK_TEXT_VIEW, "console_screen"); GtkTextBuffer *text_buf = gtk_text_view_get_buffer(text_view); gtk_text_view_set_buffer( builder_get(GTK_TEXT_VIEW, "console_sidebar_output"), gtk_text_buffer_new(gtk_text_buffer_get_tag_table(text_buf)) ); gtk_text_buffer_create_tag(text_buf, "bold", "weight", PANGO_WEIGHT_BOLD, NULL); gtk_text_buffer_create_tag(text_buf, "underline", "underline", PANGO_UNDERLINE_SINGLE, "underline-set", true, NULL); gtk_text_buffer_create_tag(text_buf, "dashed_underline", "underline", PANGO_UNDERLINE_DOUBLE, "underline-set", true, NULL); g_mutex_init(&gui_data.debugger_input_mutex); g_cond_init(&gui_data.debugger_input_cond); g_rec_mutex_init(&gui_data.console_output_lock); if (!gui_data.debugger_input_queue) { gui_data.debugger_input_queue = g_ptr_array_sized_new(4); } if (!gui_data.pending_console_output) { gui_data.pending_console_output = gtk_text_buffer_new(gtk_text_buffer_get_tag_table(text_buf)); } } // 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) { // initialize SameBoy core init(); init_audio(); init_controllers(); connect_signal_handlers(app); create_canvas(); setup_console(); 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(); while (gui_data.stopping); g_object_unref(gui_data.builder); save_settings(); free_settings(); SDL_Quit(); if (gui_data.image_buffers[0]) g_free(gui_data.image_buffers[0]); if (gui_data.image_buffers[1]) g_free(gui_data.image_buffers[1]); if (gui_data.image_buffers[2]) g_free(gui_data.image_buffers[2]); free_shader(&gui_data.shader); free_master_shader(); GB_free(&gb); } // 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.show_console GAction // Opens the console static void activate_show_console(GSimpleAction *action, GVariant *parameter, gpointer app) { if (gui_data.debugger_input_queue) { while (gui_data.debugger_input_queue->len) { g_ptr_array_remove_index_fast(gui_data.debugger_input_queue, gui_data.debugger_input_queue->len - 1); } } gtk_widget_show_all(builder_get(GTK_WIDGET, "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.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 // Closes a ROM static void activate_close(GSimpleAction *action, GVariant *parameter, gpointer app) { stop(); GB_free(&gb); // Clear the screen as side effect update_window_geometry(); gtk_widget_queue_draw(gui_data.fallback_canvas ? GTK_WIDGET(gui_data.fallback_canvas) : GTK_WIDGET(gui_data.gl_area)); // Clear the VRAM viewer g_mutex_lock(&gui_data.tileset_buffer_mutex); memset(gui_data.tileset_buffer, 0, sizeof gui_data.tileset_buffer); g_mutex_unlock(&gui_data.tileset_buffer_mutex); g_mutex_lock(&gui_data.tilemap_buffer_mutex); memset(gui_data.tilemap_buffer, 0, sizeof gui_data.tilemap_buffer); g_mutex_unlock(&gui_data.tilemap_buffer_mutex); gtk_stack_set_visible_child_name(builder_get(GTK_STACK, "vram_viewer_stack"), "vram_viewer_tileset"); gtk_tree_view_set_model(builder_get(GTK_TREE_VIEW, "vram_viewer_sprites"), NULL); gtk_tree_view_set_model(builder_get(GTK_TREE_VIEW, "vram_viewer_palettes"), NULL); // Redraw the 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); } // app.preferences GAction // Opens the preferences window static void activate_preferences(GSimpleAction *action, GVariant *parameter, gpointer app) { gtk_widget_show_all(GTK_WIDGET(get_object("preferences"))); } // app.quit GAction // Exits the application static void activate_quit(GSimpleAction *action, GVariant *parameter, gpointer app) { quit(); } // app.clear_console GAction // Clears the debugger console static void activate_clear_console(GSimpleAction *action, GVariant *parameter, gpointer app) { g_rec_mutex_lock(&gui_data.console_output_lock); GtkTextView *text_view = builder_get(GTK_TEXT_VIEW, "console_screen"); GtkTextBuffer *text_buf = gtk_text_view_get_buffer(text_view); gtk_text_buffer_set_text(text_buf, "", -1); g_rec_mutex_unlock(&gui_data.console_output_lock); } static void on_model_changed(GSimpleAction *action, GVariant *value, gpointer user_data_ptr) { if (!GB_is_inited(&gb)) { g_simple_action_set_state(action, value); return; } const gchar *model_str = g_variant_get_string(value, NULL); 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; g_simple_action_set_state(action, value); reset(); break; default: // Action has been canceled break; } run(); gtk_widget_destroy(GTK_WIDGET(dialog)); } static void on_mute_changed(GSimpleAction *action, GVariant *value, gpointer user_data_ptr) { gboolean do_mute = g_variant_get_boolean(value); GB_audio_set_paused(do_mute); 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_boot_rom_location_changed(GtkWidget *w, gpointer user_data_ptr) { GtkComboBox *box = GTK_COMBO_BOX(w); const gchar *id = gtk_combo_box_get_active_id(box); if (id == NULL) return; if (g_strcmp0(id, "other") == 0) { GtkFileChooserNative *native = gtk_file_chooser_native_new("Select Folder", gui_data.preferences, GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER, "_Select", "_Cancel"); gint res = gtk_native_dialog_run(GTK_NATIVE_DIALOG(native)); if (res == GTK_RESPONSE_ACCEPT) { config.boot_rom_path = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(native)); update_boot_rom_selector(gui_data.builder); } g_object_unref(native); } else { config.boot_rom_path = (gchar *)id; } } G_MODULE_EXPORT void on_cgb_model_changed(GtkWidget *w, gpointer user_data_ptr) { GtkComboBox *box = GTK_COMBO_BOX(w); config.cgb_revision_name = (gchar *)gtk_combo_box_get_active_id(box); } G_MODULE_EXPORT void on_color_correction_changed(GtkWidget *w, gpointer user_data_ptr) { GtkComboBox *box = GTK_COMBO_BOX(w); config.color_correction_id = (gchar *)gtk_combo_box_get_active_id(box); if (GB_is_inited(&gb)) { GB_set_color_correction_mode(&gb, get_color_correction_mode()); } } G_MODULE_EXPORT void on_frame_blending_changed(GtkWidget *w, gpointer user_data_ptr) { GtkComboBox *box = GTK_COMBO_BOX(w); config.frame_blending_mode = (gchar *)gtk_combo_box_get_active_id(box); } G_MODULE_EXPORT void on_display_border_changed(GtkWidget *w, gpointer user_data_ptr) { GtkComboBox *box = GTK_COMBO_BOX(w); config.display_border_mode = (gchar *)gtk_combo_box_get_active_id(box); gui_data.border_mode_changed = true; } G_MODULE_EXPORT void on_monochrome_palette_changed(GtkWidget *w, gpointer user_data_ptr) { GtkComboBox *box = GTK_COMBO_BOX(w); config.monochrome_palette_id = (gchar *)gtk_combo_box_get_active_id(box); GB_set_palette(&gb, get_monochrome_palette()); } G_MODULE_EXPORT void on_color_menubar_override_changed(GtkWidget *w, gpointer user_data_ptr) { config.menubar_override = (gchar *)gtk_combo_box_get_active_id(GTK_COMBO_BOX(w)); } G_MODULE_EXPORT void on_dmg_model_changed(GtkWidget *w, gpointer user_data_ptr) { GtkComboBox *box = GTK_COMBO_BOX(w); config.dmg_revision_name = (gchar *)gtk_combo_box_get_active_id(box); } G_MODULE_EXPORT void on_graphic_filter_changed(GtkWidget *w, gpointer user_data_ptr) { GtkComboBox *box = GTK_COMBO_BOX(w); config.shader = (gchar *)gtk_combo_box_get_active_id(box); free_shader(&gui_data.shader); init_shader_with_name(&gui_data.shader, config.shader); } G_MODULE_EXPORT void on_highpass_filter_changed(GtkWidget *w, gpointer user_data_ptr) { config.high_pass_filter_id = (gchar *)gtk_combo_box_get_active_id(GTK_COMBO_BOX(w)); if (GB_is_inited(&gb)) { GB_set_highpass_filter_mode(&gb, get_highpass_mode()); } } G_MODULE_EXPORT void on_keep_aspect_ratio_changed(GtkWidget *w, gpointer user_data_ptr) { GtkCheckButton *button = GTK_CHECK_BUTTON(w); gboolean value = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(button)); config.keep_aspect_ratio = value; update_viewport(); } G_MODULE_EXPORT void on_rewind_duration_changed(GtkWidget *w, gpointer user_data_ptr) { GtkComboBox *box = GTK_COMBO_BOX(w); config.rewind_duration = g_ascii_strtoll(gtk_combo_box_get_active_id(box), NULL, 10); GB_set_rewind_length(&gb, config.rewind_duration); } G_MODULE_EXPORT void on_sample_rate_changed(GtkWidget *w, gpointer user_data_ptr) { GtkComboBox *box = GTK_COMBO_BOX(w); config.sample_rate = g_ascii_strtoll(gtk_combo_box_get_active_id(box), NULL, 10); if (config.sample_rate == -1) { gui_data.sample_rate = GB_audio_default_sample_rate(); } else { gui_data.sample_rate = config.sample_rate; } init_audio(); } G_MODULE_EXPORT void on_sgb_model_changed(GtkWidget *w, gpointer user_data_ptr) { GtkComboBox *box = GTK_COMBO_BOX(w); config.sgb_revision_name = (gchar *)gtk_combo_box_get_active_id(box); } G_MODULE_EXPORT void on_use_integer_scaling_changed(GtkWidget *w, gpointer user_data_ptr) { GtkCheckButton *button = GTK_CHECK_BUTTON(w); gboolean value = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(button)); config.use_integer_scaling = value; update_viewport(); } G_MODULE_EXPORT void console_on_enter(GtkWidget *w, gpointer user_data_ptr) { GtkEntry *input = GTK_ENTRY(w); const gchar *_text = gtk_entry_get_text(input); gchar *text = g_strdup(_text); if (g_strcmp0("", text) == 0 && g_strcmp0("", gui_data.last_console_input) < 0) { text = g_strdup(gui_data.last_console_input); } else if (text) { if (gui_data.last_console_input != NULL) g_free(gui_data.last_console_input); gui_data.last_console_input = g_strdup(text); } if (!gui_data.in_sync_input) { console_log(&gb, "> ", 0); } console_log(&gb, text, 0); console_log(&gb, "\n", 0); g_mutex_lock(&gui_data.debugger_input_mutex); g_ptr_array_add(gui_data.debugger_input_queue, (gpointer)text); g_cond_signal(&gui_data.debugger_input_cond); g_mutex_unlock(&gui_data.debugger_input_mutex); // clear input gtk_entry_set_text(input, ""); } 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, NULL, "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", "" }, { 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; }