gtk2/gsk/gl/gskgldriver.c
Matthias Clasen 1057588a8f gsk: Fix the gl texture cache
We need to include both the scale and the filtering
in the key for the texture cache, since those affect
the texture.

This fixes misrendering in the recorder in the inspector
whenever transforms are involved. An example where this
was showing up is testrevealer's swing transition.
2020-09-09 13:55:09 -04:00

809 lines
22 KiB
C

#include "config.h"
#include "gskgldriverprivate.h"
#include "gskdebugprivate.h"
#include "gskprofilerprivate.h"
#include "gdk/gdkglcontextprivate.h"
#include "gdk/gdktextureprivate.h"
#include "gdk/gdkgltextureprivate.h"
#include <gdk/gdk.h>
#include <epoxy/gl.h>
typedef struct {
GLuint fbo_id;
GLuint depth_stencil_id;
} Fbo;
typedef struct {
GLuint texture_id;
int width;
int height;
GLuint min_filter;
GLuint mag_filter;
Fbo fbo;
GdkTexture *user;
guint in_use : 1;
guint permanent : 1;
/* TODO: Make this optional and not for every texture... */
TextureSlice *slices;
guint n_slices;
} Texture;
struct _GskGLDriver
{
GObject parent_instance;
GdkGLContext *gl_context;
GskProfiler *profiler;
struct {
GQuark created_textures;
GQuark reused_textures;
GQuark surface_uploads;
} counters;
Fbo default_fbo;
GHashTable *textures; /* texture_id -> Texture */
GHashTable *pointer_textures; /* pointer -> texture_id */
const Texture *bound_source_texture;
int max_texture_size;
gboolean in_frame : 1;
};
G_DEFINE_TYPE (GskGLDriver, gsk_gl_driver, G_TYPE_OBJECT)
static Texture *
texture_new (void)
{
return g_slice_new0 (Texture);
}
static inline void
fbo_clear (const Fbo *f)
{
if (f->depth_stencil_id != 0)
glDeleteRenderbuffers (1, &f->depth_stencil_id);
glDeleteFramebuffers (1, &f->fbo_id);
}
static void
texture_free (gpointer data)
{
Texture *t = data;
guint i;
if (t->user)
gdk_texture_clear_render_data (t->user);
if (t->fbo.fbo_id != 0)
fbo_clear (&t->fbo);
if (t->texture_id != 0)
{
glDeleteTextures (1, &t->texture_id);
}
else
{
g_assert_cmpint (t->n_slices, >, 0);
for (i = 0; i < t->n_slices; i ++)
glDeleteTextures (1, &t->slices[i].texture_id);
}
g_slice_free (Texture, t);
}
static void
gsk_gl_driver_set_texture_parameters (GskGLDriver *self,
int min_filter,
int mag_filter)
{
glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, min_filter);
glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, mag_filter);
glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
}
static void
gsk_gl_driver_finalize (GObject *gobject)
{
GskGLDriver *self = GSK_GL_DRIVER (gobject);
gdk_gl_context_make_current (self->gl_context);
g_clear_pointer (&self->textures, g_hash_table_unref);
g_clear_pointer (&self->pointer_textures, g_hash_table_unref);
g_clear_object (&self->profiler);
if (self->gl_context == gdk_gl_context_get_current ())
gdk_gl_context_clear_current ();
G_OBJECT_CLASS (gsk_gl_driver_parent_class)->finalize (gobject);
}
static void
gsk_gl_driver_class_init (GskGLDriverClass *klass)
{
GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
gobject_class->finalize = gsk_gl_driver_finalize;
}
static void
gsk_gl_driver_init (GskGLDriver *self)
{
self->textures = g_hash_table_new_full (NULL, NULL, NULL, texture_free);
self->max_texture_size = -1;
#ifdef G_ENABLE_DEBUG
self->profiler = gsk_profiler_new ();
self->counters.created_textures = gsk_profiler_add_counter (self->profiler,
"created_textures",
"Textures created this frame",
TRUE);
self->counters.reused_textures = gsk_profiler_add_counter (self->profiler,
"reused_textures",
"Textures reused this frame",
TRUE);
self->counters.surface_uploads = gsk_profiler_add_counter (self->profiler,
"surface_uploads",
"Texture uploads from surfaces this frame",
TRUE);
#endif
}
GskGLDriver *
gsk_gl_driver_new (GdkGLContext *context)
{
GskGLDriver *self;
g_return_val_if_fail (GDK_IS_GL_CONTEXT (context), NULL);
self = (GskGLDriver *) g_object_new (GSK_TYPE_GL_DRIVER, NULL);
self->gl_context = context;
return self;
}
void
gsk_gl_driver_begin_frame (GskGLDriver *self)
{
g_return_if_fail (GSK_IS_GL_DRIVER (self));
g_return_if_fail (!self->in_frame);
self->in_frame = TRUE;
if (self->max_texture_size < 0)
{
glGetIntegerv (GL_MAX_TEXTURE_SIZE, (GLint *) &self->max_texture_size);
GSK_NOTE (OPENGL, g_message ("GL max texture size: %d", self->max_texture_size));
}
glBindFramebuffer (GL_FRAMEBUFFER, 0);
glActiveTexture (GL_TEXTURE0);
glBindTexture (GL_TEXTURE_2D, 0);
glActiveTexture (GL_TEXTURE0 + 1);
glBindTexture (GL_TEXTURE_2D, 0);
glBindVertexArray (0);
glUseProgram (0);
glActiveTexture (GL_TEXTURE0);
#ifdef G_ENABLE_DEBUG
gsk_profiler_reset (self->profiler);
#endif
}
gboolean
gsk_gl_driver_in_frame (GskGLDriver *self)
{
return self->in_frame;
}
void
gsk_gl_driver_end_frame (GskGLDriver *self)
{
g_return_if_fail (GSK_IS_GL_DRIVER (self));
g_return_if_fail (self->in_frame);
self->bound_source_texture = NULL;
self->default_fbo.fbo_id = 0;
#ifdef G_ENABLE_DEBUG
GSK_NOTE (OPENGL,
g_message ("Textures created: %" G_GINT64_FORMAT "\n"
" Textures reused: %" G_GINT64_FORMAT "\n"
" Surface uploads: %" G_GINT64_FORMAT,
gsk_profiler_counter_get (self->profiler, self->counters.created_textures),
gsk_profiler_counter_get (self->profiler, self->counters.reused_textures),
gsk_profiler_counter_get (self->profiler, self->counters.surface_uploads)));
#endif
GSK_NOTE (OPENGL,
g_message ("*** Frame end: textures=%d",
g_hash_table_size (self->textures)));
self->in_frame = FALSE;
}
int
gsk_gl_driver_collect_textures (GskGLDriver *self)
{
GHashTableIter iter;
gpointer value_p = NULL;
int old_size;
g_return_val_if_fail (GSK_IS_GL_DRIVER (self), 0);
g_return_val_if_fail (!self->in_frame, 0);
old_size = g_hash_table_size (self->textures);
g_hash_table_iter_init (&iter, self->textures);
while (g_hash_table_iter_next (&iter, NULL, &value_p))
{
Texture *t = value_p;
if (t->user || t->permanent)
continue;
if (t->in_use)
{
t->in_use = FALSE;
if (t->fbo.fbo_id != 0)
{
fbo_clear (&t->fbo);
t->fbo.fbo_id = 0;
}
}
else
{
/* Remove from self->pointer_textures. */
/* TODO: Is there a better way for this? */
if (self->pointer_textures)
{
GHashTableIter pointer_iter;
gpointer value;
gpointer p;
g_hash_table_iter_init (&pointer_iter, self->pointer_textures);
while (g_hash_table_iter_next (&pointer_iter, &p, &value))
{
if (GPOINTER_TO_INT (value) == t->texture_id)
{
g_hash_table_iter_remove (&pointer_iter);
break;
}
}
}
g_hash_table_iter_remove (&iter);
}
}
return old_size - g_hash_table_size (self->textures);
}
GdkGLContext *
gsk_gl_driver_get_gl_context (GskGLDriver *self)
{
return self->gl_context;
}
int
gsk_gl_driver_get_max_texture_size (GskGLDriver *self)
{
if (self->max_texture_size < 0)
{
if (gdk_gl_context_get_use_es (self->gl_context))
return 2048;
return 1024;
}
return self->max_texture_size;
}
static Texture *
gsk_gl_driver_get_texture (GskGLDriver *self,
int texture_id)
{
Texture *t;
if (g_hash_table_lookup_extended (self->textures, GINT_TO_POINTER (texture_id), NULL, (gpointer *) &t))
return t;
return NULL;
}
static Texture *
create_texture (GskGLDriver *self,
float fwidth,
float fheight)
{
guint texture_id;
Texture *t;
int width = ceilf (fwidth);
int height = ceilf (fheight);
g_assert (width > 0);
g_assert (height > 0);
if (width > self->max_texture_size ||
height > self->max_texture_size)
{
g_critical ("Texture %d x %d is bigger than supported texture limit of %d; clipping...",
width, height,
self->max_texture_size);
width = MIN (width, self->max_texture_size);
height = MIN (height, self->max_texture_size);
}
glGenTextures (1, &texture_id);
t = texture_new ();
t->texture_id = texture_id;
t->width = width;
t->height = height;
t->min_filter = GL_NEAREST;
t->mag_filter = GL_NEAREST;
t->in_use = TRUE;
g_hash_table_insert (self->textures, GINT_TO_POINTER (texture_id), t);
#ifdef G_ENABLE_DEBUG
gsk_profiler_counter_inc (self->profiler, self->counters.created_textures);
#endif
return t;
}
static void
gsk_gl_driver_release_texture (gpointer data)
{
Texture *t = data;
t->user = NULL;
}
void
gsk_gl_driver_slice_texture (GskGLDriver *self,
GdkTexture *texture,
TextureSlice **out_slices,
guint *out_n_slices)
{
const int max_texture_size = gsk_gl_driver_get_max_texture_size (self) / 4; // XXX Too much?
const int tex_width = texture->width;
const int tex_height = texture->height;
const int cols = (texture->width / max_texture_size) + 1;
const int rows = (texture->height / max_texture_size) + 1;
int col, row;
int x = 0, y = 0; /* Position in the texture */
TextureSlice *slices;
Texture *tex;
g_assert (tex_width > max_texture_size || tex_height > max_texture_size);
tex = gdk_texture_get_render_data (texture, self);
if (tex != NULL)
{
g_assert (tex->n_slices > 0);
*out_slices = tex->slices;
*out_n_slices = tex->n_slices;
return;
}
slices = g_new0 (TextureSlice, cols * rows);
/* TODO: (Perf):
* We still create a surface here, which should obviously be unnecessary
* and we should eventually remove it and upload the data directly.
*/
for (col = 0; col < cols; col ++)
{
const int slice_width = MIN (max_texture_size, texture->width - x);
const int stride = slice_width * 4;
for (row = 0; row < rows; row ++)
{
const int slice_height = MIN (max_texture_size, texture->height - y);
const int slice_index = (col * rows) + row;
guchar *data;
guint texture_id;
cairo_surface_t *surface;
data = g_malloc (sizeof (guchar) * stride * slice_height);
gdk_texture_download_area (texture,
&(GdkRectangle){x, y, slice_width, slice_height},
data, stride);
surface = cairo_image_surface_create_for_data (data,
CAIRO_FORMAT_ARGB32,
slice_width, slice_height,
stride);
glGenTextures (1, &texture_id);
#ifdef G_ENABLE_DEBUG
gsk_profiler_counter_inc (self->profiler, self->counters.created_textures);
#endif
glBindTexture (GL_TEXTURE_2D, texture_id);
gsk_gl_driver_set_texture_parameters (self, GL_NEAREST, GL_NEAREST);
gdk_cairo_surface_upload_to_gl (surface, GL_TEXTURE_2D, slice_width, slice_height, NULL);
#ifdef G_ENABLE_DEBUG
gsk_profiler_counter_inc (self->profiler, self->counters.surface_uploads);
#endif
slices[slice_index].rect = (GdkRectangle){x, y, slice_width, slice_height};
slices[slice_index].texture_id = texture_id;
g_free (data);
cairo_surface_destroy (surface);
y += slice_height;
}
y = 0;
x += slice_width;
}
/* Allocate one Texture for the entire thing. */
tex = texture_new ();
tex->width = texture->width;
tex->height = texture->height;
tex->min_filter = GL_NEAREST;
tex->mag_filter = GL_NEAREST;
tex->in_use = TRUE;
tex->slices = slices;
tex->n_slices = cols * rows;
/* Use texture_free as destroy notify here since we are not inserting this Texture
* into self->textures! */
gdk_texture_set_render_data (texture, self, tex, texture_free);
*out_slices = slices;
*out_n_slices = cols * rows;
}
int
gsk_gl_driver_get_texture_for_texture (GskGLDriver *self,
GdkTexture *texture,
int min_filter,
int mag_filter)
{
Texture *t;
cairo_surface_t *surface;
if (GDK_IS_GL_TEXTURE (texture))
{
GdkGLContext *texture_context = gdk_gl_texture_get_context ((GdkGLTexture *)texture);
if (texture_context != self->gl_context)
{
/* In this case, we have to temporarily make the texture's context the current one,
* download its data into our context and then create a texture from it. */
if (texture_context)
gdk_gl_context_make_current (texture_context);
surface = gdk_texture_download_surface (texture);
gdk_gl_context_make_current (self->gl_context);
}
else
{
/* A GL texture from the same GL context is a simple task... */
return gdk_gl_texture_get_id ((GdkGLTexture *)texture);
}
}
else
{
t = gdk_texture_get_render_data (texture, self);
if (t)
{
if (t->min_filter == min_filter && t->mag_filter == mag_filter)
return t->texture_id;
}
surface = gdk_texture_download_surface (texture);
}
t = create_texture (self, gdk_texture_get_width (texture), gdk_texture_get_height (texture));
if (gdk_texture_set_render_data (texture, self, t, gsk_gl_driver_release_texture))
t->user = texture;
gsk_gl_driver_bind_source_texture (self, t->texture_id);
gsk_gl_driver_init_texture_with_surface (self,
t->texture_id,
surface,
min_filter,
mag_filter);
gdk_gl_context_label_object_printf (self->gl_context, GL_TEXTURE, t->texture_id,
"GdkTexture<%p> %d", texture, t->texture_id);
cairo_surface_destroy (surface);
return t->texture_id;
}
static guint
texture_key_hash (gconstpointer v)
{
const GskTextureKey *k = (GskTextureKey *)v;
return GPOINTER_TO_UINT (k->pointer)
+ (guint)(k->scale*100)
+ (guint)k->filter;
}
static gboolean
texture_key_equal (gconstpointer v1, gconstpointer v2)
{
const GskTextureKey *k1 = (GskTextureKey *)v1;
const GskTextureKey *k2 = (GskTextureKey *)v2;
return k1->pointer == k2->pointer &&
k1->scale == k2->scale &&
k1->filter == k2->filter;
}
int
gsk_gl_driver_get_texture_for_key (GskGLDriver *self,
GskTextureKey *key)
{
int id = 0;
if (G_UNLIKELY (self->pointer_textures == NULL))
self->pointer_textures = g_hash_table_new_full (texture_key_hash, texture_key_equal, g_free, NULL);
id = GPOINTER_TO_INT (g_hash_table_lookup (self->pointer_textures, key));
if (id != 0)
{
Texture *t;
t = g_hash_table_lookup (self->textures, GINT_TO_POINTER (id));
if (t != NULL)
t->in_use = TRUE;
}
return id;
}
void
gsk_gl_driver_set_texture_for_key (GskGLDriver *self,
GskTextureKey *key,
int texture_id)
{
GskTextureKey *k;
if (G_UNLIKELY (self->pointer_textures == NULL))
self->pointer_textures = g_hash_table_new_full (texture_key_hash, texture_key_equal, g_free, NULL);
k = g_new (GskTextureKey, 1);
*k = *key;
g_hash_table_insert (self->pointer_textures, k, GINT_TO_POINTER (texture_id));
}
int
gsk_gl_driver_create_texture (GskGLDriver *self,
float width,
float height)
{
Texture *t;
g_return_val_if_fail (GSK_IS_GL_DRIVER (self), -1);
t = create_texture (self, width, height);
return t->texture_id;
}
void
gsk_gl_driver_create_render_target (GskGLDriver *self,
int width,
int height,
int min_filter,
int mag_filter,
int *out_texture_id,
int *out_render_target_id)
{
GLuint fbo_id;
Texture *texture;
g_return_if_fail (self->in_frame);
texture = create_texture (self, width, height);
gsk_gl_driver_bind_source_texture (self, texture->texture_id);
gsk_gl_driver_init_texture_empty (self, texture->texture_id, min_filter, mag_filter);
glGenFramebuffers (1, &fbo_id);
glBindFramebuffer (GL_FRAMEBUFFER, fbo_id);
glFramebufferTexture2D (GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture->texture_id, 0);
#if 0
if (add_depth_buffer || add_stencil_buffer)
{
glGenRenderbuffersEXT (1, &depth_stencil_buffer_id);
gdk_gl_context_label_object_printf (self->gl_context, GL_RENDERBUFFER, depth_stencil_buffer_id,
"%s buffer for %d", add_depth_buffer ? "Depth" : "Stencil", texture_id);
}
else
depth_stencil_buffer_id = 0;
glBindRenderbuffer (GL_RENDERBUFFER, depth_stencil_buffer_id);
if (add_depth_buffer || add_stencil_buffer)
{
if (add_stencil_buffer)
glRenderbufferStorage (GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, t->width, t->height);
else
glRenderbufferStorage (GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, t->width, t->height);
if (add_depth_buffer)
glFramebufferRenderbuffer (GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT,
GL_RENDERBUFFER, depth_stencil_buffer_id);
if (add_stencil_buffer)
glFramebufferRenderbufferEXT (GL_FRAMEBUFFER, GL_STENCIL_ATTACHMENT,
GL_RENDERBUFFER, depth_stencil_buffer_id);
texture->fbo.depth_stencil_id = depth_stencil_buffer_id;
}
#endif
texture->fbo.fbo_id = fbo_id;
g_assert_cmphex (glCheckFramebufferStatus (GL_FRAMEBUFFER), ==, GL_FRAMEBUFFER_COMPLETE);
glBindFramebuffer (GL_FRAMEBUFFER, self->default_fbo.fbo_id);
*out_texture_id = texture->texture_id;
*out_render_target_id = fbo_id;
}
/* Mark the texture permanent, meaning it won'e be reused by the GLDriver.
* E.g. to store it in some other cache. */
void
gsk_gl_driver_mark_texture_permanent (GskGLDriver *self,
int texture_id)
{
Texture *t = gsk_gl_driver_get_texture (self, texture_id);
g_assert (t != NULL);
t->permanent = TRUE;
}
void
gsk_gl_driver_bind_source_texture (GskGLDriver *self,
int texture_id)
{
Texture *t;
g_return_if_fail (GSK_IS_GL_DRIVER (self));
g_return_if_fail (self->in_frame);
t = gsk_gl_driver_get_texture (self, texture_id);
if (t == NULL)
{
g_critical ("No texture %d found.", texture_id);
return;
}
if (self->bound_source_texture != t)
{
glActiveTexture (GL_TEXTURE0);
glBindTexture (GL_TEXTURE_2D, t->texture_id);
self->bound_source_texture = t;
}
}
void
gsk_gl_driver_destroy_texture (GskGLDriver *self,
int texture_id)
{
g_return_if_fail (GSK_IS_GL_DRIVER (self));
g_hash_table_remove (self->textures, GINT_TO_POINTER (texture_id));
}
void
gsk_gl_driver_init_texture_empty (GskGLDriver *self,
int texture_id,
int min_filter,
int mag_filter)
{
Texture *t;
g_return_if_fail (GSK_IS_GL_DRIVER (self));
t = gsk_gl_driver_get_texture (self, texture_id);
if (t == NULL)
{
g_critical ("No texture %d found.", texture_id);
return;
}
if (self->bound_source_texture != t)
{
g_critical ("You must bind the texture before initializing it.");
return;
}
t->min_filter = min_filter;
t->mag_filter = mag_filter;
gsk_gl_driver_set_texture_parameters (self, t->min_filter, t->mag_filter);
if (gdk_gl_context_get_use_es (self->gl_context))
glTexImage2D (GL_TEXTURE_2D, 0, GL_RGBA8, t->width, t->height, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
else
glTexImage2D (GL_TEXTURE_2D, 0, GL_RGBA8, t->width, t->height, 0, GL_BGRA, GL_UNSIGNED_BYTE, NULL);
glBindTexture (GL_TEXTURE_2D, 0);
}
static gboolean
filter_uses_mipmaps (int filter)
{
return filter != GL_NEAREST && filter != GL_LINEAR;
}
void
gsk_gl_driver_init_texture_with_surface (GskGLDriver *self,
int texture_id,
cairo_surface_t *surface,
int min_filter,
int mag_filter)
{
Texture *t;
g_return_if_fail (GSK_IS_GL_DRIVER (self));
t = gsk_gl_driver_get_texture (self, texture_id);
if (t == NULL)
{
g_critical ("No texture %d found.", texture_id);
return;
}
if (self->bound_source_texture != t)
{
g_critical ("You must bind the texture before initializing it.");
return;
}
gsk_gl_driver_set_texture_parameters (self, min_filter, mag_filter);
gdk_cairo_surface_upload_to_gl (surface, GL_TEXTURE_2D, t->width, t->height, NULL);
#ifdef G_ENABLE_DEBUG
gsk_profiler_counter_inc (self->profiler, self->counters.surface_uploads);
#endif
t->min_filter = min_filter;
t->mag_filter = mag_filter;
if (filter_uses_mipmaps (t->min_filter))
glGenerateMipmap (GL_TEXTURE_2D);
}