#include "main.h" static GtkApplication *main_application; static GtkBuilder *builder; static GtkGLArea *gl_area; static GtkDrawingArea *fallback_canvas; static GtkApplicationWindow *main_window; static GtkBox *main_window_container; static GtkWindow *preferences; static GtkWindow *vram_viewer; static GtkWindow *memory_viewer; static GtkWindow *console; static GtkWindow *printer; static shader_t shader; static UserData user_data = { NULL }; static GB_gameboy_t gb; static uint32_t *image_buffers[3]; static unsigned char current_buffer; static bool supports_gl; static bool paused = false; static bool underclock_down = false, rewind_down = false, do_rewind = false, rewind_paused = false, turbo_down = false; static double clock_mutliplier = 1.0; static char *battery_save_path_ptr; static Rect viewport = {0}; static Rect scrollRect = {0}; static bool vram_viewer_visible = false; static bool running = true; #define tileset_buffer_length 256 * 192 * 4 static uint32_t tileset_buffer[tileset_buffer_length] = {0}; #define tilemap_buffer_length 256 * 256 * 4 static uint32_t tilemap_buffer[tilemap_buffer_length] = {0}; static uint8_t pressed_buttons; // List of GActions for the `app` prefix static const GActionEntry app_entries[] = { { "quit", activate_quit, NULL, NULL, NULL }, { "about", activate_about, NULL, NULL, NULL }, { "open", activate_open, NULL, NULL, NULL }, { "open_gtk_debugger", activate_open_gtk_debugger, NULL, NULL, NULL }, { "preferences", activate_preferences, NULL, NULL, NULL }, { "open_vram_viewer", activate_open_vram_viewer, NULL, NULL, NULL }, { "open_memory_viewer", activate_open_memory_viewer, NULL, NULL, NULL }, }; int main(int argc, char *argv[]) { // Create our GApplication and tell GTK that we are able to handle files 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, &user_data.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, &user_data.config_path, "Override the path of the configuration file", "" }, { NULL } }; // Setup our command line information g_application_add_main_option_entries(G_APPLICATION(main_application), entries); g_application_set_option_context_parameter_string(G_APPLICATION(main_application), "[FILE…]"); g_application_set_option_context_summary(G_APPLICATION(main_application), "SameBoy is an open source Game Boy (DMG) and Game Boy Color (CGB) emulator."); // Add signal handlers g_signal_connect(main_application, "handle-local-options", G_CALLBACK(handle_local_options), &user_data); g_signal_connect(main_application, "startup", G_CALLBACK(startup), &user_data); g_signal_connect(main_application, "activate", G_CALLBACK(activate), &user_data); g_signal_connect(main_application, "open", G_CALLBACK(open), &user_data); g_signal_connect(main_application, "shutdown", G_CALLBACK(shutdown), &user_data); // Start our GApplication main loop int status = g_application_run(G_APPLICATION(main_application), argc, argv); g_object_unref(main_application); return status; } // This function gets called after the parsing of the commandline options has occurred. static gint handle_local_options(GApplication *app, GVariantDict *options, gpointer user_data_gptr) { UserData *user_data = user_data_gptr; guint32 count; if (g_variant_dict_lookup(options, "version", "b", &count)) { g_print("SameBoy v" xstr(VERSION) "\n"); return EXIT_SUCCESS; } if (g_variant_dict_lookup(options, "fullscreen", "b", &count)) { user_data->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")) { if (g_str_has_suffix(model_name, "-B") || g_strcmp0(model_name, "DMG") == 0) { user_data->model = GB_MODEL_DMG_B; } else { user_data->model = GB_MODEL_DMG_B; g_printerr("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) { user_data->model = GB_MODEL_SGB; } else if (g_str_has_suffix(model_name, "-PAL")) { user_data->model = GB_MODEL_SGB | GB_MODEL_PAL_BIT; } else if (g_str_has_suffix(model_name, "2")) { user_data->model = GB_MODEL_SGB2; } else { user_data->model = GB_MODEL_SGB2; g_printerr("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")) { user_data->model = GB_MODEL_CGB_C; } else if (g_str_has_suffix(model_name, "-E") || g_strcmp0(model_name, "CGB") == 0) { user_data->model = GB_MODEL_CGB_E; } else { user_data->model = GB_MODEL_CGB_E; g_printerr("Unsupported revision: %s\nFalling back to CGB-E", model_name); } } else if (g_str_has_prefix(model_name, "AGB")) { user_data->model = GB_MODEL_AGB; } else { g_printerr("Unknown model: %s\n", model_name); exit(EXIT_FAILURE); } } return -1; } // 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; } // The main function for the OpenGL version check workaround void gl_check_realize(GtkWidget *w, gpointer user_data_gptr) { gboolean *result = (gboolean *) user_data_gptr; 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_printerr("Failed to create context: %s\n", 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_print("OpenGL version: %d\n", version); *result = version >= 32; } } // 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 a GtkDrawingArea as a fallback in case we can’t use OpenGL static void create_fallback_canvas(void) { fallback_canvas = GTK_DRAWING_AREA(gtk_drawing_area_new()); g_signal_connect(fallback_canvas, "draw", G_CALLBACK(on_draw_fallback), NULL); g_signal_connect(fallback_canvas, "size-allocate", G_CALLBACK(resize), NULL); gtk_box_pack_end(GTK_BOX(main_window_container), GTK_WIDGET(fallback_canvas), TRUE, TRUE, 0); } // 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_print("XDG_CURRENT_DESKTOP: %s\nGDMSESSION: %s\nDESKTOP_SESSION: %s\nChosen value: %s\nShow menu in shell: %d\n", 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_error("Unreachable\n"); break; case MENUBAR_SHOW_IN_SHELL: g_print("Showing menu in the shell\n"); gtk_application_set_menubar(GTK_APPLICATION(app), menubar_model); break; case MENUBAR_SHOW_IN_WINDOW: { g_print("Showing menu in the window\n"); GtkMenuBar *menubar = GTK_MENU_BAR(gtk_menu_bar_new_from_model(menubar_model)); gtk_box_pack_start(GTK_BOX(main_window_container), GTK_WIDGET(menubar), FALSE, FALSE, 0); break; } case MENUBAR_SHOW_HAMBURGER: { g_print("Showing hamburger\n"); // 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(main_window))); gtk_window_set_titlebar(GTK_WINDOW(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; } } } // 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 *list = gtk_container_get_children(container); while (list) { if (GTK_IS_COMBO_BOX(list->data)) { gtk_combo_box_set_row_separator_func(GTK_COMBO_BOX(list->data), is_separator, NULL, NULL); } if (GTK_IS_CONTAINER(list->data)) { set_combo_box_row_separator_func(GTK_CONTAINER(list->data)); } list = list->next; } g_list_free_full(list, NULL); } // 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; return result; } // Determines how many frame buffers to use static unsigned char number_of_buffers(void) { // TODO bool should_blend = !fallback_canvas; 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 image_buffers[(current_buffer + 1) % number_of_buffers()]; } // Returns the current finished frame static uint32_t *get_current_buffer(void) { return image_buffers[current_buffer]; } // Returns the previous finished frame static uint32_t *get_previous_buffer(void) { return image_buffers[(current_buffer + 2) % number_of_buffers()]; } // Cycles the buffers static void flip(void) { current_buffer = (current_buffer + 1) % number_of_buffers(); } // This functions gets called immediately after registration of the GApplication static void startup(GApplication *app, gpointer user_data_gptr) { UserData *user_data = user_data_gptr; // 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. supports_gl = test_gl_support(); g_print("OpenGL supported: %s\n", supports_gl? "Yes" : "No"); builder = gtk_builder_new_from_resource(RESOURCE_PREFIX "ui/window.ui"); gtk_builder_connect_signals(builder, NULL); // Setup application actions g_action_map_add_action_entries(G_ACTION_MAP(app), app_entries, G_N_ELEMENTS(app_entries), app); #if NDEBUG // Disable when not compiled in debug mode g_simple_action_set_enabled(G_SIMPLE_ACTION(g_action_map_lookup_action(G_ACTION_MAP(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 preferences = GTK_WINDOW(get_object("preferences")); g_signal_connect(preferences, "realize", G_CALLBACK(on_preferences_realize), (gpointer) builder); init_settings(user_data->config_path, preferences); vram_viewer = GTK_WINDOW(get_object("vram_viewer")); memory_viewer = GTK_WINDOW(get_object("memory_viewer")); console = GTK_WINDOW(get_object("console")); printer = GTK_WINDOW(get_object("printer")); // setup main window main_window = GTK_APPLICATION_WINDOW(gtk_application_window_new(GTK_APPLICATION(app))); main_window_container = GTK_BOX(gtk_box_new(GTK_ORIENTATION_VERTICAL, 0)); gtk_window_set_title(GTK_WINDOW(main_window), "SameBoy"); gtk_application_window_set_show_menubar(main_window, false); gtk_container_add(GTK_CONTAINER(main_window), GTK_WIDGET(main_window_container)); setup_menu(app); // Insert separators into `GtkComboBox`es set_combo_box_row_separator_func(GTK_CONTAINER(preferences)); set_combo_box_row_separator_func(GTK_CONTAINER(vram_viewer)); set_combo_box_row_separator_func(GTK_CONTAINER(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(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, g_list_nth_data(icon_list, 3)); // reuse the 64x64 icon gtk_about_dialog_set_version(about_dialog, "v" xstr(VERSION)); g_list_free(icon_list); } // This function gets called when the GApplication gets activated, i.e. it is ready to show widgets. static void activate(GApplication *app, gpointer user_data_gptr) { UserData *user_data = user_data_gptr; if (user_data->fullscreen) { gtk_window_fullscreen(GTK_WINDOW(main_window)); } // Connect signal handlers gtk_widget_add_events(GTK_WIDGET(main_window), GDK_KEY_PRESS_MASK); gtk_widget_add_events(GTK_WIDGET(main_window), GDK_KEY_RELEASE_MASK); g_signal_connect(main_window, "destroy", G_CALLBACK(on_quit), app); g_signal_connect(main_window, "key_press_event", G_CALLBACK(on_key_press), NULL); g_signal_connect(main_window, "key_release_event", G_CALLBACK(on_key_press), NULL); g_signal_connect(vram_viewer, "realize", G_CALLBACK(on_vram_viewer_realize), NULL); g_signal_connect(vram_viewer, "unrealize", G_CALLBACK(on_vram_viewer_unrealize), NULL); // Just hide our sub-windows when closing them g_signal_connect(preferences, "delete-event", G_CALLBACK(gtk_widget_hide_on_delete), NULL); g_signal_connect(vram_viewer, "delete-event", G_CALLBACK(gtk_widget_hide_on_delete), NULL); g_signal_connect(memory_viewer, "delete-event", G_CALLBACK(gtk_widget_hide_on_delete), NULL); g_signal_connect(console, "delete-event", G_CALLBACK(gtk_widget_hide_on_delete), NULL); g_signal_connect(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); // create our renderer area if (supports_gl) { gl_area = GTK_GL_AREA(gtk_gl_area_new()); gtk_gl_area_set_required_version(gl_area, 3, 2); gtk_gl_area_set_auto_render(gl_area, false); gtk_gl_area_set_has_alpha(gl_area, false); gtk_gl_area_set_has_depth_buffer(gl_area, false); gtk_gl_area_set_has_stencil_buffer(gl_area, false); g_signal_connect(gl_area, "realize", G_CALLBACK(gl_init), NULL); gtk_box_pack_end(GTK_BOX(main_window_container), GTK_WIDGET(gl_area), TRUE, TRUE, 0); } else { create_fallback_canvas(); } gtk_application_add_window(GTK_APPLICATION(app), GTK_WINDOW(main_window)); gtk_widget_show_all(GTK_WIDGET(main_window)); // Start the emulation loop. // This loop takes care of the GTK main loop. run(app, user_data); } // This function gets called when the application is closed. static void shutdown(GApplication *app, GFile **files, gint n_files, const gchar *hint, gpointer user_data_gptr) { g_print("SHUTDOWN\n"); save_settings(); free_settings(); } // 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 user_data_gptr) { UserData *user_data = user_data_gptr; if (n_files > 1) { g_printerr("More than one file specified\n"); exit(EXIT_FAILURE); } user_data->file = files[0]; // We have handled the files, now activate the application activate(app, user_data_gptr); } // 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(GApplication *app) { // Tell our own main loop to quit. // This will allow our run() and therefore our activate() methods to end. running = false; // Quit our application properly. // This fires the “shutdown” signal. g_application_quit(app); } static gboolean on_key_press(GtkWidget *w, GdkEventKey *event, gpointer data) { uint8_t mask; // TODO: Allow control remapping in the GUI switch (event->keyval) { case GDK_KEY_w: mask = BUTTON_MASK_UP; break; case GDK_KEY_a: mask = BUTTON_MASK_LEFT; break; case GDK_KEY_s: mask = BUTTON_MASK_DOWN; break; case GDK_KEY_d: mask = BUTTON_MASK_RIGHT; break; case GDK_KEY_g: mask = BUTTON_MASK_SELECT; break; case GDK_KEY_h: mask = BUTTON_MASK_START; break; case GDK_KEY_k: mask = BUTTON_MASK_B; break; case GDK_KEY_l: mask = BUTTON_MASK_A; break; } if (event->type == GDK_KEY_PRESS) { pressed_buttons |= mask; } else if (event->type == GDK_KEY_RELEASE) { pressed_buttons &= ~mask; } return FALSE; } // 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.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(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(vram_viewer)); } // app.open GAction // Opens a ROM file static void activate_open(GSimpleAction *action, GVariant *parameter, gpointer app) { GtkFileChooserNative *native = gtk_file_chooser_native_new("Open File", GTK_WINDOW(main_window), GTK_FILE_CHOOSER_ACTION_OPEN, "_Open", "_Cancel"); gint res = gtk_native_dialog_run(GTK_NATIVE_DIALOG(native)); if (res == GTK_RESPONSE_ACCEPT) { // TODO: Emit an event for our emulation loop g_print("%s\n", gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(native))); } g_object_unref(native); } // 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(G_APPLICATION(app)); } // `destroy` signal GCallback // Exits the application static void on_quit(GtkWidget *w, gpointer app) { quit(G_APPLICATION(app)); } // TODO: Comment static void gl_init(GtkWidget *w) { GtkGLArea *gl_area = GTK_GL_AREA(w); g_print("GL_INIT\n"); const char *renderer; g_print("GL Context: %p\n", 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_print("GtkGLArea on %s\n", renderer ? renderer : "Unknown"); if (config.shader == NULL || (!init_shader_with_name(&shader, config.shader) && !init_shader_with_name(&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_printerr("GtkGLArea: %s\n", gtk_gl_area_get_error(gl_area)->message); } create_fallback_canvas(); } // TODO: Comment static void gl_draw() { render_texture(get_current_buffer(), get_previous_buffer()); } // TODO: Comment static void gl_finish() { } // 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, viewport.x, viewport.y); cairo_scale(cr, viewport.w / screen_width, 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; } // TODO: Comment static void resize() { update_viewport(); } // Gets called when the VRAM viewer gets realized static void on_vram_viewer_realize() { vram_viewer_visible = true; } // Gets called when the VRAM viewer gets unrealized static void on_vram_viewer_unrealize() { 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) { 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 *) 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); } 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) { 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 *) 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, 0, 0, width, height); for (unsigned x = 0; x < 2; x++) { for (unsigned y = 0; y < 2; y++) { Rect rect = scrollRect; 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); } return FALSE; } G_MODULE_EXPORT void on_boot_rom_location_changed(GtkWidget *w, gpointer user_data_gptr) { 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", 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(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_gptr) { 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_gptr) { 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_color_menubar_override_changed(GtkWidget *w, gpointer user_data_gptr) { 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_gptr) { 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_gptr) { GtkComboBox *box = GTK_COMBO_BOX(w); config.shader = (gchar *)gtk_combo_box_get_active_id(box); init_shader_with_name(&shader, config.shader); } G_MODULE_EXPORT void on_highpass_filter_changed(GtkWidget *w, gpointer user_data_gptr) { 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_gptr) { 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_gptr) { GtkComboBox *box = GTK_COMBO_BOX(w); config.rewind_duration = g_ascii_strtoll(gtk_combo_box_get_active_id(box), NULL, 10); } G_MODULE_EXPORT void on_sgb_model_changed(GtkWidget *w, gpointer user_data_gptr) { 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_gptr) { GtkCheckButton *button = GTK_CHECK_BUTTON(w); gboolean value = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(button)); config.use_integer_scaling = value; update_viewport(); } 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 void render_texture(void *pixels, void *previous) { static void *_pixels = NULL; if (pixels) { _pixels = pixels; } glClearColor(0, 0, 0, 1); glClear(GL_COLOR_BUFFER_BIT); render_bitmap_with_shader(&shader, _pixels, previous, GB_get_screen_width(&gb), GB_get_screen_height(&gb), viewport.x, viewport.y, viewport.w, viewport.h); } static void update_viewport(void) { GtkWidget *w = fallback_canvas ? GTK_WIDGET(fallback_canvas) : GTK_WIDGET(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); viewport = (Rect){ (win_width - new_width) / 2, (win_height - new_height) / 2, new_width, new_height }; if (!fallback_canvas) glViewport(viewport.x, viewport.y, viewport.w, viewport.h); } static void update_window_geometry() { // 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(main_window), NULL, &hints, (GdkWindowHints)(GDK_HINT_MIN_SIZE) ); gtk_window_resize(GTK_WINDOW(main_window), GB_get_screen_width(&gb) * 2, GB_get_screen_height(&gb) * 2 ); // Setup our image buffers if (image_buffers[0]) free(image_buffers[0]); if (image_buffers[1]) free(image_buffers[1]); if (image_buffers[2]) free(image_buffers[2]); size_t buffer_size = sizeof(image_buffers[0][0]) * GB_get_screen_width(&gb) * GB_get_screen_height(&gb); image_buffers[0] = malloc(buffer_size); image_buffers[1] = malloc(buffer_size); image_buffers[2] = malloc(buffer_size); } static void handle_events(GB_gameboy_t *gb) { while (gtk_events_pending()) { gtk_main_iteration(); } GB_set_key_state(gb, GB_KEY_RIGHT, pressed_buttons & BUTTON_MASK_RIGHT); GB_set_key_state(gb, GB_KEY_LEFT, pressed_buttons & BUTTON_MASK_LEFT); GB_set_key_state(gb, GB_KEY_UP, pressed_buttons & BUTTON_MASK_UP); GB_set_key_state(gb, GB_KEY_DOWN, pressed_buttons & BUTTON_MASK_DOWN); GB_set_key_state(gb, GB_KEY_A, pressed_buttons & BUTTON_MASK_A); GB_set_key_state(gb, GB_KEY_B, pressed_buttons & BUTTON_MASK_B); GB_set_key_state(gb, GB_KEY_SELECT, pressed_buttons & BUTTON_MASK_SELECT); GB_set_key_state(gb, GB_KEY_START, pressed_buttons & BUTTON_MASK_START); } static void vblank(GB_gameboy_t *gb) { flip(); GB_set_pixels_output(gb, get_pixels()); // Queue drawing of the current frame if (fallback_canvas) { gtk_widget_queue_draw(GTK_WIDGET(main_window)); } else if (gl_area) { gtk_gl_area_queue_render(gl_area); } if (vram_viewer_visible) { // TODO: Only update what is needed GB_draw_tileset(gb, tileset_buffer, GB_PALETTE_NONE, 0); GB_draw_tilemap(gb, tilemap_buffer, GB_PALETTE_AUTO, 0, GB_MAP_AUTO, GB_TILESET_AUTO); scrollRect = (Rect){ GB_read_memory(gb, 0xFF00 | GB_IO_SCX), GB_read_memory(gb, 0xFF00 | GB_IO_SCY), 160, 144 }; // Queue a redraw of the VRAM viewer gtk_widget_queue_draw(GTK_WIDGET(vram_viewer)); } } static void run(GApplication *app, UserData *user_data) { GB_model_t prev_model = GB_get_model(&gb); GB_model_t model = user_data->model? user_data->model : GB_MODEL_CGB_E; // TODO: Model from config if (GB_is_inited(&gb)) { GB_switch_model_and_reset(&gb, model); GtkRequisition minimum_size; GtkRequisition natural_size; gtk_widget_get_preferred_size(GTK_WIDGET(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(); } } else { GB_init(&gb, model); update_window_geometry(); GB_set_vblank_callback(&gb, (GB_vblank_callback_t) vblank); GB_set_pixels_output(&gb, get_current_buffer()); GB_set_rgb_encode_callback(&gb, rgb_encode); // GB_set_sample_rate(&gb, have_aspec.freq); 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); } GError *error; char *boot_rom_path; char *boot_rom_name; GBytes *boot_rom_f; const guchar *boot_rom_data; gsize boot_rom_size; if (user_data->boot_rom_path != NULL) { g_print("Trying to load boot ROM from %s\n", user_data->boot_rom_path); if (GB_load_boot_rom(&gb, user_data->boot_rom_path)) { g_printerr("Falling back to boot ROM from config\n"); goto config_boot_rom; } } else { config_boot_rom: switch (model) { case GB_MODEL_DMG_B: boot_rom_name = "dmg_boot.bin"; break; case GB_MODEL_SGB: case GB_MODEL_SGB_PAL: case GB_MODEL_SGB_NO_SFC: boot_rom_name = "sgb_boot.bin"; break; case GB_MODEL_SGB2: case GB_MODEL_SGB2_NO_SFC: boot_rom_name = "sgb2_boot.bin"; break; case GB_MODEL_CGB_C: case GB_MODEL_CGB_E: boot_rom_name = "cgb_boot.bin"; break; case GB_MODEL_AGB: boot_rom_name = "agb_boot.bin"; break; } 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_print("Trying to load boot ROM from %s\n", boot_rom_path); if (GB_load_boot_rom(&gb, boot_rom_path)) { g_printerr("Falling back to internal boot ROM\n"); goto internal_boot_rom; } } 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); if (boot_rom_f == NULL) { g_printerr("Failed to load internal boot ROM: %s\n", 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); } } if (user_data->file != NULL && GB_load_rom(&gb, g_file_get_path(user_data->file)) == 0) { /* Run emulation */ while (running) { if (paused || rewind_paused) { while (gtk_events_pending()) { gtk_main_iteration(); } } else { if (do_rewind) { GB_rewind_pop(&gb); if (turbo_down) { GB_rewind_pop(&gb); } if (!GB_rewind_pop(&gb)) { rewind_paused = true; } do_rewind = false; } GB_run(&gb); } } } }