Upcoming Events
Unite 2010
11/10 - 11/12 @ Montréal, Canada

GDC China
12/5 - 12/7 @ Shanghai, China

Asia Game Show 2010
12/24 - 12/27  

GDC 2011
2/28 - 3/4 @ San Francisco, CA

More events...
Quick Stats
96 people currently visiting GDNet.
2406 articles in the reference section.

Help us fight cancer!
Join SETI Team GDNet!
Link to us Events 4 Gamers
Intel sponsors gamedev.net search:

Contents
 What is DirectX?
 COM
 Setting Up
 DirectDraw
 Cooperation Levels
 Creating Sufaces

 Printable version
 Discuss this article
 in the forums



The Series
 Beginning Windows
 Programming

 Using Resources
 in Win32 Programs

 Tracking Your
 Window/Using GDI

 Introduction
 to DirectX

 Palettes and Pixels
 in DirectDraw

 Bitmapped Graphics
 in DirectDraw

 Developing the
 Game Structure

 Basic Tile Engines
 Adding Characters
 Tips and Tricks

Overview of DirectDraw

To set up DirectDraw for use in your program, you need to do a minimum of four things in the initialization section of your program. They are:

  1. Create a DirectDraw object.
  2. Set the cooperation level.
  3. Set the screen resolution and color depth (fullscreen applications).
  4. Create at least one DirectDraw surface.

Before looking at how these steps are accomplished, let's go over a brief explanation of what each one means. First, you need to create a DirectDraw object, meaning we want to retrieve a pointer to the IDirectDraw7 interface. That's pretty simple, right? There are actually three ways to do this. You can make a direct COM call, or use one of two DirectDraw wrapper function. Each has its merits, so we'll be looking at all three of them in a bit.

Second, you need to set the cooperation level. This is probably new to you. Cooperation is a concept that's introduced because Windows is a multitasking operating system. The idea is that all programs running at any given time have to inform Windows what resources they're going to be using, and in what way. This is to make sure Windows doesn't try to take resources essential to your program and allocate them to something else, and so it has the details of how your program will be operating. Don't worry, it's just a simple function call.

The third item on the list is familiar, right? If you're writing a fullscreen application, like a game will usually be, you need to set the screen resolution and color depth to where you want them. Doing this in a windowed application is usually not a good idea, because it can cause problems with other programs that are running at the same time. You'd also have to make certain to restore it when you are finished. In fullscreen mode, setting the resolution is just a single call to DirectDraw, and when your program ends, the Windows desktop settings are restored.

Lastly, and most importantly, is the concept of a DirectDraw surface. Manipulating surfaces is what DirectDraw is all about. Basically, a surface is an area in memory reserved for graphics and graphical operations. The size of a DirectDraw surface is defined by its width and height, in pixels, so you can think of it as a rectangular area for drawing graphics. It has its own interface, called IDirectDrawSurface7. There are three main kinds of surfaces, each of which we'll be using between this article and the next.

Primary surfaces: Every DirectDraw application must have a primary surface in order to accomplish anything. The primary surface is the surface that represents the user's display. Its contents are always visible. As such, a primary surface is automatically set to the width and height of the screen mode.

Back buffers: Back buffers are surfaces that are attached to the primary surface, but not visible. This is how animation is created without flicker. Normally, you draw each frame on a back buffer, and then copy the contents of the back buffer to the primary urface, causing it to appear instantaneously. Since they are attached to the primary surface, they are also the same size as the display.

Offscreen buffers: These are very much like back buffers, only they are not attached to the primary surface. They are most often used for storing bitmaps, although you can do anything you want with them. Offscreen buffers can be any size you want. The only limitation is the amount of memory the system has.

DirectDraw surfaces can either be created in system memory, or directly on the video card in VRAM. If you have all of your surfaces in video memory, the speed is going to be fantastic. System memory is slower. Also, if you have one surface stored in video memory, and one stored in system memory, performance will suffer quite a bit, especially if the video card in question has lousy memory bandwidth. Anyway, the moral is that if you can get away with creating all of your surfaces in video memory, it's probably worth doing so.

All right, now that we have some idea for what we have to do, let's see how we go about doing it. Here's the plan. We're going to create a fullscreen DirectX application that runs in 640x480x16bpp. I'll go through all the DirectX stuff you need to do that, but before you can go setting up DirectX, you need to have a window to operate on. That much is up to you! We went through it in my first article, so you should be pretty familiar with creating windows by now. Since this is going to be a fullscreen application, you're going to want a window that doesn't have any Windows controls on it, so for the window style, use WS_POUP | WS_VISIBLE. Got it? All right, here we go.

Creating a DirectDraw Object

As I said before, there are three ways to do this. We could either use one of two DirectDraw wrapper functions, or make a call directly to the COM object. Let's look at all of them, just to get ourselves used to this stuff. The last method I'll show you is by far the easiest, so you'll probably want to use that. As for the other two, they give you a first look at a few different things you'll come across again. First, have a look at DirectDrawCreate():

HRESULT WINAPI DirectDrawCreate( GUID FAR *lpGUID, LPDIRECTDRAW FAR *lplpDD, IUnknown FAR *pUnkOuter );

Looks a bit strange, doesn't it? The HRESULT return type is standard for DirectDraw functions. If successful, the return value is DD_OK. Each function has several constants it can return describing various errors, but in the interest of keeping the length down a bit, I won't list the values for every function. You can always look them up. One thing I will mention is that there are two useful macros you can use to determine the result of a DirectDraw function call: SUCCEEDED() and FAILED(). Pretty self-explanatory, wouldn't you say? Just put the function call inside the macro to see what's happening. Anyway, the parameters for the function are:

GUID FAR *lpGUID: This is the address of the GUID identifying the driver to use. For the default, simply pass NULL. Otherwise, there are two values you can use here for debugging purposes:

DDCREATE_EMULATIONONLYCreating a DirectDraw object with this flag means that no hardware support will be utilized, even if it is available. All operations are handled in the HEL.
DDCREATE_HARDWAREONLYCreating a DirectDraw object with this flag means that only the HAL will be used; if the hardware doesn't support an operation you call, the function calling that operation will return an error rather than invoking the HEL.

LPDIRECTDRAW FAR *lplpDD: This is the address of a pointer to a DirectDraw object; it will be initialized as such if the function succeeds. You must declare a variable of type LPDIRECTDRAW that will hold your interface pointer, then pass the address to that pointer.

IUnknown FAR *pUnkOuter: This parameter is reserved for advanced COM features that are not yet implemented. Always pass NULL.

That wasn't so bad, but there is a problem. This function gives you a pointer to an IDirectDraw interface, but we want a pointer to the IDirectDraw7 interface! What do we do? The answer is to call the QueryInterface() method of the DirectDraw object we have obtained, and request the IDirectDraw7 interface. Here's what the call looks like:

HRESULT QueryInterface( REFIID iid, // Identifier of the requested interface void **ppvObject // Address of output variable that receives the ); // interface pointer requested in iid

The parameters are pretty straightforward:

REFIID iid: Remember when I said GUIDs are sometimes called IIDs for interfaces? This is what I was talking about. The IID for IDirectDraw7 is IID_IDirectDraw7. To use this constant, you must link the dxguid.lib library to your project.

void **ppvObject: Similar to what we did with DirectDrawCreate(), except here you should declare a pointer of type LPDIRECTDRAW7, and pass its address. If you're using Visual C++ 6.0, you'll probably need a typecast here.

Now we have two interface pointers: one to an IDirectDraw interface, and one to IDirectDraw7. The latter is what we want; the former is useless. Remember that when you're done using an interface, you must call its Release() method to decrement the reference count for its COM object. The prototype for Release() is simple:

ULONG Release(void);

The return value is the resulting reference count, which you would only need to worry about for testing or debugging purposes. Also, it's recommended that for safety, you should set a pointer to a released interface to NULL. It's also usually a good idea to set such pointers to NULL when you declare them in the first place. Are you following me? It can be a bit much to remember in the beginning, but it'll become second nature to you in no time. Let's bring it all together and look at an example of getting an IDirectDraw7 interface pointer:

LPDIRECTDRAW lpdd = NULL; // pointer to IDirectDraw (temporary) LPDIRECTDRAW7 lpdd7 = NULL; // pointer to IDirectDraw7 (what we want) // get the IDirectDraw interface pointer if (FAILED(DirectDrawCreate(NULL, &lpdd, NULL))) { // error-handling code here } // query for IDirectDraw7 pointer if (FAILED(lpdd->QueryInterface(IID_IDirectDraw7, (void**)&lpdd7))) { // error-handling code here } else { // success! release IDirectDraw since we don't need it anymore lpdd->Release(); lpdd = NULL; }

Now, if you're a C programmer, you may be a little confused by the way I called QueryInterface() and Release(). You've probably seen the -> operator before. It's used to dereference members of a struct when using a pointer to the struct rather than the variable itself. It's the same thing with objects, except that in this case, the member you're making reference to is a function instead of a variable. While we're on the topic, I'll introduce another bit of C++ notation as well, the scope resolution operator (::). It's used to show the class (or interface, in our case) of which a certain function or variable is a member. In this case, QueryInterface() is a method of the base interface IUnknown, so we would refer to it as IUnknown::QueryInterface(). I'll be using this often in the future, so remember it!

To be honest, that's only useful for demonstrating how to use the QueryInterface() method, which is a part of all DirectX interfaces, so let's move on. Next, just to show you what it looks like, let's use the COM method. The nice thing about doing it this way is that you can get an IDirectDraw7 interface pointer immediately, instead of getting the older pointer and querying for the new one. First, you have to initialize COM, like this:

HRESULT CoInitialize(LPVOID pvReserved);

It doesn't get much easier. The parameter is reserved as must be set to NULL. When you're finished with COM calls, you need to uninitialize it, with an even easier call:

void CoUninitialize(void);

I usually call CoInitialize() in the very beginning of my DirectX programs, and call CoUninitialize() at the very end, after I've released all my DirectX objects. Once COM is initialized, you can get the pointer you're after by calling CoCreateInstance(), which can get a little ugly:

STDAPI CoCreateInstance( REFCLSID rclsid, // Class identifier (CLSID) of the object LPUNKNOWN pUnkOuter, // Pointer to whether object is or isn't part // of an aggregate DWORD dwClsContext, // Context for running executable code REFIID riid, // Reference to the identifier of the interface LPVOID *ppv // Address of output variable that receives ); // the interface pointer requested in riid

If the call succeeds, the return value is S_OK. The parameters require a bit of explanation, so here's the list:

REFCLSID rclsid: This is a class identifier (not to be confused with the IID), which also has constants defined for it. For IDirectDraw7, use CLSID_DirectDraw. Notice the version number is not specified because this is a class identifier, not an interface identifier.

LPUNKNOWN pUnkOuter: This is the same thing we saw in DirectDrawCreate(). Set it to NULL.

DWORD dwClsContext: The value required here is called an execution context, and it defines the way that the code that manages the newly created object will run. The values it can take are defined in an enumeration called CLSCTX. In this case, use CLSCTX_ALL, which includes all the possible values.

REFIID riid: We saw this in our call to QueryInterface(). The IID is IID_DirectDraw7.

LPVOID *ppv: This was also in DirectDrawCreate(), and is the address of the pointer to our interface.

That one call takes the place of our calls to DirectDrawCreate(), QueryInterface(), and Release() in the previous example, so it's a bit less code, but it really doesn't matter which way you decide to use. The COM call is a little less hassle than the first method we saw, since it doesn't require getting that extra interface pointer. Once you create an object using CoCreateInstance(), though, you have to initialize it by calling the Initialize() method of the object. In C++ this would be written as IDirectDraw7::Initialize(). Here's the prototype:

HRESULT Initialize(GUID FAR *lpGUID);

Use the same GUID you would have used in the call to DirectDrawCreate(). In other words, NULL. So before moving on, let me show you the example of using COM to create a DirectDraw object:

LPDIRECTDRAW7 lpdd7; // interface pointer // initialize COM CoInitialize(NULL); // create the object CoCreateInstance(CLSID_DirectDraw, NULL, CLSCTX_ALL, IID_IDirectDraw7, (void**)&lpdd7); // initialize the object lpdd7->Initialize(NULL);

It's good to see an example of calling COM directly, since there may come a time when you want to -- or have to -- do this instead of calling one of the high-level wrapper functions. Finally, now that you've seen the hard ways to do this, let's use the easy way. There is another wrapper function that takes care of everything we want to do, in a single call. No querying for higher interfaces, no setting up COM, no nothing. Here's the function:

DirectDrawCreateEx( GUID FAR *lpGuid, LPVOID *lplpDD, REFIID iid, IUnknown FAR *pUnkOuter );

All these parameters should look familiar, because we've just seen them. The first, second, and fourth parameters are the same ones we passed to DirectDrawCreate(), only in this case we need to cast the address of our interface pointer to void** -- don't ask me why; it wasn't my idea. The third parameter, riid, is the interface ID that we passed to CoCreateInstance, so just use IID_IDirectDraw7 and we're good to go. That's it! Now that we've got our DirectDraw object, we can start doing things with it. The first two things we want to do are setting the cooperation level and screen resolution, so let's take a look.




Next : Cooperation and Resolution