diff --git a/src/main/java/bdv/img/cache/Cache.java b/src/main/java/bdv/img/cache/Cache.java
index e2edb3e9c47155d3939adb7cf9e1b40a655265ed..488e0ab9617886a20da5f9d08cbcee8701f89ecc 100644
--- a/src/main/java/bdv/img/cache/Cache.java
+++ b/src/main/java/bdv/img/cache/Cache.java
@@ -26,5 +26,5 @@ public interface Cache
 	/**
 	 * (Re-)initialize the IO time budget.
 	 */
-	public void initIoTimeBudget( final long[] partialBudget, final boolean reinitialize );
+	public void initIoTimeBudget( final long[] partialBudget );
 }
diff --git a/src/main/java/bdv/img/cache/CacheIoTiming.java b/src/main/java/bdv/img/cache/CacheIoTiming.java
index d8ff08b32dcfeccdfe9548c970b878ed3095aa4a..b2ff0cf83297e5ff5658527b993490bb540e2472 100644
--- a/src/main/java/bdv/img/cache/CacheIoTiming.java
+++ b/src/main/java/bdv/img/cache/CacheIoTiming.java
@@ -20,22 +20,31 @@ public class CacheIoTiming
 	 */
 	public static class IoTimeBudget
 	{
-		private final long[] initialBudget;
-
 		private final long[] budget;
 
-		public IoTimeBudget( final long[] initBudget )
+		public IoTimeBudget( final int numLevels )
+		{
+			budget = new long[ numLevels ];
+		}
+
+		public synchronized void reset( final long[] partialBudget )
 		{
-			initialBudget = initBudget.clone();
-			for ( int l = 1; l < initialBudget.length; ++l )
-				if ( initialBudget[ l ] > initialBudget[ l - 1 ] )
-					initialBudget[ l ] = initialBudget[ l - 1 ];
-			budget = initialBudget.clone();
+			if ( partialBudget == null )
+				clear();
+			else
+			{
+				for ( int i = 0; i < budget.length; ++i )
+					budget[ i ] = partialBudget.length > i ? partialBudget[ i ] : partialBudget[ partialBudget.length - 1 ];
+				for ( int i = 1; i < budget.length; ++i )
+					if ( budget[ i ] > budget[ i - 1 ] )
+						budget[ i ] = budget[ i - 1 ];
+			}
 		}
 
-		public void reset()
+		public synchronized void clear()
 		{
-			System.arraycopy( initialBudget, 0, budget, 0, budget.length );
+			for ( int i = 0; i < budget.length; ++i )
+				budget[ i ] = 0;
 		}
 
 		public synchronized long timeLeft( final int level )
diff --git a/src/main/java/bdv/img/cache/VolatileGlobalCellCache.java b/src/main/java/bdv/img/cache/VolatileGlobalCellCache.java
index 5745b990f35e5c8074684d4674f2b4f29b979307..e94e6b353eb5795535fae3da6e58b2ffd54d376c 100644
--- a/src/main/java/bdv/img/cache/VolatileGlobalCellCache.java
+++ b/src/main/java/bdv/img/cache/VolatileGlobalCellCache.java
@@ -4,6 +4,8 @@ import java.lang.ref.Reference;
 import java.lang.ref.SoftReference;
 import java.lang.ref.WeakReference;
 import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
 import java.util.concurrent.ConcurrentHashMap;
 
 import net.imglib2.img.basictypeaccess.volatiles.VolatileAccess;
@@ -99,6 +101,12 @@ public class VolatileGlobalCellCache< A extends VolatileAccess > implements Cach
 
 	protected final ConcurrentHashMap< Key, Reference< Entry > > softReferenceCache = new ConcurrentHashMap< Key, Reference< Entry > >();
 
+	/**
+	 * Keeps references to the {@link Entry entries} accessed in the current
+	 * frame, such that they cannot be cleared from the cache prematurely.
+	 */
+	protected final List< Entry > currentFrameEntries = Collections.synchronizedList( new ArrayList< Entry >() );
+
 	protected final BlockingFetchQueues< Key > queue;
 
 	protected volatile long currentQueueFrame = 0;
@@ -123,7 +131,10 @@ public class VolatileGlobalCellCache< A extends VolatileAccess > implements Cach
 				{
 					try
 					{
-						Thread.sleep( 1 );
+						synchronized ( lock )
+						{
+							lock.wait( waitMillis );
+						}
 					}
 					catch ( final InterruptedException e )
 					{}
@@ -139,6 +150,8 @@ public class VolatileGlobalCellCache< A extends VolatileAccess > implements Cach
 			}
 		}
 
+		private final Object lock = new Object();
+
 		private volatile long pauseUntilTimeMillis = 0;
 
 		public void pauseUntil( final long timeMillis )
@@ -150,25 +163,36 @@ public class VolatileGlobalCellCache< A extends VolatileAccess > implements Cach
 		public void wakeUp()
 		{
 			pauseUntilTimeMillis = 0;
+			synchronized ( lock )
+			{
+				lock.notify();
+			}
 		}
 	}
 
-	public void pauseFetcherThreads()
-	{
-		pauseFetcherThreadsUntil( System.currentTimeMillis() + 5 );
-	}
-
-	public void pauseFetcherThreads( final long ms )
+	/**
+	 * pause all {@link Fetcher} threads for the specified number of milliseconds.
+	 */
+	public void pauseFetcherThreadsFor( final long ms )
 	{
 		pauseFetcherThreadsUntil( System.currentTimeMillis() + ms );
 	}
 
+	/**
+	 * pause all {@link Fetcher} threads until the given time (see
+	 * {@link System#currentTimeMillis()}).
+	 */
 	public void pauseFetcherThreadsUntil( final long timeMillis )
 	{
 		for ( final Fetcher f : fetchers )
 			f.pauseUntil( timeMillis );
 	}
 
+	/**
+	 * Wake up all Fetcher threads immediately. This ends any
+	 * {@link #pauseFetcherThreadsFor(long)} and
+	 * {@link #pauseFetcherThreadsUntil(long)} set earlier.
+	 */
 	public void wakeFetcherThreads()
 	{
 		for ( final Fetcher f : fetchers )
@@ -179,7 +203,7 @@ public class VolatileGlobalCellCache< A extends VolatileAccess > implements Cach
 
 	private final CacheArrayLoader< A > loader;
 
-	public VolatileGlobalCellCache( final CacheArrayLoader< A > loader, final int numTimepoints, final int numSetups, final int maxNumLevels, final int[] maxLevels )
+	public VolatileGlobalCellCache( final CacheArrayLoader< A > loader, final int numTimepoints, final int numSetups, final int maxNumLevels, final int[] maxLevels, final int numFetcherThreads )
 	{
 		this.loader = loader;
 		this.numTimepoints = numTimepoints;
@@ -189,7 +213,7 @@ public class VolatileGlobalCellCache< A extends VolatileAccess > implements Cach
 
 		queue = new BlockingFetchQueues< Key >( maxNumLevels );
 		fetchers = new ArrayList< Fetcher >();
-		for ( int i = 0; i < 1; ++i ) // TODO: add numFetcherThreads parameter
+		for ( int i = 0; i < numFetcherThreads; ++i )
 		{
 			final Fetcher f = new Fetcher();
 			f.setDaemon( true );
@@ -240,7 +264,9 @@ public class VolatileGlobalCellCache< A extends VolatileAccess > implements Cach
 				{
 					final VolatileCell< A > cell = new VolatileCell< A >( cellDims, cellMin, loader.loadArray( timepoint, setup, level, cellDims, cellMin ) );
 					entry.data = cell;
+					entry.enqueueFrame = Long.MAX_VALUE;
 					softReferenceCache.put( entry.key, new SoftReference< Entry >( entry ) );
+					entry.notifyAll();
 				}
 			}
 		}
@@ -258,6 +284,7 @@ public class VolatileGlobalCellCache< A extends VolatileAccess > implements Cach
 			final Key k = entry.key;
 			final int priority = maxLevels[ k.setup ] - k.level;
 			queue.put( k, priority );
+			currentFrameEntries.add( entry );
 		}
 	}
 
@@ -272,26 +299,26 @@ public class VolatileGlobalCellCache< A extends VolatileAccess > implements Cach
 		final IoTimeBudget budget = stats.getIoTimeBudget();
 		final Key k = entry.key;
 		final int priority = maxLevels[ k.setup ] - k.level;
-		if ( budget.timeLeft( priority ) > 0 )
+		final long timeLeft = budget.timeLeft( priority );
+		if ( timeLeft > 0 )
 		{
-			pauseFetcherThreads( 1000 );
-			final long t0 = stats.getIoNanoTime();
-			stats.start();
-			while( true )
+			synchronized ( entry )
+			{
+				if ( entry.data.getData().isValid() )
+					return;
+				enqueueEntry( entry );
+				final long t0 = stats.getIoNanoTime();
+				stats.start();
 				try
 				{
-					loadEntryIfNotValid( entry );
-					break;
+					entry.wait( timeLeft  / 1000000l, 1 );
 				}
 				catch ( final InterruptedException e )
 				{}
-			stats.stop();
-			final long t = stats.getIoNanoTime() - t0;
-			budget.use( t, priority );
-			if ( budget.timeLeft( 0 ) <= 0 )
-				wakeFetcherThreads();
-			else
-				pauseFetcherThreads( 3 );
+				stats.stop();
+				final long t = stats.getIoNanoTime() - t0;
+				budget.use( t, priority );
+			}
 		}
 		else
 			enqueueEntry( entry );
@@ -432,6 +459,7 @@ public class VolatileGlobalCellCache< A extends VolatileAccess > implements Cach
 	public void prepareNextFrame()
 	{
 		queue.clear();
+		currentFrameEntries.clear();
 		++currentQueueFrame;
 	}
 
@@ -445,23 +473,14 @@ public class VolatileGlobalCellCache< A extends VolatileAccess > implements Cach
 	 *            smaller-equal the budget for level <em>j</em>. If <em>n</em>
 	 *            is smaller than the maximum number of mipmap levels, the
 	 *            remaining priority levels are filled up with budget[n].
-	 * @param reinitialize
-	 *            If true, the IO time budget is initialized to the given
-	 *            partial budget. If false, the IO time budget is reset to the
-	 *            previous initial values.
 	 */
 	@Override
-	public void initIoTimeBudget( final long[] partialBudget, final boolean reinitialize )
+	public void initIoTimeBudget( final long[] partialBudget )
 	{
 		final IoStatistics stats = CacheIoTiming.getThreadGroupIoStatistics();
-		if ( reinitialize || stats.getIoTimeBudget() == null )
-		{
-			final long[] budget = new long[ maxNumLevels ];
-			for ( int i = 0; i < budget.length; ++i )
-				budget[ i ] = partialBudget.length > i ? partialBudget[ i ] : partialBudget[ partialBudget.length - 1 ];
-			stats.setIoTimeBudget( new IoTimeBudget( budget ) );
-		}
-		stats.getIoTimeBudget().reset();
+		if ( stats.getIoTimeBudget() == null )
+			stats.setIoTimeBudget( new IoTimeBudget( maxNumLevels ) );
+		stats.getIoTimeBudget().reset( partialBudget );
 	}
 
 	public class Hdf5CellCache implements CellCache< A >
diff --git a/src/main/java/bdv/img/hdf5/Hdf5ImageLoader.java b/src/main/java/bdv/img/hdf5/Hdf5ImageLoader.java
index c61450cb0d52b7f4e9cb0b807981e4f35681440d..35b338a4868af60eceae2b00fb2d4d28a3981558 100644
--- a/src/main/java/bdv/img/hdf5/Hdf5ImageLoader.java
+++ b/src/main/java/bdv/img/hdf5/Hdf5ImageLoader.java
@@ -141,7 +141,7 @@ public class Hdf5ImageLoader implements ViewerImgLoader
 		cachedDimensions = new long[ numTimepoints * numSetups * maxNumLevels ][];
 		cachedExistence = new Boolean[ numTimepoints * numSetups * maxNumLevels ];
 
-		cache = new VolatileGlobalCellCache< VolatileShortArray >( new Hdf5VolatileShortArrayLoader( hdf5Reader ), numTimepoints, numSetups, maxNumLevels, maxLevels );
+		cache = new VolatileGlobalCellCache< VolatileShortArray >( new Hdf5VolatileShortArrayLoader( hdf5Reader ), numTimepoints, numSetups, maxNumLevels, maxLevels, 1 );
 	}
 
 	@Override
@@ -255,8 +255,7 @@ public class Hdf5ImageLoader implements ViewerImgLoader
 			System.err.println( "image data for " + view.getBasename() + " level " + level + " could not be found. Partition file missing?" );
 			return getMissingDataImage( view, level, new VolatileUnsignedShortType() );
 		}
-		final CellImg< VolatileUnsignedShortType, VolatileShortArray, VolatileCell< VolatileShortArray > >  img = prepareCachedImage( view, level, LoadingStrategy.VOLATILE );
-//		final CellImg< VolatileUnsignedShortType, VolatileShortArray, VolatileCell< VolatileShortArray > >  img = prepareCachedImage( view, level, LoadingStrategy.BUDGETED );
+		final CellImg< VolatileUnsignedShortType, VolatileShortArray, VolatileCell< VolatileShortArray > >  img = prepareCachedImage( view, level, LoadingStrategy.BUDGETED );
 		final VolatileUnsignedShortType linkedType = new VolatileUnsignedShortType( img );
 		img.setLinkedType( linkedType );
 		return img;
@@ -325,7 +324,11 @@ public class Hdf5ImageLoader implements ViewerImgLoader
 			final String cellsPath = Util.getCellsPath( timepoint, setup, level );
 			HDF5DataSetInformation info = null;
 			boolean exists = false;
-			cache.pauseFetcherThreads();
+			// pause Fetcher threads for 5 ms. There will be more calls to
+			// getImageDimension() because this happens when a timepoint is
+			// loaded, and all setups for the timepoint are loaded then. We
+			// don't want to interleave this with block loading operations.
+			cache.pauseFetcherThreadsFor( 5 );
 			synchronized ( hdf5Reader )
 			{
 				try {
diff --git a/src/main/java/bdv/img/hdf5/Hdf5VolatileShortArrayLoader.java b/src/main/java/bdv/img/hdf5/Hdf5VolatileShortArrayLoader.java
index 07eee2dc17566734dae5b92d45faad48df57d4fc..69da6ed268f0193cb8b8627102c74afa59ebab38 100644
--- a/src/main/java/bdv/img/hdf5/Hdf5VolatileShortArrayLoader.java
+++ b/src/main/java/bdv/img/hdf5/Hdf5VolatileShortArrayLoader.java
@@ -2,9 +2,6 @@ package bdv.img.hdf5;
 
 import static bdv.img.hdf5.Util.getCellsPath;
 import static bdv.img.hdf5.Util.reorder;
-
-import java.io.PrintStream;
-
 import net.imglib2.img.basictypeaccess.volatiles.array.VolatileShortArray;
 import bdv.img.cache.CacheArrayLoader;
 import ch.systemsx.cisd.base.mdarray.MDShortArray;
@@ -16,74 +13,27 @@ public class Hdf5VolatileShortArrayLoader implements CacheArrayLoader< VolatileS
 
 	private VolatileShortArray theEmptyArray;
 
-	PrintStream log = System.out;
-
 	public Hdf5VolatileShortArrayLoader( final IHDF5Reader hdf5Reader )
 	{
 		this.hdf5Reader = hdf5Reader;
 		theEmptyArray = new VolatileShortArray( 32 * 32 * 32, false );
 	}
 
-//	@Override
-//	public VolatileShortArray loadArray( final int timepoint, final int setup, final int level, final int[] dimensions, final long[] min ) throws InterruptedException
-//	{
-//		final MDShortArray array;
-//		synchronized ( hdf5Reader )
-//		{
-//			if ( Thread.interrupted() )
-//				throw new InterruptedException();
-//			array = hdf5Reader.readShortMDArrayBlockWithOffset( getCellsPath( timepoint, setup, level ), reorder( dimensions ), reorder( min ) );
-//		}
-//		return new VolatileShortArray( array.getAsFlatArray(), true );
-//	}
-
-	public static volatile long pStart = System.currentTimeMillis();
-
-	public static volatile long pEnd = System.currentTimeMillis();
-
-	public static volatile long tLoad = 0;
-
-	public static volatile long sLoad = 0;
-
 	private final int[] reorderedDimensions = new int[ 3 ];
 
 	private final long[] reorderedMin = new long[ 3 ];
 
 	@Override
-	public VolatileShortArray loadArray( final int timepoint, final int setup, final int level, final int[] dimensions, final long[] min )
+	public VolatileShortArray loadArray( final int timepoint, final int setup, final int level, final int[] dimensions, final long[] min ) throws InterruptedException
 	{
 		final MDShortArray array;
 		synchronized ( hdf5Reader )
 		{
-			pStart = System.currentTimeMillis();
-//			log.println( String.format( "%3d   %d   %d  { %2d, %2d, %2d }   { %4d, %4d, %4d }", timepoint, setup, level,
-//					dimensions[0], dimensions[1], dimensions[2],
-//					min[0], min[1], min[2] ) );
-			final long msBetweenLoads = pStart - pEnd;
-			if ( msBetweenLoads > 2 )
-			{
-				log.println( msBetweenLoads + " ms pause before this load." );
-//				final StringWriter sw = new StringWriter();
-//				final StackTraceElement[] trace = Thread.currentThread().getStackTrace();
-//				for ( final StackTraceElement elem : trace )
-//					sw.write( elem.getClassName() + "." + elem.getMethodName() + "\n" );
-//				log.println( sw.toString() );
-			}
-			final long t0 = System.currentTimeMillis();
+			if ( Thread.interrupted() )
+				throw new InterruptedException();
 			reorder( dimensions, reorderedDimensions );
 			reorder( min, reorderedMin );
 			array = hdf5Reader.readShortMDArrayBlockWithOffset( getCellsPath( timepoint, setup, level ), reorderedDimensions, reorderedMin );
-			pEnd = System.currentTimeMillis();
-			final long t = pEnd - t0;
-			final long size = array.size();
-			tLoad += t;
-			sLoad += size;
-			if ( sLoad > 1000000 )
-			{
-				log.println( String.format( "%.0f k shorts/sec ", ( ( double ) sLoad / tLoad ) ) );
-				tLoad = 1;
-				sLoad = 1;
-			}
 		}
 		return new VolatileShortArray( array.getAsFlatArray(), true );
 	}
@@ -103,4 +53,46 @@ public class Hdf5VolatileShortArrayLoader implements CacheArrayLoader< VolatileS
 	public int getBytesPerElement() {
 		return 2;
 	}
+
+//	PrintStream log = System.out;
+//	public static volatile long pStart = System.currentTimeMillis();
+//	public static volatile long pEnd = System.currentTimeMillis();
+//	public static volatile long tLoad = 0;
+//	public static volatile long sLoad = 0;
+//
+//	@Override
+//	public VolatileShortArray loadArray( final int timepoint, final int setup, final int level, final int[] dimensions, final long[] min )
+//	{
+//		final MDShortArray array;
+//		synchronized ( hdf5Reader )
+//		{
+//			pStart = System.currentTimeMillis();
+//			final long msBetweenLoads = pStart - pEnd;
+//			if ( msBetweenLoads > 2 )
+//			{
+//				log.println( msBetweenLoads + " ms pause before this load." );
+//				final StringWriter sw = new StringWriter();
+//				final StackTraceElement[] trace = Thread.currentThread().getStackTrace();
+//				for ( final StackTraceElement elem : trace )
+//					sw.write( elem.getClassName() + "." + elem.getMethodName() + "\n" );
+//				log.println( sw.toString() );
+//			}
+//			final long t0 = System.currentTimeMillis();
+//			reorder( dimensions, reorderedDimensions );
+//			reorder( min, reorderedMin );
+//			array = hdf5Reader.readShortMDArrayBlockWithOffset( getCellsPath( timepoint, setup, level ), reorderedDimensions, reorderedMin );
+//			pEnd = System.currentTimeMillis();
+//			final long t = System.currentTimeMillis() - t0;
+//			final long size = array.size();
+//			tLoad += t;
+//			sLoad += size;
+//			if ( sLoad > 1000000 )
+//			{
+//				log.println( String.format( "%.0f k shorts/sec ", ( ( double ) sLoad / tLoad ) ) );
+//				tLoad = 1;
+//				sLoad = 1;
+//			}
+//		}
+//		return new VolatileShortArray( array.getAsFlatArray(), true );
+//	}
 }
diff --git a/src/main/java/bdv/tools/RecordMovieDialog.java b/src/main/java/bdv/tools/RecordMovieDialog.java
index cb5518a97e5dd636f2acb7d465a8768bf7ca39e4..f5cc64a3037e299bd94447b944f3f2cb529d9c3c 100644
--- a/src/main/java/bdv/tools/RecordMovieDialog.java
+++ b/src/main/java/bdv/tools/RecordMovieDialog.java
@@ -262,7 +262,7 @@ public class RecordMovieDialog extends JDialog implements OverlayRenderer
 		final MultiResolutionRenderer renderer = new MultiResolutionRenderer( target, new PainterThread( null ), new double[] { 1 }, 0, false, 1, null, false, new Cache()
 		{
 			@Override
-			public void initIoTimeBudget( final long[] partialBudget, final boolean reinitialize )
+			public void initIoTimeBudget( final long[] partialBudget )
 			{}
 
 			@Override
diff --git a/src/main/java/bdv/viewer/render/MultiResolutionRenderer.java b/src/main/java/bdv/viewer/render/MultiResolutionRenderer.java
index eb95b33ceeabb6752526d77f37ed096c13e3a467..3fa0227dec204a594c76a768722caa98faf044b8 100644
--- a/src/main/java/bdv/viewer/render/MultiResolutionRenderer.java
+++ b/src/main/java/bdv/viewer/render/MultiResolutionRenderer.java
@@ -237,7 +237,10 @@ public class MultiResolutionRenderer
 	protected int previousTimepoint;
 
 	// TODO: should be settable
-	protected long[] iobudget = new long[] { 500l * 1000000l,  0l * 1000000l };
+	protected long[] iobudget = new long[] { 100l * 1000000l,  10l * 1000000l };
+
+	// TODO: should be settable
+	protected boolean prefetchCells = true;
 
 	/**
 	 * @param display
@@ -453,19 +456,6 @@ public class MultiResolutionRenderer
 		}
 
 		// try rendering
-//		if ( clearQueue )
-//			cache.prepareNextFrame();
-
-//		if ( clearQueue )
-//			try
-//			{
-//				Thread.sleep( 100 );
-//			}
-//			catch ( final InterruptedException e1 )
-//			{
-//				// TODO Auto-generated catch block
-//				e1.printStackTrace();
-//			}
 		final boolean success = p.map( createProjector );
 		final long rendertime = p.getLastFrameRenderNanoTime();
 
@@ -497,11 +487,9 @@ public class MultiResolutionRenderer
 						if ( rendertime < targetRenderNanos && maxScreenScaleIndex > 0 )
 							maxScreenScaleIndex--;
 					}
-//					System.out.println( "created projector" );
 //					System.out.println( String.format( "rendering:%4d ms", rendertime / 1000000 ) );
 //					System.out.println( "scale = " + currentScreenScaleIndex );
 //					System.out.println( "maxScreenScaleIndex = " + maxScreenScaleIndex + "  (" + screenImages[ maxScreenScaleIndex ][ 0 ].dimension( 0 ) + " x " + screenImages[ maxScreenScaleIndex ][ 0 ].dimension( 1 ) + ")" );
-//					System.out.println();
 				}
 
 				if ( currentScreenScaleIndex > 0 )
@@ -517,8 +505,6 @@ public class MultiResolutionRenderer
 					requestRepaint( currentScreenScaleIndex );
 				}
 			}
-//			else
-//				System.out.println("! success");
 		}
 
 		return success;
@@ -530,7 +516,6 @@ public class MultiResolutionRenderer
 	 */
 	public synchronized void requestRepaint()
 	{
-//		System.out.println("requestRepaint()");
 		newFrameRequest = true;
 		requestRepaint( maxScreenScaleIndex );
 	}
@@ -556,7 +541,7 @@ public class MultiResolutionRenderer
 	{
 		synchronized ( viewerState )
 		{
-			cache.initIoTimeBudget( iobudget, false );
+			cache.initIoTimeBudget( null ); // clear time budget such that prefetching doesn't wait for loading blocks.
 			final List< SourceState< ? > > sources = viewerState.getSources();
 			final List< Integer > visibleSourceIndices = viewerState.getVisibleSourceIndices();
 			VolatileProjector projector;
@@ -586,6 +571,7 @@ public class MultiResolutionRenderer
 				projector = new AccumulateProjectorARGB( sourceProjectors, sourceImages, screenImage, numRenderingThreads );
 			}
 			previousTimepoint = viewerState.getCurrentTimepoint();
+			cache.initIoTimeBudget( iobudget );
 			return projector;
 		}
 	}
@@ -667,12 +653,14 @@ public class MultiResolutionRenderer
 			levels.add( getTransformedSource( viewerState, spimSource, screenScaleTransform, bestLevel ) );
 			if ( nLevels - 1 != bestLevel )
 				levels.add( getTransformedSource( viewerState, spimSource, screenScaleTransform, nLevels - 1 ) );
-			final long t0 = System.currentTimeMillis();
-			if ( nLevels - 1 != bestLevel )
-				prefetch( viewerState, spimSource, screenScaleTransform, nLevels - 1, screenImage );
-			prefetch( viewerState, spimSource, screenScaleTransform, bestLevel, screenImage );
-			final long t1 = System.currentTimeMillis() - t0;
-			System.out.println( "prefetch: " + t1 + " ms" );
+
+			if ( prefetchCells )
+			{
+				if ( nLevels - 1 != bestLevel )
+					prefetch( viewerState, spimSource, screenScaleTransform, nLevels - 1, screenImage );
+				prefetch( viewerState, spimSource, screenScaleTransform, bestLevel, screenImage );
+			}
+
 			// slight abuse of newFrameRequest: we only want this two-pass
 			// rendering to happen once then switch to normal multi-pass
 			// rendering if we remain longer on this frame.
@@ -682,11 +670,10 @@ public class MultiResolutionRenderer
 		{
 			for ( int i = bestLevel; i < nLevels; ++i )
 				levels.add( getTransformedSource( viewerState, spimSource, screenScaleTransform, i ) );
-			final long t0 = System.currentTimeMillis();
-			for ( int i = nLevels - 1; i >= bestLevel; --i )
-				prefetch( viewerState, spimSource, screenScaleTransform, i, screenImage );
-			final long t1 = System.currentTimeMillis() - t0;
-			System.out.println( "prefetch: " + t1 + " ms" );
+
+			if ( prefetchCells )
+				for ( int i = nLevels - 1; i >= bestLevel; --i )
+					prefetch( viewerState, spimSource, screenScaleTransform, i, screenImage );
 		}
 //		for ( int i = bestLevel - 1; i >= 0; --i )
 //			levels.add( getTransformedSource( viewerState, spimSource, screenScaleTransform, i ) );
@@ -714,24 +701,17 @@ public class MultiResolutionRenderer
 			final int mipmapIndex,
 			final Dimensions screenInterval )
 	{
-//		if ( mipmapIndex != source.getNumMipmapLevels() - 1 )
-//			return;
 		final int timepoint = viewerState.getCurrentTimepoint();
 		final RandomAccessibleInterval< T > img = source.getSource( timepoint, mipmapIndex );
 		if ( CellImg.class.isInstance( img ) )
 		{
-//			System.out.println( "is CellImg" );
-			final CellImg< ?, ?, ? > cellImg = ( CellImg ) img;
+			final CellImg< ?, ?, ? > cellImg = ( CellImg< ?, ?, ? > ) img;
 			final int[] cellDimensions = new int[ 3 ];
 			cellImg.getCells().cellDimensions( cellDimensions );
 			final long[] dimensions = new long[ 3 ];
 			cellImg.dimensions( dimensions );
 			final RandomAccess< ? > cellsRandomAccess = cellImg.getCells().randomAccess();
 
-//			System.out.println( net.imglib2.util.Util.printCoordinates( cellDimensions ) );
-//			System.out.println( net.imglib2.util.Util.printCoordinates( dimensions ) );
-//			System.out.println();
-
 			final Interpolation interpolation = viewerState.getInterpolation();
 
 			final AffineTransform3D sourceToScreen = new AffineTransform3D();
@@ -739,7 +719,7 @@ public class MultiResolutionRenderer
 			sourceToScreen.concatenate( source.getSourceTransform( timepoint, mipmapIndex ) );
 			sourceToScreen.preConcatenate( screenScaleTransform );
 
-			PlayWithGeometry.scan( sourceToScreen, cellDimensions, dimensions, screenInterval, interpolation, cellsRandomAccess );
+			Prefetcher.fetchCells( sourceToScreen, cellDimensions, dimensions, screenInterval, interpolation, cellsRandomAccess );
 		}
 	}
 }
diff --git a/src/main/java/bdv/viewer/render/PlayWithGeometry.java b/src/main/java/bdv/viewer/render/PlayWithGeometry.java
deleted file mode 100644
index 05a76ab823f7f6dffeeffdc714c3161b5cf917c6..0000000000000000000000000000000000000000
--- a/src/main/java/bdv/viewer/render/PlayWithGeometry.java
+++ /dev/null
@@ -1,196 +0,0 @@
-package bdv.viewer.render;
-
-import net.imglib2.Dimensions;
-import net.imglib2.RandomAccess;
-import net.imglib2.RealPoint;
-import net.imglib2.realtransform.AffineTransform3D;
-import bdv.viewer.Interpolation;
-
-public class PlayWithGeometry
-{
-	public static void scan( final AffineTransform3D sourceToScreen, final int[] cellDimensions, final long[] dimensions, final Dimensions screenInterval,  final Interpolation interpolation, final RandomAccess< ? > cellsRandomAccess )
-	{
-		new Prefetcher().scan( sourceToScreen, cellDimensions, dimensions, screenInterval, interpolation, cellsRandomAccess );
-	}
-
-	static class Prefetcher
-	{
-		private final double[] xStep = new double[ 3 ];
-
-		private final double[] offsetNeg = new double[ 3 ];
-
-		private final double[] offsetPos = new double[ 3 ];
-
-		private static final double eps = 0.0000001;
-
-		public void scan( final AffineTransform3D sourceToScreen, final int[] cellDimensions, final long[] dimensions, final Dimensions screenInterval, final Interpolation interpolation, final RandomAccess< ? > cellsRandomAccess )
-		{
-			final RealPoint pSource = new RealPoint( 3 );
-			final RealPoint pScreen = new RealPoint( 3 );
-			final int[] minCell = new int[ 3 ];
-			final int[] maxCell = new int[ 3 ];
-			final int w = ( int ) screenInterval.dimension( 0 );
-			final int h = ( int ) screenInterval.dimension( 1 );
-
-			for ( int d = 0; d < 3; ++d )
-				maxCell[ d ] = ( int ) ( ( dimensions[ d ] - 1 ) / cellDimensions[ d ] );
-
-			// compute bounding box
-			final RealPoint[] screenCorners = new RealPoint[ 4 ];
-			screenCorners[ 0 ] = new RealPoint( 0, 0, 0 );
-			screenCorners[ 1 ] = new RealPoint( w, 0, 0 );
-			screenCorners[ 2 ] = new RealPoint( w, h, 0 );
-			screenCorners[ 3 ] = new RealPoint( 0, h, 0 );
-			final RealPoint sourceCorner = new RealPoint( 3 );
-			final double[] bbMin = new double[] { Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY };
-			final double[] bbMax = new double[] { Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY };
-			for ( int i = 0; i < 4; ++i )
-			{
-				sourceToScreen.applyInverse( sourceCorner, screenCorners[ i ] );
-				for ( int d = 0; d < 3; ++d )
-				{
-					final double p = sourceCorner.getDoublePosition( d );
-					if ( p < bbMin[ d ] )
-						bbMin[ d ] = p;
-					if ( p > bbMax[ d ] )
-						bbMax[ d ] = p;
-				}
-			}
-			for ( int d = 0; d < 3; ++d )
-			{
-				minCell[ d ] = Math.min( maxCell[ d ], Math.max( ( int ) bbMin[ d ] / cellDimensions[ d ] - 1, 0 ) );
-				maxCell[ d ] = Math.max( 0, Math.min( ( int ) bbMax[ d ] / cellDimensions[ d ] + 1, maxCell[ d ] ) );
-			}
-
-			checkProtoCell( cellDimensions, sourceToScreen, interpolation );
-			getStep( cellDimensions, sourceToScreen );
-
-			pSource.setPosition( ( minCell[ 2 ] - 1 ) * cellDimensions[ 2 ], 2 );
-			for ( cellsRandomAccess.setPosition( minCell[ 2 ], 2 ); cellsRandomAccess.getIntPosition( 2 ) <= maxCell[ 2 ]; cellsRandomAccess.fwd( 2 ) )
-			{
-				pSource.move( cellDimensions[ 2 ], 2 );
-				pSource.setPosition( ( minCell[ 1 ] - 1 ) * cellDimensions[ 1 ], 1 );
-				for ( cellsRandomAccess.setPosition( minCell[ 1 ], 1 ); cellsRandomAccess.getIntPosition( 1 ) <= maxCell[ 1 ]; cellsRandomAccess.fwd( 1 ) )
-				{
-					pSource.move( cellDimensions[ 1 ], 1 );
-
-					// find first and last cell that hits z
-					pSource.setPosition( minCell[ 0 ] * cellDimensions[ 0 ], 0 );
-					sourceToScreen.apply( pSource, pScreen );
-					final double z0 = pScreen.getDoublePosition( 2 );
-					int nStart = 0;
-					int nStop = 0;
-					if ( xStep[ 2 ] > eps )
-					{
-						nStart = minCell[ 0 ] + Math.max( 0, ( int ) Math.ceil( - ( z0 + offsetPos[ 2 ] ) / xStep[ 2 ] ) );
-						if ( nStart > maxCell[ 0 ] )
-							continue;
-						nStop = Math.min( maxCell[ 0 ], minCell[ 0 ] + ( int ) Math.floor( - ( z0 + offsetNeg[ 2 ] ) / xStep[ 2 ] ) );
-						if ( nStop < minCell[ 0 ] )
-							continue;
-					}
-					else if ( xStep[ 2 ] < - eps )
-					{
-						nStart = minCell[ 0 ] + Math.max( 0, ( int ) Math.ceil( - ( z0 + offsetNeg[ 2 ] ) / xStep[ 2 ] ) );
-						if ( nStart > maxCell[ 0 ] )
-							continue;
-						nStop = Math.min( maxCell[ 0 ], minCell[ 0 ] + ( int ) Math.floor( - ( z0 + offsetPos[ 2 ] ) / xStep[ 2 ] ) );
-						if ( nStop < minCell[ 0 ] )
-							continue;
-					}
-					else
-					{
-						if ( z0 + offsetNeg[ 2 ] > 0 || z0 + offsetPos[ 2 ] < 0 )
-							continue;
-						nStart = minCell[ 0 ];
-						nStop = maxCell[ 0 ];
-					}
-
-					pSource.setPosition( nStart * cellDimensions[ 0 ], 0 );
-					for ( cellsRandomAccess.setPosition( nStart, 0 ); cellsRandomAccess.getIntPosition( 0 ) <= nStop; cellsRandomAccess.fwd( 0 ) )
-					{
-						sourceToScreen.apply( pSource, pScreen );
-						final double x = pScreen.getDoublePosition( 0 );
-						final double y = pScreen.getDoublePosition( 1 );
-						if (    ( x + offsetPos[ 0 ] >= 0 ) &&
-								( x + offsetNeg[ 0 ] < w ) &&
-								( y + offsetPos[ 1 ] >= 0 ) &&
-								( y + offsetNeg[ 1 ] < h ) )
-						{
-							cellsRandomAccess.get();
-						}
-						pSource.move( cellDimensions[ 0 ], 0 );
-					}
-				}
-			}
-		}
-
-		void getStep( final int[] cellStep, final AffineTransform3D sourceToScreen )
-		{
-			final RealPoint p0 = new RealPoint( 3 );
-			final RealPoint p1 = new RealPoint( 3 );
-			p1.setPosition( cellStep[ 0 ], 0 );
-			final RealPoint s0 = new RealPoint( 3 );
-			final RealPoint s1 = new RealPoint( 3 );
-			sourceToScreen.apply( p0, s0 );
-			sourceToScreen.apply( p1, s1 );
-			for ( int d = 0; d < 3; ++d )
-				xStep[ d ] = s1.getDoublePosition( d ) - s0.getDoublePosition( d );
-		}
-
-		void checkProtoCell( final int[] cellDims, final AffineTransform3D sourceToScreen, final Interpolation interpolation )
-		{
-			final RealPoint pSource = new RealPoint( 3 );
-			final RealPoint pScreenAnchor = new RealPoint( 3 );
-			sourceToScreen.apply( pSource, pScreenAnchor );
-
-			final RealPoint[] pScreen = new RealPoint[ 8 ];
-			final double[] cellMin = new double[ 3 ];
-			final double[] cellSize = new double[] { cellDims[ 0 ], cellDims[ 1 ], cellDims[ 2 ] };
-			if ( interpolation == Interpolation.NEARESTNEIGHBOR )
-			{
-				for ( int d = 0; d < 3; ++d )
-				{
-					cellMin[ d ] -= 0.5;
-					cellSize[ d ] -= 0.5;
-				}
-			}
-			else // Interpolation.NLINEAR
-			{
-				for ( int d = 0; d < 3; ++d )
-					cellMin[ d ] -= 1;
-			}
-			int i = 0;
-			for ( int z = 0; z < 2; ++z )
-			{
-				pSource.setPosition( ( z == 0 ) ? cellMin[ 2 ] : cellSize[ 2 ], 2 );
-				for ( int y = 0; y < 2; ++y )
-				{
-					pSource.setPosition( ( y == 0 ) ? cellMin[ 1 ] : cellSize[ 1 ], 1 );
-					for ( int x = 0; x < 2; ++x )
-					{
-						pSource.setPosition( ( x == 0 ) ? cellMin[ 0 ] : cellSize[ 0 ], 0 );
-						pScreen[ i ] = new RealPoint( 3 );
-						sourceToScreen.apply( pSource, pScreen[ i++ ] );
-					}
-				}
-			}
-
-			for ( int d = 0; d < 3; ++d )
-			{
-				double min = pScreen[ 0 ].getDoublePosition( d );
-				double max = pScreen[ 0 ].getDoublePosition( d );
-				for ( i = 1; i < 8; ++i )
-				{
-					final double p = pScreen[ i ].getDoublePosition( d );
-					if ( p < min )
-						min = p;
-					if ( p > max )
-						max = p;
-				}
-				offsetNeg[ d ] = min - pScreenAnchor.getDoublePosition( d );
-				offsetPos[ d ] = max - pScreenAnchor.getDoublePosition( d );
-			}
-		}
-	}
-}
diff --git a/src/main/java/bdv/viewer/render/Prefetcher.java b/src/main/java/bdv/viewer/render/Prefetcher.java
new file mode 100644
index 0000000000000000000000000000000000000000..527f45617d0db73dd345fad97fca96a4ea926733
--- /dev/null
+++ b/src/main/java/bdv/viewer/render/Prefetcher.java
@@ -0,0 +1,250 @@
+package bdv.viewer.render;
+
+import net.imglib2.Dimensions;
+import net.imglib2.RandomAccess;
+import net.imglib2.RealPoint;
+import net.imglib2.img.cell.CellImg;
+import net.imglib2.realtransform.AffineTransform3D;
+import bdv.viewer.Interpolation;
+
+public class Prefetcher
+{
+	/**
+	 * Access cells that will be needed for rendering to the screen.
+	 *
+	 * @param sourceToScreen
+	 *            source-to-screen transform
+	 * @param cellDimensions
+	 *            standard size of a source cell
+	 * @param dimensions
+	 *            dimensions of the source {@link CellImg}
+	 * @param screenInterval
+	 *            the interval of the screen that will be rendered
+	 * @param interpolation
+	 *            the interpolation method
+	 * @param cellsRandomAccess
+	 *            access to the source cells
+	 */
+	public static void fetchCells( final AffineTransform3D sourceToScreen, final int[] cellDimensions, final long[] dimensions, final Dimensions screenInterval,  final Interpolation interpolation, final RandomAccess< ? > cellsRandomAccess )
+	{
+		new Prefetcher().scan( sourceToScreen, cellDimensions, dimensions, screenInterval, interpolation, cellsRandomAccess );
+	}
+
+	private Prefetcher()
+	{}
+
+	/**
+	 * The transformed vector in screen coordinate when moving by by one cell in
+	 * X direction.
+	 */
+	private final double[] xStep = new double[ 3 ];
+
+	/**
+	 * The min of the bounding box of a cell under the current transform and
+	 * interpolation method, as seen from the min corner of the cell.
+	 */
+	private final double[] offsetNeg = new double[ 3 ];
+
+	/**
+	 * The max of the bounding box of a cell under the current transform and
+	 * interpolation method, as seen from the min corner of the cell.
+	 */
+	private final double[] offsetPos = new double[ 3 ];
+
+	private static final double eps = 0.0000001;
+
+	/**
+	 * Access cells that will be needed for rendering to the screen.
+	 *
+	 * @param sourceToScreen
+	 *            source-to-screen transform
+	 * @param cellDimensions
+	 *            standard size of a source cell
+	 * @param dimensions
+	 *            dimensions of the source {@link CellImg}
+	 * @param screenInterval
+	 *            the interval of the screen that will be rendered
+	 * @param interpolation
+	 *            the interpolation method
+	 * @param cellsRandomAccess
+	 *            access to the source cells
+	 */
+	private void scan( final AffineTransform3D sourceToScreen, final int[] cellDimensions, final long[] dimensions, final Dimensions screenInterval, final Interpolation interpolation, final RandomAccess< ? > cellsRandomAccess )
+	{
+		final RealPoint pSource = new RealPoint( 3 );
+		final RealPoint pScreen = new RealPoint( 3 );
+		final int[] minCell = new int[ 3 ];
+		final int[] maxCell = new int[ 3 ];
+		final int w = ( int ) screenInterval.dimension( 0 );
+		final int h = ( int ) screenInterval.dimension( 1 );
+
+		for ( int d = 0; d < 3; ++d )
+			maxCell[ d ] = ( int ) ( ( dimensions[ d ] - 1 ) / cellDimensions[ d ] );
+
+		// compute bounding box
+		final RealPoint[] screenCorners = new RealPoint[ 4 ];
+		screenCorners[ 0 ] = new RealPoint( 0, 0, 0 );
+		screenCorners[ 1 ] = new RealPoint( w, 0, 0 );
+		screenCorners[ 2 ] = new RealPoint( w, h, 0 );
+		screenCorners[ 3 ] = new RealPoint( 0, h, 0 );
+		final RealPoint sourceCorner = new RealPoint( 3 );
+		final double[] bbMin = new double[] { Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY };
+		final double[] bbMax = new double[] { Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY };
+		for ( int i = 0; i < 4; ++i )
+		{
+			sourceToScreen.applyInverse( sourceCorner, screenCorners[ i ] );
+			for ( int d = 0; d < 3; ++d )
+			{
+				final double p = sourceCorner.getDoublePosition( d );
+				if ( p < bbMin[ d ] )
+					bbMin[ d ] = p;
+				if ( p > bbMax[ d ] )
+					bbMax[ d ] = p;
+			}
+		}
+		for ( int d = 0; d < 3; ++d )
+		{
+			minCell[ d ] = Math.min( maxCell[ d ], Math.max( ( int ) bbMin[ d ] / cellDimensions[ d ] - 1, 0 ) );
+			maxCell[ d ] = Math.max( 0, Math.min( ( int ) bbMax[ d ] / cellDimensions[ d ] + 1, maxCell[ d ] ) );
+		}
+
+		checkProtoCell( cellDimensions, sourceToScreen, interpolation );
+		getXStep( cellDimensions, sourceToScreen );
+
+		pSource.setPosition( ( minCell[ 2 ] - 1 ) * cellDimensions[ 2 ], 2 );
+		for ( cellsRandomAccess.setPosition( minCell[ 2 ], 2 ); cellsRandomAccess.getIntPosition( 2 ) <= maxCell[ 2 ]; cellsRandomAccess.fwd( 2 ) )
+		{
+			pSource.move( cellDimensions[ 2 ], 2 );
+			pSource.setPosition( ( minCell[ 1 ] - 1 ) * cellDimensions[ 1 ], 1 );
+			for ( cellsRandomAccess.setPosition( minCell[ 1 ], 1 ); cellsRandomAccess.getIntPosition( 1 ) <= maxCell[ 1 ]; cellsRandomAccess.fwd( 1 ) )
+			{
+				pSource.move( cellDimensions[ 1 ], 1 );
+
+				// find first and last cell that hits z
+				pSource.setPosition( minCell[ 0 ] * cellDimensions[ 0 ], 0 );
+				sourceToScreen.apply( pSource, pScreen );
+				final double z0 = pScreen.getDoublePosition( 2 );
+				int nStart = 0;
+				int nStop = 0;
+				if ( xStep[ 2 ] > eps )
+				{
+					nStart = minCell[ 0 ] + Math.max( 0, ( int ) Math.ceil( - ( z0 + offsetPos[ 2 ] ) / xStep[ 2 ] ) );
+					if ( nStart > maxCell[ 0 ] )
+						continue;
+					nStop = Math.min( maxCell[ 0 ], minCell[ 0 ] + ( int ) Math.floor( - ( z0 + offsetNeg[ 2 ] ) / xStep[ 2 ] ) );
+					if ( nStop < minCell[ 0 ] )
+						continue;
+				}
+				else if ( xStep[ 2 ] < - eps )
+				{
+					nStart = minCell[ 0 ] + Math.max( 0, ( int ) Math.ceil( - ( z0 + offsetNeg[ 2 ] ) / xStep[ 2 ] ) );
+					if ( nStart > maxCell[ 0 ] )
+						continue;
+					nStop = Math.min( maxCell[ 0 ], minCell[ 0 ] + ( int ) Math.floor( - ( z0 + offsetPos[ 2 ] ) / xStep[ 2 ] ) );
+					if ( nStop < minCell[ 0 ] )
+						continue;
+				}
+				else
+				{
+					if ( z0 + offsetNeg[ 2 ] > 0 || z0 + offsetPos[ 2 ] < 0 )
+						continue;
+					nStart = minCell[ 0 ];
+					nStop = maxCell[ 0 ];
+				}
+
+				pSource.setPosition( nStart * cellDimensions[ 0 ], 0 );
+				for ( cellsRandomAccess.setPosition( nStart, 0 ); cellsRandomAccess.getIntPosition( 0 ) <= nStop; cellsRandomAccess.fwd( 0 ) )
+				{
+					sourceToScreen.apply( pSource, pScreen );
+					final double x = pScreen.getDoublePosition( 0 );
+					final double y = pScreen.getDoublePosition( 1 );
+					if (    ( x + offsetPos[ 0 ] >= 0 ) &&
+							( x + offsetNeg[ 0 ] < w ) &&
+							( y + offsetPos[ 1 ] >= 0 ) &&
+							( y + offsetNeg[ 1 ] < h ) )
+					{
+						cellsRandomAccess.get();
+					}
+					pSource.move( cellDimensions[ 0 ], 0 );
+				}
+			}
+		}
+	}
+
+	/**
+	 * Get the transformed vector in screen coordinate when moving by
+	 * cellStep[0] in X direction.
+	 */
+	private void getXStep( final int[] cellStep, final AffineTransform3D sourceToScreen )
+	{
+		final RealPoint p0 = new RealPoint( 3 );
+		final RealPoint p1 = new RealPoint( 3 );
+		p1.setPosition( cellStep[ 0 ], 0 );
+		final RealPoint s0 = new RealPoint( 3 );
+		final RealPoint s1 = new RealPoint( 3 );
+		sourceToScreen.apply( p0, s0 );
+		sourceToScreen.apply( p1, s1 );
+		for ( int d = 0; d < 3; ++d )
+			xStep[ d ] = s1.getDoublePosition( d ) - s0.getDoublePosition( d );
+	}
+
+	/**
+	 * Get the bounding box of a cell under the current transform and
+	 * interpolation method. Set {@link #offsetNeg} and {@link #offsetPos} as
+	 * seen from the min corner of the cell.
+	 */
+	private void checkProtoCell( final int[] cellDims, final AffineTransform3D sourceToScreen, final Interpolation interpolation )
+	{
+		final RealPoint pSource = new RealPoint( 3 );
+		final RealPoint pScreenAnchor = new RealPoint( 3 );
+		sourceToScreen.apply( pSource, pScreenAnchor );
+
+		final RealPoint[] pScreen = new RealPoint[ 8 ];
+		final double[] cellMin = new double[ 3 ];
+		final double[] cellSize = new double[] { cellDims[ 0 ], cellDims[ 1 ], cellDims[ 2 ] };
+		if ( interpolation == Interpolation.NEARESTNEIGHBOR )
+		{
+			for ( int d = 0; d < 3; ++d )
+			{
+				cellMin[ d ] -= 0.5;
+				cellSize[ d ] -= 0.5;
+			}
+		}
+		else // Interpolation.NLINEAR
+		{
+			for ( int d = 0; d < 3; ++d )
+				cellMin[ d ] -= 1;
+		}
+		int i = 0;
+		for ( int z = 0; z < 2; ++z )
+		{
+			pSource.setPosition( ( z == 0 ) ? cellMin[ 2 ] : cellSize[ 2 ], 2 );
+			for ( int y = 0; y < 2; ++y )
+			{
+				pSource.setPosition( ( y == 0 ) ? cellMin[ 1 ] : cellSize[ 1 ], 1 );
+				for ( int x = 0; x < 2; ++x )
+				{
+					pSource.setPosition( ( x == 0 ) ? cellMin[ 0 ] : cellSize[ 0 ], 0 );
+					pScreen[ i ] = new RealPoint( 3 );
+					sourceToScreen.apply( pSource, pScreen[ i++ ] );
+				}
+			}
+		}
+
+		for ( int d = 0; d < 3; ++d )
+		{
+			double min = pScreen[ 0 ].getDoublePosition( d );
+			double max = pScreen[ 0 ].getDoublePosition( d );
+			for ( i = 1; i < 8; ++i )
+			{
+				final double p = pScreen[ i ].getDoublePosition( d );
+				if ( p < min )
+					min = p;
+				if ( p > max )
+					max = p;
+			}
+			offsetNeg[ d ] = min - pScreenAnchor.getDoublePosition( d );
+			offsetPos[ d ] = max - pScreenAnchor.getDoublePosition( d );
+		}
+	}
+}