diff options
Diffstat (limited to 'fiv-view.c')
-rw-r--r-- | fiv-view.c | 634 |
1 files changed, 530 insertions, 104 deletions
@@ -1,7 +1,7 @@ // // fiv-view.c: image viewing widget // -// Copyright (c) 2021 - 2022, Přemysl Eric Janouch <p@janouch.name> +// Copyright (c) 2021 - 2024, Přemysl Eric Janouch <p@janouch.name> // // Permission to use, copy, modify, and/or distribute this software for any // purpose with or without fee is hereby granted. @@ -24,6 +24,7 @@ #include <math.h> #include <stdbool.h> +#include <epoxy/gl.h> #include <gtk/gtk.h> #ifdef GDK_WINDOWING_X11 #include <gdk/gdkx.h> @@ -63,10 +64,10 @@ struct _FivView { gchar *messages; ///< Image load information gchar *uri; ///< Path to the current image (if any) - cairo_surface_t *image; ///< The loaded image (sequence) - cairo_surface_t *page; ///< Current page within image, weak - cairo_surface_t *page_scaled; ///< Current page within image, scaled - cairo_surface_t *frame; ///< Current frame within page, weak + FivIoImage *image; ///< The loaded image (sequence) + FivIoImage *page; ///< Current page within image, weak + FivIoImage *page_scaled; ///< Current page within image, scaled + FivIoImage *frame; ///< Current frame within page, weak FivIoOrientation orientation; ///< Current page orientation bool enable_cms : 1; ///< Smooth scaling toggle bool filter : 1; ///< Smooth scaling toggle @@ -77,12 +78,16 @@ struct _FivView { double scale; ///< Scaling factor double drag_start[2]; ///< Adjustment values for drag origin - cairo_surface_t *enhance_swap; ///< Quick swap in/out - FivIoProfile screen_cms_profile; ///< Target colour profile for widget + FivIoImage *enhance_swap; ///< Quick swap in/out + FivIoProfile *screen_cms_profile; ///< Target colour profile for widget int remaining_loops; ///< Greater than zero if limited gint64 frame_time; ///< Current frame's start, µs precision gulong frame_update_connection; ///< GdkFrameClock::update + + GdkGLContext *gl_context; ///< OpenGL context + bool gl_initialized; ///< Objects have been created + GLuint gl_program; ///< Linked render program }; G_DEFINE_TYPE_EXTENDED(FivView, fiv_view, GTK_TYPE_WIDGET, 0, @@ -110,10 +115,10 @@ static FivIoOrientation view_mirror[9] = { [FivIoOrientationMirror0] = FivIoOrientation0, [FivIoOrientation180] = FivIoOrientationMirror180, [FivIoOrientationMirror180] = FivIoOrientation180, - [FivIoOrientationMirror270] = FivIoOrientation270, - [FivIoOrientation90] = FivIoOrientationMirror90, - [FivIoOrientationMirror90] = FivIoOrientation90, - [FivIoOrientation270] = FivIoOrientationMirror270, + [FivIoOrientationMirror270] = FivIoOrientation90, + [FivIoOrientation90] = FivIoOrientationMirror270, + [FivIoOrientationMirror90] = FivIoOrientation270, + [FivIoOrientation270] = FivIoOrientationMirror90, }; static FivIoOrientation view_right[9] = { @@ -161,6 +166,147 @@ enum { // Globals are, sadly, the canonical way of storing signal numbers. static guint view_signals[LAST_SIGNAL]; +// --- OpenGL ------------------------------------------------------------------ +// While GTK+ 3 technically still supports legacy desktop OpenGL 2.0[1], +// we will pick the 3.3 core profile, which is fairly old by now. +// It doesn't seem to make any sense to go below 3.2. +// +// [1] https://stackoverflow.com/a/37923507/76313 +// +// OpenGL ES +// +// Currently, we do not support OpenGL ES at all--it needs its own shaders +// (if only because of different #version statements), and also further analysis +// as to what is our minimum version requirement. While GTK+ 3 can again go +// down as low as OpenGL ES 2.0, this might be too much of a hassle to support. +// +// ES can be forced via GDK_GL=gles, if gdk_gl_context_set_required_version() +// doesn't stand in the way. +// +// Let's not forget that this is a desktop image viewer first and foremost. + +static const char * +gl_error_string(GLenum err) +{ + switch (err) { + case GL_NO_ERROR: + return "no error"; + case GL_CONTEXT_LOST: + return "context lost"; + case GL_INVALID_ENUM: + return "invalid enum"; + case GL_INVALID_VALUE: + return "invalid value"; + case GL_INVALID_OPERATION: + return "invalid operation"; + case GL_INVALID_FRAMEBUFFER_OPERATION: + return "invalid framebuffer operation"; + case GL_OUT_OF_MEMORY: + return "out of memory"; + case GL_STACK_UNDERFLOW: + return "stack underflow"; + case GL_STACK_OVERFLOW: + return "stack overflow"; + default: + return NULL; + } +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static const char *gl_vertex = + "#version 330\n" + "layout(location = 0) in vec4 position;\n" + "out vec2 coordinates;\n" + "void main() {\n" + "\tcoordinates = position.zw;\n" + "\tgl_Position = vec4(position.xy, 0., 1.);\n" + "}\n"; + +static const char *gl_fragment = + "#version 330\n" + "in vec2 coordinates;\n" + "layout(location = 0) out vec4 color;\n" + "uniform sampler2D picture;\n" + "uniform bool checkerboard;\n" + "\n" + "vec3 checker() {\n" + "\tvec2 xy = gl_FragCoord.xy / 20.;\n" + "\tif (checkerboard && (int(floor(xy.x) + floor(xy.y)) & 1) == 0)\n" + "\t\treturn vec3(0.98);\n" + "\telse\n" + "\t\treturn vec3(1.00);\n" + "}\n" + "\n" + "void main() {\n" + "\tvec3 c = checker();\n" + "\tvec4 t = texture(picture, coordinates);\n" + "\t// Premultiplied blending with a solid background.\n" + "\t// XXX: This is only correct for linear components.\n" + "\tcolor = vec4(c * (1. - t.a) + t.rgb, 1.);\n" + "}\n"; + +static GLuint +gl_make_shader(int type, const char *glsl) +{ + GLuint shader = glCreateShader(type); + glShaderSource(shader, 1, &glsl, NULL); + glCompileShader(shader); + + GLint status = 0; + glGetShaderiv(shader, GL_COMPILE_STATUS, &status); + if (!status) { + GLint len = 0; + glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &len); + + GLchar *buffer = g_malloc0(len + 1); + glGetShaderInfoLog(shader, len, NULL, buffer); + g_warning("GL shader compilation failed: %s", buffer); + g_free(buffer); + + glDeleteShader(shader); + return 0; + } + return shader; +} + +static GLuint +gl_make_program(void) +{ + GLuint vertex = gl_make_shader(GL_VERTEX_SHADER, gl_vertex); + GLuint fragment = gl_make_shader(GL_FRAGMENT_SHADER, gl_fragment); + if (!vertex || !fragment) { + glDeleteShader(vertex); + glDeleteShader(fragment); + return 0; + } + + GLuint program = glCreateProgram(); + glAttachShader(program, vertex); + glAttachShader(program, fragment); + glLinkProgram(program); + glDeleteShader(vertex); + glDeleteShader(fragment); + + GLint status = 0; + glGetProgramiv(program, GL_LINK_STATUS, &status); + if (!status) { + GLint len = 0; + glGetProgramiv(program, GL_INFO_LOG_LENGTH, &len); + + GLchar *buffer = g_malloc0(len + 1); + glGetProgramInfoLog(program, len, NULL, buffer); + g_warning("GL program linking failed: %s", buffer); + g_free(buffer); + + glDeleteProgram(program); + return 0; + } + return program; +} + +// ----------------------------------------------------------------------------- + static void on_adjustment_value_changed( G_GNUC_UNUSED GtkAdjustment *adjustment, gpointer user_data) @@ -198,12 +344,14 @@ update_adjustments(FivView *self) if (self->hadjustment) { gtk_adjustment_configure(self->hadjustment, - gtk_adjustment_get_value(self->hadjustment), 0, dw, + gtk_adjustment_get_value(self->hadjustment), + 0, MAX(dw, alloc.width), alloc.width * 0.1, alloc.width * 0.9, alloc.width); } if (self->vadjustment) { gtk_adjustment_configure(self->vadjustment, - gtk_adjustment_get_value(self->vadjustment), 0, dh, + gtk_adjustment_get_value(self->vadjustment), + 0, MAX(dh, alloc.height), alloc.height * 0.1, alloc.height * 0.9, alloc.height); } } @@ -234,9 +382,9 @@ fiv_view_finalize(GObject *gobject) { FivView *self = FIV_VIEW(gobject); g_clear_pointer(&self->screen_cms_profile, fiv_io_profile_free); - g_clear_pointer(&self->enhance_swap, cairo_surface_destroy); - g_clear_pointer(&self->image, cairo_surface_destroy); - g_clear_pointer(&self->page_scaled, cairo_surface_destroy); + g_clear_pointer(&self->enhance_swap, fiv_io_image_unref); + g_clear_pointer(&self->image, fiv_io_image_unref); + g_clear_pointer(&self->page_scaled, fiv_io_image_unref); g_free(self->uri); g_free(self->messages); @@ -283,15 +431,13 @@ fiv_view_get_property( g_value_set_boolean(value, !!self->image); break; case PROP_CAN_ANIMATE: - g_value_set_boolean(value, self->page && - cairo_surface_get_user_data(self->page, &fiv_io_key_frame_next)); + g_value_set_boolean(value, self->page && self->page->frame_next); break; case PROP_HAS_PREVIOUS_PAGE: g_value_set_boolean(value, self->image && self->page != self->image); break; case PROP_HAS_NEXT_PAGE: - g_value_set_boolean(value, self->page && - cairo_surface_get_user_data(self->page, &fiv_io_key_page_next)); + g_value_set_boolean(value, self->page && self->page->page_next); break; case PROP_HADJUSTMENT: @@ -403,21 +549,35 @@ static void prescale_page(FivView *self) { FivIoRenderClosure *closure = NULL; - if (!self->image || !(closure = - cairo_surface_get_user_data(self->page, &fiv_io_key_render))) + if (!self->image || !(closure = self->page->render)) return; // TODO(p): Restart the animation. No vector formats currently animate. g_return_if_fail(!self->frame_update_connection); + // Optimization, taking into account the workaround in set_scale(). + if (!self->page_scaled && + (self->scale == 1 || self->scale == 0.999999999999999)) + return; + // If it fails, the previous frame pointer may become invalid. - g_clear_pointer(&self->page_scaled, cairo_surface_destroy); - self->frame = self->page_scaled = closure->render(closure, self->scale); + g_clear_pointer(&self->page_scaled, fiv_io_image_unref); + self->frame = self->page_scaled = closure->render(closure, + self->enable_cms ? fiv_io_cmm_get_default() : NULL, + self->enable_cms ? self->screen_cms_profile : NULL, self->scale); if (!self->page_scaled) self->frame = self->page; } static void +set_source_image(FivView *self, cairo_t *cr) +{ + cairo_surface_t *surface = fiv_io_image_to_surface_noref(self->frame); + cairo_set_source_surface(cr, surface, 0, 0); + cairo_surface_destroy(surface); +} + +static void fiv_view_size_allocate(GtkWidget *widget, GtkAllocation *allocation) { GTK_WIDGET_CLASS(fiv_view_parent_class)->size_allocate(widget, allocation); @@ -448,6 +608,27 @@ out: // // Note that Wayland does not have any appropriate protocol, as of writing: // https://gitlab.freedesktop.org/wayland/wayland-protocols/-/merge_requests/14 +static FivIoProfile * +monitor_cms_profile(GdkWindow *root, int num) +{ + char atom[32] = ""; + g_snprintf(atom, sizeof atom, "_ICC_PROFILE%c%d", num ? '_' : '\0', num); + + // Sadly, there is no nice GTK+/GDK mechanism to watch this for changes. + int format = 0, length = 0; + GdkAtom type = GDK_NONE; + guchar *data = NULL; + FivIoProfile *result = NULL; + if (gdk_property_get(root, gdk_atom_intern(atom, FALSE), GDK_NONE, 0, + 8 << 20 /* MiB */, FALSE, &type, &format, &length, &data)) { + if (format == 8 && length > 0) + result = fiv_io_cmm_get_profile( + fiv_io_cmm_get_default(), data, length); + g_free(data); + } + return result; +} + static void reload_screen_cms_profile(FivView *self, GdkWindow *window) { @@ -465,7 +646,8 @@ reload_screen_cms_profile(FivView *self, GdkWindow *window) gchar *data = NULL; gsize length = 0; if (g_file_get_contents(path, &data, &length, NULL)) - self->screen_cms_profile = fiv_io_profile_new(data, length); + self->screen_cms_profile = fiv_io_cmm_get_profile( + fiv_io_cmm_get_default(), data, length); g_free(data); } g_free(path); @@ -477,6 +659,7 @@ reload_screen_cms_profile(FivView *self, GdkWindow *window) GdkDisplay *display = gdk_window_get_display(window); GdkMonitor *monitor = gdk_display_get_monitor_at_window(display, window); + GdkWindow *root = gdk_screen_get_root_window(gdk_window_get_screen(window)); int num = -1; for (int i = gdk_display_get_n_monitors(display); num < 0 && i--; ) @@ -485,24 +668,14 @@ reload_screen_cms_profile(FivView *self, GdkWindow *window) if (num < 0) goto out; - char atom[32] = ""; - g_snprintf(atom, sizeof atom, "_ICC_PROFILE%c%d", num ? '_' : '\0', num); - - // Sadly, there is no nice GTK+/GDK mechanism to watch this for changes. - int format = 0, length = 0; - GdkAtom type = GDK_NONE; - guchar *data = NULL; - GdkWindow *root = gdk_screen_get_root_window(gdk_window_get_screen(window)); - if (gdk_property_get(root, gdk_atom_intern(atom, FALSE), GDK_NONE, 0, - 8 << 20 /* MiB */, FALSE, &type, &format, &length, &data)) { - if (format == 8 && length > 0) - self->screen_cms_profile = fiv_io_profile_new(data, length); - g_free(data); - } + // Cater to xiccd limitations (agalakhov/xiccd#33). + if (!(self->screen_cms_profile = monitor_cms_profile(root, num)) && num) + self->screen_cms_profile = monitor_cms_profile(root, 0); out: if (!self->screen_cms_profile) - self->screen_cms_profile = fiv_io_profile_new_sRGB(); + self->screen_cms_profile = + fiv_io_cmm_get_profile_sRGB(fiv_io_cmm_get_default()); } static void @@ -536,6 +709,9 @@ fiv_view_realize(GtkWidget *widget) GdkWindow *window = gdk_window_new(gtk_widget_get_parent_window(widget), &attributes, GDK_WA_X | GDK_WA_Y | GDK_WA_VISUAL); + GSettings *settings = g_settings_new(PROJECT_NS PROJECT_NAME); + gboolean opengl = g_settings_get_boolean(settings, "opengl"); + // Without the following call, or the rendering mode set to "recording", // RGB30 degrades to RGB24, because gdk_window_begin_paint_internal() // creates backing stores using cairo_content_t constants. @@ -545,19 +721,272 @@ fiv_view_realize(GtkWidget *widget) // Note that this disables double buffering, and sometimes causes artefacts, // see: https://gitlab.gnome.org/GNOME/gtk/-/issues/2560 // - // If GTK+'s OpenGL integration fails to deliver, we need to use the window - // directly, sidestepping the toolkit entirely. - GSettings *settings = g_settings_new(PROJECT_NS PROJECT_NAME); + // GTK+'s OpenGL integration is terrible, so we may need to use + // the X11 subwindow directly, sidestepping the toolkit entirely. if (GDK_IS_X11_WINDOW(window) && g_settings_get_boolean(settings, "native-view-window")) gdk_window_ensure_native(window); #endif // GDK_WINDOWING_X11 + g_object_unref(settings); gtk_widget_register_window(widget, window); gtk_widget_set_window(widget, window); gtk_widget_set_realized(widget, TRUE); reload_screen_cms_profile(FIV_VIEW(widget), window); + + FivView *self = FIV_VIEW(widget); + g_clear_object(&self->gl_context); + if (!opengl) + return; + + GError *error = NULL; + GdkGLContext *gl_context = gdk_window_create_gl_context(window, &error); + if (!gl_context) { + g_warning("GL: %s", error->message); + g_error_free(error); + return; + } + + gdk_gl_context_set_use_es(gl_context, FALSE); + gdk_gl_context_set_required_version(gl_context, 3, 3); + gdk_gl_context_set_debug_enabled(gl_context, TRUE); + + if (!gdk_gl_context_realize(gl_context, &error)) { + g_warning("GL: %s", error->message); + g_error_free(error); + g_object_unref(gl_context); + return; + } + + self->gl_context = gl_context; +} + +static void GLAPIENTRY +gl_on_message(G_GNUC_UNUSED GLenum source, GLenum type, G_GNUC_UNUSED GLuint id, + G_GNUC_UNUSED GLenum severity, G_GNUC_UNUSED GLsizei length, + const GLchar *message, G_GNUC_UNUSED const void *user_data) +{ + if (type == GL_DEBUG_TYPE_ERROR) + g_warning("GL: error: %s", message); + else + g_debug("GL: %s", message); +} + +static void +fiv_view_unrealize(GtkWidget *widget) +{ + FivView *self = FIV_VIEW(widget); + if (self->gl_context) { + if (self->gl_initialized) { + gdk_gl_context_make_current(self->gl_context); + glDeleteProgram(self->gl_program); + } + if (self->gl_context == gdk_gl_context_get_current()) + gdk_gl_context_clear_current(); + + g_clear_object(&self->gl_context); + } + + GTK_WIDGET_CLASS(fiv_view_parent_class)->unrealize(widget); +} + +static bool +gl_draw(FivView *self, cairo_t *cr) +{ + gdk_gl_context_make_current(self->gl_context); + + if (!self->gl_initialized) { + GLuint program = gl_make_program(); + if (!program) + return false; + + glDisable(GL_SCISSOR_TEST); + glDisable(GL_STENCIL_TEST); + glDisable(GL_DEPTH_TEST); + glDisable(GL_CULL_FACE); + glDisable(GL_BLEND); + if (epoxy_has_gl_extension("GL_ARB_debug_output")) { + glEnable(GL_DEBUG_OUTPUT); + glDebugMessageCallback(gl_on_message, NULL); + } + + self->gl_program = program; + self->gl_initialized = true; + } + + // This limit is always less than that of Cairo/pixman, + // and we'd have to figure out tiling. + GLint max = 0; + glGetIntegerv(GL_MAX_TEXTURE_SIZE, &max); + if (max < (GLint) self->frame->width || + max < (GLint) self->frame->height) { + g_warning("OpenGL max. texture size is too small"); + return false; + } + + GtkAllocation allocation; + gtk_widget_get_allocation(GTK_WIDGET(self), &allocation); + int dw = 0, dh = 0, dx = 0, dy = 0; + get_display_dimensions(self, &dw, &dh); + + int clipw = dw, cliph = dh; + double x1 = 0., y1 = 0., x2 = 1., y2 = 1.; + if (self->hadjustment) + x1 = floor(gtk_adjustment_get_value(self->hadjustment)) / dw; + if (self->vadjustment) + y1 = floor(gtk_adjustment_get_value(self->vadjustment)) / dh; + + if (dw <= allocation.width) { + dx = round((allocation.width - dw) / 2.); + } else { + x2 = x1 + (double) allocation.width / dw; + clipw = allocation.width; + } + + if (dh <= allocation.height) { + dy = round((allocation.height - dh) / 2.); + } else { + y2 = y1 + (double) allocation.height / dh; + cliph = allocation.height; + } + + int scale = gtk_widget_get_scale_factor(GTK_WIDGET(self)); + clipw *= scale; + cliph *= scale; + + enum { SRC, DEST }; + GLuint textures[2] = {}; + glGenTextures(2, textures); + + // https://stackoverflow.com/questions/25157306 0..1 + // GL_TEXTURE_RECTANGLE seems kind-of useful + glBindTexture(GL_TEXTURE_2D, textures[SRC]); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + if (self->filter) { + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + } else { + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + } + + // GL_UNPACK_ALIGNMENT is initially 4, which is fine for these. + // Texture swizzling is OpenGL 3.3. + if (self->frame->format == CAIRO_FORMAT_ARGB32) { + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, + self->frame->width, self->frame->height, + 0, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, self->frame->data); + } else if (self->frame->format == CAIRO_FORMAT_RGB24) { + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_A, GL_ONE); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, + self->frame->width, self->frame->height, + 0, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, self->frame->data); + } else if (self->frame->format == CAIRO_FORMAT_RGB30) { + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_A, GL_ONE); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, + self->frame->width, self->frame->height, + 0, GL_BGRA, GL_UNSIGNED_INT_2_10_10_10_REV, self->frame->data); + } else { + g_warning("GL: unsupported bitmap format"); + } + + // GtkGLArea creates textures like this. + glBindTexture(GL_TEXTURE_2D, textures[DEST]); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, clipw, cliph, 0, GL_BGRA, + GL_UNSIGNED_BYTE, NULL); + + glViewport(0, 0, clipw, cliph); + + GLuint vao = 0; + glGenVertexArrays(1, &vao); + + GLuint frame_buffer = 0; + glGenFramebuffers(1, &frame_buffer); + glBindFramebuffer(GL_FRAMEBUFFER, frame_buffer); + glFramebufferTexture2D( + GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, textures[DEST], 0); + + glClearColor(0., 0., 0., 1.); + glClear(GL_COLOR_BUFFER_BIT); + + GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER); + if (status != GL_FRAMEBUFFER_COMPLETE) + g_warning("GL framebuffer status: %u", status); + + glUseProgram(self->gl_program); + GLint position_location = glGetAttribLocation( + self->gl_program, "position"); + GLint picture_location = glGetUniformLocation( + self->gl_program, "picture"); + GLint checkerboard_location = glGetUniformLocation( + self->gl_program, "checkerboard"); + + glUniform1i(picture_location, 0); + glUniform1i(checkerboard_location, self->checkerboard); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, textures[SRC]); + + // Note that the Y axis is flipped in the table. + double vertices[][4] = { + {-1., -1., x1, y2}, + {+1., -1., x2, y2}, + {+1., +1., x2, y1}, + {-1., +1., x1, y1}, + }; + + cairo_matrix_t matrix = fiv_io_orientation_matrix(self->orientation, 1, 1); + cairo_matrix_transform_point(&matrix, &vertices[0][2], &vertices[0][3]); + cairo_matrix_transform_point(&matrix, &vertices[1][2], &vertices[1][3]); + cairo_matrix_transform_point(&matrix, &vertices[2][2], &vertices[2][3]); + cairo_matrix_transform_point(&matrix, &vertices[3][2], &vertices[3][3]); + + GLuint vertex_buffer = 0; + glGenBuffers(1, &vertex_buffer); + glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer); + glBufferData(GL_ARRAY_BUFFER, sizeof vertices, vertices, GL_STATIC_DRAW); + glBindVertexArray(vao); + glVertexAttribPointer(position_location, + G_N_ELEMENTS(vertices[0]), GL_DOUBLE, GL_FALSE, sizeof vertices[0], 0); + glEnableVertexAttribArray(position_location); + glDrawArrays(GL_TRIANGLE_FAN, 0, G_N_ELEMENTS(vertices)); + glDisableVertexAttribArray(position_location); + glBindVertexArray(0); + glBindBuffer(GL_ARRAY_BUFFER, 0); + glUseProgram(0); + glBindFramebuffer(GL_FRAMEBUFFER, 0); + + // XXX: Native GdkWindows send this to the software fallback path. + // XXX: This only reliably alpha blends when using the software fallback, + // such as with a native window, because 7237f5d in GTK+ 3 is a regression. + // We had to resort to rendering the checkerboard pattern in the shader. + // Unfortunately, it is hard to retrieve the theme colours from CSS. + GdkWindow *window = gtk_widget_get_window(GTK_WIDGET(self)); + cairo_translate(cr, dx, dy); + gdk_cairo_draw_from_gl( + cr, window, textures[DEST], GL_TEXTURE, scale, 0, 0, clipw, cliph); + gdk_gl_context_make_current(self->gl_context); + + glDeleteBuffers(1, &vertex_buffer); + glDeleteTextures(2, textures); + glDeleteVertexArrays(1, &vao); + glDeleteFramebuffers(1, &frame_buffer); + + // TODO(p): Possibly use this clue as a hint to use Cairo rendering. + GLenum err = 0; + while ((err = glGetError()) != GL_NO_ERROR) { + const char *string = gl_error_string(err); + if (string) + g_warning("GL: error: %s", string); + else + g_warning("GL: error: %u", err); + } + + gdk_gl_context_clear_current(); + return true; } static gboolean @@ -574,8 +1003,10 @@ fiv_view_draw(GtkWidget *widget, cairo_t *cr) if (!self->image || !gtk_cairo_should_draw_window(cr, gtk_widget_get_window(widget))) return TRUE; + if (self->gl_context && gl_draw(self, cr)) + return TRUE; - int dw, dh; + int dw = 0, dh = 0; get_display_dimensions(self, &dw, &dh); double x = 0; @@ -606,37 +1037,19 @@ fiv_view_draw(GtkWidget *widget, cairo_t *cr) // Then all frames are pre-scaled. if (self->page_scaled) { - cairo_set_source_surface(cr, self->frame, 0, 0); + set_source_image(self, cr); cairo_pattern_set_matrix(cairo_get_source(cr), &matrix); cairo_paint(cr); return TRUE; } - // FIXME: Recording surfaces do not work well with CAIRO_SURFACE_TYPE_XLIB, - // we always get a shitty pixmap, where transparency contains junk. - if (cairo_surface_get_type(self->frame) == CAIRO_SURFACE_TYPE_RECORDING) { - cairo_surface_t *image = - cairo_image_surface_create(CAIRO_FORMAT_ARGB32, dw, dh); - cairo_t *tcr = cairo_create(image); - cairo_scale(tcr, self->scale, self->scale); - cairo_set_source_surface(tcr, self->frame, 0, 0); - cairo_pattern_set_matrix(cairo_get_source(tcr), &matrix); - cairo_paint(tcr); - cairo_destroy(tcr); - - cairo_set_source_surface(cr, image, 0, 0); - cairo_paint(cr); - cairo_surface_destroy(image); - return TRUE; - } - // XXX: The rounding together with padding may result in up to // a pixel's worth of made-up picture data. cairo_rectangle(cr, 0, 0, dw, dh); cairo_clip(cr); cairo_scale(cr, self->scale, self->scale); - cairo_set_source_surface(cr, self->frame, 0, 0); + set_source_image(self, cr); cairo_pattern_t *pattern = cairo_get_source(cr); cairo_pattern_set_matrix(pattern, &matrix); @@ -810,15 +1223,13 @@ stop_animating(FivView *self) self->frame_time = 0; self->frame_update_connection = 0; - self->remaining_loops = 0; g_object_notify_by_pspec(G_OBJECT(self), view_properties[PROP_PLAYING]); } static gboolean advance_frame(FivView *self) { - cairo_surface_t *next = - cairo_surface_get_user_data(self->frame, &fiv_io_key_frame_next); + FivIoImage *next = self->frame->frame_next; if (next) { self->frame = next; } else { @@ -836,8 +1247,7 @@ advance_animation(FivView *self, GdkFrameClock *clock) gint64 now = gdk_frame_clock_get_frame_time(clock); while (true) { // TODO(p): See if infinite frames can actually happen, and how. - intptr_t duration = (intptr_t) cairo_surface_get_user_data( - self->frame, &fiv_io_key_frame_duration); + int64_t duration = self->frame->frame_duration; if (duration < 0) return FALSE; @@ -875,32 +1285,43 @@ start_animating(FivView *self) stop_animating(self); GdkFrameClock *clock = gtk_widget_get_frame_clock(GTK_WIDGET(self)); - if (!clock || !self->image || - !cairo_surface_get_user_data(self->page, &fiv_io_key_frame_next)) + if (!clock || !self->image || !self->page->frame_next) return; self->frame_time = gdk_frame_clock_get_frame_time(clock); self->frame_update_connection = g_signal_connect( clock, "update", G_CALLBACK(on_frame_clock_update), self); - self->remaining_loops = - (uintptr_t) cairo_surface_get_user_data(self->page, &fiv_io_key_loops); + + // Only restart looping the animation if it has stopped at the end. + if (!self->remaining_loops) { + self->remaining_loops = self->page->loops; + if (self->remaining_loops && !self->frame->frame_next) { + self->frame = self->page; + gtk_widget_queue_draw(GTK_WIDGET(self)); + } + } gdk_frame_clock_begin_updating(clock); g_object_notify_by_pspec(G_OBJECT(self), view_properties[PROP_PLAYING]); } static void -switch_page(FivView *self, cairo_surface_t *page) +switch_page(FivView *self, FivIoImage *page) { - g_clear_pointer(&self->page_scaled, cairo_surface_destroy); + g_clear_pointer(&self->page_scaled, fiv_io_image_unref); self->frame = self->page = page; + + // XXX: When self->scale_to_fit is in effect, + // this uses an old value that may no longer be appropriate, + // resulting in wasted effort. prescale_page(self); if (!self->page || - (self->orientation = (uintptr_t) cairo_surface_get_user_data( - self->page, &fiv_io_key_orientation)) == FivIoOrientationUnknown) + (self->orientation = self->page->orientation) == + FivIoOrientationUnknown) self->orientation = FivIoOrientation0; + self->remaining_loops = 0; start_animating(self); gtk_widget_queue_resize(GTK_WIDGET(self)); @@ -1027,7 +1448,7 @@ copy(FivView *self) cairo_surface_t *transformed = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, w, h); cairo_t *cr = cairo_create(transformed); - cairo_set_source_surface(cr, self->frame, 0, 0); + set_source_image(self, cr); cairo_pattern_set_matrix(cairo_get_source(cr), &matrix); cairo_paint(cr); cairo_destroy(cr); @@ -1065,7 +1486,7 @@ on_draw_page(G_GNUC_UNUSED GtkPrintOperation *operation, cairo_t *cr = gtk_print_context_get_cairo_context(context); cairo_scale(cr, scale, scale); - cairo_set_source_surface(cr, self->frame, 0, 0); + set_source_image(self, cr); cairo_pattern_set_matrix(cairo_get_source(cr), &matrix); cairo_paint(cr); } @@ -1100,10 +1521,10 @@ print(FivView *self) } static gboolean -save_as(FivView *self, cairo_surface_t *frame) +save_as(FivView *self, FivIoImage *frame) { GtkWindow *window = get_toplevel(GTK_WIDGET(self)); - FivIoProfile target = NULL; + FivIoProfile *target = NULL; if (self->enable_cms && (target = self->screen_cms_profile)) { GtkWidget *dialog = gtk_message_dialog_new(window, GTK_DIALOG_MODAL, GTK_MESSAGE_WARNING, GTK_BUTTONS_CLOSE, "%s", @@ -1279,6 +1700,7 @@ fiv_view_class_init(FivViewClass *klass) widget_class->map = fiv_view_map; widget_class->unmap = fiv_view_unmap; widget_class->realize = fiv_view_realize; + widget_class->unrealize = fiv_view_unrealize; widget_class->draw = fiv_view_draw; widget_class->button_press_event = fiv_view_button_press_event; widget_class->scroll_event = fiv_view_scroll_event; @@ -1362,11 +1784,12 @@ fiv_view_init(FivView *self) // --- Public interface -------------------------------------------------------- -static cairo_surface_t * +static FivIoImage * open_without_swapping_in(FivView *self, const char *uri) { FivIoOpenContext ctx = { .uri = uri, + .cmm = self->enable_cms ? fiv_io_cmm_get_default() : NULL, .screen_profile = self->enable_cms ? self->screen_cms_profile : NULL, .screen_dpi = 96, // TODO(p): Try to retrieve it from the screen. .enhance = self->enhance, @@ -1374,7 +1797,7 @@ open_without_swapping_in(FivView *self, const char *uri) }; GError *error = NULL; - cairo_surface_t *surface = fiv_io_open(&ctx, &error); + FivIoImage *image = fiv_io_open(&ctx, &error); if (error) { g_ptr_array_add(ctx.warnings, g_strdup(error->message)); g_error_free(error); @@ -1387,7 +1810,7 @@ open_without_swapping_in(FivView *self, const char *uri) } g_ptr_array_free(ctx.warnings, TRUE); - return surface; + return image; } // TODO(p): Progressive picture loading, or at least async/cancellable. @@ -1395,18 +1818,18 @@ gboolean fiv_view_set_uri(FivView *self, const char *uri) { // This is extremely expensive, and only works sometimes. - g_clear_pointer(&self->enhance_swap, cairo_surface_destroy); + g_clear_pointer(&self->enhance_swap, fiv_io_image_unref); if (self->enhance) { self->enhance = FALSE; g_object_notify_by_pspec( G_OBJECT(self), view_properties[PROP_ENHANCE]); } - cairo_surface_t *surface = open_without_swapping_in(self, uri); - g_clear_pointer(&self->image, cairo_surface_destroy); + FivIoImage *image = open_without_swapping_in(self, uri); + g_clear_pointer(&self->image, fiv_io_image_unref); self->frame = self->page = NULL; - self->image = surface; + self->image = image; switch_page(self, self->image); // Otherwise, adjustment values and zoom are retained implicitly. @@ -1418,15 +1841,15 @@ fiv_view_set_uri(FivView *self, const char *uri) g_object_notify_by_pspec(G_OBJECT(self), view_properties[PROP_MESSAGES]); g_object_notify_by_pspec(G_OBJECT(self), view_properties[PROP_HAS_IMAGE]); - return surface != NULL; + return image != NULL; } static void page_step(FivView *self, int step) { - cairo_user_data_key_t *key = - step < 0 ? &fiv_io_key_page_previous : &fiv_io_key_page_next; - cairo_surface_t *page = cairo_surface_get_user_data(self->page, key); + FivIoImage *page = step < 0 + ? self->page->page_previous + : self->page->page_next; if (page) switch_page(self, page); } @@ -1435,31 +1858,35 @@ static void frame_step(FivView *self, int step) { stop_animating(self); - cairo_user_data_key_t *key = - step < 0 ? &fiv_io_key_frame_previous : &fiv_io_key_frame_next; - if (!step || !(self->frame = cairo_surface_get_user_data(self->frame, key))) + + if (step > 0) { + // Decrease the loop counter as if running on a timer. + (void) advance_frame(self); + } else if (!step || !(self->frame = self->frame->frame_previous)) { self->frame = self->page; + self->remaining_loops = 0; + } gtk_widget_queue_draw(GTK_WIDGET(self)); } static gboolean reload(FivView *self) { - cairo_surface_t *surface = open_without_swapping_in(self, self->uri); + FivIoImage *image = open_without_swapping_in(self, self->uri); g_object_notify_by_pspec(G_OBJECT(self), view_properties[PROP_MESSAGES]); - if (!surface) + if (!image) return FALSE; - g_clear_pointer(&self->image, cairo_surface_destroy); - g_clear_pointer(&self->enhance_swap, cairo_surface_destroy); - switch_page(self, (self->image = surface)); + g_clear_pointer(&self->image, fiv_io_image_unref); + g_clear_pointer(&self->enhance_swap, fiv_io_image_unref); + switch_page(self, (self->image = image)); return TRUE; } static void swap_enhanced_image(FivView *self) { - cairo_surface_t *saved = self->image; + FivIoImage *saved = self->image; self->image = self->page = self->frame = NULL; if (self->enhance_swap) { @@ -1546,9 +1973,8 @@ fiv_view_command(FivView *self, FivViewCommand command) break; case FIV_VIEW_COMMAND_PAGE_NEXT: page_step(self, +1); break; case FIV_VIEW_COMMAND_PAGE_LAST: - for (cairo_surface_t *s = self->page; - (s = cairo_surface_get_user_data(s, &fiv_io_key_page_next)); ) - self->page = s; + for (FivIoImage *I = self->page; (I = I->page_next); ) + self->page = I; switch_page(self, self->page); break; case FIV_VIEW_COMMAND_FRAME_FIRST: |