Game Programming Genesis Part IV : Introduction to DirectX

Published December 19, 2000 by Joseph Farrell, posted by Myopic Rhino
Do you see issues with this article? Let us know.
Advertisement
(Editor's Note - this article requires additional formatting work and is incomplete)

[size="5"]Introduction

Ahh, it's a good day. Know why? Because today we'll be taking a first look at the awesome creation that is DirectX. It's several times faster than Windows GDI, usable from different languages and different platforms, and supports everything from plotting pixels to advanced 3D effects, from playing simple sound effects to streaming digital music, from handling the keyboard to using Force Feedback controllers. It even gives you a hand with networking, and installing the run-time libraries on the user's machine. Of course, it's a huge subject and it would take several books to cover all of it, not to mention about a hundred times more knowledge than I have to give you, but I can get you started. :)

For this article you need only the knowledge of Windows programming covered in my first few articles, and a working knowledge of C. Since I'll be talking a bit about the Component Object Model on which DirectX is based, some knowledge of C++ will definitely help out this time around. If you don't have it, don't worry about a thing. I'll give you a brief explanation of the C++ elements of the article as we get to them. Rest assured that you don't need to know too much about C++ to use DirectX. All right? Let's do it.


[size="5"]What is DirectX?

DirectX is a game developer's API, or Application Development Interface. It is a group of software libraries that lets you make non-hardware-specific function calls, and handles all the details of the machine it happens to be running on. If the hardware supports the functions you're using, then hardware acceleration does the job, which means it's FAST. If not, DirectX has software emulation that takes over. It will be slower -- much slower in some cases -- but the point is that you don't have to worry about the low-level details. You can just focus on the game. The layer of DirectX that takes care of hardware-supported features is called the hardware acceleration layer (HAL), and the software emulation layer that kicks in if the hardware doesn't support a feature is called the hardware emulation layer (HEL).

DirectX has several components, each one with a specific purpose. DirectDraw is probably the most important one, because in the end, all graphics end up going through it. Used by itself, it's basically a bitmap engine used for 2D graphics, but Direct3D -- another component of DirectX -- also works through DirectDraw when it comes time to display each frame. DirectDraw is what we'll be concentrating our attention on today. Other components of DirectX include DirectInput, which handles every input device you can think of, and probably a lot of devices neither of us has ever heard of; DirectSound, which is used for digital sound and music; DirectMusic, which is a more recent addition to the DirectX package and is used for MIDI music; and DirectPlay, for networking.

DirectX is based on a standard called the Component Object Model, which can be a bit confusing. Creating your own COM objects can be a tricky subject -- don't ask me how to do it -- but understanding the way it works isn't too bad. I'm only going to cover the properties of COM, not the details about how you would create a COM object yourself. So if this next section confuses you a little, don't worry. You don't have to recreate the functionality I'm describing. Using COM objects is a lot easier than creating them, as we'll see a bit later.


[size="5"]The Component Object Model (COM)

COM is a specification that defines rules by which reusable software components, called COM objects, are created. Each object is a collection of interfaces, which themselves are collections of functions -- basically, C++ classes. Using Microsoft's notation, the name of an interface always begins with an "I," much like the way regular class names begin with a "C."

Every COM interface is derived from a special base class called [font="Courier New"][color="#000080"]IUnknown[/color][/font], which has only three methods: [font="Courier New"][color="#000080"]QueryInterface()[/color][/font], [font="Courier New"][color="#000080"]AddRef()[/color][/font], and [font="Courier New"][color="#000080"]Release()[/color][/font]. For you C programmers, a method is simply a member function of a class, and a class is nothing more than a [font="Courier New"][color="#000080"]struct[/color][/font] whose member functions or variables may not be directly accessible to the user. I know that's not a great definition, but I don't want to get into a lengthy discussion of OOP here! Anyway, back to the three methods of [font="Courier New"][color="#000080"]IUnknown[/color][/font]:

[font="Courier New"][color="#000080"]QueryInterface()[/color][/font]: This is probably the most important function of a COM object. It is used to retrieve a pointer to the interface you want to work with. In order to get such a pointer, you must know the GUID of the interface. GUID stands for "globally unique identifier" and is a 128-bit value used to identify the interface. GUIDs used in this way are often called interface IDs, or IIDs. When creating a COM object, you can't just make up a GUID. You must use a special program to create one; most Win32 compilers come with such a tool. These programs apply an algorithm that guarantees that no GUID will ever be generated twice.

It sounds impossible, but it's not. There are enough combinations that these programs can contain rules by which they generate GUIDs such that no combination will ever be repeated. Let me put it this way. There are six billion people living on this planet. If every one of those people had five million kids, and every one of those kids had five million dogs, and men, women, children and dogs alike sat around all day, every day, generating one GUID per second, it would take them about seventy million years to use up all the combinations. That's probably not going to happen. :) I know of only one dog that sits around generating GUIDs, and it takes him at least ten seconds for each one.

[font="Courier New"][color="#000080"]AddRef()[/color][/font]: COM objects use a variable called a reference count to track their usage. This function simply adds one to the reference count for that particular COM object, and must be called when retrieving a pointer to one of the interfaces contained within that COM object. You almost never do this manually. There are several ways you can obtain an interface pointer, and the ones you'll be using automatically call [font="Courier New"][color="#000080"]AddRef()[/color][/font].

[font="Courier New"][color="#000080"]Release()[/color][/font]: When you're finished using an interface pointer, you must release it. This decrements the reference count for the COM object. So when you're writing a program that will be using one or more COM objects, the reference count for each COM object should be at zero when the program ends. Sometimes, if you create an object, and then use that object to create a child object, releasing the parent will also release the child. Since this is not guaranteed, it's usually a good idea to release objects in the reverse order that you created them in.

One of the nice things about COM is that its objects can be used with any language, and on any platform. How is this possible? One of the guidelines defining the COM specification states that every COM object must match the binary image that would be generated by a Microsoft Visual C++ compiler. It seems a bit strange to think of it that way, but consider this: once you compile your code, it just becomes binary data. You can't look at a string of 1s and 0s and divine whether it was C++ or Visual Basic before it was compiled, can you? Neither can I.

Another feature of COM is that you can recreate or update COM objects without changing or recompiling the programs that use them. The reason this is possible is that COM objects are usually dynamic-link libraries ([font="Courier New"][color="#000080"].DLL[/color][/font] files). They aren't compiled into programs that use them; they are linked at run-time. Thus, if a program you write ships with a few COM objects you created, and later on you want to add or change something in one of those COM objects, you simply need to supply users with the new [font="Courier New"][color="#000080"].DLL[/color][/font] file. No recompiling the main program is necessary.

The new version of the COM object, however, must contain all of the old interfaces and functions. This is to preserve backwards-compatibility. It guarantees that any program created to run with old versions of COM objects will function exactly the same if it's using new versions of those objects. Since DirectX follows the COM specification, the same rules apply. A program developed with DirectX 5.0 will work just fine if the user has DirectX 7.0 installed on his computer. Cool, hey?


[size="5"]Setting Up

There are three main things you need to develop programs with DirectX. First are the COM objects themselves, which are inside [font="Courier New"][color="#000080"].DLL[/color][/font] files. These [font="Courier New"][color="#000080"].DLL[/color][/font]s need to be registered with Windows, which is what happens when you install DirectX. Inside these objects are interfaces like [font="Courier New"][color="#000080"]IDirectDraw[/color][/font] that we'll be using to create DirectX applications. But this isn't enough, because handling DirectX on the COM level would be tedious and frustrating. There are times when you'll make a few direct COM calls, but for the most part, we want to use something easier.

That's where the static libaries ([font="Courier New"][color="#000080"].LIB[/color][/font] files) come into play. These are the main part of the DirectX SDK, which is available free from Microsoft. They contain "wrapper" functions to make using DirectX a whole lot easier. To use the various components of DirectX, you need to link the appropriate libraries to your project. For DirectDraw, this would be [font="Courier New"][color="#000080"]ddraw.lib[/color][/font].

Finally, you need the DirectX header files ([font="Courier New"][color="#000080"].H[/color][/font] files) that contain the function prototypes, macros, constants, and variable types that are used for each component of DirectX. For DirectDraw, this would be [font="Courier New"][color="#000080"]ddraw.h[/color][/font].

To make sure you're using the correct versions of these files, you must set your compiler to include the directories where you have installed the SDK. There are several lists of directories to search for library and header files, and the SDK directories must be first on the list. This is important, because most Win32 compilers ship with some version of these files already included. If the SDK directories are not first on the list, the older versions that came with your compiler will be included. The two directories you need to set are those for library and header files. For example, if you installed the DirectX SDK in your [font="Courier New"][color="#000080"]c:\mssdk[/color][/font] directory, the first item on the include file directory list should be [font="Courier New"][color="#000080"]c:\mssdk\include[/color][/font] and the first item on the library directory list should be [font="Courier New"][color="#000080"]c:\mssdk\lib[/color][/font]. In Visual C++, you can find these settings by choosing "Options..." from the Tools menu.

For this and any remaining articles dealing with DirectX, we will be using DirectX 7.0 (or higher). If you don't have the DirectX SDK, you'll need to get it from Microsoft. You don't need the whole thing, just the essentials. The entire SDK has tons of examples packed in with it, which makes for a whopping 128MB download. If you're like me and you're not lucky enough to have high-speed Internet access, you probably don't want to wait 12 hours for that sucker to download. Scroll down on the SDK download page, and you'll find the links for only the libaries, headers, and docs. It's around 10MB that way, which is much more reasonable.


[size="5"]DirectX Version Numbers

You wouldn't think something as simple as a version number would require an explanation, but remember who we're dealing with here. :) Microsoft may have created an incredible technology in DirectX, but they're still out to confuse everyone. With each release of DirectX, not all of the interfaces are updated. Therefore, even though there have been seven versions of DirectX, there have not been seven versions of DirectDraw. When DirectX 6 was the most recent version, the most recent interface for DirectDraw was [font="Courier New"][color="#000080"]IDirectDraw4[/color][/font], not [font="Courier New"][color="#000080"]IDirectDraw6[/color][/font]. The most recent release which updated DirectDraw was DirectX 7, and so we'll be dealing with [font="Courier New"][color="#000080"]IDirectDraw7[/color][/font]. Strange, hey? I thought I'd bring that up now, so you know what's going on when you see it later.

One last thing. When I wrote this article, DirectX 7 was the most recent API available, but you've probably heard that now we have DirectX 8. You have probably also heard that as of now, DirectDraw is no longer being updated. Instead, there's DirectX Graphics, which is one big catch-all graphics API. Now, just because DirectDraw isn't being updated doesn't mean we can't still use it. This is COM, after all. If you want to write 2D games with DirectX 8 interfaces, you need to employ 3D methods to create a 2D view. It sounds cool, and it is, since using 3D gives you access to more hardware-supported features like alpha-blending, but it has a problem. It's going to be slow if the user's machine doesn't have hardware acceleration.

DirectDraw is easier to learn, and its software layer is nice and fast since the added processor burden of using 3D is not present. So I will be using DirectDraw in this series. This also has to do with the fact that this and the next couple of articles in the series were written before the release of DirectX 8, and also because I still know very little about how to use the DirectX 8 interfaces, so I can hardly go telling you how to do it, can I? :) In any case, DirectDraw still makes a great introduction to the world of DirectX.


[size="5"]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 [font="Courier New"][color="#000080"]IDirectDraw7[/color][/font] 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 [font="Courier New"][color="#000080"]IDirectDrawSurface7[/color][/font]. 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 surface, 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 [font="Courier New"][color="#000080"]WS_POUP | WS_VISIBLE[/color][/font]. Got it? All right, here we go.


[size="5"]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 [font="Courier New"][color="#000080"]DirectDrawCreate()[/color][/font]:

HRESULT WINAPI DirectDrawCreate(
GUID FAR *lpGUID,
LPDIRECTDRAW FAR *lplpDD,
IUnknown FAR *pUnkOuter
);
Looks a bit strange, doesn't it? The [font="Courier New"][color="#000080"]HRESULT[/color][/font] return type is standard for DirectDraw functions. If successful, the return value is [font="Courier New"][color="#000080"]DD_OK[/color][/font]. 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: [font="Courier New"][color="#000080"]SUCCEEDED()[/color][/font] and [font="Courier New"][color="#000080"]FAILED()[/color][/font]. 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:

[font="Courier New"][color="#000080"]GUID FAR *lpGUID[/color][/font]: This is the address of the GUID identifying the driver to use. For the default, simply pass [font="Courier New"][color="#000080"]NULL[/color][/font]. 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.[font="Courier New"][color="#000080"]LPDIRECTDRAW FAR *lplpDD[/color][/font]: 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 [font="Courier New"][color="#000080"]LPDIRECTDRAW[/color][/font] that will hold your interface pointer, then pass the address to that pointer.

[font="Courier New"][color="#000080"]IUnknown FAR *pUnkOuter[/color][/font]: This parameter is reserved for advanced COM features that are not yet implemented. Always pass [font="Courier New"][color="#000080"]NULL[/color][/font].

That wasn't so bad, but there is a problem. This function gives you a pointer to an [font="Courier New"][color="#000080"]IDirectDraw[/color][/font] interface, but we want a pointer to the [font="Courier New"][color="#000080"]IDirectDraw7[/color][/font] interface! What do we do? The answer is to call the [font="Courier New"][color="#000080"]QueryInterface()[/color][/font] method of the DirectDraw object we have obtained, and request the [font="Courier New"][color="#000080"]IDirectDraw7[/color][/font] 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
);

The parameters are pretty straightforward:

[font="Courier New"][color="#000080"]iREFIID id[/color][/font]: Remember when I said GUIDs are sometimes called IIDs for interfaces? This is what I was talking about. The IID for [font="Courier New"][color="#000080"]IDirectDraw7[/color][/font] is [font="Courier New"][color="#000080"]IID_IDirectDraw7[/color][/font]. To use this constant, you must link the [font="Courier New"][color="#000080"]dxguid.lib[/color][/font] library to your project.

[font="Courier New"][color="#000080"]void **ppvObject[/color][/font]: Similar to what we did with [font="Courier New"][color="#000080"]DirectDrawCreate()[/color][/font], except here you should declare a pointer of type [font="Courier New"][color="#000080"]LPDIRECTDRAW7[/color][/font], 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 [font="Courier New"][color="#000080"]IDirectDraw[/color][/font] interface, and one to [font="Courier New"][color="#000080"]IDirectDraw7[/color][/font]. The latter is what we want; the former is useless. Remember that when you're done using an interface, you must call its [font="Courier New"][color="#000080"]Release()[/color][/font] method to decrement the reference count for its COM object. The prototype for [font="Courier New"][color="#000080"]Release()[/color][/font] 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 [font="Courier New"][color="#000080"]NULL[/color][/font]. It's also usually a good idea to set such pointers to [font="Courier New"][color="#000080"]NULL[/color][/font] 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 [font="Courier New"][color="#000080"]IDirectDraw7[/color][/font] 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 [font="Courier New"][color="#000080"]QueryInterface()[/color][/font] and [font="Courier New"][color="#000080"]Release()[/color][/font]. You've probably seen the -> operator before. It's used to dereference members of a [font="Courier New"][color="#000080"]struct[/color][/font] when using a pointer to the [font="Courier New"][color="#000080"]struct[/color][/font] 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, [font="Courier New"][color="#000080"]QueryInterface()[/color][/font] is a method of the base interface [font="Courier New"][color="#000080"]IUnknown[/color][/font], so we would refer to it as [font="Courier New"][color="#000080"]IUnknown::QueryInterface()[/color][/font]. 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 [font="Courier New"][color="#000080"]IDirectDraw7[/color][/font] 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 [font="Courier New"][color="#000080"]NULL[/color][/font]. When you're finished with COM calls, you need to uninitialize it, with an even easier call:

void CoUninitialize(void);
I usually call [font="Courier New"][color="#000080"]CoInitialize()[/color][/font] in the very beginning of my DirectX programs, and call [font="Courier New"][color="#000080"]CoUninitialize()[/color][/font] 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 [font="Courier New"][color="#000080"]CoCreateInstance()[/color][/font], 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 [font="Courier New"][color="#000080"]S_OK[/color][/font]. The parameters require a bit of explanation, so here's the list:

[font="Courier New"][color="#000080"]REFCLSID rclsid[/color][/font]: This is a class identifier (not to be confused with the IID), which also has constants defined for it. For [font="Courier New"][color="#000080"]IDirectDraw7[/color][/font], use [font="Courier New"][color="#000080"]CLSID_DirectDraw[/color][/font]. Notice the version number is not specified because this is a class identifier, not an interface identifier.

[font="Courier New"][color="#000080"]LPUNKNOWN pUnkOuter[/color][/font]: This is the same thing we saw in [font="Courier New"][color="#000080"]DirectDrawCreate()[/color][/font]. Set it to [font="Courier New"][color="#000080"]NULL[/color][/font].

[font="Courier New"][color="#000080"]DWORD dwClsContext[/color][/font]: 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 [color="#000080"]CLSCTX[/color]. In this case, use [color="#000080"]CLSCTX_ALL[/color], which includes all the possible values.

[font="Courier New"][color="#000080"]REFIID riid[/color][/font]: We saw this in our call to [font="Courier New"][color="#000080"]QueryInterface()[/color][/font]. The IID is [font="Courier New"][color="#000080"]IID_DirectDraw7[/color][/font].

[font="Courier New"][color="#000080"]LPVOID *ppv[/color][/font]: This was also in [font="Courier New"][color="#000080"]DirectDrawCreate()[/color][/font], and is the address of the pointer to our interface.

That one call takes the place of our calls to [font="Courier New"][color="#000080"]DirectDrawCreate()[/color][/font], [font="Courier New"][color="#000080"]QueryInterface()[/color][/font], and [font="Courier New"][color="#000080"]Release()[/color][/font] 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 [font="Courier New"][color="#000080"]CoCreateInstance()[/color][/font], though, you have to initialize it by calling the [font="Courier New"][color="#000080"]Initialize()[/color][/font] method of the object. In C++ this would be written as [font="Courier New"][color="#000080"]IDirectDraw7::Initialize()[/color][/font]. Here's the prototype:

HRESULT Initialize(GUID FAR *lpGUID);
Use the same GUID you would have used in the call to [font="Courier New"][color="#000080"]DirectDrawCreate()[/color][/font]. In other words, [font="Courier New"][color="#000080"]NULL[/color][/font]. 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 [font="Courier New"][color="#000080"]DirectDrawCreate()[/color][/font], only in this case we need to cast the address of our interface pointer to [font="Courier New"][color="#000080"]void**[/color][/font] -- don't ask me why; it wasn't my idea. The third parameter, [font="Courier New"][color="#000080"]riid[/color][/font], is the interface ID that we passed to [font="Courier New"][color="#000080"]CoCreateInstance[/color][/font], so just use [font="Courier New"][color="#000080"]IID_IDirectDraw7[/color][/font] 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.


[size="5"]Setting Cooperation and Resolution

We don't need anything fancy here; setting the cooperation level with Windows is as easy as calling [font="Courier New"][color="#000080"]IDirectDraw7::SetCooperativeLevel()[/color][/font], and setting the resolution is a simple call to [font="Courier New"][color="#000080"]IDirectDraw7::SetDisplayMode()[/color][/font]. Is that cool or what? First up is the cooperation level. Here's the function you use:

HRESULT SetCooperativeLevel(
HWND hWnd,
DWORD dwFlags
);
The return type is the [font="Courier New"][color="#000080"]HRESULT[/color][/font] you should already be getting used to seeing from these DirectX functions. On all such calls, you should be using the [font="Courier New"][color="#000080"]SUCCEEDED()[/color][/font] or [font="Courier New"][color="#000080"]FAILED()[/color][/font] macros to test the result of the call. Here are the parameters:

[font="Courier New"][color="#000080"]HWND hWnd[/color][/font]: Something familiar! Pass the handle to your main window, so Windows knows who's going to be hording all of its resources. :)

[font="Courier New"][color="#000080"]DWORD dwFlags[/color][/font]: This should look familiar too. Every time we've seen a [font="Courier New"][color="#000080"]dwFlags[/color][/font] parameter, it almost always results in a big list of constants that can be combined with the | operator. I'd hate to disappoint you!

DDSCL_ALLOWMODEXAllows the use of Mode X display modes (like 320x200, 320x240, or 320x400). This must be combined with DDSCL_EXCLUSIVE and DDSCL_FULLSCREEN.DDSCL_ALLOWREBOOTAllows the use of the Ctrl+Alt+Del Windows shortcut while running.DDSCL_EXCLUSIVEIndicates the application will have exclusive access to the display screen; must be combined with DDSCL_FULLSCREEN.DDSCL_FULLSCREENMicrosoft's effort to create redundancy; it must be combined with DDSCL_EXCLUSIVE. :)DDSCL_NORMALIndicates a normal Windows application. Use this for windowed DirectX programs. Cannot be combined with DDSCL_ALLOWMODEX, DDSCL_EXCLUSIVE, or DDSCL_FULLSCREEN.DDSCL_NOWINDOWCHANGESIndicates that DirectDraw may not minimze or restore the application window on activation.There are also a few flags specific to Windows 98 and NT 5.0 that deal with device windows, but I've left them off the list because we'll only be using these simple flags. Since we want to create a fullscreen application in 640x480x16 resolution, we would use the following call:

lpdd7->SetCooperativeLevel(hwnd, DDSCL_ALLOWREBOOT | DDSCL_EXCLUSIVE | DDSCL_FULLSCREEN);
So now that the cooperation level is all taken care of, let's look at the function used to change the screen resolution:

HRESULT SetDisplayMode(
DWORD dwWidth,
DWORD dwHeight,
DWORD dwBPP,
DWORD dwRefreshRate,
DWORD dwFlags
);
Remember to test for success or failure! Most of the parameters are just what you'd expect:

[font="Courier New"][color="#000080"]DWORD dwWidth, dwHeight[/color][/font]: The dimensions of the new display mode, in pixels.

[font="Courier New"][color="#000080"]DWORD dwBPP[/color][/font]: The color depth of the new display mode, in bits per pixel. This can be set to 8, 16, 24, or 32. be warned, most video cards do not support 24-bit color.

[font="Courier New"][color="#000080"]DWORD dwRefreshRate[/color][/font]: This is used to set the refresh rate for the screen, but you should probably just set it to 0, which indicates that the default for your display mode should be used.

[font="Courier New"][color="#000080"]DWORD dwFlags[/color][/font]: Sorry, I haven't got a big list for you this time. The only valid flag is [font="Courier New"][color="#000080"]DDSDM_STANDARDVGAMODE[/color][/font], which sets screen mode [font="Courier New"][color="#000080"]0x13[/color][/font] (the DOS coder's best friend!) instead of Mode X 320x200x8 mode. If you're using something other than 320x200x8, which you almost certainly will be, set this to 0.

That's all it takes to set the resolution to what you want it to be. Keep in mind that the video card may not support every resolution you want to create. 640x480, 800x600, 1024x876, etc. are pretty standard, but if you ask for something like 542x336, you'll almost certainly get an error. Some devices will build some odd display modes, but the idea is to be compatible with as many machines as possible, right? Let's move on.


[size="5"]Creating Surfaces

It's time for something that requires a little more than just a function call! Creating surfaces isn't too tough. Actually, it is accomplished with a single function call, but first you need to fill out a structure that describes the surface you want to create. Before I show you this, I just want to say that you don't have to fill out the whole thing, so never fear. :) Here it is, the [font="Courier New"][color="#000080"]DDSURFACEDESC2[/color][/font]:

typedef struct _DDSURFACEDESC2 {
DWORD dwSize;
DWORD dwFlags;
DWORD dwHeight;
DWORD dwWidth;
union
{
LONG lPitch;
DWORD dwLinearSize;
} DUMMYUNIONNAMEN(1);
DWORD dwBackBufferCount;
union
{
DWORD dwMipMapCount;
DWORD dwRefreshRate;
} DUMMYUNIONNAMEN(2);
DWORD dwAlphaBitDepth;
DWORD dwReserved;
LPVOID lpSurface;
DDCOLORKEY ddckCKDestOverlay;
DDCOLORKEY ddckCKDestBlt;
DDCOLORKEY ddckCKSrcOverlay;
DDCOLORKEY ddckCKSrcBlt;
DDPIXELFORMAT ddpfPixelFormat;
DDSCAPS2 ddsCaps;
DWORD dwTextureStage;
} DDSURFACEDESC2, FAR *LPDDSURFACEDESC2;
DirectX has lots of big structures. This one is pretty big, and what's worse, it's got all sorts of structures nested inside it! Anyway, let's take a look at this monster and see what it's got to offer. I'm only going to cover the important ones, so I don't grow old writing this.

[font="Courier New"][color="#000080"]DWORD dwSize[/color][/font]: Any DirectX structure has a dwSize member, which must be set to the size of the structure. It's there so that functions receiving a pointer to one of these structures can determine its size.

[font="Courier New"][color="#000080"]DWORD dwFlags[/color][/font]: Great, more flags. :) These flags are used to tell a receiving function which fields are important. Any field containing valid information must have its corresponding flag specified in [font="Courier New"][color="#000080"]dwFlags[/color][/font], and they can all be combined with | as usual. Here's the list:

DDSD_ALLIndicates that all input fields are valid.DDSD_ALPHABITDEPTHIndicates that the dwAlphaBitDepth member is valid.DDSD_BACKBUFFERCOUNTIndicates that the dwBackBufferCount member is valid.DDSD_CAPSIndicates that the ddsCaps member is valid.DDSD_CKDESTBLTIndicates that the ddckDestBlt member is valid.DDSD_CKDESTOVERLAYIndicates that the ddckDestOverlay member is valid.DDSD_CKSRCBLTIndicates that the ddckSrcBlt member is valid.DDSD_CKSRCOVERLAYIndicates that the ddckSrcOverlay member is valid.DDSD_HEIGHTIndicates that the dwHeight member is valid.DDSD_LINEARSIZEIndicates that the dwLinearSize member is valid.DDSD_LPSURFACEIndicates that the lpSurface member is valid.DDSD_MIPMAPCOUNTIndicates that the dwMipMapCount member is valid.DDSD_PITCHIndicates that the lPitch member is valid.DDSD_PIXELFORMATIndicates that the ddpfPixelFormat member is valid.DDSD_REFRESHRATEIndicates that the dwRefreshRate member is valid.DDSD_TEXTURESTAGEIndicates that the dwTextureStage member is valid.DDSD_WIDTHIndicates that the dwWidth member is valid.[font="Courier New"][color="#000080"]DWORD dwHeight, dwWidth[/color][/font]: These are the dimensions of the surface in pixels.

[font="Courier New"][color="#000080"]LONG lPitch[/color][/font]: This one needs to be explained a bit. The [font="Courier New"][color="#000080"]lPitch[/color][/font] member represents the number of bytes in each display line. You'd think this would be obvious. For example, in 640x480x16, there are 640 pixels in each line, and each one requires 2 bytes for color information, so the pitch (also called the "stride") should be 1280 bytes, right? Well, on some video cards, it will be greater than 1280. The extra memory on each line doesn't hold any graphical data, but sometimes it's there because the video card can't create a perfectly linear memory mode for the display mode. This will happen on a very small percentage of video cards, but you need to take it into account.

[font="Courier New"][color="#000080"]LPVOID lpSurface[/color][/font]: This is a pointer to the memory represented by the surface. No matter what display mode you're using, DirectDraw creates a linear addressing mode you can use to manipulate the pixels of the surface. In order to do this, you must lock the surface... but we're getting ahead of ourselves!

[font="Courier New"][color="#000080"]DWORD dwBackBufferCount[/color][/font]: This is the number of back buffers that will be chained to the primary surface, either for double- or triple-buffering, or for a page flipping chain. Again, more on this later.

[font="Courier New"][color="#000080"]DWORD ddckDestBlt, ddckSrcBlt[/color][/font]: These are color keys, which control which color pixels can be written to, or written, respectively. This is used to set transparent colors in bitmaps, as we'll see in a future article.

[font="Courier New"][color="#000080"]DDPIXELFORMAT ddpfPixelFormat[/color][/font]: This structure contains a number of descriptors for the pixel format of the display mode. We'll be covering this next time as well, so I won't show you this giant structure right now.

[font="Courier New"][color="#000080"]DDSCAPS2 ddsCaps[/color][/font]: This is the last important one, and it's another structure full of control flags. Thankfully, the structure a small one, and only one of its members is important right now. Take a look:

typedef struct _DDSCAPS2{
DWORD dwCaps;
DWORD dwCaps2;
DWORD dwCaps3;
DWORD dwCaps4;
} DDSCAPS2, FAR* LPDDSCAPS2;
The only important one is [font="Courier New"][color="#000080"]dwCaps[/color][/font]. The third and fourth members aren't even used at all; they're just there for future expansion. Anyway, [font="Courier New"][color="#000080"]dwCaps[/color][/font] can take the following values, logically combined with the | operator. This is only a partial list, leaving out some that are advanced or not currently implemented.

DDSCAPS_BACKBUFFERIndicates that this surface is a back buffer on a surface flipping structure.DDSCAPS_COMPLEXIndicates a complex surface, which consists of a primary surfaces and one or more attached surfaces, usually for page flipping.DDSCAPS_FLIPIndicates that this surface is part of a surface flipping structure. The result is that a front buffer along with one or more back buffers are created.DDSCAPS_FRONTBUFFERIndicates that this surface is the front buffer on a surface flipping structure.DDSCAPS_LOCALVIDMEMIndicates that this surface exists in true, local video memory rather than non-local video memory. If this flag is specified then DDSCAPS_VIDEOMEMORY must be specified as well. This cannot be used with the DDSCAPS_NONLOCALVIDMEM flag. DDSCAPS_MODEXIndicates that this surface is a 320x200 or 320x240 Mode X surface.DDSCAPS_NONLOCALVIDMEMIndicates that this surface exists in nonlocal video memory rather than true, local video memory. If this flag is specified, then DDSCAPS_VIDEOMEMORY flag must be specified as well. This cannot be used with the DDSCAPS_LOCALVIDMEM flag.DDSCAPS_OFFSCREENPLAINIndicates that this is any plain offscreen surface that is not a texture, back buffer, etc.DDSCAPS_OWNDCIndicates that this surface will have a device context association for a long period.DDSCAPS_PRIMARYSURFACEIndicates that this is the primary surface.DDSCAPS_STANDARDVGAMODEIndicates that this surface is a standard VGA surface rather than a Mode X surface. Cannot be used with the DDSCAPS_MODEX flag.DDSCAPS_SYSTEMMEMORYIndicates that this surface exists in system memory.DDSCAPS_TEXTUREIndicates that this surface can be used as a 3D texture, though it is not necessary to use it that way.DDSCAPS_VIDEOMEMORYIndicates that this surface exists in display memory.All right, we're finally done looking at structures. Now we're just about ready to create a surface. The first step is to fill out a [font="Courier New"][color="#000080"]DDSURFACEDESC2[/color][/font] structure. Microsoft recommends that before using a structure which you won't be filling out completely, you should zero it out to initialize it. To this end, I usually use a macro something like this:

#define INIT_DXSTRUCT(dxs) { ZeroMemory(&dxs, sizeof(dxs)); dds.dwSize = sizeof(dxs); }
This can be used for any DirectX structure, since they all have the [font="Courier New"][color="#000080"]dwSize[/color][/font] member, so it's pretty convenient. If you've never seen [font="Courier New"][color="#000080"]ZeroMemory()[/color][/font] before, it's just a macro that expands into a [font="Courier New"][color="#000080"]memset()[/color][/font] call. It's [font="Courier New"][color="#000080"]#defined[/color][/font] in the Windows headers, so you don't need to [font="Courier New"][color="#000080"]#include[/color][/font] anything new to use it.

After initializing the structure, the minimum you'll need to fill out depends on the surface. For primary surfaces, you'll just need [font="Courier New"][color="#000080"]ddsCaps[/color][/font] and [font="Courier New"][color="#000080"]dwBackBufferCount[/color][/font]. For offscreen buffers, you'll also need [font="Courier New"][color="#000080"]dwHeight[/color][/font] and [font="Courier New"][color="#000080"]dwWidth[/color][/font], but not [font="Courier New"][color="#000080"]dwBackBufferCount[/color][/font]. For some surfaces you may also want to use the color keys, but we're not that far yet. After you've filled out the structure, you make a call to [font="Courier New"][color="#000080"]IDirectDraw7::CreateSurface()[/color][/font], which looks like this:

HRESULT CreateSurface(
LPDDSURFACEDESC2 lpDDSurfaceDesc,
LPDIRECTDRAWSURFACE7 FAR *lplpDDSurface,
IUnknown FAR *pUnkOuter
);
The parameters, you can probably figure out. But here they are, since we're just getting used to all this crazy DirectX stuff:

[font="Courier New"][color="#000080"]LPDDSURFACEDESC2 lpDDSurfaceDesc[/color][/font]: This is a pointer to the [font="Courier New"][color="#000080"]DDSURFACEDESC2[/color][/font] you'll need to fill out to describe the surface.

[font="Courier New"][color="#000080"]LPDIRECTDRAWSURFACE7 FAR *lplpDDSurface[/color][/font]: Since a surface is defined by an interface pointer, you need to create a variable of type [font="Courier New"][color="#000080"]LPDIRECTDRAWSURFACE7[/color][/font] to hold the pointer, and then pass its address here.

[font="Courier New"][color="#000080"]IUnknown FAR *pUnkOuter[/color][/font]: Starting to see a pattern? Whenever you see something called [font="Courier New"][color="#000080"]pUnkOuter[/color][/font], it's for advanced COM stuff we don't want to mess around with. :) Set it to [font="Courier New"][color="#000080"]NULL[/color][/font].

Let's run through an example. Suppose that for our program, we want a primary surface with one back buffer attached, and one offscreen buffer to use for bitmap storage. Assuming we already have our [font="Courier New"][color="#000080"]IDirectDraw7[/color][/font] interface pointer, the following code would create the primary surface:

DDSURFACEDESC2 ddsd; // surface description structure
LPDIRECTDRAWSURFACE7 lpddsPrimary = NULL; // primary surface

// set up primary drawing surface
INIT_DXSTRUCT(ddsd); // initialize ddsd
ddsd.dwFlags = DDSD_CAPS | DDSD_BACKBUFFERCOUNT; // valid flags
ddsd.dwBackBufferCount = 1; // one back buffer
ddsd.ddsCaps.dwCaps = DDSCAPS_PRIMARYSURFACE | // primary surface
DDSCAPS_COMPLEX | // back buffer is chained
DDSCAPS_FLIP | // allow page flipping
DDSCAPS_VIDEOMEMORY; // create in video memory

// create primary surface
if (FAILED(lpdd7->CreateSurface(&ddsd, &lpddsPrimary, NULL)))
{
// error-handling code here
}
Now, when you specify the [font="Courier New"][color="#000080"]DDSCAPS_COMPLEX[/color][/font] flag, the call to [font="Courier New"][color="#000080"]CreateSurface()[/color][/font] actually creates the front buffer along with any back buffers it may have. Since we specified that there was to be one back buffer, all we need to do is get the pointer to it. This is done by calling [font="Courier New"][color="#000080"]IDirectDrawSurface7::GetAttachedSurface()[/color][/font]:

HRESULT GetAttachedSurface(
LPDDSCAPS2 lpDDSCaps,
LPDIRECTDRAWSURFACE7 FAR *lplpDDAttachedSurface
);
The parameters are pretty easy to deal with:

[font="Courier New"][color="#000080"]LPDDSCAPS2 lpDDSCaps[/color][/font]: A pointer to a [font="Courier New"][color="#000080"]DDSCAPS2[/color][/font] structure which describes the attached surface. You can use the corresponding member of our [font="Courier New"][color="#000080"]DDSURFACEDESC2[/color][/font] structure.

[font="Courier New"][color="#000080"]LPDIRECTDRAWSURFACE7 FAR *lplpDDAttachedSurface[/color][/font]: This is the address of the interface pointer that will represent the attached surface. Simply declare a pointer and pass its address.

With that relatively painless function, we can get the back buffer which was created along with the primary surface. The following code does the job:

LPDIRECTDRAWSURFACE7 lpddsBack = NULL; // back buffer

// get the attached surface
ddsd.ddsCaps.dwCaps = DDSCAPS_BACKBUFFER;
if (FAILED(lpddsPrimary->GetAttachedSurface(&ddsd.ddsCaps, &lpddsBack)))

{
// error-handling code here
}
Are you starting to get the hang of this stuff? If you're still having trouble remembering all these steps, just give it time. And nobody remembers every member of every structure there is, so don't worry about those huge lists of members and flags. That's why you have your MSDN Library CD. :) Anyway, the last step is to create the offscreen buffer. Suppose we wanted it to be 400 pixels wide and 300 pixels high. We would go about creating it like this:
Cancel Save
0 Likes 0 Comments

Comments

Nobody has left a comment. You can be the first!
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!

Part 4 introduces the structure of DirectX, the Component Object Model (COM), and the fundamentals of DirectDraw.

Advertisement
Advertisement