From b183d32b15379359211c82d19afd435de27efe06 Mon Sep 17 00:00:00 2001 From: John Cupitt Date: Thu, 23 Aug 2012 16:08:08 +0100 Subject: [PATCH] use a hash table for tilecache now we can have large numbers of tiles, use a hash table for tile lookup --- ChangeLog | 1 + libvips/conversion/old-tilecache.c | 513 ----------------------------- libvips/conversion/tilecache.c | 225 ++++++++----- 3 files changed, 135 insertions(+), 604 deletions(-) delete mode 100644 libvips/conversion/old-tilecache.c diff --git a/ChangeLog b/ChangeLog index 93017227..db28a956 100644 --- a/ChangeLog +++ b/ChangeLog @@ -12,6 +12,7 @@ - fix spurious warnings about exif updates - VipsSequential has an integrated cache and stalls out of order threads - add a line cache ... sizes up dynamically with request size +- tilecache / linecache use a hash table not a linear list 20/7/12 started 7.30.0 - support "rs" mode in vips7 diff --git a/libvips/conversion/old-tilecache.c b/libvips/conversion/old-tilecache.c deleted file mode 100644 index 180f275b..00000000 --- a/libvips/conversion/old-tilecache.c +++ /dev/null @@ -1,513 +0,0 @@ -/* Tile cache from tiff2vips ... broken out so it can be shared with - * openexr read. - * - * This isn't the same as the sinkscreen cache: we don't sub-divide, and we - * single-thread our callee. - * - * 23/8/06 - * - take ownership of reused tiles in case the cache is being shared - * 13/2/07 - * - release ownership after fillng with pixels in case we read across - * threads - * 4/2/10 - * - gtkdoc - * 12/12/10 - * - use im_prepare_to() and avoid making a sequence for every cache tile - * 5/12/11 - * - rework as a class - * 23/6/12 - * - listen for "minimise" signal - */ - -/* - - This file is part of VIPS. - - VIPS 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 of the License, or - (at your option) any later version. - - This program 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 cache of the GNU Lesser General Public License - along with this program; if not, write to the Free Software - Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - - */ - -/* - - These files are distributed with VIPS - http://www.vips.ecs.soton.ac.uk - - */ - -/* -#define VIPS_DEBUG - */ - -#ifdef HAVE_CONFIG_H -#include -#endif /*HAVE_CONFIG_H*/ -#include - -#include -#include -#include - -#include -#include -#include - -#include "conversion.h" - -/* A tile in our cache. - */ -typedef struct { - struct _VipsTileCache *cache; - - VipsRegion *region; /* Region with private mem for data */ - int time; /* Time of last use for flush */ - int x; /* xy pos in VIPS image cods */ - int y; -} VipsTile; - -typedef struct _VipsTileCache { - VipsConversion parent_instance; - - VipsImage *in; - int tile_width; - int tile_height; - int max_tiles; - VipsCacheStrategy strategy; - - int time; /* Update ticks for LRU here */ - int ntiles; /* Current cache size */ - GMutex *lock; /* Lock everything here */ - GSList *tiles; /* List of tiles */ -} VipsTileCache; - -typedef VipsConversionClass VipsTileCacheClass; - -G_DEFINE_TYPE( VipsTileCache, vips_tile_cache, VIPS_TYPE_CONVERSION ); - -static void -vips_tile_destroy( VipsTile *tile ) -{ - VipsTileCache *cache = tile->cache; - - cache->tiles = g_slist_remove( cache->tiles, tile ); - cache->ntiles -= 1; - g_assert( cache->ntiles >= 0 ); - tile->cache = NULL; - - VIPS_UNREF( tile->region ); - - vips_free( tile ); -} - -static void -vips_tile_cache_drop_all( VipsTileCache *cache ) -{ - while( cache->tiles ) { - VipsTile *tile = (VipsTile *) cache->tiles->data; - - vips_tile_destroy( tile ); - } -} - -static void -vips_tile_cache_dispose( GObject *gobject ) -{ - VipsTileCache *cache = (VipsTileCache *) gobject; - - vips_tile_cache_drop_all( cache ); - VIPS_FREEF( g_mutex_free, cache->lock ); - - G_OBJECT_CLASS( vips_tile_cache_parent_class )->dispose( gobject ); -} - -static VipsTile * -vips_tile_new( VipsTileCache *cache ) -{ - VipsTile *tile; - - if( !(tile = VIPS_NEW( NULL, VipsTile )) ) - return( NULL ); - - tile->cache = cache; - tile->region = NULL; - tile->time = cache->time; - tile->x = -1; - tile->y = -1; - cache->tiles = g_slist_prepend( cache->tiles, tile ); - g_assert( cache->ntiles >= 0 ); - cache->ntiles += 1; - - if( !(tile->region = vips_region_new( cache->in )) ) { - vips_tile_destroy( tile ); - return( NULL ); - } - vips__region_no_ownership( tile->region ); - - return( tile ); -} - -static int -vips_tile_move( VipsTile *tile, int x, int y ) -{ - VipsRect area; - - tile->x = x; - tile->y = y; - - area.left = x; - area.top = y; - area.width = tile->cache->tile_width; - area.height = tile->cache->tile_height; - - if( vips_region_buffer( tile->region, &area ) ) - return( -1 ); - - return( 0 ); -} - -/* Do we have a tile in the cache? - * - * FIXME .. use a hash? - */ -static VipsTile * -vips_tile_search( VipsTileCache *cache, int x, int y ) -{ - GSList *p; - - for( p = cache->tiles; p; p = p->next ) { - VipsTile *tile = (VipsTile *) p->data; - - if( tile->x == x && tile->y == y ) - return( tile ); - } - - return( NULL ); -} - -static void -vips_tile_touch( VipsTile *tile ) -{ - g_assert( tile->cache->ntiles >= 0 ); - - tile->time = tile->cache->time++; -} - -/* Fill a tile with pixels. - */ -static int -vips_tile_fill( VipsTile *tile, VipsRegion *in ) -{ - VipsRect area; - - VIPS_DEBUG_MSG( "tilecache: filling tile %d x %d\n", tile->x, tile->y ); - - area.left = tile->x; - area.top = tile->y; - area.width = tile->cache->tile_width; - area.height = tile->cache->tile_height; - - if( vips_region_prepare_to( in, tile->region, - &area, area.left, area.top ) ) - return( -1 ); - - vips_tile_touch( tile ); - - return( 0 ); -} - -/* Find existing tile, make a new tile, or if we have a full set of tiles, - * reuse LRU. - */ -static VipsTile * -vips_tile_find( VipsTileCache *cache, VipsRegion *in, int x, int y ) -{ - VipsTile *tile; - int oldest, topmost; - GSList *p; - - /* In cache already? - */ - if( (tile = vips_tile_search( cache, x, y )) ) { - vips_tile_touch( tile ); - - return( tile ); - } - - /* VipsTileCache not full? - */ - if( cache->max_tiles == -1 || - cache->ntiles < cache->max_tiles ) { - if( !(tile = vips_tile_new( cache )) || - vips_tile_move( tile, x, y ) || - vips_tile_fill( tile, in ) ) - return( NULL ); - - return( tile ); - } - - /* Reuse an old one. - */ - switch( cache->strategy ) { - case VIPS_CACHE_RANDOM: - oldest = cache->time; - tile = NULL; - for( p = cache->tiles; p; p = p->next ) { - VipsTile *t = (VipsTile *) p->data; - - if( t->time < oldest ) { - oldest = t->time; - tile = t; - } - } - break; - - case VIPS_CACHE_SEQUENTIAL: - topmost = cache->in->Ysize; - tile = NULL; - for( p = cache->tiles; p; p = p->next ) { - VipsTile *t = (VipsTile *) p->data; - - if( t->y < topmost ) { - topmost = t->y; - tile = t; - } - } - break; - - default: - g_assert( 0 ); - } - - g_assert( tile ); - - VIPS_DEBUG_MSG( "tilecache: reusing tile %d x %d\n", tile->x, tile->y ); - - if( vips_tile_move( tile, x, y ) || - vips_tile_fill( tile, in ) ) - return( NULL ); - - return( tile ); -} - -/* Generate func. - */ -static int -vips_tile_cache_gen( VipsRegion *or, - void *seq, void *a, void *b, gboolean *stop ) -{ - VipsRegion *in = (VipsRegion *) seq; - VipsTileCache *cache = (VipsTileCache *) b; - const int tw = cache->tile_width; - const int th = cache->tile_height; - VipsRect *r = &or->valid; - - /* Find top left of tiles we need. - */ - int xs = (r->left / tw) * tw; - int ys = (r->top / th) * th; - - int x, y; - - g_mutex_lock( cache->lock ); - - /* If the output region fits within a tile, we could save a copy by - * routing the output region directly to the tile. - * - * However this would mean that tile drop on minimise could then leave - * dangling pointers, if minimise were called on an active pipeline. - */ - - VIPS_DEBUG_MSG( "vips_tile_cache_gen: " - "left = %d, top = %d, width = %d, height = %d\n", - r->left, r->top, r->width, r->height ); - - for( y = ys; y < VIPS_RECT_BOTTOM( r ); y += th ) - for( x = xs; x < VIPS_RECT_RIGHT( r ); x += tw ) { - VipsTile *tile; - VipsRect tarea; - VipsRect hit; - - if( !(tile = vips_tile_find( cache, in, x, y )) ) { - g_mutex_unlock( cache->lock ); - return( -1 ); - } - - /* The area of the tile. - */ - tarea.left = x; - tarea.top = y; - tarea.width = tw; - tarea.height = th; - - /* The part of the tile that we need. - */ - vips_rect_intersectrect( &tarea, r, &hit ); - vips_region_copy( tile->region, or, &hit, - hit.left, hit.top ); - } - - g_mutex_unlock( cache->lock ); - - return( 0 ); -} - -static void -vips_tile_cache_minimise( VipsImage *image, VipsTileCache *cache ) -{ - vips_tile_cache_drop_all( cache ); -} - -static int -vips_tile_cache_build( VipsObject *object ) -{ - VipsConversion *conversion = VIPS_CONVERSION( object ); - VipsTileCache *cache = (VipsTileCache *) object; - - VIPS_DEBUG_MSG( "vips_tile_cache_build\n" ); - - if( VIPS_OBJECT_CLASS( vips_tile_cache_parent_class )->build( object ) ) - return( -1 ); - - if( vips_image_pio_input( cache->in ) ) - return( -1 ); - - if( vips_image_copy_fields( conversion->out, cache->in ) ) - return( -1 ); - vips_demand_hint( conversion->out, - VIPS_DEMAND_STYLE_SMALLTILE, cache->in, NULL ); - - g_signal_connect( conversion->out, "minimise", - G_CALLBACK( vips_tile_cache_minimise ), cache ); - - if( vips_image_generate( conversion->out, - vips_start_one, vips_tile_cache_gen, vips_stop_one, - cache->in, cache ) ) - return( -1 ); - - return( 0 ); -} - -static void -vips_tile_cache_class_init( VipsTileCacheClass *class ) -{ - GObjectClass *gobject_class = G_OBJECT_CLASS( class ); - VipsObjectClass *vobject_class = VIPS_OBJECT_CLASS( class ); - - VIPS_DEBUG_MSG( "vips_tile_cache_class_init\n" ); - - gobject_class->dispose = vips_tile_cache_dispose; - gobject_class->set_property = vips_object_set_property; - gobject_class->get_property = vips_object_get_property; - - vobject_class->nickname = "tilecache"; - vobject_class->description = _( "cache an image" ); - vobject_class->build = vips_tile_cache_build; - - VIPS_ARG_IMAGE( class, "in", 1, - _( "Input" ), - _( "Input image" ), - VIPS_ARGUMENT_REQUIRED_INPUT, - G_STRUCT_OFFSET( VipsTileCache, in ) ); - - VIPS_ARG_INT( class, "tile_width", 3, - _( "Tile width" ), - _( "Tile width in pixels" ), - VIPS_ARGUMENT_OPTIONAL_INPUT, - G_STRUCT_OFFSET( VipsTileCache, tile_width ), - 1, 1000000, 128 ); - - VIPS_ARG_INT( class, "tile_height", 3, - _( "Tile height" ), - _( "Tile height in pixels" ), - VIPS_ARGUMENT_OPTIONAL_INPUT, - G_STRUCT_OFFSET( VipsTileCache, tile_height ), - 1, 1000000, 128 ); - - VIPS_ARG_INT( class, "max_tiles", 3, - _( "Max tiles" ), - _( "Maximum number of tiles to cache" ), - VIPS_ARGUMENT_OPTIONAL_INPUT, - G_STRUCT_OFFSET( VipsTileCache, max_tiles ), - -1, 1000000, 1000 ); - - VIPS_ARG_ENUM( class, "strategy", 3, - _( "Strategy" ), - _( "Expected access pattern" ), - VIPS_ARGUMENT_OPTIONAL_INPUT, - G_STRUCT_OFFSET( VipsTileCache, strategy ), - VIPS_TYPE_CACHE_STRATEGY, VIPS_CACHE_RANDOM ); -} - -static void -vips_tile_cache_init( VipsTileCache *cache ) -{ - cache->tile_width = 128; - cache->tile_height = 128; - cache->max_tiles = 1000; - cache->strategy = VIPS_CACHE_RANDOM; - - cache->time = 0; - cache->ntiles = 0; - cache->lock = g_mutex_new(); - cache->tiles = NULL; -} - -/** - * vips_tilecache: - * @in: input image - * @out: output image - * @...: %NULL-terminated list of optional named arguments - * - * Optional arguments: - * - * @tile_width: width of tiles in cache - * @tile_height: height of tiles in cache - * @max_tiles: maximum number of tiles to cache - * @strategy: hint expected access pattern #VipsCacheStrategy - * - * This operation behaves rather like vips_copy() between images - * @in and @out, except that it keeps a cache of computed pixels. - * This cache is made of up to @max_tiles tiles (a value of -1 - * means any number of tiles), and each tile is of size @tile_width - * by @tile_height pixels. - * - * Each cache tile is made with a single call to - * vips_image_prepare(). - * - * When the cache fills, a tile is chosen for reuse. If @strategy is - * #VIPS_CACHE_RANDOM, then the least-recently-used tile is reused. If - * @strategy is #VIPS_CACHE_SEQUENTIAL, the top-most tile is reused. - * - * By default, @tile_width and @tile_height are 128 pixels, and the operation - * will cache up to 1,000 tiles. @strategy defaults to #VIPS_CACHE_RANDOM. - * - * This is a lower-level operation than vips_image_cache() since it does no - * subdivision and it single-threads its callee. It is suitable for caching - * the output of operations like exr2vips() on tiled images. - * - * See also: vips_image_cache(). - * - * Returns: 0 on success, -1 on error. - */ -int -vips_tilecache( VipsImage *in, VipsImage **out, ... ) -{ - va_list ap; - int result; - - va_start( ap, out ); - result = vips_call_split( "tilecache", ap, in, out ); - va_end( ap ); - - return( result ); -} diff --git a/libvips/conversion/tilecache.c b/libvips/conversion/tilecache.c index 34ca87e9..a8fe9a63 100644 --- a/libvips/conversion/tilecache.c +++ b/libvips/conversion/tilecache.c @@ -18,6 +18,7 @@ * - listen for "minimise" signal * 23/8/12 * - split to line and tile cache + * - use a hash table instead of a list */ /* @@ -67,13 +68,18 @@ /* A tile in our cache. */ -typedef struct { +typedef struct _VipsTile { struct _VipsBlockCache *cache; VipsRegion *region; /* Region with private mem for data */ + + /* Tile position. Just use left/top to calculate a hash. This is the + * key for the hash table. Don't use region->valid in case the region + * pointer is NULL. + */ + VipsRect pos; + int time; /* Time of last use for flush */ - int x; /* xy pos in VIPS image cods */ - int y; } VipsTile; typedef struct _VipsBlockCache { @@ -88,7 +94,7 @@ typedef struct _VipsBlockCache { int time; /* Update ticks for LRU here */ int ntiles; /* Current cache size */ GMutex *lock; /* Lock everything here */ - GSList *tiles; /* List of tiles */ + GHashTable *tiles; /* Tiles, hashed by coordinates */ } VipsBlockCache; typedef VipsConversionClass VipsBlockCacheClass; @@ -97,29 +103,10 @@ G_DEFINE_TYPE( VipsBlockCache, vips_block_cache, VIPS_TYPE_CONVERSION ); #define VIPS_TYPE_BLOCK_CACHE (vips_block_cache_get_type()) -static void -vips_tile_destroy( VipsTile *tile ) -{ - VipsBlockCache *cache = tile->cache; - - cache->tiles = g_slist_remove( cache->tiles, tile ); - cache->ntiles -= 1; - g_assert( cache->ntiles >= 0 ); - tile->cache = NULL; - - VIPS_UNREF( tile->region ); - - vips_free( tile ); -} - static void vips_block_cache_drop_all( VipsBlockCache *cache ) { - while( cache->tiles ) { - VipsTile *tile = (VipsTile *) cache->tiles->data; - - vips_tile_destroy( tile ); - } + g_hash_table_remove_all( cache->tiles ); } static void @@ -133,8 +120,29 @@ vips_block_cache_dispose( GObject *gobject ) G_OBJECT_CLASS( vips_block_cache_parent_class )->dispose( gobject ); } +static int +vips_tile_move( VipsTile *tile, int x, int y ) +{ + /* We are changing x/y and therefore the hash value. We must unlink + * from the old hash position and relink at the new place. + */ + g_hash_table_steal( tile->cache->tiles, &tile->pos ); + + tile->pos.left = x; + tile->pos.top = y; + tile->pos.width = tile->cache->tile_width; + tile->pos.height = tile->cache->tile_height; + + g_hash_table_insert( tile->cache->tiles, &tile->pos, tile ); + + if( vips_region_buffer( tile->region, &tile->pos ) ) + return( -1 ); + + return( 0 ); +} + static VipsTile * -vips_tile_new( VipsBlockCache *cache ) +vips_tile_new( VipsBlockCache *cache, int x, int y ) { VipsTile *tile; @@ -144,57 +152,44 @@ vips_tile_new( VipsBlockCache *cache ) tile->cache = cache; tile->region = NULL; tile->time = cache->time; - tile->x = -1; - tile->y = -1; - cache->tiles = g_slist_prepend( cache->tiles, tile ); + tile->pos.left = x; + tile->pos.top = y; + tile->pos.width = cache->tile_width; + tile->pos.height = cache->tile_height; + g_hash_table_insert( cache->tiles, &tile->pos, tile ); g_assert( cache->ntiles >= 0 ); cache->ntiles += 1; if( !(tile->region = vips_region_new( cache->in )) ) { - vips_tile_destroy( tile ); + g_hash_table_remove( cache->tiles, &tile->pos ); return( NULL ); } + vips__region_no_ownership( tile->region ); + if( vips_tile_move( tile, x, y ) ) { + g_hash_table_remove( cache->tiles, &tile->pos ); + return( NULL ); + } + return( tile ); } -static int -vips_tile_move( VipsTile *tile, int x, int y ) -{ - VipsRect area; - - tile->x = x; - tile->y = y; - - area.left = x; - area.top = y; - area.width = tile->cache->tile_width; - area.height = tile->cache->tile_height; - - if( vips_region_buffer( tile->region, &area ) ) - return( -1 ); - - return( 0 ); -} - /* Do we have a tile in the cache? - * - * FIXME .. use a hash? */ static VipsTile * vips_tile_search( VipsBlockCache *cache, int x, int y ) { - GSList *p; + VipsRect pos; + VipsTile *tile; - for( p = cache->tiles; p; p = p->next ) { - VipsTile *tile = (VipsTile *) p->data; + pos.left = x; + pos.top = y; + pos.width = cache->tile_width; + pos.height = cache->tile_height; + tile = (VipsTile *) g_hash_table_lookup( cache->tiles, &pos ); - if( tile->x == x && tile->y == y ) - return( tile ); - } - - return( NULL ); + return( tile ); } static void @@ -210,17 +205,10 @@ vips_tile_touch( VipsTile *tile ) static int vips_tile_fill( VipsTile *tile, VipsRegion *in ) { - VipsRect area; - VIPS_DEBUG_MSG( "tilecache: filling tile %d x %d\n", tile->x, tile->y ); - area.left = tile->x; - area.top = tile->y; - area.width = tile->cache->tile_width; - area.height = tile->cache->tile_height; - if( vips_region_prepare_to( in, tile->region, - &area, area.left, area.top ) ) + &tile->pos, tile->pos.left, tile->pos.top ) ) return( -1 ); vips_tile_touch( tile ); @@ -228,6 +216,37 @@ vips_tile_fill( VipsTile *tile, VipsRegion *in ) return( 0 ); } +typedef struct _VipsTileSearch { + VipsTile *tile; + + int oldest; + int topmost; +} VipsTileSearch; + +void +vips_tile_oldest( gpointer key, gpointer value, gpointer user_data ) +{ + VipsTile *tile = (VipsTile *) value; + VipsTileSearch *search = (VipsTileSearch *) user_data; + + if( tile->time < search->oldest ) { + search->oldest = tile->time; + search->tile = tile; + } +} + +void +vips_tile_topmost( gpointer key, gpointer value, gpointer user_data ) +{ + VipsTile *tile = (VipsTile *) value; + VipsTileSearch *search = (VipsTileSearch *) user_data; + + if( tile->pos.top < search->topmost ) { + search->topmost = tile->pos.top; + search->tile = tile; + } +} + /* Find existing tile, make a new tile, or if we have a full set of tiles, * reuse LRU. */ @@ -235,8 +254,7 @@ static VipsTile * vips_tile_find( VipsBlockCache *cache, VipsRegion *in, int x, int y ) { VipsTile *tile; - int oldest, topmost; - GSList *p; + VipsTileSearch search; /* In cache already? */ @@ -250,8 +268,7 @@ vips_tile_find( VipsBlockCache *cache, VipsRegion *in, int x, int y ) */ if( cache->max_tiles == -1 || cache->ntiles < cache->max_tiles ) { - if( !(tile = vips_tile_new( cache )) || - vips_tile_move( tile, x, y ) || + if( !(tile = vips_tile_new( cache, x, y )) || vips_tile_fill( tile, in ) ) return( NULL ); @@ -262,29 +279,19 @@ vips_tile_find( VipsBlockCache *cache, VipsRegion *in, int x, int y ) */ switch( cache->strategy ) { case VIPS_CACHE_RANDOM: - oldest = cache->time; - tile = NULL; - for( p = cache->tiles; p; p = p->next ) { - VipsTile *t = (VipsTile *) p->data; - - if( t->time < oldest ) { - oldest = t->time; - tile = t; - } - } + search.oldest = cache->time; + search.tile = NULL; + g_hash_table_foreach( cache->tiles, + vips_tile_oldest, &search ); + tile = search.tile; break; case VIPS_CACHE_SEQUENTIAL: - topmost = cache->in->Ysize; - tile = NULL; - for( p = cache->tiles; p; p = p->next ) { - VipsTile *t = (VipsTile *) p->data; - - if( t->y < topmost ) { - topmost = t->y; - tile = t; - } - } + search.topmost = cache->in->Ysize; + search.tile = NULL; + g_hash_table_foreach( cache->tiles, + vips_tile_topmost, &search ); + tile = search.tile; break; default: @@ -356,6 +363,38 @@ vips_block_cache_class_init( VipsBlockCacheClass *class ) VIPS_TYPE_CACHE_STRATEGY, VIPS_CACHE_RANDOM ); } +static unsigned int +vips_rect_hash( VipsRect *pos ) +{ + guint hash; + + /* We could shift down by the tile size? + */ + hash = pos->left ^ (pos->top << 16); + + return( hash ); +} + +static gboolean +vips_rect_equal( VipsRect *a, VipsRect *b ) +{ + return( a->left == b->left && a->top == b->top ); +} + +static void +vips_tile_destroy( VipsTile *tile ) +{ + VipsBlockCache *cache = tile->cache; + + cache->ntiles -= 1; + g_assert( cache->ntiles >= 0 ); + tile->cache = NULL; + + VIPS_UNREF( tile->region ); + + vips_free( tile ); +} + static void vips_block_cache_init( VipsBlockCache *cache ) { @@ -367,7 +406,11 @@ vips_block_cache_init( VipsBlockCache *cache ) cache->time = 0; cache->ntiles = 0; cache->lock = g_mutex_new(); - cache->tiles = NULL; + cache->tiles = g_hash_table_new_full( + (GHashFunc) vips_rect_hash, + (GEqualFunc) vips_rect_equal, + NULL, + (GDestroyNotify) vips_tile_destroy ); } typedef struct _VipsTileCache {