DirectX/9.0/Direct3D/Initialization
One of the most important and complex parts of Direct3D is initialization. There are two ways to go about it, the correct (read: tedious, hard, annoying, but worth learning) way which the majority of this chapter is on, and the easy way, which will be used as an introduction, as some portions are required no matter how you do it. At the end of this section, we will apply this to our benchmarking application.
The Easy Way
editThe first thing you must do to begin initializing Direct3D is to create a Direct3D object. This object will be essential throughout the rest of the book. Luckily, that does not mean that it is hard. In fact, the only thing you have to do is call a function
IDirect3D9* Direct3DCreate9( UINT SDKVersion );
This function returns the address of the IDirect3D9 interface it created, or our Direct3D object. You need to tell it what version of DirectX the SDK is with the parameter. Generally you just pass D3D_SDK_VERSION, unless you want an older interface for some reason.
IDirect3D9* d3dobject = Direct3DCreate9( D3D_SDK_VERSION );
//Please note that DirectX uses the same naming conventions as Win32,
//so you can also initialize the variable as a LPDIRECT3D9 instead of a IDirect3D9*.
//Also, it's a good idea to make sure the device was created.
//If it wasn't, d3dobject will be null, and DirectX9 or greater probably isn't installed
And that's that. Unfortunately, the rest of the process isn't so easy. The next thing you need is a set of presentation parameters, which are stored in a D3DPRESENT_PARAMETERS structure. If you look inside, you'll notice there are a lot of variables. You don't need to fill them all, not even most of them. Just make sure that they're all zeroed. The only two you're worried about are Windowed, and SwapEffect. For Windowed, you can just set it to true. SwapEffect depends on what you want. For this tutorial, the fastest, D3DSWAPEFFECT_DISCARD, will work fine. As of now, your code should look something like this:
D3DPRESENT_PARAMETERS d3dpresent;
memset( &d3dpresent, 0, sizeof( D3DPRESENT_PARAMETERS ) );
d3dpresent.Windowed = TRUE;
d3dpresent.SwapEffect = D3DSWAPEFFECT_DISCARD;
Now we can actually create the device. This can be accomplished with a call to a function referenced by d3dobject:
IDirect3D9::CreateDevice( UINT Adapter, D3DDEVTYPE DeviceType, HWND hFocusWindow, DWORD BehaviorFlags,
D3DPRESENT_PARAMETERS * pPresentationParameters, IDirect3DDevice9 ** ppReturnedDeviceInterface );
This returns either 0 (success), or an error. There are many parameters, but most have values which always work. The first is the adapter number. Right now, we can just use D3DADAPTER_DEFAULT, which always works. DeviceType also has a value that always works: D3DDEVTYPE_REF. This is very slow, and should not be used unless absolutely necessary; but for right now, it will suffice (It also does not work without SDK libraries installed, so don't try it on other computers). Our next parameter is our window's handle, which is not covered in this book (See Windows Programming). BehaviorFlags will work with D3DCREATE_SOFTWARE_VERTEXPROCESSING, another slow choice. pPresentationParameters (why does Microsoft use such long names?) gets the address of our present parameters structure, and ppReturnedDeviceInterface gets a pointer to the pointer we want filled. So our call looks like this:
d3dobject->CreateDevice( D3DADAPTER_DEFAULT, D3DDEVTYPE_REF, window, D3DCREATE_SOFTWARE_VERTEXPROCESSING, &d3dpresent, &d3ddevice );
And that's how you initialize a device! Of course, you almost certainly want a faster device with more capabilities, and you really should check for errors so your user doesn't lose memory if your program crashes, but that's covered next.
The Right Way
editThe Adapter
editAll (good) commercial games that use DirectX initialize the device the right way. Nearly all (if not all) allow you to choose which graphics card, or adapter, their engine is running on. So that is where we will start. Determining the graphics cards available is very useful. First, it gives a degree of freedom to the user, which is always important. For commercial games, it also allows them to create a list which has settings which work the best with that card, thereby making it easier for the user. We will certainly implement this feature into our benchmark application, albeit only for the first reason. The first thing that you must do (after initializing the IDirect3D9 interface, of course) is determine the number of adapters available. This is a simple function:
UINT IDirect3D9::GetAdapterCount( );
It returns the number of adapters available. The next thing we need to do is get an array of the D3DADAPTER_IDENTIFIER9 structures. After creating the array, we can fill it with a loop and a call to:
HRESULT IDirect3D9::GetAdapterIdentifier( UINT Adapter, DWORD Flags, D3DADAPTER_IDENTIFIER9 * pIdentifier );
This returns D3D_OK if the parameters were valid, or D3DERR_INVALIDCALL if something was wrong. The value passed to adapter is can be anything inside [0,GetAdapterCount()). Flags should be 0 (unless we want Windows to download new driver certificates, which we don't), and pIdentifier should be the address of the current index. And now here's our code:
UINT adaptercount = d3dobject->GetAdapterCount( );
D3DADAPTER_IDENTIFIER9* adapters = ( D3DADAPTER_IDENTIFIER9* )malloc( sizeof( D3DADAPTER_IDENTIFIER9 ) * adaptercount );
for( int i = 0; i < adaptercount; i++ )
{
d3dobject->GetAdapterIdentifier( i, 0, &( adapters[i] ) );
}
We can then choose the adapter our application will use based on which video card the user chooses (using the message handler), or whichever the program thinks is best based on the details in the D3DADAPTER_IDENTIFIER9 structure.
Device Capabilities and Buffer Formats
editAfter we have chosen the adapter, we can find out what it can do with a D3DCAPS9 structure. Unfortunately, this is a somewhat drawn out process. You need to call:
HRESULT IDirect3D9::GetDeviceCaps( INT Adapter, D3DDEVTYPE DeviceType, D3DCAPS9 * pCaps );
The parameters are a bit tricky. Adapter and pCaps are simple, they're just the adapter number we chose and a pointer to the D3DCAPS structure we need filled. DeviceType is the problem. It must be determined with another function. We can check for a hardware device with:
HRESULT IDirect3D9::CheckDeviceType( UINT Adapter, D3DDEVTYPE DeviceType, D3DFORMAT DisplayFormat, D3DFORMAT BackBufferFormat,
BOOL Windowed );
After calling this, we can check if the device type worked based on whether it returned D3D_OK. The parameters, however, require another piece of information. For Adapter, we give the adapter number. DeviceType is the type we want to check for, and should be D3DDEVTYPE_HAL. Since we are windowed, that should be TRUE. DisplayFormat and BackBufferFormat are what we need to determine, and they should generally (for fullscreen, they must be) should be the same thing. They can be resolved through trial and error in a nested if statement, starting with the format we'd prefer, and going through most of them. Here is a list of valid Display and Back Buffer formats: http://msdn.microsoft.com/en-us/library/bb172558(VS.85).aspx If it never returns D3D_OK, you're stuck with D3DDEVTYPE_REF or D3DDEVTYPE_SW. We'll skip the formats with alpha fields, because they aren't supported by fullscreen, and we'll find the best format for either HAL or REF, because we'll need it later. Here's what our code will look like:
//adapternum is to have been defined earlier in the program and contains the choice of adapter
//fullscreen is also to have been previously defined, and contains either TRUE or FALSE
D3DCAPS9 caps;
D3DFORMAT format;
D3DDEVTYPE devicetype; //We'll need this and format later
if( d3dobject->CheckDeviceType( adapternum, D3DDEVTYPE_HAL, D3DFMT_X8R8G8B8, D3DFMT_X8R8G8B8, window ) != D3D_OK )
//Best format, almost always works
{
if( d3dobject->CheckDeviceType( adapternum, D3DDEVTYPE_HAL, D3DFMT_X1R5G5B5, D3DFMT_X1R5G5B5, window ) != D3D_OK ) {
if( d3dobject->CheckDeviceType( adapternum, D3DDEVTYPE_HAL, D3DFMT_R5G6B5, D3DFMT_R5G6B5, window ) != D3D_OK ) {
devicetype = D3DDEVTYPE_REF;
if( d3dobject->CheckDeviceType( adapternum, D3DDEVTYPE_REF, D3DFMT_X8R8G8B8, D3DFMT_X8R8G8B8, window ) != D3D_OK ) {
if( d3dobject->CheckDeviceType( adapternum, D3DDEVTYPE_REF, D3DFMT_X1R5G5B5, D3DFMT_X1R5G5B5, window ) != D3D_OK ) {
if( d3dobject->CheckDeviceType( adapternum, D3DDEVTYPE_REF, D3DFMT_R5G6B5, D3DFMT_R5G6B5, window ) != D3D_OK ) {
//Error - No valid format or device detected!
//You need to decide what to do - if it is windowed, you can try using D3DFMT_UNKNOWN
} else {
format = D3DFMT_R5G6B5;
} } else {
format = D3DFMT_X1R5G5B5;
} } else {
format = D3DFMT_X8R8G8B8;
} } else {
devicetype = D3DDEVTYPE_HAL;
format = D3DFMT_R5G6B5;
} } else {
devicetype = D3DDEVTYPE_HAL;
format = D3DFMT_X1R5G5B5;
} } else {
devicetype = D3DDEVTYPE_HAL;
format = D3DFMT_X8R8G8B8;
}
d3dobject->GetDeviceCaps( ( INT )adapternum, devicetype, &caps );
Some people who know how to do this may point out that you could just use IDirect3D9*->GetAdapterDisplayMode to determine a valid format for windowed applications. I did not do this here because it does not work when you're working in fullscreen, and the code above always works. In any case, you now have the device caps for the adapter. What do we do with it? The first thing we can do is find out if the hardware supports transformations or lighting. We can do that with the DevCaps variable in caps. You just need to use some bitwise operations to see if it has D3DDEVCAPS_HWTRANSFORMANDLIGHT. If it does, we will be able to use D3DCREATE_HARDWARE_VERTEXPROCESSING instead of D3DCREATE_SOFTWARE_VERTEXPROCESSING in CreateDevice. We are also able to extract other information about what we can pass through with it, such as D3DCREATE_PUREDEVICE (bad for debugging, very good for speed), which requires hardware.
DWORD vtx_proc;
if( caps.DevCaps & D3DDEVCAPS_HWTRANSFORMANDLIGHT ) {
vtx_proc = D3DCREATE_HARDWARE_VERTEXPROCESSING;
if( caps.DevCaps & D3DDEVCAPS_PUREDEVICE ) {
vtx_proc |= D3DCREATE_PUREDEVICE;
}
} else {
vtx_proc = D3DCREATE_SOFTWARE_VERTEXPROCESSING;
}
We can also determine the present intervals we can use. It stores the valid intervals as a bit mask in its PresentationIntervals field. Any of the values on this page can be used: http://msdn.microsoft.com/en-us/library/bb172585(VS.85).aspx . If we want to see if the device supports a certain form, we just do this:
DWORD presinterval;
if( caps.PresentationIntervals & D3DPRESENT_INTERVAL_FOUR )
{
presinterval = D3DPRESENT_INTERVAL_FOUR;
}
There is more information about the device in the CAPS structure, but it is of little use currently (read: I don't know what to do with it).
Depth Stencils
editObtaining valid a depth stencil is very similar to getting a valid format. It requires a call to
IDirect3D9::CheckDeviceFormat( UINT Adapter, D3DDEVTYPE DeviceType, D3DFORMAT AdapterFormat, DWORD Usage,
D3DRESOURCETYPE RType, D3DFORMAT CheckFormat );
and returns D3D_OK if it was successful. The non-obvious parameters are Usage, RType, and CheckFormat. Usage tells what type of format we are checking; Depth stencils; and should be D3DUSAGE_DEPTHSTENCIL. RType tells what form of resource we are using, and should be D3DRTYPE_SURFACE. CheckFormat is the depth stencil format we want to check. Determining this requires another trial and error approach; and uses the formats with a "D" in the name at the format list: http://msdn.microsoft.com/en-us/library/bb172558(VS.85).aspx. Here's an example:
D3DFORMAT depthstencil;
if( d3dobject->CheckDeviceFormat( adapter, devicetype, format, D3DUSAGE_DEPTHSTENCIL, D3DRTYPE_SURFACE, D3DFMT_D32 ) != D3D_OK )
{
...
} else {
depthstencil = D3DFMT_32;
}
Multisampling
editMultisampling is yet another trial and error procedure, and luckily the last one. We'll be determining two values to use: the multisampling level and the maximum quality of multisampling. These are both determined with this function:
HRESULT IDirect3D9::CheckDeviceMultiSampleType( UINT Adapter, D3DDEVTYPE DeviceType, D3DFORMAT SurfaceFormat,
BOOL Windowed, D3DMULTISAMPLE_TYPE MultiSampleType, DWORD* pQualityLevels );
CheckDeviceMultiSampleType returns D3D_OK if the multisampling typw is valid. It also gives us the maximum quality level in pQualityLevels. Our D3DFORMAT parameter should be our back buffer format, and pQualityLevels should be the address of a DWORD. D3DMULTISAMPLE_TYPE can be any of the values listed here: http://msdn.microsoft.com/en-us/library/bb172574(VS.85).aspx As always, here's our example usage:
DWORD quality;
D3DMULTISAMPLE_TYPE multisample;
if( d3dobject->CheckDeviceMultiSampleType( adapter, devicetype, format, !fullscreen, D3DMULTISAMPLE_16_SAMPLES,
&quality ) != D3D_OK )
{
...
} else {
multisample = D3DMULTISAMPLE_16_SAMPLES;
}
Display Modes
editOur next goal is to determine the best display mode available. When compared to the other tasks; it is pretty easy. There are two applicable methods to do this. The first is to simply use what the user is currently using. You can do this with a single call to:
HRESULT IDirect3D9::GetAdapterDisplayMode( UINT Adapter, D3DDISPLAYMODE * pMode );
pMode is the return that matters. It's a structure which gives us the resolution, refresh rate, and format the user is using. This method is quick and easy, but not as adaptive to environments as is sometimes needed. The next method is more complex, but more adaptive. You need two functions:
UINT IDirect3D9::GetAdapterModeCount( UINT Adapter, D3DFORMAT Format );
and
HRESULT EnumAdapterModes( UINT Adapter, D3DFORMAT Format, UINT Mode, D3DDISPLAYMODE* pMode );
GetAdapterModeCount is simple. It takes the adapter and the back buffer format and gives the number of display modes. EnumAdapterModes is a little more complex, but still simple. It also takes the adapter and back buffer format, but it also needs a number and a place to put mode. The number needs to be less than what GetAdapterModeCount returns, and pMode just needs to be some usable memory. You generally use the two in a loop to find the values you want. For example:
D3DDISPLAYMODE dispmode, bestmode;
d3dobject->EnumAdapterModes( adapter, format, 0, &bestmode );
for( UINT i = 1; i < d3dobject->GetAdapterModeCount( adapter, format ); i++ )
{
d3dobject->EnumAdapterModes( adapter, format, i, &dispmode );
if( dispmode.Width > bestmode.Width )
{
bestmode.Width = dispmode.Width;
bestmode.Height = dispmode.Height;
bestmode.RefreshRate = dispmode.RefreshRate;
continue;
}
if( dispmode.Height > bestmode.Height )
{
bestmode.Height = dispmode.Height;
bestmode.RefreshRate = dispmode.RefreshRate;
continue;
}
if( dispmode.RefreshRate > bestmode.RefreshRate )
{
bestmode.RefreshRate = dispmode.RefreshRate;
continue;
}
}
This code gets the absolute best display mode for the chosen format and puts it into bestmode.
Initialize
editNow that we have all of this data we can initialize our device the way it should be done - fast and flexible! Our first goal is to fill in the D3DPRESENT_PARAMETERS structure. This is where the bulk of our data goes. Here's the structure in all its glory:
struct D3DPRESENT_PARAMETERS {
UINT BackBufferWidth, BackBufferHeight;
D3DFORMAT BackBufferFormat;
UINT BackBufferCount;
D3DMULTISAMPLE_TYPE MultiSampleType;
DWORD MultiSampleQuality;
D3DSWAPEFFECT SwapEffect;
HWND hDeviceWindow;
BOOL Windowed;
BOOL EnableAutoDepthStencil;/
D3DFORMAT AutoDepthStencilFormat;
DWORD Flags;
UINT FullScreen_RefreshRateInHz;
UINT PresentationInterval;
}
And here's how we'll fill it in (in a mostly linear order):
d3dpresent.BackBufferWidth = bestmode.Width;
d3dpresent.BackBufferHeight = bestmode.Height;
d3dpresent.BackBufferFormat = format;
d3dpresent.MultiSampleType = multisample;
d3dpresent.MultiSampleQuality = quality;
d3dpresent.hDeviceWindow = window;
d3dpresent.Windowed = !fullscreen;
d3dpresent.AutoDepthStencilFormat = depthstencil;
d3dpresent.FullScreen_RefreshRateInHz = bestmode.RefreshRate;
d3dpresent.PresentationInterval = presinterval;
If you noticed, we didn't fill in all of the parameters. That's because we need to know what our application will do - we have (for the most part) free range on the parameters left. Our first unfilled parameter is BackBufferCount. We can fill this with any value between 0 and D3DPRESENT_BACK_BUFFERS_MAX. Generally we want this to be 1. Next we have SwapEffect. A list of valid swap effects can be found here: http://msdn.microsoft.com/en-us/library/bb172612(VS.85).aspx. Generally this is D3DSWAPEFFECT_DISCARD. This MUST be D3DSWAPEFFECT_DISCARD if we set MultiSampleType to anything other than D3DMULTISAMPLE_NONE. Now we need to set EnableAutoDepthStencil. This can be TRUE or FALSE. If we want Direct3D to manage the depth stencil (we do), it should be TRUE. Finally, we need to get a value for Flags. This can be 0 or any of the values listed here: http://msdn.microsoft.com/en-us/library/bb172586(VS.85).aspx. We'll use 0. And here's our code for the rest of the parameters:
d3dpresent.BackBufferCount = 1;
d3dpresent.SwapEffect = D3DSWAPEFFECT_DISCARD;
d3dpresent.EnableAutoDepthStencil = TRUE;
d3dpresent.Flags = 0;
Now we can call the CreateDevice function. If you'll recall, it looked like this:
HRESULT IDirect3D9::CreateDevice( UINT Adapter, D3DDEVTYPE DeviceType, HWND hFocusWindow, DWORD BehaviorFlags,
D3DPRESENT_PARAMETERS * pPresentationParameters, IDirect3DDevice9 ** ppReturnedDeviceInterface );
The only parameter that needs explaining is BehaviorFlags. BehaviorFlags specifies many things as a bitmask. A complete list of constants it can include is here: http://msdn.microsoft.com/en-us/library/bb172527(VS.85).aspx. We will just be using it for the type of vertex processing we want. So we just plug in our vtx_proc variable. And this is our final device initialization function!
d3dobject->CreateDevice( adapter, devicetype, window, vtx_proc, &d3dpresent, &d3ddevice );
Cleaning Up
editAlthough not strictly involved in initialization, it is good to talk about freeing up the memory you allocated in the initialization section. All devices created with the Direct3D object as well as the object itself must be released with their Release functions. Here, the only things we allocated were the device and the object, so we need to do:
d3ddevice->Release( );
d3dobject->Release( );