gtk/gsk/gl/gskglglyphcache.c
Matthias Clasen c5af463843 Cache glyph textures in render nodes
This is a quick implementation that avoids many
glyph cache lookups. We keep an array of direct
pointers in the text render node, and throw those
cached pointers away whenever any atlases have
been dropped (since that may invalidate the cached
glyphs).
2019-10-15 19:44:26 -04:00

386 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)
{
gsk_gl_glyph_cache_entry_validate (cache, value);
*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;
self->atlas_timestamp++;
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)));
}
}
void
gsk_gl_glyph_cache_entry_validate (GskGLGlyphCache *cache,
GskGLCachedGlyph *value)
{
value->accessed = TRUE;
if (value->atlas && !value->used)
{
gsk_gl_texture_atlas_mark_used (value->atlas, value->draw_width, value->draw_height);
value->used = TRUE;
}
}