Chapter 4. Efficient Image Handling: Tiles

Table of Contents
Tile Cache
Tile Iterators

Whether you knew it or not, lots of blood, sweat, and tears have gone into making the modern GIMP a tile-based graphics application. And no, that doesn't have anything to do with the "Tile of the Day". What does it mean?

Well, our graphics are two dimensional, but the memory they're stored in is accessed by a one-dimensional index. The usual approach to storing a graphic is to store the whole thing as one long array, stringing one row on after another. This works fine, until your images get rather large. Say you have a 1000 x 1000 RGBA layer, that takes 4 MB. Drawing a vertical line down the image requires loading the entire thing into active memory, every row from top to bottom.

GIMP's approach is to break the image up into a series of tiles. Now when you draw that vertical line, only the affected tiles need be in memory.

What's that mean to the user? Smoother handling of large images, hopefully. What's that mean to you, the plug-in developer? It's just something to keep in mind if you're at all concerned about the speed with which you communicate with GIMP. If you can view the world in a similar fashion, things go much smoother for the two of you. Otherwise you'll be running around picking up pieces from different tiles… Also, if it's practical for you to work on a single tile at a time instead of the entire image, you'll use less memory yourself.

Tile Cache

If you're going through the image a row or column at a time, you're repeatedly accessing the same tiles over and over again. That means GIMP keeps sending you the tile, you use 1/64th of the information on it, ask for the next tile over, only to want that tile back again when you need the next row. This is rather inefficient, and consequently, slow.

One way to solve this problem would be to restructure your plug-in so it's not so troublesome in what it requests— we'll talk about that in a moment. But there's another way to solve it which is often much easier, from the programmer's prospective. And that's to use a tile cache.

This tile cache is separate from GIMP's tile cache, and is local to the plug-in. The plug-in will store tiles here after receiving them from GIMP (if there is room for them). That way, the next time you need data from that tile, it's already handy.

Enabling a tile cache is easy. You have a choice of two calls. From gimp.h:

void gimp_tile_cache_size(gulong kilobytes);
void gimp_tile_cache_ntiles(gulong ntiles);

The first sets the cache size in kilobytes, the second sets the cache size based on number of tiles… more or less. Here's what gimp_tile_cache_ntiles does (libgimp/gimptile.c, v1.0.0):

	gimp_tile_cache_size ((ntiles * _gimp_tile_width * _gimp_tile_height * 4) / 1024);

A couple things with this… The integer division by 1024 here may cause it to round down, which probably isn't a good thing. Also, the multiplication by four assumes four bytes per pixel, which isn't a good assumption if you're not always dealing with RGBA images. In short, you may want to do these calculations and call gimp_tile_cache_size yourself.

How big a tile cache to make? As many tiles as you'll be using at once. If you're going by row, for example, cache however many tiles are in one row. If you're both reading from tiles and writing to shadow tiles, double the size of the cache. You can find out the tile size by calling gimp_tile_width() and gimp_tile_height().

Stuff you Don't Need to Know: Tile size is defined at compile time in app/tile.h. The size 64x64 is optimal for an Intel x86 because their memory page size is 4096 bytes (64x64x1). You could change the tile size, but there are currently a few things which will break if you do, because they have some assumptions built in.

FIXME: Write about common data access methods, such as the row-buffer technique.

Tile Iterators

The first time I looked at plug-in code, I crept bravely in, until I hit a line that looked something like this:

at which point I ran away screaming.

After a while, I came back, sat down, and found out it's what Quartic refers to as a tile iterator. Its job is to go through the pixel region one tile at a time. You do not have to use this method, you may use the pixel_rgn_[get|set]_ calls as described above. But the reason it's useful is because a tile at a time is the most efficient way for GIMP to do things, and these two functions take care of the messy stuff for you.

gimp_pixel_rgns_register's first parameter tells it how many pixel regions you're going to be iterating over, followed by pointers to those pixel regions. After some hocus-pocus, it returns a pointer which is used by gimp_pixel_rgns_process.

Looking back at that plug-in's code:

Example 4-1. Using the tile iterator.

  GimpPixelRgn region;
  GimpDrawable *drawable;

  drawable = gimp_drawable_get(drawable_ID);

  gimp_pixel_rgn_init (&region, drawable, 
                       x1, y1, (x2 - x1), (y2 - y1), 
                       TRUE, TRUE);

  for (pr = gimp_pixel_rgns_register (1, &region); 
       pr != NULL; 
       pr = gimp_pixel_rgns_process (pr)) {
          /* Fun Goes On Here */

The upshot of all that gimp_pixel_rgns_ magic is, if while in the loop, you look back inside your region, you'll find all sorts of goodies. now holds the image data for the area beginning at region.x, region.y, with a width of region.w and height region.h. Here, take a look at the data structure (gimp.h):

struct _GimpPixelRgn
  guchar       *data;          /* pointer to region data */
  GimpDrawable *drawable;      /* pointer to drawable */
  guint         bpp;           /* bytes per pixel */
  guint         rowstride;     /* bytes per pixel row */
  guint         x, y;          /* origin */
  guint         w, h;          /* width and height of region */
  guint         dirty : 1;     /* will this region be dirtied? */
  guint         shadow : 1;    /* will this region use the shadow or normal tiles */
  guint         process_count; /* used internally */

To write to the image using this technique, all you have to do is change, provided that you flagged your region as "dirty" (and hopefully "shadow" as well) when you initialized it.