diff --git a/gtk3/main.c b/gtk3/main.c index e530cdd..1b0b313 100644 --- a/gtk3/main.c +++ b/gtk3/main.c @@ -1,6 +1,60 @@ -#include "main.h" +#define G_LOG_USE_STRUCTURED + +#include +#include +#include +#include +#include +#include +#include + +#include "macros.h" +#include "settings.h" +#include "shader.h" + +// used for audio and game controllers +#include "SDL.h" #include "../SDL/audio/audio.h" +#define JOYSTICK_HIGH 0x4000 +#define JOYSTICK_LOW 0x3800 + +#define BUTTON_MASK_A 0x01 +#define BUTTON_MASK_B 0x02 +#define BUTTON_MASK_START 0x04 +#define BUTTON_MASK_SELECT 0x08 +#define BUTTON_MASK_UP 0x10 +#define BUTTON_MASK_DOWN 0x20 +#define BUTTON_MASK_LEFT 0x40 +#define BUTTON_MASK_RIGHT 0x80 + +typedef struct GuiData { + struct CliOptionData { + gchar *config_path; + gchar *boot_rom_path; + gboolean fullscreen; + GB_model_t model; + gchar *prefix; + } cli_options; + + GFile *file; + gint sample_rate; + + bool stopped; + GB_model_t prev_model; +} GuiData; + +typedef struct{ + int16_t x, y; + uint16_t w, h; +} Rect; + +typedef struct LogData { + GB_gameboy_t *gb; + const char *string; + GB_log_attributes attributes; +} LogData; + static SDL_GameController *controller = NULL; static const GThread *main_thread; @@ -39,6 +93,7 @@ static gchar *vram_viewer_active_tab = ""; static gboolean vram_viewer_is_cgb = false; static uint8_t vram_viewer_palette_data[16][0x40]; static bool border_mode_changed = false; +static bool audio_initialized = false; static volatile bool running = false; static volatile bool stopping = false; @@ -68,6 +123,22 @@ static GCond debugger_input_cond; static GRecMutex console_output_lock; static GPtrArray *debugger_input_queue; +// Forward declarations of the actions +static void activate_open(GSimpleAction *action, GVariant *parameter, gpointer app); +static void activate_close(GSimpleAction *action, GVariant *parameter, gpointer app); +static void activate_reset(GSimpleAction *action, GVariant *parameter, gpointer app); +static void activate_show_console(GSimpleAction *action, GVariant *parameter, gpointer app); +static void activate_open_gtk_debugger(GSimpleAction *action, GVariant *parameter, gpointer app); +static void activate_open_memory_viewer(GSimpleAction *action, GVariant *parameter, gpointer app); +static void activate_open_vram_viewer(GSimpleAction *action, GVariant *parameter, gpointer app); +static void activate_clear_console(GSimpleAction *action, GVariant *parameter, gpointer app); +static void activate_quit(GSimpleAction *action, GVariant *parameter, gpointer app); +static void activate_about(GSimpleAction *action, GVariant *parameter, gpointer app); +static void activate_preferences(GSimpleAction *action, GVariant *parameter, gpointer app); +static void on_pause_changed(GSimpleAction *action, GVariant *value, gpointer user_data_ptr); +static void on_mute_changed(GSimpleAction *action, GVariant *value, gpointer user_data_ptr); +static void on_model_changed(GSimpleAction *action, GVariant *value, gpointer user_data_ptr); + static const GActionEntry file_entries[] = { { "open", activate_open, NULL, NULL, NULL }, { "close", activate_close, NULL, NULL, NULL }, @@ -101,47 +172,48 @@ static const GActionEntry app_entries[] = { { "change_model", NULL, "s", "@s 'CGB'", on_model_changed }, }; -int main(int argc, char *argv[]) { - main_thread = g_thread_self(); +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; +} - // initialize GB_model_t to invalid value - gui_data.cli_options.model = -1; - gui_data.prev_model = -1; +static uint32_t convert_color(uint16_t color) { + const uint8_t r = ((uint16_t)(color & 0x1F) * 255) / 31; + const uint8_t g = ((uint16_t)((color >> 5) & 0x1F) * 255) / 31; + const uint8_t b = ((uint16_t)((color >> 10) & 0x1F) * 255) / 31; - // 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); + return (r << 16) | (g << 8) | b; +} - // Define our command line parameters - GOptionEntry entries[] = { - { "version", 'v', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, NULL, "Show the application version", NULL }, - { "fullscreen", 'f', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, NULL, "Start in fullscreen mode", NULL }, - { "bootrom", 'b', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &gui_data.cli_options.boot_rom_path, "Path to the boot ROM to use", "" }, - { "model", 'm', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, NULL, "Override the model type to emulate", "" }, - { "config", 'c', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &gui_data.cli_options.config_path, "Override the path of the configuration file", "" }, - { NULL } - }; - // Setup our command line information - g_application_add_main_option_entries(G_APPLICATION(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."); +static void palette_color_data_func(GtkTreeViewColumn *col, GtkCellRenderer *renderer, GtkTreeModel *model, GtkTreeIter *iter, gpointer user_data_ptr) { + const gchar *title = gtk_tree_view_column_get_title(col); + const uint8_t color_index = g_ascii_strtoll(&title[6], NULL, 10); + const uint8_t column_index = 2 + (2 * color_index); - // Add signal handlers - g_signal_connect(main_application, "handle-local-options", G_CALLBACK(handle_local_options), &gui_data); - g_signal_connect(main_application, "startup", G_CALLBACK(startup), &gui_data); - g_signal_connect(main_application, "activate", G_CALLBACK(activate), &gui_data); - g_signal_connect(main_application, "open", G_CALLBACK(open), &gui_data); - g_signal_connect(main_application, "shutdown", G_CALLBACK(shutdown), &gui_data); + GValue color_val = G_VALUE_INIT; + gtk_tree_model_get_value(model, iter, column_index, &color_val); + gint color = g_value_get_int(&color_val); + gchar *color_string = g_strdup_printf("#%06x", color); - // Start our GApplication main loop - int status = g_application_run(G_APPLICATION(main_application), argc, argv); - g_object_unref(main_application); + gint lightness = 0.299 * ((color >> 16) & 0xFF) + 0.587 * ((color >> 8) & 0xFF) + 0.114 * (color & 0xFF); - return status; + GValue color_str = G_VALUE_INIT; + g_value_init(&color_str, G_TYPE_STRING); + g_value_set_string(&color_str, color_string); + g_object_set_property(G_OBJECT(renderer), "background", &color_str); + + GValue fg_color_str = G_VALUE_INIT; + g_value_init(&fg_color_str, G_TYPE_STRING); + g_value_set_static_string(&fg_color_str, (lightness > 0x7F)? "#000000" : "#FFFFFF"); + g_object_set_property(G_OBJECT(renderer), "foreground", &fg_color_str); + + g_value_unset(&color_val); + g_value_unset(&color_str); + g_value_unset(&fg_color_str); + g_free(color_string); } // This function gets called after the parsing of the commandline options has occurred. -static gint handle_local_options(GApplication *app, GVariantDict *options, gpointer gui_data_gptr) { - GuiData *gui_data = gui_data_gptr; +static gint handle_local_options(GApplication *app, GVariantDict *options, gpointer null_ptr) { guint32 count; if (g_variant_dict_lookup(options, "version", "b", &count)) { @@ -150,7 +222,7 @@ static gint handle_local_options(GApplication *app, GVariantDict *options, gpoin } if (g_variant_dict_lookup(options, "fullscreen", "b", &count)) { - gui_data->cli_options.fullscreen = true; + gui_data.cli_options.fullscreen = true; } // Handle model override @@ -160,54 +232,54 @@ static gint handle_local_options(GApplication *app, GVariantDict *options, gpoin // TODO: Synchronize with GB_model_t (Core/gb.h) if (g_str_has_prefix(model_name, "DMG")) { - gui_data->cli_options.prefix = "DMG"; + gui_data.cli_options.prefix = "DMG"; if (g_str_has_suffix(model_name, "-B") || g_strcmp0(model_name, "DMG") == 0) { - gui_data->cli_options.model = GB_MODEL_DMG_B; + gui_data.cli_options.model = GB_MODEL_DMG_B; } else { - gui_data->cli_options.model = GB_MODEL_DMG_B; + gui_data.cli_options.model = GB_MODEL_DMG_B; g_warning("Unsupported revision: %s\nFalling back to DMG-B", model_name); } } else if (g_str_has_prefix(model_name, "SGB")) { - gui_data->cli_options.prefix = "SGB"; + gui_data.cli_options.prefix = "SGB"; if (g_str_has_suffix(model_name, "-NTSC") || g_strcmp0(model_name, "SGB") == 0) { - gui_data->cli_options.model = GB_MODEL_SGB; + gui_data.cli_options.model = GB_MODEL_SGB; } else if (g_str_has_suffix(model_name, "-PAL")) { - gui_data->cli_options.model = GB_MODEL_SGB | GB_MODEL_PAL_BIT; + gui_data.cli_options.model = GB_MODEL_SGB | GB_MODEL_PAL_BIT; } else if (g_str_has_suffix(model_name, "2")) { - gui_data->cli_options.model = GB_MODEL_SGB2; + gui_data.cli_options.model = GB_MODEL_SGB2; } else { - gui_data->cli_options.model = GB_MODEL_SGB2; + gui_data.cli_options.model = GB_MODEL_SGB2; g_warning("Unsupported revision: %s\nFalling back to SGB2", model_name); } } else if (g_str_has_prefix(model_name, "CGB")) { - gui_data->cli_options.prefix = "CGB"; + gui_data.cli_options.prefix = "CGB"; if (g_str_has_suffix(model_name, "-C")) { - gui_data->cli_options.model = GB_MODEL_CGB_C; + gui_data.cli_options.model = GB_MODEL_CGB_C; } else if (g_str_has_suffix(model_name, "-E") || g_strcmp0(model_name, "CGB") == 0) { - gui_data->cli_options.model = GB_MODEL_CGB_E; + gui_data.cli_options.model = GB_MODEL_CGB_E; } else { - gui_data->cli_options.model = GB_MODEL_CGB_E; + gui_data.cli_options.model = GB_MODEL_CGB_E; g_warning("Unsupported revision: %s\nFalling back to CGB-E", model_name); } } else if (g_str_has_prefix(model_name, "AGB")) { - gui_data->cli_options.prefix = "AGB"; + gui_data.cli_options.prefix = "AGB"; - gui_data->cli_options.model = GB_MODEL_AGB; + gui_data.cli_options.model = GB_MODEL_AGB; } else { - gui_data->cli_options.prefix = NULL; + gui_data.cli_options.prefix = NULL; g_warning("Unknown model: %s", model_name); exit(EXIT_FAILURE); @@ -217,25 +289,9 @@ static gint handle_local_options(GApplication *app, GVariantDict *options, gpoin 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; +void gl_check_realize(GtkWidget *w, gpointer user_data_ptr) { + gboolean *result = (gboolean *) user_data_ptr; GError *error = NULL; GdkWindow *gdk_window = gtk_widget_get_window(w); @@ -262,7 +318,24 @@ void gl_check_realize(GtkWidget *w, gpointer user_data_gptr) { } } -static gboolean init_controllers() { +// Workaround to figure out if we have proper OpenGL support. +// Otherwise the application would crash after our GtkGlArea is realized +// and the context it uses is a legacy OpenGL 1.4 context because +// GTK3 calls OpenGL 2.0+ functions on it. +gboolean test_gl_support(void) { + gboolean result = false; + + GtkWidget *window = gtk_window_new(GTK_WINDOW_TOPLEVEL); + g_signal_connect(window, "realize", G_CALLBACK(gl_check_realize), &result); + gtk_widget_realize(window); + gtk_widget_destroy(window); + window = NULL; + + return result; +} + + +static gboolean init_controllers(void) { if (SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) < 0) { g_warning("Failed to initialize game controller support: %s", SDL_GetError()); return false; @@ -304,8 +377,7 @@ static gboolean init_controllers() { return true; } -static bool audio_initialized = false; -static gboolean init_audio() { +static gboolean init_audio(void) { bool audio_playing = GB_audio_is_playing(); if (audio_initialized) { @@ -328,7 +400,7 @@ static gboolean init_audio() { return audio_initialized = true; } -static GB_model_t get_model() { +static GB_model_t get_model(void) { if (gui_data.cli_options.model != -1) { return gui_data.cli_options.model; } @@ -371,57 +443,53 @@ static void gb_audio_callback(GB_gameboy_t *gb, GB_sample_t *sample) { GB_audio_queue_sample(sample); } -// Console TODO: -// TODO: clear sidebar when switching to async mode -// TODO: Command history (up / down arrow in input) -// TODO: reverse search of commands -// TODO: search in output -static char *sync_console_input(GB_gameboy_t *gb) { - update_debugger_sidebar(gb); - console_log(gb, "> ", 0); - in_sync_input = true; - - g_mutex_lock(&debugger_input_mutex); - g_cond_wait(&debugger_input_cond, &debugger_input_mutex); - - gchar *input = NULL; - const gchar *_input = g_ptr_array_index(debugger_input_queue, 0); - input = g_strdup(_input); - gpointer ptr = g_ptr_array_remove_index(debugger_input_queue, 0); - if (ptr) g_free(ptr); - - g_mutex_unlock(&debugger_input_mutex); - - in_sync_input = false; - - return input; -} - -static void clear_sidebar() { +static void clear_sidebar(void) { GtkTextView *sidebar_output = builder_get(GTK_TEXT_VIEW, "console_sidebar_output"); GtkTextBuffer *sidebar_output_text_buf = gtk_text_view_get_buffer(sidebar_output); gtk_text_buffer_set_text(sidebar_output_text_buf, "", -1); } -static char *async_console_input(GB_gameboy_t *gb) { - // TODO: This is rather ugly - g_idle_add((GSourceFunc) clear_sidebar, NULL); +static gboolean scroll_to_bottom(GtkTextView *textview, GtkTextMark *mark) { + GtkTextBuffer *buffer = gtk_text_view_get_buffer(textview); + GtkTextIter iter; - if (debugger_input_queue->len == 0) return NULL; + gtk_text_buffer_get_end_iter(buffer, &iter); + gtk_text_iter_set_line_offset(&iter, 0); - g_mutex_lock(&debugger_input_mutex); + gtk_text_buffer_move_mark(buffer, mark, &iter); + gtk_text_view_scroll_to_mark(textview, mark, 0.0, true, 0.0, 0.10); - gchar *input = NULL; - const gchar *_input = g_ptr_array_index(debugger_input_queue, 0); - if (_input) { - input = g_strdup(_input); - gpointer ptr = g_ptr_array_remove_index(debugger_input_queue, 0); - if (ptr) g_free(ptr); + gtk_text_buffer_delete_mark(buffer, mark); + + return true; +} + +static void append_pending_output(void) { + g_rec_mutex_lock(&console_output_lock); + + if (should_clear_sidebar) { + clear_sidebar(); + should_clear_sidebar = false; } - g_mutex_unlock(&debugger_input_mutex); + if (gtk_text_buffer_get_char_count(pending_console_output) > 0) { + GtkTextView *text_view = builder_get(GTK_TEXT_VIEW, log_to_sidebar? "console_sidebar_output" : "console_screen"); + GtkTextBuffer *text_buf = gtk_text_view_get_buffer(text_view); + GtkTextIter start; + GtkTextIter end; + gtk_text_buffer_get_start_iter(pending_console_output, &start); + gtk_text_buffer_get_end_iter(pending_console_output, &end); - return input; + GtkTextIter iter; + gtk_text_buffer_get_end_iter(text_buf, &iter); + gtk_text_buffer_insert_range(text_buf, &iter, &start, &end); + + scroll_to_bottom(text_view, gtk_text_buffer_create_mark(text_buf, NULL, &iter, true)); + + gtk_text_buffer_set_text(pending_console_output, "", -1); + } + + g_rec_mutex_unlock(&console_output_lock); } static void update_debugger_sidebar(GB_gameboy_t *gb) { @@ -482,49 +550,6 @@ static void update_debugger_sidebar(GB_gameboy_t *gb) { g_rec_mutex_unlock(&console_output_lock); } -static gboolean scroll_to_bottom(GtkTextView *textview, GtkTextMark *mark) { - GtkTextBuffer *buffer = gtk_text_view_get_buffer(textview); - GtkTextIter iter; - - gtk_text_buffer_get_end_iter(buffer, &iter); - gtk_text_iter_set_line_offset(&iter, 0); - - gtk_text_buffer_move_mark(buffer, mark, &iter); - gtk_text_view_scroll_to_mark(textview, mark, 0.0, true, 0.0, 0.10); - - gtk_text_buffer_delete_mark(buffer, mark); - - return true; -} - -static void append_pending_output(void) { - g_rec_mutex_lock(&console_output_lock); - - if (should_clear_sidebar) { - clear_sidebar(); - should_clear_sidebar = false; - } - - if (gtk_text_buffer_get_char_count(pending_console_output) > 0) { - GtkTextView *text_view = builder_get(GTK_TEXT_VIEW, log_to_sidebar? "console_sidebar_output" : "console_screen"); - GtkTextBuffer *text_buf = gtk_text_view_get_buffer(text_view); - GtkTextIter start; - GtkTextIter end; - gtk_text_buffer_get_start_iter(pending_console_output, &start); - gtk_text_buffer_get_end_iter(pending_console_output, &end); - - GtkTextIter iter; - gtk_text_buffer_get_end_iter(text_buf, &iter); - gtk_text_buffer_insert_range(text_buf, &iter, &start, &end); - - scroll_to_bottom(text_view, gtk_text_buffer_create_mark(text_buf, NULL, &iter, true)); - - gtk_text_buffer_set_text(pending_console_output, "", -1); - } - - g_rec_mutex_unlock(&console_output_lock); -} - static void console_log(GB_gameboy_t *gb, const char *string, GB_log_attributes attributes) { g_rec_mutex_lock(&console_output_lock); @@ -558,6 +583,53 @@ static void console_log(GB_gameboy_t *gb, const char *string, GB_log_attributes g_rec_mutex_unlock(&console_output_lock); } +// Console TODO: +// TODO: clear sidebar when switching to async mode +// TODO: Command history (up / down arrow in input) +// TODO: reverse search of commands +// TODO: search in output +static char *sync_console_input(GB_gameboy_t *gb) { + update_debugger_sidebar(gb); + console_log(gb, "> ", 0); + in_sync_input = true; + + g_mutex_lock(&debugger_input_mutex); + g_cond_wait(&debugger_input_cond, &debugger_input_mutex); + + gchar *input = NULL; + const gchar *_input = g_ptr_array_index(debugger_input_queue, 0); + input = g_strdup(_input); + gpointer ptr = g_ptr_array_remove_index(debugger_input_queue, 0); + if (ptr) g_free(ptr); + + g_mutex_unlock(&debugger_input_mutex); + + in_sync_input = false; + + return input; +} + +static char *async_console_input(GB_gameboy_t *gb) { + // TODO: This is rather ugly + g_idle_add((GSourceFunc) clear_sidebar, NULL); + + if (debugger_input_queue->len == 0) return NULL; + + g_mutex_lock(&debugger_input_mutex); + + gchar *input = NULL; + const gchar *_input = g_ptr_array_index(debugger_input_queue, 0); + if (_input) { + input = g_strdup(_input); + gpointer ptr = g_ptr_array_remove_index(debugger_input_queue, 0); + if (ptr) g_free(ptr); + } + + g_mutex_unlock(&debugger_input_mutex); + + return input; +} + // Returns a `GApplication`s `GMenuModel` by ID // GApplication menus are loaded from `gtk/menus.ui`, `gtk/menus-traditional.ui` and `gtk/menus-common.ui`. static GMenuModel *get_menu_model(GApplication *app, const char *id) { @@ -568,14 +640,6 @@ static GMenuModel *get_menu_model(GApplication *app, const char *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. @@ -656,6 +720,18 @@ static void setup_menu(GApplication *app) { } } +// Determines if a ComboBox entry should be converted into a separator. +// Each element with a text value of `` will be converted into a separator element. +static gboolean is_separator(GtkTreeModel *model, GtkTreeIter *iter, gpointer data) { + gchar *text = NULL; + + gtk_tree_model_get(model, iter, 0, &text, -1); + gboolean result = g_strcmp0("", text) == 0; + g_free(text); + + return result; +} + // Recursively goes through all children of the given container and sets // our `is_separator` function to all children of type`GtkComboBox` static void set_combo_box_row_separator_func(GtkContainer *container) { @@ -674,18 +750,6 @@ static void set_combo_box_row_separator_func(GtkContainer *container) { g_list_free(children); } -// Determines if a ComboBox entry should be converted into a separator. -// Each element with a text value of `` will be converted into a separator element. -static gboolean is_separator(GtkTreeModel *model, GtkTreeIter *iter, gpointer data) { - gchar *text = NULL; - - gtk_tree_model_get(model, iter, 0, &text, -1); - gboolean result = g_strcmp0("", text) == 0; - g_free(text); - - return result; -} - // Determines how many frame buffers to use static unsigned char number_of_buffers(void) { if (fallback_canvas) return 2; @@ -715,10 +779,6 @@ static void flip(void) { current_buffer = (current_buffer + 1) % number_of_buffers(); } -static void quit_interrupt(int ignored) { - quit(); -} - // WHY DO WE NEED SUCH AN UGLY METHOD, GTK?! static void action_entries_set_enabled(const GActionEntry *entries, unsigned n_entries, bool value) { // Assumes null-terminated if n_entries == -1 @@ -730,6 +790,548 @@ static void action_entries_set_enabled(const GActionEntry *entries, unsigned n_e } } +static void update_window_geometry(void) { + // Set size hints + GdkGeometry hints; + hints.min_width = GB_get_screen_width(&gb); + hints.min_height = GB_get_screen_height(&gb); + + gtk_window_set_geometry_hints( + GTK_WINDOW(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]) g_free(image_buffers[0]); + if (image_buffers[1]) g_free(image_buffers[1]); + if (image_buffers[2]) g_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] = g_malloc0(buffer_size); + image_buffers[1] = g_malloc0(buffer_size); + image_buffers[2] = g_malloc0(buffer_size); + + if (GB_is_inited(&gb)) { + GB_set_pixels_output(&gb, get_pixels()); + } +} + +static void stop(void) { + if (!running) return; + + GB_audio_set_paused(true); + GB_debugger_set_disabled(&gb, true); + + if (GB_debugger_is_stopped(&gb)) { + // [self interruptDebugInputRead]; + } + + stopping = true; + running = false; + while (stopping); + + GB_debugger_set_disabled(&gb, false); + gui_data.stopped = true; +} + +static void on_vblank(gpointer data) { + if (!vram_viewer_updating && vram_viewer_visible) { + vram_viewer_updating = true; + + if (g_strcmp0("vram_viewer_sprites", vram_viewer_active_tab) == 0) { + GtkTreeIter iter; + GtkTreeView *tree_view = builder_get(GTK_TREE_VIEW, "vram_viewer_sprites"); + // gtk_tree_view_set_model(tree_view, NULL); // Do we need this? + + GtkListStore *store = gtk_list_store_new(7, + GDK_TYPE_PIXBUF, // Preview image + G_TYPE_STRING, // X position + G_TYPE_STRING, // Y position + G_TYPE_STRING, // Tile + G_TYPE_STRING, // Tile Address + G_TYPE_STRING, // OAM Address + G_TYPE_STRING // Attributes + ); + + gtk_tree_model_get_iter_first(GTK_TREE_MODEL(store), &iter); + + for (unsigned row = 0; row < oamCount; ++row) { + GdkPixbuf *pixbuf = gdk_pixbuf_new_from_bytes( + g_bytes_new(oamInfo[row].image, 128 * sizeof(uint32_t)), + GDK_COLORSPACE_RGB, true, 8, 8, oamHeight, 8 * sizeof(uint32_t) + ); + + GdkPixbuf *dest = gdk_pixbuf_new(GDK_COLORSPACE_RGB, true, 8, 8 * 2, oamHeight * 2); + + gdk_pixbuf_scale(pixbuf, dest, + 0, 0, 8 * 2, oamHeight * 2, + 0, 0, 2.0, 2.0, + GDK_INTERP_NEAREST + ); + + gtk_list_store_insert_with_values(store, &iter, -1, + 0, dest, + 1, itoa(oamInfo[row].x - 8), + 2, itoa(oamInfo[row].y - 16), + 3, g_strdup_printf("$%02x", oamInfo[row].tile), + 4, g_strdup_printf("$%04x", 0x8000 + oamInfo[row].tile * 0x10), + 5, g_strdup_printf("$%04x", oamInfo[row].oam_addr), + 6, vram_viewer_is_cgb + ? g_strdup_printf("%c%c%c%d%d", + oamInfo[row].flags & 0x80? 'P' : '-', + oamInfo[row].flags & 0x40? 'Y' : '-', + oamInfo[row].flags & 0x20? 'X' : '-', + oamInfo[row].flags & 0x08? 1 : 0, + oamInfo[row].flags & 0x07) + : g_strdup_printf("%c%c%c%d", + oamInfo[row].flags & 0x80? 'P' : '-', + oamInfo[row].flags & 0x40? 'Y' : '-', + oamInfo[row].flags & 0x20? 'X' : '-', + oamInfo[row].flags & 0x10? 1 : 0), + -1 + ); + + g_object_unref(pixbuf); + g_object_unref(dest); + } + + gtk_tree_view_set_model(tree_view, GTK_TREE_MODEL(store)); + g_object_unref(store); + } + else if (g_strcmp0("vram_viewer_palettes", vram_viewer_active_tab) == 0) { + GtkTreeIter iter; + GtkTreeView *tree_view = builder_get(GTK_TREE_VIEW, "vram_viewer_palettes"); + // gtk_tree_view_set_model(tree_view, NULL); // Do we need this? + + GtkListStore *store = gtk_list_store_new(9, + G_TYPE_STRING, // Name + + G_TYPE_STRING, // Color 0 string + G_TYPE_INT, // Color 0 integer + + G_TYPE_STRING, // Color 1 string + G_TYPE_INT, // Color 1 integer + + G_TYPE_STRING, // Color 2 string + G_TYPE_INT, // Color 2 integer + + G_TYPE_STRING, // Color 3 string + G_TYPE_INT // Color 3 integer + ); + + gtk_tree_model_get_iter_first(GTK_TREE_MODEL(store), &iter); + + for (unsigned row = 0; row < 16; ++row) { + uint8_t offset = (row & 7) * 4; + + uint16_t color_0 = (vram_viewer_palette_data[row][((0 + offset) << 1) + 1] << 8) | vram_viewer_palette_data[row][((0 + offset) << 1)]; + uint16_t color_1 = (vram_viewer_palette_data[row][((1 + offset) << 1) + 1] << 8) | vram_viewer_palette_data[row][((1 + offset) << 1)]; + uint16_t color_2 = (vram_viewer_palette_data[row][((2 + offset) << 1) + 1] << 8) | vram_viewer_palette_data[row][((2 + offset) << 1)]; + uint16_t color_3 = (vram_viewer_palette_data[row][((3 + offset) << 1) + 1] << 8) | vram_viewer_palette_data[row][((3 + offset) << 1)]; + + gtk_list_store_insert_with_values(store, &iter, -1, + 0, g_strdup_printf("%s %d", row >=8 ? "Object" : "Background", row & 7), + 1, g_strdup_printf("$%04x", color_0 & 0x7FFF), + 2, convert_color(color_0), + 3, g_strdup_printf("$%04x", color_1 & 0x7FFF), + 4, convert_color(color_1), + 5, g_strdup_printf("$%04x", color_2 & 0x7FFF), + 6, convert_color(color_2), + 7, g_strdup_printf("$%04x", color_3 & 0x7FFF), + 8, convert_color(color_3), + -1 + ); + } + + GtkTreeViewColumn *column_0 = gtk_tree_view_get_column(tree_view, 1); + GtkTreeViewColumn *column_1 = gtk_tree_view_get_column(tree_view, 2); + GtkTreeViewColumn *column_2 = gtk_tree_view_get_column(tree_view, 3); + GtkTreeViewColumn *column_3 = gtk_tree_view_get_column(tree_view, 4); + + GtkCellRendererText *cell_renderer_0 = builder_get(GTK_CELL_RENDERER_TEXT, "vram_viewer_palette_cell_renderer_0"); + GtkCellRendererText *cell_renderer_1 = builder_get(GTK_CELL_RENDERER_TEXT, "vram_viewer_palette_cell_renderer_1"); + GtkCellRendererText *cell_renderer_2 = builder_get(GTK_CELL_RENDERER_TEXT, "vram_viewer_palette_cell_renderer_2"); + GtkCellRendererText *cell_renderer_3 = builder_get(GTK_CELL_RENDERER_TEXT, "vram_viewer_palette_cell_renderer_3"); + + gtk_tree_view_column_set_cell_data_func(column_0, GTK_CELL_RENDERER(cell_renderer_0), palette_color_data_func, NULL, NULL); + gtk_tree_view_column_set_cell_data_func(column_1, GTK_CELL_RENDERER(cell_renderer_1), palette_color_data_func, NULL, NULL); + gtk_tree_view_column_set_cell_data_func(column_2, GTK_CELL_RENDERER(cell_renderer_2), palette_color_data_func, NULL, NULL); + gtk_tree_view_column_set_cell_data_func(column_3, GTK_CELL_RENDERER(cell_renderer_3), palette_color_data_func, NULL, NULL); + + gtk_tree_view_set_model(tree_view, GTK_TREE_MODEL(store)); + g_object_unref(store); + } + + // Queue a redraw of the VRAM viewer + gtk_widget_queue_draw(GTK_WIDGET(vram_viewer)); + + vram_viewer_updating = false; + } + + // 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); + } +} + +static void vblank(GB_gameboy_t *gb) { + flip(); + + if (border_mode_changed) { + GB_set_border_mode(gb, get_display_border_mode()); + update_window_geometry(); + + border_mode_changed = false; + } + + GB_set_pixels_output(gb, get_pixels()); + + if (underclock_down && clock_mutliplier > 0.5) { + clock_mutliplier -= 1.0/16; + GB_set_clock_multiplier(gb, clock_mutliplier); + } + else if (!underclock_down && clock_mutliplier < 1.0) { + clock_mutliplier += 1.0/16; + GB_set_clock_multiplier(gb, clock_mutliplier); + } + + if (g_strcmp0("vram_viewer_tileset", vram_viewer_active_tab) == 0) { + const gchar *palette_id = gtk_combo_box_get_active_id(builder_get(GTK_COMBO_BOX, "vram_viewer_tileset_palette_selector")); + + GB_palette_type_t palette_type = g_str_has_prefix(palette_id, "bg")? GB_PALETTE_BACKGROUND : GB_PALETTE_OAM; + uint8_t palette_index = g_ascii_digit_value(palette_id[palette_type == GB_PALETTE_OAM ? 3 : 2]); + + GB_draw_tileset(gb, tileset_buffer, + palette_type, + palette_index + ); + } + else if (g_strcmp0("vram_viewer_tilemap", vram_viewer_active_tab) == 0) { + const gchar *palette_id = gtk_combo_box_get_active_id(builder_get(GTK_COMBO_BOX, "vram_viewer_tilemap_palette_selector")); + uint8_t palette_index = 0; + GB_palette_type_t palette_type = GB_PALETTE_AUTO; + + if (g_strcmp0("auto", palette_id) != 0) { + palette_type = g_str_has_prefix(palette_id, "bg")? GB_PALETTE_BACKGROUND : GB_PALETTE_OAM; + palette_index = g_ascii_digit_value(palette_id[palette_type == GB_PALETTE_OAM ? 3 : 2]); + } + + GB_map_type_t map_type = GB_MAP_AUTO; + const gchar *map_type_id = gtk_combo_box_get_active_id(builder_get(GTK_COMBO_BOX, "vram_viewer_tilemap_tilemap_selector")); + if (g_strcmp0("auto", map_type_id) != 0) { + map_type = (g_strcmp0("9800", map_type_id) == 0)? GB_MAP_9800 : GB_MAP_9C00; + } + + GB_tileset_type_t tileset_type = GB_TILESET_AUTO; + const gchar *tileset_type_id = gtk_combo_box_get_active_id(builder_get(GTK_COMBO_BOX, "vram_viewer_tilemap_tileset_selector")); + if (g_strcmp0("auto", tileset_type_id) != 0) { + tileset_type = (g_strcmp0("8800", tileset_type_id) == 0)? GB_TILESET_8800 : GB_TILESET_8000; + } + + GB_draw_tilemap(gb, tilemap_buffer, + palette_type, + palette_index, + map_type, + tileset_type + ); + + scrollRect = (Rect){ + GB_read_memory(gb, 0xFF00 | GB_IO_SCX), + GB_read_memory(gb, 0xFF00 | GB_IO_SCY), + 160, 144 + }; + } + else if (g_strcmp0("vram_viewer_sprites", vram_viewer_active_tab) == 0) { + oamCount = GB_get_oam_info(gb, oamInfo, &oamHeight); + vram_viewer_is_cgb = GB_is_cgb(gb); + } + else if (g_strcmp0("vram_viewer_palettes", vram_viewer_active_tab) == 0) { + size_t size; + + for (unsigned row = 0; row < 16; ++row) { + uint8_t *palette_data = GB_get_direct_access(gb, row >= 8? GB_DIRECT_ACCESS_OBP : GB_DIRECT_ACCESS_BGP, &size, NULL); + memcpy(vram_viewer_palette_data[row], palette_data, size); + } + } + + do_rewind = rewind_down; + + g_idle_add((GSourceFunc) on_vblank, NULL); +} + +static void handle_events(GB_gameboy_t *gb) { + SDL_GameControllerUpdate(); + + uint8_t controller_state = 0; + + if (controller) { + int16_t x_axis = SDL_GameControllerGetAxis(controller, SDL_CONTROLLER_AXIS_LEFTX); + int16_t y_axis = SDL_GameControllerGetAxis(controller, SDL_CONTROLLER_AXIS_LEFTY); + + if (x_axis >= JOYSTICK_HIGH) { + controller_state |= BUTTON_MASK_RIGHT; + } + else if (x_axis <= -JOYSTICK_HIGH) { + controller_state |= BUTTON_MASK_LEFT; + } + + if (y_axis >= JOYSTICK_HIGH) { + controller_state |= BUTTON_MASK_DOWN; + } + else if (y_axis <= -JOYSTICK_HIGH) { + controller_state |= BUTTON_MASK_UP; + } + + if (SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_DPAD_RIGHT)) controller_state |= BUTTON_MASK_RIGHT; + if (SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_DPAD_LEFT)) controller_state |= BUTTON_MASK_LEFT; + if (SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_DPAD_UP)) controller_state |= BUTTON_MASK_UP; + if (SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_DPAD_DOWN)) controller_state |= BUTTON_MASK_DOWN; + if (SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_A)) controller_state |= BUTTON_MASK_A; + if (SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_B)) controller_state |= BUTTON_MASK_B; + if (SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_BACK)) controller_state |= BUTTON_MASK_SELECT; + if (SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_START)) controller_state |= BUTTON_MASK_START; + } + + GB_set_key_state(gb, GB_KEY_RIGHT, (pressed_buttons & BUTTON_MASK_RIGHT) | (controller_state & BUTTON_MASK_RIGHT)); + GB_set_key_state(gb, GB_KEY_LEFT, (pressed_buttons & BUTTON_MASK_LEFT) | (controller_state & BUTTON_MASK_LEFT)); + GB_set_key_state(gb, GB_KEY_UP, (pressed_buttons & BUTTON_MASK_UP) | (controller_state & BUTTON_MASK_UP)); + GB_set_key_state(gb, GB_KEY_DOWN, (pressed_buttons & BUTTON_MASK_DOWN) | (controller_state & BUTTON_MASK_DOWN)); + GB_set_key_state(gb, GB_KEY_A, (pressed_buttons & BUTTON_MASK_A) | (controller_state & BUTTON_MASK_A)); + GB_set_key_state(gb, GB_KEY_B, (pressed_buttons & BUTTON_MASK_B) | (controller_state & BUTTON_MASK_B)); + GB_set_key_state(gb, GB_KEY_SELECT, (pressed_buttons & BUTTON_MASK_SELECT) | (controller_state & BUTTON_MASK_SELECT)); + GB_set_key_state(gb, GB_KEY_START, (pressed_buttons & BUTTON_MASK_START) | (controller_state & BUTTON_MASK_START)); +} + +static void load_boot_rom(GB_gameboy_t *gb, GB_boot_rom_t type) { + GError *error = NULL; + char *boot_rom_path = NULL; + GBytes *boot_rom_f = NULL; + const guchar *boot_rom_data; + gsize boot_rom_size; + + static const char *const names[] = { + [GB_BOOT_ROM_DMG0] = "dmg0_boot.bin", + [GB_BOOT_ROM_DMG] = "dmg_boot.bin", + [GB_BOOT_ROM_MGB] = "mgb_boot.bin", + [GB_BOOT_ROM_SGB] = "sgb_boot.bin", + [GB_BOOT_ROM_SGB2] = "sgb2_boot.bin", + [GB_BOOT_ROM_CGB0] = "cgb0_boot.bin", + [GB_BOOT_ROM_CGB] = "cgb_boot.bin", + [GB_BOOT_ROM_AGB] = "agb_boot.bin", + }; + + const char *const boot_rom_name = names[type]; + + if (gui_data.cli_options.boot_rom_path != NULL) { + g_message("[CLI override] Trying to load boot ROM from %s", gui_data.cli_options.boot_rom_path); + if (GB_load_boot_rom(gb, gui_data.cli_options.boot_rom_path)) { + g_warning("Falling back to boot ROM from config"); + goto config_boot_rom; + } + } + else { config_boot_rom: + if (config.boot_rom_path != NULL && g_strcmp0(config.boot_rom_path, "other") != 0 && g_strcmp0(config.boot_rom_path, "auto") != 0) { + boot_rom_path = g_build_filename(config.boot_rom_path, boot_rom_name, NULL); + g_message("Trying to load boot ROM from %s", boot_rom_path); + + if (GB_load_boot_rom(gb, boot_rom_path)) { + g_free(boot_rom_path); + g_warning("Falling back to internal boot ROM"); + goto internal_boot_rom; + } + + g_free(boot_rom_path); + } + else { internal_boot_rom: + boot_rom_path = g_build_filename(RESOURCE_PREFIX "bootroms/", boot_rom_name, NULL); + boot_rom_f = g_resources_lookup_data(boot_rom_path, G_RESOURCE_LOOKUP_FLAGS_NONE, &error); + + g_message("Loading internal boot ROM: %s", boot_rom_path); + g_free(boot_rom_path); + + if (boot_rom_f == NULL) { + g_warning("Failed to load internal boot ROM: %s", boot_rom_path); + g_error_free(error); + exit(EXIT_FAILURE); + } + + boot_rom_data = g_bytes_get_data(boot_rom_f, &boot_rom_size); + GB_load_boot_rom_from_buffer(gb, boot_rom_data, boot_rom_size); + + g_bytes_unref(boot_rom_f); + } + } +} + +static void init(void) { + if (GB_is_inited(&gb)) return; + + GB_init(&gb, get_model()); + + GB_set_vblank_callback(&gb, vblank); + GB_set_pixels_output(&gb, get_current_buffer()); + GB_set_rgb_encode_callback(&gb, rgb_encode); + GB_set_sample_rate(&gb, GB_audio_get_sample_rate()); + GB_set_color_correction_mode(&gb, get_color_correction_mode()); + GB_set_highpass_filter_mode(&gb, get_highpass_mode()); + GB_set_rewind_length(&gb, config.rewind_duration); + GB_set_update_input_hint_callback(&gb, handle_events); + GB_apu_set_sample_callback(&gb, gb_audio_callback); + GB_set_input_callback(&gb, sync_console_input); + GB_set_async_input_callback(&gb, async_console_input); + GB_set_log_callback(&gb, console_log); + GB_set_boot_rom_load_callback(&gb, load_boot_rom); + + if (get_display_border_mode() <= GB_BORDER_ALWAYS) { + GB_set_border_mode(&gb, get_display_border_mode()); + } + + update_window_geometry(); +} + +static void reset(void) { + g_debug("Reset: %d == %d", get_model(), gui_data.prev_model); + GB_model_t current_model = get_model(); + + if (gui_data.prev_model == -1 || gui_data.prev_model == current_model) { + GB_reset(&gb); + } + else { + GB_switch_model_and_reset(&gb, current_model); + } + + GB_set_palette(&gb, get_monochrome_palette()); + + gui_data.prev_model = get_model(); + + GtkRequisition minimum_size; + GtkRequisition natural_size; + gtk_widget_get_preferred_size(GTK_WIDGET(main_window), &minimum_size, &natural_size); + + // Check SGB -> non-SGB and non-SGB to SGB transitions + if (GB_get_screen_width(&gb) != minimum_size.width || GB_get_screen_height(&gb) != minimum_size.height) { + update_window_geometry(); + } + + bool success = false; + if (gui_data.file) { + char *path = g_file_get_path(gui_data.file); + + if (GB_load_rom(&gb, path) == 0) { + success = true; + } + else { + g_warning("Failed to load ROM: %s", path); + } + + g_free(path); + } + + action_set_enabled(main_application, "close", success); + action_entries_set_enabled(emulation_entries, G_N_ELEMENTS(emulation_entries), success); +} + +static void start(void) { + running = true; + gui_data.stopped = false; + + GB_audio_clear_queue(); + GB_audio_set_paused(false); + + /* Run emulation */ + while (running) { + if (rewind_paused) { + handle_events(&gb); + g_usleep(G_USEC_PER_SEC / 8); + } + 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); + } + } + + stopping = false; +} + +// Prevent dependency loop +static void run(void); + +// app.reset GAction +// Resets the emulation +static void activate_reset(GSimpleAction *action, GVariant *parameter, gpointer app) { + if (!GB_is_inited(&gb)) { + init(); + } + + stop(); + reset(); + run(); +} + +static gpointer run_thread(gpointer null_ptr) { + if (!gui_data.file) return NULL; + + if (gui_data.stopped) { + start(); + } + else { + init(); + reset(); + start(); + } + + return NULL; +} + +static void run(void) { + if (running) return; + while (stopping); + + g_thread_new("CoreLoop", run_thread, NULL); +} + +// Tell our application to quit. +// After this functions has been called the `shutdown` signal will be issued. +// +// TODO: Make sure we have a way to quit our emulation loop before `shutdown` gets called +static void quit(void) { + stop(); + + // Quit our application properly. + // This fires the “shutdown” signal. + g_application_quit(G_APPLICATION(main_application)); +} + +static void quit_interrupt(int ignored) { + quit(); +} + +// `destroy` signal GCallback +// Exits the application +static void on_quit(GtkWidget *w, gpointer app) { + quit(); +} + static void create_action_groups(GApplication *app) { g_action_map_add_action_entries(G_ACTION_MAP(app), emulation_entries, G_N_ELEMENTS(emulation_entries), NULL); g_action_map_add_action_entries(G_ACTION_MAP(app), developer_entries, G_N_ELEMENTS(developer_entries), NULL); @@ -741,257 +1343,6 @@ static void create_action_groups(GApplication *app) { action_entries_set_enabled(emulation_entries, G_N_ELEMENTS(emulation_entries), false); } -// This functions gets called immediately after registration of the GApplication -static void startup(GApplication *app, gpointer gui_data_gptr) { - GuiData *gui_data = gui_data_gptr; - - signal(SIGINT, quit_interrupt); - - // Very ugly workaround for GtkGlArea! - // When a GtkGlArea is realized and it creates a legacy GL 1.4 context - // it tries to use GL 2.0 functions to render the window which leads to the application crashing. - // So we initialize GTK, create a dummy GtkWindow object, attach a `realize` callback and - // in this callback create a GdkGLContext on this window. But instead of running the GTK main loop - // we just realize and destroy the dummy window and compare the context’s version in the realize callback. - supports_gl = test_gl_support(); - g_debug("OpenGL supported: %s", supports_gl? "Yes" : "No"); - - builder = gtk_builder_new_from_resource(RESOURCE_PREFIX "ui/window.ui"); - gtk_builder_connect_signals(builder, NULL); - - create_action_groups(app); - - if (gui_data->cli_options.prefix != NULL) { - GAction *action = g_action_map_lookup_action(G_ACTION_MAP(main_application), "change_model"); - g_action_change_state(action, g_variant_new_string(gui_data->cli_options.prefix)); - } - -#if NDEBUG - // Disable when not compiled in debug mode - action_set_enabled(app, "open_gtk_debugger", false); - - // Remove the menubar override - gtk_widget_destroy(builder_get(GTK_WIDGET, "menubar_override_selector_label")); - gtk_widget_destroy(builder_get(GTK_WIDGET, "menubar_override_selector")); -#endif - - preferences = GTK_WINDOW(get_object("preferences")); - - g_signal_connect(preferences, "realize", G_CALLBACK(on_preferences_realize), (gpointer) builder); - init_settings(gui_data->cli_options.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")); - - if (config.sample_rate == -1) { - gui_data->sample_rate = GB_audio_default_sample_rate(); - } - else { - gui_data->sample_rate = config.sample_rate; - } - - // setup main window - 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, gdk_pixbuf_new_from_resource(icons[2], NULL)); // reuse the 64x64 icon - gtk_about_dialog_set_version(about_dialog, "v" xstr(VERSION)); - g_list_free_full(icon_list, g_object_unref); -} - -static void connect_signal_handlers(GApplication *app) { - // Connect signal handlers - gtk_widget_add_events(GTK_WIDGET(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(main_window, "window-state-event", G_CALLBACK(on_window_state_change), 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); - - gtk_widget_add_events(builder_get(GTK_WIDGET, "vram_viewer_tileset_canvas"), GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK); - gtk_widget_add_events(builder_get(GTK_WIDGET, "vram_viewer_tilemap_canvas"), GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK); - - g_signal_connect(get_object("vram_viewer_tileset_canvas"), "motion_notify_event", G_CALLBACK(on_motion_vram_viewer_tileset), NULL); - g_signal_connect(get_object("vram_viewer_tilemap_canvas"), "motion_notify_event", G_CALLBACK(on_motion_vram_viewer_tilemap), NULL); - - g_signal_connect(get_object("vram_viewer_stack"), "notify::visible-child", G_CALLBACK(on_vram_tab_change), NULL); -} - -static void create_canvas() { - // 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(); - } - - GdkScreen *screen = gdk_screen_get_default(); - GtkCssProvider *provider = gtk_css_provider_new(); - gtk_css_provider_load_from_resource(provider, RESOURCE_PREFIX "css/main.css"); - gtk_style_context_add_provider_for_screen(screen, GTK_STYLE_PROVIDER(provider), GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); -} - -static void setup_console() { - GtkTextView *text_view = builder_get(GTK_TEXT_VIEW, "console_screen"); - GtkTextBuffer *text_buf = gtk_text_view_get_buffer(text_view); - - gtk_text_view_set_buffer( - builder_get(GTK_TEXT_VIEW, "console_sidebar_output"), - gtk_text_buffer_new(gtk_text_buffer_get_tag_table(text_buf)) - ); - - gtk_text_buffer_create_tag(text_buf, "bold", "weight", PANGO_WEIGHT_BOLD, NULL); - gtk_text_buffer_create_tag(text_buf, "underline", "underline", PANGO_UNDERLINE_SINGLE, "underline-set", true, NULL); - gtk_text_buffer_create_tag(text_buf, "dashed_underline", "underline", PANGO_UNDERLINE_DOUBLE, "underline-set", true, NULL); - - g_mutex_init(&debugger_input_mutex); - g_cond_init(&debugger_input_cond); - g_rec_mutex_init(&console_output_lock); - - if (!debugger_input_queue) { - debugger_input_queue = g_ptr_array_sized_new(4); - } - - if (!pending_console_output) { - pending_console_output = gtk_text_buffer_new(gtk_text_buffer_get_tag_table(text_buf)); - } -} - -// This function gets called when the GApplication gets activated, i.e. it is ready to show widgets. -static void activate(GApplication *app, gpointer gui_data_gptr) { - GuiData *gui_data = gui_data_gptr; - - // initialize SameBoy core - init(gui_data); - - init_audio(); - init_controllers(); - - connect_signal_handlers(app); - create_canvas(); - setup_console(); - - if (gui_data->cli_options.fullscreen) { - gtk_window_fullscreen(GTK_WINDOW(main_window)); - } - - gtk_application_add_window(GTK_APPLICATION(app), GTK_WINDOW(main_window)); - gtk_widget_show_all(GTK_WIDGET(main_window)); - - // Start the emulation thread - run(gui_data); -} - -// This function gets called when the application is closed. -static void shutdown(GApplication *app, GFile **files, gint n_files, const gchar *hint, gpointer gui_data_gptr) { - g_debug("SHUTDOWN"); - - stop(&gui_data); - while (stopping); - - g_object_unref(builder); - - save_settings(); - free_settings(); - SDL_Quit(); - - if (image_buffers[0]) g_free(image_buffers[0]); - if (image_buffers[1]) g_free(image_buffers[1]); - if (image_buffers[2]) g_free(image_buffers[2]); - free_shader(&shader); - free_master_shader(); - - GB_free(&gb); -} - -// This function gets called when there are files to open. -// Note: When `open` gets called `activate` won’t fire unless we call it ourselves. -static void open(GApplication *app, GFile **files, gint n_files, const gchar *hint, gpointer gui_data_gptr) { - GuiData *gui_data = gui_data_gptr; - - if (n_files > 1) { - g_warning("More than one file specified"); - exit(EXIT_FAILURE); - } - - gui_data->file = g_file_dup(files[0]); - - // We have handled the files, now activate the application - activate(app, gui_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() { - stop(&gui_data); - - // Quit our application properly. - // This fires the “shutdown” signal. - g_application_quit(G_APPLICATION(main_application)); -} - static gboolean on_key_press(GtkWidget *w, GdkEventKey *event, gpointer data) { uint8_t mask; @@ -1055,278 +1406,14 @@ static void on_window_state_change(GtkWidget *w, GdkEventWindowState *event, gpo is_fullscreen = event->new_window_state & GDK_WINDOW_STATE_FULLSCREEN; } -// app.about GAction -// Opens the about dialog -static void activate_about(GSimpleAction *action, GVariant *parameter, gpointer app) { - GObject *dialog = get_object("about_dialog"); - gtk_dialog_run(GTK_DIALOG(dialog)); - gtk_widget_hide(GTK_WIDGET(dialog)); -} - -// app.show_console GAction -// Opens the console -static void activate_show_console(GSimpleAction *action, GVariant *parameter, gpointer app) { - if (debugger_input_queue) { - while (debugger_input_queue->len) { - g_ptr_array_remove_index_fast(debugger_input_queue, debugger_input_queue->len - 1); - } - } - - gtk_widget_show_all(builder_get(GTK_WIDGET, "console")); -} - -// app.open_gtk_debugger GAction -// Opens the GTK debugger -static void activate_open_gtk_debugger(GSimpleAction *action, GVariant *parameter, gpointer app) { - gtk_window_set_interactive_debugging(true); -} - -// app.open_memory_viewer GAction -// Opens the memory viewer window -static void activate_open_memory_viewer(GSimpleAction *action, GVariant *parameter, gpointer app) { - gtk_widget_show_all(GTK_WIDGET(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) { - stop(&gui_data); - - 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) { - const char* path = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(native)); - gui_data.file = g_file_new_for_path(path); - - activate_reset(action, parameter, app); - } - else { - run(&gui_data); - } - - g_object_unref(native); -} - -// app.close GAction -// Closes a ROM -static void activate_close(GSimpleAction *action, GVariant *parameter, gpointer app) { - stop(&gui_data); - GB_free(&gb); - - // Clear the screen as side effect - update_window_geometry(); - gtk_widget_queue_draw(fallback_canvas ? GTK_WIDGET(fallback_canvas) : GTK_WIDGET(gl_area)); - - // Clear the VRAM viewer - g_mutex_lock(&tileset_buffer_mutex); - memset(tileset_buffer, 0, sizeof tileset_buffer); - g_mutex_unlock(&tileset_buffer_mutex); - - g_mutex_lock(&tilemap_buffer_mutex); - memset(tilemap_buffer, 0, sizeof tilemap_buffer); - g_mutex_unlock(&tilemap_buffer_mutex); - - gtk_stack_set_visible_child_name(builder_get(GTK_STACK, "vram_viewer_stack"), "vram_viewer_tileset"); - gtk_tree_view_set_model(builder_get(GTK_TREE_VIEW, "vram_viewer_sprites"), NULL); - gtk_tree_view_set_model(builder_get(GTK_TREE_VIEW, "vram_viewer_palettes"), NULL); - - // Redraw the VRAM viewer - gtk_widget_queue_draw(GTK_WIDGET(vram_viewer)); - - // Update menu action states - action_set_enabled(main_application, "close", false); - action_entries_set_enabled(emulation_entries, G_N_ELEMENTS(emulation_entries), false); -} - -// app.preferences GAction -// Opens the preferences window -static void activate_preferences(GSimpleAction *action, GVariant *parameter, gpointer app) { - gtk_widget_show_all(GTK_WIDGET(get_object("preferences"))); -} - -// app.quit GAction -// Exits the application -static void activate_quit(GSimpleAction *action, GVariant *parameter, gpointer app) { - quit(); -} - -// app.reset GAction -// Resets the emulation -static void activate_reset(GSimpleAction *action, GVariant *parameter, gpointer app) { - if (!GB_is_inited(&gb)) { - init(&gui_data); - } - - stop(&gui_data); - reset(&gui_data); - run(&gui_data); -} - -// app.clear_console GAction -// Clears the debugger console -static void activate_clear_console(GSimpleAction *action, GVariant *parameter, gpointer app) { - g_rec_mutex_lock(&console_output_lock); - - GtkTextView *text_view = builder_get(GTK_TEXT_VIEW, "console_screen"); - GtkTextBuffer *text_buf = gtk_text_view_get_buffer(text_view); - gtk_text_buffer_set_text(text_buf, "", -1); - - g_rec_mutex_unlock(&console_output_lock); -} - -static void on_model_changed(GSimpleAction *action, GVariant *value, gpointer user_data) { - if (!GB_is_inited(&gb)) { - g_simple_action_set_state(action, value); - return; - } - - const gchar *model_str = g_variant_get_string(value, NULL); - - GtkMessageDialog *dialog = GTK_MESSAGE_DIALOG(gtk_message_dialog_new( - GTK_WINDOW(main_window), - GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT, - GTK_MESSAGE_QUESTION, - GTK_BUTTONS_YES_NO, - "Changing the emulated model requires a reset.\nChange model and reset game?" - )); - - stop(&gui_data); - gint result = gtk_dialog_run(GTK_DIALOG(dialog)); - - switch (result) { - case GTK_RESPONSE_YES: - // Reset the CLI model override - gui_data.cli_options.model = -1; - - g_simple_action_set_state(action, value); - reset(&gui_data); - break; - default: - // Action has been canceled - break; - } - - run(&gui_data); - gtk_widget_destroy(GTK_WIDGET(dialog)); -} - -static void on_mute_changed(GSimpleAction *action, GVariant *value, gpointer user_data) { - gboolean do_mute = g_variant_get_boolean(value); - - GB_audio_set_paused(do_mute); - g_simple_action_set_state(action, value); -} - -static void on_pause_changed(GSimpleAction *action, GVariant *value, gpointer user_data) { - if (g_variant_get_boolean(value)) { - stop(&gui_data); - } - else { - run(&gui_data); - } - - g_simple_action_set_state(action, value); -} - -// `destroy` signal GCallback -// Exits the application -static void on_quit(GtkWidget *w, gpointer app) { - quit(); -} - -// TODO: Comment -static void gl_init(GtkWidget *w) { - GtkGLArea *gl_area = GTK_GL_AREA(w); - - g_debug("GL_INIT"); - const char *renderer; - - g_debug("GL Context: %p", gtk_gl_area_get_context(gl_area)); - - gtk_gl_area_make_current(gl_area); - - if (gtk_gl_area_get_error(gl_area) != NULL) { - goto error; - } - - renderer = (char *)glGetString(GL_RENDERER); - g_debug("GtkGLArea on %s", renderer ? renderer : "Unknown"); - - if (config.shader == NULL || (!init_shader_with_name(&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_warning("GtkGLArea: %s", 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() { +static void on_vram_viewer_realize(void) { vram_viewer_visible = true; vram_viewer_active_tab = (gchar *)gtk_stack_get_visible_child_name(builder_get(GTK_STACK, "vram_viewer_stack")); } // Gets called when the VRAM viewer gets unrealized -static void on_vram_viewer_unrealize() { +static void on_vram_viewer_unrealize(void) { vram_viewer_visible = false; } @@ -1562,101 +1649,48 @@ static void on_vram_tab_change(GtkWidget *widget, GParamSpec *pspec, GtkStackSwi vram_viewer_active_tab = (gchar *)gtk_stack_get_visible_child_name(builder_get(GTK_STACK, "vram_viewer_stack")); } -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; +// This functions gets called immediately after registration of the GApplication +static void startup(GApplication *app, gpointer null_ptr) { + signal(SIGINT, quit_interrupt); - 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)); + // 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_debug("OpenGL supported: %s", supports_gl? "Yes" : "No"); - if (res == GTK_RESPONSE_ACCEPT) { - config.boot_rom_path = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(native)); - update_boot_rom_selector(builder); - } + builder = gtk_builder_new_from_resource(RESOURCE_PREFIX "ui/window.ui"); + gtk_builder_connect_signals(builder, NULL); - g_object_unref(native); + create_action_groups(app); + + if (gui_data.cli_options.prefix != NULL) { + GAction *action = g_action_map_lookup_action(G_ACTION_MAP(main_application), "change_model"); + g_action_change_state(action, g_variant_new_string(gui_data.cli_options.prefix)); } - 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); -} +#if NDEBUG + // Disable when not compiled in debug mode + action_set_enabled(app, "open_gtk_debugger", false); -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); + // 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 - if (GB_is_inited(&gb)) { - GB_set_color_correction_mode(&gb, get_color_correction_mode()); - } -} + preferences = GTK_WINDOW(get_object("preferences")); -G_MODULE_EXPORT void on_frame_blending_changed(GtkWidget *w, gpointer user_data_gptr) { - GtkComboBox *box = GTK_COMBO_BOX(w); - config.frame_blending_mode = (gchar *)gtk_combo_box_get_active_id(box); -} + g_signal_connect(preferences, "realize", G_CALLBACK(on_preferences_realize), (gpointer) builder); + init_settings(gui_data.cli_options.config_path, preferences); -G_MODULE_EXPORT void on_display_border_changed(GtkWidget *w, gpointer user_data_gptr) { - GtkComboBox *box = GTK_COMBO_BOX(w); - config.display_border_mode = (gchar *)gtk_combo_box_get_active_id(box); + vram_viewer = GTK_WINDOW(get_object("vram_viewer")); + memory_viewer = GTK_WINDOW(get_object("memory_viewer")); - border_mode_changed = true; -} - -G_MODULE_EXPORT void on_monochrome_palette_changed(GtkWidget *w, gpointer user_data_gptr) { - GtkComboBox *box = GTK_COMBO_BOX(w); - config.monochrome_palette_id = (gchar *)gtk_combo_box_get_active_id(box); - - GB_set_palette(&gb, get_monochrome_palette()); -} - -G_MODULE_EXPORT void on_color_menubar_override_changed(GtkWidget *w, gpointer user_data_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); - - free_shader(&shader); - 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); - GB_set_rewind_length(&gb, config.rewind_duration); -} - -G_MODULE_EXPORT void on_sample_rate_changed(GtkWidget *w, gpointer user_data_gptr) { - GtkComboBox *box = GTK_COMBO_BOX(w); - config.sample_rate = g_ascii_strtoll(gtk_combo_box_get_active_id(box), NULL, 10); + console = GTK_WINDOW(get_object("console")); + printer = GTK_WINDOW(get_object("printer")); if (config.sample_rate == -1) { gui_data.sample_rate = GB_audio_default_sample_rate(); @@ -1665,82 +1699,80 @@ G_MODULE_EXPORT void on_sample_rate_changed(GtkWidget *w, gpointer user_data_gpt gui_data.sample_rate = config.sample_rate; } - init_audio(); + // 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, gdk_pixbuf_new_from_resource(icons[2], NULL)); // reuse the 64x64 icon + gtk_about_dialog_set_version(about_dialog, "v" xstr(VERSION)); + g_list_free_full(icon_list, g_object_unref); } -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); -} +static void connect_signal_handlers(GApplication *app) { + // 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_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_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(main_window, "window-state-event", G_CALLBACK(on_window_state_change), NULL); -G_MODULE_EXPORT void console_on_enter(GtkWidget *w, gpointer user_data_gptr) { - GtkEntry *input = GTK_ENTRY(w); - const gchar *_text = gtk_entry_get_text(input); - gchar *text = g_strdup(_text); + 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); - if (g_strcmp0("", text) == 0 && g_strcmp0("", last_console_input) < 0) { - text = g_strdup(last_console_input); - } - else if (text) { - if (last_console_input != NULL) g_free(last_console_input); - last_console_input = g_strdup(text); - } + // 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); - if (!in_sync_input) { - console_log(&gb, "> ", 0); - } + 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); - console_log(&gb, text, 0); - console_log(&gb, "\n", 0); + gtk_widget_add_events(builder_get(GTK_WIDGET, "vram_viewer_tileset_canvas"), GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK); + gtk_widget_add_events(builder_get(GTK_WIDGET, "vram_viewer_tilemap_canvas"), GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK); - g_mutex_lock(&debugger_input_mutex); - g_ptr_array_add(debugger_input_queue, (gpointer)text); - g_cond_signal(&debugger_input_cond); - g_mutex_unlock(&debugger_input_mutex); + g_signal_connect(get_object("vram_viewer_tileset_canvas"), "motion_notify_event", G_CALLBACK(on_motion_vram_viewer_tileset), NULL); + g_signal_connect(get_object("vram_viewer_tilemap_canvas"), "motion_notify_event", G_CALLBACK(on_motion_vram_viewer_tilemap), NULL); - // clear input - gtk_entry_set_text(input, ""); -} - -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); - - GB_frame_blending_mode_t mode = get_frame_blending_mode(); - if (!previous) { - mode = GB_FRAME_BLENDING_MODE_DISABLED; - } - else if (mode == GB_FRAME_BLENDING_MODE_ACCURATE) { - if (GB_is_sgb(&gb)) { - mode = GB_FRAME_BLENDING_MODE_SIMPLE; - } - else { - mode = GB_is_odd_frame(&gb)? GB_FRAME_BLENDING_MODE_ACCURATE_ODD : GB_FRAME_BLENDING_MODE_ACCURATE_EVEN; - } - } - - render_bitmap_with_shader( - &shader, _pixels, previous, - GB_get_screen_width(&gb), GB_get_screen_height(&gb), - viewport.x, viewport.y, viewport.w, viewport.h, - mode - ); + g_signal_connect(get_object("vram_viewer_stack"), "notify::visible-child", G_CALLBACK(on_vram_tab_change), NULL); } static void update_viewport(void) { @@ -1779,545 +1811,578 @@ static void update_viewport(void) { 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); +// TODO: Comment +static void gl_draw(void) { + uint32_t *pixels = get_current_buffer(); + uint32_t *previous = get_previous_buffer(); - gtk_window_set_geometry_hints( - GTK_WINDOW(main_window), - NULL, - &hints, - (GdkWindowHints)(GDK_HINT_MIN_SIZE) + static void *_pixels = NULL; + + if (pixels) { + _pixels = pixels; + } + + glClearColor(0, 0, 0, 1); + glClear(GL_COLOR_BUFFER_BIT); + + GB_frame_blending_mode_t mode = get_frame_blending_mode(); + if (!previous) { + mode = GB_FRAME_BLENDING_MODE_DISABLED; + } + else if (mode == GB_FRAME_BLENDING_MODE_ACCURATE) { + if (GB_is_sgb(&gb)) { + mode = GB_FRAME_BLENDING_MODE_SIMPLE; + } + else { + mode = GB_is_odd_frame(&gb)? GB_FRAME_BLENDING_MODE_ACCURATE_ODD : GB_FRAME_BLENDING_MODE_ACCURATE_EVEN; + } + } + + render_bitmap_with_shader( + &shader, _pixels, previous, + GB_get_screen_width(&gb), GB_get_screen_height(&gb), + viewport.x, viewport.y, viewport.w, viewport.h, + mode + ); +} + +// TODO: Comment +static void gl_finish(void) { } + +// TODO: Comment +static void resize(void) { + update_viewport(); +} + +// TODO: Comment +static gboolean on_draw_fallback(GtkWidget *widget, cairo_t *cr, gpointer data) { + GtkStyleContext *context = gtk_widget_get_style_context(widget); + guint width = gtk_widget_get_allocated_width(widget); + guint height = gtk_widget_get_allocated_height(widget); + + guint screen_width = GB_get_screen_width(&gb); + guint screen_height = GB_get_screen_height(&gb); + + gtk_render_background(context, cr, 0, 0, width, height); + + cairo_surface_t *surface = cairo_image_surface_create_for_data( + (unsigned char *) get_current_buffer(), + CAIRO_FORMAT_RGB24, + screen_width, + screen_height, + cairo_format_stride_for_width(CAIRO_FORMAT_RGB24, screen_width) ); - gtk_window_resize(GTK_WINDOW(main_window), - GB_get_screen_width(&gb) * 2, - GB_get_screen_height(&gb) * 2 + 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; +} + +// 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); +} + +// TODO: Comment +static void gl_init(GtkWidget *w) { + GtkGLArea *gl_area = GTK_GL_AREA(w); + + g_debug("GL_INIT"); + const char *renderer; + + g_debug("GL Context: %p", gtk_gl_area_get_context(gl_area)); + + gtk_gl_area_make_current(gl_area); + + if (gtk_gl_area_get_error(gl_area) != NULL) { + goto error; + } + + renderer = (char *)glGetString(GL_RENDERER); + g_debug("GtkGLArea on %s", renderer ? renderer : "Unknown"); + + if (config.shader == NULL || (!init_shader_with_name(&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_warning("GtkGLArea: %s", gtk_gl_area_get_error(gl_area)->message); + } + + create_fallback_canvas(); +} + +static void create_canvas(void) { + // 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(); + } + + GdkScreen *screen = gdk_screen_get_default(); + GtkCssProvider *provider = gtk_css_provider_new(); + gtk_css_provider_load_from_resource(provider, RESOURCE_PREFIX "css/main.css"); + gtk_style_context_add_provider_for_screen(screen, GTK_STYLE_PROVIDER(provider), GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); +} + +static void setup_console(void) { + GtkTextView *text_view = builder_get(GTK_TEXT_VIEW, "console_screen"); + GtkTextBuffer *text_buf = gtk_text_view_get_buffer(text_view); + + gtk_text_view_set_buffer( + builder_get(GTK_TEXT_VIEW, "console_sidebar_output"), + gtk_text_buffer_new(gtk_text_buffer_get_tag_table(text_buf)) ); - // Setup our image buffers + gtk_text_buffer_create_tag(text_buf, "bold", "weight", PANGO_WEIGHT_BOLD, NULL); + gtk_text_buffer_create_tag(text_buf, "underline", "underline", PANGO_UNDERLINE_SINGLE, "underline-set", true, NULL); + gtk_text_buffer_create_tag(text_buf, "dashed_underline", "underline", PANGO_UNDERLINE_DOUBLE, "underline-set", true, NULL); + + g_mutex_init(&debugger_input_mutex); + g_cond_init(&debugger_input_cond); + g_rec_mutex_init(&console_output_lock); + + if (!debugger_input_queue) { + debugger_input_queue = g_ptr_array_sized_new(4); + } + + if (!pending_console_output) { + pending_console_output = gtk_text_buffer_new(gtk_text_buffer_get_tag_table(text_buf)); + } +} + +// This function gets called when the GApplication gets activated, i.e. it is ready to show widgets. +static void activate(GApplication *app, gpointer null_ptr) { + // initialize SameBoy core + init(); + + init_audio(); + init_controllers(); + + connect_signal_handlers(app); + create_canvas(); + setup_console(); + + if (gui_data.cli_options.fullscreen) { + gtk_window_fullscreen(GTK_WINDOW(main_window)); + } + + gtk_application_add_window(GTK_APPLICATION(app), GTK_WINDOW(main_window)); + gtk_widget_show_all(GTK_WIDGET(main_window)); + + // Start the emulation thread + run(); +} + +// This function gets called when the application is closed. +static void shutdown(GApplication *app, GFile **files, gint n_files, const gchar *hint, gpointer null_ptr) { + g_debug("SHUTDOWN"); + + stop(); + while (stopping); + + g_object_unref(builder); + + save_settings(); + free_settings(); + SDL_Quit(); + if (image_buffers[0]) g_free(image_buffers[0]); if (image_buffers[1]) g_free(image_buffers[1]); if (image_buffers[2]) g_free(image_buffers[2]); + free_shader(&shader); + free_master_shader(); - size_t buffer_size = sizeof(image_buffers[0][0]) * GB_get_screen_width(&gb) * GB_get_screen_height(&gb); + GB_free(&gb); +} - image_buffers[0] = g_malloc0(buffer_size); - image_buffers[1] = g_malloc0(buffer_size); - image_buffers[2] = g_malloc0(buffer_size); +// This function gets called when there are files to open. +// Note: When `open` gets called `activate` won’t fire unless we call it ourselves. +static void open(GApplication *app, GFile **files, gint n_files, const gchar *hint, gpointer null_ptr) { + if (n_files > 1) { + g_warning("More than one file specified"); + exit(EXIT_FAILURE); + } + + gui_data.file = g_file_dup(files[0]); + + // We have handled the files, now activate the application + activate(app, NULL); +} + +// app.about GAction +// Opens the about dialog +static void activate_about(GSimpleAction *action, GVariant *parameter, gpointer app) { + GObject *dialog = get_object("about_dialog"); + gtk_dialog_run(GTK_DIALOG(dialog)); + gtk_widget_hide(GTK_WIDGET(dialog)); +} + +// app.show_console GAction +// Opens the console +static void activate_show_console(GSimpleAction *action, GVariant *parameter, gpointer app) { + if (debugger_input_queue) { + while (debugger_input_queue->len) { + g_ptr_array_remove_index_fast(debugger_input_queue, debugger_input_queue->len - 1); + } + } + + gtk_widget_show_all(builder_get(GTK_WIDGET, "console")); +} + +// app.open_gtk_debugger GAction +// Opens the GTK debugger +static void activate_open_gtk_debugger(GSimpleAction *action, GVariant *parameter, gpointer app) { + gtk_window_set_interactive_debugging(true); +} + +// app.open_memory_viewer GAction +// Opens the memory viewer window +static void activate_open_memory_viewer(GSimpleAction *action, GVariant *parameter, gpointer app) { + gtk_widget_show_all(GTK_WIDGET(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) { + stop(); + + 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) { + const char* path = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(native)); + gui_data.file = g_file_new_for_path(path); + + activate_reset(action, parameter, app); + } + else { + run(); + } + + g_object_unref(native); +} + +// app.close GAction +// Closes a ROM +static void activate_close(GSimpleAction *action, GVariant *parameter, gpointer app) { + stop(); + GB_free(&gb); + + // Clear the screen as side effect + update_window_geometry(); + gtk_widget_queue_draw(fallback_canvas ? GTK_WIDGET(fallback_canvas) : GTK_WIDGET(gl_area)); + + // Clear the VRAM viewer + g_mutex_lock(&tileset_buffer_mutex); + memset(tileset_buffer, 0, sizeof tileset_buffer); + g_mutex_unlock(&tileset_buffer_mutex); + + g_mutex_lock(&tilemap_buffer_mutex); + memset(tilemap_buffer, 0, sizeof tilemap_buffer); + g_mutex_unlock(&tilemap_buffer_mutex); + + gtk_stack_set_visible_child_name(builder_get(GTK_STACK, "vram_viewer_stack"), "vram_viewer_tileset"); + gtk_tree_view_set_model(builder_get(GTK_TREE_VIEW, "vram_viewer_sprites"), NULL); + gtk_tree_view_set_model(builder_get(GTK_TREE_VIEW, "vram_viewer_palettes"), NULL); + + // Redraw the VRAM viewer + gtk_widget_queue_draw(GTK_WIDGET(vram_viewer)); + + // Update menu action states + action_set_enabled(main_application, "close", false); + action_entries_set_enabled(emulation_entries, G_N_ELEMENTS(emulation_entries), false); +} + +// app.preferences GAction +// Opens the preferences window +static void activate_preferences(GSimpleAction *action, GVariant *parameter, gpointer app) { + gtk_widget_show_all(GTK_WIDGET(get_object("preferences"))); +} + +// app.quit GAction +// Exits the application +static void activate_quit(GSimpleAction *action, GVariant *parameter, gpointer app) { + quit(); +} + +// app.clear_console GAction +// Clears the debugger console +static void activate_clear_console(GSimpleAction *action, GVariant *parameter, gpointer app) { + g_rec_mutex_lock(&console_output_lock); + + GtkTextView *text_view = builder_get(GTK_TEXT_VIEW, "console_screen"); + GtkTextBuffer *text_buf = gtk_text_view_get_buffer(text_view); + gtk_text_buffer_set_text(text_buf, "", -1); + + g_rec_mutex_unlock(&console_output_lock); +} + +static void on_model_changed(GSimpleAction *action, GVariant *value, gpointer user_data_ptr) { + if (!GB_is_inited(&gb)) { + g_simple_action_set_state(action, value); + return; + } + + const gchar *model_str = g_variant_get_string(value, NULL); + + GtkMessageDialog *dialog = GTK_MESSAGE_DIALOG(gtk_message_dialog_new( + GTK_WINDOW(main_window), + GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT, + GTK_MESSAGE_QUESTION, + GTK_BUTTONS_YES_NO, + "Changing the emulated model requires a reset.\nChange model and reset game?" + )); + + stop(); + gint result = gtk_dialog_run(GTK_DIALOG(dialog)); + + switch (result) { + case GTK_RESPONSE_YES: + // Reset the CLI model override + gui_data.cli_options.model = -1; + + g_simple_action_set_state(action, value); + reset(); + break; + default: + // Action has been canceled + break; + } + + run(); + gtk_widget_destroy(GTK_WIDGET(dialog)); +} + +static void on_mute_changed(GSimpleAction *action, GVariant *value, gpointer user_data_ptr) { + gboolean do_mute = g_variant_get_boolean(value); + + GB_audio_set_paused(do_mute); + g_simple_action_set_state(action, value); +} + +static void on_pause_changed(GSimpleAction *action, GVariant *value, gpointer user_data_ptr) { + if (g_variant_get_boolean(value)) { + stop(); + } + else { + run(); + } + + g_simple_action_set_state(action, value); +} + +G_MODULE_EXPORT void on_boot_rom_location_changed(GtkWidget *w, gpointer user_data_ptr) { + GtkComboBox *box = GTK_COMBO_BOX(w); + const gchar *id = gtk_combo_box_get_active_id(box); + if (id == NULL) return; + + if (g_strcmp0(id, "other") == 0) { + GtkFileChooserNative *native = gtk_file_chooser_native_new("Select Folder", 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_ptr) { + GtkComboBox *box = GTK_COMBO_BOX(w); + config.cgb_revision_name = (gchar *)gtk_combo_box_get_active_id(box); +} + +G_MODULE_EXPORT void on_color_correction_changed(GtkWidget *w, gpointer user_data_ptr) { + GtkComboBox *box = GTK_COMBO_BOX(w); + config.color_correction_id = (gchar *)gtk_combo_box_get_active_id(box); if (GB_is_inited(&gb)) { - GB_set_pixels_output(&gb, get_pixels()); + GB_set_color_correction_mode(&gb, get_color_correction_mode()); } } -static void handle_events(GB_gameboy_t *gb) { - SDL_GameControllerUpdate(); - - uint8_t controller_state = 0; - - if (controller) { - int16_t x_axis = SDL_GameControllerGetAxis(controller, SDL_CONTROLLER_AXIS_LEFTX); - int16_t y_axis = SDL_GameControllerGetAxis(controller, SDL_CONTROLLER_AXIS_LEFTY); - - if (x_axis >= JOYSTICK_HIGH) { - controller_state |= BUTTON_MASK_RIGHT; - } - else if (x_axis <= -JOYSTICK_HIGH) { - controller_state |= BUTTON_MASK_LEFT; - } - - if (y_axis >= JOYSTICK_HIGH) { - controller_state |= BUTTON_MASK_DOWN; - } - else if (y_axis <= -JOYSTICK_HIGH) { - controller_state |= BUTTON_MASK_UP; - } - - if (SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_DPAD_RIGHT)) controller_state |= BUTTON_MASK_RIGHT; - if (SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_DPAD_LEFT)) controller_state |= BUTTON_MASK_LEFT; - if (SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_DPAD_UP)) controller_state |= BUTTON_MASK_UP; - if (SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_DPAD_DOWN)) controller_state |= BUTTON_MASK_DOWN; - if (SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_A)) controller_state |= BUTTON_MASK_A; - if (SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_B)) controller_state |= BUTTON_MASK_B; - if (SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_BACK)) controller_state |= BUTTON_MASK_SELECT; - if (SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_START)) controller_state |= BUTTON_MASK_START; - } - - GB_set_key_state(gb, GB_KEY_RIGHT, (pressed_buttons & BUTTON_MASK_RIGHT) | (controller_state & BUTTON_MASK_RIGHT)); - GB_set_key_state(gb, GB_KEY_LEFT, (pressed_buttons & BUTTON_MASK_LEFT) | (controller_state & BUTTON_MASK_LEFT)); - GB_set_key_state(gb, GB_KEY_UP, (pressed_buttons & BUTTON_MASK_UP) | (controller_state & BUTTON_MASK_UP)); - GB_set_key_state(gb, GB_KEY_DOWN, (pressed_buttons & BUTTON_MASK_DOWN) | (controller_state & BUTTON_MASK_DOWN)); - GB_set_key_state(gb, GB_KEY_A, (pressed_buttons & BUTTON_MASK_A) | (controller_state & BUTTON_MASK_A)); - GB_set_key_state(gb, GB_KEY_B, (pressed_buttons & BUTTON_MASK_B) | (controller_state & BUTTON_MASK_B)); - GB_set_key_state(gb, GB_KEY_SELECT, (pressed_buttons & BUTTON_MASK_SELECT) | (controller_state & BUTTON_MASK_SELECT)); - GB_set_key_state(gb, GB_KEY_START, (pressed_buttons & BUTTON_MASK_START) | (controller_state & BUTTON_MASK_START)); +G_MODULE_EXPORT void on_frame_blending_changed(GtkWidget *w, gpointer user_data_ptr) { + GtkComboBox *box = GTK_COMBO_BOX(w); + config.frame_blending_mode = (gchar *)gtk_combo_box_get_active_id(box); } -static uint32_t convert_color(uint16_t color) { - const uint8_t r = ((uint16_t)(color & 0x1F) * 255) / 31; - const uint8_t g = ((uint16_t)((color >> 5) & 0x1F) * 255) / 31; - const uint8_t b = ((uint16_t)((color >> 10) & 0x1F) * 255) / 31; +G_MODULE_EXPORT void on_display_border_changed(GtkWidget *w, gpointer user_data_ptr) { + GtkComboBox *box = GTK_COMBO_BOX(w); + config.display_border_mode = (gchar *)gtk_combo_box_get_active_id(box); - return (r << 16) | (g << 8) | b; + border_mode_changed = true; } -static void palette_color_data_func(GtkTreeViewColumn *col, GtkCellRenderer *renderer, GtkTreeModel *model, GtkTreeIter *iter, gpointer user_data) { - const gchar *title = gtk_tree_view_column_get_title(col); - const uint8_t color_index = g_ascii_strtoll(&title[6], NULL, 10); - const uint8_t column_index = 2 + (2 * color_index); - - GValue color_val = G_VALUE_INIT; - gtk_tree_model_get_value(model, iter, column_index, &color_val); - gint color = g_value_get_int(&color_val); - gchar *color_string = g_strdup_printf("#%06x", color); - - gint lightness = 0.299 * ((color >> 16) & 0xFF) + 0.587 * ((color >> 8) & 0xFF) + 0.114 * (color & 0xFF); - - GValue color_str = G_VALUE_INIT; - g_value_init(&color_str, G_TYPE_STRING); - g_value_set_string(&color_str, color_string); - g_object_set_property(G_OBJECT(renderer), "background", &color_str); - - GValue fg_color_str = G_VALUE_INIT; - g_value_init(&fg_color_str, G_TYPE_STRING); - g_value_set_static_string(&fg_color_str, (lightness > 0x7F)? "#000000" : "#FFFFFF"); - g_object_set_property(G_OBJECT(renderer), "foreground", &fg_color_str); - - g_value_unset(&color_val); - g_value_unset(&color_str); - g_value_unset(&fg_color_str); - g_free(color_string); -} - -static void on_vblank(gpointer data) { - if (!vram_viewer_updating && vram_viewer_visible) { - vram_viewer_updating = true; - - if (g_strcmp0("vram_viewer_sprites", vram_viewer_active_tab) == 0) { - GtkTreeIter iter; - GtkTreeView *tree_view = builder_get(GTK_TREE_VIEW, "vram_viewer_sprites"); - // gtk_tree_view_set_model(tree_view, NULL); // Do we need this? - - GtkListStore *store = gtk_list_store_new(7, - GDK_TYPE_PIXBUF, // Preview image - G_TYPE_STRING, // X position - G_TYPE_STRING, // Y position - G_TYPE_STRING, // Tile - G_TYPE_STRING, // Tile Address - G_TYPE_STRING, // OAM Address - G_TYPE_STRING // Attributes - ); - - gtk_tree_model_get_iter_first(GTK_TREE_MODEL(store), &iter); - - for (unsigned row = 0; row < oamCount; ++row) { - GdkPixbuf *pixbuf = gdk_pixbuf_new_from_bytes( - g_bytes_new(oamInfo[row].image, 128 * sizeof(uint32_t)), - GDK_COLORSPACE_RGB, true, 8, 8, oamHeight, 8 * sizeof(uint32_t) - ); - - GdkPixbuf *dest = gdk_pixbuf_new(GDK_COLORSPACE_RGB, true, 8, 8 * 2, oamHeight * 2); - - gdk_pixbuf_scale(pixbuf, dest, - 0, 0, 8 * 2, oamHeight * 2, - 0, 0, 2.0, 2.0, - GDK_INTERP_NEAREST - ); - - gtk_list_store_insert_with_values(store, &iter, -1, - 0, dest, - 1, itoa(oamInfo[row].x - 8), - 2, itoa(oamInfo[row].y - 16), - 3, g_strdup_printf("$%02x", oamInfo[row].tile), - 4, g_strdup_printf("$%04x", 0x8000 + oamInfo[row].tile * 0x10), - 5, g_strdup_printf("$%04x", oamInfo[row].oam_addr), - 6, vram_viewer_is_cgb - ? g_strdup_printf("%c%c%c%d%d", - oamInfo[row].flags & 0x80? 'P' : '-', - oamInfo[row].flags & 0x40? 'Y' : '-', - oamInfo[row].flags & 0x20? 'X' : '-', - oamInfo[row].flags & 0x08? 1 : 0, - oamInfo[row].flags & 0x07) - : g_strdup_printf("%c%c%c%d", - oamInfo[row].flags & 0x80? 'P' : '-', - oamInfo[row].flags & 0x40? 'Y' : '-', - oamInfo[row].flags & 0x20? 'X' : '-', - oamInfo[row].flags & 0x10? 1 : 0), - -1 - ); - - g_object_unref(pixbuf); - g_object_unref(dest); - } - - gtk_tree_view_set_model(tree_view, GTK_TREE_MODEL(store)); - g_object_unref(store); - } - else if (g_strcmp0("vram_viewer_palettes", vram_viewer_active_tab) == 0) { - GtkTreeIter iter; - GtkTreeView *tree_view = builder_get(GTK_TREE_VIEW, "vram_viewer_palettes"); - // gtk_tree_view_set_model(tree_view, NULL); // Do we need this? - - GtkListStore *store = gtk_list_store_new(9, - G_TYPE_STRING, // Name - - G_TYPE_STRING, // Color 0 string - G_TYPE_INT, // Color 0 integer - - G_TYPE_STRING, // Color 1 string - G_TYPE_INT, // Color 1 integer - - G_TYPE_STRING, // Color 2 string - G_TYPE_INT, // Color 2 integer - - G_TYPE_STRING, // Color 3 string - G_TYPE_INT // Color 3 integer - ); - - gtk_tree_model_get_iter_first(GTK_TREE_MODEL(store), &iter); - - for (unsigned row = 0; row < 16; ++row) { - uint8_t offset = (row & 7) * 4; - - uint16_t color_0 = (vram_viewer_palette_data[row][((0 + offset) << 1) + 1] << 8) | vram_viewer_palette_data[row][((0 + offset) << 1)]; - uint16_t color_1 = (vram_viewer_palette_data[row][((1 + offset) << 1) + 1] << 8) | vram_viewer_palette_data[row][((1 + offset) << 1)]; - uint16_t color_2 = (vram_viewer_palette_data[row][((2 + offset) << 1) + 1] << 8) | vram_viewer_palette_data[row][((2 + offset) << 1)]; - uint16_t color_3 = (vram_viewer_palette_data[row][((3 + offset) << 1) + 1] << 8) | vram_viewer_palette_data[row][((3 + offset) << 1)]; - - gtk_list_store_insert_with_values(store, &iter, -1, - 0, g_strdup_printf("%s %d", row >=8 ? "Object" : "Background", row & 7), - 1, g_strdup_printf("$%04x", color_0 & 0x7FFF), - 2, convert_color(color_0), - 3, g_strdup_printf("$%04x", color_1 & 0x7FFF), - 4, convert_color(color_1), - 5, g_strdup_printf("$%04x", color_2 & 0x7FFF), - 6, convert_color(color_2), - 7, g_strdup_printf("$%04x", color_3 & 0x7FFF), - 8, convert_color(color_3), - -1 - ); - } - - GtkTreeViewColumn *column_0 = gtk_tree_view_get_column(tree_view, 1); - GtkTreeViewColumn *column_1 = gtk_tree_view_get_column(tree_view, 2); - GtkTreeViewColumn *column_2 = gtk_tree_view_get_column(tree_view, 3); - GtkTreeViewColumn *column_3 = gtk_tree_view_get_column(tree_view, 4); - - GtkCellRendererText *cell_renderer_0 = builder_get(GTK_CELL_RENDERER_TEXT, "vram_viewer_palette_cell_renderer_0"); - GtkCellRendererText *cell_renderer_1 = builder_get(GTK_CELL_RENDERER_TEXT, "vram_viewer_palette_cell_renderer_1"); - GtkCellRendererText *cell_renderer_2 = builder_get(GTK_CELL_RENDERER_TEXT, "vram_viewer_palette_cell_renderer_2"); - GtkCellRendererText *cell_renderer_3 = builder_get(GTK_CELL_RENDERER_TEXT, "vram_viewer_palette_cell_renderer_3"); - - gtk_tree_view_column_set_cell_data_func(column_0, GTK_CELL_RENDERER(cell_renderer_0), palette_color_data_func, NULL, NULL); - gtk_tree_view_column_set_cell_data_func(column_1, GTK_CELL_RENDERER(cell_renderer_1), palette_color_data_func, NULL, NULL); - gtk_tree_view_column_set_cell_data_func(column_2, GTK_CELL_RENDERER(cell_renderer_2), palette_color_data_func, NULL, NULL); - gtk_tree_view_column_set_cell_data_func(column_3, GTK_CELL_RENDERER(cell_renderer_3), palette_color_data_func, NULL, NULL); - - gtk_tree_view_set_model(tree_view, GTK_TREE_MODEL(store)); - g_object_unref(store); - } - - // Queue a redraw of the VRAM viewer - gtk_widget_queue_draw(GTK_WIDGET(vram_viewer)); - - vram_viewer_updating = false; - } - - // 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); - } -} - -static void vblank(GB_gameboy_t *gb) { - flip(); - - if (border_mode_changed) { - GB_set_border_mode(gb, get_display_border_mode()); - update_window_geometry(); - - border_mode_changed = false; - } - - GB_set_pixels_output(gb, get_pixels()); - - if (underclock_down && clock_mutliplier > 0.5) { - clock_mutliplier -= 1.0/16; - GB_set_clock_multiplier(gb, clock_mutliplier); - } - else if (!underclock_down && clock_mutliplier < 1.0) { - clock_mutliplier += 1.0/16; - GB_set_clock_multiplier(gb, clock_mutliplier); - } - - if (g_strcmp0("vram_viewer_tileset", vram_viewer_active_tab) == 0) { - const gchar *palette_id = gtk_combo_box_get_active_id(builder_get(GTK_COMBO_BOX, "vram_viewer_tileset_palette_selector")); - - GB_palette_type_t palette_type = g_str_has_prefix(palette_id, "bg")? GB_PALETTE_BACKGROUND : GB_PALETTE_OAM; - uint8_t palette_index = g_ascii_digit_value(palette_id[palette_type == GB_PALETTE_OAM ? 3 : 2]); - - GB_draw_tileset(gb, tileset_buffer, - palette_type, - palette_index - ); - } - else if (g_strcmp0("vram_viewer_tilemap", vram_viewer_active_tab) == 0) { - const gchar *palette_id = gtk_combo_box_get_active_id(builder_get(GTK_COMBO_BOX, "vram_viewer_tilemap_palette_selector")); - uint8_t palette_index = 0; - GB_palette_type_t palette_type = GB_PALETTE_AUTO; - - if (g_strcmp0("auto", palette_id) != 0) { - palette_type = g_str_has_prefix(palette_id, "bg")? GB_PALETTE_BACKGROUND : GB_PALETTE_OAM; - palette_index = g_ascii_digit_value(palette_id[palette_type == GB_PALETTE_OAM ? 3 : 2]); - } - - GB_map_type_t map_type = GB_MAP_AUTO; - const gchar *map_type_id = gtk_combo_box_get_active_id(builder_get(GTK_COMBO_BOX, "vram_viewer_tilemap_tilemap_selector")); - if (g_strcmp0("auto", map_type_id) != 0) { - map_type = (g_strcmp0("9800", map_type_id) == 0)? GB_MAP_9800 : GB_MAP_9C00; - } - - GB_tileset_type_t tileset_type = GB_TILESET_AUTO; - const gchar *tileset_type_id = gtk_combo_box_get_active_id(builder_get(GTK_COMBO_BOX, "vram_viewer_tilemap_tileset_selector")); - if (g_strcmp0("auto", tileset_type_id) != 0) { - tileset_type = (g_strcmp0("8800", tileset_type_id) == 0)? GB_TILESET_8800 : GB_TILESET_8000; - } - - GB_draw_tilemap(gb, tilemap_buffer, - palette_type, - palette_index, - map_type, - tileset_type - ); - - scrollRect = (Rect){ - GB_read_memory(gb, 0xFF00 | GB_IO_SCX), - GB_read_memory(gb, 0xFF00 | GB_IO_SCY), - 160, 144 - }; - } - else if (g_strcmp0("vram_viewer_sprites", vram_viewer_active_tab) == 0) { - oamCount = GB_get_oam_info(gb, oamInfo, &oamHeight); - vram_viewer_is_cgb = GB_is_cgb(gb); - } - else if (g_strcmp0("vram_viewer_palettes", vram_viewer_active_tab) == 0) { - size_t size; - - for (unsigned row = 0; row < 16; ++row) { - uint8_t *palette_data = GB_get_direct_access(gb, row >= 8? GB_DIRECT_ACCESS_OBP : GB_DIRECT_ACCESS_BGP, &size, NULL); - memcpy(vram_viewer_palette_data[row], palette_data, size); - } - } - - do_rewind = rewind_down; - - g_idle_add((GSourceFunc) on_vblank, NULL); -} - -static void run(GuiData *gui_data) { - if (running) return; - while (stopping); - - g_thread_new("CoreLoop", run_thread, gui_data); -} - -static gpointer run_thread(gpointer gui_data_gptr) { - GuiData *gui_data = gui_data_gptr; - - if (!gui_data->file) return NULL; - - if (gui_data->stopped) { - start(gui_data); - } - else { - init(gui_data); - reset(gui_data); - start(gui_data); - } - - return NULL; -} - -static void init(GuiData *gui_data) { - if (GB_is_inited(&gb)) return; - - GB_init(&gb, get_model()); - - GB_set_vblank_callback(&gb, vblank); - GB_set_pixels_output(&gb, get_current_buffer()); - GB_set_rgb_encode_callback(&gb, rgb_encode); - GB_set_sample_rate(&gb, GB_audio_get_sample_rate()); - GB_set_color_correction_mode(&gb, get_color_correction_mode()); - GB_set_highpass_filter_mode(&gb, get_highpass_mode()); - GB_set_rewind_length(&gb, config.rewind_duration); - GB_set_update_input_hint_callback(&gb, handle_events); - GB_apu_set_sample_callback(&gb, gb_audio_callback); - GB_set_input_callback(&gb, sync_console_input); - GB_set_async_input_callback(&gb, async_console_input); - GB_set_log_callback(&gb, console_log); - GB_set_boot_rom_load_callback(&gb, load_boot_rom); - - if (get_display_border_mode() <= GB_BORDER_ALWAYS) { - GB_set_border_mode(&gb, get_display_border_mode()); - } - - update_window_geometry(); -} - -static void load_boot_rom(GB_gameboy_t *gb, GB_boot_rom_t type) { - GError *error = NULL; - char *boot_rom_path = NULL; - GBytes *boot_rom_f = NULL; - const guchar *boot_rom_data; - gsize boot_rom_size; - - static const char *const names[] = { - [GB_BOOT_ROM_DMG0] = "dmg0_boot.bin", - [GB_BOOT_ROM_DMG] = "dmg_boot.bin", - [GB_BOOT_ROM_MGB] = "mgb_boot.bin", - [GB_BOOT_ROM_SGB] = "sgb_boot.bin", - [GB_BOOT_ROM_SGB2] = "sgb2_boot.bin", - [GB_BOOT_ROM_CGB0] = "cgb0_boot.bin", - [GB_BOOT_ROM_CGB] = "cgb_boot.bin", - [GB_BOOT_ROM_AGB] = "agb_boot.bin", - }; - - const char *const boot_rom_name = names[type]; - - if (gui_data.cli_options.boot_rom_path != NULL) { - g_message("[CLI override] Trying to load boot ROM from %s", gui_data.cli_options.boot_rom_path); - if (GB_load_boot_rom(gb, gui_data.cli_options.boot_rom_path)) { - g_warning("Falling back to boot ROM from config"); - goto config_boot_rom; - } - } - else { config_boot_rom: - if (config.boot_rom_path != NULL && g_strcmp0(config.boot_rom_path, "other") != 0 && g_strcmp0(config.boot_rom_path, "auto") != 0) { - boot_rom_path = g_build_filename(config.boot_rom_path, boot_rom_name, NULL); - g_message("Trying to load boot ROM from %s", boot_rom_path); - - if (GB_load_boot_rom(gb, boot_rom_path)) { - g_free(boot_rom_path); - g_warning("Falling back to internal boot ROM"); - goto internal_boot_rom; - } - - g_free(boot_rom_path); - } - else { internal_boot_rom: - boot_rom_path = g_build_filename(RESOURCE_PREFIX "bootroms/", boot_rom_name, NULL); - boot_rom_f = g_resources_lookup_data(boot_rom_path, G_RESOURCE_LOOKUP_FLAGS_NONE, &error); - - g_message("Loading internal boot ROM: %s", boot_rom_path); - g_free(boot_rom_path); - - if (boot_rom_f == NULL) { - g_warning("Failed to load internal boot ROM: %s", boot_rom_path); - g_error_free(error); - exit(EXIT_FAILURE); - } - - boot_rom_data = g_bytes_get_data(boot_rom_f, &boot_rom_size); - GB_load_boot_rom_from_buffer(gb, boot_rom_data, boot_rom_size); - - g_bytes_unref(boot_rom_f); - } - } -} - -static void stop(GuiData *gui_data) { - if (!running) return; - - GB_audio_set_paused(true); - GB_debugger_set_disabled(&gb, true); - - if (GB_debugger_is_stopped(&gb)) { - // [self interruptDebugInputRead]; - } - - stopping = true; - running = false; - while (stopping); - - GB_debugger_set_disabled(&gb, false); - gui_data->stopped = true; -} - -static void reset(GuiData *gui_data) { - g_debug("Reset: %d == %d", get_model(), gui_data->prev_model); - GB_model_t current_model = get_model(); - - if (gui_data->prev_model == -1 || gui_data->prev_model == current_model) { - GB_reset(&gb); - } - else { - GB_switch_model_and_reset(&gb, current_model); - } +G_MODULE_EXPORT void on_monochrome_palette_changed(GtkWidget *w, gpointer user_data_ptr) { + GtkComboBox *box = GTK_COMBO_BOX(w); + config.monochrome_palette_id = (gchar *)gtk_combo_box_get_active_id(box); GB_set_palette(&gb, get_monochrome_palette()); - - gui_data->prev_model = get_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(); - } - - bool success = false; - if (gui_data->file) { - char *path = g_file_get_path(gui_data->file); - - if (GB_load_rom(&gb, path) == 0) { - success = true; - } - else { - g_warning("Failed to load ROM: %s", path); - } - - g_free(path); - } - - action_set_enabled(main_application, "close", success); - action_entries_set_enabled(emulation_entries, G_N_ELEMENTS(emulation_entries), success); } -static void start(GuiData *gui_data) { - running = true; - gui_data->stopped = false; +G_MODULE_EXPORT void on_color_menubar_override_changed(GtkWidget *w, gpointer user_data_ptr) { + config.menubar_override = (gchar *)gtk_combo_box_get_active_id(GTK_COMBO_BOX(w)); +} - GB_audio_clear_queue(); - GB_audio_set_paused(false); +G_MODULE_EXPORT void on_dmg_model_changed(GtkWidget *w, gpointer user_data_ptr) { + GtkComboBox *box = GTK_COMBO_BOX(w); + config.dmg_revision_name = (gchar *)gtk_combo_box_get_active_id(box); +} - /* Run emulation */ - while (running) { - if (rewind_paused) { - handle_events(&gb); - g_usleep(G_USEC_PER_SEC / 8); - } - 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); - } +G_MODULE_EXPORT void on_graphic_filter_changed(GtkWidget *w, gpointer user_data_ptr) { + GtkComboBox *box = GTK_COMBO_BOX(w); + config.shader = (gchar *)gtk_combo_box_get_active_id(box); + + free_shader(&shader); + init_shader_with_name(&shader, config.shader); +} + +G_MODULE_EXPORT void on_highpass_filter_changed(GtkWidget *w, gpointer user_data_ptr) { + config.high_pass_filter_id = (gchar *)gtk_combo_box_get_active_id(GTK_COMBO_BOX(w)); + + if (GB_is_inited(&gb)) { + GB_set_highpass_filter_mode(&gb, get_highpass_mode()); + } +} + +G_MODULE_EXPORT void on_keep_aspect_ratio_changed(GtkWidget *w, gpointer user_data_ptr) { + GtkCheckButton *button = GTK_CHECK_BUTTON(w); + gboolean value = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(button)); + config.keep_aspect_ratio = value; + update_viewport(); +} + +G_MODULE_EXPORT void on_rewind_duration_changed(GtkWidget *w, gpointer user_data_ptr) { + GtkComboBox *box = GTK_COMBO_BOX(w); + config.rewind_duration = g_ascii_strtoll(gtk_combo_box_get_active_id(box), NULL, 10); + GB_set_rewind_length(&gb, config.rewind_duration); +} + +G_MODULE_EXPORT void on_sample_rate_changed(GtkWidget *w, gpointer user_data_ptr) { + GtkComboBox *box = GTK_COMBO_BOX(w); + config.sample_rate = g_ascii_strtoll(gtk_combo_box_get_active_id(box), NULL, 10); + + if (config.sample_rate == -1) { + gui_data.sample_rate = GB_audio_default_sample_rate(); + } + else { + gui_data.sample_rate = config.sample_rate; } - stopping = false; + init_audio(); +} + +G_MODULE_EXPORT void on_sgb_model_changed(GtkWidget *w, gpointer user_data_ptr) { + GtkComboBox *box = GTK_COMBO_BOX(w); + config.sgb_revision_name = (gchar *)gtk_combo_box_get_active_id(box); +} + +G_MODULE_EXPORT void on_use_integer_scaling_changed(GtkWidget *w, gpointer user_data_ptr) { + GtkCheckButton *button = GTK_CHECK_BUTTON(w); + gboolean value = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(button)); + config.use_integer_scaling = value; + update_viewport(); +} + +G_MODULE_EXPORT void console_on_enter(GtkWidget *w, gpointer user_data_ptr) { + GtkEntry *input = GTK_ENTRY(w); + const gchar *_text = gtk_entry_get_text(input); + gchar *text = g_strdup(_text); + + if (g_strcmp0("", text) == 0 && g_strcmp0("", last_console_input) < 0) { + text = g_strdup(last_console_input); + } + else if (text) { + if (last_console_input != NULL) g_free(last_console_input); + last_console_input = g_strdup(text); + } + + if (!in_sync_input) { + console_log(&gb, "> ", 0); + } + + console_log(&gb, text, 0); + console_log(&gb, "\n", 0); + + g_mutex_lock(&debugger_input_mutex); + g_ptr_array_add(debugger_input_queue, (gpointer)text); + g_cond_signal(&debugger_input_cond); + g_mutex_unlock(&debugger_input_mutex); + + // clear input + gtk_entry_set_text(input, ""); +} + +int main(int argc, char *argv[]) { + main_thread = g_thread_self(); + + // initialize GB_model_t to invalid value + gui_data.cli_options.model = -1; + gui_data.prev_model = -1; + + // 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, &gui_data.cli_options.boot_rom_path, "Path to the boot ROM to use", "" }, + { "model", 'm', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, NULL, "Override the model type to emulate", "" }, + { "config", 'c', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &gui_data.cli_options.config_path, "Override the path of the configuration file", "" }, + { NULL } + }; + // Setup our command line information + g_application_add_main_option_entries(G_APPLICATION(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), NULL); + g_signal_connect(main_application, "startup", G_CALLBACK(startup), NULL); + g_signal_connect(main_application, "activate", G_CALLBACK(activate), NULL); + g_signal_connect(main_application, "open", G_CALLBACK(open), NULL); + g_signal_connect(main_application, "shutdown", G_CALLBACK(shutdown), NULL); + + // 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 deleted file mode 100644 index 6775440..0000000 --- a/gtk3/main.h +++ /dev/null @@ -1,177 +0,0 @@ -#ifndef main_h -#define main_h - -// used for audio and game controllers -#include "SDL.h" - -#define G_LOG_USE_STRUCTURED - -#include -#include -#include -#include -#include -#include -#include - -#include "macros.h" -#include "settings.h" -#include "shader.h" - -typedef struct GuiData { - struct CliOptionData { - gchar *config_path; - gchar *boot_rom_path; - gboolean fullscreen; - GB_model_t model; - gchar *prefix; - } cli_options; - - GFile *file; - gint sample_rate; - - bool stopped; - GB_model_t prev_model; -} GuiData; - -typedef struct{ - int16_t x, y; - uint16_t w, h; -} Rect; - -typedef struct LogData { - GB_gameboy_t *gb; - const char *string; - GB_log_attributes attributes; -} LogData; - -#define JOYSTICK_HIGH 0x4000 -#define JOYSTICK_LOW 0x3800 - -#define BUTTON_MASK_A 0x01 -#define BUTTON_MASK_B 0x02 -#define BUTTON_MASK_START 0x04 -#define BUTTON_MASK_SELECT 0x08 -#define BUTTON_MASK_UP 0x10 -#define BUTTON_MASK_DOWN 0x20 -#define BUTTON_MASK_LEFT 0x40 -#define BUTTON_MASK_RIGHT 0x80 - -int main(int argc, char *argv[]); - -static gint handle_local_options(GApplication *app, GVariantDict *options, gpointer user_data_gptr); - -// GtkGlArea crash workaround -gboolean test_gl_support(void); -void gl_check_realize(GtkWidget *w, gpointer user_data_gptr); - -static gboolean init_controllers(); -static gboolean init_audio(); -static void gb_audio_callback(GB_gameboy_t *gb, GB_sample_t *sample); -static char *sync_console_input(GB_gameboy_t *gb); -static char *async_console_input(GB_gameboy_t *gb); -static void update_debugger_sidebar(GB_gameboy_t *gb); -static void append_pending_output(); -static void on_console_log(gpointer user_data_gptr); -static void console_log(GB_gameboy_t *gb, const char *string, GB_log_attributes attributes); - -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); - -static void quit_interrupt(int ignored); - -// 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(); -static gboolean on_key_press(GtkWidget *w, GdkEventKey *event, gpointer data); -static void on_window_state_change(GtkWidget *w, GdkEventWindowState *event, gpointer data); - -// App actions -static void activate_about(GSimpleAction *action, GVariant *parameter, gpointer app); -static void activate_show_console(GSimpleAction *action, GVariant *parameter, gpointer app); -static void activate_open_gtk_debugger(GSimpleAction *action, GVariant *parameter, gpointer app); -static void activate_open_memory_viewer(GSimpleAction *action, GVariant *parameter, gpointer app); -static void activate_open_vram_viewer(GSimpleAction *action, GVariant *parameter, gpointer app); -static void activate_open(GSimpleAction *action, GVariant *parameter, gpointer app); -static void activate_close(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); -static void activate_reset(GSimpleAction *action, GVariant *parameter, gpointer app); -static void activate_clear_console(GSimpleAction *action, GVariant *parameter, gpointer app); - -static void on_model_changed(GSimpleAction *action, GVariant *value, gpointer user_data); -static void on_mute_changed(GSimpleAction *action, GVariant *value, gpointer user_data); -static void on_pause_changed(GSimpleAction *action, GVariant *value, gpointer user_data); - -static void on_quit(GtkWidget *w, 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_tileset(GtkWidget *widget, cairo_t *cr, gpointer data); -static gboolean on_draw_vram_viewer_tilemap(GtkWidget *widget, cairo_t *cr, gpointer data); -static gboolean on_motion_vram_viewer_tileset(GtkWidget *widget, GdkEventMotion *event); -static gboolean on_motion_vram_viewer_tilemap(GtkWidget *widget, GdkEventMotion *event); -static void on_vram_tab_change(GtkWidget *widget, GParamSpec *pspec, GtkStackSwitcher *self); - -// 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_frame_blending_changed(GtkWidget *w, gpointer user_data_gptr); -G_MODULE_EXPORT void on_display_border_changed(GtkWidget *w, gpointer user_data_gptr); -G_MODULE_EXPORT void on_monochrome_palette_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); -G_MODULE_EXPORT void console_on_enter(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 void render_texture(void *pixels, void *previous); -static void update_viewport(void); -static void update_window_geometry(); - -static void run(GuiData *gui_data); -static gpointer run_thread(gpointer user_data_gptr); -static void init(GuiData *gui_data); -static void load_boot_rom(GB_gameboy_t *gb, GB_boot_rom_t type); - -static void handle_events(GB_gameboy_t *gb); -static uint32_t convert_color(uint16_t color); -static void palette_color_data_func(GtkTreeViewColumn *col, GtkCellRenderer *renderer, GtkTreeModel *model, GtkTreeIter *iter, gpointer user_data); -static void on_vblank(gpointer data); -static void vblank(GB_gameboy_t *gb); - -static void reset(GuiData *gui_data); -static void stop(GuiData *gui_data); -static void start(GuiData *gui_data); - -#endif /* main_h */