2742 lines
89 KiB
C
2742 lines
89 KiB
C
#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 can’t 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 application’s menu.
|
||
//
|
||
// This function tries to stick to the desktop environment’s 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, ®ister_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 context’s 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 = g_strdup(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;
|
||
|
||
g_free(config.emulation.model);
|
||
config.emulation.model = g_strdup(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 can’t 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` won’t 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;
|
||
}
|