forked from AuroraMiddleware/gtk
a0bbd14325
Pass a scale factor when caching glyphs or looking them up in the cache. The glyphs in the cache are rendered with subpixel precision determined by the scale. Update all callers to pass a scale factor according to the window scale. This lets us render crisp glyphs on hidpi systems.
486 lines
12 KiB
C
486 lines
12 KiB
C
#include "config.h"
|
|
|
|
#include "gskvulkanglyphcacheprivate.h"
|
|
|
|
#include "gskvulkanimageprivate.h"
|
|
#include "gskdebugprivate.h"
|
|
#include "gskprivate.h"
|
|
|
|
#include <graphene.h>
|
|
|
|
/* Parameters for our cache eviction strategy.
|
|
*
|
|
* Each cached glyph has an age that gets reset every time a cached glyph gets used.
|
|
* Glyphs that have not been used for the MAX_AGE frames are considered old. We keep
|
|
* count of the pixels of each atlas that are taken up by old glyphs. We check the
|
|
* fraction of old pixels every CHECK_INTERVAL frames, and if it is above MAX_OLD, then
|
|
* we drop the atlas an all the glyphs contained in it from the cache.
|
|
*/
|
|
|
|
#define MAX_AGE 60
|
|
#define CHECK_INTERVAL 10
|
|
#define MAX_OLD 0.333
|
|
|
|
|
|
typedef struct {
|
|
cairo_surface_t *surface;
|
|
GskVulkanImage *image;
|
|
int width, height;
|
|
int x, y, y0;
|
|
int num_glyphs;
|
|
GList *dirty_glyphs;
|
|
guint old_pixels;
|
|
} Atlas;
|
|
|
|
struct _GskVulkanGlyphCache {
|
|
GObject parent_instance;
|
|
|
|
GdkVulkanContext *vulkan;
|
|
|
|
GHashTable *hash_table;
|
|
GPtrArray *atlases;
|
|
|
|
guint64 timestamp;
|
|
};
|
|
|
|
struct _GskVulkanGlyphCacheClass {
|
|
GObjectClass parent_class;
|
|
};
|
|
|
|
G_DEFINE_TYPE (GskVulkanGlyphCache, gsk_vulkan_glyph_cache, G_TYPE_OBJECT)
|
|
|
|
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);
|
|
static void dirty_glyph_free (gpointer v);
|
|
|
|
static Atlas *
|
|
create_atlas (GskVulkanGlyphCache *cache)
|
|
{
|
|
Atlas *atlas;
|
|
|
|
atlas = g_new0 (Atlas, 1);
|
|
atlas->width = 512;
|
|
atlas->height = 512;
|
|
atlas->y0 = 1;
|
|
atlas->y = 1;
|
|
atlas->x = 1;
|
|
atlas->image = NULL;
|
|
atlas->num_glyphs = 0;
|
|
atlas->dirty_glyphs = NULL;
|
|
|
|
return atlas;
|
|
}
|
|
|
|
static void
|
|
free_atlas (gpointer v)
|
|
{
|
|
Atlas *atlas = v;
|
|
|
|
if (atlas->surface)
|
|
cairo_surface_destroy (atlas->surface);
|
|
g_clear_object (&atlas->image);
|
|
g_list_free_full (atlas->dirty_glyphs, dirty_glyph_free);
|
|
g_free (atlas);
|
|
}
|
|
|
|
static void
|
|
gsk_vulkan_glyph_cache_init (GskVulkanGlyphCache *cache)
|
|
{
|
|
cache->hash_table = g_hash_table_new_full (glyph_cache_hash, glyph_cache_equal,
|
|
glyph_cache_key_free, glyph_cache_value_free);
|
|
cache->atlases = g_ptr_array_new_with_free_func (free_atlas);
|
|
}
|
|
|
|
static void
|
|
gsk_vulkan_glyph_cache_finalize (GObject *object)
|
|
{
|
|
GskVulkanGlyphCache *cache = GSK_VULKAN_GLYPH_CACHE (object);
|
|
|
|
g_ptr_array_unref (cache->atlases);
|
|
g_hash_table_unref (cache->hash_table);
|
|
|
|
G_OBJECT_CLASS (gsk_vulkan_glyph_cache_parent_class)->finalize (object);
|
|
}
|
|
|
|
static void
|
|
gsk_vulkan_glyph_cache_class_init (GskVulkanGlyphCacheClass *klass)
|
|
{
|
|
GObjectClass *object_class = G_OBJECT_CLASS (klass);
|
|
|
|
object_class->finalize = gsk_vulkan_glyph_cache_finalize;
|
|
}
|
|
|
|
typedef struct {
|
|
PangoFont *font;
|
|
PangoGlyph glyph;
|
|
guint scale; /* times 1024 */
|
|
} GlyphCacheKey;
|
|
|
|
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->scale == key2->scale;
|
|
}
|
|
|
|
static guint
|
|
glyph_cache_hash (gconstpointer v)
|
|
{
|
|
const GlyphCacheKey *key = v;
|
|
|
|
return GPOINTER_TO_UINT (key->font) ^ key->glyph ^ key->scale;
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
typedef struct {
|
|
GlyphCacheKey *key;
|
|
GskVulkanCachedGlyph *value;
|
|
cairo_surface_t *surface;
|
|
} DirtyGlyph;
|
|
|
|
static void
|
|
dirty_glyph_free (gpointer v)
|
|
{
|
|
DirtyGlyph *glyph = v;
|
|
|
|
if (glyph->surface)
|
|
cairo_surface_destroy (glyph->surface);
|
|
g_free (glyph);
|
|
}
|
|
|
|
static void
|
|
add_to_cache (GskVulkanGlyphCache *cache,
|
|
GlyphCacheKey *key,
|
|
GskVulkanCachedGlyph *value)
|
|
{
|
|
Atlas *atlas;
|
|
int i;
|
|
DirtyGlyph *dirty;
|
|
int width = value->draw_width * key->scale / 1024;
|
|
int height = value->draw_height * key->scale / 1024;
|
|
|
|
for (i = 0; i < cache->atlases->len; i++)
|
|
{
|
|
int x, y, y0;
|
|
|
|
atlas = g_ptr_array_index (cache->atlases, i);
|
|
x = atlas->x;
|
|
y = atlas->y;
|
|
y0 = atlas->y0;
|
|
|
|
if (atlas->x + width + 1 >= atlas->width)
|
|
{
|
|
/* start a new row */
|
|
y0 = y + 1;
|
|
x = 1;
|
|
}
|
|
|
|
if (y0 + height + 1 >= atlas->height)
|
|
continue;
|
|
|
|
atlas->y0 = y0;
|
|
atlas->x = x;
|
|
atlas->y = y;
|
|
break;
|
|
}
|
|
|
|
if (i == cache->atlases->len)
|
|
{
|
|
atlas = create_atlas (cache);
|
|
g_ptr_array_add (cache->atlases, atlas);
|
|
}
|
|
|
|
value->tx = (float)atlas->x / atlas->width;
|
|
value->ty = (float)atlas->y0 / atlas->height;
|
|
value->tw = (float)width / atlas->width;
|
|
value->th = (float)height / atlas->height;
|
|
|
|
value->texture_index = i;
|
|
|
|
dirty = g_new (DirtyGlyph, 1);
|
|
dirty->key = key;
|
|
dirty->value = value;
|
|
atlas->dirty_glyphs = g_list_prepend (atlas->dirty_glyphs, dirty);
|
|
|
|
atlas->x = atlas->x + width + 1;
|
|
atlas->y = MAX (atlas->y, atlas->y0 + height + 1);
|
|
|
|
atlas->num_glyphs++;
|
|
|
|
#ifdef G_ENABLE_DEBUG
|
|
if (GSK_DEBUG_CHECK(GLYPH_CACHE))
|
|
{
|
|
g_print ("Glyph cache:\n");
|
|
for (i = 0; i < cache->atlases->len; i++)
|
|
{
|
|
atlas = g_ptr_array_index (cache->atlases, i);
|
|
g_print ("\tAtlas %d (%dx%d): %d glyphs (%d dirty), %.2g%% old pixels, filled to %d, %d / %d\n",
|
|
i, atlas->width, atlas->height,
|
|
atlas->num_glyphs, g_list_length (atlas->dirty_glyphs),
|
|
100.0 * (double)atlas->old_pixels / (double)(atlas->width * atlas->height),
|
|
atlas->x, atlas->y0, atlas->y);
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
static void
|
|
render_glyph (Atlas *atlas,
|
|
DirtyGlyph *glyph,
|
|
GskImageRegion *region)
|
|
{
|
|
GlyphCacheKey *key = glyph->key;
|
|
GskVulkanCachedGlyph *value = glyph->value;
|
|
cairo_surface_t *surface;
|
|
cairo_t *cr;
|
|
cairo_scaled_font_t *scaled_font;
|
|
cairo_glyph_t cg;
|
|
|
|
surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32,
|
|
value->draw_width * key->scale / 1024,
|
|
value->draw_height * key->scale / 1024);
|
|
cairo_surface_set_device_scale (surface, key->scale / 1024.0, key->scale / 1024.0);
|
|
|
|
cr = cairo_create (surface);
|
|
|
|
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))
|
|
return;
|
|
|
|
cairo_set_scaled_font (cr, scaled_font);
|
|
cairo_set_source_rgba (cr, 1, 1, 1, 1);
|
|
|
|
cg.index = key->glyph;
|
|
cg.x = - value->draw_x;
|
|
cg.y = - value->draw_y;
|
|
|
|
cairo_show_glyphs (cr, &cg, 1);
|
|
|
|
cairo_destroy (cr);
|
|
|
|
glyph->surface = surface;
|
|
|
|
region->data = cairo_image_surface_get_data (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->x = (gsize)(value->tx * atlas->width);
|
|
region->y = (gsize)(value->ty * atlas->height);
|
|
}
|
|
|
|
static void
|
|
upload_dirty_glyphs (Atlas *atlas,
|
|
GskVulkanUploader *uploader)
|
|
{
|
|
GList *l;
|
|
guint num_regions;
|
|
GskImageRegion *regions;
|
|
int i;
|
|
|
|
num_regions = g_list_length (atlas->dirty_glyphs);
|
|
regions = alloca (sizeof (GskImageRegion) * num_regions);
|
|
|
|
for (l = atlas->dirty_glyphs, i = 0; l; l = l->next, i++)
|
|
render_glyph (atlas, (DirtyGlyph *)l->data, ®ions[i]);
|
|
|
|
GSK_NOTE (GLYPH_CACHE,
|
|
g_print ("uploading %d glyphs to cache\n", num_regions));
|
|
|
|
gsk_vulkan_image_upload_regions (atlas->image, uploader, num_regions, regions);
|
|
|
|
g_list_free_full (atlas->dirty_glyphs, dirty_glyph_free);
|
|
atlas->dirty_glyphs = NULL;
|
|
}
|
|
|
|
GskVulkanGlyphCache *
|
|
gsk_vulkan_glyph_cache_new (GdkVulkanContext *vulkan)
|
|
{
|
|
GskVulkanGlyphCache *cache;
|
|
|
|
cache = GSK_VULKAN_GLYPH_CACHE (g_object_new (GSK_TYPE_VULKAN_GLYPH_CACHE, NULL));
|
|
cache->vulkan = vulkan;
|
|
g_ptr_array_add (cache->atlases, create_atlas (cache));
|
|
|
|
return cache;
|
|
}
|
|
|
|
GskVulkanCachedGlyph *
|
|
gsk_vulkan_glyph_cache_lookup (GskVulkanGlyphCache *cache,
|
|
gboolean create,
|
|
PangoFont *font,
|
|
PangoGlyph glyph,
|
|
float scale)
|
|
{
|
|
GlyphCacheKey lookup_key;
|
|
GskVulkanCachedGlyph *value;
|
|
|
|
lookup_key.font = font;
|
|
lookup_key.glyph = glyph;
|
|
lookup_key.scale = (guint)(scale * 1024);
|
|
|
|
value = g_hash_table_lookup (cache->hash_table, &lookup_key);
|
|
|
|
if (value)
|
|
{
|
|
if (cache->timestamp - value->timestamp >= MAX_AGE)
|
|
{
|
|
Atlas *atlas = g_ptr_array_index (cache->atlases, value->texture_index);
|
|
|
|
atlas->old_pixels -= value->draw_width * value->draw_height;
|
|
value->timestamp = cache->timestamp;
|
|
}
|
|
}
|
|
|
|
if (create && value == NULL)
|
|
{
|
|
GlyphCacheKey *key;
|
|
PangoRectangle ink_rect;
|
|
|
|
key = g_new (GlyphCacheKey, 1);
|
|
value = g_new0 (GskVulkanCachedGlyph, 1);
|
|
|
|
pango_font_get_glyph_extents (font, glyph, &ink_rect, NULL);
|
|
pango_extents_to_pixels (&ink_rect, NULL);
|
|
|
|
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->timestamp = cache->timestamp;
|
|
|
|
key->font = g_object_ref (font);
|
|
key->glyph = glyph;
|
|
key->scale = (guint)(scale * 1024);
|
|
|
|
if (ink_rect.width > 0 && ink_rect.height > 0)
|
|
add_to_cache (cache, key, value);
|
|
|
|
g_hash_table_insert (cache->hash_table, key, value);
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
GskVulkanImage *
|
|
gsk_vulkan_glyph_cache_get_glyph_image (GskVulkanGlyphCache *cache,
|
|
GskVulkanUploader *uploader,
|
|
guint index)
|
|
{
|
|
Atlas *atlas;
|
|
|
|
g_return_val_if_fail (index < cache->atlases->len, NULL);
|
|
|
|
atlas = g_ptr_array_index (cache->atlases, index);
|
|
|
|
if (atlas->image == NULL)
|
|
atlas->image = gsk_vulkan_image_new_for_atlas (cache->vulkan, atlas->width, atlas->height);
|
|
|
|
if (atlas->dirty_glyphs)
|
|
upload_dirty_glyphs (atlas, uploader);
|
|
|
|
return atlas->image;
|
|
}
|
|
|
|
void
|
|
gsk_vulkan_glyph_cache_begin_frame (GskVulkanGlyphCache *cache)
|
|
{
|
|
int i, j;
|
|
guint *drops;
|
|
guint *shifts;
|
|
guint len;
|
|
GHashTableIter iter;
|
|
GlyphCacheKey *key;
|
|
GskVulkanCachedGlyph *value;
|
|
guint dropped = 0;
|
|
|
|
cache->timestamp++;
|
|
|
|
if (cache->timestamp % CHECK_INTERVAL != 0)
|
|
return;
|
|
|
|
len = cache->atlases->len;
|
|
|
|
/* look for glyphs that have grown old since last time */
|
|
g_hash_table_iter_init (&iter, cache->hash_table);
|
|
while (g_hash_table_iter_next (&iter, (gpointer *)&key, (gpointer *)&value))
|
|
{
|
|
guint age;
|
|
|
|
age = cache->timestamp - value->timestamp;
|
|
if (MAX_AGE <= age && age < MAX_AGE + CHECK_INTERVAL)
|
|
{
|
|
Atlas *atlas = g_ptr_array_index (cache->atlases, value->texture_index);
|
|
atlas->old_pixels += value->draw_width * value->draw_height;
|
|
}
|
|
}
|
|
|
|
drops = g_alloca (sizeof (guint) * len);
|
|
shifts = g_alloca (sizeof (guint) * len);
|
|
|
|
for (i = 0; i < len; i++)
|
|
{
|
|
drops[i] = 0;
|
|
shifts[i] = i;
|
|
}
|
|
|
|
/* look for atlases to drop, and create a mapping of updated texture indices */
|
|
for (i = cache->atlases->len - 1; i >= 0; i--)
|
|
{
|
|
Atlas *atlas = g_ptr_array_index (cache->atlases, i);
|
|
|
|
if (atlas->old_pixels > MAX_OLD * atlas->width * atlas->height)
|
|
{
|
|
GSK_NOTE(GLYPH_CACHE,
|
|
g_print ("Dropping atlas %d (%g.2%% old)\n", i, 100.0 * (double)atlas->old_pixels / (double)(atlas->width * atlas->height)));
|
|
g_ptr_array_remove_index (cache->atlases, i);
|
|
|
|
drops[i] = 1;
|
|
for (j = i; j + 1 < len; j++)
|
|
shifts[j + 1] = shifts[j];
|
|
}
|
|
}
|
|
|
|
/* no atlas dropped, we're done */
|
|
if (len == cache->atlases->len)
|
|
return;
|
|
|
|
/* purge glyphs and update texture indices */
|
|
g_hash_table_iter_init (&iter, cache->hash_table);
|
|
|
|
while (g_hash_table_iter_next (&iter, (gpointer *)&key, (gpointer *)&value))
|
|
{
|
|
if (drops[value->texture_index])
|
|
{
|
|
dropped++;
|
|
g_hash_table_iter_remove (&iter);
|
|
}
|
|
else
|
|
{
|
|
value->texture_index = shifts[value->texture_index];
|
|
}
|
|
}
|
|
|
|
GSK_NOTE(GLYPH_CACHE, g_print ("Dropped %d glyphs\n", dropped));
|
|
}
|