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:
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;}
Here's a diagram of what the code does:
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:- Register the window class, which is a template for a window
- Determine the size of the window to create
- Create a window using the previously registered class (template)
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; }
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;
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 class name to use as the window template - we specify the same class name we registered above here
- 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.
- The window style - we want to pass the same flags we used to call AdjustWindowRect here
- 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.
- 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.
- 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".
- The menu to use - Since we have no menu on our window, we set this to NULL too.
- 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.
- 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.
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);}
- 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.
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);}
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:
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):
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:
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.
Looking at some of the articles on GDNet, I don't think it's really too long, but it's certainly not one of the shortest ones. So, unless a few people think otherwise, I'll just leave it as one article.