21.9.08

Translating Manually-Buffered Graphics

This is a sequel of sorts to my previous post on double-buffering and builds on what I did there.

When I was 14 or so, I wrote a game in Visual Basic 3. It was a snake game, very originally named Nybbles, where multiple players steered their hungry, ever-increasing snake around the board to eat targets. Each target caused the snake to increase in size, making it more difficult to move around the board without hitting itself (or the other players!). When I was making this, I ran into a classic redraw problem: it took too long to redraw the entire board.

Perhaps it wasn't the most efficient method, but I stored the snakes' position on the game board in a large 2-dimensional array, where each position had a number, 1-4 for a player, or 0 if the square was blank. I used this for drawing the board and collision detection. Drawing the board was technically simple: iterate through every square and draw the correct colour segment in each one.

Of course, since each snake moved 1 square per turn, I quickly realized I could merely draw the new head segment, and redraw the tail segment as a normal square. This method only required drawing two squares per snake, which was much more responsive.

Fast-forward a decade or so, and I have returned to the same problem. I'm working on my math graphing program, and just added zoom and translation control via the mouse.



The goal: To enable dragging the graph around to view a different part of it.

The problem: Recalculating the points and redrawing the entire screen can potentially be too slow.

The solution: Translate the valid portion of the already-drawn graph, and invalidate and redraw the rest. Since the MouseMove event can be fired for every pixel change if the mouse is moving slow enough, this should provide serious time savings.



Here's a basic look at step 1, which is shown above. This graph was dragged to the down and right, leaving the black area as an invalided region. First I need to translate the graph in the direction of the mouse movement, and second, I can fill in the black area to complete the graph.
protected override void OnMouseMove(MouseEventArgs e)
{
if (this.MouseMode == MouseMode.Move && e.Button == MouseButtons.Left)
{
int xOffset = (e.X - this.startDragLocation.X);
int yOffset = (e.Y - this.startDragLocation.Y);

Rectangle translated = new Rectangle(new Point(xOffset, yOffset), this.Size);
using (BufferedGraphics newBuffer =
this.context.Allocate(this.buffer.Graphics, translated))
{
// Draw the current image translated into the newBuffer,
// then copy it back, but with everything shifted.
this.buffer.Render(newBuffer.Graphics);
newBuffer.Render(this.buffer.Graphics);
}

using (Graphics graphics = this.CreateGraphics())
{
buffer.Render(graphics);

This is (slightly simplified) my code for moving the buffer. I create a new buffer with the offset passed in as the targetRectangle, and when I render the old buffer into it, it draws it with an offset. Then I render that back into the original buffer, overwriting the previous image. After this swap, the original buffer has been translated, so I can discard the new buffer.

This may not be the best or most efficient way to achieve this, but it's the only way I could find. From reading the documentation on MSDN, one would have have a hard time believing this actually works..but it does. I looked into the TranslateTransform of the buffer's graphics, but that didn't yield any results. This appears to be very fast, so until I hear otherwise, I'm keeping it.

Rendering the original (now current) buffer to the screen gives something like the screenshot above, except remnants of the previous screen is visible instead of the black border. This is the next phase, where I draw the invalidated regions.
Region invalidedRegion = new Region(new Rectangle(Point.Empty, this.Size));
invalidedRegion.Exclude(translated);

this.Invalidate(invalidedRegion); <---

The region to invalidate is pretty easy to calculate. I start with the footprint of the control, and exclude the location of the translated image. I planned to use the built-in handling for invalidated regions, but it turned out to be too slow. The issue is simply that it queues up OnPaint() messages to redraw all the rectangles contained in the region--but these will execute after any pending OnMouseMove messages. Rapid movement will cause multiple OnMouseMove events, and the invalid region for each needs to be redrawn in sync with the location of the graph.
RectangleF[] invalidRectangles = invalidedRegion.GetRegionScans(new Matrix());
foreach (RectangleF ir in invalidRectangles)
{
this.OnPaint(new PaintEventArgs(graphics,
new Rectangle((int)ir.X, (int)ir.Y,
(int)ir.Width, (int)ir.Height)));
}

This piece of code replaces the Invalidate() call. It breaks up the region into rectangles (only 1 or 2 rectangles can be created from the moves we are doing), and forces an immediate OnPaint() of that rectangle.

Of course, this entire operation requires me to rewrite the DrawAxis and DrawGraph methods to take a region, otherwise they will redraw everything, which defeats the purpose of these changes. OnPaint takes a PaintEventArgs which contains a clipping rectangle--the same one I constructed above. I will pass that to the Draw methods, so that they draw everything whenever the entire control is invalidated, and they will redraw selectively when dragging the image around.

The result: This is much faster than before. There is still some fine-tuning required, as it slows down a bit if I maximize the window. This causes some glitching at the edges, but overall it looks pretty smooth thanks to the double buffering.