David Joffe's Guide to Programming Games with DirectX

Chapter 1: Introduction to DirectX

1.1 A few quick words about these articles

This document is provided as is, blah blah blah, I am not responsible if it in any way causes harm to you.

The samples in this document are developed for Visual C/C++ MFC applications, but most of the material covered is pretty much MFC-independent.

1.2 What is DirectX?

DirectX is Microsoft's answer to the questions of Game Developers who wanted to know how they were going to write games optimally under Windows95. Games normally require a lot of computing power for their high-speed graphics, and Windows95 was "getting in the way".

DirectX is an API (Application Programming Interface) which provides a lot of functionality that is useful mostly in games programming but in other applications as well. DirectX consists of, at the moment, 5 main sections, as shown in the table below.

1.2.1 DirectX Components

DirectDraw2 dimensional graphics capabilities, surfaces, double buffering, etc
Direct3DA relatively extensively functional 3D graphics programming API.
DirectSoundSound; 3D sound
DirectPlaySimplifies network game development
DirectInputHandles input from various peripherals

1.3 DirectX performance and hardware acceleration

Although the performance of Direct3D in software only is not too shabby at all, DirectX is designed with hardware acceleration in mind. This means that graphics board vendors can create cards that support DirectDraw and Direct3D functionality on board, in hardware; and supply relevant DirectX drivers for the card. Allowing these functions to be performed on board the graphics card frees the CPU (Central Processing Unit) to do other things. Typical Direct3D accelerators would also have at least 2 or preferably 4 or more Megabytes of onboard RAM to store bitmaps (computerised images made up of small dots called "pixels"), textures, sprites, overlays and more.

DirectDraw and Direct3D are built as a relatively thin layer above the hardware, using what is called the DirectDraw "hardware abstraction layer" (HAL). For functionality not provided by a certain card, an equivalent software implementation would be provided through the "hardware emulation layer" (HEL).


Diagram not yet illustrating the DirectDraw/Direct3D HAL.


David Joffe's Guide to Programming Games with DirectX

Chapter 2: Palettes, Gaming concepts, double buffering etc

2.1 Video Modes

Screen modes come in several flavours, based on how many bits are used to store the color of each pixel on the screen. Naturally, the more bits you use per pixel, the more colours you can display at once; but there is more data to move into graphics memory to update the screen.

These modes are available, typically, in the following resolutions:

with 320x200 and 640x480 being the two most popular modes for games at the moment. Monitor's generally have a width 4/3 times their height, so with modes where the number of pixels along the width is 4/3 times the number of pixels along the height, the pixels will have an aspect ratio of 1. That is to say, 100 pixels in one direction should then be the same physical length as 100 pixels in a perpendicular direction. Note that 320/200 does not have this property; so in 320x200 pixels are actually stretched to be taller than they are wide.

2.2 Color theory

Every possible visible color can be made by combining in various proportions the three primary colours Red, Green and Blue. This system is called the RGB system. The Red, Green and Blue values are normally specified in the range 0 to 255 inclusive. So pure bright red would be RGB(255,0,0); purple would be RGB(255,0,255); grey is RGB(150,150,150); black is RGB(0,0,0), etc.

2.2.1 High-color and true-color modes

In high-color and true-color modes, the pixels on the screen are stored in video memory as their corresponding RGB make-up values. For example, if the top left pixel on the screen was green, then (in true-color mode) the first three bytes in video memory would be 0, 255 and 0.

In high-color modes the RGB values are specified using (if I remember correctly) 5, 6 and 5 bits for red, green and blue respectively, so in the above example the first two bytes in video memory would be, in binary: 00000111 11100000.

2.2.2 Palette-based, or "indexed" modes

Urgh.

In the most common of these modes, that is, 8-bit, the programmer has a choice of 28 = 256 different colors that can be displayed on the screen at once. The RGB make-up values for each of these 256 colors are stored in a table of RGB records, each 3 bytes long. Thus, each pixel takes one byte in video memory, and the value of that byte specifies an index into the table (called the "palette") which is used to look up the RGB values to generate the pixel on the screen.

Creating an application around this palette is a pain. But using a palette offers several advantages, which include:

2.2.3 ModeX

ModeX is a special type of mode in which the contents of graphics memory (i.e. what appears on the screen) is stored in a somewhat complex planar format. DirectDraw knows how to write to ModeX surfaces, but the Windows GDI doesn't, so be careful when trying to mix GDI and DirectDraw ModeX surfaces. When setting the DirectDraw fullscreen mode, it is possible to choose whether or not DirectDraw may create ModeX surfaces.

2.3 A few gaming concepts you'll need to know to write games

2.3.1 Bitmaps and sprites

A bitmap is an image on the computer that is stored as an array of pixel values. That's a pretty crappy description. Basically, a bitmap is any picture on the computer. That little "home" picture at the bottom of this page is a bitmap. A sprite is the same thing as a bitmap, except normally it means a bitmap that has transparent areas. Sprites are an extremely important component of games. They have a million and one uses. For example, your mouse cursor qualifies as a sprite. The monsters in DOOM are also sprites. They are flat images with transparent areas that always face you. Note that the sprite always faces you - this doesn't mean the monster is facing you. Anyway, enough said about bitmaps and sprites, I think.

2.3.2 Double buffering and page flipping

Double-buffering is a rather nifty technique that is very important in acheiving fast, smooth animation in your application. It's not the only thing you'll need to achieve this, but it's definitely one of the more important ones. Basically, it works like this. Your game draws what the user is meant to see on the screen onto an off-screen surface. The upshot of this is that the user doesn't have to *watch* the elements of the screen being drawn, as this causes all sorts of icky artefacts, like flickering. Now, only when the image is ready for viewing (or "finished rendering", if you want to speak a bit more technically) is it shown to the user. There are two basic ways of getting this image from the off-screen surface, and onto the screen. The first is to copy the contents from the off-screen surface, into the screen memory. The other is to use hardware page-flipping, which means that you instruct the graphics hardware to refresh the screen from that point on from a different point in memory - typically you let it point to your off-screen surface. Now the area in memory that the screen *was* drawing from becomes your off-screen surface, and when you've drawn to that, you do the page-flip again.

A problem that can arise from this technique is "tearing". Your monitor redraws the image on the screen fairly frequently, normally at around 70 times per second (or 70 Hertz). It normally draws from top to bottom. Now, it can happen that the screen has only drawn half of its image, when you decide to instruct it to start drawing something else, using any one of the two techniques described above. When you do this, the bottom half of the screen is drawn using the new image. This is called tearing, or shearing. A solution exists, however. It is possible to time your page flipping to co-incide with the end of a screen refresh. I'll stop here though, having let you know that it is possible.

2.4 Clipping and DirectDraw clippers

Clipping is the name given to the technique of preventing drawing routines from drawing off the edge of the screen or other rectangular bounding area such as a window. If not performed, the general result could best be described as a mess. In DirectDraw, for example, when using windowed mode; Windows basically gives DirectDraw the right to draw anywhere on the screen that it wants to. However, a well-behaved DirectDraw application would normally only draw into it's own window. DirectX has an object called a "clipper" that can be attached to a DirectDraw surface to prevent it drawing outside of the window.

2.5 DirectDraw surfaces

DirectDraw uses "surfaces" to access any section of memory, either video memory or system memory, that is used to store (normally) bitmaps, texture maps, sprites, and the current contents of the screen or a window.

DirectDraw also provides support for "overlays"; a special type of sprite. An overlay is normally a surface that houses a bitmap with transparent sections that will be "overlaid" on a scene. For example, a racing car game might use an overlay for the image of the cockpit controls and window frame.

The memory a DirectDraw surface uses can be lost in some circumstances, because DirectDraw has to share resources with the GDI. It is necessary for you application to check regularly that this hasn't happened, and to restore the surfaces if it has.

2.6 DirectX return values and error-checking

DirectX functions return an HRESULT as an error-code. Since DirectX objects are based on the COM architecture, the correct way to check if a DirectX function has failed is to use the macros SUCCEEDED() and FAILED(), with the HRESULT as the parameter. It is not merely sufficient to check if, for example, your DirectDraw HRESULT is equal to DD_OK, since it is possible for COM objects to have multiple return values as success values.

2.6 DirectX debugging

When you install the DirectX SDK you get a choice of whether to install the retail version of the libraries, or the debug version. The debug version will actually write diagnostic messages to your Visual C++ TRACE output window, which is very useful. If you want to mainly play DirectX games, install the retail version. If you want to do mainly DirectX development, install the debug version.


David Joffe's Guide to Programming Games with DirectX

Chapter 3: A simple DirectDraw sample

3.1 Setting up DirectX under Visual C/C++

I most likely won't be doing DirectX development under Watcom or Borland C/C++ or Delphi or VisualBASIC etc; so if you want such info included, please send as much info as you can on generally getting DirectX programs to compile in these environments + troubleshooting etc.

Firstly, the directories must be set up so that Visual C/C++ can find the DirectX include files and libraries.

Access the Tools/Options/Directories tabbed dialog.

Select "library directories" from the drop-down list, and add the directory of the DX SDK libraries, e.g. "d:\dxsdk\sdk\lib"

Select "include directories" from the drop-down list, and add the directory of the DX SDK header files, e.g. "d:\dxsdk\sdk\inc".

If you are going to be using some of the DX utility headers used in the samples, then also add the samples\misc directory, e.g. "d:\dxsdk\sdk\samples\misc".

Note: In Visual C/C++ 4.2, the header file directory for the DirectX SDK must be located *before* the default VC++ development include directories, so that the compiler searches the DXSDK directories first. As far as I can tell, the reason for this is that with VC++ 4.2 some of the DirectX was included, but those header files seem to be out of date or something because they don't work.

To use DirectDraw you must tell it which library file the DirectDraw routines are in. Ditto for Direct3D. Go to Build/Settings/Link and in the Object/Library modules box add ddraw.lib to use DirectDraw, and d3drm.lib if you want to use Direct3D Retained mode. Seperate these with spaces.

3.2 The DirectDraw sample

Firstly, heres a screenshot of the small simple sample application we're putting together here.

[Screenshot 1]

The general routine for starting a DirectDraw application is the following:

  1. Set up our global variables
  2. Initialize a DirectDraw object
  3. Set the "cooperative level" and display modes as necessary (explained later)
  4. Create front and back flipping surfaces, and a clipper if necessary
  5. Attach the clipper if you're in windowed mode
  6. Perform flipping. If in full-screen mode, just flip. If in windowed mode, you need to blit from the back surface to the primary surface each frame.

3.3 Setting up global variables: Globals.h and Globals.cpp

We are going to need a number of global variables for our DirectDraw application. Firstly, we need a variable for accessing the DirectDraw object. Also, we'll need a boolean variable to track whether or not we are in full-screen mode, called bFullScreen. Then we need two DirectDrawSurface variables for the primary surface and the back surface which form our double-buffered surface-flipping structure. For windowed mode, we'll need a clipper object to attach to the primary surface to prevent DirectDraw from drawing outside of the window's edges. If using a 256 color mode, we'll probably want a structure to store the palette we are going to use. Then, we need an HWND object to let the clipper know which window's boundaries it should clip to. Finally, we'll need a boolean variable to start and stop animation of the DirectDraw application, once it is initialized. Create a global variable for purposes of accessing the DirectDraw object, and a boolean variable to track whether or not we are in full-screen mode.

These global variables, along with the global functions, I place in a seperate file called Globals.cpp which I add to my project in Visual C/C++. They could all have been members of a class, but I have chosen to make them global for easier access across all modules, to conceptually seperate the DX stuff from the rest of the program code, and to make it simpler to use these functions in non-MFC applications.

LPDIRECTDRAW pDD;                // DirectDraw object
LPDIRECTDRAWSURFACE pDDSPrimary; // DirectDraw primary surface
LPDIRECTDRAWSURFACE pDDSBack;    // DirectDraw back surface
LPDIRECTDRAWPALETTE pDDPal;      // Palette (coming sometime soonish)
LPDIRECTDRAWCLIPPER pClipper;    // Clipper for windowed mode
HWND ddWnd;                      // Handle of window
BOOL bFullScreen;                // are we in fullscreen mode?
BOOL bAnimating;                 // are we animating?

I also make a function to initialize all of these variables:

void InitDirectXGlobals()
{
    pDD = NULL;
    pDDSPrimary = NULL;
    pDDSBack = NULL;
    pDDPal = NULL;
    pClipper = NULL;
    bFullScreen = FALSE;
    bAnimating = FALSE;
}

Here is the general layout of my Globals.h and Globals.cpp files:

Globals.h

#ifndef _GLOBALS_H_
#define _GLOBALS_H_

include <d3drm.h>

extern LPDIRECTDRAW pDD;
...

extern void InitDirectXGlobals();
...

#endif

Globals.cpp

#include "stdafx.h"

#include <ddraw.h>
#include <d3drm.h>

#include "Globals.h"

LPDIRECTDRAW pDD;
...

void InitDirectXGlobals()
{
	pDD = NULL;
...
}

3.4 Initializing the DirectDraw system

Now we need a routine to initialize the directdraw system. The DirectDraw system must be initialized every time you switch between full-screen and windowed mode (I think :) - gimme a bit of time). The DirectDrawCreate function call is used to create a DirectDraw object.

BOOL InitDDraw()
{
    HRESULT hr;

    // Create the DirectDraw object
    // The first NULL means use the active display driver
    // The last parameter must be NULL
    hr = DirectDrawCreate(NULL, &pDD, NULL);
	if (FAILED(hr)) {
        TRACE("Unable to create DDraw object\n");
        return FALSE;
    }

    // The DirectDraw object initialized successfully
    return TRUE;
}

3.5 Setting the screen mode

The function SetCooperativeLevel is used to tell the system whether or not we want to use full-screen mode or windowed. In full-screen mode, we have to get exclusive access to the DirectDraw device, and then set the display mode. For windowed mode, we set the cooperative level to normal.

BOOL SetMode() {
    HRESULT hr;
    if (bFullScreen) {
        // Set the "cooperative level" so we can use full-screen mode
        hr = pDD->SetCooperativeLevel(AfxGetMainWnd()->GetSafeHwnd(),
            DDSCL_EXCLUSIVE | DDSCL_FULLSCREEN | DDSCL_NOWINDOWCHANGES);
        if (FAILED(hr)) {
            pDD->Release();
            return FALSE;
        }
        // Set 640x480x256 full-screen mode
        hr = pDD->SetDisplayMode(640, 480, 8);
        if (FAILED(hr)) {
            TRACE("Error setting display mode: %d\n", int(LOWORD(hr)));
            pDD->Release();
            return FALSE;
        }
    } else {
        // Set DDSCL_NORMAL to use windowed mode
        hr = pDD->SetCooperativeLevel(AfxGetMainWnd()->GetSafeHwnd(),
            DDSCL_NORMAL);
    }

    // Success
    return TRUE;
}

3.6 Creating surfaces

OK ... now that we've got that bit of initialization out of the way, we need to create a flipping structure. No, I'm not cursing the structure .. "flipping" as in screen page-flipping :).

Anyway, we need to create one main surface that everyone will see, and a "back" surface. All drawing is done to the back surface. When we are finished drawing we need to make what we've drawn visible. In full-screen mode, we just need to call a routine called Flip, which will turn the current back surface into the primary surface and vice versa. In windowed mode, we don't actually flip the surfaces - we copy the contents of the back buffer onto the primary buffer, which is what's inside the window. In other words, we "blit" the back surface onto the primary surface.

Anyway, here is the bit of code to create the surfaces. Right now the code is ignoring full-screen mode and only catering for windowed mode, but that'll change. Also, if there are errors in this code, consider them "exercises" ... :). Naah, just kidding. If there are errors, mail me.

UINT CreatePrimarySurface()
{
    DDSURFACEDESC ddsd; // A structure to describe the surface we want
    HRESULT hr;         // Holds return values for function calls

    // Screw the full-screen mode (for now)

    // This clears all fields of the DDSURFACEDESC structure to 0
    memset(&ddsd, 0, sizeof(ddsd));
    // The first parameter of the structure must contain the
    // size of the structure.
    ddsd.dwSize = sizeof(ddsd);

    // The dwFlags paramater tell DirectDraw which DDSURFACEDESC
    // fields will contain valid values
    ddsd.dwFlags = DDSD_CAPS;
    ddsd.ddsCaps.dwCaps = DDSCAPS_PRIMARYSURFACE;

    // Create the primary surface
    hr = pDD->CreateSurface(&ddsd, &pDDSPrimary, NULL);
    if (FAILED(hr))
    {
        TRACE("Error: pDD->CreateSurface (primary)\n");
        TraceErrorDD(hr);
        pDD->Release();
        pDD = NULL;
        return 1;
    }

    // Now to create the back buffer
    ddsd.dwFlags = DDSD_WIDTH | DDSD_HEIGHT | DDSD_CAPS;
    // Make our off-screen surface 320x240
    ddsd.dwWidth = 320;
    ddsd.dwHeight = 240;

    // Ignore this: I'm trying stuff still
    /*memset(&ddsd.ddpfPixelFormat, 0, sizeof(ddsd.ddpfPixelFormat));
	ddsd.ddpfPixelFormat.dwSize = sizeof(ddsd.ddpfPixelFormat);
	ddsd.ddpfPixelFormat.dwFlags = DDPF_PALETTEINDEXED8;
//	ddsd.ddpfPixelFormat.dwRGBBitCount = 8;*/

    // Create an offscreen surface
    ddsd.ddsCaps.dwCaps = DDSCAPS_OFFSCREENPLAIN;

    hr = pDD->CreateSurface(&ddsd, &pDDSBack, NULL);
    if (FAILED(hr)) {
        TRACE("Error: CreateSurface (back)\n");
        TraceErrorDD(hr);
        return 2;
    }

    // success
    return 0;
}

3.7 Creating the Clipper

Now that we've created the surfaces, we need to create a clipper (if we're running in windowed mode), and attach the clipper to the primary surface. This prevents DirectDraw from drawing outside the windows client area.

UINT CreateClipper()
{
    HRESULT hr;

    // Create the clipper using the DirectDraw object
    hr = pDD->CreateClipper(0, &pClipper, NULL);
    if (FAILED(hr)) {
        TRACE("Error: CreateClipper\n");
        TraceErrorDD(hr);
        return 1;
    }

    // Assign your window's HWND to the clipper
    ddWnd = AfxGetMainWnd()->GetSafeHwnd();
    hr = pClipper->SetHWnd(0, ddWnd);
    if (FAILED(hr)) {
        TRACE("Error: SetHWnd\n");
        TraceErrorDD(hr);
        return 2;
    }

    // Attach the clipper to the primary surface
    hr = pDDSPrimary->SetClipper(pClipper);
    if (FAILED(hr)) {
        TRACE("Error: SetClipper\n");
        TraceErrorDD(hr);
        return 3;
    }
    // success
    return 0;
}

3.8 Putting it all together

Now that we have all these initialization routines, we need to actually call them, so the question is, where to call them? In an MFC application, a logical place to do this is in the application's InitInstance routine.

BOOL CDirectDrawApp::InitInstance()
{
#ifdef _AFXDLL
    Enable3dControls();
#else
    Enable3dControlsStatic();
#endif

    LoadStdProfileSettings(0);

    CSingleDocTemplate* pDocTemplate;
    pDocTemplate = new CSingleDocTemplate(
        IDR_MAINFRAME,
        RUNTIME_CLASS(CDirectDrawDoc),
        RUNTIME_CLASS(CMainFrame),
        RUNTIME_CLASS(CDirectDrawView));
    AddDocTemplate(pDocTemplate);

    CCommandLineInfo cmdInfo;
    ParseCommandLine(cmdInfo);

    if (!ProcessShellCommand(cmdInfo))
        return FALSE;

    InitDirectXGlobals();
    InitDDraw();
    SetMode();
// These will be coming soonish
//    TRACE("Calling LoadJascPalette\n");
//    LoadJascPalette("inspect.pal", 10, 240);
    TRACE("Calling CreatePrimarySurface\n");
    CreatePrimarySurface();
    TRACE("Calling CreateClipper\n");
    CreateClipper();
// Also coming soonish
//    TRACE("Calling AttachPalette\n");
//    AttachPalette(pDDPal);

    // Start the animation
    bAnimating = TRUE;

    return TRUE;
}

3.9 Restoring lost surfaces

As if all this initialization wasn't enough, we also have to make sure our DirectDraw surfaces are not getting "lost". The memory associated with DirectDraw surfaces can be released under certain circumstances, because it has to share resources with the Windows GDI. So each time we render, we first have to check if our surfaces have been lost and Restore them if they have. This is accomplished with the IsLost function.

// Checks if the memory associated with surfaces
// is lost and restores if necessary.
// Not sure about fullscreen/windowed;
// I'll get back to you people on this :)
BOOL CheckSurfaces()
{
    // Check the primary surface
    if (pDDSPrimary) {
        if (pDDSPrimary->IsLost() == DDERR_SURFACELOST) {
            pDDSPrimary->Restore();
            return FALSE;
        }
    }
    return TRUE;
}

3.10 The rendering loop

Now that we've got most of the general initialization out of the way, we need to set up a rendering loop. This is basically the main loop of the game, the so-called HeartBeat function. So we're going to call it just that.

The HeartBeat function gets called during your applications idle-time processing; so we need to override the application's OnIdle function. Use ClassWizard or the toolbar wizard thingy to create a handler for idle-time processing for your main application class. Here I am assuming you know a bit about MFC. Sorry about that; that'll probably change though with time as my knowledge improves and as I have more time to work on this.

Note that your applications OnIdle handler gets called even when your program does not have the current focus, and we'd rather not have your application do anything unless it has the current focus. Trust me on this one :). This is what we use bAnimating for; we set it to FALSE when we receive the WM_ACTIVATE(APP?) message with a parameter of FALSE. We set this variable to TRUE just after we've set up DirectDraw and the surfaces etc., and we are ready to begin.

BOOL CDirectDrawApp::OnIdle(LONG lCount) 
{
    CWinApp::OnIdle(lCount); // Call the previous default handler

    if (bAnimating) {
        // Our game's heartbeat function
        HeartBeat();
        // prevent's too much flicker. We'll need a smarter system
        // soon, this one is temporary
        Sleep(50);
    }
    // request more idle-time, so that we can render the next loop!
    return TRUE;
}

3.11 The HeartBeat function

Now let's look at the heartbeat function. At the moment mine just draws a silly block in the top left of the window that changes color each frame.

BOOL CDirectDrawApp::HeartBeat()
{
    // Variables for the blocks color, in RGB format
    static r = 0;
    static g = 100;
    static b = 150;
    r++;
    g += 3;
    b += 2;
    if (r > 255) r = 0;
    if (g > 255) g = 0;
    if (b > 255) b = 0;

    // The destination rectangle on my 320x240 off-screen surface
    CRect rc(10,30,100,70);

    // We draw the rectangle using a blit.
    // This structure describes how to do the blit.
    DDBLTFX fx;
    // Zero out all fields of the blit effects structure
    memset(&fx, 0, sizeof(fx));
    // The dwSize field must contain the size of the structure
    fx.dwSize = sizeof(DDBLTFX);

    // Set the color we want to draw the rectangle
    fx.dwFillColor = RGB(r,g,b);

    // Blit. Note that we blit to the back buffer
    HRESULT hr;
    hr = pDDSBack->Blt(&rc, NULL, NULL, DDBLT_COLORFILL | DDBLT_WAIT,
        &fx);
    if (FAILED(hr)) {
        TRACE("Error blitting: ");
        TraceErrorDD(hr);
        return FALSE;
    }

    // Call our routine for flipping the surfaces
    FlipSurfaces();

    // No major errors
    return TRUE;
}
Note that Blt parameter DDBLT_WAIT. If a surface is busy then DirectDraw will return an error, without performing the blit. Passing the DDBLT_WAIT option will instruct DirectDraw to wait until the surface becomes available and then perform the blit.

3.12 Flipping surfaces

Now let's look at the function that performs the surface flipping. This time I *do* take full-screen mode into account ... I'm inconsistent, eh? :-).

UINT FlipSurfaces()
{
    HRESULT hr;
    // source and destination rectangles
    RECT rcSrc;
    RECT rcDest;
    POINT p;

    // Make sure no surfaces have been lost
    CheckSurfaces();

    // if we're windowed do the blit, else just Flip
    if (!bFullScreen)
    {
        // find out where on the primary surface our window lives
        p.x = 0; p.y = 0;
        ::ClientToScreen(ddWnd, &p);
        ::GetClientRect(ddWnd, &rcDest);
        OffsetRect(&rcDest, p.x, p.y);
        SetRect(&rcSrc, 0, 0, 320, 240);
        hr = pDDSPrimary->Blt(&rcDest, pDDSBack, &rcSrc, DDBLT_WAIT,
            NULL);
    } else {
        hr = pDDSPrimary->Flip(NULL, DDFLIP_WAIT);
    }

    // success!
    return 0;
}

3.13 Tracing DirectDraw errors

Here is a pretty useful debugging function for DirectX apps, called TraceErrorDD:

void TraceErrorDD(HRESULT hErr)
{       
    switch (hErr)
    {
	case DDERR_ALREADYINITIALIZED:
		TRACE("DDERR_ALREADYINITIALIZED"); break;
	case DDERR_CANNOTATTACHSURFACE:
		TRACE("DDERR_CANNOTATTACHSURFACE"); break;
	case DDERR_CANNOTDETACHSURFACE:
		TRACE("DDERR_CANNOTDETACHSURFACE"); break;
	case DDERR_CURRENTLYNOTAVAIL:
		TRACE("DDERR_CURRENTLYNOTAVAIL"); break;
	case DDERR_EXCEPTION:
		TRACE("DDERR_EXCEPTION"); break;
	case DDERR_GENERIC:
		TRACE("DDERR_GENERIC"); break;
	case DDERR_HEIGHTALIGN:
		TRACE("DDERR_HEIGHTALIGN"); break;
	case DDERR_INCOMPATIBLEPRIMARY:
		TRACE("DDERR_INCOMPATIBLEPRIMARY"); break;
	case DDERR_INVALIDCAPS:
		TRACE("DDERR_INVALIDCAPS"); break;
	case DDERR_INVALIDCLIPLIST:
		TRACE("DDERR_INVALIDCLIPLIST"); break;
	case DDERR_INVALIDMODE:
		TRACE("DDERR_INVALIDMODE"); break;
	case DDERR_INVALIDOBJECT:
		TRACE("DDERR_INVALIDOBJECT"); break;
	case DDERR_INVALIDPARAMS:
		TRACE("DDERR_INVALIDPARAMS"); break;
	case DDERR_INVALIDPIXELFORMAT:
		TRACE("DDERR_INVALIDPIXELFORMAT"); break;
	case DDERR_INVALIDRECT:
		TRACE("DDERR_INVALIDRECT"); break;
	case DDERR_LOCKEDSURFACES:
		TRACE("DDERR_LOCKEDSURFACES"); break;
	case DDERR_NO3D:
		TRACE("DDERR_NO3D"); break;
	case DDERR_NOALPHAHW:
		TRACE("DDERR_NOALPHAHW"); break;
	case DDERR_NOCLIPLIST:
		TRACE("DDERR_NOCLIPLIST"); break;
	case DDERR_NOCOLORCONVHW:
		TRACE("DDERR_NOCOLORCONVHW"); break;
	case DDERR_NOCOOPERATIVELEVELSET:
		TRACE("DDERR_NOCOOPERATIVELEVELSET"); break;
	case DDERR_NOCOLORKEY:
		TRACE("DDERR_NOCOLORKEY"); break;
	case DDERR_NOCOLORKEYHW:
		TRACE("DDERR_NOCOLORKEYHW"); break;
	case DDERR_NODIRECTDRAWSUPPORT:
		TRACE("DDERR_NODIRECTDRAWSUPPORT"); break;
	case DDERR_NOEXCLUSIVEMODE:
		TRACE("DDERR_NOEXCLUSIVEMODE"); break;
	case DDERR_NOFLIPHW:
		TRACE("DDERR_NOFLIPHW"); break;
	case DDERR_NOGDI:
		TRACE("DDERR_NOGDI"); break;
	case DDERR_NOMIRRORHW:
		TRACE("DDERR_NOMIRRORHW"); break;
	case DDERR_NOTFOUND:
		TRACE("DDERR_NOTFOUND"); break;
	case DDERR_NOOVERLAYHW:
		TRACE("DDERR_NOOVERLAYHW"); break;
	case DDERR_NORASTEROPHW:
		TRACE("DDERR_NORASTEROPHW"); break;
	case DDERR_NOROTATIONHW:
		TRACE("DDERR_NOROTATIONHW"); break;
	case DDERR_NOSTRETCHHW:
		TRACE("DDERR_NOSTRETCHHW"); break;
	case DDERR_NOT4BITCOLOR:
		TRACE("DDERR_NOT4BITCOLOR"); break;
	case DDERR_NOT4BITCOLORINDEX:
		TRACE("DDERR_NOT4BITCOLORINDEX"); break;
	case DDERR_NOT8BITCOLOR:
		TRACE("DDERR_NOT8BITCOLOR"); break;
	case DDERR_NOTEXTUREHW:
		TRACE("DDERR_NOTEXTUREHW"); break;
	case DDERR_NOVSYNCHW:
		TRACE("DDERR_NOVSYNCHW"); break;
	case DDERR_NOZBUFFERHW:
		TRACE("DDERR_NOZBUFFERHW"); break;
	case DDERR_NOZOVERLAYHW:
		TRACE("DDERR_NOZOVERLAYHW"); break;
	case DDERR_OUTOFCAPS:
		TRACE("DDERR_OUTOFCAPS"); break;
	case DDERR_OUTOFMEMORY:
		TRACE("DDERR_OUTOFMEMORY"); break;
	case DDERR_OUTOFVIDEOMEMORY:
		TRACE("DDERR_OUTOFVIDEOMEMORY"); break;
	case DDERR_OVERLAYCANTCLIP:
		TRACE("DDERR_OVERLAYCANTCLIP"); break;
	case DDERR_OVERLAYCOLORKEYONLYONEACTIVE:
		TRACE("DDERR_OVERLAYCOLORKEYONLYONEACTIVE"); break;
	case DDERR_PALETTEBUSY:
		TRACE("DDERR_PALETTEBUSY"); break;
	case DDERR_COLORKEYNOTSET:
		TRACE("DDERR_COLORKEYNOTSET"); break;
	case DDERR_SURFACEALREADYATTACHED:
		TRACE("DDERR_SURFACEALREADYATTACHED"); break;
	case DDERR_SURFACEALREADYDEPENDENT:
		TRACE("DDERR_SURFACEALREADYDEPENDENT"); break;
	case DDERR_SURFACEBUSY:
		TRACE("DDERR_SURFACEBUSY"); break;
	case DDERR_CANTLOCKSURFACE:
		TRACE("DDERR_CANTLOCKSURFACE"); break;
	case DDERR_SURFACEISOBSCURED:
		TRACE("DDERR_SURFACEISOBSCURED"); break;
	case DDERR_SURFACELOST:
		TRACE("DDERR_SURFACELOST"); break;
	case DDERR_SURFACENOTATTACHED:
		TRACE("DDERR_SURFACENOTATTACHED"); break;
	case DDERR_TOOBIGHEIGHT:
		TRACE("DDERR_TOOBIGHEIGHT"); break;
	case DDERR_TOOBIGSIZE:
		TRACE("DDERR_TOOBIGSIZE"); break;
	case DDERR_TOOBIGWIDTH:
		TRACE("DDERR_TOOBIGWIDTH"); break;
	case DDERR_UNSUPPORTED:
		TRACE("DDERR_UNSUPPORTED"); break;
	case DDERR_UNSUPPORTEDFORMAT:
		TRACE("DDERR_UNSUPPORTEDFORMAT"); break;
	case DDERR_UNSUPPORTEDMASK:
		TRACE("DDERR_UNSUPPORTEDMASK"); break;
	case DDERR_VERTICALBLANKINPROGRESS:
		TRACE("DDERR_VERTICALBLANKINPROGRESS"); break;
	case DDERR_WASSTILLDRAWING:
		TRACE("DDERR_WASSTILLDRAWING"); break;
	case DDERR_XALIGN:
		TRACE("DDERR_XALIGN"); break;
	case DDERR_INVALIDDIRECTDRAWGUID:
		TRACE("DDERR_INVALIDDIRECTDRAWGUID"); break;
	case DDERR_DIRECTDRAWALREADYCREATED:
		TRACE("DDERR_DIRECTDRAWALREADYCREATED"); break;
	case DDERR_NODIRECTDRAWHW:
		TRACE("DDERR_NODIRECTDRAWHW"); break;
	case DDERR_PRIMARYSURFACEALREADYEXISTS:
		TRACE("DDERR_PRIMARYSURFACEALREADYEXISTS"); break;
	case DDERR_NOEMULATION:
		TRACE("DDERR_NOEMULATION"); break;
	case DDERR_REGIONTOOSMALL:
		TRACE("DDERR_REGIONTOOSMALL"); break;
	case DDERR_CLIPPERISUSINGHWND:
		TRACE("DDERR_CLIPPERISUSINGHWND"); break;
	case DDERR_NOCLIPPERATTACHED:
		TRACE("DDERR_NOCLIPPERATTACHED"); break;
	case DDERR_NOHWND:
		TRACE("DDERR_NOHWND"); break;
	case DDERR_HWNDSUBCLASSED:
		TRACE("DDERR_HWNDSUBCLASSED"); break;
	case DDERR_HWNDALREADYSET:
		TRACE("DDERR_HWNDALREADYSET"); break;
	case DDERR_NOPALETTEATTACHED:
		TRACE("DDERR_NOPALETTEATTACHED"); break;
	case DDERR_NOPALETTEHW:
		TRACE("DDERR_NOPALETTEHW"); break;
	case DDERR_BLTFASTCANTCLIP:
		TRACE("DDERR_BLTFASTCANTCLIP"); break;
	case DDERR_NOBLTHW:
		TRACE("DDERR_NOBLTHW"); break;
	case DDERR_NODDROPSHW:
		TRACE("DDERR_NODDROPSHW"); break;
	case DDERR_OVERLAYNOTVISIBLE:
		TRACE("DDERR_OVERLAYNOTVISIBLE"); break;
	case DDERR_NOOVERLAYDEST:
		TRACE("DDERR_NOOVERLAYDEST"); break;
	case DDERR_INVALIDPOSITION:
		TRACE("DDERR_INVALIDPOSITION"); break;
	case DDERR_NOTAOVERLAYSURFACE:
		TRACE("DDERR_NOTAOVERLAYSURFACE"); break;
	case DDERR_EXCLUSIVEMODEALREADYSET:
		TRACE("DDERR_EXCLUSIVEMODEALREADYSET"); break;
	case DDERR_NOTFLIPPABLE:
		TRACE("DDERR_NOTFLIPPABLE"); break;
	case DDERR_CANTDUPLICATE:
		TRACE("DDERR_CANTDUPLICATE"); break;
	case DDERR_NOTLOCKED:
		TRACE("DDERR_NOTLOCKED"); break;
	case DDERR_CANTCREATEDC:
		TRACE("DDERR_CANTCREATEDC"); break;
	case DDERR_NODC:
		TRACE("DDERR_NODC"); break;
	case DDERR_WRONGMODE:
		TRACE("DDERR_WRONGMODE"); break;
	case DDERR_IMPLICITLYCREATED:
		TRACE("DDERR_IMPLICITLYCREATED"); break;
	case DDERR_NOTPALETTIZED:
		TRACE("DDERR_NOTPALETTIZED"); break;
	case DDERR_UNSUPPORTEDMODE:
		TRACE("DDERR_UNSUPPORTEDMODE"); break;
	case DDERR_NOMIPMAPHW:
		TRACE("DDERR_NOMIPMAPHW"); break;
	case DDERR_INVALIDSURFACETYPE:
		TRACE("DDERR_INVALIDSURFACETYPE"); break;
	case DDERR_DCALREADYCREATED:
		TRACE("DDERR_DCALREADYCREATED"); break;
	case DDERR_CANTPAGELOCK:
		TRACE("DDERR_CANTPAGELOCK"); break;
	case DDERR_CANTPAGEUNLOCK:
		TRACE("DDERR_CANTPAGEUNLOCK"); break;
	case DDERR_NOTPAGELOCKED:
		TRACE("DDERR_NOTPAGELOCKED"); break;
	case DDERR_NOTINITIALIZED:
		TRACE("DDERR_NOTINITIALIZED"); break;
	default:
		TRACE("Unknown Error"); break;
	}
	TRACE("\n");
}

3.14 Cleaning up

Coming soon.


David Joffe's Guide to Programming Games with DirectX

Chapter 4: A simple Direct3D Retained mode sample

4.1 Direct3D: An Overview

Over here I'll shove in some basics, like coordinate systems, world and object coordinate systems, etc. For now I'll assume you're at least a little familiar with 3D programming. Blah blah blah, differences between immediate and retained mode, etc etc.

4.1.1 Devices

Direct3D interfaces with the surface it is rendering to (e.g. screen memory, system memory) using an IDirect3DRMDevice object. More than one type of rendering device can exist and a specific rendering device must be chosen for a scene. For example, there is normally a device for RGB rendering and a device for Mono rendering (these names refer to the lighting model used for rendering. Mono means that only white lights can exist in the scene, while RGB supports colored lights, and is thus slower). Additional devices may be installed that make use of 3D hardware acceleration. It is possible to iterate through the installed D3D devices by enumarating through them (EnumDevices). It is possible to have two different devices rendering to the same surface.

4.1.2 Viewports

The IDirect3DRMViewport object is used to keep track of how our 3D scene is rendered onto the device. It is possible to have multiple viewports per device, and it is also possible to have a viewport rendering to more than one device. The viewport object keeps track of the camera, front and back clipping fields, field of view etc.

4.1.3 Frames

A frame in Direct3D is basically used to store an object's position and orientation information, relative to a given frame of reference, which is where the term frame comes from. Frames are positioned relative to other frames, or to the world coordinates. Frames are used to store the positions of objects in the scene as well as other things like lights. OK, so I'm explaining it badly. It's late, I'm tired, I'll revise it soon. To add an object to the scene we have to attach the object to a frame. The object is called a visual in Direct3D, since it represents what the user sees. So, a visual has no meaningful position or orientation information itself, but when attached to a frame, it is transformed when rendered according to the transformation information in the frame. Multiple frames may use the same visual. This can save a lot of time and memory in a situation like, for example, a forest or a small fleet of spacecraft, where you have a bunch of objects that look exactly the same but all exist in different positions and orientations.

Here is a crummy ASCII diagram of a single visual attached to two frames which are at different positions:

   _____
  /    /| <- Cube (visual)
 /    / |<==========================>[Frame1: (21, 3, 4)]
+----+  |
|    | /<===========================>[Frame2: (-12, 10, -6)]
|    |/
+----+

If both of these frames were attached to the scene frame, then our scene would have 2 cubes in it; one at (21, 3, 4) and the other at (-12, 10, -6).

4.1.4 Materials

Coming sometime.

4.2 The Direct3D RM Sample

Firstly, heres a screenshot of the small simple sample application we're putting together here.

[Screenshot 2]

4.3 Setting up global variables

Before we start we'll need a few global variables.

LPDIRECTDRAW pDD;                // A DirectDraw object
LPDIRECT3DRM pD3DRM;             // A Direct3D RM object
LPDIRECTDRAWSURFACE pDDSPrimary; // DirectDraw primary surface
LPDIRECTDRAWSURFACE pDDSBack;    // DirectDraw back surface
LPDIRECTDRAWPALETTE pDDPal;      // Palette for primary surface
LPDIRECTDRAWCLIPPER pClipper;    // Clipper for windowed mode
LPDIRECT3DRMDEVICE pD3DRMDevice; // A device
LPDIRECT3DRMVIEWPORT pViewport;  // A viewport
LPDIRECT3DRMFRAME pCamera;       // A camera
LPDIRECT3DRMFRAME pScene;        // The scene
LPDIRECT3DRMFRAME pCube;         // The one and only object in
                                 // our scene
BOOL bFullScreen;                // Are we in full-screen mode?
BOOL bAnimating;                 // Has our animating begun?
HWND ddWnd;                      // HWND of the DDraw window

Note that we need both a DirectDraw object and a Direct3D object to create a Direct3D application. This is because Direct3D works in conjunction with DirectDraw. As before, we need a primary and a back surface for our double-buffering, and a clipper to handle window-clipping in windowed mode. The palette object is still not discusses in this tutorial (yet). We have objects for the device and viewport, and we have frame objects to keep track of the scene and the scene's camera. Also, we have a frame that is used for the object we'll have in this scene.

Here is a routine just to initially flatten these globals:

void InitDirectXGlobals()
{
    pDD = NULL;
    pD3DRM = NULL;
    pDDSPrimary = NULL;
    pDDSBack = NULL;
    pDDPal = NULL;
    pClipper = NULL;
    pD3DRMDevice = NULL;
    pViewport = NULL;
    pCamera = NULL;
    pScene = NULL;
    pCube = NULL;

    bFullScreen = FALSE;
    bAnimating = FALSE;
}

4.4 From 'Initializing the DirectDraw system' to 'Creating the clipper'

These steps all proceed exactly as in the DirectDraw sample, with the exception of the CreateSurface function, where the back surface has to created with the DDSCAPS_3DDEVICE, since it will be used for 3d rendering:

UINT CreatePrimarySurface()
{
    .
    .
    .
    // Create an offscreen surface, specifying 3d device
    ddsd.ddsCaps.dwCaps = DDSCAPS_OFFSCREENPLAIN | DDSCAPS_3DDEVICE;
    .
    .
    .
}

4.5 Creating the Direct3D Retained Mode object

Now we need to create an IDirect3DRM object. This is achieved, quite simply, by calling the Direct3DRMCreate function.

UINT CreateDirect3DRM()
{
    HRESULT hr;
    // Create the IDirect3DRM object.
    hr = Direct3DRMCreate(&pD3DRM);
    if (FAILED(hr)) {
        TRACE("Error creating Direct3d RM object\n");
        return 1;
    }
    return 0;
}

4.6 Creating the device for rendering

We create the device object from the back surface, since this surface is the one we will render to.

UINT CreateDevice()
{
    HRESULT hr;
    hr = pD3DRM->CreateDeviceFromSurface(
        NULL, pDD, pDDSBack, &pD3DRMDevice);
    if (FAILED(hr)) {
        TRACE("Error %d creating d3drm device\n", int(LOWORD(hr)));
        return 1;
    }
    // success
    return 0;
}

4.7 Creating the viewport

We do a bit more than just create the viewport here. We create the scene object and the camera object, as well as set the ambient light for the scene, and create a directional light.

UINT CreateViewport()
{
    HRESULT hr;

    // First create the scene frame
    hr = pD3DRM->CreateFrame(NULL, &pScene);
    if (FAILED(hr)) {
        TRACE("Error creating the scene frame\n");
        return 1;
    }

    // Next, create the camera as a child of the scene
    hr = pD3DRM->CreateFrame(pScene, &pCamera);
    if (FAILED(hr)) {
        TRACE("Error creating the scene frame\n");
        return 2;
    }
    // Set the camera to lie somewhere on the negative z-axis, and
    // point towards the origin
    pCamera->SetPosition(
        pScene, D3DVAL(0.0), D3DVAL(0.0), D3DVAL(-300.0));
    pCamera->SetOrientation(
        pScene,
        D3DVAL(0.0), D3DVAL(0.0), D3DVAL(1.0),
        D3DVAL(0.0), D3DVAL(1.0), D3DVAL(0.0));

    // create lights
    LPDIRECT3DRMLIGHT pLightAmbient = NULL;
    LPDIRECT3DRMLIGHT pLightDirectional = NULL;
    LPDIRECT3DRMFRAME pLights = NULL;

    // Create two lights and a frame to attach them to
    // I haven't quite figured out the CreateLight's second
    // parameter yet.
    pD3DRM->CreateFrame(pScene, &pLights);
    pD3DRM->CreateLight(D3DRMLIGHT_AMBIENT, pD3DRMCreateColorRGB(
        D3DVALUE(0.3), D3DVALUE(0.3), D3DVALUE(0.3)),
        &pLightAmbient);
    pD3DRM->CreateLight(D3DRMLIGHT_DIRECTIONAL, D3DRMCreateColorRGB(
        D3DVALUE(0.8), D3DVALUE(0.8), D3DVALUE(0.8)),
        &pLightDirectional);

    // Orient the directional light
    pLights->SetOrientation(pScene,
        D3DVALUE(30.0), D3DVALUE(-20.0), D3DVALUE(50.0),
        D3DVALUE(0.0), D3DVALUE(1.0), D3DVALUE(0.0));

    // Add ambient light to the scene, and the directional light
    // to the pLights frame
    pScene->AddLight(pLightAmbient);
    pLights->AddLight(pLightDirectional);

    // Create the viewport on the device
    hr = pD3DRM->CreateViewport(pD3DRMDevice,
        pCamera, 10, 10, 300, 220, &pViewport);
    if (FAILED(hr)) {
        TRACE("Error creating viewport\n");
        return 3;
    }
    // set the back clipping field
    hr = pViewport->SetBack(D3DVAL(5000.0));

    // Release the temporary lights created. It seems
    // they will have been copied for the scene during AddLight
    pLightAmbient->Release();
    pLightDirectional->Release();

    // success
    return 0;
}

4.8 Creating your scene

We need some objects for the scene. Let's create a cube! Wheee! Here is the cube.x file, zipped. Unzip it into the directory from which your app runs.

SetRotation assigns values in radians to a frame that it must rotate around it's local axis each time the frame is rendered using Tick. Direct3D automatically performs this rotation when Tick is called.

// Put stuff into the scene
UINT CreateDefaultScene()
{
    HRESULT hr;

    // Create the frame for the cube
    hr = pD3DRM->CreateFrame(pScene, &pCube);
    if (FAILED(hr)) {
        TRACE("Error creating frame pCube\n");
        return 1;
    }
    // Load the cube
    hr = pCube->Load("CUBE.X", NULL, D3DRMLOAD_FROMFILE, NULL, NULL);
    if (FAILED(hr)) {
        TRACE("Error [%d] loading cube.x\n", int(LOWORD(hr)));
        return 2;
    }
    // Set cube's position/orientation relative to scene
    pCube->SetPosition(pScene,
        D3DVALUE(0.0), D3DVALUE(0.0), D3DVALUE(0.0));
    pCube->SetRotation(pScene,
        D3DVALUE(1.0), D3DVALUE(1.0), D3DVALUE(0.0), D3DVALUE(0.1));

    // success
    return 0;
}

4.9 Putting it all together

Here is the tail-end of the app's InitInstance function:

    InitDirectXGlobals();
    TRACE("Calling InitDDraw\n");
    InitDDraw();
    SetMode();
//    TRACE("Calling LoadJascPalette\n");
//    LoadJascPalette("inspect.pal", 10, 240);
    TRACE("Calling CreatePrimarySurface\n");
    CreatePrimarySurface();
    TRACE("Calling CreateClipper\n");
    CreateClipper();
//    TRACE("Calling AttachPalette\n");
//    AttachPalette(pDDPal);
    TRACE("Calling CreateDirect3DRM\n");
    CreateDirect3DRM();
    TRACE("Calling CreateDevice\n");
    CreateDevice();
    TRACE("Calling CreateViewport\n");
    CreateViewport();
    TRACE("Calling CreateDefaultScene\n");
    CreateDefaultScene();

    bAnimating = TRUE;
	
    return TRUE;
}

4.10 Restoring lost surfaces

Same as the DirectDraw sample:

BOOL CheckSurfaces()
{
    // Check the primary surface
    if (pDDSPrimary) {
        if (pDDSPrimary->IsLost() == DDERR_SURFACELOST) {
            pDDSPrimary->Restore();
            return FALSE;
        }
    }
    return TRUE;
}

4.11 The Rendering loop

Same as the DirectDraw sample:

BOOL CD3dRmAppApp::OnIdle(LONG lCount) 
{
    CWinApp::OnIdle(lCount);
    if (bAnimating) {
        HeartBeat();
        Sleep(50);
    }
    return TRUE;
}

4.12 The HeartBeat function

BOOL CD3dRmAppApp::HeartBeat()
{
    HRESULT hr;
//    if (!CheckSurfaces) bForceUpdate = TRUE;
//    if (bForceUpdate) pViewport->ForceUpdate(10,10,300,220);
    hr = pD3DRM->Tick(D3DVALUE(1.0));
    if (FAILED(hr)) {
        TRACE("Tick error!\n");
        return FALSE;
    }

    // Call our routine for flipping the surfaces
    FlipSurfaces();

    // No major errors
    return TRUE;
}

4.13 Flipping surfaces

Same as for DirectDraw sample.

4.14 Tracing Direct3D errors

Coming soon(?).

4.15 Cleaning up

Coming soonish(?).

Article updated: 2 September 1997


Article by David Joffe
http://www.geocities.com/SoHo/Lofts/2018/


The Game Programming MegaSite
The Entire Site ©1996,1997,1998 Matt Reiferson.
Any Questions/Comments, Feel Free To E-Mail Me.