#include #include #include #include #include #include #include #include "shader.h" #define str(x) #x #define xstr(x) str(x) typedef struct UserData { bool fullscreen; GFile *file; } UserData; typedef struct{ int16_t x, y; uint16_t w, h; } Rect; static void run(UserData *user_data); GtkBuilder *builder; GtkApplicationWindow *main_window; GtkGLArea *gl_area; shader_t shader; GB_gameboy_t gb; static uint32_t *image_buffers[3]; static unsigned char current_buffer; 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 rect; unsigned char number_of_buffers(void) { bool should_blend = true; return should_blend? 3 : 2; } void flip(void) { current_buffer = (current_buffer + 1) % number_of_buffers(); } uint32_t *get_pixels(void) { return image_buffers[(current_buffer + 1) % number_of_buffers()]; } uint32_t *get_current_buffer(void) { return image_buffers[current_buffer]; } uint32_t *get_previous_buffer(void) { return image_buffers[(current_buffer + 2) % number_of_buffers()]; } 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), rect.x, rect.y, rect.w, rect.h); } static void vblank(GB_gameboy_t *gb) { flip(); GB_set_pixels_output(gb, get_pixels()); // Queue drawing of the current frame gtk_gl_area_queue_render(gl_area); while (gtk_events_pending()) { gtk_main_iteration(); } } void update_viewport(void) { int win_width = gtk_widget_get_allocated_width(GTK_WIDGET(gl_area)); int win_height = gtk_widget_get_allocated_height(GTK_WIDGET(gl_area)); double x_factor = win_width / (double) GB_get_screen_width(&gb); double y_factor = win_height / (double) GB_get_screen_height(&gb); if (true /*configuration.scaling_mode == GB_SDL_SCALING_INTEGER_FACTOR*/) { x_factor = (int)(x_factor); y_factor = (int)(y_factor); } /*if (configuration.scaling_mode != GB_SDL_SCALING_ENTIRE_WINDOW) { if (x_factor > y_factor) { x_factor = y_factor; } else { y_factor = x_factor; } }*/ unsigned new_width = x_factor * GB_get_screen_width(&gb); unsigned new_height = y_factor * GB_get_screen_height(&gb); rect = (Rect){(win_width - new_width) / 2, (win_height - new_height) / 2, new_width, new_height}; glViewport(rect.x, rect.y, rect.w, rect.h); } // 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; } // 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); } // Returns true if the application should show a menubar static gboolean show_menubar(void) { GtkSettings *settings = gtk_settings_get_default(); gboolean result; g_object_get(settings, "gtk-shell-shows-menubar", &result, NULL); return result; } // Returns a GObject by ID from our GtkBuilder instance static GObject *get_object(gchararray id) { return gtk_builder_get_object(builder, id); } // 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; } // app.quit GAction // Exits the application static void activate_quit(GSimpleAction *action, GVariant *parameter, gpointer user_data) { g_application_quit(G_APPLICATION(user_data)); } // app.about GAction // Opens the about dialog static void activate_about(GSimpleAction *action, GVariant *parameter, gpointer user_data) { GObject *dialog = get_object("about_dialog"); gtk_dialog_run(GTK_DIALOG(dialog)); gtk_widget_hide(GTK_WIDGET(dialog)); } // List of GActions for the `app` prefix static GActionEntry app_entries[] = { { "quit", activate_quit, NULL, NULL, NULL }, { "about", activate_about, NULL, NULL, NULL }, }; G_MODULE_EXPORT void on_show_window(GtkWidget *w, gpointer window) { gtk_widget_show_all(GTK_WIDGET(window)); } G_MODULE_EXPORT void on_boot_rom_location_changed(GtkWidget *w, gpointer user_data_gptr) { GtkComboBox *box = GTK_COMBO_BOX(w); g_print("Active: %s", gtk_combo_box_get_active_id(box)); } G_MODULE_EXPORT void gl_init() { const char *renderer; gtk_gl_area_make_current(gl_area); if (gtk_gl_area_get_error(gl_area) != NULL) { return; } renderer = (char *) glGetString(GL_RENDERER); g_print("GtkGLArea on %s\n", renderer ? renderer : "Unknown"); if (!init_shader_with_name(&shader, /*configuration.filter*/ "OmniScale")) { init_shader_with_name(&shader, "NearestNeighbor"); } } G_MODULE_EXPORT void gl_resize() { update_viewport(); } G_MODULE_EXPORT void gl_draw() { render_texture(get_current_buffer(), get_previous_buffer()); } G_MODULE_EXPORT void gl_finish() { } // 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; builder = gtk_builder_new_from_resource(RESOURCE_PREFIX "ui/window.ui"); gtk_builder_connect_signals(builder, NULL); g_action_map_add_action_entries(G_ACTION_MAP(app), app_entries, G_N_ELEMENTS(app_entries), app); GtkWindow *preferences = GTK_WINDOW(get_object("preferences")); set_combo_box_row_separator_func(GTK_CONTAINER(preferences)); GtkWindow *vram_viewer = GTK_WINDOW(get_object("vram_viewer")); set_combo_box_row_separator_func(GTK_CONTAINER(vram_viewer)); // setup main window main_window = GTK_APPLICATION_WINDOW(gtk_application_window_new(GTK_APPLICATION(app))); gtk_application_window_set_show_menubar(main_window, true); // create our renderer area gl_area = GTK_GL_AREA(gtk_gl_area_new()); gtk_gl_area_set_auto_render(gl_area, false); g_signal_connect(gl_area, "realize", G_CALLBACK(gl_init), NULL); g_signal_connect(gl_area, "render", G_CALLBACK(gl_draw), NULL); g_signal_connect(gl_area, "resize", G_CALLBACK(gl_resize), NULL); g_signal_connect(gl_area, "unrealize", G_CALLBACK(gl_finish), NULL); gtk_container_add(GTK_CONTAINER(main_window), GTK_WIDGET(gl_area)); // Handle the whole menubar situation … if (show_menubar()) { // Show a classic menubar GMenuModel *menubar = get_menu_model(app, "menubar"); gtk_application_set_menubar(GTK_APPLICATION(app), menubar); } else { // Attach a custom title bar GtkWidget *titlebar = GTK_WIDGET(gtk_builder_get_object(builder, "main_header_bar")); 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")); GMenuModel *hamburger_menu = get_menu_model(app, "menubar"); gtk_menu_button_set_menu_model(hamburger_button, hamburger_menu); } gtk_window_set_title(GTK_WINDOW(main_window), "SameBoy v" xstr(VERSION)); // 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)); } g_signal_connect(main_window, "destroy", G_CALLBACK(gtk_main_quit), NULL); gtk_application_add_window(GTK_APPLICATION(app), GTK_WINDOW(main_window)); gtk_widget_show_all(GTK_WIDGET(main_window)); run(user_data); } // 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"); g_application_quit(app); return; } user_data->file = files[0]; // We have handled the files, now activate the application activate(app, user_data_gptr); } // 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; } return -1; } static uint32_t rgb_encode(GB_gameboy_t *gb, uint8_t r, uint8_t g, uint8_t b) { uint32_t color = 0xFF000000 | (b << 16) | (g << 8) | r; // abgr return color; } 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 ); 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 run(UserData *user_data) { GB_model_t prev_model = GB_get_model(&gb); GB_model_t model = GB_MODEL_CGB_E; 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, configuration.color_correction_mode); // GB_set_highpass_filter_mode(&gb, configuration.highpass_mode); // GB_set_rewind_length(&gb, configuration.rewind_length); // GB_set_update_input_hint_callback(&gb, handle_events); // GB_apu_set_sample_callback(&gb, gb_audio_callback); } GError *gerror; GBytes *boot_rom_f; const guchar *boot_rom_data; gsize boot_rom_size; boot_rom_f = g_resources_lookup_data(RESOURCE_PREFIX "bootroms/cgb_boot.bin", G_RESOURCE_LOOKUP_FLAGS_NONE, &gerror); 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)); } /* Run emulation */ while (true) { 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); } } } int main(int argc, char *argv[]) { // Create our GApplication and tell GTK that we are able to handle files GtkApplication *app = gtk_application_new(APP_ID, G_APPLICATION_HANDLES_OPEN); UserData user_data = { NULL }; // 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 }, { NULL } }; // Setup our command line information g_application_add_main_option_entries(G_APPLICATION(app), entries); g_application_set_option_context_parameter_string(G_APPLICATION(app), "[FILE…]"); g_application_set_option_context_summary(G_APPLICATION(app), "SameBoy is an open source Game Boy (DMG) and Game Boy Color (CGB) emulator."); // Add signal handlers g_signal_connect(app, "handle-local-options", G_CALLBACK(handle_local_options), &user_data); g_signal_connect(app, "startup", G_CALLBACK(startup), &user_data); g_signal_connect(app, "activate", G_CALLBACK(activate), &user_data); g_signal_connect(app, "open", G_CALLBACK(open), &user_data); #ifndef NDEBUG //gtk_window_set_interactive_debugging(true); #endif // Start our GApplication main loop int status = g_application_run(G_APPLICATION(app), argc, argv); g_object_unref(app); return status; }