JavaScript EditorFree JavaScript Editor     Ajax Editor 



Main Page
  Previous Section Next Section

Working with DirectX COM Objects

Now that you have an idea what DirectX is and how COM works, let's take a closer look at how they actually work together. Like I said, there are a number of COM objects that make up DirectX. These COM objects are contained within your system as DLLs when you load the run-time version of DirectX. When you run a third-party DirectX game, what happens is that one or more of these DLLs are loaded by the DirectX application, and then interfaces are requested and the methods (functions) of the interfaces are used to get the work done. That's the run-time side of things.

The compile-time angle is a little different. The designers of DirectX knew that they were dealing with us game programmers, and assumed that most of us hate Windows programming—very true. Alas, they knew that they better keep the COM stuff to a minimum, or else game programmers would really hate using DirectX. Thus, 90% of the DirectX COM objects are wrapped in nice little function calls that take care of the COM stuff. So, you don't have to call CoCreateInstance(), do COM initialization, and stuff like that. However, you may have to query for a new interface with QueryInterface(), but we'll get to that in a bit. The point is, DirectX really tries to hide the tedium of working with COM from you so you can work with the core functionality of DirectX.

With all that said, to compile a DirectX program, you must include a number of import libraries that have the COM wrappers within them so you can make calls to DirectX using those wrapper functions to create the COM objects. For the most part, the libraries you need are

DDRAW.LIB
DSOUND.LIB
DINPUT.LIB
DINPUT8.LIB
DSETUP.LIB
DPLAYX.LIB
D3DIM.LIB
D3DRM.LIB

But remember, these libraries don't contain the COM objects themselves. These are only wrapper libraries and hooks that make calls to load the DirectX DLLs themselves, which are the COM objects. Finally, when you do call one of the DirectX COM objects, the result is usually just an interface pointer. This is where are the action occurs. Just like in the example of DEMO5_1.CPP, once you have the interface pointer, you're free to make function calls—or more correctly in C++ speak, method calls. However, if you're a C programmer, take a quick look at the next section if you feel uncomfortable with function pointers. If you're a C++ programmer, you can skip ahead to the next section if you want.

COM and Function Pointers

Once you have created a COM object and retrieved an interface pointer, what you really have is a VTABLE (Virtual Function Table) pointer. Take a look at Figure 5.7 to see this graphically. Virtual functions are used so that you can code with function calls that are not bound until run-time. This is the key to COM and virtual functions. In essence, C++ has this built in, but you can do the same thing with C by using straight function pointers.

Figure 5.7. Virtual Function Table architecture.

graphics/05fig07.gif

A function pointer is a type of pointer used to make calls to a function. But instead of the function being hard-bound to some code, you can move it around as long as the prototype of the function pointer is the same as the function(s) you point it to. For example, say that you want to write a graphics driver function to plot a pixel on the screen. But also suppose that you have dozens of different video cards to support and they all work differently, as shown in Figure 5.8.

Figure 5.8. Software design needed to support different video cards.

graphics/05fig08.gif

You want to call the plot pixel function the same way for all these video cards, but the internal code is different depending on what card is plugged in. Here's a typical C programmer's solution:

int SetPixel(int x, int y, int color, int card)
{
// what video card do we have?
switch(card)
      {
      case ATI:    { /* hardware specific code */ } break;
      case VOODOO: { /* hardware specific code */ } break;
      case SIII:   { /* hardware specific code */ } break;
      .
      .
      .
      default:     { /* standard VGA code */  } break;

      } // end switch

// return success
return(1);

} // end SetPixel

Do you see the problem with this? First, the switch statement sucks. It's slow, long, prone to errors, and you might break the function while adding support for another card. A better solution for straight C is to use function pointers like this:

// function pointer declaration, weird huh?
int (* SetPixel)(int x, int y, int color);
// now here's all our set pixel functions

int SetPixel_ATI(int x, int y, int color)
{
// code for ATI

} // end SetPixel_ATI

///////////////////////////////////////////////////////////

int SetPixel_VOODOO(int x, int y, int color)
{
// code for VOODOO

} // end SetPixel_VOODOO

///////////////////////////////////////////////////////////

int SetPixel_SIII(int x, int y, int color)
{
// code for SIII

} // end SetPixel_SIII

Now you're ready to rock. When the system starts up, it checks what kind of card is installed and then, once and only once, sets the generic function pointer to point to the correct card's function. For example, if you wanted SetPixel() to point to the ATI version, you would code it like this:

// assigning a function pointer
SetPixel = SetPixel_ATI;

Isn't that easy? Figure 5.9 shows what this looks like graphically.

Figure 5.9. Using function pointers to enable different code blocks.

graphics/05fig09.gif

Notice that SetPixel() is, in a way, an alias for SetPixel_ATI(). This is the key to function pointers. Now, to call SetPixel() you make a normal call, but instead of calling the empty SetPixel(), the call really calls SetPixel_ATI():

// this really calls SetPixel_ATI(10,20,4);
SetPixel(10,20,4);

The point is that your code always looks the same, but it does different things based on how you assign the function pointer. This is such a cool technology that much of C++ and virtual functions are based on it. That's all virtual functions really are—late binding of function pointers, but nicely built into the language and then built up as you've done here.

With that in mind, let's see how you would finish your generic video driver link-up… All you have to do is test to see which card is installed, set the SetPixel() function pointer once to the proper SetPixel*() function, and that's it. Take a look:

int SetCard(int card)
{
// assign the function pointer based on the card
switch(card)
      {
      case ATI:
           {
           SetPixel = SetPixel_ATI;

           } break;

      case VOODOO:
           {
           SetPixel = SetPixel_VOODOO;
           } break;

      case SIII:
           {
           SetPixel = SetPixel_SIII;
           } break;

      default: break;

      } // end switch

} // end SetCard

At the beginning of your code, you would make a call to the set up function like this:

SetCard(card);

And from then on, you're good to go. This is how function pointers and virtual functions are used in C++, so now let's see how these techniques are used with DirectX.

Creating and Using DirectX Interfaces

At this point, I think you understand that COM objects are collections of interfaces, which are simply function pointers (and more specifically, VTABLEs). Hence, all you need to do to work with a DirectX COM object is create it, retrieve an interface pointer, and then make calls to the interface using the proper syntax. As an example, I'll use the main DirectDraw interface to show how this is done.

First off, you need three things to experiment with DirectDraw:

  • The DirectDraw run-time COM object(s) and DLLs must be loaded and registered. This is what the DirectX installer does.

  • You must include the DDRAW.LIB import library in your Win32 programs so that the wrapper functions you call are linked in.

  • You need to include DDRAW.H in your program so the compiler can see the header information, prototypes, and data types for DirectDraw.

With that in mind, here's the data type for a DirectDraw 1.0 interface pointer:

LPDIRECTDRAW lpdd = NULL;

and here is the interface pointer type for DirectDraw 4.0:

LPDIRECTDRAW4 lpdd = NULL;

and for DirectDraw 7.0:

LPDIRECTDRAW7 lpdd = NULL;

And for 8.0 there isn't any!

Now, to create a DirectDraw COM object and retrieve an interface pointer to the DirectDraw object (which represents the video card), all you need to do is use the wrapper function DirectDrawCreate() like this:

DirectDrawCreate(NULL, &lpdd, NULL);

This will return the basic DirectDraw interface 1.0. In Chapter 6, "First Contact: DirectDraw," I go into the parameters in detail. But for now, just be aware that this call creates a DirectDraw object and assigns the interface pointer to lpdd.

Now you're in business and can make calls to DirectDraw. But wait a minute! You don't know the methods or functions that are available—that's why you're reading this book <BG>. As an example, here's how you would set the video mode to 640x480 with 256 colors:

lpdd->SetVideoMode(640, 480, 256);

Is that simple or what? About the only extra work is the pointer dereference from the DirectDraw interface pointer lpdd—that's it. Of course, what's really happening is a lookup in the virtual table of the interface, but don't be concerned about that.

In essence, any call to DirectX takes the following form:

interface_pointer->method_name(parameter list);

Also, you can get any other interfaces that you might want to work with (for example, Direct3D) from the original DirectDraw interface by using QueryInterface(). Moreover, since there are multiple versions of DirectX floating around, a while ago Microsoft stopped writing wrapped functions to retrieve the latest interface for everything, so sometimes you have to manually retrieve the latest DirectX interface yourself with QueryInterface(). Let's take a look at that.

Querying for Interfaces

The weird thing about DirectX is that all the version numbers are out of sync. This is a bit of a problem, and definitely a cause for confusion. Here's the deal: When the first version of DirectX came out, the DirectDraw interface was named like this:

IDIRECTDRAW

Then, when DirectX 2.0 came out, DirectDraw was upgraded to version 2.0, so we had this:

IDIRECTDRAW
IDIRECTDRAW2

Now, at version 6.0, we have something like this:

IDIRECTDRAW
IDIRECTDRAW2
IDIRECTDRAW4

Then with version 7.0, we have something like this:

IDIRECTDRAW
IDIRECTDRAW2
IDIRECTDRAW4

IDIRECTDRAW7

And now with version 8.0 there is no support for DirectDraw, so you still only have IDIRECTDRAW7 as the latest interface —get it?

Wait a minute—what happened to interfaces 3 and 5? I have no idea, but this is the problem. Hence, the idea is that even though you're using DirectX 8.0, it doesn't mean that the interfaces are up to that version. Moreover, they can all be out of sync. DirectX 6.0 may have DirectDraw interfaces up to IDIRECTDRAW4, but DirectSound is only up to interface version 1.0, which is simply called IDIRECTSOUND. You can see the mess we're in! The moral of the story is that whenever you use a DirectX interface, you should make sure that you're using the latest version. If you're not sure, use the revision 1.0 interface pointer from the generic create function to get the latest version.

Here's an example of what I'm talking about: DirectDrawCreate() returns a revision 1.0 interface pointer, but DirectDraw is really up to IDIRECTDRAW7. So how do you take advantage of this new functionality?

There are two ways to do this: with low-level COM functions or with QueryInterfaced(). Let's use the latter. The process goes like this: First, you create the DirectDraw COM interface with a call to DirectDrawCreate(). This returns a boring IDIRECTDRAW interface pointer. Then, you make a call to QueryInterface() using this pointer and you retrieve it using the Interface ID (or GUID) for IDIRECTDRAW7. Here's an example:

LPDIRECTDRAW  lpdd;   // version 1.0
LPDIRECTDRAW7 lpdd7;  // version 7.0

// create version 1.0 DirectDraw object interface
DirectDrawCreate(NULL, &lpdd, NULL);

// now look in DDRAW.H header, find IDIRECTDRAW7 interface
// ID and use it to query for the interface
lpdd->QueryInterface(IID_IDirectDraw7, &lpdd7);

At this point, you have two interface pointers. But you don't need the pointer to IDIRECTDRAW, so you should release it:

// release, decrement reference count
lpdd->Release();

// set to NULL to be safe
lpdd = NULL;

Remember this? You should release an interface when you're done with it. Hence, when your program terminates, you would also release the IDIRECTDRAW7 interface like this:

// release, decrement reference count
lpdd7->Release();

// set to NULL to be safe
lpdd7 = NULL;

Ok, now that you see how to get one interface from another, there is a ray of light—in DirectX 7.0 Microsoft added a new DirectDrawCreateEx() function that actually returns the IDIRECTDRAW7 interface! Amazing huh? Then they killed DirectDraw in version 8.0, but who cares? We can still use the function:

HRESULT WINAPI DirectDrawCreateEx(
  GUID FAR *lpGUID,  // the GUID of the driver, NULL for active display
  LPVOID *lplpDD,    // receiver of the interface
  REFIID iid,       // the interface ID of the interface you are requesting
  IUnknown FAR *pUnkOuter  // advanced COM, NULL
);

This new function allows you to send the requested DirectDraw version in iid and the function will create the COM object for you, thus, we just call the function like this:

LPDIRECTDRAW7 lpdd;  // version 7.0

// create version 7.0 DirectDraw object interface
DirectDrawCreateEx(NULL, (void **)&lpdd, IID_IDirectDraw7, NULL);

Basically, the call to DirectDrawCreateEx() creates the requested interface directly, so you don't have to go thru the intermediary of DirectDraw 1.0. Well, that's all there is to using DirectX and COM. Of course, you haven't seen all the hundreds of functions that DirectX components have or all the interfaces—but you will <BG>.

      Previous Section Next Section
    



    JavaScript EditorAjax Editor     JavaScript Editor