Using DirectDraw in Windowed ModesThe last subject that I want to touch upon is using DirectDraw in windowed mode. The problem with windowed mode, as far as games go, is that you have very little control over the initial setting of the color depth and resolution. Writing DirectDraw applications that run full-screen is hard enough, but generalizing to a windowed mode is much more complex. You have to take into consideration that the user may start your application at any resolution and/or color depth. This means that the performance of your application may suffer. Not only that, but your game may be designed to work solely for 8- or 16-bit modes, and your code could fall apart completely if the user is in a higher color depth. Although writing games that work in both windowed mode and full-screen mode is the best of both worlds, I'm going to stick to full-screen mode to simplify most of the work and demos. However, I may create a number of windowed applications that run specifically in 800x600 or higher resolutions with a color depth of 8 bits. This way you can more easily debug your DirectX application and/or have other output windows, including the graphics output. In any case, let's take a look at how to write a windowed DirectX application and how to manipulate the primary surface. The first thing to know about windowed DirectDraw applications is that the primary surface is the entire screen display, not just your window! Take a look at Figure 7.38 to see this graphically. What this means is that you can't just write to the screen display blindly, or you'll end up mangling the client areas of other application windows. Of course, this may be your intent if you're writing a screen saver or other screen manipulation program. However, in most cases, you'll only want to write to the client area of your application's window. That means that somehow you must find the exact coordinates of your client window and then make sure only to draw in that area. Figure 7.38. In windowed modes, the entire desktop is mapped to the DirectDraw primary surface.The second problem is with clipping. If you intend to use the Blt() function, it has no idea about your client window and will blit over the edges of the client window. This means that somehow you must tell the DirectDraw clipping system that you have a window on the screen so that it will make sure to clip to the window no matter where it's moved to or how it's sized. This brings us to yet another problem—what if the window is moved or resized by the user? True, you can at least force the window to stay the same size, but movement is an absolute must. Otherwise, why even have a windowed application? To handle this, you must track the WM_SIZE and WM_MOVE messages. The next problem is 8-bit palettes. If the video mode is 8-bit mode and you want to change the palette, you're in for trouble. Windows has a Palette Manager that you must appease if you're going to mess with the 8-bit palette, because you'll probably make all other Windows applications look ugly. In most cases, you can change the palette, but it's a good idea to leave about 20 of the palette entries alone (the Windows and system colors) so the Palette Manager can make the other applications look something like what they're supposed to. Finally, the most obvious problems are those of bit blitting and pixel manipulation based on varying modes. Hence, you have to write code that handles all color depths if you really want to be robust. So I'll begin with the details of getting into a windowed mode. You already know how to do this! The only difference is that you don't set the video mode or create a secondary back buffer. You're not allowed to page flip in windowed modes. You must either use the blitter to double buffer or do it yourself, but you can't set up a complex surface chain and then call Flip(). It won't work. No problem, really; you'll just create another surface that's the same size as your window's client area, draw on it, and blit it to the client area of your window on the primary buffer. That way you can avoid screen flicker. Let me show you the code to create a windowed DirectDraw application. First, the DirectDraw initialization: LPDIRECTDRAW7 lpdd = NULL; // used to hold the IDIRECTDRAW7 interface // create the IDirectDraw7 interface if (FAILED(DirectDrawCreateEx(NULL, (void **)&lpdd, IID_IDirectDraw7, NULL))) return(0); // set cooperation to full screen if (FAILED(lpdd->SetCooperativeLevel(main_window_handle, DDSCL_NORMAL))) return(0); // clear ddsd and set size DDRAW_INIT_STRUCT(ddsd); // enable valid fields ddsd.dwFlags = DDSD_CAPS; // request primary surface ddsd.ddsCaps.dwCaps = DDSCAPS_PRIMARYSURFACE; // create the primary surface if (FAILED(lpdd->CreateSurface(&ddsd, &lpddsprimary, NULL))) return(0); The key thing here is the cooperation level setting. Notice that it's DDSCL_NORMAL, rather than the usual (DDSCL_FULLSCREEN | DDSCL_ALLOWMODEX | DDSCL_EXCLUSIVE | DDSCL_ALLOWREBOOT) that you've been using for full-screen modes. Also, when you're creating the application window, you'll want to use WS_OVERLAPPED or maybe WS_OVERLAPPEDWINDOW rather than WS_POPUP. The WS_POPUP style creates a window without any title, controls, and so on. WS_OVERLAPPED creates a window with a title, but it can't be resized, and the WS_OVERLAPPEDWINDOW style creates a fully functional window with all the controls. However, in most cases I like to use WS_OVERLAPPED because I don't want to deal with the resizing problem—it's up to you. For an example of what you know so far, take a look at DEMO7_18.CPP|EXE. It basically creates a windowed DirectX application with a client window that's 400x400, along with a primary surface. Drawing Pixels in a WindowOkay, now let's move on to accessing the client area of the window. There are two things to remember: The primary surface is the entire screen, and you don't know the color depth. Let's look at the first problem—finding the client area of the window. Because the user can move a window anywhere on the screen, the client coordinates are always changing—that is, if you think in terms of absolute coordinates. You need to find a way to figure out the upper-left corner of the client area in screen coordinates and then use that as your origin for pixel plotting. The function you need to use (which I've mentioned before) is GetWindowRect(): BOOL GetWindowRect(HWND hWnd, // handle of window LPRECT lpRect); // address of structure // for window coordinates WARNING GetWindowRect() actually retrieves the coordinates of your entire window, including the controls and border. I'll show you how to figure out the exact client coordinates in a bit, but keep that in mind… When you send the window handle of your application window, the function returns the screen coordinates of the client area of your window in lpRect. So all you need to do is call this function to retrieve the upper-left corner of your window in screen coordinates. Of course, every time the window moves, the coordinates change, so you must call GetWindowRect() every frame or when you receive a WM_MOVE message. I prefer calling the function once a frame since there's no reason to process more Window's messages then you have to! Now that you know the screen coordinates of your window's client area, you're ready to manipulate the pixels. But wait a minute—what's the pixel format? I'm glad you asked, because you know the answer! That's right, you need to use the GetPixelFormat() function at the beginning of your program to determine the color depth and then, based on this, make calls to different pixel plotting functions. So somewhere in your program, maybe in Game_Init() after setting up DirectDraw, you should make a call to GetPixelFormat() from the primary surface, like this: int pixel_format = 0; // global to hold the bpp DDPIXELFORMAT ddpixelformat; // hold the pixel format // clean out the structure and set it up DDRAW_INIT_STRUCT(ddpixelformat); // get the pixel format lpddsprimary->GetPixelFormat(&ddpixelformat); // set global pixel format pixel_format = ddpixelformat.dwRGBBitCount; Then, once you know the pixel format, you can use some conditional logic, function pointers, or virtual functions to set the pixel plotting function to the correct color depth. To keep things simple, you'll just use some conditional logic that tests the global pixel_format variable before plotting. Here's some code that plots random pixels at random locations in the client area of the window: DDSURFACEDESC2 ddsd; // directdraw surface description RECT client; // used to hold client rectangle // get the window's client rectangle in screen coordinates GetWindowRect(main_window_handle, &client); // initialize structure DDRAW_INIT_STRUCT(ddsd); // lock the primary surface lpddsprimary->Lock(NULL,&ddsd, DDLOCK_SURFACEMEMORYPTR | DDLOCK_WAIT,NULL); // get video pointer to primary surface // cast to UCHAR * since we don't know what we are // dealing with yet and I like bytes :) UCHAR *primary_buffer = (UCHAR *)ddsd.lpSurface; // what is the color depth? if (pixel_format == 32) { // draw 10 random pixels in 32 bit mode for (int index=0; index<10; index++) { int x=rand()%(client.right - client.left) + client.left; int y=rand()%(client.bottom - client.top) + client.top; DWORD color = __RGB32BIT(0,rand()%256, rand()%256, rand()%256); *((DWORD *)(primary_buffer + x*4 + y*ddsd.lPitch)) = color; } // end for index } // end if 24 bit else if (pixel_format == 24) { // draw 10 random pixels in 24 bit mode (very rare???) for (int index=0; index<10; index++) { int x=rand()%(client.right - client.left) + client.left; int y=rand()%(client.bottom - client.top) + client.top; ((primary_buffer + x*3 + y*ddsd.lPitch))[0] = rand()%256; ((primary_buffer + x*3 + y*ddsd.lPitch))[1] = rand()%256; ((primary_buffer + x*3 + y*ddsd.lPitch))[2] = rand()%256; } // end for index } // end if 24 bit else if (pixel_format == 16) { // draw 10 random pixels in 16 bit mode for (int index=0; index<10; index++) { int x=rand()%(client.right - client.left) + client.left; int y=rand()%(client.bottom - client.top) + client.top; USHORT color = __RGB16BIT565(rand()%256, rand()%256, rand()%256); *((USHORT *)(primary_buffer + x*2 + y*ddsd.lPitch)) = color; } // end for index } // end if 16 bit else {// assume 8 bits per pixel // draw 10 random pixels in 8 bit mode for (int index=0; index<10; index++) { int x=rand()%(client.right - client.left) + client.left; int y=rand()%(client.bottom - client.top) + client.top; UCHAR color = rand()%256; primary_buffer[x + y*ddsd.lPitch] = color; } // end for index } // end else // unlock primary buffer lpddsprimary->Unlock(NULL); TRICK Of course, this code is an optimization nightmare, and it hurts me to even show you such slow, crude, troglodyte code, but it's easy to understand. In real life, you would use sfunction pointers or a virtual function, completely remove all multiplies and mods, and use incremental addressing. Anyway, just wanted to indemnify myself <BG>. For an example of plotting pixels in any color depth, take a look at DEMO7_19.CPP|EXE. It creates a 400x400 window and then plots pixels in the client area (well, almost). Try running the program with different color depths, and notice that it still works! When you're done, come back and I'll talk about rendering more accurately to the actual interior client area… Finding the Real Client Area (51)The problem with windows is that when you create a window using CreateWindow() or CreateWindowEx(), you're specifying the total width and size of the window, including any controls. Hence, if you create a blank WS_POPUP window without any controls, the size of the window is exactly the size of the client area. Presto—no drama. On the other hand, the second you add controls, menus, borders, whatever, and make the call to CreateWindowEx(), Windows shrinks the interior client area so that it can fit in all the controls. The result: a working area that's less than what you desired. Figure 7.39 illustrates this cosmic dilemma. The solution is to resize your window taking into consideration the border, controls, etc. Figure 7.39. A window's client area is smaller than the window surrounding it.For example, let's say you want a window that has a working area of 640x480, but you want to also have a border, a menu, and standard window controls. What you need to do is calculate how many pixels all the extra Windows gadgetry takes in the X and Y direction, and then simply increase the size of your window until the working client area is the desired size. The magical function that computes the size of a window with various styles is called AdjustWindowRectEx() and is shown here: BOOL AdjustWindowRectEx( LPRECT lpRect, // pointer to client-rectangle structure DWORD dwStyle, // window styles BOOL bMenu, // menu-present flag DWORD dwExStyle);// extended style You fill in all the parameters, and the function resizes the structure data sent in lpRect to take into consideration all the extra styles and flags for the window. To use the function, you first set up a RECT structure with the desired client area size, say, 640x480. Then you make the call, along with all the proper parameters you created the original window with. However, I never can remember what I set the window to, or the exact names of the flags, so you can ask Windows to tell you what you did and save some neurons. Here's the call, along with the window's helper functions that query the styles based on the HWND that you send: // the client size we desire RECT window_rect = {0,0,640,480}; // make the call to adjust window_rect AdjustWindowRectEx(&window_rect, GetWindowStyle(main_window_handle), GetMenu(main_window_handle) != NULL, GetWindowExStyle(main_window_handle)); // now resize the window with a call to MoveWindow() MoveWindow(main_window_handle, CW_USEDEFAULT, // x position CW_USEDEFAULT, // y position window_rect.right – window_rect.left, // width window_rect.bottom – window_rect.top, // height FALSE); And that's it, baby! The above code will be very crucial when we create the 16-bit library for the engine, since when we request a 640x480 or whatever size window, we mean the client area needs to be that big, thus the window MUST always be resized if it has any controls taking up client area. Clipping a DirectX WindowNow we're getting somewhere! The next piece of the DirectDraw windowed mode conundrum is to get the clipping working. Make sure that you understand that clipping only matters to the blitter; it has no effect on what you do directly with the primary surface since the primary surface is actually the entire video screen, yes that's how you draw all over the Windows desktop. You've seen the IDIRECTDRAWCLIPPER interface before, so I'm not going to belabor it anymore. (I still have the shakes from figuring out the exact nature of the coordinates in a RECT structure.) The first thing you need to do is create a DirectDraw clipper, like this: LPDIRECTDRAWCLIPPER lpddclipper = NULL; // hold the clipper if (FAILED(lpdd->CreateClipper(0,&lpddclipper,NULL))) return(0); Next, you must attach the clipper to your application's window with IDIRECTDRAWCLIPPER::SetHWnd(). This associates the clipper to your window and handles all the details for sizing and movement for you. In fact, you don't even have to send a clipping list at all; it's all automatic. The function is so simple. Here's the prototype: HRESULT SetHWnd(DWORD dwFlags, // unused, set to 0 HWND hWnd); // app window handle Here's the call to associate the clipper with your main window: if (FAILED(lpddclipper->SetHWnd(0, main_window_handle))) return(0); Next you have to associate the clipper with the surface you want to clip—in this case, the primary surface. To do this, you use SetClipper(), which you've already seen: if (FAILED(lpddsprimary->SetClipper(lpddclipper))) return(0); WARNING Now you're ready to rock. There's one little DirectX problem, though. The reference count on the clipper is now at 2—1 for the creation, 2 for the call to SetClipper(). This is fine, but destroying the surface won't kill the clipper. You must make another call to lpddclipper->Release() to kill it, after releasing the surface with a call to lpddsprimary->Release(). The bottom line is you might think that you killed the clipper with a call to lpddclipper->Release(), but that only reduces the reference count to 1. Alas, Microsoft recommends that you make a call to lpddclipper->Release() right after the preceding code sequence, so that the reference count of lpddclipper is 1 as it should be—Whatever! Remember that the clipping that's attached to the window only matters for blits to the primary surface—that is, the contents of the window. But in most cases, you'll create an offscreen surface to simulate a double buffer, blit to that, and then copy the offscreen surface to the primary buffer using the blitter—a crude form of page flipping. Alas, attaching the blitter to the primary buffer only helps if the blits you make to it (the offscreen buffer) go out of bounds. And this will only occur if the user resizes. Working with 8-Bit Windowed ModesThe last topic I want to touch upon is 8-bit windowed mode and the palette. To make a long story short, you can't just create a palette, do anything you want with it, and then attach it to the primary surface. You must work a little with the Windows Palette Manager. Window palette management under GDI is beyond the scope of this book (I always wanted to say that), and I don't feel like boring you with all the stupid details. The bottom line is, if you run your game in windowed 256-color mode, you're going to have fewer than 256 colors at your disposal. Each application running on the desktop has a logical palette that contains the desired colors of the application. However, the physical palette is the one that really matters. The physical palette reflects the actual hardware palette, and this is the compromise that Windows works with. When your application gets the focus, your logical palette is realized—in other words, the Windows Palette Manager starts mapping your colors to the physical palette as best it can. Sometimes it does a good job, and sometimes it doesn't. Moreover, the way you set up your logical palette's flags determines how much slack Windows has to work with. Finally, at the very least, Windows needs 20 colors out of the palette: the first and last 10. These are reserved for Windows colors and are the bare minimum to make Windows applications look reasonable. The trick to creating a 256-color mode windowed application is restraining your artwork to 236 colors or less—so you have room for the Windows colors—and then setting the palette flags of your logical palette appropriately. Windows will then leave them alone when your palette is realized. Here's the code that creates a generic palette. You can change the code to use your own RGB value for the entries from 10 to 245, inclusive. This code simply makes them all gray: LPDIRECTDRAW7 lpdd; // this is already setup PALETTEENTRY palette[256]; // holds palette data LPDIRECTDRAWPALETTE lpddpal = NULL; // palette interface // first set up the windows static entries // note it's irrelevant what we make them for (int index = 0; index < 10 ; index++) { // the first 10 static entries palette[index].peFlags = PC_EXPLICIT; palette[index].peRed = index; palette[index].peGreen = 0; palette[index].peBlue = 0; // The last 10 static entries: palette[index+246].peFlags = PC_EXPLICIT; palette[index+246].peRed = index+246; palette[index+246].peGreen = 0; palette[index+246].peBlue = 0; } // end for index // Now set up our entries. You would load these from // a file etc., but for now we'll make them grey for (index = 10; index < 246; index ++) { palette[index].peFlags = PC_NOCOLLAPSE; palette[index].peRed = 64; palette[index].peGreen = 64; palette[index].peBlue = 64; } // end for index // Create the palette. if (FAILED(lpdd->CreatePalette(DDPCAPS_8BIT, palette, &lpddpal,NULL))) { /* error */ } // attach the palette to the primary surface... Notice the use of PC_EXPLICIT and PC_NOCOLLAPSE. PC_EXPLICIT means "these colors map to hardware," and PC_NOCOLLAPSE means "don't try to map these colors to other entries; leave them as they are." If you wanted to animate some color registers, you would also logically OR the flag PC_RESERVED. This tells the palette manager not to map other Windows applications colors to the entry because it may change at any time. |