mirror of
https://gitlab.gnome.org/GNOME/gtk.git
synced 2025-01-14 06:10:21 +00:00
c64836e1c9
This moves a lot of the texture atlas control out of the driver and into the various texture libraries through their base GskGLTextureLibrary class. Additionally, this gives more control to libraries on allocating which can be necessary for some tooling such as future Glyphy integration. As part of this, the 1x1 pixel initialization is moved to the Glyph library which is the only place where it is actually needed. The compact vfunc now is responsible for compaction and it allows for us to iterate the atlas hashtable a single time instead of twice as we were doing previously. The init_atlas vfunc is used to do per-library initialization such as adding a 1x1 pixel in the Glyph cache used for coloring lines. The allocate vfunc purely allocates but does no upload. This can be useful for situations where a library wants to reuse the allocator from the base class but does not want to actually insert a key/value entry. The glyph library uses this for it's 1x1 pixel. In the future, we will also likely want to decouple the rectangle packing implementation from the atlas structure, or at least move it into a union so that we do not allocate unused memory for alternate allocators.
581 lines
18 KiB
C
581 lines
18 KiB
C
/* gskgltexturelibrary.c
|
|
*
|
|
* Copyright 2020 Christian Hergert <chergert@redhat.com>
|
|
*
|
|
* This library is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU Lesser General Public
|
|
* License as published by the Free Software Foundation; either
|
|
* version 2.1 of the License, or (at your option) any later version.
|
|
*
|
|
* This library is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
* Lesser General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Lesser General Public
|
|
* License along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*
|
|
* SPDX-License-Identifier: LGPL-2.1-or-later
|
|
*/
|
|
|
|
#include "config.h"
|
|
|
|
#include <gdk/gdkglcontextprivate.h>
|
|
#include <gsk/gskdebugprivate.h>
|
|
|
|
#include "gskglcommandqueueprivate.h"
|
|
#include "gskgldriverprivate.h"
|
|
#include "gskgltexturelibraryprivate.h"
|
|
|
|
#define DEFAULT_MAX_FRAME_AGE 60
|
|
#define DEFAULT_ATLAS_WIDTH 512
|
|
#define DEFAULT_ATLAS_HEIGHT 512
|
|
#define MAX_OLD_RATIO 0.5
|
|
|
|
G_DEFINE_ABSTRACT_TYPE (GskGLTextureLibrary, gsk_gl_texture_library, G_TYPE_OBJECT)
|
|
|
|
enum {
|
|
PROP_0,
|
|
PROP_DRIVER,
|
|
N_PROPS
|
|
};
|
|
|
|
static GParamSpec *properties [N_PROPS];
|
|
|
|
static void
|
|
gsk_gl_texture_atlas_free (GskGLTextureAtlas *atlas)
|
|
{
|
|
if (atlas->texture_id != 0)
|
|
{
|
|
glDeleteTextures (1, &atlas->texture_id);
|
|
atlas->texture_id = 0;
|
|
}
|
|
|
|
g_clear_pointer (&atlas->nodes, g_free);
|
|
g_slice_free (GskGLTextureAtlas, atlas);
|
|
}
|
|
|
|
static gboolean
|
|
gsk_gl_texture_library_real_compact (GskGLTextureLibrary *self,
|
|
gint64 frame_id)
|
|
{
|
|
GPtrArray *removed = NULL;
|
|
gboolean ret = FALSE;
|
|
gboolean periodic_scan;
|
|
|
|
g_assert (GSK_IS_GL_TEXTURE_LIBRARY (self));
|
|
|
|
periodic_scan = (self->max_frame_age > 0 &&
|
|
(frame_id % self->max_frame_age) == 0);
|
|
|
|
for (guint i = self->atlases->len; i > 0; i--)
|
|
{
|
|
GskGLTextureAtlas *atlas = g_ptr_array_index (self->atlases, i - 1);
|
|
|
|
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,
|
|
100.0 * gsk_gl_texture_atlas_get_unused_ratio (atlas)));
|
|
if (removed == NULL)
|
|
removed = g_ptr_array_new_with_free_func ((GDestroyNotify)gsk_gl_texture_atlas_free);
|
|
g_ptr_array_add (removed, g_ptr_array_steal_index (self->atlases, i - 1));
|
|
}
|
|
}
|
|
|
|
if (periodic_scan || removed != NULL)
|
|
{
|
|
GskGLTextureAtlasEntry *entry;
|
|
GHashTableIter iter;
|
|
guint dropped = 0;
|
|
guint atlased = 0;
|
|
|
|
g_hash_table_iter_init (&iter, self->hash_table);
|
|
while (g_hash_table_iter_next (&iter, NULL, (gpointer *)&entry))
|
|
{
|
|
if (entry->is_atlased)
|
|
{
|
|
if (removed && g_ptr_array_find (removed, entry->atlas, NULL))
|
|
{
|
|
g_hash_table_iter_remove (&iter);
|
|
dropped++;
|
|
}
|
|
else if (periodic_scan)
|
|
{
|
|
gsk_gl_texture_atlas_entry_mark_unused (entry);
|
|
entry->accessed = FALSE;
|
|
if (entry->is_atlased)
|
|
atlased++;
|
|
}
|
|
}
|
|
else if (!entry->accessed)
|
|
{
|
|
gsk_gl_driver_release_texture (self->driver, entry->texture);
|
|
g_hash_table_iter_remove (&iter);
|
|
dropped++;
|
|
}
|
|
}
|
|
|
|
GSK_NOTE (GLYPH_CACHE, g_message ("%s: Dropped %d individual items",
|
|
G_OBJECT_TYPE_NAME (self),
|
|
dropped);
|
|
g_message ("%s: %d items cached (%d atlased, %d individually)",
|
|
G_OBJECT_TYPE_NAME (self),
|
|
g_hash_table_size (self->hash_table),
|
|
atlased,
|
|
g_hash_table_size (self->hash_table) - atlased));
|
|
|
|
if (dropped > 0)
|
|
gsk_gl_texture_library_clear_cache (self);
|
|
|
|
ret = TRUE;
|
|
|
|
g_clear_pointer (&removed, g_ptr_array_unref);
|
|
}
|
|
|
|
GSK_NOTE (GLYPH_CACHE, {
|
|
static gint64 last_message;
|
|
gint64 now = g_get_monotonic_time ();
|
|
if (now - last_message > G_USEC_PER_SEC)
|
|
{
|
|
last_message = now;
|
|
g_message ("%s contains %d atlases",
|
|
G_OBJECT_TYPE_NAME (self),
|
|
self->atlases->len);
|
|
}
|
|
});
|
|
|
|
return ret;
|
|
}
|
|
|
|
static gboolean
|
|
gsk_gl_texture_library_real_allocate (GskGLTextureLibrary *self,
|
|
GskGLTextureAtlas *atlas,
|
|
int width,
|
|
int height,
|
|
int *out_x,
|
|
int *out_y)
|
|
{
|
|
stbrp_rect rect;
|
|
|
|
g_assert (GSK_IS_GL_TEXTURE_LIBRARY (self));
|
|
g_assert (atlas != NULL);
|
|
g_assert (width > 0);
|
|
g_assert (height > 0);
|
|
g_assert (out_x != NULL);
|
|
g_assert (out_y != NULL);
|
|
|
|
rect.w = width;
|
|
rect.h = height;
|
|
|
|
stbrp_pack_rects (&atlas->context, &rect, 1);
|
|
|
|
if (rect.was_packed)
|
|
{
|
|
*out_x = rect.x;
|
|
*out_y = rect.y;
|
|
}
|
|
|
|
return rect.was_packed;
|
|
}
|
|
|
|
static void
|
|
gsk_gl_texture_library_constructed (GObject *object)
|
|
{
|
|
G_OBJECT_CLASS (gsk_gl_texture_library_parent_class)->constructed (object);
|
|
|
|
g_assert (GSK_GL_TEXTURE_LIBRARY (object)->hash_table != NULL);
|
|
}
|
|
|
|
static void
|
|
gsk_gl_texture_library_dispose (GObject *object)
|
|
{
|
|
GskGLTextureLibrary *self = (GskGLTextureLibrary *)object;
|
|
|
|
g_clear_pointer (&self->atlases, g_ptr_array_unref);
|
|
g_clear_object (&self->driver);
|
|
|
|
G_OBJECT_CLASS (gsk_gl_texture_library_parent_class)->dispose (object);
|
|
}
|
|
|
|
static void
|
|
gsk_gl_texture_library_get_property (GObject *object,
|
|
guint prop_id,
|
|
GValue *value,
|
|
GParamSpec *pspec)
|
|
{
|
|
GskGLTextureLibrary *self = GSK_GL_TEXTURE_LIBRARY (object);
|
|
|
|
switch (prop_id)
|
|
{
|
|
case PROP_DRIVER:
|
|
g_value_set_object (value, self->driver);
|
|
break;
|
|
|
|
default:
|
|
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
|
|
}
|
|
}
|
|
|
|
static void
|
|
gsk_gl_texture_library_set_property (GObject *object,
|
|
guint prop_id,
|
|
const GValue *value,
|
|
GParamSpec *pspec)
|
|
{
|
|
GskGLTextureLibrary *self = GSK_GL_TEXTURE_LIBRARY (object);
|
|
|
|
switch (prop_id)
|
|
{
|
|
case PROP_DRIVER:
|
|
self->driver = g_value_dup_object (value);
|
|
break;
|
|
|
|
default:
|
|
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
|
|
}
|
|
}
|
|
|
|
static void
|
|
gsk_gl_texture_library_class_init (GskGLTextureLibraryClass *klass)
|
|
{
|
|
GObjectClass *object_class = G_OBJECT_CLASS (klass);
|
|
|
|
object_class->constructed = gsk_gl_texture_library_constructed;
|
|
object_class->dispose = gsk_gl_texture_library_dispose;
|
|
object_class->get_property = gsk_gl_texture_library_get_property;
|
|
object_class->set_property = gsk_gl_texture_library_set_property;
|
|
|
|
klass->compact = gsk_gl_texture_library_real_compact;
|
|
klass->allocate = gsk_gl_texture_library_real_allocate;
|
|
|
|
properties [PROP_DRIVER] =
|
|
g_param_spec_object ("driver",
|
|
"Driver",
|
|
"Driver",
|
|
GSK_TYPE_GL_DRIVER,
|
|
(G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
|
|
|
|
g_object_class_install_properties (object_class, N_PROPS, properties);
|
|
}
|
|
|
|
static void
|
|
gsk_gl_texture_library_init (GskGLTextureLibrary *self)
|
|
{
|
|
self->max_frame_age = DEFAULT_MAX_FRAME_AGE;
|
|
self->atlas_width = DEFAULT_ATLAS_WIDTH;
|
|
self->atlas_height = DEFAULT_ATLAS_HEIGHT;
|
|
self->atlases = g_ptr_array_new_with_free_func ((GDestroyNotify)gsk_gl_texture_atlas_free);
|
|
}
|
|
|
|
void
|
|
gsk_gl_texture_library_set_funcs (GskGLTextureLibrary *self,
|
|
GHashFunc hash_func,
|
|
GEqualFunc equal_func,
|
|
GDestroyNotify key_destroy,
|
|
GDestroyNotify value_destroy)
|
|
{
|
|
g_return_if_fail (GSK_IS_GL_TEXTURE_LIBRARY (self));
|
|
g_return_if_fail (self->hash_table == NULL);
|
|
|
|
self->hash_table = g_hash_table_new_full (hash_func, equal_func,
|
|
key_destroy, value_destroy);
|
|
}
|
|
|
|
void
|
|
gsk_gl_texture_library_begin_frame (GskGLTextureLibrary *self,
|
|
gint64 frame_id)
|
|
{
|
|
g_return_if_fail (GSK_IS_GL_TEXTURE_LIBRARY (self));
|
|
|
|
gsk_gl_texture_library_compact (self, frame_id);
|
|
|
|
if (GSK_GL_TEXTURE_LIBRARY_GET_CLASS (self)->begin_frame)
|
|
GSK_GL_TEXTURE_LIBRARY_GET_CLASS (self)->begin_frame (self, frame_id);
|
|
}
|
|
|
|
static GskGLTexture *
|
|
gsk_gl_texture_library_pack_one (GskGLTextureLibrary *self,
|
|
guint width,
|
|
guint height)
|
|
{
|
|
GskGLTexture *texture;
|
|
|
|
g_assert (GSK_IS_GL_TEXTURE_LIBRARY (self));
|
|
|
|
if (width > self->driver->command_queue->max_texture_size ||
|
|
height > self->driver->command_queue->max_texture_size)
|
|
{
|
|
g_warning ("Clipping requested texture of size %ux%u to maximum allowable size %u.",
|
|
width, height, self->driver->command_queue->max_texture_size);
|
|
width = MIN (width, self->driver->command_queue->max_texture_size);
|
|
height = MIN (height, self->driver->command_queue->max_texture_size);
|
|
}
|
|
|
|
texture = gsk_gl_driver_create_texture (self->driver, width, height, GL_RGBA8, GL_LINEAR, GL_LINEAR);
|
|
texture->permanent = TRUE;
|
|
|
|
return texture;
|
|
}
|
|
|
|
static void
|
|
gsk_gl_texture_library_pack_any_atlas (GskGLTextureLibrary *self,
|
|
int width,
|
|
int height,
|
|
GskGLTextureAtlas **out_atlas,
|
|
int *out_x,
|
|
int *out_y)
|
|
{
|
|
GskGLTextureAtlas *atlas = NULL;
|
|
int x, y;
|
|
|
|
g_assert (GSK_IS_GL_TEXTURE_LIBRARY (self));
|
|
g_assert (width > 0);
|
|
g_assert (height > 0);
|
|
g_assert (out_atlas != NULL);
|
|
g_assert (out_x != NULL);
|
|
g_assert (out_y != NULL);
|
|
|
|
for (guint i = 0; i < self->atlases->len; i++)
|
|
{
|
|
atlas = g_ptr_array_index (self->atlases, i);
|
|
|
|
if (gsk_gl_texture_library_allocate (self, atlas, width, height, &x, &y))
|
|
break;
|
|
|
|
atlas = NULL;
|
|
}
|
|
|
|
if (atlas == NULL)
|
|
{
|
|
/* No atlas has enough space, so create a new one... */
|
|
atlas = gsk_gl_texture_library_acquire_atlas (self);
|
|
|
|
/* Pack it onto that one, which surely has enough space... */
|
|
if (!gsk_gl_texture_library_allocate (self, atlas, width, height, &x, &y))
|
|
g_assert_not_reached ();
|
|
}
|
|
|
|
*out_atlas = atlas;
|
|
*out_x = x;
|
|
*out_y = y;
|
|
}
|
|
|
|
gpointer
|
|
gsk_gl_texture_library_pack (GskGLTextureLibrary *self,
|
|
gpointer key,
|
|
gsize valuelen,
|
|
guint width,
|
|
guint height,
|
|
int padding,
|
|
guint *out_packed_x,
|
|
guint *out_packed_y)
|
|
{
|
|
GskGLTextureAtlasEntry *entry;
|
|
GskGLTextureAtlas *atlas = NULL;
|
|
|
|
g_assert (GSK_IS_GL_TEXTURE_LIBRARY (self));
|
|
g_assert (key != NULL);
|
|
g_assert (valuelen > sizeof (GskGLTextureAtlasEntry));
|
|
g_assert (out_packed_x != NULL);
|
|
g_assert (out_packed_y != NULL);
|
|
|
|
entry = g_slice_alloc0 (valuelen);
|
|
entry->n_pixels = width * height;
|
|
entry->accessed = TRUE;
|
|
entry->used = TRUE;
|
|
|
|
/* If our size is invisible then we just want an entry in the
|
|
* cache for faster lookups, but do not actually spend any texture
|
|
* allocations on this entry.
|
|
*/
|
|
if (width <= 0 && height <= 0)
|
|
{
|
|
entry->is_atlased = FALSE;
|
|
entry->texture = NULL;
|
|
entry->area.x = 0.0f;
|
|
entry->area.y = 0.0f;
|
|
entry->area.x2 = 0.0f;
|
|
entry->area.y2 = 0.0f;
|
|
|
|
*out_packed_x = 0;
|
|
*out_packed_y = 0;
|
|
}
|
|
else if (self->max_entry_size == 0 ||
|
|
(width <= self->max_entry_size &&
|
|
height <= self->max_entry_size))
|
|
{
|
|
int packed_x;
|
|
int packed_y;
|
|
|
|
gsk_gl_texture_library_pack_any_atlas (self,
|
|
padding + width + padding,
|
|
padding + height + padding,
|
|
&atlas,
|
|
&packed_x,
|
|
&packed_y);
|
|
|
|
entry->atlas = atlas;
|
|
entry->is_atlased = TRUE;
|
|
entry->area.x = (packed_x + padding) / (float)atlas->width;
|
|
entry->area.y = (packed_y + padding) / (float)atlas->height;
|
|
entry->area.x2 = (packed_x + padding + width) / (float)atlas->width;
|
|
entry->area.y2 = (packed_y + padding + height) / (float)atlas->height;
|
|
|
|
*out_packed_x = packed_x;
|
|
*out_packed_y = packed_y;
|
|
}
|
|
else
|
|
{
|
|
GskGLTexture *texture = gsk_gl_texture_library_pack_one (self,
|
|
padding + width + padding,
|
|
padding + height + padding);
|
|
|
|
entry->texture = texture;
|
|
entry->is_atlased = FALSE;
|
|
entry->accessed = TRUE;
|
|
entry->area.x = padding / (float) (padding + width + padding);
|
|
entry->area.y = padding / (float) (padding + height + padding);
|
|
entry->area.x2 = (padding + width) / (float) (padding + width + padding);
|
|
entry->area.y2 = (padding + height) / (float) (padding + height + padding);
|
|
|
|
*out_packed_x = 0;
|
|
*out_packed_y = 0;
|
|
}
|
|
|
|
g_hash_table_insert (self->hash_table, key, entry);
|
|
|
|
return entry;
|
|
}
|
|
|
|
/*
|
|
* gsk_gl_texture_library_clear_cache:
|
|
*
|
|
* Clear the front cache if the texture library is using one. For
|
|
* example the glyph cache would drop it's front cache to force
|
|
* next lookups to fall through to the GHashTable key lookup.
|
|
*/
|
|
void
|
|
gsk_gl_texture_library_clear_cache (GskGLTextureLibrary *self)
|
|
{
|
|
g_return_if_fail (GSK_IS_GL_TEXTURE_LIBRARY (self));
|
|
|
|
if (GSK_GL_TEXTURE_LIBRARY_GET_CLASS (self)->clear_cache)
|
|
GSK_GL_TEXTURE_LIBRARY_GET_CLASS (self)->clear_cache (self);
|
|
}
|
|
|
|
/*
|
|
* gsk_gl_texture_library_compact:
|
|
*
|
|
* Requests that the texture library compact it's altases. That
|
|
* generally means to traverse them to look for unused pixels over
|
|
* a certain threshold and release them if necessary.
|
|
*
|
|
* Returns: %TRUE if any compaction occurred.
|
|
*/
|
|
gboolean
|
|
gsk_gl_texture_library_compact (GskGLTextureLibrary *self,
|
|
gint64 frame_id)
|
|
{
|
|
g_return_val_if_fail (GSK_IS_GL_TEXTURE_LIBRARY (self), FALSE);
|
|
|
|
if (GSK_GL_TEXTURE_LIBRARY_GET_CLASS (self)->compact)
|
|
return GSK_GL_TEXTURE_LIBRARY_GET_CLASS (self)->compact (self, frame_id);
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
void
|
|
gsk_gl_texture_library_reset (GskGLTextureLibrary *self)
|
|
{
|
|
g_return_if_fail (GSK_IS_GL_TEXTURE_LIBRARY (self));
|
|
|
|
gsk_gl_texture_library_clear_cache (self);
|
|
|
|
g_hash_table_remove_all (self->hash_table);
|
|
|
|
if (self->atlases->len)
|
|
g_ptr_array_remove_range (self->atlases, 0, self->atlases->len);
|
|
}
|
|
|
|
void
|
|
gsk_gl_texture_library_set_atlas_size (GskGLTextureLibrary *self,
|
|
int width,
|
|
int height)
|
|
{
|
|
g_return_if_fail (GSK_IS_GL_TEXTURE_LIBRARY (self));
|
|
|
|
if (width <= 0)
|
|
width = DEFAULT_ATLAS_WIDTH;
|
|
|
|
if (height <= 0)
|
|
height = DEFAULT_ATLAS_HEIGHT;
|
|
|
|
self->atlas_height = height;
|
|
self->atlas_width = width;
|
|
|
|
gsk_gl_texture_library_reset (self);
|
|
}
|
|
|
|
/*
|
|
* gsk_gl_texture_library_acquire_atlas:
|
|
*
|
|
* Allocates a new texture atlas based on the current size
|
|
* and format requirements.
|
|
*/
|
|
GskGLTextureAtlas *
|
|
gsk_gl_texture_library_acquire_atlas (GskGLTextureLibrary *self)
|
|
{
|
|
GskGLTextureAtlas *atlas;
|
|
|
|
g_return_val_if_fail (GSK_IS_GL_TEXTURE_LIBRARY (self), NULL);
|
|
g_return_val_if_fail (GSK_IS_GL_DRIVER (self->driver), NULL);
|
|
g_return_val_if_fail (GSK_IS_GL_COMMAND_QUEUE (self->driver->command_queue), NULL);
|
|
g_return_val_if_fail (self->atlas_width > 0, NULL);
|
|
g_return_val_if_fail (self->atlas_height > 0, NULL);
|
|
|
|
atlas = g_slice_new0 (GskGLTextureAtlas);
|
|
atlas->width = self->atlas_width;
|
|
atlas->height = self->atlas_height;
|
|
/* TODO: We might want to change the strategy about the amount of
|
|
* nodes here? stb_rect_pack.h says width is optimal. */
|
|
atlas->nodes = g_malloc0_n (atlas->width, sizeof (struct stbrp_node));
|
|
stbrp_init_target (&atlas->context, atlas->width, atlas->height, atlas->nodes, atlas->width);
|
|
atlas->texture_id = gsk_gl_command_queue_create_texture (self->driver->command_queue,
|
|
atlas->width,
|
|
atlas->height,
|
|
GL_RGBA8,
|
|
GL_LINEAR,
|
|
GL_LINEAR);
|
|
|
|
gdk_gl_context_label_object_printf (gdk_gl_context_get_current (),
|
|
GL_TEXTURE, atlas->texture_id,
|
|
"Texture atlas %d",
|
|
atlas->texture_id);
|
|
|
|
g_ptr_array_add (self->atlases, atlas);
|
|
|
|
if (GSK_GL_TEXTURE_LIBRARY_GET_CLASS (self)->init_atlas)
|
|
GSK_GL_TEXTURE_LIBRARY_GET_CLASS (self)->init_atlas (self, atlas);
|
|
|
|
return atlas;
|
|
}
|
|
|
|
gboolean
|
|
gsk_gl_texture_library_allocate (GskGLTextureLibrary *self,
|
|
GskGLTextureAtlas *atlas,
|
|
int width,
|
|
int height,
|
|
int *out_x,
|
|
int *out_y)
|
|
{
|
|
g_assert (GSK_IS_GL_TEXTURE_LIBRARY (self));
|
|
g_assert (atlas != NULL);
|
|
g_assert (width > 0);
|
|
g_assert (height > 0);
|
|
g_assert (out_x != NULL);
|
|
g_assert (out_y != NULL);
|
|
|
|
return GSK_GL_TEXTURE_LIBRARY_GET_CLASS (self)->allocate (self, atlas, width, height, out_x, out_y);
|
|
}
|