Resolution Switching Without Using DirectX
by Ashley Matheson

Introduction

My son's six. Big deal, I know, but let a proud, overzealous Dad continue for a minute. He's just starting to get into playing games on my computer at home. And at the age of six, he's not quite up to playing Quake 3, or Tomb Raider (although he gets a big kick out of watching me play). But games that are more his ability are some of the earlier Sierra games, and, in particular, Hasbro Interactive's Battleship. He really likes that game and can play quite a good game too.

But there's one serious problem with that game. It REQUIRES you to have your desktop running in 256 color mode. Period. It's a DirectX game, sure enough, but it doesn't do the mode switch for you. Not quite sure why, and if anyone out there has any idea why they'd do that, I'd be glad to hear it.

So, picture this. Dad's working away in 16 bit color. He heads to bed and forgets to turn the machine back down to 8 bit before going to bed. Next day, at around 10:00 Dad gets a call at the office from one little PO'd camper, asking why his game won't run. After a few phone calls like that, it was time to correct the situation.

So, after about two evenings of coding, and a weekend of cleaning up the code, here's my results.

Some Win32 API Calls

All in all, it's a pretty easy task to switch the resolution. All we really need are two Win32 API calls, EnumDisplaySettings and ChangeDisplaySettings. As well, we'll need to look at the structure DEVMODE, which is used by both methods.

If you're on the MSDN, you can do a look-up on that API function, but here's the gist of it.

EnumDisplaySettings essentially gets information about any one of the available video modes. By making several calls to this function, you can get a list of all the available graphics modes. I'm going to cheat a bit here, and grab some of the explanations of these functions from the MSDN web site. I'll explain them as they relate to what we're going to do. The function looks like this:

BOOL EnumDisplaySettings(
  LPCTSTR lpszDeviceName,  // display device
  DWORD iModeNum,          // graphics mode
  LPDEVMODE lpDevMode      // graphics mode settings
);
Parameters

lpszDeviceName

Pointer to a null-terminated string that specifies the display device whose graphics mode the function will obtain information about. In Windows 95 and 98 (and our app), lpszDeviceName must be NULL.

iModeNum

This specifies the type of information to retrieve. This value can be a graphics mode index which we will use in our program.

lpDevMode

This is a pointer to a DEVMODE structure into which the function stores information about the specified graphics mode. One of the things that we are going to have to do before calling EnumDisplaySettings, set the dmSize member to sizeof(DEVMODE).

If we call EnumDisplaySettings repeatedly, how do we know how many time to iterate? It's pretty simple. The boolean return value from EnumDisplaySettings indicates if we've still got more values in the internal list of graphic modes available to us. When we get a false value back, we've hit the end of the list.

So, without going too far into detail now, let's assume that we've iterated through the list of graphics modes, and stored them in a list (I'll get to the list in a bit). From that, we can now determine if a requested display mode is in the list. If it is, we can then change the display mode with a call to ChangeDisplaySettings. In a nutshell, here's what we're looking at:

LONG ChangeDisplaySettings(
  LPDEVMODE lpDevMode,  // graphics mode
  DWORD dwflags,        // graphics mode options
);
Parameters

lpDevMode

This is a pointer to a DEVMODE structure describing the graphics mode to switch to. We've used the DEVMODE structure before, now let's look at some of the more important members of that structure:

MemberMeaning
dmBitsPerPelBits per pixel
dmPelsWidthPixel width
dmPelsHeightPixel height
dmDisplayFlagsMode flags
dmDisplayFrequencyMode frequency
dmPositionWindows 98, Windows 2000: Position of the device in a multimonitor configuration
dmFieldsField Flags. See below.

As you can see, dmFields needs a bit of explanation. dmFields is used to determine which member of this structure is going to be used in changing the display setting. The valid flags are as follows:

FlagMeaning
DM_BITSPERPELUse the dmBitsPerPel value.
DM_PELSWIDTHUse the dmPelsWidth value.
DM_PELSHEIGHTUse the dmPelsHeight value.
DM_DISPLAYFLAGSUse the dmDisplayFlags value.
DM_DISPLAYFREQUENCYUse the dmDisplayFrequency value.
DM_POSITIONWindows 98, Windows 2000: Use the dmPosition value.

If lpDevMode is NULL, all the values currently in the registry will be used for the display setting. Passing NULL for the lpDevMode parameter and 0 for the dwFlags parameter is the easiest way to return to the default mode after a dynamic mode change.

dwFlags

We use this to determine how the video mode will be changed. For our purposes, we use CDS_FULLSCREEN, since this is the least obtrusive. It doesn't touch the registry, screw around with desktop icons, or other such foolishness.

Return Values

The ChangeDisplaySettings function returns one of the following values.

ValueMeaning
DISP_CHANGE_SUCCESSFULThe settings change was successful.
DISP_CHANGE_RESTARTThe computer must be restarted in order for the graphics mode to work.
DISP_CHANGE_BADFLAGSAn invalid set of flags was passed in.
DISP_CHANGE_BADPARAMAn invalid parameter was passed in. This can include an invalid flag or combination of flags.
DISP_CHANGE_FAILEDThe display driver failed the specified graphics mode.
DISP_CHANGE_BADMODEThe graphics mode is not supported.
DISP_CHANGE_NOTUPDATEDWindows NT/2000: Unable to write settings to the registry.

Implementation

OK. Now it's time to look at how we'd code that. I've designed a class called ResManager. It resides in a file called change.h and change.cpp. These files are meant to hold more than just the ResManager class, just in case you were wondering why the name of the file. Below is change.h (minus comments):

#include <stdio.h>
#include <list.h>
class ResManager
{
public:
  ResManager();
  ResManager(int width, int height, int depth);
  ~ResManager();

  void ChangeRes(int width, int height, int depth);
  void RestoreResolution();

  void ListResolutions();

private:
  DEVMODE m_Current;
  std::list<DEVMODE> m_stdDeviceModes;

};

Pretty easy code here. Publicly, ResManager has two constructors, a default constructor (with no parameters) and a constructor that immediately switches the resolution. To switch the resolution at any time, make a call to ChangeRes. To restore the original resolution, make the call to RestoreResolution. ListResolutions will list (to the debug window) the available video modes.

Privately, we have the current video mode stored in m_Current. Also, we have an STL List container holding a list of available Device modes in m_stdDeviceModes.

How do we implement this? Let's go through the implementation of each method one at a time.

First, the default constructor:

ResManager::ResManager()
{
  int  nModeExist;
  DEVMODE devMode;
  devMode.dmSize = sizeof(DEVMODE);

  for (int i=0; ;i++) 
  {
    nModeExist = EnumDisplaySettings(NULL, i, 
        &devMode);

    if (nModeExist != 1) 
    {
      // End of modes.  bail out.
      break;
    }
    else
    {
      // Add the driver to the list.
        m_stdDeviceModes.push_front(devMode);
    }
  }
}

Simply put, we iterate through EnumDisplaySettings by incrementing i, until EnumDisplaySettings return a false. For each display mode we find, we add it to the list container m_stdDeviceModes.

Now, the parameterized constructor:

ResManager::ResManager(int width, int height, int depth)
{
  ResManager();
  ChangeRes(width, height, depth);
}

Editor's Note: The above code will not work as described. From Jason W:
The call to the default constructor in the above code fragment is creating a temporary object on the stack and then destroying it (since it isn't assigned to any variable). What you wanted to do was to have the code in the default constructor called under the current object's context. This cannot be done in C++. The way to do this is to have a common Init() function that is called from your different constructors.
This change has been made in the accompanying source code.

Ahh, the joy of C++. All we do is call the default constructor, and then ChangeRes. Easy as pie.

To Change the resolution we need the following:

void ResManager::ChangeRes(int width, int height, int depth)
{
  bool success = false;
  int nModeSwitch;
  std::list<DEVMODE>::iterator resIter;

  // Iterate through the DeviceModes list, looking for a match
  // if one is found, switch to that resolution and bail.
  // Otherwise, leave alone.
  resIter = m_stdDeviceModes.begin();

  while (resIter != m_stdDeviceModes.end())
  {
    if ( ((*resIter).dmBitsPerPel == depth)
         &&((*resIter).dmPelsWidth == width) 
         &&((*resIter).dmPelsHeight == height))
    {
      nModeSwitch = ChangeDisplaySettings(&(*resIter), 
              CDS_FULLSCREEN); 

      if (nModeSwitch==DISP_CHANGE_SUCCESSFUL) break;

      // Whoops, it didn't work.  Possibly running 
      // Win95?  Try explicity defining the parameters
      // to switch
      int nCloseMode = 0;
      EnumDisplaySettings(NULL, nCloseMode, (*resIter));
      (*resIter).dmBitsPerPel = depth; 
      (*resIter).dmPelsWidth  = width; 
      (*resIter).dmPelsHeight = height; 
      (*resIter).dmFields = DM_BITSPERPEL 
            | DM_PELSWIDTH 
            | DM_PELSHEIGHT; 

      nModeSwitch = ChangeDisplaySettings(&(*resIter), 
            CDS_FULLSCREEN); 

      if(nModeSwitch == DISP_CHANGE_SUCCESSFUL) 
      {
        success = true;
        break;
      }

      // Nope, what else can we try?  Separately change 
      // the BitDepth and then resolution
      (*resIter).dmFields = DM_BITSPERPEL; 
      nModeSwitch = ChangeDisplaySettings(&(*resIter), 
            CDS_FULLSCREEN);

      if(nModeSwitch == DISP_CHANGE_SUCCESSFUL) 
      {
        (*resIter).dmFields = DM_PELSWIDTH 
            | DM_PELSHEIGHT; 
        nModeSwitch = 
          ChangeDisplaySettings(&(*resIter), 
            CDS_FULLSCREEN); 

        if(nModeSwitch == DISP_CHANGE_SUCCESSFUL) 
        {
          success = true;
          break;
        }
        ChangeDisplaySettings(NULL, 0);
        break;
      }
    }
    resIter++;
  }

  // Did we change the resolution?
  if (true == success)
  {
    m_Current = (*resIter);
  }
  else
  {
    // Error and list the available Video modes ...
    ListResolutions();
  }
}

Maybe a bit of explanation here. We need to run a couple of tests first. First off, we try to find the requested resolution from the available resolutions. If we fail, we should list out the available video modes to the user.

Assuming we got an available video mode, we then try to switch to it. First off, we try the easy approach, by doing a straight ChangeDisplaySettings. If that doesn't work, we then explicitly define the parameters that we want to use in changing the video resolution. If that doesn't work, we then try to change the Resolution, then the bit depth as separate calls. If those don't work, then nothing's going to work, and we bail.

To Restore the resolution is a simple matter. Here's the code:

void ResManager::RestoreResolution()
{
  ChangeDisplaySettings(NULL, 0);
}

And to List the Resolutions, we again do the iteration thing:

void ResManager::ListResolutions()
{
  char buf[255];
  std::list::iterator resIter;

  // Iterate through the DeviceModes list,
  // displaying a summary of each
  resIter = m_stdDeviceModes.begin();

  sprintf(buf, "The following Resolutions are available:");
  Log(LOG_MESSAGE, buf);

  while (resIter != m_stdDeviceModes.end())
  {
    sprintf(buf,
      "Width: %d, Height:%d, BPP: %d",
      (*resIter).dmBitsPerPel, 
      (*resIter).dmPelsWidth, (*resIter).dmPelsHeight);
    Log(LOG_MESSAGE, buf);
    resIter++;
  }
}

And that, good folks, is a quick and dirty implementation of a non-DirectX based resolution switcher. The app that uses this is a console app that reads the command line, switches the resolution based on those arguments, and launches a program. The rezswitch program waits until the program has terminated, and then switches the resolution back to the original mode. How that program works isn't really related to game programming, so I'm going to skip it (that, and it's pretty rough around the edges as well). The project is a Visual Studio 6 project, so if anyone wants to do the conversion to other compilers (as I only have VC++ 6), feel free to do so.

OK. So that's it. Kind of a neat little programming exercise. What's next? Well, one of the spare-time projects that I'm working on is a 3D GUI system. You know, 3D windows, Buttons, scrollboxes. That kind of thing. Right now I'm in the design phase, and I might share some of that with you in a future article. But right now, it's still pretty raw.

Anyway, for questions, comments and/or issues you may have with the code, or just to shoot the breeze, please feel free to contact me at: ashleymatheson@hotmail.com.

References

Hey, even I don't know it all. Here's some of the references that I used in writing this article:

Microsoft's on-line API reference
http://msdn.microsoft.com/library/default.htm

Ryan Haksi's code for changing resolutions. Another fellow Canadian. And as such, I can't seem to get a hold of the guy. This is the last known website that was posted in his code:
http://home.bc.rogers.wave.ca/borealis/opengl.html.

Download the source

Discuss this article in the forums


Date this article was posted to GameDev.net: 5/17/2000
(Note that this date does not necessarily correspond to the date the article was written)

See Also:
Windows

© 1999-2011 Gamedev.net. All rights reserved. Terms of Use Privacy Policy
Comments? Questions? Feedback? Click here!