/* SPDX-FileCopyrightText: 2024 Blender Authors * * SPDX-License-Identifier: GPL-2.0-or-later */ /** \file * \ingroup bke */ #undef DEBUG_MESSAGES #include /* for qsort */ #include #include #include "MEM_CacheLimiterC-Api.h" #include "MEM_guardedalloc.h" #include "BLI_ghash.h" #include "BLI_mempool.h" #include "BLI_string.h" #include "IMB_cache.hh" #include "IMB_imbuf.hh" #include "IMB_imbuf_types.hh" namespace blender { #ifdef DEBUG_MESSAGES # if defined __GNUC__ # define PRINT(format, args...) printf(format, ##args) # else # define PRINT(format, ...) printf(__VA_ARGS__) # endif #else # define PRINT(format, ...) #endif static MEM_CacheLimiterC *limitor = nullptr; /* Image buffers managed by a ImBufCache might be using their own caches (used by color * management). In practice this means that, for example, freeing ImBufCache used by MovieClip * will request freeing ImBufCache owned by ImBuf. Freeing ImBufCache needs to be thread-safe, * so regular mutex will not work here, hence the recursive lock. */ static std::recursive_mutex limitor_lock; struct ImBufCache { char name[64]; GHash *hash; GHashHashFP hashfp; GHashCmpFP cmpfp; ImBufCacheGetKeyDataFP getdatafp; ImBufCacheGetPriorityDataFP getprioritydatafp; ImBufCacheGetItemPriorityFP getitempriorityfp; ImBufCachePriorityDeleterFP prioritydeleterfp; BLI_mempool *keys_pool; BLI_mempool *items_pool; BLI_mempool *userkeys_pool; int keysize; void *last_userkey; int totseg, *points, proxy, render_flags; /* for visual statistics optimization */ int pad; }; struct ImBufCacheKey { ImBufCache *cache_owner; void *userkey; }; struct ImBufCacheItem { ImBufCache *cache_owner; ImBuf *ibuf; MEM_CacheLimiterHandleC *c_handle; void *priority_data; /* Indicates that #ibuf is null, because there was an error during load. */ bool added_empty; }; static uint imbufcache_hashhash(const void *keyv) { const ImBufCacheKey *key = static_cast(keyv); return key->cache_owner->hashfp(key->userkey); } static bool imbufcache_hashcmp(const void *av, const void *bv) { const ImBufCacheKey *a = static_cast(av); const ImBufCacheKey *b = static_cast(bv); return a->cache_owner->cmpfp(a->userkey, b->userkey); } static void imbufcache_keyfree(void *val) { ImBufCacheKey *key = static_cast(val); BLI_mempool_free(key->cache_owner->userkeys_pool, key->userkey); BLI_mempool_free(key->cache_owner->keys_pool, key); } static void imbufcache_valfree(void *val) { ImBufCacheItem *item = static_cast(val); ImBufCache *cache = item->cache_owner; PRINT("%s: cache '%s' free item %p buffer %p\n", __func__, cache->name, item, item->ibuf); if (item->c_handle) { limitor_lock.lock(); MEM_CacheLimiter_unmanage(item->c_handle); limitor_lock.unlock(); } if (item->ibuf) { IMB_freeImBuf(item->ibuf); } if (item->priority_data && cache->prioritydeleterfp) { cache->prioritydeleterfp(item->priority_data); } BLI_mempool_free(item->cache_owner->items_pool, item); } static void check_unused_keys(ImBufCache *cache) { GHashIterator gh_iter; BLI_ghashIterator_init(&gh_iter, cache->hash); while (!BLI_ghashIterator_done(&gh_iter)) { const ImBufCacheKey *key = static_cast( BLI_ghashIterator_getKey(&gh_iter)); const ImBufCacheItem *item = static_cast( BLI_ghashIterator_getValue(&gh_iter)); BLI_ghashIterator_step(&gh_iter); if (item->added_empty) { /* Don't remove entries that have been added empty. Those indicate that the image couldn't be * loaded correctly. */ continue; } bool remove = !item->ibuf; if (remove) { PRINT("%s: cache '%s' remove item %p without buffer\n", __func__, cache->name, item); } if (remove) { BLI_ghash_remove(cache->hash, key, imbufcache_keyfree, imbufcache_valfree); } } } static int compare_int(const void *av, const void *bv) { const int *a = static_cast(const_cast(av)); const int *b = static_cast(const_cast(bv)); return *a - *b; } static void imbufcache_destructor(void *p) { ImBufCacheItem *item = static_cast(p); if (item) { item->c_handle = nullptr; if (item->ibuf) { ImBufCache *cache = item->cache_owner; PRINT("%s: cache '%s' destroy item %p buffer %p\n", __func__, cache->name, item, item->ibuf); IMB_freeImBuf(item->ibuf); item->ibuf = nullptr; /* force cached segments to be updated */ MEM_SAFE_DELETE(cache->points); } } } static size_t get_size_in_memory(ImBuf *ibuf) { /* Keep textures in the memory to avoid constant file reload on viewport update. */ if (ibuf->userflags & IB_PERSISTENT) { return 0; } return IMB_get_size_in_memory(ibuf); } static size_t get_item_size(void *p) { size_t size = sizeof(ImBufCacheItem); ImBufCacheItem *item = static_cast(p); if (item->ibuf) { size += get_size_in_memory(item->ibuf); } return size; } static int get_item_priority(void *item_v, int default_priority) { ImBufCacheItem *item = static_cast(item_v); ImBufCache *cache = item->cache_owner; int priority; if (!cache->getitempriorityfp) { PRINT("%s: cache '%s' item %p use default priority %d\n", __func__, cache->name, item, default_priority); return default_priority; } priority = cache->getitempriorityfp(cache->last_userkey, item->priority_data); PRINT("%s: cache '%s' item %p priority %d\n", __func__, cache->name, item, priority); return priority; } static bool get_item_destroyable(void *item_v) { ImBufCacheItem *item = static_cast(item_v); if (item->ibuf == nullptr) { return true; } /* IB_BITMAPDIRTY means image was modified from inside blender and * changes are not saved to disk. * * Such buffers are never to be freed. */ if ((item->ibuf->userflags & IB_BITMAPDIRTY) || (item->ibuf->userflags & IB_PERSISTENT)) { return false; } return true; } void IMB_cache_init() { limitor = new_MEM_CacheLimiter(imbufcache_destructor, get_item_size); MEM_CacheLimiter_ItemPriority_Func_set(limitor, get_item_priority); MEM_CacheLimiter_ItemDestroyable_Func_set(limitor, get_item_destroyable); } void IMB_cache_destruct() { if (limitor) { delete_MEM_CacheLimiter(limitor); limitor = nullptr; } } ImBufCache *IMB_cache_create(const char *name, int keysize, GHashHashFP hashfp, GHashCmpFP cmpfp) { ImBufCache *cache; PRINT("%s: cache '%s' create\n", __func__, name); cache = MEM_new_zeroed("ImBufCache"); STRNCPY(cache->name, name); cache->keys_pool = BLI_mempool_create(sizeof(ImBufCacheKey), 0, 64, BLI_MEMPOOL_NOP); cache->items_pool = BLI_mempool_create(sizeof(ImBufCacheItem), 0, 64, BLI_MEMPOOL_NOP); cache->userkeys_pool = BLI_mempool_create(keysize, 0, 64, BLI_MEMPOOL_NOP); cache->hash = BLI_ghash_new( imbufcache_hashhash, imbufcache_hashcmp, "MovieClip ImBuf cache hash"); cache->keysize = keysize; cache->hashfp = hashfp; cache->cmpfp = cmpfp; cache->proxy = -1; return cache; } void IMB_cache_set_getdata_callback(ImBufCache *cache, ImBufCacheGetKeyDataFP getdatafp) { cache->getdatafp = getdatafp; } void IMB_cache_set_priority_callback(ImBufCache *cache, ImBufCacheGetPriorityDataFP getprioritydatafp, ImBufCacheGetItemPriorityFP getitempriorityfp, ImBufCachePriorityDeleterFP prioritydeleterfp) { cache->last_userkey = MEM_new_uninitialized(cache->keysize, "movie cache last user key"); cache->getprioritydatafp = getprioritydatafp; cache->getitempriorityfp = getitempriorityfp; cache->prioritydeleterfp = prioritydeleterfp; } static void do_imbufcache_put(ImBufCache *cache, void *userkey, ImBuf *ibuf, bool need_lock) { ImBufCacheKey *key; ImBufCacheItem *item; if (!limitor) { IMB_cache_init(); } if (ibuf != nullptr) { IMB_refImBuf(ibuf); } key = static_cast(BLI_mempool_alloc(cache->keys_pool)); key->cache_owner = cache; key->userkey = BLI_mempool_alloc(cache->userkeys_pool); memcpy(key->userkey, userkey, cache->keysize); item = static_cast(BLI_mempool_alloc(cache->items_pool)); PRINT("%s: cache '%s' put %p, item %p\n", __func__, cache->name, ibuf, item); item->ibuf = ibuf; item->cache_owner = cache; item->c_handle = nullptr; item->priority_data = nullptr; item->added_empty = ibuf == nullptr; if (cache->getprioritydatafp) { item->priority_data = cache->getprioritydatafp(userkey); } BLI_ghash_reinsert(cache->hash, key, item, imbufcache_keyfree, imbufcache_valfree); if (cache->last_userkey) { memcpy(cache->last_userkey, userkey, cache->keysize); } if (need_lock) { limitor_lock.lock(); } item->c_handle = MEM_CacheLimiter_insert(limitor, item); MEM_CacheLimiter_ref(item->c_handle); MEM_CacheLimiter_enforce_limits(limitor); MEM_CacheLimiter_unref(item->c_handle); if (need_lock) { limitor_lock.unlock(); } /* cache limiter can't remove unused keys which points to destroyed values */ check_unused_keys(cache); MEM_SAFE_DELETE(cache->points); } void IMB_cache_put(ImBufCache *cache, void *userkey, ImBuf *ibuf) { do_imbufcache_put(cache, userkey, ibuf, true); } bool IMB_cache_put_if_possible(ImBufCache *cache, void *userkey, ImBuf *ibuf) { size_t mem_in_use, mem_limit, elem_size; bool result = false; elem_size = (ibuf == nullptr) ? 0 : get_size_in_memory(ibuf); mem_limit = MEM_CacheLimiter_get_maximum(); limitor_lock.lock(); mem_in_use = MEM_CacheLimiter_get_memory_in_use(limitor); if (mem_in_use + elem_size <= mem_limit) { do_imbufcache_put(cache, userkey, ibuf, false); result = true; } limitor_lock.unlock(); return result; } void IMB_cache_remove(ImBufCache *cache, void *userkey) { ImBufCacheKey key; key.cache_owner = cache; key.userkey = userkey; BLI_ghash_remove(cache->hash, &key, imbufcache_keyfree, imbufcache_valfree); } ImBuf *IMB_cache_get(ImBufCache *cache, void *userkey, bool *r_is_cached_empty) { ImBufCacheKey key; ImBufCacheItem *item; key.cache_owner = cache; key.userkey = userkey; item = static_cast(BLI_ghash_lookup(cache->hash, &key)); if (r_is_cached_empty) { *r_is_cached_empty = false; } if (item) { if (item->ibuf) { std::lock_guard lock(limitor_lock); /* Check again, the condition might have changed before we acquired the lock. */ if (item->ibuf) { MEM_CacheLimiter_touch(item->c_handle); IMB_refImBuf(item->ibuf); return item->ibuf; } } if (r_is_cached_empty && item->added_empty) { *r_is_cached_empty = true; } } return nullptr; } bool IMB_cache_has_frame(ImBufCache *cache, void *userkey) { ImBufCacheKey key; ImBufCacheItem *item; key.cache_owner = cache; key.userkey = userkey; item = static_cast(BLI_ghash_lookup(cache->hash, &key)); return item != nullptr; } void IMB_cache_free(ImBufCache *cache) { PRINT("%s: cache '%s' free\n", __func__, cache->name); BLI_ghash_free(cache->hash, imbufcache_keyfree, imbufcache_valfree); BLI_mempool_destroy(cache->keys_pool); BLI_mempool_destroy(cache->items_pool); BLI_mempool_destroy(cache->userkeys_pool); if (cache->points) { MEM_delete(cache->points); } if (cache->last_userkey) { MEM_delete_void(cache->last_userkey); } MEM_delete(cache); } void IMB_cache_cleanup(ImBufCache *cache, bool(cleanup_check_cb)(ImBuf *ibuf, void *userkey, void *userdata), void *userdata) { GHashIterator gh_iter; check_unused_keys(cache); BLI_ghashIterator_init(&gh_iter, cache->hash); while (!BLI_ghashIterator_done(&gh_iter)) { ImBufCacheKey *key = static_cast(BLI_ghashIterator_getKey(&gh_iter)); ImBufCacheItem *item = static_cast(BLI_ghashIterator_getValue(&gh_iter)); BLI_ghashIterator_step(&gh_iter); if (cleanup_check_cb(item->ibuf, key->userkey, userdata)) { PRINT("%s: cache '%s' remove item %p\n", __func__, cache->name, item); BLI_ghash_remove(cache->hash, key, imbufcache_keyfree, imbufcache_valfree); } } } void IMB_cache_get_cache_segments( ImBufCache *cache, int proxy, int render_flags, int *r_totseg, int **r_points) { *r_totseg = 0; *r_points = nullptr; if (!cache->getdatafp) { return; } if (cache->proxy != proxy || cache->render_flags != render_flags) { MEM_SAFE_DELETE(cache->points); } if (cache->points) { *r_totseg = cache->totseg; *r_points = cache->points; } else { int totframe = BLI_ghash_len(cache->hash); int *frames = MEM_new_array_zeroed(totframe, "movieclip cache frames"); int a, totseg = 0; GHashIterator gh_iter; a = 0; GHASH_ITER (gh_iter, cache->hash) { ImBufCacheKey *key = static_cast(BLI_ghashIterator_getKey(&gh_iter)); ImBufCacheItem *item = static_cast(BLI_ghashIterator_getValue(&gh_iter)); int framenr, curproxy, curflags; if (item->ibuf) { cache->getdatafp(key->userkey, &framenr, &curproxy, &curflags); if (curproxy == proxy && curflags == render_flags) { frames[a++] = framenr; } } } qsort(frames, totframe, sizeof(int), compare_int); /* count */ for (a = 0; a < totframe; a++) { if (a && frames[a] - frames[a - 1] != 1) { totseg++; } if (a == totframe - 1) { totseg++; } } if (totseg) { int b, *points; points = MEM_new_array_zeroed(2 * size_t(totseg), "movieclip cache segments"); /* fill */ for (a = 0, b = 0; a < totframe; a++) { if (a == 0) { points[b++] = frames[a]; } if (a && frames[a] - frames[a - 1] != 1) { points[b++] = frames[a - 1]; points[b++] = frames[a]; } if (a == totframe - 1) { points[b++] = frames[a]; } } *r_totseg = totseg; *r_points = points; cache->totseg = totseg; cache->points = points; cache->proxy = proxy; cache->render_flags = render_flags; } MEM_delete(frames); } } ImBufCacheIter *IMB_cacheIter_new(ImBufCache *cache) { GHashIterator *iter; check_unused_keys(cache); iter = BLI_ghashIterator_new(cache->hash); return reinterpret_cast(iter); } void IMB_cacheIter_free(ImBufCacheIter *iter) { BLI_ghashIterator_free(reinterpret_cast(iter)); } bool IMB_cacheIter_done(ImBufCacheIter *iter) { return BLI_ghashIterator_done(reinterpret_cast(iter)); } void IMB_cacheIter_step(ImBufCacheIter *iter) { BLI_ghashIterator_step(reinterpret_cast(iter)); } ImBuf *IMB_cacheIter_getImBuf(ImBufCacheIter *iter) { ImBufCacheItem *item = static_cast( BLI_ghashIterator_getValue(reinterpret_cast(iter))); return item->ibuf; } void *IMB_cacheIter_getUserKey(ImBufCacheIter *iter) { ImBufCacheKey *key = static_cast( BLI_ghashIterator_getKey(reinterpret_cast(iter))); return key->userkey; } } // namespace blender