SameBoy/gtk3/main.c

1148 lines
38 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.

#include "main.h"
static GtkApplication *main_application;
static GtkBuilder *builder;
static GtkGLArea *gl_area;
static GtkDrawingArea *fallback_canvas;
static GtkApplicationWindow *main_window;
static GtkBox *main_window_container;
static GtkWindow *preferences;
static GtkWindow *vram_viewer;
static GtkWindow *memory_viewer;
static GtkWindow *console;
static GtkWindow *printer;
static shader_t shader;
static UserData user_data = { NULL };
static GB_gameboy_t gb;
static uint32_t *image_buffers[3];
static unsigned char current_buffer;
static bool supports_gl;
static bool paused = false;
static bool underclock_down = false, rewind_down = false, do_rewind = false, rewind_paused = false, turbo_down = false;
static double clock_mutliplier = 1.0;
static char *battery_save_path_ptr;
static Rect viewport = {0};
static Rect scrollRect = {0};
static bool vram_viewer_visible = false;
static bool running = true;
#define tileset_buffer_length 256 * 192 * 4
static uint32_t tileset_buffer[tileset_buffer_length] = {0};
#define tilemap_buffer_length 256 * 256 * 4
static uint32_t tilemap_buffer[tilemap_buffer_length] = {0};
static uint8_t pressed_buttons;
// List of GActions for the `app` prefix
static const GActionEntry app_entries[] = {
{ "quit", activate_quit, NULL, NULL, NULL },
{ "about", activate_about, NULL, NULL, NULL },
{ "open", activate_open, NULL, NULL, NULL },
{ "open_gtk_debugger", activate_open_gtk_debugger, NULL, NULL, NULL },
{ "preferences", activate_preferences, NULL, NULL, NULL },
{ "open_vram_viewer", activate_open_vram_viewer, NULL, NULL, NULL },
{ "open_memory_viewer", activate_open_memory_viewer, NULL, NULL, NULL },
};
int main(int argc, char *argv[]) {
// Create our GApplication and tell GTK that we are able to handle files
main_application = gtk_application_new(APP_ID, G_APPLICATION_NON_UNIQUE | G_APPLICATION_HANDLES_OPEN);
// Define our command line parameters
GOptionEntry entries[] = {
{ "version", 'v', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, NULL, "Show the application version", NULL },
{ "fullscreen", 'f', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, NULL, "Start in fullscreen mode", NULL },
{ "bootrom", 'b', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &user_data.boot_rom_path, "Path to the boot ROM to use", "<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, &user_data.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(main_application), entries);
g_application_set_option_context_parameter_string(G_APPLICATION(main_application), "[FILE…]");
g_application_set_option_context_summary(G_APPLICATION(main_application), "SameBoy is an open source Game Boy (DMG) and Game Boy Color (CGB) emulator.");
// Add signal handlers
g_signal_connect(main_application, "handle-local-options", G_CALLBACK(handle_local_options), &user_data);
g_signal_connect(main_application, "startup", G_CALLBACK(startup), &user_data);
g_signal_connect(main_application, "activate", G_CALLBACK(activate), &user_data);
g_signal_connect(main_application, "open", G_CALLBACK(open), &user_data);
g_signal_connect(main_application, "shutdown", G_CALLBACK(shutdown), &user_data);
// Start our GApplication main loop
int status = g_application_run(G_APPLICATION(main_application), argc, argv);
g_object_unref(main_application);
return status;
}
// This function gets called after the parsing of the commandline options has occurred.
static gint handle_local_options(GApplication *app, GVariantDict *options, gpointer user_data_gptr) {
UserData *user_data = user_data_gptr;
guint32 count;
if (g_variant_dict_lookup(options, "version", "b", &count)) {
g_print("SameBoy v" xstr(VERSION) "\n");
return EXIT_SUCCESS;
}
if (g_variant_dict_lookup(options, "fullscreen", "b", &count)) {
user_data->fullscreen = true;
}
// Handle model override
GVariant *model_name_var = g_variant_dict_lookup_value(options, "model", G_VARIANT_TYPE_STRING);
if (model_name_var != NULL) {
const gchar *model_name = g_variant_get_string(model_name_var, NULL);
// TODO: Synchronize with GB_model_t (Core/gb.h)
if (g_str_has_prefix(model_name, "DMG")) {
if (g_str_has_suffix(model_name, "-B") || g_strcmp0(model_name, "DMG") == 0) {
user_data->model = GB_MODEL_DMG_B;
}
else {
user_data->model = GB_MODEL_DMG_B;
g_printerr("Unsupported revision: %s\nFalling back to DMG-B", model_name);
}
}
else if (g_str_has_prefix(model_name, "SGB")) {
if (g_str_has_suffix(model_name, "-NTSC") || g_strcmp0(model_name, "SGB") == 0) {
user_data->model = GB_MODEL_SGB;
}
else if (g_str_has_suffix(model_name, "-PAL")) {
user_data->model = GB_MODEL_SGB | GB_MODEL_PAL_BIT;
}
else if (g_str_has_suffix(model_name, "2")) {
user_data->model = GB_MODEL_SGB2;
}
else {
user_data->model = GB_MODEL_SGB2;
g_printerr("Unsupported revision: %s\nFalling back to SGB2", model_name);
}
}
else if (g_str_has_prefix(model_name, "CGB")) {
if (g_str_has_suffix(model_name, "-C")) {
user_data->model = GB_MODEL_CGB_C;
}
else if (g_str_has_suffix(model_name, "-E") || g_strcmp0(model_name, "CGB") == 0) {
user_data->model = GB_MODEL_CGB_E;
}
else {
user_data->model = GB_MODEL_CGB_E;
g_printerr("Unsupported revision: %s\nFalling back to CGB-E", model_name);
}
}
else if (g_str_has_prefix(model_name, "AGB")) {
user_data->model = GB_MODEL_AGB;
}
else {
g_printerr("Unknown model: %s\n", model_name);
exit(EXIT_FAILURE);
}
}
return -1;
}
// Workaround to figure out if we have proper OpenGL support.
// Otherwise the application would crash after our GtkGlArea is realized
// and the context it uses is a legacy OpenGL 1.4 context because
// GTK3 calls OpenGL 2.0+ functions on it.
gboolean test_gl_support(void) {
gboolean result = FALSE;
GtkWidget *window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
g_signal_connect(window, "realize", G_CALLBACK(gl_check_realize), &result);
gtk_widget_realize(window);
gtk_widget_destroy(window);
window = NULL;
return result;
}
// The main function for the OpenGL version check workaround
void gl_check_realize(GtkWidget *w, gpointer user_data_gptr) {
gboolean *result = (gboolean *) user_data_gptr;
GError *error = NULL;
GdkWindow *gdk_window = gtk_widget_get_window(w);
GdkGLContext *context = gdk_window_create_gl_context(gdk_window, &error);
if (error != NULL) {
g_printerr("Failed to create context: %s\n", error->message);
g_error_free(error);
*result = FALSE;
}
else {
gdk_gl_context_make_current(context);
int version = epoxy_gl_version();
g_object_run_dispose(G_OBJECT(context));
g_object_unref(context);
context = NULL;
gdk_gl_context_clear_current();
g_print("OpenGL version: %d\n", version);
*result = version >= 32;
}
}
// Returns a `GApplication`s `GMenuModel` by ID
// GApplication menus are loaded from `gtk/menus.ui`, `gtk/menus-traditional.ui` and `gtk/menus-common.ui`.
static GMenuModel *get_menu_model(GApplication *app, const char *id) {
GMenu *menu;
menu = gtk_application_get_menu_by_id(GTK_APPLICATION(app), id);
return menu ? G_MENU_MODEL(g_object_ref_sink(menu)) : NULL;
}
// Create a GtkDrawingArea as a fallback in case we cant 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 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_print("XDG_CURRENT_DESKTOP: %s\nGDMSESSION: %s\nDESKTOP_SESSION: %s\nChosen value: %s\nShow menu in shell: %d\n", xdg_current_desktop, gdm_session, desktop_session, desktop, show_in_shell);
if (desktop != NULL && show_in_shell) {
menubar_type = MENUBAR_SHOW_IN_SHELL;
}
else if (desktop != NULL && g_str_match_string("GNOME", desktop, FALSE)) {
if (g_str_match_string("GNOME-Flashback", desktop, FALSE) || g_str_match_string("GNOME-Classic", desktop, FALSE)) {
menubar_type = MENUBAR_SHOW_IN_WINDOW;
}
else if (gdm_session != NULL && (g_str_match_string("gnome-classic", gdm_session, FALSE) || g_str_match_string("gnome-flashback", gdm_session, FALSE))) {
menubar_type = MENUBAR_SHOW_IN_WINDOW;
}
else {
menubar_type = MENUBAR_SHOW_HAMBURGER;
}
}
else {
menubar_type = MENUBAR_SHOW_IN_WINDOW;
}
}
switch (menubar_type) {
case MENUBAR_AUTO:
g_error("Unreachable\n");
break;
case MENUBAR_SHOW_IN_SHELL:
g_print("Showing menu in the shell\n");
gtk_application_set_menubar(GTK_APPLICATION(app), menubar_model);
break;
case MENUBAR_SHOW_IN_WINDOW: {
g_print("Showing menu in the window\n");
GtkMenuBar *menubar = GTK_MENU_BAR(gtk_menu_bar_new_from_model(menubar_model));
gtk_box_pack_start(GTK_BOX(main_window_container), GTK_WIDGET(menubar), FALSE, FALSE, 0);
break;
}
case MENUBAR_SHOW_HAMBURGER: {
g_print("Showing hamburger\n");
// Attach a custom title bar
GtkWidget *titlebar = builder_get(GTK_WIDGET, "main_header_bar");
gtk_header_bar_set_title(GTK_HEADER_BAR(titlebar), gtk_window_get_title(GTK_WINDOW(main_window)));
gtk_window_set_titlebar(GTK_WINDOW(main_window), titlebar);
// Disable menubar
gtk_application_set_menubar(GTK_APPLICATION(app), NULL);
// Hook menubar up to the hamburger button
GtkMenuButton *hamburger_button = GTK_MENU_BUTTON(get_object("hamburger_button"));
gtk_menu_button_set_menu_model(hamburger_button, menubar_model);
break;
}
}
}
// Recursively goes through all children of the given container and sets
// our `is_separator` function to all children of type`GtkComboBox`
static void set_combo_box_row_separator_func(GtkContainer *container) {
GList *list = gtk_container_get_children(container);
while (list) {
if (GTK_IS_COMBO_BOX(list->data)) {
gtk_combo_box_set_row_separator_func(GTK_COMBO_BOX(list->data), is_separator, NULL, NULL);
}
if (GTK_IS_CONTAINER(list->data)) {
set_combo_box_row_separator_func(GTK_CONTAINER(list->data));
}
list = list->next;
}
g_list_free_full(list, NULL);
}
// Determines if a ComboBox entry should be converted into a separator.
// Each element with a text value of `<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;
return result;
}
// Determines how many frame buffers to use
static unsigned char number_of_buffers(void) {
// TODO
bool should_blend = !fallback_canvas;
return should_blend? 3 : 2;
}
// Returns the buffer that should be used by the Core to render a new frame to
static uint32_t *get_pixels(void) {
return image_buffers[(current_buffer + 1) % number_of_buffers()];
}
// Returns the current finished frame
static uint32_t *get_current_buffer(void) {
return image_buffers[current_buffer];
}
// Returns the previous finished frame
static uint32_t *get_previous_buffer(void) {
return image_buffers[(current_buffer + 2) % number_of_buffers()];
}
// Cycles the buffers
static void flip(void) {
current_buffer = (current_buffer + 1) % number_of_buffers();
}
// This functions gets called immediately after registration of the GApplication
static void startup(GApplication *app, gpointer user_data_gptr) {
UserData *user_data = user_data_gptr;
// Very ugly workaround for GtkGlArea!
// When a GtkGlArea is realized and it creates a legacy GL 1.4 context
// it tries to use GL 2.0 functions to render the window which leads to the application crashing.
// So we initialize GTK, create a dummy GtkWindow object, attach a `realize` callback and
// in this callback create a GdkGLContext on this window. But instead of running the GTK main loop
// we just realize and destroy the dummy window and compare the contexts version in the realize callback.
supports_gl = test_gl_support();
g_print("OpenGL supported: %s\n", supports_gl? "Yes" : "No");
builder = gtk_builder_new_from_resource(RESOURCE_PREFIX "ui/window.ui");
gtk_builder_connect_signals(builder, NULL);
// Setup application actions
g_action_map_add_action_entries(G_ACTION_MAP(app), app_entries, G_N_ELEMENTS(app_entries), app);
#if NDEBUG
// Disable when not compiled in debug mode
g_simple_action_set_enabled(G_SIMPLE_ACTION(g_action_map_lookup_action(G_ACTION_MAP(app), "open_gtk_debugger")), false);
// Remove the menubar override
gtk_widget_destroy(builder_get(GTK_WIDGET, "menubar_override_selector_label"));
gtk_widget_destroy(builder_get(GTK_WIDGET, "menubar_override_selector"));
#endif
preferences = GTK_WINDOW(get_object("preferences"));
g_signal_connect(preferences, "realize", G_CALLBACK(on_preferences_realize), (gpointer) builder);
init_settings(user_data->config_path, preferences);
vram_viewer = GTK_WINDOW(get_object("vram_viewer"));
memory_viewer = GTK_WINDOW(get_object("memory_viewer"));
console = GTK_WINDOW(get_object("console"));
printer = GTK_WINDOW(get_object("printer"));
// setup main window
main_window = GTK_APPLICATION_WINDOW(gtk_application_window_new(GTK_APPLICATION(app)));
main_window_container = GTK_BOX(gtk_box_new(GTK_ORIENTATION_VERTICAL, 0));
gtk_window_set_title(GTK_WINDOW(main_window), "SameBoy");
gtk_application_window_set_show_menubar(main_window, false);
gtk_container_add(GTK_CONTAINER(main_window), GTK_WIDGET(main_window_container));
setup_menu(app);
// Insert separators into `GtkComboBox`es
set_combo_box_row_separator_func(GTK_CONTAINER(preferences));
set_combo_box_row_separator_func(GTK_CONTAINER(vram_viewer));
set_combo_box_row_separator_func(GTK_CONTAINER(memory_viewer));
// Define a set of window icons
GList *icon_list = NULL;
static char* icons[] = {
RESOURCE_PREFIX "logo_256.png",
RESOURCE_PREFIX "logo_128.png",
RESOURCE_PREFIX "logo_64.png",
RESOURCE_PREFIX "logo_48.png",
RESOURCE_PREFIX "logo_32.png",
RESOURCE_PREFIX "logo_16.png"
};
// Create list of GdkPixbufs
for (int i = 0; i < (sizeof(icons) / sizeof(const char*)); ++i) {
GdkPixbuf *icon = gdk_pixbuf_new_from_resource(icons[i], NULL);
if (!icon) continue;
icon_list = g_list_prepend(icon_list, icon);
}
// Let GTK choose the proper icon
gtk_window_set_icon_list(GTK_WINDOW(main_window), icon_list);
// Add missing information to the about dialog
GtkAboutDialog *about_dialog = GTK_ABOUT_DIALOG(get_object("about_dialog"));
gtk_about_dialog_set_logo(about_dialog, g_list_nth_data(icon_list, 3)); // reuse the 64x64 icon
gtk_about_dialog_set_version(about_dialog, "v" xstr(VERSION));
g_list_free(icon_list);
}
// This function gets called when the GApplication gets activated, i.e. it is ready to show widgets.
static void activate(GApplication *app, gpointer user_data_gptr) {
UserData *user_data = user_data_gptr;
if (user_data->fullscreen) {
gtk_window_fullscreen(GTK_WINDOW(main_window));
}
// Connect signal handlers
gtk_widget_add_events(GTK_WIDGET(main_window), GDK_KEY_PRESS_MASK);
gtk_widget_add_events(GTK_WIDGET(main_window), GDK_KEY_RELEASE_MASK);
g_signal_connect(main_window, "destroy", G_CALLBACK(on_quit), app);
g_signal_connect(main_window, "key_press_event", G_CALLBACK(on_key_press), NULL);
g_signal_connect(main_window, "key_release_event", G_CALLBACK(on_key_press), NULL);
g_signal_connect(vram_viewer, "realize", G_CALLBACK(on_vram_viewer_realize), NULL);
g_signal_connect(vram_viewer, "unrealize", G_CALLBACK(on_vram_viewer_unrealize), NULL);
// Just hide our sub-windows when closing them
g_signal_connect(preferences, "delete-event", G_CALLBACK(gtk_widget_hide_on_delete), NULL);
g_signal_connect(vram_viewer, "delete-event", G_CALLBACK(gtk_widget_hide_on_delete), NULL);
g_signal_connect(memory_viewer, "delete-event", G_CALLBACK(gtk_widget_hide_on_delete), NULL);
g_signal_connect(console, "delete-event", G_CALLBACK(gtk_widget_hide_on_delete), NULL);
g_signal_connect(printer, "delete-event", G_CALLBACK(gtk_widget_hide_on_delete), NULL);
g_signal_connect(get_object("vram_viewer_tileset_canvas"), "draw", G_CALLBACK(on_draw_vram_viewer_tileset), NULL);
g_signal_connect(get_object("vram_viewer_tilemap_canvas"), "draw", G_CALLBACK(on_draw_vram_viewer_tilemap), NULL);
// create our renderer area
if (supports_gl) {
gl_area = GTK_GL_AREA(gtk_gl_area_new());
gtk_gl_area_set_required_version(gl_area, 3, 2);
gtk_gl_area_set_auto_render(gl_area, false);
gtk_gl_area_set_has_alpha(gl_area, false);
gtk_gl_area_set_has_depth_buffer(gl_area, false);
gtk_gl_area_set_has_stencil_buffer(gl_area, false);
g_signal_connect(gl_area, "realize", G_CALLBACK(gl_init), NULL);
gtk_box_pack_end(GTK_BOX(main_window_container), GTK_WIDGET(gl_area), TRUE, TRUE, 0);
}
else {
create_fallback_canvas();
}
gtk_application_add_window(GTK_APPLICATION(app), GTK_WINDOW(main_window));
gtk_widget_show_all(GTK_WIDGET(main_window));
// Start the emulation loop.
// This loop takes care of the GTK main loop.
run(app, user_data);
}
// This function gets called when the application is closed.
static void shutdown(GApplication *app, GFile **files, gint n_files, const gchar *hint, gpointer user_data_gptr) {
g_print("SHUTDOWN\n");
save_settings();
free_settings();
}
// This function gets called when there are files to open.
// Note: When `open` gets called `activate` wont fire unless we call it ourselves.
static void open(GApplication *app, GFile **files, gint n_files, const gchar *hint, gpointer user_data_gptr) {
UserData *user_data = user_data_gptr;
if (n_files > 1) {
g_printerr("More than one file specified\n");
exit(EXIT_FAILURE);
}
user_data->file = files[0];
// We have handled the files, now activate the application
activate(app, user_data_gptr);
}
// Tell our application to quit.
// After this functions has been called the `shutdown` signal will be issued.
//
// TODO: Make sure we have a way to quit our emulation loop before `shutdown` gets called
static void quit(GApplication *app) {
// Tell our own main loop to quit.
// This will allow our run() and therefore our activate() methods to end.
running = false;
// Quit our application properly.
// This fires the “shutdown” signal.
g_application_quit(app);
}
static gboolean on_key_press(GtkWidget *w, GdkEventKey *event, gpointer data) {
uint8_t mask;
// TODO: Allow control remapping in the GUI
switch (event->keyval) {
case GDK_KEY_w: mask = BUTTON_MASK_UP; break;
case GDK_KEY_a: mask = BUTTON_MASK_LEFT; break;
case GDK_KEY_s: mask = BUTTON_MASK_DOWN; break;
case GDK_KEY_d: mask = BUTTON_MASK_RIGHT; break;
case GDK_KEY_g: mask = BUTTON_MASK_SELECT; break;
case GDK_KEY_h: mask = BUTTON_MASK_START; break;
case GDK_KEY_k: mask = BUTTON_MASK_B; break;
case GDK_KEY_l: mask = BUTTON_MASK_A; break;
}
if (event->type == GDK_KEY_PRESS) {
pressed_buttons |= mask;
}
else if (event->type == GDK_KEY_RELEASE) {
pressed_buttons &= ~mask;
}
return FALSE;
}
// app.about GAction
// Opens the about dialog
static void activate_about(GSimpleAction *action, GVariant *parameter, gpointer app) {
GObject *dialog = get_object("about_dialog");
gtk_dialog_run(GTK_DIALOG(dialog));
gtk_widget_hide(GTK_WIDGET(dialog));
}
// app.open_gtk_debugger GAction
// Opens the GTK debugger
static void activate_open_gtk_debugger(GSimpleAction *action, GVariant *parameter, gpointer app) {
gtk_window_set_interactive_debugging(true);
}
// app.open_memory_viewer GAction
// Opens the memory viewer window
static void activate_open_memory_viewer(GSimpleAction *action, GVariant *parameter, gpointer app) {
gtk_widget_show_all(GTK_WIDGET(memory_viewer));
}
// app.open_vram_viewer GAction
// Opens the VRAM viewer window
static void activate_open_vram_viewer(GSimpleAction *action, GVariant *parameter, gpointer app) {
gtk_widget_show_all(GTK_WIDGET(vram_viewer));
}
// app.open GAction
// Opens a ROM file
static void activate_open(GSimpleAction *action, GVariant *parameter, gpointer app) {
GtkFileChooserNative *native = gtk_file_chooser_native_new("Open File", GTK_WINDOW(main_window), GTK_FILE_CHOOSER_ACTION_OPEN, "_Open", "_Cancel");
gint res = gtk_native_dialog_run(GTK_NATIVE_DIALOG(native));
if (res == GTK_RESPONSE_ACCEPT) {
// TODO: Emit an event for our emulation loop
g_print("%s\n", gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(native)));
}
g_object_unref(native);
}
// app.preferences GAction
// Opens the preferences window
static void activate_preferences(GSimpleAction *action, GVariant *parameter, gpointer app) {
gtk_widget_show_all(GTK_WIDGET(get_object("preferences")));
}
// app.quit GAction
// Exits the application
static void activate_quit(GSimpleAction *action, GVariant *parameter, gpointer app) {
quit(G_APPLICATION(app));
}
// `destroy` signal GCallback
// Exits the application
static void on_quit(GtkWidget *w, gpointer app) {
quit(G_APPLICATION(app));
}
// TODO: Comment
static void gl_init(GtkWidget *w) {
GtkGLArea *gl_area = GTK_GL_AREA(w);
g_print("GL_INIT\n");
const char *renderer;
g_print("GL Context: %p\n", gtk_gl_area_get_context(gl_area));
gtk_gl_area_make_current(gl_area);
if (gtk_gl_area_get_error(gl_area) != NULL) {
goto error;
}
renderer = (char *)glGetString(GL_RENDERER);
g_print("GtkGLArea on %s\n", renderer ? renderer : "Unknown");
if (config.shader == NULL || (!init_shader_with_name(&shader, config.shader) && !init_shader_with_name(&shader, "NearestNeighbor"))) {
GError *error = g_error_new_literal(g_quark_from_string("sameboy-gl-error"), 1, "Failed to initialize shaders");
gtk_gl_area_set_error(gl_area, error);
}
else {
g_signal_connect(gl_area, "render", G_CALLBACK(gl_draw), NULL);
g_signal_connect(gl_area, "resize", G_CALLBACK(resize), NULL);
g_signal_connect(gl_area, "unrealize", G_CALLBACK(gl_finish), NULL);
return;
}
error:
if (gtk_gl_area_get_error(gl_area) != NULL) {
g_printerr("GtkGLArea: %s\n", gtk_gl_area_get_error(gl_area)->message);
}
create_fallback_canvas();
}
// TODO: Comment
static void gl_draw() {
render_texture(get_current_buffer(), get_previous_buffer());
}
// TODO: Comment
static void gl_finish() { }
// TODO: Comment
static gboolean on_draw_fallback(GtkWidget *widget, cairo_t *cr, gpointer data) {
GtkStyleContext *context = gtk_widget_get_style_context(widget);
guint width = gtk_widget_get_allocated_width(widget);
guint height = gtk_widget_get_allocated_height(widget);
guint screen_width = GB_get_screen_width(&gb);
guint screen_height = GB_get_screen_height(&gb);
gtk_render_background(context, cr, 0, 0, width, height);
cairo_surface_t *surface = cairo_image_surface_create_for_data(
(unsigned char *) get_current_buffer(),
CAIRO_FORMAT_RGB24,
screen_width,
screen_height,
cairo_format_stride_for_width(CAIRO_FORMAT_RGB24, screen_width)
);
cairo_translate(cr, viewport.x, viewport.y);
cairo_scale(cr, viewport.w / screen_width, viewport.h / screen_height);
cairo_set_source_surface(cr, surface, 0, 0);
cairo_pattern_set_filter(cairo_get_source(cr), CAIRO_FILTER_NEAREST);
cairo_paint(cr);
return FALSE;
}
// TODO: Comment
static void resize() {
update_viewport();
}
// Gets called when the VRAM viewer gets realized
static void on_vram_viewer_realize() {
vram_viewer_visible = true;
}
// Gets called when the VRAM viewer gets unrealized
static void on_vram_viewer_unrealize() {
vram_viewer_visible = false;
}
// Gets called when the tileset viewer should be redrawn
static gboolean on_draw_vram_viewer_tileset(GtkWidget *widget, cairo_t *cr, gpointer data) {
guint width, height;
GtkStyleContext *context;
context = gtk_widget_get_style_context(widget);
width = gtk_widget_get_allocated_width(widget);
height = gtk_widget_get_allocated_height(widget);
gtk_render_background(context, cr, 0, 0, width, height);
cairo_surface_t *surface = cairo_image_surface_create_for_data(
(unsigned char *) tileset_buffer,
CAIRO_FORMAT_RGB24,
256,
192,
cairo_format_stride_for_width(CAIRO_FORMAT_RGB24, 256)
);
cairo_scale(cr, 2.0, 2.0);
cairo_set_source_surface(cr, surface, 0, 0);
cairo_pattern_set_filter(cairo_get_source(cr), CAIRO_FILTER_NEAREST);
cairo_paint(cr);
if (gtk_toggle_button_get_active(builder_get(GTK_TOGGLE_BUTTON, "vram_viewer_tileset_toggle_grid_button"))) {
cairo_set_source_rgba(cr, 0.0, 0.0, 0.0, 0.25);
cairo_set_line_width(cr, 1);
const int divisions_x = 256 / 8;
const int divisions_y = 192 / 8;
for (int i = 0; i < divisions_x; i++) {
const int j = 256 * i;
cairo_move_to(cr, j / divisions_x, 0);
cairo_line_to(cr, j / divisions_x, 192);
}
for (int i = 0; i < divisions_y; i++) {
const int j = 192 * i;
cairo_move_to(cr, 0, j / divisions_y);
cairo_line_to(cr, 256, j / divisions_y);
}
cairo_stroke(cr);
}
return FALSE;
}
// Gets called when the tilemap viewer should be redrawn
static gboolean on_draw_vram_viewer_tilemap(GtkWidget *widget, cairo_t *cr, gpointer data) {
guint width, height;
GtkStyleContext *context;
context = gtk_widget_get_style_context(widget);
width = gtk_widget_get_allocated_width(widget);
height = gtk_widget_get_allocated_height(widget);
gtk_render_background(context, cr, 0, 0, width, height);
cairo_surface_t *surface = cairo_image_surface_create_for_data(
(unsigned char *) tilemap_buffer,
CAIRO_FORMAT_RGB24,
256,
256,
cairo_format_stride_for_width(CAIRO_FORMAT_RGB24, 256)
);
cairo_scale(cr, 2.0, 2.0);
cairo_set_source_surface(cr, surface, 0, 0);
cairo_pattern_set_filter(cairo_get_source(cr), CAIRO_FILTER_NEAREST);
cairo_paint(cr);
if (gtk_toggle_button_get_active(builder_get(GTK_TOGGLE_BUTTON, "vram_viewer_tilemap_toggle_grid_button"))) {
cairo_set_source_rgba(cr, 0.0, 0.0, 0.0, 0.25);
cairo_set_line_width(cr, 1);
const int divisions = 256 / 8;
for (int i = 0; i < divisions; i++) {
const int j = 256 * i;
cairo_move_to(cr, j / divisions, 0);
cairo_line_to(cr, j / divisions, 256);
cairo_move_to(cr, 0, j / divisions);
cairo_line_to(cr, 256, j / divisions);
}
cairo_stroke(cr);
}
if (gtk_toggle_button_get_active(builder_get(GTK_TOGGLE_BUTTON, "vram_viewer_tilemap_toggle_scrolling_button"))) {
cairo_rectangle(cr, 0, 0, width, height);
for (unsigned x = 0; x < 2; x++) {
for (unsigned y = 0; y < 2; y++) {
Rect rect = scrollRect;
rect.x -= 256 * x;
rect.y += 256 * y;
cairo_rectangle(cr, rect.x, rect.y, rect.w, rect.h);
}
}
cairo_set_fill_rule(cr, CAIRO_FILL_RULE_EVEN_ODD);
cairo_set_line_width(cr, 2);
cairo_set_line_join(cr, CAIRO_LINE_JOIN_ROUND);
cairo_set_source_rgba(cr, 0.2, 0.2, 0.2, 0.5);
cairo_fill_preserve(cr);
cairo_clip_preserve(cr);
cairo_set_source_rgba(cr, 0.0, 0.0, 0.0, 0.6);
cairo_stroke(cr);
}
return FALSE;
}
G_MODULE_EXPORT void on_boot_rom_location_changed(GtkWidget *w, gpointer user_data_gptr) {
GtkComboBox *box = GTK_COMBO_BOX(w);
const gchar *id = gtk_combo_box_get_active_id(box);
if (id == NULL) return;
if (g_strcmp0(id, "other") == 0) {
GtkFileChooserNative *native = gtk_file_chooser_native_new("Select Folder", preferences, GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER, "_Select", "_Cancel");
gint res = gtk_native_dialog_run(GTK_NATIVE_DIALOG(native));
if (res == GTK_RESPONSE_ACCEPT) {
config.boot_rom_path = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(native));
update_boot_rom_selector(builder);
}
g_object_unref(native);
}
else {
config.boot_rom_path = (gchar *)id;
}
}
G_MODULE_EXPORT void on_cgb_model_changed(GtkWidget *w, gpointer user_data_gptr) {
GtkComboBox *box = GTK_COMBO_BOX(w);
config.cgb_revision_name = (gchar *)gtk_combo_box_get_active_id(box);
}
G_MODULE_EXPORT void on_color_correction_changed(GtkWidget *w, gpointer user_data_gptr) {
GtkComboBox *box = GTK_COMBO_BOX(w);
config.color_correction_id = (gchar *)gtk_combo_box_get_active_id(box);
if (GB_is_inited(&gb)) {
GB_set_color_correction_mode(&gb, get_color_correction_mode());
}
}
G_MODULE_EXPORT void on_color_menubar_override_changed(GtkWidget *w, gpointer user_data_gptr) {
config.menubar_override = (gchar *)gtk_combo_box_get_active_id(GTK_COMBO_BOX(w));
}
G_MODULE_EXPORT void on_dmg_model_changed(GtkWidget *w, gpointer user_data_gptr) {
GtkComboBox *box = GTK_COMBO_BOX(w);
config.dmg_revision_name = (gchar *)gtk_combo_box_get_active_id(box);
}
G_MODULE_EXPORT void on_graphic_filter_changed(GtkWidget *w, gpointer user_data_gptr) {
GtkComboBox *box = GTK_COMBO_BOX(w);
config.shader = (gchar *)gtk_combo_box_get_active_id(box);
init_shader_with_name(&shader, config.shader);
}
G_MODULE_EXPORT void on_highpass_filter_changed(GtkWidget *w, gpointer user_data_gptr) {
config.high_pass_filter_id = (gchar *)gtk_combo_box_get_active_id(GTK_COMBO_BOX(w));
if (GB_is_inited(&gb)) {
GB_set_highpass_filter_mode(&gb, get_highpass_mode());
}
}
G_MODULE_EXPORT void on_keep_aspect_ratio_changed(GtkWidget *w, gpointer user_data_gptr) {
GtkCheckButton *button = GTK_CHECK_BUTTON(w);
gboolean value = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(button));
config.keep_aspect_ratio = value;
update_viewport();
}
G_MODULE_EXPORT void on_rewind_duration_changed(GtkWidget *w, gpointer user_data_gptr) {
GtkComboBox *box = GTK_COMBO_BOX(w);
config.rewind_duration = g_ascii_strtoll(gtk_combo_box_get_active_id(box), NULL, 10);
}
G_MODULE_EXPORT void on_sgb_model_changed(GtkWidget *w, gpointer user_data_gptr) {
GtkComboBox *box = GTK_COMBO_BOX(w);
config.sgb_revision_name = (gchar *)gtk_combo_box_get_active_id(box);
}
G_MODULE_EXPORT void on_use_integer_scaling_changed(GtkWidget *w, gpointer user_data_gptr) {
GtkCheckButton *button = GTK_CHECK_BUTTON(w);
gboolean value = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(button));
config.use_integer_scaling = value;
update_viewport();
}
static uint32_t rgb_encode(GB_gameboy_t *gb, uint8_t r, uint8_t g, uint8_t b) {
return 0xFF000000 | (r << 16) | (g << 8) | b;
}
static void render_texture(void *pixels, void *previous) {
static void *_pixels = NULL;
if (pixels) {
_pixels = pixels;
}
glClearColor(0, 0, 0, 1);
glClear(GL_COLOR_BUFFER_BIT);
render_bitmap_with_shader(&shader, _pixels, previous, GB_get_screen_width(&gb), GB_get_screen_height(&gb), viewport.x, viewport.y, viewport.w, viewport.h);
}
static void update_viewport(void) {
GtkWidget *w = fallback_canvas ? GTK_WIDGET(fallback_canvas) : GTK_WIDGET(gl_area);
int win_width = gtk_widget_get_allocated_width(w);
int win_height = gtk_widget_get_allocated_height(w);
double x_factor = win_width / (double) GB_get_screen_width(&gb);
double y_factor = win_height / (double) GB_get_screen_height(&gb);
if (config.use_integer_scaling) {
x_factor = (int)(x_factor);
y_factor = (int)(y_factor);
}
if (config.keep_aspect_ratio) {
if (x_factor > y_factor) {
x_factor = y_factor;
}
else {
y_factor = x_factor;
}
}
unsigned new_width = x_factor * GB_get_screen_width(&gb);
unsigned new_height = y_factor * GB_get_screen_height(&gb);
viewport = (Rect){
(win_width - new_width) / 2,
(win_height - new_height) / 2,
new_width,
new_height
};
if (!fallback_canvas) glViewport(viewport.x, viewport.y, viewport.w, viewport.h);
}
static void update_window_geometry() {
// Set size hints
GdkGeometry hints;
hints.min_width = GB_get_screen_width(&gb);
hints.min_height = GB_get_screen_height(&gb);
gtk_window_set_geometry_hints(
GTK_WINDOW(main_window),
NULL,
&hints,
(GdkWindowHints)(GDK_HINT_MIN_SIZE)
);
gtk_window_resize(GTK_WINDOW(main_window),
GB_get_screen_width(&gb) * 2,
GB_get_screen_height(&gb) * 2
);
// Setup our image buffers
if (image_buffers[0]) free(image_buffers[0]);
if (image_buffers[1]) free(image_buffers[1]);
if (image_buffers[2]) free(image_buffers[2]);
size_t buffer_size = sizeof(image_buffers[0][0]) * GB_get_screen_width(&gb) * GB_get_screen_height(&gb);
image_buffers[0] = malloc(buffer_size);
image_buffers[1] = malloc(buffer_size);
image_buffers[2] = malloc(buffer_size);
}
static void handle_events(GB_gameboy_t *gb) {
while (gtk_events_pending()) {
gtk_main_iteration();
}
GB_set_key_state(gb, GB_KEY_RIGHT, pressed_buttons & BUTTON_MASK_RIGHT);
GB_set_key_state(gb, GB_KEY_LEFT, pressed_buttons & BUTTON_MASK_LEFT);
GB_set_key_state(gb, GB_KEY_UP, pressed_buttons & BUTTON_MASK_UP);
GB_set_key_state(gb, GB_KEY_DOWN, pressed_buttons & BUTTON_MASK_DOWN);
GB_set_key_state(gb, GB_KEY_A, pressed_buttons & BUTTON_MASK_A);
GB_set_key_state(gb, GB_KEY_B, pressed_buttons & BUTTON_MASK_B);
GB_set_key_state(gb, GB_KEY_SELECT, pressed_buttons & BUTTON_MASK_SELECT);
GB_set_key_state(gb, GB_KEY_START, pressed_buttons & BUTTON_MASK_START);
}
static void vblank(GB_gameboy_t *gb) {
flip();
GB_set_pixels_output(gb, get_pixels());
// Queue drawing of the current frame
if (fallback_canvas) {
gtk_widget_queue_draw(GTK_WIDGET(main_window));
}
else if (gl_area) {
gtk_gl_area_queue_render(gl_area);
}
if (vram_viewer_visible) {
// TODO: Only update what is needed
GB_draw_tileset(gb, tileset_buffer, GB_PALETTE_NONE, 0);
GB_draw_tilemap(gb, tilemap_buffer, GB_PALETTE_AUTO, 0, GB_MAP_AUTO, GB_TILESET_AUTO);
scrollRect = (Rect){
GB_read_memory(gb, 0xFF00 | GB_IO_SCX),
GB_read_memory(gb, 0xFF00 | GB_IO_SCY),
160, 144
};
// Queue a redraw of the VRAM viewer
gtk_widget_queue_draw(GTK_WIDGET(vram_viewer));
}
}
static void run(GApplication *app, UserData *user_data) {
GB_model_t prev_model = GB_get_model(&gb);
GB_model_t model = user_data->model? user_data->model : GB_MODEL_CGB_E; // TODO: Model from config
if (GB_is_inited(&gb)) {
GB_switch_model_and_reset(&gb, model);
GtkRequisition minimum_size;
GtkRequisition natural_size;
gtk_widget_get_preferred_size(GTK_WIDGET(main_window), &minimum_size, &natural_size);
// Check SGB -> non-SGB and non-SGB to SGB transitions
if (GB_get_screen_width(&gb) != minimum_size.width || GB_get_screen_height(&gb) != minimum_size.height) {
update_window_geometry();
}
}
else {
GB_init(&gb, model);
update_window_geometry();
GB_set_vblank_callback(&gb, (GB_vblank_callback_t) vblank);
GB_set_pixels_output(&gb, get_current_buffer());
GB_set_rgb_encode_callback(&gb, rgb_encode);
// GB_set_sample_rate(&gb, have_aspec.freq);
GB_set_color_correction_mode(&gb, get_color_correction_mode());
GB_set_highpass_filter_mode(&gb, get_highpass_mode());
GB_set_rewind_length(&gb, config.rewind_duration);
GB_set_update_input_hint_callback(&gb, handle_events);
// GB_apu_set_sample_callback(&gb, gb_audio_callback);
}
GError *error;
char *boot_rom_path;
char *boot_rom_name;
GBytes *boot_rom_f;
const guchar *boot_rom_data;
gsize boot_rom_size;
if (user_data->boot_rom_path != NULL) {
g_print("Trying to load boot ROM from %s\n", user_data->boot_rom_path);
if (GB_load_boot_rom(&gb, user_data->boot_rom_path)) {
g_printerr("Falling back to boot ROM from config\n");
goto config_boot_rom;
}
}
else { config_boot_rom:
switch (model) {
case GB_MODEL_DMG_B:
boot_rom_name = "dmg_boot.bin";
break;
case GB_MODEL_SGB:
case GB_MODEL_SGB_PAL:
case GB_MODEL_SGB_NO_SFC:
boot_rom_name = "sgb_boot.bin";
break;
case GB_MODEL_SGB2:
case GB_MODEL_SGB2_NO_SFC:
boot_rom_name = "sgb2_boot.bin";
break;
case GB_MODEL_CGB_C:
case GB_MODEL_CGB_E:
boot_rom_name = "cgb_boot.bin";
break;
case GB_MODEL_AGB:
boot_rom_name = "agb_boot.bin";
break;
}
if (config.boot_rom_path != NULL && g_strcmp0(config.boot_rom_path, "other") != 0 && g_strcmp0(config.boot_rom_path, "auto") != 0) {
boot_rom_path = g_build_filename(config.boot_rom_path, boot_rom_name, NULL);
g_print("Trying to load boot ROM from %s\n", boot_rom_path);
if (GB_load_boot_rom(&gb, boot_rom_path)) {
g_printerr("Falling back to internal boot ROM\n");
goto internal_boot_rom;
}
}
else { internal_boot_rom:
boot_rom_path = g_build_filename(RESOURCE_PREFIX "bootroms/", boot_rom_name, NULL);
boot_rom_f = g_resources_lookup_data(boot_rom_path, G_RESOURCE_LOOKUP_FLAGS_NONE, &error);
if (boot_rom_f == NULL) {
g_printerr("Failed to load internal boot ROM: %s\n", boot_rom_path);
g_error_free(error);
exit(EXIT_FAILURE);
}
boot_rom_data = g_bytes_get_data(boot_rom_f, &boot_rom_size);
GB_load_boot_rom_from_buffer(&gb, boot_rom_data, boot_rom_size);
}
}
if (user_data->file != NULL && GB_load_rom(&gb, g_file_get_path(user_data->file)) == 0) {
/* Run emulation */
while (running) {
if (paused || rewind_paused) {
while (gtk_events_pending()) {
gtk_main_iteration();
}
}
else {
if (do_rewind) {
GB_rewind_pop(&gb);
if (turbo_down) {
GB_rewind_pop(&gb);
}
if (!GB_rewind_pop(&gb)) {
rewind_paused = true;
}
do_rewind = false;
}
GB_run(&gb);
}
}
}
}