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:

  1. 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.
  2. Reference checking: Tiles with a reference count of 1 (meaning only the cache holds a reference) are marked for eviction.
  3. 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 from m_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_MB attribute enforces a hard RAM limit via m_max_memory_bytes, preventing render farm crashes.
  • Atomic tracking: m_mem_used maintains precise byte counts of cached tiles using lock-free atomic operations.
  • Clock-hand eviction: The check_max_mem() sweep uses a clock algorithm with m_tile_sweep_mutex to safely remove unreferenced tiles when limits are exceeded.
  • Reference counting: ImageCacheTile objects use RefCnt to ensure active tiles are never evicted during reads.
  • Thread efficiency: ImageCachePerThreadInfo enables lock-free access patterns for high-concurrency VFX workflows.
  • Global sharing: The shared_image_cache singleton 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →