SameBoy/gtk3/main.c

2741 lines
89 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

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

#define G_LOG_USE_STRUCTURED
#include <gtk/gtk.h>
#include <epoxy/gl.h>
#include <signal.h>
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#include <Core/gb.h>
#include "settings.h"
#include "shader.h"
#include "check_menu_radio_group.h"
// used for audio and game controllers
#include "SDL.h"
#include "../SDL/audio/audio.h"
#define JOYSTICK_HIGH 0x4000
#define JOYSTICK_LOW 0x3800
#define BUTTON_MASK_A 0x01
#define BUTTON_MASK_B 0x02
#define BUTTON_MASK_START 0x04
#define BUTTON_MASK_SELECT 0x08
#define BUTTON_MASK_UP 0x10
#define BUTTON_MASK_DOWN 0x20
#define BUTTON_MASK_LEFT 0x40
#define BUTTON_MASK_RIGHT 0x80
#define tileset_buffer_length 256 * 192 * 4
#define tilemap_buffer_length 256 * 256 * 4
#define str(x) #x
#define xstr(x) str(x)
#define get_object(id) gtk_builder_get_object(gui_data.builder, id)
#define builder_get(type, id) type(get_object(id))
#define action_set_enabled(map, name, value) g_simple_action_set_enabled(G_SIMPLE_ACTION(g_action_map_lookup_action(G_ACTION_MAP(map), name)), value);
typedef struct{
int16_t x, y;
uint16_t w, h;
} Rect;
typedef struct GuiData {
struct CliOptionData {
gchar *config_path;
gchar *boot_rom_path;
gboolean fullscreen;
GB_model_t model;
gboolean force_software_renderer;
} cli_options;
GFile *file;
gint sample_rate;
GDateTime *config_modification_date;
char *battery_save_path;
char *cheats_save_path;
GB_model_t prev_model;
const GThread *main_thread;
volatile bool running;
volatile bool stopping;
volatile bool stopped;
// GTK pointers
GtkApplication *main_application;
GtkBuilder *builder;
GtkApplicationWindow *main_window;
GtkBox *main_window_container;
GtkGLArea *gl_area;
GtkDrawingArea *fallback_canvas;
GtkWindow *preferences;
GtkWindow *vram_viewer;
GtkWindow *memory_viewer;
GtkWindow *console;
GtkWindow *printer;
// Debugger state
GtkTextBuffer *pending_console_output;
gboolean in_sync_input;
gchar *last_console_input;
gboolean log_to_sidebar;
gboolean should_clear_sidebar;
GMutex debugger_input_mutex;
GCond debugger_input_cond;
GRecMutex console_output_lock;
GPtrArray *debugger_input_queue;
bool vram_viewer_visible;
bool vram_viewer_updating;
gchar *vram_viewer_active_tab;
gboolean vram_viewer_is_cgb;
uint8_t vram_viewer_palette_data[16][0x40];
GB_oam_info_t oam_info[40];
uint16_t oam_count;
uint8_t oam_height;
uint32_t tileset_buffer[tileset_buffer_length];
uint32_t tilemap_buffer[tilemap_buffer_length];
GMutex tileset_buffer_mutex;
GMutex tilemap_buffer_mutex;
Rect scroll_rect;
// Audio and video
bool audio_initialized;
uint32_t *image_buffers[3];
unsigned char current_buffer;
Rect viewport;
bool border_mode_changed;
bool is_fullscreen;
bool supports_gl;
shader_t shader;
unsigned last_screen_width;
unsigned last_screen_height;
// Fast forward / slow motion
bool underclock_down;
bool rewind_down;
bool do_rewind;
bool rewind_paused;
bool turbo_down;
double clock_mutliplier;
double analog_clock_multiplier;
bool analog_clock_multiplier_valid;
// Input
uint8_t pressed_buttons;
struct Controller_t {
SDL_GameController *controller;
SDL_Haptic *haptic;
bool ignore_rumble;
} *controllers;
unsigned controller_count;
struct Controller_t *last_used_controller; // Used for rumble
} GuiData;
// Initialize the GuiData
static GuiData gui_data = {
.cli_options = {
.fullscreen = false,
.model = -1,
},
.prev_model = -1,
.running = false,
.stopping = false,
.stopped = false,
.in_sync_input = false,
.log_to_sidebar = false,
.should_clear_sidebar = false,
.vram_viewer_visible = false,
.vram_viewer_updating = false,
.vram_viewer_active_tab = "",
.vram_viewer_is_cgb = false,
.audio_initialized = false,
.border_mode_changed = false,
.underclock_down = false,
.rewind_down = false,
.rewind_paused = false,
.turbo_down = false,
.clock_mutliplier = 1.0,
.analog_clock_multiplier = 1.0,
};
GB_gameboy_t gb;
typedef enum {
INPUT_UP,
INPUT_DOWN,
INPUT_LEFT,
INPUT_RIGHT,
INPUT_A,
INPUT_B,
INPUT_START,
INPUT_SELECT,
INPUT_TURBO,
INPUT_REWIND,
INPUT_SLOWDOWN,
INPUT_FULLSCREEN,
} input_names_t;
static unsigned key_map[] = {
[INPUT_UP] = GDK_KEY_w,
[INPUT_LEFT] = GDK_KEY_a,
[INPUT_DOWN] = GDK_KEY_s,
[INPUT_RIGHT] = GDK_KEY_d,
[INPUT_A] = GDK_KEY_l,
[INPUT_B] = GDK_KEY_k,
[INPUT_START] = GDK_KEY_h,
[INPUT_SELECT] = GDK_KEY_g,
[INPUT_TURBO] = GDK_KEY_space,
[INPUT_REWIND] = GDK_KEY_Tab,
[INPUT_SLOWDOWN] = GDK_KEY_Shift_L,
[INPUT_FULLSCREEN] = GDK_KEY_F11,
};
// 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 void replace_extension(const char *src, size_t length, char *dest, const char *ext) {
memcpy(dest, src, length);
dest[length] = 0;
/* Remove extension */
for (size_t i = length; i--;) {
if (dest[i] == '/') break;
if (dest[i] == '.') {
dest[i] = 0;
break;
}
}
/* Add new extension */
strcat(dest, ext);
}
static double clamp_double(double min, double max, double value) {
if (value < min) return min;
if (value > max) return max;
return value;
}
static double max_double(double a, double b) {
if (a > b) return a;
return b;
}
static double min_double(double a, double b) {
if (a < b) return a;
return b;
}
static uint32_t rgb_encode(GB_gameboy_t *gb, uint8_t r, uint8_t g, uint8_t b) {
return 0xFF000000 | (r << 16) | (g << 8) | b;
}
static uint32_t convert_color(uint16_t color) {
const uint8_t r = ((uint16_t)(color & 0x1F) * 255) / 31;
const uint8_t g = ((uint16_t)((color >> 5) & 0x1F) * 255) / 31;
const uint8_t b = ((uint16_t)((color >> 10) & 0x1F) * 255) / 31;
return (r << 16) | (g << 8) | b;
}
static void palette_color_data_func(GtkTreeViewColumn *col, GtkCellRenderer *renderer, GtkTreeModel *model, GtkTreeIter *iter, gpointer user_data_ptr) {
const gchar *title = gtk_tree_view_column_get_title(col);
const uint8_t color_index = g_ascii_strtoll(&title[6], NULL, 10);
const uint8_t column_index = 2 + (2 * color_index);
GValue color_val = G_VALUE_INIT;
gtk_tree_model_get_value(model, iter, column_index, &color_val);
gint color = g_value_get_int(&color_val);
gchar *color_string = g_strdup_printf("#%06x", color);
gint lightness = 0.299 * ((color >> 16) & 0xFF) + 0.587 * ((color >> 8) & 0xFF) + 0.114 * (color & 0xFF);
GValue color_str = G_VALUE_INIT;
g_value_init(&color_str, G_TYPE_STRING);
g_value_set_string(&color_str, color_string);
g_object_set_property(G_OBJECT(renderer), "background", &color_str);
GValue fg_color_str = G_VALUE_INIT;
g_value_init(&fg_color_str, G_TYPE_STRING);
g_value_set_static_string(&fg_color_str, (lightness > 0x7F)? "#000000" : "#FFFFFF");
g_object_set_property(G_OBJECT(renderer), "foreground", &fg_color_str);
g_value_unset(&color_val);
g_value_unset(&color_str);
g_value_unset(&fg_color_str);
g_free(color_string);
}
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;
}
// The main function for the OpenGL version check workaround
void gl_check_realize(GtkWidget *w, gpointer user_data_ptr) {
gboolean *result = (gboolean *) user_data_ptr;
GError *error = NULL;
GdkWindow *gdk_window = gtk_widget_get_window(w);
GdkGLContext *context = gdk_window_create_gl_context(gdk_window, &error);
if (error != NULL) {
g_warning("Failed to create context: %s", error->message);
g_error_free(error);
*result = false;
}
else {
gdk_gl_context_make_current(context);
int version = epoxy_gl_version();
g_object_run_dispose(G_OBJECT(context));
g_object_unref(context);
context = NULL;
gdk_gl_context_clear_current();
g_debug("OpenGL version: %d", version);
*result = version >= 32;
}
}
// Workaround to figure out if we have proper OpenGL support.
// Otherwise the application would crash after our GtkGlArea is realized
// and the context it uses is a legacy OpenGL 1.4 context because
// GTK3 calls OpenGL 2.0+ functions on it.
gboolean test_gl_support(void) {
gboolean result = false;
GtkWidget *window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
g_signal_connect(window, "realize", G_CALLBACK(gl_check_realize), &result);
gtk_widget_realize(window);
gtk_widget_destroy(window);
window = NULL;
return result;
}
static gboolean init_controllers(void) {
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 GB_model_t get_model_type(void) {
if (gui_data.cli_options.model != -1) {
return gui_data.cli_options.model;
}
return get_model();
}
static void gb_audio_callback(GB_gameboy_t *gb, GB_sample_t *sample) {
if (gui_data.turbo_down) {
static unsigned skip = 0;
skip++;
if (skip == GB_audio_get_sample_rate() / 8) {
skip = 0;
}
if (skip > GB_audio_get_sample_rate() / 16) {
return;
}
}
if (GB_audio_get_queue_length() / sizeof(*sample) > GB_audio_get_sample_rate() / 4) {
return;
}
GB_audio_queue_sample(sample);
}
static void 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 void clear_sidebar(void) {
GtkTextView *sidebar_output = builder_get(GTK_TEXT_VIEW, "console_sidebar_output");
GtkTextBuffer *sidebar_output_text_buf = gtk_text_view_get_buffer(sidebar_output);
gtk_text_buffer_set_text(sidebar_output_text_buf, "", -1);
}
static gboolean scroll_to_bottom(GtkTextView *textview, GtkTextMark *mark) {
GtkTextBuffer *buffer = gtk_text_view_get_buffer(textview);
GtkTextIter iter;
gtk_text_buffer_get_end_iter(buffer, &iter);
gtk_text_iter_set_line_offset(&iter, 0);
gtk_text_buffer_move_mark(buffer, mark, &iter);
gtk_text_view_scroll_to_mark(textview, mark, 0.0, true, 0.0, 0.10);
gtk_text_buffer_delete_mark(buffer, mark);
return true;
}
static void append_pending_output(void) {
g_rec_mutex_lock(&gui_data.console_output_lock);
if (gui_data.should_clear_sidebar) {
clear_sidebar();
gui_data.should_clear_sidebar = false;
}
if (gtk_text_buffer_get_char_count(gui_data.pending_console_output) > 0) {
GtkTextView *text_view = builder_get(GTK_TEXT_VIEW, gui_data.log_to_sidebar? "console_sidebar_output" : "console_screen");
GtkTextBuffer *text_buf = gtk_text_view_get_buffer(text_view);
GtkTextIter start;
GtkTextIter end;
gtk_text_buffer_get_start_iter(gui_data.pending_console_output, &start);
gtk_text_buffer_get_end_iter(gui_data.pending_console_output, &end);
GtkTextIter iter;
gtk_text_buffer_get_end_iter(text_buf, &iter);
gtk_text_buffer_insert_range(text_buf, &iter, &start, &end);
scroll_to_bottom(text_view, gtk_text_buffer_create_mark(text_buf, NULL, &iter, true));
gtk_text_buffer_set_text(gui_data.pending_console_output, "", -1);
}
g_rec_mutex_unlock(&gui_data.console_output_lock);
}
static void update_debugger_sidebar(GB_gameboy_t *gb) {
if (!GB_debugger_is_stopped(gb)) {
return;
}
if (gui_data.main_thread != g_thread_self()) {
g_idle_add((GSourceFunc) update_debugger_sidebar, gb);
return;
}
g_rec_mutex_lock(&gui_data.console_output_lock);
gui_data.should_clear_sidebar = true;
append_pending_output();
gui_data.log_to_sidebar = true;
g_rec_mutex_unlock(&gui_data.console_output_lock);
GtkTextView *sidebar_input = builder_get(GTK_TEXT_VIEW, "console_sidebar_input");
GtkTextBuffer *sidebar_input_text_buf = gtk_text_view_get_buffer(sidebar_input);
gint line_count = gtk_text_buffer_get_line_count(sidebar_input_text_buf);
for (unsigned line = 0; line < line_count; ++line) {
GtkTextIter start_iter;
GtkTextIter end_iter;
gunichar ch;
gtk_text_buffer_get_iter_at_line(sidebar_input_text_buf, &start_iter, line);
end_iter = start_iter;
do {
ch = gtk_text_iter_get_char(&end_iter);
if (!gtk_text_iter_forward_char(&end_iter)) {
break;
}
}
while (ch != '\n');
gchar *cmd = gtk_text_buffer_get_text(sidebar_input_text_buf, &start_iter, &end_iter, false);
g_strchug(cmd); // trim leading whitespace
g_strchomp(cmd); // trim trailing whitespace
if (g_strcmp0("", cmd) != 0) {
char *duped = g_strdup(cmd);
GB_attributed_log(gb, GB_LOG_BOLD, "%s:\n", duped);
GB_debugger_execute_command(gb, duped);
GB_log(gb, "\n");
g_free(duped);
}
g_free(cmd);
}
g_rec_mutex_lock(&gui_data.console_output_lock);
append_pending_output();
gui_data.log_to_sidebar = false;
g_rec_mutex_unlock(&gui_data.console_output_lock);
}
static void console_log(GB_gameboy_t *gb, const char *string, GB_log_attributes attributes) {
g_rec_mutex_lock(&gui_data.console_output_lock);
if (string != NULL && !g_str_equal("", string)) {
GtkTextIter iter;
GtkTextIter start;
// Append attributed text to "gui_data.pending_console_output" GtkTextBuffer
gtk_text_buffer_get_end_iter(gui_data.pending_console_output, &iter);
GtkTextMark *start_mark = gtk_text_buffer_create_mark(gui_data.pending_console_output, NULL, &iter, true);
gtk_text_buffer_insert(gui_data.pending_console_output, &iter, g_strdup(string), -1);
gtk_text_buffer_get_iter_at_mark(gui_data.pending_console_output, &start, start_mark);
if (attributes & GB_LOG_BOLD) {
gtk_text_buffer_apply_tag_by_name(gui_data.pending_console_output, "bold", &start, &iter);
}
if (attributes & GB_LOG_DASHED_UNDERLINE) {
gtk_text_buffer_apply_tag_by_name(gui_data.pending_console_output, "dashed_underline", &start, &iter);
}
if (attributes & GB_LOG_UNDERLINE) {
gtk_text_buffer_apply_tag_by_name(gui_data.pending_console_output, "underline", &start, &iter);
}
gtk_text_buffer_delete_mark(gui_data.pending_console_output, start_mark);
g_idle_add((GSourceFunc) append_pending_output, NULL);
}
g_rec_mutex_unlock(&gui_data.console_output_lock);
}
// Console TODO:
// TODO: clear sidebar when switching to async mode
// TODO: Command history (up / down arrow in input)
// TODO: reverse search of commands
// TODO: search in output
static char *sync_console_input(GB_gameboy_t *gb) {
update_debugger_sidebar(gb);
console_log(gb, "> ", 0);
gui_data.in_sync_input = true;
g_mutex_lock(&gui_data.debugger_input_mutex);
g_cond_wait(&gui_data.debugger_input_cond, &gui_data.debugger_input_mutex);
gchar *input = NULL;
const gchar *_input = g_ptr_array_index(gui_data.debugger_input_queue, 0);
input = g_strdup(_input);
gpointer ptr = g_ptr_array_remove_index(gui_data.debugger_input_queue, 0);
if (ptr) g_free(ptr);
g_mutex_unlock(&gui_data.debugger_input_mutex);
gui_data.in_sync_input = false;
return input;
}
static char *async_console_input(GB_gameboy_t *gb) {
// TODO: This is rather ugly
g_idle_add((GSourceFunc) clear_sidebar, NULL);
if (gui_data.debugger_input_queue->len == 0) return NULL;
g_mutex_lock(&gui_data.debugger_input_mutex);
gchar *input = NULL;
const gchar *_input = g_ptr_array_index(gui_data.debugger_input_queue, 0);
if (_input) {
input = g_strdup(_input);
gpointer ptr = g_ptr_array_remove_index(gui_data.debugger_input_queue, 0);
if (ptr) g_free(ptr);
}
g_mutex_unlock(&gui_data.debugger_input_mutex);
return input;
}
GtkWidget *menubar_to_menu(GtkMenuBar *menubar) {
GtkWidget *menu = gtk_menu_new();
g_autoptr(GList) iter = gtk_container_get_children(GTK_CONTAINER(menubar));
while (iter) {
GtkWidget *item = GTK_WIDGET(iter->data);
gtk_widget_reparent(item, menu);
iter = iter->next;
}
return menu;
}
// 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 = 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;
}
}
unsigned new_width = x_factor * GB_get_screen_width(&gb);
unsigned new_height = y_factor * GB_get_screen_height(&gb);
gui_data.viewport = (Rect){
(win_width - new_width) / 2,
(win_height - new_height) / 2,
new_width,
new_height
};
if (!gui_data.fallback_canvas) glViewport(gui_data.viewport.x, gui_data.viewport.y, gui_data.viewport.w, gui_data.viewport.h);
}
// 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());
}
update_viewport();
}
static void stop(void) {
if (!gui_data.running) return;
GB_audio_set_paused(true);
GB_debugger_set_disabled(&gb, true);
if (GB_debugger_is_stopped(&gb)) {
// [self interruptDebugInputRead];
}
gui_data.stopping = true;
gui_data.running = false;
while (gui_data.stopping);
GB_debugger_set_disabled(&gb, false);
gui_data.stopped = true;
}
static void on_vblank(gpointer data) {
if (!gui_data.vram_viewer_updating && gui_data.vram_viewer_visible) {
gui_data.vram_viewer_updating = true;
if (g_strcmp0("vram_viewer_sprites", gui_data.vram_viewer_active_tab) == 0) {
GtkTreeIter iter;
GtkTreeView *tree_view = builder_get(GTK_TREE_VIEW, "vram_viewer_sprites");
// gtk_tree_view_set_model(tree_view, NULL); // Do we need this?
GtkListStore *store = gtk_list_store_new(7,
GDK_TYPE_PIXBUF, // Preview image
G_TYPE_STRING, // X position
G_TYPE_STRING, // Y position
G_TYPE_STRING, // Tile
G_TYPE_STRING, // Tile Address
G_TYPE_STRING, // OAM Address
G_TYPE_STRING // Attributes
);
gtk_tree_model_get_iter_first(GTK_TREE_MODEL(store), &iter);
for (unsigned row = 0; row < gui_data.oam_count; ++row) {
GdkPixbuf *pixbuf = gdk_pixbuf_new_from_bytes(
g_bytes_new(gui_data.oam_info[row].image, 128 * sizeof(uint32_t)),
GDK_COLORSPACE_RGB, true, 8, 8, gui_data.oam_height, 8 * sizeof(uint32_t)
);
GdkPixbuf *dest = gdk_pixbuf_new(GDK_COLORSPACE_RGB, true, 8, 8 * 2, gui_data.oam_height * 2);
gdk_pixbuf_scale(pixbuf, dest,
0, 0, 8 * 2, gui_data.oam_height * 2,
0, 0, 2.0, 2.0,
GDK_INTERP_NEAREST
);
gtk_list_store_insert_with_values(store, &iter, -1,
0, dest,
1, g_strdup_printf("%i", gui_data.oam_info[row].x - 8),
2, g_strdup_printf("%i", gui_data.oam_info[row].y - 16),
3, g_strdup_printf("$%02x", gui_data.oam_info[row].tile),
4, g_strdup_printf("$%04x", 0x8000 + gui_data.oam_info[row].tile * 0x10),
5, g_strdup_printf("$%04x", gui_data.oam_info[row].oam_addr),
6, gui_data.vram_viewer_is_cgb
? g_strdup_printf("%c%c%c%d%d",
gui_data.oam_info[row].flags & 0x80? 'P' : '-',
gui_data.oam_info[row].flags & 0x40? 'Y' : '-',
gui_data.oam_info[row].flags & 0x20? 'X' : '-',
gui_data.oam_info[row].flags & 0x08? 1 : 0,
gui_data.oam_info[row].flags & 0x07)
: g_strdup_printf("%c%c%c%d",
gui_data.oam_info[row].flags & 0x80? 'P' : '-',
gui_data.oam_info[row].flags & 0x40? 'Y' : '-',
gui_data.oam_info[row].flags & 0x20? 'X' : '-',
gui_data.oam_info[row].flags & 0x10? 1 : 0),
-1
);
g_object_unref(pixbuf);
g_object_unref(dest);
}
gtk_tree_view_set_model(tree_view, GTK_TREE_MODEL(store));
g_object_unref(store);
}
else if (g_strcmp0("vram_viewer_palettes", gui_data.vram_viewer_active_tab) == 0) {
GtkTreeIter iter;
GtkTreeView *tree_view = builder_get(GTK_TREE_VIEW, "vram_viewer_palettes");
// gtk_tree_view_set_model(tree_view, NULL); // Do we need this?
GtkListStore *store = gtk_list_store_new(9,
G_TYPE_STRING, // Name
G_TYPE_STRING, // Color 0 string
G_TYPE_INT, // Color 0 integer
G_TYPE_STRING, // Color 1 string
G_TYPE_INT, // Color 1 integer
G_TYPE_STRING, // Color 2 string
G_TYPE_INT, // Color 2 integer
G_TYPE_STRING, // Color 3 string
G_TYPE_INT // Color 3 integer
);
gtk_tree_model_get_iter_first(GTK_TREE_MODEL(store), &iter);
for (unsigned row = 0; row < 16; ++row) {
uint8_t offset = (row & 7) * 4;
uint16_t color_0 = (gui_data.vram_viewer_palette_data[row][((0 + offset) << 1) + 1] << 8) | gui_data.vram_viewer_palette_data[row][((0 + offset) << 1)];
uint16_t color_1 = (gui_data.vram_viewer_palette_data[row][((1 + offset) << 1) + 1] << 8) | gui_data.vram_viewer_palette_data[row][((1 + offset) << 1)];
uint16_t color_2 = (gui_data.vram_viewer_palette_data[row][((2 + offset) << 1) + 1] << 8) | gui_data.vram_viewer_palette_data[row][((2 + offset) << 1)];
uint16_t color_3 = (gui_data.vram_viewer_palette_data[row][((3 + offset) << 1) + 1] << 8) | gui_data.vram_viewer_palette_data[row][((3 + offset) << 1)];
gtk_list_store_insert_with_values(store, &iter, -1,
0, g_strdup_printf("%s %d", row >=8 ? "Object" : "Background", row & 7),
1, g_strdup_printf("$%04x", color_0 & 0x7FFF),
2, convert_color(color_0),
3, g_strdup_printf("$%04x", color_1 & 0x7FFF),
4, convert_color(color_1),
5, g_strdup_printf("$%04x", color_2 & 0x7FFF),
6, convert_color(color_2),
7, g_strdup_printf("$%04x", color_3 & 0x7FFF),
8, convert_color(color_3),
-1
);
}
GtkTreeViewColumn *column_0 = gtk_tree_view_get_column(tree_view, 1);
GtkTreeViewColumn *column_1 = gtk_tree_view_get_column(tree_view, 2);
GtkTreeViewColumn *column_2 = gtk_tree_view_get_column(tree_view, 3);
GtkTreeViewColumn *column_3 = gtk_tree_view_get_column(tree_view, 4);
GtkCellRendererText *cell_renderer_0 = builder_get(GTK_CELL_RENDERER_TEXT, "vram_viewer_palette_cell_renderer_0");
GtkCellRendererText *cell_renderer_1 = builder_get(GTK_CELL_RENDERER_TEXT, "vram_viewer_palette_cell_renderer_1");
GtkCellRendererText *cell_renderer_2 = builder_get(GTK_CELL_RENDERER_TEXT, "vram_viewer_palette_cell_renderer_2");
GtkCellRendererText *cell_renderer_3 = builder_get(GTK_CELL_RENDERER_TEXT, "vram_viewer_palette_cell_renderer_3");
gtk_tree_view_column_set_cell_data_func(column_0, GTK_CELL_RENDERER(cell_renderer_0), palette_color_data_func, NULL, NULL);
gtk_tree_view_column_set_cell_data_func(column_1, GTK_CELL_RENDERER(cell_renderer_1), palette_color_data_func, NULL, NULL);
gtk_tree_view_column_set_cell_data_func(column_2, GTK_CELL_RENDERER(cell_renderer_2), palette_color_data_func, NULL, NULL);
gtk_tree_view_column_set_cell_data_func(column_3, GTK_CELL_RENDERER(cell_renderer_3), palette_color_data_func, NULL, NULL);
gtk_tree_view_set_model(tree_view, GTK_TREE_MODEL(store));
g_object_unref(store);
}
// Queue a redraw of the VRAM viewer
gtk_widget_queue_draw(GTK_WIDGET(gui_data.vram_viewer));
gui_data.vram_viewer_updating = false;
}
// Queue drawing of the current frame
if (gui_data.fallback_canvas) {
gtk_widget_queue_draw(GTK_WIDGET(gui_data.main_window));
}
else if (gui_data.gl_area) {
gtk_gl_area_queue_render(gui_data.gl_area);
}
}
static void vblank(GB_gameboy_t *gb) {
flip();
if (gui_data.border_mode_changed) {
GB_set_border_mode(gb, get_display_border_mode());
update_window_geometry();
gui_data.border_mode_changed = false;
}
GB_set_pixels_output(gb, get_pixels());
// 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;
}
}
if (g_strcmp0("vram_viewer_tileset", gui_data.vram_viewer_active_tab) == 0) {
const gchar *palette_id = gtk_combo_box_get_active_id(builder_get(GTK_COMBO_BOX, "vram_viewer_tileset_palette_selector"));
GB_palette_type_t palette_type = g_str_has_prefix(palette_id, "bg")? GB_PALETTE_BACKGROUND : GB_PALETTE_OAM;
uint8_t palette_index = g_ascii_digit_value(palette_id[palette_type == GB_PALETTE_OAM ? 3 : 2]);
GB_draw_tileset(gb, gui_data.tileset_buffer,
palette_type,
palette_index
);
}
else if (g_strcmp0("vram_viewer_tilemap", gui_data.vram_viewer_active_tab) == 0) {
const gchar *palette_id = gtk_combo_box_get_active_id(builder_get(GTK_COMBO_BOX, "vram_viewer_tilemap_palette_selector"));
uint8_t palette_index = 0;
GB_palette_type_t palette_type = GB_PALETTE_AUTO;
if (g_strcmp0("auto", palette_id) != 0) {
palette_type = g_str_has_prefix(palette_id, "bg")? GB_PALETTE_BACKGROUND : GB_PALETTE_OAM;
palette_index = g_ascii_digit_value(palette_id[palette_type == GB_PALETTE_OAM ? 3 : 2]);
}
GB_map_type_t map_type = GB_MAP_AUTO;
const gchar *map_type_id = gtk_combo_box_get_active_id(builder_get(GTK_COMBO_BOX, "vram_viewer_tilemap_tilemap_selector"));
if (g_strcmp0("auto", map_type_id) != 0) {
map_type = (g_strcmp0("9800", map_type_id) == 0)? GB_MAP_9800 : GB_MAP_9C00;
}
GB_tileset_type_t tileset_type = GB_TILESET_AUTO;
const gchar *tileset_type_id = gtk_combo_box_get_active_id(builder_get(GTK_COMBO_BOX, "vram_viewer_tilemap_tileset_selector"));
if (g_strcmp0("auto", tileset_type_id) != 0) {
tileset_type = (g_strcmp0("8800", tileset_type_id) == 0)? GB_TILESET_8800 : GB_TILESET_8000;
}
GB_draw_tilemap(gb, gui_data.tilemap_buffer,
palette_type,
palette_index,
map_type,
tileset_type
);
gui_data.scroll_rect = (Rect){
GB_read_memory(gb, 0xFF00 | GB_IO_SCX),
GB_read_memory(gb, 0xFF00 | GB_IO_SCY),
160, 144
};
}
else if (g_strcmp0("vram_viewer_sprites", gui_data.vram_viewer_active_tab) == 0) {
gui_data.oam_count = GB_get_oam_info(gb, gui_data.oam_info, &gui_data.oam_height);
gui_data.vram_viewer_is_cgb = GB_is_cgb(gb);
}
else if (g_strcmp0("vram_viewer_palettes", gui_data.vram_viewer_active_tab) == 0) {
size_t size;
for (unsigned row = 0; row < 16; ++row) {
uint8_t *palette_data = GB_get_direct_access(gb, row >= 8? GB_DIRECT_ACCESS_OBP : GB_DIRECT_ACCESS_BGP, &size, NULL);
memcpy(gui_data.vram_viewer_palette_data[row], palette_data, size);
}
}
gui_data.do_rewind = gui_data.rewind_down;
g_idle_add((GSourceFunc) on_vblank, NULL);
}
static void handle_events(GB_gameboy_t *gb) {
SDL_GameControllerUpdate();
uint8_t controller_state = 0;
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, get_model_type());
GB_set_vblank_callback(&gb, vblank);
GB_set_pixels_output(&gb, get_current_buffer());
GB_set_rgb_encode_callback(&gb, rgb_encode);
GB_set_sample_rate(&gb, GB_audio_get_sample_rate());
GB_set_color_correction_mode(&gb, get_color_correction_mode());
GB_set_highpass_filter_mode(&gb, get_highpass_mode());
GB_set_rewind_length(&gb, config.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, get_rumble_mode());
if (get_display_border_mode() <= GB_BORDER_ALWAYS) {
GB_set_border_mode(&gb, get_display_border_mode());
}
update_window_geometry();
}
static void reset(void) {
g_debug("Reset: %d == %d", get_model_type(), gui_data.prev_model);
GB_model_t current_model = get_model_type();
if (gui_data.prev_model == -1 || gui_data.prev_model == current_model) {
GB_reset(&gb);
}
else {
GB_switch_model_and_reset(&gb, current_model);
}
GB_set_palette(&gb, get_monochrome_palette());
gui_data.prev_model = get_model_type();
// 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();
while (gui_data.stopping);
GtkWindow *window = gui_data.main_window ? GTK_WINDOW(gui_data.main_window) : NULL;
save_settings(window, gui_data.config_modification_date);
free_settings();
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;
}
// Gets called when the VRAM viewer gets realized
static void on_vram_viewer_realize(void) {
gui_data.vram_viewer_visible = true;
gui_data.vram_viewer_active_tab = (gchar *)gtk_stack_get_visible_child_name(builder_get(GTK_STACK, "vram_viewer_stack"));
}
// Gets called when the VRAM viewer gets unrealized
static void on_vram_viewer_unrealize(void) {
gui_data.vram_viewer_visible = false;
}
// Gets called when the tileset viewer should be redrawn
static gboolean on_draw_vram_viewer_tileset(GtkWidget *widget, cairo_t *cr, gpointer data) {
g_mutex_lock(&gui_data.tileset_buffer_mutex);
guint width, height;
GtkStyleContext *context;
context = gtk_widget_get_style_context(widget);
width = gtk_widget_get_allocated_width(widget);
height = gtk_widget_get_allocated_height(widget);
gtk_render_background(context, cr, 0, 0, width, height);
cairo_surface_t *surface = cairo_image_surface_create_for_data(
(unsigned char *) gui_data.tileset_buffer,
CAIRO_FORMAT_RGB24,
256,
192,
cairo_format_stride_for_width(CAIRO_FORMAT_RGB24, 256)
);
cairo_scale(cr, 2.0, 2.0);
cairo_set_source_surface(cr, surface, 0, 0);
cairo_pattern_set_filter(cairo_get_source(cr), CAIRO_FILTER_NEAREST);
cairo_paint(cr);
if (gtk_toggle_button_get_active(builder_get(GTK_TOGGLE_BUTTON, "vram_viewer_tileset_toggle_grid_button"))) {
cairo_set_source_rgba(cr, 0.0, 0.0, 0.0, 0.25);
cairo_set_line_width(cr, 1);
const int divisions_x = 256 / 8;
const int divisions_y = 192 / 8;
for (int i = 0; i < divisions_x; i++) {
const int j = 256 * i;
cairo_move_to(cr, j / divisions_x, 0);
cairo_line_to(cr, j / divisions_x, 192);
}
for (int i = 0; i < divisions_y; i++) {
const int j = 192 * i;
cairo_move_to(cr, 0, j / divisions_y);
cairo_line_to(cr, 256, j / divisions_y);
}
cairo_stroke(cr);
}
g_mutex_unlock(&gui_data.tileset_buffer_mutex);
return false;
}
// Gets called when the tilemap viewer should be redrawn
static gboolean on_draw_vram_viewer_tilemap(GtkWidget *widget, cairo_t *cr, gpointer data) {
g_mutex_lock(&gui_data.tilemap_buffer_mutex);
guint width, height;
GtkStyleContext *context;
context = gtk_widget_get_style_context(widget);
width = gtk_widget_get_allocated_width(widget);
height = gtk_widget_get_allocated_height(widget);
gtk_render_background(context, cr, 0, 0, width, height);
cairo_surface_t *surface = cairo_image_surface_create_for_data(
(unsigned char *) gui_data.tilemap_buffer,
CAIRO_FORMAT_RGB24,
256,
256,
cairo_format_stride_for_width(CAIRO_FORMAT_RGB24, 256)
);
cairo_scale(cr, 2.0, 2.0);
cairo_set_source_surface(cr, surface, 0, 0);
cairo_pattern_set_filter(cairo_get_source(cr), CAIRO_FILTER_NEAREST);
cairo_paint(cr);
if (gtk_toggle_button_get_active(builder_get(GTK_TOGGLE_BUTTON, "vram_viewer_tilemap_toggle_grid_button"))) {
cairo_set_source_rgba(cr, 0.0, 0.0, 0.0, 0.25);
cairo_set_line_width(cr, 1);
const int divisions = 256 / 8;
for (int i = 0; i < divisions; i++) {
const int j = 256 * i;
cairo_move_to(cr, j / divisions, 0);
cairo_line_to(cr, j / divisions, 256);
cairo_move_to(cr, 0, j / divisions);
cairo_line_to(cr, 256, j / divisions);
}
cairo_stroke(cr);
}
if (gtk_toggle_button_get_active(builder_get(GTK_TOGGLE_BUTTON, "vram_viewer_tilemap_toggle_scrolling_button"))) {
cairo_rectangle(cr, -2, -2, width + 2, height + 2);
for (unsigned x = 0; x < 2; x++) {
for (unsigned y = 0; y < 2; y++) {
Rect rect = gui_data.scroll_rect;
rect.x -= 256 * x;
rect.y += 256 * y;
cairo_rectangle(cr, rect.x, rect.y, rect.w, rect.h);
}
}
cairo_set_fill_rule(cr, CAIRO_FILL_RULE_EVEN_ODD);
cairo_set_line_width(cr, 2);
cairo_set_line_join(cr, CAIRO_LINE_JOIN_ROUND);
cairo_set_source_rgba(cr, 0.2, 0.2, 0.2, 0.5);
cairo_fill_preserve(cr);
cairo_clip_preserve(cr);
cairo_set_source_rgba(cr, 0.0, 0.0, 0.0, 0.6);
cairo_stroke(cr);
}
g_mutex_unlock(&gui_data.tilemap_buffer_mutex);
return false;
}
static gboolean on_motion_vram_viewer_tileset(GtkWidget *widget, GdkEventMotion *event) {
int x, y;
if (event->is_hint) {
gdk_window_get_pointer (event->window, &x, &y, NULL);
}
else {
x = event->x;
y = event->y;
}
// Compensate for our canvas scale
x /= 2;
y /= 2;
uint8_t bank = x >= 128? 1 : 0;
x &= 127;
uint16_t tile = x / 8 + y / 8 * 16;
GtkLabel *status = builder_get(GTK_LABEL, "vram_viewer_status");
gtk_label_set_text(status, g_strdup_printf("Tile number $%02x at %d:$%04x", tile & 0xFF, bank, 0x8000 + tile * 0x10));
return true;
}
static gboolean on_motion_vram_viewer_tilemap(GtkWidget *widget, GdkEventMotion *event) {
int x, y;
if (event->is_hint) {
gdk_window_get_pointer (event->window, &x, &y, NULL);
}
else {
x = event->x;
y = event->y;
}
// Compensate for our canvas scale
x /= 2;
y /= 2;
GtkLabel *status = builder_get(GTK_LABEL, "vram_viewer_status");
uint16_t map_offset = x / 8 + y / 8 * 32;
uint16_t map_base = 0x1800;
GB_map_type_t map_type = GB_MAP_AUTO;
const gchar *map_type_id = gtk_combo_box_get_active_id(builder_get(GTK_COMBO_BOX, "vram_viewer_tilemap_tilemap_selector"));
if (g_strcmp0("auto", map_type_id) != 0) {
map_type = (g_strcmp0("9800", map_type_id) == 0)? GB_MAP_9800 : GB_MAP_9C00;
}
GB_tileset_type_t tileset_type = GB_TILESET_AUTO;
const gchar *tileset_type_id = gtk_combo_box_get_active_id(builder_get(GTK_COMBO_BOX, "vram_viewer_tilemap_tileset_selector"));
if (g_strcmp0("auto", tileset_type_id) != 0) {
tileset_type = (g_strcmp0("8800", tileset_type_id) == 0)? GB_TILESET_8800 : GB_TILESET_8000;
}
uint8_t lcdc = ((uint8_t *)GB_get_direct_access(&gb, GB_DIRECT_ACCESS_IO, NULL, NULL))[GB_IO_LCDC];
uint8_t *vram = GB_get_direct_access(&gb, GB_DIRECT_ACCESS_VRAM, NULL, NULL);
if (map_type == GB_MAP_9C00 || (map_type == GB_MAP_AUTO && lcdc & 0x08)) {
map_base = 0x1c00;
}
if (tileset_type == GB_TILESET_AUTO) {
tileset_type = (lcdc & 0x10)? GB_TILESET_8800 : GB_TILESET_8000;
}
uint8_t tile = vram[map_base + map_offset];
uint16_t tile_address = 0;
if (tileset_type == GB_TILESET_8000) {
tile_address = 0x8000 + tile * 0x10;
}
else {
tile_address = 0x9000 + (int8_t)tile * 0x10;
}
if (GB_is_cgb(&gb)) {
uint8_t attributes = vram[map_base + map_offset + 0x2000];
gtk_label_set_text(status, g_strdup_printf("Tile number $%02x (%d:$%04x) at map address $%04x (Attributes: %c%c%c%d%d)",
tile,
attributes & 0x8? 1 : 0,
tile_address,
0x8000 + map_base + map_offset,
(attributes & 0x80) ? 'P' : '-',
(attributes & 0x40) ? 'V' : '-',
(attributes & 0x20) ? 'H' : '-',
attributes & 0x8? 1 : 0,
attributes & 0x7
));
}
else {
gtk_label_set_text(status, g_strdup_printf("Tile number $%02x ($%04x) at map address $%04x",
tile,
tile_address,
0x8000 + map_base + map_offset
));
}
return true;
}
static void on_vram_tab_change(GtkWidget *widget, GParamSpec *pspec, GtkStackSwitcher *self) {
gtk_label_set_text(builder_get(GTK_LABEL, "vram_viewer_status"), "");
gui_data.vram_viewer_active_tab = (gchar *)gtk_stack_get_visible_child_name(builder_get(GTK_STACK, "vram_viewer_stack"));
}
// This functions gets called immediately after registration of the GApplication
static void startup(GApplication *app, gpointer null_ptr) {
signal(SIGINT, quit_interrupt);
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_settings(app, gui_data.cli_options.config_path, &gui_data.config_modification_date, gui_data.preferences);
gui_data.vram_viewer = GTK_WINDOW(get_object("vram_viewer"));
gui_data.memory_viewer = GTK_WINDOW(get_object("memory_viewer"));
gui_data.console = GTK_WINDOW(get_object("console"));
gui_data.printer = GTK_WINDOW(get_object("printer"));
if (config.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);
g_signal_connect(gui_data.vram_viewer, "realize", G_CALLBACK(on_vram_viewer_realize), NULL);
g_signal_connect(gui_data.vram_viewer, "unrealize", G_CALLBACK(on_vram_viewer_unrealize), NULL);
// Just hide our sub-windows when closing them
g_signal_connect(gui_data.preferences, "delete-event", G_CALLBACK(gtk_widget_hide_on_delete), NULL);
g_signal_connect(gui_data.vram_viewer, "delete-event", G_CALLBACK(gtk_widget_hide_on_delete), NULL);
g_signal_connect(gui_data.memory_viewer, "delete-event", G_CALLBACK(gtk_widget_hide_on_delete), NULL);
g_signal_connect(gui_data.console, "delete-event", G_CALLBACK(gtk_widget_hide_on_delete), NULL);
g_signal_connect(gui_data.printer, "delete-event", G_CALLBACK(gtk_widget_hide_on_delete), NULL);
g_signal_connect(get_object("vram_viewer_tileset_canvas"), "draw", G_CALLBACK(on_draw_vram_viewer_tileset), NULL);
g_signal_connect(get_object("vram_viewer_tilemap_canvas"), "draw", G_CALLBACK(on_draw_vram_viewer_tilemap), NULL);
gtk_widget_add_events(builder_get(GTK_WIDGET, "vram_viewer_tileset_canvas"), GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK);
gtk_widget_add_events(builder_get(GTK_WIDGET, "vram_viewer_tilemap_canvas"), GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK);
g_signal_connect(get_object("vram_viewer_tileset_canvas"), "motion_notify_event", G_CALLBACK(on_motion_vram_viewer_tileset), NULL);
g_signal_connect(get_object("vram_viewer_tilemap_canvas"), "motion_notify_event", G_CALLBACK(on_motion_vram_viewer_tilemap), NULL);
g_signal_connect(get_object("vram_viewer_stack"), "notify::visible-child", G_CALLBACK(on_vram_tab_change), NULL);
}
// TODO: Comment
static void gl_draw(void) {
uint32_t *pixels = get_current_buffer();
uint32_t *previous = get_previous_buffer();
static void *_pixels = NULL;
if (pixels) {
_pixels = pixels;
}
glClearColor(0, 0, 0, 1);
glClear(GL_COLOR_BUFFER_BIT);
GB_frame_blending_mode_t mode = get_frame_blending_mode();
if (!previous) {
mode = GB_FRAME_BLENDING_MODE_DISABLED;
}
else if (mode == GB_FRAME_BLENDING_MODE_ACCURATE) {
if (GB_is_sgb(&gb)) {
mode = GB_FRAME_BLENDING_MODE_SIMPLE;
}
else {
mode = GB_is_odd_frame(&gb)? GB_FRAME_BLENDING_MODE_ACCURATE_ODD : GB_FRAME_BLENDING_MODE_ACCURATE_EVEN;
}
}
render_bitmap_with_shader(
&gui_data.shader, _pixels, previous,
GB_get_screen_width(&gb), GB_get_screen_height(&gb),
gui_data.viewport.x, gui_data.viewport.y, gui_data.viewport.w, gui_data.viewport.h,
mode
);
}
// TODO: Comment
static void gl_finish(void) { }
// TODO: Comment
static void resize(void) {
update_viewport();
}
// TODO: Comment
static gboolean on_draw_fallback(GtkWidget *widget, cairo_t *cr, gpointer data) {
GtkStyleContext *context = gtk_widget_get_style_context(widget);
guint width = gtk_widget_get_allocated_width(widget);
guint height = gtk_widget_get_allocated_height(widget);
guint screen_width = GB_get_screen_width(&gb);
guint screen_height = GB_get_screen_height(&gb);
gtk_render_background(context, cr, 0, 0, width, height);
cairo_surface_t *surface = cairo_image_surface_create_for_data(
(unsigned char *) get_current_buffer(),
CAIRO_FORMAT_RGB24,
screen_width,
screen_height,
cairo_format_stride_for_width(CAIRO_FORMAT_RGB24, screen_width)
);
cairo_translate(cr, gui_data.viewport.x, gui_data.viewport.y);
cairo_scale(cr, gui_data.viewport.w / screen_width, gui_data.viewport.h / screen_height);
cairo_set_source_surface(cr, surface, 0, 0);
cairo_pattern_set_filter(cairo_get_source(cr), CAIRO_FILTER_NEAREST);
cairo_paint(cr);
return false;
}
// Create a GtkDrawingArea as a fallback in case we cant use OpenGL
static void create_fallback_canvas(void) {
gui_data.fallback_canvas = GTK_DRAWING_AREA(gtk_drawing_area_new());
g_signal_connect(gui_data.fallback_canvas, "draw", G_CALLBACK(on_draw_fallback), NULL);
g_signal_connect(gui_data.fallback_canvas, "size-allocate", G_CALLBACK(resize), NULL);
gtk_box_pack_end(GTK_BOX(gui_data.main_window_container), GTK_WIDGET(gui_data.fallback_canvas), true, true, 0);
}
// TODO: Comment
static void gl_init(GtkWidget *w) {
GtkGLArea *gl_area = GTK_GL_AREA(w);
g_debug("GL_INIT");
const char *renderer;
g_debug("GL Context: %p", gtk_gl_area_get_context(gl_area));
gtk_gl_area_make_current(gl_area);
if (gtk_gl_area_get_error(gl_area) != NULL) {
goto error;
}
renderer = (char *)glGetString(GL_RENDERER);
g_debug("GtkGLArea on %s", renderer ? renderer : "Unknown");
if (config.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();
while (gui_data.stopping);
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);
}
// Clear the VRAM viewer
g_mutex_lock(&gui_data.tileset_buffer_mutex);
memset(gui_data.tileset_buffer, 0, sizeof gui_data.tileset_buffer);
g_mutex_unlock(&gui_data.tileset_buffer_mutex);
g_mutex_lock(&gui_data.tilemap_buffer_mutex);
memset(gui_data.tilemap_buffer, 0, sizeof gui_data.tilemap_buffer);
g_mutex_unlock(&gui_data.tilemap_buffer_mutex);
gtk_stack_set_visible_child_name(builder_get(GTK_STACK, "vram_viewer_stack"), "vram_viewer_tileset");
gtk_tree_view_set_model(builder_get(GTK_TREE_VIEW, "vram_viewer_sprites"), NULL);
gtk_tree_view_set_model(builder_get(GTK_TREE_VIEW, "vram_viewer_palettes"), NULL);
// Redraw the VRAM viewer
gtk_widget_queue_draw(GTK_WIDGET(gui_data.vram_viewer));
// Update menu action states
action_set_enabled(gui_data.main_application, "close", false);
action_entries_set_enabled(emulation_entries, G_N_ELEMENTS(emulation_entries), false);
// 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, 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, 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, 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, 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;
}