Windowed Mode and Full-screen Mode


You should always ask yourself what your game buys by running in Windowed mode. (I'm talking about the release build of your game here.) It would be very normal to support a crippled form of windowed mode in a debug or test build, so that programmers and testers can develop on a single monitor system. A game that supports windowed and full-screen modes for the general public has a lot of work to do. I would humbly submit that unless your game is a mass market title like Microsoft Casino, you should resist the temptation to run in windowed mode and spend programming time doing other things.

The problem lies not in some deficiency in Microsoft engineers, rather it is one of design. Games, by their nature, are written to take the best advantage of the hardware to run as fast as they can. This means that games are device dependant, or at least device aware. They care if the video card supports hardware transforms and lighting. They care if the audio card supports surround sound. A normal Windows application, like Excel, couldn't care less.

To run fast on a wide variety of hardware, games tend to convert their graphics to a format that is closely aligned with the video cards' current configuration. This configuration is tweakable by the game, mostly in terms of the resolution and bit depth, but usually not in terms of the pixel format. This conversion, which usually takes place as assets are loaded to play the current level, makes switching hardware configuration on the fly expensive and fragile from the coder's viewpoint.

A Tale from the Pixel Mines

Here's an example of a game I worked on, Microsoft Casino, that was a mass market title and therefore had to run well in windowed and full-screen modes. The native resolution of the game was 800x600x16. Windowed mode would create a client window at 800x600 resolution and use the current bit depth and pixel format of the Window's desktop. The format and resolution of the player's desktop was not necessarily the same as the full-screen mode. Thus, when the player hit Alt-Enter to switch from one to the other, it was likely that all the graphics assets currently loaded had to be thrown out and reloaded to match the new format. Of course, the player could force the same event by changing the desktop settings while the game window was up. They could change from 16-bit to 32-bit, and the game would have to reinitialize the display surfaces and reload and reconvert all the graphics. DirectX surfaces cannot convert from one pixel format to another in a Blt() or BltFast() call, so if you change the display surface everything else in your game has to change to match it.

Gotcha

This doesn't apply to textures. The format of a texture is dependant on the capabilities of the video card and has little or nothing to do with the format of the display surface.

As you read the rest of this section, remember that you can consider the option of running your game in full-screen mode for the players. If you do this allow a functional windowed mode that doesn't support all of the compatibility concerns of regular Windows applications for development and debugging on a single monitor system. When these transitions take place, DirectX detects the change in pixel format, and marks all surfaces effected by this transition as a lost surface. It essentially means that the reference to a DirectX surface still exists, but the bits are invalid and need to be reconstructed to reflect the new format. Lost surfaces can also occur as a result of DirectX managing surface memory.

Under DirectX 9, most memory can be set to managed when you create it. This is true for textures, display surfaces, and even things like vertex buffers. If you allow DirectX to manage these resources, you won't have to worry about this memory becoming invalid, since DirectX will take control of doing the reallocations and conversions.

If you opt to manage the memory yourself, you'll have to write code to deal with lost or incompatible surfaces, lest your game suffer a nasty crash when the players do something wacky like change the screen display.

Lost or Incompatible Surfaces

Does this mean, exactly, that the surface is lost? It means that the operating system has taken away the bits that compose the surface and allocated it to something else, and there's not enough information for DirectX to recompose the bits without your help. The definition of the surface's structure is still intact, so DirectX can easily recreate it, assuming there's enough room. The actual bits on the surface, however, have been destroyed. If all that needs doing is reallocating the bits, you can simply call Restore():

 // assume m_pdds points to a DirectX surface if (m_pdds->IsLost()) {   m_pdds->Restore(); } 

The code above might be all you need, in the case of a display surface that gets overwritten every frame, but what should you do about a surface whose bits were constructed by your tender loving care? Perhaps you created a nice fractal or simply drawn some text with a GDI font and placed the surface in video memory for super fast retrieval and screen composition. You'll need to recreate those bits if the surface is ever lost, which implies you have some way of reinitializing all your "losable" surfaces.

One way to do this is to define a C++ interface called IRestorable. The interface will have two methods that you can probably predict:

 class IRestorable { public:   virtual bool VIsLost() = 0;   virtual bool VRestore() = 0; }; class Surface : public IRestorable {    // Implement the IRestorable Surface    LPDIRECTDRAWSURFACE7 m_pdds;    virtual bool VIsLost() { return (!m_pdds || m_pdds->IsLost(); }    virtual bool VRestore() { return (m_pdds && SUCCESS(m_pdds->Restore()) ); }    // The remainder of the Surface class is yours to implement! }; 

Of course, the Surface class would have more code in it that that—this is just an architecture lesson! The crazy surface I spoke of before would implement its own restore function, calling the base class first:

 bool CrazyFractalTextSurface::VRestore() {   if (!Surface::VRestore())     return false;   DrawMyFractal();   DrawMyText(); }; 

Assuming you have a list of these Surface classes around, it becomes a simple matter to iterate through the list and call VRestore() on every one of them.

When do you need to do this? Is it necessary to restore every surface in your whole game when you detect one that has gone to the dark side? Perhaps not; it depends on the return values from a failed DirectX call. There are two primary cases:

  • DDERR_SURFACELOST: The surface's memory bits are gone, but the definition is still valid. Restoring the surface will make everything good again, and it doesn't mean you'll have to restore everything else.

  • DDERR_WRONGMODE: The surface's bits are still intact, but the bits don't have a compatible pixel format. The surface can't be restored because it's isn't lost. It must be thrown out completely and reloaded with the correct pixel format. It's likely you'll have to do this for everything.

The player can only change the display settings by bringing up the desktop properties window, which should send a WM_ACTIVE message to your game—a good thing to catch. When your game goes active again, one of the first things you should do is check your backbuffer. If it is lost, you should try to restore it. If the restore fails, the most likely reason is a mode switch on the desktop. Here's what you can do about it:

 // Note: This code is DirectX 7 inline HRESULT HandleModeChanges() {   HRESULT hr;   hr = m_myDirectDrawObject->TestCooperativeLevel();   do   {     if( SUCCEEDED( hr ) )     {        // This means that mode changes had taken place, surfaces        // were lost but still we are in the original mode, so we        // simply restore all surfaces and keep going.        bool ok = RestoreEverything();        // test the DisplayFrame one time to be sure....        if( ok && SUCCEEDED( hr = yourcode::DisplayFrame() ) )          return hr;     }     if( hr == DDERR_WRONGMODE )     {       // This means that the desktop mode has changed       // we can destroy and recreate everything back again.       return yourcode::InitDirectDrawMode( m_bWindowed );     }     // If we get here it means that some app took exclusive mode access        // we need to sit in a loop till we get back to the right mode.     Sleep(500);   } while( DDERR_EXCLUSIVEMODEALREADYSET ==                (hr = m_myDirectDrawObject->TestCooperativeLevel()));   // Busted! Something has gone terribly wrong.   return true; } bool RestoreEverything() {   for (SurfaceList::iterator i=m_surfaces.begin(); i!=m_surfaces.end(); i++)      if (FAILED( (*i)->VRestore() ) )                      return false;   return true; }; 

The call to TestCooperativeLevel tests the system to find out if the device is restorable. If the call returns anything other than DD_OK you get to tear down your display and reinitialize it from scratch. Since it is likely that the new display will have a different pixel format from the original, you'll likely throw out all your graphics resources as well.

Did you notice that the whole thing is in a while loop? The reason is that it is possible another application has exclusive control of the device. Our game has to wait until the other application is done, hence the while loop. The call to RestoreEverything simply iterates the list of surfaces and calls VRestore() for each surface.

Bad Windows

When you create a windowed mode game, it's a good idea to grab the pixel format of the desktop and set your DirectX surfaces to match it. Doing this will enhance the speed of your game significantly, since Windows won't have to perform a pixel-by-pixel conversion operation when you present the backbuffer. DirectX does this by default if you don't specify a pixel format—a nice convenience. This feature becomes significantly less convenient on those video cards that support 24-bit display formats. If the player's desktop is set to 24-bit, DirectX will happily create a 24-bit primary and backbuffer surface. Everything will work swimmingly until you attempt to attach that surface to a Direct3D device.

Most Direct3D devices don't support 24-bit video memory surfaces, and for good reason. They can be incredibly slow. The same problem exists with running Windows in 256-color mode, not that you could stand looking at it.

Gotcha

Another case that sneaks up on you is a desktop running in a very low resolution. If your game runs at 800 600, the smallest desktop that will support your game is 1024 768. Why? Because an 800 600 client area will completely cover your desktop, leaving you no room for a title bar or other non-client control areas. Players won't be able to drag the window around, with nothing to grab. You might as well run in full-screen.

Here's some code you can use to detect unsupported window modes:

 bool ValidWindowedMode ( ) {   CRect crDesktop;   int iColorDepth;   // Grab the new windows dimensions and color depth   ::SystemParametersInfo( SPI_GETWORKAREA, 0, &crDesktop, 0 );   HDC d = ::GetDC(NULL);   iColorDepth = GetDeviceCaps(d, BITSPIXEL);   ::ReleaseDC(NULL, d);   if ( crDesktop.Width() <= yourcode::SCREEN_WIDTH ||      crDesktop.Height() <= yourcode::SCREEN_HEIGHT ||     ( iColorDepth == 24  ) ||     ( iColorDepth < 16 ) )   {     return false;   }   else   {     return true;   } } 

This function checks the current desktop dimensions with SystemParametersInfo(), and the color depth of the desktop with GetDeviceCaps(), both Win32 functions. This function sends back a negative result if the desktop is set to anything other than 16-bit or 32-bit, or is smaller than or equal to the expected client area of the game.

If you find that your player has their desktop in an unsupported mode, flip into full-screen mode and finish initializing your game. When your game is up and running, bring up a dialog box that tells them why they aren't in windowed mode any more.

GDI Dialog Boxes and Flipping

The GDI doesn't understand flipping surfaces, so if you ever try to call MessageBox and nothing happens, it's likely that the GDI just drew your message box to the backbuffer and is waiting for you to respond. If you are going to launch a GDI dialog box inside a DirectX application that uses flipping surfaces, you must call FlipToGDISurface() to make sure the GDI is drawing to the right place.

Best Practice

Many games create their own dialog boxes, and don't use the GDI at all. It stands to reason that if something goes wrong with your game's early stage initialization, you'll want to bring up a dialog of some kind. Thus, it seems that there is a dual purpose need for a smart message box system. In one of my games I created an Ask() method that accepted one parameter—a constant that defined the question. The function was smart enough to flip to GDI if necessary and use game dialogs and localized text strings if they were initialized. There were a few cases where a message box was needed to inform players of odd initialization errors, before the pretty game-specific dialog box assets or localized string table were loaded. In those cases I bailed and used English text and a GDI MessageBox to communicate the problem.

Messages You Have to Take

There are a few windows messages you shouldn't ignore. It might seem a lot of work to write all the code for these messages but in the end it's worth it. Your game will be a nicely behaved Windows application. I know what you're thinking, a nicely behaved Windows application sounds a little like "cafeteria food" or "military intelligence" but in the end, you'd rather have a stable, predictable Windows application that something that broke every time a player hit Alt-Tab.

These messages happen during window creation:

  • WM_NCCREATE: Handle this message if you must do something before your WM_CREATE message is sent. Other than that, there's nothing special about this message.

  • WM_CREATE: This message is sent when you have a window ready to play with, but it hasn't been drawn yet.

While your window is alive, you should handle these messages:

  • WM_ACTIVATE: This is the message that Windows sends to applications when they are going active or inactive, such as when a player hits Alt-Tab to cover the fact that they are playing games instead of working. This is a better message than WM_SETFOCUS.

  • WM_SYSCOMMAND: This message is sent when the player hits a system command sequence such as Alt-F4 to close the game.

  • WM_MOVE: This message is sent whenever your Window is dragged.

  • WM_NCLBUTTONDBCLK: This is a message sent to the window when the player double clicks in the non-client area of the window. It's a common thing to switch to full-screen mode when users do this.

  • WM_DEVICECHANGE: This is a good message to handle, because you can never tell when crazy players are going to remove their CDs or DVDs.

  • WM_POWERBROADCAST: With so many people playing games on portables, you'd better deal with a low power condition if you can.

  • WM_DISPLAYCHANGE: If you happen to catch it in the act, the display change message can give you a warning about display changes.

  • WM_ENTERSIZEMOVE, WM_EXITSIZEMOVE: These messages are sent before and after the window is being moved or resized.

  • WM_GETMINMAXINFO: Useful if you want the minimum or maximum size of your game window to be limited in scope.

When your window closes, you'll want to handle the following messages. There's nothing special at all about them, but it's useful to know that they arrive in your message pump in a specific order:

  • WM_CLOSE: This message is sent to you when Windows wants your application to close, or you can send it to yourself when the player presses the exit controls. This is your last chance to abort closing, so don't forget to ask your player if he or she want to save his or her game!

  • WM_DESTROY: Closing is not an option at this point. It's the future. It is one of the last messages your window will handle.

  • WM_NCDESTROY: This is the last message your window will handle.

Let's look at some specific versions of some of these handlers to get a better idea of what some of the oddball messages are up to.

WM_ACTIVATE

This is the message that you handle to figure out any heinous trickery that some player has played on their computer while your game was sleeping:

 case WM_ACTIVATE: {   bool active = wParam != WA_INACTIVE;   HWND pWndOther = lParam;   // Pause or resume audio   if ( active )   {     yourcode::ResumeAudio();     // Set the mode if it has changed     if ( m_bWindowedMode && ! ValidWindowedMode() )     {       yourcode::ResetDisplay();     }   }   else   {     yourcode::PauseAudio();     yourcode::SavePausedBackground();   }   // This is a function that will do anything such as restart animations, etc.   your::TogglePause(active); } 

The message parameters are sucked from wParam and lParam first. If the window has gone active there are two important tasks. First, you want to restart your audio, and second you want to call the function we discussed earlier to validate the current display. If it isn't compatible, you'll have to reinitialize to full-screen mode. If the application is going inactive you should also do two things. First, you should pause your audio. It can be really annoying to have the game audio squawking in the background while you're trying to talk to someone on the phone, especially if they thought you were doing something other than playing games.

Best Pratice

It's also a good idea is to save your current backbuffer in a regular off-screen surface. Why bother? It turns out that when a player hits Ctrl-Alt-Del, video surfaces can be lost. This is an excellent way to force your game to suffer a surface loss condition. If you have a copy of your backbuffer in off-screen memory, you can use it to paint while your application is inactive. Just because your game is inactive doesn't mean that it doesn't get paint messages. You might drag another window over your game.

WM_SYSCOMMAND

This message covers a lot of ground, because like the WM_COMMAND message, it's lots of message rolled up into one. The wParam holds the message identifier. There are two that games should handle:

  • SC_CLOSE: This is another message that is sent to close your game. Treat it exactly the same as you would if your player pressed the right controls to exit the game.

  • SC_MAXIMIZE: This message can be a useful shortcut for putting your game in full-screen mode; it's completely up to you.

WM_MOVE

If your game holds local parameters about its screen whereabouts, you'll want to handle the WM_MOVE message. Here's an example:

 case WM_MOVE: {     if( m_bWindowed )     {        GetClientRect( m_hWnd, &m_rcWindow );        ClientToScreen( m_hWnd, &m_rcWindow );     }     else     {      SetRect( &m_rcWindow, 0, 0,        GetSystemMetrics(SM_CXSCREEN),        GetSystemMetrics(SM_CYSCREEN) );     }   yourcode::Present();      // this is a call to your draw function } 

This WM_MOVE handler shows the code you'll use to set your RECT and m_rcWindow to the correct coordinates whether your game is in windowed mode or full-screen mode. It's a good idea to call your draw function in the move handler as well, after the new screen coordinates have been set.

WM_DEVICECHANGE

You might feel as I do that if a player removes the CD from the drive while the game is running, they get what they deserve. But, hey everyone makes mistakes once in a while. If you handle this message, you can be reasonably sure that your game won't crash and burn. If the game is actively reading from the removable media and it is ripped from the drive, you can't really tell what will happen. This is especially true of memory mapped files. I'd avoid using them on any type of removable media such as a CD or DVD.

 case WM_DEVICECHANGE: {   int eventType = wParam;   if ( yourcode::IsMinimumInstall() )            // reads game data to find out   {     if ( (UINT) nEventType == DBT_DEVICEREMOVECOMPLETE )     {       TCHAR *searchFile = yourcode::CDCheckFile();       while (!yourcode::FileExists(searchFile))       {          if (yourcode::Ask(QUESTION_WHERES_THE_CD)==IDCANCEL)          {            yourcode::AbortGame();          }       }       return BROADCAST_QUERY_DENY;        // denied!     }     }     return TRUE; } 

This code assumes you've written a few functions for your game. The first one, yourcode:: IsMinimumInstall(), should return true if the player is running the game with a minimum install and therefore needs the CD in the drive. Presumably you'd read this from some install file or system registry key.

The next predicate, comparing the event type to DBT_DEVICEREMOVECOMPLETE is how Windows tells you the drive eject button has been pressed. The code then enters a loop, which checks for the existence of a marker file on the CD. A marker file is a file with either a unique name or unique contents, like a fingerprint. You could just as easily read the volume label but I've found that simulating removable media with a network drive is easier if you use a marker file. The IT staff gets nervous when programmers start requesting hourly volume name changes on network drives. I'd make it easy and look for a file that is guaranteed to be on the CD, uncompressed, and easy to find.

If the file isn't found you bring up a dialog box that asks the user to find the CD. It should have two buttons: OK and Cancel. If they press cancel, you should abort the game. If they press OK, you should check for the existence of the marker file just to be sure that everything is ok. The return value, BROADCAST_QUERY_DENY, tells Windows that you don't want the eject button to actually eject the CD.

You might wonder why the CD marker file is checked even though you are telling Windows to deny the eject command. Simple: Some removable media hardware doesn't give a damn about waiting for Windows to tell it what to do. A good example of this would be a mechanical eject system like you see on some floppy drives. It's a good idea to be safe and check for the marker file.

WM_POWERBROADCAST

There are plenty of people out there playing on portable computers, including me, I might add. Please handle this message, and do something with it!

 case WM_POWERBROADCAST {   // Don't allow the game to go into sleep mode   if ( wParam == PBT_APMQUERYSUSPEND )     return BROADCAST_QUERY_DENY;   else if ( wParam == PBT_APMBATTERYLOW )   {     yourcode::ForceGameToSaveAndExit();   }   return true; } 

One thing you can do is disable sleep mode while the game is running. We did this because on some machines DirectX drivers never recovered from sleep mode! You should definitely provide some mechanism for saving the game and exiting when the battery begins to run low on electrons.

WM_DISPLAYCHANGE

When the player resets the desktop settings, Windows sends this message to tell you what the new settings are. Save off the settings and you can use them to validate the new mode:

 case WM_DISPLAYCHANGE: {   m_crDesktop = CRect( 0, 0, LOWORD(lParam), HIWORD(lParam) );   m_iColorDepth = wParam;   return 0; } 

WM_ENTERSIZEMOVE, WM_EXITSIZEMOVE

These messages are sent before and after you drag or resize your window. One useful thing to do is to deactivate your game when you begin the drag and reactivate it when you exit the drag. Why? If you've saved off the background buffer, the dragging or resizing will be much snappier, since your game isn't running in the background. This can be especially true for resizing.

WM_GETMINMAXINFO

This message is a great example of how Windows asks your application for a piece of information. In this case it is sent so Windows can figure out if your game has a minimum or maximum client area size:

 case WM_GETMINMAXINFO: {   // effectively keeps the window at exactly it's current size!   LPMINMAXINFO lpMMI = (LPMINMAXINFO)lParam;   lpMMI->ptMinTrackSize.x = lpMMI->ptMaxTrackSize.x = m_crWindow.Width();   lpMMI->ptMinTrackSize.y = lpMMI->ptMaxTrackSize.y = m_crWindow.Height();   return 0; } 

We used this message to essentially disable resizing, but you could use it to keep your window from growing or shrinking beyond certain limits.




Game Coding Complete
Game Coding Complete
ISBN: 1932111751
EAN: 2147483647
Year: 2003
Pages: 139

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net