sunlight

Error Handling

Introduction

In this part of the tutorial, we will cover:

  • Error handling
  • Surface recovery
  • Co-operation and application switching
  • BltFast clipping

Previous View Code Download Code Next

Error Handling

In our code, there are many points of failure: the window creation could fail, the DirectX calls could fail, files might not be available, surfaces could be lost. If you Alt+Tab away from your program, you will notice that when you return, no graphics will be displayed. We will remedy this situation now.

Firstly, we need to have a system of error handling. We can recover from some errors, while others are fatal and we should exit gracefully, giving enough information to the user that they know what's going on, while storing enough information that we can determine the exact point of failure during development.

Useful items include the FormatMessage function, which gives you an error message for an error code. We will be using this extensively. In fact, we will define a small function that uses FormatMessage to get an error description, loads a format string from the resources and uses MessageBox to display the error:

static  TCHAR   szErrorBuf[512], szMessage[256];

void DisplayError(LPCTSTR szFile, int nLine, HRESULT hResult, LPCTSTR lpMessage)
{
    // Get an error description using FormatMessage.
    FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM | 
            FORMAT_MESSAGE_IGNORE_INSERTS,
        NULL, hResult, 0, szMessage, 
        sizeof(szMessage) / sizeof(TCHAR), NULL);

    // Create the string.
    wsprintf(szErrorBuf, _T("%s\nFile %s, line %d, error code 0x%08X: %s"), 
        lpMessage, szFile, nLine, hResult, szMessage);

    // Dump the error to the debugger, if present.
    OutputDebugString(szErrorBuf);

    // Display the error using MessageBox.
    MessageBox(NULL, szErrorBuf, _T("Error"), MB_OK);
}

We can use a convenient macro to display the file and line of error:

#define DISPLAYERROR(h, s)\
    DisplayError(__FILE__, __LINE__, h, _T(s))
#define DISPLAYLASTERROR(s)\
    DisplayError(__FILE__, __LINE__, GetLastError(), _T(s))

The main points of failure are the DirectX calls. For example, in the DirectMusic initialisation:

    h = ::CoCreateInstance(CLSID_DirectMusicPerformance, NULL, 
                     CLSCTX_INPROC, IID_IDirectMusicPerformance8,
                     (void **)&g_pPerformance);
    if (FAILED(h))
    {
        DISPLAYERROR(h, "DirectMusic could not be initialised. DirectX 8 may not be installed.");
        return FALSE;
    }

We will test for the return value in WinMain. We will also change the InitDirectDraw and InitDirectInput functions so that they return a sensible value, so we can do:

    if (!InitDirectMusic())
        return 1;
    if (!InitDirectInput())
    {
        DisplayError(_T("Failed to initialise DirectInput."));
        ExitDirectMusic();
        return 1;
    }

    if (!InitDirectDraw())
    {
        ExitDirectInput();
        ExitDirectMusic();
        return 1;
    }

Note that initialising DirectDraw is the last thing we do, since it is likely to change the screen mode. 

Now we've handled all the possible initialisation errors, it's time to change our viewpoint and look at the other problems: co-operation with other programs.

Recovering Lost Surfaces

When other applications take control from your program, they will want control of the video memory. This includes Windows GDI as well as other DirectDraw applications. It is therefore essential for there to be a way to throw your surfaces out of video memory once another application takes control.

DirectDraw does this automatically. When another application requests a surface, DirectDraw may throw your surface out altogether. When you regain control, DirectDraw will re-establish your surface - but the contents have been lost. To mark this, DirectDraw gives an error when you access this surface until you call Restore.

However, calling Restore will just give you a blank surface - you will have to reload the contents of that surface. We will have to do this every time a surface is lost. Conveniently, our DDUTIL.CPP file contains the function DDReloadBitmap, which we can use to reload the contents of our surface.

There are a limited number of times a surface can be lost:

  • When the display mode is changed
  • When another application receives exclusive control

We will check for the WM_ACTIVATE message (and other related messages) and restore all our surfaces in one go when that happens. We will write the RestoreAllSurfaces function, and we will also call this when BltFast returns DDERR_SURFACELOST (i.e. when a surface is lost).

void RestoreAllSurfaces()
{
	pDD->RestoreAllSurfaces();
	DDReLoadBitmap(lpA, "a.bmp");
	DDReLoadBitmap(lpSprites, "sprites.bmp");
}

We will also write a function called BackBlt that mirrors the functionality of BltFast, but offers us some error handling.

BOOL BackBlt(int x, int y, IDirectDrawSurface7 *lpSurf, LPCRECT pRect, DWORD dwFlags)
{
	HRESULT	h;
	RECT	rc = *pRect;

	h = lpBackBuffer->BltFast(x, y, lpSurf, &rc, dwFlags);

	if (h == DDERR_SURFACELOST)
	{	RestoreAllSurfaces();
		h = lpBackBuffer->BltFast(x, y, lpSurf, &rc, dwFlags);
	}
	
	if (h != DD_OK)
		return FALSE;
	return TRUE;
}

So now, within the message handler:

    case WM_ACTIVATE:
        if (LOWORD(wParam) == WA_INACTIVE)
        {
            g_bActive = FALSE;
            UnacquireDevices();
        }
        else
        {
            g_bActive = TRUE;
            AcquireDevices();
        }
        return 0;

Here, we have introduced a new flag, g_bActive. This is set to TRUE at the end of InitDirectDraw, and if it is FALSE on entry into OnIdle, we do no drawing. This ensures we do not try to draw when our application is not active.

This message handler also takes care of acquiring and unacquiring devices when control switches between our application and others.

Clipping

We will make one further adjustment to make our BackBlt function complete.

When you reach the edges of the screen, BltFast will simply fail when it overlaps the edge. This is not usually terribly useful, so we will add a little calculation that makes the rectangle smaller if necessary.

	if ((x > 640) || (y > 480))
		return TRUE;

	if (x < 0)
	{	rc.left -= x;
		x = 0;
	}
	if (y < 0)
	{	rc.top -= y;
		y = 0;
	}
	
	if (rc.left < 0)
		rc.left = 0;
	if (rc.top < 0)
		rc.top = 0;
	
	if ((x + rc.right - rc.left) > 640)
		rc.right -= (x + rc.right - rc.left) - 640;
	if ((y + rc.bottom - rc.top) > 480)
		rc.bottom -= (y + rc.bottom - rc.top) - 480;

	if ((rc.right <= rc.left) || (rc.bottom <= rc.top))
		return TRUE;

Conclusion

Now we have a large program (some 700 lines long), you might be wondering where all the elegant simplicity of the original went! However, this is a robust, well-behaved application, and almost all the code is common from one program to the next - all you need change is the sprites and the OnIdle function. You could split the other utility functions into another file and it would be far cleaner, which is what we're going to do next.

 

Copyright © David McCabe, 1998 - 2001. All rights reserved.

You will need to download and install the m-math control to display any equations on this Web site. Without this control, you will not see most of the equations. Please do not e-mail me asking why the equations do not display!