SameBoy/gtk3/main.c

2015 lines
66 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

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

#define G_LOG_USE_STRUCTURED
#include <gtk/gtk.h>
#include <signal.h>
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#include <Core/gb.h>
#include "types.h"
#include "config.h"
#include "util.h"
#include "shader.h"
#include "check_menu_radio_group.h"
#include "vram_viewer.h"
// used for audio and game controllers
#include "SDL.h"
#include "../SDL/audio/audio.h"
#define JOYSTICK_HIGH 0x4000
#define JOYSTICK_LOW 0x3800
#define BUTTON_MASK_A 0x01
#define BUTTON_MASK_B 0x02
#define BUTTON_MASK_START 0x04
#define BUTTON_MASK_SELECT 0x08
#define BUTTON_MASK_UP 0x10
#define BUTTON_MASK_DOWN 0x20
#define BUTTON_MASK_LEFT 0x40
#define BUTTON_MASK_RIGHT 0x80
#define str(x) #x
#define xstr(x) str(x)
#define get_object(id) gtk_builder_get_object(gui_data.builder, id)
#define builder_get(type, id) type(get_object(id))
#define action_set_enabled(map, name, value) g_simple_action_set_enabled(G_SIMPLE_ACTION(g_action_map_lookup_action(G_ACTION_MAP(map), name)), value);
// Initialize the GuiData
static GuiData gui_data = {
.cli_options = {
.fullscreen = false,
.model = -1,
},
.prev_model = -1,
.running = false,
.stopping = false,
.stopped = false,
.in_sync_input = false,
.log_to_sidebar = false,
.should_clear_sidebar = false,
.audio_initialized = false,
.border_mode_changed = false,
.underclock_down = false,
.rewind_down = false,
.rewind_paused = false,
.turbo_down = false,
.clock_mutliplier = 1.0,
.analog_clock_multiplier = 1.0,
};
GB_gameboy_t gb;
// Forward declarations of the actions
static void activate_open(GSimpleAction *action, GVariant *parameter, gpointer app);
static void activate_close(GSimpleAction *action, GVariant *parameter, gpointer app);
static void activate_reset(GSimpleAction *action, GVariant *parameter, gpointer app);
static void activate_show_console(GSimpleAction *action, GVariant *parameter, gpointer app);
static void activate_open_gtk_debugger(GSimpleAction *action, GVariant *parameter, gpointer app);
static void activate_open_memory_viewer(GSimpleAction *action, GVariant *parameter, gpointer app);
static void activate_open_vram_viewer(GSimpleAction *action, GVariant *parameter, gpointer app);
static void activate_clear_console(GSimpleAction *action, GVariant *parameter, gpointer app);
static void activate_quit(GSimpleAction *action, GVariant *parameter, gpointer app);
static void activate_about(GSimpleAction *action, GVariant *parameter, gpointer app);
static void activate_preferences(GSimpleAction *action, GVariant *parameter, gpointer app);
static void on_pause_changed(GSimpleAction *action, GVariant *value, gpointer user_data_ptr);
static void on_mute_changed(GSimpleAction *action, GVariant *value, gpointer user_data_ptr);
static const GActionEntry file_entries[] = {
{ "open", activate_open, NULL, NULL, NULL },
{ "close", activate_close, NULL, NULL, NULL },
};
static const GActionEntry edit_entries[] = {
};
static const GActionEntry emulation_entries[] = {
{ "reset", activate_reset, NULL, NULL, NULL },
{ "pause", NULL, NULL, "false", on_pause_changed },
{ "save_state", NULL, NULL, NULL, NULL },
{ "load_state", NULL, NULL, NULL, NULL },
};
static const GActionEntry developer_entries[] = {
{ "show_console", activate_show_console, NULL, NULL, NULL },
{ "open_gtk_debugger", activate_open_gtk_debugger, NULL, NULL, NULL },
// { "open_memory_viewer", activate_open_memory_viewer, NULL, NULL, NULL },
{ "open_vram_viewer", activate_open_vram_viewer, NULL, NULL, NULL },
{ "toggle_developer_mode", NULL, NULL, "false", NULL },
{ "clear_console", activate_clear_console, NULL, NULL, NULL },
};
static const GActionEntry app_entries[] = {
{ "quit", activate_quit, NULL, NULL, NULL },
{ "about", activate_about, NULL, NULL, NULL },
{ "preferences", activate_preferences, NULL, NULL, NULL },
{ "toggle_mute", NULL, NULL, "false", on_mute_changed },
};
static const char* get_sdl_joystick_power_level_name(SDL_JoystickPowerLevel level) {
switch (level) {
case SDL_JOYSTICK_POWER_EMPTY: return "Empty";
case SDL_JOYSTICK_POWER_LOW: return "Low";
case SDL_JOYSTICK_POWER_MEDIUM: return "Medium";
case SDL_JOYSTICK_POWER_FULL: return "Full";
case SDL_JOYSTICK_POWER_WIRED: return "Wired";
case SDL_JOYSTICK_POWER_MAX: return "Max";
case SDL_JOYSTICK_POWER_UNKNOWN:
default:
return "Unknown";
}
}
// This function gets called after the parsing of the commandline options has occurred.
static gint handle_local_options(GApplication *app, GVariantDict *options, gpointer null_ptr) {
guint32 count;
if (g_variant_dict_lookup(options, "version", "b", &count)) {
g_message("SameBoy v" xstr(VERSION));
return EXIT_SUCCESS;
}
// 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) {
gui_data.cli_options.model = GB_MODEL_DMG_B;
}
else {
gui_data.cli_options.model = GB_MODEL_DMG_B;
g_warning("Unsupported revision: %s\nFalling back to DMG-B", model_name);
}
}
else if (g_str_has_prefix(model_name, "SGB")) {
if (g_str_has_suffix(model_name, "-NTSC") || g_strcmp0(model_name, "SGB") == 0) {
gui_data.cli_options.model = GB_MODEL_SGB;
}
else if (g_str_has_suffix(model_name, "-PAL")) {
gui_data.cli_options.model = GB_MODEL_SGB | GB_MODEL_PAL_BIT;
}
else if (g_str_has_suffix(model_name, "2")) {
gui_data.cli_options.model = GB_MODEL_SGB2;
}
else {
gui_data.cli_options.model = GB_MODEL_SGB2;
g_warning("Unsupported revision: %s\nFalling back to SGB2", model_name);
}
}
else if (g_str_has_prefix(model_name, "CGB")) {
if (g_str_has_suffix(model_name, "-C")) {
gui_data.cli_options.model = GB_MODEL_CGB_C;
}
else if (g_str_has_suffix(model_name, "-E") || g_strcmp0(model_name, "CGB") == 0) {
gui_data.cli_options.model = GB_MODEL_CGB_E;
}
else {
gui_data.cli_options.model = GB_MODEL_CGB_E;
g_warning("Unsupported revision: %s\nFalling back to CGB-E", model_name);
}
}
else if (g_str_has_prefix(model_name, "AGB")) {
gui_data.cli_options.model = GB_MODEL_AGB;
}
else {
g_warning("Unknown model: %s", model_name);
exit(EXIT_FAILURE);
}
}
return -1;
}
static gboolean init_controllers(void) {
SDL_version compiled;
SDL_version linked;
SDL_VERSION(&compiled);
SDL_GetVersion(&linked);
g_debug("Compiled against SDL version %d.%d.%d", compiled.major, compiled.minor, compiled.patch);
g_debug("Linked against SDL version %d.%d.%d", linked.major, linked.minor, linked.patch);
g_debug("Initializing game controllers");
if (SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) < 0) {
g_warning("Failed to initialize game controller support: %s", SDL_GetError());
return false;
}
g_debug("Initializing haptic feedback");
if (SDL_InitSubSystem(SDL_INIT_HAPTIC) < 0) {
g_warning("Failed to initialize haptic feedback support: %s", SDL_GetError());
}
g_debug("Loading custom game controller database");
GError *error = NULL;
GBytes *db_f = g_resources_lookup_data(RESOURCE_PREFIX "gamecontrollerdb.txt", G_RESOURCE_LOOKUP_FLAGS_NONE, &error);
if (db_f != NULL) {
gsize db_data_size = 0;
const guchar *db_data = g_bytes_get_data(db_f, &db_data_size);
const gint val = SDL_GameControllerAddMappingsFromRW(SDL_RWFromMem((void *)db_data, db_data_size), 1);
if (val < 0) {
g_warning("Failed to load controller mappings: %s", SDL_GetError());
}
g_bytes_unref(db_f);
}
if (error != NULL) g_clear_error(&error);
g_message("Number of found controllers: %d", SDL_NumJoysticks());
// In the “worst” case all joysticks are valid game controllers
gui_data.controllers = g_malloc0(sizeof(struct Controller_t) * SDL_NumJoysticks());
for (int i = 0; i < SDL_NumJoysticks(); ++i) {
if (SDL_IsGameController(i)) {
struct Controller_t *s = &gui_data.controllers[i];
s->controller = SDL_GameControllerOpen(i);
if (s->controller) {
SDL_Joystick *joystick = SDL_GameControllerGetJoystick(s->controller);
SDL_JoystickPowerLevel power_level = SDL_JoystickCurrentPowerLevel(joystick);
if (SDL_JoystickIsHaptic(joystick)) {
s->haptic = SDL_HapticOpenFromJoystick(joystick);
if (s->haptic && SDL_HapticRumbleSupported(s->haptic)) {
SDL_HapticRumbleInit(s->haptic);
}
else {
if (s->haptic == NULL) {
g_warning("%s", SDL_GetError());
}
SDL_HapticClose(s->haptic);
s->haptic = NULL;
}
}
// Blacklist the WUP-028 for now
if (SDL_JoystickGetVendor(joystick) == 0x057e
&& SDL_JoystickGetProduct(joystick) == 0x0337
&& SDL_JoystickGetProductVersion(joystick) == 0x0100) {
s->ignore_rumble = true;
}
char guid_str[33];
SDL_JoystickGUID guid = SDL_JoystickGetGUID(joystick);
SDL_JoystickGetGUIDString(guid, guid_str, sizeof(guid_str));
g_message("Controller #%u (%s): %s; Haptic Feedback: %d; Power level: %s; Player index: %u; Instance ID: %u", i, guid_str, SDL_GameControllerName(s->controller), s->haptic != NULL, get_sdl_joystick_power_level_name(power_level), SDL_JoystickGetPlayerIndex(joystick), SDL_JoystickInstanceID(joystick));
gui_data.controller_count++;
}
else {
g_warning("Could not open controller %i: %s", i, SDL_GetError());
}
}
}
return true;
}
static gboolean init_audio(void) {
bool audio_playing = GB_audio_is_playing();
if (gui_data.audio_initialized) {
GB_audio_destroy();
gui_data.audio_initialized = false;
}
#ifdef USE_SDL_AUDIO
if (SDL_InitSubSystem(SDL_INIT_AUDIO) < 0) {
g_warning("Failed to initialize audio: %s", SDL_GetError());
return false;
}
#endif
GB_audio_init(gui_data.sample_rate);
GB_set_sample_rate(&gb, GB_audio_get_sample_rate());
// restore playing state
GB_audio_set_paused(!audio_playing);
return gui_data.audio_initialized = true;
}
static void gb_audio_callback(GB_gameboy_t *gb, GB_sample_t *sample) {
if (gui_data.turbo_down) {
static unsigned skip = 0;
skip++;
if (skip == GB_audio_get_sample_rate() / 8) {
skip = 0;
}
if (skip > GB_audio_get_sample_rate() / 16) {
return;
}
}
if (GB_audio_get_queue_length() / sizeof(*sample) > GB_audio_get_sample_rate() / 4) {
return;
}
GB_audio_queue_sample(sample);
}
static void rumble_callback(GB_gameboy_t *gb, double amp) {
if (!gui_data.controllers || gui_data.controller_count == 0 || !gui_data.last_used_controller) return;
struct Controller_t *s = gui_data.last_used_controller;
if (s->ignore_rumble) return;
if (s->haptic) {
if (amp > 0.0) {
SDL_HapticRumblePlay(s->haptic, amp, 100.0);
}
else {
SDL_HapticRumbleStop(s->haptic);
}
}
else {
if (amp == 0.0) {
SDL_GameControllerRumble(s->controller, 0, 0, 0);
}
else {
Uint16 intensity = (float) 0xFFFF * amp;
SDL_GameControllerRumble(s->controller, intensity, intensity, 100);
}
}
}
static gboolean clear_sidebar(void) {
GtkTextView *sidebar_output = builder_get(GTK_TEXT_VIEW, "console_sidebar_output");
GtkTextBuffer *sidebar_output_text_buf = gtk_text_view_get_buffer(sidebar_output);
gtk_text_buffer_set_text(sidebar_output_text_buf, "", -1);
return false;
}
static gboolean scroll_to_bottom(GtkTextView *textview, GtkTextMark *mark) {
GtkTextBuffer *buffer = gtk_text_view_get_buffer(textview);
GtkTextIter iter;
gtk_text_buffer_get_end_iter(buffer, &iter);
gtk_text_iter_set_line_offset(&iter, 0);
gtk_text_buffer_move_mark(buffer, mark, &iter);
gtk_text_view_scroll_to_mark(textview, mark, 0.0, true, 0.0, 0.10);
gtk_text_buffer_delete_mark(buffer, mark);
return true;
}
static gboolean append_pending_output(void) {
g_rec_mutex_lock(&gui_data.console_output_lock);
if (gui_data.should_clear_sidebar) {
clear_sidebar();
gui_data.should_clear_sidebar = false;
}
if (gtk_text_buffer_get_char_count(gui_data.pending_console_output) > 0) {
GtkTextView *text_view = builder_get(GTK_TEXT_VIEW, gui_data.log_to_sidebar? "console_sidebar_output" : "console_screen");
GtkTextBuffer *text_buf = gtk_text_view_get_buffer(text_view);
GtkTextIter start;
GtkTextIter end;
gtk_text_buffer_get_start_iter(gui_data.pending_console_output, &start);
gtk_text_buffer_get_end_iter(gui_data.pending_console_output, &end);
GtkTextIter iter;
gtk_text_buffer_get_end_iter(text_buf, &iter);
gtk_text_buffer_insert_range(text_buf, &iter, &start, &end);
scroll_to_bottom(text_view, gtk_text_buffer_create_mark(text_buf, NULL, &iter, true));
gtk_text_buffer_set_text(gui_data.pending_console_output, "", -1);
}
g_rec_mutex_unlock(&gui_data.console_output_lock);
return false;
}
static gboolean update_debugger_sidebar(GB_gameboy_t *gb) {
if (!GB_debugger_is_stopped(gb)) {
return false;
}
if (gui_data.main_thread != g_thread_self()) {
g_idle_add((GSourceFunc) update_debugger_sidebar, gb);
return false;
}
g_rec_mutex_lock(&gui_data.console_output_lock);
gui_data.should_clear_sidebar = true;
append_pending_output();
gui_data.log_to_sidebar = true;
g_rec_mutex_unlock(&gui_data.console_output_lock);
GtkTextView *sidebar_input = builder_get(GTK_TEXT_VIEW, "console_sidebar_input");
GtkTextBuffer *sidebar_input_text_buf = gtk_text_view_get_buffer(sidebar_input);
gint line_count = gtk_text_buffer_get_line_count(sidebar_input_text_buf);
for (unsigned line = 0; line < line_count; ++line) {
GtkTextIter start_iter;
GtkTextIter end_iter;
gunichar ch;
gtk_text_buffer_get_iter_at_line(sidebar_input_text_buf, &start_iter, line);
end_iter = start_iter;
do {
ch = gtk_text_iter_get_char(&end_iter);
if (!gtk_text_iter_forward_char(&end_iter)) {
break;
}
}
while (ch != '\n');
gchar *cmd = gtk_text_buffer_get_text(sidebar_input_text_buf, &start_iter, &end_iter, false);
g_strchug(cmd); // trim leading whitespace
g_strchomp(cmd); // trim trailing whitespace
if (g_strcmp0("", cmd) != 0) {
char *duped = g_strdup(cmd);
GB_attributed_log(gb, GB_LOG_BOLD, "%s:\n", duped);
GB_debugger_execute_command(gb, duped);
GB_log(gb, "\n");
g_free(duped);
}
g_free(cmd);
}
g_rec_mutex_lock(&gui_data.console_output_lock);
append_pending_output();
gui_data.log_to_sidebar = false;
g_rec_mutex_unlock(&gui_data.console_output_lock);
return false;
}
static void console_log(GB_gameboy_t *gb, const char *string, GB_log_attributes attributes) {
g_rec_mutex_lock(&gui_data.console_output_lock);
if (string != NULL && !g_str_equal("", string)) {
GtkTextIter iter;
GtkTextIter start;
// Append attributed text to "gui_data.pending_console_output" GtkTextBuffer
gtk_text_buffer_get_end_iter(gui_data.pending_console_output, &iter);
GtkTextMark *start_mark = gtk_text_buffer_create_mark(gui_data.pending_console_output, NULL, &iter, true);
gtk_text_buffer_insert(gui_data.pending_console_output, &iter, g_strdup(string), -1);
gtk_text_buffer_get_iter_at_mark(gui_data.pending_console_output, &start, start_mark);
if (attributes & GB_LOG_BOLD) {
gtk_text_buffer_apply_tag_by_name(gui_data.pending_console_output, "bold", &start, &iter);
}
if (attributes & GB_LOG_DASHED_UNDERLINE) {
gtk_text_buffer_apply_tag_by_name(gui_data.pending_console_output, "dashed_underline", &start, &iter);
}
if (attributes & GB_LOG_UNDERLINE) {
gtk_text_buffer_apply_tag_by_name(gui_data.pending_console_output, "underline", &start, &iter);
}
gtk_text_buffer_delete_mark(gui_data.pending_console_output, start_mark);
g_idle_add((GSourceFunc) append_pending_output, NULL);
}
g_rec_mutex_unlock(&gui_data.console_output_lock);
}
// Console TODO:
// TODO: clear sidebar when switching to async mode
// TODO: Command history (up / down arrow in input)
// TODO: reverse search of commands
// TODO: search in output
static char *sync_console_input(GB_gameboy_t *gb) {
update_debugger_sidebar(gb);
console_log(gb, "> ", 0);
gui_data.in_sync_input = true;
g_mutex_lock(&gui_data.debugger_input_mutex);
g_cond_wait(&gui_data.debugger_input_cond, &gui_data.debugger_input_mutex);
gchar *input = NULL;
const gchar *_input = g_ptr_array_index(gui_data.debugger_input_queue, 0);
input = g_strdup(_input);
gpointer ptr = g_ptr_array_remove_index(gui_data.debugger_input_queue, 0);
if (ptr) g_free(ptr);
g_mutex_unlock(&gui_data.debugger_input_mutex);
gui_data.in_sync_input = false;
return input;
}
static char *async_console_input(GB_gameboy_t *gb) {
// TODO: This is rather ugly
g_idle_add((GSourceFunc) clear_sidebar, NULL);
if (gui_data.debugger_input_queue->len == 0) return NULL;
g_mutex_lock(&gui_data.debugger_input_mutex);
gchar *input = NULL;
const gchar *_input = g_ptr_array_index(gui_data.debugger_input_queue, 0);
if (_input) {
input = g_strdup(_input);
gpointer ptr = g_ptr_array_remove_index(gui_data.debugger_input_queue, 0);
if (ptr) g_free(ptr);
}
g_mutex_unlock(&gui_data.debugger_input_mutex);
return input;
}
// Creating these items in the UI defintion files was buggy in some desktop
// environments and the manual call of `g_signal_connect` was needed anyway
// because the UI definition cant define string arguments for signal handlers.
static void create_model_menu_items() {
bool on_change_model(GtkWidget *, gpointer);
static const char *const model_names[] = {
"Game Boy",
"Super Game Boy",
"Game Boy Color",
"Game Boy Advance",
NULL
};
static const char *const model_codes[] = {
"DMG",
"SGB",
"CGB",
"GBA",
NULL
};
// Find the menu item index of the previous sibling of the new menu items
GtkWidget *before = builder_get(GTK_WIDGET, "before_model_changer");
GtkContainer *parent = GTK_CONTAINER(gtk_widget_get_parent(before));
g_autoptr(GList) list = gtk_container_get_children(parent);
gint position = g_list_index(list, before);
CheckMenuItemGroup *model_group = check_menu_item_group_new((char **) model_names, (char **) model_codes);
check_menu_item_group_insert_into_menu_shell(model_group, GTK_MENU_SHELL(parent), position + 1);
check_menu_item_group_connect_toggle_signal(model_group, on_change_model);
check_menu_item_group_activate(model_group, config.emulation.model);
static const char *const peripheral_names[] = {
"None",
"Game Boy Printer",
NULL
};
static const char *const peripheral_codes[] = {
"NONE",
"PRINTER",
NULL,
};
CheckMenuItemGroup *link_group = check_menu_item_group_new((char **) peripheral_names, (char **) peripheral_codes);
check_menu_item_group_insert_into_menu_shell(link_group, GTK_MENU_SHELL(builder_get(GTK_MENU_SHELL, "link_menu")), 0);
// check_menu_item_group_connect_toggle_signal(link_group, on_change_linked_device);
check_menu_item_group_activate(link_group, "NONE");
}
// 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) {
create_model_menu_items();
GtkMenuBar *menubar = builder_get(GTK_MENU_BAR, "main_menu");
gtk_box_pack_start(GTK_BOX(gui_data.main_window_container), GTK_WIDGET(menubar), false, false, 0);
}
// Determines if a ComboBox entry should be converted into a separator.
// Each element with a text value of `<separator>` will be converted into a separator element.
static gboolean is_separator(GtkTreeModel *model, GtkTreeIter *iter, gpointer data) {
gchar *text = NULL;
gtk_tree_model_get(model, iter, 0, &text, -1);
gboolean result = g_strcmp0("<separator>", text) == 0;
g_free(text);
return result;
}
// Recursively goes through all children of the given container and sets
// our `is_separator` function to all children of type`GtkComboBox`
static void set_combo_box_row_separator_func(GtkContainer *container) {
GList *children = gtk_container_get_children(container);
for (GList *l = children; l; l = l->next) {
if (GTK_IS_COMBO_BOX(l->data)) {
gtk_combo_box_set_row_separator_func(GTK_COMBO_BOX(l->data), is_separator, NULL, NULL);
}
if (GTK_IS_CONTAINER(l->data)) {
set_combo_box_row_separator_func(GTK_CONTAINER(l->data));
}
}
g_list_free(children);
}
// Determines how many frame buffers to use
static unsigned char number_of_buffers(void) {
if (gui_data.fallback_canvas) return 2;
bool should_blend = config_get_frame_blending_mode() != GB_FRAME_BLENDING_MODE_DISABLED;
return should_blend? 3 : 2;
}
// Returns the buffer that should be used by the Core to render a new frame to
static uint32_t *get_pixels(void) {
return gui_data.image_buffers[(gui_data.current_buffer + 1) % number_of_buffers()];
}
// Returns the current finished frame
static uint32_t *get_current_buffer(void) {
return gui_data.image_buffers[gui_data.current_buffer];
}
// Returns the previous finished frame
static uint32_t *get_previous_buffer(void) {
return gui_data.image_buffers[(gui_data.current_buffer + 2) % number_of_buffers()];
}
// Cycles the buffers
static void flip(void) {
gui_data.current_buffer = (gui_data.current_buffer + 1) % number_of_buffers();
}
static void update_viewport(void) {
GtkWidget *w = gui_data.fallback_canvas ? GTK_WIDGET(gui_data.fallback_canvas) : GTK_WIDGET(gui_data.gl_area);
int win_width = gtk_widget_get_allocated_width(w);
int win_height = gtk_widget_get_allocated_height(w);
double x_factor = win_width / (double) GB_get_screen_width(&gb);
double y_factor = win_height / (double) GB_get_screen_height(&gb);
if (config.video.use_integer_scaling) {
x_factor = (int)(x_factor);
y_factor = (int)(y_factor);
}
if (config.video.keep_aspect_ratio) {
if (x_factor > y_factor) {
x_factor = y_factor;
}
else {
y_factor = x_factor;
}
}
if (gui_data.gl_area) {
unsigned new_width = x_factor * GB_get_screen_width(&gb);
unsigned new_height = y_factor * GB_get_screen_height(&gb);
gui_data.viewport = (Rect){
(win_width - new_width) / 2,
(win_height - new_height) / 2,
new_width,
new_height
};
glViewport(gui_data.viewport.x, gui_data.viewport.y, gui_data.viewport.width, gui_data.viewport.height);
}
}
// WHY DO WE NEED SUCH AN UGLY METHOD, GTK?!
static void action_entries_set_enabled(const GActionEntry *entries, unsigned n_entries, bool value) {
// Assumes null-terminated if n_entries == -1
for (unsigned i = 0; n_entries == -1 ? entries[i].name != NULL : i < n_entries; i++) {
const GActionEntry *entry = &entries[i];
if (entry->name == NULL) continue;
action_set_enabled(gui_data.main_application, entry->name, value);
}
}
static void update_window_geometry(void) {
g_debug("update_window_geometry: %u×%u → %u×%u", gui_data.last_screen_width, gui_data.last_screen_height, GB_get_screen_width(&gb), GB_get_screen_height(&gb));
GtkWidget *w = gui_data.fallback_canvas ? GTK_WIDGET(gui_data.fallback_canvas) : GTK_WIDGET(gui_data.gl_area);
signed win_width = gtk_widget_get_allocated_width(w);
signed win_height = gtk_widget_get_allocated_height(w);
signed menu_height = gtk_widget_get_allocated_height(builder_get(GTK_WIDGET, "main_menu"));
unsigned _factor = win_width > win_height ? win_width / GB_get_screen_width(&gb) : win_height / GB_get_screen_height(&gb);
unsigned factor = _factor < 2 ? 2 : _factor;
unsigned new_width = GB_get_screen_width(&gb) * factor;
unsigned new_height = GB_get_screen_height(&gb) * factor + menu_height;
// Set size hints
GdkGeometry hints;
hints.min_width = GB_get_screen_width(&gb);
hints.min_height = GB_get_screen_height(&gb) + menu_height;
gtk_window_set_geometry_hints(
GTK_WINDOW(gui_data.main_window),
NULL,
&hints,
(GdkWindowHints)(GDK_HINT_MIN_SIZE)
);
if (new_width > win_width || new_height > win_height) {
gtk_window_resize(GTK_WINDOW(gui_data.main_window), new_width, new_height);
}
// Setup our image buffers
if (gui_data.image_buffers[0]) g_free(gui_data.image_buffers[0]);
if (gui_data.image_buffers[1]) g_free(gui_data.image_buffers[1]);
if (gui_data.image_buffers[2]) g_free(gui_data.image_buffers[2]);
size_t buffer_size = sizeof(gui_data.image_buffers[0][0]) * GB_get_screen_width(&gb) * GB_get_screen_height(&gb);
gui_data.image_buffers[0] = g_malloc0(buffer_size);
gui_data.image_buffers[1] = g_malloc0(buffer_size);
gui_data.image_buffers[2] = g_malloc0(buffer_size);
gui_data.last_screen_width = GB_get_screen_width(&gb);
gui_data.last_screen_height = GB_get_screen_height(&gb);
if (GB_is_inited(&gb)) {
GB_set_pixels_output(&gb, get_pixels());
}
}
static void stop(void) {
if (!gui_data.running) return;
GB_audio_set_paused(true);
GB_debugger_set_disabled(&gb, true);
if (GB_debugger_is_stopped(&gb)) {
// [self interruptDebugInputRead];
}
gui_data.stopping = true;
gui_data.running = false;
while (gui_data.stopping);
GB_debugger_set_disabled(&gb, false);
gui_data.stopped = true;
}
static gboolean on_vblank(GB_gameboy_t *gb) {
// Queue drawing of the current frame
if (gui_data.fallback_canvas) {
gtk_widget_queue_draw(GTK_WIDGET(gui_data.main_window));
}
else if (gui_data.gl_area) {
gtk_gl_area_queue_render(gui_data.gl_area);
}
gtk_widget_queue_draw(GTK_WIDGET(gui_data.vram_viewer));
return false;
}
static void vblank(GB_gameboy_t *gb) {
flip();
if (gui_data.border_mode_changed) {
GB_set_border_mode(gb, config_get_display_border_mode());
update_window_geometry();
gui_data.border_mode_changed = false;
}
GB_set_pixels_output(gb, get_pixels());
// Handle the speed modifiers:
// The binary slowdown is limited to half speed.
// The analog multiplier can go down to a third and up to three times full speed.
if (gui_data.underclock_down && gui_data.clock_mutliplier > 0.5) {
gui_data.clock_mutliplier -= 1.0 / 16;
//gui_data.clock_mutliplier = clamp_double(0.5, 1.0, gui_data.clock_mutliplier - 1.0 / 16);
GB_set_clock_multiplier(gb, gui_data.clock_mutliplier);
}
else if (!gui_data.underclock_down && gui_data.clock_mutliplier < 1.0) {
gui_data.clock_mutliplier += 1.0 / 16;
//gui_data.clock_mutliplier = clamp_double(0.5, 1.0, gui_data.clock_mutliplier + 1.0 / 16);
GB_set_clock_multiplier(gb, gui_data.clock_mutliplier);
}
else if (config.controls.analog_speed_controls && gui_data.analog_clock_multiplier_valid) {
GB_set_clock_multiplier(gb, gui_data.analog_clock_multiplier);
if (gui_data.analog_clock_multiplier == 1.0) {
gui_data.analog_clock_multiplier_valid = false;
}
}
gui_data.do_rewind = gui_data.rewind_down;
vram_viewer_update(gui_data.vram_viewer, gb);
g_idle_add((GSourceFunc) on_vblank, gb);
}
static void handle_events(GB_gameboy_t *gb) {
SDL_GameControllerUpdate();
uint8_t controller_state = 0;
gui_data.analog_clock_multiplier = 1.0;
for (unsigned i = 0; i < gui_data.controller_count; i++) {
struct Controller_t *s = &gui_data.controllers[i];
int16_t left_x_axis = SDL_GameControllerGetAxis(s->controller, SDL_CONTROLLER_AXIS_LEFTX);
int16_t left_y_axis = SDL_GameControllerGetAxis(s->controller, SDL_CONTROLLER_AXIS_LEFTY);
int16_t right_x_axis = SDL_GameControllerGetAxis(s->controller, SDL_CONTROLLER_AXIS_RIGHTX);
int16_t right_y_axis = SDL_GameControllerGetAxis(s->controller, SDL_CONTROLLER_AXIS_RIGHTY);
if (config.controls.analog_speed_controls) {
double left_trigger = (double) SDL_GameControllerGetAxis(s->controller, SDL_CONTROLLER_AXIS_TRIGGERLEFT) / (double) 32767;
double right_trigger = (double) SDL_GameControllerGetAxis(s->controller, SDL_CONTROLLER_AXIS_TRIGGERRIGHT) / (double) 32767;
if (left_trigger > 0.0) {
gui_data.analog_clock_multiplier = min_double(gui_data.analog_clock_multiplier, clamp_double(1.0 / 3, 1.0, 1 - left_trigger + 0.2));
gui_data.analog_clock_multiplier_valid = true;
}
else if (right_trigger > 0.0) {
gui_data.analog_clock_multiplier = max_double(gui_data.analog_clock_multiplier, clamp_double(1.0, 3.0, right_trigger * 3 + 0.8));
gui_data.analog_clock_multiplier_valid = true;
}
}
if (left_x_axis >= JOYSTICK_HIGH || right_x_axis >= JOYSTICK_HIGH) {
gui_data.last_used_controller = s;
controller_state |= BUTTON_MASK_RIGHT;
}
else if (left_x_axis <= -JOYSTICK_HIGH || right_x_axis <= -JOYSTICK_HIGH) {
gui_data.last_used_controller = s;
controller_state |= BUTTON_MASK_LEFT;
}
if (left_y_axis >= JOYSTICK_HIGH || right_y_axis >= JOYSTICK_HIGH) {
gui_data.last_used_controller = s;
controller_state |= BUTTON_MASK_DOWN;
}
else if (left_y_axis <= -JOYSTICK_HIGH || right_y_axis <= -JOYSTICK_HIGH) {
gui_data.last_used_controller = s;
controller_state |= BUTTON_MASK_UP;
}
if (SDL_GameControllerGetButton(s->controller, SDL_CONTROLLER_BUTTON_DPAD_RIGHT)) {
gui_data.last_used_controller = s;
controller_state |= BUTTON_MASK_RIGHT;
}
if (SDL_GameControllerGetButton(s->controller, SDL_CONTROLLER_BUTTON_DPAD_LEFT)) {
gui_data.last_used_controller = s;
controller_state |= BUTTON_MASK_LEFT;
}
if (SDL_GameControllerGetButton(s->controller, SDL_CONTROLLER_BUTTON_DPAD_UP)) {
gui_data.last_used_controller = s;
controller_state |= BUTTON_MASK_UP;
}
if (SDL_GameControllerGetButton(s->controller, SDL_CONTROLLER_BUTTON_DPAD_DOWN)) {
gui_data.last_used_controller = s;
controller_state |= BUTTON_MASK_DOWN;
}
if (SDL_GameControllerGetButton(s->controller, SDL_CONTROLLER_BUTTON_A)) {
gui_data.last_used_controller = s;
controller_state |= BUTTON_MASK_A;
}
if (SDL_GameControllerGetButton(s->controller, SDL_CONTROLLER_BUTTON_B)) {
gui_data.last_used_controller = s;
controller_state |= BUTTON_MASK_B;
}
if (SDL_GameControllerGetButton(s->controller, SDL_CONTROLLER_BUTTON_BACK)) {
gui_data.last_used_controller = s;
controller_state |= BUTTON_MASK_SELECT;
}
if (SDL_GameControllerGetButton(s->controller, SDL_CONTROLLER_BUTTON_START)) {
gui_data.last_used_controller = s;
controller_state |= BUTTON_MASK_START;
}
}
GB_set_key_state(gb, GB_KEY_RIGHT, (gui_data.pressed_buttons & BUTTON_MASK_RIGHT) | (controller_state & BUTTON_MASK_RIGHT));
GB_set_key_state(gb, GB_KEY_LEFT, (gui_data.pressed_buttons & BUTTON_MASK_LEFT) | (controller_state & BUTTON_MASK_LEFT));
GB_set_key_state(gb, GB_KEY_UP, (gui_data.pressed_buttons & BUTTON_MASK_UP) | (controller_state & BUTTON_MASK_UP));
GB_set_key_state(gb, GB_KEY_DOWN, (gui_data.pressed_buttons & BUTTON_MASK_DOWN) | (controller_state & BUTTON_MASK_DOWN));
GB_set_key_state(gb, GB_KEY_A, (gui_data.pressed_buttons & BUTTON_MASK_A) | (controller_state & BUTTON_MASK_A));
GB_set_key_state(gb, GB_KEY_B, (gui_data.pressed_buttons & BUTTON_MASK_B) | (controller_state & BUTTON_MASK_B));
GB_set_key_state(gb, GB_KEY_SELECT, (gui_data.pressed_buttons & BUTTON_MASK_SELECT) | (controller_state & BUTTON_MASK_SELECT));
GB_set_key_state(gb, GB_KEY_START, (gui_data.pressed_buttons & BUTTON_MASK_START) | (controller_state & BUTTON_MASK_START));
}
static void load_boot_rom(GB_gameboy_t *gb, GB_boot_rom_t type) {
GError *error = NULL;
char *boot_rom_path = NULL;
GBytes *boot_rom_f = NULL;
const guchar *boot_rom_data;
gsize boot_rom_size;
static const char *const names[] = {
[GB_BOOT_ROM_DMG0] = "dmg0_boot.bin",
[GB_BOOT_ROM_DMG] = "dmg_boot.bin",
[GB_BOOT_ROM_MGB] = "mgb_boot.bin",
[GB_BOOT_ROM_SGB] = "sgb_boot.bin",
[GB_BOOT_ROM_SGB2] = "sgb2_boot.bin",
[GB_BOOT_ROM_CGB0] = "cgb0_boot.bin",
[GB_BOOT_ROM_CGB] = "cgb_boot.bin",
[GB_BOOT_ROM_AGB] = "agb_boot.bin",
};
const char *const boot_rom_name = names[type];
if (gui_data.cli_options.boot_rom_path != NULL) {
g_message("[CLI override] Trying to load boot ROM from %s", gui_data.cli_options.boot_rom_path);
if (GB_load_boot_rom(gb, gui_data.cli_options.boot_rom_path)) {
g_warning("Falling back to boot ROM from config");
goto config_boot_rom;
}
}
else { config_boot_rom:
if (config.emulation.boot_rom_path != NULL && g_strcmp0(config.emulation.boot_rom_path, "other") != 0 && g_strcmp0(config.emulation.boot_rom_path, "auto") != 0) {
boot_rom_path = g_build_filename(config.emulation.boot_rom_path, boot_rom_name, NULL);
g_message("Trying to load boot ROM from %s", boot_rom_path);
if (GB_load_boot_rom(gb, boot_rom_path)) {
g_free(boot_rom_path);
g_warning("Falling back to internal boot ROM");
goto internal_boot_rom;
}
g_free(boot_rom_path);
}
else { internal_boot_rom:
boot_rom_path = g_build_filename(RESOURCE_PREFIX "bootroms/", boot_rom_name, NULL);
boot_rom_f = g_resources_lookup_data(boot_rom_path, G_RESOURCE_LOOKUP_FLAGS_NONE, &error);
g_message("Loading internal boot ROM: %s", boot_rom_path);
g_free(boot_rom_path);
if (boot_rom_f == NULL) {
g_warning("Failed to load internal boot ROM: %s", boot_rom_path);
g_error_free(error);
exit(EXIT_FAILURE);
}
boot_rom_data = g_bytes_get_data(boot_rom_f, &boot_rom_size);
GB_load_boot_rom_from_buffer(gb, boot_rom_data, boot_rom_size);
g_bytes_unref(boot_rom_f);
}
}
}
static void init(void) {
if (GB_is_inited(&gb)) return;
GB_init(&gb, config_get_model_type(&gui_data));
GB_set_vblank_callback(&gb, vblank);
GB_set_pixels_output(&gb, get_current_buffer());
GB_set_rgb_encode_callback(&gb, rgb_encode);
GB_set_sample_rate(&gb, GB_audio_get_sample_rate());
GB_set_color_correction_mode(&gb, config_get_color_correction_mode());
GB_set_highpass_filter_mode(&gb, config_get_highpass_mode());
GB_set_rewind_length(&gb, config.emulation.rewind_duration);
GB_set_update_input_hint_callback(&gb, handle_events);
GB_apu_set_sample_callback(&gb, gb_audio_callback);
GB_set_input_callback(&gb, sync_console_input);
GB_set_async_input_callback(&gb, async_console_input);
GB_set_log_callback(&gb, console_log);
GB_set_boot_rom_load_callback(&gb, load_boot_rom);
GB_set_rumble_callback(&gb, rumble_callback);
GB_set_rumble_mode(&gb, config_get_rumble_mode());
if (config_get_display_border_mode() <= GB_BORDER_ALWAYS) {
GB_set_border_mode(&gb, config_get_display_border_mode());
}
update_window_geometry();
}
static void reset(void) {
g_debug("Reset: %d == %d", config_get_model_type(&gui_data), gui_data.prev_model);
GB_model_t current_model = config_get_model_type(&gui_data);
if (gui_data.prev_model == -1 || gui_data.prev_model == current_model) {
GB_reset(&gb);
}
else {
GB_switch_model_and_reset(&gb, current_model);
}
GB_set_palette(&gb, config_get_monochrome_palette());
gui_data.prev_model = config_get_model_type(&gui_data);
// Check SGB -> non-SGB and non-SGB to SGB transitions
if (GB_get_screen_width(&gb) != gui_data.last_screen_width || GB_get_screen_height(&gb) != gui_data.last_screen_height) {
update_window_geometry();
}
bool success = false;
if (gui_data.file) {
char *path = g_file_get_path(gui_data.file);
char *ext = strrchr(path, '.');
int result;
GB_debugger_clear_symbols(&gb);
if (g_strcmp0(ext + 1, "isx") == 0) {
result = GB_load_isx(&gb, path);
}
else {
result = GB_load_rom(&gb, path);
}
if (result == 0) {
success = true;
}
else {
g_warning("Failed to load ROM: %s", path);
}
GB_load_battery(&gb, gui_data.battery_save_path);
GB_load_cheats(&gb, gui_data.cheats_save_path);
GError *error = NULL;
GBytes *register_sym_f = g_resources_lookup_data(RESOURCE_PREFIX "Misc/registers.sym", G_RESOURCE_LOOKUP_FLAGS_NONE, &error);
if (register_sym_f) {
gsize register_sym_size;
const gchar *register_sym_data = g_bytes_get_data(register_sym_f, &register_sym_size);
GB_debugger_load_symbol_file_from_buffer(&gb, register_sym_data, register_sym_size);
g_bytes_unref(register_sym_f);
}
size_t path_length = strlen(path);
char sym_file_path[path_length + 5];
replace_extension(path, path_length, sym_file_path, ".sym");
GB_debugger_load_symbol_file(&gb, sym_file_path);
g_free(path);
}
action_set_enabled(gui_data.main_application, "close", success);
action_entries_set_enabled(emulation_entries, G_N_ELEMENTS(emulation_entries), success);
}
static void start(void) {
gui_data.running = true;
gui_data.stopped = false;
GB_audio_clear_queue();
GB_audio_set_paused(config.audio.muted);
/* Run emulation */
while (gui_data.running) {
if (gui_data.rewind_paused) {
handle_events(&gb);
g_usleep(G_USEC_PER_SEC / 8);
}
else {
if (gui_data.do_rewind) {
GB_rewind_pop(&gb);
if (gui_data.turbo_down) {
GB_rewind_pop(&gb);
}
if (!GB_rewind_pop(&gb)) {
gui_data.rewind_paused = true;
}
gui_data.do_rewind = false;
}
GB_run(&gb);
}
}
if (gui_data.file) {
GB_save_battery(&gb, gui_data.battery_save_path);
GB_save_cheats(&gb, gui_data.cheats_save_path);
}
gui_data.stopping = false;
}
// Prevent dependency loop
static void run(void);
// app.reset GAction
// Resets the emulation
static void activate_reset(GSimpleAction *action, GVariant *parameter, gpointer app) {
if (!GB_is_inited(&gb)) {
init();
}
stop();
reset();
run();
}
static gpointer run_thread(gpointer null_ptr) {
if (!gui_data.file) return NULL;
char *path = g_file_get_path(gui_data.file);
size_t path_length = strlen(path);
/* At the worst case, size is strlen(path) + 4 bytes for .sav + NULL */
char battery_save_path[path_length + 5];
char cheats_save_path[path_length + 5];
replace_extension(path, path_length, battery_save_path, ".sav");
replace_extension(path, path_length, cheats_save_path, ".cht");
gui_data.battery_save_path = battery_save_path;
gui_data.cheats_save_path = cheats_save_path;
if (!GB_is_inited(&gb)) {
init();
}
if (gui_data.stopped) {
start();
}
else {
reset();
start();
}
return NULL;
}
static void run(void) {
if (gui_data.running) return;
while (gui_data.stopping);
g_thread_new("CoreLoop", run_thread, NULL);
}
// Tell our application to quit.
// After this functions has been called the `shutdown` signal will be issued.
//
// TODO: Make sure we have a way to quit our emulation loop before `shutdown` gets called
static void quit(void) {
g_debug("quit(void);");
stop();
GtkWindow *window = gui_data.main_window ? GTK_WINDOW(gui_data.main_window) : NULL;
save_config(window, gui_data.config_modification_date);
free_config();
for (unsigned i = 0; i < gui_data.controller_count; i++) {
struct Controller_t *s = &gui_data.controllers[i];
SDL_HapticClose(s->haptic);
SDL_GameControllerClose(s->controller);
}
// Quit our application properly.
// This fires the “shutdown” signal.
g_application_quit(G_APPLICATION(gui_data.main_application));
}
static void quit_interrupt(int ignored) {
g_debug("quit_interrupt(%d);", ignored);
quit();
}
static void create_action_groups(GApplication *app) {
g_action_map_add_action_entries(G_ACTION_MAP(app), emulation_entries, G_N_ELEMENTS(emulation_entries), NULL);
g_action_map_add_action_entries(G_ACTION_MAP(app), developer_entries, G_N_ELEMENTS(developer_entries), NULL);
g_action_map_add_action_entries(G_ACTION_MAP(app), app_entries, G_N_ELEMENTS(app_entries), NULL);
g_action_map_add_action_entries(G_ACTION_MAP(app), file_entries, G_N_ELEMENTS(file_entries), NULL);
g_action_map_add_action_entries(G_ACTION_MAP(app), edit_entries, G_N_ELEMENTS(edit_entries), NULL);
action_set_enabled(app, "close", false);
action_entries_set_enabled(emulation_entries, G_N_ELEMENTS(emulation_entries), false);
}
static gboolean on_key_press(GtkWidget *w, GdkEventKey *event, gpointer data) {
uint8_t mask;
if (event->keyval == key_map[INPUT_UP]) mask = BUTTON_MASK_UP;
if (event->keyval == key_map[INPUT_DOWN]) mask = BUTTON_MASK_DOWN;
if (event->keyval == key_map[INPUT_LEFT]) mask = BUTTON_MASK_LEFT;
if (event->keyval == key_map[INPUT_RIGHT]) mask = BUTTON_MASK_RIGHT;
if (event->keyval == key_map[INPUT_START]) mask = BUTTON_MASK_START;
if (event->keyval == key_map[INPUT_SELECT]) mask = BUTTON_MASK_SELECT;
if (event->keyval == key_map[INPUT_A]) mask = BUTTON_MASK_A;
if (event->keyval == key_map[INPUT_B]) mask = BUTTON_MASK_B;
if (event->keyval == key_map[INPUT_REWIND]) {
gui_data.rewind_down = event->type == GDK_KEY_PRESS;
GB_set_turbo_mode(&gb, gui_data.turbo_down, gui_data.turbo_down && gui_data.rewind_down);
if (event->type == GDK_KEY_RELEASE) {
gui_data.rewind_paused = false;
}
}
if (event->keyval == key_map[INPUT_TURBO]) {
gui_data.turbo_down = event->type == GDK_KEY_PRESS;
gui_data.analog_clock_multiplier_valid = false;
GB_audio_clear_queue();
GB_set_turbo_mode(&gb, gui_data.turbo_down, gui_data.turbo_down && gui_data.rewind_down);
}
if (event->keyval == key_map[INPUT_SLOWDOWN]) {
gui_data.underclock_down = event->type == GDK_KEY_PRESS;
gui_data.analog_clock_multiplier_valid = false;
}
if (event->keyval == key_map[INPUT_FULLSCREEN]) {
if (event->type == GDK_KEY_RELEASE) {
if (gui_data.is_fullscreen) {
gtk_window_unfullscreen(GTK_WINDOW(gui_data.main_window));
}
else {
gtk_window_fullscreen(GTK_WINDOW(gui_data.main_window));
}
}
}
if (event->type == GDK_KEY_PRESS) {
gui_data.pressed_buttons |= mask;
}
else if (event->type == GDK_KEY_RELEASE) {
gui_data.pressed_buttons &= ~mask;
}
return false;
}
static void on_window_state_change(GtkWidget *w, GdkEventWindowState *event, gpointer data) {
gui_data.is_fullscreen = event->new_window_state & GDK_WINDOW_STATE_FULLSCREEN;
}
// This functions gets called immediately after registration of the GApplication
static void startup(GApplication *app, gpointer null_ptr) {
signal(SIGINT, quit_interrupt);
g_debug("GTK version %u.%u.%u", gtk_get_major_version(), gtk_get_minor_version(), gtk_get_micro_version());
if (gui_data.cli_options.force_software_renderer) {
g_message("Forcing fallback renderer!");
}
else {
// Very ugly workaround for GtkGlArea!
// When a GtkGlArea is realized and it creates a legacy GL 1.4 context
// it tries to use GL 2.0 functions to render the window which leads to the application crashing.
// So we initialize GTK, create a dummy GtkWindow object, attach a `realize` callback and
// in this callback create a GdkGLContext on this window. But instead of running the GTK main loop
// we just realize and destroy the dummy window and compare the contexts version in the realize callback.
gui_data.supports_gl = test_gl_support();
g_debug("OpenGL supported: %s", gui_data.supports_gl? "Yes" : "No");
}
gui_data.builder = gtk_builder_new_from_resource(RESOURCE_PREFIX "ui/window.ui");
gtk_builder_connect_signals(gui_data.builder, NULL);
create_action_groups(app);
#if NDEBUG
// Disable when not compiled in debug mode
action_set_enabled(app, "open_gtk_debugger", false);
#endif
gui_data.preferences = GTK_WINDOW(get_object("preferences"));
g_signal_connect(gui_data.preferences, "realize", G_CALLBACK(on_preferences_realize), (gpointer) gui_data.builder);
init_config(app, gui_data.cli_options.config_path, &gui_data.config_modification_date, gui_data.preferences);
gui_data.vram_viewer = vram_viewer_new();
gui_data.memory_viewer = GTK_WINDOW(get_object("memory_viewer"));
gui_data.console = GTK_WINDOW(get_object("console"));
gui_data.printer = GTK_WINDOW(get_object("printer"));
if (config.audio.sample_rate == -1) {
gui_data.sample_rate = GB_audio_default_sample_rate();
}
else {
gui_data.sample_rate = config.audio.sample_rate;
}
// setup main window
gui_data.main_window = GTK_APPLICATION_WINDOW(gtk_application_window_new(GTK_APPLICATION(app)));
gui_data.main_window_container = GTK_BOX(gtk_box_new(GTK_ORIENTATION_VERTICAL, 0));
gtk_window_set_title(GTK_WINDOW(gui_data.main_window), "SameBoy");
gtk_application_window_set_show_menubar(gui_data.main_window, false);
gtk_container_add(GTK_CONTAINER(gui_data.main_window), GTK_WIDGET(gui_data.main_window_container));
setup_menu(app);
// Insert separators into `GtkComboBox`es
set_combo_box_row_separator_func(GTK_CONTAINER(gui_data.preferences));
set_combo_box_row_separator_func(GTK_CONTAINER(gui_data.vram_viewer));
set_combo_box_row_separator_func(GTK_CONTAINER(gui_data.memory_viewer));
// Define a set of window icons
GList *icon_list = NULL;
static char* icons[] = {
RESOURCE_PREFIX "logo_256.png",
RESOURCE_PREFIX "logo_128.png",
RESOURCE_PREFIX "logo_64.png",
RESOURCE_PREFIX "logo_48.png",
RESOURCE_PREFIX "logo_32.png",
RESOURCE_PREFIX "logo_16.png"
};
// Create list of GdkPixbufs
for (int i = 0; i < (sizeof(icons) / sizeof(const char*)); ++i) {
GdkPixbuf *icon = gdk_pixbuf_new_from_resource(icons[i], NULL);
if (!icon) continue;
icon_list = g_list_prepend(icon_list, icon);
}
// Let GTK choose the proper icon
gtk_window_set_default_icon_list(icon_list);
// Add missing information to the about dialog
GtkAboutDialog *about_dialog = GTK_ABOUT_DIALOG(get_object("about_dialog"));
gtk_about_dialog_set_logo(about_dialog, gdk_pixbuf_new_from_resource(icons[2], NULL)); // reuse the 64x64 icon
gtk_about_dialog_set_version(about_dialog, "v" xstr(VERSION));
g_list_free_full(icon_list, g_object_unref);
}
G_MODULE_EXPORT void on_quit_activate(GtkWidget *w, gpointer user_data_ptr) {
quit();
}
bool on_change_model(GtkWidget *widget, gpointer user_data) {
GtkCheckMenuItem *check_menu_item = GTK_CHECK_MENU_ITEM(widget);
gchar *model_str = (gchar *) user_data;
if (!gtk_check_menu_item_get_active(check_menu_item)) {
return true;
}
else if (!GB_is_inited(&gb)) {
gui_data.cli_options.model = -1;
config.emulation.model = model_str;
return false;
}
GtkMessageDialog *dialog = GTK_MESSAGE_DIALOG(gtk_message_dialog_new(
GTK_WINDOW(gui_data.main_window),
GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT,
GTK_MESSAGE_QUESTION,
GTK_BUTTONS_YES_NO,
"Changing the emulated model requires a reset.\nChange model and reset game?"
));
stop();
gint result = gtk_dialog_run(GTK_DIALOG(dialog));
switch (result) {
case GTK_RESPONSE_YES:
// Reset the CLI model override
gui_data.cli_options.model = -1;
config.emulation.model = model_str;
reset();
break;
default:
// Action has been canceled
break;
}
run();
gtk_widget_destroy(GTK_WIDGET(dialog));
return result != GTK_RESPONSE_YES;
}
static void connect_signal_handlers(GApplication *app) {
// Connect signal handlers
gtk_widget_add_events(GTK_WIDGET(gui_data.main_window), GDK_KEY_PRESS_MASK);
gtk_widget_add_events(GTK_WIDGET(gui_data.main_window), GDK_KEY_RELEASE_MASK);
g_signal_connect(gui_data.main_window, "destroy", G_CALLBACK(on_quit_activate), app);
g_signal_connect(gui_data.main_window, "key_press_event", G_CALLBACK(on_key_press), NULL);
g_signal_connect(gui_data.main_window, "key_release_event", G_CALLBACK(on_key_press), NULL);
g_signal_connect(gui_data.main_window, "window-state-event", G_CALLBACK(on_window_state_change), NULL);
// Just hide our sub-windows when closing them
g_signal_connect(gui_data.preferences, "delete-event", G_CALLBACK(gtk_widget_hide_on_delete), NULL);
g_signal_connect(gui_data.vram_viewer, "delete-event", G_CALLBACK(gtk_widget_hide_on_delete), NULL);
g_signal_connect(gui_data.memory_viewer, "delete-event", G_CALLBACK(gtk_widget_hide_on_delete), NULL);
g_signal_connect(gui_data.console, "delete-event", G_CALLBACK(gtk_widget_hide_on_delete), NULL);
g_signal_connect(gui_data.printer, "delete-event", G_CALLBACK(gtk_widget_hide_on_delete), NULL);
}
// TODO: Comment
static void gl_draw(void) {
uint32_t *pixels = get_current_buffer();
uint32_t *previous = get_previous_buffer();
static void *_pixels = NULL;
if (pixels) {
_pixels = pixels;
}
glClearColor(0, 0, 0, 1);
glClear(GL_COLOR_BUFFER_BIT);
GB_frame_blending_mode_t mode = config_get_frame_blending_mode();
if (!previous) {
mode = GB_FRAME_BLENDING_MODE_DISABLED;
}
else if (mode == GB_FRAME_BLENDING_MODE_ACCURATE) {
if (GB_is_sgb(&gb)) {
mode = GB_FRAME_BLENDING_MODE_SIMPLE;
}
else {
mode = GB_is_odd_frame(&gb)? GB_FRAME_BLENDING_MODE_ACCURATE_ODD : GB_FRAME_BLENDING_MODE_ACCURATE_EVEN;
}
}
render_bitmap_with_shader(
&gui_data.shader, _pixels, previous,
GB_get_screen_width(&gb), GB_get_screen_height(&gb),
gui_data.viewport.x, gui_data.viewport.y, gui_data.viewport.width, gui_data.viewport.height,
mode
);
}
// TODO: Comment
static void gl_finish(void) { }
// TODO: Comment
static void resize(void) {
update_viewport();
}
// TODO: Comment
static gboolean on_draw_fallback(GtkWidget *widget, cairo_t *cr, gpointer data) {
GtkStyleContext *context = gtk_widget_get_style_context(widget);
guint width = gtk_widget_get_allocated_width(widget);
guint height = gtk_widget_get_allocated_height(widget);
guint screen_width = GB_get_screen_width(&gb);
guint screen_height = GB_get_screen_height(&gb);
gtk_render_background(context, cr, 0, 0, width, height);
gtk_render_frame(context, cr, 0, 0, width, height);
cairo_surface_t *surface = cairo_image_surface_create_for_data(
(unsigned char *) get_current_buffer(),
CAIRO_FORMAT_RGB24,
screen_width,
screen_height,
cairo_format_stride_for_width(CAIRO_FORMAT_RGB24, screen_width)
);
cairo_translate(cr, gui_data.viewport.x, gui_data.viewport.y);
cairo_scale(cr, gui_data.viewport.width / screen_width, gui_data.viewport.height / screen_height);
cairo_set_source_surface(cr, surface, 0, 0);
cairo_pattern_set_filter(cairo_get_source(cr), CAIRO_FILTER_NEAREST);
cairo_paint(cr);
return false;
}
// Create a GtkDrawingArea as a fallback in case we cant use OpenGL
static void create_fallback_canvas(void) {
gui_data.fallback_canvas = GTK_DRAWING_AREA(gtk_drawing_area_new());
g_signal_connect(gui_data.fallback_canvas, "draw", G_CALLBACK(on_draw_fallback), NULL);
g_signal_connect(gui_data.fallback_canvas, "size-allocate", G_CALLBACK(resize), NULL);
gtk_box_pack_end(GTK_BOX(gui_data.main_window_container), GTK_WIDGET(gui_data.fallback_canvas), true, true, 0);
}
// TODO: Comment
static void gl_init(GtkWidget *w) {
GtkGLArea *gl_area = GTK_GL_AREA(w);
g_debug("GL_INIT");
const char *renderer;
g_debug("GL Context: %p", gtk_gl_area_get_context(gl_area));
gtk_gl_area_make_current(gl_area);
if (gtk_gl_area_get_error(gl_area) != NULL) {
goto error;
}
renderer = (char *)glGetString(GL_RENDERER);
g_debug("GtkGLArea on %s", renderer ? renderer : "Unknown");
if (config.video.shader == NULL || (!init_shader_with_name(&gui_data.shader, config.video.shader) && !init_shader_with_name(&gui_data.shader, "NearestNeighbor"))) {
GError *error = g_error_new_literal(g_quark_from_string("sameboy-gl-error"), 1, "Failed to initialize shaders");
gtk_gl_area_set_error(gl_area, error);
}
else {
g_signal_connect(gl_area, "render", G_CALLBACK(gl_draw), NULL);
g_signal_connect(gl_area, "resize", G_CALLBACK(resize), NULL);
g_signal_connect(gl_area, "unrealize", G_CALLBACK(gl_finish), NULL);
return;
}
error:
if (gtk_gl_area_get_error(gl_area) != NULL) {
g_warning("GtkGLArea: %s", gtk_gl_area_get_error(gl_area)->message);
}
create_fallback_canvas();
}
static void create_canvas(void) {
// create our renderer area
if (gui_data.supports_gl) {
gui_data.gl_area = GTK_GL_AREA(gtk_gl_area_new());
gtk_gl_area_set_required_version(gui_data.gl_area, 3, 2);
gtk_gl_area_set_auto_render(gui_data.gl_area, false);
gtk_gl_area_set_has_alpha(gui_data.gl_area, false);
gtk_gl_area_set_has_depth_buffer(gui_data.gl_area, false);
gtk_gl_area_set_has_stencil_buffer(gui_data.gl_area, false);
g_signal_connect(gui_data.gl_area, "realize", G_CALLBACK(gl_init), NULL);
gtk_box_pack_end(GTK_BOX(gui_data.main_window_container), GTK_WIDGET(gui_data.gl_area), true, true, 0);
}
else {
create_fallback_canvas();
}
GdkScreen *screen = gdk_screen_get_default();
GtkCssProvider *provider = gtk_css_provider_new();
gtk_css_provider_load_from_resource(provider, RESOURCE_PREFIX "css/main.css");
gtk_style_context_add_provider_for_screen(screen, GTK_STYLE_PROVIDER(provider), GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
}
static void setup_console(void) {
GtkTextView *text_view = builder_get(GTK_TEXT_VIEW, "console_screen");
GtkTextBuffer *text_buf = gtk_text_view_get_buffer(text_view);
gtk_text_view_set_buffer(
builder_get(GTK_TEXT_VIEW, "console_sidebar_output"),
gtk_text_buffer_new(gtk_text_buffer_get_tag_table(text_buf))
);
gtk_text_buffer_create_tag(text_buf, "bold", "weight", PANGO_WEIGHT_BOLD, NULL);
gtk_text_buffer_create_tag(text_buf, "underline", "underline", PANGO_UNDERLINE_SINGLE, "underline-set", true, NULL);
gtk_text_buffer_create_tag(text_buf, "dashed_underline", "underline", PANGO_UNDERLINE_DOUBLE, "underline-set", true, NULL);
g_mutex_init(&gui_data.debugger_input_mutex);
g_cond_init(&gui_data.debugger_input_cond);
g_rec_mutex_init(&gui_data.console_output_lock);
if (!gui_data.debugger_input_queue) {
gui_data.debugger_input_queue = g_ptr_array_sized_new(4);
}
if (!gui_data.pending_console_output) {
gui_data.pending_console_output = gtk_text_buffer_new(gtk_text_buffer_get_tag_table(text_buf));
}
}
// This function gets called when the GApplication gets activated, i.e. it is ready to show widgets.
static void activate(GApplication *app, gpointer null_ptr) {
init_audio();
init_controllers();
connect_signal_handlers(app);
create_canvas();
setup_console();
if (gui_data.cli_options.fullscreen) {
gtk_window_fullscreen(GTK_WINDOW(gui_data.main_window));
}
gtk_application_add_window(GTK_APPLICATION(app), GTK_WINDOW(gui_data.main_window));
gtk_widget_show_all(GTK_WIDGET(gui_data.main_window));
update_window_geometry();
// Start the emulation thread
run();
}
// This function gets called when the application is closed.
static void shutdown(GApplication *app, GFile **files, gint n_files, const gchar *hint, gpointer null_ptr) {
g_debug("SHUTDOWN");
stop();
SDL_Quit();
if (gui_data.image_buffers[0]) g_free(gui_data.image_buffers[0]);
if (gui_data.image_buffers[1]) g_free(gui_data.image_buffers[1]);
if (gui_data.image_buffers[2]) g_free(gui_data.image_buffers[2]);
free_shader(&gui_data.shader);
free_master_shader();
GB_free(&gb);
g_object_unref(gui_data.builder);
}
// This function gets called when there are files to open.
// Note: When `open` gets called `activate` wont fire unless we call it ourselves.
static void open(GApplication *app, GFile **files, gint n_files, const gchar *hint, gpointer null_ptr) {
if (n_files > 1) {
g_warning("More than one file specified");
exit(EXIT_FAILURE);
}
gui_data.file = g_file_dup(files[0]);
// We have handled the files, now activate the application
activate(app, NULL);
}
// app.about GAction
// Opens the about dialog
static void activate_about(GSimpleAction *action, GVariant *parameter, gpointer app) {
GObject *dialog = get_object("about_dialog");
gtk_dialog_run(GTK_DIALOG(dialog));
gtk_widget_hide(GTK_WIDGET(dialog));
}
// app.show_console GAction
// Opens the console
static void activate_show_console(GSimpleAction *action, GVariant *parameter, gpointer app) {
if (gui_data.debugger_input_queue) {
while (gui_data.debugger_input_queue->len) {
g_ptr_array_remove_index_fast(gui_data.debugger_input_queue, gui_data.debugger_input_queue->len - 1);
}
}
gtk_widget_show_all(builder_get(GTK_WIDGET, "console"));
}
// app.open_gtk_debugger GAction
// Opens the GTK debugger
static void activate_open_gtk_debugger(GSimpleAction *action, GVariant *parameter, gpointer app) {
gtk_window_set_interactive_debugging(true);
}
// app.open_memory_viewer GAction
// Opens the memory viewer window
static void activate_open_memory_viewer(GSimpleAction *action, GVariant *parameter, gpointer app) {
gtk_widget_show_all(GTK_WIDGET(gui_data.memory_viewer));
}
// app.open_vram_viewer GAction
// Opens the VRAM viewer window
static void activate_open_vram_viewer(GSimpleAction *action, GVariant *parameter, gpointer app) {
gtk_widget_show_all(GTK_WIDGET(gui_data.vram_viewer));
}
// Closes a ROM
static void close_rom(void) {
stop();
GB_free(&gb);
// Clear the screen as side effect
update_window_geometry();
if (gui_data.fallback_canvas) {
gtk_widget_queue_draw(GTK_WIDGET(gui_data.main_window));
}
else if (gui_data.gl_area) {
gtk_gl_area_queue_render(gui_data.gl_area);
}
vram_viewer_clear(gui_data.vram_viewer);
// Redraw the VRAM viewer
gtk_widget_queue_draw(GTK_WIDGET(gui_data.vram_viewer));
// Update menu action states
action_set_enabled(gui_data.main_application, "close", false);
action_entries_set_enabled(emulation_entries, G_N_ELEMENTS(emulation_entries), false);
// Try force the queued redraws
while (g_main_context_pending(NULL)) {
g_main_context_iteration(NULL, FALSE);
}
}
// app.open GAction
// Opens a ROM file
static void activate_open(GSimpleAction *action, GVariant *parameter, gpointer app) {
stop();
GtkFileChooserNative *native = gtk_file_chooser_native_new("Open File", GTK_WINDOW(gui_data.main_window), GTK_FILE_CHOOSER_ACTION_OPEN, "_Open", "_Cancel");
gint res = gtk_native_dialog_run(GTK_NATIVE_DIALOG(native));
if (res == GTK_RESPONSE_ACCEPT) {
const char* path = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(native));
gui_data.file = g_file_new_for_path(path);
activate_reset(action, parameter, app);
}
else {
run();
}
g_object_unref(native);
}
// app.close GAction
static void activate_close(GSimpleAction *action, GVariant *parameter, gpointer app) {
close_rom();
}
// app.preferences GAction
// Opens the preferences window
static void activate_preferences(GSimpleAction *action, GVariant *parameter, gpointer app) {
gtk_widget_show_all(GTK_WIDGET(get_object("preferences")));
}
// app.quit GAction
// Exits the application
static void activate_quit(GSimpleAction *action, GVariant *parameter, gpointer app) {
quit();
}
// app.clear_console GAction
// Clears the debugger console
static void activate_clear_console(GSimpleAction *action, GVariant *parameter, gpointer app) {
g_rec_mutex_lock(&gui_data.console_output_lock);
GtkTextView *text_view = builder_get(GTK_TEXT_VIEW, "console_screen");
GtkTextBuffer *text_buf = gtk_text_view_get_buffer(text_view);
gtk_text_buffer_set_text(text_buf, "", -1);
g_rec_mutex_unlock(&gui_data.console_output_lock);
}
static void on_mute_changed(GSimpleAction *action, GVariant *value, gpointer user_data_ptr) {
config.audio.muted = g_variant_get_boolean(value);
GB_audio_set_paused(config.audio.muted);
g_simple_action_set_state(action, value);
}
static void on_pause_changed(GSimpleAction *action, GVariant *value, gpointer user_data_ptr) {
if (g_variant_get_boolean(value)) {
stop();
}
else {
run();
}
g_simple_action_set_state(action, value);
}
G_MODULE_EXPORT void on_open_recent_activate(GtkRecentChooser *chooser, gpointer user_data_ptr) {
stop();
gchar *uri = gtk_recent_chooser_get_current_uri(chooser);
GFile *file = g_file_new_for_uri(uri);
if (g_file_query_exists(file, NULL)) {
gui_data.file = file;
// Add the file back to the top of the list
GtkRecentManager *manager = gtk_recent_manager_get_default();
gtk_recent_manager_add_item(manager, uri);
// TODO: Not nice
activate_reset(NULL, NULL, NULL);
}
else {
// TODO
g_warning("File not found: %s", uri);
close_rom();
}
}
G_MODULE_EXPORT void on_boot_rom_location_changed(GtkWidget *w, gpointer user_data_ptr) {
GtkComboBox *box = GTK_COMBO_BOX(w);
const gchar *id = gtk_combo_box_get_active_id(box);
if (id == NULL) return;
if (g_strcmp0(id, "other") == 0) {
GtkFileChooserNative *native = gtk_file_chooser_native_new("Select Folder", gui_data.preferences, GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER, "_Select", "_Cancel");
gint res = gtk_native_dialog_run(GTK_NATIVE_DIALOG(native));
if (res == GTK_RESPONSE_ACCEPT) {
config.emulation.boot_rom_path = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(native));
update_boot_rom_selector(gui_data.builder);
}
g_object_unref(native);
}
else {
config.emulation.boot_rom_path = (gchar *)id;
}
}
G_MODULE_EXPORT void on_cgb_model_changed(GtkWidget *w, gpointer user_data_ptr) {
GtkComboBox *box = GTK_COMBO_BOX(w);
config.emulation.cgb_revision_name = (gchar *)gtk_combo_box_get_active_id(box);
}
G_MODULE_EXPORT void on_color_correction_changed(GtkWidget *w, gpointer user_data_ptr) {
GtkComboBox *box = GTK_COMBO_BOX(w);
config.video.color_correction_id = (gchar *)gtk_combo_box_get_active_id(box);
if (GB_is_inited(&gb)) {
GB_set_color_correction_mode(&gb, config_get_color_correction_mode());
}
}
G_MODULE_EXPORT void on_frame_blending_changed(GtkWidget *w, gpointer user_data_ptr) {
GtkComboBox *box = GTK_COMBO_BOX(w);
config.video.frame_blending_mode = (gchar *)gtk_combo_box_get_active_id(box);
}
G_MODULE_EXPORT void on_display_border_changed(GtkWidget *w, gpointer user_data_ptr) {
GtkComboBox *box = GTK_COMBO_BOX(w);
config.video.display_border_mode = (gchar *)gtk_combo_box_get_active_id(box);
gui_data.border_mode_changed = true;
}
G_MODULE_EXPORT void on_monochrome_palette_changed(GtkWidget *w, gpointer user_data_ptr) {
GtkComboBox *box = GTK_COMBO_BOX(w);
config.video.monochrome_palette_id = (gchar *)gtk_combo_box_get_active_id(box);
GB_set_palette(&gb, config_get_monochrome_palette());
}
G_MODULE_EXPORT void on_dmg_model_changed(GtkWidget *w, gpointer user_data_ptr) {
GtkComboBox *box = GTK_COMBO_BOX(w);
config.emulation.dmg_revision_name = (gchar *)gtk_combo_box_get_active_id(box);
}
G_MODULE_EXPORT void on_graphic_filter_changed(GtkWidget *w, gpointer user_data_ptr) {
GtkComboBox *box = GTK_COMBO_BOX(w);
config.video.shader = (gchar *)gtk_combo_box_get_active_id(box);
free_shader(&gui_data.shader);
init_shader_with_name(&gui_data.shader, config.video.shader);
}
G_MODULE_EXPORT void on_highpass_filter_changed(GtkWidget *w, gpointer user_data_ptr) {
config.audio.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, config_get_highpass_mode());
}
}
G_MODULE_EXPORT void on_keep_aspect_ratio_changed(GtkWidget *w, gpointer user_data_ptr) {
GtkCheckButton *button = GTK_CHECK_BUTTON(w);
gboolean value = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(button));
config.video.keep_aspect_ratio = value;
update_viewport();
}
G_MODULE_EXPORT void on_rewind_duration_changed(GtkWidget *w, gpointer user_data_ptr) {
GtkComboBox *box = GTK_COMBO_BOX(w);
config.emulation.rewind_duration = g_ascii_strtoll(gtk_combo_box_get_active_id(box), NULL, 10);
GB_set_rewind_length(&gb, config.emulation.rewind_duration);
}
G_MODULE_EXPORT void on_sample_rate_changed(GtkWidget *w, gpointer user_data_ptr) {
GtkComboBox *box = GTK_COMBO_BOX(w);
config.audio.sample_rate = g_ascii_strtoll(gtk_combo_box_get_active_id(box), NULL, 10);
if (config.audio.sample_rate == -1) {
gui_data.sample_rate = GB_audio_default_sample_rate();
}
else {
gui_data.sample_rate = config.audio.sample_rate;
}
init_audio();
}
G_MODULE_EXPORT void on_sgb_model_changed(GtkWidget *w, gpointer user_data_ptr) {
GtkComboBox *box = GTK_COMBO_BOX(w);
config.emulation.sgb_revision_name = (gchar *)gtk_combo_box_get_active_id(box);
}
G_MODULE_EXPORT void on_use_integer_scaling_changed(GtkWidget *w, gpointer user_data_ptr) {
GtkCheckButton *button = GTK_CHECK_BUTTON(w);
gboolean value = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(button));
config.video.use_integer_scaling = value;
update_viewport();
}
G_MODULE_EXPORT void console_on_enter(GtkWidget *w, gpointer user_data_ptr) {
GtkEntry *input = GTK_ENTRY(w);
const gchar *_text = gtk_entry_get_text(input);
gchar *text = g_strdup(_text);
if (g_strcmp0("", text) == 0 && g_strcmp0("", gui_data.last_console_input) < 0) {
text = g_strdup(gui_data.last_console_input);
}
else if (text) {
if (gui_data.last_console_input != NULL) g_free(gui_data.last_console_input);
gui_data.last_console_input = g_strdup(text);
}
if (!gui_data.in_sync_input) {
console_log(&gb, "> ", 0);
}
console_log(&gb, text, 0);
console_log(&gb, "\n", 0);
g_mutex_lock(&gui_data.debugger_input_mutex);
g_ptr_array_add(gui_data.debugger_input_queue, (gpointer)text);
g_cond_signal(&gui_data.debugger_input_cond);
g_mutex_unlock(&gui_data.debugger_input_mutex);
// clear input
gtk_entry_set_text(input, "");
}
G_MODULE_EXPORT void on_rumble_mode_changed(GtkWidget *w, gpointer user_data_ptr) {
GtkComboBox *box = GTK_COMBO_BOX(w);
config.controls.rumble_mode = (gchar *)gtk_combo_box_get_active_id(box);
GB_set_rumble_mode(&gb, config_get_rumble_mode());
}
G_MODULE_EXPORT void on_analog_speed_controls_changed(GtkWidget *w, gpointer user_data_ptr) {
GtkCheckButton *button = GTK_CHECK_BUTTON(w);
gboolean value = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(button));
config.controls.analog_speed_controls = value;
}
int main(int argc, char *argv[]) {
gui_data.main_thread = g_thread_self();
// Create our GApplication and tell GTK that we are able to handle files
gui_data.main_application = gtk_application_new(APP_ID, G_APPLICATION_NON_UNIQUE | G_APPLICATION_HANDLES_OPEN);
// Define our command line parameters
GOptionEntry entries[] = {
{ "version", 'v', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, NULL, "Show the application version", NULL },
{ "fullscreen", 'f', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &gui_data.cli_options.fullscreen, "Start in fullscreen mode", NULL },
{ "bootrom", 'b', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &gui_data.cli_options.boot_rom_path, "Path to the boot ROM to use", "<file path>" },
{ "model", 'm', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, NULL, "Override the model type to emulate", "<model type>" },
{ "config", 'c', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &gui_data.cli_options.config_path, "Override the path of the configuration file", "<file path>" },
{ "no-gl", 's', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &gui_data.cli_options.force_software_renderer, "Do not use OpenGL for rendering", NULL },
{ NULL }
};
// Setup our command line information
g_application_add_main_option_entries(G_APPLICATION(gui_data.main_application), entries);
g_application_set_option_context_parameter_string(G_APPLICATION(gui_data.main_application), "[FILE…]");
g_application_set_option_context_summary(G_APPLICATION(gui_data.main_application), "SameBoy is an open source Game Boy (DMG) and Game Boy Color (CGB) emulator.");
// Add signal handlers
g_signal_connect(gui_data.main_application, "handle-local-options", G_CALLBACK(handle_local_options), NULL);
g_signal_connect(gui_data.main_application, "startup", G_CALLBACK(startup), NULL);
g_signal_connect(gui_data.main_application, "activate", G_CALLBACK(activate), NULL);
g_signal_connect(gui_data.main_application, "open", G_CALLBACK(open), NULL);
g_signal_connect(gui_data.main_application, "shutdown", G_CALLBACK(shutdown), NULL);
// Start our GApplication main loop
int status = g_application_run(G_APPLICATION(gui_data.main_application), argc, argv);
g_object_unref(gui_data.main_application);
return status;
}