Basics of DirectDraw Game Programming

Doug Klopfenstein
Microsoft Corporation

October 1997

Introduction

DirectDraw® is part of the recently released Microsoft® DirectX® 5 Software Development Kit (SDK). For those of you who have just returned from a desert island, the DirectX 5 SDK is the latest iteration of the software development kit originally called the Game SDK. The DirectX 5 SDK contains a set of dynamic-link libraries (DLLs) for graphics acceleration, 3D graphics services, audio acceleration, advanced connectivity, joystick manipulation, and CD-ROM automation.

Although there are innumerable topics that could be examined in great detail in the DirectX 5 SDK, in this article I am going to focus on how to get started programming the graphics portion of a game using DirectDraw. This process is relatively simple, although it does require a rudimentary understanding of OLE and Component Object Model (COM) interfaces. However, don't panic. All the information you need to know about OLE and COM will be covered here.

Once you have finished this article, you should be able to program a simple DirectDraw game. However, I am only going to cover how to use the most basic functions of DirectDraw. The examples I am using only allow full-screen, page flipping surfaces. I am not going to cover topics on using DirectDraw in a window, applying textures to a 3D surface, using video clips on a DirectDraw surface, or how to render to a surface using Direct3D. All of these subjects are beyond the scope of this document. If you are interested in these topics, for the time being you will have to read the DirectX 5 documentation for more information. (The DirectX 5 documentation is located on the MSDN™ Library under Platform SDK/Graphics and Multimedia Services.)

Requirements for Using the DirectX 5 SDK

The DirectX 5 SDK can be run under either Windows® 95 or Windows NT® version 5.0. For the purposes of this article, I am going to assume you are using an IBM-PC–class computer running Windows 95. I know that is an arbitrary decision, but Windows NT 5.0 has not yet been officially released, so I'm going to keep this simple. In addition, to actually program, compile, and build an application using DirectDraw, you will need the DirectX 5 SDK installed on your computer. You can download this SDK from the Web at http://www.microsoft.com/directx/resources/devdl.htm. The DirectX 5 SDK has also been distributed to MSDN Library subscribers at the Professional level and above.

Also, I will assume you have a C or C++ compiler capable of compiling 32-bit applications installed on your computer, and that you have a working knowledge of either C or C++ programming. Although it is possible to use other programming languages with the DirectX 5 SDK, that is beyond the scope of what I am going to talk about here. You should also have a working knowledge of Windows programming.

If you are using a C compiler, you will also need to include the Win32® SDK on your computer. The Win32 SDK contains the library files you will need to create executable programs from the examples supplied on the DirectX 5 SDK.

The DirectDraw API

DirectDraw is only one component of the DirectX 5 SDK. DirectDraw was designed for speed; it lets you access the hardware at its most basic level, bypassing the multiple layers normally associated with Windows-based graphics. This makes DirectDraw ideal for game programming, where the emphasis is on creating as smooth and as fast a rendering to the screen as possible.

But, of course, the most important thing about DirectDraw is that it gives you a common interface to different manufacturer's display adapters. You don't have to worry about whether the program you are writing will work. DirectDraw uses the information contained in the hardware abstraction layer (HAL) to determine the capabilities of the hardware on the display adapter. (The HAL is supplied by the display adapter manufacturer.) The HAL provides a common interface between multiple manufacturers of display adapters and applications developers using DirectDraw.

However, DirectDraw is not limited to using the hardware available on the display adapter. If your game indicates that you want to use a specific type of display hardware, such as a hardware blitter, but that hardware does not exist on the end user's system, your application will use the hardware emulation layer (HEL) included with DirectDraw. In this case, DirectDraw uses the built-in hardware emulation to emulate the missing hardware required by your program. There are some caveats to the HEL, however, mainly in regard to how slow you want to allow your game to go. We'll cover this later under the topic, Determining the Capabilities of the Display Hardware.

The following figure shows DirectDraw and its association with other Windows display components:

Figure 1. The relationship between DirectDraw and Windows display components

The DirectDraw API consists of a DirectDraw object that represents an individual display adapter. In addition, the API contains a DirectDrawSurface object that represents a surface, a DirectDrawPalette object that represents the palette of a surface, and a DirectDrawClipper object that represents a clip list. You can use a DirectDraw object to create DirectDrawSurface and DirectDrawPalette objects. (You can also use the DirectDraw object to create a DirectDrawClipper object, but the clipper object can also be created independently. But that's another subject and won't be discussed here.) The only things you need to work with in creating a full-screen game are the DirectDraw, DirectDrawSurface, and DirectDrawPalette objects.

To understand how these objects work, you must have a basic understanding of OLE and its COM interface. The COM interface is the basis of all DirectDraw programming. If you are already familiar with OLE, you can skip the next topic, where I will discuss what you need to know about the COM interface.

DirectDraw, OLE, and the COM Interface

DirectDraw was designed around OLE and the COM interface. If you are not familiar with OLE programming, this may seem an enormous obstacle in getting started with DirectDraw. Nothing could be further from the truth. Although a lot of words have been expended in numerous books explaining how OLE and the COM interface work, there are actually only a few things you need to know about them to successfully use DirectDraw.

To begin, let's start with the definitions of OLE and the COM interface. OLE is Microsoft's object-based technology for sharing information and services across process and machine boundaries. COM stands for Component Object Model, and is the interface model used in OLE programming. Now that you know those definitions, forget them—they aren't important to your programming. Instead, let's get down to the real subject: the absolute minimum you need to understand about the COM interface to be able to use DirectDraw.

Basically, the COM interface is made up of three components: objects, interfaces, and methods. An object is essentially a black box. You don't need to worry about what is in it because the only way to communicate with this black box is through the interface. The means of communicating through the interface is with the use of methods. You can figuratively think of the object as an integrated circuit (for those of you with a hardware background). The way to communicate with the integrated circuit (object) is by sending or receiving signals (methods) through the pins (interface).

All COM interfaces derive from the IUnknown interface in OLE. The IUnknown interface maintains the lifetime of the DirectDraw object by keeping a reference count of the object. In addition, it provides the means of determining the available interfaces for that object. The IUnknown interface is made up of three methods: AddRef, Release, and QueryInterface.

AddRef and Release are responsible for keeping a reference count of an object. When an instance of an object is created, the reference count for that object is set to 1. Whenever any function returns a pointer to an interface associated with that object, the function must also call AddRef through that pointer to increment the reference count by 1. (In addition, when another application binds to the object, the reference count is also incremented by 1. For our purposes, that information is not important, since in this article I won't be binding another application to the game.)

When you are finished using an interface, you should call Release through the pointer to the interface to decrement the reference count by one. To deallocate the object, the reference count must be zero. Once the reference count reaches 0, the object is destroyed and all its interfaces are invalid.

The third IUnknown method, QueryInterface, queries the object about whether it supports a specific interface. If the object does, QueryInterface returns with a pointer to that interface.

How does AddRef, QueryInterface, and Release relate to DirectDraw? First of all, there is no reason to use AddRef or QueryInterface in a simple game using just DirectDraw. The functions that create various types of DirectDraw objects take care of incrementing the reference count and returning a pointer to the object's interface. You do, however, have to perform a Release for every interface pointer that is implicitly created. If you don't keep tabs of the reference count, you can easily end up with memory leaks. When we get to the code examples later on, I'll show you how it's done. (You may have to use AddRef and QueryInterface if you are binding an application to a DirectDraw object being used by another application. Also, if you use Direct3D, you will have to use QueryInterface to return a pointer to the Direct3D interface. Both of these topics are beyond the scope of this document.)

For now, let's look at a sample line of C code that shows how to use an IDirectDraw interface method:

ddrval = lpDD->lpVtbl->SetDisplayMode( lpDD, ScreenX, ScreenY,
                                       ScreenBpp );

In this line of code, you are using the SetDisplayMode method to, well, set the display mode and return a value that denotes success or failure. That's not really important. What is important is the means required to get a proper pointer to the actual method you need to use. You cannot directly access the IDirectDraw interface methods. When an object is instantiated, it creates a virtual function table, called the vtable, that contains pointers to all of the interface methods used by that object. The only way for your application to gain access to these methods is through the vtable. In our example above, the pointer to the DirectDraw object (lpDD) points to the pointer to the vtable (lpVtbl), which in turn contains pointers to all of the methods in the object—specifically, in this case, to the SetDisplayMode method. The connection between your application and the interface methods in the object can be visualized like this:

Figure 2.

The next sample shows how to do the same thing using C++:

ddrval = lpDD->SetDisplayMode( ScreenX, ScreenY, ScreenBpp );

Note that the vtable is no longer explicitly used. Instead, the pointer to the vtable is implicit, and C++ automatically passes the object (lpDD) as a first parameter. The this pointer is no longer needed because C++ implicitly performs the method using the pointer to the current object (in this case, lpDD).

If you want to learn more about OLE and the COM interface, you can take a look at the book Inside OLE by Kraig Brockschmidt (MSDN Library/Books/Inside OLE). This is the best book I have seen for clear explanations of how OLE and COM work. I recommend that you read the first chapter and about half of the second to get a clear understanding of the COM interface. That should be sufficient to explain the relationships between DirectDraw and COM interfaces (if I haven't already).

Getting Started with DirectDraw

I mentioned earlier that you will need to install the DirectX 5 SDK on your computer. You also need to install a C or C++ compiler. For the purposes of this document, I am going to assume that you are using Microsoft Visual C++® version 5.0 as the compiler and that you have installed both the compiler and the SDK in their default directories. If you are using another compiler, or have installed the compiler and the SDK in a directory other than the default directory, you will have to make the appropriate changes to the examples shown here for your programs to work correctly.

Since my purpose is to show you the very basics of DirectDraw programming, I am going to use some of the rudimentary samples supplied as DirectDraw examples on the DirectX 5 SDK. They show how to set up DirectDraw, and use the DirectDraw methods to perform simple functions. Once we have finished with those examples, you should be capable of more easily understanding a more complex game example supplied on the DirectX 5 SDK CD.

But before we begin, you will need to set up the compiler environment to run with the DirectX 5 SDK. How you do this depends on how you are using Visual C++ to compile the examples. I'll show you how to set up the proper environment from scratch using the Microsoft Developer Studio, or, if you are like me, using NMAKE from a command-line prompt.

Setting Up the Microsoft Developer Studio

For those of you who like working with user interfaces, Visual C++ provides the Microsoft Developer Studio. To begin compiling DirectX 5 SDK examples, you will need to open a new project workspace, insert the appropriate files, and set various environment variables to enable the compiler to find the appropriate library and include files. The following procedures demonstrate the complete set-up process for the first example I am going to discuss, DDEX1.

Once you have opened the Microsoft Developer Studio, use the following steps to create the project workspace.

  1. On the File menu, click New.

  2. In the New dialog box, select the Project tab.

  3. From the Projects list, select Win32 Application.

  4. In the Location text box, you can select a different directory for the location of the project.

  5. In the Project name text box, type DDEX1.

  6. Click OK. A new folder called DDEX1 Classes will appear on the left side of the workspace window.

Once you have created the project workspace, use the following steps to insert the appropriate files in the workspace.

  1. On the Project menu, select Add To Project and click Files. The Insert Files into Project dialog box appears.

  2. Browse to the DXSDK\SDK\SAMPLES\DDEX1 directory. Select all of the C++ files there. Click OK.

  3. Select the FileView pane to show the projects and files included in the project workspace.

  4. To see the list of files in the DDEX1 folder, click the + immediately to the left of the DDEX1 files folder.

The next steps select the directories for the include files you will need to compile and link the DirectDraw samples.

  1. On the Tools menu, click Options. The Options dialog box appears.

  2. Select the Directories tab.

  3. In the Show Directories For list box, select the Include files category.

  4. In the Directories box, double-click the blank line at the bottom of the list (indicated by the empty rectangle), and type C:\DXSDK\SDK\LIB.

  5. Press Enter.

  6. In the Directories box, double-click the blank line at the bottom of the list and type C:\DXSDK\SDK\SAMPLES\MISC.

  7. Click the C:\DXSDK\SDK\LIB line. Press the Move Item Up arrow in the Directories box until the C:\DXSDK\SDK\LIB line is at the top of the list.

  8. Click the C:\DXSDK\SDK\SAMPLES\MISC line. Press the Move Item Up arrow in the Directories box until the C:\DXSDK\SDK\SAMPLES\MISC line is directly under the C:\DXSDK\SDK\LIB line.

  9. Click OK.

Next, use the following steps to select the directories for the library files.

  1. In the Show Directories For list box, select the Library files category.

  2. In the Directories box, double-click the blank line at the bottom of the list and type C:\DXSDK\SDK\LIB.

  3. Press Enter.

  4. Click on the C:\DXSDK\SDK\LIB line. Press the Move Item Up arrow in the Directories box until the C:\DXSDK\LIB line is at the top of the list.

  5. Click OK.

Finally, use the following steps to include the library modules for the build:

  1. On the Project menu, click Settings. The Project Settings dialog box appears.

  2. Select the Link tab.

  3. From the Category drop-down box, select General.

  4. In the Object/library modules list box, add Ddraw.lib and Winmm.lib.

  5. Click OK.

You are probably wondering why C:\DXSDK\SDK\INC, C:\DXSDK\SDK\SAMPLES\MISC, and C:\DXSDK\SDK\LIB had to be moved to the top of the Directories box. That's because Visual C++ version 5.0 contains all of the DirectX 3 include files and libraries. To ensure that the new DirectX 5 include files and libraries are used instead, you have to put them at the top of the list. That way, Visual C++ 5.0 finds them first and uses them instead of the older DirectX 3 files.

Most of the steps that were taken here will be passed to any new project. The directory paths and link modules are permanently added to the environment variables.

Setting Up Paths for NMAKE

If you are like me and prefer using NMAKE, you essentially have to set up the paths to the proper library modules and include files for DirectX 5. With the new, longer path names used in Visual C++ 5.0, you have to do some extra work to get NMAKE to function properly. I'll give you some general hints here about how to get started

  1. The first thing to do is edit the Vcvars32.bat file included in the C:\Program Files\DevStudio\VC\bin directory. You have to set up the environment variables to reflect the way you are using C++. (For example, if you loaded all of the C++ directories onto your hard drive, you have to comment out the environment paths that point to your CD-ROM drive.)

  2. Next, you can use the following text to create a .BAT file that includes all the paths you need for DirectX 5:
    @echo off
    set INCLUDE=%INCLUDE%;C:\DXSDK\SDK\INC
    set LIB=%LIB%;C:\DXSDK\SDK\LIB
    
  3. Next, you have to increase the initial environment size of your MS-DOS® window (thanks to the new extra-long paths included with Visual C++ 5.0). To do this, enter the following command from your MS-DOS prompt:
    command /E:1000
    

    This adds a thousand bytes to your initial environment size. Note, however, that you have to enter this first in your MS-DOS window because any other environment variables you had set before entering this command line will be lost.

  4. Next, run the Vcvars32.bat file to set the Visual C++ 5.0 environment variables.

  5. Lastly, run the .BAT file you created with the DirectX 5 environment variables.

To actually compile the samples, go to the sample directory (C:\DXDSK\SDK\SAMPLES\DDEX4, for example) and type:

NMAKE

This will create a DEBUG directory in the directory you are in and place the executable file in that directory.

You can also permanently set up the initial environment size of the MS-DOS window in Windows 95 by using the following steps:

  1. Using Windows Explorer, find the location of the MS-DOS Prompt you use to open an MS-DOS window.

  2. Click the MS-DOS Prompt icon once and select Properties from the File menu of Windows Explorer.

  3. Select the Memory tab and change the Initial environment drop-down box to 1024.

The Basics of DirectDraw (DDEX1)

To use DirectDraw, you first create an instance of the DirectDraw object, which represents the display adapter on the computer, then use the interface methods to manipulate the object to do your bidding. You will also need to create one or more instances of a DirectDrawSurface object to be able to display your game on a graphics surface.

To demonstrate this, I am going to show you how DDEX1 on the DirectX 5 SDK creates the DirectDraw object, then creates a primary surface with a back buffer, and then flips between the surfaces.

Note   The DDEXx example files are written in C++. If you are using a C compiler, you will have to make some changes to the files for them to successfully compile. (At the very least, you will need to add the vtable and this pointers to the interface methods.)

DirectDraw Initialization

The DDEX1 program contains the DirectDraw initialization code in the doInit function.

/*
 * Create the main DirectDraw object.
 */
ddrval = DirectDrawCreate( NULL, &lpDD, NULL );
if( ddrval == DD_OK )
{
    // Get exclusive mode.
    ddrval = lpDD->SetCooperativeLevel( hwnd,
                            DDSCL_EXCLUSIVE | DDSCL_FULLSCREEN );
    if(ddrval == DD_OK )
    {
        ddrval = lpDD->SetDisplayMode( 640, 480, 8 );
        if( ddrval == DD_OK )
        {
            // Create the primary surface with 1 back buffer.
            ddsd.dwSize = sizeof( ddsd );
            ddsd.dwFlags = DDSD_CAPS | DDSD_BACKBUFFERCOUNT;
            ddsd.ddsCaps.dwCaps = DDSCAPS_PRIMARYSURFACE |
                                  DDSCAPS_FLIP | 
                                  DDSCAPS_COMPLEX;
            ddsd.dwBackBufferCount = 1;
            ddrval = lpDD->CreateSurface( &ddsd, &lpDDSPrimary, NULL );
            if( ddrval == DD_OK )
            {
                // Get a pointer to the back buffer.
                ddscaps.dwCaps = DDSCAPS_BACKBUFFER;
                ddrval = lpDDSPrimary->GetAttachedSurface(&ddscaps, 
                                                      &lpDDSBack);
                if( ddrval == DD_OK )
                {
                    // Draw some text.
                    if (lpDDSPrimary->GetDC(&hdc) == DD_OK)
                    {
                        SetBkColor( hdc, RGB( 0, 0, 255 ) );
                        SetTextColor( hdc, RGB( 255, 255, 0 ) );
                        TextOut( hdc, 0, 0, szFrontMsg, 
                                               lstrlen(szFrontMsg) );
                        lpDDSPrimary->ReleaseDC(hdc);
                    }

                    if (lpDDSBack->GetDC(&hdc) == DD_OK)
                    {
                        SetBkColor( hdc, RGB( 0, 0, 255 ) );
                        SetTextColor( hdc, RGB( 255, 255, 0 ) );
                        TextOut( hdc, 0, 0, szBackMsg, 
                                               lstrlen(szBackMsg) );
                        lpDDSBack->ReleaseDC(hdc);
                    }

                    // Create a timer to flip the pages.
                    if( SetTimer( hwnd, TIMER_ID, TIMER_RATE, NULL ) )
                    {
                         return TRUE;
                    }
                }
            }
        }
    }
}

wsprintf(buf, "Direct Draw Init Failed (%08lx)\n", ddrval );
  .
  .
  .

Each of the steps used in initializing the DirectDraw object and creating and preparing the surfaces are covered in the following sections.

Creating a DirectDraw object

To create an instance of a DirectDraw object, your application should use the DirectDrawCreate API function. (Notice that I said "should" in the last sentence. There is at least one other way to create a DirectDraw object—using the CoCreateInstance function in OLE—but that won't be discussed here.) DirectDrawCreate takes a globally unique identifier (GUID) that represents the display device and, in most cases, is set to NULL (which uses the default display driver for the system), the address of a pointer that identifies the location of the DirectDraw object if it is created, and a third parameter that is always set to NULL (it is included for future expansion).

The following example shows how to create the DirectDraw object and determine whether the creation was successful or not:

ddrval = DirectDrawCreate( NULL, &lpDD, NULL );
if( ddrval == DD_OK )
{
   // lpDD is a valid DirectDraw object.
}
else
{
   // DirectDraw object could not be created.
}

Using the IDirectDraw2 and IDirectDrawSurface3 interfaces

As you read through the rest of this article, you will note that all of the DDEXx examples use the older versions of the IDirectDraw and IDirectDrawSurface interfaces. That's because the samples in the DirectX 5 SDK were not updated to use the new IDirectDraw2 and IDirectDrawSurface3 interfaces. However, you should always use the most up-to-date version of the interfaces. Both the IDirectDraw2 and IDirectDrawSurface3 interfaces can be obtained using the QueryInterface method on the original interfaces.

The following code sample shows how to obtain an IDirectDraw2 interface:

//Create an IDirectDraw2 interface.
LPDIRECTDRAW  lpDD;
LPDIRECTDRAW2 lpDD2;

ddrval = DirectDrawCreate( NULL, &lpDD, NULL );
if( ddrval != DD_OK )
    return;

ddrval = lpDD->SetCooperativeLevel( hwnd, DDSCL_NORMAL );
if( ddrval != DD_OK )
    return;

ddrval = lpDD->QueryInterface( IID_IDirectDraw2, (LPVOID *)&lpDD2);
if( ddrval != DD_OK )
    return;

The following code sample shows how to obtain an IDirectDrawSurface3 interface:

LPDIRECTDRAWSURFACE  lpSurf;
LPDIRECTDRAWSURFACE3 lpSurf3;

//Create surfaces.
memset( &ddsd, 0, sizeof(ddsd) );
ddsd.dwSize = sizeof( ddsd );
ddsd.dwFlags = DDSD_CAPS | DDSD_WIDTH | DDSD_HEIGHT;
ddsd.ddsCaps.dwCaps = DDSCAPS_OFFSCREENPLAIN |
        DDSCAPS_SYSTEMMEMORY;
ddsd.dwWidth = 10;
ddsd.dwHeight = 10;

ddrval = lpDD2->CreateSurface( &ddsd, &lpSurf, NULL );
if( ddrval != DD_OK )
    return;

ddrval = lpSurf->QueryInterface( IID_IDirectDrawSurface3,
        (LPVOID *)&lpSurf3);
if( ddrval != DD_OK )
    return;

Because it is possible that there have been significant changes made between one version of an interface and another, you should never mix methods between the original interface and its replacement (for example, between IDirectDrawSurface and IDirectDrawSurface3). Unpredictable and possibly unpleasant behavior may result from mixing versions of interfaces.

Setting the display mode

The next step in setting up DirectDraw is to set the display mode. Setting the display mode of your application using DirectDraw essentially requires two steps. The first step is to call the IDirectDraw::SetCooperativeLevel method to set the low-level requirements for the mode. Once these requirements are established, use the IDirectDraw::SetDisplayMode method to select the resolution of the display.

Determining the application's behavior

Before you can change the resolution of your display, at a minimum you must specify the DDSCL_EXCLUSIVE and DDSCL_FULLSCREEN flags under the dwFlags parameter of the IDirectDraw::SetCooperativeLevel method. This gives your application complete control of the display device; no other application will be able to share the display device at that point. In addition, the DDSCL_FULLSCREEN flag sets the application into full-screen mode. Although the desktop is still available, as can be seen by running DDEX1 and pressing the Alt and Tab keys simultaneously, your application covers the entire desktop, and only your application will be able to write to the screen.

The following example demonstrates the use of IDirectDraw::SetCooperativeLevel:

HRESULT      ddrval;
LPDIRECTDRAW   lpDD;      // Already created by DirectDrawCreate

ddrval = lpDD->SetCooperativeLevel( hwnd, DDSCL_EXCLUSIVE |
          DDSCL_FULLSCREEN );
if( ddrval == DD_OK )
{
   // Exclusive mode was successful.
}
else
{
   // Not successful.
   // However, the application can still run.
}

If IDirectDraw::SetCooperativeLevel does not return DD_OK, you can still run your application, although I wouldn't recommend it. Your application won't be in full-screen mode, and may not be capable of the performance your application requires. If you do want to continue running your application, you should still display an error that lets end users know what is happening and perhaps let them choose whether they want to continue or not.

One requirement for using IDirectDraw::SetCooperativeLevel is that you must pass a handle to a window (HWND) to allow Windows to determine when your application terminates abnormally. For example, if a GP fault occurs and GDI is flipped to the back buffer, the end user will never get the Windows screen back. To prevent this from occurring, DirectDraw has a process waiting in the background that essentially traps messages that are being sent to that window, and uses these messages to determine when the application terminates. This imposes some restrictions. First of all, you have to specify the window handle that is actually getting messages for your application. That is, if you create another window you have to make sure that you specify the one that is actually active. Otherwise there are a number of things that aren't going to work, including the possibility of GDI getting messed up when your application terminates, or the possibility that pressing Alt+Tab won't work.

Changing modes

Once you have selected the application's behavior, you can use the IDirectDraw::SetDisplayMode method to change the resolution of the display. The following example shows how to set the display mode to 640×480×8 bpp:

HRESULT      ddrval;
LPDIRECTDRAW   lpDD;      // Already created

ddrval = lpDD->SetDisplayMode( 640, 480, 8 );
if( ddrval == DD_OK )
{
   // Mode changed
}
else
{
   // Mode cannot be changed.
   // Mode is either not supported
   // or someone else has exclusive mode.
}

When you set the display mode, you should ensure that if the end user's hardware is not capable of supporting higher resolutions, your application falls back to a standard mode that is supported by a majority of display adapters. For example, your application could be designed to run on all systems that support 640×480×8 as a standard backup resolution. (IDirectDraw::SetDisplayMode returns a DDERR_INVALIDMODE error value if the display adapter was incapable of being set to the desired resolution. Therefore, you should use the IDirectDraw::EnumDisplayMode method to determine the capabilities of the end user's display adapter before trying to set the display mode.)

Creating flippable surfaces

Once you have set the display mode, you must create the surfaces on which you will be placing your application. Since, in the DDEX1 example, we are using the IDirectDraw::SetCooperativeLevel method to set the mode to full-screen exclusive mode, you can create surfaces that page flip between the surfaces. If we were using IDirectDraw::SetCooperativeLevel to set the mode to DDSCL_NORMAL, you could only create surfaces that blit between the surfaces.

Defining the surface requirements

The first step in creating flippable surfaces is to define the surface requirements in a DDSURFACEDESC structure. The following example shows the structure definitions and flags needed to create a flippable surface:

// Create the primary surface with 1 back buffer.
ddsd.dwSize = sizeof( ddsd );
ddsd.dwFlags = DDSD_CAPS | DDSD_BACKBUFFERCOUNT;
ddsd.ddsCaps.dwCaps = DDSCAPS_PRIMARYSURFACE |
      DDSCAPS_FLIP | DDSCAPS_COMPLEX;

ddsd.dwBackBufferCount = 1;

In the example, the dwSize member is set to the size of the DDSURFACEDESC structure. This is to prevent any DirectDraw method call you use from returning with an invalid member error. (In essence, the dwSize member allows for future expansion of the DDSURFACEDESC structure.)

The dwFlags member determines which fields in the DDSURFACEDESC structure will be filled with valid information. For the DDEX1 example, we are going to set dwFlags to specify that you want to use the DDSCAPS structure (DDSD_CAPS) and that you want to create a back buffer (DDSD_BACKBUFFERCOUNT).

The dwCaps member in the example indicates the flags that will be used in the DDSCAPS structure. In this case, it specifies a primary surface (DDSCAPS_PRIMARYSURFACE), a flipping surface (DDSCAPS_FLIP), and a complex surface (DDSCAPS_COMPLEX). A complex surface is a surface that has more than one actual surface.

Finally, the example specifies one back buffer. The back buffer is where the backgrounds and sprites will actually be written. The back buffer will then be flipped to the primary surface. In the DDEX1 example, the number of back buffers is set to 1. You can, however, create as many back buffers as the amount of display memory will allow. For more information on creating more than one back buffer, see Triple Buffering.

Surface memory can be either display memory or system memory. DirectDraw uses system memory if the application runs out of display memory (for example, if you specify more than one back buffer on a display adapter with only 1 megabyte of RAM). You can also specify whether to use only system memory or only display memory by setting the dwCaps member in the DDSCAPS structure to DDSCAPS_SYSTEMMEMORY or DDSCAPS_VIDEOMEMORY. (If you specify DDSCAPS_VIDEOMEMORY, but not enough memory is available to create the surface, IDirectDraw::CreateSurface will return with a DDERR_OUTOFVIDEOMEMORY error

Creating the surfaces

Once the DDSURFACEDESC structure is filled out, you can use it and lpDD, the pointer to the DirectDraw object that was created by the DirectDrawCreate function, to call the IDirectDraw::CreateSurface method, as shown in the following example:

ddrval = lpDD->CreateSurface( &ddsd, &lpDDSPrimary, NULL );
if( ddrval == DD_OK )
{
   // lpDDSPrimary points to a new surface.
}
else
{
   // Surface was not created
   return FALSE;
}

The lpDDSPrimary parameter will point to the primary surface returned by IDirectDraw::CreateSurface if the call succeeds.

Once the pointer to the primary surface is available, you can then use the IDirectDrawSurface::GetAttachedSurface method to get a pointer to the back buffer, as shown in the following example:

ddscaps.dwCaps = DDSCAPS_BACKBUFFER;
ddrval = lpDDSPrimary->GetAttachedSurface( &ddcaps, &lpDDSBack );
if( ddrval == DD_OK )
{
   // lpDDSBack points to the back buffer.
}
else
{
   return FALSE;
}

By supplying the address of the surface's primary surface and by setting the capabilities value with the DDSCAPS_BACKBUFFER flag, the lpDDSBack parameter will point to the back buffer if the IDirectDrawSurface::GetAttachedSurface call succeeds.

Rendering to the Surfaces

Once the primary surface and a back buffer have been created, the DDEX1 example renders some text on the primary surface and back buffer surface using standard Windows functions, as shown in the following example:

if (lpDDSPrimary->GetDC(&hdc) == DD_OK)
{
    SetBkColor( hdc, RGB( 0, 0, 255 ) );
    SetTextColor( hdc, RGB( 255, 255, 0 ) );
    TextOut( hdc, 0, 0, szFrontMsg, lstrlen(szFrontMsg) );
    lpDDSPrimary->ReleaseDC(hdc);
}

if (lpDDSBack->GetDC(&hdc) == DD_OK)
{
    SetBkColor( hdc, RGB( 0, 0, 255 ) );
    SetTextColor( hdc, RGB( 255, 255, 0 ) );
    TextOut( hdc, 0, 0, szBackMsg, lstrlen(szBackMsg) );
    lpDDSBack->ReleaseDC(hdc);
}

The example uses the IDirectDrawSurface::GetDC method to get the handle to the device context, and internally locks the surface. If you are not going to use Windows functions that require a handle to a device context, you could use the IDirectDrawSurface::Lock and IDirectDrawSurface::Unlock methods to lock and unlock the back buffer.

Locking the surface memory (whether the whole surface or part of a surface) ensures that your application and the system blitter cannot obtain access to the surface memory at the same time. This prevents errors from occurring while your application is writing to surface memory. In addition, your application cannot page flip until the surface memory is unlocked.

Once the surface is locked, the example uses the standard Windows GDI functions SetBkColor to set the background color, SetTextColor to select the color of the text to be placed on the background, then TextOut to print the text and background color onto the surfaces.

Once the text has been written to the buffer, the example uses the IDirectDrawSurface::ReleaseDC method to unlock the surface and release the handle. Whenever your application finishes writing to the back buffer, you must call either IDirectDrawSurface::ReleaseDC or IDirectDrawSurface::Unlock, depending on your application. Your application cannot flip the surface until the surface is unlocked.

You might be wondering why this initialization routine writes to the primary surface. Generally, when you write to a surface, it is the back buffer, which you then flip to the primary surface to be displayed. In the case of DDEX1, there is a significant delay before the first flip, so DDEX1 writes to the primary buffer in the initialization routine to prevent a long gap in the beginning of the display. As you will see later on, the DDEX1 example only writes to the back buffer during WM_TIMER. An initialization routine or title page may be the only place where you might want to write to the primary surface.

Note   Once the surface is unlocked using IDirectDrawSurface::Unlock, the pointer to the surface memory is invalid. You must run IDirectDrawSurface::Lock again to obtain a valid pointer to the surface memory.

Writing and Flipping the Surfaces

Once the initialization is finished, the DDEX1 application proceeds to the message loop. It is during this loop that the back buffer is locked, new text is written, the back buffer is unlocked, and the surfaces are flipped. WM_TIMER contains most of the code for writing to and flipping the surfaces.

Writing to the Surface

The first half of the WM_TIMER message is devoted to writing to the back buffer. Most of the techniques used here have already been discussed in the Rendering to the Surfaces section, but I'll go over it briefly again. Here is what it looks like in DDEX1:

case WM_TIMER:
    // Flip surfaces
    if( bActive )
    {
        if (lpDDSBack->GetDC(&hdc) == DD_OK)
        {
            SetBkColor( hdc, RGB( 0, 0, 255 ) );
            SetTextColor( hdc, RGB( 255, 255, 0 ) );
            if( phase )
            {
                TextOut( hdc, 0, 0, szFrontMsg, lstrlen(szFrontMsg) );
                phase = 0;
            }
            else
            {
                TextOut( hdc, 0, 0, szBackMsg, lstrlen(szBackMsg) );
                phase = 1;
            }
            lpDDSBack->ReleaseDC(hdc);
        }

The GetDC line locks the back buffer in preparation for writing. The SetBkColor and SetTextColor functions set the colors of the background and text.

Next, the variable "phase" determines whether the primary buffer message or the back buffer message should be written. If "phase" equals 1, the primary surface message is written, and "phase" is set to 0. If "phase" equals 0, the back buffer message is written, and "phase" is set to 1. You should note, however, that in both cases the messages are written to the back buffer.

Once the message is written to the back buffer, the back buffer is unlocked using the IDirectDrawSurface::ReleaseDC method.

Flipping surfaces

Once the surface memory is unlocked, you can then use the IDirectDrawSurface::Flip method to flip the back buffer to the primary surface. The following example shows how this is done in DDEX1:

        while( 1 )
        {
            HRESULT ddrval;
            ddrval = lpDDSPrimary->Flip( NULL, 0 );
            if( ddrval == DD_OK )
            {
                break;
            }
            if( ddrval == DDERR_SURFACELOST )
            {
                ddrval = lpDDSPrimary->Restore();
                if( ddrval != DD_OK )
                {
                    break;
                }
            }
            if( ddrval != DDERR_WASSTILLDRAWING )
            {
                break;
            }
        }

In the example, lpDDSPrimary designates the primary surface and its associated back buffer. When IDirectDrawSurface::Flip is called, the front and back surfaces are exchanged (only the pointers to the surfaces are changed; no data is actually moved). If the flip is successful and returns DD_OK, the application breaks from the while loop.

If the flip returns with a DDERR_SURFACELOST value, an attempt is made to restore the surface using the IDirectDrawSurface::Restore method. If successful, the application loops back to the IDirectDrawSurface::Flip call and tries again. If the restore is unsuccessful, the application breaks from the while loop and returns with an error.

One important thing to note is that even after you have called IDirectDrawSurface::Flip, the exchange does not complete immediately. It schedules a flip for the next time a vertical blank occurs on the system. If, for example , the previous flip has not occurred, IDirectDrawSurface::Flip will return DDERR_WASSTILLDRAWING. In the example, the IDirectDrawSurface::Flip call will continue to loop until it returns DD_OK.

Deallocating the DirectDraw Objects

When you press the F12 key, the DDEX1 application processes the WM_DESTROY message before exiting the application. This message calls the finiObjects function, which contains all of the IUnknown Release calls, as shown below:

static void finiObjects( void )
{
    if( lpDD != NULL )
    {
        if( lpDDSPrimary != NULL )
        {
            lpDDSPrimary->Release();
            lpDDSPrimary = NULL;
        }
        lpDD->Release();
        lpDD = NULL;
    }
} /* finiObjects */

This is pretty straightforward. The application checks to see if the pointers to the DirectDraw object (lpDD) and the DirectDrawSurface object (lpDDSPrimary) are not equal to NULL, which, of course, they aren't. Then DDEX1 calls the IDirectDrawSurface::Release method to decrease the reference count of the DirectDrawSurface object by 1. Since this brings the reference count to 0, the DirectDrawSurface object is deallocated. The DirectDraw Surface pointer is then destroyed by setting its value to NULL. The application then calls IDirectDraw::Release to decrease the reference count of the DirectDraw object to 0, deallocating the DirectDraw object. This pointer is then also destroyed by setting its value to NULL.

Expanding on the Basics (DDEX2 and DDEX3)

DDEX1 contains one of the most basic possible implementations of DirectDraw. It creates the DirectDraw and DirectDrawSurface objects, creates a primary surface and its associated back buffer, prints some text on the back buffer, then flips between surfaces.

The second DirectDraw sample in the DirectX 5 SDK (DDEX2) expands on the DDEX1 application. DDEX2 includes functionality to load a bitmap file onto the back buffer.

The third DirectDraw sample expands this functionality even further. DDEX3 creates two off-screen surfaces in addition to the primary surface and back buffer, and loads a bitmap file into each off-screen surface. It then uses the IDirectDrawSurface::BltFast method to copy the contents of an off-screen surface to the back buffer, then flips the buffers and copies the next off-screen surface to the back buffer.

The following sections will examine this new functionality in more detail.

Loading a Bitmap on a Surface

Once again, as in DDEX1, doInit is the initialization function for the DDEX2 application. Although the DirectDraw initialization doesn't look quite the same in DDEX2 as it did in DDEX1, it is essentially the same, until it reaches the following code:

lpDDPal = DDLoadPalette(lpDD, szBackground);

    if (lpDDPal == NULL)
        goto error;

    ddrval = lpDDSPrimary->SetPalette(lpDDPal);

    if( ddrval != DD_OK )
        goto error;

    // Load a bitmap into the back buffer.
    ddrval = DDReLoadBitmap(lpDDSBack, szBackground);

    if( ddrval != DD_OK )
        goto error;

Creating the palette

The first line of the code is a return value from the function DDLoadPalette. If you are curious about where to find DDLoadPalette, it can be found in the Ddutil.cpp file located in the \DXSDK\SDK\SAMPLES\MISC directory. You will find that the Ddutil.cpp file is used by most of the DirectDraw example files on the DirectX 5 SDK. Essentially, it contains the functions for loading bitmaps and palettes from either files or resources. Rather than repeat the same code over and over again in the example files, these functions were placed in a file that could be reused.

Note   If you are using the Microsoft Developer Studio to compile DDEX2 and the rest of the DirectDraw examples supplied with the DirectX 5 SDK, you will need to include the file Ddutil.cpp in the list of files in the DDEXx files workspace.

To include Ddutil.cpp in the workspace:

  1. On the Project menu, click Files under Add To Project.

  2. Click Browse.

  3. Click the DXSDK\SDK\SAMPLES\MISC directory.

  4. Click Ddutil.cpp.

  5. Click OK.

For DDEX2, the DDLoadPalette function creates a DirectDrawPalette object from the Back.bmp file. The DDLoadPalette function actually checks to see if a file or resource for creating a palette exists. If it doesn't, it creates a default palette. For DDEX2, it extracts the palette information from the bitmap file and stores it in a structure pointed to by ape. It then creates the DirectDrawPalette object, as shown in the following code:

pdd->CreatePalette(DDPCAPS_8BIT, ape, &ddpal, NULL);
return ddpal;

When the IDirectDraw::CreatePalette method returns, the ddpal parameter will point to the DirectDrawPalette object, which is then returned from the DDLoadPalette call.

The ape parameter is a pointer to a structure that can contain either 2, 4, 16, or 256 entries, organized linearly. The number of entries depends on the dwFlags parameter in the IDirectDraw::CreatePalette call. In this case, the dwFlags parameter is set to DDPCAPS_8BIT, which indicates that there are 256 entries in this structure. Each entry contains four bytes (a red channel, a green channel, a blue channel, and a flags byte).

Setting the palette

After creating the palette, you then pass the pointer to the DirectDrawPalette object (ddpal) to the primary surface by calling the IDirectDrawSurface::SetPalette method, as shown in the following example:

ddrval = lpDDSPrimary->SetPalette(lpDDPal);

if( ddrval != DD_OK )
    // SetPalette failed.

Once you have called IDirectDrawSurface::SetPalette, the DirectDrawPalette object is then hooked to the DirectDrawSurface object. Any time you need to change the palette, all you do is create a new palette and set the palette again. (That's how it's done in this example. There are other ways of changing the palette, however, as we shall see in the other examples.)

Loading a bitmap on the back buffer

Once the DirectDrawPalette object is hooked to the DirectDrawSurface object, DDEX2 then loads the Back.bmp bitmap on the back buffer, using the following code:

// Load a bitmap into the back buffer.
ddrval = DDReLoadBitmap(lpDDSBack, szBackground);

if( ddrval != DD_OK )
    // Load failed.

DDReLoadBitmap is another function found in Ddutil.cpp. It loads a bitmap from a file or resource into an already existing DirectDraw surface. (You could use DDLoadBitmap to actually create a surface and load the bitmap into that surface, such as is done in DDEX5.) For DDEX2, it loads the Back.bmp file pointed to by szBackground onto the back buffer pointed to by lpDDSBack. DDReLoadBitmap calls the DDCopyBitmap function to actually copy the file onto the back buffer and stretch it to the proper size.

The DDCopyBitmap function copies the bitmap into memory, then uses the GetObject function to get the size of the bitmap. It then uses the following code to get the size of the back buffer on which it will place the bitmap:

//
// Get size of surface.
//
ddsd.dwSize = sizeof(ddsd);
ddsd.dwFlags = DDSD_HEIGHT | DDSD_WIDTH;
pdds->GetSurfaceDesc(&ddsd);

ddsd is a pointer to the DDSURFACEDESC structure. This structure stores the current description of the DirectDraw surface. In this case, the DDSURFACEDESC members that we need to look at describe the height and width of the surface, which are indicated by DDSD_HEIGHT and DDSD_WIDTH. The call to the IDirectDrawSurface::GetSurfaceDesc method will then load the structure with the proper values. For DDEX2, the values will be 480 for the height and 640 for the width.

The DDCopyBitmap function locks the surface and copies the bitmap to the back buffer, stretching or compressing it as applicable using the StretchBlt routine, as shown below:

if ((hr = pdds->GetDC(&hdc)) == DD_OK)
{
    StretchBlt(hdc, 0, 0, ddsd.dwWidth, ddsd.dwHeight, hdcImage, x, y,
                   dx, dy, SRCCOPY);
    pdds->ReleaseDC(hdc);
}

Flipping the surfaces

Flipping surfaces in the DDEX2 example is essentially the same process as that in the DDEX1 example, except that if the surface is lost (DDERR_SURFACELOST), the bitmap must be reloaded on the back buffer using the DDReLoadBitmap function after the surface is restored.

Blitting from an Off-screen Surface

DDEX2 takes a bitmap and puts it in the back buffer, then flips between the back buffer and the primary buffer. This is not a very realistic approach to displaying bitmaps. DDEX3 expands on the capabilities of DDEX2 by including two off-screen buffers in which the two bitmaps, one for the even screen and one for the odd screen, are stored. It then blits one of the screens onto the back buffer, flips the surfaces, blits the other screen to the back buffer, and flips the surface.

Creating the off-screen surfaces

The following code is added to the doInit function in DDEX3 to create the two off-screen buffers:

// Create an offscreen bitmap.
ddsd.dwFlags = DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH;
ddsd.ddsCaps.dwCaps = DDSCAPS_OFFSCREENPLAIN;
ddsd.dwHeight = 480;
ddsd.dwWidth = 640;
ddrval = lpDD->CreateSurface( &ddsd, &lpDDSOne, NULL );
if( ddrval != DD_OK )
{
    return initFail(hwnd);
}

// Create another offscreen bitmap.
ddrval = lpDD->CreateSurface( &ddsd, &lpDDSTwo, NULL );
if( ddrval != DD_OK )
{
    return initFail(hwnd);
}

As you can see from this code, the dwFlags member specifies that the application will use the DDSCAPS structure and will set the height and width of the buffer. The surface will be an off-screen plain buffer, as indicated by the DDSCAPS_OFFSCREEN flag set in the DDSCAPS structure. The height and the width are then set as 480 and 640, respectively, in the DDSURFACEDESC structure. The surface is then created using the IDirectDraw::CreateSurface method.

Because both of the off-screen plain buffers are the same size, the only requirement for creating the second buffer is to run IDirectDraw::CreateSurface again (with a different pointer name, of course).

You can also specifically ask that the off-screen buffer be placed in system memory or display memory by setting either the DDSCAPS_SYSTEMMEMORY or the DDSCAPS_VIDEOMEMORY capability in the DDSCAPS structure. By saving the bitmaps in display memory, you can increase the speed of the transfers between the off-screen surfaces and the back buffer. This will become more important when we begin discussing bitmap animation later on. However, for now you should note that if you specify DDSCAPS_VIDEOMEMORY for the off-screen buffer, but not enough display memory is available to hold the entire bitmap, a DDERR_OUTOFVIDEOMEMORY error value will be returned when you attempt to create the surface.

Loading the bitmaps to the off-screen surfaces

After the two off-screen surfaces are created, DDEX3 uses the InitSurfaces routine to load the bitmaps from the Frntback.bmp file onto the surfaces. The InitSurfaces routine uses the DDCopyBitmap function located in Ddutil.cpp to load both of the bitmaps, as shown in the following code:

// Load our bitmap resource.
hbm = (HBITMAP)LoadImage(GetModuleHandle(NULL), szBitmap,
    IMAGE_BITMAP, 0, 0, LR_CREATEDIBSECTION);

if (hbm == NULL)
    return FALSE;

DDCopyBitmap(lpDDSOne, hbm, 0, 0,   640, 480);
DDCopyBitmap(lpDDSTwo, hbm, 0, 480, 640, 480);
DeleteObject(hbm);

return TRUE;

If you look at the Frntback.bmp file in Microsoft Paint or another drawing application, you can see that the bitmap is made up of two screens, one on top of the other. The DDCopyBitmap function breaks the bitmap in two at the point where the screens meet, and loads the first bitmap into the first off-screen surface (lpDDSOne) and the second into the second off-screen surface (lpDDSTwo).

Blitting the off-screen surfaces to the back buffer

Once again, WM_TIMER contains the code for writing to and flipping surfaces. In the case of DDEX3, it contains the following code to select the proper off-screen surface and blit it to the back buffer:

    rcRect.left = 0;
    rcRect.top = 0;
    rcRect.right = 640;
    rcRect.bottom = 480;
    if(phase)
    {
        pdds = lpDDSTwo;
        phase = 0;
    }
    else
    {
        pdds = lpDDSOne;
        phase = 1;
    }
    while( 1 )
    {
        ddrval = lpDDSBack->BltFast( 0, 0, pdds, &rcRect, FALSE );
        if( ddrval == DD_OK )
        {
            break;
        }

The "phase" determines which off-screen surface is going to be blitted to the back buffer. The IDirectDrawSurface::BltFast method is then called to blit the selected off-screen surface onto the back buffer, starting at position 0, 0, the upper left corner. The parameter rcRect points to the RECT structure that defines the upper left and lower right corners of the off-screen surface that will be blitted from. The last parameter is set to FALSE (or 0), indicating that no specific transfer flags are used.

I would like to add just a few words here about choosing whether to use the IDirectDrawSurface::Blt method or IDirectDrawSurface::BltFast. If you are performing a blit from an off-screen plain buffer that is in display memory, you should use IDirectDrawSurface::BltFast. Although you won't really gain any speed on systems that use hardware blitters on their display adapters, the blit will take about 10 percent less time on systems that use hardware emulation to perform the blit. Because of this, I recommend using IDirectDrawSurface::BltFast for all display operations that blit from display memory to display memory. If you are blitting from system memory or require special hardware flags, however, you'll have to use IDirectDrawSurface::Blt.

Once the off-screen surface is loaded in the back buffer, the back buffer and the primary surface are flipped in much the same way as shown in previous examples.

Creating Simple Animations (DDEX4 and DDEX5)

All of the examples we have discussed so far contain simple implementations of drawing to a back buffer, then flipping the back buffer to the primary surface. These examples, however, are running at an excruciatingly slow speed. The next examples, DDEX4 and DDEX5, perform their functions in real time, more like a real game.

DDEX4 shows how to set a color key for the surface, and how to use the IDirectDrawSurface::BltFast method to copy portions of the off-screen surface to the back buffer to generate animation. It demonstrates how to watch the return value from IDirectDrawSurface::BltFast to determine whether the blit succeeded or not.

DDEX5 adds to the functionality of DDEX4 by demonstrating how to read the palette, then modify it while the animation proceeds.

Color Keys and Bitmap Animation

The DDEX3 example shows a primitive form of placing bitmaps into an off-screen buffer before they are blitted to the back buffer. The next example, DDEX4, uses the techniques I have already described in previous sections to load a background and a series of sprites into an off-screen surface, then uses the IDirectDrawSurface::BltFast method to copy portions of the off-screen surface to the back buffer, thereby generating a simple bitmap animation.

The bitmap file used by DDEX4, All.bmp, contains the background and sixty iterations of a rotating red donut with a black background. The DDEX4 example contains new routines that set the color key for the rotating donut sprites, then copies the appropriate sprite to the back buffer from the off-screen surface.

I would like to point out that in DDEX4, the "donut" is referred to as a "torus." I have always had trouble trying to keep from envisioning young Hollywood actresses every time I hear the plural of torus (tori), so I am going to call them donuts here. I hope you don't mind.

Setting the color key

In addition to the other functions found in the doInit function of previous examples, the DDEX4 example contains the code to set the color key for the sprites. Color keys are used for setting a color value that will be used for transparency. When using a hardware blitter, all the pixels of a rectangle are blitted except the value that was set as the color key, thereby creating non-rectangular sprites on a surface. The code for setting the color key in DDEX4 is shown below:

    // Set the color key for this bitmap (black).
    // NOTE this bitmap has black as entry 255 in the color table.
//    ddck.dwColorSpaceLowValue = 0xff;
//    ddck.dwColorSpaceHighValue = 0xff;
//    lpDDSOne->SetColorKey( DDCKEY_SRCBLT, &ddck );

    // If we did not want to hard code the palette index (0xff)
    // we can also set the color key like so...
    DDSetColorKey(lpDDSOne, RGB(0,0,0));

    return TRUE;

The example shows two different ways of setting the color key. The first method, the three lines commented out of the code, sets the high and low range of the color key to the hexadecimal value FF, or a decimal value of 255, in the DDCOLORKEY structure, then calls the IDirectDrawSurface::SetColorKey method to force the color key to the color black. This assumes, of course, that the bitmap has black as palette index entry 255 in the color table. For the DDEX4 example, black is index entry 0, so you'll have to change that if you want to see this sample work using the IDirectDrawSurface::SetColorKey call.

What if your bitmap doesn't have entry 255 set to black? You can also select the color key by setting the RGB values for the color you want in the call to the DDSetColorKey function. The RGB value for black is 0, 0, 0. The DDSetColorKey function in Ddutil.cpp calls the DDColorMatch function, also in Ddutil.cpp. The DDColorMatch function stores the current color value of the pixel at location 0, 0 on the bitmap located in the lpDDSOne surface, then takes the RGB values you supplied and sets the pixel at location 0, 0 to that color. It then masks the value of the color with the number of bits per pixel that are available. Once that is done, the original color is put back in location 0, 0, and the call returns to DDSetColorKey with the actual color key value. Once returned, the color key value is placed in the dwColorSpaceLowValue member of the DDCOLORKEY structure and is also copied to the dwColorSpaceHighValue member. The call to IDirectDrawSurface::SetColorKey then sets the color key.

You may have noticed the reference to CLR_INVALID in DDSetColorKey and DDColorMatch. If you pass this value as the color key in the DDSetColorKey call in DDEX4, the pixel in the upper left corner of the bitmap will be used as the color key. As the DDEX4 bitmap is delivered, that doesn't mean much since the color of the pixel at 0, 0 is a shade of gray. If, however, you would like to see how to use the pixel at 0, 0 as the color key for the DDEX4 example, you can do so by loading the bitmap file, All.bmp, into your drawing application, then change the single pixel at 0, 0 to black. Be sure to save the change (it's hard to see). Then change the DDEX4 line that calls DDSetColorKey to:

DDSetColorKey(lpDDSOne, CLR_INVALID);

Recompile the DDEX4 example, and ensure that the resource definition file is also recompiled so that the new bitmap is included. (To ensure that the resource definition file is recompiled, you could just add and then delete a space in the Ddex4.rc file.) The DDEX4 example will now use the pixel at 0, 0 (now set to black) as the color key.

Animation in DDEX4

The DDEX4 example uses the updateFrame function to create a simple animation using the red donuts included in the All.bmp file. The animation consists of three red donuts positioned in a triangle rotating at various speeds. The example compares the Win32 GetTickCount with the number of milliseconds since the last call to GetTickCount to determine whether to redraw any of the sprites. It then uses the IDirectDrawSurface::BltFast method to first blit the background from the off-screen surface (lpDDSOne) to the back buffer, then blit the sprites to the back buffer using the color key you had set earlier to determine which pixels are transparent. After the sprites are blitted to the back buffer, DDEX4 calls the IDirectDrawSurface::Flip method to flip the back buffer and the primary surface.

Note that when IDirectDrawSurface::BltFast is used to blit the background from the off-screen surface, the dwTrans parameter that specifies the type of transfer is set to DDBLTFAST_NOCOLORKEY. This indicates that a normal blit will occur with no transparency bits. Later, when the red donuts are blitted to the back buffer, the dwTrans parameter is set to DDBLTFAST_SRCCOLORKEY. This indicates that a blit will occur with the color key for transparency as it is defined, in this case, in the lpDDSOne buffer.

In this example, the entire background is redrawn each time through the updateFrame function. One way of optimizing this example would be to redraw only that portion of the background that changes while rotating the red donuts. Since the location and size of the rectangles that make up the donut sprites never change, you should easily be able to modify the DDEX4 example with this optimization.

Dynamically Modifying Palettes

The DDEX5 example is a modification of the DDEX4 example that demonstrates how to dynamically change the palette entries while an application is running. Although this may not be commonly used in games, the DDEX5 example will give you a taste of how DirectDraw palettes can be manipulated.

The following code in DDEX5 loads the palette entries with the values in the lower half of the All.bmp file (the part that contains the series of red donuts):

// First, set all colors as unused.
for(i=0; i<256; i++)
{
    torusColors[i] = 0;
}

// Lock the surface and scan the lower part (the torus area)
// and remember all the indexes we find.
ddsd.dwSize = sizeof(ddsd);
while (lpDDSOne->Lock(NULL, &ddsd, 0, NULL) == DDERR_WASSTILLDRAWING)
    ;

// Now search through the torus frames and mark used colors.
for( y=480; y<480+384; y++ )
{
    for( x=0; x<640; x++ )
    {
        torusColors[((BYTE *)ddsd.lpSurface)[y*ddsd.lPitch+x]] = 1;
    }
    }

lpDDSOne->Unlock(NULL);

The array torusColors is used as an indicator of the color index of the palette used in the lower half of the All.bmp file. Before it is used, all of the values in the torusColors array are reset to 0. The off-screen buffer is then locked in preparation for determining whether a color index value is used.

The array torusColors is set to start at row 480 and column 0 of the bitmap. The color index value in the array is determined by the byte of data at the location in memory where the bitmap surface is located. This location is determined by the lpSurface member in the DDSURFACEDESC structure, which is pointing to the memory location corresponding to row 480 and column 0 of the bitmap (y*lPitch+x). The location of the specific color index value is then set to 1. The y value (row) is multiplied by the lPitch value (found in the DDSURFACEDESC structure) to get the actual location of the pixel in linear memory.

The color index values that are set in torusColors will be used later to determine which colors in the palette will be rotated. Since there are no common colors between the background and the red donuts, only those colors associated with the red donuts will be rotated. If you want to check out whether this is true or not, just remove the *lpitch from the array and see what happens when you recompile and run the program. (Without multiplying y×lpitch, the red donuts are never reached and only the colors found in the background will be indexed and later rotated.)

Width vs. pitch

Before we continue with rotating the palettes, let me first go off on a tangent for a second and talk about the difference between width and pitch in display memories.

If your application writes to display memory, the pitch may need to be a different value from the width, since not all display memory is laid out as one linear block. For rectangular memory, for example, the pitch of the display memory will include the width of the bitmap plus part of a cache. The following figure shows the difference between width and pitch in rectangular memory:

Figure 3. Width versus pitch in rectangular memory

In this figure, the front buffer and back buffer are both 640×480×8, and the cache is 384×480×8. To reach the address of the next line to write to the buffer, you must add 640 and 384 to get 1024, which is the beginning of the next line.

The moral is, when rendering directly into surface memory, always use the pitch returned by the IDirectDrawSurface::Lock method (or the IDirectDrawSurface::GetDC method). Don't assume a pitch based solely on the display mode. If your application works on some display adapters but looks garbled on others, this may be your problem.

Rotating the palettes

The updateFrame function in DDEX5 works in much the same way as it did in DDEX4. It first blits the background into the back buffer, then blits the three donuts in the foreground. However, before it flips the surfaces, updateFrame changes the palette of the primary surface from the palette index that was created in the doInit function, as shown in the following code.

// Change the palette.
if(lpDDPal->GetEntries( 0, 0, 256, pe ) != DD_OK)
{
    return;
}

for(i=1; i<256; i++)
{
    if(!torusColors[i])
    {
       continue;
    }
    pe[i].peRed = (pe[i].peRed+2) % 256;
    pe[i].peGreen = (pe[i].peGreen+1) % 256;
    pe[i].peBlue = (pe[i].peBlue+3) % 256;
}

if(lpDDPal->SetEntries( 0, 0, 256, pe) != DD_OK)
{
    return;
}

The IDirectDrawPalette::GetEntries method in the first line queries palette values from a DirectDrawPalette object. Since the palette entry values pointed to by pe should be valid, the method will return DD_OK and continue. The loop that follows checks torusColors to determine whether the color index was set to one during its initialization. If the color index is set to one, the red, green, and blue values in the palette entry pointed to by pe are rotated.

Once all the marked palette entries are rotated, the IDirectDrawPalette::SetEntries method is called to actually change the entries in the DirectDrawPalette. This change takes place immediately if you are working with a palette set to the primary surface. With this done, the surfaces are then flipped.

Examining the Space Donuts Example

The five basic DirectDraw example applications (DDEXx) show you the most rudimentary ways of using DirectDraw. To get a more accurate picture of how DirectDraw can be used in your games, the DirectX 5 SDK also contains a simple game called Space Donuts (located in the C:\DXSDK\SDK\SAMPLES\DONUTS directory). Space Donuts contains many of the same functions that we have discussed so far, but are incorporated into code that more closely resembles how you will be using DirectDraw in your games.

Most of the DirectDraw methods we have examined here are in the Space Donuts application, so I'm not going to go over them again. You should spend a little time looking over the Space Donuts code to see how the DirectDraw methods are actually used in a game.

Some Optimizations and Customizations

All of the DirectDraw examples supplied with the DirectX 5 SDK are relatively simple and assume a lot of things about the system they are running on. In this section, I will examine some optimizations and customizations that will allow your code to work in real-world situations.

Getting the Flip and Blit Status

When the IDirectDrawSurface::Flip method is called, the primary surface and back buffer are exchanged. However, the exchange may not occur immediately. For example, if a previous flip has not finished, or if it did not succeed, IDirectDrawSurface::Flip will return DDERR_WASSTILLDRAWING. In the examples supplied with the DirectX 5 SDK, the IDirectDrawSurface::Flip call will continue to loop until it returns DD_OK. Also, a IDirectDrawSurface::Flip call does not complete immediately. It schedules a flip for the next time a vertical blank occurs on the system.

Just waiting for the DDERR_WASSTILLDRAWING value to go away is very inefficient. Instead, you could create a function that calls the IDirectDrawSurface::GetFlipStatus method on the back buffer to determine whether the previous flip has finished.

If the previous flip has not finished and the status call returns with DDERR_WASSTILLDRAWING, you can use the spare time to perform some other game logic, then check the status again. If the previous flip has finished, you can then perform the next flip. The following example shows how this could be done:

while( lpDDSBack->GetFlipStatus( DDGFS_ISFLIPDONE ) == DDERR_WASSTILLDRAWING );

      // Wait for flip to finish -- do real game stuff here
      // rather than just burn CPU.

ddrval = lpDDSPrimary->Flip( NULL, 0 );

You can use the IDirectDrawSurface::GetBltStatus method in much the same way to determine whether a blit has finished. Because IDirectDrawSurface::GetFlipStatus and IDirectDrawSurface::GetBltStatus return immediately, you can use them periodically in your game logic with little loss in speed.

Blitting with Color Fill

You can use the IDirectDrawSurface::Blt method to perform a color fill of the most common color you want to be displayed. For example, if the most common color you display is blue, you can use IDirectDrawSurface::Blt with the DDBLT_COLORFILL flag to first fill the surface with the color blue and then write everything else on top of it. This allows you to fill in the most common color very quickly, and you then only have to write a minimum amount of colors to the surface.

The following example shows one way to perform a color fill:

DDBLTFX         ddbltfx;

ddbltfx.dwSize = sizeof( ddbltfx );
ddbltfx.dwFillColor = 0;
ddrval = lpDDSPrimary->Blt(
   NULL,         // destination
   NULL, NULL,      // source rectangle
   DDBLT_COLORFILL, &ddbltfx );

switch( ddrval )
{
   case DDERR_WASSTILLDRAWING:
      .
      .
      .
   case DDERR_SURFACELOST:
      .
      .
      .
   case DD_OK:
      .
      .
      .
   default:
}

Determining the Capabilities of the Display Hardware

DirectDraw uses hardware emulation to perform the DirectDraw functions not supported by the end user's hardware. To accelerate performance of your DirectDraw applications, you should determine the capabilities of the end user's display hardware after you have created a DirectDraw object. DirectDraw will make use of any display acceleration hardware available on the end user's system. Note that your application must supply DirectDraw with a list of the hardware emulation it needs in case the display adapter on the end user's system does not contain the display acceleration hardware required by your application.

Use the IDirectDraw::GetCaps method to fill in the capabilities of the display hardware. The DirectDraw device driver for the hardware fills in the values of the dwCaps member of the DDCAPS structure. These values identify the capabilities of the display acceleration hardware on the system. The DDCAPS structure contains the address of the DDSCAPS structure that supplies hardware emulation requirements for the application. Hardware emulation will be used in case any or all of the DirectDraw hardware capabilities are not available on the display adapter. You must supply the hardware emulation values that your application requires in the DDSCAPS structure.

Of course, when developing your game, it is up to you to decide whether the hardware capabilities returned from the display adapter on the end user's system will be conducive to a good game-playing experience, and what to do about it if it won't be.

Storing Bitmaps in Display Memory

Blitting from display memory to display memory is usually much more efficient than blitting from system memory to display memory. Because of this, you should store as many as possible of the sprites your game uses in display memory.

Most display adapter hardware contains enough extra memory to store more than simply the primary surface and the back buffer. You can use the dwVidMemTotal and dwVidMemFree members in the DDCAPS structure (if you used the IDirectDraw::GetCaps method to get the capabilities of the end user's display hardware) to determine the amount of available memory for storing bitmaps in the display adapter's memory. If you want to see how this works, use the DirectX Viewer application supplied with the DirectX 5 SDK, and, under DirectDraw Devices, select the Primary Display Driver folder, then select the General folder. The amount of total display memory (minus the primary surface) and the amount of free memory is displayed. Each time a surface is added to the DirectDraw object, the amount of free memory will decrease by the amount of memory used by the added surface.

Triple Buffering

In some cases it may be possible to speed up the process of displaying your game by using triple buffering. In triple buffering, there is one primary surface and two back buffers. The following code shows how to initialize a triple buffering scheme:

// Create the primary surface with two back buffers.
ddsd.dwSize = sizeof( ddsd );
ddsd.dwFlags = DDSD_CAPS | DDSD_BACKBUFFERCOUNT;
ddsd.ddsCaps.dwCaps = DDSCAPS_PRIMARYSURFACE |
                      DDSCAPS_FLIP | 
                      DDSCAPS_COMPLEX;
ddsd.dwBackBufferCount = 2;
ddrval = lpDD->CreateSurface( &ddsd, &lpDDSPrimary, NULL );
if( ddrval == DD_OK )
{
    // Get a pointer to the first back buffer.
    ddscaps.dwCaps = DDSCAPS_BACKBUFFER;
    ddrval = lpDDSPrimary->GetAttachedSurface(&ddscaps, 
                                         &lpDDSBackOne);
    if( ddrval != DD_OK )
        // Error message here
    // Get a pointer to the second back buffer.
    ddscaps.dwCaps = DDSCAPS_BACKBUFFER;
    ddrval = lpDDSPrimary->GetAttachedSurface(&ddscaps, 
                                         &lpDDSBackTwo);

Triple buffering allows your application to continue blitting to a back buffer even if a flip has not completed and the first back buffer's blit has already finished. Performing a flip is not a synchronous event; one flip may take longer than another. Because of this, if your application uses only one back buffer, it may spend some time idling while waiting for the IDirectDrawSurface::Flip method to return with DD_OK.

Note that if the capabilities of all three surfaces match, you must use the IDirectDrawSurface::EnumAttachedSurfaces method to first obtain the attached surfaces, then use IDirectDrawSurface::GetAttachedSurface as shown in the sample above. In addition, you will also have to create a separate EnumSurfacesCallback for each of the back buffers.

Where to Go from Here

By now you should have a fairly clear understanding of how to use DirectDraw in simple games. Of course, this is just the mere tip of the iceberg when it comes to DirectDraw programming. To move on to more advanced methods, you should check out some of the other samples included with the DirectX 5 SDK. These include:

Other samples you can examine for their DirectDraw code include Iklowns, Foxbear, Duel, Palette, and Flip2d. Although these last samples don't contain any unique DirectDraw code, every little bit helps, I always say.

Good luck and happy programming.