How ImageCache Manages Memory for Large Image Files in VFX Production
ImageCache employs a thread-safe tile cache with atomic memory accounting and a clock-hand eviction sweep to enforce strict RAM budgets while streaming gigabyte-scale VFX assets.
OpenImageIO (OIIO) serves as the backbone for VFX pipelines handling terabyte-scale image archives. The ImageCache system solves the critical challenge of how to manage memory for large image files without loading entire gigabyte-scale assets into RAM. Through tile-based storage, reference counting, and deterministic eviction policies implemented in src/libtexture/imagecache.cpp, OIIO ensures render farms remain stable when processing massive multi-resolution EXR sequences.
Configuring the Memory Budget with max_memory_MB
VFX studios control RAM consumption via the max_memory_MB attribute, which defaults to approximately 1 GB. When you call ic->attribute("max_memory_MB", 8192.0f), the implementation in ImageCacheImpl::attribute() (lines 2485‑2500) converts this value to bytes and stores it in ImageCacheImpl::m_max_memory_bytes. This hard ceiling determines when the cache must begin evicting tiles to free space.
std::shared_ptr<ImageCache> ic = ImageCache::create(true);
ic->attribute("max_memory_MB", 16384.0f); // 16 GB budget for high-res plates
Atomic Memory Accounting
Every cached tile contributes to a global atomic counter called m_mem_used. When add_tile_to_cache() inserts a new tile, it atomically adds the tile’s memsize() to this counter (initialized at line 1990). This lock-free accounting enables precise tracking of the cache’s RAM footprint across multiple threads without contention.
The Clock-Hand Eviction Sweep
When m_mem_used exceeds m_max_memory_bytes, the check_max_mem() function triggers a cleanup cycle starting at line 2892. This implementation uses a clock-hand sweep algorithm that traverses the tile hash table (lines 2938‑2990), examining each tile’s reference count.
The sweep proceeds as follows:
- Lock acquisition: The thread attempts to lock
m_tile_sweep_mutex. If another thread is already cleaning, the current thread returns immediately, avoiding redundant work. - Reference checking: Tiles with a reference count of 1 (meaning only the cache holds a reference) are marked for eviction.
- Safe release: The sweep calls
ImageCacheTile::release()(lines 2951‑2954), which decrements the count. When the count reaches 0, the tile is deleted and its memory subtracted fromm_mem_used.
This process continues until the cache footprint drops below the configured limit.
Reference Counting Safety
ImageCacheTile objects inherit from RefCnt, providing intrusive reference counting. Active tiles held by ImageBuf instances or render threads maintain elevated reference counts, preventing the sweep from evicting data currently in use. Only stale tiles—those not currently being read—are eligible for removal, ensuring thread safety without stopping the world.
Per-Thread Optimization
To minimize lock contention, OIIO uses ImageCachePerThreadInfo structures declared in src/libtexture/imagecache_pvt.h. These per-thread objects cache statistics and maintain pointers back to the global ImageCacheImpl, allowing lock-free reads for most operations. When a thread requests a tile, it first checks its local context before hitting the global hash table, significantly reducing synchronization overhead in multi-threaded render farm environments.
The Shared Cache Singleton
Most OIIO tools (oiiotool, maketx, etc.) rely on a global shared_image_cache declared in the anonymous namespace at lines 53‑56. Created via ImageCache::create(true), this singleton prevents duplicate tile storage across different processing stages, maximizing memory efficiency when multiple operations access the same massive image sequences.
Practical Implementation Examples
The following examples demonstrate configuring and using ImageCache for production VFX workloads:
// C++: Configure a 16 GB cache for 8K plate processing
#include <OpenImageIO/imagecache.h>
#include <OpenImageIO/imagebuf.h>
using namespace OIIO;
std::shared_ptr<ImageCache> ic = ImageCache::create(true);
ic->attribute("max_memory_MB", 16384.0f);
// Load region on demand; cache handles tiling automatically
ImageBuf plate("big_shot_v001.exr");
float pixel[4];
plate.getpixel(4096, 2048, pixel); // Triggers tile load if not cached
// Force eviction of all tiles when shot changes
ic->invalidate_all();
# Python: Memory-constrained batch processing
import OpenImageIO as oiio
ic = oiio.ImageCache.create(shared=True)
ic.attribute("max_memory_MB", 8192.0) # 8 GB limit
img = oiio.ImageBuf("deep_scanline.exr")
pixels = img.get_pixels(0, 5120, 0, 2700) # Pulls only required tiles
ic.invalidate_all() # Clear before next asset
Summary
- Strict memory ceiling: The
max_memory_MBattribute enforces a hard RAM limit viam_max_memory_bytes, preventing render farm crashes. - Atomic tracking:
m_mem_usedmaintains precise byte counts of cached tiles using lock-free atomic operations. - Clock-hand eviction: The
check_max_mem()sweep uses a clock algorithm withm_tile_sweep_mutexto safely remove unreferenced tiles when limits are exceeded. - Reference counting:
ImageCacheTileobjects useRefCntto ensure active tiles are never evicted during reads. - Thread efficiency:
ImageCachePerThreadInfoenables lock-free access patterns for high-concurrency VFX workflows. - Global sharing: The
shared_image_cachesingleton maximizes efficiency across OIIO tool chains.
Frequently Asked Questions
How does ImageCache determine which tiles to evict when memory is full?
The clock-hand sweep in check_max_mem() traverses the tile hash table and evicts tiles with a reference count of 1, meaning they are not currently in use by any thread. This ensures only stale, unreferenced data is removed while active tiles remain cached.
Is ImageCache thread-safe for concurrent access across multiple threads?
Yes. ImageCache uses a combination of atomic counters for memory accounting and a dedicated m_tile_sweep_mutex to ensure only one thread performs eviction at a time. Additionally, ImageCachePerThreadInfo structures provide lock-free read paths for tile lookups, making the system safe for high-concurrency render farm environments.
What is the default memory limit for ImageCache?
The default max_memory_MB value is approximately 1 GB. You can query or modify this limit at runtime using ImageCache::attribute() to match your workstation or render node capacity.
How can I force ImageCache to release all memory immediately?
Call invalidate_all() on your ImageCache instance. This method drops every cached tile and resets m_mem_used to zero, effectively clearing the cache without destroying the instance. This is useful when switching between shots or freeing RAM for other pipeline stages.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →