Moving from Exclusive Mode to Windowed Mode in DirectX Part I
by null_pointer

Introduction

Okay, it's supposed to be really simple to run your game in both exclusive mode and windowed mode, but it does take some work to get it running properly (and elegantly). For this article, I'll be using C++ because it's very common in the industry. You can wrap this example in classes to make it easier, if that's what you want.

I'm also assuming that you're familiar with setting up and using DirectDraw in exclusive mode, so I won't go into the details of that here. Read on!

The Design

Many parts of DirectDraw initialization are different for windowed mode. The best way to do it is first to create the DirectDraw object when your program starts up. Second, you create all your surfaces, set the cooperative level, set the display mode, fill out any variables you need, etc. This second stage is where all the changes between exclusive mode and windowed mode are found. So, your functions are set up like this:

void CreateDirectDraw(); void DestroyDirectDraw();

and

void CreateSurfaces(bool bExclusive, int nWidth, int nHeight, int nBPP); void DestroySurfaces();

The first set (CreateDirectDraw and DestroyDirectDraw) create the DirectDraw object and destroy it, respectively. You should be able to fill those out yourself. The second set (CreateSurfaces and DestroySurfaces) handle everything else associated with setting up and shutting down DirectDraw. Looking at the parameter, bExclusive, you'll see that they create either the exclusive mode surfaces or the windowed mode surfaces, along with all the other objects necessary to use them. The width, height, and bpp parameters specify the display mode to use, if bExclusive is true.

We'll need to modify the game loop a little to handle windowed mode. We'll also need a function that handles switching modes that'll get it up and running:

void SwitchMode(bool bExclusive, int nWidth, int nHeight, int nBPP);

Read on for the implementation of these functions!

CreateSurfaces(…)

We'll split this function up into two sections, which initialize DirectDraw for either exclusive mode or windowed mode. That is done like this:

if( bExclusive ) { // exclusive code // save the mode g_bExclusive = bExclusive; } else { // windowed code // save the mode g_bExclusive = bExclusive; }

Another thing you'll want to do is to create a global variable (g_bExclusive) to remember whether we're in exclusive mode or not; we'll use that flag during the game loop. Don't forget about the g_bExclusive flag! It's very important that we keep track of the mode.

You can place all of the code you've already written in the exclusive code section. Have it use the nWidth, nHeight, and nBPP for the display mode, etc. We'll add our own code to the windowed code section. (I'll do this with several functions, splitting them up into two sections. Just so you know what I'm doing!)

As I mentioned before, when this function is called, DirectDraw has already been created. So, the next step in initializing DirectDraw is to set the cooperative level via lpDD->SetCooperativeLevel(). Pass the handle of your main window, and DDSCL_NORMAL:

lpDD->SetCooperativeLevel(hMainWnd, DDSCL_NORMAL);

You can also OR the DDSCL_MULTITHREADED flag if you want to use multiple threads. Remember, you can't set the display mode in windowed mode; so, the next step is to create the primary surface and back buffer.

You need a very different "buffering system" in windowed mode. You can't create a primary surface with attached back buffers and flip() them, because you don't have exclusive access to the video hardware. Flipping is the process of swapping the address of the current primary surface with one of its attached back buffers. Obviously, you can't do this in windowed mode, because you're sharing the primary surface with all the other apps.

The system you need in windowed mode is to create your primary surface with this DDSURFACEDESC2:

DDSURFACEDESC2 ddsd; ZeroMemory(&ddsd, sizeof(ddsd)); ddsd.dwSize = sizeof( ddsd ); ddsd.dwFlags = DDSD_CAPS; ddsd.ddsCaps.dwCaps = DDSCAPS_PRIMARYSURFACE;

That code simply uses the existing format of the screen for the primary surface, which, by the way, you can't change in windowed mode. Also, note that we didn't use the DDSD_BACKBUFFERCOUNT flag; that's only for exclusive mode apps.

Then you create your back buffer like so:

DDSURFACEDESC2 ddsd; ZeroMemory(&ddsd, sizeof(ddsd)); ddsd.dwSize = sizeof( ddsd ); ddsd.dwFlags = DDSD_CAPS | DDSD_WIDTH | DDSD_HEIGHT; ddsd.ddsCaps.dwCaps = DDSCAPS_OFFSCREENPLAIN; ddsd.dwWidth = 640; // whatever you want ddsd.dwHeight = 480; // whatever you want

Note here that we didn't use the DDSCAPS_BACKBUFFER flag, because that's also just for exclusive mode apps.

Remember, in DirectDraw, the primary surface is always the entire screen. To keep yourself from drawing all over the screen in windowed mode, you attach a clipper to your primary surface, and attach it to your main window as well (that's the easy way):

LPDIRECTDRAWCLIPPER lpddClipper; lpDD->CreateClipper(...lpddClipper...); lpddClipper->SetHWnd(...hMainWnd...); lpddsPrimary->SetClipper(...lpddClipper...);

There are other parameters to these functions; for simplicity's sake I simply did not list them.

Well, that's it for the CreateSurfaces function. We'll look at cleaning up next.

DestroySurfaces(…)

Your shutdown code is also different. Instead of releasing just the DirectDraw object and the primary surface, you also have to release your back buffer and clipper. Again, use an if statement to separate the exclusive code from the windowed code:

if( bExclusive ) { // exclusive code } else { // windowed code }

Put your own exclusive code in the exclusive section, and we'll add the windowed code.

This is the code to add in the windowed code section:

if( lpddBack ) { // release the back buffer lpddBack->Release(); lpddBack = NULL; } if( lpddPrimary ) { // release the clipper (indirectly) lpddPrimary->SetClipper(NULL); lpddClipper = NULL; // release the primary surface lpddPrimary->Release(); lpddPrimary = NULL; }

Once you've added that code, we'll go on to the game loop.

The Game Loop

You may think that the only real difference in the game loop is that you blit the back buffer to the primary surface when you're in windowed mode instead of flipping… We'll start with that part though. Split your rendering function into two sections, separated by an if statement:

if( g_bExclusive ) { // exclusive code } else { // windowed code }

Where did that g_bExclusive flag come from? It's that global variable that we use to keep track of the mode we're working in.

Put your old exclusive mode rendering code in the exclusive code section, then add a call to Blt in the windowed code section. Use the primary surface as the destination, and the back buffer as the source, like this:

lpddPrimary->Blt(NULL, lpddBack, NULL, DDBLT_WAIT, NULL);

That's about as simple as you can get! But wait, that code draws to the whole primary surface, not just our window! How do we get DirectDraw to copy the entire back buffer to our main window's client area? Well, we obtain the client area of the window, then convert the two points of the rectangle into points that are relative to the upper left corner of the screen. Pass that rect as the first parameter of the Blt call and you're set! Here's some code to do that:

// calculate the client rect in screen coordinates RECT rect; ZeroMemory(&rect, sizeof( rect )); // get the client area GetClientRect(hMainWnd, &rect); // copy the rect's data into two points POINT p1; POINT p2; p1.x = rect.left; p1.y = rect.top; p2.x = rect.right; p2.y = rect.bottom; // convert it to screen coordinates (like DirectDraw uses) ClientToScreen(hMainWnd, &p1); ClientToScreen(hMainWnd, &p2); // copy the two points' data back into the rect rect.left = p1.x; rect.top = p1.y; rect.right = p2.x; rect.bottom = p2.y; // blit the back buffer to our window's position g_lpPrimary->Blt(&rect, g_lpBack, NULL, DDBLT_WAIT, NULL);

There's one more part to your game loop: you can't hog the system like you could in exclusive mode. Whenever Windows (or another app) wants resources, you must give it to them. This is because the user has made the choice, not just the OS. A good Windows app lets the user switch between windows, using other apps at the same time. That's why it's called Windows!

So, how do we do that? Well, what consumes most of the computer's power? The game loop, of course! So, we "pause" the game automatically when the user switches to something else, and "resume" it automatically when the user switches back to our app. (In games like massive online multiplayer games, this is not plausible; you'll have to think of something else. Tip: you could at least pause the rendering, or slow it down a little.) Anyway, to pause the game loop, we add a variable to keep track of whether the game is running or not:

bool bRunGame;

Then, we handle a certain Windows message in our main WndProc: WM_ACTIVATE. Whenever our main window receives the focus, we get one of these messages. We get another one of those messages when our main window loses the focus (which happens when another window gains the focus). Checking the value of wParam tells us if we're gaining or losing the focus. Set the bRunGame flag like this:

if( LOWORD( wParam ) == WA_INACTIVE ) { // the user is now working with another app bRunGame = false; } else { // the user has now switched back to our app bRunGame = true; }

To use this variable we have just set up, check it before you continue the game loop. Replace your program's main message loop (probably in WinMain) with this; you'll note that the game loop is embedded in the message loop (near the end):

MSG msg; ZeroMemory(&msg, sizeof(msg)); for( ;; ) { if( PeekMessage(&msg, NULL, NULL, NULL, PM_NOREMOVE) ) { // retrieve a message GetMessage(&msg, NULL, NULL, NULL); if( msg.message == WM_QUIT ) break; // only way out of the for( ;; ) loop // dispatch the message to our WndProc TranslateMessage(&msg); DispatchMessage(&msg); } else { if( bRunGame ) { // game code here } } } return msg.wParam;

Then place your game code inside that if statement we just added, where it says: // game code here. This completes the game loop section. Whew! We need to now look at changing modes while running the program. Trust me, it'll be the easiest section in this article!!

Switching Modes While Running

Ah, we finally get to switching modes…

Well, we'll use a single function to switch modes. It'll be capable of changing between exclusive and windowed, and setting the display mode (while in exclusive mode). Here it is:

void ChangeDisplayMode(bool bExclusive, int nWidth, int nHeight, int nBPP);

Kind of simple, isn't it? No, well…I'll explain it a little. When you want to switch from exclusive mode to windowed mode, you call it like this:

ChangeDisplayMode(false, 0, 0, 0); // windowed

When you want to switch to exclusive mode, or just change between resolutions and color depths, you call it like this:

ChangeDisplayMode(true, 640, 480, 16); // 640x480x16 exclusive ChangeDisplayMode(true, 800, 600, 32); // 800x600x32 exclusive

Not bad, huh? Ok, let's flesh it out a little. Here's the whole function:

ChangeDisplayMode(bool bExclusive, int nWidth, int nHeight, int nBPP) { // destroy any existing surfaces and clippers. DestroySurfaces(); // create new surfaces and change the // cooperative level and display mode CreateSurfaces(bExclusive, int nWidth, int nHeight, int nBPP); }

That's it for switching modes!

There are lots of ways to wring more performance out of a windowed mode DirectX app. There are also a few ways to make it easier for the end user to use. We'll try to achieve a blend of both. You'll have to wait for the next article for those tips though!

Good Luck!

- null_pointer
e-mail: ratt96963@aol.com
web site: http://www.freeyellow.com/members8/nullpointer
site name: Sabre Multimedia

Get the demo!

Discuss this article in the forums


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

See Also:
General

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