SameBoy/gtk3/gb_screen.c

359 lines
10 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.

#include "gb_screen.h"
#include "config.h"
#include "util.h"
#include <stdint.h>
struct _GbScreen {
GtkGLArea parent;
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_GL_AREA);
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_render(GtkGLArea *gl_area, GdkGLContext *context) {
GbScreen *self = (GbScreen *) gl_area;
if (!self->use_gl) return true;
static gint scaled_width, scaled_height;
static double scale_x, scale_y;
GtkWidget *widget = GTK_WIDGET(gl_area);
int width = gtk_widget_get_allocated_width(widget);
int height = gtk_widget_get_allocated_height(widget);
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
};
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
);
return true;
}
static gboolean gb_screen_draw(GtkWidget *widget, cairo_t *cr) {
GbScreen *self = (GbScreen *) widget;
if (self->use_gl) {
return GTK_WIDGET_CLASS(gb_screen_parent_class)->draw(widget, cr);
}
static gint scaled_width, scaled_height;
static double scale_x, scale_y;
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
};
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 true;
}
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_realize(GtkWidget *widget) {
GbScreen *self = (GbScreen *)widget;
GtkGLArea *gl_area = GTK_GL_AREA(widget);
if (self->use_gl) {
gtk_gl_area_set_required_version(gl_area, 3, 2);
gtk_gl_area_set_auto_render(gl_area, true);
gtk_gl_area_set_has_alpha(gl_area, false);
gtk_gl_area_set_has_depth_buffer(gl_area, false);
gtk_gl_area_set_has_stencil_buffer(gl_area, false);
GTK_WIDGET_CLASS(gb_screen_parent_class)->realize(widget);
GdkGLContext *context = gtk_gl_area_get_context(gl_area);
if (context) {
gtk_gl_area_make_current(gl_area);
int major, minor;
gdk_gl_context_get_version(context, &major, &minor);
if (gtk_gl_area_get_error(gl_area) != NULL) {
goto error;
}
g_debug("GL (%d.%d) Context: %p", major, minor, context);
g_debug("GtkGLArea on %s", glGetString(GL_RENDERER));
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(gl_area, error);
}
else {
return;
}
}
}
else {
GTK_WIDGET_CLASS(gb_screen_parent_class)->realize(widget);
}
error:
if (gtk_gl_area_get_error(gl_area) != NULL) {
g_warning("GtkGLArea: %s", gtk_gl_area_get_error(gl_area)->message);
}
self->use_gl = false;
g_info("Using Cairo for rendering");
}
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 contexts version in the realize callback.
self->use_gl = self->use_gl && test_gl_support();
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_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;
GTK_WIDGET_CLASS(class)->realize = gb_screen_realize;
GTK_GL_AREA_CLASS(class)->render = gb_screen_render;
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);
}