Sign in to follow this  
  • entries
    557
  • comments
    1237
  • views
    422058

Untitled

Sign in to follow this  

630 views

Tutorial 1 is done. It's quite a bit longer than I'd like for a tutorial that seems to do very little, but it's there at least. Source code can be downloaded from Here.

Any feedback is welcomed, and if anyone sees any errors or typos, please let me know. So, here goes:




Overview

This tutorial describes the layout of our framework, getting Direct3D set up, and drawing a blue screen. Nothing too exciting there, I'll admit, but it's a first step. You'll need the latest DirectX SDK (November 2008 at the time of writing) and Microsoft Visual Studio 2005 or 2008, any version (Visual Studio 2008 Express can be downloaded for free from Here).
This tutorial assumes you have a reasonable grasp of C++, have installed Visual Studio, and the DirectX SDK, and have set up Visual Studio's directories to use the DirectX SDK (Which is usually handled by the installer).
A basic knowledge of Win32 applications is useful, but not necessary - this tutorial will cover Win32 basics, but will not go particularly in depth.

Setting up the Debug Runtimes

The DirectX debug runtimes are the single most useful debugging aid you can have when doing DirectX development. When a DirectX function fails, the debug runtimes will give you a message in Visual Studio's debug output. For example:
Direct3D9: (ERROR) :Multisampling requires D3DSWAPEFFECT_DISCARD. ValidatePresentParameters fails.
D3D9 Helper: IDirect3D9::CreateDevice failed: D3DERR_INVALIDCALL


This tells you the error in plain English, the function that failed, and the error code returned to your application.
The debug runtimes can be turned on or off through the DirectX Control Panel, which is in Start Menu -> Programs -> DirectX SDK -> DirectX Utilities -> DirectX Control Panel.

You should see something like the following:
DirectX Control Panel
You want to make sure that "Use Debug Version of Direct3D 9" is selected, and the "Debug Output Level" slider is set to "More". If the option to use the debug runtimes is greyed out, then you probably have an out of date SDK and need to update.

WinMain

Open up the Visual Studio project (Tutorial01_2008 for VS2008, or Tutorial01_2005 for VS2005), and open Main/Main.cpp. This file contains WinMain, which is the entry point for Windows applications. It's provided here for reference:

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE, LPSTR, int)
{
// Create window
D3DWindow wnd;
if(wnd.Create(hInstance))
{
// Main loop
MSG msg;
for(;;) // "forever"
{
// Process all pending window messages
while(PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}

// Render
if(wnd.Tick())
break;
}
}
wnd.Destroy();

// See if there's any error messsage pending
const std::wstring& strError = wnd.GetError();
if(!strError.empty())
{
MessageBox(NULL, strError.c_str(), L"Error", MB_OK | MB_ICONERROR);
}

return -1;
}


The WinMain function creates a D3DWindow class instance, calls Create() on it, and then enters an infinite loop. Inside that loop, the code processes all pending window messages using the standard window message pump, and then calls D3DWindow::Tick to do the actual rendering. If the Tick function returns true, the code breaks out of the loop, shows any errors that the window may have pending, and then exits.

Here's a diagram of what the code does:
Window message pump

When a Windows application has created a window, the Operating System sends messages to the application via the message queue to inform it of events like the user moving the mouse over the window, clicking any of the minimize, maximize or close buttons, and so on. Each thread has its own message queue, and messages pertaining to any window the thread has created go into that message queue. In our code we only have one thread (The main thread), so we only have one message queue.
The application is responsible for pulling messages out of the message queue and processing them. This is done with the PeekMessage or GetMessage functions. The PeekMessage function polls the message queue to see if there's any messages pending, returns TRUE if there is and optionally removes the message from the queue (Which is what the PM_REMOVE parameter does). GetMessage on the other hand will hang until a message arrives. This is good for GUI applications and tools, because the application doesn't use up any CPU time while it's waiting for messages. However, since we want to constantly be rendering and doing other things, we don't want to block, so we use PeekMessage.
Next up, TranslateMessage is called. The details of this don't matter too much, this function checks for the user pressing a key down, sees if any keys like shift are held at the time, and converts the key to upper case or shifted.
Finally, DispatchMessage is called, which dispatches the message to the window procedure associated with the window the message is for. This ends up calling the StaticWndProc function in our code (Which we'll cover shortly).

Notice that there's a double loop here, so all window messages are processed before one tick of the D3DWindow class. If only one window message was processed, then there's a possibility that the window's Tick function could cause another window message to be generated, which would mean that the window message queue would eventually fill up and in the worst case crash or use up large amounts of memory, and in the best case this would cause the window to lag when you drag it around, minimize it, and so on.

Creating the window

There are two main steps to creating a window using the Win32 API:
  1. Register the window class, which is a template for a window
  2. Determine the size of the window to create
  3. Create a window using the previously registered class (template)
In D3DWindow.cpp, the D3DWindow::Create function is responsible for both of the above steps. Let's look at how to register a window class...

Registering the window class


bool D3DWindow::Create(HINSTANCE hInstance)
{
// Register window class
WNDCLASSEX wc;
memset(&wc, 0, sizeof(wc));
wc.cbSize = sizeof(wc);
wc.style = CS_HREDRAW | CS_VREDRAW;
wc.lpfnWndProc = StaticWndProc;
wc.hInstance = hInstance;
wc.hIcon = LoadIcon(NULL, MAKEINTRESOURCE(IDI_APPLICATION));
wc.hCursor = LoadCursor(NULL, MAKEINTRESOURCE(IDC_ARROW));
wc.lpszClassName = s_szWindowClassname;
wc.hIconSm = wc.hIcon;
if(!RegisterClassEx(&wc))
{
m_strError = L"RegisterClassEx() failed. Error: " +
Util::Win32ErrorToString(GetLastError());
return false;
}


First we declare a WNDCLASSEX struct, set it to zero with memset, and then initialize the cbSize member of the struct (The cb prefix stands for "Count of Bytes") to the size of the WNDCLASSEX struct. A lot of Win32 structures have the structure's size as the first member variable, this is for two reasons. Firstly, it allows the OS code to be pretty sure you passed it a valid struct. If you pass it a pointer to some gibberish memory or an uninitialized struct, then the first 4 bytes indicating the size of the struct will be gibberish, and won't match the OS's idea of how big the struct should be. The second reason is that this allows future versions of Windows to extend the struct and add extra information at the end. The newer version of Windows will see that the size corresponds to the size of an "extended" struct, so it knows to read the extra data at the end. If an older application uses an older version of the struct which is smaller, the OS can see that the size matches an older format, so it won't try to use the extra data (Which would be gibberish memory).
The next thing we set is the style flags for this window class. CS_HREDRAW | CS_VREDRAW just means that we want to redraw the window when the width or height changes.
After that we have a pointer to our window procedure. This is the function that will be called when our app calls DispatchMessage from WinMain with a message referring to our window. There's two things to note here, first that the function must be a static or global function (I.e. not a member function), and secondly that you must never cast the function pointer here. A lot of tutorials will have code similar to the following:
wc.lpfnWndProc = (WNDPROC)WndProc;

Doing that is an extremely bad idea. If the code doesn't compile without that cast, then you're effectively telling the compiler "Look, shut up. I know what I'm doing". The OS will try to call your window proc assuming that it matches the WNDPROC prototype (Which is a correct window procedure function pointer), but because it doesn't you're likely to crash your application (if you're lucky) or corrupt the stack and have some strange behaviour that will be pretty difficult to find the cause of (If you're unlucky). If the code does compile without the cast then there's no point in having it, and if you accidently change the arguments, return type or calling convention of your window procedure you'll end up with a crash or stack corruption.
So don't cast function pointers!
We pass the address of our StaticWndProc function, and work some magic to get that from a static function to a member function (Which we'll see shortly).
After the window procedure function address, we have to pass in a HINSTANCE. This is the handle to the application that owns the window class. Since we just want the window class to be used from our own application, we pass the handle of our application we got from WinMain here. This allows you to have two applications, both using the same window class name, because application A will only be able to see the classes registered by application A or any DLLs it has loaded, and application B will only be able to see the classes registered by application B or any DLLs it has loaded.
The next to variables we fill in are the images to use for the icon in the top left of the window, and the cursor to use when the mouse is over the window. We don't want anything fancy for this tutorial, so we just use the LoadIcon and LoadCursor functions to get the standard "Windows application" icon and the standard arrow cursor.
The window class name comes next; this is the name of this window class which will be referred to when we create the window, and finally the small version of the window icon.
The icon specified in the hIcon variable is actually used when Windows needs a large icon, like when you Alt+Tab between windows, and the hIconSm icon is displayed when Windows needs a small icon, such as to display on the task bar or in the top left of the window. Windows will automatically resize icons for us, so we can just use the same icon for large and small, and let Windows take care of the resizing for us.

Now we've initialized the WNDCLASSEX struct, we can register it with Windows, through the RegisterClassEx function.
If RegisterClassEx fails for whatever reason (For instance, if another application has registered the same window class name globally), then the error message is stored in the m_strError member variable, we unregister the window class with a call to UnregisterClass, and the function returns false, which will cause the code in WinMain to bail out and display this error message via a standard Windows MessageBox call.
The Util namespace simply contains a couple of functions for converting Windows and DirectX error codes to human readable strings, feel free to have a look at the implementation for them in Util.cpp if you're curious how they work.

Determining the window size

Once the window class is registered, we use AdjustWindowRect to determine the window size based on the client area size:

// Get the window size for the requested client area size
DWORD dwStyle = (WS_OVERLAPPEDWINDOW | WS_VISIBLE) & ~WS_THICKFRAME;
SIZE sizeWindow;
RECT rc;
rc.top = rc.left = 0;
rc.right = s_nWindowWidth;
rc.bottom = s_nWindowHeight;
AdjustWindowRect(&rc, dwStyle, FALSE);
sizeWindow.cx = rc.right - rc.left;
sizeWindow.cy = rc.bottom - rc.top;


This is also an important step that some tutorials fail to include. The size passed to CreateWindow is the size of the window, which includes the title bar, a menu if the window has one, and the menu borders. The actual drawable area of the window is called the client area, and that's the size of area that D3D draws to.
Window size vs Client area
If you miss out this step, then you'll end up with a window that's, say 640x480, but a client area that might be 634x454. That means that when D3D draws to the window, it has to shrink its image to fit it in the window. That makes is slightly lower performance, but more importantly means that you can get strange graphical artefacts that you'll spend ages tearing your hair out trying to find the source of, particularly when you try drawing either lines or points, which are one pixel wide on the backbuffer - meaning they'll vanish in between screen pixels.
The AdjustWindowRect function takes a RECT struct indicating the desired client area size, a combination of flags indicating the window style - how it looks; the above value just means a "normal" window, which defaults to visible and doesn't have a thick frame that you can drag with the mouse to resize, and a boolean indicating if the window has a menu or not.

Creating the window

Now we know the size of the window to create, we go ahead and create it with a call to CreateWindow:

// Create window
m_hWnd = CreateWindow(s_szWindowClassname, s_szWindowTitle, dwStyle, CW_USEDEFAULT,
CW_USEDEFAULT, sizeWindow.cx, sizeWindow.cy, NULL, NULL, hInstance, this);
if(!m_hWnd)
{
m_strError = L"CreateWindowEx() failed. Error: " +
Util::Win32ErrorToString(GetLastError());
UnregisterClass(s_szWindowClassname, hInstance);
return false;
}

// Init D3D device
if(!InitD3DDevice(sizeWindow))
{
Destroy();
return false;
}

// Done - copy HINSTANCE variable
m_hInstance = hInstance;
return true;
}


The CreateWindow function takes the following parameters:
  1. The class name to use as the window template - we specify the same class name we registered above here
  2. The window title - this is the text shown on the window title bar and as a tooltip when you hover over the window on the taskbar.
  3. The window style - we want to pass the same flags we used to call AdjustWindowRect here
  4. The window X and Y position - CW_USEDEFAULT tells Windows to place the window wherever it wants, this usually means that newly created windows will be cascaded from near the top left corner of the desktop.
  5. The window width and height - as mentioned before, CreateWindow takes the size of the window, not the client area. So we pass in the size of the window we obtained from AdjustWindowRect earlier.
  6. The parent window - Since you can have a hierarchy of windows, the OS needs to know what window is the owner of this window. Since we're creating a top level window, we just set this to NULL to mean "no parent".
  7. The menu to use - Since we have no menu on our window, we set this to NULL too.
  8. The handle to the process containing the window class - this is the same HINSTANCE value we passed to RegisterClassEx, so the OS can find the window class data.
  9. An optional user data parameter - This is the value that is picked up in the WM_CREATE (And WM_NCCREATE) message. We set this to the address of this class for reasons we'll see in a moment.
If CreateWindow succeeds, it returns a handle to the new window, which we store. If it fails, then it returns null, so we log the error and return.

The window message handler

Lets have a look at the StaticWndProc function, which is the function called by Windows:

LRESULT CALLBACK D3DWindow::StaticWndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
// Get pointer to window
D3DWindow* pParent;
if(uMsg == WM_NCCREATE)
{
// This is the WM_NCCREATE message, so we can grab the "Extra" parameter and save
// it as extra data for this HWND handle.
pParent = (D3DWindow*)((LPCREATESTRUCT)lParam)->lpCreateParams;
SetWindowLongPtr(hWnd, GWLP_USERDATA, (LONG)(LONG_PTR)pParent);
}
else
{
// See if we have a stored pointer for this HWND handle. If not, then this is a
// message that's come along before WM_NCCREATE, so just do default handling.
pParent = (D3DWindow*)(LONG_PTR)GetWindowLongPtr(hWnd, GWLP_USERDATA);
if(!pParent)
return DefWindowProc(hWnd, uMsg, wParam, lParam);
}

// Store HWND here. If the message proc is called during the CreateWindow call, then
// the HWND won't have been saved yet, but the WndProc() function might need it.
pParent->m_hWnd = hWnd;
return pParent->WndProc(uMsg, wParam, lParam);

}


When Windows has a message for us, it passes us 4 parameters:
  • HWND hWnd - the handle to the window this message pertains to.
  • UINT uMsg - the ID of this message.
  • WPARAM wParam and LPARAM lParam - Two variables, the meaning of which depends on the message being processed.
To keep things nice and object orientated, we want to have the window represented by a C++ class; the D3DWindow class. Unfortunately, the Win32 API is a C API, and can't deal with C++ member functions like that. What we need to do is give the Win32 API a normal function or a static member function, and have that static function call a member function for us, acting as a proxy. To do that, the static function needs to have a pointer to the class, so it can call the non-static member function. We could do this by using a global variable or a static member variable, but that means we can only have one instance of the window at once, which might be limiting. Fortunately, the Win32 API provides us with a way to associate user data with the HWND window handle which refers to our window, and which is passed to the window procedure, via the GetWindowLongPtr and SetWindowLongPtr functions. We can store a pointer to the D3DWindow class in that extra data segment, then pull it out in the static function, and call the member function on it.
The first thing the StaticWndProc function does is see if the message being received is the WM_NCCREATE message. This is one of the fist messages received, and the lParam parameter is a pointer to a CREATESTRUCT structure, which contains the user data parameter that we passed in the last parameter to CreateWindow. So, we get that user data parameter, cast it to a D3DWindow pointer (Since it was the this pointer we put in there), and then store it using the SetWindowLongPtr function.
Similarly, if the message isn't WM_NCCREATE, we see if we have the pointer to our D3DWindow class stored and if not we call the DefWindowProc function. This function is used for default handling of any window message, so we don't have to include functionality for every single window message the OS can send us. In fact, you can even specify DefWindowProc as the WNDPROC variable in the window class structure if you really want, although then you don't have any control over any window messages, and won't know when your window is closed for a start.
If we do have a pointer to the D3DWindow class, then we store the window handle, and call the non-static version of the window message handler. We need to store the window handle, because there's a few messages that are processed within the CreateWindow function (Such as WM_NCCREATE and WM_CREATE), and we only store the return value of CreateWindow after it's returned.
The non-static window message handler isn't particularly involved, but it's very easy to add support for more messages, which is good to know. Let's take a look at the WndProc function:

LRESULT D3DWindow::WndProc(UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch(uMsg)
{
case WM_DESTROY:
m_bQuit = true;
break;
}

return DefWindowProc(m_hWnd, uMsg, wParam, lParam);
}


This function catches the WM_DESTROY notification and sets the m_bQuit member variable to true. For all other messages received, we don't care, so we just call DefWindowProc and return whatever it returns.
The WM_DESTROY notification gets sent to our window procedure when the OS destroys the window, so this is when the user has closed the window.

Preparing to set up Direct3D

Finally, once the window is created, the InitD3DDevice function is called, which we'll cover in a moment. If this function succeeds, we store the HINSTANCE handle we were passed and return back to WinMain to process the window messages and call our tick function once per frame.

Setting up Direct3D


Now that we've got all of the windowing code out of the way, we can get on with the fun stuff; the Direct3D setup code. So, the InitD3DDevice function:

bool D3DWindow::InitD3DDevice(const SIZE& sizeBackBuffer)
{
// First, get a D3D pointer
m_pD3D = Direct3DCreate9(D3D_SDK_VERSION);
if(!m_pD3D)
{
m_strError = L"Direct3DCreate9() failed. Error: " + Util::DXErrorToString(GetLastError());
return false;
}

// Grab the current desktop format
D3DFORMAT fmtBackbuffer;
D3DDISPLAYMODE mode;
HRESULT hResult = m_pD3D->GetAdapterDisplayMode(D3DADAPTER_DEFAULT, &mode);
if(FAILED(hResult))
{
m_strError = L"GetAdapterDisplayMode() failed. Error: " + Util::DXErrorToString(hResult);
m_pD3D->Release();
m_pD3D = NULL;
return false;
}
fmtBackbuffer = mode.Format;

// Need to see if this format is ok as a backbuffer format in this adapter mode
hResult = m_pD3D->CheckDeviceFormat(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL,
mode.Format, D3DUSAGE_RENDERTARGET, D3DRTYPE_SURFACE, fmtBackbuffer);
if(FAILED(hResult))
{
m_strError = L"Unable to choose a display format!";
m_pD3D->Release();
m_pD3D = NULL;
return false;
}

// Get capabilities for this device
D3DCAPS9 caps;
hResult = m_pD3D->GetDeviceCaps(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, &caps);
if(FAILED(hResult))
{
m_strError = L"GetDeviceCaps() failed. Error: " + Util::DXErrorToString(hResult);
m_pD3D->Release();
m_pD3D = NULL;
return false;
}

// Fill in present parameters
m_thePresentParams.BackBufferWidth = sizeBackBuffer.cx;
m_thePresentParams.BackBufferHeight = sizeBackBuffer.cy;
m_thePresentParams.BackBufferFormat = fmtBackbuffer;
m_thePresentParams.BackBufferCount = 1;
m_thePresentParams.MultiSampleType = D3DMULTISAMPLE_NONE;
m_thePresentParams.MultiSampleQuality = 0;
m_thePresentParams.SwapEffect = D3DSWAPEFFECT_DISCARD;
m_thePresentParams.hDeviceWindow = m_hWnd;
m_thePresentParams.Windowed = TRUE;
m_thePresentParams.EnableAutoDepthStencil = FALSE;
m_thePresentParams.AutoDepthStencilFormat = D3DFMT_UNKNOWN;
m_thePresentParams.Flags = 0;
m_thePresentParams.FullScreen_RefreshRateInHz = 0;
m_thePresentParams.PresentationInterval = D3DPRESENT_INTERVAL_DEFAULT;

// See if hardware vertex processing is available
DWORD dwFlags;
if(caps.DevCaps & D3DDEVCAPS_HWTRANSFORMANDLIGHT)
dwFlags = D3DCREATE_HARDWARE_VERTEXPROCESSING;
else
dwFlags = D3DCREATE_SOFTWARE_VERTEXPROCESSING;

// Create the device
hResult = m_pD3D->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, m_hWnd,
dwFlags, &m_thePresentParams, &m_pDevice);
if(FAILED(hResult))
{
m_strError = L"CreateDevice() failed. Error: " + Util::DXErrorToString(hResult);
m_pD3D->Release();
m_pD3D = NULL;
return false;
}

// All done
return true;
}







Creating a D3D interface
The first thing this function does is get a pointer to an IDirect3D9 interface, and store it in the m_pD3D member variable. This is done by calling the Direct3DCreate9 function, passing in the version requested - which will always be D3D_SDK_VERSION to indicate you want to use the same version of D3D as the SDK you have installed, unless you're doing something strange and exotic. If this function fails (I.e. if D3D9 is not properly installed), then Direct3DCreate9 returns a null pointer, and our function stores the error and bails out.

A bit about pixel formats
The next bit requires a little explanation. When you set up Direct3D, you have to ask for a particular format for everything to be rendered in. These days, it's almost always 32-bit XRGB, which means that every pixel on the screen requires 32-bits (4 bytes) of memory, with 8 bits being unused (For padding), 8 bits for the red channel, 8 bits for green, and 8 bits for blue:
A 32-bit pixel
Older graphics cards might prefer a 16-bit mode, where there's usually 5 bits for the red channel, 6 bits for green, and 5 for blue (Since the human eye is more sensitive to green):
A 16-bit pixel
To add more confusion, other graphics cards might use a 24-bit format, which is 8 bits for each of the red, green and blue channels, with no extra padding. However, there's not really anything to stop graphics cards from wanting even more exotic formats.
So how do we know what format to use? Well, we could just assume that the best format is 32-bit XRGB, but that means we can't support older cards, or possibly even newer cards - there might be a new graphics card that comes out in 5 years time that can't do 32-bit modes, and can only do 128-bit. Or, we could try querying the graphics card to ask it what graphics modes it supports, but that'd require a fair chunk of code. We'll do that in a later tutorial for completeness, but for now there's an easy way out - just use whatever format the desktop is in. We know that format works, since that's the format the user is using.
D3D uses the D3DFORMAT enumeration to describe a pixel format, which describes the number of bits used for red, green and blue, and the order they come in.

Getting the desktop pixel format
The IDirect3D9::GetAdapterDisplayMode function is used to get the display mode that a particular graphics adapter in. "Wait, graphics adapter?" Yes, D3D supports multiple graphics adapter. A graphics adapter is essentially a way to interface with a monitor. If a PC has two graphics cards in it, you can talk to either card by doing various things to adapter indices 0 and 1, and most dual monitor setups are handled by one card exposing two adapters as well. D3DADAPTER_DEFAULT refers to the primary monitor, which is what you'll usually use.
So, we call IDirect3D9::GetAdapterDisplayMode, which fills in a D3DDISPLAYMODE structure for us, and returns an HRESULT type to tell us if the function succeeded or failed. Most DirectX functions will return an HRESULT to indicate success or failure. Some tutorials erroneously check the return value against D3D_OK, which is the standard "Everything is ok" return value. However, an HRESULT contains more than just success or failure, it also contains the error code. More than that, it can contain the success code too. Yep, that's right - there's multiple success codes. Some DirectX functions return values other than D3D_OK to indicate success, but with some warnings. For instance, there's a DirectInput function which is used for acquiring access to a DirectInput device, which returns DI_OK for success, or S_FALSE if the application already had access to the device - I.e. "The function succeeded, but didn't do anything". Anoyingly I can't think of any Direct3D functions that return a success code other than D3D_OK (Feel free to E-mail me if you know of any :)), but it's good practice to never directly test against D3D_OK. Instead, DirectX provides us with two macros, SUCCEEDED and FAILED, which evaluate to true or false depending on whether or not the HRESULT is a success or a failure code.
So anyway, if the function fails, we log the error, free the D3D interface pointer, and bail. If it succeeds, we grab the display format from the D3DDISPLAYMODE structure and store it in the fmtBackBuffer variable. For anyone who's interested, the structure is defined as the following:

typedef struct _D3DDISPLAYMODE
{
UINT Width;
UINT Height;
UINT RefreshRate;
D3DFORMAT Format;
} D3DDISPLAYMODE;







So, if for any reason you want to find out what resolution or refresh rate the primary monitor is using, you can also grab that from this structure.

Checking if the format is usable
Once we know what format the desktop is in, we need to check that this format is actually usable as a backbuffer format - which is the format that D3D will render in. To do that, we call the IDirect3D9::CheckDeviceFormat function. This function is used for checking all sorts of formats in all sorts of situations, hence the reasonably large number of parameters. Put simply, the parameters passed tell D3D, in this order:
  • Check this format for the default graphics adapter.

  • Check for hardware support rather than software emulation.

  • The desktop will be in the display format described by mode.Format.

  • We want to know if the format is suitable as a render target (backbuffer).

  • We want to know if the format is suitable for a surface (As supposed to a texture or other D3D resource type).

  • The format we're checking is fmtBackbuffer.


If this format isn't acceptable for whatever reason, we record the error and bail out. I can't actually think of any reason why this function would fail, but again, it's good practice, and we'll need to check this for later tutorials.

Getting the device caps
Next up, we ask the graphics card (Well, the driver) what capabilities, or caps it supports. To do that, we call the IDirect3D9::GetCaps function, which gets the capabilities for a particular graphics adapter. We'll use these caps in a moment.

Filling in the present parameters
We know what format we're going to use for rendering, so we can start to fill in the D3DPRESENT_PARAMETERS structure. This structure contains pretty much everything D3D needs to know in order to get set up for rendering. Let's go over what each of the parameters means:
  • BackBufferWidth: The width of the backbuffer we want D3D to create.

  • BackBufferHeight: The height of the backbuffer we want D3D to create.

  • BackBufferFormat: The pixel format of the backbuffer we want D3D to create.

  • BackBufferCount: The number of backbuffers we want D3D to create for us. 1 backbuffer means a backbuffer and also what's currently being displayed on the screen, so two buffers in total, which is known as double buffering. If you want, you could set this number to 2 to enable triple buffering, but that's probably overkill for us just now and won't make any difference to performance. For a description of triple buffering, have a look at the
    Wikipedia Article.

  • MultiSampleType: The type of multisampling we want. Multisampling may be covered in a later article, but for now all you need to know is that we're not using it. Have a read over the Wikipedia Article on multisampling if you're particularly interested.

  • MultiSampleQuality: The quality for multisampling. As above, we can ignore this for now.

  • SwapEffect: How we want D3D to get the backbuffer onto the frontbuffer for display. D3DSWAPEFFECT_DISCARD means "Do whatever you like, just do it fast". The down side of that is that D3D will trample all over the backbuffer once it's done that, because one method it could use would be swapping the pointers to the front and back buffer, which is extremely fast, but means the backbuffer is now the frontbuffer, and may contain gibberish, depending on what the driver does internally. The debug runtimes will enforce this by filling the backbuffer to green and magenta on alternating frames, to help you spot bugs caused by you assuming that the backbuffer contains valid data.

  • hDeviceWindow: The window D3D should render to.

  • Windowed: TRUE if we want D3D to run in windowed mode, FALSE to run in fullscreen mode. We only want windowed mode just now, because fullscreen mode requires more things to check and is much harder to debug.

  • EnableAutoDepthStencil: Specifies whether or not we want D3D to create a depthbuffer and / or stencil buffer for us. We don't want or need this just yet, we'll come back to this in a later article. So for now, we'll set this to FALSE to disable the depth and stencil buffers.

  • AutoDepthStencilFormat: The format of the depth / stencil buffer. Since we're not using one, this parameter is ignored so we can set it to D3DFMT_UNKNOWN.

  • Flags: Any additional flags we want to pass to D3D. We don;t have anything interesting to say, so we keep quiet and set this to 0 to mean "No flags".

  • FullScreen_RefreshRateInHz: The refresh rate to use if we're fullscreen. 0 means "Default", and is what we have to specify when we're running in windowed mode.

  • PresentationInterval: How frequently to present the backbuffer and update the screen. We can tell the graphics card to present as fast as possible, we can tell it to present whenever there's a vertical blank, or we can tell it to present every 2nd, 3rd or 4th vertical blank. We set this to D3DPRESENT_INTERVAL_DEFAULT to present every vertical blank, and not use too much processing power to do it. Presenting every 2nd, 3rd or 4th vertical blank isn't supported by all cards, and probably isn't that useful. Presenting as quickly as possible is another alternative, but would use up 100% of the CPU and could cause tearing. See the Wikipedia Article on tearing if you'd like to read about tearing.



Checking for hardware vertex processing support
Now the present parameters are set up, we want to check to see if the graphics card can do hardware vertex processing. This means that the conversion from 3D coordinates to 2D screen coordinates is done on the GPU rather than the CPU, which is much more efficient. Graphics cards prior to the GeForce 2 range of cards are unable to perform hardware vertex processing on the GPU, so this needs to be done by D3D on the CPU instead, which isn't as efficient as doing it on the GPU, but it's still pretty quick. Depending on the capabilities of the graphics card and driver, we set dwFlags to D3DCREATE_HARDWARE_VERTEXPROCESSING or D3DCREATE_SOFTWARE_VERTEXPROCESSING.

Creating the D3D device
Finally, it's time to create the Direct3D device. This is the primary interface between your application and the graphics card. This is also the step that's most likely to go wrong, since D3D has a lot of complicated things to do internally, especially if it needs to change into fullscreen mode. If there's any D3D function you check the return value of, IDirect3D9::CreateDevice is the most critical. If this function fails and you don't notice, it's not going to fill in the m_pDevice pointer, and your application will crash spectacularly. So, we check the return value here, and if the function fails, we clean up and bail out. If it succeeds, we report back to the Create function with our success.

Rendering!


The window is created, the Direct3D device is created, and we're ready to render. So let's take a look at the Tick function:

bool D3DWindow::Tick()
{
// See if the window is being destroyed
if(!IsWindow(m_hWnd))
return m_bQuit;

// Test cooperative level first
HRESULT hResult = m_pDevice->TestCooperativeLevel();
if(FAILED(hResult))
{
m_strError = L"TestCooperativeLevel() failed. Error: " + Util::DXErrorToString(hResult);
m_bQuit = true;
return m_bQuit;
}

// Render
m_pDevice->Clear(0, NULL, D3DCLEAR_TARGET, D3DCOLOR_XRGB(128, 128, 255), 0.0f, 0);

// Present
hResult = m_pDevice->Present(NULL, NULL, NULL, NULL);
if(FAILED(hResult))
{
m_strError = L"Present() failed. Error: " + Util::DXErrorToString(hResult);
m_bQuit = true;
return m_bQuit;
}

return m_bQuit;
}







Checking if the window is valid
The first thing this function does is call IsWindow to check that the window is still valid. If the application is shutting down, then the window might not be valid and D3D will be unable to render to it, which will cause various functions to fail. So, if the window isn't valid we bail.

Checking for loss of the device
Once we know that the window is still valid, we see if the D3D device is lost, by calling the IDirect3DDevice9::TestCooperativeLevel function. A D3D device can become lost if the video card gets reset. This is usually caused by the user Alt+tabbing away from your application if you're running in fullscreen mode, but it can also be caused by changing the desktop resolution if you're running in windowed mode. It's worth noting that in Windows Vista, it's rather difficult to cause the device to become lost. We're going to cover lost devices later, for now we just see if the device is lost or otherwise invalid, and if so we store the error, set the quit flag and bail out, which will cause WinMain to shut down the window and show the error.

Rendering
Now we know we're good to go for this frame, we can do the rendering. For now we're doing something simple and just clearing the backbuffer to a super intelligent shade of the colour blue. We do this with the IDirect3DDevice9::Clear function, which can be used to clear the backbuffer, depth buffer, and the stencil buffer at the same time, and can be used to clear a subsection of all three. We're not using a depth or a stencil buffer, so we just pass D3DCLEAR_TARGET as the third parameter, and we want to clear the whole buffer rather than a subregion, so we pass 0 and NULL as the first and second parameters respectively. The fourth parameter is the colour to clear the backbuffer to, as a D3DCOLOR type. The D3DCOLOR_XRGB macro can be used to make a D3DCOLOR value from a red, green and blue value between 0 and 255. The last two parameters are the values to clear the depth and stencil buffers to, which are ignored since we're only passing D3DCLEAR_TARGET as the third parameter.

Presenting
We're done rendering, so we need to communicate this to D3D so it can present the backbuffer. This is done through the cunningly named IDirect3DDevice9::Present function. The parameters to this function will usually all be NULL, but can be used to present a subregion of the backbuffer, or to present to a different window.

Done!
Everything is done, so we return the m_bQuit flag to WinMain, so it can know if we want to quit. If you compile and run the code, you should get a screen similar to the following:
Screenshot

Shutting down


The last think we'll cover in this tutorial is shutting down. Cleaning up after yourself is always important to do, and when using Direct3D it's no different. Failing to shut down properly can cause the desktop to get stuck at the resolution the application is in if it's running in fullscreen mode, can cause odd bugs and slowdowns, and can in extreme cases with buggy drivers, crash the entire system. So it's important to do!
I won't bother listing the code here again, but everything is shut down in the reverse order it was created in. This is another "good practice" thing to do, so we kill the Direct3D device first, then the Direct3D interface, and then finally destroy the window once D3D has been entirely shut down. Destroying the window before D3D is a bad idea, and can also cause some odd problems, particularly in fullscreen mode.

Conclusion


By now you know how to create a window, process messages from the operating system, check the graphics card capabilities, create a Direct3D device, and do some rendering. We've gone over all of the functions used, and discussed the alternative values and reasons for most of them.

If you have any comments about this article, I'd be happy to hear them!




Fin. Like I said, if anyone has any feedback, I'd like to hear it. One thing I was considering was splitting it into two tutorials; one simply dealing with creating the window and pumping messages, and then another dealing with D3D. But then that'd make the first article somewhat dull.

Meh, what do you think?

EDIT 26/01/09: Re-worded some stuff, went into a lot more detail abount the windows message pump and the static and non-static window procedures.
Sign in to follow this  


19 Comments


Recommended Comments

The article looks great to me.. may I ask you why did you choose D3D9 instead of D3D10?

An user looking for a good tutorial to learn D3D programming shouldn't have a large codebase to update. Since Windows7/DX11 are coming, IMHO a proper choice could be to start with Vista/DX10.

Share this comment


Link to comment
While I can't evaluate the content (you and I both know I have no idea), may I make two wee suggestions?

1. At the start when you state the things you presume the reader is sufficient in, I'd link to decent tutorials that do just those things.

2. Put an Index at the start, with jump links to the relevant sections.

Share this comment


Link to comment
Quote:
Original post by undead
The article looks great to me.. may I ask you why did you choose D3D9 instead of D3D10?
I've not used D3D10 myself, and I believe D3D9 still has a few years left in it. I'd consider myself versed in D3D9.

Quote:
Original post by undead
An user looking for a good tutorial to learn D3D programming shouldn't have a large codebase to update. Since Windows7/DX11 are coming, IMHO a proper choice could be to start with Vista/DX10.
This is partially true - Vista, yes, but D3D10 requires a DX10 card, which a lot of people still don't have.
Writing applications for D3D10 would mean that the person writing the code wouldn't be able to give the game / app to any friends running XP or a DX9 card.


Quote:
Original post by Sonnenblume
While I can't evaluate the content (you and I both know I have no idea), may I make two wee suggestions?

1. At the start when you state the things you presume the reader is sufficient in, I'd link to decent tutorials that do just those things.

2. Put an Index at the start, with jump links to the relevant sections.
Both good points, I'll add them soon. I was already considering #2, I just forgot to do it before putting it on my site.

Share this comment


Link to comment
NICE journal!!! i think it would also be nice if you put this also in the resource and article section that way it can be easily accessed :D
KEEP UP THE GOOD WORK!!!

Share this comment


Link to comment
Nice entry!

Also, I didn't know about the size of window vs available client area! I immediately went to my game, did some debugging and figured out if we were having that issue (since we were getting interesting graphics artifacts on some machines a few weeks ago). Due to our Window style though, we didn't run into that issue though, but I changed our code so if anyone changes the style in the future, nothing weird will happen. :) I fixed the bug that never happened, woohoo!

Share this comment


Link to comment
While the tutorial itself is good (thorough and explains things well), and your efforts to make it as complete as possible are obvious, IMHO it would be better to write about more advanced stuff. This is simply due to the fact that there are numerous tutorials/articles/samples about "getting started with direct3D" all over the internet. I think it would be much more useful for the community if you write for the intermediate and advanced levels of Direct3D programming and related issues. In other words, it's physically painful to see someone of your caliber wasting valuable efforts on an area that has been over-saturated already.

I mean come'on, screw beginners and let's get on with the juicy stuff. Tell us about things like good engine design, how to structure code to present a neat API to the engine's clients, how to design for extensibility using plugins, how to manage effects and materials, how to arrange shaders into tiers, how to separate material shaders and lighting schemes, how to make good use of multithreading, how to write an efficient post-processing framework, how to write an editor for the engine, how to handle dynamic key-action assignment, how to store geometry for optimal visibility culling, how to handle animations and cutscenes, how to incorporate physics and sound engines, how to make a AAA title in two weeks! Well, maybe not the last bit.

If you're planning on starting from the beginning and then moving on to more advanced stuff, then I think by the time you finish your seventh or so tutorial, you will have grown tired or bored and you will drop it. And we will be left with yet another bunch of tutorials that show you everything up to how to load a texture without using D3DXCreateTextureFromFile(). Again, this is my personal opinion and I realize that I'm usually not good at phrasing what I mean the right way, so if I have offended you somehow, know that I didn't mean it. I harbor nothing but great respect for you (I've been lurking the forums for quite a long while now and, simply put, I'm a fan of yours!). Happy new year.

Share this comment


Link to comment
Quote:
Original post by hikikomori-san
While the tutorial itself is good (thorough and explains things well), and your efforts to make it as complete as possible are obvious, IMHO it would be better to write about more advanced stuff. This is simply due to the fact that there are numerous tutorials/articles/samples about "getting started with direct3D" all over the internet. I think it would be much more useful for the community if you write for the intermediate and advanced levels of Direct3D programming and related issues. In other words, it's physically painful to see someone of your caliber wasting valuable efforts on an area that has been over-saturated already.

I mean come'on, screw beginners and let's get on with the juicy stuff. Tell us about things like good engine design, how to structure code to present a neat API to the engine's clients, how to design for extensibility using plugins, how to manage effects and materials, how to arrange shaders into tiers, how to separate material shaders and lighting schemes, how to make good use of multithreading, how to write an efficient post-processing framework, how to write an editor for the engine, how to handle dynamic key-action assignment, how to store geometry for optimal visibility culling, how to handle animations and cutscenes, how to incorporate physics and sound engines, how to make a AAA title in two weeks! Well, maybe not the last bit.

If you're planning on starting from the beginning and then moving on to more advanced stuff, then I think by the time you finish your seventh or so tutorial, you will have grown tired or bored and you will drop it. And we will be left with yet another bunch of tutorials that show you everything up to how to load a texture without using D3DXCreateTextureFromFile(). Again, this is my personal opinion and I realize that I'm usually not good at phrasing what I mean the right way, so if I have offended you somehow, know that I didn't mean it. I harbor nothing but great respect for you (I've been lurking the forums for quite a long while now and, simply put, I'm a fan of yours!). Happy new year.
I definitely see what you mean here, but I'd like to get the basics done first.

My reason for starting with the basics is really just so I can link people to my tutorials when I see they've been using DirectXTutorial.com. I do intend to get some more advanced stuff done, I have a few ideas for things like a truetype font renderer (Without using ID3DXFont), sprite sheets / texture manager (Would work well with the truetype font renderer too), and a basic sprite class.

I do see what you mean about getting fed up writing tutorials - I'm particularly bad at starting things and never finishing them - but I'd like to give it a go anyway. I can always jump around and do some more advanced tutorials, and then come back and lay the groundwork for them. That might not make sense at the time, but once they're all written, they should progress naturally in the intended order.

Share this comment


Link to comment
Brilliant!

Ive paid for the premuim membership on dxtut.com and this explantions alot more imo about WinMain and the message loop , 1 thing thats carelessly done on his site is the shaders part , i cant seem to get his shaders tut to run and just crash's

I would really like it if you covererd these topics in the furture tuts

1.meshs / and how to create mesh
mesh bone heirchy for animated meshs
sprites / animated sprites
shaders / pixel shader | vertex shader
render states , sampler states , texstates ,texturestagestate ( enough to make your head go POP_)
particles
camera (epscially first person control)
matrix maths , i need a really good help with length or a vector and normalizing it , still dont understand whats its doing ...

i basically need indepth tuts on all these topics :D

Share this comment


Link to comment
Quote:
Original post by Anddos
I would really like it if you covererd these topics in the furture tuts

1.meshs / and how to create mesh
mesh bone heirchy for animated meshs
sprites / animated sprites
shaders / pixel shader | vertex shader
render states , sampler states , texstates ,texturestagestate ( enough to make your head go POP_)
particles
camera (epscially first person control)
matrix maths , i need a really good help with length or a vector and normalizing it , still dont understand whats its doing ...

i basically need indepth tuts on all these topics :D
They all sound like good ideas, although I'll probably skim over matrix maths rather than go into it (I.e. explain how the functions work, not how matrices work).

Share this comment


Link to comment
Thank you for a great tutorial. It's hard for those that need a tutorial to judge its completeness, as jpetrie has written an excellent piece on. That said, as a person who needs this tutorial it feels very complete, and from what I've seen it's one of the best tutorials on this subject.

Thank you so much for taking the time and effort to do something like this RIGHT, and not just throw out some code and comments from your own learning experience (which most of the other tutorials seem like, to me).

I do hope you keep doing these tutorials, I'll recommend them to everyone in my class. :)

Share this comment


Link to comment
Well Undead, Evil Steve should base his tutorials on the scale of users using which OS and DirectX version... for example, even though Windows 7 has already reached its beta stage and DirectX 11 is coming out users are still mainly all the way back with Windows XP and DirectX 9, so in short would it really make sense for Evil Steve to make any tutorials for DirectX 10 or 11? it wouldn't benefit as many people.

Fissure.

Share this comment


Link to comment
Thanks Evil Steve!

This is exactly the sort of thing I've been looking for. DirectX is not exactly the easiest thing in the world to get into. After going through numerous tutorials, including those on directxtutorial.com, I have gotten quite tired of dealing with the same slow, buggy code. Needless to say, a beginner can become really frustrated in a hurry given all of the poor advice out there. It's nice seeing someone actually do things correct in the first tutorial. In most of the ones I've looked at, they tell you up front that they are going to do things wrong and fix them later...not a good idea in my book.

I do have one question. If I were to make several other classes for different things in the game that would be rendered (maps, the player, creatures, etc.), what is a clean way to give the render function access to those objects? Maybe this is a silly question, but that is what has kept me from encapsulating all of the DirectX stuff in it's own class like you have done.

Share this comment


Link to comment
I found this becaus I was having problems getting mouse coordinates with GetCursorPos() between fullscreen and window mode. It mostly fixes my problem but still in windowed mode the cursor is like two pixels off where it would be in fullscreen. So checking if the mouse is in a button its a couple pixels inside the button before the check is true in windowed mode whereas in fullscreen its right on the edge where it should be.

I could just manually adjust the coordinate but there has to be a better way to fix it.

Share this comment


Link to comment
Sorry if this has already been said, I didn't read everyone's comments. I agree with you about making the tutorials DX9, for compatibility's sake. That said, if you wanted to make it easier for someone using this tutorial to switch to DX10 later on, you could just stick to the programmable pipeline in this tutorial, and omit the fixed function pipeline entirely? Or you could introduce them both and then say that you're only going to use the programmable pipeline seeing as fixed function will eventually fall out of use entirely. Just a thought.

cheers,
metal

Share this comment


Link to comment
Can I ask anyone replying to this post to reply in my most recent journal post please? I tend not to check my older posts nearly as often as I check the more recent ones [smile]

Quote:
Original post by bgoose
I do have one question. If I were to make several other classes for different things in the game that would be rendered (maps, the player, creatures, etc.), what is a clean way to give the render function access to those objects? Maybe this is a silly question, but that is what has kept me from encapsulating all of the DirectX stuff in it's own class like you have done.
What the SDK does, is pass a pointer to the device to each of the classes that needs it, but that's not really ideal.

What I'd do is split D3DWindow into two classes; one for managing the D3D device and rendering, and one for just handling the window and associated messages.
You could then have everything that needs to be rendered take a pointer to the renderer class (rather than the window).
When it comes down to it, all of those classes will need access to the D3D device at some point, unless you go out of your way to avoid it (Which I wouldn't recommend for a beginner).

It's really up to you. I wouldn't worry too much about it though [smile]

Share this comment


Link to comment
Quote:
Original post by bgoose
I do have one question. If I were to make several other classes for different things in the game that would be rendered (maps, the player, creatures, etc.), what is a clean way to give the render function access to those objects? Maybe this is a silly question, but that is what has kept me from encapsulating all of the DirectX stuff in it's own class like you have done.
Sorry for the late reply, I don't tend to view the comments of my older entries that often.

The way this is usually done is with a scene graph - you have one class instance which is a collection of all the objects that can be rendered, usually stored as a vector of pointers to a common base class (The common base class would need at least a virtual Render() function). Then, every game tick you just loop over all the the items in that list and call the Render() function, passing in your LPDIRECT3DDEVICE9 pointer.
A more advanced method would cache render states and sort the scene graph to minimize state changes, etc.

Share this comment


Link to comment

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now