SameBoy/gtk3/main.c

657 lines
20 KiB
C
Raw Normal View History

2020-04-11 16:24:55 +00:00
#include <gtk/gtk.h>
#include <epoxy/gl.h>
2020-04-11 16:24:55 +00:00
#include <signal.h>
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#include <Core/gb.h>
#include "shader.h"
2020-04-11 16:24:55 +00:00
#define str(x) #x
#define xstr(x) str(x)
#define SETTINGS_FILE "sameboy-gtk3-settings.ini"
typedef struct UserData {
bool fullscreen;
GFile *file;
const gchar* bootrom_path;
GB_model_t model;
} UserData;
typedef struct{
int16_t x, y;
uint16_t w, h;
} Rect;
static void run(UserData *user_data);
static GKeyFile *key_file;
static GtkApplication *main_application;
static GtkBuilder *builder;
static GtkApplicationWindow *main_window;
static GtkGLArea *gl_area;
static shader_t shader;
static gchar* settings_file_path;
2020-04-11 16:24:55 +00:00
static GB_gameboy_t gb;
static uint32_t *image_buffers[3];
static unsigned char current_buffer;
static bool paused = false;
static bool underclock_down = false, rewind_down = false, do_rewind = false, rewind_paused = false, turbo_down = false;
static double clock_mutliplier = 1.0;
static char *battery_save_path_ptr;
static Rect rect;
static bool running = true;
static void save_settings(void) {
GError *error = NULL;
// Save as a file.
if (!g_key_file_save_to_file(key_file, settings_file_path, &error)) {
g_warning ("Error saving %s: %s", settings_file_path, error->message);
g_error_free(error);
return;
}
}
static unsigned char number_of_buffers(void) {
bool should_blend = true;
2020-04-11 16:24:55 +00:00
return should_blend? 3 : 2;
}
2020-04-11 16:24:55 +00:00
static void flip(void) {
current_buffer = (current_buffer + 1) % number_of_buffers();
}
static uint32_t *get_pixels(void) {
return image_buffers[(current_buffer + 1) % number_of_buffers()];
}
static uint32_t *get_current_buffer(void) {
return image_buffers[current_buffer];
}
static uint32_t *get_previous_buffer(void) {
return image_buffers[(current_buffer + 2) % number_of_buffers()];
}
static void render_texture(void *pixels, void *previous) {
static void *_pixels = NULL;
if (pixels) {
_pixels = pixels;
}
glClearColor(0, 0, 0, 1);
glClear(GL_COLOR_BUFFER_BIT);
render_bitmap_with_shader(&shader, _pixels, previous, GB_get_screen_width(&gb), GB_get_screen_height(&gb), rect.x, rect.y, rect.w, rect.h);
}
static void vblank(GB_gameboy_t *gb) {
flip();
GB_set_pixels_output(gb, get_pixels());
// Queue drawing of the current frame
gtk_gl_area_queue_render(gl_area);
while (gtk_events_pending()) {
gtk_main_iteration();
}
}
static void update_viewport(void) {
int win_width = gtk_widget_get_allocated_width(GTK_WIDGET(gl_area));
int win_height = gtk_widget_get_allocated_height(GTK_WIDGET(gl_area));
double x_factor = win_width / (double) GB_get_screen_width(&gb);
double y_factor = win_height / (double) GB_get_screen_height(&gb);
if (true /*configuration.scaling_mode == GB_SDL_SCALING_INTEGER_FACTOR*/) {
x_factor = (int)(x_factor);
y_factor = (int)(y_factor);
}
/*if (configuration.scaling_mode != GB_SDL_SCALING_ENTIRE_WINDOW) {
if (x_factor > y_factor) {
x_factor = y_factor;
}
else {
y_factor = x_factor;
}
}*/
unsigned new_width = x_factor * GB_get_screen_width(&gb);
unsigned new_height = y_factor * GB_get_screen_height(&gb);
rect = (Rect){(win_width - new_width) / 2, (win_height - new_height) / 2, new_width, new_height};
glViewport(rect.x, rect.y, rect.w, rect.h);
}
2020-04-11 16:24:55 +00:00
// 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;
}
// Recursively goes through all children of the given container and sets
// our `is_separator` function to all children of type`GtkComboBox`
static void set_combo_box_row_separator_func(GtkContainer *container) {
GList *list = gtk_container_get_children(container);
while (list) {
if (GTK_IS_COMBO_BOX(list->data)) {
gtk_combo_box_set_row_separator_func(GTK_COMBO_BOX(list->data), is_separator, NULL, NULL);
}
if (GTK_IS_CONTAINER(list->data)) {
set_combo_box_row_separator_func(GTK_CONTAINER(list->data));
}
list = list->next;
}
g_list_free_full(list, NULL);
}
// Returns true if the application should show a menubar
static gboolean show_menubar(void) {
GtkSettings *settings = gtk_settings_get_default();
gboolean result;
g_object_get(settings, "gtk-shell-shows-menubar", &result, NULL);
return result;
}
// Returns a GObject by ID from our GtkBuilder instance
static GObject *get_object(gchararray id) {
return gtk_builder_get_object(builder, id);
}
// Returns a `GApplication`s `GMenuModel` by ID
// GApplication menus are loaded from `gtk/menus.ui`, `gtk/menus-traditional.ui` and `gtk/menus-common.ui`.
static GMenuModel *get_menu_model(GApplication *app, const char *id) {
GMenu *menu;
menu = gtk_application_get_menu_by_id(GTK_APPLICATION(app), id);
return menu ? G_MENU_MODEL(g_object_ref_sink(menu)) : NULL;
}
static void quit(GApplication *app) {
g_application_quit(app);
running = false;
}
2020-04-11 16:24:55 +00:00
// app.quit GAction
// Exits the application
static void activate_quit(GSimpleAction *action, GVariant *parameter, gpointer user_data) {
quit(G_APPLICATION(user_data));
2020-04-11 16:24:55 +00:00
}
// app.about GAction
// Opens the about dialog
static void activate_about(GSimpleAction *action, GVariant *parameter, gpointer user_data) {
GObject *dialog = get_object("about_dialog");
gtk_dialog_run(GTK_DIALOG(dialog));
gtk_widget_hide(GTK_WIDGET(dialog));
}
// app.open_gtk_debugger GAction
// Opens the GTK debugger
static void activate_open_gtk_debugger(GSimpleAction *action, GVariant *parameter, gpointer user_data) {
gtk_window_set_interactive_debugging(true);
}
// app.preferences GAction
// Opens the preferences window
static void activate_preferences(GSimpleAction *action, GVariant *parameter, gpointer user_data) {
gtk_widget_show_all(GTK_WIDGET(get_object("preferences")));
}
2020-04-11 16:24:55 +00:00
// List of GActions for the `app` prefix
static GActionEntry app_entries[] = {
{ "quit", activate_quit, NULL, NULL, NULL },
{ "about", activate_about, NULL, NULL, NULL },
{ "open_gtk_debugger", activate_open_gtk_debugger, NULL, NULL, NULL },
{ "preferences", activate_preferences, NULL, NULL, NULL },
2020-04-11 16:24:55 +00:00
};
G_MODULE_EXPORT void on_quit(GtkWidget *w, gpointer app) {
quit(G_APPLICATION(app));
}
2020-04-11 16:24:55 +00:00
G_MODULE_EXPORT void on_show_window(GtkWidget *w, gpointer window) {
gtk_widget_show_all(GTK_WIDGET(window));
}
G_MODULE_EXPORT void on_boot_rom_location_changed(GtkWidget *w, gpointer user_data_gptr) {
GtkComboBox *box = GTK_COMBO_BOX(w);
g_print("Active: %s", gtk_combo_box_get_active_id(box));
}
G_MODULE_EXPORT void gl_init() {
const char *renderer;
gtk_gl_area_make_current(gl_area);
if (gtk_gl_area_get_error(gl_area) != NULL) {
return;
}
renderer = (char *) glGetString(GL_RENDERER);
g_print("GtkGLArea on %s\n", renderer ? renderer : "Unknown");
if (!init_shader_with_name(&shader, /*configuration.filter*/ "OmniScale")) {
init_shader_with_name(&shader, "NearestNeighbor");
}
}
G_MODULE_EXPORT void gl_resize() {
update_viewport();
}
G_MODULE_EXPORT void gl_draw() {
render_texture(get_current_buffer(), get_previous_buffer());
}
G_MODULE_EXPORT void gl_finish() { }
2020-04-11 16:24:55 +00:00
// This functions gets called immediately after registration of the GApplication
static void startup(GApplication *app, gpointer user_data_gptr) {
// UserData *user_data = user_data_gptr;
builder = gtk_builder_new_from_resource(RESOURCE_PREFIX "ui/window.ui");
gtk_builder_connect_signals(builder, NULL);
g_action_map_add_action_entries(G_ACTION_MAP(app), app_entries, G_N_ELEMENTS(app_entries), app);
GtkWindow *preferences = GTK_WINDOW(get_object("preferences"));
set_combo_box_row_separator_func(GTK_CONTAINER(preferences));
GtkWindow *vram_viewer = GTK_WINDOW(get_object("vram_viewer"));
set_combo_box_row_separator_func(GTK_CONTAINER(vram_viewer));
// setup main window
main_window = GTK_APPLICATION_WINDOW(gtk_application_window_new(GTK_APPLICATION(app)));
gtk_application_window_set_show_menubar(main_window, true);
2020-04-11 16:24:55 +00:00
// create our renderer area
gl_area = GTK_GL_AREA(gtk_gl_area_new());
gtk_gl_area_set_auto_render(gl_area, false);
g_signal_connect(gl_area, "realize", G_CALLBACK(gl_init), NULL);
g_signal_connect(gl_area, "render", G_CALLBACK(gl_draw), NULL);
g_signal_connect(gl_area, "resize", G_CALLBACK(gl_resize), NULL);
g_signal_connect(gl_area, "unrealize", G_CALLBACK(gl_finish), NULL);
gtk_container_add(GTK_CONTAINER(main_window), GTK_WIDGET(gl_area));
2020-04-11 16:24:55 +00:00
GError *error = NULL;
settings_file_path = g_build_filename(g_get_user_config_dir(), SETTINGS_FILE, NULL);
key_file = g_key_file_new();
g_print("Trying to load settings from %s\n", settings_file_path);
if (!g_key_file_load_from_file(key_file, settings_file_path, G_KEY_FILE_KEEP_COMMENTS | G_KEY_FILE_KEEP_TRANSLATIONS, &error)) {
if (!g_error_matches(error, G_FILE_ERROR, G_FILE_ERROR_NOENT)) {
g_warning("Error loading %s: %s", settings_file_path, error->message);
}
g_error_free(error);
}
// Handle the whole menubar situation …
if (show_menubar()) {
// Show a classic menubar
2020-04-11 16:24:55 +00:00
GMenuModel *menubar = get_menu_model(app, "menubar");
gtk_application_set_menubar(GTK_APPLICATION(app), menubar);
}
else {
// Attach a custom title bar
GtkWidget *titlebar = GTK_WIDGET(gtk_builder_get_object(builder, "main_header_bar"));
gtk_window_set_titlebar(GTK_WINDOW(main_window), titlebar);
2020-04-11 16:24:55 +00:00
// Disable menubar
gtk_application_set_menubar(GTK_APPLICATION(app), NULL);
// Hook menubar up to the hamburger button
GtkMenuButton *hamburger_button = GTK_MENU_BUTTON(get_object("hamburger_button"));
GMenuModel *hamburger_menu = get_menu_model(app, "menubar");
gtk_menu_button_set_menu_model(hamburger_button, hamburger_menu);
}
gtk_window_set_title(GTK_WINDOW(main_window), "SameBoy v" xstr(VERSION));
2020-04-11 16:24:55 +00:00
// 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);
2020-04-11 16:24:55 +00:00
// 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));
2020-04-11 16:24:55 +00:00
}
g_signal_connect(main_window, "destroy", G_CALLBACK(on_quit), app);
gtk_application_add_window(GTK_APPLICATION(app), GTK_WINDOW(main_window));
2020-04-11 16:24:55 +00:00
gtk_widget_show_all(GTK_WIDGET(main_window));
// Start the emulation loop.
// This loop takes care of the GTK main loop.
run(user_data);
2020-04-11 16:24:55 +00:00
}
// 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);
2020-04-11 16:24:55 +00:00
}
user_data->file = files[0];
// We have handled the files, now activate the application
activate(app, user_data_gptr);
}
static void shutdown(GApplication *app, GFile **files, gint n_files, const gchar *hint, gpointer user_data_gptr) {
g_print("SHUTDOWN\n");
save_settings();
}
2020-04-11 16:24:55 +00:00
// 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);
}
}
2020-04-11 16:24:55 +00:00
return -1;
}
static uint32_t rgb_encode(GB_gameboy_t *gb, uint8_t r, uint8_t g, uint8_t b) {
// We use GL_RGBA and GL_UNSIGNED_BYTE for our texture upload,
// so OpenGL expects pixel data in RGBA order in memory.
#ifdef GB_LITTLE_ENDIAN
// ABGR
uint32_t color = 0xFF000000 | (b << 16) | (g << 8) | r;
#else
// RGBA
uint32_t color = (r << 24) | (g << 16) | (b << 8) | 0xFF;
#endif
return color;
}
static void update_window_geometry() {
// Set size hints
GdkGeometry hints;
hints.min_width = GB_get_screen_width(&gb);
hints.min_height = GB_get_screen_height(&gb);
gtk_window_set_geometry_hints(
GTK_WINDOW(main_window),
NULL,
&hints,
(GdkWindowHints)(GDK_HINT_MIN_SIZE)
);
gtk_window_resize(GTK_WINDOW(main_window),
GB_get_screen_width(&gb) * 2,
GB_get_screen_height(&gb) * 2
);
// 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 run(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, configuration.color_correction_mode);
// GB_set_highpass_filter_mode(&gb, configuration.highpass_mode);
// GB_set_rewind_length(&gb, configuration.rewind_length);
// GB_set_update_input_hint_callback(&gb, handle_events);
// GB_apu_set_sample_callback(&gb, gb_audio_callback);
}
GError *error;
char *boot_rom_path;
GBytes *boot_rom_f;
const guchar *boot_rom_data;
gsize boot_rom_size;
if (user_data->bootrom_path) {
g_print("Trying to load boot ROM from %s\n", user_data->bootrom_path);
if (GB_load_boot_rom(&gb, user_data->bootrom_path)) {
g_printerr("Falling back to internal boot ROM\n");
goto internal_bootrom;
}
}
else { internal_bootrom:
switch (model) {
case GB_MODEL_DMG_B:
boot_rom_path = RESOURCE_PREFIX "bootroms/dmg_boot.bin";
break;
case GB_MODEL_SGB:
case GB_MODEL_SGB_PAL:
case GB_MODEL_SGB_NO_SFC:
boot_rom_path = RESOURCE_PREFIX "bootroms/sgb_boot.bin";
break;
case GB_MODEL_SGB2:
case GB_MODEL_SGB2_NO_SFC:
boot_rom_path = RESOURCE_PREFIX "bootroms/sgb2_boot.bin";
break;
case GB_MODEL_CGB_C:
case GB_MODEL_CGB_E:
boot_rom_path = RESOURCE_PREFIX "bootroms/cgb_boot.bin";
break;
case GB_MODEL_AGB:
boot_rom_path = RESOURCE_PREFIX "bootroms/agb_boot.bin";
break;
}
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));
}
/* 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);
}
}
}
int main(int argc, char *argv[]) {
2020-04-11 16:24:55 +00:00
// Create our GApplication and tell GTK that we are able to handle files
main_application = gtk_application_new(APP_ID, G_APPLICATION_HANDLES_OPEN);
2020-04-11 16:24:55 +00:00
UserData user_data = { NULL };
// Define our command line parameters
GOptionEntry entries[] = {
{ "version", 'v', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, NULL, "Show the application version", NULL },
{ "fullscreen", 'f', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, NULL, "Start in fullscreen mode", NULL },
{ "bootrom", 'b', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &user_data.bootrom_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>" },
2020-04-11 16:24:55 +00:00
{ 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.");
2020-04-11 16:24:55 +00:00
// 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);
2020-04-11 16:24:55 +00:00
// Start our GApplication main loop
int status = g_application_run(G_APPLICATION(main_application), argc, argv);
g_object_unref(main_application);
2020-04-11 16:24:55 +00:00
return status;
}