Merge branch 'wip/otte/big-bigger-oom' into 'main'

Implement tiling for large textures

Closes #6324

See merge request GNOME/gtk!7447
This commit is contained in:
Benjamin Otte 2024-07-12 17:47:53 +00:00
commit 2ae345aa8f
7 changed files with 458 additions and 54 deletions

View File

@ -260,7 +260,10 @@ gsk_gl_device_get_for_display (GdkDisplay *display,
gdk_gl_context_make_current (context);
glGetIntegerv (GL_MAX_TEXTURE_SIZE, &max_texture_size);
gsk_gpu_device_setup (GSK_GPU_DEVICE (self), display, max_texture_size);
gsk_gpu_device_setup (GSK_GPU_DEVICE (self),
display,
max_texture_size,
GSK_GPU_DEVICE_DEFAULT_TILE_SIZE);
self->version_string = gdk_gl_context_get_glsl_version_string (context);
self->api = gdk_gl_context_get_api (context);

View File

@ -32,6 +32,7 @@ typedef struct _GskGpuCachedClass GskGpuCachedClass;
typedef struct _GskGpuCachedAtlas GskGpuCachedAtlas;
typedef struct _GskGpuCachedGlyph GskGpuCachedGlyph;
typedef struct _GskGpuCachedTexture GskGpuCachedTexture;
typedef struct _GskGpuCachedTile GskGpuCachedTile;
struct _GskGpuCache
{
@ -44,6 +45,7 @@ struct _GskGpuCache
GHashTable *texture_cache;
GHashTable *ccs_texture_caches[GDK_COLOR_STATE_N_IDS];
GHashTable *tile_cache;
GHashTable *glyph_cache;
GskGpuCachedAtlas *current_atlas;
@ -398,6 +400,185 @@ gsk_gpu_cached_texture_new (GskGpuCache *cache,
return self;
}
/* }}} */
/* {{{ CachedTile */
struct _GskGpuCachedTile
{
GskGpuCached parent;
GdkTexture *texture;
gsize tile_id;
/* atomic */ int use_count; /* We count the use by the cache (via the linked
* list) and by the texture (via weak ref)
*/
gsize *dead_pixels_counter;
GskGpuImage *image;
GdkColorState *color_state;
};
static void
gsk_gpu_cached_tile_free (GskGpuCache *cache,
GskGpuCached *cached)
{
GskGpuCachedTile *self = (GskGpuCachedTile *) cached;
gpointer key, value;
g_clear_object (&self->image);
g_clear_pointer (&self->color_state, gdk_color_state_unref);
if (g_hash_table_steal_extended (cache->tile_cache, self, &key, &value))
{
/* If the texture has been reused already, we put the entry back */
if ((GskGpuCached *) value != cached)
g_hash_table_insert (cache->tile_cache, key, value);
}
/* If the cached item itself is still in use by the texture, we leave
* it to the weak ref or render data to free it.
*/
if (g_atomic_int_dec_and_test (&self->use_count))
{
g_free (self);
return;
}
}
static inline gboolean
gsk_gpu_cached_tile_is_invalid (GskGpuCachedTile *self)
{
/* If the use count is less than 2, the orignal texture has died,
* and the memory may have been reused for a new texture, so we
* can't hand out the image that is for the original texture.
*/
return g_atomic_int_get (&self->use_count) < 2;
}
static gboolean
gsk_gpu_cached_tile_should_collect (GskGpuCache *cache,
GskGpuCached *cached,
gint64 cache_timeout,
gint64 timestamp)
{
GskGpuCachedTile *self = (GskGpuCachedTile *) cached;
return gsk_gpu_cached_is_old (cache, cached, cache_timeout, timestamp) ||
gsk_gpu_cached_tile_is_invalid (self);
}
static const GskGpuCachedClass GSK_GPU_CACHED_TILE_CLASS =
{
sizeof (GskGpuCachedTile),
gsk_gpu_cached_tile_free,
gsk_gpu_cached_tile_should_collect
};
/* Note: this function can run in an arbitrary thread, so it can
* only access things atomically
*/
static void
gsk_gpu_cached_tile_destroy_cb (gpointer data)
{
GskGpuCachedTile *self = data;
if (!gsk_gpu_cached_tile_is_invalid (self))
g_atomic_pointer_add (self->dead_pixels_counter, ((GskGpuCached *) self)->pixels);
if (g_atomic_int_dec_and_test (&self->use_count))
g_free (self);
}
static guint
gsk_gpu_cached_tile_hash (gconstpointer data)
{
const GskGpuCachedTile *self = data;
return g_direct_hash (self->texture) ^ self->tile_id;
}
static gboolean
gsk_gpu_cached_tile_equal (gconstpointer data_a,
gconstpointer data_b)
{
const GskGpuCachedTile *a = data_a;
const GskGpuCachedTile *b = data_b;
return a->texture == b->texture &&
a->tile_id == b->tile_id;
}
static GskGpuCachedTile *
gsk_gpu_cached_tile_new (GskGpuCache *cache,
GdkTexture *texture,
guint tile_id,
GskGpuImage *image,
GdkColorState *color_state)
{
GskGpuCachedTile *self;
self = gsk_gpu_cached_new (cache, &GSK_GPU_CACHED_TILE_CLASS, NULL);
self->texture = texture;
self->tile_id = tile_id;
self->image = g_object_ref (image);
self->color_state = gdk_color_state_ref (color_state);
((GskGpuCached *)self)->pixels = gsk_gpu_image_get_width (image) * gsk_gpu_image_get_height (image);
self->dead_pixels_counter = &cache->dead_texture_pixels;
self->use_count = 2;
g_object_weak_ref (G_OBJECT (texture), (GWeakNotify) gsk_gpu_cached_tile_destroy_cb, self);
if (cache->tile_cache == NULL)
cache->tile_cache = g_hash_table_new (gsk_gpu_cached_tile_hash,
gsk_gpu_cached_tile_equal);
g_hash_table_add (cache->tile_cache, self);
return self;
}
GskGpuImage *
gsk_gpu_cache_lookup_tile (GskGpuCache *self,
GdkTexture *texture,
gsize tile_id,
gint64 timestamp,
GdkColorState **out_color_state)
{
GskGpuCachedTile *tile;
GskGpuCachedTile lookup = {
.texture = texture,
.tile_id = tile_id
};
if (self->tile_cache == NULL)
return NULL;
tile = g_hash_table_lookup (self->tile_cache, &lookup);
if (tile == NULL)
return NULL;
gsk_gpu_cached_use (self, (GskGpuCached *) tile, timestamp);
*out_color_state = tile->color_state;
return g_object_ref (tile->image);
}
void
gsk_gpu_cache_cache_tile (GskGpuCache *self,
gint64 timestamp,
GdkTexture *texture,
guint tile_id,
GskGpuImage *image,
GdkColorState *color_state)
{
GskGpuCachedTile *tile;
tile = gsk_gpu_cached_tile_new (self, texture, tile_id, image, color_state);
gsk_gpu_cached_use (self, (GskGpuCached *) tile, timestamp);
}
/* }}} */
/* {{{ CachedGlyph */
@ -600,6 +781,7 @@ gsk_gpu_cache_dispose (GObject *object)
gsk_gpu_cache_clear_cache (self);
g_hash_table_unref (self->glyph_cache);
g_clear_pointer (&self->tile_cache, g_hash_table_unref);
g_hash_table_unref (self->texture_cache);
G_OBJECT_CLASS (gsk_gpu_cache_parent_class)->dispose (object);

View File

@ -39,6 +39,17 @@ void gsk_gpu_cache_cache_texture_image (GskGpuC
gint64 timestamp,
GskGpuImage *image,
GdkColorState *color_state);
GskGpuImage * gsk_gpu_cache_lookup_tile (GskGpuCache *self,
GdkTexture *texture,
gsize tile_id,
gint64 timestamp,
GdkColorState **out_color_state);
void gsk_gpu_cache_cache_tile (GskGpuCache *self,
gint64 timestamp,
GdkTexture *texture,
guint tile_id,
GskGpuImage *image,
GdkColorState *color_state);
typedef enum
{

View File

@ -22,6 +22,7 @@ struct _GskGpuDevicePrivate
{
GdkDisplay *display;
gsize max_image_size;
gsize tile_size;
GskGpuCache *cache; /* we don't own a ref, but manage the cache */
guint cache_gc_source;
@ -142,13 +143,15 @@ gsk_gpu_device_init (GskGpuDevice *self)
void
gsk_gpu_device_setup (GskGpuDevice *self,
GdkDisplay *display,
gsize max_image_size)
gsize max_image_size,
gsize tile_size)
{
GskGpuDevicePrivate *priv = gsk_gpu_device_get_instance_private (self);
const char *str;
priv->display = g_object_ref (display);
priv->max_image_size = max_image_size;
priv->tile_size = tile_size;
priv->cache_timeout = CACHE_TIMEOUT;
str = g_getenv ("GSK_CACHE_TIMEOUT");
@ -200,6 +203,19 @@ gsk_gpu_device_get_cache (GskGpuDevice *self)
return priv->cache;
}
/*<private>
* gsk_gpu_device_get_max_image_size:
* @self: a device
*
* Returns the max image size supported by this device.
*
* This maps to GL_MAX_TEXTURE_SIZE on GL, but Vulkan is more flexible with
* per-format size limits, so this is an estimate and code should still handle
* failures of image creation at smaller sizes. (Besides handling them anyway
* in case of OOM.)
*
* Returns: The maximum size in pixels for creating a GskGpuImage
**/
gsize
gsk_gpu_device_get_max_image_size (GskGpuDevice *self)
{
@ -208,6 +224,25 @@ gsk_gpu_device_get_max_image_size (GskGpuDevice *self)
return priv->max_image_size;
}
/*<private>
* gsk_gpu_device_get_tile_size:
* @self: a device
*
* The suggested size for tiling images. This value will be small enough so that
* image creation never fails due to size constraints. It should also not be too
* large to allow efficient caching of tiles and evictions of unused tiles
* (think of an image editor showing only a section of a large image).
*
* Returns: The suggested size of tiles when tiling images.
**/
gsize
gsk_gpu_device_get_tile_size (GskGpuDevice *self)
{
GskGpuDevicePrivate *priv = gsk_gpu_device_get_instance_private (self);
return priv->tile_size;
}
GskGpuImage *
gsk_gpu_device_create_offscreen_image (GskGpuDevice *self,
gboolean with_mipmap,

View File

@ -6,6 +6,8 @@
G_BEGIN_DECLS
#define GSK_GPU_DEVICE_DEFAULT_TILE_SIZE 1024
#define GSK_TYPE_GPU_DEVICE (gsk_gpu_device_get_type ())
#define GSK_GPU_DEVICE(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), GSK_TYPE_GPU_DEVICE, GskGpuDevice))
#define GSK_GPU_DEVICE_CLASS(k) (G_TYPE_CHECK_CLASS_CAST ((k), GSK_TYPE_GPU_DEVICE, GskGpuDeviceClass))
@ -50,12 +52,14 @@ GType gsk_gpu_device_get_type (void) G
void gsk_gpu_device_setup (GskGpuDevice *self,
GdkDisplay *display,
gsize max_image_size);
gsize max_image_size,
gsize tile_size);
void gsk_gpu_device_maybe_gc (GskGpuDevice *self);
void gsk_gpu_device_queue_gc (GskGpuDevice *self);
GdkDisplay * gsk_gpu_device_get_display (GskGpuDevice *self);
GskGpuCache * gsk_gpu_device_get_cache (GskGpuDevice *self);
gsize gsk_gpu_device_get_max_image_size (GskGpuDevice *self);
gsize gsk_gpu_device_get_tile_size (GskGpuDevice *self);
GskGpuImage * gsk_gpu_device_create_offscreen_image (GskGpuDevice *self,
gboolean with_mipmap,

View File

@ -43,8 +43,10 @@
#include "gskprivate.h"
#include "gdk/gdkcolorstateprivate.h"
#include "gdk/gdkmemorytextureprivate.h"
#include "gdk/gdkrgbaprivate.h"
#include "gdk/gdksubsurfaceprivate.h"
#include "gdk/gdktextureprivate.h"
/* the epsilon we allow pixels to be off due to rounding errors.
* Chosen rather randomly.
@ -1725,6 +1727,163 @@ gsk_gpu_lookup_texture (GskGpuFrame *frame,
return image;
}
static GskGpuSampler
gsk_gpu_sampler_for_scaling_filter (GskScalingFilter scaling_filter)
{
switch (scaling_filter)
{
case GSK_SCALING_FILTER_LINEAR:
return GSK_GPU_SAMPLER_DEFAULT;
case GSK_SCALING_FILTER_NEAREST:
return GSK_GPU_SAMPLER_NEAREST;
case GSK_SCALING_FILTER_TRILINEAR:
return GSK_GPU_SAMPLER_MIPMAP_DEFAULT;
default:
g_assert_not_reached ();
return GSK_GPU_SAMPLER_DEFAULT;
}
}
/* must be set up with BLEND_ADD to avoid seams */
static void
gsk_gpu_node_processor_draw_texture_tiles (GskGpuNodeProcessor *self,
const graphene_rect_t *texture_bounds,
GdkTexture *texture,
GskScalingFilter scaling_filter)
{
GskGpuCache *cache;
GskGpuDevice *device;
gint64 timestamp;
GskGpuImage *tile;
GdkColorState *tile_cs;
GskGpuSampler sampler;
gboolean need_mipmap;
GdkMemoryTexture *memtex;
GdkTexture *subtex;
float scaled_tile_width, scaled_tile_height;
gsize tile_size, width, height, n_width, n_height, x, y;
graphene_rect_t clip_bounds;
device = gsk_gpu_frame_get_device (self->frame);
cache = gsk_gpu_device_get_cache (device);
timestamp = gsk_gpu_frame_get_timestamp (self->frame);
sampler = gsk_gpu_sampler_for_scaling_filter (scaling_filter);
need_mipmap = scaling_filter == GSK_SCALING_FILTER_TRILINEAR;
gsk_gpu_node_processor_get_clip_bounds (self, &clip_bounds);
tile_size = gsk_gpu_device_get_tile_size (device);
width = gdk_texture_get_width (texture);
height = gdk_texture_get_height (texture);
n_width = (width + tile_size - 1) / tile_size;
n_height = (height + tile_size - 1) / tile_size;
scaled_tile_width = texture_bounds->size.width * tile_size / width;
scaled_tile_height = texture_bounds->size.height * tile_size / height;
memtex = NULL;
for (y = 0; y < n_height; y++)
{
for (x = 0; x < n_width; x++)
{
graphene_rect_t tile_rect = GRAPHENE_RECT_INIT (texture_bounds->origin.x + scaled_tile_width * x,
texture_bounds->origin.y + scaled_tile_height * y,
scaled_tile_width,
scaled_tile_height);
if (!gsk_rect_intersection (&tile_rect, texture_bounds, &tile_rect) ||
!gsk_rect_intersects (&clip_bounds, &tile_rect))
continue;
tile = gsk_gpu_cache_lookup_tile (cache, texture, y * n_width + x, timestamp, &tile_cs);
if (tile == NULL)
{
if (memtex == NULL)
memtex = gdk_memory_texture_from_texture (texture);
subtex = gdk_memory_texture_new_subtexture (memtex,
x * tile_size,
y * tile_size,
MIN (tile_size, width - x * tile_size),
MIN (tile_size, height - y * tile_size));
tile = gsk_gpu_upload_texture_op_try (self->frame, need_mipmap, subtex);
g_object_unref (subtex);
if (tile == NULL)
{
g_warning ("failed to create %zux%zu tile for %zux%zu texture. Out of memory?",
tile_size, tile_size, width, height);
goto out;
}
tile_cs = gdk_texture_get_color_state (texture);
if (gsk_gpu_image_get_flags (tile) & GSK_GPU_IMAGE_SRGB)
{
tile_cs = gdk_color_state_get_no_srgb_tf (tile_cs);
g_assert (tile_cs);
}
gsk_gpu_cache_cache_tile (cache, timestamp, texture, y * n_width + x, tile, tile_cs);
}
if (need_mipmap &&
(gsk_gpu_image_get_flags (tile) & (GSK_GPU_IMAGE_STRAIGHT_ALPHA | GSK_GPU_IMAGE_CAN_MIPMAP)) != GSK_GPU_IMAGE_CAN_MIPMAP)
{
tile = gsk_gpu_copy_image (self->frame, self->ccs, tile, tile_cs, TRUE);
tile_cs = self->ccs;
gsk_gpu_cache_cache_tile (cache, timestamp, texture, y * n_width + x, tile, tile_cs);
}
if (need_mipmap && !(gsk_gpu_image_get_flags (tile) & GSK_GPU_IMAGE_MIPMAP))
gsk_gpu_mipmap_op (self->frame, tile);
gsk_gpu_node_processor_image_op (self,
tile,
tile_cs,
sampler,
&tile_rect,
&tile_rect);
g_object_unref (tile);
}
}
out:
g_clear_object (&memtex);
}
static GskGpuImage *
gsk_gpu_get_texture_tiles_as_image (GskGpuFrame *frame,
GdkColorState *ccs,
const graphene_rect_t *clip_bounds,
const graphene_vec2_t *scale,
const graphene_rect_t *texture_bounds,
GdkTexture *texture,
GskScalingFilter scaling_filter)
{
GskGpuNodeProcessor self;
GskGpuImage *image;
image = gsk_gpu_node_processor_init_draw (&self,
frame,
ccs,
gdk_texture_get_depth (texture),
scale,
clip_bounds);
if (image == NULL)
return NULL;
self.blend = GSK_GPU_BLEND_ADD;
self.pending_globals |= GSK_GPU_GLOBAL_BLEND;
gsk_gpu_node_processor_sync_globals (&self, 0);
gsk_gpu_node_processor_draw_texture_tiles (&self,
texture_bounds,
texture,
scaling_filter);
gsk_gpu_node_processor_finish_draw (&self, image);
return image;
}
static void
gsk_gpu_node_processor_add_texture_node (GskGpuNodeProcessor *self,
GskRenderNode *node)
@ -1741,11 +1900,26 @@ gsk_gpu_node_processor_add_texture_node (GskGpuNodeProcessor *self,
if (image == NULL)
{
GSK_DEBUG (FALLBACK, "Unsupported texture format %u for size %dx%d",
gdk_texture_get_format (texture),
gdk_texture_get_width (texture),
gdk_texture_get_height (texture));
gsk_gpu_node_processor_add_cairo_node (self, node);
graphene_rect_t clip, rounded_clip;
if (!gsk_gpu_node_processor_clip_node_bounds (self, node, &clip))
return;
rect_round_to_pixels (&clip, &self->scale, &self->offset, &rounded_clip);
image = gsk_gpu_get_texture_tiles_as_image (self->frame,
self->ccs,
&rounded_clip,
&self->scale,
&node->bounds,
texture,
should_mipmap ? GSK_SCALING_FILTER_TRILINEAR : GSK_SCALING_FILTER_LINEAR);
gsk_gpu_node_processor_image_op (self,
image,
self->ccs,
GSK_GPU_SAMPLER_DEFAULT,
&clip,
&rounded_clip);
g_object_unref (image);
return;
}
@ -1797,14 +1971,25 @@ gsk_gpu_get_texture_node_as_image (GskGpuFrame *frame,
GdkTexture *texture = gsk_texture_node_get_texture (node);
GdkColorState *image_cs;
GskGpuImage *image;
gboolean should_mipmap;
if (texture_node_should_mipmap (node, frame, scale))
return gsk_gpu_get_node_as_image_via_offscreen (frame, ccs, clip_bounds, scale, node, out_bounds);
should_mipmap = texture_node_should_mipmap (node, frame, scale);
image = gsk_gpu_lookup_texture (frame, ccs, texture, FALSE, &image_cs);
/* Happens ie for oversized textures */
if (image == NULL)
{
image = gsk_gpu_get_texture_tiles_as_image (frame,
ccs,
clip_bounds,
scale,
&node->bounds,
gsk_texture_node_get_texture (node),
should_mipmap ? GSK_SCALING_FILTER_TRILINEAR : GSK_SCALING_FILTER_LINEAR);
*out_bounds = *clip_bounds;
return image;
}
if (should_mipmap)
return gsk_gpu_get_node_as_image_via_offscreen (frame, ccs, clip_bounds, scale, node, out_bounds);
if (!gdk_color_state_equal (ccs, image_cs) ||
@ -1833,8 +2018,15 @@ gsk_gpu_node_processor_add_texture_scale_node (GskGpuNodeProcessor *self,
guint32 descriptor;
gboolean need_mipmap, need_offscreen;
need_offscreen = self->modelview != NULL ||
texture = gsk_texture_scale_node_get_texture (node);
scaling_filter = gsk_texture_scale_node_get_filter (node);
need_mipmap = scaling_filter == GSK_SCALING_FILTER_TRILINEAR;
image = gsk_gpu_lookup_texture (self->frame, self->ccs, texture, need_mipmap, &image_cs);
need_offscreen = image == NULL ||
self->modelview != NULL ||
!graphene_vec2_equal (&self->scale, graphene_vec2_one ());
if (need_offscreen)
{
GskGpuImage *offscreen;
@ -1853,6 +2045,15 @@ gsk_gpu_node_processor_add_texture_scale_node (GskGpuNodeProcessor *self,
return;
clip_bounds.size.width = ceilf (clip_bounds.size.width);
clip_bounds.size.height = ceilf (clip_bounds.size.height);
if (image == NULL)
offscreen = gsk_gpu_get_texture_tiles_as_image (self->frame,
self->ccs,
&clip_bounds,
graphene_vec2_one (),
&node->bounds,
texture,
scaling_filter);
else
offscreen = gsk_gpu_node_processor_create_offscreen (self->frame,
self->ccs,
graphene_vec2_one (),
@ -1870,22 +2071,6 @@ gsk_gpu_node_processor_add_texture_scale_node (GskGpuNodeProcessor *self,
return;
}
texture = gsk_texture_scale_node_get_texture (node);
scaling_filter = gsk_texture_scale_node_get_filter (node);
need_mipmap = scaling_filter == GSK_SCALING_FILTER_TRILINEAR;
image = gsk_gpu_lookup_texture (self->frame, self->ccs, texture, need_mipmap, &image_cs);
if (image == NULL)
{
GSK_DEBUG (FALLBACK, "Unsupported texture format %u for size %dx%d",
gdk_texture_get_format (texture),
gdk_texture_get_width (texture),
gdk_texture_get_height (texture));
gsk_gpu_node_processor_add_cairo_node (self, node);
return;
}
if ((gsk_gpu_image_get_flags (image) & GSK_GPU_IMAGE_STRAIGHT_ALPHA) ||
(need_mipmap && !(gsk_gpu_image_get_flags (image) & GSK_GPU_IMAGE_CAN_MIPMAP)) ||
!gdk_color_state_equal (image_cs, self->ccs))
@ -1902,24 +2087,7 @@ gsk_gpu_node_processor_add_texture_scale_node (GskGpuNodeProcessor *self,
if (need_mipmap && !(gsk_gpu_image_get_flags (image) & GSK_GPU_IMAGE_MIPMAP))
gsk_gpu_mipmap_op (self->frame, image);
switch (scaling_filter)
{
case GSK_SCALING_FILTER_LINEAR:
descriptor = gsk_gpu_node_processor_add_image (self, image, GSK_GPU_SAMPLER_DEFAULT);
break;
case GSK_SCALING_FILTER_NEAREST:
descriptor = gsk_gpu_node_processor_add_image (self, image, GSK_GPU_SAMPLER_NEAREST);
break;
case GSK_SCALING_FILTER_TRILINEAR:
descriptor = gsk_gpu_node_processor_add_image (self, image, GSK_GPU_SAMPLER_MIPMAP_DEFAULT);
break;
default:
g_assert_not_reached ();
return;
}
descriptor = gsk_gpu_node_processor_add_image (self, image, gsk_gpu_sampler_for_scaling_filter (scaling_filter));
gsk_gpu_texture_op (self->frame,
gsk_gpu_clip_get_shader_clip (&self->clip, &self->offset, &node->bounds),

View File

@ -575,7 +575,8 @@ gsk_vulkan_device_setup (GskVulkanDevice *self,
self->max_immutable_samplers = MIN (self->max_samplers / 3, 32);
gsk_gpu_device_setup (GSK_GPU_DEVICE (self),
display,
vk_props.properties.limits.maxImageDimension2D);
vk_props.properties.limits.maxImageDimension2D,
GSK_GPU_DEVICE_DEFAULT_TILE_SIZE);
}
GskGpuDevice *