diff --git a/gtk3/main.c b/gtk3/main.c index f0d6739..9479cf6 100644 --- a/gtk3/main.c +++ b/gtk3/main.c @@ -1,40 +1,4 @@ -#include -#include -#include -#include -#include -#include -#include - -#include "macros.h" -#include "settings.h" -#include "shader.h" - -typedef struct UserData { - bool fullscreen; - GFile *file; - gchar *boot_rom_path; - gchar *config_path; - GB_model_t model; -} UserData; - -typedef struct{ - int16_t x, y; - uint16_t w, h; -} Rect; - -#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 - -static uint8_t pressed_buttons; - -static void run(GApplication *app, UserData *user_data); +#include "main.h" static GtkApplication *main_application; static GtkBuilder *builder; @@ -72,142 +36,162 @@ static uint32_t tileset_buffer[tileset_buffer_length] = {0}; static const size_t tilemap_buffer_length = 256 * 256 * 4; static uint32_t tilemap_buffer[tilemap_buffer_length] = {0}; -static unsigned char number_of_buffers(void) { - bool should_blend = !fallback_canvas; +static uint8_t pressed_buttons; - return should_blend? 3 : 2; +// 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; } -static void flip(void) { - current_buffer = (current_buffer + 1) % number_of_buffers(); -} +// 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; -static uint32_t *get_pixels(void) { - return image_buffers[(current_buffer + 1) % number_of_buffers()]; -} - -static uint32_t *get_current_buffer(void) { - return image_buffers[current_buffer]; -} - -static uint32_t *get_previous_buffer(void) { - return image_buffers[(current_buffer + 2) % number_of_buffers()]; -} - -static void render_texture(void *pixels, void *previous) { - static void *_pixels = NULL; - if (pixels) { - _pixels = pixels; + if (g_variant_dict_lookup(options, "version", "b", &count)) { + g_print("SameBoy v" xstr(VERSION) "\n"); + return EXIT_SUCCESS; } - 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 handle_events(GB_gameboy_t *gb) { - while (gtk_events_pending()) { - gtk_main_iteration(); + if (g_variant_dict_lookup(options, "fullscreen", "b", &count)) { + user_data->fullscreen = true; } - 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); -} + // 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); -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); - - // Queue a redraw of the VRAM viewer - gtk_widget_queue_draw(GTK_WIDGET(vram_viewer)); - } -} - -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; + // 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 { - y_factor = x_factor; + g_printerr("Unknown model: %s\n", model_name); + exit(EXIT_FAILURE); } } - 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 - }; - - if (!fallback_canvas) glViewport(rect.x, rect.y, rect.w, rect.h); + return -1; } -// 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; +// 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; - gtk_tree_model_get(model, iter, 0, &text, -1); - gboolean result = g_strcmp0("", text) == 0; + 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; } -// 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); +// 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; - 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); - } + GError *error = NULL; + GdkWindow *gdk_window = gtk_widget_get_window(w); + GdkGLContext *context = gdk_window_create_gl_context(gdk_window, &error); - if (GTK_IS_CONTAINER(list->data)) { - set_combo_box_row_separator_func(GTK_CONTAINER(list->data)); - } - - list = list->next; + 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; - g_list_free_full(list, NULL); + gdk_gl_context_clear_current(); + + g_print("OpenGL version: %d\n", version); + + *result = version >= 32; + } } // Returns a `GApplication`s `GMenuModel` by ID @@ -220,144 +204,7 @@ static GMenuModel *get_menu_model(GApplication *app, const char *id) { return menu ? G_MENU_MODEL(g_object_ref_sink(menu)) : NULL; } -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); -} - -// app.quit GAction -// Exits the application -static void activate_quit(GSimpleAction *action, GVariant *parameter, gpointer app) { - quit(G_APPLICATION(app)); -} - -// 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.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.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_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 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) { - g_print("%s\n", gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(native))); - } - - g_object_unref(native); -} - -// List of GActions for the `app` prefix -static 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 }, -}; - -G_MODULE_EXPORT 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; -} - -G_MODULE_EXPORT void on_quit(GtkWidget *w, gpointer app) { - quit(G_APPLICATION(app)); -} - -G_MODULE_EXPORT void on_show_window(GtkWidget *w, gpointer window) { - gtk_widget_show_all(GTK_WIDGET(window)); -} - -G_MODULE_EXPORT 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, rect.x, rect.y); - cairo_scale(cr, rect.w / screen_width, rect.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; -} - -G_MODULE_EXPORT void resize() { - update_viewport(); -} - +// 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); @@ -365,187 +212,12 @@ static void create_fallback_canvas(void) { gtk_box_pack_end(GTK_BOX(main_window_container), GTK_WIDGET(fallback_canvas), TRUE, TRUE, 0); } -G_MODULE_EXPORT void gl_draw() { - render_texture(get_current_buffer(), get_previous_buffer()); -} - -G_MODULE_EXPORT void gl_finish() { } - -G_MODULE_EXPORT 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(); -} - -G_MODULE_EXPORT void on_vram_viewer_realize() { - vram_viewer_visible = true; -} - -G_MODULE_EXPORT void on_vram_viewer_unrealize() { - vram_viewer_visible = false; -} - -G_MODULE_EXPORT 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_set_source_surface(cr, surface, 0, 0); - cairo_paint(cr); - - return FALSE; -} - -G_MODULE_EXPORT 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_set_source_surface(cr, surface, 0, 0); - cairo_paint(cr); - - return FALSE; -} - -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_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(); -} - -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_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_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_cgb_model_changed(GtkWidget *w, gpointer user_data_gptr) { - GtkComboBox *box = GTK_COMBO_BOX(w); - g_print("New value: %s\n", gtk_combo_box_get_active_id(box)); -} - -G_MODULE_EXPORT void on_sgb_model_changed(GtkWidget *w, gpointer user_data_gptr) { - GtkComboBox *box = GTK_COMBO_BOX(w); - g_print("New value: %s\n", gtk_combo_box_get_active_id(box)); -} - -G_MODULE_EXPORT void on_dmg_model_changed(GtkWidget *w, gpointer user_data_gptr) { - GtkComboBox *box = GTK_COMBO_BOX(w); - g_print("New value: %s\n", gtk_combo_box_get_active_id(box)); -} - -G_MODULE_EXPORT void on_rewind_duration_changed(GtkWidget *w, gpointer user_data_gptr) { - GtkComboBox *box = GTK_COMBO_BOX(w); - g_print("New value: %s\n", gtk_combo_box_get_active_id(box)); -} - -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_color_menubar_override_changed(GtkWidget *w, gpointer user_data_gptr) { - config.menubar_override = (gchar *)gtk_combo_box_get_active_id(GTK_COMBO_BOX(w)); -} - +// 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(); @@ -619,47 +291,65 @@ static void setup_menu(GApplication *app) { } } -G_MODULE_EXPORT void gl_check_realize(GtkWidget *w, gpointer user_data_gptr) { - gboolean *result = (gboolean *) user_data_gptr; +// 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); - GError *error = NULL; - GdkWindow *gdk_window = gtk_widget_get_window(w); - GdkGLContext *context = gdk_window_create_gl_context(gdk_window, &error); + 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 (error != NULL) { - g_printerr("Failed to create context: %s\n", error->message); - g_error_free(error); - *result = FALSE; + if (GTK_IS_CONTAINER(list->data)) { + set_combo_box_row_separator_func(GTK_CONTAINER(list->data)); + } + + list = list->next; } - 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; - } + g_list_free_full(list, NULL); } -gboolean test_gl_support(void) { - gboolean result = FALSE; +// 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_init(NULL, NULL); - 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; + 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; @@ -795,6 +485,14 @@ static void activate(GApplication *app, gpointer user_data_gptr) { 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) { @@ -811,80 +509,324 @@ static void open(GApplication *app, GFile **files, gint n_files, const gchar *hi activate(app, user_data_gptr); } -// 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"); +// 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; - save_settings(); - free_settings(); + // Quit our application properly. + // This fires the “shutdown” signal. + g_application_quit(app); } -// 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; +static gboolean on_key_press(GtkWidget *w, GdkEventKey *event, gpointer data) { + uint8_t mask; - if (g_variant_dict_lookup(options, "version", "b", &count)) { - g_print("SameBoy v" xstr(VERSION) "\n"); - return EXIT_SUCCESS; + // 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; } - if (g_variant_dict_lookup(options, "fullscreen", "b", &count)) { - user_data->fullscreen = true; + 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))); } - // 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); + g_object_unref(native); +} - // 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); - } +// 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; } - return -1; + 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, rect.x, rect.y); + cairo_scale(cr, rect.w / screen_width, rect.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_set_source_surface(cr, surface, 0, 0); + cairo_paint(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_set_source_surface(cr, surface, 0, 0); + cairo_paint(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); + g_print("New value: %s\n", 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); + g_print("New value: %s\n", 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); + g_print("New value: %s\n", gtk_combo_box_get_active_id(box)); +} + +G_MODULE_EXPORT void on_sgb_model_changed(GtkWidget *w, gpointer user_data_gptr) { + GtkComboBox *box = GTK_COMBO_BOX(w); + g_print("New value: %s\n", 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) { @@ -913,6 +855,54 @@ static uint32_t rgb_encode_fallback(GB_gameboy_t *gb, uint8_t r, uint8_t g, uint return color; } +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), rect.x, rect.y, rect.w, rect.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); + + rect = (Rect){ + (win_width - new_width) / 2, + (win_height - new_height) / 2, + new_width, + new_height + }; + + if (!fallback_canvas) glViewport(rect.x, rect.y, rect.w, rect.h); +} + static void update_window_geometry() { // Set size hints GdkGeometry hints; @@ -943,6 +933,43 @@ static void update_window_geometry() { 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); + + // 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 @@ -1063,35 +1090,3 @@ static void run(GApplication *app, UserData *user_data) { } } } - -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; -} diff --git a/gtk3/main.h b/gtk3/main.h new file mode 100644 index 0000000..f2eb600 --- /dev/null +++ b/gtk3/main.h @@ -0,0 +1,115 @@ +#ifndef main_h +#define main_h + +#include +#include +#include +#include +#include +#include +#include + +#include "macros.h" +#include "settings.h" +#include "shader.h" + +typedef struct UserData { + bool fullscreen; + GFile *file; + gchar *boot_rom_path; + gchar *config_path; + GB_model_t model; +} UserData; + +typedef struct{ + int16_t x, y; + uint16_t w, h; +} Rect; + +#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 + +int main(int argc, char *argv[]); + +static gint handle_local_options(GApplication *app, GVariantDict *options, gpointer user_data_gptr); + +// GtkGlArea crash workaround +void gl_check_realize(GtkWidget *w, gpointer user_data_gptr); +gboolean test_gl_support(void); + +static GMenuModel *get_menu_model(GApplication *app, const char *id); +static void create_fallback_canvas(void); +static void setup_menu(GApplication *app); +static void set_combo_box_row_separator_func(GtkContainer *container); +static gboolean is_separator(GtkTreeModel *model, GtkTreeIter *iter, gpointer data); + +// Rendering support functions +static unsigned char number_of_buffers(void); +static uint32_t *get_pixels(void); +static uint32_t *get_current_buffer(void); +static uint32_t *get_previous_buffer(void); +static void flip(void); + +// GApplication signals +static void startup(GApplication *app, gpointer user_data_gptr); +static void activate(GApplication *app, gpointer user_data_gptr); +static void shutdown(GApplication *app, GFile **files, gint n_files, const gchar *hint, gpointer user_data_gptr); +static void open(GApplication *app, GFile **files, gint n_files, const gchar *hint, gpointer user_data_gptr); +static void quit(GApplication *app); +static gboolean on_key_press(GtkWidget *w, GdkEventKey *event, gpointer data); + +// App actions +static void activate_about(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_open(GSimpleAction *action, GVariant *parameter, gpointer app); +static void activate_preferences(GSimpleAction *action, GVariant *parameter, gpointer app); +static void activate_quit(GSimpleAction *action, GVariant *parameter, gpointer app); + +// Signal callback +static void on_quit(GtkWidget *w, gpointer app); + +// Renderer area bindings +static void gl_init(GtkWidget *w); +static void gl_draw(); +static void gl_finish(); +static gboolean on_draw_fallback(GtkWidget *widget, cairo_t *cr, gpointer data); +static void resize(); + +// VRAM viewer bindings +static void on_vram_viewer_realize(); +static void on_vram_viewer_unrealize(); +static gboolean on_draw_vram_viewer_tilemap(GtkWidget *widget, cairo_t *cr, gpointer data); +static gboolean on_draw_vram_viewer_tileset(GtkWidget *widget, cairo_t *cr, gpointer data); + +// Option bindings +G_MODULE_EXPORT void on_boot_rom_location_changed(GtkWidget *w, gpointer user_data_gptr); +G_MODULE_EXPORT void on_cgb_model_changed(GtkWidget *w, gpointer user_data_gptr); +G_MODULE_EXPORT void on_color_correction_changed(GtkWidget *w, gpointer user_data_gptr); +G_MODULE_EXPORT void on_color_menubar_override_changed(GtkWidget *w, gpointer user_data_gptr); +G_MODULE_EXPORT void on_dmg_model_changed(GtkWidget *w, gpointer user_data_gptr); +G_MODULE_EXPORT void on_graphic_filter_changed(GtkWidget *w, gpointer user_data_gptr); +G_MODULE_EXPORT void on_highpass_filter_changed(GtkWidget *w, gpointer user_data_gptr); +G_MODULE_EXPORT void on_keep_aspect_ratio_changed(GtkWidget *w, gpointer user_data_gptr); +G_MODULE_EXPORT void on_rewind_duration_changed(GtkWidget *w, gpointer user_data_gptr); +G_MODULE_EXPORT void on_sgb_model_changed(GtkWidget *w, gpointer user_data_gptr); +G_MODULE_EXPORT void on_use_integer_scaling_changed(GtkWidget *w, gpointer user_data_gptr); + +static uint32_t rgb_encode(GB_gameboy_t *gb, uint8_t r, uint8_t g, uint8_t b); +static uint32_t rgb_encode_fallback(GB_gameboy_t *gb, uint8_t r, uint8_t g, uint8_t b); +static void render_texture(void *pixels, void *previous); +static void update_viewport(void); +static void update_window_geometry(); + +static void handle_events(GB_gameboy_t *gb); +static void vblank(GB_gameboy_t *gb); +static void run(GApplication *app, UserData *user_data); + +#endif /* main_h */ diff --git a/gtk3/resources/ui/window.ui b/gtk3/resources/ui/window.ui index 531d204..9a09146 100644 --- a/gtk3/resources/ui/window.ui +++ b/gtk3/resources/ui/window.ui @@ -648,8 +648,9 @@ Maximilian Mader https://github.com/max-m False Automatic - Force Menubar - Force Hamburger Menu + Force Menubar In Desktop Shell + Force Menubar In Window + Force Hamburger Menu