diff --git a/gdk/gdkinternals.h b/gdk/gdkinternals.h index 143ed59529..13b139f5ce 100644 --- a/gdk/gdkinternals.h +++ b/gdk/gdkinternals.h @@ -274,6 +274,8 @@ void gdk_surface_move_resize (GdkSurface *surface, gint width, gint height); +GdkGLContext *gdk_surface_get_shared_data_gl_context (GdkSurface *surface); + G_END_DECLS #endif /* __GDK_INTERNALS_H__ */ diff --git a/gdk/gdksurface.c b/gdk/gdksurface.c index 6f0358d0cc..3beff1e9cf 100644 --- a/gdk/gdksurface.c +++ b/gdk/gdksurface.c @@ -1106,6 +1106,46 @@ gdk_surface_get_state (GdkSurface *surface) return surface->state; } +GdkGLContext * +gdk_surface_get_shared_data_gl_context (GdkSurface *surface) +{ + static int in_shared_data_creation; + GdkDisplay *display; + GdkGLContext *context; + + if (in_shared_data_creation) + return NULL; + + in_shared_data_creation = 1; + + display = gdk_surface_get_display (surface); + context = (GdkGLContext *)g_object_get_data (G_OBJECT (display), "gdk-gl-shared-data-context"); + if (context == NULL) + { + GError *error = NULL; + context = GDK_SURFACE_GET_CLASS (surface)->create_gl_context (surface, FALSE, NULL, &error); + if (context == NULL) + { + g_warning ("Failed to create shared context: %s", error->message); + g_clear_error (&error); + } + + gdk_gl_context_realize (context, &error); + if (context == NULL) + { + g_warning ("Failed to realize shared context: %s", error->message); + g_clear_error (&error); + } + + + g_object_set_data (G_OBJECT (display), "gdk-gl-shared-data-context", context); + } + + in_shared_data_creation = 0; + + return context; +} + GdkGLContext * gdk_surface_get_paint_gl_context (GdkSurface *surface, GError **error) diff --git a/gdk/wayland/gdkglcontext-wayland.c b/gdk/wayland/gdkglcontext-wayland.c index 79751cb8fc..edeca4916c 100644 --- a/gdk/wayland/gdkglcontext-wayland.c +++ b/gdk/wayland/gdkglcontext-wayland.c @@ -47,6 +47,7 @@ gdk_wayland_gl_context_realize (GdkGLContext *context, GdkWaylandGLContext *context_wayland = GDK_WAYLAND_GL_CONTEXT (context); GdkDisplay *display = gdk_gl_context_get_display (context); GdkGLContext *share = gdk_gl_context_get_shared_context (context); + GdkGLContext *shared_data_context = gdk_surface_get_shared_data_gl_context (gdk_gl_context_get_surface (context)); GdkWaylandDisplay *display_wayland = GDK_WAYLAND_DISPLAY (display); EGLContext ctx; EGLint context_attribs[N_EGL_ATTRS]; @@ -114,7 +115,8 @@ gdk_wayland_gl_context_realize (GdkGLContext *context, ctx = eglCreateContext (display_wayland->egl_display, context_wayland->egl_config, share != NULL ? GDK_WAYLAND_GL_CONTEXT (share)->egl_context - : EGL_NO_CONTEXT, + : shared_data_context != NULL ? GDK_WAYLAND_GL_CONTEXT (shared_data_context)->egl_context + : EGL_NO_CONTEXT, context_attribs); /* If context creation failed without the legacy bit, let's try again with it */ @@ -136,7 +138,8 @@ gdk_wayland_gl_context_realize (GdkGLContext *context, ctx = eglCreateContext (display_wayland->egl_display, context_wayland->egl_config, share != NULL ? GDK_WAYLAND_GL_CONTEXT (share)->egl_context - : EGL_NO_CONTEXT, + : shared_data_context != NULL ? GDK_WAYLAND_GL_CONTEXT (shared_data_context)->egl_context + : EGL_NO_CONTEXT, context_attribs); } diff --git a/gdk/x11/gdkglcontext-x11.c b/gdk/x11/gdkglcontext-x11.c index b95a18517c..1385713609 100644 --- a/gdk/x11/gdkglcontext-x11.c +++ b/gdk/x11/gdkglcontext-x11.c @@ -576,6 +576,7 @@ gdk_x11_gl_context_realize (GdkGLContext *context, Display *dpy; DrawableInfo *info; GdkGLContext *share; + GdkGLContext *shared_data_context; GdkSurface *surface; gboolean debug_bit, compat_bit, legacy_bit, es_bit; int major, minor, flags; @@ -586,6 +587,7 @@ gdk_x11_gl_context_realize (GdkGLContext *context, context_x11 = GDK_X11_GL_CONTEXT (context); display_x11 = GDK_X11_DISPLAY (display); share = gdk_gl_context_get_shared_context (context); + shared_data_context = gdk_surface_get_shared_data_gl_context (surface); gdk_gl_context_get_required_version (context, &major, &minor); debug_bit = gdk_gl_context_get_debug_enabled (context); @@ -625,7 +627,7 @@ gdk_x11_gl_context_realize (GdkGLContext *context, if (legacy_bit && !GDK_X11_DISPLAY (display)->has_glx_create_context) { GDK_DISPLAY_NOTE (display, OPENGL, g_message ("Creating legacy GL context on request")); - context_x11->glx_context = create_legacy_context (display, context_x11->glx_config, share); + context_x11->glx_context = create_legacy_context (display, context_x11->glx_config, share ? share : shared_data_context); } else { @@ -650,14 +652,14 @@ gdk_x11_gl_context_realize (GdkGLContext *context, GDK_DISPLAY_NOTE (display, OPENGL, g_message ("Creating GL3 context")); context_x11->glx_context = create_gl3_context (display, context_x11->glx_config, - share, + share ? share : shared_data_context, profile, flags, major, minor); /* Fall back to legacy in case the GL3 context creation failed */ if (context_x11->glx_context == NULL) { GDK_DISPLAY_NOTE (display, OPENGL, g_message ("Creating fallback legacy context")); - context_x11->glx_context = create_legacy_context (display, context_x11->glx_config, share); + context_x11->glx_context = create_legacy_context (display, context_x11->glx_config, share ? share : shared_data_context); legacy_bit = TRUE; es_bit = FALSE; } diff --git a/gsk/gl/gskgldriver.c b/gsk/gl/gskgldriver.c index 276f01fc6c..321681d507 100644 --- a/gsk/gl/gskgldriver.c +++ b/gsk/gl/gskgldriver.c @@ -669,7 +669,7 @@ gsk_gl_driver_create_render_target (GskGLDriver *self, texture_id = gsk_gl_driver_create_texture (self, width, height); gsk_gl_driver_bind_source_texture (self, texture_id); - gsk_gl_driver_init_texture_empty (self, texture_id); + gsk_gl_driver_init_texture_empty (self, texture_id, GL_NEAREST, GL_NEAREST); render_target = create_render_target (self, texture_id, FALSE, FALSE); @@ -727,7 +727,9 @@ gsk_gl_driver_destroy_texture (GskGLDriver *self, void gsk_gl_driver_init_texture_empty (GskGLDriver *self, - int texture_id) + int texture_id, + int min_filter, + int mag_filter) { Texture *t; @@ -746,6 +748,9 @@ gsk_gl_driver_init_texture_empty (GskGLDriver *self, 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)) diff --git a/gsk/gl/gskgldriverprivate.h b/gsk/gl/gskgldriverprivate.h index 1e57a855f1..83d0c46af9 100644 --- a/gsk/gl/gskgldriverprivate.h +++ b/gsk/gl/gskgldriverprivate.h @@ -53,7 +53,9 @@ void gsk_gl_driver_bind_source_texture (GskGLDriver *driver int texture_id); void gsk_gl_driver_init_texture_empty (GskGLDriver *driver, - int texture_id); + int texture_id, + int min_filter, + int max_filter); void gsk_gl_driver_init_texture_with_surface (GskGLDriver *driver, int texture_id, cairo_surface_t *surface, diff --git a/gsk/gl/gskglglyphcache.c b/gsk/gl/gskglglyphcache.c index 28cb9063e2..6d8872a6cd 100644 --- a/gsk/gl/gskglglyphcache.c +++ b/gsk/gl/gskglglyphcache.c @@ -4,6 +4,7 @@ #include "gskgldriverprivate.h" #include "gskdebugprivate.h" #include "gskprivate.h" +#include "gskgltextureatlasprivate.h" #include "gdk/gdkglcontextprivate.h" @@ -11,20 +12,18 @@ #include #include -/* Parameters for our cache eviction strategy. +/* 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. + * 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_FRAME_AGE frames are considered 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. */ -#define MAX_AGE 60 -#define CHECK_INTERVAL 10 -#define MAX_OLD 0.333 - -#define ATLAS_SIZE 512 +#define MAX_FRAME_AGE (5 * 60) static guint glyph_cache_hash (gconstpointer v); static gboolean glyph_cache_equal (gconstpointer v1, @@ -32,71 +31,47 @@ static gboolean glyph_cache_equal (gconstpointer v1, static void glyph_cache_key_free (gpointer v); static void glyph_cache_value_free (gpointer v); -static GskGLGlyphAtlas * -create_atlas (GskGLGlyphCache *self, - int width, - int height) +GskGLGlyphCache * +gsk_gl_glyph_cache_new (GdkDisplay *display, + GskGLTextureAtlases *atlases) { - GskGLGlyphAtlas *atlas; + GskGLGlyphCache *glyph_cache; - atlas = g_new0 (GskGLGlyphAtlas, 1); - atlas->width = MAX (width, ATLAS_SIZE); - atlas->height = MAX (height, ATLAS_SIZE); - atlas->y0 = 1; - atlas->y = 1; - atlas->x = 1; - atlas->image = NULL; + glyph_cache = g_new0 (GskGLGlyphCache, 1); - GSK_RENDERER_NOTE(self->renderer, GLYPH_CACHE, g_message ("Create atlas %d x %d", atlas->width, atlas->height)); + 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); - return atlas; + glyph_cache->atlases = gsk_gl_texture_atlases_ref (atlases); + + glyph_cache->ref_count = 1; + + return glyph_cache; } -static void -free_atlas (gpointer v) +GskGLGlyphCache * +gsk_gl_glyph_cache_ref (GskGLGlyphCache *self) { - GskGLGlyphAtlas *atlas = v; + self->ref_count++; - if (atlas->image) - { - g_assert (atlas->image->texture_id == 0); - g_free (atlas->image); - } - - g_free (atlas); + return self; } void -gsk_gl_glyph_cache_init (GskGLGlyphCache *self, - GskRenderer *renderer, - GskGLDriver *gl_driver) +gsk_gl_glyph_cache_unref (GskGLGlyphCache *self) { - self->hash_table = g_hash_table_new_full (glyph_cache_hash, glyph_cache_equal, - glyph_cache_key_free, glyph_cache_value_free); - self->atlases = g_ptr_array_new_with_free_func (free_atlas); + g_assert (self->ref_count > 0); - self->renderer = renderer; - self->gl_driver = gl_driver; -} - -void -gsk_gl_glyph_cache_free (GskGLGlyphCache *self) -{ - guint i; - - for (i = 0; i < self->atlases->len; i ++) + if (self->ref_count == 1) { - GskGLGlyphAtlas *atlas = g_ptr_array_index (self->atlases, i); - - if (atlas->image) - { - gsk_gl_image_destroy (atlas->image, self->gl_driver); - atlas->image->texture_id = 0; - } + gsk_gl_texture_atlases_unref (self->atlases); + g_hash_table_unref (self->hash_table); + g_free (self); + return; } - g_ptr_array_unref (self->atlases); - g_hash_table_unref (self->hash_table); + self->ref_count--; } static gboolean @@ -133,82 +108,11 @@ glyph_cache_value_free (gpointer v) g_free (v); } -static void -add_to_cache (GskGLGlyphCache *cache, - GlyphCacheKey *key, - GskGLCachedGlyph *value) -{ - GskGLGlyphAtlas *atlas; - int i; - 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, width + 2, height + 2); - 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->atlas = atlas; - - atlas->pending_glyph.key = key; - atlas->pending_glyph.value = value; - - atlas->x = atlas->x + width + 1; - atlas->y = MAX (atlas->y, atlas->y0 + height + 1); - -#ifdef G_ENABLE_DEBUG - if (GSK_RENDERER_DEBUG_CHECK (cache->renderer, GLYPH_CACHE)) - { - for (i = 0; i < cache->atlases->len; i++) - { - atlas = g_ptr_array_index (cache->atlases, i); - g_message ("atlas %d (%dx%d): %.2g%% old pixels, filled to %d, %d / %d", - i, atlas->width, atlas->height, - 100.0 * (double)atlas->old_pixels / (double)(atlas->width * atlas->height), - atlas->x, atlas->y0, atlas->y); - } - } -#endif -} - static gboolean -render_glyph (const GskGLGlyphAtlas *atlas, - DirtyGlyph *glyph, - GskImageRegion *region) +render_glyph (GlyphCacheKey *key, + GskGLCachedGlyph *value, + GskImageRegion *region) { - GlyphCacheKey *key = glyph->key; - GskGLCachedGlyph *value = glyph->value; cairo_surface_t *surface; cairo_t *cr; cairo_scaled_font_t *scaled_font; @@ -220,16 +124,14 @@ render_glyph (const GskGLGlyphAtlas *atlas, 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 FALSE; + { + 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; - /* TODO: Give glyphs that large their own texture in the proper size. Don't - * put them in the atlas at all. */ - if (surface_width > atlas->width || surface_height > atlas->height) - return FALSE; - 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, @@ -262,43 +164,120 @@ render_glyph (const GskGLGlyphAtlas *atlas, region->height = cairo_image_surface_get_height (surface); region->stride = cairo_image_surface_get_stride (surface); region->data = data; - region->x = (gsize)(value->tx * atlas->width); - region->y = (gsize)(value->ty * atlas->height); + 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_dirty_glyph (GskGLGlyphCache *self, - GskGLGlyphAtlas *atlas) +upload_glyph (GlyphCacheKey *key, + GskGLCachedGlyph *value) { - GskImageRegion region; + GskImageRegion r; - g_assert (atlas->pending_glyph.key != NULL); + gdk_gl_context_push_debug_group_printf (gdk_gl_context_get_current (), + "Uploading glyph %d", + key->glyph); - gdk_gl_context_push_debug_group_printf (gsk_gl_driver_get_gl_context (self->gl_driver), - "Uploading glyph %d", atlas->pending_glyph.key->glyph); - - if (render_glyph (atlas, &atlas->pending_glyph, ®ion)) + if (render_glyph (key, value, &r)) { - - gsk_gl_image_upload_regions (atlas->image, self->gl_driver, 1, ®ion); - - g_free (region.data); + glBindTexture (GL_TEXTURE_2D, value->texture_id); + glTextureSubImage2D (value->texture_id, 0, + r.x, r.y, r.width, r.height, + GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, + r.data); + g_free (r.data); } - gdk_gl_context_pop_debug_group (gsk_gl_driver_get_gl_context (self->gl_driver)); - atlas->pending_glyph.key = NULL; - atlas->pending_glyph.value = NULL; + gdk_gl_context_pop_debug_group (gdk_gl_context_get_current ()); } -const GskGLCachedGlyph * -gsk_gl_glyph_cache_lookup (GskGLGlyphCache *cache, - gboolean create, - PangoFont *font, - PangoGlyph glyph, - float scale) +static void +add_to_cache (GskGLGlyphCache *self, + GlyphCacheKey *key, + GskGLCachedGlyph *value) +{ + const int width = value->draw_width * key->scale / 1024; + const int height = value->draw_height * key->scale / 1024; + 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; + + upload_glyph (key, value); +} + +void +gsk_gl_glyph_cache_get_texture (GskGLDriver *driver, + PangoFont *font, + PangoGlyph glyph, + float scale, + GskGLCachedGlyph *value) +{ + PangoRectangle ink_rect; + GlyphCacheKey key; + int width, height; + guint texture_id; + + pango_font_get_glyph_extents (font, glyph, &ink_rect, NULL); + pango_extents_to_pixels (&ink_rect, NULL); + + key.font = font; + key.glyph = glyph; + key.scale = (guint)(scale * 1024); + + value->atlas = NULL; + value->timestamp = 0; + + 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->tx = 0.0f; + value->ty = 0.0f; + value->tw = 1.0f; + value->th = 1.0f; + + width = value->draw_width * key.scale / 1024; + height = value->draw_height * key.scale / 1024; + + texture_id = gsk_gl_driver_create_texture (driver, width, height); + gsk_gl_driver_bind_source_texture (driver, texture_id); + gsk_gl_driver_init_texture_empty (driver, texture_id, GL_NEAREST, GL_NEAREST); + + value->texture_id = texture_id; + + upload_glyph (&key, value); +} + +gboolean +gsk_gl_glyph_cache_lookup (GskGLGlyphCache *cache, + PangoFont *font, + PangoGlyph glyph, + float scale, + GskGLCachedGlyph *cached_glyph_out) { GskGLCachedGlyph *value; @@ -313,159 +292,104 @@ gsk_gl_glyph_cache_lookup (GskGLGlyphCache *cache, { const guint age = cache->timestamp - value->timestamp; - if (MAX_AGE <= age) + if (age > MAX_FRAME_AGE) { - GskGLGlyphAtlas *atlas = value->atlas; + GskGLTextureAtlas *atlas = value->atlas; - if (atlas) - atlas->old_pixels -= value->draw_width * value->draw_height; + if (atlas && !value->used) + { + gsk_gl_texture_atlas_mark_used (atlas, value->draw_width, value->draw_height); + value->used = TRUE; + } + + value->timestamp = cache->timestamp; } value->timestamp = cache->timestamp; } - if (create && value == NULL) + if (value == NULL) { - GlyphCacheKey *key; PangoRectangle ink_rect; - key = g_new0 (GlyphCacheKey, 1); - value = g_new0 (GskGLCachedGlyph, 1); - pango_font_get_glyph_extents (font, glyph, &ink_rect, NULL); pango_extents_to_pixels (&ink_rect, NULL); + 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->timestamp = cache->timestamp; value->atlas = NULL; /* For now */ - value->scale = (guint)(scale * 1024); - key->font = g_object_ref (font); - key->glyph = glyph; - key->scale = (guint)(scale * 1024); + if (ink_rect.width < 128 && ink_rect.height < 128) + { + GlyphCacheKey *key; - if (ink_rect.width > 0 && ink_rect.height > 0 && key->scale > 0) - add_to_cache (cache, key, value); + key = g_new0 (GlyphCacheKey, 1); - g_hash_table_insert (cache->hash_table, key, value); + key->font = g_object_ref (font); + key->glyph = glyph; + key->scale = (guint)(scale * 1024); + + if (ink_rect.width > 0 && ink_rect.height > 0 && key->scale > 0) + add_to_cache (cache, key, value); + + *cached_glyph_out = *value; + g_hash_table_insert (cache->hash_table, key, value); + } + else + { + *cached_glyph_out = *value; + glyph_cache_value_free (value); + } } - - return value; -} - -GskGLImage * -gsk_gl_glyph_cache_get_glyph_image (GskGLGlyphCache *self, - const GskGLCachedGlyph *glyph) -{ - GskGLGlyphAtlas *atlas = glyph->atlas; - - g_assert (atlas != NULL); - - if (atlas->image == NULL) + else { - atlas->image = g_new0 (GskGLImage, 1); - gsk_gl_image_create (atlas->image, self->gl_driver, atlas->width, atlas->height); - gdk_gl_context_label_object_printf (gsk_gl_driver_get_gl_context (self->gl_driver), - GL_TEXTURE, atlas->image->texture_id, - "Glyph atlas %d", atlas->image->texture_id); + *cached_glyph_out = *value; } - if (atlas->pending_glyph.key != NULL) - upload_dirty_glyph (self, atlas); - - return atlas->image; + return cached_glyph_out->atlas != NULL; } void gsk_gl_glyph_cache_begin_frame (GskGLGlyphCache *self) { - int i; GHashTableIter iter; GlyphCacheKey *key; GskGLCachedGlyph *value; - GHashTable *removed; + guint dropped = 0; self->timestamp++; - if ((self->timestamp - 1) % CHECK_INTERVAL != 0) - return; - - removed = g_hash_table_new (g_direct_hash, g_direct_equal); - - /* look for glyphs that have grown old since last time */ g_hash_table_iter_init (&iter, self->hash_table); while (g_hash_table_iter_next (&iter, (gpointer *)&key, (gpointer *)&value)) { - const guint age = self->timestamp - value->timestamp; + guint pos; - if (MAX_AGE <= age && age < MAX_AGE + CHECK_INTERVAL) + if (!g_ptr_array_find (self->atlases->atlases, value->atlas, &pos)) { - GskGLGlyphAtlas *atlas = value->atlas; - - if (atlas) - atlas->old_pixels += value->draw_width * value->draw_height; + g_hash_table_iter_remove (&iter); + dropped++; } - } - - /* look for atlases to drop, and create a mapping of updated texture indices */ - for (i = self->atlases->len - 1; i >= 0; i--) - { - GskGLGlyphAtlas *atlas = g_ptr_array_index (self->atlases, i); - - if (atlas->old_pixels > MAX_OLD * atlas->width * atlas->height) + else { - GSK_RENDERER_NOTE(self->renderer, GLYPH_CACHE, - g_message ("Dropping atlas %d (%g.2%% old)", - i, 100.0 * (double)atlas->old_pixels / (double)(atlas->width * atlas->height))); + const guint age = self->timestamp - value->timestamp; - if (atlas->image) + if (age > MAX_FRAME_AGE) { - gsk_gl_image_destroy (atlas->image, self->gl_driver); - atlas->image->texture_id = 0; - } + GskGLTextureAtlas *atlas = value->atlas; - g_hash_table_add (removed, atlas); - - g_ptr_array_remove_index (self->atlases, i); - } - } - - if (g_hash_table_size (removed) > 0) - { - guint dropped = 0; - - /* Remove all glyphs whose atlas was removed */ - g_hash_table_iter_init (&iter, self->hash_table); - while (g_hash_table_iter_next (&iter, (gpointer *)&key, (gpointer *)&value)) - { - if (g_hash_table_contains (removed, value->atlas)) - { - g_hash_table_iter_remove (&iter); - dropped++; + if (atlas && value->used) + { + gsk_gl_texture_atlas_mark_unused (atlas, value->draw_width, value->draw_height); + value->used = FALSE; + } } } - - GSK_RENDERER_NOTE(self->renderer, GLYPH_CACHE, if (dropped > 0) g_message ("Dropped %d glyphs", dropped)); } - g_hash_table_unref (removed); - -#if 0 - for (i = 0; i < self->atlases->len; i++) - { - GskGLGlyphAtlas *atlas = g_ptr_array_index (self->atlases, i); - - if (atlas->image) - { - char *filename; - - filename = g_strdup_printf ("glyphatlas%d-%ld.png", i, self->timestamp); - gsk_gl_image_write_to_png (atlas->image, self->gl_driver, filename); - g_free (filename); - } - } -#endif + GSK_NOTE(GLYPH_CACHE, if (dropped > 0) g_message ("Dropped %d glyphs", dropped)); } diff --git a/gsk/gl/gskglglyphcacheprivate.h b/gsk/gl/gskglglyphcacheprivate.h index 00c3dab95c..72fce67539 100644 --- a/gsk/gl/gskglglyphcacheprivate.h +++ b/gsk/gl/gskglglyphcacheprivate.h @@ -3,17 +3,17 @@ #include "gskgldriverprivate.h" #include "gskglimageprivate.h" -#include "gskrendererprivate.h" +#include "gskgltextureatlasprivate.h" #include #include typedef struct { - GskGLDriver *gl_driver; - GskRenderer *renderer; + int ref_count; + GdkDisplay *display; GHashTable *hash_table; - GPtrArray *atlases; + GskGLTextureAtlases *atlases; guint64 timestamp; } GskGLGlyphCache; @@ -25,28 +25,12 @@ typedef struct guint scale; /* times 1024 */ } GlyphCacheKey; -typedef struct _DirtyGlyph DirtyGlyph; typedef struct _GskGLCachedGlyph GskGLCachedGlyph; -struct _DirtyGlyph -{ - GlyphCacheKey *key; - GskGLCachedGlyph *value; -}; - -typedef struct -{ - GskGLImage *image; - int width, height; - int x, y, y0; - guint old_pixels; - - DirtyGlyph pending_glyph; -} GskGLGlyphAtlas; - struct _GskGLCachedGlyph { - GskGLGlyphAtlas *atlas; + GskGLTextureAtlas *atlas; + guint texture_id; float tx; float ty; @@ -58,23 +42,25 @@ struct _GskGLCachedGlyph int draw_width; int draw_height; - float scale; - guint64 timestamp; + guint used: 1; }; -void gsk_gl_glyph_cache_init (GskGLGlyphCache *self, - GskRenderer *renderer, - GskGLDriver *gl_driver); -void gsk_gl_glyph_cache_free (GskGLGlyphCache *self); +GskGLGlyphCache * gsk_gl_glyph_cache_new (GdkDisplay *display, + GskGLTextureAtlases *atlases); +GskGLGlyphCache * gsk_gl_glyph_cache_ref (GskGLGlyphCache *self); +void gsk_gl_glyph_cache_unref (GskGLGlyphCache *self); void gsk_gl_glyph_cache_begin_frame (GskGLGlyphCache *self); -GskGLImage * gsk_gl_glyph_cache_get_glyph_image (GskGLGlyphCache *self, - const GskGLCachedGlyph *glyph); -const GskGLCachedGlyph * gsk_gl_glyph_cache_lookup (GskGLGlyphCache *self, - gboolean create, +gboolean gsk_gl_glyph_cache_lookup (GskGLGlyphCache *self, PangoFont *font, PangoGlyph glyph, - float scale); + float scale, + GskGLCachedGlyph *cached_glyph_out); +void gsk_gl_glyph_cache_get_texture (GskGLDriver *driver, + PangoFont *font, + PangoGlyph glyph, + float scale, + GskGLCachedGlyph *glyph_out); #endif diff --git a/gsk/gl/gskgliconcache.c b/gsk/gl/gskgliconcache.c new file mode 100644 index 0000000000..c0ad9af374 --- /dev/null +++ b/gsk/gl/gskgliconcache.c @@ -0,0 +1,222 @@ +#include "gskgliconcacheprivate.h" +#include "gskgltextureatlasprivate.h" +#include "gdk/gdktextureprivate.h" +#include "gdk/gdkglcontextprivate.h" + +#include + +#define MAX_FRAME_AGE (5 * 60) + +typedef struct +{ + graphene_rect_t texture_rect; + GskGLTextureAtlas *atlas; + int frame_age; /* Number of frames this icon is unused */ + guint used: 1; +} IconData; + +static void +icon_data_free (gpointer p) +{ + g_free (p); +} + +GskGLIconCache * +gsk_gl_icon_cache_new (GdkDisplay *display, + GskGLTextureAtlases *atlases) +{ + GskGLIconCache *self; + + self = g_new0 (GskGLIconCache, 1); + + self->display = display; + self->icons = g_hash_table_new_full (NULL, NULL, NULL, icon_data_free); + self->atlases = gsk_gl_texture_atlases_ref (atlases); + self->ref_count = 1; + + return self; +} + +GskGLIconCache * +gsk_gl_icon_cache_ref (GskGLIconCache *self) +{ + self->ref_count++; + + return self; +} + +void +gsk_gl_icon_cache_unref (GskGLIconCache *self) +{ + g_assert (self->ref_count > 0); + + if (self->ref_count == 1) + { + gsk_gl_texture_atlases_unref (self->atlases); + g_hash_table_unref (self->icons); + g_free (self); + return; + } + + self->ref_count--; +} + +void +gsk_gl_icon_cache_begin_frame (GskGLIconCache *self) +{ + GHashTableIter iter; + GdkTexture *texture; + IconData *icon_data; + + /* Increase frame age of all icons */ + g_hash_table_iter_init (&iter, self->icons); + while (g_hash_table_iter_next (&iter, (gpointer *)&texture, (gpointer *)&icon_data)) + { + guint pos; + + if (!g_ptr_array_find (self->atlases->atlases, icon_data->atlas, &pos)) + { + g_hash_table_iter_remove (&iter); + } + else + { + icon_data->frame_age ++; + + if (icon_data->frame_age > MAX_FRAME_AGE) + { + + if (icon_data->used) + { + const int w = icon_data->texture_rect.size.width * icon_data->atlas->width; + const int h = icon_data->texture_rect.size.height * icon_data->atlas->height; + + gsk_gl_texture_atlas_mark_unused (icon_data->atlas, w + 2, h + 2); + icon_data->used = FALSE; + } + /* We do NOT remove the icon here. Instead, We wait until we drop the entire atlas. + * This way we can revive it when we use it again. */ + } + } + } +} + +/* FIXME: this could probably be done more efficiently */ +static cairo_surface_t * +pad_surface (cairo_surface_t *surface) +{ + cairo_surface_t *padded; + cairo_t *cr; + cairo_pattern_t *pattern; + cairo_matrix_t matrix; + + padded = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, + cairo_image_surface_get_width (surface) + 2, + cairo_image_surface_get_height (surface) + 2); + + cr = cairo_create (padded); + + pattern = cairo_pattern_create_for_surface (surface); + cairo_pattern_set_extend (pattern, CAIRO_EXTEND_PAD); + + cairo_matrix_init_translate (&matrix, -1, -1); + cairo_pattern_set_matrix (pattern, &matrix); + + cairo_set_source (cr, pattern); + cairo_paint (cr); + + cairo_destroy (cr); + cairo_pattern_destroy (pattern); + + return padded; +} + +static void +upload_region_or_else (GskGLIconCache *self, + guint texture_id, + GskImageRegion *region) +{ + glBindTexture (GL_TEXTURE_2D, texture_id); + glTextureSubImage2D (texture_id, 0, region->x, region->y, region->width, region->height, + GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, region->data); +} + +void +gsk_gl_icon_cache_lookup_or_add (GskGLIconCache *self, + GdkTexture *texture, + int *out_texture_id, + graphene_rect_t *out_texture_rect) +{ + IconData *icon_data = g_hash_table_lookup (self->icons, texture); + + if (icon_data) + { + icon_data->frame_age = 0; + if (!icon_data->used) + { + const int w = icon_data->texture_rect.size.width * icon_data->atlas->width; + const int h = icon_data->texture_rect.size.height * icon_data->atlas->height; + + gsk_gl_texture_atlas_mark_used (icon_data->atlas, w + 2, h + 2); + icon_data->used = TRUE; + } + + *out_texture_id = icon_data->atlas->texture_id; + *out_texture_rect = icon_data->texture_rect; + return; + } + + /* texture not on any atlas yet. Find a suitable one. */ + { + const int width = gdk_texture_get_width (texture); + const int height = gdk_texture_get_height (texture); + GskGLTextureAtlas *atlas = NULL; + int packed_x = 0; + int packed_y = 0; + GskImageRegion region; + cairo_surface_t *surface; + cairo_surface_t *padded_surface; + + gsk_gl_texture_atlases_pack (self->atlases, width + 2, height + 2, &atlas, &packed_x, &packed_y); + + icon_data = g_new0 (IconData, 1); + icon_data->atlas = atlas; + icon_data->frame_age = 0; + icon_data->used = TRUE; + graphene_rect_init (&icon_data->texture_rect, + (float)(packed_x + 1) / atlas->width, + (float)(packed_y + 1) / atlas->height, + (float)width / atlas->width, + (float)height / atlas->height); + + g_hash_table_insert (self->icons, texture, icon_data); + + /* actually upload the texture */ + surface = gdk_texture_download_surface (texture); + padded_surface = pad_surface (surface); + region.x = packed_x; + region.y = packed_y; + region.width = width + 2; + region.height = height + 2; + region.data = cairo_image_surface_get_data (padded_surface); + + gdk_gl_context_push_debug_group_printf (gdk_gl_context_get_current (), + "Uploading texture"); + + upload_region_or_else (self, atlas->texture_id, ®ion); + + gdk_gl_context_pop_debug_group (gdk_gl_context_get_current ()); + + *out_texture_id = atlas->texture_id; + *out_texture_rect = icon_data->texture_rect; + + cairo_surface_destroy (surface); + cairo_surface_destroy (padded_surface); + +#if 0 + /* Some obvious debugging */ + static int k; + gsk_gl_image_write_to_png (&atlas->image, self->gl_driver, + g_strdup_printf ("icon%d.png", k ++)); +#endif + } +} diff --git a/gsk/gl/gskgliconcacheprivate.h b/gsk/gl/gskgliconcacheprivate.h new file mode 100644 index 0000000000..ade371b930 --- /dev/null +++ b/gsk/gl/gskgliconcacheprivate.h @@ -0,0 +1,33 @@ +#ifndef __GSK_GL_ICON_CACHE_PRIVATE_H__ +#define __GSK_GL_ICON_CACHE_PRIVATE_H__ + +#include "gskgldriverprivate.h" +#include "gskglimageprivate.h" +#include "gskrendererprivate.h" +#include "gskgltextureatlasprivate.h" +#include +#include + +typedef struct +{ + int ref_count; + + GdkDisplay *display; + GskGLDriver *gl_driver; + + GskGLTextureAtlases *atlases; + GHashTable *icons; /* GdkTexture -> IconData */ + +} GskGLIconCache; + +GskGLIconCache * gsk_gl_icon_cache_new (GdkDisplay *display, + GskGLTextureAtlases *atlases); +GskGLIconCache * gsk_gl_icon_cache_ref (GskGLIconCache *self); +void gsk_gl_icon_cache_unref (GskGLIconCache *self); +void gsk_gl_icon_cache_begin_frame (GskGLIconCache *self); +void gsk_gl_icon_cache_lookup_or_add (GskGLIconCache *self, + GdkTexture *texture, + int *out_texture_id, + graphene_rect_t *out_texture_rect); + +#endif diff --git a/gsk/gl/gskglimage.c b/gsk/gl/gskglimage.c index 0de2e75bdc..71d7bce2a3 100644 --- a/gsk/gl/gskglimage.c +++ b/gsk/gl/gskglimage.c @@ -6,14 +6,16 @@ void gsk_gl_image_create (GskGLImage *self, GskGLDriver *gl_driver, int width, - int height) + int height, + int min_filter, + int mag_filter) { self->texture_id = gsk_gl_driver_create_texture (gl_driver, width, height); self->width = width; self->height = height; gsk_gl_driver_bind_source_texture (gl_driver, self->texture_id); - gsk_gl_driver_init_texture_empty (gl_driver, self->texture_id); + gsk_gl_driver_init_texture_empty (gl_driver, self->texture_id, min_filter, mag_filter); gsk_gl_driver_mark_texture_permanent (gl_driver, self->texture_id); } @@ -22,6 +24,7 @@ gsk_gl_image_destroy (GskGLImage *self, GskGLDriver *gl_driver) { gsk_gl_driver_destroy_texture (gl_driver, self->texture_id); + self->texture_id = 0; } void @@ -43,23 +46,15 @@ gsk_gl_image_write_to_png (const GskGLImage *self, } void -gsk_gl_image_upload_regions (GskGLImage *self, - GskGLDriver *gl_driver, - guint n_regions, - const GskImageRegion *regions) +gsk_gl_image_upload_region (GskGLImage *self, + GskGLDriver *gl_driver, + const GskImageRegion *region) { - guint i; + gsk_gl_driver_bind_source_texture (gl_driver, self->texture_id); + glBindTexture (GL_TEXTURE_2D, self->texture_id); - for (i = 0; i < n_regions; i ++) - { - const GskImageRegion *region = ®ions[i]; - - gsk_gl_driver_bind_source_texture (gl_driver, self->texture_id); - glBindTexture (GL_TEXTURE_2D, self->texture_id); - - glTexSubImage2D (GL_TEXTURE_2D, 0, region->x, region->y, region->width, region->height, - GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, region->data); - } + glTexSubImage2D (GL_TEXTURE_2D, 0, region->x, region->y, region->width, region->height, + GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, region->data); #ifdef G_ENABLE_DEBUG /*gsk_gl_driver_bind_source_texture (gl_driver, self->texture_id);*/ diff --git a/gsk/gl/gskglimageprivate.h b/gsk/gl/gskglimageprivate.h index 547f0cb171..3d706723b8 100644 --- a/gsk/gl/gskglimageprivate.h +++ b/gsk/gl/gskglimageprivate.h @@ -24,16 +24,17 @@ typedef struct void gsk_gl_image_create (GskGLImage *self, GskGLDriver *gl_driver, int width, - int height); + int height, + int min_filter, + int mag_filter); void gsk_gl_image_destroy (GskGLImage *self, GskGLDriver *gl_driver); void gsk_gl_image_write_to_png (const GskGLImage *self, GskGLDriver *gl_driver, const char *filename); -void gsk_gl_image_upload_regions (GskGLImage *self, +void gsk_gl_image_upload_region (GskGLImage *self, GskGLDriver *gl_driver, - guint n_regions, - const GskImageRegion *regions); + const GskImageRegion *region); #endif diff --git a/gsk/gl/gskglrenderer.c b/gsk/gl/gskglrenderer.c index 0cced099f7..9a3bd13e76 100644 --- a/gsk/gl/gskglrenderer.c +++ b/gsk/gl/gskglrenderer.c @@ -11,6 +11,7 @@ #include "gsktransformprivate.h" #include "gskshaderbuilderprivate.h" #include "gskglglyphcacheprivate.h" +#include "gskgliconcacheprivate.h" #include "gskglrenderopsprivate.h" #include "gskcairoblurprivate.h" #include "gskglshadowcacheprivate.h" @@ -66,6 +67,26 @@ typedef enum DUMP_FRAMEBUFFER = 1 << 3 } OffscreenFlags; +typedef struct +{ + int texture_id; + float x; + float y; + float x2; + float y2; +} TextureRegion; + +static inline void +init_full_texture_region (TextureRegion *r, + int texture_id) +{ + r->texture_id = texture_id; + r->x = 0; + r->y = 0; + r->x2 = 1; + r->y2 = 1; +} + static void G_GNUC_UNUSED print_render_node_tree (GskRenderNode *root, int level) { @@ -186,15 +207,6 @@ font_has_color_glyphs (const PangoFont *font) return has_color; } -static void -get_gl_scaling_filters (GskRenderNode *node, - int *min_filter_r, - int *mag_filter_r) -{ - *min_filter_r = GL_LINEAR; - *mag_filter_r = GL_LINEAR; -} - static inline void rgba_to_float (const GdkRGBA *c, float *f) @@ -296,7 +308,7 @@ static void add_offscreen_ops (GskGLRenderer *self, RenderOpBuilder *builder, const graphene_rect_t *bounds, GskRenderNode *child_node, - int *texture_id, + TextureRegion *region_out, gboolean *is_offscreen, guint flags); static void gsk_gl_renderer_add_render_ops (GskGLRenderer *self, @@ -334,7 +346,9 @@ struct _GskGLRenderer RenderOpBuilder op_builder; GArray *render_ops; - GskGLGlyphCache glyph_cache; + GskGLTextureAtlases *atlases; + GskGLGlyphCache *glyph_cache; + GskGLIconCache *icon_cache; GskGLShadowCache shadow_cache; #ifdef G_ENABLE_DEBUG @@ -569,7 +583,7 @@ render_text_node (GskGLRenderer *self, for (i = 0; i < num_glyphs; i++) { const PangoGlyphInfo *gi = &glyphs[i]; - const GskGLCachedGlyph *glyph; + GskGLCachedGlyph glyph; float glyph_x, glyph_y, glyph_w, glyph_h; float tx, ty, tx2, ty2; double cx; @@ -578,31 +592,41 @@ render_text_node (GskGLRenderer *self, if (gi->glyph == PANGO_GLYPH_EMPTY) continue; - glyph = gsk_gl_glyph_cache_lookup (&self->glyph_cache, - TRUE, - (PangoFont *)font, - gi->glyph, - text_scale); + gsk_gl_glyph_cache_lookup (self->glyph_cache, + (PangoFont *)font, + gi->glyph, + text_scale, + &glyph); /* e.g. whitespace */ - if (glyph->draw_width <= 0 || glyph->draw_height <= 0 || glyph->scale <= 0) + if (glyph.draw_width <= 0 || glyph.draw_height <= 0) goto next; + /* big glyphs are not cached */ + if (!glyph.texture_id) + { + gsk_gl_glyph_cache_get_texture (self->gl_driver, + (PangoFont *)font, + gi->glyph, + text_scale, + &glyph); + g_assert (glyph.texture_id != 0); + } + cx = (double)(x_position + gi->geometry.x_offset) / PANGO_SCALE; cy = (double)(gi->geometry.y_offset) / PANGO_SCALE; - ops_set_texture (builder, gsk_gl_glyph_cache_get_glyph_image (&self->glyph_cache, - glyph)->texture_id); + ops_set_texture (builder, glyph.texture_id); - tx = glyph->tx; - ty = glyph->ty; - tx2 = tx + glyph->tw; - ty2 = ty + glyph->th; + tx = glyph.tx; + ty = glyph.ty; + tx2 = tx + glyph.tw; + ty2 = ty + glyph.th; - glyph_x = x + cx + glyph->draw_x; - glyph_y = y + cy + glyph->draw_y; - glyph_w = glyph->draw_width; - glyph_h = glyph->draw_height; + glyph_x = x + cx + glyph.draw_x; + glyph_y = y + cy + glyph.draw_y; + glyph_w = glyph.draw_width; + glyph_h = glyph.draw_height; ops_draw (builder, (GskQuadVertex[GL_N_VERTICES]) { { { glyph_x, glyph_y }, { tx, ty }, }, @@ -776,6 +800,42 @@ render_color_node (GskGLRenderer *self, ops_draw (builder, vertex_data); } +static inline void +upload_texture (GskGLRenderer *self, + GdkTexture *texture, + TextureRegion *out_region) +{ + int texture_id; + + if (texture->width <= 128 && + texture->height <= 128) + { + graphene_rect_t trect; + + gsk_gl_icon_cache_lookup_or_add (self->icon_cache, + texture, + &texture_id, + &trect); + out_region->x = trect.origin.x; + out_region->y = trect.origin.y; + out_region->x2 = out_region->x + trect.size.width; + out_region->y2 = out_region->y + trect.size.height; + } + else + { + texture_id = gsk_gl_driver_get_texture_for_texture (self->gl_driver, + texture, + GL_LINEAR, + GL_LINEAR); + out_region->x = 0; + out_region->y = 0; + out_region->x2 = 1; + out_region->y2 = 1; + } + + out_region->texture_id = texture_id; +} + static inline void render_texture_node (GskGLRenderer *self, GskRenderNode *node, @@ -823,27 +883,21 @@ render_texture_node (GskGLRenderer *self, } else { - int gl_min_filter = GL_NEAREST, gl_mag_filter = GL_NEAREST; - int texture_id; + TextureRegion r; - get_gl_scaling_filters (node, &gl_min_filter, &gl_mag_filter); + upload_texture (self, texture, &r); - texture_id = gsk_gl_driver_get_texture_for_texture (self->gl_driver, - texture, - gl_min_filter, - gl_mag_filter); ops_set_program (builder, &self->blit_program); - ops_set_texture (builder, texture_id); - + ops_set_texture (builder, r.texture_id); ops_draw (builder, (GskQuadVertex[GL_N_VERTICES]) { - { { min_x, min_y }, { 0, 0 }, }, - { { min_x, max_y }, { 0, 1 }, }, - { { max_x, min_y }, { 1, 0 }, }, + { { min_x, min_y }, { r.x, r.y }, }, + { { min_x, max_y }, { r.x, r.y2 }, }, + { { max_x, min_y }, { r.x2, r.y }, }, - { { max_x, max_y }, { 1, 1 }, }, - { { min_x, max_y }, { 0, 1 }, }, - { { max_x, min_y }, { 1, 0 }, }, + { { max_x, max_y }, { r.x2, r.y2 }, }, + { { min_x, max_y }, { r.x, r.y2 }, }, + { { max_x, min_y }, { r.x2, r.y }, }, }); } } @@ -904,7 +958,7 @@ render_transform_node (GskGLRenderer *self, const float min_y = child->bounds.origin.y; const float max_x = min_x + child->bounds.size.width; const float max_y = min_y + child->bounds.size.height; - int texture_id; + TextureRegion region; gboolean is_offscreen; /* For non-trivial transforms, we draw everything on a texture and then * draw the texture transformed. */ @@ -915,24 +969,24 @@ render_transform_node (GskGLRenderer *self, add_offscreen_ops (self, builder, &child->bounds, child, - &texture_id, &is_offscreen, + ®ion, &is_offscreen, RESET_CLIP | RESET_OPACITY); gsk_transform_to_matrix (node_transform, &mat); ops_push_modelview (builder, node_transform); - ops_set_texture (builder, texture_id); + ops_set_texture (builder, region.texture_id); ops_set_program (builder, &self->blit_program); if (is_offscreen) { const GskQuadVertex offscreen_vertex_data[GL_N_VERTICES] = { - { { min_x, min_y }, { 0, 1 }, }, - { { min_x, max_y }, { 0, 0 }, }, - { { max_x, min_y }, { 1, 1 }, }, + { { min_x, min_y }, { region.x, region.y2 }, }, + { { min_x, max_y }, { region.x, region.y }, }, + { { max_x, min_y }, { region.x2, region.y2 }, }, - { { max_x, max_y }, { 1, 0 }, }, - { { min_x, max_y }, { 0, 0 }, }, - { { max_x, min_y }, { 1, 1 }, }, + { { max_x, max_y }, { region.x2, region.y }, }, + { { min_x, max_y }, { region.x, region.y }, }, + { { max_x, min_y }, { region.x2, region.y2 }, }, }; ops_draw (builder, offscreen_vertex_data); @@ -940,13 +994,13 @@ render_transform_node (GskGLRenderer *self, else { const GskQuadVertex onscreen_vertex_data[GL_N_VERTICES] = { - { { min_x, min_y }, { 0, 0 }, }, - { { min_x, max_y }, { 0, 1 }, }, - { { max_x, min_y }, { 1, 0 }, }, + { { min_x, min_y }, { region.x, region.y }, }, + { { min_x, max_y }, { region.x, region.y2 }, }, + { { max_x, min_y }, { region.x2, region.y }, }, - { { max_x, max_y }, { 1, 1 }, }, - { { min_x, max_y }, { 0, 1 }, }, - { { max_x, min_y }, { 1, 0 }, }, + { { max_x, max_y }, { region.x2, region.y2 }, }, + { { min_x, max_y }, { region.x, region.y2 }, }, + { { max_x, min_y }, { region.x2, region.y }, }, }; ops_draw (builder, onscreen_vertex_data); @@ -1206,7 +1260,7 @@ render_rounded_clip_node (GskGLRenderer *self, const float max_y = min_y + node->bounds.size.height; graphene_matrix_t scale_matrix; gboolean is_offscreen; - int texture_id; + TextureRegion region; /* NOTE: We are *not* transforming the clip by the current modelview here. * We instead draw the untransformed clip to a texture and then transform * that texture. @@ -1227,18 +1281,18 @@ render_rounded_clip_node (GskGLRenderer *self, ops_push_clip (builder, &child_clip); add_offscreen_ops (self, builder, &node->bounds, child, - &texture_id, &is_offscreen, + ®ion, &is_offscreen, FORCE_OFFSCREEN | RESET_OPACITY); ops_pop_clip (builder); ops_set_program (builder, &self->blit_program); - ops_set_texture (builder, texture_id); + ops_set_texture (builder, region.texture_id); ops_draw (builder, (GskQuadVertex[GL_N_VERTICES]) { { { min_x, min_y }, { 0, 1 }, }, { { min_x, max_y }, { 0, 0 }, }, { { max_x, min_y }, { 1, 1 }, }, - + { { max_x, max_y }, { 1, 0 }, }, { { min_x, max_y }, { 0, 0 }, }, { { max_x, min_y }, { 1, 1 }, }, @@ -1258,13 +1312,13 @@ render_color_matrix_node (GskGLRenderer *self, const float max_x = min_x + node->bounds.size.width; const float max_y = min_y + node->bounds.size.height; GskRenderNode *child = gsk_color_matrix_node_get_child (node); - int texture_id; + TextureRegion region; gboolean is_offscreen; add_offscreen_ops (self, builder, &node->bounds, child, - &texture_id, &is_offscreen, + ®ion, &is_offscreen, RESET_CLIP | RESET_OPACITY); ops_set_program (builder, &self->color_matrix_program); @@ -1272,25 +1326,35 @@ render_color_matrix_node (GskGLRenderer *self, gsk_color_matrix_node_peek_color_matrix (node), gsk_color_matrix_node_peek_color_offset (node)); - ops_set_texture (builder, texture_id); + ops_set_texture (builder, region.texture_id); if (is_offscreen) { GskQuadVertex offscreen_vertex_data[GL_N_VERTICES] = { - { { min_x, min_y }, { 0, 1 }, }, - { { min_x, max_y }, { 0, 0 }, }, - { { max_x, min_y }, { 1, 1 }, }, + { { min_x, min_y }, { region.x, region.y2 }, }, + { { min_x, max_y }, { region.x, region.y }, }, + { { max_x, min_y }, { region.x2, region.y2 }, }, - { { max_x, max_y }, { 1, 0 }, }, - { { min_x, max_y }, { 0, 0 }, }, - { { max_x, min_y }, { 1, 1 }, }, + { { max_x, max_y }, { region.x2, region.y }, }, + { { min_x, max_y }, { region.x, region.y }, }, + { { max_x, min_y }, { region.x2, region.y2 }, }, }; ops_draw (builder, offscreen_vertex_data); } else { - ops_draw (builder, vertex_data); + const GskQuadVertex onscreen_vertex_data[GL_N_VERTICES] = { + { { min_x, min_y }, { region.x, region.y }, }, + { { min_x, max_y }, { region.x, region.y2 }, }, + { { max_x, min_y }, { region.x2, region.y }, }, + + { { max_x, max_y }, { region.x2, region.y2 }, }, + { { min_x, max_y }, { region.x, region.y2 }, }, + { { max_x, min_y }, { region.x2, region.y }, }, + }; + + ops_draw (builder, onscreen_vertex_data); } } @@ -1305,7 +1369,7 @@ render_blur_node (GskGLRenderer *self, const float max_x = min_x + node->bounds.size.width; const float max_y = min_y + node->bounds.size.height; const float blur_radius = gsk_blur_node_get_radius (node); - int texture_id; + TextureRegion region; gboolean is_offscreen; RenderOp op; @@ -1323,7 +1387,7 @@ render_blur_node (GskGLRenderer *self, add_offscreen_ops (self, builder, &node->bounds, gsk_blur_node_get_child (node), - &texture_id, &is_offscreen, + ®ion, &is_offscreen, RESET_CLIP | FORCE_OFFSCREEN | RESET_OPACITY); ops_set_program (builder, &self->blur_program); @@ -1332,7 +1396,7 @@ render_blur_node (GskGLRenderer *self, op.blur.radius = gsk_blur_node_get_radius (node); ops_add (builder, &op); - ops_set_texture (builder, texture_id); + ops_set_texture (builder, region.texture_id); if (is_offscreen) { @@ -1340,7 +1404,7 @@ render_blur_node (GskGLRenderer *self, { { min_x, min_y }, { 0, 1 }, }, { { min_x, max_y }, { 0, 0 }, }, { { max_x, min_y }, { 1, 1 }, }, - + { { max_x, max_y }, { 1, 0 }, }, { { min_x, max_y }, { 0, 0 }, }, { { max_x, min_y }, { 1, 1 }, }, @@ -1825,7 +1889,7 @@ render_shadow_node (GskGLRenderer *self, const GskShadow *shadow = gsk_shadow_node_peek_shadow (node, i); const float dx = shadow->dx; const float dy = shadow->dy; - int texture_id; + TextureRegion region; gboolean is_offscreen; g_assert (shadow->radius <= 0); @@ -1849,22 +1913,22 @@ render_shadow_node (GskGLRenderer *self, /* Draw the child offscreen, without the offset. */ add_offscreen_ops (self, builder, &shadow_child->bounds, - shadow_child, &texture_id, &is_offscreen, + shadow_child, ®ion, &is_offscreen, RESET_CLIP | RESET_OPACITY); ops_set_program (builder, &self->coloring_program); ops_set_color (builder, &shadow->color); - ops_set_texture (builder, texture_id); + ops_set_texture (builder, region.texture_id); if (is_offscreen) { const GskQuadVertex offscreen_vertex_data[GL_N_VERTICES] = { - { { dx + min_x, dy + min_y }, { 0, 1 }, }, - { { dx + min_x, dy + max_y }, { 0, 0 }, }, - { { dx + max_x, dy + min_y }, { 1, 1 }, }, + { { dx + min_x, dy + min_y }, { region.x, region.y2 }, }, + { { dx + min_x, dy + max_y }, { region.x, region.y }, }, + { { dx + max_x, dy + min_y }, { region.x2, region.y2 }, }, - { { dx + max_x, dy + max_y }, { 1, 0 }, }, - { { dx + min_x, dy + max_y }, { 0, 0 }, }, - { { dx + max_x, dy + min_y }, { 1, 1 }, }, + { { dx + max_x, dy + max_y }, { region.x2, region.y }, }, + { { dx + min_x, dy + max_y }, { region.x, region.y }, }, + { { dx + max_x, dy + min_y }, { region.x2, region.y2 }, }, }; ops_draw (builder, offscreen_vertex_data); @@ -1872,13 +1936,13 @@ render_shadow_node (GskGLRenderer *self, else { const GskQuadVertex onscreen_vertex_data[GL_N_VERTICES] = { - { { dx + min_x, dy + min_y }, { 0, 0 }, }, - { { dx + min_x, dy + max_y }, { 0, 1 }, }, - { { dx + max_x, dy + min_y }, { 1, 0 }, }, + { { dx + min_x, dy + min_y }, { region.x, region.y }, }, + { { dx + min_x, dy + max_y }, { region.x, region.y2 }, }, + { { dx + max_x, dy + min_y }, { region.x2, region.y }, }, - { { dx + max_x, dy + max_y }, { 1, 1 }, }, - { { dx + min_x, dy + max_y }, { 0, 1 }, }, - { { dx + max_x, dy + min_y }, { 1, 0 }, }, + { { dx + max_x, dy + max_y }, { region.x2, region.y2 }, }, + { { dx + min_x, dy + max_y }, { region.x, region.y2 }, }, + { { dx + max_x, dy + min_y }, { region.x2, region.y }, }, }; ops_draw (builder, onscreen_vertex_data); @@ -1901,8 +1965,8 @@ render_cross_fade_node (GskGLRenderer *self, GskRenderNode *start_node = gsk_cross_fade_node_get_start_child (node); GskRenderNode *end_node = gsk_cross_fade_node_get_end_child (node); float progress = gsk_cross_fade_node_get_progress (node); - int start_texture_id; - int end_texture_id; + TextureRegion start_region; + TextureRegion end_region; gboolean is_offscreen1, is_offscreen2; RenderOp op; const GskQuadVertex vertex_data[GL_N_VERTICES] = { @@ -1921,21 +1985,21 @@ render_cross_fade_node (GskGLRenderer *self, add_offscreen_ops (self, builder, &node->bounds, start_node, - &start_texture_id, &is_offscreen1, + &start_region, &is_offscreen1, FORCE_OFFSCREEN | RESET_CLIP | RESET_OPACITY); add_offscreen_ops (self, builder, &node->bounds, end_node, - &end_texture_id, &is_offscreen2, + &end_region, &is_offscreen2, FORCE_OFFSCREEN | RESET_CLIP | RESET_OPACITY); ops_set_program (builder, &self->cross_fade_program); op.op = OP_CHANGE_CROSS_FADE; op.cross_fade.progress = progress; - op.cross_fade.source2 = end_texture_id; + op.cross_fade.source2 = end_region.texture_id; ops_add (builder, &op); - ops_set_texture (builder, start_texture_id); + ops_set_texture (builder, start_region.texture_id); ops_draw (builder, vertex_data); } @@ -1951,8 +2015,8 @@ render_blend_node (GskGLRenderer *self, const float min_y = builder->dy + node->bounds.origin.y; const float max_x = min_x + node->bounds.size.width; const float max_y = min_y + node->bounds.size.height; - int top_texture_id; - int bottom_texture_id; + TextureRegion top_region; + TextureRegion bottom_region; gboolean is_offscreen1, is_offscreen2; RenderOp op; const GskQuadVertex vertex_data[GL_N_VERTICES] = { @@ -1970,19 +2034,19 @@ render_blend_node (GskGLRenderer *self, add_offscreen_ops (self, builder, &node->bounds, bottom_child, - &bottom_texture_id, &is_offscreen1, + &bottom_region, &is_offscreen1, FORCE_OFFSCREEN | RESET_CLIP); add_offscreen_ops (self, builder, &node->bounds, top_child, - &top_texture_id, &is_offscreen2, + &top_region, &is_offscreen2, FORCE_OFFSCREEN | RESET_CLIP); ops_set_program (builder, &self->blend_program); - ops_set_texture (builder, bottom_texture_id); + ops_set_texture (builder, bottom_region.texture_id); op.op = OP_CHANGE_BLEND; - op.blend.source2 = top_texture_id; + op.blend.source2 = top_region.texture_id; op.blend.mode = gsk_blend_node_get_blend_mode (node); ops_add (builder, &op); ops_draw (builder, vertex_data); @@ -2445,6 +2509,68 @@ gsk_gl_renderer_create_programs (GskGLRenderer *self, return TRUE; } +static GskGLTextureAtlases * +get_texture_atlases_for_display (GdkDisplay *display) +{ + GskGLTextureAtlases *atlases; + + if (g_getenv ("GSK_NO_SHARED_CACHES")) + return gsk_gl_texture_atlases_new (); + + atlases = (GskGLTextureAtlases*)g_object_get_data (G_OBJECT (display), "gsk-gl-texture-atlases"); + if (atlases == NULL) + { + atlases = gsk_gl_texture_atlases_new (); + g_object_set_data_full (G_OBJECT (display), "gsk-gl-texture-atlases", + atlases, + (GDestroyNotify) gsk_gl_texture_atlases_unref); + } + + return gsk_gl_texture_atlases_ref (atlases); +} + +static GskGLGlyphCache * +get_glyph_cache_for_display (GdkDisplay *display, + GskGLTextureAtlases *atlases) +{ + GskGLGlyphCache *glyph_cache; + + if (g_getenv ("GSK_NO_SHARED_CACHES")) + return gsk_gl_glyph_cache_new (display, atlases); + + glyph_cache = (GskGLGlyphCache*)g_object_get_data (G_OBJECT (display), "gsk-gl-glyph-cache"); + if (glyph_cache == NULL) + { + glyph_cache = gsk_gl_glyph_cache_new (display, atlases); + g_object_set_data_full (G_OBJECT (display), "gsk-gl-glyph-cache", + glyph_cache, + (GDestroyNotify) gsk_gl_glyph_cache_unref); + } + + return gsk_gl_glyph_cache_ref (glyph_cache); +} + +static GskGLIconCache * +get_icon_cache_for_display (GdkDisplay *display, + GskGLTextureAtlases *atlases) +{ + GskGLIconCache *icon_cache; + + if (g_getenv ("GSK_NO_SHARED_CACHES")) + return gsk_gl_icon_cache_new (display, atlases); + + icon_cache = (GskGLIconCache*)g_object_get_data (G_OBJECT (display), "gsk-gl-icon-cache"); + if (icon_cache == NULL) + { + icon_cache = gsk_gl_icon_cache_new (display, atlases); + g_object_set_data_full (G_OBJECT (display), "gsk-gl-icon-cache", + icon_cache, + (GDestroyNotify) gsk_gl_icon_cache_unref); + } + + return gsk_gl_icon_cache_ref (icon_cache); +} + static gboolean gsk_gl_renderer_realize (GskRenderer *renderer, GdkSurface *surface, @@ -2475,7 +2601,9 @@ gsk_gl_renderer_realize (GskRenderer *renderer, if (!gsk_gl_renderer_create_programs (self, error)) return FALSE; - gsk_gl_glyph_cache_init (&self->glyph_cache, renderer, self->gl_driver); + self->atlases = get_texture_atlases_for_display (gdk_surface_get_display (surface)); + self->glyph_cache = get_glyph_cache_for_display (gdk_surface_get_display (surface), self->atlases); + self->icon_cache = get_icon_cache_for_display (gdk_surface_get_display (surface), self->atlases); gsk_gl_shadow_cache_init (&self->shadow_cache); return TRUE; @@ -2500,7 +2628,9 @@ gsk_gl_renderer_unrealize (GskRenderer *renderer) for (i = 0; i < GL_N_PROGRAMS; i ++) glDeleteProgram (self->programs[i].id); - gsk_gl_glyph_cache_free (&self->glyph_cache); + g_clear_pointer (&self->glyph_cache, gsk_gl_glyph_cache_unref); + g_clear_pointer (&self->icon_cache, gsk_gl_icon_cache_unref); + g_clear_pointer (&self->atlases, gsk_gl_texture_atlases_unref); gsk_gl_shadow_cache_free (&self->shadow_cache, self->gl_driver); g_clear_object (&self->gl_profiler); @@ -2717,7 +2847,7 @@ add_offscreen_ops (GskGLRenderer *self, RenderOpBuilder *builder, const graphene_rect_t *bounds, GskRenderNode *child_node, - int *texture_id_out, + TextureRegion *texture_region_out, gboolean *is_offscreen, guint flags) { @@ -2742,14 +2872,8 @@ add_offscreen_ops (GskGLRenderer *self, (flags & FORCE_OFFSCREEN) == 0) { GdkTexture *texture = gsk_texture_node_get_texture (child_node); - int gl_min_filter = GL_NEAREST, gl_mag_filter = GL_NEAREST; - get_gl_scaling_filters (child_node, &gl_min_filter, &gl_mag_filter); - - *texture_id_out = gsk_gl_driver_get_texture_for_texture (self->gl_driver, - texture, - gl_min_filter, - gl_mag_filter); + upload_texture (self, texture, texture_region_out); *is_offscreen = FALSE; return; } @@ -2760,7 +2884,7 @@ add_offscreen_ops (GskGLRenderer *self, if (cached_id != 0) { - *texture_id_out = cached_id; + init_full_texture_region (texture_region_out, cached_id); /* We didn't render it offscreen, but hand out an offscreen texture id */ *is_offscreen = TRUE; return; @@ -2832,7 +2956,7 @@ add_offscreen_ops (GskGLRenderer *self, ops_set_render_target (builder, prev_render_target); *is_offscreen = TRUE; - *texture_id_out = texture_id; + init_full_texture_region (texture_region_out, texture_id); gsk_gl_driver_set_texture_for_pointer (self->gl_driver, child_node, texture_id); } @@ -3066,7 +3190,9 @@ gsk_gl_renderer_do_render (GskRenderer *renderer, ORTHO_FAR_PLANE); graphene_matrix_scale (&projection, 1, -1, 1); - gsk_gl_glyph_cache_begin_frame (&self->glyph_cache); + gsk_gl_texture_atlases_begin_frame (self->atlases); + gsk_gl_glyph_cache_begin_frame (self->glyph_cache); + gsk_gl_icon_cache_begin_frame (self->icon_cache); gsk_gl_shadow_cache_begin_frame (&self->shadow_cache, self->gl_driver); ops_set_projection (&self->op_builder, &projection); diff --git a/gsk/gl/gskgltextureatlas.c b/gsk/gl/gskgltextureatlas.c new file mode 100644 index 0000000000..04dbb710b6 --- /dev/null +++ b/gsk/gl/gskgltextureatlas.c @@ -0,0 +1,297 @@ + +#include "config.h" +#include "gskgltextureatlasprivate.h" +#include "gskdebugprivate.h" +#include "gdkglcontextprivate.h" +#include + +#define ATLAS_SIZE (512) +#define MAX_OLD_RATIO 0.5 + +static void +free_atlas (gpointer v) +{ + GskGLTextureAtlas *atlas = v; + + gsk_gl_texture_atlas_free (atlas); + + g_free (atlas); +} + +GskGLTextureAtlases * +gsk_gl_texture_atlases_new (void) +{ + GskGLTextureAtlases *self; + + self = g_new (GskGLTextureAtlases, 1); + self->atlases = g_ptr_array_new_with_free_func (free_atlas); + + self->ref_count = 1; + + return self; +} + +GskGLTextureAtlases * +gsk_gl_texture_atlases_ref (GskGLTextureAtlases *self) +{ + self->ref_count++; + + return self; +} + +void +gsk_gl_texture_atlases_unref (GskGLTextureAtlases *self) +{ + g_assert (self->ref_count > 0); + + if (self->ref_count == 1) + { + g_ptr_array_unref (self->atlases); + g_free (self); + return; + } + + self->ref_count--; +} + +#if 0 +static void +write_atlas_to_png (GskGLTextureAtlas *atlas, + const char *filename) +{ + int stride = cairo_format_stride_for_width (CAIRO_FORMAT_ARGB32, atlas->width); + guchar *data = g_malloc (atlas->height * stride); + cairo_surface_t *s; + + glBindTexture (GL_TEXTURE_2D, atlas->texture_id); + glGetTexImage (GL_TEXTURE_2D, 0, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, data); + s = cairo_image_surface_create_for_data (data, CAIRO_FORMAT_ARGB32, atlas->width, atlas->height, stride); + cairo_surface_write_to_png (s, filename); + + cairo_surface_destroy (s); + g_free (data); +} +#endif + +void +gsk_gl_texture_atlases_begin_frame (GskGLTextureAtlases *self) +{ + int i; + + for (i = self->atlases->len - 1; i >= 0; i--) + { + GskGLTextureAtlas *atlas = g_ptr_array_index (self->atlases, i); + + if (gsk_gl_texture_atlas_get_unused_ratio (atlas) > MAX_OLD_RATIO) + { + GSK_NOTE(GLYPH_CACHE, + g_message ("Dropping atlas %d (%g.2%% old)", i, + gsk_gl_texture_atlas_get_unused_ratio (atlas))); + + if (atlas->texture_id != 0) + { + glDeleteTextures (1, &atlas->texture_id); + atlas->texture_id = 0; + } + + g_ptr_array_remove_index (self->atlases, i); + } + } + +#if 0 + { + static guint timestamp; + + timestamp++; + if (timestamp % 10 == 0) + for (i = 0; i < self->atlases->len; i++) + { + GskGLTextureAtlas *atlas = g_ptr_array_index (self->atlases, i); + + if (atlas->texture_id) + { + char *filename; + + filename = g_strdup_printf ("textureatlas%d-%u.png", i, timestamp); + write_atlas_to_png (atlas, filename); + g_free (filename); + } + } + } +#endif +} + +gboolean +gsk_gl_texture_atlases_pack (GskGLTextureAtlases *self, + int width, + int height, + GskGLTextureAtlas **atlas_out, + int *out_x, + int *out_y) +{ + GskGLTextureAtlas *atlas; + int x, y; + int i; + + g_assert (width < ATLAS_SIZE); + g_assert (height < ATLAS_SIZE); + + atlas = NULL; + + for (i = 0; i < self->atlases->len; i++) + { + atlas = g_ptr_array_index (self->atlases, i); + + if (gsk_gl_texture_atlas_pack (atlas, width, height, &x, &y)) + break; + + atlas = NULL; + } + + if (atlas == NULL) + { + /* No atlas has enough space, so create a new one... */ + atlas = g_malloc (sizeof (GskGLTextureAtlas)); + gsk_gl_texture_atlas_init (atlas, ATLAS_SIZE, ATLAS_SIZE); + gsk_gl_texture_atlas_realize (atlas); + g_ptr_array_add (self->atlases, atlas); + + /* Pack it onto that one, which surely has enough space... */ + gsk_gl_texture_atlas_pack (atlas, width, height, &x, &y); + } + + *atlas_out = atlas; + *out_x = x; + *out_y = y; + + return TRUE; +} + +void +gsk_gl_texture_atlas_init (GskGLTextureAtlas *self, + int width, + int height) +{ + memset (self, 0, sizeof (*self)); + + self->texture_id = 0; + self->width = width; + self->height = height; + + /* TODO: We might want to change the strategy about the amount of + * nodes here? stb_rect_pack.h says with is optimal. */ + self->nodes = g_malloc0 (sizeof (struct stbrp_node) * width); + stbrp_init_target (&self->context, + width, height, + self->nodes, + width); + + gsk_gl_texture_atlas_realize (self); +} + +void +gsk_gl_texture_atlas_free (GskGLTextureAtlas *self) +{ + if (self->texture_id != 0) + { + glDeleteTextures (1, &self->texture_id); + self->texture_id = 0; + } + + g_clear_pointer (&self->nodes, g_free); +} + +void +gsk_gl_texture_atlas_mark_unused (GskGLTextureAtlas *self, + int width, + int height) +{ + self->unused_pixels += (width * height); +} + + +void +gsk_gl_texture_atlas_mark_used (GskGLTextureAtlas *self, + int width, + int height) +{ + self->unused_pixels -= (width * height); + + g_assert (self->unused_pixels >= 0); +} + +gboolean +gsk_gl_texture_atlas_pack (GskGLTextureAtlas *self, + int width, + int height, + int *out_x, + int *out_y) +{ + stbrp_rect rect; + + g_assert (out_x); + g_assert (out_y); + + rect.w = width; + rect.h = height; + + stbrp_pack_rects (&self->context, &rect, 1); + + if (rect.was_packed) + { + *out_x = rect.x; + *out_y = rect.y; + } + + return rect.was_packed; +} + +double +gsk_gl_texture_atlas_get_unused_ratio (const GskGLTextureAtlas *self) +{ + if (self->unused_pixels > 0) + return (double)(self->unused_pixels) / (double)(self->width * self->height); + + return 0.0; +} + +/* Not using gdk_gl_driver_create_texture here, since we want + * this texture to survive the driver and stay around until + * the display gets closed. + */ +static guint +create_shared_texture (int width, + int height) +{ + guint texture_id; + + glGenTextures (1, &texture_id); + glBindTexture (GL_TEXTURE_2D, texture_id); + + glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + + 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 (gdk_gl_context_get_use_es (gdk_gl_context_get_current ())) + glTexImage2D (GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL); + else + glTexImage2D (GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_BGRA, GL_UNSIGNED_BYTE, NULL); + + glBindTexture (GL_TEXTURE_2D, 0); + + return texture_id; +} + +void +gsk_gl_texture_atlas_realize (GskGLTextureAtlas *atlas) +{ + if (atlas->texture_id) + return; + + atlas->texture_id = create_shared_texture (atlas->width, atlas->height); + gdk_gl_context_label_object_printf (gdk_gl_context_get_current (), + GL_TEXTURE, atlas->texture_id, + "Glyph atlas %d", atlas->texture_id); +} diff --git a/gsk/gl/gskgltextureatlasprivate.h b/gsk/gl/gskgltextureatlasprivate.h new file mode 100644 index 0000000000..2864fd8d05 --- /dev/null +++ b/gsk/gl/gskgltextureatlasprivate.h @@ -0,0 +1,71 @@ + +#ifndef __GSK_GL_TEXTURE_ATLAS_H__ +#define __GSK_GL_TEXTURE_ATLAS_H__ + +#include "stb_rect_pack.h" +#include "gskglimageprivate.h" +#include "gskgldriverprivate.h" + +struct _GskGLTextureAtlas +{ + struct stbrp_context context; + struct stbrp_node *nodes; + + int width; + int height; + + guint texture_id; + + int unused_pixels; /* Pixels of rects that have been used at some point, + But are now unused. */ + + void *user_data; +}; +typedef struct _GskGLTextureAtlas GskGLTextureAtlas; + +struct _GskGLTextureAtlases +{ + int ref_count; + + GPtrArray *atlases; +}; +typedef struct _GskGLTextureAtlases GskGLTextureAtlases; + +GskGLTextureAtlases *gsk_gl_texture_atlases_new (void); +GskGLTextureAtlases *gsk_gl_texture_atlases_ref (GskGLTextureAtlases *atlases); +void gsk_gl_texture_atlases_unref (GskGLTextureAtlases *atlases); + +void gsk_gl_texture_atlases_begin_frame (GskGLTextureAtlases *atlases); +gboolean gsk_gl_texture_atlases_pack (GskGLTextureAtlases *atlases, + int width, + int height, + GskGLTextureAtlas **atlas_out, + int *out_x, + int *out_y); + +void gsk_gl_texture_atlas_init (GskGLTextureAtlas *self, + int width, + int height); + +void gsk_gl_texture_atlas_free (GskGLTextureAtlas *self); + +void gsk_gl_texture_atlas_realize (GskGLTextureAtlas *self); + +void gsk_gl_texture_atlas_mark_unused (GskGLTextureAtlas *self, + int width, + int height); + +void gsk_gl_texture_atlas_mark_used (GskGLTextureAtlas *self, + int width, + int height); + + +gboolean gsk_gl_texture_atlas_pack (GskGLTextureAtlas *self, + int width, + int height, + int *out_x, + int *out_y); + +double gsk_gl_texture_atlas_get_unused_ratio (const GskGLTextureAtlas *self); + +#endif diff --git a/gsk/gl/stb_rect_pack.c b/gsk/gl/stb_rect_pack.c new file mode 100644 index 0000000000..8305e860de --- /dev/null +++ b/gsk/gl/stb_rect_pack.c @@ -0,0 +1,431 @@ + +#include "stb_rect_pack.h" +#define STB_RECT_PACK_IMPLEMENTATION +////////////////////////////////////////////////////////////////////////////// +// +// IMPLEMENTATION SECTION +// + +#ifdef STB_RECT_PACK_IMPLEMENTATION +#ifndef STBRP_SORT +#include +#define STBRP_SORT qsort +#endif + +#ifndef STBRP_ASSERT +#include +#define STBRP_ASSERT assert +#endif + +#ifdef _MSC_VER +#define STBRP__NOTUSED(v) (void)(v) +#else +#define STBRP__NOTUSED(v) (void)sizeof(v) +#endif + +enum +{ + STBRP__INIT_skyline = 1 +}; + +STBRP_DEF void stbrp_setup_heuristic(stbrp_context *context, int heuristic) +{ + switch (context->init_mode) { + case STBRP__INIT_skyline: + STBRP_ASSERT(heuristic == STBRP_HEURISTIC_Skyline_BL_sortHeight || heuristic == STBRP_HEURISTIC_Skyline_BF_sortHeight); + context->heuristic = heuristic; + break; + default: + STBRP_ASSERT(0); + } +} + +STBRP_DEF void stbrp_setup_allow_out_of_mem(stbrp_context *context, int allow_out_of_mem) +{ + if (allow_out_of_mem) + // if it's ok to run out of memory, then don't bother aligning them; + // this gives better packing, but may fail due to OOM (even though + // the rectangles easily fit). @TODO a smarter approach would be to only + // quantize once we've hit OOM, then we could get rid of this parameter. + context->align = 1; + else { + // if it's not ok to run out of memory, then quantize the widths + // so that num_nodes is always enough nodes. + // + // I.e. num_nodes * align >= width + // align >= width / num_nodes + // align = ceil(width/num_nodes) + + context->align = (context->width + context->num_nodes-1) / context->num_nodes; + } +} + +STBRP_DEF void stbrp_init_target(stbrp_context *context, int width, int height, stbrp_node *nodes, int num_nodes) +{ + int i; +#ifndef STBRP_LARGE_RECTS + STBRP_ASSERT(width <= 0xffff && height <= 0xffff); +#endif + + for (i=0; i < num_nodes-1; ++i) + nodes[i].next = &nodes[i+1]; + nodes[i].next = NULL; + context->init_mode = STBRP__INIT_skyline; + context->heuristic = STBRP_HEURISTIC_Skyline_default; + context->free_head = &nodes[0]; + context->active_head = &context->extra[0]; + context->width = width; + context->height = height; + context->num_nodes = num_nodes; + stbrp_setup_allow_out_of_mem(context, 0); + + // node 0 is the full width, node 1 is the sentinel (lets us not store width explicitly) + context->extra[0].x = 0; + context->extra[0].y = 0; + context->extra[0].next = &context->extra[1]; + context->extra[1].x = (stbrp_coord) width; +#ifdef STBRP_LARGE_RECTS + context->extra[1].y = (1<<30); +#else + context->extra[1].y = 65535; +#endif + context->extra[1].next = NULL; +} + +// find minimum y position if it starts at x1 +static int stbrp__skyline_find_min_y(stbrp_context *c, stbrp_node *first, int x0, int width, int *pwaste) +{ + stbrp_node *node = first; + int x1 = x0 + width; + int min_y, visited_width, waste_area; + + STBRP__NOTUSED(c); + + STBRP_ASSERT(first->x <= x0); + + #if 0 + // skip in case we're past the node + while (node->next->x <= x0) + ++node; + #else + STBRP_ASSERT(node->next->x > x0); // we ended up handling this in the caller for efficiency + #endif + + STBRP_ASSERT(node->x <= x0); + + min_y = 0; + waste_area = 0; + visited_width = 0; + while (node->x < x1) { + if (node->y > min_y) { + // raise min_y higher. + // we've accounted for all waste up to min_y, + // but we'll now add more waste for everything we've visted + waste_area += visited_width * (node->y - min_y); + min_y = node->y; + // the first time through, visited_width might be reduced + if (node->x < x0) + visited_width += node->next->x - x0; + else + visited_width += node->next->x - node->x; + } else { + // add waste area + int under_width = node->next->x - node->x; + if (under_width + visited_width > width) + under_width = width - visited_width; + waste_area += under_width * (min_y - node->y); + visited_width += under_width; + } + node = node->next; + } + + *pwaste = waste_area; + return min_y; +} + +typedef struct +{ + int x,y; + stbrp_node **prev_link; +} stbrp__findresult; + +static stbrp__findresult stbrp__skyline_find_best_pos(stbrp_context *c, int width, int height) +{ + int best_waste = (1<<30), best_x, best_y = (1 << 30); + stbrp__findresult fr; + stbrp_node **prev, *node, *tail, **best = NULL; + + // align to multiple of c->align + width = (width + c->align - 1); + width -= width % c->align; + STBRP_ASSERT(width % c->align == 0); + + node = c->active_head; + prev = &c->active_head; + while (node->x + width <= c->width) { + int y,waste; + y = stbrp__skyline_find_min_y(c, node, node->x, width, &waste); + if (c->heuristic == STBRP_HEURISTIC_Skyline_BL_sortHeight) { // actually just want to test BL + // bottom left + if (y < best_y) { + best_y = y; + best = prev; + } + } else { + // best-fit + if (y + height <= c->height) { + // can only use it if it first vertically + if (y < best_y || (y == best_y && waste < best_waste)) { + best_y = y; + best_waste = waste; + best = prev; + } + } + } + prev = &node->next; + node = node->next; + } + + best_x = (best == NULL) ? 0 : (*best)->x; + + // if doing best-fit (BF), we also have to try aligning right edge to each node position + // + // e.g, if fitting + // + // ____________________ + // |____________________| + // + // into + // + // | | + // | ____________| + // |____________| + // + // then right-aligned reduces waste, but bottom-left BL is always chooses left-aligned + // + // This makes BF take about 2x the time + + if (c->heuristic == STBRP_HEURISTIC_Skyline_BF_sortHeight) { + tail = c->active_head; + node = c->active_head; + prev = &c->active_head; + // find first node that's admissible + while (tail->x < width) + tail = tail->next; + while (tail) { + int xpos = tail->x - width; + int y,waste; + STBRP_ASSERT(xpos >= 0); + // find the left position that matches this + while (node->next->x <= xpos) { + prev = &node->next; + node = node->next; + } + STBRP_ASSERT(node->next->x > xpos && node->x <= xpos); + y = stbrp__skyline_find_min_y(c, node, xpos, width, &waste); + if (y + height < c->height) { + if (y <= best_y) { + if (y < best_y || waste < best_waste || (waste==best_waste && xpos < best_x)) { + best_x = xpos; + STBRP_ASSERT(y <= best_y); + best_y = y; + best_waste = waste; + best = prev; + } + } + } + tail = tail->next; + } + } + + fr.prev_link = best; + fr.x = best_x; + fr.y = best_y; + return fr; +} + +static stbrp__findresult stbrp__skyline_pack_rectangle(stbrp_context *context, int width, int height) +{ + // find best position according to heuristic + stbrp__findresult res = stbrp__skyline_find_best_pos(context, width, height); + stbrp_node *node, *cur; + + // bail if: + // 1. it failed + // 2. the best node doesn't fit (we don't always check this) + // 3. we're out of memory + if (res.prev_link == NULL || res.y + height > context->height || context->free_head == NULL) { + res.prev_link = NULL; + return res; + } + + // on success, create new node + node = context->free_head; + node->x = (stbrp_coord) res.x; + node->y = (stbrp_coord) (res.y + height); + + context->free_head = node->next; + + // insert the new node into the right starting point, and + // let 'cur' point to the remaining nodes needing to be + // stiched back in + + cur = *res.prev_link; + if (cur->x < res.x) { + // preserve the existing one, so start testing with the next one + stbrp_node *next = cur->next; + cur->next = node; + cur = next; + } else { + *res.prev_link = node; + } + + // from here, traverse cur and free the nodes, until we get to one + // that shouldn't be freed + while (cur->next && cur->next->x <= res.x + width) { + stbrp_node *next = cur->next; + // move the current node to the free list + cur->next = context->free_head; + context->free_head = cur; + cur = next; + } + + // stitch the list back in + node->next = cur; + + if (cur->x < res.x + width) + cur->x = (stbrp_coord) (res.x + width); + +#ifdef _DEBUG + cur = context->active_head; + while (cur->x < context->width) { + STBRP_ASSERT(cur->x < cur->next->x); + cur = cur->next; + } + STBRP_ASSERT(cur->next == NULL); + + { + int count=0; + cur = context->active_head; + while (cur) { + cur = cur->next; + ++count; + } + cur = context->free_head; + while (cur) { + cur = cur->next; + ++count; + } + STBRP_ASSERT(count == context->num_nodes+2); + } +#endif + + return res; +} + +static int rect_height_compare(const void *a, const void *b) +{ + const stbrp_rect *p = (const stbrp_rect *) a; + const stbrp_rect *q = (const stbrp_rect *) b; + if (p->h > q->h) + return -1; + if (p->h < q->h) + return 1; + return (p->w > q->w) ? -1 : (p->w < q->w); +} + +static int rect_original_order(const void *a, const void *b) +{ + const stbrp_rect *p = (const stbrp_rect *) a; + const stbrp_rect *q = (const stbrp_rect *) b; + return (p->was_packed < q->was_packed) ? -1 : (p->was_packed > q->was_packed); +} + +#ifdef STBRP_LARGE_RECTS +#define STBRP__MAXVAL 0xffffffff +#else +#define STBRP__MAXVAL 0xffff +#endif + +STBRP_DEF int stbrp_pack_rects(stbrp_context *context, stbrp_rect *rects, int num_rects) +{ + int i, all_rects_packed = 1; + + // we use the 'was_packed' field internally to allow sorting/unsorting + for (i=0; i < num_rects; ++i) { + rects[i].was_packed = i; + } + + // sort according to heuristic + STBRP_SORT(rects, num_rects, sizeof(rects[0]), rect_height_compare); + + for (i=0; i < num_rects; ++i) { + if (rects[i].w == 0 || rects[i].h == 0) { + rects[i].x = rects[i].y = 0; // empty rect needs no space + } else { + stbrp__findresult fr = stbrp__skyline_pack_rectangle(context, rects[i].w, rects[i].h); + if (fr.prev_link) { + rects[i].x = (stbrp_coord) fr.x; + rects[i].y = (stbrp_coord) fr.y; + } else { + rects[i].x = rects[i].y = STBRP__MAXVAL; + } + } + } + + // unsort + STBRP_SORT(rects, num_rects, sizeof(rects[0]), rect_original_order); + + // set was_packed flags and all_rects_packed status + for (i=0; i < num_rects; ++i) { + rects[i].was_packed = !(rects[i].x == STBRP__MAXVAL && rects[i].y == STBRP__MAXVAL); + if (!rects[i].was_packed) + all_rects_packed = 0; + } + + // return the all_rects_packed status + return all_rects_packed; +} +#endif + +/* +------------------------------------------------------------------------------ +This software is available under 2 licenses -- choose whichever you prefer. +------------------------------------------------------------------------------ +ALTERNATIVE A - MIT License +Copyright (c) 2017 Sean Barrett +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +------------------------------------------------------------------------------ +ALTERNATIVE B - Public Domain (www.unlicense.org) +This is free and unencumbered software released into the public domain. +Anyone is free to copy, modify, publish, use, compile, sell, or distribute this +software, either in source code form or as a compiled binary, for any purpose, +commercial or non-commercial, and by any means. +In jurisdictions that recognize copyright laws, the author or authors of this +software dedicate any and all copyright interest in the software to the public +domain. We make this dedication for the benefit of the public at large and to +the detriment of our heirs and successors. We intend this dedication to be an +overt act of relinquishment in perpetuity of all present and future rights to +this software under copyright law. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +------------------------------------------------------------------------------ +*/ diff --git a/gsk/gl/stb_rect_pack.h b/gsk/gl/stb_rect_pack.h new file mode 100644 index 0000000000..3ecb1c2fa2 --- /dev/null +++ b/gsk/gl/stb_rect_pack.h @@ -0,0 +1,191 @@ +// stb_rect_pack.h - v0.99 - public domain - rectangle packing +// Sean Barrett 2014 +// +// Useful for e.g. packing rectangular textures into an atlas. +// Does not do rotation. +// +// Not necessarily the awesomest packing method, but better than +// the totally naive one in stb_truetype (which is primarily what +// this is meant to replace). +// +// Has only had a few tests run, may have issues. +// +// More docs to come. +// +// No memory allocations; uses qsort() and assert() from stdlib. +// Can override those by defining STBRP_SORT and STBRP_ASSERT. +// +// This library currently uses the Skyline Bottom-Left algorithm. +// +// Please note: better rectangle packers are welcome! Please +// implement them to the same API, but with a different init +// function. +// +// Credits +// +// Library +// Sean Barrett +// Minor features +// Martins Mozeiko +// github:IntellectualKitty +// +// Bugfixes / warning fixes +// Jeremy Jaussaud +// +// Version history: +// +// 0.99 (2019-02-07) warning fixes +// 0.11 (2017-03-03) return packing success/fail result +// 0.10 (2016-10-25) remove cast-away-const to avoid warnings +// 0.09 (2016-08-27) fix compiler warnings +// 0.08 (2015-09-13) really fix bug with empty rects (w=0 or h=0) +// 0.07 (2015-09-13) fix bug with empty rects (w=0 or h=0) +// 0.06 (2015-04-15) added STBRP_SORT to allow replacing qsort +// 0.05: added STBRP_ASSERT to allow replacing assert +// 0.04: fixed minor bug in STBRP_LARGE_RECTS support +// 0.01: initial release +// +// LICENSE +// +// See end of file for license information. + +////////////////////////////////////////////////////////////////////////////// +// +// INCLUDE SECTION +// + +#ifndef STB_INCLUDE_STB_RECT_PACK_H +#define STB_INCLUDE_STB_RECT_PACK_H + +#define STB_RECT_PACK_VERSION 1 + +#ifdef STBRP_STATIC +#define STBRP_DEF static +#else +#define STBRP_DEF extern +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct stbrp_context stbrp_context; +typedef struct stbrp_node stbrp_node; +typedef struct stbrp_rect stbrp_rect; + +#ifdef STBRP_LARGE_RECTS +typedef int stbrp_coord; +#else +typedef unsigned short stbrp_coord; +#endif + +STBRP_DEF int stbrp_pack_rects (stbrp_context *context, stbrp_rect *rects, int num_rects); +// Assign packed locations to rectangles. The rectangles are of type +// 'stbrp_rect' defined below, stored in the array 'rects', and there +// are 'num_rects' many of them. +// +// Rectangles which are successfully packed have the 'was_packed' flag +// set to a non-zero value and 'x' and 'y' store the minimum location +// on each axis (i.e. bottom-left in cartesian coordinates, top-left +// if you imagine y increasing downwards). Rectangles which do not fit +// have the 'was_packed' flag set to 0. +// +// You should not try to access the 'rects' array from another thread +// while this function is running, as the function temporarily reorders +// the array while it executes. +// +// To pack into another rectangle, you need to call stbrp_init_target +// again. To continue packing into the same rectangle, you can call +// this function again. Calling this multiple times with multiple rect +// arrays will probably produce worse packing results than calling it +// a single time with the full rectangle array, but the option is +// available. +// +// The function returns 1 if all of the rectangles were successfully +// packed and 0 otherwise. + +struct stbrp_rect +{ + // reserved for your use: + int id; + + // input: + stbrp_coord w, h; + + // output: + stbrp_coord x, y; + int was_packed; // non-zero if valid packing + +}; // 16 bytes, nominally + + +STBRP_DEF void stbrp_init_target (stbrp_context *context, int width, int height, stbrp_node *nodes, int num_nodes); +// Initialize a rectangle packer to: +// pack a rectangle that is 'width' by 'height' in dimensions +// using temporary storage provided by the array 'nodes', which is 'num_nodes' long +// +// You must call this function every time you start packing into a new target. +// +// There is no "shutdown" function. The 'nodes' memory must stay valid for +// the following stbrp_pack_rects() call (or calls), but can be freed after +// the call (or calls) finish. +// +// Note: to guarantee best results, either: +// 1. make sure 'num_nodes' >= 'width' +// or 2. call stbrp_allow_out_of_mem() defined below with 'allow_out_of_mem = 1' +// +// If you don't do either of the above things, widths will be quantized to multiples +// of small integers to guarantee the algorithm doesn't run out of temporary storage. +// +// If you do #2, then the non-quantized algorithm will be used, but the algorithm +// may run out of temporary storage and be unable to pack some rectangles. + +STBRP_DEF void stbrp_setup_allow_out_of_mem (stbrp_context *context, int allow_out_of_mem); +// Optionally call this function after init but before doing any packing to +// change the handling of the out-of-temp-memory scenario, described above. +// If you call init again, this will be reset to the default (false). + + +STBRP_DEF void stbrp_setup_heuristic (stbrp_context *context, int heuristic); +// Optionally select which packing heuristic the library should use. Different +// heuristics will produce better/worse results for different data sets. +// If you call init again, this will be reset to the default. + +enum +{ + STBRP_HEURISTIC_Skyline_default=0, + STBRP_HEURISTIC_Skyline_BL_sortHeight = STBRP_HEURISTIC_Skyline_default, + STBRP_HEURISTIC_Skyline_BF_sortHeight +}; + + +////////////////////////////////////////////////////////////////////////////// +// +// the details of the following structures don't matter to you, but they must +// be visible so you can handle the memory allocations for them + +struct stbrp_node +{ + stbrp_coord x,y; + stbrp_node *next; +}; + +struct stbrp_context +{ + int width; + int height; + int align; + int init_mode; + int heuristic; + int num_nodes; + stbrp_node *active_head; + stbrp_node *free_head; + stbrp_node extra[2]; // we allocate two extra nodes so optimal user-node-count is 'width' not 'width+2' +}; + +#ifdef __cplusplus +} +#endif + +#endif + diff --git a/gsk/meson.build b/gsk/meson.build index 55064806b6..dd8a4c967b 100644 --- a/gsk/meson.build +++ b/gsk/meson.build @@ -45,6 +45,9 @@ gsk_private_sources = files([ 'gl/gskglrenderops.c', 'gl/gskglshadowcache.c', 'gl/gskglnodesample.c', + 'gl/gskgltextureatlas.c', + 'gl/gskgliconcache.c', + 'gl/stb_rect_pack.c', ]) gsk_public_headers = files([ diff --git a/tests/testblur.c b/tests/testblur.c index bbbf888da5..028356d1e6 100644 --- a/tests/testblur.c +++ b/tests/testblur.c @@ -36,7 +36,9 @@ snapshot_blur (GtkWidget *widget, static void -gtk_blur_box_init (GtkBlurBox *box) {} +gtk_blur_box_init (GtkBlurBox *box) { + box->radius = 1; +} static void gtk_blur_box_class_init (GtkBlurBoxClass *klass) @@ -99,7 +101,7 @@ main (int argc, char **argv) gtk_container_add (GTK_CONTAINER (blur_box), value_label); - scale = gtk_scale_new_with_range (GTK_ORIENTATION_HORIZONTAL, 0, 10, 0.5); + scale = gtk_scale_new_with_range (GTK_ORIENTATION_HORIZONTAL, 0, 10, 0.05); gtk_widget_set_size_request (scale, 200, -1); gtk_widget_set_halign (scale, GTK_ALIGN_CENTER); gtk_widget_set_valign (scale, GTK_ALIGN_CENTER);