# How ImageCache Manages Memory for Large Image Files in VFX Production

> Discover how ImageCache leverages its thread-safe tile cache, atomic memory accounting, and clock-hand eviction to manage RAM for massive VFX image files. Optimize your workflow today.

- Repository: [Academy Software Foundation/openimageio](https://github.com/academysoftwarefoundation/openimageio)
- Tags: internals
- Published: 2026-02-23

---

**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`](https://github.com/academysoftwarefoundation/openimageio/blob/main/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](https://github.com/AcademySoftwareFoundation/OpenImageIO/blob/main/src/libtexture/imagecache.cpp#L2485-L2500)) 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.

```cpp
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](https://github.com/AcademySoftwareFoundation/OpenImageIO/blob/main/src/libtexture/imagecache.cpp#L1990)). 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](https://github.com/AcademySoftwareFoundation/OpenImageIO/blob/main/src/libtexture/imagecache.cpp#L2892). This implementation uses a **clock-hand sweep** algorithm that traverses the tile hash table (lines [2938‑2990](https://github.com/AcademySoftwareFoundation/OpenImageIO/blob/main/src/libtexture/imagecache.cpp#L2938-L2990)), 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](https://github.com/AcademySoftwareFoundation/OpenImageIO/blob/main/src/libtexture/imagecache.cpp#L2951-L2954)), 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`](https://github.com/academysoftwarefoundation/openimageio/blob/main/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](https://github.com/AcademySoftwareFoundation/OpenImageIO/blob/main/src/libtexture/imagecache.cpp#L53-L56). 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:

```cpp
// 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

# 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.