Programming for DirectX 8 with lcc-win32

 

Using COM.. 1

DirectX COM Documentation conventions. 2

What is a COM object?. 2

Objects and Interfaces. 2

GUIDs. 3

HRESULT Values. 4

The Address of a Pointer 5

Creating a COM object 5

Using COM Interfaces. 7

Requesting Additional Interfaces. 7

Managing a COM Object's Lifetime. 8

Incrementing and Decrementing the Reference Count 8

Using macros to call DirectX methods. 9

The IUnknown interface. 9

Using Callback functions. 10

Implementing a Callback Function. 10

Version Checking. 11

Checking the operating System version. 15

Preparing for Compilation. 16

 

 

Most DirectX application development with lcc-win32 involves conventional programming techniques. However, there are several aspects of DirectX programming that may be unfamiliar to users. This section provides a brief overview of several specialized topics, along with some guidelines for compiling and debugging DirectX applications.

·                    Using COM

·                    Using Callback Functions

·                    Version Checking

·                    Compiling DirectX Samples and Other DirectX Applications

·                    Debugging DirectX Applications

 

Using COM

The Component Object Model (COM) is an object-oriented programming model used by numerous applications. Because the bulk of the DirectX run time is in the form of COM-compliant objects, all DirectX developers need to have at least a basic understanding of COM principles and programming techniques. However, although COM has a reputation for being difficult and complex, the COM programming required by most DirectX applications is straightforward.

There are two distinct types of COM programming:

·                    Using existing COM objects. This is not much more difficult than using C++ objects.

·                    Implementing your own COM objects. This can be a complicated and demanding task. Much of COM's reputation for complexity comes from this type of COM programming.

Most DirectX applications need to use only the COM objects provided by DirectX. They do not need to implement their own COM objects. In other words, most DirectX developers will need to concern themselves only with the first, and easiest, type of COM programming.

This section provides a brief introduction to using the COM objects provided by DirectX. It is primarily intended for novice COM programmers. For a more detailed discussion of how to use COM objects, or for information on how to implement your own COM objects, see Further Information.

 

·                    What is a COM Object?

·                    Creating a COM Object

·                    Using COM Interfaces

·                    Managing a COM Object's Lifetime

·                    Using C to Access COM Objects

·                    Using Macros to Call DirectX COM Methods

·                    DirectX COM Documentation Conventions

·                    IUnknown Interface

 

DirectX COM Documentation conventions

By convention, COM methods and interfaces are referred to in the documentation by their equivalent C++ class names. The Initialize method of the IDirectPlay8Peer interface is thus referred to as IDirectPlay8Peer::Initialize. The primary reason for this convention is that different interfaces may export a method with the same name but with entirely different functionality and syntax. For example, many interfaces have an Init or Initialize method, but the functionality and parameters may be quite different. Using the C++ class name is a convenient way to uniquely identify the method. Note that this is used here for easy of use only, you shouldn’t try to use this C++ syntax in code for lcc-win32.

 

What is a COM object?

COM objects are basically black boxes that can be used by applications to perform one or more tasks. They are most commonly implemented as DLLs. Like a conventional DLL, COM objects expose methods that your application can call to perform any of the supported tasks.

COM objects contain procedures (methods), and some of them contain data (properties). They expose an interface, i.e. a set of data and procedures, that can be called or set/unset by the calling application.

 

A COM object's public methods are grouped into one or more interfaces. To use a method, you must create the object and obtain the appropriate interface from the object. An interface typically contains a related set of methods that provide access to a particular feature of the object. For example, the IDirect3DCubeTexture8 interface contains methods that enable you to manipulate cube texture resources. Any methods that are not part of an interface are not accessible.

 

COM objects are not created in the same way as C structures. There are several ways to create a COM object, but all involve COM-specific techniques. The DirectX API includes a variety of helper functions and methods that simplify creating most of the DirectX objects.

 

You must use COM-specific techniques to control the lifetime of the object. You should call the AddRef method, at the beginning, and always call Release when you are done using an object.

 

COM objects do not need to be explicitly loaded. COM objects are typically contained in a DLL. However, you do not need to explicitly load the DLL or link to a static library in order to use a COM object. Each COM object has a unique registered identifier that is used to create the object. The COM runtime automatically loads the correct DLL.

 

Here is, to put a concrete example, a code snippet that will call the COM Internet Explorer object:

 

CLSID clsExcelApp;

CLSIDFromProgID(

L"InternetExplorer.Application", &clsExplorerApp);

 

The COM API CLSIDFromProgID will look in the registry and call the correct DLL for you.

 

COM is a binary specification. COM objects can be written in and accessed from a variety of languages. You don't need to know anything about the object's source code. For example, lcc-win32 applications routinely use COM objects that were written in C++.

 

Objects and Interfaces

 

It is important to understand the distinction between objects and interfaces. In casual usage, an object may sometimes be referred to by the name of its principle interface. However, strictly speaking, the two terms are not interchangeable.

 

 An object may expose any number of interfaces. For example, while all objects must expose the IUnknown interface, they normally expose at least one additional interface, and they might expose many. In order to use a particular method, you must not only create the object, you must also obtain the correct interface.

 

More than one object might expose the same interface. An interface is a group of methods that perform a specified set of operations. The interface definition specifies only the syntax of the methods and their general functionality. Any COM object that needs to support a particular set of operations can do so by exposing a suitable interface. Some interfaces are highly specialized and are exposed only by a single object. Others are useful in a variety of circumstances and are exposed by many objects. The extreme case is the IUnknown interface, which must be exposed by all COM objects.[1]

 

The COM standard requires that an interface definition must not change once it has been published. You cannot, for example, add a new method to an existing, published interface. You must instead create a new interface. While there are no restrictions on what methods must be in an interface, a common practice is to have the next-generation interface include all the of the old interface's methods, plus any new methods.

 

It is not unusual for an interface to have several generations. Typically, all generations perform essentially the same overall task, but they will be different in detail. Often, an object will expose every generation of interface. Doing so allows older applications to continue using the object's older interfaces, while newer applications can take advantage of the features of the newer interfaces.

 

Typically, a family of interfaces will all have the same name, plus an integer indicating the generation. For example, if the original interface were named IMyInterface, the next two generations would be called IMyInterface2 and IMyInterface3. DirectX typically labels successive generations of interfaces with the DirectX version number.

 

GUIDs

Globally Unique Identifiers (GUIDs) are a key part of the COM programming model. At its most basic, a GUID is a 128-bit structure. However, GUIDs are created in such as way as to guarantee that no two GUIDs are the same. COM uses GUIDS extensively for two primary purposes:

  1. To uniquely identify a particular COM object. A GUID that is assigned to a COM object is called a Class ID (CLSID). You use a CLSID when you want to create an instance of the associated COM object.
  2. To uniquely identify a particular COM interface. The GUID that is associated with a particular COM interface is called an Interface ID (IID). You use an IID when you request a particular interface from an object. An interface's IID will be the same, regardless of which object exposes the interface.

 

For convenience, documentation normally refers to objects and interfaces by a descriptive name such as IDirect3D8. In the context of the documentation, there is rarely any danger of confusion. However, strictly speaking, there is no guarantee that another object or interface does not have the same descriptive name. The only unambiguous way to refer to a particular object or interface is by its GUID.

 

Although GUIDs are structures, they are often expressed as an equivalent string. The general format of the string form of a GUID is "{VVVVVVVV-WWWW-XXXX-YYYY-ZZZZZZZZZZZZ}", where each letter corresponds to a hexadecimal integer. For example, the string form of the IID for the IDirect3D8 interface is:

{1DD9E8DA-1C77-4D40-B0CF-98FEFDFF9512}

 

Because the actual GUID is somewhat clumsy to use and easy to mistype, an equivalent name is usually provided as well. You can use this name instead of the actual structure when you call functions such as CoCreateInstance. The customary naming convention is to prepend either IID_ or CLSID_ to the descriptive name of the interface or object, respectively. For example, the name of the IDirect3D8 interface's IID is IID_IDirect3D8.

HRESULT Values

All COM methods return a 32-bit integer called an HRESULT. With most methods, the HRESULT is essentially a structure that contains two separate pieces of information:

  1. Whether the method succeeded or failed.
  2. More detailed information about the outcome of the operation supported by the method.

 

Some methods return HRESULT values only from the standard set defined in win.h. However, methods are free to return custom HRESULT values with more specialized information. These values are normally documented on the method's reference page.[2]

 

While HRESULT values are often used to return error information, you should not think of them as error codes. The fact that the bit that indicates success or failure is stored separately from the bits that contain the detailed information allows HRESULT values to have any number of success and failure codes. By convention, success codes are given names with an S_ prefix, and failure codes with an E_ prefix. For example, the two most commonly used codes are S_OK and E_FAIL, which indicate simple success or failure, respectively.

 

The fact that COM methods may return a variety of success or failure codes means that you have to be careful how you test the HRESULT value. For example, consider a hypothetical method with documented return values of S_OK if successful and E_FAIL if not. However, remember that the method may also return other failure or success codes. The following code fragment illustrates the danger of using a simple test. The hr value is the HRESULT that was returned by the method.

 

if(hr == E_FAIL)

  {

    //Handle the failure

  }

else

  {

    //Handle the success

  }

 

As long as the method returns only E_FAIL to indicate failure, this test will work properly. However, the method might also return an error value such as E_NOTIMPL or E_INVALIDARG. That value would be interpreted as a success, perhaps causing your application to fail.

 

If you need detailed information on the outcome of the method call, you will need to test each relevant HRESULT value. However, you may be interested only in whether the method succeeded or failed. A robust way to test whether an HRESULT value indicates success or failure is to pass the value to the one of the following macros, defined in Win.h.

  1. The SUCCEEDED macro returns TRUE for a success code and FALSE for a failure code.
  2. The FAILED macro returns TRUE for a failure code and FALSE for a success code.

 

You can fix the preceding code fragment by using the FAILED macro.

 

if(FAILED(hr))

  {

    //Handle the failure

  }

else

  {

    //Handle the success

  }

 

This code fragment properly treats E_NOTIMPL and E_INVALIDARG as failures.

 

Although most COM methods return structured HRESULT values, a small number use the HRESULT to return a simple integer. Implicitly, these methods are always successful. If you pass an HRESULT of this sort to the SUCCESS macro, the macro will always return TRUE. A commonly used example is the IUnknown::Release method. This method decrements an object's reference count by one and returns the current reference count. See Managing the Object's Lifetime for a discussion of reference counting.

 

The Address of a Pointer

If you look at a few COM method reference pages, you will probably run across something like the following:

 

 

HRESULT CreateDevice(

  ...,

  IDirect3DDevice8** ppReturnedDeviceInterface

);

 

While a normal pointer is quite familiar to any lcc-win32 developer, COM often uses an additional level of indirection. This second level of indirection is indicated by a "**" following the type declaration, and the variable name typically has a "pp" prefix. For the example given above, ppReturnedDeviceInterface parameter is typically referred to as the address of a pointer to an IDirect3DDevice8 interface.

 

You do not access a COM object's methods directly. Instead, you must obtain a pointer to an interface that exposes the method. To invoke the method, you use essentially the lpVtbl member of the interface structure, and then you dereference that pointer. For example, to invoke the IMyInterface::DoSomething method, you would use the following syntax.

 

IMyInterface *pMyIface;

...

pMyIface->lpVtbl->DoSomething(pMyIface, ...);

 

 

Note that this is slightly different to the documentation for C++. In C++ you would write:

 

IMyInterface *pMyIface;

...

pMyIface->DoSomething(...);

 

In C++, the compiler makes the lpVtbl indirection, and adds an invisible parameter to the method(s call. Since in C we surely do not WANT the compiler writing code behind our backs, we do that ourselves when we want it.

 

The need for a second level of indirection comes from the fact that you do not create interface pointers directly. You must call one of variety of methods, such as the CreateDevice method shown above. To use such a method to obtain an interface pointer, you declare a variable as a pointer to the desired interface, and pass the address of that variable to the method. In other words, you pass the method the address of a pointer. When the method returns, the variable will point to the requested interface, and you can use that pointer to call any of the interface's methods. See Obtaining and Using COM Interfaces for further discussion of how to use interface pointers.

 

Creating a COM object

There are several ways to create COM objects. The two most common ones used in DirectX programming are:

 

  1. Directly, by passing the object's CLSID to the CoCreateInstance function, as shown in the example above. The function will create an instance of the object, and it will return a pointer to an interface that you specify.
  2. Indirectly, by calling a DirectX method or function that creates the object for you. The method creates the object and returns an interface on the object. When you create an object this way, you usually cannot specify which interface should be returned.

 

Before you create any objects, COM must be initialized by calling the “CoInitialize” function. If you are creating objects indirectly, the object creation method will handle this task. If you need to create an object with “CoCreateInstance”, you must call CoInitialize explicitly. When you are finished, COM must be uninitialized by calling CoUninitialize. If you make a call to CoInitialize you must match it with a call to CoUninitialize. Typically, applications that need to explicitly initialize COM do so in their startup routine, and they uninitialize COM in their cleanup routine, or at the beginning and end of the WinMain procedure.

 

To create a new instance of a COM object with CoCreateInstance, you must have the object's CLSID. If this CLSID is in the header files, or otherwise publicly available, you will find it in the reference documentation or the appropriate header file.

 

The CoCreateInstance function has five parameters. For the COM objects you will be using with DirectX, you can normally set the parameters as follows

  1. rclsid. A reference to a CLSID. You can use a direct reference, since lcc-win32 supports this, or just a normal pointer. In both cases, the address of the CLSID will be passed to the function.
  2. pUnkOuter. Set this parameter to NULL. It is used only if you are aggregating objects.
  3. dwClsContext. Set this parameter to CLSCTX_INPROC_SERVER. This setting indicates that the object is implemented as a DLL and will run as part of your application's process.
  4. riid. Set this parameter to the IID of the interface that you would like to have returned. The function will create the object, and it will return the requested interface pointer in the ppv parameter.
  5. ppv. Set this parameter to the address of a pointer that will be set to the interface specified by riid when the function returns. This variable should be declared as a pointer to the requested interface, and the reference to the pointer in the parameter list should be cast as (LPVOID *).

For example, the following code fragment creates a new instance of the DirectPlay8 object, and it returns a pointer to the IDirectPlay8Peer interface in the g_pDP variable. If an error occurs, a message box is displayed, and the application terminates.

 

IDirectPlay8Peer*  g_pDP = NULL;

...

CoInitialize( NULL );

...

hr = CoCreateInstance(&CLSID_DirectPlay8,

 NULL, CLSCTX_INPROC_SERVER,

          &IID_IDirectPlay8Peer, (LPVOID*) &g_pDP );

 

if( FAILED( hr ) )

  {

    MessageBox( NULL,

TEXT("Failed Creating IDirectPlay8Peer. "),

     TEXT("DirectPlay Sample"), MB_OK | MB_ICONERROR );

    return FALSE;

  }

 

Creating an object indirectly is usually much simpler. You pass the object creation method the address of an interface pointer. The method then creates the object and returns an interface pointer. When you create an object indirectly, you typically cannot choose which interface the method will return. However, you can often specify a variety of things about how the object should be created. For example, the following code fragment calls the IDirect3D8::CreateDevice method discussed earlier to create a device object to represent a display adapter. It returns a pointer to the object's IDirect3DDevice8 interface. The first four parameters provide a variety of information needed to create the object, and the fifth parameter receives the interface pointer. See the reference documentation for details.

 

IDirect3DDevice8 *g_pd3dDevice = NULL;

...

if(FAILED(g_pD3D->lpVtbl->CreateDevice(g_pD3D,

D3DADAPTER_DEFAULT,

     3DDEVTYPE_HAL,

hWnd,                            D3DCREATE_SOFTWARE_VERTEXPROCESSING,

     &d3dpp,

     &g_pd3dDevice )))

  return E_FAIL;

 

Using COM Interfaces

 

When the object is created, the creation method returns an interface pointer. You can then use that pointer to access any of the interface's methods. The syntax is identical to that used with a pointer to a C++ method. The following code fragment extends the example given in the previous section. After creating the DirectPlay8 object, the example uses the IDirectPlay8Peer interface pointer returned by CoCreateInstance to initialize the object by calling the IDirectPlay8Peer::Initialize method. Error correction code is omitted for clarity.

 

IDirectPlay8Peer*  g_pDP = NULL;

...

CoInitialize( NULL );

...

hr = CoCreateInstance(CLSID_DirectPlay8,NULL,

CLSCTX_INPROC_SERVER,

    IID_IDirectPlay8Peer, (LPVOID*) &g_pDP );

hr = g_pDP->lpVtbl->Initialize(g_pDP, NULL,

DirectPlayMessageHandler, 0 );

 

Requesting Additional Interfaces

In many cases, the interface pointer that you receive from the creation method may be the only one that you need. In fact, it is not uncommon for an object to export only one interface other than IUnknown. However, many objects export multiple interfaces, and you may need pointers to several of them. If you need more interfaces than the one returned by the creation method, there is no need to create a new object. Instead, you request another interface pointer by using the object's IUnknown::QueryInterface method.

 

If you create your object with CoCreateInstance, you can request an IUnknown interface pointer, and then call IUnknown::QueryInterface to request every interface you need. However, this approach is inconvenient if you need only a single interface, and it doesn't work at all if you use an object creation method that does not allow you to specify which interface pointer should be returned. In practice, you usually don't need to obtain an explicit IUnknown pointer because all COM interfaces inherit from or extend the IUnknown interface.

 

Extending an interface is similar to inheriting in object oriented programming. The child interface exposes all of the parent interface's methods, plus one or more of its own. In fact, you will often see "inherits from" used instead of "extends". What you need to remember is that the inheritance is internal to the object. You can use the child interface to call any of the child's or the parent's methods.

Because all interfaces are children of IUnknown you can use any of the interface pointers you already have for the object to call QueryInterface. When you do so, you must provide the IID of the interface you are requesting and the address of a pointer that will contain the interface pointer when the method returns.

 

For example, the following code fragment calls IDirectSound8::CreateSoundBuffer to create a primary sound buffer object. This object exposes several interfaces. The CreateSoundBuffer method returns an IDirectSoundBuffer8 interface. The subsequent code then uses the IDirectSoundBuffer8 interface to call QueryInterface to request an IDirectSound3DListener8 interface.

IDirectSoundBuffer8* pDSBPrimary = NULL;

IDirectSound3DListener8* pDSListener;

...

if(FAILED(hr = g_pDS->lpVtbl->CreateSoundBuffer(g_pDS,

 &dsbd, &pDSBPrimary, NULL )))

  return hr;

 

if(FAILED(hr = pDSBPrimary->lpVtbl->QueryInterface(

gDSBPrimary,&IID_IDirectSound3DListener8,

    (LPVOID *)&pDSListener)))

  return hr;

 

Managing a COM Object's Lifetime

 

When an object is created, the system allocates the necessary memory resources. When an object is no longer needed, it should be destroyed. The system can use that memory for other purposes. COM does not enable you to create or destroy objects directly. The reason for this practice is that the same object may be used by more than one application. If one of those applications were to destroy the object, the other applications would probably fail. Instead COM uses a system of reference counting, to control an object's lifetime.

An object's reference count is the number of times one of its interfaces has been requested. Each time an interface is requested, the reference count is incremented. An application releases an interface when that interface is no longer needed, decrementing the reference count. As long as the reference count is greater than zero, the object remains in memory. When the reference count reaches zero, the object destroys itself. You don't need to know anything about the reference count of an object. As long as you obtain and release an object's interfaces properly, the object will have the appropriate lifetime.[3]

 

Incrementing and Decrementing the Reference Count

Whenever you obtain a new interface pointer, the reference count must be incremented by a call to IUnknown::AddRef. However, your application does not usually need to call this method. If you obtain an interface pointer by calling an object creation method, or by calling IUnknown::QueryInterface, the object will automatically increment the reference count. However, if you create an interface pointer in some other way, such as copying an existing pointer, you must explicitly call IUnknown::AddRef. Otherwise, when you release the original interface pointer, the object may be destroyed even though you may still need to use the copy of the pointer.

You must release all interface pointers, regardless of whether you or the object incremented the reference count. When you no longer need an interface pointer, call IUnknown::Release to decrement the reference count. A common practice is to initialize all interface pointers to NULL, and set them back to NULL when they are released. That allows you to test all interface pointers in your cleanup code. Those that are non-NULL are still active and must be released before you terminate the application.

The following code fragment extends the sample discussed in Requesting Additional Interfaces to illustrate how to handle reference counting.

IDirectSoundBuffer8* pDSBPrimary = NULL;

IDirectSound3DListener8* pDSListener = NULL;

IDirectSound3DListener8* pDSListener2 = NULL;

...

//Create the object and obtain an additional interface.

//The object increments the reference count.

if(FAILED(hr = g_pDS->lpVtbl->CreateSoundBuffer(g_pDS,

 &dsbd, &pDSBPrimary, NULL )))

  return hr;

 

if(FAILED(hr=pDSBPrimary->lpVtbl->QueryInterface(

pDSBPrimary,&IID_IDirectSound3DListener8,

   (LPVOID *)&pDSListener)))

  return hr;

 

//Make a copy of the IDirectSound3DListener8 interface pointer.

//Call AddRef to increment the reference count and to ensure that

//the object is not destroyed prematurely

pDSListener2 = pDSListener;

pDSListener2->lpVtbl->AddRef(pDSListener2);

...

//Cleanup code. Check to see if the pointers are still active.

//If they are, call Release to release the interface.

if(pDSBPrimary != NULL)

{

  pDSBPrimary->lpVtbl->Release(pDSBPrimary);

  pDSBPrimary = NULL;

}

if(pDSListener != NULL)

{

  pDSListener->lpVtbl->Release(pDSListener);

  pDSListener = NULL;

}

if(pDSListener2 != NULL)

{

  pDSListener2->lpVtbl->Release(pDSListener2);

  pDSListener2 = NULL;

}

 

Using macros to call DirectX methods.

Many of the DirectX interfaces have macros defined for each method that make using the methods in your applications simpler.

 

For example, the following code fragment from the d3d8.h header file shows the definitions of the C macros for the IDirect3D8::GetAdapterIdentifier method.

 

#define IDirect3D8_GetAdapterIdentifier(p,a,b,c) \

(p)-lpVtbl->GetAdapterIdentifier(a,b,c)

...

 

To use one of these macros, you must first obtain a pointer to the associated interface. The first parameter of the macro must be set to that pointer. The remaining parameters map to the method's parameters. The macro's return value is the HRESULT value that is returned by the method. The following code fragment uses a macro to call the IDirect3D8::GetAdapterIdentifier method. pD3D is a pointer to an IDirect3D8 interface.

 

hr = IDirect3D8_GetAdapterIdentifier(pD3D,

                                     Adapter,

                                     dwFlags,

                                     pIdentifier);

 

The IUnknown interface

All COM objects support an interface called IUnknown. This interface provides DirectX with control of the object's lifetime and the ability to retrieve other interfaces implemented by the object. IUnknown has three methods.

  1. AddRef increments the object's reference count by 1 when an interface or another application binds itself to the object.
  2. QueryInterface queries the object about the features that it supports by requesting pointers to a specific interface.
  3. Release decrements the object's reference count by 1. When the count reaches 0, the object is deallocated.

 

The AddRef and Release methods maintain an object's reference count. For example, if you create a Direct3D object, the object's reference count is set to 1. Every time a function returns a pointer to an interface for that object, the function must call AddRef through that pointer to increment the reference count. Match each AddRef call with a call to Release. Before the pointer can be destroyed, you must call Release through that pointer. After an object's reference count reaches 0, the object is destroyed, and all interfaces to it become invalid.

The QueryInterface method determines whether an object supports a specific interface. If an object supports an interface, QueryInterface returns a pointer to that interface. You then can use the methods of that interface to communicate with the object. If QueryInterface successfully returns a pointer to an interface, it implicitly calls AddRef to increment the reference count, so your application must call Release to decrement the reference count before destroying the pointer to the interface.

 

Using Callback functions

A callback function is essentially an event handler that is implemented by an application and called by the system. Windows applications typically implement multiple callback functions, each one designed for a particular set of events. When an event occurs, the system notifies the application by calling the appropriate callback function. The callback function also usually has a parameter list that the system can use to pass the application more detailed information about the event. The most common example of a callback function is the window procedure. This function is used by the system to pass Windows messages to the applications that owns the window.

 

DirectX uses callback functions for a variety of purposes. For example, your system supports multiple devices. DirectInput represents each device by a device object, that contains the details of the device's capabilities. Your application will typically need to enumerate the available devices and to examine the device objects in order to handle user input properly. To do this enumeration, you must implement a DIEnumDeviceObjectsCallback callback function.

 

You start the enumeration process by calling IDirectInputDevice8::EnumObjects and passing the method a pointer to your DIEnumDeviceObjectsCallback callback function. The system will call this function once for each device, and it will pass in a DIDEVICEOBJECTINSTANCE structure containing information about the device's capabilities. After your callback function has processed the information, it can return DIENUM_CONTINUE to request the next device object, or DIENUM_STOP to stop the enumeration.

 

DirectX uses a number of other callback functions for a variety of purposes. For details, see the documentation for the particular DirectX component.

 

Implementing a Callback Function

Because callback functions have different purposes and usage, they are documented in the appropriate Reference section in much the same way as a regular function. However, a callback function reference is essentially a template that describes how to implement the function, not a normal API reference. The callback function reference provides the following information:

  1. How the function is used
  2. The function's syntax
  3. An explanation of the information that will be contained by each parameter
  4. An explanation of the possible return values

 

You should declare the function as a CALLBACK or WINAPI type. Either type is acceptable. You can use any function name that you wish. The name used in the documentation is simply a convenient label for a particular callback function.

 

Implement the function according to the description in the reference. The implementation details will depend on the particular function and the requirements of your application. See the sample code for some examples of how to implement various callback functions.

 

Pass a pointer to the function to the appropriate DirectX component. The DirectX component can then use the function pointer to call the function. The way in which you pass this pointer varies from function to function, so you should see the particular function's reference for details.

 

The following code fragment is borrowed from the DirectInput Joystick sample. It sketches out the essential elements of a DIEnumDeviceObjectsCallback implementation that is used to enumerate the axes of a joystick.

 

//The function declaration

BOOL CALLBACK EnumAxesCallback(

 const DIDEVICEOBJECTINSTANCE* pdidoi,

     VOID* pContext );

...

 

//Pass the function pointer to DirectInput by calling the

//IDirectInputDevice8::EnumObjects method

if ( FAILED( hr = g_pJoystick->lpVtbl->EnumObjects(

g_pJoystick,

EnumAxesCallback,

          (VOID*)hDlg,

          DIDFT_AXIS )));

...

 

//The function implementation

BOOL CALLBACK EnumAxesCallback(

 const DIDEVICEOBJECTINSTANCE* pdidoi,

                                VOID* pContext )

{

//Process the information passed in through the two parameters

//Return DIENUM_CONTINUE to request the next device object

//Return DIENUM_STOP to stop the enumeration

}

 

Version Checking

Applications sometimes need to know which version of DirectX is currently available on the system. For example, if an older version of DirectX is on the system, your application may need to scale itself to the capabilities of that version or install the most recent version.

 

There is no direct way to obtain the DirectX version number. However, each version has a characteristic set of objects and interfaces. Because any version of DirectX supports all previous versions, this set of interfaces and objects will be supported by the version in which they are introduced and all subsequent versions. Thus, the preferred way to determine whether your desired version is available is to test for its characteristic objects or interfaces. As long as those are present, your application will work normally even though you might be using a more recent version of DirectX.

 

For example, suppose you need version 6.1 support. The DirectMusic object (CLSID_DirectMusic) was introduced in DirectX version 6.1. You can test for the presence of the DirectMusic object by attempting to create it with CoCreateInstance. If you are successful, you have version 6.1 or later, and you will be able to use all the DirectX 6.1 capabilities.

 

Here is a function that makes all the tests for you. You will find the complete application in the samples distributed with lcc-win32, in the directory lcc\examples\directx\dxver

 

//-----------------------------------------------------------------------------

// Name: GetDXVersion()

// Desc: This function returns the DirectX version number as follows:

//          0x0000 = No DirectX installed

//          0x0100 = DirectX version 1 installed

//          0x0200 = DirectX 2 installed

//          0x0300 = DirectX 3 installed

//          0x0500 = At least DirectX 5 installed.

//          0x0600 = At least DirectX 6 installed.

//          0x0601 = At least DirectX 6.1 installed.

//          0x0700 = At least DirectX 7 installed.

//          0x0800 = At least DirectX 8 installed.

//

//       Please note that this code is intended as a general guideline. Your

//       app will probably be able to simply query for functionality (via

//       QueryInterface) for one or two components.

//

//       Please also note:

//          "if( dwDXVersion != 0x500 ) return FALSE;" is VERY BAD.

//          "if( dwDXVersion <  0x500 ) return FALSE;" is MUCH BETTER.

//       to ensure your app will run on future releases of DirectX.

//-----------------------------------------------------------------------------

DWORD GetDXVersion()

{

    DIRECTDRAWCREATE     DirectDrawCreate   = NULL;

    DIRECTDRAWCREATEEX   DirectDrawCreateEx = NULL;

    DIRECTINPUTCREATE    DirectInputCreate  = NULL;

    HINSTANCE            hDDrawDLL          = NULL;

    HINSTANCE            hDInputDLL         = NULL;

    HINSTANCE            hD3D8DLL           = NULL;

    LPDIRECTDRAW         pDDraw             = NULL;

    LPDIRECTDRAW2        pDDraw2            = NULL;

    LPDIRECTDRAWSURFACE  pSurf              = NULL;

    LPDIRECTDRAWSURFACE3 pSurf3             = NULL;

    LPDIRECTDRAWSURFACE4 pSurf4             = NULL;

    DWORD                dwDXVersion        = 0;

    HRESULT              hr;

 

    // First see if DDRAW.DLL even exists.

    hDDrawDLL = LoadLibrary( "DDRAW.DLL" );

    if( hDDrawDLL == NULL )

    {

        dwDXVersion = 0;

        return dwDXVersion;

    }

 

    // See if we can create the DirectDraw object.

    DirectDrawCreate =(DIRECTDRAWCREATE)GetProcAddress(hDDrawDLL,"DirectDrawCreate" );

    if( DirectDrawCreate == NULL )

    {

        dwDXVersion = 0;

        FreeLibrary( hDDrawDLL );

        OutputDebugString( "Couldn't LoadLibrary DDraw\r\n" );

        return dwDXVersion;

    }

 

    hr = DirectDrawCreate( NULL, &pDDraw, NULL );

    if( FAILED(hr) )

    {

        dwDXVersion = 0;

        FreeLibrary( hDDrawDLL );

        OutputDebugString( "Couldn't create DDraw\r\n" );

        return dwDXVersion;

    }

 

    // So DirectDraw exists.  We are at least DX1.

    dwDXVersion = 0x100;

 

    // Let's see if IID_IDirectDraw2 exists.

    hr = pDDraw->lpVtbl->QueryInterface(pDDraw,& IID_IDirectDraw2, (VOID**)&pDDraw2 );

    if( FAILED(hr) )

    {

        // No IDirectDraw2 exists... must be DX1

        pDDraw->lpVtbl->Release(pDDraw);

        FreeLibrary( hDDrawDLL );

        OutputDebugString( "Couldn't QI DDraw2\r\n" );

        return dwDXVersion;

    }

 

    // IDirectDraw2 exists. We must be at least DX2

    pDDraw2->lpVtbl->Release(pDDraw2);

    dwDXVersion = 0x200;

 

 

       //-------------------------------------------------------------------------

    // DirectX 3.0 Checks

       //-------------------------------------------------------------------------

 

    // DirectInput was added for DX3

    hDInputDLL = LoadLibrary( "DINPUT.DLL" );

    if( hDInputDLL == NULL )

    {

        // No DInput... must not be DX3

        OutputDebugString( "Couldn't LoadLibrary DInput\r\n" );

        pDDraw->lpVtbl->Release(pDDraw);

        return dwDXVersion;

    }

 

    DirectInputCreate = (DIRECTINPUTCREATE)GetProcAddress( hDInputDLL,

                                                        "DirectInputCreateA" );

    if( DirectInputCreate == NULL )

    {

        // No DInput... must be DX2

        FreeLibrary( hDInputDLL );

        FreeLibrary( hDDrawDLL );

        pDDraw->lpVtbl->Release(pDDraw);

        OutputDebugString( "Couldn't GetProcAddress DInputCreate\r\n" );

        return dwDXVersion;

    }

 

    // DirectInputCreate exists. We are at least DX3

    dwDXVersion = 0x300;

    FreeLibrary( hDInputDLL );

 

    // Can do checks for 3a vs 3b here

 

 

    //-------------------------------------------------------------------------

    // DirectX 5.0 Checks

    //-------------------------------------------------------------------------

 

    // We can tell if DX5 is present by checking for the existence of

    // IDirectDrawSurface3. First, we need a surface to QI off of.

    DDSURFACEDESC ddsd;

    ZeroMemory( &ddsd, sizeof(ddsd) );

    ddsd.dwSize         = sizeof(ddsd);

    ddsd.dwFlags        = DDSD_CAPS;

    ddsd.ddsCaps.dwCaps = DDSCAPS_PRIMARYSURFACE;

 

    hr = pDDraw->lpVtbl->SetCooperativeLevel(pDDraw, NULL, DDSCL_NORMAL );

    if( FAILED(hr) )

    {

        // Failure. This means DDraw isn't properly installed.

        pDDraw->lpVtbl->Release(pDDraw);

        FreeLibrary( hDDrawDLL );

        dwDXVersion = 0;

        OutputDebugString( "Couldn't Set coop level\r\n" );

        return dwDXVersion;

    }

 

    hr = pDDraw->lpVtbl->CreateSurface(pDDraw, &ddsd, &pSurf, NULL );

    if( FAILED(hr) )

    {

        // Failure. This means DDraw isn't properly installed.

        pDDraw->lpVtbl->Release(pDDraw);

        FreeLibrary( hDDrawDLL );

        dwDXVersion = 0;

        OutputDebugString( "Couldn't CreateSurface\r\n" );

        return dwDXVersion;

    }

 

    // Query for the IDirectDrawSurface3 interface

    if( FAILED( pSurf->lpVtbl->QueryInterface(pSurf,&IID_IDirectDrawSurface3,

                                       (VOID**)&pSurf3 ) ) )

    {

        pDDraw->lpVtbl->Release(pDDraw);

        FreeLibrary( hDDrawDLL );

        return dwDXVersion;

    }

 

    // QI for IDirectDrawSurface3 succeeded. We must be at least DX5

    dwDXVersion = 0x500;

 

 

    //-------------------------------------------------------------------------

    // DirectX 6.0 Checks

    //-------------------------------------------------------------------------

 

    // The IDirectDrawSurface4 interface was introduced with DX 6.0

    if( FAILED( pSurf->lpVtbl->QueryInterface(pSurf,& IID_IDirectDrawSurface4,

                                       (VOID**)&pSurf4 ) ) )

    {

        pDDraw->lpVtbl->Release(pDDraw);

        FreeLibrary( hDDrawDLL );

        return dwDXVersion;

    }

 

    // IDirectDrawSurface4 was create successfully. We must be at least DX6

    dwDXVersion = 0x600;

    pSurf->lpVtbl->Release(pSurf);

    pDDraw->lpVtbl->Release(pDDraw);

 

 

    //-------------------------------------------------------------------------

    // DirectX 6.1 Checks

    //-------------------------------------------------------------------------

 

    // Check for DMusic, which was introduced with DX6.1

    LPDIRECTMUSIC pDMusic = NULL;

    CoInitialize( NULL );

    hr = CoCreateInstance(&CLSID_DirectMusic, NULL, CLSCTX_INPROC_SERVER,

                           &IID_IDirectMusic, (VOID**)&pDMusic );

    if( FAILED(hr) )

    {

        OutputDebugString( "Couldn't create CLSID_DirectMusic\r\n" );

        FreeLibrary( hDDrawDLL );

        return dwDXVersion;

    }

 

    // DirectMusic was created successfully. We must be at least DX6.1

    dwDXVersion = 0x601;

    pDMusic->lpVtbl->Release(pDMusic);

    CoUninitialize();

   

 

    //-------------------------------------------------------------------------

    // DirectX 7.0 Checks

    //-------------------------------------------------------------------------

 

    // Check for DirectX 7 by creating a DDraw7 object

    LPDIRECTDRAW7 pDD7;

    DirectDrawCreateEx = (DIRECTDRAWCREATEEX)GetProcAddress( hDDrawDLL,

                                                       "DirectDrawCreateEx" );

    if( NULL == DirectDrawCreateEx )

    {

        FreeLibrary( hDDrawDLL );

        return dwDXVersion;

    }

 

    if( FAILED( DirectDrawCreateEx( NULL, (VOID**)&pDD7,& IID_IDirectDraw7,

                                    NULL ) ) )

    {

        FreeLibrary( hDDrawDLL );

        return dwDXVersion;

    }

 

    // DDraw7 was created successfully. We must be at least DX7.0

    dwDXVersion = 0x700;

    pDD7->lpVtbl->Release(pDD7);

 

 

    //-------------------------------------------------------------------------

    // DirectX 8.0 Checks

    //-------------------------------------------------------------------------

 

    // Simply see if D3D8.dll exists.

    hD3D8DLL = LoadLibrary( "D3D8.DLL" );

    if( hD3D8DLL == NULL )

    {

           FreeLibrary( hDDrawDLL );

        return dwDXVersion;

    }

 

    // D3D8.dll exists. We must be at least DX8.0

    dwDXVersion = 0x800;

 

 

    //-------------------------------------------------------------------------

    // End of checking for versions of DirectX

    //-------------------------------------------------------------------------

 

    // Close open libraries and return

    FreeLibrary( hDDrawDLL );

    FreeLibrary( hD3D8DLL );

   

    return dwDXVersion;

}

Checking the operating System version.

In addition to determining the DirectX version, applications may also need to know what operating system they are running on, and perhaps which Service Pack has been installed. The preferred way to check the operating system version is to use the Windows API function, GetVersionEx. This function returns an OSVERSIONINFO structure that contains a variety of information including:

   Whether the system is Microsoft Windows NT®-based or Windows 95/98/ME

   The major and minor version numbers

   The service pack number, for Windows NT-based systems

The general procedure is to determine the earliest version of the operating system that your application is compliant with. If that version or later is installed, you can safely install and run your application.[4]

 

The following sample function illustrates how to use GetVersionEx to test the operating system version. If the installed version is identical to or more recent than the version specified in the parameter list, the function returns TRUE. You can then safely install and execute your application. Otherwise the function returns FALSE, indicating that your application will not run properly, if at all.

#include <windows.h>

#include <stdio.h>

#include <tchar.h>

 

BOOL bIsWindowsVersionOK(   DWORD dwWin9xMajor, DWORD dwWin9xMinor,

                            DWORD dwWinNTMajor, DWORD dwWinNTMinor, WORD wWinNTSPMajor )

{

    OSVERSIONINFO           osvi;

   

    // Initialize the OSVERSIONINFO structure.

    memset( &osvi, sizeof( osvi ) );

    osvi.dwOSVersionInfoSize = sizeof( osvi );

       

    GetVersionEx( &osvi );  // Assume this function succeeds.

 

    // Split code paths for NT and Win9x   

    if( osvi.dwPlatformId == VER_PLATFORM_WIN32_WINDOWS )

    {

        // Check the major version.

        if( osvi.dwMajorVersion > dwWin9xMajor )

            return TRUE;

        else if( osvi.dwMajorVersion == dwWin9xMajor )

        {

            // Check the minor version.

            if( osvi.dwMinorVersion >= dwWin9xMinor )

                return TRUE;

        }

    }

    else if( osvi.dwPlatformId == VER_PLATFORM_WIN32_NT )

    {

        // Check the major version.

        if( osvi.dwMajorVersion > dwWinNTMajor )

            return TRUE;

        else if( osvi.dwMajorVersion == dwWinNTMajor )

        {

            // Check the minor version.

            if( osvi.dwMinorVersion > dwWinNTMinor )

                return TRUE;

            else if( osvi.dwMinorVersion == dwWinNTMinor )

            {

                // Check the service pack.

                DWORD dwServicePack = 0;

 

                if( osvi.szCSDVersion )

                {

                    _stscanf(   osvi.szCSDVersion,

                                _T("Service Pack %d"),

                                &dwServicePack );

                }

                return ( dwServicePack >= wWinNTSPMajor );

            }

        }

    }

    return FALSE;

}

 

Preparing for Compilation

 

Project link libraries

If you are using the provided sample project files, you do not need to verify these settings. They are specified with the project files. For new applications, on the Project menu, click Configuration.

You should verify that the application is linked to the appropriate standard DirectX link libraries.  They are: ddraw.lib, dxguid.lib, and ole32.lib at the bare minimum.

Debug vs. Retail DLLs

The Microsoft DirectX SDK installation program provides the option of installing either debug or retail builds of the DirectX dynamic-link libraries (DLLs).

When you develop software in C, it is best to install the debug versions of the DLLs. This option installs both debug and retail DLLs on your system. The retail option installs only the retail DLLs. The debug DLLs have additional code that validates internal data structures and output debug error messages, using the Microsoft Win32 API OutputDebugString function, while your program is executing.  The strings that are passed to that primitive are shown by the lcc-win32 debugger in the events window, or in a message box. When an error occurs, the debug output gives you a more detailed description of the problem. The debug DLLs execute more slowly than the retail DLLs but are much more useful for debugging an application. Be sure to ship the retail version with your application.

You can use the DirectX Control Panel utility to switch between the debug and retail builds of DirectInput, Direct3D, and DirectMusic. To enable this feature, select the debug option when you install the SDK.


 

DXGetErrorMsg

Returns the name associated with a DirectX error code.

 

TCHAR* DXGetErrorString8(  HRESULT hr);

 

Parameters

hr

HRESULT value returned from a DirectX method. This handles only error codes.

 

Return Values

If successful, this function returns a pointer to a string that contains the name of the error code. If Unicode is set, DXGetErrorString8 will return a Unicode string. Otherwise, it will return an ANSI string.

Remarks

This function is designed to retrieve the text equivalent of a DirectX error message from a Direct3D, D3DX, DirectPlay, DirectInput, DirectMusic, or DirectSound method. For example, if you set hr to 0x88768686, DXGetErrorString8 will return D3DERR_DEVICELOST.

 

If an error code maps to more than one text string, DXGetErrorMsg will return a generic string. For example, there are several DirectX error codes, such as DIERR_OUTOFMEMORY, that indicate that you are out of memory, and all map to the same value. If you set hr to any of these codes, DXGetErrorString8 will return E_OUTOFMEMORY.

To use this, function you need to link with dxerr.lib.



[1] If an object exposes an interface, it must support every method in the interface definition, i.e. the table of function pointer must be filled with valid pointers. In other words, you can call any method and be confident that it exists. However, the details of how a particular method is implemented may vary from object to object. For example, different objects may use different algorithms to arrive at the final result. There is also no guarantee that a method will be supported in a non-trivial way. Sometimes an object exposes a commonly used interface, but needs to support only a subset of the methods. You will still be able to call the remaining methods successfully, but they will return E_NOTIMPL.  You should refer to the documentation to see how any particular object implements an interface.

[2] The list of HRESULT values that you find on a method's reference page is often only a subset of the possible values that may be returned. The list typically covers only those values that are specific to the method and those standard values that have some method-specific meaning. You should assume that a method may return a variety of standard HRESULT values, even if they are not explicitly documented.

[3] Properly handling reference counting is a crucial part of COM programming. Failure to do so can easily create a memory leak. One of the most common mistakes that COM programmers make is failing to release an interface. When this happens, the reference count will never reach zero, and the object will remain in memory indefinitely.

 

[4] The operating system information returned by GetVersionEx should not be used to test for the presence or version number of DirectX. In particular, the fact that a system is Windows NT-based does not necessarily mean that DirectX is absent. The Windows 2000 operating system, for example, includes DirectX 7.0 or later. Future versions of Windows NT-based operating systems can also be expected to include a version of DirectX. Use the procedures described in the previous section to determine whether DirectX is present and, if so, the version number.