SameBoy/gtk3/main.c

1361 lines
45 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

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

#define G_LOG_USE_STRUCTURED
#include <gtk/gtk.h>
#include <signal.h>
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#include <Core/gb.h>
#include "types.h"
#include "config.h"
#include "util.h"
#include "check_menu_radio_group.h"
#include "gb_screen.h"
#include "console_window.h"
#include "preferences_window.h"
#include "vram_viewer_window.h"
// used for audio and game controllers
#include "SDL.h"
#include "../SDL/audio/audio.h"
#define JOYSTICK_HIGH 0x4000
#define JOYSTICK_LOW 0x3800
#define BUTTON_MASK_A 0x01
#define BUTTON_MASK_B 0x02
#define BUTTON_MASK_START 0x04
#define BUTTON_MASK_SELECT 0x08
#define BUTTON_MASK_UP 0x10
#define BUTTON_MASK_DOWN 0x20
#define BUTTON_MASK_LEFT 0x40
#define BUTTON_MASK_RIGHT 0x80
#define str(x) #x
#define xstr(x) str(x)
#define get_object(id) gtk_builder_get_object(gui_data.builder, id)
#define builder_get(type, id) type(get_object(id))
#define action_set_enabled(map, name, value) g_simple_action_set_enabled(G_SIMPLE_ACTION(g_action_map_lookup_action(G_ACTION_MAP(map), name)), value);
// Initialize the GuiData
static GuiData gui_data = {
.cli_options = {
.fullscreen = false,
.model = -1,
},
.prev_model = -1,
.running = false,
.stopping = false,
.stopped = false,
.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,
};
static GB_gameboy_t gb;
// Forward declarations of the actions
static void activate_open(GSimpleAction *action, GVariant *parameter, gpointer app);
static void activate_close(GSimpleAction *action, GVariant *parameter, gpointer app);
static void activate_reset(GSimpleAction *action, GVariant *parameter, gpointer app);
static void activate_show_console(GSimpleAction *action, GVariant *parameter, gpointer app);
static void activate_open_gtk_debugger(GSimpleAction *action, GVariant *parameter, gpointer app);
static void activate_open_memory_viewer(GSimpleAction *action, GVariant *parameter, gpointer app);
static void activate_open_vram_viewer(GSimpleAction *action, GVariant *parameter, gpointer app);
static void activate_clear_console(GSimpleAction *action, GVariant *parameter, gpointer app);
static void activate_quit(GSimpleAction *action, GVariant *parameter, gpointer app);
static void activate_about(GSimpleAction *action, GVariant *parameter, gpointer app);
static void activate_preferences(GSimpleAction *action, GVariant *parameter, gpointer app);
static void on_pause_changed(GSimpleAction *action, GVariant *value, gpointer user_data_ptr);
static void on_mute_changed(GSimpleAction *action, GVariant *value, gpointer user_data_ptr);
static const GActionEntry file_entries[] = {
{ "open", activate_open, NULL, NULL, NULL },
{ "close", activate_close, NULL, NULL, NULL },
};
static const GActionEntry edit_entries[] = {
};
static const GActionEntry emulation_entries[] = {
{ "reset", activate_reset, NULL, NULL, NULL },
{ "pause", NULL, NULL, "false", on_pause_changed },
{ "save_state", NULL, NULL, NULL, NULL },
{ "load_state", NULL, NULL, NULL, NULL },
};
static const GActionEntry developer_entries[] = {
{ "show_console", activate_show_console, NULL, NULL, NULL },
{ "open_gtk_debugger", activate_open_gtk_debugger, NULL, NULL, NULL },
// { "open_memory_viewer", activate_open_memory_viewer, NULL, NULL, NULL },
{ "open_vram_viewer", activate_open_vram_viewer, NULL, NULL, NULL },
{ "toggle_developer_mode", NULL, NULL, "false", NULL },
{ "clear_console", activate_clear_console, NULL, NULL, NULL },
};
static const GActionEntry app_entries[] = {
{ "quit", activate_quit, NULL, NULL, NULL },
{ "about", activate_about, NULL, NULL, NULL },
{ "preferences", activate_preferences, NULL, NULL, NULL },
{ "toggle_mute", NULL, NULL, "false", on_mute_changed },
};
static const char* get_sdl_joystick_power_level_name(SDL_JoystickPowerLevel level) {
switch (level) {
case SDL_JOYSTICK_POWER_EMPTY: return "Empty";
case SDL_JOYSTICK_POWER_LOW: return "Low";
case SDL_JOYSTICK_POWER_MEDIUM: return "Medium";
case SDL_JOYSTICK_POWER_FULL: return "Full";
case SDL_JOYSTICK_POWER_WIRED: return "Wired";
case SDL_JOYSTICK_POWER_MAX: return "Max";
case SDL_JOYSTICK_POWER_UNKNOWN:
default:
return "Unknown";
}
}
// This function gets called after the parsing of the commandline options has occurred.
static gint handle_local_options(GApplication *app, GVariantDict *options, gpointer null_ptr) {
guint32 count;
if (g_variant_dict_lookup(options, "version", "b", &count)) {
g_message("SameBoy v" xstr(VERSION));
return EXIT_SUCCESS;
}
// Handle model override
GVariant *model_name_var = g_variant_dict_lookup_value(options, "model", G_VARIANT_TYPE_STRING);
if (model_name_var != NULL) {
const gchar *model_name = g_variant_get_string(model_name_var, NULL);
// TODO: Synchronize with GB_model_t (Core/gb.h)
if (g_str_has_prefix(model_name, "DMG")) {
if (g_str_has_suffix(model_name, "-B") || g_strcmp0(model_name, "DMG") == 0) {
gui_data.cli_options.model = GB_MODEL_DMG_B;
}
else {
gui_data.cli_options.model = GB_MODEL_DMG_B;
g_warning("Unsupported revision: %s\nFalling back to DMG-B", model_name);
}
}
else if (g_str_has_prefix(model_name, "SGB")) {
if (g_str_has_suffix(model_name, "-NTSC") || g_strcmp0(model_name, "SGB") == 0) {
gui_data.cli_options.model = GB_MODEL_SGB;
}
else if (g_str_has_suffix(model_name, "-PAL")) {
gui_data.cli_options.model = GB_MODEL_SGB | GB_MODEL_PAL_BIT;
}
else if (g_str_has_suffix(model_name, "2")) {
gui_data.cli_options.model = GB_MODEL_SGB2;
}
else {
gui_data.cli_options.model = GB_MODEL_SGB2;
g_warning("Unsupported revision: %s\nFalling back to SGB2", model_name);
}
}
else if (g_str_has_prefix(model_name, "CGB")) {
if (g_str_has_suffix(model_name, "-C")) {
gui_data.cli_options.model = GB_MODEL_CGB_C;
}
else if (g_str_has_suffix(model_name, "-E") || g_strcmp0(model_name, "CGB") == 0) {
gui_data.cli_options.model = GB_MODEL_CGB_E;
}
else {
gui_data.cli_options.model = GB_MODEL_CGB_E;
g_warning("Unsupported revision: %s\nFalling back to CGB-E", model_name);
}
}
else if (g_str_has_prefix(model_name, "AGB")) {
gui_data.cli_options.model = GB_MODEL_AGB;
}
else {
g_warning("Unknown model: %s", model_name);
exit(EXIT_FAILURE);
}
}
return -1;
}
static gboolean init_controllers(void) {
SDL_version compiled;
SDL_version linked;
SDL_VERSION(&compiled);
SDL_GetVersion(&linked);
g_debug("Compiled against SDL version %d.%d.%d", compiled.major, compiled.minor, compiled.patch);
g_debug("Linked against SDL version %d.%d.%d", linked.major, linked.minor, linked.patch);
g_debug("Initializing game controllers");
if (SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) < 0) {
g_warning("Failed to initialize game controller support: %s", SDL_GetError());
return false;
}
g_debug("Initializing haptic feedback");
if (SDL_InitSubSystem(SDL_INIT_HAPTIC) < 0) {
g_warning("Failed to initialize haptic feedback support: %s", SDL_GetError());
}
g_debug("Loading custom game controller database");
GError *error = NULL;
GBytes *db_f = g_resources_lookup_data(RESOURCE_PREFIX "gamecontrollerdb.txt", G_RESOURCE_LOOKUP_FLAGS_NONE, &error);
if (db_f != NULL) {
gsize db_data_size = 0;
const guchar *db_data = g_bytes_get_data(db_f, &db_data_size);
const gint val = SDL_GameControllerAddMappingsFromRW(SDL_RWFromMem((void *)db_data, db_data_size), 1);
if (val < 0) {
g_warning("Failed to load controller mappings: %s", SDL_GetError());
}
g_bytes_unref(db_f);
}
if (error != NULL) g_clear_error(&error);
g_message("Number of found controllers: %d", SDL_NumJoysticks());
// In the “worst” case all joysticks are valid game controllers
gui_data.controllers = g_malloc0(sizeof(struct Controller_t) * SDL_NumJoysticks());
for (int i = 0; i < SDL_NumJoysticks(); ++i) {
if (SDL_IsGameController(i)) {
struct Controller_t *s = &gui_data.controllers[i];
s->controller = SDL_GameControllerOpen(i);
if (s->controller) {
SDL_Joystick *joystick = SDL_GameControllerGetJoystick(s->controller);
SDL_JoystickPowerLevel power_level = SDL_JoystickCurrentPowerLevel(joystick);
if (SDL_JoystickIsHaptic(joystick)) {
s->haptic = SDL_HapticOpenFromJoystick(joystick);
if (s->haptic && SDL_HapticRumbleSupported(s->haptic)) {
SDL_HapticRumbleInit(s->haptic);
}
else {
if (s->haptic == NULL) {
g_warning("%s", SDL_GetError());
}
SDL_HapticClose(s->haptic);
s->haptic = NULL;
}
}
// Blacklist the WUP-028 for now
if (SDL_JoystickGetVendor(joystick) == 0x057e
&& SDL_JoystickGetProduct(joystick) == 0x0337
&& SDL_JoystickGetProductVersion(joystick) == 0x0100) {
s->ignore_rumble = true;
}
char guid_str[33];
SDL_JoystickGUID guid = SDL_JoystickGetGUID(joystick);
SDL_JoystickGetGUIDString(guid, guid_str, sizeof(guid_str));
g_message("Controller #%u (%s): %s; Haptic Feedback: %d; Power level: %s; Player index: %u; Instance ID: %u", i, guid_str, SDL_GameControllerName(s->controller), s->haptic != NULL, get_sdl_joystick_power_level_name(power_level), SDL_JoystickGetPlayerIndex(joystick), SDL_JoystickInstanceID(joystick));
gui_data.controller_count++;
}
else {
g_warning("Could not open controller %i: %s", i, SDL_GetError());
}
}
}
return true;
}
static gboolean init_audio(void) {
bool audio_playing = GB_audio_is_playing();
if (gui_data.audio_initialized) {
GB_audio_destroy();
gui_data.audio_initialized = false;
}
#ifdef USE_SDL_AUDIO
if (SDL_InitSubSystem(SDL_INIT_AUDIO) < 0) {
g_warning("Failed to initialize audio: %s", SDL_GetError());
return false;
}
#endif
GB_audio_init(gui_data.sample_rate);
GB_set_sample_rate(&gb, GB_audio_get_sample_rate());
// restore playing state
GB_audio_set_paused(!audio_playing);
return gui_data.audio_initialized = true;
}
static void gb_audio_callback(GB_gameboy_t *gb, GB_sample_t *sample) {
if (gui_data.turbo_down) {
static unsigned skip = 0;
skip++;
if (skip == GB_audio_get_sample_rate() / 8) {
skip = 0;
}
if (skip > GB_audio_get_sample_rate() / 16) {
return;
}
}
if (GB_audio_get_queue_length() / sizeof(*sample) > GB_audio_get_sample_rate() / 4) {
return;
}
GB_audio_queue_sample(sample);
}
static void rumble_callback(GB_gameboy_t *gb, double amp) {
if (!gui_data.controllers || gui_data.controller_count == 0 || !gui_data.last_used_controller) return;
struct Controller_t *s = gui_data.last_used_controller;
if (s->ignore_rumble) return;
if (s->haptic) {
if (amp > 0.0) {
SDL_HapticRumblePlay(s->haptic, amp, 100.0);
}
else {
SDL_HapticRumbleStop(s->haptic);
}
}
else {
if (amp == 0.0) {
SDL_GameControllerRumble(s->controller, 0, 0, 0);
}
else {
Uint16 intensity = (float) 0xFFFF * amp;
SDL_GameControllerRumble(s->controller, intensity, intensity, 100);
}
}
}
// Creating these items in the UI defintion files was buggy in some desktop
// environments and the manual call of `g_signal_connect` was needed anyway
// because the UI definition cant define string arguments for signal handlers.
static void create_model_menu_items() {
bool on_change_model(GtkWidget *, gpointer);
static const char *const model_names[] = {
"Game Boy",
"Super Game Boy",
"Game Boy Color",
"Game Boy Advance",
NULL
};
static const char *const model_codes[] = {
"DMG",
"SGB",
"CGB",
"GBA",
NULL
};
// Find the menu item index of the previous sibling of the new menu items
GtkWidget *before = builder_get(GTK_WIDGET, "before_model_changer");
GtkContainer *parent = GTK_CONTAINER(gtk_widget_get_parent(before));
g_autoptr(GList) list = gtk_container_get_children(parent);
gint position = g_list_index(list, before);
CheckMenuItemGroup *model_group = check_menu_item_group_new((char **) model_names, (char **) model_codes);
check_menu_item_group_insert_into_menu_shell(model_group, GTK_MENU_SHELL(parent), position + 1);
check_menu_item_group_connect_toggle_signal(model_group, on_change_model);
check_menu_item_group_activate(model_group, config.emulation.model);
static const char *const peripheral_names[] = {
"None",
"Game Boy Printer",
NULL
};
static const char *const peripheral_codes[] = {
"NONE",
"PRINTER",
NULL,
};
CheckMenuItemGroup *link_group = check_menu_item_group_new((char **) peripheral_names, (char **) peripheral_codes);
check_menu_item_group_insert_into_menu_shell(link_group, GTK_MENU_SHELL(builder_get(GTK_MENU_SHELL, "link_menu")), 0);
// check_menu_item_group_connect_toggle_signal(link_group, on_change_linked_device);
check_menu_item_group_activate(link_group, "NONE");
}
// Create our applications menu.
//
// This function tries to stick to the desktop environments conventions.
// For the GNOME Shell it uses a hamburger menu, otherwise it either lets
// the desktop environment shell handle the menu if it signals support for it
// or uses a standard menubar inside the window.
static void setup_menu(GApplication *app) {
create_model_menu_items();
GtkMenuBar *menubar = builder_get(GTK_MENU_BAR, "main_menu");
gtk_box_pack_start(GTK_BOX(gui_data.main_window_container), GTK_WIDGET(menubar), false, false, 0);
}
// 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 stop(void) {
if (!gui_data.running) return;
GB_audio_set_paused(true);
GB_debugger_set_disabled(&gb, true);
if (GB_debugger_is_stopped(&gb)) {
// [self interruptDebugInputRead];
}
gui_data.stopping = true;
gui_data.running = false;
while (gui_data.stopping);
GB_debugger_set_disabled(&gb, false);
gui_data.stopped = true;
}
static gboolean on_vblank(GB_gameboy_t *gb) {
gb_screen_queue_render(gui_data.screen);
gtk_widget_queue_draw(GTK_WIDGET(gui_data.vram_viewer));
return false;
}
static void vblank(GB_gameboy_t *gb) {
gb_screen_flip(gui_data.screen);
if (gui_data.border_mode_changed) {
GB_set_border_mode(gb, config_get_display_border_mode());
gb_screen_set_resolution(gui_data.screen, GB_get_screen_width(gb), GB_get_screen_height(gb));
GB_set_pixels_output(gb, gb_screen_get_pixels(gui_data.screen));
gui_data.border_mode_changed = false;
}
GB_set_pixels_output(gb, gb_screen_get_pixels(gui_data.screen));
// Handle the speed modifiers:
// The binary slowdown is limited to half speed.
// The analog multiplier can go down to a third and up to three times full speed.
if (gui_data.underclock_down && gui_data.clock_mutliplier > 0.5) {
gui_data.clock_mutliplier -= 1.0 / 16;
//gui_data.clock_mutliplier = clamp_double(0.5, 1.0, gui_data.clock_mutliplier - 1.0 / 16);
GB_set_clock_multiplier(gb, gui_data.clock_mutliplier);
}
else if (!gui_data.underclock_down && gui_data.clock_mutliplier < 1.0) {
gui_data.clock_mutliplier += 1.0 / 16;
//gui_data.clock_mutliplier = clamp_double(0.5, 1.0, gui_data.clock_mutliplier + 1.0 / 16);
GB_set_clock_multiplier(gb, gui_data.clock_mutliplier);
}
else if (config.controls.analog_speed_controls && gui_data.analog_clock_multiplier_valid) {
GB_set_clock_multiplier(gb, gui_data.analog_clock_multiplier);
if (gui_data.analog_clock_multiplier == 1.0) {
gui_data.analog_clock_multiplier_valid = false;
}
}
gui_data.do_rewind = gui_data.rewind_down;
vram_viewer_update(gui_data.vram_viewer, gb);
GB_frame_blending_mode_t mode = config_get_frame_blending_mode();
if (!gb_screen_get_previous_buffer(gui_data.screen)) {
mode = GB_FRAME_BLENDING_MODE_DISABLED;
}
else if (mode == GB_FRAME_BLENDING_MODE_ACCURATE) {
mode = GB_FRAME_BLENDING_MODE_DISABLED;
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;
}
}
gb_screen_set_blending_mode(gui_data.screen, mode);
g_idle_add((GSourceFunc) on_vblank, gb);
}
static void handle_events(GB_gameboy_t *gb) {
SDL_GameControllerUpdate();
uint8_t controller_state = 0;
gui_data.analog_clock_multiplier = 1.0;
for (unsigned i = 0; i < gui_data.controller_count; i++) {
struct Controller_t *s = &gui_data.controllers[i];
int16_t left_x_axis = SDL_GameControllerGetAxis(s->controller, SDL_CONTROLLER_AXIS_LEFTX);
int16_t left_y_axis = SDL_GameControllerGetAxis(s->controller, SDL_CONTROLLER_AXIS_LEFTY);
int16_t right_x_axis = SDL_GameControllerGetAxis(s->controller, SDL_CONTROLLER_AXIS_RIGHTX);
int16_t right_y_axis = SDL_GameControllerGetAxis(s->controller, SDL_CONTROLLER_AXIS_RIGHTY);
if (config.controls.analog_speed_controls) {
double left_trigger = (double) SDL_GameControllerGetAxis(s->controller, SDL_CONTROLLER_AXIS_TRIGGERLEFT) / (double) 32767;
double right_trigger = (double) SDL_GameControllerGetAxis(s->controller, SDL_CONTROLLER_AXIS_TRIGGERRIGHT) / (double) 32767;
if (left_trigger > 0.0) {
gui_data.analog_clock_multiplier = min_double(gui_data.analog_clock_multiplier, clamp_double(1.0 / 3, 1.0, 1 - left_trigger + 0.2));
gui_data.analog_clock_multiplier_valid = true;
}
else if (right_trigger > 0.0) {
gui_data.analog_clock_multiplier = max_double(gui_data.analog_clock_multiplier, clamp_double(1.0, 3.0, right_trigger * 3 + 0.8));
gui_data.analog_clock_multiplier_valid = true;
}
}
if (left_x_axis >= JOYSTICK_HIGH || right_x_axis >= JOYSTICK_HIGH) {
gui_data.last_used_controller = s;
controller_state |= BUTTON_MASK_RIGHT;
}
else if (left_x_axis <= -JOYSTICK_HIGH || right_x_axis <= -JOYSTICK_HIGH) {
gui_data.last_used_controller = s;
controller_state |= BUTTON_MASK_LEFT;
}
if (left_y_axis >= JOYSTICK_HIGH || right_y_axis >= JOYSTICK_HIGH) {
gui_data.last_used_controller = s;
controller_state |= BUTTON_MASK_DOWN;
}
else if (left_y_axis <= -JOYSTICK_HIGH || right_y_axis <= -JOYSTICK_HIGH) {
gui_data.last_used_controller = s;
controller_state |= BUTTON_MASK_UP;
}
if (SDL_GameControllerGetButton(s->controller, SDL_CONTROLLER_BUTTON_DPAD_RIGHT)) {
gui_data.last_used_controller = s;
controller_state |= BUTTON_MASK_RIGHT;
}
if (SDL_GameControllerGetButton(s->controller, SDL_CONTROLLER_BUTTON_DPAD_LEFT)) {
gui_data.last_used_controller = s;
controller_state |= BUTTON_MASK_LEFT;
}
if (SDL_GameControllerGetButton(s->controller, SDL_CONTROLLER_BUTTON_DPAD_UP)) {
gui_data.last_used_controller = s;
controller_state |= BUTTON_MASK_UP;
}
if (SDL_GameControllerGetButton(s->controller, SDL_CONTROLLER_BUTTON_DPAD_DOWN)) {
gui_data.last_used_controller = s;
controller_state |= BUTTON_MASK_DOWN;
}
if (SDL_GameControllerGetButton(s->controller, SDL_CONTROLLER_BUTTON_A)) {
gui_data.last_used_controller = s;
controller_state |= BUTTON_MASK_A;
}
if (SDL_GameControllerGetButton(s->controller, SDL_CONTROLLER_BUTTON_B)) {
gui_data.last_used_controller = s;
controller_state |= BUTTON_MASK_B;
}
if (SDL_GameControllerGetButton(s->controller, SDL_CONTROLLER_BUTTON_BACK)) {
gui_data.last_used_controller = s;
controller_state |= BUTTON_MASK_SELECT;
}
if (SDL_GameControllerGetButton(s->controller, SDL_CONTROLLER_BUTTON_START)) {
gui_data.last_used_controller = s;
controller_state |= BUTTON_MASK_START;
}
}
GB_set_key_state(gb, GB_KEY_RIGHT, (gui_data.pressed_buttons & BUTTON_MASK_RIGHT) | (controller_state & BUTTON_MASK_RIGHT));
GB_set_key_state(gb, GB_KEY_LEFT, (gui_data.pressed_buttons & BUTTON_MASK_LEFT) | (controller_state & BUTTON_MASK_LEFT));
GB_set_key_state(gb, GB_KEY_UP, (gui_data.pressed_buttons & BUTTON_MASK_UP) | (controller_state & BUTTON_MASK_UP));
GB_set_key_state(gb, GB_KEY_DOWN, (gui_data.pressed_buttons & BUTTON_MASK_DOWN) | (controller_state & BUTTON_MASK_DOWN));
GB_set_key_state(gb, GB_KEY_A, (gui_data.pressed_buttons & BUTTON_MASK_A) | (controller_state & BUTTON_MASK_A));
GB_set_key_state(gb, GB_KEY_B, (gui_data.pressed_buttons & BUTTON_MASK_B) | (controller_state & BUTTON_MASK_B));
GB_set_key_state(gb, GB_KEY_SELECT, (gui_data.pressed_buttons & BUTTON_MASK_SELECT) | (controller_state & BUTTON_MASK_SELECT));
GB_set_key_state(gb, GB_KEY_START, (gui_data.pressed_buttons & BUTTON_MASK_START) | (controller_state & BUTTON_MASK_START));
}
static void load_boot_rom(GB_gameboy_t *gb, GB_boot_rom_t type) {
GError *error = NULL;
char *boot_rom_path = NULL;
GBytes *boot_rom_f = NULL;
const guchar *boot_rom_data;
gsize boot_rom_size;
static const char *const names[] = {
[GB_BOOT_ROM_DMG0] = "dmg0_boot.bin",
[GB_BOOT_ROM_DMG] = "dmg_boot.bin",
[GB_BOOT_ROM_MGB] = "mgb_boot.bin",
[GB_BOOT_ROM_SGB] = "sgb_boot.bin",
[GB_BOOT_ROM_SGB2] = "sgb2_boot.bin",
[GB_BOOT_ROM_CGB0] = "cgb0_boot.bin",
[GB_BOOT_ROM_CGB] = "cgb_boot.bin",
[GB_BOOT_ROM_AGB] = "agb_boot.bin",
};
const char *const boot_rom_name = names[type];
if (gui_data.cli_options.boot_rom_path != NULL) {
g_message("[CLI override] Trying to load boot ROM from %s", gui_data.cli_options.boot_rom_path);
if (GB_load_boot_rom(gb, gui_data.cli_options.boot_rom_path)) {
g_warning("Falling back to boot ROM from config");
goto config_boot_rom;
}
}
else { config_boot_rom:
if (config.emulation.boot_rom_path != NULL && g_strcmp0(config.emulation.boot_rom_path, "other") != 0 && g_strcmp0(config.emulation.boot_rom_path, "auto") != 0) {
boot_rom_path = g_build_filename(config.emulation.boot_rom_path, boot_rom_name, NULL);
g_message("Trying to load boot ROM from %s", boot_rom_path);
if (GB_load_boot_rom(gb, boot_rom_path)) {
g_free(boot_rom_path);
g_warning("Falling back to internal boot ROM");
goto internal_boot_rom;
}
g_free(boot_rom_path);
}
else { internal_boot_rom:
boot_rom_path = g_build_filename(RESOURCE_PREFIX "bootroms/", boot_rom_name, NULL);
boot_rom_f = g_resources_lookup_data(boot_rom_path, G_RESOURCE_LOOKUP_FLAGS_NONE, &error);
g_message("Loading internal boot ROM: %s", boot_rom_path);
g_free(boot_rom_path);
if (boot_rom_f == NULL) {
g_warning("Failed to load internal boot ROM: %s", boot_rom_path);
g_error_free(error);
exit(EXIT_FAILURE);
}
boot_rom_data = g_bytes_get_data(boot_rom_f, &boot_rom_size);
GB_load_boot_rom_from_buffer(gb, boot_rom_data, boot_rom_size);
g_bytes_unref(boot_rom_f);
}
}
}
static char *wrapped_console_get_async_input(GB_gameboy_t *gb) {
return console_get_async_input(gui_data.console, gb);
}
static char *wrapped_console_get_sync_input(GB_gameboy_t *gb) {
return console_get_sync_input(gui_data.console, gb);
}
static void wrapped_console_log(GB_gameboy_t *gb, const char *message, GB_log_attributes attributes) {
console_log(gui_data.console, message, attributes);
}
static void init(void) {
if (GB_is_inited(&gb)) return;
GB_init(&gb, config_get_model_type(&gui_data));
GB_set_vblank_callback(&gb, vblank);
GB_set_rgb_encode_callback(&gb, rgb_encode);
GB_set_pixels_output(&gb, gb_screen_get_current_buffer(gui_data.screen));
GB_set_color_correction_mode(&gb, config_get_color_correction_mode());
if (config_get_display_border_mode() <= GB_BORDER_ALWAYS) {
GB_set_border_mode(&gb, config_get_display_border_mode());
}
GB_apu_set_sample_callback(&gb, gb_audio_callback);
GB_set_sample_rate(&gb, GB_audio_get_sample_rate());
GB_set_highpass_filter_mode(&gb, config_get_highpass_mode());
GB_set_log_callback(&gb, wrapped_console_log);
GB_set_input_callback(&gb, wrapped_console_get_sync_input);
GB_set_async_input_callback(&gb, wrapped_console_get_async_input);
GB_set_boot_rom_load_callback(&gb, load_boot_rom);
GB_set_update_input_hint_callback(&gb, handle_events);
GB_set_rumble_callback(&gb, rumble_callback);
GB_set_rumble_mode(&gb, config_get_rumble_mode());
GB_set_rewind_length(&gb, config.emulation.rewind_duration);
}
static void reset(void) {
g_debug("Reset: %d == %d", config_get_model_type(&gui_data), gui_data.prev_model);
GB_model_t current_model = config_get_model_type(&gui_data);
if (gui_data.prev_model == -1 || gui_data.prev_model == current_model) {
GB_reset(&gb);
}
else {
GB_switch_model_and_reset(&gb, current_model);
}
GB_set_palette(&gb, config_get_monochrome_palette());
gui_data.prev_model = config_get_model_type(&gui_data);
// Check SGB -> non-SGB and non-SGB to SGB transitions
if (GB_get_screen_width(&gb) != gui_data.last_screen_width || GB_get_screen_height(&gb) != gui_data.last_screen_height) {
gb_screen_set_resolution(gui_data.screen, GB_get_screen_width(&gb), GB_get_screen_height(&gb));
GB_set_pixels_output(&gb, gb_screen_get_pixels(gui_data.screen));
}
bool success = false;
if (gui_data.file) {
char *path = g_file_get_path(gui_data.file);
char *ext = strrchr(path, '.');
int result;
GB_debugger_clear_symbols(&gb);
if (g_strcmp0(ext + 1, "isx") == 0) {
result = GB_load_isx(&gb, path);
}
else {
result = GB_load_rom(&gb, path);
}
if (result == 0) {
success = true;
}
else {
g_warning("Failed to load ROM: %s", path);
}
GB_load_battery(&gb, gui_data.battery_save_path);
GB_load_cheats(&gb, gui_data.cheats_save_path);
GError *error = NULL;
GBytes *register_sym_f = g_resources_lookup_data(RESOURCE_PREFIX "Misc/registers.sym", G_RESOURCE_LOOKUP_FLAGS_NONE, &error);
if (register_sym_f) {
gsize register_sym_size;
const gchar *register_sym_data = g_bytes_get_data(register_sym_f, &register_sym_size);
GB_debugger_load_symbol_file_from_buffer(&gb, register_sym_data, register_sym_size);
g_bytes_unref(register_sym_f);
}
size_t path_length = strlen(path);
char sym_file_path[path_length + 5];
replace_extension(path, path_length, sym_file_path, ".sym");
GB_debugger_load_symbol_file(&gb, sym_file_path);
g_free(path);
}
action_set_enabled(gui_data.main_application, "close", success);
action_entries_set_enabled(emulation_entries, G_N_ELEMENTS(emulation_entries), success);
}
static void start(void) {
gui_data.running = true;
gui_data.stopped = false;
GB_audio_clear_queue();
GB_audio_set_paused(config.audio.muted);
/* Run emulation */
while (gui_data.running) {
if (gui_data.rewind_paused) {
handle_events(&gb);
g_usleep(G_USEC_PER_SEC / 8);
}
else {
if (gui_data.do_rewind) {
GB_rewind_pop(&gb);
if (gui_data.turbo_down) {
GB_rewind_pop(&gb);
}
if (!GB_rewind_pop(&gb)) {
gui_data.rewind_paused = true;
}
gui_data.do_rewind = false;
}
GB_run(&gb);
}
}
if (gui_data.file) {
GB_save_battery(&gb, gui_data.battery_save_path);
GB_save_cheats(&gb, gui_data.cheats_save_path);
}
gui_data.stopping = false;
}
// Prevent dependency loop
static void run(void);
// app.reset GAction
// Resets the emulation
static void activate_reset(GSimpleAction *action, GVariant *parameter, gpointer app) {
if (!GB_is_inited(&gb)) {
init();
}
stop();
reset();
run();
}
static gpointer run_thread(gpointer null_ptr) {
if (!gui_data.file) return NULL;
char *path = g_file_get_path(gui_data.file);
size_t path_length = strlen(path);
/* At the worst case, size is strlen(path) + 4 bytes for .sav + NULL */
char battery_save_path[path_length + 5];
char cheats_save_path[path_length + 5];
replace_extension(path, path_length, battery_save_path, ".sav");
replace_extension(path, path_length, cheats_save_path, ".cht");
gui_data.battery_save_path = battery_save_path;
gui_data.cheats_save_path = cheats_save_path;
if (!GB_is_inited(&gb)) {
init();
}
if (gui_data.stopped) {
start();
}
else {
reset();
start();
}
return NULL;
}
static void run(void) {
if (gui_data.running) return;
while (gui_data.stopping);
g_thread_new("CoreLoop", run_thread, NULL);
}
// Tell our application to quit.
// After this functions has been called the `shutdown` signal will be issued.
//
// TODO: Make sure we have a way to quit our emulation loop before `shutdown` gets called
static void quit(void) {
g_debug("quit(void);");
stop();
GtkWindow *window = gui_data.main_window ? GTK_WINDOW(gui_data.main_window) : NULL;
save_config(window, gui_data.config_modification_date);
free_config();
for (unsigned i = 0; i < gui_data.controller_count; i++) {
struct Controller_t *s = &gui_data.controllers[i];
SDL_HapticClose(s->haptic);
SDL_GameControllerClose(s->controller);
}
// Quit our application properly.
// This fires the “shutdown” signal.
g_application_quit(G_APPLICATION(gui_data.main_application));
}
static void quit_interrupt(int ignored) {
g_debug("quit_interrupt(%d);", ignored);
quit();
}
static void create_action_groups(GApplication *app) {
g_action_map_add_action_entries(G_ACTION_MAP(app), emulation_entries, G_N_ELEMENTS(emulation_entries), NULL);
g_action_map_add_action_entries(G_ACTION_MAP(app), developer_entries, G_N_ELEMENTS(developer_entries), NULL);
g_action_map_add_action_entries(G_ACTION_MAP(app), app_entries, G_N_ELEMENTS(app_entries), NULL);
g_action_map_add_action_entries(G_ACTION_MAP(app), file_entries, G_N_ELEMENTS(file_entries), NULL);
g_action_map_add_action_entries(G_ACTION_MAP(app), edit_entries, G_N_ELEMENTS(edit_entries), NULL);
action_set_enabled(app, "close", false);
action_entries_set_enabled(emulation_entries, G_N_ELEMENTS(emulation_entries), false);
}
static gboolean on_key_press(GtkWidget *w, GdkEventKey *event, gpointer data) {
uint8_t mask;
if (event->keyval == key_map[INPUT_UP]) mask = BUTTON_MASK_UP;
if (event->keyval == key_map[INPUT_DOWN]) mask = BUTTON_MASK_DOWN;
if (event->keyval == key_map[INPUT_LEFT]) mask = BUTTON_MASK_LEFT;
if (event->keyval == key_map[INPUT_RIGHT]) mask = BUTTON_MASK_RIGHT;
if (event->keyval == key_map[INPUT_START]) mask = BUTTON_MASK_START;
if (event->keyval == key_map[INPUT_SELECT]) mask = BUTTON_MASK_SELECT;
if (event->keyval == key_map[INPUT_A]) mask = BUTTON_MASK_A;
if (event->keyval == key_map[INPUT_B]) mask = BUTTON_MASK_B;
if (event->keyval == key_map[INPUT_REWIND]) {
gui_data.rewind_down = event->type == GDK_KEY_PRESS;
GB_set_turbo_mode(&gb, gui_data.turbo_down, gui_data.turbo_down && gui_data.rewind_down);
if (event->type == GDK_KEY_RELEASE) {
gui_data.rewind_paused = false;
}
}
if (event->keyval == key_map[INPUT_TURBO]) {
gui_data.turbo_down = event->type == GDK_KEY_PRESS;
gui_data.analog_clock_multiplier_valid = false;
GB_audio_clear_queue();
GB_set_turbo_mode(&gb, gui_data.turbo_down, gui_data.turbo_down && gui_data.rewind_down);
}
if (event->keyval == key_map[INPUT_SLOWDOWN]) {
gui_data.underclock_down = event->type == GDK_KEY_PRESS;
gui_data.analog_clock_multiplier_valid = false;
}
if (event->keyval == key_map[INPUT_FULLSCREEN]) {
if (event->type == GDK_KEY_RELEASE) {
if (gui_data.is_fullscreen) {
gtk_window_unfullscreen(GTK_WINDOW(gui_data.main_window));
}
else {
gtk_window_fullscreen(GTK_WINDOW(gui_data.main_window));
}
}
}
if (event->type == GDK_KEY_PRESS) {
gui_data.pressed_buttons |= mask;
}
else if (event->type == GDK_KEY_RELEASE) {
gui_data.pressed_buttons &= ~mask;
}
return false;
}
static void on_window_state_change(GtkWidget *w, GdkEventWindowState *event, gpointer data) {
gui_data.is_fullscreen = event->new_window_state & GDK_WINDOW_STATE_FULLSCREEN;
}
// This functions gets called immediately after registration of the GApplication
static void startup(GApplication *app, gpointer null_ptr) {
signal(SIGINT, quit_interrupt);
g_debug("GTK version %u.%u.%u", gtk_get_major_version(), gtk_get_minor_version(), gtk_get_micro_version());
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
init_config(app, gui_data.cli_options.config_path, &gui_data.config_modification_date);
gui_data.screen = gb_screen_new(gui_data.cli_options.force_software_renderer);
gui_data.console = console_window_new();
gui_data.preferences = preferences_window_new(&gb);
gui_data.vram_viewer = vram_viewer_window_new();
gui_data.memory_viewer = GTK_WINDOW(get_object("memory_viewer"));
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));
gtk_box_pack_end(GTK_BOX(gui_data.main_window_container), GTK_WIDGET(gui_data.screen), true, true, 0);
setup_menu(app);
// Insert separators into `GtkComboBox`es
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);
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);
}
G_MODULE_EXPORT void on_quit_activate(GtkWidget *w, gpointer user_data_ptr) {
quit();
}
bool on_change_model(GtkWidget *widget, gpointer user_data) {
GtkCheckMenuItem *check_menu_item = GTK_CHECK_MENU_ITEM(widget);
gchar *model_str = (gchar *) user_data;
if (!gtk_check_menu_item_get_active(check_menu_item)) {
return true;
}
else if (!GB_is_inited(&gb)) {
gui_data.cli_options.model = -1;
config.emulation.model = model_str;
return false;
}
GtkMessageDialog *dialog = GTK_MESSAGE_DIALOG(gtk_message_dialog_new(
GTK_WINDOW(gui_data.main_window),
GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT,
GTK_MESSAGE_QUESTION,
GTK_BUTTONS_YES_NO,
"Changing the emulated model requires a reset.\nChange model and reset game?"
));
stop();
gint result = gtk_dialog_run(GTK_DIALOG(dialog));
switch (result) {
case GTK_RESPONSE_YES:
// Reset the CLI model override
gui_data.cli_options.model = -1;
config.emulation.model = model_str;
reset();
break;
default:
// Action has been canceled
break;
}
run();
gtk_widget_destroy(GTK_WIDGET(dialog));
return result != GTK_RESPONSE_YES;
}
void on_preferences_notify_border(PreferencesWindow *pref, const gchar *name) {
gui_data.border_mode_changed = true;
}
void on_preferences_notify_shader(PreferencesWindow *pref, const gchar *name) {
gb_screen_set_shader(gui_data.screen, name);
}
void on_preferences_notify_sample_rate(PreferencesWindow *pref, const guint *sample_rate) {
if (*sample_rate == -1) {
gui_data.sample_rate = GB_audio_default_sample_rate();
}
else {
gui_data.sample_rate = *sample_rate;
}
init_audio();
}
static void connect_signal_handlers(GApplication *app) {
// Connect signal handlers
gtk_widget_add_events(GTK_WIDGET(gui_data.main_window), GDK_KEY_PRESS_MASK);
gtk_widget_add_events(GTK_WIDGET(gui_data.main_window), GDK_KEY_RELEASE_MASK);
g_signal_connect(gui_data.main_window, "destroy", G_CALLBACK(on_quit_activate), app);
g_signal_connect(gui_data.main_window, "key-press-event", G_CALLBACK(on_key_press), NULL);
g_signal_connect(gui_data.main_window, "key-release-event", G_CALLBACK(on_key_press), NULL);
g_signal_connect(gui_data.main_window, "window-state-event", G_CALLBACK(on_window_state_change), NULL);
// Just hide our sub-windows when closing them
g_signal_connect(gui_data.preferences, "delete-event", G_CALLBACK(gtk_widget_hide_on_delete), NULL);
g_signal_connect(gui_data.vram_viewer, "delete-event", G_CALLBACK(gtk_widget_hide_on_delete), NULL);
g_signal_connect(gui_data.memory_viewer, "delete-event", G_CALLBACK(gtk_widget_hide_on_delete), NULL);
g_signal_connect(gui_data.console, "delete-event", G_CALLBACK(gtk_widget_hide_on_delete), NULL);
g_signal_connect(gui_data.printer, "delete-event", G_CALLBACK(gtk_widget_hide_on_delete), NULL);
g_signal_connect(gui_data.preferences, "pref-update::video-display-border-mode", G_CALLBACK(on_preferences_notify_border), NULL);
g_signal_connect(gui_data.preferences, "pref-update::video-shader", G_CALLBACK(on_preferences_notify_shader), NULL);
g_signal_connect(gui_data.preferences, "pref-update::audio-sample-rate", G_CALLBACK(on_preferences_notify_sample_rate), NULL);
}
// 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);
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));
// Start the emulation thread
run();
}
// This function gets called when the application is closed.
static void shutdown(GApplication *app, GFile **files, gint n_files, const gchar *hint, gpointer null_ptr) {
g_debug("SHUTDOWN");
stop();
SDL_Quit();
GB_free(&gb);
g_object_unref(gui_data.builder);
}
// This function gets called when there are files to open.
// Note: When `open` gets called `activate` wont fire unless we call it ourselves.
static void open(GApplication *app, GFile **files, gint n_files, const gchar *hint, gpointer null_ptr) {
if (n_files > 1) {
g_warning("More than one file specified");
exit(EXIT_FAILURE);
}
gui_data.file = g_file_dup(files[0]);
// We have handled the files, now activate the application
activate(app, NULL);
}
// app.about GAction
// Opens the about dialog
static void activate_about(GSimpleAction *action, GVariant *parameter, gpointer app) {
GObject *dialog = get_object("about_dialog");
gtk_dialog_run(GTK_DIALOG(dialog));
gtk_widget_hide(GTK_WIDGET(dialog));
}
// app.preferences GAction
// Opens the preferences window
static void activate_preferences(GSimpleAction *action, GVariant *parameter, gpointer app) {
gtk_widget_show_all(GTK_WIDGET(gui_data.preferences));
}
// app.show_console GAction
// Opens the console
static void activate_show_console(GSimpleAction *action, GVariant *parameter, gpointer app) {
gtk_widget_show_all(GTK_WIDGET(gui_data.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));
}
// app.clear_console GAction
// Clears the debugger console
static void activate_clear_console(GSimpleAction *action, GVariant *parameter, gpointer app) {
console_clear(gui_data.console);
}
// Closes a ROM
static void close_rom(void) {
stop();
GB_free(&gb);
gb_screen_clear(gui_data.screen);
gb_screen_queue_render(gui_data.screen);
vram_viewer_clear(gui_data.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.quit GAction
// Exits the application
static void activate_quit(GSimpleAction *action, GVariant *parameter, gpointer app) {
quit();
}
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();
}
}
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;
}