The Need For Speed
smee, build 9
In build 9 I make three simple optimizations that improve rendering speed. Be sure to run this build in version 1.5 of the JDK, as 1.6 seems to have some issues that I am not currently fully aware of.
Smart Viewport Refresh
The first optimization involves the rendering of the viewport while scrolling. When scrolling around the map, you do not technically need to redraw the full viewport. All you need to redraw is the portion of the map that has just become visible. The rest you can just "shift" in the appropriate direction with a fast memory copy.
Luckily, Java's JViewport class, which is used by JScrollPane, takes care of the fast memory copy. All I had to do was figure out what portion of the viewport had just become visible. Fortunately, the implementation of JScrollPane makes this easy.
A JScrollPane "wraps" the component you wish to scroll, in my case MapPanel. When I override getPreferredSize() in MapPanel, I am basically telling Swing that this is a component that is the full size of the map, in pixels, according to the current zoom factor. You can think of it as one huge graphics surface.
In order to perform custom rendering for components in Swing, you must override the paintComponent() method. The important part is that you render the full component, in this case the full map. When your component is wrapped inside a JScrollPane, the clipping rectangle of your component is modified to the size and location of the scroll pane's viewport prior to each call to paintComponent().
More accurately, the scroll pane sets the clipping rectangle to the size of the "new" region that just scrolled into view! The "old" region is shifted speedily in the correct direction automatically, as mentioned above. This is the only region that truly needs to be accurately and fully rendered inside paintComponent().
Armed with this knowledge, I modified paintComponent() to get the current clipping rectangle and compute the region of tiles that fall within it. Now when scrolling a large 100x100 map, you'll notice quite an improvement in speed, particularly when scrolling in smaller increments.
This works out well for a map editor, since when you are editing tiles, you are generally sitting still most of the time. And when you do adjust the viewport, it is often in smaller increments when fleshing out nearby regions. There are situations when editing larger maps that this can still be an annoyancce when scrolling to far corners quickly, but there are two key ways to make this negligible as well. I will discuss these in future entries.
Tile Caching
When at higher zoom factors, you will also have noticed that the viewport was quite sluggish. Particularly when plotting tiles. Part of this reason is that Java does not render scaled bitmaps very quickly. They should technically take advantage of hardware acceleration when it is available, but even this often proves to be slower than desirable.
To help combat this problem, I created the TileCache class. It is a very simple class that maintains a hash of pre-scaled versions of tiles at various zoom factors. Raw hardware accelerated images with no transformations are plenty fast. We take a hit in memory consumption, but more advanced caching schemes can help manage this.
TileCache keys the hash by the "master" image, or the image at its original size. The value associated with each key is an instance of the inner class CachedVersions. This class maintains an array of four BufferedImage objects in an array, one element for each zoom factor.
The get_cached_tile() method of TileCache requests the tile (a BufferedImage) for a given zoom factor. If that tile is not yet present in the hash, it is added, with a corresponding new CachedVersions instance. In this way, tiles are lazily added to the cache as they are requested.
From here, the scaled version of the tile is requested from the CachedVersions instance, via its get_version() method. If the requested version of that tile has not yet been generated, it is created on the spot and added to the array of scaled versions for that tile at the appropriate index. In this way, scaled versions of each tile are lazily created as they are requested.
That's pretty much all there is to it! I wanted to keep things as simple as possible for introducing the concept. Future entries will see some more advanced and interesting caching schemes.
Smart Tile Plotting Refresh
The final and perhaps most annoying slowdown in previous builds involved plotting tiles when the viewport was rather large. Every plot triggered a full repaint of the viewport! This resulted in very few plots appearing when racing the cursor across the viewport during plotting.
The answer to this one is obvious enough: only refresh the tile which was just plotted.
It's almost that easy. The only caveat is that I allow tiles that are larger than the standard 32x32 size. The tree tile, for example, is 64x64. Sparing you the gruesome details, this basically means that I have to make sure and update a few tiles surrounding the current tile for each plot, so that the rendered portion remains accurate.
A further wrinkle is caused by refreshing these non-standard-sized tiles directly to video memory, which is what I am doing when I grab a hold of MapPanel's graphics context manually inside refresh_tile(). This caused a bit of annoying flicker when tree tiles overlap.
This is solved by creating a temporary back buffer that is the size of the region to be updated, and rendering the map to that first. Then the back buffer is rendered to the appropriate location within the MapPanel.
Obviously this would be unwise for particularly large non-standard tile sizes, but more advanced implementations can of course create a back buffer of the largest necessary size ahead of time and only use the needed portion. This dilutes the simplicity of the example however. Future entries will cover more advanced solutions.
Challenge
Here's a riddle. The rendering order of the tree tiles when plotting on any map smaller than the largest size (100x100) is incorrect! And I don't know why. You can plot trees and scroll them in and out of view to examine the results. It took me a while before I decided to call it quits.
Can you find the mysterious bug before I do? Is there an intellect that surpasses my own? Will the universe implode if you push the red button? Find out, in the next thrilling edition of... AEN'S BLOG.