Starting Direct3D9: Setting Up a Window

Published May 20, 2003 by Mason Smith, posted by Myopic Rhino
Do you see issues with this article? Let us know.
Advertisement

Starting Direct3D9: Setting Up a Window
by Mason Smith

This article will teach you the basics of Direct3D and using it to setup a Direct3D-enabled window. Even though I will quickly go over the window code, this article assumes that you already know how to setup a window in Win32. If not, there are plenty of resources that will show you how, such as MSDN.

So You Want To Learn Direct3D...

If you're still deciding between learning OpenGL and Direct3D (not DirectX), then this article probably won't help you decide. There is a great Direct3D vs. OpenGL article here on GDnet, so check it out. Anyway, now that you want to learn Direct3D, you're probably wondering exactly what it is and how it makes such pretty colors.

Direct3D uses devices to render graphics to your monitor. To Direct3D, a device is anything that can control the monitor and the T&L (transformation and lighting) hardware. A device can be your new Geforce4, your old Voodoo 2, or any other 2D/3D hardware that's connected to your computer. It can also be a software reference device, which runs on your computer, but we'll talk about all this later.

That's Fine, But Why Aren't we Coding Yet?

Alright, let's get to it. First thing we do is create a simple window, which I'll demonstrate with a simple code listing here


int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, 
LPSTR lpCmdLine, int nCmdShow)
{
  WNDCLASSEX windowClass;  // window class
  HWND hWND = NULL;

  // fill out the window class structure
  windowClass.cbSize    = sizeof(WNDCLASSEX);
  windowClass.style     = CS_HREDRAW | CS_VREDRAW;
  windowClass.lpfnWndProc   = WindowProc;
  windowClass.cbClsExtra    = 0;
  windowClass.cbWndExtra    = 0;
  windowClass.hInstance     = hInstance;
  windowClass.hIcon         = LoadIcon(NULL, IDI_APPLICATION);
  windowClass.hCursor       = LoadCursor(NULL, IDC_ARROW);
  windowClass.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH);

  windowClass.lpszMenuName  = NULL;
  windowClass.lpszClassName = "window";
  windowClass.hIconSm       = LoadIcon(NULL, IDI_WINLOGO);

  // register the windows class
  if (!RegisterClassEx(&windowClass))
    return;


  hWND = CreateWindowEx(NULL, "window", "Simple Direct3D Program",
                        WS_OVERLAPPEDWINDOW | WS_VISIBLE,
                        0, 0, 400, 400,
                        NULL, NULL, hInstance, NULL);

  // check if window creation failed (hWND would equal NULL)
  if (!hWND)
    return 0;
        
  if (FAILED(InitD3D(hWND, 800, 600, false)))
  {
    DestroyD3D();
    return 0;
  }

  MSG msg;
        
  while (1)
  {
    PeekMessage(&msg, hWND, NULL, NULL, PM_REMOVE);

    if (msg.message == WM_QUIT)
      break;
    else
    {
      Render();

      TranslateMessage(&msg);
      DispatchMessage(&msg);
  }
}

DestroyD3D();
UnregisterClass("window", hInstance);
return (msg.wParam);
}

What's with that Crazy Event Loop?

This classic code creates a simple window that's 400x400. I've omitted the window procedure because there's nothing special in it. If you haven't done any real-time programming, the event loop might be new to you so I'll explain it quickly.

Normally, you use GetMessage to wait for a windows message to process. This is fine for Win32 applications that depend solely on user input to execute actions. In a game, however, you still have to update AI, move your particle systems, and do about a billion other things at 60+ fps. This doesn't work when you have to sit around and wait for the user to press something before anything happens. PeekMessage allows you to quickly grab a message, if there is one, and check it without waiting for the user to create one. This way, you can check to see if the user has quit, and if it hasn't, update the screen as normal.

Now let's focus our attention on the task at hand. We are going to be filling InitDirect3D(), Render(), and DestroyD3D(). To initialize Direct3D we can follow 3 simple steps:

Initializing Direct3D

  1. Use COM to create an IDirect3D9 object
  2. Fill in a struct that determines the properties of your device
  3. Use the IDirect3D9 object to create an IDirect3DDevice9 object

Sounds simple enough, right? Remember, since we are using Direct3D, you must include d3D9.h and d3dx9.h. You must also link d3d9.lib, d3dx9.lib, and dxguid.lib.

1. Use COM to Create an IDirect3D9 Object

Like all subsystems of DirectX, Direct3D uses COM. In other words, you have to use COM to create any Direct3D interface, such as IDirect3D9 and IDirect3DDevice9. Luckily, the old days of cryptic QueryInterface calls are (almost) gone, and Direct3D provides a much nicer way of creating your object: Direct3DCreate9.Let's look at the function.


IDirect3D9* Direct3DCreate9(UINT SDKVersion);

This extremely simple function takes only one parameter, the current SDK version. You always put the constant D3D_SDK_VERSION, which is defined in d3d9.h, as that parameter. If the function is successful, it returns a pointer to an IDirect3D9 object. Otherwise, it returns NULL. Here's how you would use it:


// somewhere in globals
IDirect3D9* pD3D9 = NULL;

HRESULT InitDirect3D(HWND hwnd, int width, int height, bool fullscreen)
{
  pD3D9 = Direct3DCreate9(D3D_SDK_VERSION);
  if (pD3D9 == NULL)
    return E_FAIL;

The previous code creates the IDirect3D9 object and stores it in pD3D9. It returns an error code, E_FAIL, if the function returns NULL. Well, that's it for the first step.

2. Fill in a Struct that Determines the Properties of your Device

This step gets a little uglier than the last one, but it's still no big. First, you want to create a D3DPRESENT_PARAMETERS structure and zero it out using ZeroMemory. Let's look at what's in the structure:


typedef struct _D3DPRESENT_PARAMETERS_ {
    UINT                    BackBufferWidth;
    UINT                    BackBufferHeight;
    D3DFORMAT               BackBufferFormat;
    UINT                    BackBufferCount;
    D3DMULTISAMPLE_TYPE     MultiSampleType;
    D3DSWAPEFFECT           SwapEffect;
    HWND                    hDeviceWindow;
    BOOL                    Windowed;
    BOOL                    EnableAutoDepthStencil;
    D3DFORMAT               AutoDepthStencilFormat;
    DWORD                   Flags;
    UINT                    FullScreen_RefreshRateInHz;
    UINT                    PresentationInterval;
} D3DPRESENT_PARAMETERS;

BackBufferWidth/Height determines the width and height of the back buffer that you will render to. BackBufferFormat is the color format of the back buffer, which we'll talk about in a minute. BackBufferCount is the number of the back buffers you want to have. Normally, you'll just have one. MultiSampleType determines how many levels of multisampling you want. MultiSampling is used for fullscreen anti-aliasing, but we don't need it, so you'll set it to D3DMULTISAMPLE_NONE. SwapEffect determines what happens to the front buffer when you swap it with the back buffer. Since we won't use the front buffer again after it is presented to the screen, we can just use D3DSWAPEFFECT_DISCARD.

hDeviceWindow is the window that the device will render to, so you set it to the handle to the window you created in WinMain. Windowed is whether you want the device to be windowed (true) or fullscreen (false). EnableAutoDepthStencil determines whether or not we want Direct3D to automatically create a depth-buffer for us to do depth-testing. Most of the time you'll want this, so set it to true. AutoDepthStencilFormat is the format of the depth buffer that you want Direct3D to create for you. For this sample, we'll use D3DFMT_D16, which creates a 16-bit depth buffer and no stencil buffer.

Flags allows you to specify some miscellaneous features that don't fit in with the other members. For our purposes we need only to set it to 0. The only other option is D3DPRESENTFLAG_LOCKABLE_BACKBUFFER, which allows you to lock your back buffer and alter it directly. FullScreen_RefreshRateInHz determines how fast the display adapter refreshes the screen. We'll use D3DPRESENT_RATE_DEFAULT, which uses the default rate. Lastly, PresentationInterval determines the maximum rate that the back buffers can be presented. The default, which is required for windowed devices, is D3DPRESENT_INTERVAL_DEFAULT. When you use fullscreen devices, you can either use the default or D3DPRESENT_INTERVAL_IMMEDIATE, which just waits for the vertical trace.

Well, that was a lot but you know how to fill in everything except BackBufferFormat. In windowed mode, the format of the device has to match the color format of your desktop. So how do you get the format of your desktop? IDirect3D9::GetAdapterDisplayMode()


HRESULT IDirect3D9::GetAdapterDisplayMode(UINT Adapter, D3DDISPLAYMODE* mode);

Adapter is the number that represents the display adapter to get the display mode from. Normally, you'll use D3DADAPTER_DEFAULT. mode is a pointer that you pass into the function that stores the display mode of the adapter. Once you have that, you can set the BackBufferFormat to the Format member of D3DDISPLAYMODE. All this has been a lot to take in, so let's just see some code


// get the display mode
D3DDISPLAYMODE d3ddm;
pD3D9->GetAdapterDisplayMode(D3DADAPTER_DEFAULT, &d3ddm);

// set the presentation parameters
D3DPRESENT_PARAMETERS d3dpp;
ZeroMemory(&d3dpp, sizeof(d3dpp));
d3dpp.BackBufferWidth = width;
d3dpp.BackBufferHeight = height;
d3dpp.BackBufferCount = 1;
d3dpp.BackBufferFormat = d3ddm.Format;
d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
d3dpp.Windowed = !fullscreen;
d3dpp.EnableAutoDepthStencil = true;
d3dpp.AutoDepthStencilFormat = D3DFMT_D16;
d3dpp.FullScreen_RefreshRateInHz = D3DPRESENT_RATE_DEFAULT;
d3dpp.FullScreen_PresentationInterval = D3DPRESENT_INTERVAL_IMMEDIATE;

Nothing here should be out of the ordinary. First we get the display mode from the adapter. Then, we create the D3DPRESENT_PARAMETERS structure and fill it in with all the data. Notice that we only fill in the FullScreen_* members if the device is going to be fullscreen and that we didn't fill in the Flags paramter. This is because the ZeroMemory function automatically fills Flags with 0 and the Fullscreen_* members with their defaults, which are both defined as 0. Now that we've finished step 2, let's move on to step 3.

3. Use the IDirect3D9 object to create an IDirect3DDevice9 object

Fortunately for us, this step only requires one function call: IDirect3D9::CreateDevice(). Let's look at the method.


IDirect3D9::CreateDevice(UINT Adapter, 
        D3DDEVTYPE DeviceType, 
        HWND hFocusWindow,
        DWORD BehaviorFlags,
        D3DPRESENT_PARAMETERS* pPresentationParameters,
        IDirect3DDevice9** ppReturnedDeviceInterface);

Adapter is the same thing that it was in GetAdapterDisplayMode, so you'll again set it to D3DADAPTER_DEFAULT. DeviceType refers to the discussion earlier in the article. The device type can be either D3DDEVTYPE_HAL or D3DDEVTYPE_REF.

When the device is HAL (Hardware Abstraction Layer), it uses your 3D-card. This means that graphics will be (hopefully) a lot faster, but it has its drawbacks. Most likely, your graphics card will not implement the entire feature set. For example, you 3D-card may support bump mapping but not spherical environment mapping. So you wouldn't be able to do any spherical environment mapping in your program. The REF (software reference) device uses a software rasterizer, as opposed to hardware, to process the graphics. It implements the entire feature set, but it does so very slowly. So you'd be able to run spherical environment mapping programs even if your hardware doesn't support them, but your program would probably run at 2 fps. We'll use D3DDEVTYPE_HAL for this program.

hFocusWindow is the window that your device will render to, namely the one you created in WinMain. BehaviorFlags determines some miscellaneous aspects of our device. You must set BehaviorFlags to either D3DCREATE_HARDWARE_VERTEXPROCESSING, D3DCREATE_SOFTWARE_VERTEXPROCESSING, or D3DCREATE_MIXED_VERTEXPROCESSING. These flags determine whether the vertices are processed on the GPU, CPU, or a combination of the two. We'll use D3DCREATE_SOFTWARE_VERTEXPROCESSING for our sample program. There are other flags that can be set to BehaviorFlags. D3DCREATE_MULTITHREADED makes the device safe for use in multithreaded applications, even though it is a little slower. D3DCREATE_MANAGED indicates that all resources, such as vertex buffers and textures, are managed by the device and do not need to be managed by the client program.

pPresentationParameters should be filled with a pointer to the D3DPRESENT_PARAMETERS structure we just made. Lastly, the ppReturnedDeviceInterrface should be fed a pointer to a pointer to the IDirect3DDevice9 object that it will fill with the newly created device.

One important thing to mention about CreateDevice is that it automatically changes the window to fullscreen if necessary, allocates the back buffer, changes the color format, and all that good stuff. This means that, for all you OpenGL people, you don't have to go through PIXELFORMATDESCRIPTORS and whatnot. Now let's put the final piece of the puzzle together.


// somewhere in globals
IDirect3DDevice9* pD3DDevice9 = NULL;

  // back in InitD3D
  if (FAILED(pD3D9->CreateDevice(D3DADAPTER_DEFAULT, 
                                 D3DDEVTYPE_HAL, hwnd,
                                 D3DCREATE_SOFTWARE_VERTEXPROCESSING,
                                 &d3dpp, &pD3DDevice9)))
  {
    return E_FAIL;
  }
  return S_OK;
}

Let's Put Something On the Screen Already!

Now that we've finished creating the device we can render something.


void Render(void)
{
  pD3DDevice9->Clear(0, NULL, D3DCLEAR_TARGET,
                     D3DCOLOR_XRGB(0, 0, 255), 1.0f, 0);

  pD3DDevice9->BeginScene();

  // all of your rendering would go here

  pD3DDevice9->EndScene();

  pD3DDevice9->Present(NULL, NULL, NULL, NULL);
}

This Render function starts by clearing the back buffer, using the Clear method.


HRESULT IDirect3DDevice9::Clear(DWORD Count, CONST D3DRECT* pRects,
                                DWORD Flags, DWORD Color, float Z, DWORD Stencil);

Count asks for the number of D3DRECTs that you have passed to pRects. When you pass an array of D3DRECTs to pRects, you can tell the device to clear only those portions of the buffer, as opposed to the entire buffer. Since we have no use for that, we just use 0, tell it that we're not sending any D3DRECTs. pRects is the array of D3DRECTs that you would pass, but we're going to use NULL to indicate that we want to clear the entire buffer.

Flags allows us to specify exactly what we want to clear. We use D3DCLEAR_TARGET, to indicate that we want to clear the render target, which is the back buffer's color. We could also OR it ( | ) with D3DCLEAR_ZBUFFER and/or D3DCLEAR_STENCIL, to clear the z-buffer or the stencil buffer. Color represents the color that we are going to clear the back buffer to. In this case, we use a macro, D3DCOLOR_XRGB, to create a DWORD form of the color (0, 0, 255). If you wanted an alpha value for the color, you can use the macro D3DCOLOR_ARGB, which takes four parameters. Z is what we would clear the z-buffer to if we were using it. The same goes for Stencil. In both cases, we just put in what we would normally use for them, 1.0f and 0, even though the function ignores them.

IDirect3DDevice9::BeginScene allows you to begin rendering to the back buffer. All calls that render onto the screen must be made between BeginScene and EndScene. IDirect3DDevice9::EndScene stops the rendering of polygons onto the buffer.

IDirect3DDevice9::Present presents the back buffer onto the screen. Let's look at the function header:


HRESULT IDirect3DDevice9::Present(CONST RECT* pSourceRect,
                                  CONST RECT* pDestRect,
                                  HWND hDestWindowOverride,
                                  CONST RGNDATA* pDirtyRegion);

pSourceRect and pDestRect must both be NULL because we used D3DSWAPEFFECT_DISCARD. hDestWindowOverride is the window that the device will present the back buffer to. Since we set it to NULL, it uses the window that we used to create the device. The last parameter, pDirtyRegion, is no longer used by the function and should be set to NULL.

Now that we've had our fun, we have to release the interfaces.


void DestroyD3D(void)
{
  if (pD3DDevice9)
  {
    pD3DDevice9->Release();
    pD3DDevice9 = NULL;
  }

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

Remember that since Direct3D is COM-driven, we have to release the interfaces in the opposite order that we created them. We also set them to NULL afterwards so we don't try to use them since they're already released.

Well, That's It!

It's been fun, but that's all I have to say for now. I'm including the source code for this article for you to browse, but remember that the point of this article is so that you can write the code yourself. I hope you learned something from this article and I look forward to writing more.

References:

Special Effects Fame Programming With DirectX, by Mason McCuskey

Tricks of the Windows Game Programming Gurus, by Andr? LaMothe

"DirectX vs. OpenGL: Which API to Use When, Where, and Why", GameDev.net, by Promit Roy

DirectX Documentation

MSDN

Cancel Save
0 Likes -1 Comments

Comments

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