From 91538aaba53f294b1739e5055266ff17df7b6a54 Mon Sep 17 00:00:00 2001 From: Přemysl Eric Janouch Date: Fri, 12 Nov 2021 13:45:22 +0100 Subject: Add an experimental OpenGL renderer --- README.adoc | 4 +- fiv-io.c | 18 ++- fiv-io.h | 2 + fiv-view.c | 412 +++++++++++++++++++++++++++++++++++++++++++++++++++++++- fiv.gschema.xml | 7 + meson.build | 1 + 6 files changed, 430 insertions(+), 14 deletions(-) diff --git a/README.adoc b/README.adoc index 07cf5ce..9ca9e65 100644 --- a/README.adoc +++ b/README.adoc @@ -13,7 +13,7 @@ Features photos, HEIC, AVIF, SVG, X11 cursors and TIFF, or whatever your gdk-pixbuf modules manage to load. - Employs high-performance file format libraries: Wuffs and libjpeg-turbo. - - Makes use of 30-bit X.org visuals, whenever it's possible and appropriate. + - Can make use of 30-bit X.org visuals, under certain conditions. - Has a notion of pages, and tries to load all included content within files. - Can keep the zoom and position when browsing, to help with comparing zoomed-in images. @@ -43,7 +43,7 @@ Building and Running Build-only dependencies: Meson, pkg-config, asciidoctor or asciidoc (recommended but optional) + Runtime dependencies: gtk+-3.0, glib>=2.64, pixman-1, shared-mime-info, - libturbojpeg, libwebp, librsvg-2.0 (for icons) + + libturbojpeg, libwebp, libepoxy, librsvg-2.0 (for icons) + Optional dependencies: lcms2, Little CMS fast float plugin, LibRaw, librsvg-2.0, xcursor, libheif, libtiff, ExifTool, resvg (unstable API, needs to be requested explicitly) + diff --git a/fiv-io.c b/fiv-io.c index e3d8c12..485a91a 100644 --- a/fiv-io.c +++ b/fiv-io.c @@ -3451,34 +3451,40 @@ fiv_io_orientation_apply(const FivIoImage *image, FivIoOrientation orientation, double *width, double *height) { fiv_io_orientation_dimensions(image, orientation, width, height); + return fiv_io_orientation_matrix(orientation, *width, *height); +} +cairo_matrix_t +fiv_io_orientation_matrix( + FivIoOrientation orientation, double width, double height) +{ cairo_matrix_t matrix = {}; cairo_matrix_init_identity(&matrix); switch (orientation) { case FivIoOrientation90: cairo_matrix_rotate(&matrix, -M_PI_2); - cairo_matrix_translate(&matrix, -*width, 0); + cairo_matrix_translate(&matrix, -width, 0); break; case FivIoOrientation180: cairo_matrix_scale(&matrix, -1, -1); - cairo_matrix_translate(&matrix, -*width, -*height); + cairo_matrix_translate(&matrix, -width, -height); break; case FivIoOrientation270: cairo_matrix_rotate(&matrix, +M_PI_2); - cairo_matrix_translate(&matrix, 0, -*height); + cairo_matrix_translate(&matrix, 0, -height); break; case FivIoOrientationMirror0: cairo_matrix_scale(&matrix, -1, +1); - cairo_matrix_translate(&matrix, -*width, 0); + cairo_matrix_translate(&matrix, -width, 0); break; case FivIoOrientationMirror90: cairo_matrix_rotate(&matrix, +M_PI_2); cairo_matrix_scale(&matrix, -1, +1); - cairo_matrix_translate(&matrix, -*width, -*height); + cairo_matrix_translate(&matrix, -width, -height); break; case FivIoOrientationMirror180: cairo_matrix_scale(&matrix, +1, -1); - cairo_matrix_translate(&matrix, 0, -*height); + cairo_matrix_translate(&matrix, 0, -height); break; case FivIoOrientationMirror270: cairo_matrix_rotate(&matrix, -M_PI_2); diff --git a/fiv-io.h b/fiv-io.h index c8d22c4..a3b482c 100644 --- a/fiv-io.h +++ b/fiv-io.h @@ -185,6 +185,8 @@ FivIoImage *fiv_io_open_png_thumbnail(const char *path, GError **error); /// and its target dimensions. cairo_matrix_t fiv_io_orientation_apply(const FivIoImage *image, FivIoOrientation orientation, double *width, double *height); +cairo_matrix_t fiv_io_orientation_matrix( + FivIoOrientation orientation, double width, double height); void fiv_io_orientation_dimensions(const FivIoImage *image, FivIoOrientation orientation, double *width, double *height); diff --git a/fiv-view.c b/fiv-view.c index fbcb769..fb01b3a 100644 --- a/fiv-view.c +++ b/fiv-view.c @@ -1,7 +1,7 @@ // // fiv-view.c: image viewing widget // -// Copyright (c) 2021 - 2022, Přemysl Eric Janouch +// Copyright (c) 2021 - 2024, Přemysl Eric Janouch // // 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 #include +#include #include #ifdef GDK_WINDOWING_X11 #include @@ -83,6 +84,10 @@ struct _FivView { 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, @@ -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) @@ -562,6 +708,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. @@ -571,20 +720,268 @@ 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); - g_object_unref(settings); #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; + } + + 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, 1, 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 @@ -601,8 +998,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; @@ -1296,6 +1695,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; diff --git a/fiv.gschema.xml b/fiv.gschema.xml index b399a8f..f869d2f 100644 --- a/fiv.gschema.xml +++ b/fiv.gschema.xml @@ -17,6 +17,13 @@ double buffering. + + false + Use experimental OpenGL rendering + + OpenGL within GTK+ is highly problematic--you don't want this. + + false Use a dark theme variant on start-up diff --git a/meson.build b/meson.build index bf18196..a8fc947 100644 --- a/meson.build +++ b/meson.build @@ -36,6 +36,7 @@ gdkpixbuf = dependency('gdk-pixbuf-2.0', required : get_option('gdk-pixbuf')) dependencies = [ dependency('gtk+-3.0'), dependency('pixman-1'), + dependency('epoxy'), dependency('libjpeg'), dependency('libturbojpeg'), -- cgit v1.2.3-70-g09d2