#include "gb_screen.h" #include "config.h" #include "util.h" #include struct _GbScreen { GtkBin parent; GtkGLArea *gl_area; GtkDrawingArea *fallback; bool use_gl; shader_t shader; uint32_t *image_buffers[3]; unsigned char current_buffer; unsigned screen_width; unsigned screen_height; GB_frame_blending_mode_t blending_mode; }; G_DEFINE_TYPE(GbScreen, gb_screen, GTK_TYPE_BIN); typedef enum { PROP_USE_GL = 1, N_PROPERTIES } GbScreenProperty; static GParamSpec *obj_properties[N_PROPERTIES] = { NULL, }; static void gb_screen_finalize(GObject *object) { GbScreen *self = (GbScreen *) object; if (self->image_buffers[0]) g_free(self->image_buffers[0]); if (self->image_buffers[1]) g_free(self->image_buffers[1]); if (self->image_buffers[2]) g_free(self->image_buffers[2]); free_shader(&self->shader); free_master_shader(); G_OBJECT_CLASS(gb_screen_parent_class)->finalize(object); } static void gb_screen_get_natural_size(GbScreen *self, gint *natural_width, gint *natural_height, double *scale_x_ptr, double *scale_y_ptr) { int width = gtk_widget_get_allocated_width(GTK_WIDGET(self)); int height = gtk_widget_get_allocated_height(GTK_WIDGET(self)); double scale_x = width / (double)self->screen_width; double scale_y = height / (double)self->screen_height; if (config.video.use_integer_scaling) { scale_x = (unsigned)(scale_x); scale_y = (unsigned)(scale_y); } if (config.video.keep_aspect_ratio) { if (scale_x > scale_y) { scale_x = scale_y; } else { scale_y = scale_x; } } scale_x = max_double(1.0, scale_x); scale_y = max_double(1.0, scale_y); if (natural_width) *natural_width = self->screen_width * scale_x; if (natural_height) *natural_height = self->screen_height * scale_y; if (scale_x_ptr) *scale_x_ptr = scale_x; if (scale_y_ptr) *scale_y_ptr = scale_y; } static gboolean gb_screen_draw(GtkWidget *widget, cairo_t *cr) { GTK_WIDGET_CLASS(gb_screen_parent_class)->draw(widget, cr); static gint scaled_width, scaled_height; static double scale_x, scale_y; GbScreen *self = (GbScreen *) widget; GtkStyleContext *context = gtk_widget_get_style_context(widget); int width = gtk_widget_get_allocated_width(widget); int height = gtk_widget_get_allocated_height(widget); gtk_render_background(context, cr, 0, 0, width, height); gtk_render_frame(context, cr, 0, 0, width, height); gb_screen_get_natural_size(self, &scaled_width, &scaled_height, &scale_x, &scale_y); Rect viewport = { (width - scaled_width) / 2, (height - scaled_height) / 2, scaled_width, scaled_height }; if (self->use_gl) { glViewport(viewport.x, viewport.y, scaled_width, scaled_height); uint32_t *pixels = gb_screen_get_current_buffer(self); uint32_t *previous = gb_screen_get_previous_buffer(self); static void *_pixels = NULL; if (pixels) { _pixels = pixels; } glClearColor(0, 0, 0, 1); glClear(GL_COLOR_BUFFER_BIT); render_bitmap_with_shader( &self->shader, _pixels, previous, self->screen_width, self->screen_height, viewport.x, viewport.y, viewport.width, viewport.height, self->blending_mode ); // gtk_gl_area_queue_render(self->gl_area); } else { cairo_surface_t *surface = cairo_image_surface_create_for_data( (unsigned char *) gb_screen_get_current_buffer(self), CAIRO_FORMAT_RGB24, self->screen_width, self->screen_height, cairo_format_stride_for_width(CAIRO_FORMAT_RGB24, self->screen_width) ); cairo_translate(cr, viewport.x, viewport.y); cairo_scale(cr, scale_x, scale_y); cairo_set_source_surface(cr, surface, 0, 0); cairo_pattern_set_filter(cairo_get_source(cr), CAIRO_FILTER_NEAREST); cairo_paint(cr); cairo_surface_destroy(surface); } return false; } static void gb_screen_get_preferred_width(GtkWidget *widget, gint *minimum_width, gint *natural_width) { GbScreen *self = (GbScreen *)widget; *minimum_width = self->screen_width; gb_screen_get_natural_size(self, natural_width, NULL, NULL, NULL); } static void gb_screen_get_preferred_height(GtkWidget *widget, gint *minimum_height, gint *natural_height) { GbScreen *self = (GbScreen *)widget; *minimum_height = self->screen_height; gb_screen_get_natural_size(self, NULL, natural_height, NULL, NULL); } static void gb_screen_set_property(GObject *object, guint property_id, const GValue *value, GParamSpec *pspec) { GbScreen *self = (GbScreen *) object; switch ((GbScreenProperty) property_id) { case PROP_USE_GL: self->use_gl = g_value_get_boolean(value); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); } } static void gb_screen_get_property(GObject *object, guint property_id, GValue *value, GParamSpec *pspec) { GbScreen *self = (GbScreen *) object; switch ((GbScreenProperty) property_id) { case PROP_USE_GL: g_value_set_boolean(value, self->use_gl); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); } } static void gb_screen_gl_area_realized(GtkWidget *widget, GObject *object) { GbScreen *self = (GbScreen *)object; GtkGLArea *gl_area = GTK_GL_AREA(widget); g_debug("GL Context: %p", gtk_gl_area_get_context(self->gl_area)); gtk_gl_area_make_current(self->gl_area); if (gtk_gl_area_get_error(self->gl_area) != NULL) { goto error; } const char *renderer = (char *)glGetString(GL_RENDERER); g_debug("GtkGLArea on %s", renderer ? renderer : "Unknown"); if (config.video.shader == NULL || (!init_shader_with_name(&self->shader, config.video.shader) && !init_shader_with_name(&self->shader, "NearestNeighbor"))) { GError *error = g_error_new_literal(g_quark_from_string("sameboy-gl-error"), 1, "Failed to initialize shaders"); gtk_gl_area_set_error(self->gl_area, error); } else { g_info("Using OpenGL for rendering"); G_OBJECT_CLASS(gb_screen_parent_class)->constructed(object); return; } error: if (gtk_gl_area_get_error(self->gl_area) != NULL) { g_warning("GtkGLArea: %s", gtk_gl_area_get_error(self->gl_area)->message); } gtk_widget_destroy(GTK_WIDGET(self->gl_area)); self->gl_area = NULL; self->use_gl = false; g_info("Using Cairo for rendering"); G_OBJECT_CLASS(gb_screen_parent_class)->constructed(object); } static void gb_screen_constructed(GObject *object) { GbScreen *self = (GbScreen *)object; // Very ugly workaround for GtkGlArea! // When a GtkGlArea is realized and it creates a legacy GL 1.4 context // it tries to use GL 2.0 functions to render the window which leads to the application crashing. // So we initialize GTK, create a dummy GtkWindow object, attach a `realize` callback and // in this callback create a GdkGLContext on this window. But instead of running the GTK main loop // we just realize and destroy the dummy window and compare the context’s version in the realize callback. self->use_gl = self->use_gl && test_gl_support(); if (self->use_gl) { self->gl_area = GTK_GL_AREA(gtk_gl_area_new()); g_signal_connect(self->gl_area, "realize", G_CALLBACK(gb_screen_gl_area_realized), object); gtk_gl_area_set_required_version(self->gl_area, 3, 2); gtk_gl_area_set_auto_render(self->gl_area, false); gtk_gl_area_set_has_alpha(self->gl_area, false); gtk_gl_area_set_has_depth_buffer(self->gl_area, false); gtk_gl_area_set_has_stencil_buffer(self->gl_area, false); gtk_container_add(GTK_CONTAINER(self), GTK_WIDGET(self->gl_area)); } else { g_info("Using Cairo for rendering"); G_OBJECT_CLASS(gb_screen_parent_class)->constructed(object); } } static void gb_screen_class_init(GbScreenClass *class) { obj_properties[PROP_USE_GL] = g_param_spec_boolean( "use-gl", "Use OpenGL", "Whether to use OpenGL for rendering.", true, G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_PRIVATE ); G_OBJECT_CLASS(class)->finalize = gb_screen_finalize; G_OBJECT_CLASS(class)->set_property = gb_screen_set_property; G_OBJECT_CLASS(class)->get_property = gb_screen_get_property; G_OBJECT_CLASS(class)->constructed = gb_screen_constructed; GTK_WIDGET_CLASS(class)->draw = gb_screen_draw; GTK_WIDGET_CLASS(class)->get_preferred_width = gb_screen_get_preferred_width; GTK_WIDGET_CLASS(class)->get_preferred_height = gb_screen_get_preferred_height; g_object_class_install_properties(G_OBJECT_CLASS(class), N_PROPERTIES, obj_properties); } static void gb_screen_init(GbScreen *self) { gb_screen_set_resolution(self, 160, 144); gb_screen_clear(self); } GbScreen *gb_screen_new(bool force_fallback) { g_debug("force_fallback: %d", force_fallback); return g_object_new(GB_SCREEN_TYPE, "use-gl", !force_fallback, NULL); } void gb_screen_clear(GbScreen *self) { for (unsigned i = 0; i < 3; i++) { memset(self->image_buffers[i], 0, self->screen_width * self->screen_height * sizeof(uint32_t)); } } bool gb_screen_uses_fallback(GbScreen *self) { return !self->use_gl; } // Determines how many frame buffers to use static uint8_t number_of_buffers(GbScreen *self) { if (!self->use_gl) return 2; bool should_blend = config_get_frame_blending_mode() != GB_FRAME_BLENDING_MODE_DISABLED; return should_blend? 3 : 2; } // Returns the buffer that should be used by the Core to render a new frame to uint32_t *gb_screen_get_pixels(GbScreen *self) { return self->image_buffers[(self->current_buffer + 1) % number_of_buffers(self)]; } // Returns the current finished frame uint32_t *gb_screen_get_current_buffer(GbScreen *self) { return self->image_buffers[self->current_buffer]; } // Returns the previous finished frame uint32_t *gb_screen_get_previous_buffer(GbScreen *self) { return self->image_buffers[(self->current_buffer + 2) % number_of_buffers(self)]; } // Cycles the buffers void gb_screen_flip(GbScreen *self) { self->current_buffer = (self->current_buffer + 1) % number_of_buffers(self); } void gb_screen_set_resolution(GbScreen *self, unsigned width, unsigned height) { self->screen_width = width; self->screen_height = height; for (unsigned i = 0; i < 3; i++) { self->image_buffers[i] = g_realloc_n( self->image_buffers[i], self->screen_width * self->screen_height, sizeof(uint32_t) ); } gtk_widget_queue_resize(GTK_WIDGET(self)); } void gb_screen_set_blending_mode(GbScreen *self, GB_frame_blending_mode_t mode) { self->blending_mode = mode; } void gb_screen_set_shader(GbScreen *self, const char *shader_name) { if (!self->use_gl) return; free_shader(&self->shader); init_shader_with_name(&self->shader, shader_name); }