Initializing Direct3D

Introduction

Creating Direct3D

Creating a Direct3D Device

HRESULTs and Testing For Success

Finding a usable D3DFORMAT

Render Loop

Lost Devices

Lesson Downloads

Further Reading

MSDN Links For Functions/Concepts Introduced Here

Introduction

By the end of this lesson you will be able to create your first Direct3D application. It won't do much, but it will create a window and initialize Direct3D. We'll also cover clearing the display and presenting the results to the user. We won't do any real rendering in this lesson.

Creating Direct3D

The first step is to create an IDirect3D9 object and it is easy. Here's the code to create the object and then free it:

IDirect3D9 *g_D3D=NULL;

   g_D3D = Direct3DCreate9( D3D_SDK_VERSION);
   if(!g_D3D){
      //Handle error

   }

   //Then at the end of your application

   if(g_D3D){
      g_D3D->Release();
      g_D3D=NULL;
   }

First, I declare my pointer to a IDirect3D9 object. This is worth noting because in many places you will see the declaration use LPDIRECT3D9. LPDIRECT3D9 is just a typedef, which is a pointer to an IDirect3D9 object. All of the DirectX objects have typedefs which are in all-caps and preceded by "LP". I prefer to use the underlying type for 2 reasons. I find long all-caps declarations to be less legible and I prefer my pointers to look like pointers. So it's easier to read and more obvious what you're doing. Increased readability in your code is always a good thing. It's a personal choice though. Use the one that makes the most sense to you.

Direct3DCreate9 takes only 1 parameter. In almost every case, you'll use D3D_SDK_VERSION. This creates a Direct3D object that is compatible with the version of the SDK you're using. If for some reason you needed to work with an older version of Direct3D9, you could pass that version instead. Likely you will never need to do this, so just pass D3D_SDK_VERSION.

When you are done using the IDirect3D9 object, you call its Release method. The quick short explanation of this method is that it frees the object. The long description is a bit more involved. DirectX uses COM (Component Object Model) and all of it's objects inherit from IUnknown. When an IUnknown object is created an internal reference count is incremented and when Release is called the reference count is decremented. When the reference count reaches 0 the object is freed.

An object's reference count can be incremented directly by calling its AddRef method. Advanced users will want to do this to manage objects shared in many places. Reference counts are often manipulated indirectly. If you are rendering a mesh, Direct3D will increment its reference count and decrement it when it's done. This way even if you freed the model because you thought it wasn't being used, Direct3D could still properly render the mesh.

A detailed understanding of COM isn't required to use DirectX but it does help to understand these basic concepts of how to use it. For further information, see the Further Reading section below.

When we call Release, we first check if the pointer is NULL. Dereferencing a NULL pointer will cause an exception which will terminate the application. If the pointer isn't NULL we call Release and then set the pointer to NULL to prevent it from being freed multiple times. A Microsoft-defined MACRO, SAFE_RELEASE, does this as well. SAFE_RELEASE is defined in dxutil.h which is in the Common folder of the SDK.

Creating a Direct3D Device

A Direct3D Device (IDirect3DDevice9, we'll refer to it simply as a device for the rest of the tutorial) is used to communicate with a video card. There is a one-to-one relationship between video cards and devices. If you wanted to render to multiple video cards at the same time, you would need 1 device for each card. Unlike IDirect3D, creating a device is fairly involved. Since this is an introductory tutorial, we'll skim over the more advanced issues and leave those for a later tutorial.

There is an intimidating amount of data that can be provided when creating a device. To avoid having to provide 80 different parameters (many optional) to a single function, we instead fill out a data structure to specify how we want our device created. This structure is called D3DPRESENT_PARAMETERS. I think someone at Microsoft has a broken Caps Lock key. Anyway, here's what the structure looks like:

struct D3DPRESENT_PARAMETERS{
   UINT                BackBufferWidth;
   UINT                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;
};

Here's what they all are:

BackBufferWidth and BackBufferHeight
The width and height of the back buffer. In a full-screen application these must match a supported display mode. For this tutorial we will use a safe default of 640x480.
BackBufferFormat
This describes the bit depth of the back buffer (16 and 32-bit are typical). 24-bit back buffers are not supported in DX9. The format also describes the how the format of the bits in each pixel. The 2 most common 16-bit formats are D3DFMT_R5G6B5 and D3DFMT_X1R5G5B5. Both are 16-bit formats, but the second has 1 bit unused, while the first assigns the extra bit to the green component. You can query the device to find a usable format. I'll show you how to do that later in this tutorial. In Windowed mode you can set this to D3DFMT_UNKNOWN, in which case it will use the format of the desktop.
BackBufferCount
Legal values are from 0 to 3, with 0 being treated as 1. Typically only a single back buffer is used. This is called double-buffering since you draw to the back buffer and then the scene is displayed as a whole to the user. More buffers can smooth out your frame rate, but they can also cause input lag (delay between when the user hits a key and when he sees the results). They also consume extra memory.
MultiSampleType and MultiSampleQuality
These control multisampling which is a form of full-screen anti-aliasing. It can significantly improve the quality of your rendered scene, but can cost you memory and performance. Also, many cards will not support multisampling at all. We won't cover multisampling in this tutorial, we'll just set the type to D3DMULTISAMPLE_NONE and the quality to 0.
SwapEffect
Legal values for this are D3DSWAPEFFECT_DISCARD, D3DSWAPEFFECT_FLIP and D3DSWAPEFFECT_COPY. D3DSWAPEFFECT_DISCARD will cause the device to use the fastest method it can to present the back buffer so that's what we'll use. The other Swap Effects are beyond the scope of this lesson.
hDeviceWindow
Just set this to your window. Advanced users who are interested in supporting systems with more than video card can read the documentation for details on handling multiple devices and windows.
Windowed
If false, Direct3D will take over the display and change the display mode to match the dimensions given in BackBufferWidth and BackBufferHeight. If true, the display mode will not be changed and you will render to the client area of the Device Window.
EnableAutoDepthStencil
This sets whether the application will be using a depth and/or stencil buffer. For now we'll set this to FALSE.
AutoDepthStencilFormat
Similar to the BackBufferFormat, this sets the bit depth and format used by the Depth/Stencil buffer. When EnableAutoDepthStencil is FALSE, this is ignored. When not using a Depth/Stencil buffer, I set this to D3DFMT_UNKNOWN.
Flags
This is for advanced use. For now just set it to 0.
FullScreen_RefreshRateInHz
Given in Hz, Sets the refresh rate for the display. If the rate given does not match a rate that the device and monitor can handle, the attempt to create the device will either fail, or it will send a signal to the monitor that it cannot handle. To be safe set it to D3DPRESENT_RATE_DEFAULT.
PresentationInterval
This controls the delay in presenting the back buffer to the user. D3DPRESENT_INTERVAL_DEFAULT will wait for the vertical retrace. This prevents visual tearing, but your frame rate is limited to the display's refresh rate. D3DPRESENT_INTERVAL_IMMEDIATE performs the update immediately allowing much higher frame rates. It can cause visual tearing though.

With that out of the way, we can create our device. Let's look at the prototype for the method.

HRESULT CreateDevice(
   UINT Adapter,
   D3DDEVTYPE DeviceType,
   HWND hFocusWindow,
   DWORD BehaviorFlags,
   D3DPRESENT_PARAMETERS *pPresentationParameters,
   IDirect3DDevice9** ppReturnedDeviceInterface
);
Adapter
This specifies which display adapter the device should be associated with. In most cases there is a one-to-one relationship between adapters and video cards. On cards that support multi-head (they can drive multiple monitors from a single card) each "head" may be its own adapter. To get the primary display, use D3DADAPTER_DEFAULT.
DeviceType
In most cases you will use D3DDEVTYPE_HAL. HAL stands for Hardware Acceleration Layer and makes the best use of your video card. D3DDEVTYPE_REF does all of its processing in software. While this is incredibly slow (single digit frames per second are typical) it is a good debugging aid. REF is short for Reference Rasterizer. This is an complete implementation of the Direct3D spec. It supports many operations that your video card may not. While using HAL, if you find something that doesn't work properly you can run it on REF and if it's still wrong, it is likely a bug in your program, otherwise it may be a bug in the video driver. Also, REF is installed with the SDK, so don't count on it being available on your user's PCs. D3DDEVTYPE_SW exists to support pluggable software implementations, but so far none exist.
hFocusWindow
Set this to the same value as the DeviceWindow in your presentation parameters. NOTE:If running in full-screen, the focus must be a top level window. Child windows and controls are not valid.
BehaviorFlags
This controls a lot of behind-the-scenes behaviours. Most are for advanced use so we won't cover them here. The 3 most common flags are: D3DCREATE_SOFTWARE_VERTEXPROCESSING, D3DCREATE_HARDWARE_VERTEXPROCESSING, and D3DCREATE_MIXED_VERTEXPROCESSING. One and only one of these three flags can be used. SOFTWARE sets all vertex processing (lighting, for example) to be done in software. This is supported on all cards and has very relaxed limitations in what it can do. Since it's done in software, all vertex processing is done on the CPU. With HARDWARE, all of the vertex processing is done by the video card. This is faster because video cards are specifically designed for this work and also because it frees up your CPU to work on other things. Most modern cards support HARDWARE vertex processing. Specifying MIXED allows you to switch back and forth between SOFTWARE and HARDWARE. If there are operations that your hardware does not support, you can switch to software for those, and then switch to hardware for everything else.
pPresentationParameters
This is just a pointer to your PRESENTATION_PARAMETERS structure.
IDirect3DDevice9
If the call to CreateDevice succeeds, your new device will be returned here.

Here is some example code to bring it all together.

IDirect3D9 *d3d;  //Presumed to be properly initialized already

HWND wnd;   //Presumed to be initialized

D3DFORMAT format=D3DFMT_R5G6B5; //For simplicity we'll hard-code this for now.

D3DPRESENT_PARAMETERS pp;
IDirect3DDevice9 *p_device=NULL;
HRESULT hr;

   //Even though we set all of it's members, it's still good practice to zero it out

   ZeroMemory(&pp,sizeof(D3DPRESENT_PARAMETERS));

   pp.BackBufferCount= 1;  //We only need a single back buffer

   pp.MultiSampleType=D3DMULTISAMPLE_NONE; //No multi-sampling

   pp.MultiSampleQuality=0;                //No multi-sampling

   pp.SwapEffect = D3DSWAPEFFECT_DISCARD;  // Throw away previous frames, we don't need them

   pp.hDeviceWindow=wnd;  //This is our main (and only) window

   pp.Flags=0;            //No flags to set

   pp.FullScreen_RefreshRateInHz=D3DPRESENT_RATE_DEFAULT; //Default Refresh Rate

   pp.PresentationInterval=D3DPRESENT_INTERVAL_DEFAULT;   //Default Presentation rate

   pp.BackBufferFormat=format;      //Display format

   pp.EnableAutoDepthStencil=FALSE; //No depth/stencil buffer


   if(is_app_fullscreen){
      pp.Windowed          = FALSE;
      pp.BackBufferWidth   = 640;
      pp.BackBufferHeight  = 480;
   }else{
      pp.Windowed          = TRUE;
   }

   hr=p_d3d->CreateDevice(D3DADAPTER_DEFAULT, //The default adapter, on a multi-monitor system

                                              //there can be more than one.

                          D3DDEVTYPE_HAL, //Use hardware acceleration rather than the software renderer

                          //Our Window

                          wnd,
                          //Process vertices in software. This is slower than in hardware,

                          //But will work on all graphics cards.

                          D3DCREATE_SOFTWARE_VERTEXPROCESSING,
                          //Our D3DPRESENT_PARAMETERS structure, so it knows what we want to build

                          &pp,
                          //This will be set to point to the new device

                          &p_device);
   if(FAILED(hr)){
      // Handle error

   }

HRESULTs and Testing For Success

Virtually all methods in DirectX return an HRESULT for success/failure. The proper way to test if the method failed is with the FAILED macro. There is also a SUCCEEDED macro to test for success. In many examples, success will be tested by comparing the HRESULT with D3D_OK. This is not the proper way to do it. They only guaranteed method of testing for failure is with the supplied macros.

An HRESULT contains more than just a success/failure flag. It can provide much more information on what happened. For example, if you call CreateDevice and there isn't enough memory available it will return D3DERR_OUTOFVIDEOMEMORY. To get the error code into a human-readable format you can use the DirectX Error Lookup tool that ships with the SDK. Give it the numeric value of the error (in hex or decimal form) and it will explain the error. Another way is to use DXGetErrorString9 which takes a single HRESULT as a parameter and returns a pointer to a string describing the error. In my Common library I use it to print the error codes out to a log file.

Finding a usable D3DFORMAT

Earlier we used a hard-coded value for D3DFORMAT. This is a bad idea since the user's video card may not support the format you chose. In that case, your application will fail to create a device. The proper way to do it is to query the device to see if it supports a given back buffer format. To do that we use the CheckDeviceType method of our IDirect3D9 object before we create our device.

HRESULT CheckDeviceType(
   UINT Adapter,
   D3DDEVTYPE DeviceType,
   D3DFORMAT DisplayFormat,
   D3DFORMAT BackBufferFormat,
   BOOL Windowed
);
Adapter
This is the same value you'll be using in the call to CreateDevice. It identifies which video adapter you wish to use.
DeviceType
Since you want to test for modes supported by the video card, use D3DDEVTYPE_HAL.
DisplayFormat
This is the format used by the display. In full-screen this will be the same as the back buffer format. In windowed mode, this is the format of the user's current desktop. You can use IDirect3D9->GetAdapterDisplayMode to find the current display mode.
BackBufferFormat
This is the format the back buffer will be in. In full-screen mode this has to be the same as DisplayFormat. In windowed mode, they can be different but choosing compatible modes can be complicated.
Windowed
Because there are different rules for full-screen and windowed mode, you have to specify which you're using.

Here's an example of testing to see if D3DFMT_R5G6B5 is available in full-screen mode.

HRESULT hr;

   hr=d3d->CheckDeviceType(D3DADAPTER_DEFAULT,//Adapter

                           D3DDEVTYPE_HAL,    //DeviceType

                           D3DFMT_R5G6B5,     //DisplayFormat

                           D3DFMT_R5G6B5,     //BackBufferFormat

                           false);            //Windowed

   if(SUCCEEDED(hr)){
      //This mode is valid

   }else{
      //Either an error occurred or the mode is unavailable

   }

In windowed mode, it's best to just specify D3DFMT_UNKNOWN in your CreateDevice call. While you can have a back buffer that is a different format than your display, there is rarely any benefit.

Render Loop

We aren't going to actually render anything in this tutorial but I do want to sketch out the framework around our rendering. Here is a quick look at a basic main loop.

bool g_app_done=false;
HRESULT hr;

   while(!g_app_done){
      
      dhMessagePump();   //Check for window messages


      hr=render();   //Draw our incredibly cool graphics


   }

We loop until a global flag is set to signal the end of our application. This flag gets set by our window message handler. dhMessagePump is a function in my Common library that wraps the code to process the message queue. The message queue is covered in my Windows Primer lesson.

Each time through the loop we call our render function. That's all there is to the basic loop. A bit later in the we'll add some code to better handle error conditions.

The first thing you need to do in a render function is to clear the back buffer. The current back buffer could display a previous frame or random garbage. One exception to this is when you're using the debug runtime with the D3DSWAPEFFECT_DISCARD presentation flag. In this case the back buffer is cleared to alternating green and red. If you ever see the background of your application flashing those colours, your back buffer is not getting cleared properly. You clear the back buffer with the Clear method of your device. It can be used to clear the entire back buffer, or just select rectangles of it.

HRESULT Clear(
   DWORD Count,
   const D3DRECT *pRects,
   DWORD Flags,
   D3DCOLOR Color,
   float Z,
   DWORD Stencil
);
Count
The number of individual rectangles you want cleared. If you're clearing the whole back buffer, set this to 0.
pRects
This is an array of RECT structs that are to be cleared. If you want the entire back buffer cleared, set this to NULL.
Flags
Allowed flags (in any combination) are D3DCLEAR_STENCIL, D3DCLEAR_TARGET and D3DCLEAR_ZBUFFER. You can only specify D3DCLEAR_STENCIL if you have a stencil buffer, and D3DCLEAR_ZBUFFER if you have a Z (depth) buffer. If you specify a flag without a matching surface, the call will fail. Also, at least one of these flags must be specified.
Color
This is the colour you wish to set as the background of your back buffer. You can specify it as a 32-bit ARGB value (in hex: 0xAARRGGBB).
Z
This value is used to clear the depth buffer. Legal values range from 0.0 to 1.0, with 0.0 being the closest distance and 1.0 being the farthest.
Stencil
The Stencil buffer will be cleared to this value. It's an integer value ranging from 0 to 2n-1, where n is the depth of the stencil buffer.

To alert the device that you're about to being rendering you call the BeginScene method and to flag that you've finished rendering you call the EndScene method. Since neither one takes any parameters, I won't bother showing the prototypes.

After that you need to present the back buffer to the user. To do that you call the Present method. Here's its prototype.

HRESULT Present(
   const RECT *pSourceRect,
   const RECT *pDestRect,
   HWND hDestWindowOverride,
   const RGNDATA *pDirtyRegion
);
pSourceRect
This specifies a sub-rectangle to use as the source. Generally you want to present the whole buffer, so just pass in NULL.
pDestRect
This is a sub-rectangle of the display that you want the back buffer displayed on to. Again, this is typically left as NULL to cover the whole surface.
hDestWindowOverride
If set to NULL, the back buffer will be copied to the window you specified when you created your device. Setting this to a different window allows you to present to a different window.
pDirtyRegion
This specifies that only part of the image needs to be updated and it defines that region. This is an optimizing hint and there is no guarantee that additional area will not be updated. Generally just leave it NULL.

Now that we've gone over each component, let's pull it together.

   hr=g_d3d_device->Clear(0,  //Number of rectangles to clear, we're clearing everything so set it to 0

                          NULL, //Pointer to the rectangles to clear, NULL to clear whole display

                          D3DCLEAR_TARGET,   //What to clear.  We don't have a Z Buffer or Stencil Buffer

                          0x00000000, //Colour to clear to (AARRGGBB)

                          1.0f,  //Value to clear ZBuffer to, doesn't matter since we don't have one

                          0 );   //Stencil clear value, again, we don't have one, this value doesn't matter

   if(FAILED(hr)){
      //Handle error

   }

   //Notify the device that we're ready to render

   hr=g_d3d_device->BeginScene();
   if(FAILED(hr)){
      //Handle error

   }


   //Put cool stuff here


   //Notify the device that we're finished rendering for this frame

   g_d3d_device->EndScene();

   //Show the results

   hr=g_d3d_device->Present(NULL,  //Source rectangle to display, NULL for all of it

                            NULL,  //Destination rectangle, NULL to fill whole display

                            NULL,  //Target window, if NULL uses device window set in CreateDevice

                            NULL );//Dirty Region, set it to NULL


   if(FAILED(hr)){
      //Handle error

   }

If we were actually rendering anything, it would go in place of the "Put cool stuff here" comment.

Lost Devices

There is one last important thing to cover, Lost Devices. If you have a full-screen application and a user hits ALT+TAB to get back to the desktop, your device loses it's access to the display. There are a few other events that can cause it to happen, but users ALT+TABing away is the most common.

When you've lost your device you are unable to render anything. Attempts to call Present will simply return the error D3DERR_DEVICELOST. One drastic way to sort things out is to free everything and re-create it. But there is an easier way.

Call TestCooperativeLevel which polls the device to check its status. If it returns with success, then your device is fine. If it returns D3DERR_DEVICELOST then your device has been lost and cannot yet be Reset. Before a device can be Reset certain resources must be freed, this is a good time to do it. In my examples I've created a function called FreeVolatileResources for this purpose. I also have a function called InitVolatileResources which is used after the device is successfully Reset. The resources that will not survive a Reset will be covered in later lessons as we encounter them.

A return value of D3DERR_DEVICENOTRESET means it is safe to reset the device. TestCooperativeLevel takes no parameters. To reset the device you call the device's Reset method.

HRESULT Reset(
   D3DPRESENT_PARAMETERS *pPresentationParameters
);

As you can see Reset only takes a single parameter, a pointer to your D3DPRESENT_PARAMETERS. For this reason, it's a good idea to keep a copy of your present parameters hanging around. It's also worth noting that you can change your presentation parameters. For example, you could use the Reset method to switch between full-screen and windowed mode. Here's the code to handle a lost device. This code would be called from the main loop after detecting a lost device.

HRESULT hr;

   hr=p_device->TestCooperativeLevel();

   if(hr == D3DERR_DEVICELOST) { //Device is lost and cannot be reset yet


      Sleep(500); //Wait a bit so we don't burn through cycles for no reason

      FreeVolatileResources();

   }else if(hr == D3DERR_DEVICENOTRESET){ //Lost but we can reset it now


      hr=p_device->Reset(p_pp);
      if(SUCCEEDED(hr)){
         InitVolatileResources();
      }

   }

Download the lesson source below and see how it all fits together.

Lesson Downloads

Further Reading

Essential COM by Don Box
Amazon Links:

MSDN Links For Functions/Concepts Introduced Here

Back