SameBoy/gtk3/main.c

1378 lines
46 KiB
C
Raw Normal View History

2020-04-30 01:36:02 +00:00
#define G_LOG_USE_STRUCTURED
#include <gtk/gtk.h>
#include <signal.h>
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
2020-04-30 01:36:02 +00:00
#include <Core/gb.h>
2020-05-16 15:48:29 +00:00
#include "types.h"
2020-05-16 14:56:09 +00:00
#include "config.h"
2020-05-16 15:48:29 +00:00
#include "util.h"
#include "check_menu_radio_group.h"
2020-04-30 01:36:02 +00:00
#include "gb_screen.h"
#include "console_window.h"
#include "preferences_window.h"
#include "vram_viewer_window.h"
2020-04-30 01:36:02 +00:00
// used for audio and game controllers
#include "SDL.h"
#include "../SDL/audio/audio.h"
2020-04-30 01:36:02 +00:00
#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,
2019-10-14 21:46:03 +00:00
.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;
2020-04-30 01:36:02 +00:00
// 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);
2020-04-11 22:38:18 +00:00
static const GActionEntry file_entries[] = {
2019-09-30 14:40:55 +00:00
{ "open", activate_open, NULL, NULL, NULL },
2020-04-11 22:38:18 +00:00
{ "close", activate_close, NULL, NULL, NULL },
};
static const GActionEntry edit_entries[] = {
2021-01-01 16:03:16 +00:00
2020-04-11 22:38:18 +00:00
};
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 },
2019-09-30 14:40:55 +00:00
{ "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 },
2019-09-30 14:40:55 +00:00
};
2020-04-11 22:38:18 +00:00
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";
}
}
2019-09-30 14:40:55 +00:00
// This function gets called after the parsing of the commandline options has occurred.
2020-04-30 01:36:02 +00:00
static gint handle_local_options(GApplication *app, GVariantDict *options, gpointer null_ptr) {
2019-09-30 14:40:55 +00:00
guint32 count;
2019-09-30 14:40:55 +00:00
if (g_variant_dict_lookup(options, "version", "b", &count)) {
2020-04-10 02:07:01 +00:00
g_message("SameBoy v" xstr(VERSION));
2019-09-30 14:40:55 +00:00
return EXIT_SUCCESS;
}
2019-09-30 14:40:55 +00:00
// Handle model override
GVariant *model_name_var = g_variant_dict_lookup_value(options, "model", G_VARIANT_TYPE_STRING);
if (model_name_var != NULL) {
const gchar *model_name = g_variant_get_string(model_name_var, NULL);
2019-09-30 14:40:55 +00:00
// TODO: Synchronize with GB_model_t (Core/gb.h)
if (g_str_has_prefix(model_name, "DMG")) {
if (g_str_has_suffix(model_name, "-B") || g_strcmp0(model_name, "DMG") == 0) {
2020-04-30 01:36:02 +00:00
gui_data.cli_options.model = GB_MODEL_DMG_B;
2019-09-30 14:40:55 +00:00
}
else {
2020-04-30 01:36:02 +00:00
gui_data.cli_options.model = GB_MODEL_DMG_B;
2020-04-10 02:07:01 +00:00
g_warning("Unsupported revision: %s\nFalling back to DMG-B", model_name);
2019-09-30 14:40:55 +00:00
}
}
else if (g_str_has_prefix(model_name, "SGB")) {
if (g_str_has_suffix(model_name, "-NTSC") || g_strcmp0(model_name, "SGB") == 0) {
2020-04-30 01:36:02 +00:00
gui_data.cli_options.model = GB_MODEL_SGB;
2019-09-30 14:40:55 +00:00
}
else if (g_str_has_suffix(model_name, "-PAL")) {
2020-04-30 01:36:02 +00:00
gui_data.cli_options.model = GB_MODEL_SGB | GB_MODEL_PAL_BIT;
2019-09-30 14:40:55 +00:00
}
else if (g_str_has_suffix(model_name, "2")) {
2020-04-30 01:36:02 +00:00
gui_data.cli_options.model = GB_MODEL_SGB2;
2019-09-30 14:40:55 +00:00
}
else {
2020-04-30 01:36:02 +00:00
gui_data.cli_options.model = GB_MODEL_SGB2;
2020-04-10 02:07:01 +00:00
g_warning("Unsupported revision: %s\nFalling back to SGB2", model_name);
2019-09-30 14:40:55 +00:00
}
}
else if (g_str_has_prefix(model_name, "CGB")) {
if (g_str_has_suffix(model_name, "-C")) {
2020-04-30 01:36:02 +00:00
gui_data.cli_options.model = GB_MODEL_CGB_C;
2019-09-30 14:40:55 +00:00
}
else if (g_str_has_suffix(model_name, "-E") || g_strcmp0(model_name, "CGB") == 0) {
2020-04-30 01:36:02 +00:00
gui_data.cli_options.model = GB_MODEL_CGB_E;
2019-09-30 14:40:55 +00:00
}
else {
2020-04-30 01:36:02 +00:00
gui_data.cli_options.model = GB_MODEL_CGB_E;
2020-04-10 02:07:01 +00:00
g_warning("Unsupported revision: %s\nFalling back to CGB-E", model_name);
2019-09-30 14:40:55 +00:00
}
}
else if (g_str_has_prefix(model_name, "AGB")) {
2020-04-30 01:36:02 +00:00
gui_data.cli_options.model = GB_MODEL_AGB;
2019-09-24 18:22:07 +00:00
}
else {
2020-04-10 02:07:01 +00:00
g_warning("Unknown model: %s", model_name);
2019-09-30 14:40:55 +00:00
exit(EXIT_FAILURE);
2019-09-24 18:22:07 +00:00
}
}
2019-09-30 14:40:55 +00:00
return -1;
}
2020-04-30 01:36:02 +00:00
static gboolean init_controllers(void) {
2020-05-02 16:15:57 +00:00
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) {
2020-04-10 02:07:01 +00:00
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) {
2020-04-10 02:07:01 +00:00
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());
2020-05-02 16:15:57 +00:00
// In the “worst” case all joysticks are valid game controllers
gui_data.controllers = g_malloc0(sizeof(struct Controller_t) * SDL_NumJoysticks());
2020-05-02 16:15:57 +00:00
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);
2020-05-02 16:15:57 +00:00
if (s->controller) {
SDL_Joystick *joystick = SDL_GameControllerGetJoystick(s->controller);
SDL_JoystickPowerLevel power_level = SDL_JoystickCurrentPowerLevel(joystick);
2020-05-02 16:15:57 +00:00
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());
}
2021-01-01 16:03:16 +00:00
SDL_HapticClose(s->haptic);
s->haptic = NULL;
}
}
2020-05-02 16:15:57 +00:00
// 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 {
2020-05-02 16:15:57 +00:00
g_warning("Could not open controller %i: %s", i, SDL_GetError());
}
}
}
return true;
}
2020-04-30 01:36:02 +00:00
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());
2021-01-01 16:03:16 +00:00
// 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);
2020-04-11 16:24:55 +00:00
static const char *const model_names[] = {
"Game Boy",
"Super Game Boy",
"Game Boy Color",
"Game Boy Advance",
NULL
};
2020-04-11 16:24:55 +00:00
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);
}
2020-04-11 22:38:18 +00:00
// 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);
2020-04-11 22:38:18 +00:00
}
}
2020-04-30 01:36:02 +00:00
static void stop(void) {
if (!gui_data.running) return;
2020-04-30 01:36:02 +00:00
GB_audio_set_paused(true);
GB_debugger_set_disabled(&gb, true);
2020-04-30 01:36:02 +00:00
if (GB_debugger_is_stopped(&gb)) {
// [self interruptDebugInputRead];
}
gui_data.stopping = true;
gui_data.running = false;
while (gui_data.stopping);
2020-04-30 01:36:02 +00:00
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;
2020-04-11 22:38:18 +00:00
}
2020-04-30 01:36:02 +00:00
static void vblank(GB_gameboy_t *gb) {
gb_screen_flip(gui_data.screen);
2020-04-11 22:38:18 +00:00
if (gui_data.border_mode_changed) {
2020-05-16 14:56:09 +00:00
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));
2020-04-11 22:38:18 +00:00
gui_data.border_mode_changed = false;
2020-04-30 01:36:02 +00:00
}
2020-04-11 22:38:18 +00:00
GB_set_pixels_output(gb, gb_screen_get_pixels(gui_data.screen));
2020-04-11 22:38:18 +00:00
// 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);
2020-04-30 01:36:02 +00:00
}
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);
2020-04-11 22:38:18 +00:00
}
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;
}
}
2020-04-11 22:38:18 +00:00
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);
}
2020-04-30 01:36:02 +00:00
static void handle_events(GB_gameboy_t *gb) {
SDL_GameControllerUpdate();
2019-09-30 14:40:55 +00:00
2020-04-30 01:36:02 +00:00
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];
2020-05-04 01:30:10 +00:00
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);
2019-09-30 14:40:55 +00:00
2020-05-04 01:30:10 +00:00
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) {
2020-05-04 01:30:10 +00:00
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;
2020-05-04 01:30:10 +00:00
}
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;
2020-05-04 01:30:10 +00:00
}
}
if (left_x_axis >= JOYSTICK_HIGH || right_x_axis >= JOYSTICK_HIGH) {
gui_data.last_used_controller = s;
2020-04-30 01:36:02 +00:00
controller_state |= BUTTON_MASK_RIGHT;
}
2020-05-04 01:30:10 +00:00
else if (left_x_axis <= -JOYSTICK_HIGH || right_x_axis <= -JOYSTICK_HIGH) {
gui_data.last_used_controller = s;
2020-04-30 01:36:02 +00:00
controller_state |= BUTTON_MASK_LEFT;
}
2020-05-04 01:30:10 +00:00
if (left_y_axis >= JOYSTICK_HIGH || right_y_axis >= JOYSTICK_HIGH) {
gui_data.last_used_controller = s;
2020-04-30 01:36:02 +00:00
controller_state |= BUTTON_MASK_DOWN;
}
2020-05-04 01:30:10 +00:00
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;
2020-04-30 01:36:02 +00:00
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;
}
2021-01-01 16:03:16 +00:00
if (SDL_GameControllerGetButton(s->controller, SDL_CONTROLLER_BUTTON_B)) {
gui_data.last_used_controller = s;
controller_state |= BUTTON_MASK_B;
}
2021-01-01 16:03:16 +00:00
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;
}
2020-04-30 01:36:02 +00:00
}
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));
2020-04-30 01:36:02 +00:00
}
2020-04-30 01:36:02 +00:00
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;
2020-04-30 01:36:02 +00:00
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",
};
2020-04-30 01:36:02 +00:00
const char *const boot_rom_name = names[type];
2020-04-30 01:36:02 +00:00
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;
}
2019-09-30 14:40:55 +00:00
}
2020-04-30 01:36:02 +00:00
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);
2020-04-30 01:36:02 +00:00
g_message("Trying to load boot ROM from %s", boot_rom_path);
2020-04-30 01:36:02 +00:00
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;
}
2019-09-30 14:40:55 +00:00
2020-04-30 01:36:02 +00:00
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);
2020-04-30 01:36:02 +00:00
g_message("Loading internal boot ROM: %s", boot_rom_path);
g_free(boot_rom_path);
2020-04-30 01:36:02 +00:00
if (boot_rom_f == NULL) {
g_warning("Failed to load internal boot ROM: %s", boot_rom_path);
g_error_free(error);
exit(EXIT_FAILURE);
}
2020-04-30 01:36:02 +00:00
boot_rom_data = g_bytes_get_data(boot_rom_f, &boot_rom_size);
GB_load_boot_rom_from_buffer(gb, boot_rom_data, boot_rom_size);
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);
}
2020-04-30 01:36:02 +00:00
static void init(void) {
if (GB_is_inited(&gb)) return;
2020-05-16 15:48:29 +00:00
GB_init(&gb, config_get_model_type(&gui_data));
2019-09-27 21:10:28 +00:00
2020-04-30 01:36:02 +00:00
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));
2020-05-16 14:56:09 +00:00
GB_set_color_correction_mode(&gb, config_get_color_correction_mode());
2021-01-01 17:12:32 +00:00
GB_set_light_temperature(&gb, (double) config.video.light_temperature / 256.0);
if (config_get_display_border_mode() <= GB_BORDER_ALWAYS) {
GB_set_border_mode(&gb, config_get_display_border_mode());
}
2020-04-30 01:36:02 +00:00
GB_apu_set_sample_callback(&gb, gb_audio_callback);
2021-01-01 16:03:16 +00:00
GB_set_sample_rate(&gb, GB_audio_get_sample_rate());
GB_set_highpass_filter_mode(&gb, config_get_highpass_mode());
2021-01-01 17:12:32 +00:00
GB_set_interference_volume(&gb, (double) config.audio.interference_volume / 256.0);
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);
2020-04-30 01:36:02 +00:00
}
2020-04-30 01:36:02 +00:00
static void reset(void) {
2020-05-16 15:48:29 +00:00
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);
2020-04-30 01:36:02 +00:00
if (gui_data.prev_model == -1 || gui_data.prev_model == current_model) {
GB_reset(&gb);
2019-09-27 21:10:28 +00:00
}
2020-04-11 22:38:18 +00:00
else {
2020-04-30 01:36:02 +00:00
GB_switch_model_and_reset(&gb, current_model);
2020-04-11 22:38:18 +00:00
}
2019-09-30 14:40:55 +00:00
2020-05-16 14:56:09 +00:00
GB_set_palette(&gb, config_get_monochrome_palette());
2019-09-27 21:10:28 +00:00
2020-05-16 15:48:29 +00:00
gui_data.prev_model = config_get_model_type(&gui_data);
2020-04-11 22:38:18 +00:00
2020-04-30 01:36:02 +00:00
// 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));
2020-04-30 01:36:02 +00:00
}
2020-04-11 22:38:18 +00:00
2020-04-30 01:36:02 +00:00
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) {
2020-04-30 01:36:02 +00:00
success = true;
}
else {
g_warning("Failed to load ROM: %s", path);
}
2020-04-11 22:38:18 +00:00
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);
2020-04-30 01:36:02 +00:00
g_free(path);
}
action_set_enabled(gui_data.main_application, "close", success);
2020-04-30 01:36:02 +00:00
action_entries_set_enabled(emulation_entries, G_N_ELEMENTS(emulation_entries), success);
}
2020-04-30 01:36:02 +00:00
static void start(void) {
gui_data.running = true;
2020-04-30 01:36:02 +00:00
gui_data.stopped = false;
GB_audio_clear_queue();
GB_audio_set_paused(config.audio.muted);
2020-04-30 01:36:02 +00:00
/* Run emulation */
while (gui_data.running) {
if (gui_data.rewind_paused) {
2020-04-30 01:36:02 +00:00
handle_events(&gb);
g_usleep(G_USEC_PER_SEC / 8);
}
else {
if (gui_data.do_rewind) {
2020-04-30 01:36:02 +00:00
GB_rewind_pop(&gb);
if (gui_data.turbo_down) {
2020-04-30 01:36:02 +00:00
GB_rewind_pop(&gb);
}
if (!GB_rewind_pop(&gb)) {
gui_data.rewind_paused = true;
2020-04-30 01:36:02 +00:00
}
gui_data.do_rewind = false;
2020-04-30 01:36:02 +00:00
}
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;
2019-09-30 14:40:55 +00:00
}
2020-04-30 01:36:02 +00:00
// Prevent dependency loop
static void run(void);
// app.reset GAction
// Resets the emulation
static void activate_reset(GSimpleAction *action, GVariant *parameter, gpointer app) {
2020-04-11 22:38:18 +00:00
if (!GB_is_inited(&gb)) {
2020-04-30 01:36:02 +00:00
init();
}
2020-04-11 22:38:18 +00:00
2020-04-30 01:36:02 +00:00
stop();
reset();
run();
}
2020-04-30 01:36:02 +00:00
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);
2021-01-01 16:03:16 +00:00
/* 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();
}
2020-04-30 01:36:02 +00:00
if (gui_data.stopped) {
start();
}
2020-04-30 01:36:02 +00:00
else {
reset();
start();
}
2020-04-30 01:36:02 +00:00
return NULL;
}
2020-04-30 01:36:02 +00:00
static void run(void) {
if (gui_data.running) return;
while (gui_data.stopping);
2020-04-30 01:36:02 +00:00
g_thread_new("CoreLoop", run_thread, NULL);
}
2020-04-30 01:36:02 +00:00
// Tell our application to quit.
// After this functions has been called the `shutdown` signal will be issued.
//
// TODO: Make sure we have a way to quit our emulation loop before `shutdown` gets called
static void quit(void) {
g_debug("quit(void);");
2020-04-30 01:36:02 +00:00
stop();
GtkWindow *window = gui_data.main_window ? GTK_WINDOW(gui_data.main_window) : NULL;
2020-05-16 14:56:09 +00:00
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);
}
2020-04-30 01:36:02 +00:00
// Quit our application properly.
// This fires the “shutdown” signal.
g_application_quit(G_APPLICATION(gui_data.main_application));
}
2020-04-30 01:36:02 +00:00
static void quit_interrupt(int ignored) {
g_debug("quit_interrupt(%d);", ignored);
2020-04-11 22:38:18 +00:00
quit();
2019-09-30 14:40:55 +00:00
}
2020-04-30 01:36:02 +00:00
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);
2020-04-30 01:36:02 +00:00
action_set_enabled(app, "close", false);
action_entries_set_enabled(emulation_entries, G_N_ELEMENTS(emulation_entries), false);
}
2020-04-30 01:36:02 +00:00
static gboolean on_key_press(GtkWidget *w, GdkEventKey *event, gpointer data) {
uint8_t mask;
2021-01-01 16:03:16 +00:00
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));
2020-04-30 01:36:02 +00:00
}
else {
gtk_window_fullscreen(GTK_WINDOW(gui_data.main_window));
2020-04-30 01:36:02 +00:00
}
}
2020-04-30 01:36:02 +00:00
}
2021-01-01 16:03:16 +00:00
2020-04-30 01:36:02 +00:00
if (event->type == GDK_KEY_PRESS) {
gui_data.pressed_buttons |= mask;
2020-04-30 01:36:02 +00:00
}
else if (event->type == GDK_KEY_RELEASE) {
gui_data.pressed_buttons &= ~mask;
2020-04-30 01:36:02 +00:00
}
return false;
2019-09-30 14:40:55 +00:00
}
2020-04-11 16:24:55 +00:00
2020-04-30 01:36:02 +00:00
static void on_window_state_change(GtkWidget *w, GdkEventWindowState *event, gpointer data) {
gui_data.is_fullscreen = event->new_window_state & GDK_WINDOW_STATE_FULLSCREEN;
2019-09-30 14:40:55 +00:00
}
2020-04-11 16:24:55 +00:00
2020-04-30 01:36:02 +00:00
// 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);
2020-04-30 01:36:02 +00:00
create_action_groups(app);
#if NDEBUG
// Disable when not compiled in debug mode
action_set_enabled(app, "open_gtk_debugger", false);
#endif
2020-04-11 16:24:55 +00:00
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) {
2020-04-30 01:36:02 +00:00
gui_data.sample_rate = GB_audio_default_sample_rate();
}
else {
gui_data.sample_rate = config.audio.sample_rate;
2020-04-30 01:36:02 +00:00
}
2020-04-10 20:19:40 +00:00
2020-04-30 01:36:02 +00:00
// 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));
2020-04-10 20:19:40 +00:00
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);
2020-04-11 16:24:55 +00:00
2020-04-30 01:36:02 +00:00
setup_menu(app);
2020-04-11 16:24:55 +00:00
2020-04-30 01:36:02 +00:00
// Insert separators into `GtkComboBox`es
set_combo_box_row_separator_func(GTK_CONTAINER(gui_data.memory_viewer));
2020-04-30 01:36:02 +00:00
// Define a set of window icons
GList *icon_list = NULL;
static char* icons[] = {
RESOURCE_PREFIX "logo_256.png",
RESOURCE_PREFIX "logo_128.png",
RESOURCE_PREFIX "logo_64.png",
RESOURCE_PREFIX "logo_48.png",
RESOURCE_PREFIX "logo_32.png",
RESOURCE_PREFIX "logo_16.png"
};
2020-04-30 01:36:02 +00:00
// Create list of GdkPixbufs
for (int i = 0; i < (sizeof(icons) / sizeof(const char*)); ++i) {
GdkPixbuf *icon = gdk_pixbuf_new_from_resource(icons[i], NULL);
if (!icon) continue;
2020-04-11 16:24:55 +00:00
2020-04-30 01:36:02 +00:00
icon_list = g_list_prepend(icon_list, icon);
2020-04-11 16:24:55 +00:00
}
2020-04-30 01:36:02 +00:00
// Let GTK choose the proper icon
gtk_window_set_default_icon_list(icon_list);
2020-04-11 16:24:55 +00:00
2020-04-30 01:36:02 +00:00
// 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);
2019-09-30 14:40:55 +00:00
}
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);
}
2021-01-01 17:12:32 +00:00
void on_preferences_notify_light_temperature(PreferencesWindow *pref, const gint *light_temperature) {
if (GB_is_inited(&gb)) {
// wouldnt it be nice to use the value set in the GtkAdjustment of the slider instead of 256.0 here?
GB_set_light_temperature(&gb, (double) *light_temperature / 256.0);
}
}
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();
}
2021-01-01 16:03:16 +00:00
void on_preferences_notify_interference_volume(PreferencesWindow *pref, const guint *interference_volume) {
if (GB_is_inited(&gb)) {
2021-01-01 17:12:32 +00:00
// wouldnt it be nice to use the value set in the GtkAdjustment of the slider instead of 256.0 here?
GB_set_interference_volume(&gb, (double) *interference_volume / 256.0);
2021-01-01 16:03:16 +00:00
}
}
2020-04-30 01:36:02 +00:00
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);
2019-10-14 21:46:03 +00:00
2020-04-30 01:36:02 +00:00
// 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);
2021-01-01 17:12:32 +00:00
g_signal_connect(gui_data.preferences, "pref-update::video-color-temperature", G_CALLBACK(on_preferences_notify_light_temperature), NULL);
g_signal_connect(gui_data.preferences, "pref-update::audio-sample-rate", G_CALLBACK(on_preferences_notify_sample_rate), NULL);
2021-01-01 16:03:16 +00:00
g_signal_connect(gui_data.preferences, "pref-update::audio-interference-volume", G_CALLBACK(on_preferences_notify_interference_volume), NULL);
2019-09-30 14:40:55 +00:00
}
2020-04-30 01:36:02 +00:00
// This function gets called when the GApplication gets activated, i.e. it is ready to show widgets.
static void activate(GApplication *app, gpointer null_ptr) {
init_audio();
init_controllers();
2019-10-04 22:57:03 +00:00
2020-04-30 01:36:02 +00:00
connect_signal_handlers(app);
2019-10-04 22:57:03 +00:00
2020-04-30 01:36:02 +00:00
if (gui_data.cli_options.fullscreen) {
gtk_window_fullscreen(GTK_WINDOW(gui_data.main_window));
2020-04-30 01:36:02 +00:00
}
gtk_application_add_window(GTK_APPLICATION(app), GTK_WINDOW(gui_data.main_window));
gtk_widget_show_all(GTK_WIDGET(gui_data.main_window));
2020-04-30 01:36:02 +00:00
// Start the emulation thread
run();
}
2020-04-30 01:36:02 +00:00
// This function gets called when the application is closed.
static void shutdown(GApplication *app, GFile **files, gint n_files, const gchar *hint, gpointer null_ptr) {
g_debug("SHUTDOWN");
stop();
SDL_Quit();
GB_free(&gb);
2021-01-01 16:03:16 +00:00
g_object_unref(gui_data.builder);
2020-04-30 01:36:02 +00:00
}
// This function gets called when there are files to open.
// Note: When `open` gets called `activate` wont fire unless we call it ourselves.
static void open(GApplication *app, GFile **files, gint n_files, const gchar *hint, gpointer null_ptr) {
if (n_files > 1) {
g_warning("More than one file specified");
exit(EXIT_FAILURE);
}
2020-04-30 01:36:02 +00:00
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));
}
2020-04-30 01:36:02 +00:00
// 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));
2019-09-30 14:40:55 +00:00
}
2020-04-30 01:36:02 +00:00
// app.open_gtk_debugger GAction
// Opens the GTK debugger
static void activate_open_gtk_debugger(GSimpleAction *action, GVariant *parameter, gpointer app) {
gtk_window_set_interactive_debugging(true);
}
2020-04-30 01:36:02 +00:00
// app.open_memory_viewer GAction
// Opens the memory viewer window
static void activate_open_memory_viewer(GSimpleAction *action, GVariant *parameter, gpointer app) {
gtk_widget_show_all(GTK_WIDGET(gui_data.memory_viewer));
2020-04-30 01:36:02 +00:00
}
2020-04-10 20:19:40 +00:00
2020-04-30 01:36:02 +00:00
// app.open_vram_viewer GAction
// Opens the VRAM viewer window
static void activate_open_vram_viewer(GSimpleAction *action, GVariant *parameter, gpointer app) {
gtk_widget_show_all(GTK_WIDGET(gui_data.vram_viewer));
2020-04-30 01:36:02 +00:00
}
// app.clear_console GAction
// Clears the debugger console
static void activate_clear_console(GSimpleAction *action, GVariant *parameter, gpointer app) {
console_clear(gui_data.console);
}
2020-04-30 01:36:02 +00:00
// Closes a ROM
static void close_rom(void) {
2020-04-30 01:36:02 +00:00
stop();
GB_free(&gb);
2021-01-01 16:03:16 +00:00
gb_screen_clear(gui_data.screen);
gb_screen_queue_render(gui_data.screen);
2020-05-17 21:13:59 +00:00
vram_viewer_clear(gui_data.vram_viewer);
gtk_widget_queue_draw(GTK_WIDGET(gui_data.vram_viewer));
2020-04-30 01:36:02 +00:00
// Update menu action states
action_set_enabled(gui_data.main_application, "close", false);
2020-04-30 01:36:02 +00:00
action_entries_set_enabled(emulation_entries, G_N_ELEMENTS(emulation_entries), false);
2021-01-01 16:03:16 +00:00
// 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();
2020-04-30 01:36:02 +00:00
}
2020-04-30 01:36:02 +00:00
// app.quit GAction
// Exits the application
static void activate_quit(GSimpleAction *action, GVariant *parameter, gpointer app) {
quit();
}
2020-04-30 01:36:02 +00:00
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);
2020-04-30 01:36:02 +00:00
g_simple_action_set_state(action, value);
}
2020-04-30 01:36:02 +00:00
static void on_pause_changed(GSimpleAction *action, GVariant *value, gpointer user_data_ptr) {
if (g_variant_get_boolean(value)) {
stop();
}
else {
2020-04-30 01:36:02 +00:00
run();
}
2020-04-30 01:36:02 +00:00
g_simple_action_set_state(action, value);
}
2020-04-11 22:38:18 +00:00
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();
}
}
2020-04-30 01:36:02 +00:00
int main(int argc, char *argv[]) {
gui_data.main_thread = g_thread_self();
2020-04-30 01:36:02 +00:00
// 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);
2020-04-30 01:36:02 +00:00
// Define our command line parameters
GOptionEntry entries[] = {
{ "version", 'v', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, NULL, "Show the application version", NULL },
2020-05-05 19:38:56 +00:00
{ "fullscreen", 'f', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &gui_data.cli_options.fullscreen, "Start in fullscreen mode", NULL },
2020-04-30 01:36:02 +00:00
{ "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>" },
2020-05-05 19:38:56 +00:00
{ "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 },
2020-04-30 01:36:02 +00:00
{ 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.");
2020-04-30 01:36:02 +00:00
// 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);
2020-04-30 01:36:02 +00:00
// 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);
2020-04-30 01:36:02 +00:00
return status;
}