JavaScript EditorFree JavaScript Editor     Ajax Editor 



Main Page
  Previous Section Next Section

Double Buffering

Thus far you've directly modified the contents of the primary surface, which is directly rasterized each frame by the video controller. This is fine for demos and static imagery, but what if you want to perform smooth animation? This is a definite problem; let me explain. As I alluded to earlier in the book, most computer animation is achieved by drawing each frame of animation in an offscreen buffer area and then blasting the image to the visible display surface very quickly, as shown in Figure 7.7.

Figure 7.7. Performing animation with double buffering.

graphics/07fig07.gif

This way the user can't see you erase images, generate the display, or anything else you might do in each frame. As long as the copying of the offscreen image to the visible surface is very quick, you could theoretically do it 15 times a second, or 15 fps, and still have a reasonably smooth game. However, the standard these days is at least 30 fps, so that has become the minimum to get high-quality animation.

The process of drawing an image in an offscreen area and then copying it to the display surface is called double buffering, and it's how 99 percent of all games perform animation. However, in the past (under DOS especially), there wasn't special hardware to help with this process. This obviously changed with the introduction of DirectX/ DirectDraw.

If acceleration hardware is present (and enough VRAM memory is on the video card), a process that's similar to double buffering, called page flipping, can be employed. Page flipping is roughly the same idea as double buffering, except that you draw to one of two potentially visible surfaces and then direct the hardware to make the other surface the active display surface. This basically removes the "copy" step because the hardware addressing system is used to point the video rasterizer to a different portion of memory. The end result is an instantaneous page flip and update of the visual on the screen (hence the term page flipping).

Of course, page flipping has always been possible, and many game programmers used it when programming Mode X modes (320x200, 320x240, 320x400). However, it's a down-low-and-direct technique. Assembly language and video controller programming was usually needed to accomplish the task. But with DirectDraw it's a snap. You'll get to it in the next section. I just wanted you to have an idea of where this chapter is going before I show you double buffering in detail.

Implementing double buffering is trivial. All you need to do is allocate a portion of memory that has the same geometry as the primary DirectDraw surface, draw each frame of animation on it, and then copy the double buffer memory to the primary display surface. Unfortunately, there's a problem with this scheme….

Let's say you've decided to create a 640x480x8 DirectDraw mode. Hence, you would need to allocate a double buffer that was 640x480 or a linear array of 307,200 bytes. And keep in mind that the data is mapped in a row-order form, one row for each row on the screen. This is no problem, though. Here's the code to create the double buffer:

UCHAR *double_buffer  = (UCHAR *)malloc(640*480);

Or, using the new operator in C++:

UCHAR *double_buffer = new UCHAR[640*480];

Either way you do it, you have an array of 307,200 bytes linearly addressable in memory that double_buffer points to. To address a single pixel at position (x,y), you would use the following code:

double_buffer[x + 640*y] = ...

Seems reasonable because there are 640 bytes per virtual line and you're assuming a rectangular mapping of 640 bytes per line and 480 lines. Okay, here's the problem: Assume that you've also locked a pointer to the primary display surface and it's in primary_buffer. In addition, assume that during the lock you've extracted the memory pitch and stored it in mempitch, as shown in Figure 7.8. If mempitch is equal to 640, you can use the following code to copy the double_buffer to the primary_buffer:

memcpy((void *)primary_buffer, (void *)double_buffer,640*480);
Figure 7.8. Primary display surfaces may have extra memory per line, causing addressing problems.

graphics/07fig08.gif

And almost instantly, the double_buffer will show up in the primary buffer.

TRICK

There's a potential optimization here. Notice, I'm using memcpy(). This function is rather slow because it only copies bytes (on some compilers). A better method would be to write your own DWORD or 32-bit copy function to move more data per cycle. You can do this with inline or external assembly language. You'll see how when you get to optimization theory, but this is a good example if you're taking advantage of the largest data chunk that the Pentium can process, which is a 32-bit value.


Everything seems fine, right? Wrong! The preceding memcpy() code will work only if mempitch or the primary surface stride is exactly 640 bytes per line. This may or may not be true. Alas, the preceding memcpy() code may fail terribly. A better way to write the double buffer copy function is to add a little function that tests if the memory pitch of the primary surface is 640. If so, the memcpy() is employed; if not, a line-by-line copy is used. A little slower, but the best you can do…. Here's the code for that:

// can we use a straight memory copy?
if (mempitch==640)
{
memcpy((void *)primary_buffer, (void *)double_buffer,640*480);
} // end if
else
{
// copy line by line, bummer!
for (int y=0; y<480; y++)
    {
    // copy next line of 640 bytes
    memcpy((void *)primary_buffer, (void
    *)double_buffer,640);

    // now for the tricky part...
    // advance each pointer ahead to next line

    // advance to next line which is mempitch bytes away
    primary_buffer+=mempitch;

   // we know that we need to advance 640 bytes per line
   double_buffer+=640;

   } // end for y

} // end else

Figure 7.9 shows the process graphically. As you can see, this is one of the times that you have to do the work—no cheating! However, at least you can optimize the code with 4-byte or 32-bit copy code later. That makes me feel a little better.

Figure 7.9. Copying the double buffer line by line.

graphics/07fig09.gif

As an example, I have created a demo that draws a set of random pixels on a double buffer and then copies the double buffer to the primary buffer in 640x480x8 mode. There's a long delay between copies, so you can see that the image is entirely different. The name of the program is DEMO7_4.CPP|EXE and it's on the CD. Remember to compile it yourself to add DDRAW.LIB to your project and have the header file paths set to the DirectX include directory. Here's the Game_Main() from the program, which is where all the action occurs:

int Game_Main(void *parms = NULL, int num_parms = 0)
{
// this is the main loop of the game, do all your processing
// here

UCHAR *primary_buffer = NULL; // used as alias to primary surface buffer

// make sure this isn't executed again
if (window_closed)
   return(0);

// for now test if user is hitting ESC and send WM_CLOSE
if (KEYDOWN(VK_ESCAPE))
   {
   PostMessage(main_window_handle,WM_CLOSE,0,0);
   window_closed = 1;
   } // end if

// erase double buffer
memset((void *)double_buffer,0, SCREEN_WIDTH*SCREEN_HEIGHT);

// you would perform game logic...

// draw the next frame into the double buffer
// plot 5000 random pixels
for (int index=0; index < 5000; index++)
    {
    int   x   = rand()%SCREEN_WIDTH;
    int   y   = rand()%SCREEN_HEIGHT;
    UCHAR col = rand()%256;
    double_buffer[x+y*SCREEN_WIDTH] = col;
    } // end for index

// copy the double buffer into the primary buffer
DDRAW_INIT_STRUCT(ddsd);

// lock the primary surface
lpddsprimary->Lock(NULL,&ddsd,
                   DDLOCK_SURFACEMEMORYPTR | DDLOCK_WAIT,NULL);

// get video pointer to primary surfce
primary_buffer = (UCHAR *)ddsd.lpSurface;

// test if memory is linear
if (ddsd.lPitch == SCREEN_WIDTH)
   {
   // copy memory from double buffer to primary buffer
   memcpy((void *)primary_buffer, (void *)double_buffer,
          SCREEN_WIDTH*SCREEN_HEIGHT);
   } // end if
else
   { // non-linear

   // make copy of source and destination addresses
   UCHAR *dest_ptr = primary_buffer;
   UCHAR *src_ptr  = double_buffer;

   // memory is non-linear, copy line by line
   for (int y=0; y < SCREEN_HEIGHT; y++)
       {
       // copy line
       memcpy((void *)dest_ptr, (void *)src_ptr, SCREEN_WIDTH);

       // advance pointers to next line
       dest_ptr+=ddsd.lPitch;
       src_ptr +=SCREEN_WIDTH;

       // note: the above code can be replaced with the simpler
       // memcpy(&primary_buffer[y*ddsd.lPitch],
       //        double_buffer[y*SCREEN_WIDTH], SCREEN_WIDTH);
       // but it is much slower due to the recalculation
       // and multiplication each cycle

       } // end for
   } // end else


// now unlock the primary surface
if (FAILED(lpddsprimary->Unlock(NULL)))
   return(0);

// wait a sec
Sleep(500);

// return success or failure or your own return code here
return(1);

} // end Game_Main
      Previous Section Next Section
    



    JavaScript EditorAjax Editor     JavaScript Editor