SameBoy/gtk3/main.c

2496 lines
82 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#define G_LOG_USE_STRUCTURED
#include <gtk/gtk.h>
#include <epoxy/gl.h>
#include <signal.h>
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
// TODO: right now we need access to GB_debugger_add_symbol
#define GB_INTERNAL
#include <Core/gb.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
#define tileset_buffer_length 256 * 192 * 4
#define tilemap_buffer_length 256 * 256 * 4
#define str(x) #x
#define xstr(x) str(x)
#define get_object(id) gtk_builder_get_object(gui_data.builder, id)
#define builder_get(type, id) type(get_object(id))
#define action_set_enabled(map, name, value) g_simple_action_set_enabled(G_SIMPLE_ACTION(g_action_map_lookup_action(G_ACTION_MAP(map), name)), value);
typedef struct{
int16_t x, y;
uint16_t w, h;
} Rect;
typedef struct GuiData {
struct CliOptionData {
gchar *config_path;
gchar *boot_rom_path;
gchar *prefix;
gboolean fullscreen;
GB_model_t model;
} cli_options;
GFile *file;
gint sample_rate;
char *battery_save_path;
char *cheats_save_path;
GB_model_t prev_model;
const GThread *main_thread;
volatile bool running;
volatile bool stopping;
volatile bool stopped;
// Input
SDL_GameController *controller;
uint8_t pressed_buttons;
// GTK pointers
GtkApplication *main_application;
GtkBuilder *builder;
GtkApplicationWindow *main_window;
GtkBox *main_window_container;
GtkGLArea *gl_area;
GtkDrawingArea *fallback_canvas;
GtkWindow *preferences;
GtkWindow *vram_viewer;
GtkWindow *memory_viewer;
GtkWindow *console;
GtkWindow *printer;
// Debugger state
GtkTextBuffer *pending_console_output;
gboolean in_sync_input;
gchar *last_console_input;
gboolean log_to_sidebar;
gboolean should_clear_sidebar;
GMutex debugger_input_mutex;
GCond debugger_input_cond;
GRecMutex console_output_lock;
GPtrArray *debugger_input_queue;
bool vram_viewer_visible;
bool vram_viewer_updating;
gchar *vram_viewer_active_tab;
gboolean vram_viewer_is_cgb;
uint8_t vram_viewer_palette_data[16][0x40];
GB_oam_info_t oam_info[40];
uint16_t oam_count;
uint8_t oam_height;
uint32_t tileset_buffer[tileset_buffer_length];
uint32_t tilemap_buffer[tilemap_buffer_length];
GMutex tileset_buffer_mutex;
GMutex tilemap_buffer_mutex;
Rect scroll_rect;
// Audio and video
bool audio_initialized;
uint32_t *image_buffers[3];
unsigned char current_buffer;
Rect viewport;
bool border_mode_changed;
bool is_fullscreen;
bool supports_gl;
shader_t shader;
// Fast forward / slow motion
bool underclock_down;
bool rewind_down;
bool do_rewind;
bool rewind_paused;
bool turbo_down;
double clock_mutliplier;
} GuiData;
// Initialize the GuiData
static GuiData gui_data = {
.cli_options = {
.fullscreen = false,
.model = -1,
},
.prev_model = -1,
.running = false,
.stopping = false,
.stopped = false,
.in_sync_input = false,
.log_to_sidebar = false,
.should_clear_sidebar = false,
.vram_viewer_visible = false,
.vram_viewer_updating = false,
.vram_viewer_active_tab = "",
.vram_viewer_is_cgb = false,
.audio_initialized = false,
.border_mode_changed = false,
.underclock_down = false,
.rewind_down = false,
.rewind_paused = false,
.turbo_down = false,
.clock_mutliplier = 1.0,
};
GB_gameboy_t gb;
// Forward declarations of the actions
static void activate_open(GSimpleAction *action, GVariant *parameter, gpointer app);
static void activate_close(GSimpleAction *action, GVariant *parameter, gpointer app);
static void activate_reset(GSimpleAction *action, GVariant *parameter, gpointer app);
static void activate_show_console(GSimpleAction *action, GVariant *parameter, gpointer app);
static void activate_open_gtk_debugger(GSimpleAction *action, GVariant *parameter, gpointer app);
static void activate_open_memory_viewer(GSimpleAction *action, GVariant *parameter, gpointer app);
static void activate_open_vram_viewer(GSimpleAction *action, GVariant *parameter, gpointer app);
static void activate_clear_console(GSimpleAction *action, GVariant *parameter, gpointer app);
static void activate_quit(GSimpleAction *action, GVariant *parameter, gpointer app);
static void activate_about(GSimpleAction *action, GVariant *parameter, gpointer app);
static void activate_preferences(GSimpleAction *action, GVariant *parameter, gpointer app);
static void on_pause_changed(GSimpleAction *action, GVariant *value, gpointer user_data_ptr);
static void on_mute_changed(GSimpleAction *action, GVariant *value, gpointer user_data_ptr);
static void on_model_changed(GSimpleAction *action, GVariant *value, gpointer user_data_ptr);
static const GActionEntry file_entries[] = {
{ "open", activate_open, NULL, NULL, NULL },
{ "close", activate_close, NULL, NULL, NULL },
};
static const GActionEntry edit_entries[] = {
};
static const GActionEntry emulation_entries[] = {
{ "reset", activate_reset, NULL, NULL, NULL },
{ "pause", NULL, NULL, "false", on_pause_changed },
{ "save_state", NULL, NULL, NULL, NULL },
{ "load_state", NULL, NULL, NULL, NULL },
};
static const GActionEntry developer_entries[] = {
{ "show_console", activate_show_console, NULL, NULL, NULL },
{ "open_gtk_debugger", activate_open_gtk_debugger, NULL, NULL, NULL },
{ "open_memory_viewer", activate_open_memory_viewer, NULL, NULL, NULL },
{ "open_vram_viewer", activate_open_vram_viewer, NULL, NULL, NULL },
{ "toggle_developer_mode", NULL, NULL, "false", NULL },
{ "clear_console", activate_clear_console, NULL, NULL, NULL },
};
static const GActionEntry app_entries[] = {
{ "quit", activate_quit, NULL, NULL, NULL },
{ "about", activate_about, NULL, NULL, NULL },
{ "preferences", activate_preferences, NULL, NULL, NULL },
{ "toggle_mute", NULL, NULL, "false", on_mute_changed },
{ "change_model", NULL, "s", "@s 'CGB'", on_model_changed },
};
static void replace_extension(const char *src, size_t length, char *dest, const char *ext) {
memcpy(dest, src, length);
dest[length] = 0;
/* Remove extension */
for (size_t i = length; i--;) {
if (dest[i] == '/') break;
if (dest[i] == '.') {
dest[i] = 0;
break;
}
}
/* Add new extension */
strcat(dest, ext);
}
static uint32_t rgb_encode(GB_gameboy_t *gb, uint8_t r, uint8_t g, uint8_t b) {
return 0xFF000000 | (r << 16) | (g << 8) | b;
}
static uint32_t convert_color(uint16_t color) {
const uint8_t r = ((uint16_t)(color & 0x1F) * 255) / 31;
const uint8_t g = ((uint16_t)((color >> 5) & 0x1F) * 255) / 31;
const uint8_t b = ((uint16_t)((color >> 10) & 0x1F) * 255) / 31;
return (r << 16) | (g << 8) | b;
}
static void palette_color_data_func(GtkTreeViewColumn *col, GtkCellRenderer *renderer, GtkTreeModel *model, GtkTreeIter *iter, gpointer user_data_ptr) {
const gchar *title = gtk_tree_view_column_get_title(col);
const uint8_t color_index = g_ascii_strtoll(&title[6], NULL, 10);
const uint8_t column_index = 2 + (2 * color_index);
GValue color_val = G_VALUE_INIT;
gtk_tree_model_get_value(model, iter, column_index, &color_val);
gint color = g_value_get_int(&color_val);
gchar *color_string = g_strdup_printf("#%06x", color);
gint lightness = 0.299 * ((color >> 16) & 0xFF) + 0.587 * ((color >> 8) & 0xFF) + 0.114 * (color & 0xFF);
GValue color_str = G_VALUE_INIT;
g_value_init(&color_str, G_TYPE_STRING);
g_value_set_string(&color_str, color_string);
g_object_set_property(G_OBJECT(renderer), "background", &color_str);
GValue fg_color_str = G_VALUE_INIT;
g_value_init(&fg_color_str, G_TYPE_STRING);
g_value_set_static_string(&fg_color_str, (lightness > 0x7F)? "#000000" : "#FFFFFF");
g_object_set_property(G_OBJECT(renderer), "foreground", &fg_color_str);
g_value_unset(&color_val);
g_value_unset(&color_str);
g_value_unset(&fg_color_str);
g_free(color_string);
}
// This function gets called after the parsing of the commandline options has occurred.
static gint handle_local_options(GApplication *app, GVariantDict *options, gpointer null_ptr) {
guint32 count;
if (g_variant_dict_lookup(options, "version", "b", &count)) {
g_message("SameBoy v" xstr(VERSION));
return EXIT_SUCCESS;
}
if (g_variant_dict_lookup(options, "fullscreen", "b", &count)) {
gui_data.cli_options.fullscreen = true;
}
// Handle model override
GVariant *model_name_var = g_variant_dict_lookup_value(options, "model", G_VARIANT_TYPE_STRING);
if (model_name_var != NULL) {
const gchar *model_name = g_variant_get_string(model_name_var, NULL);
// TODO: Synchronize with GB_model_t (Core/gb.h)
if (g_str_has_prefix(model_name, "DMG")) {
gui_data.cli_options.prefix = "DMG";
if (g_str_has_suffix(model_name, "-B") || g_strcmp0(model_name, "DMG") == 0) {
gui_data.cli_options.model = GB_MODEL_DMG_B;
}
else {
gui_data.cli_options.model = GB_MODEL_DMG_B;
g_warning("Unsupported revision: %s\nFalling back to DMG-B", model_name);
}
}
else if (g_str_has_prefix(model_name, "SGB")) {
gui_data.cli_options.prefix = "SGB";
if (g_str_has_suffix(model_name, "-NTSC") || g_strcmp0(model_name, "SGB") == 0) {
gui_data.cli_options.model = GB_MODEL_SGB;
}
else if (g_str_has_suffix(model_name, "-PAL")) {
gui_data.cli_options.model = GB_MODEL_SGB | GB_MODEL_PAL_BIT;
}
else if (g_str_has_suffix(model_name, "2")) {
gui_data.cli_options.model = GB_MODEL_SGB2;
}
else {
gui_data.cli_options.model = GB_MODEL_SGB2;
g_warning("Unsupported revision: %s\nFalling back to SGB2", model_name);
}
}
else if (g_str_has_prefix(model_name, "CGB")) {
gui_data.cli_options.prefix = "CGB";
if (g_str_has_suffix(model_name, "-C")) {
gui_data.cli_options.model = GB_MODEL_CGB_C;
}
else if (g_str_has_suffix(model_name, "-E") || g_strcmp0(model_name, "CGB") == 0) {
gui_data.cli_options.model = GB_MODEL_CGB_E;
}
else {
gui_data.cli_options.model = GB_MODEL_CGB_E;
g_warning("Unsupported revision: %s\nFalling back to CGB-E", model_name);
}
}
else if (g_str_has_prefix(model_name, "AGB")) {
gui_data.cli_options.prefix = "AGB";
gui_data.cli_options.model = GB_MODEL_AGB;
}
else {
gui_data.cli_options.prefix = NULL;
g_warning("Unknown model: %s", model_name);
exit(EXIT_FAILURE);
}
}
return -1;
}
// The main function for the OpenGL version check workaround
void gl_check_realize(GtkWidget *w, gpointer user_data_ptr) {
gboolean *result = (gboolean *) user_data_ptr;
GError *error = NULL;
GdkWindow *gdk_window = gtk_widget_get_window(w);
GdkGLContext *context = gdk_window_create_gl_context(gdk_window, &error);
if (error != NULL) {
g_warning("Failed to create context: %s", error->message);
g_error_free(error);
*result = false;
}
else {
gdk_gl_context_make_current(context);
int version = epoxy_gl_version();
g_object_run_dispose(G_OBJECT(context));
g_object_unref(context);
context = NULL;
gdk_gl_context_clear_current();
g_debug("OpenGL version: %d", version);
*result = version >= 32;
}
}
// Workaround to figure out if we have proper OpenGL support.
// Otherwise the application would crash after our GtkGlArea is realized
// and the context it uses is a legacy OpenGL 1.4 context because
// GTK3 calls OpenGL 2.0+ functions on it.
gboolean test_gl_support(void) {
gboolean result = false;
GtkWidget *window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
g_signal_connect(window, "realize", G_CALLBACK(gl_check_realize), &result);
gtk_widget_realize(window);
gtk_widget_destroy(window);
window = NULL;
return result;
}
static gboolean init_controllers(void) {
if (SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) < 0) {
g_warning("Failed to initialize game controller support: %s", SDL_GetError());
return false;
}
SDL_QuitSubSystem(SDL_INIT_EVENTS);
SDL_GameControllerEventState(SDL_IGNORE);
GError *error = NULL;
GBytes *db_f = g_resources_lookup_data(RESOURCE_PREFIX "gamecontrollerdb.txt", G_RESOURCE_LOOKUP_FLAGS_NONE, &error);
if (db_f != NULL) {
gsize db_data_size = 0;
const guchar *db_data = g_bytes_get_data(db_f, &db_data_size);
const gint val = SDL_GameControllerAddMappingsFromRW(SDL_RWFromMem((void *)db_data, db_data_size), 1);
if (val < 0) {
g_warning("Failed to load controller mappings: %s", SDL_GetError());
}
g_bytes_unref(db_f);
}
if (error != NULL) g_clear_error(&error);
// Open the first available controller
for (int i = 0; i < SDL_NumJoysticks(); ++i) {
if (SDL_IsGameController(i)) {
gui_data.controller = SDL_GameControllerOpen(i);
if (gui_data.controller) {
break;
}
else {
g_warning("Could not open gamecontroller %i: %s", i, SDL_GetError());
}
}
}
return true;
}
static gboolean init_audio(void) {
bool audio_playing = GB_audio_is_playing();
if (gui_data.audio_initialized) {
GB_audio_destroy();
gui_data.audio_initialized = false;
}
#ifdef USE_SDL_AUDIO
if (SDL_InitSubSystem(SDL_INIT_AUDIO) < 0) {
g_warning("Failed to initialize audio: %s", SDL_GetError());
return false;
}
#endif
GB_audio_init(gui_data.sample_rate);
GB_set_sample_rate(&gb, GB_audio_get_sample_rate());
// restore playing state
GB_audio_set_paused(!audio_playing);
return gui_data.audio_initialized = true;
}
static GB_model_t get_model(void) {
if (gui_data.cli_options.model != -1) {
return gui_data.cli_options.model;
}
GAction *action = g_action_map_lookup_action(G_ACTION_MAP(gui_data.main_application), "change_model");
GVariant *value = g_action_get_state(action);
const gchar *family = g_variant_get_string(value, NULL);
if (g_strcmp0(family, "DMG") == 0) {
return get_dmg_model();
}
else if (g_strcmp0(family, "AGB") == 0) {
return GB_MODEL_AGB;
}
else if (g_strcmp0(family, "SGB") == 0) {
return get_sgb_model();
}
return get_cgb_model();
}
static void gb_audio_callback(GB_gameboy_t *gb, GB_sample_t *sample) {
if (gui_data.turbo_down) {
static unsigned skip = 0;
skip++;
if (skip == GB_audio_get_sample_rate() / 8) {
skip = 0;
}
if (skip > GB_audio_get_sample_rate() / 16) {
return;
}
}
if (GB_audio_get_queue_length() / sizeof(*sample) > GB_audio_get_sample_rate() / 4) {
return;
}
GB_audio_queue_sample(sample);
}
static void clear_sidebar(void) {
GtkTextView *sidebar_output = builder_get(GTK_TEXT_VIEW, "console_sidebar_output");
GtkTextBuffer *sidebar_output_text_buf = gtk_text_view_get_buffer(sidebar_output);
gtk_text_buffer_set_text(sidebar_output_text_buf, "", -1);
}
static gboolean scroll_to_bottom(GtkTextView *textview, GtkTextMark *mark) {
GtkTextBuffer *buffer = gtk_text_view_get_buffer(textview);
GtkTextIter iter;
gtk_text_buffer_get_end_iter(buffer, &iter);
gtk_text_iter_set_line_offset(&iter, 0);
gtk_text_buffer_move_mark(buffer, mark, &iter);
gtk_text_view_scroll_to_mark(textview, mark, 0.0, true, 0.0, 0.10);
gtk_text_buffer_delete_mark(buffer, mark);
return true;
}
static void append_pending_output(void) {
g_rec_mutex_lock(&gui_data.console_output_lock);
if (gui_data.should_clear_sidebar) {
clear_sidebar();
gui_data.should_clear_sidebar = false;
}
if (gtk_text_buffer_get_char_count(gui_data.pending_console_output) > 0) {
GtkTextView *text_view = builder_get(GTK_TEXT_VIEW, gui_data.log_to_sidebar? "console_sidebar_output" : "console_screen");
GtkTextBuffer *text_buf = gtk_text_view_get_buffer(text_view);
GtkTextIter start;
GtkTextIter end;
gtk_text_buffer_get_start_iter(gui_data.pending_console_output, &start);
gtk_text_buffer_get_end_iter(gui_data.pending_console_output, &end);
GtkTextIter iter;
gtk_text_buffer_get_end_iter(text_buf, &iter);
gtk_text_buffer_insert_range(text_buf, &iter, &start, &end);
scroll_to_bottom(text_view, gtk_text_buffer_create_mark(text_buf, NULL, &iter, true));
gtk_text_buffer_set_text(gui_data.pending_console_output, "", -1);
}
g_rec_mutex_unlock(&gui_data.console_output_lock);
}
static void update_debugger_sidebar(GB_gameboy_t *gb) {
if (!GB_debugger_is_stopped(gb)) {
return;
}
if (gui_data.main_thread != g_thread_self()) {
g_idle_add((GSourceFunc) update_debugger_sidebar, gb);
return;
}
g_rec_mutex_lock(&gui_data.console_output_lock);
gui_data.should_clear_sidebar = true;
append_pending_output();
gui_data.log_to_sidebar = true;
g_rec_mutex_unlock(&gui_data.console_output_lock);
GtkTextView *sidebar_input = builder_get(GTK_TEXT_VIEW, "console_sidebar_input");
GtkTextBuffer *sidebar_input_text_buf = gtk_text_view_get_buffer(sidebar_input);
gint line_count = gtk_text_buffer_get_line_count(sidebar_input_text_buf);
for (unsigned line = 0; line < line_count; ++line) {
GtkTextIter start_iter;
GtkTextIter end_iter;
gunichar ch;
gtk_text_buffer_get_iter_at_line(sidebar_input_text_buf, &start_iter, line);
end_iter = start_iter;
do {
ch = gtk_text_iter_get_char(&end_iter);
if (!gtk_text_iter_forward_char(&end_iter)) {
break;
}
}
while (ch != '\n');
gchar *cmd = gtk_text_buffer_get_text(sidebar_input_text_buf, &start_iter, &end_iter, false);
g_strchug(cmd); // trim leading whitespace
g_strchomp(cmd); // trim trailing whitespace
if (g_strcmp0("", cmd) != 0) {
char *duped = g_strdup(cmd);
GB_attributed_log(gb, GB_LOG_BOLD, "%s:\n", duped);
GB_debugger_execute_command(gb, duped);
GB_log(gb, "\n");
g_free(duped);
}
g_free(cmd);
}
g_rec_mutex_lock(&gui_data.console_output_lock);
append_pending_output();
gui_data.log_to_sidebar = false;
g_rec_mutex_unlock(&gui_data.console_output_lock);
}
static void console_log(GB_gameboy_t *gb, const char *string, GB_log_attributes attributes) {
g_rec_mutex_lock(&gui_data.console_output_lock);
if (string != NULL && !g_str_equal("", string)) {
GtkTextIter iter;
GtkTextIter start;
// Append attributed text to "gui_data.pending_console_output" GtkTextBuffer
gtk_text_buffer_get_end_iter(gui_data.pending_console_output, &iter);
GtkTextMark *start_mark = gtk_text_buffer_create_mark(gui_data.pending_console_output, NULL, &iter, true);
gtk_text_buffer_insert(gui_data.pending_console_output, &iter, g_strdup(string), -1);
gtk_text_buffer_get_iter_at_mark(gui_data.pending_console_output, &start, start_mark);
if (attributes & GB_LOG_BOLD) {
gtk_text_buffer_apply_tag_by_name(gui_data.pending_console_output, "bold", &start, &iter);
}
if (attributes & GB_LOG_DASHED_UNDERLINE) {
gtk_text_buffer_apply_tag_by_name(gui_data.pending_console_output, "dashed_underline", &start, &iter);
}
if (attributes & GB_LOG_UNDERLINE) {
gtk_text_buffer_apply_tag_by_name(gui_data.pending_console_output, "underline", &start, &iter);
}
gtk_text_buffer_delete_mark(gui_data.pending_console_output, start_mark);
g_idle_add((GSourceFunc) append_pending_output, NULL);
}
g_rec_mutex_unlock(&gui_data.console_output_lock);
}
// Console TODO:
// TODO: clear sidebar when switching to async mode
// TODO: Command history (up / down arrow in input)
// TODO: reverse search of commands
// TODO: search in output
static char *sync_console_input(GB_gameboy_t *gb) {
update_debugger_sidebar(gb);
console_log(gb, "> ", 0);
gui_data.in_sync_input = true;
g_mutex_lock(&gui_data.debugger_input_mutex);
g_cond_wait(&gui_data.debugger_input_cond, &gui_data.debugger_input_mutex);
gchar *input = NULL;
const gchar *_input = g_ptr_array_index(gui_data.debugger_input_queue, 0);
input = g_strdup(_input);
gpointer ptr = g_ptr_array_remove_index(gui_data.debugger_input_queue, 0);
if (ptr) g_free(ptr);
g_mutex_unlock(&gui_data.debugger_input_mutex);
gui_data.in_sync_input = false;
return input;
}
static char *async_console_input(GB_gameboy_t *gb) {
// TODO: This is rather ugly
g_idle_add((GSourceFunc) clear_sidebar, NULL);
if (gui_data.debugger_input_queue->len == 0) return NULL;
g_mutex_lock(&gui_data.debugger_input_mutex);
gchar *input = NULL;
const gchar *_input = g_ptr_array_index(gui_data.debugger_input_queue, 0);
if (_input) {
input = g_strdup(_input);
gpointer ptr = g_ptr_array_remove_index(gui_data.debugger_input_queue, 0);
if (ptr) g_free(ptr);
}
g_mutex_unlock(&gui_data.debugger_input_mutex);
return input;
}
// Returns a `GApplication`s `GMenuModel` by ID
// GApplication menus are loaded from `gtk/menus.ui`, `gtk/menus-traditional.ui` and `gtk/menus-common.ui`.
static GMenuModel *get_menu_model(GApplication *app, const char *id) {
GMenu *menu;
menu = gtk_application_get_menu_by_id(GTK_APPLICATION(app), id);
return menu ? G_MENU_MODEL(g_object_ref_sink(menu)) : NULL;
}
// Create our applications menu.
//
// This function tries to stick to the desktop environments conventions.
// For the GNOME Shell it uses a hamburger menu, otherwise it either lets
// the desktop environment shell handle the menu if it signals support for it
// or uses a standard menubar inside the window.
static void setup_menu(GApplication *app) {
GMenuModel *menubar_model = get_menu_model(app, "menubar");
enum menubar_type_t menubar_type = get_show_menubar();
// Try to use a sane default
if (menubar_type == MENUBAR_AUTO) {
GtkSettings *settings = gtk_settings_get_default();
gboolean show_in_shell;
g_object_get(settings, "gtk-shell-shows-menubar", &show_in_shell, NULL);
const gchar *xdg_current_desktop = g_getenv("XDG_CURRENT_DESKTOP");
const gchar *gdm_session = g_getenv("GDMSESSION");
const gchar *desktop_session = g_getenv("DESKTOP_SESSION");
gchar *desktop = (gchar *)xdg_current_desktop;
if (desktop == NULL || g_str_equal(desktop, "")) desktop = (gchar *)gdm_session;
if (desktop == NULL || g_str_equal(desktop, "")) desktop = (gchar *)desktop_session;
g_debug("XDG_CURRENT_DESKTOP: %s\nGDMSESSION: %s\nDESKTOP_SESSION: %s\nChosen value: %s\nShow menu in shell: %d", xdg_current_desktop, gdm_session, desktop_session, desktop, show_in_shell);
if (desktop != NULL && show_in_shell) {
menubar_type = MENUBAR_SHOW_IN_SHELL;
}
else if (desktop != NULL && g_str_match_string("GNOME", desktop, false)) {
if (g_str_match_string("GNOME-Flashback", desktop, false) || g_str_match_string("GNOME-Classic", desktop, false)) {
menubar_type = MENUBAR_SHOW_IN_WINDOW;
}
else if (gdm_session != NULL && (g_str_match_string("gnome-classic", gdm_session, false) || g_str_match_string("gnome-flashback", gdm_session, false))) {
menubar_type = MENUBAR_SHOW_IN_WINDOW;
}
else {
menubar_type = MENUBAR_SHOW_HAMBURGER;
}
}
else {
menubar_type = MENUBAR_SHOW_IN_WINDOW;
}
}
switch (menubar_type) {
case MENUBAR_AUTO:
g_warning("Unreachable");
break;
case MENUBAR_SHOW_IN_SHELL:
g_debug("Showing menu in the shell");
gtk_application_set_menubar(GTK_APPLICATION(app), menubar_model);
break;
case MENUBAR_SHOW_IN_WINDOW: {
g_debug("Showing menu in the window");
GtkMenuBar *menubar = GTK_MENU_BAR(gtk_menu_bar_new_from_model(menubar_model));
gtk_box_pack_start(GTK_BOX(gui_data.main_window_container), GTK_WIDGET(menubar), false, false, 0);
break;
}
case MENUBAR_SHOW_HAMBURGER: {
g_debug("Showing hamburger");
// Attach a custom title bar
GtkWidget *titlebar = builder_get(GTK_WIDGET, "main_header_bar");
gtk_header_bar_set_title(GTK_HEADER_BAR(titlebar), gtk_window_get_title(GTK_WINDOW(gui_data.main_window)));
gtk_window_set_titlebar(GTK_WINDOW(gui_data.main_window), titlebar);
// Disable menubar
gtk_application_set_menubar(GTK_APPLICATION(app), NULL);
// Hook menubar up to the hamburger button
GtkMenuButton *hamburger_button = GTK_MENU_BUTTON(get_object("hamburger_button"));
gtk_menu_button_set_menu_model(hamburger_button, menubar_model);
break;
}
}
}
// Determines if a ComboBox entry should be converted into a separator.
// Each element with a text value of `<separator>` 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("<separator>", text) == 0;
g_free(text);
return result;
}
// Recursively goes through all children of the given container and sets
// our `is_separator` function to all children of type`GtkComboBox`
static void set_combo_box_row_separator_func(GtkContainer *container) {
GList *children = gtk_container_get_children(container);
for (GList *l = children; l; l = l->next) {
if (GTK_IS_COMBO_BOX(l->data)) {
gtk_combo_box_set_row_separator_func(GTK_COMBO_BOX(l->data), is_separator, NULL, NULL);
}
if (GTK_IS_CONTAINER(l->data)) {
set_combo_box_row_separator_func(GTK_CONTAINER(l->data));
}
}
g_list_free(children);
}
// Determines how many frame buffers to use
static unsigned char number_of_buffers(void) {
if (gui_data.fallback_canvas) return 2;
bool should_blend = get_frame_blending_mode() != GB_FRAME_BLENDING_MODE_DISABLED;
return should_blend? 3 : 2;
}
// Returns the buffer that should be used by the Core to render a new frame to
static uint32_t *get_pixels(void) {
return gui_data.image_buffers[(gui_data.current_buffer + 1) % number_of_buffers()];
}
// Returns the current finished frame
static uint32_t *get_current_buffer(void) {
return gui_data.image_buffers[gui_data.current_buffer];
}
// Returns the previous finished frame
static uint32_t *get_previous_buffer(void) {
return gui_data.image_buffers[(gui_data.current_buffer + 2) % number_of_buffers()];
}
// Cycles the buffers
static void flip(void) {
gui_data.current_buffer = (gui_data.current_buffer + 1) % number_of_buffers();
}
// WHY DO WE NEED SUCH AN UGLY METHOD, GTK?!
static void action_entries_set_enabled(const GActionEntry *entries, unsigned n_entries, bool value) {
// Assumes null-terminated if n_entries == -1
for (unsigned i = 0; n_entries == -1 ? entries[i].name != NULL : i < n_entries; i++) {
const GActionEntry *entry = &entries[i];
if (entry->name == NULL) continue;
action_set_enabled(gui_data.main_application, entry->name, value);
}
}
static void update_window_geometry(void) {
// Set size hints
GdkGeometry hints;
hints.min_width = GB_get_screen_width(&gb);
hints.min_height = GB_get_screen_height(&gb);
gtk_window_set_geometry_hints(
GTK_WINDOW(gui_data.main_window),
NULL,
&hints,
(GdkWindowHints)(GDK_HINT_MIN_SIZE)
);
gtk_window_resize(GTK_WINDOW(gui_data.main_window),
GB_get_screen_width(&gb) * 2,
GB_get_screen_height(&gb) * 2
);
// Setup our image buffers
if (gui_data.image_buffers[0]) g_free(gui_data.image_buffers[0]);
if (gui_data.image_buffers[1]) g_free(gui_data.image_buffers[1]);
if (gui_data.image_buffers[2]) g_free(gui_data.image_buffers[2]);
size_t buffer_size = sizeof(gui_data.image_buffers[0][0]) * GB_get_screen_width(&gb) * GB_get_screen_height(&gb);
gui_data.image_buffers[0] = g_malloc0(buffer_size);
gui_data.image_buffers[1] = g_malloc0(buffer_size);
gui_data.image_buffers[2] = g_malloc0(buffer_size);
if (GB_is_inited(&gb)) {
GB_set_pixels_output(&gb, get_pixels());
}
}
static void stop(void) {
if (!gui_data.running) return;
GB_audio_set_paused(true);
GB_debugger_set_disabled(&gb, true);
if (GB_debugger_is_stopped(&gb)) {
// [self interruptDebugInputRead];
}
gui_data.stopping = true;
gui_data.running = false;
while (gui_data.stopping);
GB_debugger_set_disabled(&gb, false);
gui_data.stopped = true;
}
static void on_vblank(gpointer data) {
if (!gui_data.vram_viewer_updating && gui_data.vram_viewer_visible) {
gui_data.vram_viewer_updating = true;
if (g_strcmp0("vram_viewer_sprites", gui_data.vram_viewer_active_tab) == 0) {
GtkTreeIter iter;
GtkTreeView *tree_view = builder_get(GTK_TREE_VIEW, "vram_viewer_sprites");
// gtk_tree_view_set_model(tree_view, NULL); // Do we need this?
GtkListStore *store = gtk_list_store_new(7,
GDK_TYPE_PIXBUF, // Preview image
G_TYPE_STRING, // X position
G_TYPE_STRING, // Y position
G_TYPE_STRING, // Tile
G_TYPE_STRING, // Tile Address
G_TYPE_STRING, // OAM Address
G_TYPE_STRING // Attributes
);
gtk_tree_model_get_iter_first(GTK_TREE_MODEL(store), &iter);
for (unsigned row = 0; row < gui_data.oam_count; ++row) {
GdkPixbuf *pixbuf = gdk_pixbuf_new_from_bytes(
g_bytes_new(gui_data.oam_info[row].image, 128 * sizeof(uint32_t)),
GDK_COLORSPACE_RGB, true, 8, 8, gui_data.oam_height, 8 * sizeof(uint32_t)
);
GdkPixbuf *dest = gdk_pixbuf_new(GDK_COLORSPACE_RGB, true, 8, 8 * 2, gui_data.oam_height * 2);
gdk_pixbuf_scale(pixbuf, dest,
0, 0, 8 * 2, gui_data.oam_height * 2,
0, 0, 2.0, 2.0,
GDK_INTERP_NEAREST
);
gtk_list_store_insert_with_values(store, &iter, -1,
0, dest,
1, g_strdup_printf("%i", gui_data.oam_info[row].x - 8),
2, g_strdup_printf("%i", gui_data.oam_info[row].y - 16),
3, g_strdup_printf("$%02x", gui_data.oam_info[row].tile),
4, g_strdup_printf("$%04x", 0x8000 + gui_data.oam_info[row].tile * 0x10),
5, g_strdup_printf("$%04x", gui_data.oam_info[row].oam_addr),
6, gui_data.vram_viewer_is_cgb
? g_strdup_printf("%c%c%c%d%d",
gui_data.oam_info[row].flags & 0x80? 'P' : '-',
gui_data.oam_info[row].flags & 0x40? 'Y' : '-',
gui_data.oam_info[row].flags & 0x20? 'X' : '-',
gui_data.oam_info[row].flags & 0x08? 1 : 0,
gui_data.oam_info[row].flags & 0x07)
: g_strdup_printf("%c%c%c%d",
gui_data.oam_info[row].flags & 0x80? 'P' : '-',
gui_data.oam_info[row].flags & 0x40? 'Y' : '-',
gui_data.oam_info[row].flags & 0x20? 'X' : '-',
gui_data.oam_info[row].flags & 0x10? 1 : 0),
-1
);
g_object_unref(pixbuf);
g_object_unref(dest);
}
gtk_tree_view_set_model(tree_view, GTK_TREE_MODEL(store));
g_object_unref(store);
}
else if (g_strcmp0("vram_viewer_palettes", gui_data.vram_viewer_active_tab) == 0) {
GtkTreeIter iter;
GtkTreeView *tree_view = builder_get(GTK_TREE_VIEW, "vram_viewer_palettes");
// gtk_tree_view_set_model(tree_view, NULL); // Do we need this?
GtkListStore *store = gtk_list_store_new(9,
G_TYPE_STRING, // Name
G_TYPE_STRING, // Color 0 string
G_TYPE_INT, // Color 0 integer
G_TYPE_STRING, // Color 1 string
G_TYPE_INT, // Color 1 integer
G_TYPE_STRING, // Color 2 string
G_TYPE_INT, // Color 2 integer
G_TYPE_STRING, // Color 3 string
G_TYPE_INT // Color 3 integer
);
gtk_tree_model_get_iter_first(GTK_TREE_MODEL(store), &iter);
for (unsigned row = 0; row < 16; ++row) {
uint8_t offset = (row & 7) * 4;
uint16_t color_0 = (gui_data.vram_viewer_palette_data[row][((0 + offset) << 1) + 1] << 8) | gui_data.vram_viewer_palette_data[row][((0 + offset) << 1)];
uint16_t color_1 = (gui_data.vram_viewer_palette_data[row][((1 + offset) << 1) + 1] << 8) | gui_data.vram_viewer_palette_data[row][((1 + offset) << 1)];
uint16_t color_2 = (gui_data.vram_viewer_palette_data[row][((2 + offset) << 1) + 1] << 8) | gui_data.vram_viewer_palette_data[row][((2 + offset) << 1)];
uint16_t color_3 = (gui_data.vram_viewer_palette_data[row][((3 + offset) << 1) + 1] << 8) | gui_data.vram_viewer_palette_data[row][((3 + offset) << 1)];
gtk_list_store_insert_with_values(store, &iter, -1,
0, g_strdup_printf("%s %d", row >=8 ? "Object" : "Background", row & 7),
1, g_strdup_printf("$%04x", color_0 & 0x7FFF),
2, convert_color(color_0),
3, g_strdup_printf("$%04x", color_1 & 0x7FFF),
4, convert_color(color_1),
5, g_strdup_printf("$%04x", color_2 & 0x7FFF),
6, convert_color(color_2),
7, g_strdup_printf("$%04x", color_3 & 0x7FFF),
8, convert_color(color_3),
-1
);
}
GtkTreeViewColumn *column_0 = gtk_tree_view_get_column(tree_view, 1);
GtkTreeViewColumn *column_1 = gtk_tree_view_get_column(tree_view, 2);
GtkTreeViewColumn *column_2 = gtk_tree_view_get_column(tree_view, 3);
GtkTreeViewColumn *column_3 = gtk_tree_view_get_column(tree_view, 4);
GtkCellRendererText *cell_renderer_0 = builder_get(GTK_CELL_RENDERER_TEXT, "vram_viewer_palette_cell_renderer_0");
GtkCellRendererText *cell_renderer_1 = builder_get(GTK_CELL_RENDERER_TEXT, "vram_viewer_palette_cell_renderer_1");
GtkCellRendererText *cell_renderer_2 = builder_get(GTK_CELL_RENDERER_TEXT, "vram_viewer_palette_cell_renderer_2");
GtkCellRendererText *cell_renderer_3 = builder_get(GTK_CELL_RENDERER_TEXT, "vram_viewer_palette_cell_renderer_3");
gtk_tree_view_column_set_cell_data_func(column_0, GTK_CELL_RENDERER(cell_renderer_0), palette_color_data_func, NULL, NULL);
gtk_tree_view_column_set_cell_data_func(column_1, GTK_CELL_RENDERER(cell_renderer_1), palette_color_data_func, NULL, NULL);
gtk_tree_view_column_set_cell_data_func(column_2, GTK_CELL_RENDERER(cell_renderer_2), palette_color_data_func, NULL, NULL);
gtk_tree_view_column_set_cell_data_func(column_3, GTK_CELL_RENDERER(cell_renderer_3), palette_color_data_func, NULL, NULL);
gtk_tree_view_set_model(tree_view, GTK_TREE_MODEL(store));
g_object_unref(store);
}
// Queue a redraw of the VRAM viewer
gtk_widget_queue_draw(GTK_WIDGET(gui_data.vram_viewer));
gui_data.vram_viewer_updating = false;
}
// Queue drawing of the current frame
if (gui_data.fallback_canvas) {
gtk_widget_queue_draw(GTK_WIDGET(gui_data.main_window));
}
else if (gui_data.gl_area) {
gtk_gl_area_queue_render(gui_data.gl_area);
}
}
static void vblank(GB_gameboy_t *gb) {
flip();
if (gui_data.border_mode_changed) {
GB_set_border_mode(gb, get_display_border_mode());
update_window_geometry();
gui_data.border_mode_changed = false;
}
GB_set_pixels_output(gb, get_pixels());
if (gui_data.underclock_down && gui_data.clock_mutliplier > 0.5) {
gui_data.clock_mutliplier -= 1.0/16;
GB_set_clock_multiplier(gb, gui_data.clock_mutliplier);
}
else if (!gui_data.underclock_down && gui_data.clock_mutliplier < 1.0) {
gui_data.clock_mutliplier += 1.0/16;
GB_set_clock_multiplier(gb, gui_data.clock_mutliplier);
}
if (g_strcmp0("vram_viewer_tileset", gui_data.vram_viewer_active_tab) == 0) {
const gchar *palette_id = gtk_combo_box_get_active_id(builder_get(GTK_COMBO_BOX, "vram_viewer_tileset_palette_selector"));
GB_palette_type_t palette_type = g_str_has_prefix(palette_id, "bg")? GB_PALETTE_BACKGROUND : GB_PALETTE_OAM;
uint8_t palette_index = g_ascii_digit_value(palette_id[palette_type == GB_PALETTE_OAM ? 3 : 2]);
GB_draw_tileset(gb, gui_data.tileset_buffer,
palette_type,
palette_index
);
}
else if (g_strcmp0("vram_viewer_tilemap", gui_data.vram_viewer_active_tab) == 0) {
const gchar *palette_id = gtk_combo_box_get_active_id(builder_get(GTK_COMBO_BOX, "vram_viewer_tilemap_palette_selector"));
uint8_t palette_index = 0;
GB_palette_type_t palette_type = GB_PALETTE_AUTO;
if (g_strcmp0("auto", palette_id) != 0) {
palette_type = g_str_has_prefix(palette_id, "bg")? GB_PALETTE_BACKGROUND : GB_PALETTE_OAM;
palette_index = g_ascii_digit_value(palette_id[palette_type == GB_PALETTE_OAM ? 3 : 2]);
}
GB_map_type_t map_type = GB_MAP_AUTO;
const gchar *map_type_id = gtk_combo_box_get_active_id(builder_get(GTK_COMBO_BOX, "vram_viewer_tilemap_tilemap_selector"));
if (g_strcmp0("auto", map_type_id) != 0) {
map_type = (g_strcmp0("9800", map_type_id) == 0)? GB_MAP_9800 : GB_MAP_9C00;
}
GB_tileset_type_t tileset_type = GB_TILESET_AUTO;
const gchar *tileset_type_id = gtk_combo_box_get_active_id(builder_get(GTK_COMBO_BOX, "vram_viewer_tilemap_tileset_selector"));
if (g_strcmp0("auto", tileset_type_id) != 0) {
tileset_type = (g_strcmp0("8800", tileset_type_id) == 0)? GB_TILESET_8800 : GB_TILESET_8000;
}
GB_draw_tilemap(gb, gui_data.tilemap_buffer,
palette_type,
palette_index,
map_type,
tileset_type
);
gui_data.scroll_rect = (Rect){
GB_read_memory(gb, 0xFF00 | GB_IO_SCX),
GB_read_memory(gb, 0xFF00 | GB_IO_SCY),
160, 144
};
}
else if (g_strcmp0("vram_viewer_sprites", gui_data.vram_viewer_active_tab) == 0) {
gui_data.oam_count = GB_get_oam_info(gb, gui_data.oam_info, &gui_data.oam_height);
gui_data.vram_viewer_is_cgb = GB_is_cgb(gb);
}
else if (g_strcmp0("vram_viewer_palettes", gui_data.vram_viewer_active_tab) == 0) {
size_t size;
for (unsigned row = 0; row < 16; ++row) {
uint8_t *palette_data = GB_get_direct_access(gb, row >= 8? GB_DIRECT_ACCESS_OBP : GB_DIRECT_ACCESS_BGP, &size, NULL);
memcpy(gui_data.vram_viewer_palette_data[row], palette_data, size);
}
}
gui_data.do_rewind = gui_data.rewind_down;
g_idle_add((GSourceFunc) on_vblank, NULL);
}
static void handle_events(GB_gameboy_t *gb) {
SDL_GameControllerUpdate();
uint8_t controller_state = 0;
if (gui_data.controller) {
int16_t x_axis = SDL_GameControllerGetAxis(gui_data.controller, SDL_CONTROLLER_AXIS_LEFTX);
int16_t y_axis = SDL_GameControllerGetAxis(gui_data.controller, SDL_CONTROLLER_AXIS_LEFTY);
if (x_axis >= JOYSTICK_HIGH) {
controller_state |= BUTTON_MASK_RIGHT;
}
else if (x_axis <= -JOYSTICK_HIGH) {
controller_state |= BUTTON_MASK_LEFT;
}
if (y_axis >= JOYSTICK_HIGH) {
controller_state |= BUTTON_MASK_DOWN;
}
else if (y_axis <= -JOYSTICK_HIGH) {
controller_state |= BUTTON_MASK_UP;
}
if (SDL_GameControllerGetButton(gui_data.controller, SDL_CONTROLLER_BUTTON_DPAD_RIGHT)) controller_state |= BUTTON_MASK_RIGHT;
if (SDL_GameControllerGetButton(gui_data.controller, SDL_CONTROLLER_BUTTON_DPAD_LEFT)) controller_state |= BUTTON_MASK_LEFT;
if (SDL_GameControllerGetButton(gui_data.controller, SDL_CONTROLLER_BUTTON_DPAD_UP)) controller_state |= BUTTON_MASK_UP;
if (SDL_GameControllerGetButton(gui_data.controller, SDL_CONTROLLER_BUTTON_DPAD_DOWN)) controller_state |= BUTTON_MASK_DOWN;
if (SDL_GameControllerGetButton(gui_data.controller, SDL_CONTROLLER_BUTTON_A)) controller_state |= BUTTON_MASK_A;
if (SDL_GameControllerGetButton(gui_data.controller, SDL_CONTROLLER_BUTTON_B)) controller_state |= BUTTON_MASK_B;
if (SDL_GameControllerGetButton(gui_data.controller, SDL_CONTROLLER_BUTTON_BACK)) controller_state |= BUTTON_MASK_SELECT;
if (SDL_GameControllerGetButton(gui_data.controller, SDL_CONTROLLER_BUTTON_START)) controller_state |= BUTTON_MASK_START;
}
GB_set_key_state(gb, GB_KEY_RIGHT, (gui_data.pressed_buttons & BUTTON_MASK_RIGHT) | (controller_state & BUTTON_MASK_RIGHT));
GB_set_key_state(gb, GB_KEY_LEFT, (gui_data.pressed_buttons & BUTTON_MASK_LEFT) | (controller_state & BUTTON_MASK_LEFT));
GB_set_key_state(gb, GB_KEY_UP, (gui_data.pressed_buttons & BUTTON_MASK_UP) | (controller_state & BUTTON_MASK_UP));
GB_set_key_state(gb, GB_KEY_DOWN, (gui_data.pressed_buttons & BUTTON_MASK_DOWN) | (controller_state & BUTTON_MASK_DOWN));
GB_set_key_state(gb, GB_KEY_A, (gui_data.pressed_buttons & BUTTON_MASK_A) | (controller_state & BUTTON_MASK_A));
GB_set_key_state(gb, GB_KEY_B, (gui_data.pressed_buttons & BUTTON_MASK_B) | (controller_state & BUTTON_MASK_B));
GB_set_key_state(gb, GB_KEY_SELECT, (gui_data.pressed_buttons & BUTTON_MASK_SELECT) | (controller_state & BUTTON_MASK_SELECT));
GB_set_key_state(gb, GB_KEY_START, (gui_data.pressed_buttons & BUTTON_MASK_START) | (controller_state & BUTTON_MASK_START));
}
static void load_boot_rom(GB_gameboy_t *gb, GB_boot_rom_t type) {
GError *error = NULL;
char *boot_rom_path = NULL;
GBytes *boot_rom_f = NULL;
const guchar *boot_rom_data;
gsize boot_rom_size;
static const char *const names[] = {
[GB_BOOT_ROM_DMG0] = "dmg0_boot.bin",
[GB_BOOT_ROM_DMG] = "dmg_boot.bin",
[GB_BOOT_ROM_MGB] = "mgb_boot.bin",
[GB_BOOT_ROM_SGB] = "sgb_boot.bin",
[GB_BOOT_ROM_SGB2] = "sgb2_boot.bin",
[GB_BOOT_ROM_CGB0] = "cgb0_boot.bin",
[GB_BOOT_ROM_CGB] = "cgb_boot.bin",
[GB_BOOT_ROM_AGB] = "agb_boot.bin",
};
const char *const boot_rom_name = names[type];
if (gui_data.cli_options.boot_rom_path != NULL) {
g_message("[CLI override] Trying to load boot ROM from %s", gui_data.cli_options.boot_rom_path);
if (GB_load_boot_rom(gb, gui_data.cli_options.boot_rom_path)) {
g_warning("Falling back to boot ROM from config");
goto config_boot_rom;
}
}
else { config_boot_rom:
if (config.boot_rom_path != NULL && g_strcmp0(config.boot_rom_path, "other") != 0 && g_strcmp0(config.boot_rom_path, "auto") != 0) {
boot_rom_path = g_build_filename(config.boot_rom_path, boot_rom_name, NULL);
g_message("Trying to load boot ROM from %s", boot_rom_path);
if (GB_load_boot_rom(gb, boot_rom_path)) {
g_free(boot_rom_path);
g_warning("Falling back to internal boot ROM");
goto internal_boot_rom;
}
g_free(boot_rom_path);
}
else { internal_boot_rom:
boot_rom_path = g_build_filename(RESOURCE_PREFIX "bootroms/", boot_rom_name, NULL);
boot_rom_f = g_resources_lookup_data(boot_rom_path, G_RESOURCE_LOOKUP_FLAGS_NONE, &error);
g_message("Loading internal boot ROM: %s", boot_rom_path);
g_free(boot_rom_path);
if (boot_rom_f == NULL) {
g_warning("Failed to load internal boot ROM: %s", boot_rom_path);
g_error_free(error);
exit(EXIT_FAILURE);
}
boot_rom_data = g_bytes_get_data(boot_rom_f, &boot_rom_size);
GB_load_boot_rom_from_buffer(gb, boot_rom_data, boot_rom_size);
g_bytes_unref(boot_rom_f);
}
}
}
static void init(void) {
if (GB_is_inited(&gb)) return;
GB_init(&gb, get_model());
GB_set_vblank_callback(&gb, vblank);
GB_set_pixels_output(&gb, get_current_buffer());
GB_set_rgb_encode_callback(&gb, rgb_encode);
GB_set_sample_rate(&gb, GB_audio_get_sample_rate());
GB_set_color_correction_mode(&gb, get_color_correction_mode());
GB_set_highpass_filter_mode(&gb, get_highpass_mode());
GB_set_rewind_length(&gb, config.rewind_duration);
GB_set_update_input_hint_callback(&gb, handle_events);
GB_apu_set_sample_callback(&gb, gb_audio_callback);
GB_set_input_callback(&gb, sync_console_input);
GB_set_async_input_callback(&gb, async_console_input);
GB_set_log_callback(&gb, console_log);
GB_set_boot_rom_load_callback(&gb, load_boot_rom);
if (get_display_border_mode() <= GB_BORDER_ALWAYS) {
GB_set_border_mode(&gb, get_display_border_mode());
}
update_window_geometry();
}
static void reset(void) {
g_debug("Reset: %d == %d", get_model(), gui_data.prev_model);
GB_model_t current_model = get_model();
if (gui_data.prev_model == -1 || gui_data.prev_model == current_model) {
GB_reset(&gb);
}
else {
GB_switch_model_and_reset(&gb, current_model);
}
GB_set_palette(&gb, get_monochrome_palette());
gui_data.prev_model = get_model();
GtkRequisition minimum_size;
GtkRequisition natural_size;
gtk_widget_get_preferred_size(GTK_WIDGET(gui_data.main_window), &minimum_size, &natural_size);
// Check SGB -> non-SGB and non-SGB to SGB transitions
if (GB_get_screen_width(&gb) != minimum_size.width || GB_get_screen_height(&gb) != minimum_size.height) {
update_window_geometry();
}
bool success = false;
if (gui_data.file) {
char *path = g_file_get_path(gui_data.file);
if (GB_load_rom(&gb, path) == 0) {
success = true;
}
else {
g_warning("Failed to load ROM: %s", path);
}
GB_load_battery(&gb, gui_data.battery_save_path);
GB_load_cheats(&gb, gui_data.cheats_save_path);
// TODO: Implement without GB_INTERNAL
/* GB_debugger_load_symbol_file */ {
GError *error = NULL;
GInputStream *stream = g_resources_open_stream(RESOURCE_PREFIX "Misc/registers.sym", G_RESOURCE_LOOKUP_FLAGS_NONE, &error);
GDataInputStream *input_stream = g_data_input_stream_new(stream);
char *line = NULL;
gsize length = 0;
while ((line = g_data_input_stream_read_line_utf8(input_stream, &length, NULL, &error))) {
// skip comments
for (unsigned i = 0; i < length; i++) {
if (line[i] == ';') {
line[i] = 0;
length = i;
break;
}
}
if (length == 0) continue;
unsigned bank, address;
char symbol[length];
if (sscanf(line, "%x:%x %s", &bank, &address, symbol) == 3) {
GB_debugger_add_symbol(&gb, bank, address, symbol);
}
}
g_object_unref(stream);
}
size_t path_length = strlen(path);
char sym_file_path[path_length + 5];
replace_extension(path, path_length, sym_file_path, ".sym");
GB_debugger_load_symbol_file(&gb, sym_file_path);
g_free(path);
}
action_set_enabled(gui_data.main_application, "close", success);
action_entries_set_enabled(emulation_entries, G_N_ELEMENTS(emulation_entries), success);
}
static void start(void) {
gui_data.running = true;
gui_data.stopped = false;
GB_audio_clear_queue();
GB_audio_set_paused(false);
/* Run emulation */
while (gui_data.running) {
if (gui_data.rewind_paused) {
handle_events(&gb);
g_usleep(G_USEC_PER_SEC / 8);
}
else {
if (gui_data.do_rewind) {
GB_rewind_pop(&gb);
if (gui_data.turbo_down) {
GB_rewind_pop(&gb);
}
if (!GB_rewind_pop(&gb)) {
gui_data.rewind_paused = true;
}
gui_data.do_rewind = false;
}
GB_run(&gb);
}
}
if (gui_data.file) {
GB_save_battery(&gb, gui_data.battery_save_path);
GB_save_cheats(&gb, gui_data.cheats_save_path);
}
gui_data.stopping = false;
}
// Prevent dependency loop
static void run(void);
// app.reset GAction
// Resets the emulation
static void activate_reset(GSimpleAction *action, GVariant *parameter, gpointer app) {
if (!GB_is_inited(&gb)) {
init();
}
stop();
reset();
run();
}
static gpointer run_thread(gpointer null_ptr) {
if (!gui_data.file) return NULL;
char *path = g_file_get_path(gui_data.file);
size_t path_length = strlen(path);
/* At the worst case, size is strlen(path) + 4 bytes for .sav + NULL */
char battery_save_path[path_length + 5];
char cheats_save_path[path_length + 5];
replace_extension(path, path_length, battery_save_path, ".sav");
replace_extension(path, path_length, cheats_save_path, ".cht");
gui_data.battery_save_path = battery_save_path;
gui_data.cheats_save_path = cheats_save_path;
if (gui_data.stopped) {
start();
}
else {
init();
reset();
start();
}
return NULL;
}
static void run(void) {
if (gui_data.running) return;
while (gui_data.stopping);
g_thread_new("CoreLoop", run_thread, NULL);
}
// Tell our application to quit.
// After this functions has been called the `shutdown` signal will be issued.
//
// TODO: Make sure we have a way to quit our emulation loop before `shutdown` gets called
static void quit(void) {
stop();
// Quit our application properly.
// This fires the “shutdown” signal.
g_application_quit(G_APPLICATION(gui_data.main_application));
}
static void quit_interrupt(int ignored) {
quit();
}
// `destroy` signal GCallback
// Exits the application
static void on_quit(GtkWidget *w, gpointer app) {
quit();
}
static void create_action_groups(GApplication *app) {
g_action_map_add_action_entries(G_ACTION_MAP(app), emulation_entries, G_N_ELEMENTS(emulation_entries), NULL);
g_action_map_add_action_entries(G_ACTION_MAP(app), developer_entries, G_N_ELEMENTS(developer_entries), NULL);
g_action_map_add_action_entries(G_ACTION_MAP(app), app_entries, G_N_ELEMENTS(app_entries), NULL);
g_action_map_add_action_entries(G_ACTION_MAP(app), file_entries, G_N_ELEMENTS(file_entries), NULL);
g_action_map_add_action_entries(G_ACTION_MAP(app), edit_entries, G_N_ELEMENTS(edit_entries), NULL);
action_set_enabled(app, "close", false);
action_entries_set_enabled(emulation_entries, G_N_ELEMENTS(emulation_entries), false);
}
static gboolean on_key_press(GtkWidget *w, GdkEventKey *event, gpointer data) {
uint8_t mask;
// TODO: Allow control remapping in the GUI
switch (event->keyval) {
case GDK_KEY_w: mask = BUTTON_MASK_UP; break;
case GDK_KEY_a: mask = BUTTON_MASK_LEFT; break;
case GDK_KEY_s: mask = BUTTON_MASK_DOWN; break;
case GDK_KEY_d: mask = BUTTON_MASK_RIGHT; break;
case GDK_KEY_g: mask = BUTTON_MASK_SELECT; break;
case GDK_KEY_h: mask = BUTTON_MASK_START; break;
case GDK_KEY_k: mask = BUTTON_MASK_B; break;
case GDK_KEY_l: mask = BUTTON_MASK_A; break;
case GDK_KEY_Tab: {
gui_data.rewind_down = event->type == GDK_KEY_PRESS;
GB_set_turbo_mode(&gb, gui_data.turbo_down, gui_data.turbo_down && gui_data.rewind_down);
if (event->type == GDK_KEY_RELEASE) {
gui_data.rewind_paused = false;
}
break; }
case GDK_KEY_space: {
gui_data.turbo_down = event->type == GDK_KEY_PRESS;
GB_audio_clear_queue();
GB_set_turbo_mode(&gb, gui_data.turbo_down, gui_data.turbo_down && gui_data.rewind_down);
break; }
case GDK_KEY_dead_acute: // fall through
case GDK_KEY_acute: // fall through
case GDK_KEY_apostrophe:
gui_data.underclock_down = event->type == GDK_KEY_PRESS;
break;
case GDK_KEY_F11: {
if (event->type == GDK_KEY_RELEASE) {
if (gui_data.is_fullscreen) {
gtk_window_unfullscreen(GTK_WINDOW(gui_data.main_window));
}
else {
gtk_window_fullscreen(GTK_WINDOW(gui_data.main_window));
}
}
break; }
}
if (event->type == GDK_KEY_PRESS) {
gui_data.pressed_buttons |= mask;
}
else if (event->type == GDK_KEY_RELEASE) {
gui_data.pressed_buttons &= ~mask;
}
return false;
}
static void on_window_state_change(GtkWidget *w, GdkEventWindowState *event, gpointer data) {
gui_data.is_fullscreen = event->new_window_state & GDK_WINDOW_STATE_FULLSCREEN;
}
// Gets called when the VRAM viewer gets realized
static void on_vram_viewer_realize(void) {
gui_data.vram_viewer_visible = true;
gui_data.vram_viewer_active_tab = (gchar *)gtk_stack_get_visible_child_name(builder_get(GTK_STACK, "vram_viewer_stack"));
}
// Gets called when the VRAM viewer gets unrealized
static void on_vram_viewer_unrealize(void) {
gui_data.vram_viewer_visible = false;
}
// Gets called when the tileset viewer should be redrawn
static gboolean on_draw_vram_viewer_tileset(GtkWidget *widget, cairo_t *cr, gpointer data) {
g_mutex_lock(&gui_data.tileset_buffer_mutex);
guint width, height;
GtkStyleContext *context;
context = gtk_widget_get_style_context(widget);
width = gtk_widget_get_allocated_width(widget);
height = gtk_widget_get_allocated_height(widget);
gtk_render_background(context, cr, 0, 0, width, height);
cairo_surface_t *surface = cairo_image_surface_create_for_data(
(unsigned char *) gui_data.tileset_buffer,
CAIRO_FORMAT_RGB24,
256,
192,
cairo_format_stride_for_width(CAIRO_FORMAT_RGB24, 256)
);
cairo_scale(cr, 2.0, 2.0);
cairo_set_source_surface(cr, surface, 0, 0);
cairo_pattern_set_filter(cairo_get_source(cr), CAIRO_FILTER_NEAREST);
cairo_paint(cr);
if (gtk_toggle_button_get_active(builder_get(GTK_TOGGLE_BUTTON, "vram_viewer_tileset_toggle_grid_button"))) {
cairo_set_source_rgba(cr, 0.0, 0.0, 0.0, 0.25);
cairo_set_line_width(cr, 1);
const int divisions_x = 256 / 8;
const int divisions_y = 192 / 8;
for (int i = 0; i < divisions_x; i++) {
const int j = 256 * i;
cairo_move_to(cr, j / divisions_x, 0);
cairo_line_to(cr, j / divisions_x, 192);
}
for (int i = 0; i < divisions_y; i++) {
const int j = 192 * i;
cairo_move_to(cr, 0, j / divisions_y);
cairo_line_to(cr, 256, j / divisions_y);
}
cairo_stroke(cr);
}
g_mutex_unlock(&gui_data.tileset_buffer_mutex);
return false;
}
// Gets called when the tilemap viewer should be redrawn
static gboolean on_draw_vram_viewer_tilemap(GtkWidget *widget, cairo_t *cr, gpointer data) {
g_mutex_lock(&gui_data.tilemap_buffer_mutex);
guint width, height;
GtkStyleContext *context;
context = gtk_widget_get_style_context(widget);
width = gtk_widget_get_allocated_width(widget);
height = gtk_widget_get_allocated_height(widget);
gtk_render_background(context, cr, 0, 0, width, height);
cairo_surface_t *surface = cairo_image_surface_create_for_data(
(unsigned char *) gui_data.tilemap_buffer,
CAIRO_FORMAT_RGB24,
256,
256,
cairo_format_stride_for_width(CAIRO_FORMAT_RGB24, 256)
);
cairo_scale(cr, 2.0, 2.0);
cairo_set_source_surface(cr, surface, 0, 0);
cairo_pattern_set_filter(cairo_get_source(cr), CAIRO_FILTER_NEAREST);
cairo_paint(cr);
if (gtk_toggle_button_get_active(builder_get(GTK_TOGGLE_BUTTON, "vram_viewer_tilemap_toggle_grid_button"))) {
cairo_set_source_rgba(cr, 0.0, 0.0, 0.0, 0.25);
cairo_set_line_width(cr, 1);
const int divisions = 256 / 8;
for (int i = 0; i < divisions; i++) {
const int j = 256 * i;
cairo_move_to(cr, j / divisions, 0);
cairo_line_to(cr, j / divisions, 256);
cairo_move_to(cr, 0, j / divisions);
cairo_line_to(cr, 256, j / divisions);
}
cairo_stroke(cr);
}
if (gtk_toggle_button_get_active(builder_get(GTK_TOGGLE_BUTTON, "vram_viewer_tilemap_toggle_scrolling_button"))) {
cairo_rectangle(cr, -2, -2, width + 2, height + 2);
for (unsigned x = 0; x < 2; x++) {
for (unsigned y = 0; y < 2; y++) {
Rect rect = gui_data.scroll_rect;
rect.x -= 256 * x;
rect.y += 256 * y;
cairo_rectangle(cr, rect.x, rect.y, rect.w, rect.h);
}
}
cairo_set_fill_rule(cr, CAIRO_FILL_RULE_EVEN_ODD);
cairo_set_line_width(cr, 2);
cairo_set_line_join(cr, CAIRO_LINE_JOIN_ROUND);
cairo_set_source_rgba(cr, 0.2, 0.2, 0.2, 0.5);
cairo_fill_preserve(cr);
cairo_clip_preserve(cr);
cairo_set_source_rgba(cr, 0.0, 0.0, 0.0, 0.6);
cairo_stroke(cr);
}
g_mutex_unlock(&gui_data.tilemap_buffer_mutex);
return false;
}
static gboolean on_motion_vram_viewer_tileset(GtkWidget *widget, GdkEventMotion *event) {
int x, y;
if (event->is_hint) {
gdk_window_get_pointer (event->window, &x, &y, NULL);
}
else {
x = event->x;
y = event->y;
}
// Compensate for our canvas scale
x /= 2;
y /= 2;
uint8_t bank = x >= 128? 1 : 0;
x &= 127;
uint16_t tile = x / 8 + y / 8 * 16;
GtkLabel *status = builder_get(GTK_LABEL, "vram_viewer_status");
gtk_label_set_text(status, g_strdup_printf("Tile number $%02x at %d:$%04x", tile & 0xFF, bank, 0x8000 + tile * 0x10));
return true;
}
static gboolean on_motion_vram_viewer_tilemap(GtkWidget *widget, GdkEventMotion *event) {
int x, y;
if (event->is_hint) {
gdk_window_get_pointer (event->window, &x, &y, NULL);
}
else {
x = event->x;
y = event->y;
}
// Compensate for our canvas scale
x /= 2;
y /= 2;
GtkLabel *status = builder_get(GTK_LABEL, "vram_viewer_status");
uint16_t map_offset = x / 8 + y / 8 * 32;
uint16_t map_base = 0x1800;
GB_map_type_t map_type = GB_MAP_AUTO;
const gchar *map_type_id = gtk_combo_box_get_active_id(builder_get(GTK_COMBO_BOX, "vram_viewer_tilemap_tilemap_selector"));
if (g_strcmp0("auto", map_type_id) != 0) {
map_type = (g_strcmp0("9800", map_type_id) == 0)? GB_MAP_9800 : GB_MAP_9C00;
}
GB_tileset_type_t tileset_type = GB_TILESET_AUTO;
const gchar *tileset_type_id = gtk_combo_box_get_active_id(builder_get(GTK_COMBO_BOX, "vram_viewer_tilemap_tileset_selector"));
if (g_strcmp0("auto", tileset_type_id) != 0) {
tileset_type = (g_strcmp0("8800", tileset_type_id) == 0)? GB_TILESET_8800 : GB_TILESET_8000;
}
uint8_t lcdc = ((uint8_t *)GB_get_direct_access(&gb, GB_DIRECT_ACCESS_IO, NULL, NULL))[GB_IO_LCDC];
uint8_t *vram = GB_get_direct_access(&gb, GB_DIRECT_ACCESS_VRAM, NULL, NULL);
if (map_type == GB_MAP_9C00 || (map_type == GB_MAP_AUTO && lcdc & 0x08)) {
map_base = 0x1c00;
}
if (tileset_type == GB_TILESET_AUTO) {
tileset_type = (lcdc & 0x10)? GB_TILESET_8800 : GB_TILESET_8000;
}
uint8_t tile = vram[map_base + map_offset];
uint16_t tile_address = 0;
if (tileset_type == GB_TILESET_8000) {
tile_address = 0x8000 + tile * 0x10;
}
else {
tile_address = 0x9000 + (int8_t)tile * 0x10;
}
if (GB_is_cgb(&gb)) {
uint8_t attributes = vram[map_base + map_offset + 0x2000];
gtk_label_set_text(status, g_strdup_printf("Tile number $%02x (%d:$%04x) at map address $%04x (Attributes: %c%c%c%d%d)",
tile,
attributes & 0x8? 1 : 0,
tile_address,
0x8000 + map_base + map_offset,
(attributes & 0x80) ? 'P' : '-',
(attributes & 0x40) ? 'V' : '-',
(attributes & 0x20) ? 'H' : '-',
attributes & 0x8? 1 : 0,
attributes & 0x7
));
}
else {
gtk_label_set_text(status, g_strdup_printf("Tile number $%02x ($%04x) at map address $%04x",
tile,
tile_address,
0x8000 + map_base + map_offset
));
}
return true;
}
static void on_vram_tab_change(GtkWidget *widget, GParamSpec *pspec, GtkStackSwitcher *self) {
gtk_label_set_text(builder_get(GTK_LABEL, "vram_viewer_status"), "");
gui_data.vram_viewer_active_tab = (gchar *)gtk_stack_get_visible_child_name(builder_get(GTK_STACK, "vram_viewer_stack"));
}
// This functions gets called immediately after registration of the GApplication
static void startup(GApplication *app, gpointer null_ptr) {
signal(SIGINT, quit_interrupt);
// Very ugly workaround for GtkGlArea!
// When a GtkGlArea is realized and it creates a legacy GL 1.4 context
// it tries to use GL 2.0 functions to render the window which leads to the application crashing.
// So we initialize GTK, create a dummy GtkWindow object, attach a `realize` callback and
// in this callback create a GdkGLContext on this window. But instead of running the GTK main loop
// we just realize and destroy the dummy window and compare the contexts version in the realize callback.
gui_data.supports_gl = test_gl_support();
g_debug("OpenGL supported: %s", gui_data.supports_gl? "Yes" : "No");
gui_data.builder = gtk_builder_new_from_resource(RESOURCE_PREFIX "ui/window.ui");
gtk_builder_connect_signals(gui_data.builder, NULL);
create_action_groups(app);
if (gui_data.cli_options.prefix != NULL) {
GAction *action = g_action_map_lookup_action(G_ACTION_MAP(gui_data.main_application), "change_model");
g_action_change_state(action, g_variant_new_string(gui_data.cli_options.prefix));
}
#if NDEBUG
// Disable when not compiled in debug mode
action_set_enabled(app, "open_gtk_debugger", false);
// Remove the menubar override
gtk_widget_destroy(builder_get(GTK_WIDGET, "menubar_override_selector_label"));
gtk_widget_destroy(builder_get(GTK_WIDGET, "menubar_override_selector"));
#endif
gui_data.preferences = GTK_WINDOW(get_object("preferences"));
g_signal_connect(gui_data.preferences, "realize", G_CALLBACK(on_preferences_realize), (gpointer) gui_data.builder);
init_settings(gui_data.cli_options.config_path, gui_data.preferences);
gui_data.vram_viewer = GTK_WINDOW(get_object("vram_viewer"));
gui_data.memory_viewer = GTK_WINDOW(get_object("memory_viewer"));
gui_data.console = GTK_WINDOW(get_object("console"));
gui_data.printer = GTK_WINDOW(get_object("printer"));
if (config.sample_rate == -1) {
gui_data.sample_rate = GB_audio_default_sample_rate();
}
else {
gui_data.sample_rate = config.sample_rate;
}
// setup main window
gui_data.main_window = GTK_APPLICATION_WINDOW(gtk_application_window_new(GTK_APPLICATION(app)));
gui_data.main_window_container = GTK_BOX(gtk_box_new(GTK_ORIENTATION_VERTICAL, 0));
gtk_window_set_title(GTK_WINDOW(gui_data.main_window), "SameBoy");
gtk_application_window_set_show_menubar(gui_data.main_window, false);
gtk_container_add(GTK_CONTAINER(gui_data.main_window), GTK_WIDGET(gui_data.main_window_container));
setup_menu(app);
// Insert separators into `GtkComboBox`es
set_combo_box_row_separator_func(GTK_CONTAINER(gui_data.preferences));
set_combo_box_row_separator_func(GTK_CONTAINER(gui_data.vram_viewer));
set_combo_box_row_separator_func(GTK_CONTAINER(gui_data.memory_viewer));
// Define a set of window icons
GList *icon_list = NULL;
static char* icons[] = {
RESOURCE_PREFIX "logo_256.png",
RESOURCE_PREFIX "logo_128.png",
RESOURCE_PREFIX "logo_64.png",
RESOURCE_PREFIX "logo_48.png",
RESOURCE_PREFIX "logo_32.png",
RESOURCE_PREFIX "logo_16.png"
};
// Create list of GdkPixbufs
for (int i = 0; i < (sizeof(icons) / sizeof(const char*)); ++i) {
GdkPixbuf *icon = gdk_pixbuf_new_from_resource(icons[i], NULL);
if (!icon) continue;
icon_list = g_list_prepend(icon_list, icon);
}
// Let GTK choose the proper icon
gtk_window_set_icon_list(GTK_WINDOW(gui_data.main_window), icon_list);
// Add missing information to the about dialog
GtkAboutDialog *about_dialog = GTK_ABOUT_DIALOG(get_object("about_dialog"));
gtk_about_dialog_set_logo(about_dialog, gdk_pixbuf_new_from_resource(icons[2], NULL)); // reuse the 64x64 icon
gtk_about_dialog_set_version(about_dialog, "v" xstr(VERSION));
g_list_free_full(icon_list, g_object_unref);
}
static void connect_signal_handlers(GApplication *app) {
// Connect signal handlers
gtk_widget_add_events(GTK_WIDGET(gui_data.main_window), GDK_KEY_PRESS_MASK);
gtk_widget_add_events(GTK_WIDGET(gui_data.main_window), GDK_KEY_RELEASE_MASK);
g_signal_connect(gui_data.main_window, "destroy", G_CALLBACK(on_quit), app);
g_signal_connect(gui_data.main_window, "key_press_event", G_CALLBACK(on_key_press), NULL);
g_signal_connect(gui_data.main_window, "key_release_event", G_CALLBACK(on_key_press), NULL);
g_signal_connect(gui_data.main_window, "window-state-event", G_CALLBACK(on_window_state_change), NULL);
g_signal_connect(gui_data.vram_viewer, "realize", G_CALLBACK(on_vram_viewer_realize), NULL);
g_signal_connect(gui_data.vram_viewer, "unrealize", G_CALLBACK(on_vram_viewer_unrealize), NULL);
// Just hide our sub-windows when closing them
g_signal_connect(gui_data.preferences, "delete-event", G_CALLBACK(gtk_widget_hide_on_delete), NULL);
g_signal_connect(gui_data.vram_viewer, "delete-event", G_CALLBACK(gtk_widget_hide_on_delete), NULL);
g_signal_connect(gui_data.memory_viewer, "delete-event", G_CALLBACK(gtk_widget_hide_on_delete), NULL);
g_signal_connect(gui_data.console, "delete-event", G_CALLBACK(gtk_widget_hide_on_delete), NULL);
g_signal_connect(gui_data.printer, "delete-event", G_CALLBACK(gtk_widget_hide_on_delete), NULL);
g_signal_connect(get_object("vram_viewer_tileset_canvas"), "draw", G_CALLBACK(on_draw_vram_viewer_tileset), NULL);
g_signal_connect(get_object("vram_viewer_tilemap_canvas"), "draw", G_CALLBACK(on_draw_vram_viewer_tilemap), NULL);
gtk_widget_add_events(builder_get(GTK_WIDGET, "vram_viewer_tileset_canvas"), GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK);
gtk_widget_add_events(builder_get(GTK_WIDGET, "vram_viewer_tilemap_canvas"), GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK);
g_signal_connect(get_object("vram_viewer_tileset_canvas"), "motion_notify_event", G_CALLBACK(on_motion_vram_viewer_tileset), NULL);
g_signal_connect(get_object("vram_viewer_tilemap_canvas"), "motion_notify_event", G_CALLBACK(on_motion_vram_viewer_tilemap), NULL);
g_signal_connect(get_object("vram_viewer_stack"), "notify::visible-child", G_CALLBACK(on_vram_tab_change), NULL);
}
static void update_viewport(void) {
GtkWidget *w = gui_data.fallback_canvas ? GTK_WIDGET(gui_data.fallback_canvas) : GTK_WIDGET(gui_data.gl_area);
int win_width = gtk_widget_get_allocated_width(w);
int win_height = gtk_widget_get_allocated_height(w);
double x_factor = win_width / (double) GB_get_screen_width(&gb);
double y_factor = win_height / (double) GB_get_screen_height(&gb);
if (config.use_integer_scaling) {
x_factor = (int)(x_factor);
y_factor = (int)(y_factor);
}
if (config.keep_aspect_ratio) {
if (x_factor > y_factor) {
x_factor = y_factor;
}
else {
y_factor = x_factor;
}
}
unsigned new_width = x_factor * GB_get_screen_width(&gb);
unsigned new_height = y_factor * GB_get_screen_height(&gb);
gui_data.viewport = (Rect){
(win_width - new_width) / 2,
(win_height - new_height) / 2,
new_width,
new_height
};
if (!gui_data.fallback_canvas) glViewport(gui_data.viewport.x, gui_data.viewport.y, gui_data.viewport.w, gui_data.viewport.h);
}
// TODO: Comment
static void gl_draw(void) {
uint32_t *pixels = get_current_buffer();
uint32_t *previous = get_previous_buffer();
static void *_pixels = NULL;
if (pixels) {
_pixels = pixels;
}
glClearColor(0, 0, 0, 1);
glClear(GL_COLOR_BUFFER_BIT);
GB_frame_blending_mode_t mode = get_frame_blending_mode();
if (!previous) {
mode = GB_FRAME_BLENDING_MODE_DISABLED;
}
else if (mode == GB_FRAME_BLENDING_MODE_ACCURATE) {
if (GB_is_sgb(&gb)) {
mode = GB_FRAME_BLENDING_MODE_SIMPLE;
}
else {
mode = GB_is_odd_frame(&gb)? GB_FRAME_BLENDING_MODE_ACCURATE_ODD : GB_FRAME_BLENDING_MODE_ACCURATE_EVEN;
}
}
render_bitmap_with_shader(
&gui_data.shader, _pixels, previous,
GB_get_screen_width(&gb), GB_get_screen_height(&gb),
gui_data.viewport.x, gui_data.viewport.y, gui_data.viewport.w, gui_data.viewport.h,
mode
);
}
// TODO: Comment
static void gl_finish(void) { }
// TODO: Comment
static void resize(void) {
update_viewport();
}
// TODO: Comment
static gboolean on_draw_fallback(GtkWidget *widget, cairo_t *cr, gpointer data) {
GtkStyleContext *context = gtk_widget_get_style_context(widget);
guint width = gtk_widget_get_allocated_width(widget);
guint height = gtk_widget_get_allocated_height(widget);
guint screen_width = GB_get_screen_width(&gb);
guint screen_height = GB_get_screen_height(&gb);
gtk_render_background(context, cr, 0, 0, width, height);
cairo_surface_t *surface = cairo_image_surface_create_for_data(
(unsigned char *) get_current_buffer(),
CAIRO_FORMAT_RGB24,
screen_width,
screen_height,
cairo_format_stride_for_width(CAIRO_FORMAT_RGB24, screen_width)
);
cairo_translate(cr, gui_data.viewport.x, gui_data.viewport.y);
cairo_scale(cr, gui_data.viewport.w / screen_width, gui_data.viewport.h / screen_height);
cairo_set_source_surface(cr, surface, 0, 0);
cairo_pattern_set_filter(cairo_get_source(cr), CAIRO_FILTER_NEAREST);
cairo_paint(cr);
return false;
}
// Create a GtkDrawingArea as a fallback in case we cant use OpenGL
static void create_fallback_canvas(void) {
gui_data.fallback_canvas = GTK_DRAWING_AREA(gtk_drawing_area_new());
g_signal_connect(gui_data.fallback_canvas, "draw", G_CALLBACK(on_draw_fallback), NULL);
g_signal_connect(gui_data.fallback_canvas, "size-allocate", G_CALLBACK(resize), NULL);
gtk_box_pack_end(GTK_BOX(gui_data.main_window_container), GTK_WIDGET(gui_data.fallback_canvas), true, true, 0);
}
// TODO: Comment
static void gl_init(GtkWidget *w) {
GtkGLArea *gl_area = GTK_GL_AREA(w);
g_debug("GL_INIT");
const char *renderer;
g_debug("GL Context: %p", gtk_gl_area_get_context(gl_area));
gtk_gl_area_make_current(gl_area);
if (gtk_gl_area_get_error(gl_area) != NULL) {
goto error;
}
renderer = (char *)glGetString(GL_RENDERER);
g_debug("GtkGLArea on %s", renderer ? renderer : "Unknown");
if (config.shader == NULL || (!init_shader_with_name(&gui_data.shader, config.shader) && !init_shader_with_name(&gui_data.shader, "NearestNeighbor"))) {
GError *error = g_error_new_literal(g_quark_from_string("sameboy-gl-error"), 1, "Failed to initialize shaders");
gtk_gl_area_set_error(gl_area, error);
}
else {
g_signal_connect(gl_area, "render", G_CALLBACK(gl_draw), NULL);
g_signal_connect(gl_area, "resize", G_CALLBACK(resize), NULL);
g_signal_connect(gl_area, "unrealize", G_CALLBACK(gl_finish), NULL);
return;
}
error:
if (gtk_gl_area_get_error(gl_area) != NULL) {
g_warning("GtkGLArea: %s", gtk_gl_area_get_error(gl_area)->message);
}
create_fallback_canvas();
}
static void create_canvas(void) {
// create our renderer area
if (gui_data.supports_gl) {
gui_data.gl_area = GTK_GL_AREA(gtk_gl_area_new());
gtk_gl_area_set_required_version(gui_data.gl_area, 3, 2);
gtk_gl_area_set_auto_render(gui_data.gl_area, false);
gtk_gl_area_set_has_alpha(gui_data.gl_area, false);
gtk_gl_area_set_has_depth_buffer(gui_data.gl_area, false);
gtk_gl_area_set_has_stencil_buffer(gui_data.gl_area, false);
g_signal_connect(gui_data.gl_area, "realize", G_CALLBACK(gl_init), NULL);
gtk_box_pack_end(GTK_BOX(gui_data.main_window_container), GTK_WIDGET(gui_data.gl_area), true, true, 0);
}
else {
create_fallback_canvas();
}
GdkScreen *screen = gdk_screen_get_default();
GtkCssProvider *provider = gtk_css_provider_new();
gtk_css_provider_load_from_resource(provider, RESOURCE_PREFIX "css/main.css");
gtk_style_context_add_provider_for_screen(screen, GTK_STYLE_PROVIDER(provider), GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
}
static void setup_console(void) {
GtkTextView *text_view = builder_get(GTK_TEXT_VIEW, "console_screen");
GtkTextBuffer *text_buf = gtk_text_view_get_buffer(text_view);
gtk_text_view_set_buffer(
builder_get(GTK_TEXT_VIEW, "console_sidebar_output"),
gtk_text_buffer_new(gtk_text_buffer_get_tag_table(text_buf))
);
gtk_text_buffer_create_tag(text_buf, "bold", "weight", PANGO_WEIGHT_BOLD, NULL);
gtk_text_buffer_create_tag(text_buf, "underline", "underline", PANGO_UNDERLINE_SINGLE, "underline-set", true, NULL);
gtk_text_buffer_create_tag(text_buf, "dashed_underline", "underline", PANGO_UNDERLINE_DOUBLE, "underline-set", true, NULL);
g_mutex_init(&gui_data.debugger_input_mutex);
g_cond_init(&gui_data.debugger_input_cond);
g_rec_mutex_init(&gui_data.console_output_lock);
if (!gui_data.debugger_input_queue) {
gui_data.debugger_input_queue = g_ptr_array_sized_new(4);
}
if (!gui_data.pending_console_output) {
gui_data.pending_console_output = gtk_text_buffer_new(gtk_text_buffer_get_tag_table(text_buf));
}
}
// This function gets called when the GApplication gets activated, i.e. it is ready to show widgets.
static void activate(GApplication *app, gpointer null_ptr) {
// initialize SameBoy core
init();
init_audio();
init_controllers();
connect_signal_handlers(app);
create_canvas();
setup_console();
if (gui_data.cli_options.fullscreen) {
gtk_window_fullscreen(GTK_WINDOW(gui_data.main_window));
}
gtk_application_add_window(GTK_APPLICATION(app), GTK_WINDOW(gui_data.main_window));
gtk_widget_show_all(GTK_WIDGET(gui_data.main_window));
// Start the emulation thread
run();
}
// This function gets called when the application is closed.
static void shutdown(GApplication *app, GFile **files, gint n_files, const gchar *hint, gpointer null_ptr) {
g_debug("SHUTDOWN");
stop();
while (gui_data.stopping);
g_object_unref(gui_data.builder);
save_settings();
free_settings();
SDL_Quit();
if (gui_data.image_buffers[0]) g_free(gui_data.image_buffers[0]);
if (gui_data.image_buffers[1]) g_free(gui_data.image_buffers[1]);
if (gui_data.image_buffers[2]) g_free(gui_data.image_buffers[2]);
free_shader(&gui_data.shader);
free_master_shader();
GB_free(&gb);
}
// This function gets called when there are files to open.
// Note: When `open` gets called `activate` wont fire unless we call it ourselves.
static void open(GApplication *app, GFile **files, gint n_files, const gchar *hint, gpointer null_ptr) {
if (n_files > 1) {
g_warning("More than one file specified");
exit(EXIT_FAILURE);
}
gui_data.file = g_file_dup(files[0]);
// We have handled the files, now activate the application
activate(app, NULL);
}
// app.about GAction
// Opens the about dialog
static void activate_about(GSimpleAction *action, GVariant *parameter, gpointer app) {
GObject *dialog = get_object("about_dialog");
gtk_dialog_run(GTK_DIALOG(dialog));
gtk_widget_hide(GTK_WIDGET(dialog));
}
// app.show_console GAction
// Opens the console
static void activate_show_console(GSimpleAction *action, GVariant *parameter, gpointer app) {
if (gui_data.debugger_input_queue) {
while (gui_data.debugger_input_queue->len) {
g_ptr_array_remove_index_fast(gui_data.debugger_input_queue, gui_data.debugger_input_queue->len - 1);
}
}
gtk_widget_show_all(builder_get(GTK_WIDGET, "console"));
}
// app.open_gtk_debugger GAction
// Opens the GTK debugger
static void activate_open_gtk_debugger(GSimpleAction *action, GVariant *parameter, gpointer app) {
gtk_window_set_interactive_debugging(true);
}
// app.open_memory_viewer GAction
// Opens the memory viewer window
static void activate_open_memory_viewer(GSimpleAction *action, GVariant *parameter, gpointer app) {
gtk_widget_show_all(GTK_WIDGET(gui_data.memory_viewer));
}
// app.open_vram_viewer GAction
// Opens the VRAM viewer window
static void activate_open_vram_viewer(GSimpleAction *action, GVariant *parameter, gpointer app) {
gtk_widget_show_all(GTK_WIDGET(gui_data.vram_viewer));
}
// app.open GAction
// Opens a ROM file
static void activate_open(GSimpleAction *action, GVariant *parameter, gpointer app) {
stop();
GtkFileChooserNative *native = gtk_file_chooser_native_new("Open File", GTK_WINDOW(gui_data.main_window), GTK_FILE_CHOOSER_ACTION_OPEN, "_Open", "_Cancel");
gint res = gtk_native_dialog_run(GTK_NATIVE_DIALOG(native));
if (res == GTK_RESPONSE_ACCEPT) {
const char* path = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(native));
gui_data.file = g_file_new_for_path(path);
activate_reset(action, parameter, app);
}
else {
run();
}
g_object_unref(native);
}
// app.close GAction
// Closes a ROM
static void activate_close(GSimpleAction *action, GVariant *parameter, gpointer app) {
stop();
GB_free(&gb);
// Clear the screen as side effect
update_window_geometry();
gtk_widget_queue_draw(gui_data.fallback_canvas ? GTK_WIDGET(gui_data.fallback_canvas) : GTK_WIDGET(gui_data.gl_area));
// Clear the VRAM viewer
g_mutex_lock(&gui_data.tileset_buffer_mutex);
memset(gui_data.tileset_buffer, 0, sizeof gui_data.tileset_buffer);
g_mutex_unlock(&gui_data.tileset_buffer_mutex);
g_mutex_lock(&gui_data.tilemap_buffer_mutex);
memset(gui_data.tilemap_buffer, 0, sizeof gui_data.tilemap_buffer);
g_mutex_unlock(&gui_data.tilemap_buffer_mutex);
gtk_stack_set_visible_child_name(builder_get(GTK_STACK, "vram_viewer_stack"), "vram_viewer_tileset");
gtk_tree_view_set_model(builder_get(GTK_TREE_VIEW, "vram_viewer_sprites"), NULL);
gtk_tree_view_set_model(builder_get(GTK_TREE_VIEW, "vram_viewer_palettes"), NULL);
// Redraw the VRAM viewer
gtk_widget_queue_draw(GTK_WIDGET(gui_data.vram_viewer));
// Update menu action states
action_set_enabled(gui_data.main_application, "close", false);
action_entries_set_enabled(emulation_entries, G_N_ELEMENTS(emulation_entries), false);
}
// app.preferences GAction
// Opens the preferences window
static void activate_preferences(GSimpleAction *action, GVariant *parameter, gpointer app) {
gtk_widget_show_all(GTK_WIDGET(get_object("preferences")));
}
// app.quit GAction
// Exits the application
static void activate_quit(GSimpleAction *action, GVariant *parameter, gpointer app) {
quit();
}
// app.clear_console GAction
// Clears the debugger console
static void activate_clear_console(GSimpleAction *action, GVariant *parameter, gpointer app) {
g_rec_mutex_lock(&gui_data.console_output_lock);
GtkTextView *text_view = builder_get(GTK_TEXT_VIEW, "console_screen");
GtkTextBuffer *text_buf = gtk_text_view_get_buffer(text_view);
gtk_text_buffer_set_text(text_buf, "", -1);
g_rec_mutex_unlock(&gui_data.console_output_lock);
}
static void on_model_changed(GSimpleAction *action, GVariant *value, gpointer user_data_ptr) {
if (!GB_is_inited(&gb)) {
g_simple_action_set_state(action, value);
return;
}
const gchar *model_str = g_variant_get_string(value, NULL);
GtkMessageDialog *dialog = GTK_MESSAGE_DIALOG(gtk_message_dialog_new(
GTK_WINDOW(gui_data.main_window),
GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT,
GTK_MESSAGE_QUESTION,
GTK_BUTTONS_YES_NO,
"Changing the emulated model requires a reset.\nChange model and reset game?"
));
stop();
gint result = gtk_dialog_run(GTK_DIALOG(dialog));
switch (result) {
case GTK_RESPONSE_YES:
// Reset the CLI model override
gui_data.cli_options.model = -1;
g_simple_action_set_state(action, value);
reset();
break;
default:
// Action has been canceled
break;
}
run();
gtk_widget_destroy(GTK_WIDGET(dialog));
}
static void on_mute_changed(GSimpleAction *action, GVariant *value, gpointer user_data_ptr) {
gboolean do_mute = g_variant_get_boolean(value);
GB_audio_set_paused(do_mute);
g_simple_action_set_state(action, value);
}
static void on_pause_changed(GSimpleAction *action, GVariant *value, gpointer user_data_ptr) {
if (g_variant_get_boolean(value)) {
stop();
}
else {
run();
}
g_simple_action_set_state(action, value);
}
G_MODULE_EXPORT void on_boot_rom_location_changed(GtkWidget *w, gpointer user_data_ptr) {
GtkComboBox *box = GTK_COMBO_BOX(w);
const gchar *id = gtk_combo_box_get_active_id(box);
if (id == NULL) return;
if (g_strcmp0(id, "other") == 0) {
GtkFileChooserNative *native = gtk_file_chooser_native_new("Select Folder", gui_data.preferences, GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER, "_Select", "_Cancel");
gint res = gtk_native_dialog_run(GTK_NATIVE_DIALOG(native));
if (res == GTK_RESPONSE_ACCEPT) {
config.boot_rom_path = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(native));
update_boot_rom_selector(gui_data.builder);
}
g_object_unref(native);
}
else {
config.boot_rom_path = (gchar *)id;
}
}
G_MODULE_EXPORT void on_cgb_model_changed(GtkWidget *w, gpointer user_data_ptr) {
GtkComboBox *box = GTK_COMBO_BOX(w);
config.cgb_revision_name = (gchar *)gtk_combo_box_get_active_id(box);
}
G_MODULE_EXPORT void on_color_correction_changed(GtkWidget *w, gpointer user_data_ptr) {
GtkComboBox *box = GTK_COMBO_BOX(w);
config.color_correction_id = (gchar *)gtk_combo_box_get_active_id(box);
if (GB_is_inited(&gb)) {
GB_set_color_correction_mode(&gb, get_color_correction_mode());
}
}
G_MODULE_EXPORT void on_frame_blending_changed(GtkWidget *w, gpointer user_data_ptr) {
GtkComboBox *box = GTK_COMBO_BOX(w);
config.frame_blending_mode = (gchar *)gtk_combo_box_get_active_id(box);
}
G_MODULE_EXPORT void on_display_border_changed(GtkWidget *w, gpointer user_data_ptr) {
GtkComboBox *box = GTK_COMBO_BOX(w);
config.display_border_mode = (gchar *)gtk_combo_box_get_active_id(box);
gui_data.border_mode_changed = true;
}
G_MODULE_EXPORT void on_monochrome_palette_changed(GtkWidget *w, gpointer user_data_ptr) {
GtkComboBox *box = GTK_COMBO_BOX(w);
config.monochrome_palette_id = (gchar *)gtk_combo_box_get_active_id(box);
GB_set_palette(&gb, get_monochrome_palette());
}
G_MODULE_EXPORT void on_color_menubar_override_changed(GtkWidget *w, gpointer user_data_ptr) {
config.menubar_override = (gchar *)gtk_combo_box_get_active_id(GTK_COMBO_BOX(w));
}
G_MODULE_EXPORT void on_dmg_model_changed(GtkWidget *w, gpointer user_data_ptr) {
GtkComboBox *box = GTK_COMBO_BOX(w);
config.dmg_revision_name = (gchar *)gtk_combo_box_get_active_id(box);
}
G_MODULE_EXPORT void on_graphic_filter_changed(GtkWidget *w, gpointer user_data_ptr) {
GtkComboBox *box = GTK_COMBO_BOX(w);
config.shader = (gchar *)gtk_combo_box_get_active_id(box);
free_shader(&gui_data.shader);
init_shader_with_name(&gui_data.shader, config.shader);
}
G_MODULE_EXPORT void on_highpass_filter_changed(GtkWidget *w, gpointer user_data_ptr) {
config.high_pass_filter_id = (gchar *)gtk_combo_box_get_active_id(GTK_COMBO_BOX(w));
if (GB_is_inited(&gb)) {
GB_set_highpass_filter_mode(&gb, get_highpass_mode());
}
}
G_MODULE_EXPORT void on_keep_aspect_ratio_changed(GtkWidget *w, gpointer user_data_ptr) {
GtkCheckButton *button = GTK_CHECK_BUTTON(w);
gboolean value = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(button));
config.keep_aspect_ratio = value;
update_viewport();
}
G_MODULE_EXPORT void on_rewind_duration_changed(GtkWidget *w, gpointer user_data_ptr) {
GtkComboBox *box = GTK_COMBO_BOX(w);
config.rewind_duration = g_ascii_strtoll(gtk_combo_box_get_active_id(box), NULL, 10);
GB_set_rewind_length(&gb, config.rewind_duration);
}
G_MODULE_EXPORT void on_sample_rate_changed(GtkWidget *w, gpointer user_data_ptr) {
GtkComboBox *box = GTK_COMBO_BOX(w);
config.sample_rate = g_ascii_strtoll(gtk_combo_box_get_active_id(box), NULL, 10);
if (config.sample_rate == -1) {
gui_data.sample_rate = GB_audio_default_sample_rate();
}
else {
gui_data.sample_rate = config.sample_rate;
}
init_audio();
}
G_MODULE_EXPORT void on_sgb_model_changed(GtkWidget *w, gpointer user_data_ptr) {
GtkComboBox *box = GTK_COMBO_BOX(w);
config.sgb_revision_name = (gchar *)gtk_combo_box_get_active_id(box);
}
G_MODULE_EXPORT void on_use_integer_scaling_changed(GtkWidget *w, gpointer user_data_ptr) {
GtkCheckButton *button = GTK_CHECK_BUTTON(w);
gboolean value = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(button));
config.use_integer_scaling = value;
update_viewport();
}
G_MODULE_EXPORT void console_on_enter(GtkWidget *w, gpointer user_data_ptr) {
GtkEntry *input = GTK_ENTRY(w);
const gchar *_text = gtk_entry_get_text(input);
gchar *text = g_strdup(_text);
if (g_strcmp0("", text) == 0 && g_strcmp0("", gui_data.last_console_input) < 0) {
text = g_strdup(gui_data.last_console_input);
}
else if (text) {
if (gui_data.last_console_input != NULL) g_free(gui_data.last_console_input);
gui_data.last_console_input = g_strdup(text);
}
if (!gui_data.in_sync_input) {
console_log(&gb, "> ", 0);
}
console_log(&gb, text, 0);
console_log(&gb, "\n", 0);
g_mutex_lock(&gui_data.debugger_input_mutex);
g_ptr_array_add(gui_data.debugger_input_queue, (gpointer)text);
g_cond_signal(&gui_data.debugger_input_cond);
g_mutex_unlock(&gui_data.debugger_input_mutex);
// clear input
gtk_entry_set_text(input, "");
}
int main(int argc, char *argv[]) {
gui_data.main_thread = g_thread_self();
// Create our GApplication and tell GTK that we are able to handle files
gui_data.main_application = gtk_application_new(APP_ID, G_APPLICATION_NON_UNIQUE | G_APPLICATION_HANDLES_OPEN);
// Define our command line parameters
GOptionEntry entries[] = {
{ "version", 'v', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, NULL, "Show the application version", NULL },
{ "fullscreen", 'f', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, NULL, "Start in fullscreen mode", NULL },
{ "bootrom", 'b', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &gui_data.cli_options.boot_rom_path, "Path to the boot ROM to use", "<file path>" },
{ "model", 'm', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, NULL, "Override the model type to emulate", "<model type>" },
{ "config", 'c', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &gui_data.cli_options.config_path, "Override the path of the configuration file", "<file path>" },
{ NULL }
};
// Setup our command line information
g_application_add_main_option_entries(G_APPLICATION(gui_data.main_application), entries);
g_application_set_option_context_parameter_string(G_APPLICATION(gui_data.main_application), "[FILE…]");
g_application_set_option_context_summary(G_APPLICATION(gui_data.main_application), "SameBoy is an open source Game Boy (DMG) and Game Boy Color (CGB) emulator.");
// Add signal handlers
g_signal_connect(gui_data.main_application, "handle-local-options", G_CALLBACK(handle_local_options), NULL);
g_signal_connect(gui_data.main_application, "startup", G_CALLBACK(startup), NULL);
g_signal_connect(gui_data.main_application, "activate", G_CALLBACK(activate), NULL);
g_signal_connect(gui_data.main_application, "open", G_CALLBACK(open), NULL);
g_signal_connect(gui_data.main_application, "shutdown", G_CALLBACK(shutdown), NULL);
// Start our GApplication main loop
int status = g_application_run(G_APPLICATION(gui_data.main_application), argc, argv);
g_object_unref(gui_data.main_application);
return status;
}