Optimizing a drawing routine 
 
By Fredrik Mörk

OK, this is my first project that I’ve done with the eminent SineEngine and Blitter Object controls. When I started out it was intended as a minimal implementation of the controls, giving an idea of how easy they are to use. As I went along though, since they really are easy to use, I ended up doing some optimizations work as well. So my minimal implementation tutorial instead became a small tutorial on optimizing code. Still, the application is not really complicated. What does it do? Basically it uses the SineEngine control to receive X and Y coordinated traveling along a path, using these coordinated to draw lines that go back and forth across the screen, leaving a fading trail after them. The lines are, of course, drawn on a Blitter Object control. The sine settings are changed every 10 seconds. It also displays the current frame rate.

The interesting thing is that you can explore how some simple code optimizations affects performance, by switching them on and off. There are three things you can do:

  • Run the application in full screen mode using DirectX, or in a window.
  • Run the application with Windows API drawing commands or with internal VB drawing commands
  • Run the application so that it draws each line from black to white, or so that it draws all line segments with similar color each round.
You can combine these, so that you get 4 different combinations of drawing code that you can run, and two different screen modes. On my machine, that is not particularly fast (as a matter of fact, it’s pretty slow), the lowest frame rate I get in window mode is 9 FPS and the fastest is 77 (that’s a substantial increase!). On more modern machines the fastest code is so fast so that the frame rate should be limited to some max amount in order to make it enjoyable to look at. OK, so let’s go into the code. When making a graphics effect like this, the most logical way to do it is to draw each line, from one end to the other, at a time. The working order is as follows:
  • Work with the first line:
  • Draw the black element of that line
  • Continue with each element of that line until the white one is drawn
  • Continue with the next line, starting with the black element

The code could look something like this:

 
For i = nLBound To nUBound
                
    bobMain.Buffer.Line (Points(i, nLBound).X, Points(i, nLBound).Y)-(Points(i, nLBound).X, Points(i, nLBound).Y)
                
    For j = 1 To NBR_OF_POINTS
        bobMain.Buffer.Line (Points(j - 1, i).X, Points(j - 1, i).Y)-(Points(j, i).X, Points(j, i).Y), lLineShades(j)
    
Next j, i

A total of five lines of code is all that is needed to make the drawing. Of course, we have already updated the Points array with correct coordinates and everything (this is done in cooperation with the SineEngine, and consists of four lines of code). So, as you can see, not a lot of code is needed.

At this point I usually start to glance at the Windows API. So, I change the drawing commands into their GDI equivalents:

For i = nLBound To nUBound
                
    nRet = MoveToEx(lTargetHDc, Points(i, nLBound).X, Points(i, nLBound).Y, DummyPoint    
    nRet = LineTo(lTargetHDc, Points(i, nLBound).X, Points(i, nLBound).Y)
                
    For j = 1 To NBR_OF_POINTS
        bobMain.Buffer.ForeColor = lLineShades(j)
        nRet = LineTo(lTargetHDc, Points(j, i).X, Points(j, i).Y)
        
Next j, i

This method results in more lines of code (eight to be exact), but also result in a 50% performance increase on my machine. So, just by using GDI commands, which are implemented pretty much in the same way as VB command, we earn 50% performance!

But still I wasn’t happy…

The sines that are sent to the SineEngine contains 40 balls, and each line has 40 points (or segments), so for each frame 1600 line segments are drawn. For each frame, the application will run the inner for-loop 1600 times. With the GDI version on my machine, it would be executed over 14000 times per second. Obviously this is the place to look for optimization possibilities.

In code listing 2 above, the inner for-loop contains three lines of code. One of these is really essential; LineTo. Without this line of code, no line is draw. But just for testing purposes I commented that line out, in order to see how the frame rate was affected, and the result was that the frame rate increased by 50%. That’s not bad! OK, so what happens with the other line of code, the one that sets the color? Simple test; comment that line out, and make the LineTo execute; the frame rate increased by over 600%! That’s even better. So, the conclusion was that the code for setting the drawing color takes a lot more time that the drawing itself. And, of course, we really need to draw in the inner loop, since the lines won’t be visualized otherwise.

The problem with the above code is that is presents the slowest drawing code available in this application, but is usually the first one you would write, since it’s very logical. For each line, you draw each line segment in order, from black to white. This means that for each frame, the application does two things 1600 times: it sets the colour to draw with, and it draws a line. Now, each line has 40 segments, in colors (or rather, shades) ranging from black to white. All lines share these properties. So there are only 40 shades of gray. What would happen if we, instead of drawing each line at a time, drew each color at a time? Code listing 2 is changed into this;

For i = 1 To NBR_OF_POINTS
            
    bobMain.Buffer.ForeColor = lLineShades(i)
          
    For j = nLBound To nUBound
        nRet = MoveToEx(lTargetHDc, Points(i - 1, j).X, Points(i - 1, j).Y, DummyPoint)
        nRet = LineTo(lTargetHDc, Points(i, j).X, Points(i, j).Y)
        
Next j, i

The change is that I switched the for-loop positions, so that the one that was innermost before, now is the outer loop. So the work order is as follows:

  • Set color to draw with (start with black)
  • Loop through the lines and draw all segments with that color
  • Continue with next color, etc

Disregarding the full screen/window mode switch, the remaining four different drawing code blocks in the application give the following performance (on my computer):

Code Speed Index
VB drawing commands, one line at a time 100
GDI drawing commands, one line at a time 150
VB drawing commands, one color at a time 163
GDI drawing commands, one color at a time 950
So, by the time the slowest version draws 100 frames, the fastest one will draw 950.


Download the code used in this application and check it out.

Require SineMaker controls or S.M.A.K version: 1.0 or higher
Download the project: optdraw.zip (File size: 31 KB)

 

 

Copyright © 2002 Lars-Håkan Jönsson