SameBoy/gtk3/main.c

1585 lines
52 KiB
C
Raw Normal View History

2019-09-30 14:40:55 +00:00
#include "main.h"
2019-10-05 23:13:45 +00:00
static ALCdevice *al_device;
static ALCcontext *al_context;
static ALuint al_source;
static ALuint al_buffer[96000*2];
static ALuint next_buffer = 0;
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;
2019-09-21 19:56:19 +00:00
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;
2019-10-01 17:56:53 +00:00
#define tileset_buffer_length 256 * 192 * 4
static uint32_t tileset_buffer[tileset_buffer_length] = {0};
2019-10-01 17:56:53 +00:00
#define tilemap_buffer_length 256 * 256 * 4
static uint32_t tilemap_buffer[tilemap_buffer_length] = {0};
2019-10-03 23:40:50 +00:00
static GB_oam_info_t oamInfo[40];
static uint16_t oamCount;
static uint8_t oamHeight;
2019-09-30 14:40:55 +00:00
static uint8_t pressed_buttons;
2019-09-30 14:40:55 +00:00
// 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 },
};
2019-09-30 14:40:55 +00:00
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);
2019-09-30 14:40:55 +00:00
// 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.");
2019-09-30 14:40: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);
2019-09-30 14:40:55 +00:00
// Start our GApplication main loop
int status = g_application_run(G_APPLICATION(main_application), argc, argv);
g_object_unref(main_application);
2019-09-30 14:40:55 +00:00
return status;
}
2019-09-30 14:40: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;
2019-09-30 14:40:55 +00:00
if (g_variant_dict_lookup(options, "version", "b", &count)) {
g_print("SameBoy v" xstr(VERSION) "\n");
return EXIT_SUCCESS;
}
2019-09-30 14:40:55 +00:00
if (g_variant_dict_lookup(options, "fullscreen", "b", &count)) {
user_data->fullscreen = true;
}
2019-09-30 14:40:55 +00:00
// 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);
2019-09-30 14:40:55 +00:00
// 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;
2019-09-24 18:22:07 +00:00
}
else {
2019-09-30 14:40:55 +00:00
g_printerr("Unknown model: %s\n", model_name);
exit(EXIT_FAILURE);
2019-09-24 18:22:07 +00:00
}
}
2019-09-30 14:40:55 +00:00
return -1;
}
2019-09-30 14:40:55 +00:00
// 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;
2019-09-21 19:56:19 +00:00
2019-09-30 14:40:55 +00:00
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;
2019-09-21 19:56:19 +00:00
return result;
}
2019-09-30 14:40:55 +00:00
// 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;
2019-09-21 19:56:19 +00:00
2019-09-30 14:40:55 +00:00
GError *error = NULL;
GdkWindow *gdk_window = gtk_widget_get_window(w);
GdkGLContext *context = gdk_window_create_gl_context(gdk_window, &error);
2019-09-21 19:56:19 +00:00
2019-09-30 14:40:55 +00:00
if (error != NULL) {
g_printerr("Failed to create context: %s\n", error->message);
g_error_free(error);
*result = FALSE;
2019-09-21 19:56:19 +00:00
}
2019-09-30 14:40:55 +00:00
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;
2019-09-21 19:56:19 +00:00
2019-09-30 14:40:55 +00:00
gdk_gl_context_clear_current();
g_print("OpenGL version: %d\n", version);
*result = version >= 32;
}
2019-09-21 19:56:19 +00:00
}
2019-10-05 23:13:45 +00:00
static void setup_audio() {
#define RETURN_IF_AL_ERROR(msg) { \
GError *error = openal_error(msg); \
if (error) { \
g_printerr("OpenAL: (%d) %s\n", error->code, error->message); \
g_clear_error(&error); \
return; \
} \
}
al_device = alcOpenDevice(NULL);
if (!al_device) RETURN_IF_AL_ERROR("Failed to open device")
al_context = alcCreateContext(al_device, NULL);
if (!alcMakeContextCurrent(al_context)) RETURN_IF_AL_ERROR("Failed to create context")
alGenSources((ALuint)1, &al_source);
RETURN_IF_AL_ERROR(NULL)
alSourcef(al_source, AL_PITCH, 1);
RETURN_IF_AL_ERROR(NULL)
alSourcef(al_source, AL_GAIN, 1);
RETURN_IF_AL_ERROR(NULL)
alSource3f(al_source, AL_POSITION, 0, 0, 0);
RETURN_IF_AL_ERROR(NULL)
alSource3f(al_source, AL_VELOCITY, 0, 0, 0);
RETURN_IF_AL_ERROR(NULL)
alSourcei(al_source, AL_LOOPING, AL_FALSE);
RETURN_IF_AL_ERROR(NULL)
alSourcei(al_source, AL_SOURCE_TYPE, AL_STREAMING);
RETURN_IF_AL_ERROR(NULL)
alGenBuffers((ALuint)96000*2, al_buffer);
RETURN_IF_AL_ERROR("Failed to create audio buffer")
}
static void gb_audio_callback(GB_gameboy_t *gb, GB_sample_t *sample) {
alBufferData(al_buffer[next_buffer], AL_FORMAT_STEREO16, sample, 4, 96000);
GError *error = openal_error("alBufferData");
if (error) {
g_printerr("OpenAL: (%d) %s\n", error->code, error->message);
g_clear_error(&error);
}
alSourceQueueBuffers(al_source, 1, &al_buffer[next_buffer]);
error = openal_error("alSourceQueueBuffers");
if (error) {
g_printerr("OpenAL: (%d) %s\n", error->code, error->message);
g_clear_error(&error);
}
next_buffer = (next_buffer + 1) % (96000*2);
}
2019-09-21 19:56:19 +00:00
// 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;
}
2019-09-30 14:40:55 +00:00
// 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);
}
2019-09-30 14:40:55 +00:00
// 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();
2019-09-21 19:56:19 +00:00
2019-09-30 14:40:55 +00:00
// 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);
2019-09-21 19:56:19 +00:00
2019-09-30 14:40:55 +00:00
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;
2019-09-30 14:40:55 +00:00
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);
2019-09-30 14:40:55 +00:00
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;
}
}
2019-09-30 14:40:55 +00:00
switch (menubar_type) {
case MENUBAR_AUTO:
g_error("Unreachable\n");
break;
2019-09-30 14:40:55 +00:00
case MENUBAR_SHOW_IN_SHELL:
g_print("Showing menu in the shell\n");
gtk_application_set_menubar(GTK_APPLICATION(app), menubar_model);
break;
2019-09-30 14:40:55 +00:00
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;
}
2019-09-30 14:40:55 +00:00
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)));
2019-09-30 14:40:55 +00:00
gtk_window_set_titlebar(GTK_WINDOW(main_window), titlebar);
2019-09-21 19:56:19 +00:00
2019-09-30 14:40:55 +00:00
// Disable menubar
gtk_application_set_menubar(GTK_APPLICATION(app), NULL);
2019-09-30 14:40:55 +00:00
// 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;
}
}
}
2019-09-30 14:40:55 +00:00
// 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;
}
2019-09-30 14:40:55 +00:00
g_list_free_full(list, NULL);
2019-09-21 19:56:19 +00:00
}
2019-09-30 14:40: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;
2019-09-30 14:40:55 +00:00
gtk_tree_model_get(model, iter, 0, &text, -1);
gboolean result = g_strcmp0("<separator>", text) == 0;
2019-09-30 14:40:55 +00:00
return result;
}
2019-09-30 14:40:55 +00:00
// Determines how many frame buffers to use
static unsigned char number_of_buffers(void) {
// TODO
bool should_blend = !fallback_canvas;
2019-09-30 14:40:55 +00:00
return should_blend? 3 : 2;
}
2019-09-30 14:40:55 +00:00
// 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()];
}
2019-09-30 14:40:55 +00:00
// Returns the current finished frame
static uint32_t *get_current_buffer(void) {
return image_buffers[current_buffer];
}
2019-09-30 14:40:55 +00:00
// Returns the previous finished frame
static uint32_t *get_previous_buffer(void) {
return image_buffers[(current_buffer + 2) % number_of_buffers()];
}
2019-09-30 14:40:55 +00:00
// Cycles the buffers
static void flip(void) {
current_buffer = (current_buffer + 1) % number_of_buffers();
}
2019-09-30 14:40: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;
2019-09-30 14:40:55 +00:00
// 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");
2019-09-30 14:40:55 +00:00
builder = gtk_builder_new_from_resource(RESOURCE_PREFIX "ui/window.ui");
gtk_builder_connect_signals(builder, NULL);
2019-09-30 14:40:55 +00:00
// Setup application actions
g_action_map_add_action_entries(G_ACTION_MAP(app), app_entries, G_N_ELEMENTS(app_entries), app);
2019-09-30 14:40:55 +00:00
#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);
2019-09-30 14:40:55 +00:00
// 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"));
2019-09-30 14:40:55 +00:00
#endif
2019-09-30 14:40:55 +00:00
preferences = GTK_WINDOW(get_object("preferences"));
2019-09-30 14:40:55 +00:00
g_signal_connect(preferences, "realize", G_CALLBACK(on_preferences_realize), (gpointer) builder);
init_settings(user_data->config_path, preferences);
2019-09-30 14:40:55 +00:00
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"));
2019-09-30 14:40:55 +00:00
// 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));
2019-09-30 14:40:55 +00:00
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));
2019-09-30 14:40:55 +00:00
setup_menu(app);
2019-09-30 14:40:55 +00:00
// 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));
2019-09-30 14:40: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"
};
2019-09-30 14:40:55 +00:00
// 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);
}
2019-09-30 14:40:55 +00:00
// 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;
2019-09-30 14:40:55 +00:00
if (user_data->fullscreen) {
gtk_window_fullscreen(GTK_WINDOW(main_window));
}
2019-09-30 14:40:55 +00:00
// 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);
2019-09-30 14:40:55 +00:00
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);
2019-09-30 14:40:55 +00:00
// Just hide our sub-windows when closing them
g_signal_connect(preferences, "delete-event", G_CALLBACK(gtk_widget_hide_on_delete), NULL);
g_signal_connect(vram_viewer, "delete-event", G_CALLBACK(gtk_widget_hide_on_delete), NULL);
g_signal_connect(memory_viewer, "delete-event", G_CALLBACK(gtk_widget_hide_on_delete), NULL);
g_signal_connect(console, "delete-event", G_CALLBACK(gtk_widget_hide_on_delete), NULL);
g_signal_connect(printer, "delete-event", G_CALLBACK(gtk_widget_hide_on_delete), NULL);
g_signal_connect(get_object("vram_viewer_tileset_canvas"), "draw", G_CALLBACK(on_draw_vram_viewer_tileset), NULL);
g_signal_connect(get_object("vram_viewer_tilemap_canvas"), "draw", G_CALLBACK(on_draw_vram_viewer_tilemap), NULL);
gtk_widget_add_events(builder_get(GTK_WIDGET, "vram_viewer_tileset_canvas"), GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK);
gtk_widget_add_events(builder_get(GTK_WIDGET, "vram_viewer_tilemap_canvas"), GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK);
g_signal_connect(get_object("vram_viewer_tileset_canvas"), "motion_notify_event", G_CALLBACK(on_motion_vram_viewer_tileset), NULL);
g_signal_connect(get_object("vram_viewer_tilemap_canvas"), "motion_notify_event", G_CALLBACK(on_motion_vram_viewer_tilemap), NULL);
g_signal_connect(get_object("vram_viewer_stack"), "notify::visible-child", G_CALLBACK(on_vram_tab_change), NULL);
2019-09-30 14:40:55 +00:00
// create our renderer area
if (supports_gl) {
gl_area = GTK_GL_AREA(gtk_gl_area_new());
gtk_gl_area_set_required_version(gl_area, 3, 2);
gtk_gl_area_set_auto_render(gl_area, false);
gtk_gl_area_set_has_alpha(gl_area, false);
gtk_gl_area_set_has_depth_buffer(gl_area, false);
gtk_gl_area_set_has_stencil_buffer(gl_area, false);
g_signal_connect(gl_area, "realize", G_CALLBACK(gl_init), NULL);
gtk_box_pack_end(GTK_BOX(main_window_container), GTK_WIDGET(gl_area), TRUE, TRUE, 0);
}
else {
create_fallback_canvas();
}
GdkScreen *screen = gdk_screen_get_default();
GtkCssProvider *provider = gtk_css_provider_new();
gtk_css_provider_load_from_resource(provider, RESOURCE_PREFIX "css/main.css");
gtk_style_context_add_provider_for_screen (screen, GTK_STYLE_PROVIDER(provider), GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
2019-09-30 14:40:55 +00:00
gtk_application_add_window(GTK_APPLICATION(app), GTK_WINDOW(main_window));
gtk_widget_show_all(GTK_WIDGET(main_window));
2019-10-05 23:13:45 +00:00
setup_audio();
g_thread_new("CoreLoop", run, user_data);
}
2019-09-30 14:40:55 +00:00
// 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");
2019-09-30 14:40:55 +00:00
save_settings();
free_settings();
2019-10-05 23:13:45 +00:00
alDeleteSources(1, &al_source);
alDeleteBuffers(96000*2, al_buffer);
al_device = alcGetContextsDevice(al_context);
alcMakeContextCurrent(NULL);
alcDestroyContext(al_context);
alcCloseDevice(al_device);
}
2019-09-30 14:40: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;
2019-09-30 14:40:55 +00:00
if (n_files > 1) {
g_printerr("More than one file specified\n");
exit(EXIT_FAILURE);
}
user_data->file = g_file_dup(files[0]);
2019-09-30 14:40:55 +00:00
// We have handled the files, now activate the application
activate(app, user_data_gptr);
}
2019-09-30 14:40:55 +00:00
// 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);
}
2019-09-30 14:40:55 +00:00
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;
}
2019-09-30 14:40:55 +00:00
// 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));
}
2019-09-30 14:40:55 +00:00
// 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);
}
2019-09-30 14:40:55 +00:00
// 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));
}
2019-09-27 21:10:28 +00:00
2019-09-30 14:40:55 +00:00
// 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));
}
2019-09-30 14:40:55 +00:00
// 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)));
2019-09-27 21:10:28 +00:00
}
2019-09-30 14:40:55 +00:00
g_object_unref(native);
2019-09-27 21:10:28 +00:00
}
2019-09-30 14:40:55 +00:00
// 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")));
}
2019-09-30 14:40:55 +00:00
// app.quit GAction
// Exits the application
static void activate_quit(GSimpleAction *action, GVariant *parameter, gpointer app) {
quit(G_APPLICATION(app));
}
2019-09-30 14:40:55 +00:00
// `destroy` signal GCallback
// Exits the application
static void on_quit(GtkWidget *w, gpointer app) {
quit(G_APPLICATION(app));
}
2019-09-30 14:40:55 +00:00
// TODO: Comment
static void gl_init(GtkWidget *w) {
GtkGLArea *gl_area = GTK_GL_AREA(w);
2019-09-30 14:40:55 +00:00
g_print("GL_INIT\n");
const char *renderer;
2019-09-30 14:40:55 +00:00
g_print("GL Context: %p\n", gtk_gl_area_get_context(gl_area));
2019-09-30 14:40:55 +00:00
gtk_gl_area_make_current(gl_area);
if (gtk_gl_area_get_error(gl_area) != NULL) {
goto error;
}
2019-09-30 14:40:55 +00:00
renderer = (char *)glGetString(GL_RENDERER);
g_print("GtkGLArea on %s\n", renderer ? renderer : "Unknown");
2019-09-30 14:40:55 +00:00
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 {
2019-09-30 14:40:55 +00:00
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;
}
2019-09-30 14:40:55 +00:00
error:
if (gtk_gl_area_get_error(gl_area) != NULL) {
g_printerr("GtkGLArea: %s\n", gtk_gl_area_get_error(gl_area)->message);
}
2019-09-30 14:40:55 +00:00
create_fallback_canvas();
}
2019-09-30 14:40:55 +00:00
// TODO: Comment
static void gl_draw() {
render_texture(get_current_buffer(), get_previous_buffer());
}
2019-09-30 14:40:55 +00:00
// TODO: Comment
static void gl_finish() { }
2019-09-30 14:40:55 +00:00
// 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);
2019-09-30 14:40:55 +00:00
guint screen_width = GB_get_screen_width(&gb);
guint screen_height = GB_get_screen_height(&gb);
2019-09-30 14:40:55 +00:00
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);
2019-09-30 14:40:55 +00:00
cairo_set_source_surface(cr, surface, 0, 0);
cairo_pattern_set_filter(cairo_get_source(cr), CAIRO_FILTER_NEAREST);
cairo_paint(cr);
2019-09-30 14:40:55 +00:00
return FALSE;
}
2019-09-21 19:56:19 +00:00
2019-09-30 14:40:55 +00:00
// TODO: Comment
static void resize() {
update_viewport();
}
2019-09-21 19:56:19 +00:00
2019-09-30 14:40:55 +00:00
// Gets called when the VRAM viewer gets realized
static void on_vram_viewer_realize() {
vram_viewer_visible = true;
}
2019-09-27 21:10:28 +00:00
2019-09-30 14:40:55 +00:00
// Gets called when the VRAM viewer gets unrealized
static void on_vram_viewer_unrealize() {
vram_viewer_visible = false;
}
2019-09-27 21:10:28 +00:00
2019-09-30 14:40:55 +00:00
// 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;
2019-09-21 19:56:19 +00:00
2019-09-30 14:40:55 +00:00
context = gtk_widget_get_style_context(widget);
width = gtk_widget_get_allocated_width(widget);
height = gtk_widget_get_allocated_height(widget);
2019-09-30 14:40:55 +00:00
gtk_render_background(context, cr, 0, 0, width, height);
2019-09-30 14:40:55 +00:00
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)
);
2019-09-21 19:56:19 +00:00
cairo_scale(cr, 2.0, 2.0);
2019-09-30 14:40:55 +00:00
cairo_set_source_surface(cr, surface, 0, 0);
cairo_pattern_set_filter(cairo_get_source(cr), CAIRO_FILTER_NEAREST);
2019-09-30 14:40:55 +00:00
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);
}
2019-09-30 14:40:55 +00:00
return FALSE;
}
2019-09-27 21:10:28 +00:00
2019-09-30 14:40:55 +00:00
// 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;
2019-09-21 19:56:19 +00:00
2019-09-30 14:40:55 +00:00
context = gtk_widget_get_style_context(widget);
width = gtk_widget_get_allocated_width(widget);
height = gtk_widget_get_allocated_height(widget);
2019-09-21 19:56:19 +00:00
2019-09-30 14:40:55 +00:00
gtk_render_background(context, cr, 0, 0, width, height);
2019-09-30 14:40:55 +00:00
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)
);
2019-09-21 19:56:19 +00:00
cairo_scale(cr, 2.0, 2.0);
2019-09-30 14:40:55 +00:00
cairo_set_source_surface(cr, surface, 0, 0);
cairo_pattern_set_filter(cairo_get_source(cr), CAIRO_FILTER_NEAREST);
2019-09-30 14:40:55 +00:00
cairo_paint(cr);
2019-09-21 19:56:19 +00:00
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 = 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);
}
2019-09-30 14:40:55 +00:00
return FALSE;
2019-09-21 19:56:19 +00:00
}
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"), "");
}
2019-09-30 14:40:55 +00:00
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;
2019-09-30 14:40:55 +00:00
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);
}
2019-09-30 14:40:55 +00:00
g_object_unref(native);
}
else {
2019-09-30 14:40:55 +00:00
config.boot_rom_path = (gchar *)id;
}
2019-09-30 14:40:55 +00:00
}
2019-09-30 14:40:55 +00:00
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);
2019-09-21 19:56:19 +00:00
}
2019-09-30 14:40:55 +00:00
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);
2019-09-21 19:56:19 +00:00
2019-09-30 14:40:55 +00:00
if (GB_is_inited(&gb)) {
GB_set_color_correction_mode(&gb, get_color_correction_mode());
2019-09-21 19:56:19 +00:00
}
2019-09-30 14:40:55 +00:00
}
2019-09-21 19:56:19 +00:00
2019-09-30 14:40:55 +00:00
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));
}
2019-09-21 19:56:19 +00:00
2019-09-30 14:40:55 +00:00
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);
2019-09-21 19:56:19 +00:00
}
2019-09-30 14:40:55 +00:00
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);
2019-09-30 14:40:55 +00:00
init_shader_with_name(&shader, config.shader);
}
2019-09-30 14:40:55 +00:00
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));
2019-09-21 19:56:19 +00:00
2019-09-30 14:40:55 +00:00
if (GB_is_inited(&gb)) {
GB_set_highpass_filter_mode(&gb, get_highpass_mode());
2019-09-21 19:56:19 +00:00
}
2019-09-30 14:40:55 +00:00
}
2019-09-21 19:56:19 +00:00
2019-09-30 14:40:55 +00:00
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();
}
2019-09-21 19:56:19 +00:00
2019-09-30 14:40:55 +00:00
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);
2019-09-30 14:40:55 +00:00
}
2019-09-30 14:40:55 +00:00
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);
2019-09-30 14:40:55 +00:00
}
2019-09-30 14:40:55 +00:00
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();
2019-09-21 19:56:19 +00:00
}
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;
}
2019-09-30 14:40:55 +00:00
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);
2019-09-30 14:40:55 +00:00
}
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){
2019-09-30 14:40:55 +00:00
(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);
2019-09-30 14:40:55 +00:00
}
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);
}
2019-09-30 14:40:55 +00:00
static void handle_events(GB_gameboy_t *gb) {
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);
}
2019-10-04 22:57:03 +00:00
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) {
const gchar *title = gtk_tree_view_column_get_title(col);
const uint8_t color_index = g_ascii_strtoll(&title[6], NULL, 10);
const uint8_t column_index = 2 + (2 * color_index);
GValue color_val = G_VALUE_INIT;
gtk_tree_model_get_value(model, iter, column_index, &color_val);
gint color = g_value_get_int(&color_val);
gchar *color_string = g_strdup_printf("#%06x", color);
gint lightness = 0.299 * ((color >> 16) & 0xFF) + 0.587 * ((color >> 8) & 0xFF) + 0.114 * (color & 0xFF);
GValue color_str = G_VALUE_INIT;
g_value_init(&color_str, G_TYPE_STRING);
g_value_set_string(&color_str, color_string);
g_object_set_property(G_OBJECT(renderer), "background", &color_str);
GValue fg_color_str = G_VALUE_INIT;
g_value_init(&fg_color_str, G_TYPE_STRING);
g_value_set_static_string(&fg_color_str, (lightness > 0x7F)? "#000000" : "#FFFFFF");
g_object_set_property(G_OBJECT(renderer), "foreground", &fg_color_str);
g_value_unset(&color_val);
g_value_unset(&color_str);
g_value_unset(&fg_color_str);
g_free(color_string);
}
static void on_vblank(gpointer data) {
2019-09-30 14:40:55 +00:00
// 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) {
const gchar *active = gtk_stack_get_visible_child_name(builder_get(GTK_STACK, "vram_viewer_stack"));
if (g_strcmp0("vram_viewer_tileset", active) == 0) {
const gchar *palette_id = gtk_combo_box_get_active_id(builder_get(GTK_COMBO_BOX, "vram_viewer_tileset_palette_selector"));
GB_palette_type_t palette_type = g_str_has_prefix(palette_id, "bg")? GB_PALETTE_BACKGROUND : GB_PALETTE_OAM;
uint8_t palette_index = g_ascii_digit_value(palette_id[palette_type == GB_PALETTE_OAM ? 3 : 2]);
GB_draw_tileset(&gb, tileset_buffer,
palette_type,
palette_index
);
}
else if (g_strcmp0("vram_viewer_tilemap", active) == 0) {
const gchar *palette_id = gtk_combo_box_get_active_id(builder_get(GTK_COMBO_BOX, "vram_viewer_tilemap_palette_selector"));
uint8_t palette_index = 0;
GB_palette_type_t palette_type = GB_PALETTE_AUTO;
if (g_strcmp0("auto", palette_id) != 0) {
palette_type = g_str_has_prefix(palette_id, "bg")? GB_PALETTE_BACKGROUND : GB_PALETTE_OAM;
palette_index = g_ascii_digit_value(palette_id[palette_type == GB_PALETTE_OAM ? 3 : 2]);
}
GB_map_type_t map_type = GB_MAP_AUTO;
const gchar *map_type_id = gtk_combo_box_get_active_id(builder_get(GTK_COMBO_BOX, "vram_viewer_tilemap_tilemap_selector"));
if (g_strcmp0("auto", map_type_id) != 0) {
map_type = (g_strcmp0("9800", map_type_id) == 0)? GB_MAP_9800 : GB_MAP_9C00;
}
GB_tileset_type_t tileset_type = GB_TILESET_AUTO;
const gchar *tileset_type_id = gtk_combo_box_get_active_id(builder_get(GTK_COMBO_BOX, "vram_viewer_tilemap_tileset_selector"));
if (g_strcmp0("auto", tileset_type_id) != 0) {
tileset_type = (g_strcmp0("8800", tileset_type_id) == 0)? GB_TILESET_8800 : GB_TILESET_8000;
}
GB_draw_tilemap(&gb, tilemap_buffer,
palette_type,
palette_index,
map_type,
tileset_type
);
scrollRect = (Rect){
GB_read_memory(&gb, 0xFF00 | GB_IO_SCX),
GB_read_memory(&gb, 0xFF00 | GB_IO_SCY),
160, 144
};
}
else if (g_strcmp0("vram_viewer_sprites", active) == 0) {
2019-10-03 23:40:50 +00:00
GtkTreeIter iter;
2019-10-04 22:57:03 +00:00
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
);
2019-10-03 23:40:50 +00:00
gtk_tree_model_get_iter_first(GTK_TREE_MODEL(store), &iter);
oamCount = GB_get_oam_info(&gb, oamInfo, &oamHeight);
2019-10-03 23:40:50 +00:00
for (unsigned row = 0; row < oamCount; ++row) {
GdkPixbuf *pixbuf = gdk_pixbuf_new_from_bytes(
g_bytes_new(oamInfo[row].image, 128 * sizeof(uint32_t)),
GDK_COLORSPACE_RGB, true, 8, 8, oamHeight, 8 * sizeof(uint32_t)
);
GdkPixbuf *dest = gdk_pixbuf_new(GDK_COLORSPACE_RGB, true, 8, 8 * 2, oamHeight * 2);
gdk_pixbuf_scale(pixbuf, dest,
0, 0, 8 * 2, oamHeight * 2,
0, 0, 2.0, 2.0,
GDK_INTERP_NEAREST
);
gtk_list_store_insert_with_values(store, &iter, -1,
0, dest,
1, itoa(oamInfo[row].x - 8),
2, itoa(oamInfo[row].y - 16),
3, g_strdup_printf("$%02x", oamInfo[row].tile),
4, g_strdup_printf("$%04x", 0x8000 + oamInfo[row].tile * 0x10),
5, g_strdup_printf("$%04x", oamInfo[row].oam_addr),
6, GB_is_cgb(&gb)
2019-10-03 23:40:50 +00:00
? g_strdup_printf("%c%c%c%d%d",
oamInfo[row].flags & 0x80? 'P' : '-',
oamInfo[row].flags & 0x40? 'Y' : '-',
oamInfo[row].flags & 0x20? 'X' : '-',
oamInfo[row].flags & 0x08? 1 : 0,
oamInfo[row].flags & 0x07)
: g_strdup_printf("%c%c%c%d",
oamInfo[row].flags & 0x80? 'P' : '-',
oamInfo[row].flags & 0x40? 'Y' : '-',
oamInfo[row].flags & 0x20? 'X' : '-',
oamInfo[row].flags & 0x10? 1 : 0),
-1
);
2019-10-04 22:57:03 +00:00
g_object_unref(pixbuf);
g_object_unref(dest);
2019-10-03 23:40:50 +00:00
}
2019-10-04 22:57:03 +00:00
gtk_tree_view_set_model(tree_view, GTK_TREE_MODEL(store));
g_object_unref(store);
}
else if (g_strcmp0("vram_viewer_palettes", active) == 0) {
2019-10-04 22:57:03 +00:00
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
2019-10-04 22:57:03 +00:00
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 *palette_data = GB_get_direct_access(&gb, row >= 8? GB_DIRECT_ACCESS_OBP : GB_DIRECT_ACCESS_BGP, NULL, NULL);
2019-10-04 22:57:03 +00:00
uint8_t offset = (row & 7) * 4;
uint16_t color_0 = (palette_data[((0 + offset) << 1) + 1] << 8) | palette_data[((0 + offset) << 1)];
uint16_t color_1 = (palette_data[((1 + offset) << 1) + 1] << 8) | palette_data[((1 + offset) << 1)];
uint16_t color_2 = (palette_data[((2 + offset) << 1) + 1] << 8) | palette_data[((2 + offset) << 1)];
uint16_t color_3 = (palette_data[((3 + offset) << 1) + 1] << 8) | palette_data[((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);
}
2019-09-30 14:40:55 +00:00
// Queue a redraw of the VRAM viewer
gtk_widget_queue_draw(GTK_WIDGET(vram_viewer));
}
}
static void vblank(GB_gameboy_t *gb) {
flip();
GB_set_pixels_output(gb, get_pixels());
g_idle_add((GSourceFunc) on_vblank, NULL);
}
2019-10-05 23:13:45 +00:00
static gpointer openal_run(gpointer user_data_gptr) {
GMutex audio_mutex;
GCond audio_cond;
g_cond_init(&audio_cond);
g_mutex_init(&audio_mutex);
g_mutex_lock(&audio_mutex);
gboolean play_audio = true;
while (play_audio) {
ALint processed;
alGetSourcei(al_source, AL_BUFFERS_PROCESSED, &processed);
ALint state;
alGetSourcei(al_source, AL_SOURCE_STATE, &state);
if (state != AL_PLAYING) {
alSourcePlay(al_source);
}
ALuint x = 0;
while (processed--) {
alSourceUnqueueBuffers(al_source, 1, &x);
}
gint64 end_time = g_get_monotonic_time() + 1 * G_TIME_SPAN_MILLISECOND;
g_cond_wait_until(&audio_cond, &audio_mutex, end_time);
}
return FALSE;
}
static gpointer run(gpointer user_data_gptr) {
UserData *user_data = user_data_gptr;
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);
2019-10-05 23:13:45 +00:00
GB_set_sample_rate(&gb, 96000);
GB_set_color_correction_mode(&gb, get_color_correction_mode());
GB_set_highpass_filter_mode(&gb, get_highpass_mode());
2019-09-25 00:48:07 +00:00
GB_set_rewind_length(&gb, config.rewind_duration);
GB_set_update_input_hint_callback(&gb, handle_events);
2019-10-05 23:13:45 +00:00
GB_apu_set_sample_callback(&gb, gb_audio_callback);
}
GError *error;
char *boot_rom_path;
2019-09-26 18:31:58 +00:00
char *boot_rom_name;
GBytes *boot_rom_f;
const guchar *boot_rom_data;
gsize boot_rom_size;
if (user_data->boot_rom_path != NULL) {
2019-09-26 18:31:58 +00:00
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;
}
}
2019-09-26 18:31:58 +00:00
else { config_boot_rom:
switch (model) {
case GB_MODEL_DMG_B:
2019-09-26 18:31:58 +00:00
boot_rom_name = "dmg_boot.bin";
break;
case GB_MODEL_SGB:
case GB_MODEL_SGB_PAL:
case GB_MODEL_SGB_NO_SFC:
2019-09-26 18:31:58 +00:00
boot_rom_name = "sgb_boot.bin";
break;
case GB_MODEL_SGB2:
case GB_MODEL_SGB2_NO_SFC:
2019-09-26 18:31:58 +00:00
boot_rom_name = "sgb2_boot.bin";
break;
case GB_MODEL_CGB_C:
case GB_MODEL_CGB_E:
2019-09-26 18:31:58 +00:00
boot_rom_name = "cgb_boot.bin";
break;
case GB_MODEL_AGB:
2019-09-26 18:31:58 +00:00
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) {
2019-09-26 18:31:58 +00:00
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;
}
}
2019-09-26 18:31:58 +00:00
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);
}
2019-09-26 18:31:58 +00:00
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) {
2019-10-05 23:13:45 +00:00
alSourcePlay(al_source);
g_thread_new("OpenAL", openal_run, NULL);
/* Run emulation */
while (running) {
if (paused || rewind_paused) {
}
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);
}
}
}
return NULL;
}