SameBoy/gtk3/widgets/main_window.c

583 lines
20 KiB
C
Raw Permalink 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.

#include "main_window.h"
#include <Core/gb.h>
#include "../../SDL/audio/audio.h"
#include "../config.h"
#include "gb_screen.h"
#include "main_menu.h"
#include "console_window.h"
#include "printer_window.h"
#include "preferences_window.h"
#include "vram_viewer_window.h"
struct _MainWindow {
GtkApplicationWindowClass parent_class;
// Child nodes
GtkBox *container;
GbScreen *screen;
MainMenu *main_menu;
bool force_software_renderer;
// The local SameBoy core instance
GB_gameboy_t *gb;
unsigned sample_rate;
bool running; // Indicates that the emulation thread is running
bool stopping; // Indicates that the emulation thread is about to be stopped
GFile *file;
char *battery_save_path;
char *cheats_save_path;
// Fast forward / slow motion
bool underclock_down;
bool rewind_down;
bool do_rewind;
bool rewind_paused;
bool turbo_down;
double clock_mutliplier;
double analog_clock_multiplier;
bool analog_clock_multiplier_valid;
// Local sub-windows
ConsoleWindow *console;
GtkWindow *memory_viewer; // TODO
PrinterWindow *printer;
VramViewerWindow *vram_viewer;
};
G_DEFINE_TYPE(MainWindow, main_window, GTK_TYPE_APPLICATION_WINDOW);
typedef enum {
PROP_FORCE_SOFTWARE_RENDERER = 1,
N_PROPERTIES
} MainWindowProperty;
static GParamSpec *obj_properties[N_PROPERTIES] = { NULL, };
#define perform_atomic(self, block) { \
while (!GB_is_inited(self->gb)); \
bool was_running = self->running && !GB_debugger_is_stopped(self->gb); \
if (was_running) { main_window_stop(self); } \
block; \
if (was_running) { main_window_start(self); } \
}
static void main_window_set_property(GObject *object, guint property_id, const GValue *value, GParamSpec *pspec) {
MainWindow *self = (MainWindow *) object;
switch ((MainWindowProperty) property_id) {
case PROP_FORCE_SOFTWARE_RENDERER: self->force_software_renderer = g_value_get_boolean(value); break;
default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
}
}
static void main_window_get_property(GObject *object, guint property_id, GValue *value, GParamSpec *pspec) {
MainWindow *self = (MainWindow *) object;
switch ((MainWindowProperty) property_id) {
case PROP_FORCE_SOFTWARE_RENDERER: g_value_set_boolean(value, self->force_software_renderer); break;
default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
}
}
static void on_update_color_correction(PreferencesWindow *pref, const GB_color_correction_mode_t mode, MainWindow *self) {
g_debug("on_update_color_correction(%d)", mode);
}
static void on_update_video_color_temperature(PreferencesWindow *pref, const gint light_temperature, MainWindow *self) {
g_debug("on_update_video_color_temperature(%d)", light_temperature);
if (GB_is_inited(self->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(self->gb, (double) light_temperature / 256.0);
}
}
static void on_update_monochrome_palette(PreferencesWindow *pref, const GB_palette_t *palette, MainWindow *self) {
g_debug(
"on_update_monochrome_palette(\n\trgb(%d, %d, %d),\n\trgb(%d, %d, %d),\n\trgb(%d, %d, %d),\n\trgb(%d, %d, %d),\n\trgb(%d, %d, %d)\n)",
palette->colors[0].r, palette->colors[0].g, palette->colors[0].b,
palette->colors[1].r, palette->colors[1].g, palette->colors[1].b,
palette->colors[2].r, palette->colors[2].g, palette->colors[2].b,
palette->colors[3].r, palette->colors[3].g, palette->colors[3].b,
palette->colors[4].r, palette->colors[4].g, palette->colors[4].b
);
}
static void on_update_highpass(PreferencesWindow *pref, const GB_highpass_mode_t mode, MainWindow *self) {
g_debug("on_update_highpass(%d)", mode);
}
static void on_update_rewind_duration(PreferencesWindow *pref, const guint rewind_duration, MainWindow *self) {
g_debug("on_update_rewind_duration(%d)", rewind_duration);
}
static void on_update_rumble_mode(PreferencesWindow *pref, const GB_rumble_mode_t mode, MainWindow *self) {
g_debug("on_update_rumble_mode(%d)", mode);
}
static void on_update_video_display_border_mode(PreferencesWindow *pref, const gchar *name, MainWindow *self) {
g_debug("on_update_video_display_border_mode(%s)", name);
// self->border_mode_changed = true;
}
static void on_update_video_shader(PreferencesWindow *pref, const gchar *name, MainWindow *self) {
g_debug("on_update_video_shader(%s)", name);
main_window_set_shader(self, name);
}
static void on_update_audio_sample_rate(PreferencesWindow *pref, const guint sample_rate, MainWindow *self) {
g_debug("on_update_audio_sample_rate(%d)", sample_rate);
if (sample_rate == -1) {
self->sample_rate = GB_audio_default_sample_rate();
}
else {
self->sample_rate = sample_rate;
}
// init_audio();
}
static void on_update_audio_interference_volume(PreferencesWindow *pref, const guint *interference_volume, MainWindow *self) {
g_debug("on_update_audio_interference_volume(%d)", *interference_volume);
if (GB_is_inited(self->gb)) {
// wouldnt it be nice to use the value set in the GtkAdjustment of the slider instead of 100.0 here?
GB_set_interference_volume(self->gb, (double) *interference_volume / 100.0);
}
}
static bool on_change_model(GtkWidget *parent, GtkWidget *widget, gpointer user_data) {
MainWindow *self = SAMEBOY_MAIN_WINDOW(parent);
g_debug("on_change_model");
return false;
}
struct PrinterCallbackData {
PrinterWindow *printer_window;
uint32_t *image;
uint8_t height;
uint8_t top_margin;
uint8_t bottom_margin;
uint8_t exposure;
};
static gboolean draw_printer_image(struct PrinterCallbackData *data) {
printer_window_update(data->printer_window, data->image, data->height, data->top_margin, data->bottom_margin, data->exposure);
g_free(data->image);
g_slice_free(struct PrinterCallbackData, data);
return false;
}
static void emu_thread_on_print_image(GB_gameboy_t *gb, uint32_t *image, uint8_t height, uint8_t top_margin, uint8_t bottom_margin, uint8_t exposure) {
MainWindow *self = SAMEBOY_MAIN_WINDOW(GB_get_user_data(gb));
struct PrinterCallbackData *data = g_slice_alloc(sizeof(struct PrinterCallbackData));
data->printer_window = self->printer;
data->image = g_malloc0(160 * height * sizeof(image[0]));
memcpy(data->image, image, 160 * height * sizeof(image[0]));
data->height = height;
data->top_margin = top_margin;
data->bottom_margin = bottom_margin;
data->exposure = exposure;
// We must make sure to run this function on the GTK thread
g_idle_add((GSourceFunc) draw_printer_image, data);
}
static bool on_change_linked_device(GtkWidget *parent, GtkWidget *widget, gpointer user_data) {
MainWindow *self = SAMEBOY_MAIN_WINDOW(parent);
g_debug("on_change_linked_device");
GtkCheckMenuItem *check_menu_item = GTK_CHECK_MENU_ITEM(widget);
gchar *device_id = (gchar *) user_data;
if (!gtk_check_menu_item_get_active(check_menu_item)) {
return true;
}
else if (!GB_is_inited(self->gb)) {
return false;
}
perform_atomic(self, {
if (g_strcmp0(device_id, "NONE") == 0) {
g_debug("Disconnecting serial device");
GB_disconnect_serial(self->gb);
}
else if (g_strcmp0(device_id, "PRINTER") == 0) {
g_debug("Connecting printer");
GB_connect_printer(self->gb, emu_thread_on_print_image);
}
else if (g_strcmp0(device_id, "WORKBOY") == 0) {
g_debug("Connecting WorkBoy");
g_warning("Not yet implemented!")
GB_disconnect_serial(self->gb);
}
});
return false;
}
static void emu_thread_on_load_boot_rom(GB_gameboy_t *gb, GB_boot_rom_t type) {
g_debug("on_load_boot_rom(%p, %d)", gb, type);
MainWindow *self = SAMEBOY_MAIN_WINDOW(GB_get_user_data(gb));
SameBoyApplication *app = SAMEBOY_APPLICATION(gtk_window_get_application(GTK_WINDOW(self)));
struct CliOptionData *cli_options = sameboy_application_get_cli_options(app);
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 (cli_options->boot_rom_path != NULL) {
g_message("[CLI override] Trying to load boot ROM from %s", cli_options->boot_rom_path);
if (GB_load_boot_rom(gb, 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);
if (boot_rom_f == NULL) {
g_warning("Failed to load internal boot ROM: %s", boot_rom_path);
g_error_free(error);
// exit(EXIT_FAILURE);
}
else {
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);
}
g_free(boot_rom_path);
}
}
}
static void main_window_init_core(MainWindow *self) {
if (GB_is_inited(self->gb)) return;
SameBoyApplication *app = SAMEBOY_APPLICATION(gtk_window_get_application(GTK_WINDOW(self)));
struct CliOptionData *cli_options = sameboy_application_get_cli_options(app);
GB_init(self->gb, config_get_model_type(cli_options));
GB_set_user_data(self->gb, self);
if (config.audio.sample_rate == -1) {
self->sample_rate = GB_audio_default_sample_rate();
}
else {
self->sample_rate = config.audio.sample_rate;
}
// GB_set_vblank_callback(self->gb, emu_thread_on_vblank);
// GB_set_rgb_encode_callback(self->gb, emu_thread_rgb_encode);
GB_set_pixels_output(self->gb, main_window_get_current_buffer(self));
GB_set_color_correction_mode(self->gb, config_get_color_correction_mode());
GB_set_light_temperature(self->gb, (double) config.video.light_temperature / 256.0);
if (config_get_display_border_mode() <= GB_BORDER_ALWAYS) {
GB_set_border_mode(self->gb, config_get_display_border_mode());
}
// GB_apu_set_sample_callback(self->gb, emu_thread_audio_callback);
GB_set_sample_rate(self->gb, GB_audio_get_sample_rate());
GB_set_highpass_filter_mode(self->gb, config_get_highpass_mode());
GB_set_interference_volume(self->gb, (double) config.audio.interference_volume / 100.0);
// GB_set_log_callback(self->gb, emu_thread_console_log);
// GB_set_input_callback(self->gb, emu_thread_get_sync_input);
// GB_set_async_input_callback(self->gb, emu_thread_get_async_input);
GB_set_boot_rom_load_callback(self->gb, emu_thread_on_load_boot_rom);
// GB_set_update_input_hint_callback(self->gb, emu_thread_handle_events);
// GB_set_rumble_callback(self->gb, emu_thread_rumble_callback);
GB_set_rumble_mode(self->gb, config_get_rumble_mode());
GB_set_rewind_length(self->gb, config.emulation.rewind_duration);
}
static void on_application_set(MainWindow *self, GObject *object) {
SameBoyApplication *app = SAMEBOY_APPLICATION(gtk_window_get_application(GTK_WINDOW(self)));
g_debug("on_application_set(%p, %p) => %p", self, object, app);
sameboy_application_preferences_signal_connect(app, "pref-update::color-correction", G_CALLBACK(on_update_color_correction), self);
sameboy_application_preferences_signal_connect(app, "pref-update::video-color-temperature", G_CALLBACK(on_update_video_color_temperature), self);
sameboy_application_preferences_signal_connect(app, "pref-update::monochrome-palette", G_CALLBACK(on_update_monochrome_palette), self);
sameboy_application_preferences_signal_connect(app, "pref-update::highpass", G_CALLBACK(on_update_highpass), self);
sameboy_application_preferences_signal_connect(app, "pref-update::rewind-duration", G_CALLBACK(on_update_rewind_duration), self);
sameboy_application_preferences_signal_connect(app, "pref-update::rumble-mode", G_CALLBACK(on_update_rumble_mode), self);
sameboy_application_preferences_signal_connect(app, "pref-update::video-display-border-mode", G_CALLBACK(on_update_video_display_border_mode), self);
sameboy_application_preferences_signal_connect(app, "pref-update::video-shader", G_CALLBACK(on_update_video_shader), self);
sameboy_application_preferences_signal_connect(app, "pref-update::audio-sample-rate", G_CALLBACK(on_update_audio_sample_rate), self);
sameboy_application_preferences_signal_connect(app, "pref-update::audio-interference-volume", G_CALLBACK(on_update_audio_interference_volume), self);
main_window_init_core(self);
}
static void main_window_constructed(GObject *object) {
G_OBJECT_CLASS(main_window_parent_class)->constructed(object);
MainWindow *self = (MainWindow *) object;
self->screen = gb_screen_new(self->force_software_renderer);
gtk_box_pack_end(GTK_BOX(self->container), GTK_WIDGET(self->screen), true, true, 0);
g_signal_connect(self, "notify::application", G_CALLBACK(on_application_set), self);
}
static void main_window_init(MainWindow *self) {
gtk_widget_init_template(GTK_WIDGET(self));
gtk_window_set_title(GTK_WINDOW(self), "SameBoy");
gtk_application_window_set_show_menubar(GTK_APPLICATION_WINDOW(self), false);
// Connect signal handlers
gtk_widget_add_events(GTK_WIDGET(self), GDK_KEY_PRESS_MASK);
gtk_widget_add_events(GTK_WIDGET(self), GDK_KEY_RELEASE_MASK);
GtkAccelGroup *accelGroup = gtk_accel_group_new();
gtk_window_add_accel_group(GTK_WINDOW(self), accelGroup);
gtk_widget_add_accelerator(GTK_WIDGET(self), "break-debugger-keyboard", accelGroup, GDK_KEY_C, GDK_CONTROL_MASK, GTK_ACCEL_VISIBLE);
self->gb = g_malloc(sizeof(GB_gameboy_t));
if (self->gb == NULL) {
g_warning("Out of memory!");
// TODO: Try to stop gracefully
exit(EXIT_FAILURE);
}
self->console = console_window_new(self->gb);
gtk_window_set_attached_to(GTK_WINDOW(self->console), GTK_WIDGET(self));
self->vram_viewer = vram_viewer_window_new();
gtk_window_set_attached_to(GTK_WINDOW(self->vram_viewer), GTK_WIDGET(self));
self->printer = printer_window_new();
gtk_window_set_attached_to(GTK_WINDOW(self->printer), GTK_WIDGET(self));
// Just hide our sub-windows when closing them
g_signal_connect(self->console, "delete-event", G_CALLBACK(gtk_widget_hide_on_delete), NULL);
// g_signal_connect(self->memory_viewer, "delete-event", G_CALLBACK(gtk_widget_hide_on_delete), NULL);
g_signal_connect(self->printer, "delete-event", G_CALLBACK(gtk_widget_hide_on_delete), NULL);
g_signal_connect(self->vram_viewer, "delete-event", G_CALLBACK(gtk_widget_hide_on_delete), NULL);
}
static void main_window_class_init(MainWindowClass *class) {
gtk_widget_class_set_template_from_resource(GTK_WIDGET_CLASS(class), RESOURCE_PREFIX "ui/main_window.ui");
gtk_widget_class_bind_template_child(GTK_WIDGET_CLASS(class), MainWindow, container);
gtk_widget_class_bind_template_child(GTK_WIDGET_CLASS(class), MainWindow, main_menu);
g_signal_new(
"break-debugger-keyboard", // signal name
G_TYPE_FROM_CLASS(class), // itype
G_SIGNAL_RUN_FIRST | G_SIGNAL_NO_RECURSE | G_SIGNAL_ACTION, // signal_flags
0, // class_offset
NULL, // accumulator
NULL, // accumulator_data
NULL, // c_marshaller,
G_TYPE_NONE, // return_type
0 // n_params
);
obj_properties[PROP_FORCE_SOFTWARE_RENDERER] = g_param_spec_boolean(
"force_software_renderer", "Software Renderer", "Forces the use of software rendering via Cairo",
false,
G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE
);
G_OBJECT_CLASS(class)->set_property = main_window_set_property;
G_OBJECT_CLASS(class)->get_property = main_window_get_property;
G_OBJECT_CLASS(class)->constructed = main_window_constructed;
g_object_class_install_properties(G_OBJECT_CLASS(class), N_PROPERTIES, obj_properties);
}
MainWindow *main_window_new(SameBoyApplication *application, bool force_software_renderer) {
g_debug("main_window_new(%p, %d)", application, force_software_renderer);
return g_object_new(
MAIN_WINDOW_TYPE,
"application", G_APPLICATION(application),
"force_software_renderer", force_software_renderer,
NULL
);
}
void main_window_fullscreen(MainWindow *self, bool make_fullscreen) {
if (make_fullscreen) {
gtk_window_fullscreen(GTK_WINDOW(self));
}
else {
gtk_window_unfullscreen(GTK_WINDOW(self));
}
}
void main_window_setup_menu(MainWindow *self, char *model_string) {
main_menu_setup(self->main_menu, model_string, self, on_change_model, on_change_linked_device);
}
void main_window_open_console_window(MainWindow *self) {
gtk_widget_show_all(GTK_WIDGET(self->console));
}
void main_window_open_memory_viewer_window(MainWindow *self) {
g_warning("Not yet implemented!");
}
void main_window_open_vram_viewer_window(MainWindow *self) {
gtk_widget_show_all(GTK_WIDGET(self->vram_viewer));
}
void main_window_open_printer_window(MainWindow *self) {
gtk_widget_show_all(GTK_WIDGET(self->printer));
}
// GbScreen wrappers
void main_window_clear(MainWindow *self) {
return gb_screen_clear(self->screen);
}
uint32_t *main_window_get_pixels(MainWindow *self) {
return gb_screen_get_pixels(self->screen);
}
uint32_t *main_window_get_current_buffer(MainWindow *self) {
return gb_screen_get_current_buffer(self->screen);
}
uint32_t *main_window_get_previous_buffer(MainWindow *self) {
return gb_screen_get_previous_buffer(self->screen);
}
void main_window_flip(MainWindow *self) {
return gb_screen_flip(self->screen);
}
void main_window_set_resolution(MainWindow *self, unsigned width, unsigned height) {
return gb_screen_set_resolution(self->screen, width, height);
}
void main_window_set_blending_mode(MainWindow *self, GB_frame_blending_mode_t mode) {
return gb_screen_set_blending_mode(self->screen, mode);
}
void main_window_set_shader(MainWindow *self, const char *shader_name) {
return gb_screen_set_shader(self->screen, shader_name);
}
void main_window_queue_render(MainWindow *self) {
return gb_screen_queue_render(self->screen);
}
// Core functions
static void emulation_thread(MainWindow *self) {
}
static void main_window_start_emulation_thread(MainWindow *self) {
if (self->running) return;
while (self->stopping);
g_thread_new("EmulationThread", (GThreadFunc)emulation_thread, self);
}
void main_window_start(MainWindow *self) {
g_debug("main_window_start");
self->running = true;
// TODO: Clear audio queue
while (self->running) {
if (self->rewind_paused) {
// handle_events(self->gb);
g_usleep(G_USEC_PER_SEC / 8);
}
else {
if (self->do_rewind) {
GB_rewind_pop(self->gb);
if (self->turbo_down) {
GB_rewind_pop(self->gb);
}
if (!GB_rewind_pop(self->gb)) {
self->rewind_paused = true;
}
self->do_rewind = false;
}
GB_run(self->gb);
}
}
if (self->file) {
GB_save_battery(self->gb, self->battery_save_path);
GB_save_cheats(self->gb, self->cheats_save_path);
}
self->stopping = false;
}
void main_window_stop(MainWindow *self) {
g_debug("main_window_stop");
if (self->running) return;
// GB_audio_set_paused(true);
GB_debugger_set_disabled(self->gb, true);
self->stopping = true;
self->running = false;
if (GB_debugger_is_stopped(self->gb)) {
console_window_abort_debugger(self->console);
}
while (self->stopping);
GB_debugger_set_disabled(self->gb, false);
}
void main_window_reset(MainWindow *self) {
g_debug("main_window_reset");
}