mirror of
https://gitlab.gnome.org/GNOME/gtk.git
synced 2025-01-02 17:00:19 +00:00
7bff3abe8e
We were putting big glyphs in the cache, in their own texture, but forgetting to mark the texture as permanent, so it could be reused, leading to occasional misrendering. Fix this by marking these textures as permanent, and explicitly freeing them when the cache entry gets old.
378 lines
10 KiB
C
378 lines
10 KiB
C
#include "config.h"
|
|
|
|
#include "gskglglyphcacheprivate.h"
|
|
#include "gskgldriverprivate.h"
|
|
#include "gskdebugprivate.h"
|
|
#include "gskprivate.h"
|
|
#include "gskgltextureatlasprivate.h"
|
|
|
|
#include "gdk/gdkglcontextprivate.h"
|
|
|
|
#include <graphene.h>
|
|
#include <cairo.h>
|
|
#include <epoxy/gl.h>
|
|
#include <string.h>
|
|
|
|
/* Cache eviction strategy
|
|
*
|
|
* We mark glyphs as accessed every time we use them.
|
|
* Every few frames, we mark glyphs that haven't been
|
|
* accessed since the last check as old.
|
|
*
|
|
* We keep count of the pixels of each atlas that are
|
|
* taken up by old data. When the fraction of old pixels
|
|
* gets too high, we drop the atlas and all the items it
|
|
* contained.
|
|
*
|
|
* Big glyphs are not stored in the atlas, they get their
|
|
* own texture, but they are still cached.
|
|
*/
|
|
|
|
#define MAX_FRAME_AGE (60)
|
|
#define MAX_GLYPH_SIZE 128 /* Will get its own texture if bigger */
|
|
|
|
static guint glyph_cache_hash (gconstpointer v);
|
|
static gboolean glyph_cache_equal (gconstpointer v1,
|
|
gconstpointer v2);
|
|
static void glyph_cache_key_free (gpointer v);
|
|
static void glyph_cache_value_free (gpointer v);
|
|
|
|
GskGLGlyphCache *
|
|
gsk_gl_glyph_cache_new (GdkDisplay *display,
|
|
GskGLTextureAtlases *atlases)
|
|
{
|
|
GskGLGlyphCache *glyph_cache;
|
|
|
|
glyph_cache = g_new0 (GskGLGlyphCache, 1);
|
|
|
|
glyph_cache->display = display;
|
|
glyph_cache->hash_table = g_hash_table_new_full (glyph_cache_hash, glyph_cache_equal,
|
|
glyph_cache_key_free, glyph_cache_value_free);
|
|
|
|
glyph_cache->atlases = gsk_gl_texture_atlases_ref (atlases);
|
|
|
|
glyph_cache->ref_count = 1;
|
|
|
|
return glyph_cache;
|
|
}
|
|
|
|
GskGLGlyphCache *
|
|
gsk_gl_glyph_cache_ref (GskGLGlyphCache *self)
|
|
{
|
|
self->ref_count++;
|
|
|
|
return self;
|
|
}
|
|
|
|
void
|
|
gsk_gl_glyph_cache_unref (GskGLGlyphCache *self)
|
|
{
|
|
g_assert (self->ref_count > 0);
|
|
|
|
if (self->ref_count == 1)
|
|
{
|
|
gsk_gl_texture_atlases_unref (self->atlases);
|
|
g_hash_table_unref (self->hash_table);
|
|
g_free (self);
|
|
return;
|
|
}
|
|
|
|
self->ref_count--;
|
|
}
|
|
|
|
static gboolean
|
|
glyph_cache_equal (gconstpointer v1, gconstpointer v2)
|
|
{
|
|
const GlyphCacheKey *key1 = v1;
|
|
const GlyphCacheKey *key2 = v2;
|
|
|
|
return key1->font == key2->font &&
|
|
key1->glyph == key2->glyph &&
|
|
key1->xshift == key2->xshift &&
|
|
key1->yshift == key2->yshift &&
|
|
key1->scale == key2->scale;
|
|
}
|
|
|
|
static guint
|
|
glyph_cache_hash (gconstpointer v)
|
|
{
|
|
const GlyphCacheKey *key = v;
|
|
|
|
return key->hash;
|
|
}
|
|
|
|
static void
|
|
glyph_cache_key_free (gpointer v)
|
|
{
|
|
GlyphCacheKey *f = v;
|
|
|
|
g_object_unref (f->font);
|
|
g_free (f);
|
|
}
|
|
|
|
static void
|
|
glyph_cache_value_free (gpointer v)
|
|
{
|
|
g_free (v);
|
|
}
|
|
|
|
static gboolean
|
|
render_glyph (GlyphCacheKey *key,
|
|
GskGLCachedGlyph *value,
|
|
GskImageRegion *region)
|
|
{
|
|
cairo_surface_t *surface;
|
|
cairo_t *cr;
|
|
cairo_scaled_font_t *scaled_font;
|
|
PangoGlyphString glyph_string;
|
|
PangoGlyphInfo glyph_info;
|
|
int surface_width, surface_height;
|
|
int stride;
|
|
unsigned char *data;
|
|
|
|
scaled_font = pango_cairo_font_get_scaled_font ((PangoCairoFont *)key->font);
|
|
if (G_UNLIKELY (!scaled_font || cairo_scaled_font_status (scaled_font) != CAIRO_STATUS_SUCCESS))
|
|
{
|
|
g_warning ("Failed to get a font");
|
|
return FALSE;
|
|
}
|
|
|
|
surface_width = value->draw_width * key->scale / 1024;
|
|
surface_height = value->draw_height * key->scale / 1024;
|
|
|
|
stride = cairo_format_stride_for_width (CAIRO_FORMAT_ARGB32, surface_width);
|
|
data = g_malloc0 (stride * surface_height);
|
|
surface = cairo_image_surface_create_for_data (data, CAIRO_FORMAT_ARGB32,
|
|
surface_width, surface_height,
|
|
stride);
|
|
cairo_surface_set_device_scale (surface, key->scale / 1024.0, key->scale / 1024.0);
|
|
|
|
cr = cairo_create (surface);
|
|
|
|
cairo_set_scaled_font (cr, scaled_font);
|
|
cairo_set_source_rgba (cr, 1, 1, 1, 1);
|
|
|
|
glyph_info.glyph = key->glyph;
|
|
glyph_info.geometry.width = value->draw_width * 1024;
|
|
if (glyph_info.glyph & PANGO_GLYPH_UNKNOWN_FLAG)
|
|
glyph_info.geometry.x_offset = 0;
|
|
else
|
|
glyph_info.geometry.x_offset = - value->draw_x * 1024;
|
|
glyph_info.geometry.y_offset = - value->draw_y * 1024;
|
|
|
|
glyph_string.num_glyphs = 1;
|
|
glyph_string.glyphs = &glyph_info;
|
|
|
|
pango_cairo_show_glyph_string (cr, key->font, &glyph_string);
|
|
cairo_destroy (cr);
|
|
|
|
cairo_surface_flush (surface);
|
|
|
|
region->width = cairo_image_surface_get_width (surface);
|
|
region->height = cairo_image_surface_get_height (surface);
|
|
region->stride = cairo_image_surface_get_stride (surface);
|
|
region->data = data;
|
|
if (value->atlas)
|
|
{
|
|
region->x = (gsize)(value->tx * value->atlas->width);
|
|
region->y = (gsize)(value->ty * value->atlas->height);
|
|
}
|
|
else
|
|
{
|
|
region->x = 0;
|
|
region->y = 0;
|
|
}
|
|
|
|
cairo_surface_destroy (surface);
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static void
|
|
upload_glyph (GlyphCacheKey *key,
|
|
GskGLCachedGlyph *value)
|
|
{
|
|
GskImageRegion r;
|
|
|
|
gdk_gl_context_push_debug_group_printf (gdk_gl_context_get_current (),
|
|
"Uploading glyph %d",
|
|
key->glyph);
|
|
|
|
if (render_glyph (key, value, &r))
|
|
{
|
|
glPixelStorei (GL_UNPACK_ROW_LENGTH, r.stride / 4);
|
|
glBindTexture (GL_TEXTURE_2D, value->texture_id);
|
|
glTexSubImage2D (GL_TEXTURE_2D, 0,
|
|
r.x, r.y, r.width, r.height,
|
|
GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV,
|
|
r.data);
|
|
glPixelStorei (GL_UNPACK_ROW_LENGTH, 0);
|
|
g_free (r.data);
|
|
}
|
|
|
|
gdk_gl_context_pop_debug_group (gdk_gl_context_get_current ());
|
|
}
|
|
|
|
static void
|
|
add_to_cache (GskGLGlyphCache *self,
|
|
GlyphCacheKey *key,
|
|
GskGLDriver *driver,
|
|
GskGLCachedGlyph *value)
|
|
{
|
|
const int width = value->draw_width * key->scale / 1024;
|
|
const int height = value->draw_height * key->scale / 1024;
|
|
|
|
if (width < MAX_GLYPH_SIZE && height < MAX_GLYPH_SIZE)
|
|
{
|
|
GskGLTextureAtlas *atlas = NULL;
|
|
int packed_x = 0;
|
|
int packed_y = 0;
|
|
|
|
gsk_gl_texture_atlases_pack (self->atlases, width + 2, height + 2, &atlas, &packed_x, &packed_y);
|
|
|
|
value->tx = (float)(packed_x + 1) / atlas->width;
|
|
value->ty = (float)(packed_y + 1) / atlas->height;
|
|
value->tw = (float)width / atlas->width;
|
|
value->th = (float)height / atlas->height;
|
|
value->used = TRUE;
|
|
|
|
value->atlas = atlas;
|
|
value->texture_id = atlas->texture_id;
|
|
}
|
|
else
|
|
{
|
|
value->atlas = NULL;
|
|
value->texture_id = gsk_gl_driver_create_texture (driver, width, height);
|
|
gsk_gl_driver_mark_texture_permanent (driver, value->texture_id);
|
|
|
|
gsk_gl_driver_bind_source_texture (driver, value->texture_id);
|
|
gsk_gl_driver_init_texture_empty (driver, value->texture_id, GL_LINEAR, GL_LINEAR);
|
|
|
|
value->tx = 0.0f;
|
|
value->ty = 0.0f;
|
|
value->tw = 1.0f;
|
|
value->th = 1.0f;
|
|
}
|
|
|
|
upload_glyph (key, value);
|
|
}
|
|
|
|
void
|
|
gsk_gl_glyph_cache_lookup_or_add (GskGLGlyphCache *cache,
|
|
GlyphCacheKey *lookup,
|
|
GskGLDriver *driver,
|
|
const GskGLCachedGlyph **cached_glyph_out)
|
|
{
|
|
GskGLCachedGlyph *value;
|
|
|
|
value = g_hash_table_lookup (cache->hash_table, lookup);
|
|
|
|
if (value)
|
|
{
|
|
if (value->atlas && !value->used)
|
|
{
|
|
gsk_gl_texture_atlas_mark_used (value->atlas, value->draw_width, value->draw_height);
|
|
value->used = TRUE;
|
|
}
|
|
value->accessed = TRUE;
|
|
|
|
*cached_glyph_out = value;
|
|
return;
|
|
}
|
|
|
|
{
|
|
GlyphCacheKey *key;
|
|
PangoRectangle ink_rect;
|
|
|
|
pango_font_get_glyph_extents (lookup->font, lookup->glyph, &ink_rect, NULL);
|
|
pango_extents_to_pixels (&ink_rect, NULL);
|
|
if (lookup->xshift != 0)
|
|
ink_rect.width += 1;
|
|
if (lookup->yshift != 0)
|
|
ink_rect.height += 1;
|
|
|
|
value = g_new0 (GskGLCachedGlyph, 1);
|
|
|
|
value->draw_x = ink_rect.x;
|
|
value->draw_y = ink_rect.y;
|
|
value->draw_width = ink_rect.width;
|
|
value->draw_height = ink_rect.height;
|
|
value->accessed = TRUE;
|
|
value->atlas = NULL; /* For now */
|
|
|
|
key = g_new0 (GlyphCacheKey, 1);
|
|
|
|
key->font = g_object_ref (lookup->font);
|
|
key->glyph = lookup->glyph;
|
|
key->xshift = lookup->xshift;
|
|
key->yshift = lookup->yshift;
|
|
key->scale = lookup->scale;
|
|
key->hash = lookup->hash;
|
|
|
|
if (key->scale > 0 &&
|
|
value->draw_width * key->scale / 1024 > 0 &&
|
|
value->draw_height * key->scale / 1024 > 0)
|
|
add_to_cache (cache, key, driver, value);
|
|
|
|
*cached_glyph_out = value;
|
|
g_hash_table_insert (cache->hash_table, key, value);
|
|
}
|
|
}
|
|
|
|
void
|
|
gsk_gl_glyph_cache_begin_frame (GskGLGlyphCache *self,
|
|
GskGLDriver *driver,
|
|
GPtrArray *removed_atlases)
|
|
{
|
|
GHashTableIter iter;
|
|
GlyphCacheKey *key;
|
|
GskGLCachedGlyph *value;
|
|
|
|
self->timestamp++;
|
|
|
|
if (removed_atlases->len > 0)
|
|
{
|
|
guint dropped = 0;
|
|
|
|
g_hash_table_iter_init (&iter, self->hash_table);
|
|
while (g_hash_table_iter_next (&iter, (gpointer *)&key, (gpointer *)&value))
|
|
{
|
|
if (g_ptr_array_find (removed_atlases, value->atlas, NULL))
|
|
{
|
|
g_hash_table_iter_remove (&iter);
|
|
dropped++;
|
|
}
|
|
}
|
|
|
|
GSK_NOTE(GLYPH_CACHE, if (dropped > 0) g_message ("Dropped %d glyphs", dropped));
|
|
}
|
|
|
|
if (self->timestamp % MAX_FRAME_AGE == 30)
|
|
{
|
|
g_hash_table_iter_init (&iter, self->hash_table);
|
|
while (g_hash_table_iter_next (&iter, (gpointer *)&key, (gpointer *)&value))
|
|
{
|
|
if (!value->accessed)
|
|
{
|
|
if (value->atlas)
|
|
{
|
|
if (value->used)
|
|
{
|
|
gsk_gl_texture_atlas_mark_unused (value->atlas, value->draw_width, value->draw_height);
|
|
value->used = FALSE;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
gsk_gl_driver_destroy_texture (driver, value->texture_id);
|
|
g_hash_table_iter_remove (&iter);
|
|
}
|
|
}
|
|
else
|
|
value->accessed = FALSE;
|
|
}
|
|
|
|
GSK_NOTE(GLYPH_CACHE, g_message ("%d glyphs cached", g_hash_table_size (self->hash_table)));
|
|
}
|
|
}
|